mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-08 10:34:15 +00:00
Merge pull request #749 from atmire/Edit-collection-item-template
Edit collection item template
This commit is contained in:
@@ -27,7 +27,8 @@ describe('MetadataFieldFormComponent', () => {
|
||||
/* tslint:disable:no-empty */
|
||||
const registryServiceStub = {
|
||||
getActiveMetadataField: () => observableOf(undefined),
|
||||
createOrUpdateMetadataField: (field: MetadataField) => observableOf(field),
|
||||
createMetadataField: (field: MetadataField) => observableOf(field),
|
||||
updateMetadataField: (field: MetadataField) => observableOf(field),
|
||||
cancelEditMetadataField: () => {},
|
||||
cancelEditMetadataSchema: () => {},
|
||||
clearMetadataFieldRequests: () => observableOf(undefined)
|
||||
@@ -75,7 +76,6 @@ describe('MetadataFieldFormComponent', () => {
|
||||
const scopeNote = 'fakeScopeNote';
|
||||
|
||||
const expected = Object.assign(new MetadataField(), {
|
||||
schema: metadataSchema,
|
||||
element: element,
|
||||
qualifier: qualifier,
|
||||
scopeNote: scopeNote
|
||||
|
@@ -157,19 +157,17 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
||||
this.registryService.getActiveMetadataField().pipe(take(1)).subscribe(
|
||||
(field) => {
|
||||
const values = {
|
||||
schema: this.metadataSchema,
|
||||
element: this.element.value,
|
||||
qualifier: this.qualifier.value,
|
||||
scopeNote: this.scopeNote.value
|
||||
};
|
||||
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);
|
||||
});
|
||||
} else {
|
||||
this.registryService.createOrUpdateMetadataField(Object.assign(new MetadataField(), field, {
|
||||
this.registryService.updateMetadataField(Object.assign(new MetadataField(), field, {
|
||||
id: field.id,
|
||||
schema: this.metadataSchema,
|
||||
element: (values.element ? values.element : field.element),
|
||||
qualifier: (values.qualifier ? values.qualifier : field.qualifier),
|
||||
scopeNote: (values.scopeNote ? values.scopeNote : field.scopeNote)
|
||||
|
@@ -61,7 +61,7 @@ describe('MetadataSchemaComponent', () => {
|
||||
element: 'contributor',
|
||||
qualifier: 'advisor',
|
||||
scopeNote: null,
|
||||
schema: mockSchemasList[0]
|
||||
schema: createSuccessfulRemoteDataObject$(mockSchemasList[0])
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
@@ -73,7 +73,7 @@ describe('MetadataSchemaComponent', () => {
|
||||
element: 'contributor',
|
||||
qualifier: 'author',
|
||||
scopeNote: null,
|
||||
schema: mockSchemasList[0]
|
||||
schema: createSuccessfulRemoteDataObject$(mockSchemasList[0])
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
@@ -85,7 +85,7 @@ describe('MetadataSchemaComponent', () => {
|
||||
element: 'contributor',
|
||||
qualifier: 'editor',
|
||||
scopeNote: 'test scope note',
|
||||
schema: mockSchemasList[1]
|
||||
schema: createSuccessfulRemoteDataObject$(mockSchemasList[1])
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
@@ -97,15 +97,15 @@ describe('MetadataSchemaComponent', () => {
|
||||
element: 'contributor',
|
||||
qualifier: 'illustrator',
|
||||
scopeNote: null,
|
||||
schema: mockSchemasList[1]
|
||||
schema: createSuccessfulRemoteDataObject$(mockSchemasList[1])
|
||||
}
|
||||
];
|
||||
const mockSchemas = createSuccessfulRemoteDataObject$(new PaginatedList(null, mockSchemasList));
|
||||
/* tslint:disable:no-empty */
|
||||
const registryServiceStub = {
|
||||
getMetadataSchemas: () => mockSchemas,
|
||||
getMetadataFieldsBySchema: (schema: MetadataSchema) => createSuccessfulRemoteDataObject$(new PaginatedList(null, mockFieldsList.filter((value) => value.schema === schema))),
|
||||
getMetadataSchemaByName: (schemaName: string) => createSuccessfulRemoteDataObject$(mockSchemasList.filter((value) => value.prefix === schemaName)[0]),
|
||||
getMetadataFieldsBySchema: (schema: MetadataSchema) => createSuccessfulRemoteDataObject$(new PaginatedList(null, mockFieldsList.filter((value) => value.id === 3 || value.id === 4))),
|
||||
getMetadataSchemaByPrefix: (schemaName: string) => createSuccessfulRemoteDataObject$(mockSchemasList.filter((value) => value.prefix === schemaName)[0]),
|
||||
getActiveMetadataField: () => observableOf(undefined),
|
||||
getSelectedMetadataFields: () => observableOf([]),
|
||||
editMetadataField: (schema) => {},
|
||||
|
@@ -17,6 +17,7 @@ import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operato
|
||||
import { toFindListOptions } from '../../../shared/pagination/pagination.utils';
|
||||
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||
import { combineLatest } from 'rxjs/internal/observable/combineLatest';
|
||||
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-metadata-schema',
|
||||
@@ -71,7 +72,7 @@ export class MetadataSchemaComponent implements OnInit {
|
||||
* @param params
|
||||
*/
|
||||
initialize(params) {
|
||||
this.metadataSchema$ = this.registryService.getMetadataSchemaByName(params.schemaName).pipe(getFirstSucceededRemoteDataPayload());
|
||||
this.metadataSchema$ = this.registryService.getMetadataSchemaByPrefix(params.schemaName).pipe(getFirstSucceededRemoteDataPayload());
|
||||
this.updateFields();
|
||||
}
|
||||
|
||||
@@ -91,7 +92,7 @@ export class MetadataSchemaComponent implements OnInit {
|
||||
this.metadataFields$ = combineLatest(this.metadataSchema$, this.needsUpdate$).pipe(
|
||||
switchMap(([schema, update]: [MetadataSchema, boolean]) => {
|
||||
if (update) {
|
||||
return this.registryService.getMetadataFieldsBySchema(schema, toFindListOptions(this.config));
|
||||
return this.registryService.getMetadataFieldsBySchema(schema, toFindListOptions(this.config), followLink('schema'));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
@@ -9,10 +9,13 @@ import { CreateCollectionPageGuard } from './create-collection-page/create-colle
|
||||
import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component';
|
||||
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||
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 { CollectionBreadcrumbResolver } from '../core/breadcrumbs/collection-breadcrumb.resolver';
|
||||
import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service';
|
||||
import { LinkService } from '../core/cache/builders/link.service';
|
||||
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||
|
||||
export const COLLECTION_PARENT_PARAMETER = 'parent';
|
||||
|
||||
@@ -30,6 +33,7 @@ export function getCollectionCreatePath() {
|
||||
|
||||
const COLLECTION_CREATE_PATH = 'create';
|
||||
const COLLECTION_EDIT_PATH = 'edit';
|
||||
const ITEMTEMPLATE_PATH = 'itemtemplate';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -58,6 +62,16 @@ const COLLECTION_EDIT_PATH = 'edit';
|
||||
component: DeleteCollectionPageComponent,
|
||||
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: '',
|
||||
component: CollectionPageComponent,
|
||||
@@ -75,6 +89,7 @@ const COLLECTION_EDIT_PATH = 'edit';
|
||||
],
|
||||
providers: [
|
||||
CollectionPageResolver,
|
||||
ItemTemplatePageResolver,
|
||||
CollectionBreadcrumbResolver,
|
||||
DSOBreadcrumbsService,
|
||||
LinkService,
|
||||
|
@@ -8,6 +8,8 @@ import { CollectionPageRoutingModule } from './collection-page-routing.module';
|
||||
import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component';
|
||||
import { CollectionFormComponent } from './collection-form/collection-form.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 { SearchService } from '../core/shared/search/search.service';
|
||||
import { StatisticsModule } from '../statistics/statistics.module';
|
||||
@@ -17,13 +19,15 @@ import { StatisticsModule } from '../statistics/statistics.module';
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
CollectionPageRoutingModule,
|
||||
StatisticsModule.forRoot()
|
||||
StatisticsModule.forRoot(),
|
||||
EditItemPageModule
|
||||
],
|
||||
declarations: [
|
||||
CollectionPageComponent,
|
||||
CreateCollectionPageComponent,
|
||||
DeleteCollectionPageComponent,
|
||||
CollectionFormComponent,
|
||||
EditItemTemplatePageComponent,
|
||||
CollectionItemMapperComponent
|
||||
],
|
||||
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)"
|
||||
[dso]="(dsoRD$ | async)?.payload"
|
||||
(finish)="navigateToHomePage()"></ds-collection-form>
|
||||
|
@@ -4,16 +4,54 @@ import { SharedModule } from '../../../shared/shared.module';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
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 { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { CollectionMetadataComponent } from './collection-metadata.component';
|
||||
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', () => {
|
||||
let comp: 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(() => {
|
||||
TestBed.configureTestingModule({
|
||||
@@ -21,8 +59,11 @@ describe('CollectionMetadataComponent', () => {
|
||||
declarations: [CollectionMetadataComponent],
|
||||
providers: [
|
||||
{ provide: CollectionDataService, useValue: {} },
|
||||
{ provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: { payload: {} } }) } } },
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }
|
||||
{ provide: ItemTemplateDataService, useValue: itemTemplateServiceStub },
|
||||
{ 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]
|
||||
}).compileComponents();
|
||||
@@ -31,12 +72,51 @@ describe('CollectionMetadataComponent', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CollectionMetadataComponent);
|
||||
comp = fixture.componentInstance;
|
||||
router = (comp as any).router;
|
||||
itemTemplateService = (comp as any).itemTemplateService;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('frontendURL', () => {
|
||||
it('should have the right frontendURL set', () => {
|
||||
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 { CollectionDataService } from '../../../core/data/collection-data.service';
|
||||
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 { 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
|
||||
@@ -17,13 +26,91 @@ export class CollectionMetadataComponent extends ComcolMetadataComponent<Collect
|
||||
protected frontendURL = '/collections/';
|
||||
protected type = Collection.type;
|
||||
|
||||
/**
|
||||
* The collection's item template
|
||||
*/
|
||||
itemTemplateRD$: Observable<RemoteData<Item>>;
|
||||
|
||||
public constructor(
|
||||
protected collectionDataService: CollectionDataService,
|
||||
protected itemTemplateService: ItemTemplateDataService,
|
||||
protected router: Router,
|
||||
protected route: ActivatedRoute,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected translate: TranslateService
|
||||
protected translate: TranslateService,
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected requestService: RequestService
|
||||
) {
|
||||
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 { AbstractTrackableComponent } from '../../../shared/trackable/abstract-trackable.component';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { combineLatest as observableCombineLatest } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-abstract-item-update',
|
||||
@@ -45,8 +46,9 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl
|
||||
* Initialize common properties between item-update components
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.route.parent.data.pipe(map((data) => data.item))
|
||||
.pipe(
|
||||
observableCombineLatest(this.route.data, this.route.parent.data).pipe(
|
||||
map(([data, parentData]) => Object.assign({}, data, parentData)),
|
||||
map((data) => data.item),
|
||||
first(),
|
||||
map((data: RemoteData<Item>) => data.payload)
|
||||
).subscribe((item: Item) => {
|
||||
|
@@ -78,6 +78,9 @@ import { ObjectValuesPipe } from '../../shared/utils/object-values-pipe';
|
||||
providers: [
|
||||
BundleDataService,
|
||||
ObjectValuesPipe
|
||||
],
|
||||
exports: [
|
||||
ItemMetadataComponent
|
||||
]
|
||||
})
|
||||
export class EditItemPageModule {
|
||||
|
@@ -142,6 +142,7 @@ describe('ItemBitstreamsComponent', () => {
|
||||
parent: {
|
||||
data: observableOf({ item: createMockRD(item) })
|
||||
},
|
||||
data: observableOf({}),
|
||||
url: url
|
||||
});
|
||||
bundleService = jasmine.createSpyObj('bundleService', {
|
||||
|
@@ -58,7 +58,7 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges {
|
||||
metadataFieldSuggestions: BehaviorSubject<InputSuggestion[]> = new BehaviorSubject([]);
|
||||
|
||||
constructor(
|
||||
private metadataFieldService: RegistryService,
|
||||
private registryService: RegistryService,
|
||||
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
|
||||
*/
|
||||
update(ngModel?: NgModel) {
|
||||
this.objectUpdatesService.saveChangeFieldUpdate(this.url, this.metadata);
|
||||
this.objectUpdatesService.saveChangeFieldUpdate(this.url, cloneDeep(this.metadata));
|
||||
if (hasValue(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
|
||||
*/
|
||||
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
|
||||
* Then sets all found metadata fields as metadataFieldSuggestions
|
||||
* Ignores fields from metadata schemas "relation" and "relationship"
|
||||
* @param query The query to look for
|
||||
*/
|
||||
findMetadataFieldSuggestions(query: string): void {
|
||||
if (isNotEmpty(query)) {
|
||||
this.metadataFieldService.queryMetadataFields(query).pipe(
|
||||
this.registryService.queryMetadataFields(query).pipe(
|
||||
// getSucceededRemoteData(),
|
||||
take(1),
|
||||
map((data) => data.payload.page)
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<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"
|
||||
(click)="add()"><i
|
||||
class="fas fa-plus"></i>
|
||||
@@ -22,7 +22,7 @@
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
||||
</button>
|
||||
</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>
|
||||
<tr>
|
||||
<th>{{'item.edit.metadata.headers.field' | translate}}</th>
|
||||
@@ -44,14 +44,17 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</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="my-2 float-right">
|
||||
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
||||
<div class="float-right">
|
||||
<button class="btn btn-danger mr-1" *ngIf="!(isReinstatable() | async)"
|
||||
[disabled]="!(hasChanges() | async)"
|
||||
(click)="discard()"><i
|
||||
class="fas fa-times"></i> {{"item.edit.metadata.discard-button" | translate}}
|
||||
</button>
|
||||
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
|
||||
<button class="btn btn-warning mr-1" *ngIf="isReinstatable() | async"
|
||||
(click)="reinstate()"><i
|
||||
class="fas fa-undo-alt"></i> {{"item.edit.metadata.reinstate-button" | translate}}
|
||||
</button>
|
||||
|
@@ -122,6 +122,7 @@ describe('ItemMetadataComponent', () => {
|
||||
commitUpdates: {}
|
||||
});
|
||||
routeStub = {
|
||||
data: observableOf({}),
|
||||
parent: {
|
||||
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 { ItemDataService } from '../../../core/data/item-data.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 { TranslateService } from '@ngx-translate/core';
|
||||
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 { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
|
||||
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({
|
||||
selector: 'ds-item-metadata',
|
||||
@@ -27,6 +30,18 @@ import { MetadataField } from '../../../core/metadata/metadata-field.model';
|
||||
*/
|
||||
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
|
||||
*/
|
||||
@@ -50,6 +65,9 @@ export class ItemMetadataComponent extends AbstractItemUpdateComponent {
|
||||
ngOnInit(): void {
|
||||
super.ngOnInit();
|
||||
this.metadataFields$ = this.findMetadataFields();
|
||||
if (hasNoValue(this.updateService)) {
|
||||
this.updateService = this.itemService;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,20 +106,21 @@ export class ItemMetadataComponent extends AbstractItemUpdateComponent {
|
||||
public submit() {
|
||||
this.isValid().pipe(first()).subscribe((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(
|
||||
first(),
|
||||
switchMap((metadata: MetadatumViewModel[]) => {
|
||||
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()
|
||||
).subscribe(
|
||||
(rd: RemoteData<Item>) => {
|
||||
this.item = rd.payload;
|
||||
this.checkAndFixMetadataUUIDs();
|
||||
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'));
|
||||
}
|
||||
)
|
||||
@@ -121,7 +140,14 @@ export class ItemMetadataComponent extends AbstractItemUpdateComponent {
|
||||
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))
|
||||
});
|
||||
routeStub = {
|
||||
data: observableOf({}),
|
||||
parent: {
|
||||
data: observableOf({ item: new RemoteData(false, false, true, null, item) })
|
||||
}
|
||||
|
@@ -144,6 +144,8 @@ 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 { WorkflowAction } from './tasks/models/workflow-action-object.model';
|
||||
import { ItemTemplateDataService } from './data/item-template-data.service';
|
||||
import { TemplateItem } from './shared/template-item.model';
|
||||
import { Registration } from './shared/registration.model';
|
||||
import { MetadataSchemaDataService } from './data/metadata-schema-data.service';
|
||||
import { MetadataFieldDataService } from './data/metadata-field-data.service';
|
||||
@@ -254,6 +256,7 @@ const PROVIDERS = [
|
||||
BitstreamDataService,
|
||||
EntityTypeService,
|
||||
ContentSourceResponseParsingService,
|
||||
ItemTemplateDataService,
|
||||
SearchService,
|
||||
SidebarService,
|
||||
SearchFilterService,
|
||||
@@ -329,6 +332,7 @@ export const models =
|
||||
Version,
|
||||
VersionHistory,
|
||||
WorkflowAction,
|
||||
TemplateItem,
|
||||
Registration
|
||||
];
|
||||
|
||||
|
@@ -50,9 +50,10 @@ import {
|
||||
import { RequestEntry } from './request.reducer';
|
||||
import { RequestService } from './request.service';
|
||||
import { RestRequestMethod } from './rest-request-method';
|
||||
import { UpdateDataService } from './update-data.service';
|
||||
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 rdbService: RemoteDataBuildService;
|
||||
protected abstract store: Store<CoreState>;
|
||||
@@ -75,6 +76,13 @@ export abstract class DataService<T extends CacheableObject> {
|
||||
* @returns {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);
|
||||
}
|
||||
|
||||
@@ -259,6 +267,16 @@ export abstract class DataService<T extends CacheableObject> {
|
||||
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},
|
||||
* 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
|
||||
*/
|
||||
findById(id: string, ...linksToFollow: Array<FollowLinkConfig<T>>): Observable<RemoteData<T>> {
|
||||
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
|
||||
map((endpoint: string) => this.getIDHref(endpoint, encodeURIComponent(id), ...linksToFollow)));
|
||||
const hrefObs = this.getIDHrefObs(encodeURIComponent(id), ...linksToFollow);
|
||||
|
||||
hrefObs.pipe(
|
||||
find((href: string) => hasValue(href)))
|
||||
@@ -437,7 +454,7 @@ export abstract class DataService<T extends CacheableObject> {
|
||||
*/
|
||||
create(dso: T, ...params: RequestParam[]): Observable<RemoteData<T>> {
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
const endpoint$ = this.halService.getEndpoint(this.linkPath).pipe(
|
||||
const endpoint$ = this.getEndpoint().pipe(
|
||||
isNotEmptyOperator(),
|
||||
distinctUntilChanged(),
|
||||
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 {
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
|
||||
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
|
||||
map((endpoint: string) => this.getIDHref(endpoint, dsoID)));
|
||||
const hrefObs = this.getIDHrefObs(dsoID);
|
||||
|
||||
hrefObs.pipe(
|
||||
find((href: string) => hasValue(href)),
|
||||
|
@@ -55,7 +55,7 @@ export class ItemDataService extends DataService<Item> {
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected store: Store<CoreState>,
|
||||
private bs: BrowseService,
|
||||
protected bs: BrowseService,
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected halService: HALEndpointService,
|
||||
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 { RestResponse } from '../cache/response.models';
|
||||
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 { MetadataField } from '../metadata/metadata-field.model';
|
||||
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
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', () => {
|
||||
it('should remove requests on the data service\'s endpoint', (done) => {
|
||||
metadataFieldService.clearRequests().subscribe(() => {
|
||||
|
@@ -13,14 +13,11 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
|
||||
import { METADATA_FIELD } from '../metadata/metadata-field.resource-type';
|
||||
import { MetadataField } from '../metadata/metadata-field.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 { Observable } from 'rxjs/internal/Observable';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { find, skipWhile, switchMap, tap } from 'rxjs/operators';
|
||||
import { RemoteData } from './remote-data';
|
||||
import { tap } from 'rxjs/operators';
|
||||
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
|
||||
@@ -56,24 +53,6 @@ export class MetadataFieldDataService extends DataService<MetadataField> {
|
||||
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
|
||||
* Used for refreshing lists after adding/updating/removing a metadata field from a metadata schema
|
||||
|
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);
|
||||
}
|
@@ -9,6 +9,9 @@ import { ResourceType } from '../shared/resource-type';
|
||||
import { excludeFromEquals } from '../utilities/equals.decorators';
|
||||
import { METADATA_FIELD } from './metadata-field.resource-type';
|
||||
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
|
||||
@@ -61,16 +64,15 @@ export class MetadataField extends ListableObject implements HALResource {
|
||||
* The MetadataSchema for this MetadataField
|
||||
* 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)
|
||||
schema?: MetadataSchema;
|
||||
@link(METADATA_SCHEMA)
|
||||
schema?: Observable<RemoteData<MetadataSchema>>;
|
||||
|
||||
/**
|
||||
* Method to print this metadata field as a string
|
||||
* @param separator The separator between the schema, element and qualifier in the string
|
||||
*/
|
||||
toString(separator: string = '.'): string {
|
||||
let key = this.schema.prefix + separator + this.element;
|
||||
let key = this.element;
|
||||
if (isNotEmpty(this.qualifier)) {
|
||||
key += separator + this.qualifier;
|
||||
}
|
||||
|
@@ -82,7 +82,7 @@ describe('RegistryService', () => {
|
||||
element: 'contributor',
|
||||
qualifier: 'advisor',
|
||||
scopeNote: null,
|
||||
schema: mockSchemasList[0],
|
||||
schema: createSuccessfulRemoteDataObject$(mockSchemasList[0]),
|
||||
type: MetadataField.type
|
||||
}),
|
||||
Object.assign(new MetadataField(),
|
||||
@@ -94,7 +94,7 @@ describe('RegistryService', () => {
|
||||
element: 'contributor',
|
||||
qualifier: 'author',
|
||||
scopeNote: null,
|
||||
schema: mockSchemasList[0],
|
||||
schema: createSuccessfulRemoteDataObject$(mockSchemasList[0]),
|
||||
type: MetadataField.type
|
||||
}),
|
||||
Object.assign(new MetadataField(),
|
||||
@@ -106,7 +106,7 @@ describe('RegistryService', () => {
|
||||
element: 'contributor',
|
||||
qualifier: 'editor',
|
||||
scopeNote: 'test scope note',
|
||||
schema: mockSchemasList[1],
|
||||
schema: createSuccessfulRemoteDataObject$(mockSchemasList[1]),
|
||||
type: MetadataField.type
|
||||
}),
|
||||
Object.assign(new MetadataField(),
|
||||
@@ -118,7 +118,7 @@ describe('RegistryService', () => {
|
||||
element: 'contributor',
|
||||
qualifier: 'illustrator',
|
||||
scopeNote: null,
|
||||
schema: mockSchemasList[1],
|
||||
schema: createSuccessfulRemoteDataObject$(mockSchemasList[1]),
|
||||
type: MetadataField.type
|
||||
})
|
||||
];
|
||||
@@ -134,7 +134,8 @@ describe('RegistryService', () => {
|
||||
metadataFieldService = jasmine.createSpyObj('metadataFieldService', {
|
||||
findAll: createSuccessfulRemoteDataObject$(createPaginatedList(mockFieldsList)),
|
||||
findById: createSuccessfulRemoteDataObject$(mockFieldsList[0]),
|
||||
createOrUpdateMetadataField: createSuccessfulRemoteDataObject$(mockFieldsList[0]),
|
||||
create: createSuccessfulRemoteDataObject$(mockFieldsList[0]),
|
||||
put: createSuccessfulRemoteDataObject$(mockFieldsList[0]),
|
||||
deleteAndReturnResponse: observableOf(new RestResponse(true, 200, 'OK')),
|
||||
clearRequests: observableOf('href')
|
||||
});
|
||||
@@ -178,7 +179,7 @@ describe('RegistryService', () => {
|
||||
let result;
|
||||
|
||||
beforeEach(() => {
|
||||
result = registryService.getMetadataSchemaByName(mockSchemasList[0].prefix);
|
||||
result = registryService.getMetadataSchemaByPrefix(mockSchemasList[0].prefix);
|
||||
});
|
||||
|
||||
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', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(mockStore, 'dispatch');
|
||||
@@ -325,14 +311,29 @@ describe('RegistryService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when createOrUpdateMetadataField is called', () => {
|
||||
describe('when createMetadataField is called', () => {
|
||||
let result: Observable<MetadataField>;
|
||||
|
||||
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) => {
|
||||
expect(field).toEqual(mockFieldsList[0]);
|
||||
done();
|
||||
|
@@ -2,18 +2,10 @@ import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { PaginatedList } from '../data/paginated-list';
|
||||
import { PageInfo } from '../shared/page-info.model';
|
||||
import { FindListOptions } from '../data/request.models';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import {
|
||||
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 { RestResponse } from '../cache/response.models';
|
||||
import { hasValue, hasValueOperator, isNotEmptyOperator } from '../../shared/empty.util';
|
||||
import { getFirstSucceededRemoteDataPayload } from '../shared/operators';
|
||||
import { createSelector, select, Store } from '@ngrx/store';
|
||||
import { AppState } from '../../app.reducer';
|
||||
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 { MetadataFieldDataService } from '../data/metadata-field-data.service';
|
||||
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 editMetadataSchemaSelector = createSelector(metadataRegistryStateSelector, (metadataState: MetadataRegistryState) => metadataState.editSchema);
|
||||
@@ -68,11 +62,11 @@ export class RegistryService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a metadata schema by its name
|
||||
* @param schemaName The name of the schema to find
|
||||
* Retrieves a metadata schema by its prefix
|
||||
* @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
|
||||
*/
|
||||
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
|
||||
const options: FindListOptions = Object.assign(new FindListOptions(), {
|
||||
elementsPerPage: 10000
|
||||
@@ -81,7 +75,7 @@ export class RegistryService {
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
map((schemas: PaginatedList<MetadataSchema>) => schemas.page),
|
||||
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))
|
||||
);
|
||||
}
|
||||
@@ -103,11 +97,11 @@ export class RegistryService {
|
||||
* @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
|
||||
*/
|
||||
// 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>>> {
|
||||
if (hasNoValue(options)) {
|
||||
options = {currentPage: 1, elementsPerPage: 10000} as any;
|
||||
}
|
||||
return this.metadataFieldService.findAll(options, ...linksToFollow);
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList<MetadataField>(null, []));
|
||||
}
|
||||
|
||||
public editMetadataSchema(schema: MetadataSchema) {
|
||||
@@ -240,21 +234,32 @@ export class RegistryService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Create a MetadataField
|
||||
*
|
||||
* @param field The MetadataField to create
|
||||
* @param schema The MetadataSchema to create the field in
|
||||
*/
|
||||
public createOrUpdateMetadataField(field: MetadataField): Observable<MetadataField> {
|
||||
const isUpdate = hasValue(field.id);
|
||||
return this.metadataFieldService.createOrUpdateMetadataField(field).pipe(
|
||||
public createMetadataField(field: MetadataField, schema: MetadataSchema): Observable<MetadataField> {
|
||||
return this.metadataFieldService.create(field, new RequestParam('schemaId', schema.id)).pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
hasValueOperator(),
|
||||
tap(() => {
|
||||
const fieldString = `${field.schema.prefix}.${field.element}${field.qualifier ? `.${field.qualifier}` : ''}`;
|
||||
this.showNotifications(true, isUpdate, true, {field: fieldString});
|
||||
this.showNotifications(true, false, true, {field: field.toString()});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @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>>> {
|
||||
return this.getAllMetadataFields().pipe(
|
||||
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 });
|
||||
})
|
||||
);
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList<MetadataField>(null, []));
|
||||
}
|
||||
}
|
||||
|
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');
|
@@ -4,7 +4,7 @@
|
||||
<label>{{type.value + '.edit.logo.label' | translate}}</label>
|
||||
</div>
|
||||
<ng-container *ngVar="(dso?.logo | async)?.payload as logo">
|
||||
<div class="col-12 d-inline-block alert" [ngClass]="{'alert-danger': markLogoForDeletion}" id="logo-section">
|
||||
<div class="col-12 d-inline-block alert" [ngClass]="{'alert-danger': markLogoForDeletion}" id="logo-section" *ngIf="logo">
|
||||
<div class="row">
|
||||
<div class="col-8 d-inline-block">
|
||||
<ds-comcol-page-logo [logo]="logo"></ds-comcol-page-logo>
|
||||
|
@@ -682,6 +682,28 @@
|
||||
|
||||
|
||||
|
||||
"collection.edit.template.add-button": "Add",
|
||||
|
||||
"collection.edit.template.breadcrumbs": "Item template",
|
||||
|
||||
"collection.edit.template.cancel": "Cancel",
|
||||
|
||||
"collection.edit.template.delete-button": "Delete",
|
||||
|
||||
"collection.edit.template.edit-button": "Edit",
|
||||
|
||||
"collection.edit.template.head": "Edit Template Item for Collection \"{{ collection }}\"",
|
||||
|
||||
"collection.edit.template.label": "Template item",
|
||||
|
||||
"collection.edit.template.notifications.delete.error": "Failed to delete the item template",
|
||||
|
||||
"collection.edit.template.notifications.delete.success": "Successfully deleted the item template",
|
||||
|
||||
"collection.edit.template.title": "Edit Template Item",
|
||||
|
||||
|
||||
|
||||
"collection.form.abstract": "Short Description",
|
||||
|
||||
"collection.form.description": "Introductory text (HTML)",
|
||||
@@ -1224,6 +1246,8 @@
|
||||
|
||||
"item.edit.metadata.edit.buttons.unedit": "Stop editing",
|
||||
|
||||
"item.edit.metadata.empty": "The item currently doesn't contain any metadata. Click Add to start adding a metadata value.",
|
||||
|
||||
"item.edit.metadata.headers.edit": "Edit",
|
||||
|
||||
"item.edit.metadata.headers.field": "Field",
|
||||
|
Reference in New Issue
Block a user