mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 18:14:17 +00:00
Merge branch 'master' into language-header
This commit is contained in:
@@ -104,7 +104,7 @@ DSPACE_REST_SSL # Whether the angular REST uses SSL [true/false]
|
|||||||
|
|
||||||
The same settings can also be overwritten by setting system environment variables instead, E.g.:
|
The same settings can also be overwritten by setting system environment variables instead, E.g.:
|
||||||
```bash
|
```bash
|
||||||
export DSPACE_HOST=https://dspace7.4science.cloud/server
|
export DSPACE_HOST=dspace7.4science.cloud
|
||||||
```
|
```
|
||||||
|
|
||||||
The priority works as follows: **environment variable** overrides **variable in `.env` file** overrides **`environment.(prod, dev or test).ts`** overrides **`environment.common.ts`**
|
The priority works as follows: **environment variable** overrides **variable in `.env` file** overrides **`environment.(prod, dev or test).ts`** overrides **`environment.common.ts`**
|
||||||
|
@@ -101,7 +101,7 @@
|
|||||||
"moment": "^2.22.1",
|
"moment": "^2.22.1",
|
||||||
"morgan": "^1.9.1",
|
"morgan": "^1.9.1",
|
||||||
"ng-mocks": "^8.1.0",
|
"ng-mocks": "^8.1.0",
|
||||||
"ng2-file-upload": "1.2.1",
|
"ng2-file-upload": "1.4.0",
|
||||||
"ng2-nouislider": "^1.8.2",
|
"ng2-nouislider": "^1.8.2",
|
||||||
"ngx-bootstrap": "^5.3.2",
|
"ngx-bootstrap": "^5.3.2",
|
||||||
"ngx-infinite-scroll": "6.0.1",
|
"ngx-infinite-scroll": "6.0.1",
|
||||||
|
@@ -27,7 +27,8 @@ describe('MetadataFieldFormComponent', () => {
|
|||||||
/* tslint:disable:no-empty */
|
/* tslint:disable:no-empty */
|
||||||
const registryServiceStub = {
|
const registryServiceStub = {
|
||||||
getActiveMetadataField: () => observableOf(undefined),
|
getActiveMetadataField: () => observableOf(undefined),
|
||||||
createOrUpdateMetadataField: (field: MetadataField) => observableOf(field),
|
createMetadataField: (field: MetadataField) => observableOf(field),
|
||||||
|
updateMetadataField: (field: MetadataField) => observableOf(field),
|
||||||
cancelEditMetadataField: () => {},
|
cancelEditMetadataField: () => {},
|
||||||
cancelEditMetadataSchema: () => {},
|
cancelEditMetadataSchema: () => {},
|
||||||
clearMetadataFieldRequests: () => observableOf(undefined)
|
clearMetadataFieldRequests: () => observableOf(undefined)
|
||||||
@@ -75,7 +76,6 @@ describe('MetadataFieldFormComponent', () => {
|
|||||||
const scopeNote = 'fakeScopeNote';
|
const scopeNote = 'fakeScopeNote';
|
||||||
|
|
||||||
const expected = Object.assign(new MetadataField(), {
|
const expected = Object.assign(new MetadataField(), {
|
||||||
schema: metadataSchema,
|
|
||||||
element: element,
|
element: element,
|
||||||
qualifier: qualifier,
|
qualifier: qualifier,
|
||||||
scopeNote: scopeNote
|
scopeNote: scopeNote
|
||||||
|
@@ -157,19 +157,17 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
|||||||
this.registryService.getActiveMetadataField().pipe(take(1)).subscribe(
|
this.registryService.getActiveMetadataField().pipe(take(1)).subscribe(
|
||||||
(field) => {
|
(field) => {
|
||||||
const values = {
|
const values = {
|
||||||
schema: this.metadataSchema,
|
|
||||||
element: this.element.value,
|
element: this.element.value,
|
||||||
qualifier: this.qualifier.value,
|
qualifier: this.qualifier.value,
|
||||||
scopeNote: this.scopeNote.value
|
scopeNote: this.scopeNote.value
|
||||||
};
|
};
|
||||||
if (field == null) {
|
if (field == null) {
|
||||||
this.registryService.createOrUpdateMetadataField(Object.assign(new MetadataField(), values)).subscribe((newField) => {
|
this.registryService.createMetadataField(Object.assign(new MetadataField(), values), this.metadataSchema).subscribe((newField) => {
|
||||||
this.submitForm.emit(newField);
|
this.submitForm.emit(newField);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.registryService.createOrUpdateMetadataField(Object.assign(new MetadataField(), field, {
|
this.registryService.updateMetadataField(Object.assign(new MetadataField(), field, {
|
||||||
id: field.id,
|
id: field.id,
|
||||||
schema: this.metadataSchema,
|
|
||||||
element: (values.element ? values.element : field.element),
|
element: (values.element ? values.element : field.element),
|
||||||
qualifier: (values.qualifier ? values.qualifier : field.qualifier),
|
qualifier: (values.qualifier ? values.qualifier : field.qualifier),
|
||||||
scopeNote: (values.scopeNote ? values.scopeNote : field.scopeNote)
|
scopeNote: (values.scopeNote ? values.scopeNote : field.scopeNote)
|
||||||
|
@@ -61,7 +61,7 @@ describe('MetadataSchemaComponent', () => {
|
|||||||
element: 'contributor',
|
element: 'contributor',
|
||||||
qualifier: 'advisor',
|
qualifier: 'advisor',
|
||||||
scopeNote: null,
|
scopeNote: null,
|
||||||
schema: mockSchemasList[0]
|
schema: createSuccessfulRemoteDataObject$(mockSchemasList[0])
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
@@ -73,7 +73,7 @@ describe('MetadataSchemaComponent', () => {
|
|||||||
element: 'contributor',
|
element: 'contributor',
|
||||||
qualifier: 'author',
|
qualifier: 'author',
|
||||||
scopeNote: null,
|
scopeNote: null,
|
||||||
schema: mockSchemasList[0]
|
schema: createSuccessfulRemoteDataObject$(mockSchemasList[0])
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
@@ -85,7 +85,7 @@ describe('MetadataSchemaComponent', () => {
|
|||||||
element: 'contributor',
|
element: 'contributor',
|
||||||
qualifier: 'editor',
|
qualifier: 'editor',
|
||||||
scopeNote: 'test scope note',
|
scopeNote: 'test scope note',
|
||||||
schema: mockSchemasList[1]
|
schema: createSuccessfulRemoteDataObject$(mockSchemasList[1])
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
@@ -97,15 +97,15 @@ describe('MetadataSchemaComponent', () => {
|
|||||||
element: 'contributor',
|
element: 'contributor',
|
||||||
qualifier: 'illustrator',
|
qualifier: 'illustrator',
|
||||||
scopeNote: null,
|
scopeNote: null,
|
||||||
schema: mockSchemasList[1]
|
schema: createSuccessfulRemoteDataObject$(mockSchemasList[1])
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
const mockSchemas = createSuccessfulRemoteDataObject$(new PaginatedList(null, mockSchemasList));
|
const mockSchemas = createSuccessfulRemoteDataObject$(new PaginatedList(null, mockSchemasList));
|
||||||
/* tslint:disable:no-empty */
|
/* tslint:disable:no-empty */
|
||||||
const registryServiceStub = {
|
const registryServiceStub = {
|
||||||
getMetadataSchemas: () => mockSchemas,
|
getMetadataSchemas: () => mockSchemas,
|
||||||
getMetadataFieldsBySchema: (schema: MetadataSchema) => createSuccessfulRemoteDataObject$(new PaginatedList(null, mockFieldsList.filter((value) => value.schema === schema))),
|
getMetadataFieldsBySchema: (schema: MetadataSchema) => createSuccessfulRemoteDataObject$(new PaginatedList(null, mockFieldsList.filter((value) => value.id === 3 || value.id === 4))),
|
||||||
getMetadataSchemaByName: (schemaName: string) => createSuccessfulRemoteDataObject$(mockSchemasList.filter((value) => value.prefix === schemaName)[0]),
|
getMetadataSchemaByPrefix: (schemaName: string) => createSuccessfulRemoteDataObject$(mockSchemasList.filter((value) => value.prefix === schemaName)[0]),
|
||||||
getActiveMetadataField: () => observableOf(undefined),
|
getActiveMetadataField: () => observableOf(undefined),
|
||||||
getSelectedMetadataFields: () => observableOf([]),
|
getSelectedMetadataFields: () => observableOf([]),
|
||||||
editMetadataField: (schema) => {},
|
editMetadataField: (schema) => {},
|
||||||
|
@@ -17,6 +17,7 @@ import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operato
|
|||||||
import { toFindListOptions } from '../../../shared/pagination/pagination.utils';
|
import { toFindListOptions } from '../../../shared/pagination/pagination.utils';
|
||||||
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||||
import { combineLatest } from 'rxjs/internal/observable/combineLatest';
|
import { combineLatest } from 'rxjs/internal/observable/combineLatest';
|
||||||
|
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-metadata-schema',
|
selector: 'ds-metadata-schema',
|
||||||
@@ -71,7 +72,7 @@ export class MetadataSchemaComponent implements OnInit {
|
|||||||
* @param params
|
* @param params
|
||||||
*/
|
*/
|
||||||
initialize(params) {
|
initialize(params) {
|
||||||
this.metadataSchema$ = this.registryService.getMetadataSchemaByName(params.schemaName).pipe(getFirstSucceededRemoteDataPayload());
|
this.metadataSchema$ = this.registryService.getMetadataSchemaByPrefix(params.schemaName).pipe(getFirstSucceededRemoteDataPayload());
|
||||||
this.updateFields();
|
this.updateFields();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +92,7 @@ export class MetadataSchemaComponent implements OnInit {
|
|||||||
this.metadataFields$ = combineLatest(this.metadataSchema$, this.needsUpdate$).pipe(
|
this.metadataFields$ = combineLatest(this.metadataSchema$, this.needsUpdate$).pipe(
|
||||||
switchMap(([schema, update]: [MetadataSchema, boolean]) => {
|
switchMap(([schema, update]: [MetadataSchema, boolean]) => {
|
||||||
if (update) {
|
if (update) {
|
||||||
return this.registryService.getMetadataFieldsBySchema(schema, toFindListOptions(this.config));
|
return this.registryService.getMetadataFieldsBySchema(schema, toFindListOptions(this.config), followLink('schema'));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@@ -13,6 +13,8 @@ import { AuthService } from '../../core/auth/auth.service';
|
|||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
describe('AdminSidebarComponent', () => {
|
describe('AdminSidebarComponent', () => {
|
||||||
let comp: AdminSidebarComponent;
|
let comp: AdminSidebarComponent;
|
||||||
@@ -21,13 +23,14 @@ describe('AdminSidebarComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot(), NoopAnimationsModule],
|
imports: [TranslateModule.forRoot(), NoopAnimationsModule, RouterTestingModule],
|
||||||
declarations: [AdminSidebarComponent],
|
declarations: [AdminSidebarComponent],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: Injector, useValue: {} },
|
{ provide: Injector, useValue: {} },
|
||||||
{ provide: MenuService, useValue: menuService },
|
{ provide: MenuService, useValue: menuService },
|
||||||
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
||||||
{ provide: AuthService, useClass: AuthServiceStub },
|
{ provide: AuthService, useClass: AuthServiceStub },
|
||||||
|
{ provide: ActivatedRoute, useValue: {} },
|
||||||
{
|
{
|
||||||
provide: NgbModal, useValue: {
|
provide: NgbModal, useValue: {
|
||||||
open: () => {/*comment*/}
|
open: () => {/*comment*/}
|
||||||
|
@@ -93,7 +93,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
/**
|
/**
|
||||||
* Initialize all menu sections and items for this menu
|
* Initialize all menu sections and items for this menu
|
||||||
*/
|
*/
|
||||||
private createMenu() {
|
createMenu() {
|
||||||
const menuList = [
|
const menuList = [
|
||||||
/* News */
|
/* News */
|
||||||
{
|
{
|
||||||
@@ -145,11 +145,17 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
this.modalService.open(CreateItemParentSelectorComponent);
|
this.modalService.open(CreateItemParentSelectorComponent);
|
||||||
}
|
}
|
||||||
} as OnClickMenuItemModel,
|
} as OnClickMenuItemModel,
|
||||||
// model: {
|
},
|
||||||
// type: MenuItemType.LINK,
|
{
|
||||||
// text: 'menu.section.new_item',
|
id: 'new_process',
|
||||||
// link: '/submit'
|
parentID: 'new',
|
||||||
// } as LinkMenuItemModel,
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.new_process',
|
||||||
|
link: '/processes/new'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'new_item_version',
|
id: 'new_item_version',
|
||||||
@@ -439,6 +445,19 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
icon: 'cogs',
|
icon: 'cogs',
|
||||||
index: 9
|
index: 9
|
||||||
},
|
},
|
||||||
|
/* Processes */
|
||||||
|
{
|
||||||
|
id: 'processes',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.processes',
|
||||||
|
link: '/processes'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
icon: 'terminal',
|
||||||
|
index: 10
|
||||||
|
},
|
||||||
/* Workflow */
|
/* Workflow */
|
||||||
{
|
{
|
||||||
id: 'workflow',
|
id: 'workflow',
|
||||||
@@ -453,7 +472,9 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
index: 10
|
index: 10
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, menuSection));
|
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
|
||||||
|
shouldPersistOnRouteChange: true
|
||||||
|
})));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -9,10 +9,13 @@ import { CreateCollectionPageGuard } from './create-collection-page/create-colle
|
|||||||
import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component';
|
import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component';
|
||||||
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||||
import { getCollectionModulePath } from '../app-routing.module';
|
import { getCollectionModulePath } from '../app-routing.module';
|
||||||
|
import { EditItemTemplatePageComponent } from './edit-item-template-page/edit-item-template-page.component';
|
||||||
|
import { ItemTemplatePageResolver } from './edit-item-template-page/item-template-page.resolver';
|
||||||
import { CollectionItemMapperComponent } from './collection-item-mapper/collection-item-mapper.component';
|
import { CollectionItemMapperComponent } from './collection-item-mapper/collection-item-mapper.component';
|
||||||
import { CollectionBreadcrumbResolver } from '../core/breadcrumbs/collection-breadcrumb.resolver';
|
import { CollectionBreadcrumbResolver } from '../core/breadcrumbs/collection-breadcrumb.resolver';
|
||||||
import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service';
|
import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service';
|
||||||
import { LinkService } from '../core/cache/builders/link.service';
|
import { LinkService } from '../core/cache/builders/link.service';
|
||||||
|
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
|
|
||||||
export const COLLECTION_PARENT_PARAMETER = 'parent';
|
export const COLLECTION_PARENT_PARAMETER = 'parent';
|
||||||
|
|
||||||
@@ -30,6 +33,7 @@ export function getCollectionCreatePath() {
|
|||||||
|
|
||||||
const COLLECTION_CREATE_PATH = 'create';
|
const COLLECTION_CREATE_PATH = 'create';
|
||||||
const COLLECTION_EDIT_PATH = 'edit';
|
const COLLECTION_EDIT_PATH = 'edit';
|
||||||
|
const ITEMTEMPLATE_PATH = 'itemtemplate';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -58,6 +62,16 @@ const COLLECTION_EDIT_PATH = 'edit';
|
|||||||
component: DeleteCollectionPageComponent,
|
component: DeleteCollectionPageComponent,
|
||||||
canActivate: [AuthenticatedGuard],
|
canActivate: [AuthenticatedGuard],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: ITEMTEMPLATE_PATH,
|
||||||
|
component: EditItemTemplatePageComponent,
|
||||||
|
canActivate: [AuthenticatedGuard],
|
||||||
|
resolve: {
|
||||||
|
item: ItemTemplatePageResolver,
|
||||||
|
breadcrumb: I18nBreadcrumbResolver
|
||||||
|
},
|
||||||
|
data: { title: 'collection.edit.template.title', breadcrumbKey: 'collection.edit.template' }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: CollectionPageComponent,
|
component: CollectionPageComponent,
|
||||||
@@ -75,6 +89,7 @@ const COLLECTION_EDIT_PATH = 'edit';
|
|||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
CollectionPageResolver,
|
CollectionPageResolver,
|
||||||
|
ItemTemplatePageResolver,
|
||||||
CollectionBreadcrumbResolver,
|
CollectionBreadcrumbResolver,
|
||||||
DSOBreadcrumbsService,
|
DSOBreadcrumbsService,
|
||||||
LinkService,
|
LinkService,
|
||||||
|
@@ -8,6 +8,8 @@ import { CollectionPageRoutingModule } from './collection-page-routing.module';
|
|||||||
import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component';
|
import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component';
|
||||||
import { CollectionFormComponent } from './collection-form/collection-form.component';
|
import { CollectionFormComponent } from './collection-form/collection-form.component';
|
||||||
import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component';
|
import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component';
|
||||||
|
import { EditItemTemplatePageComponent } from './edit-item-template-page/edit-item-template-page.component';
|
||||||
|
import { EditItemPageModule } from '../+item-page/edit-item-page/edit-item-page.module';
|
||||||
import { CollectionItemMapperComponent } from './collection-item-mapper/collection-item-mapper.component';
|
import { CollectionItemMapperComponent } from './collection-item-mapper/collection-item-mapper.component';
|
||||||
import { SearchService } from '../core/shared/search/search.service';
|
import { SearchService } from '../core/shared/search/search.service';
|
||||||
import { StatisticsModule } from '../statistics/statistics.module';
|
import { StatisticsModule } from '../statistics/statistics.module';
|
||||||
@@ -17,13 +19,15 @@ import { StatisticsModule } from '../statistics/statistics.module';
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
CollectionPageRoutingModule,
|
CollectionPageRoutingModule,
|
||||||
StatisticsModule.forRoot()
|
StatisticsModule.forRoot(),
|
||||||
|
EditItemPageModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
CollectionPageComponent,
|
CollectionPageComponent,
|
||||||
CreateCollectionPageComponent,
|
CreateCollectionPageComponent,
|
||||||
DeleteCollectionPageComponent,
|
DeleteCollectionPageComponent,
|
||||||
CollectionFormComponent,
|
CollectionFormComponent,
|
||||||
|
EditItemTemplatePageComponent,
|
||||||
CollectionItemMapperComponent
|
CollectionItemMapperComponent
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
|
@@ -1,3 +1,21 @@
|
|||||||
|
<div class="container-fluid mb-2" *ngVar="(itemTemplateRD$ | async) as itemTemplateRD">
|
||||||
|
<label>{{ 'collection.edit.template.label' | translate}}</label>
|
||||||
|
<div class="button-row">
|
||||||
|
<button *ngIf="!itemTemplateRD?.payload" class="btn btn-success" (click)="addItemTemplate()">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
<span class="d-none d-sm-inline"> {{"collection.edit.template.add-button" | translate}}</span>
|
||||||
|
</button>
|
||||||
|
<button *ngIf="itemTemplateRD?.payload" class="btn btn-danger" (click)="deleteItemTemplate()">
|
||||||
|
<i class="fas fa-trash-alt"></i>
|
||||||
|
<span class="d-none d-sm-inline"> {{"collection.edit.template.delete-button" | translate}}</span>
|
||||||
|
</button>
|
||||||
|
<button *ngIf="itemTemplateRD?.payload" class="btn btn-primary"
|
||||||
|
[routerLink]="'/collections/' + (dsoRD$ | async)?.payload.uuid + '/itemtemplate'">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
<span class="d-none d-sm-inline"> {{"collection.edit.template.edit-button" | translate}}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<ds-collection-form (submitForm)="onSubmit($event)"
|
<ds-collection-form (submitForm)="onSubmit($event)"
|
||||||
[dso]="(dsoRD$ | async)?.payload"
|
[dso]="(dsoRD$ | async)?.payload"
|
||||||
(finish)="navigateToHomePage()"></ds-collection-form>
|
(finish)="navigateToHomePage()"></ds-collection-form>
|
||||||
|
@@ -4,16 +4,54 @@ import { SharedModule } from '../../../shared/shared.module';
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { CollectionDataService } from '../../../core/data/collection-data.service';
|
import { CollectionDataService } from '../../../core/data/collection-data.service';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { of as observableOf } from 'rxjs/internal/observable/of';
|
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { CollectionMetadataComponent } from './collection-metadata.component';
|
import { CollectionMetadataComponent } from './collection-metadata.component';
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { ItemTemplateDataService } from '../../../core/data/item-template-data.service';
|
||||||
|
import { Collection } from '../../../core/shared/collection.model';
|
||||||
|
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
||||||
|
import { RequestService } from '../../../core/data/request.service';
|
||||||
|
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
|
|
||||||
describe('CollectionMetadataComponent', () => {
|
describe('CollectionMetadataComponent', () => {
|
||||||
let comp: CollectionMetadataComponent;
|
let comp: CollectionMetadataComponent;
|
||||||
let fixture: ComponentFixture<CollectionMetadataComponent>;
|
let fixture: ComponentFixture<CollectionMetadataComponent>;
|
||||||
|
let router: Router;
|
||||||
|
let itemTemplateService: ItemTemplateDataService;
|
||||||
|
|
||||||
|
const template = Object.assign(new Item(), {
|
||||||
|
_links: {
|
||||||
|
self: { href: 'template-selflink' }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const collection = Object.assign(new Collection(), {
|
||||||
|
uuid: 'collection-id',
|
||||||
|
id: 'collection-id',
|
||||||
|
name: 'Fake Collection',
|
||||||
|
_links: {
|
||||||
|
self: { href: 'collection-selflink' }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const itemTemplateServiceStub = Object.assign({
|
||||||
|
findByCollectionID: () => createSuccessfulRemoteDataObject$(template),
|
||||||
|
create: () => createSuccessfulRemoteDataObject$(template),
|
||||||
|
deleteByCollectionID: () => observableOf(true)
|
||||||
|
});
|
||||||
|
|
||||||
|
const notificationsService = jasmine.createSpyObj('notificationsService', {
|
||||||
|
success: {},
|
||||||
|
error: {}
|
||||||
|
});
|
||||||
|
const objectCache = jasmine.createSpyObj('objectCache', {
|
||||||
|
remove: {}
|
||||||
|
});
|
||||||
|
const requestService = jasmine.createSpyObj('requestService', {
|
||||||
|
removeByHrefSubstring: {}
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@@ -21,8 +59,11 @@ describe('CollectionMetadataComponent', () => {
|
|||||||
declarations: [CollectionMetadataComponent],
|
declarations: [CollectionMetadataComponent],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: CollectionDataService, useValue: {} },
|
{ provide: CollectionDataService, useValue: {} },
|
||||||
{ provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: { payload: {} } }) } } },
|
{ provide: ItemTemplateDataService, useValue: itemTemplateServiceStub },
|
||||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }
|
{ provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: createSuccessfulRemoteDataObject(collection) }) } } },
|
||||||
|
{ provide: NotificationsService, useValue: notificationsService },
|
||||||
|
{ provide: ObjectCacheService, useValue: objectCache },
|
||||||
|
{ provide: RequestService, useValue: requestService }
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
@@ -31,12 +72,51 @@ describe('CollectionMetadataComponent', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(CollectionMetadataComponent);
|
fixture = TestBed.createComponent(CollectionMetadataComponent);
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
|
router = (comp as any).router;
|
||||||
|
itemTemplateService = (comp as any).itemTemplateService;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('frontendURL', () => {
|
describe('frontendURL', () => {
|
||||||
it('should have the right frontendURL set', () => {
|
it('should have the right frontendURL set', () => {
|
||||||
expect((comp as any).frontendURL).toEqual('/collections/');
|
expect((comp as any).frontendURL).toEqual('/collections/');
|
||||||
})
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addItemTemplate', () => {
|
||||||
|
it('should navigate to the collection\'s itemtemplate page', () => {
|
||||||
|
spyOn(router, 'navigate');
|
||||||
|
comp.addItemTemplate();
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith(['collections', collection.uuid, 'itemtemplate']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteItemTemplate', () => {
|
||||||
|
describe('when delete returns a success', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(itemTemplateService, 'deleteByCollectionID').and.returnValue(observableOf(true));
|
||||||
|
comp.deleteItemTemplate();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display a success notification', () => {
|
||||||
|
expect(notificationsService.success).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset related object and request cache', () => {
|
||||||
|
expect(objectCache.remove).toHaveBeenCalledWith(template.self);
|
||||||
|
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(collection.self);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when delete returns a failure', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(itemTemplateService, 'deleteByCollectionID').and.returnValue(observableOf(false));
|
||||||
|
comp.deleteItemTemplate();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display an error notification', () => {
|
||||||
|
expect(notificationsService.error).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -3,8 +3,17 @@ import { ComcolMetadataComponent } from '../../../shared/comcol-forms/edit-comco
|
|||||||
import { Collection } from '../../../core/shared/collection.model';
|
import { Collection } from '../../../core/shared/collection.model';
|
||||||
import { CollectionDataService } from '../../../core/data/collection-data.service';
|
import { CollectionDataService } from '../../../core/data/collection-data.service';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { ItemTemplateDataService } from '../../../core/data/item-template-data.service';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../core/shared/operators';
|
||||||
|
import { switchMap, take } from 'rxjs/operators';
|
||||||
|
import { combineLatest as combineLatestObservable } from 'rxjs';
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
||||||
|
import { RequestService } from '../../../core/data/request.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component for editing a collection's metadata
|
* Component for editing a collection's metadata
|
||||||
@@ -17,13 +26,91 @@ export class CollectionMetadataComponent extends ComcolMetadataComponent<Collect
|
|||||||
protected frontendURL = '/collections/';
|
protected frontendURL = '/collections/';
|
||||||
protected type = Collection.type;
|
protected type = Collection.type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The collection's item template
|
||||||
|
*/
|
||||||
|
itemTemplateRD$: Observable<RemoteData<Item>>;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
protected collectionDataService: CollectionDataService,
|
protected collectionDataService: CollectionDataService,
|
||||||
|
protected itemTemplateService: ItemTemplateDataService,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
protected route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
protected notificationsService: NotificationsService,
|
protected notificationsService: NotificationsService,
|
||||||
protected translate: TranslateService
|
protected translate: TranslateService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected requestService: RequestService
|
||||||
) {
|
) {
|
||||||
super(collectionDataService, router, route, notificationsService, translate);
|
super(collectionDataService, router, route, notificationsService, translate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
super.ngOnInit();
|
||||||
|
this.initTemplateItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the collection's item template
|
||||||
|
*/
|
||||||
|
initTemplateItem() {
|
||||||
|
this.itemTemplateRD$ = this.dsoRD$.pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
switchMap((collection: Collection) => this.itemTemplateService.findByCollectionID(collection.uuid))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new item template to the collection and redirect to the item template edit page
|
||||||
|
*/
|
||||||
|
addItemTemplate() {
|
||||||
|
const collection$ = this.dsoRD$.pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
take(1)
|
||||||
|
);
|
||||||
|
const template$ = collection$.pipe(
|
||||||
|
switchMap((collection: Collection) => this.itemTemplateService.create(new Item(), collection.uuid)),
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
take(1)
|
||||||
|
);
|
||||||
|
|
||||||
|
combineLatestObservable(collection$, template$).subscribe(([collection, template]) => {
|
||||||
|
this.router.navigate(['collections', collection.uuid, 'itemtemplate']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the item template from the collection
|
||||||
|
*/
|
||||||
|
deleteItemTemplate() {
|
||||||
|
const collection$ = this.dsoRD$.pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
take(1)
|
||||||
|
);
|
||||||
|
const template$ = collection$.pipe(
|
||||||
|
switchMap((collection: Collection) => this.itemTemplateService.findByCollectionID(collection.uuid)),
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
take(1)
|
||||||
|
);
|
||||||
|
|
||||||
|
combineLatestObservable(collection$, template$).pipe(
|
||||||
|
switchMap(([collection, template]) => {
|
||||||
|
const success$ = this.itemTemplateService.deleteByCollectionID(template, collection.uuid);
|
||||||
|
this.objectCache.remove(template.self);
|
||||||
|
this.requestService.removeByHrefSubstring(collection.self);
|
||||||
|
return success$;
|
||||||
|
})
|
||||||
|
).subscribe((success: boolean) => {
|
||||||
|
if (success) {
|
||||||
|
this.notificationsService.success(null, this.translate.get('collection.edit.template.notifications.delete.success'));
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(null, this.translate.get('collection.edit.template.notifications.delete.error'));
|
||||||
|
}
|
||||||
|
this.initTemplateItem();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,9 @@
|
|||||||
|
<div class="container" *ngVar="(collectionRD$ | async)?.payload as collection">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<h2 class="border-bottom">{{ 'collection.edit.template.head' | translate:{ collection: collection?.name } }}</h2>
|
||||||
|
<ds-item-metadata [updateService]="itemTemplateService"></ds-item-metadata>
|
||||||
|
<button [routerLink]="getCollectionEditUrl(collection)" class="btn btn-outline-secondary">{{ 'collection.edit.template.cancel' | translate }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,51 @@
|
|||||||
|
import { EditItemTemplatePageComponent } from './edit-item-template-page.component';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { ItemTemplateDataService } from '../../core/data/item-template-data.service';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||||
|
import { Collection } from '../../core/shared/collection.model';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { getCollectionEditPath } from '../collection-page-routing.module';
|
||||||
|
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
||||||
|
|
||||||
|
describe('EditItemTemplatePageComponent', () => {
|
||||||
|
let comp: EditItemTemplatePageComponent;
|
||||||
|
let fixture: ComponentFixture<EditItemTemplatePageComponent>;
|
||||||
|
let itemTemplateService: ItemTemplateDataService;
|
||||||
|
let collection: Collection;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
collection = Object.assign(new Collection(), {
|
||||||
|
uuid: 'collection-id',
|
||||||
|
id: 'collection-id',
|
||||||
|
name: 'Fake Collection'
|
||||||
|
});
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
|
||||||
|
declarations: [EditItemTemplatePageComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: ItemTemplateDataService, useValue: {} },
|
||||||
|
{ provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: createSuccessfulRemoteDataObject(collection) }) } } }
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(EditItemTemplatePageComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
itemTemplateService = (comp as any).itemTemplateService;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getCollectionEditUrl', () => {
|
||||||
|
it('should return the collection\'s edit url', () => {
|
||||||
|
const url = comp.getCollectionEditUrl(collection);
|
||||||
|
expect(url).toEqual(getCollectionEditPath(collection.uuid));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,44 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { Collection } from '../../core/shared/collection.model';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { first, map } from 'rxjs/operators';
|
||||||
|
import { ItemTemplateDataService } from '../../core/data/item-template-data.service';
|
||||||
|
import { getCollectionEditPath } from '../collection-page-routing.module';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-edit-item-template-page',
|
||||||
|
templateUrl: './edit-item-template-page.component.html',
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component for editing the item template of a collection
|
||||||
|
*/
|
||||||
|
export class EditItemTemplatePageComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The collection to edit the item template for
|
||||||
|
*/
|
||||||
|
collectionRD$: Observable<RemoteData<Collection>>;
|
||||||
|
|
||||||
|
constructor(protected route: ActivatedRoute,
|
||||||
|
public itemTemplateService: ItemTemplateDataService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.collectionRD$ = this.route.parent.data.pipe(first(), map((data) => data.dso));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the URL to the collection's edit page
|
||||||
|
* @param collection
|
||||||
|
*/
|
||||||
|
getCollectionEditUrl(collection: Collection): string {
|
||||||
|
if (collection) {
|
||||||
|
return getCollectionEditPath(collection.uuid);
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,28 @@
|
|||||||
|
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||||
|
import { first } from 'rxjs/operators';
|
||||||
|
import { ItemTemplatePageResolver } from './item-template-page.resolver';
|
||||||
|
|
||||||
|
describe('ItemTemplatePageResolver', () => {
|
||||||
|
describe('resolve', () => {
|
||||||
|
let resolver: ItemTemplatePageResolver;
|
||||||
|
let itemTemplateService: any;
|
||||||
|
const uuid = '1234-65487-12354-1235';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
itemTemplateService = {
|
||||||
|
findByCollectionID: (id: string) => observableOf({ payload: { id }, hasSucceeded: true })
|
||||||
|
};
|
||||||
|
resolver = new ItemTemplatePageResolver(itemTemplateService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve an item template with the correct id', () => {
|
||||||
|
resolver.resolve({ params: { id: uuid } } as any, undefined)
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe(
|
||||||
|
(resolved) => {
|
||||||
|
expect(resolved.payload.id).toEqual(uuid);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,31 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { ItemTemplateDataService } from '../../core/data/item-template-data.service';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { find } from 'rxjs/operators';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class represents a resolver that requests a specific collection's item template before the route is activated
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ItemTemplatePageResolver implements Resolve<RemoteData<Item>> {
|
||||||
|
constructor(private itemTemplateService: ItemTemplateDataService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method for resolving a collection's item template based on the parameters in the current route
|
||||||
|
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||||
|
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||||
|
* @returns Observable<<RemoteData<Collection>> Emits the found item template based on the parameters in the current route,
|
||||||
|
* or an error if something went wrong
|
||||||
|
*/
|
||||||
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Item>> {
|
||||||
|
return this.itemTemplateService.findByCollectionID(route.params.id, followLink('templateItemOf')).pipe(
|
||||||
|
find((RD) => hasValue(RD.error) || RD.hasSucceeded),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -11,6 +11,7 @@ import { first, map } from 'rxjs/operators';
|
|||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { AbstractTrackableComponent } from '../../../shared/trackable/abstract-trackable.component';
|
import { AbstractTrackableComponent } from '../../../shared/trackable/abstract-trackable.component';
|
||||||
import { environment } from '../../../../environments/environment';
|
import { environment } from '../../../../environments/environment';
|
||||||
|
import { combineLatest as observableCombineLatest } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-abstract-item-update',
|
selector: 'ds-abstract-item-update',
|
||||||
@@ -45,8 +46,9 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl
|
|||||||
* Initialize common properties between item-update components
|
* Initialize common properties between item-update components
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.route.parent.data.pipe(map((data) => data.item))
|
observableCombineLatest(this.route.data, this.route.parent.data).pipe(
|
||||||
.pipe(
|
map(([data, parentData]) => Object.assign({}, data, parentData)),
|
||||||
|
map((data) => data.item),
|
||||||
first(),
|
first(),
|
||||||
map((data: RemoteData<Item>) => data.payload)
|
map((data: RemoteData<Item>) => data.payload)
|
||||||
).subscribe((item: Item) => {
|
).subscribe((item: Item) => {
|
||||||
|
@@ -78,6 +78,9 @@ import { ObjectValuesPipe } from '../../shared/utils/object-values-pipe';
|
|||||||
providers: [
|
providers: [
|
||||||
BundleDataService,
|
BundleDataService,
|
||||||
ObjectValuesPipe
|
ObjectValuesPipe
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
ItemMetadataComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class EditItemPageModule {
|
export class EditItemPageModule {
|
||||||
|
@@ -142,6 +142,7 @@ describe('ItemBitstreamsComponent', () => {
|
|||||||
parent: {
|
parent: {
|
||||||
data: observableOf({ item: createMockRD(item) })
|
data: observableOf({ item: createMockRD(item) })
|
||||||
},
|
},
|
||||||
|
data: observableOf({}),
|
||||||
url: url
|
url: url
|
||||||
});
|
});
|
||||||
bundleService = jasmine.createSpyObj('bundleService', {
|
bundleService = jasmine.createSpyObj('bundleService', {
|
||||||
|
@@ -58,7 +58,7 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges {
|
|||||||
metadataFieldSuggestions: BehaviorSubject<InputSuggestion[]> = new BehaviorSubject([]);
|
metadataFieldSuggestions: BehaviorSubject<InputSuggestion[]> = new BehaviorSubject([]);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private metadataFieldService: RegistryService,
|
private registryService: RegistryService,
|
||||||
private objectUpdatesService: ObjectUpdatesService,
|
private objectUpdatesService: ObjectUpdatesService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges {
|
|||||||
* Sends a new change update for this field to the object updates service
|
* Sends a new change update for this field to the object updates service
|
||||||
*/
|
*/
|
||||||
update(ngModel?: NgModel) {
|
update(ngModel?: NgModel) {
|
||||||
this.objectUpdatesService.saveChangeFieldUpdate(this.url, this.metadata);
|
this.objectUpdatesService.saveChangeFieldUpdate(this.url, cloneDeep(this.metadata));
|
||||||
if (hasValue(ngModel)) {
|
if (hasValue(ngModel)) {
|
||||||
this.checkValidity(ngModel);
|
this.checkValidity(ngModel);
|
||||||
}
|
}
|
||||||
@@ -103,7 +103,7 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges {
|
|||||||
* Sends a new remove update for this field to the object updates service
|
* Sends a new remove update for this field to the object updates service
|
||||||
*/
|
*/
|
||||||
remove() {
|
remove() {
|
||||||
this.objectUpdatesService.saveRemoveFieldUpdate(this.url, this.metadata);
|
this.objectUpdatesService.saveRemoveFieldUpdate(this.url, cloneDeep(this.metadata));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -123,11 +123,12 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges {
|
|||||||
/**
|
/**
|
||||||
* Requests all metadata fields that contain the query string in their key
|
* Requests all metadata fields that contain the query string in their key
|
||||||
* Then sets all found metadata fields as metadataFieldSuggestions
|
* Then sets all found metadata fields as metadataFieldSuggestions
|
||||||
|
* Ignores fields from metadata schemas "relation" and "relationship"
|
||||||
* @param query The query to look for
|
* @param query The query to look for
|
||||||
*/
|
*/
|
||||||
findMetadataFieldSuggestions(query: string): void {
|
findMetadataFieldSuggestions(query: string): void {
|
||||||
if (isNotEmpty(query)) {
|
if (isNotEmpty(query)) {
|
||||||
this.metadataFieldService.queryMetadataFields(query).pipe(
|
this.registryService.queryMetadataFields(query).pipe(
|
||||||
// getSucceededRemoteData(),
|
// getSucceededRemoteData(),
|
||||||
take(1),
|
take(1),
|
||||||
map((data) => data.payload.page)
|
map((data) => data.payload.page)
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<div class="item-metadata">
|
<div class="item-metadata">
|
||||||
<div class="button-row top d-flex">
|
<div class="button-row top d-flex mb-2">
|
||||||
<button class="mr-auto btn btn-success"
|
<button class="mr-auto btn btn-success"
|
||||||
(click)="add()"><i
|
(click)="add()"><i
|
||||||
class="fas fa-plus"></i>
|
class="fas fa-plus"></i>
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-responsive table-striped table-bordered">
|
<table class="table table-responsive table-striped table-bordered" *ngIf="((updates$ | async)| dsObjectValues).length > 0">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{'item.edit.metadata.headers.field' | translate}}</th>
|
<th>{{'item.edit.metadata.headers.field' | translate}}</th>
|
||||||
@@ -44,14 +44,17 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<div *ngIf="((updates$ | async)| dsObjectValues).length == 0">
|
||||||
|
<ds-alert [content]="'item.edit.metadata.empty'" [type]="AlertTypeEnum.Info"></ds-alert>
|
||||||
|
</div>
|
||||||
<div class="button-row bottom">
|
<div class="button-row bottom">
|
||||||
<div class="my-2 float-right">
|
<div class="float-right">
|
||||||
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
<button class="btn btn-danger mr-1" *ngIf="!(isReinstatable() | async)"
|
||||||
[disabled]="!(hasChanges() | async)"
|
[disabled]="!(hasChanges() | async)"
|
||||||
(click)="discard()"><i
|
(click)="discard()"><i
|
||||||
class="fas fa-times"></i> {{"item.edit.metadata.discard-button" | translate}}
|
class="fas fa-times"></i> {{"item.edit.metadata.discard-button" | translate}}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
|
<button class="btn btn-warning mr-1" *ngIf="isReinstatable() | async"
|
||||||
(click)="reinstate()"><i
|
(click)="reinstate()"><i
|
||||||
class="fas fa-undo-alt"></i> {{"item.edit.metadata.reinstate-button" | translate}}
|
class="fas fa-undo-alt"></i> {{"item.edit.metadata.reinstate-button" | translate}}
|
||||||
</button>
|
</button>
|
||||||
|
@@ -122,6 +122,7 @@ describe('ItemMetadataComponent', () => {
|
|||||||
commitUpdates: {}
|
commitUpdates: {}
|
||||||
});
|
});
|
||||||
routeStub = {
|
routeStub = {
|
||||||
|
data: observableOf({}),
|
||||||
parent: {
|
parent: {
|
||||||
data: observableOf({ item: createSuccessfulRemoteDataObject(item) })
|
data: observableOf({ item: createSuccessfulRemoteDataObject(item) })
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
import { Item } from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import { ItemDataService } from '../../../core/data/item-data.service';
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
||||||
@@ -12,10 +12,13 @@ import { RemoteData } from '../../../core/data/remote-data';
|
|||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { RegistryService } from '../../../core/registry/registry.service';
|
import { RegistryService } from '../../../core/registry/registry.service';
|
||||||
import { MetadatumViewModel } from '../../../core/shared/metadata.models';
|
import { MetadataValue, MetadatumViewModel } from '../../../core/shared/metadata.models';
|
||||||
import { Metadata } from '../../../core/shared/metadata.utils';
|
import { Metadata } from '../../../core/shared/metadata.utils';
|
||||||
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
|
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
|
||||||
import { MetadataField } from '../../../core/metadata/metadata-field.model';
|
import { MetadataField } from '../../../core/metadata/metadata-field.model';
|
||||||
|
import { UpdateDataService } from '../../../core/data/update-data.service';
|
||||||
|
import { hasNoValue, hasValue } from '../../../shared/empty.util';
|
||||||
|
import { AlertType } from '../../../shared/alert/aletr-type';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-metadata',
|
selector: 'ds-item-metadata',
|
||||||
@@ -27,6 +30,18 @@ import { MetadataField } from '../../../core/metadata/metadata-field.model';
|
|||||||
*/
|
*/
|
||||||
export class ItemMetadataComponent extends AbstractItemUpdateComponent {
|
export class ItemMetadataComponent extends AbstractItemUpdateComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The AlertType enumeration
|
||||||
|
* @type {AlertType}
|
||||||
|
*/
|
||||||
|
public AlertTypeEnum = AlertType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom update service to use for adding and committing patches
|
||||||
|
* This will default to the ItemDataService
|
||||||
|
*/
|
||||||
|
@Input() updateService: UpdateDataService<Item>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observable with a list of strings with all existing metadata field keys
|
* Observable with a list of strings with all existing metadata field keys
|
||||||
*/
|
*/
|
||||||
@@ -50,6 +65,9 @@ export class ItemMetadataComponent extends AbstractItemUpdateComponent {
|
|||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
this.metadataFields$ = this.findMetadataFields();
|
this.metadataFields$ = this.findMetadataFields();
|
||||||
|
if (hasNoValue(this.updateService)) {
|
||||||
|
this.updateService = this.itemService;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -88,20 +106,21 @@ export class ItemMetadataComponent extends AbstractItemUpdateComponent {
|
|||||||
public submit() {
|
public submit() {
|
||||||
this.isValid().pipe(first()).subscribe((isValid) => {
|
this.isValid().pipe(first()).subscribe((isValid) => {
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
const metadata$: Observable<Identifiable[]> = this.objectUpdatesService.getUpdatedFields(this.url, this.getMetadataAsListExcludingRelationships()) as Observable<MetadatumViewModel[]>;
|
const metadata$: Observable<Identifiable[]> = this.objectUpdatesService.getUpdatedFields(this.url, this.item.metadataAsList) as Observable<MetadatumViewModel[]>;
|
||||||
metadata$.pipe(
|
metadata$.pipe(
|
||||||
first(),
|
first(),
|
||||||
switchMap((metadata: MetadatumViewModel[]) => {
|
switchMap((metadata: MetadatumViewModel[]) => {
|
||||||
const updatedItem: Item = Object.assign(cloneDeep(this.item), { metadata: Metadata.toMetadataMap(metadata) });
|
const updatedItem: Item = Object.assign(cloneDeep(this.item), { metadata: Metadata.toMetadataMap(metadata) });
|
||||||
return this.itemService.update(updatedItem);
|
return this.updateService.update(updatedItem);
|
||||||
}),
|
}),
|
||||||
tap(() => this.itemService.commitUpdates()),
|
tap(() => this.updateService.commitUpdates()),
|
||||||
getSucceededRemoteData()
|
getSucceededRemoteData()
|
||||||
).subscribe(
|
).subscribe(
|
||||||
(rd: RemoteData<Item>) => {
|
(rd: RemoteData<Item>) => {
|
||||||
this.item = rd.payload;
|
this.item = rd.payload;
|
||||||
|
this.checkAndFixMetadataUUIDs();
|
||||||
this.initializeOriginalFields();
|
this.initializeOriginalFields();
|
||||||
this.updates$ = this.objectUpdatesService.getFieldUpdates(this.url, this.getMetadataAsListExcludingRelationships());
|
this.updates$ = this.objectUpdatesService.getFieldUpdates(this.url, this.item.metadataAsList);
|
||||||
this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved'));
|
this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved'));
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -121,7 +140,14 @@ export class ItemMetadataComponent extends AbstractItemUpdateComponent {
|
|||||||
map((remoteData$) => remoteData$.payload.page.map((field: MetadataField) => field.toString())));
|
map((remoteData$) => remoteData$.payload.page.map((field: MetadataField) => field.toString())));
|
||||||
}
|
}
|
||||||
|
|
||||||
getMetadataAsListExcludingRelationships(): MetadatumViewModel[] {
|
/**
|
||||||
return this.item.metadataAsList.filter((metadata: MetadatumViewModel) => !metadata.key.startsWith('relation.') && !metadata.key.startsWith('relationship.'));
|
* Check for empty metadata UUIDs and fix them (empty UUIDs would break the object-update service)
|
||||||
|
*/
|
||||||
|
checkAndFixMetadataUUIDs() {
|
||||||
|
const metadata = cloneDeep(this.item.metadata);
|
||||||
|
Object.keys(this.item.metadata).forEach((key: string) => {
|
||||||
|
metadata[key] = this.item.metadata[key].map((value) => hasValue(value.uuid) ? value : Object.assign(new MetadataValue(), value));
|
||||||
|
});
|
||||||
|
this.item.metadata = metadata;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -140,6 +140,7 @@ describe('ItemRelationshipsComponent', () => {
|
|||||||
findById: observableOf(new RemoteData(false, false, true, undefined, item))
|
findById: observableOf(new RemoteData(false, false, true, undefined, item))
|
||||||
});
|
});
|
||||||
routeStub = {
|
routeStub = {
|
||||||
|
data: observableOf({}),
|
||||||
parent: {
|
parent: {
|
||||||
data: observableOf({ item: new RemoteData(false, false, true, null, item) })
|
data: observableOf({ item: new RemoteData(false, false, true, null, item) })
|
||||||
}
|
}
|
||||||
|
@@ -114,6 +114,7 @@ export function getDSOPath(dso: DSpaceObject): string {
|
|||||||
path: PROFILE_MODULE_PATH,
|
path: PROFILE_MODULE_PATH,
|
||||||
loadChildren: './profile-page/profile-page.module#ProfilePageModule', canActivate: [AuthenticatedGuard]
|
loadChildren: './profile-page/profile-page.module#ProfilePageModule', canActivate: [AuthenticatedGuard]
|
||||||
},
|
},
|
||||||
|
{ path: 'processes', loadChildren: './process-page/process-page.module#ProcessPageModule', canActivate: [AuthenticatedGuard] },
|
||||||
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
|
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
|
@@ -462,7 +462,7 @@ describe('CommunityListService', () => {
|
|||||||
});
|
});
|
||||||
let flatNodeList;
|
let flatNodeList;
|
||||||
describe('should return list containing only flatnode corresponding to that community', () => {
|
describe('should return list containing only flatnode corresponding to that community', () => {
|
||||||
beforeAll((done) => {
|
beforeEach((done) => {
|
||||||
service.transformCommunity(communityWithSubcoms, 0, null, null)
|
service.transformCommunity(communityWithSubcoms, 0, null, null)
|
||||||
.pipe(take(1))
|
.pipe(take(1))
|
||||||
.subscribe((value) => {
|
.subscribe((value) => {
|
||||||
|
@@ -1,21 +1,38 @@
|
|||||||
import { I18nBreadcrumbResolver } from './i18n-breadcrumb.resolver';
|
import { I18nBreadcrumbResolver } from './i18n-breadcrumb.resolver';
|
||||||
|
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||||
|
|
||||||
describe('I18nBreadcrumbResolver', () => {
|
describe('I18nBreadcrumbResolver', () => {
|
||||||
describe('resolve', () => {
|
describe('resolve', () => {
|
||||||
let resolver: I18nBreadcrumbResolver;
|
let resolver: I18nBreadcrumbResolver;
|
||||||
let i18nBreadcrumbService: any;
|
let i18nBreadcrumbService: any;
|
||||||
let i18nKey: string;
|
let i18nKey: string;
|
||||||
let path: string;
|
let route: any;
|
||||||
|
let parentSegment;
|
||||||
|
let segment;
|
||||||
|
let expectedPath;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
i18nKey = 'example.key';
|
i18nKey = 'example.key';
|
||||||
path = 'rest.com/path/to/breadcrumb';
|
parentSegment = 'path';
|
||||||
|
segment = 'breadcrumb';
|
||||||
|
route = {
|
||||||
|
data: { breadcrumbKey: i18nKey },
|
||||||
|
routeConfig: {
|
||||||
|
path: segment
|
||||||
|
},
|
||||||
|
parent: {
|
||||||
|
routeConfig: {
|
||||||
|
path: parentSegment
|
||||||
|
}
|
||||||
|
} as any
|
||||||
|
};
|
||||||
|
expectedPath = new URLCombiner(parentSegment, segment).toString();
|
||||||
i18nBreadcrumbService = {};
|
i18nBreadcrumbService = {};
|
||||||
resolver = new I18nBreadcrumbResolver(i18nBreadcrumbService);
|
resolver = new I18nBreadcrumbResolver(i18nBreadcrumbService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should resolve the breadcrumb config', () => {
|
it('should resolve the breadcrumb config', () => {
|
||||||
const resolvedConfig = resolver.resolve({ data: { breadcrumbKey: i18nKey }, url: [path], pathFromRoot: [{ url: [path] }] } as any, {} as any);
|
const resolvedConfig = resolver.resolve(route, {} as any);
|
||||||
const expectedConfig = { provider: i18nBreadcrumbService, key: i18nKey, url: path };
|
const expectedConfig = { provider: i18nBreadcrumbService, key: i18nKey, url: expectedPath };
|
||||||
expect(resolvedConfig).toEqual(expectedConfig);
|
expect(resolvedConfig).toEqual(expectedConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -3,6 +3,7 @@ import { Injectable } from '@angular/core';
|
|||||||
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
||||||
import { I18nBreadcrumbsService } from './i18n-breadcrumbs.service';
|
import { I18nBreadcrumbsService } from './i18n-breadcrumbs.service';
|
||||||
import { hasNoValue } from '../../shared/empty.util';
|
import { hasNoValue } from '../../shared/empty.util';
|
||||||
|
import { currentPathFromSnapshot } from '../../shared/utils/route.utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The class that resolves a BreadcrumbConfig object with an i18n key string for a route
|
* The class that resolves a BreadcrumbConfig object with an i18n key string for a route
|
||||||
@@ -25,17 +26,7 @@ export class I18nBreadcrumbResolver implements Resolve<BreadcrumbConfig<string>>
|
|||||||
if (hasNoValue(key)) {
|
if (hasNoValue(key)) {
|
||||||
throw new Error('You provided an i18nBreadcrumbResolver for url \"' + route.url + '\" but no breadcrumbKey in the route\'s data')
|
throw new Error('You provided an i18nBreadcrumbResolver for url \"' + route.url + '\" but no breadcrumbKey in the route\'s data')
|
||||||
}
|
}
|
||||||
const fullPath = this.getResolvedUrl(route);
|
const fullPath = currentPathFromSnapshot(route);
|
||||||
return { provider: this.breadcrumbService, key: key, url: fullPath };
|
return { provider: this.breadcrumbService, key: key, url: fullPath };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve the full URL of an ActivatedRouteSnapshot
|
|
||||||
* @param route
|
|
||||||
*/
|
|
||||||
getResolvedUrl(route: ActivatedRouteSnapshot): string {
|
|
||||||
return route.pathFromRoot
|
|
||||||
.map((v) => v.url.map((segment) => segment.toString()).join('/'))
|
|
||||||
.join('/');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -7,6 +7,7 @@ import { ServerSyncBufferEffects } from './cache/server-sync-buffer.effects';
|
|||||||
import { ObjectUpdatesEffects } from './data/object-updates/object-updates.effects';
|
import { ObjectUpdatesEffects } from './data/object-updates/object-updates.effects';
|
||||||
import { RouteEffects } from './services/route.effects';
|
import { RouteEffects } from './services/route.effects';
|
||||||
import { RouterEffects } from './router/router.effects';
|
import { RouterEffects } from './router/router.effects';
|
||||||
|
import { MenuEffects } from '../shared/menu/menu.effects';
|
||||||
|
|
||||||
export const coreEffects = [
|
export const coreEffects = [
|
||||||
RequestEffects,
|
RequestEffects,
|
||||||
@@ -17,5 +18,6 @@ export const coreEffects = [
|
|||||||
ServerSyncBufferEffects,
|
ServerSyncBufferEffects,
|
||||||
ObjectUpdatesEffects,
|
ObjectUpdatesEffects,
|
||||||
RouteEffects,
|
RouteEffects,
|
||||||
RouterEffects
|
RouterEffects,
|
||||||
|
MenuEffects
|
||||||
];
|
];
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
|
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
|
||||||
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
|
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
|
||||||
|
|
||||||
import { DynamicFormLayoutService, DynamicFormService, DynamicFormValidationService } from '@ng-dynamic-forms/core';
|
import { DynamicFormLayoutService, DynamicFormService, DynamicFormValidationService } from '@ng-dynamic-forms/core';
|
||||||
import { EffectsModule } from '@ngrx/effects';
|
import { EffectsModule } from '@ngrx/effects';
|
||||||
|
|
||||||
@@ -138,13 +137,24 @@ import { VersionDataService } from './data/version-data.service';
|
|||||||
import { VersionHistoryDataService } from './data/version-history-data.service';
|
import { VersionHistoryDataService } from './data/version-history-data.service';
|
||||||
import { Version } from './shared/version.model';
|
import { Version } from './shared/version.model';
|
||||||
import { VersionHistory } from './shared/version-history.model';
|
import { VersionHistory } from './shared/version-history.model';
|
||||||
|
import { Script } from '../process-page/scripts/script.model';
|
||||||
|
import { Process } from '../process-page/processes/process.model';
|
||||||
|
import { ProcessDataService } from './data/processes/process-data.service';
|
||||||
|
import { ScriptDataService } from './data/processes/script-data.service';
|
||||||
|
import { ProcessFilesResponseParsingService } from './data/process-files-response-parsing.service';
|
||||||
import { WorkflowActionDataService } from './data/workflow-action-data.service';
|
import { WorkflowActionDataService } from './data/workflow-action-data.service';
|
||||||
import { WorkflowAction } from './tasks/models/workflow-action-object.model';
|
import { WorkflowAction } from './tasks/models/workflow-action-object.model';
|
||||||
import { LocaleInterceptor } from './locale/locale.interceptor';
|
import { LocaleInterceptor } from './locale/locale.interceptor';
|
||||||
|
import { ItemTemplateDataService } from './data/item-template-data.service';
|
||||||
|
import { TemplateItem } from './shared/template-item.model';
|
||||||
import { Registration } from './shared/registration.model';
|
import { Registration } from './shared/registration.model';
|
||||||
import { MetadataSchemaDataService } from './data/metadata-schema-data.service';
|
import { MetadataSchemaDataService } from './data/metadata-schema-data.service';
|
||||||
import { MetadataFieldDataService } from './data/metadata-field-data.service';
|
import { MetadataFieldDataService } from './data/metadata-field-data.service';
|
||||||
import { TokenResponseParsingService } from './auth/token-response-parsing.service';
|
import { TokenResponseParsingService } from './auth/token-response-parsing.service';
|
||||||
|
import { SubmissionCcLicenseDataService } from './submission/submission-cc-license-data.service';
|
||||||
|
import { SubmissionCcLicence } from './submission/models/submission-cc-license.model';
|
||||||
|
import { SubmissionCcLicenceUrl } from './submission/models/submission-cc-license-url.model';
|
||||||
|
import { SubmissionCcLicenseUrlDataService } from './submission/submission-cc-license-url-data.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When not in production, endpoint responses can be mocked for testing purposes
|
* When not in production, endpoint responses can be mocked for testing purposes
|
||||||
@@ -211,6 +221,8 @@ const PROVIDERS = [
|
|||||||
BrowseItemsResponseParsingService,
|
BrowseItemsResponseParsingService,
|
||||||
BrowseService,
|
BrowseService,
|
||||||
ConfigResponseParsingService,
|
ConfigResponseParsingService,
|
||||||
|
SubmissionCcLicenseDataService,
|
||||||
|
SubmissionCcLicenseUrlDataService,
|
||||||
SubmissionDefinitionsConfigService,
|
SubmissionDefinitionsConfigService,
|
||||||
SubmissionFormsConfigService,
|
SubmissionFormsConfigService,
|
||||||
SubmissionRestService,
|
SubmissionRestService,
|
||||||
@@ -245,6 +257,7 @@ const PROVIDERS = [
|
|||||||
BitstreamDataService,
|
BitstreamDataService,
|
||||||
EntityTypeService,
|
EntityTypeService,
|
||||||
ContentSourceResponseParsingService,
|
ContentSourceResponseParsingService,
|
||||||
|
ItemTemplateDataService,
|
||||||
SearchService,
|
SearchService,
|
||||||
SidebarService,
|
SidebarService,
|
||||||
SearchFilterService,
|
SearchFilterService,
|
||||||
@@ -259,6 +272,9 @@ const PROVIDERS = [
|
|||||||
LicenseDataService,
|
LicenseDataService,
|
||||||
ItemTypeDataService,
|
ItemTypeDataService,
|
||||||
WorkflowActionDataService,
|
WorkflowActionDataService,
|
||||||
|
ProcessDataService,
|
||||||
|
ScriptDataService,
|
||||||
|
ProcessFilesResponseParsingService,
|
||||||
MetadataSchemaDataService,
|
MetadataSchemaDataService,
|
||||||
MetadataFieldDataService,
|
MetadataFieldDataService,
|
||||||
TokenResponseParsingService,
|
TokenResponseParsingService,
|
||||||
@@ -300,6 +316,8 @@ export const models =
|
|||||||
License,
|
License,
|
||||||
WorkflowItem,
|
WorkflowItem,
|
||||||
WorkspaceItem,
|
WorkspaceItem,
|
||||||
|
SubmissionCcLicence,
|
||||||
|
SubmissionCcLicenceUrl,
|
||||||
SubmissionDefinitionsModel,
|
SubmissionDefinitionsModel,
|
||||||
SubmissionFormsModel,
|
SubmissionFormsModel,
|
||||||
SubmissionSectionModel,
|
SubmissionSectionModel,
|
||||||
@@ -316,9 +334,12 @@ export const models =
|
|||||||
ItemType,
|
ItemType,
|
||||||
ExternalSource,
|
ExternalSource,
|
||||||
ExternalSourceEntry,
|
ExternalSourceEntry,
|
||||||
|
Script,
|
||||||
|
Process,
|
||||||
Version,
|
Version,
|
||||||
VersionHistory,
|
VersionHistory,
|
||||||
WorkflowAction,
|
WorkflowAction,
|
||||||
|
TemplateItem,
|
||||||
Registration
|
Registration
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@@ -50,9 +50,10 @@ import {
|
|||||||
import { RequestEntry } from './request.reducer';
|
import { RequestEntry } from './request.reducer';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { RestRequestMethod } from './rest-request-method';
|
import { RestRequestMethod } from './rest-request-method';
|
||||||
|
import { UpdateDataService } from './update-data.service';
|
||||||
import { GenericConstructor } from '../shared/generic-constructor';
|
import { GenericConstructor } from '../shared/generic-constructor';
|
||||||
|
|
||||||
export abstract class DataService<T extends CacheableObject> {
|
export abstract class DataService<T extends CacheableObject> implements UpdateDataService<T> {
|
||||||
protected abstract requestService: RequestService;
|
protected abstract requestService: RequestService;
|
||||||
protected abstract rdbService: RemoteDataBuildService;
|
protected abstract rdbService: RemoteDataBuildService;
|
||||||
protected abstract store: Store<CoreState>;
|
protected abstract store: Store<CoreState>;
|
||||||
@@ -75,6 +76,13 @@ export abstract class DataService<T extends CacheableObject> {
|
|||||||
* @returns {Observable<string>}
|
* @returns {Observable<string>}
|
||||||
*/
|
*/
|
||||||
getBrowseEndpoint(options: FindListOptions = {}, linkPath?: string): Observable<string> {
|
getBrowseEndpoint(options: FindListOptions = {}, linkPath?: string): Observable<string> {
|
||||||
|
return this.getEndpoint();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the base endpoint for all requests
|
||||||
|
*/
|
||||||
|
protected getEndpoint(): Observable<string> {
|
||||||
return this.halService.getEndpoint(this.linkPath);
|
return this.halService.getEndpoint(this.linkPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,6 +267,16 @@ export abstract class DataService<T extends CacheableObject> {
|
|||||||
return this.buildHrefFromFindOptions(endpoint + '/' + resourceID, {}, [], ...linksToFollow);
|
return this.buildHrefFromFindOptions(endpoint + '/' + resourceID, {}, [], ...linksToFollow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an observable for the HREF of a specific object based on its identifier
|
||||||
|
* @param resourceID The identifier for the object
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
|
*/
|
||||||
|
getIDHrefObs(resourceID: string, ...linksToFollow: Array<FollowLinkConfig<T>>): Observable<string> {
|
||||||
|
return this.getEndpoint().pipe(
|
||||||
|
map((endpoint: string) => this.getIDHref(endpoint, resourceID, ...linksToFollow)));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable of {@link RemoteData} of an object, based on its ID, with a list of {@link FollowLinkConfig},
|
* Returns an observable of {@link RemoteData} of an object, based on its ID, with a list of {@link FollowLinkConfig},
|
||||||
* to automatically resolve {@link HALLink}s of the object
|
* to automatically resolve {@link HALLink}s of the object
|
||||||
@@ -266,8 +284,7 @@ export abstract class DataService<T extends CacheableObject> {
|
|||||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
*/
|
*/
|
||||||
findById(id: string, ...linksToFollow: Array<FollowLinkConfig<T>>): Observable<RemoteData<T>> {
|
findById(id: string, ...linksToFollow: Array<FollowLinkConfig<T>>): Observable<RemoteData<T>> {
|
||||||
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
|
const hrefObs = this.getIDHrefObs(encodeURIComponent(id), ...linksToFollow);
|
||||||
map((endpoint: string) => this.getIDHref(endpoint, encodeURIComponent(id), ...linksToFollow)));
|
|
||||||
|
|
||||||
hrefObs.pipe(
|
hrefObs.pipe(
|
||||||
find((href: string) => hasValue(href)))
|
find((href: string) => hasValue(href)))
|
||||||
@@ -437,7 +454,7 @@ export abstract class DataService<T extends CacheableObject> {
|
|||||||
*/
|
*/
|
||||||
create(dso: T, ...params: RequestParam[]): Observable<RemoteData<T>> {
|
create(dso: T, ...params: RequestParam[]): Observable<RemoteData<T>> {
|
||||||
const requestId = this.requestService.generateRequestId();
|
const requestId = this.requestService.generateRequestId();
|
||||||
const endpoint$ = this.halService.getEndpoint(this.linkPath).pipe(
|
const endpoint$ = this.getEndpoint().pipe(
|
||||||
isNotEmptyOperator(),
|
isNotEmptyOperator(),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
map((endpoint: string) => this.buildHrefWithParams(endpoint, params))
|
map((endpoint: string) => this.buildHrefWithParams(endpoint, params))
|
||||||
@@ -563,8 +580,7 @@ export abstract class DataService<T extends CacheableObject> {
|
|||||||
private deleteAndReturnRequestId(dsoID: string, copyVirtualMetadata?: string[]): string {
|
private deleteAndReturnRequestId(dsoID: string, copyVirtualMetadata?: string[]): string {
|
||||||
const requestId = this.requestService.generateRequestId();
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
|
||||||
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
|
const hrefObs = this.getIDHrefObs(dsoID);
|
||||||
map((endpoint: string) => this.getIDHref(endpoint, dsoID)));
|
|
||||||
|
|
||||||
hrefObs.pipe(
|
hrefObs.pipe(
|
||||||
find((href: string) => hasValue(href)),
|
find((href: string) => hasValue(href)),
|
||||||
|
@@ -55,7 +55,7 @@ export class ItemDataService extends DataService<Item> {
|
|||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected store: Store<CoreState>,
|
protected store: Store<CoreState>,
|
||||||
private bs: BrowseService,
|
protected bs: BrowseService,
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected notificationsService: NotificationsService,
|
protected notificationsService: NotificationsService,
|
||||||
|
139
src/app/core/data/item-template-data.service.spec.ts
Normal file
139
src/app/core/data/item-template-data.service.spec.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { ItemTemplateDataService } from './item-template-data.service';
|
||||||
|
import { RestRequest } from './request.models';
|
||||||
|
import { RequestEntry } from './request.reducer';
|
||||||
|
import { RestResponse } from '../cache/response.models';
|
||||||
|
import { RequestService } from './request.service';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { CoreState } from '../core.reducers';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { BrowseService } from '../browse/browse.service';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { cold } from 'jasmine-marbles';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { CollectionDataService } from './collection-data.service';
|
||||||
|
import { RestRequestMethod } from './rest-request-method';
|
||||||
|
import { Item } from '../shared/item.model';
|
||||||
|
|
||||||
|
describe('ItemTemplateDataService', () => {
|
||||||
|
let service: ItemTemplateDataService;
|
||||||
|
let itemService: any;
|
||||||
|
|
||||||
|
const item = new Item();
|
||||||
|
const collectionEndpoint = 'https://rest.api/core/collections/4af28e99-6a9c-4036-a199-e1b587046d39';
|
||||||
|
const itemEndpoint = `${collectionEndpoint}/itemtemplate`;
|
||||||
|
const scopeID = '4af28e99-6a9c-4036-a199-e1b587046d39';
|
||||||
|
const requestService = {
|
||||||
|
generateRequestId(): string {
|
||||||
|
return scopeID;
|
||||||
|
},
|
||||||
|
configure(request: RestRequest) {
|
||||||
|
// Do nothing
|
||||||
|
},
|
||||||
|
getByHref(requestHref: string) {
|
||||||
|
const responseCacheEntry = new RequestEntry();
|
||||||
|
responseCacheEntry.response = new RestResponse(true, 200, 'OK');
|
||||||
|
return observableOf(responseCacheEntry);
|
||||||
|
},
|
||||||
|
getByUUID(uuid: string) {
|
||||||
|
const responseCacheEntry = new RequestEntry();
|
||||||
|
responseCacheEntry.response = new RestResponse(true, 200, 'OK');
|
||||||
|
return observableOf(responseCacheEntry);
|
||||||
|
},
|
||||||
|
commit(method?: RestRequestMethod) {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
} as RequestService;
|
||||||
|
const rdbService = {} as RemoteDataBuildService;
|
||||||
|
const store = {} as Store<CoreState>;
|
||||||
|
const bs = {} as BrowseService;
|
||||||
|
const objectCache = {
|
||||||
|
getObjectBySelfLink(self) {
|
||||||
|
return observableOf({})
|
||||||
|
},
|
||||||
|
addPatch(self, operations) {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
} as ObjectCacheService;
|
||||||
|
const halEndpointService = {
|
||||||
|
getEndpoint(linkPath: string): Observable<string> {
|
||||||
|
return cold('a', {a: itemEndpoint});
|
||||||
|
}
|
||||||
|
} as HALEndpointService;
|
||||||
|
const notificationsService = {} as NotificationsService;
|
||||||
|
const http = {} as HttpClient;
|
||||||
|
const comparator = {
|
||||||
|
diff(first, second) {
|
||||||
|
return [{}];
|
||||||
|
}
|
||||||
|
} as any;
|
||||||
|
const collectionService = {
|
||||||
|
getIDHrefObs(id): Observable<string> {
|
||||||
|
return observableOf(collectionEndpoint);
|
||||||
|
}
|
||||||
|
} as CollectionDataService;
|
||||||
|
|
||||||
|
function initTestService() {
|
||||||
|
service = new ItemTemplateDataService(
|
||||||
|
requestService,
|
||||||
|
rdbService,
|
||||||
|
store,
|
||||||
|
bs,
|
||||||
|
objectCache,
|
||||||
|
halEndpointService,
|
||||||
|
notificationsService,
|
||||||
|
http,
|
||||||
|
comparator,
|
||||||
|
undefined,
|
||||||
|
collectionService
|
||||||
|
);
|
||||||
|
itemService = (service as any).dataService;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
initTestService();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('commitUpdates', () => {
|
||||||
|
it('should call commitUpdates on the item service implementation', () => {
|
||||||
|
spyOn(itemService, 'commitUpdates');
|
||||||
|
service.commitUpdates();
|
||||||
|
expect(itemService.commitUpdates).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('update', () => {
|
||||||
|
it('should call update on the item service implementation', () => {
|
||||||
|
spyOn(itemService, 'update');
|
||||||
|
service.update(item);
|
||||||
|
expect(itemService.update).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('findByCollectionID', () => {
|
||||||
|
it('should call findByCollectionID on the item service implementation', () => {
|
||||||
|
spyOn(itemService, 'findByCollectionID');
|
||||||
|
service.findByCollectionID(scopeID);
|
||||||
|
expect(itemService.findByCollectionID).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('create', () => {
|
||||||
|
it('should call createTemplate on the item service implementation', () => {
|
||||||
|
spyOn(itemService, 'createTemplate');
|
||||||
|
service.create(item, scopeID);
|
||||||
|
expect(itemService.createTemplate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteByCollectionID', () => {
|
||||||
|
it('should call deleteByCollectionID on the item service implementation', () => {
|
||||||
|
spyOn(itemService, 'deleteByCollectionID');
|
||||||
|
service.deleteByCollectionID(item, scopeID);
|
||||||
|
expect(itemService.deleteByCollectionID).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
193
src/app/core/data/item-template-data.service.ts
Normal file
193
src/app/core/data/item-template-data.service.ts
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ItemDataService } from './item-data.service';
|
||||||
|
import { UpdateDataService } from './update-data.service';
|
||||||
|
import { Item } from '../shared/item.model';
|
||||||
|
import { RestRequestMethod } from './rest-request-method';
|
||||||
|
import { RemoteData } from './remote-data';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||||
|
import { RequestService } from './request.service';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { CoreState } from '../core.reducers';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { BrowseService } from '../browse/browse.service';
|
||||||
|
import { CollectionDataService } from './collection-data.service';
|
||||||
|
import { switchMap } from 'rxjs/operators';
|
||||||
|
import { BundleDataService } from './bundle-data.service';
|
||||||
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
|
|
||||||
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
/**
|
||||||
|
* A custom implementation of the ItemDataService, but for collection item templates
|
||||||
|
* Makes sure to change the endpoint before sending out CRUD requests for the item template
|
||||||
|
*/
|
||||||
|
class DataServiceImpl extends ItemDataService {
|
||||||
|
protected collectionLinkPath = 'itemtemplate';
|
||||||
|
protected linkPath = 'itemtemplates';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endpoint dynamically changing depending on what request we're sending
|
||||||
|
*/
|
||||||
|
private endpoint$: Observable<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the current endpoint based on a collection?
|
||||||
|
*/
|
||||||
|
private collectionEndpoint = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected store: Store<CoreState>,
|
||||||
|
protected bs: BrowseService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected comparator: DSOChangeAnalyzer<Item>,
|
||||||
|
protected bundleService: BundleDataService,
|
||||||
|
protected collectionService: CollectionDataService) {
|
||||||
|
super(requestService, rdbService, store, bs, objectCache, halService, notificationsService, http, comparator, bundleService);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the endpoint to be based on a collection
|
||||||
|
* @param collectionID The ID of the collection to base the endpoint on
|
||||||
|
*/
|
||||||
|
private setCollectionEndpoint(collectionID: string) {
|
||||||
|
this.collectionEndpoint = true;
|
||||||
|
this.endpoint$ = this.collectionService.getIDHrefObs(collectionID).pipe(
|
||||||
|
switchMap((href: string) => this.halService.getEndpoint(this.collectionLinkPath, href))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the endpoint to the regular linkPath
|
||||||
|
*/
|
||||||
|
private setRegularEndpoint() {
|
||||||
|
this.collectionEndpoint = false;
|
||||||
|
this.endpoint$ = this.halService.getEndpoint(this.linkPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the base endpoint for all requests
|
||||||
|
* Uses the current collectionID to assemble a request endpoint for the collection's item template
|
||||||
|
*/
|
||||||
|
protected getEndpoint(): Observable<string> {
|
||||||
|
return this.endpoint$;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the current endpoint is based on a collection, simply return the collection's template endpoint, otherwise
|
||||||
|
* create a regular template endpoint
|
||||||
|
* @param resourceID
|
||||||
|
*/
|
||||||
|
getIDHrefObs(resourceID: string): Observable<string> {
|
||||||
|
if (this.collectionEndpoint) {
|
||||||
|
return this.getEndpoint();
|
||||||
|
} else {
|
||||||
|
return super.getIDHrefObs(resourceID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the collection ID and send a find by ID request
|
||||||
|
* @param collectionID
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
|
*/
|
||||||
|
findByCollectionID(collectionID: string, ...linksToFollow: Array<FollowLinkConfig<Item>>): Observable<RemoteData<Item>> {
|
||||||
|
this.setCollectionEndpoint(collectionID);
|
||||||
|
return super.findById(collectionID, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the collection ID and send a create request
|
||||||
|
* @param item
|
||||||
|
* @param collectionID
|
||||||
|
*/
|
||||||
|
createTemplate(item: Item, collectionID: string): Observable<RemoteData<Item>> {
|
||||||
|
this.setCollectionEndpoint(collectionID);
|
||||||
|
return super.create(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the collection ID and send a delete request
|
||||||
|
* @param item
|
||||||
|
* @param collectionID
|
||||||
|
*/
|
||||||
|
deleteByCollectionID(item: Item, collectionID: string): Observable<boolean> {
|
||||||
|
this.setRegularEndpoint();
|
||||||
|
return super.delete(item.uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service responsible for fetching/sending data from/to the REST API on a collection's itemtemplates endpoint
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ItemTemplateDataService implements UpdateDataService<Item> {
|
||||||
|
/**
|
||||||
|
* The data service responsible for all CRUD actions on the item
|
||||||
|
*/
|
||||||
|
private dataService: DataServiceImpl;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected store: Store<CoreState>,
|
||||||
|
protected bs: BrowseService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected comparator: DSOChangeAnalyzer<Item>,
|
||||||
|
protected bundleService: BundleDataService,
|
||||||
|
protected collectionService: CollectionDataService) {
|
||||||
|
this.dataService = new DataServiceImpl(requestService, rdbService, store, bs, objectCache, halService, notificationsService, http, comparator, bundleService, collectionService);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commit current object changes to the server
|
||||||
|
*/
|
||||||
|
commitUpdates(method?: RestRequestMethod) {
|
||||||
|
this.dataService.commitUpdates(method);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new patch to the object cache
|
||||||
|
*/
|
||||||
|
update(object: Item): Observable<RemoteData<Item>> {
|
||||||
|
return this.dataService.update(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find an item template by collection ID
|
||||||
|
* @param collectionID
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
|
*/
|
||||||
|
findByCollectionID(collectionID: string, ...linksToFollow: Array<FollowLinkConfig<Item>>): Observable<RemoteData<Item>> {
|
||||||
|
return this.dataService.findByCollectionID(collectionID, ...linksToFollow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new item template for a collection by ID
|
||||||
|
* @param item
|
||||||
|
* @param collectionID
|
||||||
|
*/
|
||||||
|
create(item: Item, collectionID: string): Observable<RemoteData<Item>> {
|
||||||
|
return this.dataService.createTemplate(item, collectionID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a template item by collection ID
|
||||||
|
* @param item
|
||||||
|
* @param collectionID
|
||||||
|
*/
|
||||||
|
deleteByCollectionID(item: Item, collectionID: string): Observable<boolean> {
|
||||||
|
return this.dataService.deleteByCollectionID(item, collectionID);
|
||||||
|
}
|
||||||
|
}
|
@@ -4,9 +4,8 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
|
|||||||
import { of as observableOf } from 'rxjs/internal/observable/of';
|
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||||
import { RestResponse } from '../cache/response.models';
|
import { RestResponse } from '../cache/response.models';
|
||||||
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
||||||
import { CreateRequest, FindListOptions, PutRequest } from './request.models';
|
import { FindListOptions } from './request.models';
|
||||||
import { MetadataFieldDataService } from './metadata-field-data.service';
|
import { MetadataFieldDataService } from './metadata-field-data.service';
|
||||||
import { MetadataField } from '../metadata/metadata-field.model';
|
|
||||||
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
@@ -64,45 +63,6 @@ describe('MetadataFieldDataService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('createOrUpdateMetadataField', () => {
|
|
||||||
let field: MetadataField;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
field = Object.assign(new MetadataField(), {
|
|
||||||
element: 'identifier',
|
|
||||||
qualifier: undefined,
|
|
||||||
schema: schema,
|
|
||||||
_links: {
|
|
||||||
self: { href: 'selflink' }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('called with a new metadata field', () => {
|
|
||||||
it('should send a CreateRequest', (done) => {
|
|
||||||
metadataFieldService.createOrUpdateMetadataField(field).subscribe(() => {
|
|
||||||
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(CreateRequest));
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('called with an existing metadata field', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
field = Object.assign(field, {
|
|
||||||
id: 'id-of-existing-field'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should send a PutRequest', (done) => {
|
|
||||||
metadataFieldService.createOrUpdateMetadataField(field).subscribe(() => {
|
|
||||||
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PutRequest));
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('clearRequests', () => {
|
describe('clearRequests', () => {
|
||||||
it('should remove requests on the data service\'s endpoint', (done) => {
|
it('should remove requests on the data service\'s endpoint', (done) => {
|
||||||
metadataFieldService.clearRequests().subscribe(() => {
|
metadataFieldService.clearRequests().subscribe(() => {
|
||||||
|
@@ -13,14 +13,11 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
|
|||||||
import { METADATA_FIELD } from '../metadata/metadata-field.resource-type';
|
import { METADATA_FIELD } from '../metadata/metadata-field.resource-type';
|
||||||
import { MetadataField } from '../metadata/metadata-field.model';
|
import { MetadataField } from '../metadata/metadata-field.model';
|
||||||
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
||||||
import { FindListOptions, FindListRequest } from './request.models';
|
import { FindListOptions } from './request.models';
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { tap } from 'rxjs/operators';
|
||||||
import { find, skipWhile, switchMap, tap } from 'rxjs/operators';
|
|
||||||
import { RemoteData } from './remote-data';
|
|
||||||
import { RequestParam } from '../cache/models/request-param.model';
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
import { PaginatedList } from './paginated-list';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service responsible for fetching/sending data from/to the REST API on the metadatafields endpoint
|
* A service responsible for fetching/sending data from/to the REST API on the metadatafields endpoint
|
||||||
@@ -56,24 +53,6 @@ export class MetadataFieldDataService extends DataService<MetadataField> {
|
|||||||
return this.searchBy(this.searchBySchemaLinkPath, optionsWithSchema, ...linksToFollow);
|
return this.searchBy(this.searchBySchemaLinkPath, optionsWithSchema, ...linksToFollow);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create or Update a MetadataField
|
|
||||||
* If the MetadataField contains an id, it is assumed the field already exists and is updated instead
|
|
||||||
* Since creating or updating is nearly identical, the only real difference is the request (and slight difference in endpoint):
|
|
||||||
* - On creation, a CreateRequest is used
|
|
||||||
* - On update, a PutRequest is used
|
|
||||||
* @param field The MetadataField to create or update
|
|
||||||
*/
|
|
||||||
createOrUpdateMetadataField(field: MetadataField): Observable<RemoteData<MetadataField>> {
|
|
||||||
const isUpdate = hasValue(field.id);
|
|
||||||
|
|
||||||
if (isUpdate) {
|
|
||||||
return this.put(field);
|
|
||||||
} else {
|
|
||||||
return this.create(field, new RequestParam('schemaId', field.schema.id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear all metadata field requests
|
* Clear all metadata field requests
|
||||||
* Used for refreshing lists after adding/updating/removing a metadata field from a metadata schema
|
* Used for refreshing lists after adding/updating/removing a metadata field from a metadata schema
|
||||||
|
41
src/app/core/data/process-files-response-parsing.service.ts
Normal file
41
src/app/core/data/process-files-response-parsing.service.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { ResponseParsingService } from './parsing.service';
|
||||||
|
import { RestRequest } from './request.models';
|
||||||
|
import { GenericSuccessResponse, RestResponse } from '../cache/response.models';
|
||||||
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
|
import { isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { PaginatedList } from './paginated-list';
|
||||||
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
|
||||||
|
import { Bitstream } from '../shared/bitstream.model';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
/**
|
||||||
|
* A ResponseParsingService used to parse DSpaceRESTV2Response coming from the REST API to a GenericSuccessResponse
|
||||||
|
* containing a PaginatedList of a process's output files
|
||||||
|
*/
|
||||||
|
export class ProcessFilesResponseParsingService implements ResponseParsingService {
|
||||||
|
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||||
|
const payload = data.payload;
|
||||||
|
|
||||||
|
let page;
|
||||||
|
if (isNotEmpty(payload._embedded) && isNotEmpty(Object.keys(payload._embedded))) {
|
||||||
|
const bitstreams = new DSpaceSerializer(Bitstream).deserializeArray(payload._embedded[Object.keys(payload._embedded)[0]]);
|
||||||
|
|
||||||
|
if (isNotEmpty(bitstreams)) {
|
||||||
|
page = new PaginatedList(Object.assign(new PageInfo(), {
|
||||||
|
elementsPerPage: bitstreams.length,
|
||||||
|
totalElements: bitstreams.length,
|
||||||
|
totalPages: 1,
|
||||||
|
currentPage: 1
|
||||||
|
}), bitstreams);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEmpty(page)) {
|
||||||
|
page = new PaginatedList(new PageInfo(), []);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GenericSuccessResponse(page, data.statusCode, data.statusText);
|
||||||
|
}
|
||||||
|
}
|
70
src/app/core/data/processes/process-data.service.ts
Normal file
70
src/app/core/data/processes/process-data.service.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { DataService } from '../data.service';
|
||||||
|
import { RequestService } from '../request.service';
|
||||||
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { CoreState } from '../../core.reducers';
|
||||||
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
|
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { DefaultChangeAnalyzer } from '../default-change-analyzer.service';
|
||||||
|
import { Process } from '../../../process-page/processes/process.model';
|
||||||
|
import { dataService } from '../../cache/builders/build-decorators';
|
||||||
|
import { PROCESS } from '../../../process-page/processes/process.resource-type';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { map, switchMap } from 'rxjs/operators';
|
||||||
|
import { ProcessFilesRequest, RestRequest } from '../request.models';
|
||||||
|
import { configureRequest, filterSuccessfulResponses } from '../../shared/operators';
|
||||||
|
import { GenericSuccessResponse } from '../../cache/response.models';
|
||||||
|
import { PaginatedList } from '../paginated-list';
|
||||||
|
import { Bitstream } from '../../shared/bitstream.model';
|
||||||
|
import { RemoteData } from '../remote-data';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
@dataService(PROCESS)
|
||||||
|
export class ProcessDataService extends DataService<Process> {
|
||||||
|
protected linkPath = 'processes';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected store: Store<CoreState>,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected comparator: DefaultChangeAnalyzer<Process>) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the endpoint for a process his files
|
||||||
|
* @param processId The ID of the process
|
||||||
|
*/
|
||||||
|
getFilesEndpoint(processId: string): Observable<string> {
|
||||||
|
return this.getBrowseEndpoint().pipe(
|
||||||
|
switchMap((href) => this.halService.getEndpoint('files', `${href}/${processId}`))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a process his output files
|
||||||
|
* @param processId The ID of the process
|
||||||
|
*/
|
||||||
|
getFiles(processId: string): Observable<RemoteData<PaginatedList<Bitstream>>> {
|
||||||
|
const request$ = this.getFilesEndpoint(processId).pipe(
|
||||||
|
map((href) => new ProcessFilesRequest(this.requestService.generateRequestId(), href)),
|
||||||
|
configureRequest(this.requestService)
|
||||||
|
);
|
||||||
|
const requestEntry$ = request$.pipe(
|
||||||
|
switchMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
||||||
|
);
|
||||||
|
const payload$ = requestEntry$.pipe(
|
||||||
|
filterSuccessfulResponses(),
|
||||||
|
map((response: GenericSuccessResponse<PaginatedList<Bitstream>>) => response.payload)
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.rdbService.toRemoteDataObservable(requestEntry$, payload$);
|
||||||
|
}
|
||||||
|
}
|
61
src/app/core/data/processes/script-data.service.ts
Normal file
61
src/app/core/data/processes/script-data.service.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { DataService } from '../data.service';
|
||||||
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { CoreState } from '../../core.reducers';
|
||||||
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
|
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { DefaultChangeAnalyzer } from '../default-change-analyzer.service';
|
||||||
|
import { Script } from '../../../process-page/scripts/script.model';
|
||||||
|
import { ProcessParameter } from '../../../process-page/processes/process-parameter.model';
|
||||||
|
import { find, map, switchMap } from 'rxjs/operators';
|
||||||
|
import { URLCombiner } from '../../url-combiner/url-combiner';
|
||||||
|
import { MultipartPostRequest, RestRequest } from '../request.models';
|
||||||
|
import { RequestService } from '../request.service';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { RequestEntry } from '../request.reducer';
|
||||||
|
import { dataService } from '../../cache/builders/build-decorators';
|
||||||
|
import { SCRIPT } from '../../../process-page/scripts/script.resource-type';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
@dataService(SCRIPT)
|
||||||
|
export class ScriptDataService extends DataService<Script> {
|
||||||
|
protected linkPath = 'scripts';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected store: Store<CoreState>,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected comparator: DefaultChangeAnalyzer<Script>) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public invoke(scriptName: string, parameters: ProcessParameter[], files: File[]): Observable<RequestEntry> {
|
||||||
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
return this.getBrowseEndpoint().pipe(
|
||||||
|
map((endpoint: string) => new URLCombiner(endpoint, scriptName, 'processes').toString()),
|
||||||
|
map((endpoint: string) => {
|
||||||
|
const body = this.getInvocationFormData(parameters, files);
|
||||||
|
return new MultipartPostRequest(requestId, endpoint, body)
|
||||||
|
}),
|
||||||
|
map((request: RestRequest) => this.requestService.configure(request)),
|
||||||
|
switchMap(() => this.requestService.getByUUID(requestId)),
|
||||||
|
find((request: RequestEntry) => request.completed)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getInvocationFormData(parameters: ProcessParameter[], files: File[]): FormData {
|
||||||
|
const form: FormData = new FormData();
|
||||||
|
form.set('properties', JSON.stringify(parameters));
|
||||||
|
files.forEach((file: File) => {
|
||||||
|
form.append('file', file);
|
||||||
|
});
|
||||||
|
return form;
|
||||||
|
}
|
||||||
|
}
|
@@ -11,12 +11,7 @@ import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.
|
|||||||
|
|
||||||
import { DSpaceRESTv2Service } from '../dspace-rest-v2/dspace-rest-v2.service';
|
import { DSpaceRESTv2Service } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||||
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
|
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
|
||||||
import {
|
import { RequestActionTypes, RequestCompleteAction, RequestExecuteAction, ResetResponseTimestampsAction } from './request.actions';
|
||||||
RequestActionTypes,
|
|
||||||
RequestCompleteAction,
|
|
||||||
RequestExecuteAction,
|
|
||||||
ResetResponseTimestampsAction
|
|
||||||
} from './request.actions';
|
|
||||||
import { RequestError, RestRequest } from './request.models';
|
import { RequestError, RestRequest } from './request.models';
|
||||||
import { RequestEntry } from './request.reducer';
|
import { RequestEntry } from './request.reducer';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
@@ -42,12 +37,12 @@ export class RequestEffects {
|
|||||||
filter((entry: RequestEntry) => hasValue(entry)),
|
filter((entry: RequestEntry) => hasValue(entry)),
|
||||||
map((entry: RequestEntry) => entry.request),
|
map((entry: RequestEntry) => entry.request),
|
||||||
flatMap((request: RestRequest) => {
|
flatMap((request: RestRequest) => {
|
||||||
let body;
|
let body = request.body;
|
||||||
if (isNotEmpty(request.body)) {
|
if (isNotEmpty(request.body) && !request.isMultipart) {
|
||||||
const serializer = new DSpaceSerializer(getClassForType(request.body.type));
|
const serializer = new DSpaceSerializer(getClassForType(request.body.type));
|
||||||
body = serializer.serialize(request.body);
|
body = serializer.serialize(request.body);
|
||||||
}
|
}
|
||||||
return this.restApi.request(request.method, request.href, body, request.options).pipe(
|
return this.restApi.request(request.method, request.href, body, request.options, request.isMultipart).pipe(
|
||||||
map((data: DSpaceRESTV2Response) => this.injector.get(request.getResponseParser()).parse(request, data)),
|
map((data: DSpaceRESTV2Response) => this.injector.get(request.getResponseParser()).parse(request, data)),
|
||||||
addToResponseCacheAndCompleteAction(request),
|
addToResponseCacheAndCompleteAction(request),
|
||||||
catchError((error: RequestError) => observableOf(new ErrorResponse(error)).pipe(
|
catchError((error: RequestError) => observableOf(new ErrorResponse(error)).pipe(
|
||||||
|
@@ -18,6 +18,7 @@ import { URLCombiner } from '../url-combiner/url-combiner';
|
|||||||
import { TaskResponseParsingService } from '../tasks/task-response-parsing.service';
|
import { TaskResponseParsingService } from '../tasks/task-response-parsing.service';
|
||||||
import { ContentSourceResponseParsingService } from './content-source-response-parsing.service';
|
import { ContentSourceResponseParsingService } from './content-source-response-parsing.service';
|
||||||
import { MappedCollectionsReponseParsingService } from './mapped-collections-reponse-parsing.service';
|
import { MappedCollectionsReponseParsingService } from './mapped-collections-reponse-parsing.service';
|
||||||
|
import { ProcessFilesResponseParsingService } from './process-files-response-parsing.service';
|
||||||
import { TokenResponseParsingService } from '../auth/token-response-parsing.service';
|
import { TokenResponseParsingService } from '../auth/token-response-parsing.service';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
@@ -31,6 +32,8 @@ export enum IdentifierType {
|
|||||||
export abstract class RestRequest {
|
export abstract class RestRequest {
|
||||||
public responseMsToLive = 10 * 1000;
|
public responseMsToLive = 10 * 1000;
|
||||||
public forceBypassCache = false;
|
public forceBypassCache = false;
|
||||||
|
public isMultipart = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public uuid: string,
|
public uuid: string,
|
||||||
public href: string,
|
public href: string,
|
||||||
@@ -73,6 +76,21 @@ export class PostRequest extends RestRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request representing a multipart post request
|
||||||
|
*/
|
||||||
|
export class MultipartPostRequest extends RestRequest {
|
||||||
|
public isMultipart = true;
|
||||||
|
constructor(
|
||||||
|
public uuid: string,
|
||||||
|
public href: string,
|
||||||
|
public body?: any,
|
||||||
|
public options?: HttpOptions
|
||||||
|
) {
|
||||||
|
super(uuid, href, RestRequestMethod.POST, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class PutRequest extends RestRequest {
|
export class PutRequest extends RestRequest {
|
||||||
constructor(
|
constructor(
|
||||||
public uuid: string,
|
public uuid: string,
|
||||||
@@ -208,6 +226,15 @@ export class MappedCollectionsRequest extends GetRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request to fetch the files of a process
|
||||||
|
*/
|
||||||
|
export class ProcessFilesRequest extends GetRequest {
|
||||||
|
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||||
|
return ProcessFilesResponseParsingService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class ConfigRequest extends GetRequest {
|
export class ConfigRequest extends GetRequest {
|
||||||
constructor(uuid: string, href: string, public options?: HttpOptions) {
|
constructor(uuid: string, href: string, public options?: HttpOptions) {
|
||||||
super(uuid, href, null, options);
|
super(uuid, href, null, options);
|
||||||
|
11
src/app/core/data/update-data.service.ts
Normal file
11
src/app/core/data/update-data.service.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { RemoteData } from './remote-data';
|
||||||
|
import { RestRequestMethod } from './rest-request-method';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a data service to update a given object
|
||||||
|
*/
|
||||||
|
export interface UpdateDataService<T> {
|
||||||
|
update(object: T): Observable<RemoteData<T>>;
|
||||||
|
commitUpdates(method?: RestRequestMethod);
|
||||||
|
}
|
@@ -69,10 +69,12 @@ export class DSpaceRESTv2Service {
|
|||||||
* an optional body for the request
|
* an optional body for the request
|
||||||
* @param options
|
* @param options
|
||||||
* the HttpOptions object
|
* the HttpOptions object
|
||||||
|
* @param isMultipart
|
||||||
|
* true when this concerns a multipart request
|
||||||
* @return {Observable<string>}
|
* @return {Observable<string>}
|
||||||
* An Observable<string> containing the response from the server
|
* An Observable<string> containing the response from the server
|
||||||
*/
|
*/
|
||||||
request(method: RestRequestMethod, url: string, body?: any, options?: HttpOptions): Observable<DSpaceRESTV2Response> {
|
request(method: RestRequestMethod, url: string, body?: any, options?: HttpOptions, isMultipart?: boolean): Observable<DSpaceRESTV2Response> {
|
||||||
const requestOptions: HttpOptions = {};
|
const requestOptions: HttpOptions = {};
|
||||||
requestOptions.body = body;
|
requestOptions.body = body;
|
||||||
if (method === RestRequestMethod.POST && isNotEmpty(body) && isNotEmpty(body.name)) {
|
if (method === RestRequestMethod.POST && isNotEmpty(body) && isNotEmpty(body.name)) {
|
||||||
@@ -98,7 +100,7 @@ export class DSpaceRESTv2Service {
|
|||||||
requestOptions.withCredentials = options.withCredentials;
|
requestOptions.withCredentials = options.withCredentials;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!requestOptions.headers.has('Content-Type')) {
|
if (!requestOptions.headers.has('Content-Type') && !isMultipart) {
|
||||||
// Because HttpHeaders is immutable, the set method returns a new object instead of updating the existing headers
|
// Because HttpHeaders is immutable, the set method returns a new object instead of updating the existing headers
|
||||||
requestOptions.headers = requestOptions.headers.set('Content-Type', DEFAULT_CONTENT_TYPE);
|
requestOptions.headers = requestOptions.headers.set('Content-Type', DEFAULT_CONTENT_TYPE);
|
||||||
}
|
}
|
||||||
|
@@ -9,6 +9,9 @@ import { ResourceType } from '../shared/resource-type';
|
|||||||
import { excludeFromEquals } from '../utilities/equals.decorators';
|
import { excludeFromEquals } from '../utilities/equals.decorators';
|
||||||
import { METADATA_FIELD } from './metadata-field.resource-type';
|
import { METADATA_FIELD } from './metadata-field.resource-type';
|
||||||
import { MetadataSchema } from './metadata-schema.model';
|
import { MetadataSchema } from './metadata-schema.model';
|
||||||
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { METADATA_SCHEMA } from './metadata-schema.resource-type';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class the represents a metadata field
|
* Class the represents a metadata field
|
||||||
@@ -61,16 +64,15 @@ export class MetadataField extends ListableObject implements HALResource {
|
|||||||
* The MetadataSchema for this MetadataField
|
* The MetadataSchema for this MetadataField
|
||||||
* Will be undefined unless the schema {@link HALLink} has been resolved.
|
* Will be undefined unless the schema {@link HALLink} has been resolved.
|
||||||
*/
|
*/
|
||||||
// TODO the responseparsingservice assumes schemas are always embedded. This should use remotedata, and be a link instead.
|
@link(METADATA_SCHEMA)
|
||||||
// @link(METADATA_SCHEMA)
|
schema?: Observable<RemoteData<MetadataSchema>>;
|
||||||
schema?: MetadataSchema;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to print this metadata field as a string
|
* Method to print this metadata field as a string
|
||||||
* @param separator The separator between the schema, element and qualifier in the string
|
* @param separator The separator between the schema, element and qualifier in the string
|
||||||
*/
|
*/
|
||||||
toString(separator: string = '.'): string {
|
toString(separator: string = '.'): string {
|
||||||
let key = this.schema.prefix + separator + this.element;
|
let key = this.element;
|
||||||
if (isNotEmpty(this.qualifier)) {
|
if (isNotEmpty(this.qualifier)) {
|
||||||
key += separator + this.qualifier;
|
key += separator + this.qualifier;
|
||||||
}
|
}
|
||||||
|
@@ -82,7 +82,7 @@ describe('RegistryService', () => {
|
|||||||
element: 'contributor',
|
element: 'contributor',
|
||||||
qualifier: 'advisor',
|
qualifier: 'advisor',
|
||||||
scopeNote: null,
|
scopeNote: null,
|
||||||
schema: mockSchemasList[0],
|
schema: createSuccessfulRemoteDataObject$(mockSchemasList[0]),
|
||||||
type: MetadataField.type
|
type: MetadataField.type
|
||||||
}),
|
}),
|
||||||
Object.assign(new MetadataField(),
|
Object.assign(new MetadataField(),
|
||||||
@@ -94,7 +94,7 @@ describe('RegistryService', () => {
|
|||||||
element: 'contributor',
|
element: 'contributor',
|
||||||
qualifier: 'author',
|
qualifier: 'author',
|
||||||
scopeNote: null,
|
scopeNote: null,
|
||||||
schema: mockSchemasList[0],
|
schema: createSuccessfulRemoteDataObject$(mockSchemasList[0]),
|
||||||
type: MetadataField.type
|
type: MetadataField.type
|
||||||
}),
|
}),
|
||||||
Object.assign(new MetadataField(),
|
Object.assign(new MetadataField(),
|
||||||
@@ -106,7 +106,7 @@ describe('RegistryService', () => {
|
|||||||
element: 'contributor',
|
element: 'contributor',
|
||||||
qualifier: 'editor',
|
qualifier: 'editor',
|
||||||
scopeNote: 'test scope note',
|
scopeNote: 'test scope note',
|
||||||
schema: mockSchemasList[1],
|
schema: createSuccessfulRemoteDataObject$(mockSchemasList[1]),
|
||||||
type: MetadataField.type
|
type: MetadataField.type
|
||||||
}),
|
}),
|
||||||
Object.assign(new MetadataField(),
|
Object.assign(new MetadataField(),
|
||||||
@@ -118,7 +118,7 @@ describe('RegistryService', () => {
|
|||||||
element: 'contributor',
|
element: 'contributor',
|
||||||
qualifier: 'illustrator',
|
qualifier: 'illustrator',
|
||||||
scopeNote: null,
|
scopeNote: null,
|
||||||
schema: mockSchemasList[1],
|
schema: createSuccessfulRemoteDataObject$(mockSchemasList[1]),
|
||||||
type: MetadataField.type
|
type: MetadataField.type
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
@@ -134,7 +134,8 @@ describe('RegistryService', () => {
|
|||||||
metadataFieldService = jasmine.createSpyObj('metadataFieldService', {
|
metadataFieldService = jasmine.createSpyObj('metadataFieldService', {
|
||||||
findAll: createSuccessfulRemoteDataObject$(createPaginatedList(mockFieldsList)),
|
findAll: createSuccessfulRemoteDataObject$(createPaginatedList(mockFieldsList)),
|
||||||
findById: createSuccessfulRemoteDataObject$(mockFieldsList[0]),
|
findById: createSuccessfulRemoteDataObject$(mockFieldsList[0]),
|
||||||
createOrUpdateMetadataField: createSuccessfulRemoteDataObject$(mockFieldsList[0]),
|
create: createSuccessfulRemoteDataObject$(mockFieldsList[0]),
|
||||||
|
put: createSuccessfulRemoteDataObject$(mockFieldsList[0]),
|
||||||
deleteAndReturnResponse: observableOf(new RestResponse(true, 200, 'OK')),
|
deleteAndReturnResponse: observableOf(new RestResponse(true, 200, 'OK')),
|
||||||
clearRequests: observableOf('href')
|
clearRequests: observableOf('href')
|
||||||
});
|
});
|
||||||
@@ -178,7 +179,7 @@ describe('RegistryService', () => {
|
|||||||
let result;
|
let result;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
result = registryService.getMetadataSchemaByName(mockSchemasList[0].prefix);
|
result = registryService.getMetadataSchemaByPrefix(mockSchemasList[0].prefix);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call metadataSchemaService.findById with the correct ID', (done) => {
|
it('should call metadataSchemaService.findById with the correct ID', (done) => {
|
||||||
@@ -189,21 +190,6 @@ describe('RegistryService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when requesting metadatafields', () => {
|
|
||||||
let result;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
result = registryService.getAllMetadataFields();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call metadataFieldService.findAll', (done) => {
|
|
||||||
result.subscribe(() => {
|
|
||||||
expect(metadataFieldService.findAll).toHaveBeenCalled();
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when dispatching to the store', () => {
|
describe('when dispatching to the store', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(mockStore, 'dispatch');
|
spyOn(mockStore, 'dispatch');
|
||||||
@@ -325,14 +311,29 @@ describe('RegistryService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when createOrUpdateMetadataField is called', () => {
|
describe('when createMetadataField is called', () => {
|
||||||
let result: Observable<MetadataField>;
|
let result: Observable<MetadataField>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
result = registryService.createOrUpdateMetadataField(mockFieldsList[0]);
|
result = registryService.createMetadataField(mockFieldsList[0], mockSchemasList[0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the created/updated metadata field', (done) => {
|
it('should return the created metadata field', (done) => {
|
||||||
|
result.subscribe((field: MetadataField) => {
|
||||||
|
expect(field).toEqual(mockFieldsList[0]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when updateMetadataField is called', () => {
|
||||||
|
let result: Observable<MetadataField>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
result = registryService.updateMetadataField(mockFieldsList[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the updated metadata field', (done) => {
|
||||||
result.subscribe((field: MetadataField) => {
|
result.subscribe((field: MetadataField) => {
|
||||||
expect(field).toEqual(mockFieldsList[0]);
|
expect(field).toEqual(mockFieldsList[0]);
|
||||||
done();
|
done();
|
||||||
|
@@ -2,18 +2,10 @@ import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
import { PaginatedList } from '../data/paginated-list';
|
import { PaginatedList } from '../data/paginated-list';
|
||||||
import { PageInfo } from '../shared/page-info.model';
|
|
||||||
import { FindListOptions } from '../data/request.models';
|
import { FindListOptions } from '../data/request.models';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RestResponse } from '../cache/response.models';
|
||||||
import { RequestService } from '../data/request.service';
|
import { hasValue, hasValueOperator, isNotEmptyOperator } from '../../shared/empty.util';
|
||||||
import {
|
import { getFirstSucceededRemoteDataPayload } from '../shared/operators';
|
||||||
MetadatafieldSuccessResponse,
|
|
||||||
MetadataschemaSuccessResponse,
|
|
||||||
RestResponse
|
|
||||||
} from '../cache/response.models';
|
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
|
||||||
import { hasNoValue, hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
|
|
||||||
import { getAllSucceededRemoteDataPayload, getFirstSucceededRemoteDataPayload } from '../shared/operators';
|
|
||||||
import { createSelector, select, Store } from '@ngrx/store';
|
import { createSelector, select, Store } from '@ngrx/store';
|
||||||
import { AppState } from '../../app.reducer';
|
import { AppState } from '../../app.reducer';
|
||||||
import { MetadataRegistryState } from '../../+admin/admin-registries/metadata-registry/metadata-registry.reducers';
|
import { MetadataRegistryState } from '../../+admin/admin-registries/metadata-registry/metadata-registry.reducers';
|
||||||
@@ -37,6 +29,8 @@ import { MetadataField } from '../metadata/metadata-field.model';
|
|||||||
import { MetadataSchemaDataService } from '../data/metadata-schema-data.service';
|
import { MetadataSchemaDataService } from '../data/metadata-schema-data.service';
|
||||||
import { MetadataFieldDataService } from '../data/metadata-field-data.service';
|
import { MetadataFieldDataService } from '../data/metadata-field-data.service';
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
|
||||||
const metadataRegistryStateSelector = (state: AppState) => state.metadataRegistry;
|
const metadataRegistryStateSelector = (state: AppState) => state.metadataRegistry;
|
||||||
const editMetadataSchemaSelector = createSelector(metadataRegistryStateSelector, (metadataState: MetadataRegistryState) => metadataState.editSchema);
|
const editMetadataSchemaSelector = createSelector(metadataRegistryStateSelector, (metadataState: MetadataRegistryState) => metadataState.editSchema);
|
||||||
@@ -68,11 +62,11 @@ export class RegistryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves a metadata schema by its name
|
* Retrieves a metadata schema by its prefix
|
||||||
* @param schemaName The name of the schema to find
|
* @param prefix The prefux of the schema to find
|
||||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
*/
|
*/
|
||||||
public getMetadataSchemaByName(schemaName: string, ...linksToFollow: Array<FollowLinkConfig<MetadataSchema>>): Observable<RemoteData<MetadataSchema>> {
|
public getMetadataSchemaByPrefix(prefix: string, ...linksToFollow: Array<FollowLinkConfig<MetadataSchema>>): Observable<RemoteData<MetadataSchema>> {
|
||||||
// Temporary options to get ALL metadataschemas until there's a rest api endpoint for fetching a specific schema
|
// Temporary options to get ALL metadataschemas until there's a rest api endpoint for fetching a specific schema
|
||||||
const options: FindListOptions = Object.assign(new FindListOptions(), {
|
const options: FindListOptions = Object.assign(new FindListOptions(), {
|
||||||
elementsPerPage: 10000
|
elementsPerPage: 10000
|
||||||
@@ -81,7 +75,7 @@ export class RegistryService {
|
|||||||
getFirstSucceededRemoteDataPayload(),
|
getFirstSucceededRemoteDataPayload(),
|
||||||
map((schemas: PaginatedList<MetadataSchema>) => schemas.page),
|
map((schemas: PaginatedList<MetadataSchema>) => schemas.page),
|
||||||
isNotEmptyOperator(),
|
isNotEmptyOperator(),
|
||||||
map((schemas: MetadataSchema[]) => schemas.filter((schema) => schema.prefix === schemaName)[0]),
|
map((schemas: MetadataSchema[]) => schemas.filter((schema) => schema.prefix === prefix)[0]),
|
||||||
flatMap((schema: MetadataSchema) => this.metadataSchemaService.findById(`${schema.id}`, ...linksToFollow))
|
flatMap((schema: MetadataSchema) => this.metadataSchemaService.findById(`${schema.id}`, ...linksToFollow))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -103,11 +97,11 @@ export class RegistryService {
|
|||||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
* @returns an observable that emits a remote data object with a page of metadata fields
|
* @returns an observable that emits a remote data object with a page of metadata fields
|
||||||
*/
|
*/
|
||||||
|
// TODO this is temporarily disabled. The performance is too bad.
|
||||||
|
// It is used down the line for validation. That validation will have to be rewritten against a new rest endpoint.
|
||||||
|
// Not by downloading the list of all fields.
|
||||||
public getAllMetadataFields(options?: FindListOptions, ...linksToFollow: Array<FollowLinkConfig<MetadataField>>): Observable<RemoteData<PaginatedList<MetadataField>>> {
|
public getAllMetadataFields(options?: FindListOptions, ...linksToFollow: Array<FollowLinkConfig<MetadataField>>): Observable<RemoteData<PaginatedList<MetadataField>>> {
|
||||||
if (hasNoValue(options)) {
|
return createSuccessfulRemoteDataObject$(new PaginatedList<MetadataField>(null, []));
|
||||||
options = {currentPage: 1, elementsPerPage: 10000} as any;
|
|
||||||
}
|
|
||||||
return this.metadataFieldService.findAll(options, ...linksToFollow);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public editMetadataSchema(schema: MetadataSchema) {
|
public editMetadataSchema(schema: MetadataSchema) {
|
||||||
@@ -240,21 +234,32 @@ export class RegistryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create or Update a MetadataField
|
* Create a MetadataField
|
||||||
* If the MetadataField contains an id, it is assumed the field already exists and is updated instead
|
*
|
||||||
* Since creating or updating is nearly identical, the only real difference is the request (and slight difference in endpoint):
|
* @param field The MetadataField to create
|
||||||
* - On creation, a CreateRequest is used
|
* @param schema The MetadataSchema to create the field in
|
||||||
* - On update, a PutRequest is used
|
|
||||||
* @param field The MetadataField to create or update
|
|
||||||
*/
|
*/
|
||||||
public createOrUpdateMetadataField(field: MetadataField): Observable<MetadataField> {
|
public createMetadataField(field: MetadataField, schema: MetadataSchema): Observable<MetadataField> {
|
||||||
const isUpdate = hasValue(field.id);
|
return this.metadataFieldService.create(field, new RequestParam('schemaId', schema.id)).pipe(
|
||||||
return this.metadataFieldService.createOrUpdateMetadataField(field).pipe(
|
|
||||||
getFirstSucceededRemoteDataPayload(),
|
getFirstSucceededRemoteDataPayload(),
|
||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
tap(() => {
|
tap(() => {
|
||||||
const fieldString = `${field.schema.prefix}.${field.element}${field.qualifier ? `.${field.qualifier}` : ''}`;
|
this.showNotifications(true, false, true, {field: field.toString()});
|
||||||
this.showNotifications(true, isUpdate, true, {field: fieldString});
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a MetadataField
|
||||||
|
*
|
||||||
|
* @param field The MetadataField to update
|
||||||
|
*/
|
||||||
|
public updateMetadataField(field: MetadataField): Observable<MetadataField> {
|
||||||
|
return this.metadataFieldService.put(field).pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
hasValueOperator(),
|
||||||
|
tap(() => {
|
||||||
|
this.showNotifications(true, true, true, {field: field.toString()});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -295,15 +300,10 @@ export class RegistryService {
|
|||||||
* @param query {string} The query to filter the field names by
|
* @param query {string} The query to filter the field names by
|
||||||
* @returns an observable that emits a remote data object with a page of metadata fields that match the query
|
* @returns an observable that emits a remote data object with a page of metadata fields that match the query
|
||||||
*/
|
*/
|
||||||
|
// TODO this is temporarily disabled. The performance is too bad.
|
||||||
|
// Querying metadatafields will need to be implemented as a search endpoint on the rest api,
|
||||||
|
// not by downloading everything and preforming the query client side.
|
||||||
queryMetadataFields(query: string): Observable<RemoteData<PaginatedList<MetadataField>>> {
|
queryMetadataFields(query: string): Observable<RemoteData<PaginatedList<MetadataField>>> {
|
||||||
return this.getAllMetadataFields().pipe(
|
return createSuccessfulRemoteDataObject$(new PaginatedList<MetadataField>(null, []));
|
||||||
map((rd: RemoteData<PaginatedList<MetadataField>>) => {
|
|
||||||
const filteredFields: MetadataField[] = rd.payload.page.filter(
|
|
||||||
(field: MetadataField) => field.toString().indexOf(query) >= 0
|
|
||||||
);
|
|
||||||
const page: PaginatedList<MetadataField> = new PaginatedList<MetadataField>(new PageInfo(), filteredFields)
|
|
||||||
return Object.assign({}, rd, { payload: page });
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { HALLink } from './hal-link.model';
|
import { HALLink } from './hal-link.model';
|
||||||
|
import { deserialize } from 'cerialize';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents HAL resources.
|
* Represents HAL resources.
|
||||||
@@ -6,10 +7,13 @@ import { HALLink } from './hal-link.model';
|
|||||||
* A HAL resource has a _links section with at least a self link.
|
* A HAL resource has a _links section with at least a self link.
|
||||||
*/
|
*/
|
||||||
export class HALResource {
|
export class HALResource {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link HALLink}s for this {@link HALResource}
|
* The {@link HALLink}s for this {@link HALResource}
|
||||||
*/
|
*/
|
||||||
|
@deserialize
|
||||||
_links: {
|
_links: {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link HALLink} that refers to this {@link HALResource}
|
* The {@link HALLink} that refers to this {@link HALResource}
|
||||||
*/
|
*/
|
||||||
|
24
src/app/core/shared/template-item.model.ts
Normal file
24
src/app/core/shared/template-item.model.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { inheritSerialization } from 'cerialize';
|
||||||
|
import { Item } from './item.model';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
import { Collection } from './collection.model';
|
||||||
|
import { ITEM_TEMPLATE } from './template-item.resource-type';
|
||||||
|
import { link, typedObject } from '../cache/builders/build-decorators';
|
||||||
|
import { COLLECTION } from './collection.resource-type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing a DSpace Template Item
|
||||||
|
*/
|
||||||
|
@typedObject
|
||||||
|
@inheritSerialization(Item)
|
||||||
|
export class TemplateItem extends Item {
|
||||||
|
static type = ITEM_TEMPLATE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Collection that this item is a template for
|
||||||
|
*/
|
||||||
|
@link(COLLECTION)
|
||||||
|
templateItemOf: Observable<RemoteData<Collection>>;
|
||||||
|
|
||||||
|
}
|
9
src/app/core/shared/template-item.resource-type.ts
Normal file
9
src/app/core/shared/template-item.resource-type.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { ResourceType } from './resource-type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The resource type for TemplateItem.
|
||||||
|
*
|
||||||
|
* Needs to be in a separate file to prevent circular
|
||||||
|
* dependencies in webpack.
|
||||||
|
*/
|
||||||
|
export const ITEM_TEMPLATE = new ResourceType('itemtemplate');
|
@@ -0,0 +1,9 @@
|
|||||||
|
import { ResourceType } from '../../shared/resource-type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The resource type for License
|
||||||
|
*
|
||||||
|
* Needs to be in a separate file to prevent circular
|
||||||
|
* dependencies in webpack.
|
||||||
|
*/
|
||||||
|
export const SUBMISSION_CC_LICENSE_URL = new ResourceType('submissioncclicenseUrl');
|
@@ -0,0 +1,9 @@
|
|||||||
|
import { ResourceType } from '../../shared/resource-type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The resource type for License
|
||||||
|
*
|
||||||
|
* Needs to be in a separate file to prevent circular
|
||||||
|
* dependencies in webpack.
|
||||||
|
*/
|
||||||
|
export const SUBMISSION_CC_LICENSE = new ResourceType('submissioncclicense');
|
@@ -0,0 +1,23 @@
|
|||||||
|
import { autoserialize, inheritSerialization } from 'cerialize';
|
||||||
|
import { typedObject } from '../../cache/builders/build-decorators';
|
||||||
|
import { excludeFromEquals } from '../../utilities/equals.decorators';
|
||||||
|
import { ResourceType } from '../../shared/resource-type';
|
||||||
|
import { HALResource } from '../../shared/hal-resource.model';
|
||||||
|
import { SUBMISSION_CC_LICENSE_URL } from './submission-cc-licence-link.resource-type';
|
||||||
|
|
||||||
|
@typedObject
|
||||||
|
@inheritSerialization(HALResource)
|
||||||
|
export class SubmissionCcLicenceUrl extends HALResource {
|
||||||
|
|
||||||
|
static type = SUBMISSION_CC_LICENSE_URL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The object type
|
||||||
|
*/
|
||||||
|
@excludeFromEquals
|
||||||
|
@autoserialize
|
||||||
|
type: ResourceType;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
url: string;
|
||||||
|
}
|
@@ -0,0 +1,42 @@
|
|||||||
|
import { autoserialize, inheritSerialization } from 'cerialize';
|
||||||
|
import { typedObject } from '../../cache/builders/build-decorators';
|
||||||
|
import { excludeFromEquals } from '../../utilities/equals.decorators';
|
||||||
|
import { ResourceType } from '../../shared/resource-type';
|
||||||
|
import { HALResource } from '../../shared/hal-resource.model';
|
||||||
|
import { SUBMISSION_CC_LICENSE } from './submission-cc-licence.resource-type';
|
||||||
|
|
||||||
|
@typedObject
|
||||||
|
@inheritSerialization(HALResource)
|
||||||
|
export class SubmissionCcLicence extends HALResource {
|
||||||
|
|
||||||
|
static type = SUBMISSION_CC_LICENSE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The object type
|
||||||
|
*/
|
||||||
|
@excludeFromEquals
|
||||||
|
@autoserialize
|
||||||
|
type: ResourceType;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
fields: Field[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Field {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
enums: Option[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Option {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
}
|
@@ -0,0 +1,15 @@
|
|||||||
|
import { Option } from './submission-cc-license.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface to represent the submission's creative commons license section data.
|
||||||
|
*/
|
||||||
|
export interface WorkspaceitemSectionCcLicenseObject {
|
||||||
|
ccLicense?: {
|
||||||
|
id: string;
|
||||||
|
fields: {
|
||||||
|
[fieldId: string]: Option;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
uri?: string;
|
||||||
|
accepted?: boolean;
|
||||||
|
}
|
@@ -1,6 +1,7 @@
|
|||||||
import { WorkspaceitemSectionFormObject } from './workspaceitem-section-form.model';
|
import { WorkspaceitemSectionFormObject } from './workspaceitem-section-form.model';
|
||||||
import { WorkspaceitemSectionLicenseObject } from './workspaceitem-section-license.model';
|
import { WorkspaceitemSectionLicenseObject } from './workspaceitem-section-license.model';
|
||||||
import { WorkspaceitemSectionUploadObject } from './workspaceitem-section-upload.model';
|
import { WorkspaceitemSectionUploadObject } from './workspaceitem-section-upload.model';
|
||||||
|
import { WorkspaceitemSectionCcLicenseObject } from './workspaceitem-section-cc-license.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An interface to represent submission's section object.
|
* An interface to represent submission's section object.
|
||||||
@@ -17,4 +18,5 @@ export type WorkspaceitemSectionDataType
|
|||||||
= WorkspaceitemSectionUploadObject
|
= WorkspaceitemSectionUploadObject
|
||||||
| WorkspaceitemSectionFormObject
|
| WorkspaceitemSectionFormObject
|
||||||
| WorkspaceitemSectionLicenseObject
|
| WorkspaceitemSectionLicenseObject
|
||||||
|
| WorkspaceitemSectionCcLicenseObject
|
||||||
| string;
|
| string;
|
||||||
|
@@ -0,0 +1,34 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { dataService } from '../cache/builders/build-decorators';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { CoreState } from '../core.reducers';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { DataService } from '../data/data.service';
|
||||||
|
import { RequestService } from '../data/request.service';
|
||||||
|
import { SUBMISSION_CC_LICENSE } from './models/submission-cc-licence.resource-type';
|
||||||
|
import { SubmissionCcLicence } from './models/submission-cc-license.model';
|
||||||
|
import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
@dataService(SUBMISSION_CC_LICENSE)
|
||||||
|
export class SubmissionCcLicenseDataService extends DataService<SubmissionCcLicence> {
|
||||||
|
|
||||||
|
protected linkPath = 'submissioncclicenses';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected comparator: DefaultChangeAnalyzer<SubmissionCcLicence>,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected store: Store<CoreState>,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,76 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { dataService } from '../cache/builders/build-decorators';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { CoreState } from '../core.reducers';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { DataService } from '../data/data.service';
|
||||||
|
import { RequestService } from '../data/request.service';
|
||||||
|
import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service';
|
||||||
|
import { SubmissionCcLicenceUrl } from './models/submission-cc-license-url.model';
|
||||||
|
import { SUBMISSION_CC_LICENSE_URL } from './models/submission-cc-licence-link.resource-type';
|
||||||
|
import { Field, Option, SubmissionCcLicence } from './models/submission-cc-license.model';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { filter, map, switchMap } from 'rxjs/operators';
|
||||||
|
import { getRemoteDataPayload, getSucceededRemoteData } from '../shared/operators';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
@dataService(SUBMISSION_CC_LICENSE_URL)
|
||||||
|
export class SubmissionCcLicenseUrlDataService extends DataService<SubmissionCcLicenceUrl> {
|
||||||
|
|
||||||
|
protected linkPath = 'submissioncclicenseUrl-search';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected comparator: DefaultChangeAnalyzer<SubmissionCcLicenceUrl>,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected store: Store<CoreState>,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the link to the Creative Commons license corresponding to the given type and options.
|
||||||
|
* @param ccLicense the Creative Commons license type
|
||||||
|
* @param options the selected options of the license fields
|
||||||
|
*/
|
||||||
|
getCcLicenseLink(ccLicense: SubmissionCcLicence, options: Map<Field, Option>): Observable<string> {
|
||||||
|
|
||||||
|
return this.getSearchByHref(
|
||||||
|
'rightsByQuestions',{
|
||||||
|
searchParams: [
|
||||||
|
{
|
||||||
|
fieldName: 'license',
|
||||||
|
fieldValue: ccLicense.id
|
||||||
|
},
|
||||||
|
...ccLicense.fields.map(
|
||||||
|
(field) => {
|
||||||
|
return {
|
||||||
|
fieldName: `answer_${field.id}`,
|
||||||
|
fieldValue: options.get(field).id,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
).pipe(
|
||||||
|
switchMap((href) => this.findByHref(href)),
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
map((response) => response.url),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getSearchEndpoint(searchMethod: string): Observable<string> {
|
||||||
|
return this.halService.getEndpoint(`${this.linkPath}`).pipe(
|
||||||
|
filter((href: string) => isNotEmpty(href)),
|
||||||
|
map((href: string) => `${href}/${searchMethod}`));
|
||||||
|
}
|
||||||
|
}
|
@@ -11,6 +11,8 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
|||||||
import { Injector, NO_ERRORS_SCHEMA } from '@angular/core';
|
import { Injector, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { MenuService } from '../shared/menu/menu.service';
|
import { MenuService } from '../shared/menu/menu.service';
|
||||||
import { MenuServiceStub } from '../shared/testing/menu-service.stub';
|
import { MenuServiceStub } from '../shared/testing/menu-service.stub';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
|
||||||
let comp: NavbarComponent;
|
let comp: NavbarComponent;
|
||||||
let fixture: ComponentFixture<NavbarComponent>;
|
let fixture: ComponentFixture<NavbarComponent>;
|
||||||
@@ -24,12 +26,14 @@ describe('NavbarComponent', () => {
|
|||||||
imports: [
|
imports: [
|
||||||
TranslateModule.forRoot(),
|
TranslateModule.forRoot(),
|
||||||
NoopAnimationsModule,
|
NoopAnimationsModule,
|
||||||
ReactiveFormsModule],
|
ReactiveFormsModule,
|
||||||
|
RouterTestingModule],
|
||||||
declarations: [NavbarComponent],
|
declarations: [NavbarComponent],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: Injector, useValue: {} },
|
{ provide: Injector, useValue: {} },
|
||||||
{ provide: MenuService, useValue: menuService },
|
{ provide: MenuService, useValue: menuService },
|
||||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
|
||||||
|
{ provide: ActivatedRoute, useValue: {} }
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
})
|
})
|
||||||
|
@@ -17,7 +17,7 @@ import { environment } from '../../environments/environment';
|
|||||||
templateUrl: './navbar.component.html',
|
templateUrl: './navbar.component.html',
|
||||||
animations: [slideMobileNav]
|
animations: [slideMobileNav]
|
||||||
})
|
})
|
||||||
export class NavbarComponent extends MenuComponent implements OnInit {
|
export class NavbarComponent extends MenuComponent {
|
||||||
/**
|
/**
|
||||||
* The menu ID of the Navbar is PUBLIC
|
* The menu ID of the Navbar is PUBLIC
|
||||||
* @type {MenuID.PUBLIC}
|
* @type {MenuID.PUBLIC}
|
||||||
@@ -93,7 +93,9 @@ export class NavbarComponent extends MenuComponent implements OnInit {
|
|||||||
} as LinkMenuItemModel
|
} as LinkMenuItemModel
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, menuSection));
|
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
|
||||||
|
shouldPersistOnRouteChange: true
|
||||||
|
})));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -0,0 +1,2 @@
|
|||||||
|
<h4 class="mt-4">{{title | translate}}</h4>
|
||||||
|
<ng-content></ng-content>
|
@@ -0,0 +1,38 @@
|
|||||||
|
import { ProcessDetailFieldComponent } from './process-detail-field.component';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { VarDirective } from '../../../shared/utils/var.directive';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
describe('ProcessDetailFieldComponent', () => {
|
||||||
|
let component: ProcessDetailFieldComponent;
|
||||||
|
let fixture: ComponentFixture<ProcessDetailFieldComponent>;
|
||||||
|
|
||||||
|
let title;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
title = 'fake.title.message';
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ProcessDetailFieldComponent, VarDirective],
|
||||||
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||||
|
providers: [
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ProcessDetailFieldComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.title = title;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the given title', () => {
|
||||||
|
const header = fixture.debugElement.query(By.css('h4')).nativeElement;
|
||||||
|
expect(header.textContent).toContain(title);
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,15 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-process-detail-field',
|
||||||
|
templateUrl: './process-detail-field.component.html',
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* A component displaying a single detail about a DSpace Process
|
||||||
|
*/
|
||||||
|
export class ProcessDetailFieldComponent {
|
||||||
|
/**
|
||||||
|
* I18n message for the header
|
||||||
|
*/
|
||||||
|
@Input() title: string;
|
||||||
|
}
|
42
src/app/process-page/detail/process-detail.component.html
Normal file
42
src/app/process-page/detail/process-detail.component.html
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<div class="container" *ngVar="(processRD$ | async)?.payload as process">
|
||||||
|
<div class="d-flex">
|
||||||
|
<h2 class="flex-grow-1">{{'process.detail.title' | translate:{id: process?.processId, name: process?.scriptName} }}</h2>
|
||||||
|
<div>
|
||||||
|
<a class="btn btn-light" [routerLink]="'/processes/new'" [queryParams]="{id: process?.processId}">{{'process.detail.create' | translate}}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ds-process-detail-field id="process-name" [title]="'process.detail.script'">
|
||||||
|
<div>{{ process?.scriptName }}</div>
|
||||||
|
</ds-process-detail-field>
|
||||||
|
|
||||||
|
<ds-process-detail-field *ngIf="process?.parameters && process?.parameters?.length > 0" id="process-arguments" [title]="'process.detail.arguments'">
|
||||||
|
<div *ngFor="let argument of process?.parameters">{{ argument?.name }} {{ argument?.value }}</div>
|
||||||
|
</ds-process-detail-field>
|
||||||
|
|
||||||
|
<div *ngVar="(filesRD$ | async)?.payload?.page as files">
|
||||||
|
<ds-process-detail-field *ngIf="files && files?.length > 0" id="process-files" [title]="'process.detail.output-files'">
|
||||||
|
<ds-file-download-link *ngFor="let file of files; let last=last;" [href]="file?._links?.content?.href" [download]="getFileName(file)">
|
||||||
|
<span>{{getFileName(file)}}</span>
|
||||||
|
<span>({{(file?.sizeBytes) | dsFileSize }})</span>
|
||||||
|
</ds-file-download-link>
|
||||||
|
</ds-process-detail-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ds-process-detail-field *ngIf="process && process.startTime" id="process-start-time" [title]="'process.detail.start-time' | translate">
|
||||||
|
<div>{{ process.startTime }}</div>
|
||||||
|
</ds-process-detail-field>
|
||||||
|
|
||||||
|
<ds-process-detail-field *ngIf="process && process.endTime" id="process-end-time" [title]="'process.detail.end-time' | translate">
|
||||||
|
<div>{{ process.endTime }}</div>
|
||||||
|
</ds-process-detail-field>
|
||||||
|
|
||||||
|
<ds-process-detail-field *ngIf="process && process.processStatus" id="process-status" [title]="'process.detail.status' | translate">
|
||||||
|
<div>{{ process.processStatus }}</div>
|
||||||
|
</ds-process-detail-field>
|
||||||
|
|
||||||
|
<!--<ds-process-detail-field id="process-output" [title]="'process.detail.output'">-->
|
||||||
|
<!--<pre class="font-weight-bold text-secondary bg-light p-3">{{'process.detail.output.alert' | translate}}</pre>-->
|
||||||
|
<!--</ds-process-detail-field>-->
|
||||||
|
|
||||||
|
<a class="btn btn-light mt-3" [routerLink]="'/processes'">{{'process.detail.back' | translate}}</a>
|
||||||
|
</div>
|
107
src/app/process-page/detail/process-detail.component.spec.ts
Normal file
107
src/app/process-page/detail/process-detail.component.spec.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { ProcessDetailComponent } from './process-detail.component';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { VarDirective } from '../../shared/utils/var.directive';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { ProcessDetailFieldComponent } from './process-detail-field/process-detail-field.component';
|
||||||
|
import { Process } from '../processes/process.model';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { FileSizePipe } from '../../shared/utils/file-size-pipe';
|
||||||
|
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||||
|
import { ProcessDataService } from '../../core/data/processes/process-data.service';
|
||||||
|
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||||
|
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||||
|
|
||||||
|
describe('ProcessDetailComponent', () => {
|
||||||
|
let component: ProcessDetailComponent;
|
||||||
|
let fixture: ComponentFixture<ProcessDetailComponent>;
|
||||||
|
|
||||||
|
let processService: ProcessDataService;
|
||||||
|
let nameService: DSONameService;
|
||||||
|
|
||||||
|
let process: Process;
|
||||||
|
let fileName: string;
|
||||||
|
let files: Bitstream[];
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
process = Object.assign(new Process(), {
|
||||||
|
processId: 1,
|
||||||
|
scriptName: 'script-name',
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: '-f',
|
||||||
|
value: 'file.xml'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '-i',
|
||||||
|
value: 'identifier'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
fileName = 'fake-file-name';
|
||||||
|
files = [
|
||||||
|
Object.assign(new Bitstream(), {
|
||||||
|
sizeBytes: 10000,
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{
|
||||||
|
value: fileName,
|
||||||
|
language: null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
_links: {
|
||||||
|
content: { href: 'file-selflink' }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
];
|
||||||
|
processService = jasmine.createSpyObj('processService', {
|
||||||
|
getFiles: createSuccessfulRemoteDataObject$(createPaginatedList(files))
|
||||||
|
});
|
||||||
|
nameService = jasmine.createSpyObj('nameService', {
|
||||||
|
getName: fileName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ProcessDetailComponent, ProcessDetailFieldComponent, VarDirective, FileSizePipe],
|
||||||
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||||
|
providers: [
|
||||||
|
{ provide: ActivatedRoute, useValue: { data: observableOf({ process: createSuccessfulRemoteDataObject(process) }) } },
|
||||||
|
{ provide: ProcessDataService, useValue: processService },
|
||||||
|
{ provide: DSONameService, useValue: nameService }
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ProcessDetailComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the script\'s name', () => {
|
||||||
|
const name = fixture.debugElement.query(By.css('#process-name')).nativeElement;
|
||||||
|
expect(name.textContent).toContain(process.scriptName);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the process\'s parameters', () => {
|
||||||
|
const args = fixture.debugElement.query(By.css('#process-arguments')).nativeElement;
|
||||||
|
process.parameters.forEach((param) => {
|
||||||
|
expect(args.textContent).toContain(`${param.name} ${param.value}`)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the process\'s output files', () => {
|
||||||
|
const processFiles = fixture.debugElement.query(By.css('#process-files')).nativeElement;
|
||||||
|
expect(processFiles.textContent).toContain(fileName);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
69
src/app/process-page/detail/process-detail.component.ts
Normal file
69
src/app/process-page/detail/process-detail.component.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { Process } from '../processes/process.model';
|
||||||
|
import { map, switchMap } from 'rxjs/operators';
|
||||||
|
import { getFirstSucceededRemoteDataPayload, redirectToPageNotFoundOn404 } from '../../core/shared/operators';
|
||||||
|
import { AlertType } from '../../shared/alert/aletr-type';
|
||||||
|
import { ProcessDataService } from '../../core/data/processes/process-data.service';
|
||||||
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
|
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||||
|
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-process-detail',
|
||||||
|
templateUrl: './process-detail.component.html',
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* A component displaying detailed information about a DSpace Process
|
||||||
|
*/
|
||||||
|
export class ProcessDetailComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The AlertType enumeration
|
||||||
|
* @type {AlertType}
|
||||||
|
*/
|
||||||
|
public AlertTypeEnum = AlertType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Process's Remote Data
|
||||||
|
*/
|
||||||
|
processRD$: Observable<RemoteData<Process>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Process's Output Files
|
||||||
|
*/
|
||||||
|
filesRD$: Observable<RemoteData<PaginatedList<Bitstream>>>;
|
||||||
|
|
||||||
|
constructor(protected route: ActivatedRoute,
|
||||||
|
protected router: Router,
|
||||||
|
protected processService: ProcessDataService,
|
||||||
|
protected nameService: DSONameService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize component properties
|
||||||
|
* Display a 404 if the process doesn't exist
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.processRD$ = this.route.data.pipe(
|
||||||
|
map((data) => data.process as RemoteData<Process>),
|
||||||
|
redirectToPageNotFoundOn404(this.router)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.filesRD$ = this.processRD$.pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
switchMap((process: Process) => this.processService.getFiles(process.processId))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the name of a bitstream
|
||||||
|
* @param bitstream
|
||||||
|
*/
|
||||||
|
getFileName(bitstream: Bitstream) {
|
||||||
|
return this.nameService.getName(bitstream);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
25
src/app/process-page/form/process-form.component.html
Normal file
25
src/app/process-page/form/process-form.component.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<h2 class="col-12">
|
||||||
|
{{headerKey | translate}}
|
||||||
|
</h2>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<form #form="ngForm" (ngSubmit)="submitForm(form)">
|
||||||
|
<ds-scripts-select [script]="selectedScript" (select)="selectedScript = $event; parameters = undefined"></ds-scripts-select>
|
||||||
|
<ds-process-parameters [initialParams]="parameters" [script]="selectedScript" (updateParameters)="parameters = $event"></ds-process-parameters>
|
||||||
|
<button [routerLink]="['/processes']" class="btn btn-light float-left">{{ 'process.new.cancel' | translate }}</button>
|
||||||
|
<button type="submit" class="btn btn-light float-right">{{ 'process.new.submit' | translate }}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<ds-script-help [script]="selectedScript"></ds-script-help>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="missingParameters.length > 0" class="mt-3 alert alert-danger validation-error">
|
||||||
|
{{'process.new.parameter.required.missing' | translate}}
|
||||||
|
<ul>
|
||||||
|
<li *ngFor="let missing of missingParameters">{{missing}}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
86
src/app/process-page/form/process-form.component.spec.ts
Normal file
86
src/app/process-page/form/process-form.component.spec.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { ProcessFormComponent } from './process-form.component';
|
||||||
|
import { ScriptDataService } from '../../core/data/processes/script-data.service';
|
||||||
|
import { ScriptParameter } from '../scripts/script-parameter.model';
|
||||||
|
import { Script } from '../scripts/script.model';
|
||||||
|
import { ProcessParameter } from '../processes/process-parameter.model';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
||||||
|
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
|
||||||
|
import { RequestService } from '../../core/data/request.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
describe('ProcessFormComponent', () => {
|
||||||
|
let component: ProcessFormComponent;
|
||||||
|
let fixture: ComponentFixture<ProcessFormComponent>;
|
||||||
|
let scriptService;
|
||||||
|
let parameterValues;
|
||||||
|
let script;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
const param1 = new ScriptParameter();
|
||||||
|
const param2 = new ScriptParameter();
|
||||||
|
script = Object.assign(new Script(), { parameters: [param1, param2] });
|
||||||
|
parameterValues = [
|
||||||
|
Object.assign(new ProcessParameter(), { name: '-a', value: 'bla' }),
|
||||||
|
Object.assign(new ProcessParameter(), { name: '-b', value: '123' }),
|
||||||
|
Object.assign(new ProcessParameter(), { name: '-c', value: 'value' }),
|
||||||
|
];
|
||||||
|
scriptService = jasmine.createSpyObj(
|
||||||
|
'scriptService',
|
||||||
|
{
|
||||||
|
invoke: observableOf({
|
||||||
|
response:
|
||||||
|
{
|
||||||
|
isSuccessful: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
})],
|
||||||
|
declarations: [ProcessFormComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: ScriptDataService, useValue: scriptService },
|
||||||
|
{ provide: NotificationsService, useClass: NotificationsServiceStub },
|
||||||
|
{ provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeBySubstring']) },
|
||||||
|
{ provide: Router, useValue: {} },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ProcessFormComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.parameters = parameterValues;
|
||||||
|
component.selectedScript = script;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call invoke on the scriptService on submit', () => {
|
||||||
|
component.submitForm({ controls: {} } as any);
|
||||||
|
expect(scriptService.invoke).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
151
src/app/process-page/form/process-form.component.ts
Normal file
151
src/app/process-page/form/process-form.component.ts
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
import { Script } from '../scripts/script.model';
|
||||||
|
import { Process } from '../processes/process.model';
|
||||||
|
import { ProcessParameter } from '../processes/process-parameter.model';
|
||||||
|
import { ScriptDataService } from '../../core/data/processes/script-data.service';
|
||||||
|
import { ControlContainer, NgForm } from '@angular/forms';
|
||||||
|
import { ScriptParameter } from '../scripts/script-parameter.model';
|
||||||
|
import { RequestEntry } from '../../core/data/request.reducer';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
import { RequestService } from '../../core/data/request.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to create a new script
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-process-form',
|
||||||
|
templateUrl: './process-form.component.html',
|
||||||
|
styleUrls: ['./process-form.component.scss'],
|
||||||
|
})
|
||||||
|
export class ProcessFormComponent implements OnInit {
|
||||||
|
/**
|
||||||
|
* The currently selected script
|
||||||
|
*/
|
||||||
|
@Input() public selectedScript: Script = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The process to create
|
||||||
|
*/
|
||||||
|
@Input() public process: Process = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The parameter values to use to start the process
|
||||||
|
*/
|
||||||
|
@Input() public parameters: ProcessParameter[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional files that are used as parameter values
|
||||||
|
*/
|
||||||
|
public files: File[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message key for the header of the form
|
||||||
|
*/
|
||||||
|
@Input() public headerKey: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains the missing parameters on submission
|
||||||
|
*/
|
||||||
|
public missingParameters = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private scriptService: ScriptDataService,
|
||||||
|
private notificationsService: NotificationsService,
|
||||||
|
private translationService: TranslateService,
|
||||||
|
private requestService: RequestService,
|
||||||
|
private router: Router) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.process = new Process();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the form, sets the parameters to correct values and invokes the script with the correct parameters
|
||||||
|
* @param form
|
||||||
|
*/
|
||||||
|
submitForm(form: NgForm) {
|
||||||
|
if (!this.validateForm(form) || this.isRequiredMissing()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stringParameters: ProcessParameter[] = this.parameters.map((parameter: ProcessParameter) => {
|
||||||
|
return {
|
||||||
|
name: parameter.name,
|
||||||
|
value: this.checkValue(parameter)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.scriptService.invoke(this.selectedScript.id, stringParameters, this.files)
|
||||||
|
.pipe(take(1))
|
||||||
|
.subscribe((requestEntry: RequestEntry) => {
|
||||||
|
if (requestEntry.response.isSuccessful) {
|
||||||
|
const title = this.translationService.get('process.new.notification.success.title');
|
||||||
|
const content = this.translationService.get('process.new.notification.success.content');
|
||||||
|
this.notificationsService.success(title, content);
|
||||||
|
this.sendBack();
|
||||||
|
} else {
|
||||||
|
const title = this.translationService.get('process.new.notification.error.title');
|
||||||
|
const content = this.translationService.get('process.new.notification.error.content');
|
||||||
|
this.notificationsService.error(title, content);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the parameter values are files
|
||||||
|
* Replaces file parameters by strings and stores the files in a separate list
|
||||||
|
* @param processParameter The parameter value to check
|
||||||
|
*/
|
||||||
|
private checkValue(processParameter: ProcessParameter): string {
|
||||||
|
if (typeof processParameter.value === 'object') {
|
||||||
|
this.files = [...this.files, processParameter.value];
|
||||||
|
return processParameter.value.name;
|
||||||
|
}
|
||||||
|
return processParameter.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the form
|
||||||
|
* Returns false if the form is invalid
|
||||||
|
* Returns true if the form is valid
|
||||||
|
* @param form The NgForm object to validate
|
||||||
|
*/
|
||||||
|
private validateForm(form: NgForm) {
|
||||||
|
let valid = true;
|
||||||
|
Object.keys(form.controls).forEach((key) => {
|
||||||
|
if (form.controls[key].invalid) {
|
||||||
|
form.controls[key].markAsDirty();
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isRequiredMissing() {
|
||||||
|
this.missingParameters = [];
|
||||||
|
const setParams: string[] = this.parameters
|
||||||
|
.map((param) => param.name);
|
||||||
|
const requiredParams: ScriptParameter[] = this.selectedScript.parameters.filter((param) => param.mandatory);
|
||||||
|
for (const rp of requiredParams) {
|
||||||
|
if (!setParams.includes(rp.name)) {
|
||||||
|
this.missingParameters.push(rp.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.missingParameters.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendBack() {
|
||||||
|
this.requestService.removeByHrefSubstring('/processes');
|
||||||
|
/* should subscribe on the previous method to know the action is finished and then navigate,
|
||||||
|
will fix this when the removeByHrefSubstring changes are merged */
|
||||||
|
this.router.navigateByUrl('/processes');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function controlContainerFactory(controlContainer?: ControlContainer) {
|
||||||
|
return controlContainer;
|
||||||
|
}
|
@@ -0,0 +1,16 @@
|
|||||||
|
<div class="form-row mb-2 mx-0">
|
||||||
|
<select id="process-parameters"
|
||||||
|
class="form-control col"
|
||||||
|
name="parameter-{{index}}"
|
||||||
|
[(ngModel)]="selectedParameter"
|
||||||
|
#param="ngModel">
|
||||||
|
<option [ngValue]="undefined">Add a parameter...</option>
|
||||||
|
<option *ngFor="let param of parameters" [ngValue]="param.name">
|
||||||
|
{{param.nameLong || param.name}}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<ds-parameter-value-input [initialValue]="parameterValue.value" [parameter]="selectedScriptParameter" (updateValue)="selectedParameterValue = $event" class="d-block col" [index]="index"></ds-parameter-value-input>
|
||||||
|
<button *ngIf="removable" class="btn btn-light col-1 remove-button" (click)="removeParameter.emit(parameterValue);"><span class="fas fa-trash"></span></button>
|
||||||
|
<span *ngIf="!removable" class="col-1"></span>
|
||||||
|
</div>
|
||||||
|
|
@@ -0,0 +1,71 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ParameterSelectComponent } from './parameter-select.component';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { ScriptParameter } from '../../../scripts/script-parameter.model';
|
||||||
|
import { ScriptParameterType } from '../../../scripts/script-parameter-type.model';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
describe('ParameterSelectComponent', () => {
|
||||||
|
let component: ParameterSelectComponent;
|
||||||
|
let fixture: ComponentFixture<ParameterSelectComponent>;
|
||||||
|
let scriptParams: ScriptParameter[];
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
scriptParams = [
|
||||||
|
Object.assign(
|
||||||
|
new ScriptParameter(),
|
||||||
|
{
|
||||||
|
name: '-a',
|
||||||
|
type: ScriptParameterType.BOOLEAN
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Object.assign(
|
||||||
|
new ScriptParameter(),
|
||||||
|
{
|
||||||
|
name: '-f',
|
||||||
|
type: ScriptParameterType.FILE
|
||||||
|
}
|
||||||
|
),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [FormsModule],
|
||||||
|
declarations: [ParameterSelectComponent],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ParameterSelectComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
|
||||||
|
component.parameters = scriptParams;
|
||||||
|
component.removable = false;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show the remove button when removable', () => {
|
||||||
|
component.removable = true;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const button = fixture.debugElement.query(By.css('button.remove-button'));
|
||||||
|
expect(button).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should hide the remove button when not removable', () => {
|
||||||
|
component.removable = false;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const button = fixture.debugElement.query(By.css('button.remove-button'));
|
||||||
|
expect(button).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,89 @@
|
|||||||
|
import { Component, EventEmitter, Input, Optional, Output } from '@angular/core';
|
||||||
|
import { ProcessParameter } from '../../../processes/process-parameter.model';
|
||||||
|
import { ScriptParameter } from '../../../scripts/script-parameter.model';
|
||||||
|
import { ControlContainer, NgForm } from '@angular/forms';
|
||||||
|
import { controlContainerFactory } from '../../process-form.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to select a single parameter for a process
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-parameter-select',
|
||||||
|
templateUrl: './parameter-select.component.html',
|
||||||
|
styleUrls: ['./parameter-select.component.scss'],
|
||||||
|
viewProviders: [{
|
||||||
|
provide: ControlContainer,
|
||||||
|
useFactory: controlContainerFactory,
|
||||||
|
deps: [[new Optional(), NgForm]]
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
export class ParameterSelectComponent {
|
||||||
|
@Input() index: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current parameter value of the selected parameter
|
||||||
|
*/
|
||||||
|
@Input() parameterValue: ProcessParameter = new ProcessParameter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The available script parameters for the script
|
||||||
|
*/
|
||||||
|
@Input() parameters: ScriptParameter[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not this selected parameter can be removed from the list
|
||||||
|
*/
|
||||||
|
@Input() removable: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits the parameter value when it's removed
|
||||||
|
*/
|
||||||
|
@Output() removeParameter: EventEmitter<ProcessParameter> = new EventEmitter<ProcessParameter>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits the updated parameter value when it changes
|
||||||
|
*/
|
||||||
|
@Output() changeParameter: EventEmitter<ProcessParameter> = new EventEmitter<ProcessParameter>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the script parameter based on the currently selected name
|
||||||
|
*/
|
||||||
|
get selectedScriptParameter(): ScriptParameter {
|
||||||
|
return this.parameters.find((parameter: ScriptParameter) => parameter.name === this.selectedParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the currently selected parameter name
|
||||||
|
*/
|
||||||
|
get selectedParameter(): string {
|
||||||
|
return this.parameterValue ? this.parameterValue.name : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the currently selected parameter based on the provided parameter name
|
||||||
|
* Emits the new value from the changeParameter output
|
||||||
|
* @param value The parameter name to set
|
||||||
|
*/
|
||||||
|
set selectedParameter(value: string) {
|
||||||
|
this.parameterValue.name = value;
|
||||||
|
this.selectedParameterValue = undefined;
|
||||||
|
this.changeParameter.emit(this.parameterValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the currently selected parameter value
|
||||||
|
*/
|
||||||
|
get selectedParameterValue(): any {
|
||||||
|
return this.parameterValue ? this.parameterValue.value : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the currently selected value for the parameter
|
||||||
|
* Emits the new value from the changeParameter output
|
||||||
|
* @param value The parameter value to set
|
||||||
|
*/
|
||||||
|
set selectedParameterValue(value: any) {
|
||||||
|
this.parameterValue.value = value;
|
||||||
|
this.changeParameter.emit(this.parameterValue);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1 @@
|
|||||||
|
<input type="hidden" value="true" name="boolean-value-{{index}}" id="boolean-value-{{index}}"/>
|
@@ -0,0 +1,32 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { BooleanValueInputComponent } from './boolean-value-input.component';
|
||||||
|
import { DebugElement } from '@angular/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
describe('BooleanValueInputComponent', () => {
|
||||||
|
let component: BooleanValueInputComponent;
|
||||||
|
let fixture: ComponentFixture<BooleanValueInputComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [BooleanValueInputComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(BooleanValueInputComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
spyOn(component.updateValue, 'emit');
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit true onInit', () => {
|
||||||
|
expect(component.updateValue.emit).toHaveBeenCalledWith(true);
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,21 @@
|
|||||||
|
import { Component, OnInit, Optional } from '@angular/core';
|
||||||
|
import { ValueInputComponent } from '../value-input.component';
|
||||||
|
import { ControlContainer, NgForm } from '@angular/forms';
|
||||||
|
import { controlContainerFactory } from '../../../process-form.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the value of a boolean parameter
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-boolean-value-input',
|
||||||
|
templateUrl: './boolean-value-input.component.html',
|
||||||
|
styleUrls: ['./boolean-value-input.component.scss'],
|
||||||
|
viewProviders: [ { provide: ControlContainer,
|
||||||
|
useFactory: controlContainerFactory,
|
||||||
|
deps: [[new Optional(), NgForm]] } ]
|
||||||
|
})
|
||||||
|
export class BooleanValueInputComponent extends ValueInputComponent<boolean> implements OnInit {
|
||||||
|
ngOnInit() {
|
||||||
|
this.updateValue.emit(true)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,7 @@
|
|||||||
|
<input required #string="ngModel" type="text" class="form-control" name="date-value-{{index}}" id="date-value-{{index}}" [ngModel]="value" (ngModelChange)="setValue($event)"/>
|
||||||
|
<div *ngIf="string.invalid && (string.dirty || string.touched)"
|
||||||
|
class="alert alert-danger validation-error">
|
||||||
|
<div *ngIf="string.errors.required">
|
||||||
|
{{'process.new.parameter.string.required' | translate}}
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,70 @@
|
|||||||
|
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { DateValueInputComponent } from './date-value-input.component';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { TranslateLoaderMock } from '../../../../../shared/mocks/translate-loader.mock';
|
||||||
|
|
||||||
|
describe('DateValueInputComponent', () => {
|
||||||
|
let component: DateValueInputComponent;
|
||||||
|
let fixture: ComponentFixture<DateValueInputComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
})],
|
||||||
|
declarations: [DateValueInputComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(DateValueInputComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show a validation error if the input field was left untouched but left empty', () => {
|
||||||
|
const validationError = fixture.debugElement.query(By.css('.validation-error'));
|
||||||
|
expect(validationError).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a validation error if the input field was touched but left empty', fakeAsync(() => {
|
||||||
|
component.value = '';
|
||||||
|
fixture.detectChanges();
|
||||||
|
tick();
|
||||||
|
|
||||||
|
const input = fixture.debugElement.query(By.css('input'));
|
||||||
|
input.triggerEventHandler('blur', null);
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const validationError = fixture.debugElement.query(By.css('.validation-error'));
|
||||||
|
expect(validationError).toBeTruthy();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should not show a validation error if the input field was touched but not left empty', fakeAsync(() => {
|
||||||
|
component.value = 'testValue';
|
||||||
|
fixture.detectChanges();
|
||||||
|
tick();
|
||||||
|
|
||||||
|
const input = fixture.debugElement.query(By.css('input'));
|
||||||
|
input.triggerEventHandler('blur', null);
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const validationError = fixture.debugElement.query(By.css('.validation-error'));
|
||||||
|
expect(validationError).toBeFalsy();
|
||||||
|
}));
|
||||||
|
});
|
@@ -0,0 +1,36 @@
|
|||||||
|
import { Component, Input, Optional } from '@angular/core';
|
||||||
|
import { ValueInputComponent } from '../value-input.component';
|
||||||
|
import { ControlContainer, NgForm } from '@angular/forms';
|
||||||
|
import { controlContainerFactory } from '../../../process-form.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the user inputted value of a date parameter
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-date-value-input',
|
||||||
|
templateUrl: './date-value-input.component.html',
|
||||||
|
styleUrls: ['./date-value-input.component.scss'],
|
||||||
|
viewProviders: [ { provide: ControlContainer,
|
||||||
|
useFactory: controlContainerFactory,
|
||||||
|
deps: [[new Optional(), NgForm]] } ]
|
||||||
|
})
|
||||||
|
export class DateValueInputComponent extends ValueInputComponent<string> {
|
||||||
|
/**
|
||||||
|
* The current value of the date string
|
||||||
|
*/
|
||||||
|
value: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initial value of the field
|
||||||
|
*/
|
||||||
|
@Input() initialValue;
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.value = this.initialValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(value) {
|
||||||
|
this.value = value;
|
||||||
|
this.updateValue.emit(value)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,13 @@
|
|||||||
|
<label for="file-upload-{{index}}" class="d-flex align-items-center m-0">
|
||||||
|
<span class="btn btn-light">
|
||||||
|
{{'process.new.parameter.file.upload-button' | translate}}
|
||||||
|
</span>
|
||||||
|
<span class="file-name ml-1">{{fileObject?.name}}</span>
|
||||||
|
</label>
|
||||||
|
<input requireFile #file="ngModel" type="file" name="file-upload-{{index}}" id="file-upload-{{index}}" class="form-control-file d-none" [ngModel]="fileObject" (ngModelChange)="setFile($event)"/>
|
||||||
|
<div *ngIf="file.invalid && (file.dirty || file.touched)"
|
||||||
|
class="alert alert-danger validation-error">
|
||||||
|
<div *ngIf="file.errors.required">
|
||||||
|
{{'process.new.parameter.file.required' | translate}}
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,6 @@
|
|||||||
|
.file-name {
|
||||||
|
flex: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
@@ -0,0 +1,57 @@
|
|||||||
|
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { FormsModule, NgForm, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { FileValueInputComponent } from './file-value-input.component';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { FileValueAccessorDirective } from '../../../../../shared/utils/file-value-accessor.directive';
|
||||||
|
import { FileValidator } from '../../../../../shared/utils/require-file.validator';
|
||||||
|
import { TranslateLoaderMock } from '../../../../../shared/mocks/translate-loader.mock';
|
||||||
|
|
||||||
|
describe('FileValueInputComponent', () => {
|
||||||
|
let component: FileValueInputComponent;
|
||||||
|
let fixture: ComponentFixture<FileValueInputComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
})],
|
||||||
|
declarations: [FileValueInputComponent, FileValueAccessorDirective, FileValidator],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(FileValueInputComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show a validation error if the input field was left untouched but left empty', () => {
|
||||||
|
const validationError = fixture.debugElement.query(By.css('.validation-error'));
|
||||||
|
expect(validationError).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a validation error if the input field was touched but left empty', () => {
|
||||||
|
const input = fixture.debugElement.query(By.css('input'));
|
||||||
|
input.triggerEventHandler('blur', null);
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const validationError = fixture.debugElement.query(By.css('.validation-error'));
|
||||||
|
expect(validationError).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,26 @@
|
|||||||
|
import { Component, Optional } from '@angular/core';
|
||||||
|
import { ValueInputComponent } from '../value-input.component';
|
||||||
|
import { ControlContainer, NgForm } from '@angular/forms';
|
||||||
|
import { controlContainerFactory } from '../../../process-form.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the user inputted value of a file parameter
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-file-value-input',
|
||||||
|
templateUrl: './file-value-input.component.html',
|
||||||
|
styleUrls: ['./file-value-input.component.scss'],
|
||||||
|
viewProviders: [ { provide: ControlContainer,
|
||||||
|
useFactory: controlContainerFactory,
|
||||||
|
deps: [[new Optional(), NgForm]] } ]
|
||||||
|
})
|
||||||
|
export class FileValueInputComponent extends ValueInputComponent<File> {
|
||||||
|
/**
|
||||||
|
* The current value of the file
|
||||||
|
*/
|
||||||
|
fileObject: File;
|
||||||
|
setFile(files) {
|
||||||
|
this.fileObject = files.length > 0 ? files[0] : undefined;
|
||||||
|
this.updateValue.emit(this.fileObject);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,7 @@
|
|||||||
|
<div [ngSwitch]="parameter?.type">
|
||||||
|
<ds-string-value-input *ngSwitchCase="parameterTypes.STRING" [initialValue]="initialValue" (updateValue)="updateValue.emit($event)" [index]="index"></ds-string-value-input>
|
||||||
|
<ds-string-value-input *ngSwitchCase="parameterTypes.OUTPUT" [initialValue]="initialValue" (updateValue)="updateValue.emit($event)" [index]="index"></ds-string-value-input>
|
||||||
|
<ds-date-value-input *ngSwitchCase="parameterTypes.DATE" [initialValue]="initialValue" (updateValue)="updateValue.emit($event)" [index]="index"></ds-date-value-input>
|
||||||
|
<ds-file-value-input *ngSwitchCase="parameterTypes.FILE" (updateValue)="updateValue.emit($event)" [index]="index"></ds-file-value-input>
|
||||||
|
<ds-boolean-value-input *ngSwitchCase="parameterTypes.BOOLEAN" (updateValue)="updateValue.emit($event)" [index]="index"></ds-boolean-value-input>
|
||||||
|
</div>
|
@@ -0,0 +1,105 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ParameterValueInputComponent } from './parameter-value-input.component';
|
||||||
|
import { ScriptParameter } from '../../../scripts/script-parameter.model';
|
||||||
|
import { ScriptParameterType } from '../../../scripts/script-parameter-type.model';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { BooleanValueInputComponent } from './boolean-value-input/boolean-value-input.component';
|
||||||
|
import { StringValueInputComponent } from './string-value-input/string-value-input.component';
|
||||||
|
import { FileValueInputComponent } from './file-value-input/file-value-input.component';
|
||||||
|
import { DateValueInputComponent } from './date-value-input/date-value-input.component';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { FileValueAccessorDirective } from '../../../../shared/utils/file-value-accessor.directive';
|
||||||
|
import { FileValidator } from '../../../../shared/utils/require-file.validator';
|
||||||
|
import { TranslateLoaderMock } from '../../../../shared/mocks/translate-loader.mock';
|
||||||
|
|
||||||
|
describe('ParameterValueInputComponent', () => {
|
||||||
|
let component: ParameterValueInputComponent;
|
||||||
|
let fixture: ComponentFixture<ParameterValueInputComponent>;
|
||||||
|
let booleanParameter;
|
||||||
|
let stringParameter;
|
||||||
|
let fileParameter;
|
||||||
|
let dateParameter;
|
||||||
|
let outputParameter;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
booleanParameter = Object.assign(new ScriptParameter(), { type: ScriptParameterType.BOOLEAN });
|
||||||
|
stringParameter = Object.assign(new ScriptParameter(), { type: ScriptParameterType.STRING });
|
||||||
|
fileParameter = Object.assign(new ScriptParameter(), { type: ScriptParameterType.FILE });
|
||||||
|
dateParameter = Object.assign(new ScriptParameter(), { type: ScriptParameterType.DATE });
|
||||||
|
outputParameter = Object.assign(new ScriptParameter(), { type: ScriptParameterType.OUTPUT });
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
})],
|
||||||
|
declarations: [
|
||||||
|
ParameterValueInputComponent,
|
||||||
|
BooleanValueInputComponent,
|
||||||
|
StringValueInputComponent,
|
||||||
|
FileValueInputComponent,
|
||||||
|
DateValueInputComponent,
|
||||||
|
FileValueAccessorDirective,
|
||||||
|
FileValidator
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ParameterValueInputComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.parameter = stringParameter;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a BooleanValueInputComponent when the parameter type is boolean', () => {
|
||||||
|
component.parameter = booleanParameter;
|
||||||
|
fixture.detectChanges();
|
||||||
|
const valueInput = fixture.debugElement.query(By.directive(BooleanValueInputComponent));
|
||||||
|
expect(valueInput).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a StringValueInputComponent when the parameter type is string', () => {
|
||||||
|
component.parameter = stringParameter;
|
||||||
|
fixture.detectChanges();
|
||||||
|
const valueInput = fixture.debugElement.query(By.directive(StringValueInputComponent));
|
||||||
|
expect(valueInput).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a FileValueInputComponent when the parameter type is file', () => {
|
||||||
|
component.parameter = fileParameter;
|
||||||
|
fixture.detectChanges();
|
||||||
|
const valueInput = fixture.debugElement.query(By.directive(FileValueInputComponent));
|
||||||
|
expect(valueInput).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a DateValueInputComponent when the parameter type is date', () => {
|
||||||
|
component.parameter = dateParameter;
|
||||||
|
fixture.detectChanges();
|
||||||
|
const valueInput = fixture.debugElement.query(By.directive(DateValueInputComponent));
|
||||||
|
expect(valueInput).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a StringValueInputComponent when the parameter type is output', () => {
|
||||||
|
component.parameter = outputParameter;
|
||||||
|
fixture.detectChanges();
|
||||||
|
const valueInput = fixture.debugElement.query(By.directive(StringValueInputComponent));
|
||||||
|
expect(valueInput).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,39 @@
|
|||||||
|
import { Component, EventEmitter, Input, Optional, Output } from '@angular/core';
|
||||||
|
import { ScriptParameterType } from '../../../scripts/script-parameter-type.model';
|
||||||
|
import { ScriptParameter } from '../../../scripts/script-parameter.model';
|
||||||
|
import { ControlContainer, NgForm } from '@angular/forms';
|
||||||
|
import { controlContainerFactory } from '../../process-form.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that renders the correct parameter value input based the script parameter's type
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-parameter-value-input',
|
||||||
|
templateUrl: './parameter-value-input.component.html',
|
||||||
|
styleUrls: ['./parameter-value-input.component.scss'],
|
||||||
|
viewProviders: [ { provide: ControlContainer,
|
||||||
|
useFactory: controlContainerFactory,
|
||||||
|
deps: [[new Optional(), NgForm]] } ]
|
||||||
|
})
|
||||||
|
export class ParameterValueInputComponent {
|
||||||
|
@Input() index: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current script parameter
|
||||||
|
*/
|
||||||
|
@Input() parameter: ScriptParameter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initial value for input
|
||||||
|
*/
|
||||||
|
@Input() initialValue: any;
|
||||||
|
/**
|
||||||
|
* Emits the value of the input when its updated
|
||||||
|
*/
|
||||||
|
@Output() updateValue: EventEmitter<any> = new EventEmitter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The available script parameter types
|
||||||
|
*/
|
||||||
|
parameterTypes = ScriptParameterType;
|
||||||
|
}
|
@@ -0,0 +1,7 @@
|
|||||||
|
<input required #string="ngModel" type="text" name="string-value-{{index}}" class="form-control" id="string-value-{{index}}" [ngModel]="value" (ngModelChange)="setValue($event)"/>
|
||||||
|
<div *ngIf="string.invalid && (string.dirty || string.touched)"
|
||||||
|
class="alert alert-danger validation-error">
|
||||||
|
<div *ngIf="string.errors.required">
|
||||||
|
{{'process.new.parameter.string.required' | translate}}
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,72 @@
|
|||||||
|
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { FormsModule, NgForm } from '@angular/forms';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { StringValueInputComponent } from './string-value-input.component';
|
||||||
|
import { TranslateLoaderMock } from '../../../../../shared/mocks/translate-loader.mock';
|
||||||
|
|
||||||
|
describe('StringValueInputComponent', () => {
|
||||||
|
let component: StringValueInputComponent;
|
||||||
|
let fixture: ComponentFixture<StringValueInputComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
})],
|
||||||
|
declarations: [StringValueInputComponent],
|
||||||
|
providers: [
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(StringValueInputComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show a validation error if the input field was left untouched but left empty', () => {
|
||||||
|
const validationError = fixture.debugElement.query(By.css('.validation-error'));
|
||||||
|
expect(validationError).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a validation error if the input field was touched but left empty', fakeAsync(() => {
|
||||||
|
component.value = '';
|
||||||
|
fixture.detectChanges();
|
||||||
|
tick();
|
||||||
|
|
||||||
|
const input = fixture.debugElement.query(By.css('input'));
|
||||||
|
input.triggerEventHandler('blur', null);
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const validationError = fixture.debugElement.query(By.css('.validation-error'));
|
||||||
|
expect(validationError).toBeTruthy();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should not show a validation error if the input field was touched but not left empty', fakeAsync(() => {
|
||||||
|
component.value = 'testValue';
|
||||||
|
fixture.detectChanges();
|
||||||
|
tick();
|
||||||
|
|
||||||
|
const input = fixture.debugElement.query(By.css('input'));
|
||||||
|
input.triggerEventHandler('blur', null);
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const validationError = fixture.debugElement.query(By.css('.validation-error'));
|
||||||
|
expect(validationError).toBeFalsy();
|
||||||
|
}));
|
||||||
|
});
|
@@ -0,0 +1,36 @@
|
|||||||
|
import { Component, Optional, Input } from '@angular/core';
|
||||||
|
import { ValueInputComponent } from '../value-input.component';
|
||||||
|
import { ControlContainer, NgForm } from '@angular/forms';
|
||||||
|
import { controlContainerFactory } from '../../../process-form.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the user inputted value of a string parameter
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-string-value-input',
|
||||||
|
templateUrl: './string-value-input.component.html',
|
||||||
|
styleUrls: ['./string-value-input.component.scss'],
|
||||||
|
viewProviders: [ { provide: ControlContainer,
|
||||||
|
useFactory: controlContainerFactory,
|
||||||
|
deps: [[new Optional(), NgForm]] } ]
|
||||||
|
})
|
||||||
|
export class StringValueInputComponent extends ValueInputComponent<string> {
|
||||||
|
/**
|
||||||
|
* The current value of the string
|
||||||
|
*/
|
||||||
|
value: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initial value of the field
|
||||||
|
*/
|
||||||
|
@Input() initialValue;
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.value = this.initialValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(value) {
|
||||||
|
this.value = value;
|
||||||
|
this.updateValue.emit(value)
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,12 @@
|
|||||||
|
import { EventEmitter, Input, Output } from '@angular/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class that represents value input components
|
||||||
|
*/
|
||||||
|
export abstract class ValueInputComponent<T> {
|
||||||
|
@Input() index: number;
|
||||||
|
/**
|
||||||
|
* Used by the subclasses to emit the value when it's updated
|
||||||
|
*/
|
||||||
|
@Output() updateValue: EventEmitter<T> = new EventEmitter<T>()
|
||||||
|
}
|
@@ -0,0 +1,11 @@
|
|||||||
|
<div class="form-group" *ngIf="script">
|
||||||
|
<label>{{'process.new.select-parameters' | translate}}</label>
|
||||||
|
<ds-parameter-select
|
||||||
|
*ngFor="let value of parameterValues; let i = index; let last = last"
|
||||||
|
[parameters]="script.parameters"
|
||||||
|
[parameterValue]="value"
|
||||||
|
[removable]="!last"
|
||||||
|
[index]="i"
|
||||||
|
(removeParameter)="removeParameter(i)"
|
||||||
|
(changeParameter)="updateParameter($event, i)"></ds-parameter-select>
|
||||||
|
</div>
|
@@ -0,0 +1,64 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ProcessParametersComponent } from './process-parameters.component';
|
||||||
|
import { ProcessParameter } from '../../processes/process-parameter.model';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { ParameterSelectComponent } from './parameter-select/parameter-select.component';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { Script } from '../../scripts/script.model';
|
||||||
|
import { ScriptParameter } from '../../scripts/script-parameter.model';
|
||||||
|
import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock';
|
||||||
|
|
||||||
|
describe('ProcessParametersComponent', () => {
|
||||||
|
let component: ProcessParametersComponent;
|
||||||
|
let fixture: ComponentFixture<ProcessParametersComponent>;
|
||||||
|
let parameterValues;
|
||||||
|
let script;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
const param1 = new ScriptParameter();
|
||||||
|
const param2 = new ScriptParameter();
|
||||||
|
script = Object.assign(new Script(), { parameters: [param1, param2] });
|
||||||
|
parameterValues = [
|
||||||
|
Object.assign(new ProcessParameter(), { name: '-a', value: 'bla' }),
|
||||||
|
Object.assign(new ProcessParameter(), { name: '-b', value: '123' }),
|
||||||
|
Object.assign(new ProcessParameter(), { name: '-c', value: 'value' }),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
})],
|
||||||
|
declarations: [ProcessParametersComponent, ParameterSelectComponent],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ProcessParametersComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.script = script;
|
||||||
|
component.parameterValues = parameterValues;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render a ParameterSelectComponent for each parameter value of the component', () => {
|
||||||
|
const selectComponents = fixture.debugElement.queryAll(By.directive(ParameterSelectComponent));
|
||||||
|
expect(selectComponents.length).toBe(parameterValues.length);
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,114 @@
|
|||||||
|
import { Component, EventEmitter, Input, OnChanges, Optional, Output, SimpleChanges } from '@angular/core';
|
||||||
|
import { Script } from '../../scripts/script.model';
|
||||||
|
import { ProcessParameter } from '../../processes/process-parameter.model';
|
||||||
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
|
import { ControlContainer, NgForm } from '@angular/forms';
|
||||||
|
import { ScriptParameter } from '../../scripts/script-parameter.model';
|
||||||
|
import { controlContainerFactory } from '../process-form.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that represents the selected list of parameters for a script
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-process-parameters',
|
||||||
|
templateUrl: './process-parameters.component.html',
|
||||||
|
styleUrls: ['./process-parameters.component.scss'],
|
||||||
|
viewProviders: [{
|
||||||
|
provide: ControlContainer,
|
||||||
|
useFactory: controlContainerFactory,
|
||||||
|
deps: [[new Optional(), NgForm]]
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
export class ProcessParametersComponent implements OnChanges {
|
||||||
|
/**
|
||||||
|
* The currently selected script
|
||||||
|
*/
|
||||||
|
@Input() script: Script;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initial parameters on load
|
||||||
|
*/
|
||||||
|
@Input() initialParams: ProcessParameter[];
|
||||||
|
/**
|
||||||
|
* Emits the parameter values when they're updated
|
||||||
|
*/
|
||||||
|
@Output() updateParameters: EventEmitter<ProcessParameter[]> = new EventEmitter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current parameter values
|
||||||
|
*/
|
||||||
|
parameterValues: ProcessParameter[];
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
if (hasValue(this.initialParams)) {
|
||||||
|
this.parameterValues = this.initialParams;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes sure the parameters are reset when the script changes
|
||||||
|
* @param changes
|
||||||
|
*/
|
||||||
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
|
if (changes.script) {
|
||||||
|
this.initParameters()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Empties the parameter values
|
||||||
|
* Initializes the first parameter value
|
||||||
|
*/
|
||||||
|
initParameters() {
|
||||||
|
if (hasValue(this.initialParams)) {
|
||||||
|
this.parameterValues = this.initialParams;
|
||||||
|
} else {
|
||||||
|
this.parameterValues = [];
|
||||||
|
this.initializeParameter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a single parameter value using its new value and index
|
||||||
|
* Adds a new parameter when the last of the parameter values is changed
|
||||||
|
* @param processParameter The new value of the parameter
|
||||||
|
* @param index The index of the parameter
|
||||||
|
*/
|
||||||
|
updateParameter(processParameter: ProcessParameter, index: number) {
|
||||||
|
this.parameterValues[index] = processParameter;
|
||||||
|
if (index === this.parameterValues.length - 1) {
|
||||||
|
this.addParameter();
|
||||||
|
}
|
||||||
|
this.updateParameters.emit(this.parameterValues.filter((param: ProcessParameter) => hasValue(param.name)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a parameter value from the list
|
||||||
|
* @param index The index of the parameter to remove
|
||||||
|
*/
|
||||||
|
removeParameter(index: number) {
|
||||||
|
this.parameterValues = this.parameterValues.filter((value, i) => i !== index);
|
||||||
|
this.updateParameters.emit(this.parameterValues.filter((param: ProcessParameter) => hasValue(param.name)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes parameter values based on the selected script
|
||||||
|
*/
|
||||||
|
initializeParameter() {
|
||||||
|
if (hasValue(this.script)) {
|
||||||
|
this.parameterValues = this.script.parameters
|
||||||
|
.filter((param) => param.mandatory)
|
||||||
|
.map(
|
||||||
|
(parameter: ScriptParameter) => Object.assign(new ProcessParameter(), { name: parameter.name })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.addParameter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an empty parameter value to the end of the list
|
||||||
|
*/
|
||||||
|
addParameter() {
|
||||||
|
this.parameterValues = [...this.parameterValues, new ProcessParameter()];
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user