From 12be9101e582f77f0b99be82d69d115776ab41bd Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 3 Oct 2019 13:16:46 +0200 Subject: [PATCH 01/84] 65272: ItemTemplateDataService --- src/app/core/core.module.ts | 2 + src/app/core/data/data.service.ts | 29 +++-- src/app/core/data/item-data.service.ts | 2 +- .../core/data/item-template-data.service.ts | 107 ++++++++++++++++++ src/app/core/data/update-data.service.ts | 8 ++ src/app/core/shared/hal-endpoint.service.ts | 4 +- 6 files changed, 142 insertions(+), 10 deletions(-) create mode 100644 src/app/core/data/item-template-data.service.ts create mode 100644 src/app/core/data/update-data.service.ts diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index f4d4dcb269..6571d26c88 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -119,6 +119,7 @@ import { MetadatafieldParsingService } from './data/metadatafield-parsing.servic import { NormalizedSubmissionUploadsModel } from './config/models/normalized-config-submission-uploads.model'; import { NormalizedBrowseEntry } from './shared/normalized-browse-entry.model'; import { BrowseDefinition } from './shared/browse-definition.model'; +import { ItemTemplateDataService } from './data/item-template-data.service'; const IMPORTS = [ CommonModule, @@ -207,6 +208,7 @@ const PROVIDERS = [ TaskResponseParsingService, ClaimedTaskDataService, PoolTaskDataService, + ItemTemplateDataService, // register AuthInterceptor as HttpInterceptor { provide: HTTP_INTERCEPTORS, diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index ad0db51980..20f727d627 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -37,8 +37,9 @@ import { NormalizedObjectBuildService } from '../cache/builders/normalized-objec import { ChangeAnalyzer } from './change-analyzer'; import { RestRequestMethod } from './rest-request-method'; import { getMapsToType } from '../cache/builders/build-decorators'; +import { UpdateDataService } from './update-data.service'; -export abstract class DataService { +export abstract class DataService implements UpdateDataService { protected abstract requestService: RequestService; protected abstract rdbService: RemoteDataBuildService; protected abstract dataBuildService: NormalizedObjectBuildService; @@ -53,6 +54,13 @@ export abstract class DataService { public abstract getBrowseEndpoint(options: FindAllOptions, linkPath?: string): Observable + /** + * Get the base endpoint for all requests + */ + protected getEndpoint(): Observable { + return this.halService.getEndpoint(this.linkPath); + } + /** * Create the HREF with given options object * @@ -146,9 +154,17 @@ export abstract class DataService { return `${endpoint}/${resourceID}`; } + /** + * Create an observable for the HREF of a specific object based on its identifier + * @param resourceID The identifier for the object + */ + getIDHrefObs(resourceID: string): Observable { + return this.getEndpoint().pipe( + map((endpoint: string) => this.getIDHref(endpoint, resourceID))); + } + findById(id: string): Observable> { - const hrefObs = this.halService.getEndpoint(this.linkPath).pipe( - map((endpoint: string) => this.getIDHref(endpoint, id))); + const hrefObs = this.getIDHrefObs(id); hrefObs.pipe( find((href: string) => hasValue(href))) @@ -234,9 +250,9 @@ export abstract class DataService { * @param {string} parentUUID * The UUID of the parent to create the new object under */ - create(dso: T, parentUUID: string): Observable> { + create(dso: T, parentUUID?: string): Observable> { const requestId = this.requestService.generateRequestId(); - const endpoint$ = this.halService.getEndpoint(this.linkPath).pipe( + const endpoint$ = this.getEndpoint().pipe( isNotEmptyOperator(), distinctUntilChanged(), map((endpoint: string) => parentUUID ? `${endpoint}?parent=${parentUUID}` : endpoint) @@ -286,8 +302,7 @@ export abstract class DataService { delete(dso: T): Observable { const requestId = this.requestService.generateRequestId(); - const hrefObs = this.halService.getEndpoint(this.linkPath).pipe( - map((endpoint: string) => this.getIDHref(endpoint, dso.uuid))); + const hrefObs = this.getIDHrefObs(dso.uuid); hrefObs.pipe( find((href: string) => hasValue(href)), diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 07d8ed8405..2777b14732 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -34,7 +34,7 @@ export class ItemDataService extends DataService { protected rdbService: RemoteDataBuildService, protected dataBuildService: NormalizedObjectBuildService, protected store: Store, - private bs: BrowseService, + protected bs: BrowseService, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, protected notificationsService: NotificationsService, diff --git a/src/app/core/data/item-template-data.service.ts b/src/app/core/data/item-template-data.service.ts new file mode 100644 index 0000000000..f13a79893c --- /dev/null +++ b/src/app/core/data/item-template-data.service.ts @@ -0,0 +1,107 @@ +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 { NormalizedObjectBuildService } from '../cache/builders/normalized-object-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'; + +/* tslint:disable:max-classes-per-file */ +class DataServiceImpl extends ItemDataService { + protected linkPath = 'itemtemplate'; + + private collectionID: string; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected dataBuildService: NormalizedObjectBuildService, + protected store: Store, + protected bs: BrowseService, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DSOChangeAnalyzer, + protected collectionService: CollectionDataService) { + super(requestService, rdbService, dataBuildService, store, bs, objectCache, halService, notificationsService, http, comparator); + } + + protected getEndpoint(): Observable { + return this.collectionService.getIDHrefObs(this.collectionID).pipe( + switchMap((href: string) => this.halService.getEndpoint(this.linkPath, href)) + ); + } + + getIDHrefObs(resourceID: string): Observable { + return this.getEndpoint(); + } + + findByCollectionID(collectionID: string): Observable> { + this.collectionID = collectionID; + return super.findById(collectionID); + } + + create(item: Item, collectionID: string): Observable> { + this.collectionID = collectionID; + return super.create(item); + } + + deleteByCollectionID(item: Item, collectionID: string): Observable { + this.collectionID = collectionID; + return super.delete(item); + } +} + +@Injectable() +export class ItemTemplateDataService implements UpdateDataService { + private dataService: DataServiceImpl; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected dataBuildService: NormalizedObjectBuildService, + protected store: Store, + protected bs: BrowseService, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DSOChangeAnalyzer, + protected collectionService: CollectionDataService) { + this.dataService = new DataServiceImpl(requestService, rdbService, dataBuildService, store, bs, objectCache, halService, notificationsService, http, comparator, collectionService); + } + + commitUpdates(method?: RestRequestMethod) { + this.dataService.commitUpdates(method); + } + + update(object: Item): Observable> { + return this.dataService.update(object); + } + + findByCollectionID(collectionID: string): Observable> { + return this.dataService.findByCollectionID(collectionID); + } + + create(item: Item, collectionID: string): Observable> { + return this.dataService.create(item, collectionID); + } + + deleteByCollectionID(item: Item, collectionID: string): Observable { + return this.dataService.deleteByCollectionID(item, collectionID); + } +} diff --git a/src/app/core/data/update-data.service.ts b/src/app/core/data/update-data.service.ts new file mode 100644 index 0000000000..d0527f6fd2 --- /dev/null +++ b/src/app/core/data/update-data.service.ts @@ -0,0 +1,8 @@ +import { Observable } from 'rxjs/internal/Observable'; +import { RemoteData } from './remote-data'; +import { RestRequestMethod } from './rest-request-method'; + +export interface UpdateDataService { + update(object: T): Observable>; + commitUpdates(method?: RestRequestMethod); +} diff --git a/src/app/core/shared/hal-endpoint.service.ts b/src/app/core/shared/hal-endpoint.service.ts index a93d54db64..117cc074ca 100644 --- a/src/app/core/shared/hal-endpoint.service.ts +++ b/src/app/core/shared/hal-endpoint.service.ts @@ -43,8 +43,8 @@ export class HALEndpointService { ); } - public getEndpoint(linkPath: string): Observable { - return this.getEndpointAt(this.getRootHref(), ...linkPath.split('/')); + public getEndpoint(linkPath: string, startHref?: string): Observable { + return this.getEndpointAt(startHref || this.getRootHref(), ...linkPath.split('/')); } /** From 143604fa4dded5d2caa50ad6ece962eafc8ef0a2 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 3 Oct 2019 15:07:05 +0200 Subject: [PATCH 02/84] 65272: Intermediate commit --- resources/i18n/en.json5 | 1 + .../collection-metadata.component.html | 3 +++ .../collection-metadata.component.ts | 22 +++++++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index 74fbb42a2d..3b71ff709a 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -139,6 +139,7 @@ "collection.edit.tabs.roles.title": "Collection Edit - Roles", "collection.edit.tabs.source.head": "Content Source", "collection.edit.tabs.source.title": "Collection Edit - Content Source", + "collection.edit.template.label": "Item template", "collection.form.abstract": "Short Description", "collection.form.description": "Introductory text (HTML)", "collection.form.errors.title.required": "Please enter a collection name", diff --git a/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html b/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html index cc7a0d5de0..b14b034842 100644 --- a/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html +++ b/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html @@ -1,3 +1,6 @@ +
+ +
{{'collection.edit.delete' diff --git a/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts b/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts index 3a9d9c8af5..7ba692ef2b 100644 --- a/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts +++ b/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts @@ -3,6 +3,12 @@ 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 } from 'rxjs/operators'; /** * Component for editing a collection's metadata @@ -14,11 +20,27 @@ import { ActivatedRoute, Router } from '@angular/router'; export class CollectionMetadataComponent extends ComcolMetadataComponent { protected frontendURL = '/collections/'; + /** + * The collection's template item + */ + templateItemRD$: Observable>; + public constructor( protected collectionDataService: CollectionDataService, + protected itemTemplateService: ItemTemplateDataService, protected router: Router, protected route: ActivatedRoute ) { super(collectionDataService, router, route); } + + ngOnInit(): void { + super.ngOnInit(); + + this.templateItemRD$ = this.dsoRD$.pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + switchMap((collection: Collection) => this.itemTemplateService.findByCollectionID(collection.uuid)) + ); + } } From cddaf539aad4e0f7c2dba4bf4133d80368dea4ce Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 3 Oct 2019 16:36:07 +0200 Subject: [PATCH 03/84] 65272: add, delete and edit button for collection item template --- resources/i18n/en.json5 | 5 ++ .../collection-metadata.component.html | 17 +++++- .../collection-metadata.component.ts | 57 +++++++++++++++++-- 3 files changed, 74 insertions(+), 5 deletions(-) diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index 3b71ff709a..9cb30fe3a2 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -139,7 +139,12 @@ "collection.edit.tabs.roles.title": "Collection Edit - Roles", "collection.edit.tabs.source.head": "Content Source", "collection.edit.tabs.source.title": "Collection Edit - Content Source", + "collection.edit.template.add-button": "Add", + "collection.edit.template.delete-button": "Delete", + "collection.edit.template.edit-button": "Edit", "collection.edit.template.label": "Item template", + "collections.edit.template.notifications.delete.error": "Failed to delete the item template", + "collections.edit.template.notifications.delete.success": "Successfully deleted the item template", "collection.form.abstract": "Short Description", "collection.form.description": "Introductory text (HTML)", "collection.form.errors.title.required": "Please enter a collection name", diff --git a/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html b/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html index b14b034842..d627f7e8ef 100644 --- a/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html +++ b/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html @@ -1,5 +1,20 @@ -
+
+
+ + + +
>; + itemTemplateRD$: Observable>; public constructor( protected collectionDataService: CollectionDataService, protected itemTemplateService: ItemTemplateDataService, protected router: Router, - protected route: ActivatedRoute + protected route: ActivatedRoute, + protected notificationsService: NotificationsService ) { super(collectionDataService, router, route); } ngOnInit(): void { super.ngOnInit(); + this.initTemplateItem(); + } - this.templateItemRD$ = this.dsoRD$.pipe( + initTemplateItem() { + this.itemTemplateRD$ = this.dsoRD$.pipe( getSucceededRemoteData(), getRemoteDataPayload(), switchMap((collection: Collection) => this.itemTemplateService.findByCollectionID(collection.uuid)) ); } + + 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']); + }); + } + + 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]) => this.itemTemplateService.deleteByCollectionID(template, collection.uuid)) + ).subscribe((success: boolean) => { + if (success) { + this.notificationsService.success(null, 'collections.edit.template.notifications.delete.success'); + this.initTemplateItem(); + } else { + this.notificationsService.error(null, 'collections.edit.template.notifications.delete.error'); + } + }); + } } From 4cc1f55272e0637538dde5c08a563b865cce8c6c Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 3 Oct 2019 17:50:48 +0200 Subject: [PATCH 04/84] 65272: EditItemTemplatePageComponent --- .../collection-page-routing.module.ts | 15 ++++++++++- .../collection-page.module.ts | 4 ++- .../edit-item-template-page.component.html | 2 ++ .../edit-item-template-page.component.ts | 25 +++++++++++++++++ .../item-template-page.resolver.ts | 27 +++++++++++++++++++ 5 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 src/app/+collection-page/edit-item-template-page/edit-item-template-page.component.html create mode 100644 src/app/+collection-page/edit-item-template-page/edit-item-template-page.component.ts create mode 100644 src/app/+collection-page/edit-item-template-page/item-template-page.resolver.ts diff --git a/src/app/+collection-page/collection-page-routing.module.ts b/src/app/+collection-page/collection-page-routing.module.ts index ad142e8fcf..2ad0346da5 100644 --- a/src/app/+collection-page/collection-page-routing.module.ts +++ b/src/app/+collection-page/collection-page-routing.module.ts @@ -9,6 +9,8 @@ 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'; export const COLLECTION_PARENT_PARAMETER = 'parent'; @@ -26,6 +28,7 @@ export function getCollectionCreatePath() { const COLLECTION_CREATE_PATH = 'create'; const COLLECTION_EDIT_PATH = ':id/edit'; +const ITEMTEMPLATE_PATH = ':id/itemtemplate'; @NgModule({ imports: [ @@ -49,6 +52,15 @@ const COLLECTION_EDIT_PATH = ':id/edit'; dso: CollectionPageResolver } }, + { + path: ITEMTEMPLATE_PATH, + component: EditItemTemplatePageComponent, + canActivate: [AuthenticatedGuard], + resolve: { + collection: CollectionPageResolver, + item: ItemTemplatePageResolver + } + }, { path: ':id', component: CollectionPageComponent, @@ -61,7 +73,8 @@ const COLLECTION_EDIT_PATH = ':id/edit'; ], providers: [ CollectionPageResolver, - CreateCollectionPageGuard + CreateCollectionPageGuard, + ItemTemplatePageResolver ] }) export class CollectionPageRoutingModule { diff --git a/src/app/+collection-page/collection-page.module.ts b/src/app/+collection-page/collection-page.module.ts index f7059deda0..d9cc0b1b14 100644 --- a/src/app/+collection-page/collection-page.module.ts +++ b/src/app/+collection-page/collection-page.module.ts @@ -9,6 +9,7 @@ import { CreateCollectionPageComponent } from './create-collection-page/create-c import { CollectionFormComponent } from './collection-form/collection-form.component'; import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component'; import { SearchService } from '../+search-page/search-service/search.service'; +import { EditItemTemplatePageComponent } from './edit-item-template-page/edit-item-template-page.component'; @NgModule({ imports: [ @@ -20,7 +21,8 @@ import { SearchService } from '../+search-page/search-service/search.service'; CollectionPageComponent, CreateCollectionPageComponent, DeleteCollectionPageComponent, - CollectionFormComponent + CollectionFormComponent, + EditItemTemplatePageComponent ], exports: [ CollectionFormComponent diff --git a/src/app/+collection-page/edit-item-template-page/edit-item-template-page.component.html b/src/app/+collection-page/edit-item-template-page/edit-item-template-page.component.html new file mode 100644 index 0000000000..11c51c5a2c --- /dev/null +++ b/src/app/+collection-page/edit-item-template-page/edit-item-template-page.component.html @@ -0,0 +1,2 @@ + + diff --git a/src/app/+collection-page/edit-item-template-page/edit-item-template-page.component.ts b/src/app/+collection-page/edit-item-template-page/edit-item-template-page.component.ts new file mode 100644 index 0000000000..38ef0439b5 --- /dev/null +++ b/src/app/+collection-page/edit-item-template-page/edit-item-template-page.component.ts @@ -0,0 +1,25 @@ +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 { Item } from '../../core/shared/item.model'; +import { ActivatedRoute } from '@angular/router'; +import { first, map } from 'rxjs/operators'; + +@Component({ + selector: 'ds-edit-item-template-page', + templateUrl: './edit-item-template-page.component.html', +}) +export class EditItemTemplatePageComponent implements OnInit { + collectionRD$: Observable>; + itemRD$: Observable>; + + constructor(protected route: ActivatedRoute) { + } + + ngOnInit(): void { + this.collectionRD$ = this.route.parent.data.pipe(first(), map((data) => data.collection)); + this.itemRD$ = this.route.parent.data.pipe(first(), map((data) => data.item)); + } + +} diff --git a/src/app/+collection-page/edit-item-template-page/item-template-page.resolver.ts b/src/app/+collection-page/edit-item-template-page/item-template-page.resolver.ts new file mode 100644 index 0000000000..dc2b3dc209 --- /dev/null +++ b/src/app/+collection-page/edit-item-template-page/item-template-page.resolver.ts @@ -0,0 +1,27 @@ +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'; + +@Injectable() +export class ItemTemplatePageResolver implements Resolve> { + constructor(private itemTemplateService: ItemTemplateDataService) { + } + + /** + * Method for resolving a collection's template item based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @returns Observable<> Emits the found template item based on the parameters in the current route, + * or an error if something went wrong + */ + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { + return this.itemTemplateService.findByCollectionID(route.params.id).pipe( + find((RD) => hasValue(RD.error) || RD.hasSucceeded), + ); + } +} From ba436a6467d7640d426faccce51ad389ae6920d8 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 4 Oct 2019 11:46:14 +0200 Subject: [PATCH 05/84] 65272: Functional EditItemTemplatePageComponent --- resources/i18n/en.json5 | 6 ++++-- .../+collection-page/collection-page.module.ts | 2 ++ .../collection-metadata.component.ts | 9 +++++---- .../edit-item-template-page.component.html | 11 +++++++++-- .../edit-item-template-page.component.ts | 14 +++++++++----- .../abstract-item-update.component.ts | 12 +++++++----- .../edit-item-page/edit-item-page.module.ts | 3 +++ .../item-metadata/item-metadata.component.ts | 17 ++++++++++++++--- src/app/core/data/item-template-data.service.ts | 13 +------------ 9 files changed, 54 insertions(+), 33 deletions(-) diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index 9cb30fe3a2..cf9a03bc13 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -140,11 +140,13 @@ "collection.edit.tabs.source.head": "Content Source", "collection.edit.tabs.source.title": "Collection Edit - Content Source", "collection.edit.template.add-button": "Add", + "collection.edit.template.cancel": "Cancel", "collection.edit.template.delete-button": "Delete", "collection.edit.template.edit-button": "Edit", "collection.edit.template.label": "Item template", - "collections.edit.template.notifications.delete.error": "Failed to delete the item template", - "collections.edit.template.notifications.delete.success": "Successfully deleted the item template", + "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 for Collection \"{{ collection }}\"", "collection.form.abstract": "Short Description", "collection.form.description": "Introductory text (HTML)", "collection.form.errors.title.required": "Please enter a collection name", diff --git a/src/app/+collection-page/collection-page.module.ts b/src/app/+collection-page/collection-page.module.ts index d9cc0b1b14..79f5379771 100644 --- a/src/app/+collection-page/collection-page.module.ts +++ b/src/app/+collection-page/collection-page.module.ts @@ -10,11 +10,13 @@ import { CollectionFormComponent } from './collection-form/collection-form.compo import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component'; import { SearchService } from '../+search-page/search-service/search.service'; import { EditItemTemplatePageComponent } from './edit-item-template-page/edit-item-template-page.component'; +import { EditItemPageModule } from '../+item-page/edit-item-page/edit-item-page.module'; @NgModule({ imports: [ CommonModule, SharedModule, + EditItemPageModule, CollectionPageRoutingModule ], declarations: [ diff --git a/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts b/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts index 6bafc9f4e7..22abdb3336 100644 --- a/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts +++ b/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts @@ -11,6 +11,7 @@ import { getRemoteDataPayload, getSucceededRemoteData } from '../../../core/shar 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'; /** * Component for editing a collection's metadata @@ -32,7 +33,8 @@ export class CollectionMetadataComponent extends ComcolMetadataComponent this.itemTemplateService.deleteByCollectionID(template, collection.uuid)) ).subscribe((success: boolean) => { if (success) { - this.notificationsService.success(null, 'collections.edit.template.notifications.delete.success'); - this.initTemplateItem(); + this.notificationsService.success(null, this.translate.get('collection.edit.template.notifications.delete.success')); } else { - this.notificationsService.error(null, 'collections.edit.template.notifications.delete.error'); + this.notificationsService.error(null, this.translate.get('collection.edit.template.notifications.delete.error')); } }); } diff --git a/src/app/+collection-page/edit-item-template-page/edit-item-template-page.component.html b/src/app/+collection-page/edit-item-template-page/edit-item-template-page.component.html index 11c51c5a2c..d74cd26d8c 100644 --- a/src/app/+collection-page/edit-item-template-page/edit-item-template-page.component.html +++ b/src/app/+collection-page/edit-item-template-page/edit-item-template-page.component.html @@ -1,2 +1,9 @@ - - +
+
+
+

{{ 'collection.edit.template.title' | translate:{ collection: collection?.name } }}

+ + +
+
+
diff --git a/src/app/+collection-page/edit-item-template-page/edit-item-template-page.component.ts b/src/app/+collection-page/edit-item-template-page/edit-item-template-page.component.ts index 38ef0439b5..214421d448 100644 --- a/src/app/+collection-page/edit-item-template-page/edit-item-template-page.component.ts +++ b/src/app/+collection-page/edit-item-template-page/edit-item-template-page.component.ts @@ -2,9 +2,10 @@ 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 { Item } from '../../core/shared/item.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', @@ -12,14 +13,17 @@ import { first, map } from 'rxjs/operators'; }) export class EditItemTemplatePageComponent implements OnInit { collectionRD$: Observable>; - itemRD$: Observable>; - constructor(protected route: ActivatedRoute) { + constructor(protected route: ActivatedRoute, + protected itemTemplateService: ItemTemplateDataService) { } ngOnInit(): void { - this.collectionRD$ = this.route.parent.data.pipe(first(), map((data) => data.collection)); - this.itemRD$ = this.route.parent.data.pipe(first(), map((data) => data.item)); + this.collectionRD$ = this.route.data.pipe(first(), map((data) => data.collection)); + } + + getCollectionEditUrl(collection: Collection): string { + return getCollectionEditPath(collection.uuid); } } diff --git a/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts b/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts index c49def3dd2..6cfed8ffdf 100644 --- a/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts +++ b/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts @@ -10,6 +10,7 @@ import { TranslateService } from '@ngx-translate/core'; import { GLOBAL_CONFIG, GlobalConfig } from '../../../../config'; import { first, map } from 'rxjs/operators'; import { RemoteData } from '../../../core/data/remote-data'; +import { combineLatest as observableCombineLatest } from 'rxjs'; @Injectable() /** @@ -55,11 +56,12 @@ export abstract class AbstractItemUpdateComponent implements OnInit { * Initialize common properties between item-update components */ ngOnInit(): void { - this.route.parent.data.pipe(map((data) => data.item)) - .pipe( - first(), - map((data: RemoteData) => data.payload) - ).subscribe((item: Item) => { + observableCombineLatest(this.route.data, this.route.parent.data).pipe( + map(([data, parentData]) => Object.assign(data, parentData)), + map((data) => data.item), + first(), + map((data: RemoteData) => data.payload) + ).subscribe((item: Item) => { this.item = item; }); diff --git a/src/app/+item-page/edit-item-page/edit-item-page.module.ts b/src/app/+item-page/edit-item-page/edit-item-page.module.ts index 236388109e..651d0ca3d4 100644 --- a/src/app/+item-page/edit-item-page/edit-item-page.module.ts +++ b/src/app/+item-page/edit-item-page/edit-item-page.module.ts @@ -47,6 +47,9 @@ import { ItemMoveComponent } from './item-move/item-move.component'; EditRelationshipComponent, EditRelationshipListComponent, ItemMoveComponent, + ], + exports: [ + ItemMetadataComponent ] }) export class EditItemPageModule { diff --git a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts index be657d71dc..74282ab54c 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts @@ -1,4 +1,4 @@ -import { Component, Inject } from '@angular/core'; +import { Component, Inject, 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'; @@ -19,6 +19,8 @@ import { 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 } from '../../../shared/empty.util'; @Component({ selector: 'ds-item-metadata', @@ -30,6 +32,12 @@ import { MetadataField } from '../../../core/metadata/metadata-field.model'; */ export class ItemMetadataComponent extends AbstractItemUpdateComponent { + /** + * A custom update service to use for adding and committing patches + * This will default to the ItemDataService + */ + @Input() updateService: UpdateDataService; + /** * Observable with a list of strings with all existing metadata field keys */ @@ -54,6 +62,9 @@ export class ItemMetadataComponent extends AbstractItemUpdateComponent { ngOnInit(): void { super.ngOnInit(); this.metadataFields$ = this.findMetadataFields(); + if (hasNoValue(this.updateService)) { + this.updateService = this.itemService; + } } /** @@ -97,9 +108,9 @@ export class ItemMetadataComponent extends AbstractItemUpdateComponent { 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) => { diff --git a/src/app/core/data/item-template-data.service.ts b/src/app/core/data/item-template-data.service.ts index f13a79893c..a6e689e360 100644 --- a/src/app/core/data/item-template-data.service.ts +++ b/src/app/core/data/item-template-data.service.ts @@ -21,7 +21,6 @@ import { switchMap } from 'rxjs/operators'; /* tslint:disable:max-classes-per-file */ class DataServiceImpl extends ItemDataService { - protected linkPath = 'itemtemplate'; private collectionID: string; @@ -40,19 +39,9 @@ class DataServiceImpl extends ItemDataService { super(requestService, rdbService, dataBuildService, store, bs, objectCache, halService, notificationsService, http, comparator); } - protected getEndpoint(): Observable { - return this.collectionService.getIDHrefObs(this.collectionID).pipe( - switchMap((href: string) => this.halService.getEndpoint(this.linkPath, href)) - ); - } - - getIDHrefObs(resourceID: string): Observable { - return this.getEndpoint(); - } - findByCollectionID(collectionID: string): Observable> { this.collectionID = collectionID; - return super.findById(collectionID); + return super.findById('961e137c-d815-4ade-aff1-0bb12f1fe965'); } create(item: Item, collectionID: string): Observable> { From cf8bba711280b5a0932ffb24c014a437fdba1f16 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 4 Oct 2019 12:38:47 +0200 Subject: [PATCH 06/84] 65272: Revert testing changes --- src/app/core/data/item-template-data.service.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/app/core/data/item-template-data.service.ts b/src/app/core/data/item-template-data.service.ts index a6e689e360..f13a79893c 100644 --- a/src/app/core/data/item-template-data.service.ts +++ b/src/app/core/data/item-template-data.service.ts @@ -21,6 +21,7 @@ import { switchMap } from 'rxjs/operators'; /* tslint:disable:max-classes-per-file */ class DataServiceImpl extends ItemDataService { + protected linkPath = 'itemtemplate'; private collectionID: string; @@ -39,9 +40,19 @@ class DataServiceImpl extends ItemDataService { super(requestService, rdbService, dataBuildService, store, bs, objectCache, halService, notificationsService, http, comparator); } + protected getEndpoint(): Observable { + return this.collectionService.getIDHrefObs(this.collectionID).pipe( + switchMap((href: string) => this.halService.getEndpoint(this.linkPath, href)) + ); + } + + getIDHrefObs(resourceID: string): Observable { + return this.getEndpoint(); + } + findByCollectionID(collectionID: string): Observable> { this.collectionID = collectionID; - return super.findById('961e137c-d815-4ade-aff1-0bb12f1fe965'); + return super.findById(collectionID); } create(item: Item, collectionID: string): Observable> { From 644ab2cfabc08721033c806307c9cd820be82900 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 4 Oct 2019 13:05:15 +0200 Subject: [PATCH 07/84] 65272: JSDocs --- .../collection-metadata.component.ts | 11 +++- .../edit-item-template-page.component.ts | 11 ++++ .../item-template-page.resolver.ts | 7 ++- .../core/data/item-template-data.service.ts | 55 +++++++++++++++++++ src/app/core/data/update-data.service.ts | 3 + 5 files changed, 84 insertions(+), 3 deletions(-) diff --git a/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts b/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts index 22abdb3336..a46c4189bb 100644 --- a/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts +++ b/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts @@ -24,7 +24,7 @@ export class CollectionMetadataComponent extends ComcolMetadataComponent>; @@ -44,6 +44,9 @@ export class CollectionMetadataComponent extends ComcolMetadataComponent>; constructor(protected route: ActivatedRoute, @@ -22,6 +29,10 @@ export class EditItemTemplatePageComponent implements OnInit { this.collectionRD$ = this.route.data.pipe(first(), map((data) => data.collection)); } + /** + * Get the URL to the collection's edit page + * @param collection + */ getCollectionEditUrl(collection: Collection): string { return getCollectionEditPath(collection.uuid); } diff --git a/src/app/+collection-page/edit-item-template-page/item-template-page.resolver.ts b/src/app/+collection-page/edit-item-template-page/item-template-page.resolver.ts index dc2b3dc209..07ccfd9eee 100644 --- a/src/app/+collection-page/edit-item-template-page/item-template-page.resolver.ts +++ b/src/app/+collection-page/edit-item-template-page/item-template-page.resolver.ts @@ -7,16 +7,19 @@ import { Observable } from 'rxjs/internal/Observable'; import { find } from 'rxjs/operators'; import { hasValue } from '../../shared/empty.util'; +/** + * This class represents a resolver that requests a specific collection's item template before the route is activated + */ @Injectable() export class ItemTemplatePageResolver implements Resolve> { constructor(private itemTemplateService: ItemTemplateDataService) { } /** - * Method for resolving a collection's template item based on the parameters in the current route + * 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<> Emits the found template item based on the parameters in the current route, + * @returns Observable<> 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> { diff --git a/src/app/core/data/item-template-data.service.ts b/src/app/core/data/item-template-data.service.ts index f13a79893c..a8eecbad21 100644 --- a/src/app/core/data/item-template-data.service.ts +++ b/src/app/core/data/item-template-data.service.ts @@ -20,9 +20,16 @@ import { CollectionDataService } from './collection-data.service'; import { switchMap } from 'rxjs/operators'; /* 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 linkPath = 'itemtemplate'; + /** + * The ID of the collection we're currently sending requests for + */ private collectionID: string; constructor( @@ -40,34 +47,62 @@ class DataServiceImpl extends ItemDataService { super(requestService, rdbService, dataBuildService, store, bs, objectCache, halService, notificationsService, http, comparator); } + /** + * 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 { return this.collectionService.getIDHrefObs(this.collectionID).pipe( switchMap((href: string) => this.halService.getEndpoint(this.linkPath, href)) ); } + /** + * Since the collection ID is included in the base endpoint, simply return the base endpoint + * @param resourceID + */ getIDHrefObs(resourceID: string): Observable { return this.getEndpoint(); } + /** + * Set the collection ID and send a find by ID request + * @param collectionID + */ findByCollectionID(collectionID: string): Observable> { this.collectionID = collectionID; return super.findById(collectionID); } + /** + * Set the collection ID and send a create request + * @param item + * @param collectionID + */ create(item: Item, collectionID: string): Observable> { this.collectionID = collectionID; return super.create(item); } + /** + * Set the collection ID and send a delete request + * @param item + * @param collectionID + */ deleteByCollectionID(item: Item, collectionID: string): Observable { this.collectionID = collectionID; return super.delete(item); } } +/** + * A service responsible for fetching/sending data from/to the REST API on a collection's itemtemplate endpoint + */ @Injectable() export class ItemTemplateDataService implements UpdateDataService { + /** + * The data service responsible for all CRUD actions on the item + */ private dataService: DataServiceImpl; constructor( @@ -85,22 +120,42 @@ export class ItemTemplateDataService implements UpdateDataService { this.dataService = new DataServiceImpl(requestService, rdbService, dataBuildService, store, bs, objectCache, halService, notificationsService, http, comparator, 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> { return this.dataService.update(object); } + /** + * Find an item template by collection ID + * @param collectionID + */ findByCollectionID(collectionID: string): Observable> { return this.dataService.findByCollectionID(collectionID); } + /** + * Create a new item template for a collection by ID + * @param item + * @param collectionID + */ create(item: Item, collectionID: string): Observable> { return this.dataService.create(item, collectionID); } + /** + * Delete a template item by collection ID + * @param item + * @param collectionID + */ deleteByCollectionID(item: Item, collectionID: string): Observable { return this.dataService.deleteByCollectionID(item, collectionID); } diff --git a/src/app/core/data/update-data.service.ts b/src/app/core/data/update-data.service.ts index d0527f6fd2..34835e14c1 100644 --- a/src/app/core/data/update-data.service.ts +++ b/src/app/core/data/update-data.service.ts @@ -2,6 +2,9 @@ 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 { update(object: T): Observable>; commitUpdates(method?: RestRequestMethod); From 90beee9a854ef1b868b030c3aea7c58ba62d0cc7 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 4 Oct 2019 13:34:56 +0200 Subject: [PATCH 08/84] 65272: Existing test fixes --- .../abstract-item-update/abstract-item-update.component.ts | 2 +- .../item-metadata/item-metadata.component.spec.ts | 1 + .../item-relationships/item-relationships.component.spec.ts | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts b/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts index 6cfed8ffdf..3b23704af5 100644 --- a/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts +++ b/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts @@ -57,7 +57,7 @@ export abstract class AbstractItemUpdateComponent implements OnInit { */ ngOnInit(): void { observableCombineLatest(this.route.data, this.route.parent.data).pipe( - map(([data, parentData]) => Object.assign(data, parentData)), + map(([data, parentData]) => Object.assign({}, data, parentData)), map((data) => data.item), first(), map((data: RemoteData) => data.payload) diff --git a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts index 0c164b7bc2..e5bf6ea0af 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts @@ -123,6 +123,7 @@ describe('ItemMetadataComponent', () => { commitUpdates: {} }); routeStub = { + data: observableOf({}), parent: { data: observableOf({ item: createSuccessfulRemoteDataObject(item) }) } diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts index b1a4e11371..f9b950b343 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts @@ -122,6 +122,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) }) } From cfdc227563ba12955d0fbfe2e8efa6e103e9ad1c Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 4 Oct 2019 14:46:59 +0200 Subject: [PATCH 09/84] 65272: CollectionMetadataComponent test import fixes --- .../collection-metadata.component.spec.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.spec.ts b/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.spec.ts index 67eab669d5..0db0b51490 100644 --- a/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.spec.ts +++ b/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.spec.ts @@ -8,18 +8,30 @@ import { ActivatedRoute } 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 { Item } from '../../../core/shared/item.model'; +import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils'; +import { ItemTemplateDataService } from '../../../core/data/item-template-data.service'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; describe('CollectionMetadataComponent', () => { let comp: CollectionMetadataComponent; let fixture: ComponentFixture; + const template = new Item(); + + const itemTemplateService = Object.assign({ + findByCollectionID: () => createSuccessfulRemoteDataObject$(template) + }); + beforeEach(async(() => { TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], declarations: [CollectionMetadataComponent], providers: [ { provide: CollectionDataService, useValue: {} }, + { provide: ItemTemplateDataService, useValue: itemTemplateService }, { provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: { payload: {} } }) } } }, + { provide: NotificationsService, useValue: {} } ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); From 7fd4b41460df3c12cc3b0ae79cbb07bae3aaf5b4 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 4 Oct 2019 17:50:53 +0200 Subject: [PATCH 10/84] 65272: Added test cases --- .../collection-metadata.component.spec.ts | 65 +++++++-- .../edit-item-template-page.component.spec.ts | 51 +++++++ .../edit-item-template-page.component.ts | 6 +- .../item-template-page.resolver.spec.ts | 28 ++++ .../data/item-template-data.service.spec.ts | 136 ++++++++++++++++++ 5 files changed, 277 insertions(+), 9 deletions(-) create mode 100644 src/app/+collection-page/edit-item-template-page/edit-item-template-page.component.spec.ts create mode 100644 src/app/+collection-page/edit-item-template-page/item-template-page.resolver.spec.ts create mode 100644 src/app/core/data/item-template-data.service.spec.ts diff --git a/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.spec.ts b/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.spec.ts index 0db0b51490..2bd932b7d2 100644 --- a/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.spec.ts +++ b/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.spec.ts @@ -4,23 +4,38 @@ 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 { Item } from '../../../core/shared/item.model'; -import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils'; import { ItemTemplateDataService } from '../../../core/data/item-template-data.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { Collection } from '../../../core/shared/collection.model'; describe('CollectionMetadataComponent', () => { let comp: CollectionMetadataComponent; let fixture: ComponentFixture; + let router: Router; + let itemTemplateService: ItemTemplateDataService; const template = new Item(); + const collection = Object.assign(new Collection(), { + uuid: 'collection-id', + id: 'collection-id', + name: 'Fake Collection' + }); - const itemTemplateService = Object.assign({ - findByCollectionID: () => createSuccessfulRemoteDataObject$(template) + const itemTemplateServiceStub = Object.assign({ + findByCollectionID: () => createSuccessfulRemoteDataObject$(template), + create: () => createSuccessfulRemoteDataObject$(template), + deleteByCollectionID: () => observableOf(true) + }); + + const notificationsService = jasmine.createSpyObj('notificationsService', { + success: {}, + error: {} }); beforeEach(async(() => { @@ -29,9 +44,9 @@ describe('CollectionMetadataComponent', () => { declarations: [CollectionMetadataComponent], providers: [ { provide: CollectionDataService, useValue: {} }, - { provide: ItemTemplateDataService, useValue: itemTemplateService }, - { provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: { payload: {} } }) } } }, - { provide: NotificationsService, useValue: {} } + { provide: ItemTemplateDataService, useValue: itemTemplateServiceStub }, + { provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: createSuccessfulRemoteDataObject(collection) }) } } }, + { provide: NotificationsService, useValue: notificationsService } ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); @@ -40,12 +55,46 @@ 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(); + }); + }); + + 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(); + }); + }); }); }); diff --git a/src/app/+collection-page/edit-item-template-page/edit-item-template-page.component.spec.ts b/src/app/+collection-page/edit-item-template-page/edit-item-template-page.component.spec.ts new file mode 100644 index 0000000000..90857712b8 --- /dev/null +++ b/src/app/+collection-page/edit-item-template-page/edit-item-template-page.component.spec.ts @@ -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 { createSuccessfulRemoteDataObject } from '../../shared/testing/utils'; +import { Collection } from '../../core/shared/collection.model'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { getCollectionEditPath } from '../collection-page-routing.module'; + +describe('EditItemTemplatePageComponent', () => { + let comp: EditItemTemplatePageComponent; + let fixture: ComponentFixture; + 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: { 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)); + }); + }); +}); diff --git a/src/app/+collection-page/edit-item-template-page/edit-item-template-page.component.ts b/src/app/+collection-page/edit-item-template-page/edit-item-template-page.component.ts index 56e21ecb37..0170ab620b 100644 --- a/src/app/+collection-page/edit-item-template-page/edit-item-template-page.component.ts +++ b/src/app/+collection-page/edit-item-template-page/edit-item-template-page.component.ts @@ -34,7 +34,11 @@ export class EditItemTemplatePageComponent implements OnInit { * @param collection */ getCollectionEditUrl(collection: Collection): string { - return getCollectionEditPath(collection.uuid); + if (collection) { + return getCollectionEditPath(collection.uuid); + } else { + return ''; + } } } diff --git a/src/app/+collection-page/edit-item-template-page/item-template-page.resolver.spec.ts b/src/app/+collection-page/edit-item-template-page/item-template-page.resolver.spec.ts new file mode 100644 index 0000000000..885adadb3f --- /dev/null +++ b/src/app/+collection-page/edit-item-template-page/item-template-page.resolver.spec.ts @@ -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); + } + ); + }); + }); +}); diff --git a/src/app/core/data/item-template-data.service.spec.ts b/src/app/core/data/item-template-data.service.spec.ts new file mode 100644 index 0000000000..38d72a6265 --- /dev/null +++ b/src/app/core/data/item-template-data.service.spec.ts @@ -0,0 +1,136 @@ +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 { NormalizedObjectBuildService } from '../cache/builders/normalized-object-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); + }, + commit(method?: RestRequestMethod) { + // Do nothing + } + } as RequestService; + const rdbService = {} as RemoteDataBuildService; + const dataBuildService = {} as NormalizedObjectBuildService; + const store = {} as Store; + const bs = {} as BrowseService; + const objectCache = { + getObjectBySelfLink(self) { + return observableOf({}) + }, + addPatch(self, operations) { + // Do nothing + } + } as ObjectCacheService; + const halEndpointService = { + getEndpoint(linkPath: string): Observable { + 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 { + return observableOf(collectionEndpoint); + } + } as CollectionDataService; + + function initTestService() { + service = new ItemTemplateDataService( + requestService, + rdbService, + dataBuildService, + store, + bs, + objectCache, + halEndpointService, + notificationsService, + http, + comparator, + 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 create on the item service implementation', () => { + spyOn(itemService, 'create'); + service.create(item, scopeID); + expect(itemService.create).toHaveBeenCalled(); + }); + }); + + describe('deleteByCollectionID', () => { + it('should call deleteByCollectionID on the item service implementation', () => { + spyOn(itemService, 'deleteByCollectionID'); + service.deleteByCollectionID(item, scopeID); + expect(itemService.deleteByCollectionID).toHaveBeenCalled(); + }); + }); +}); From b3b8ea1dee4e2a5bef396fa2bba30845303d2f74 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 14 Oct 2019 16:47:35 +0200 Subject: [PATCH 11/84] 65272: item template REST contract endpoint changes --- .../core/data/item-template-data.service.ts | 56 ++++++++++++++----- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/src/app/core/data/item-template-data.service.ts b/src/app/core/data/item-template-data.service.ts index a8eecbad21..aaa664d7ec 100644 --- a/src/app/core/data/item-template-data.service.ts +++ b/src/app/core/data/item-template-data.service.ts @@ -25,12 +25,18 @@ import { switchMap } from 'rxjs/operators'; * Makes sure to change the endpoint before sending out CRUD requests for the item template */ class DataServiceImpl extends ItemDataService { - protected linkPath = 'itemtemplate'; + protected collectionLinkPath = 'itemtemplate'; + protected linkPath = 'itemtemplates'; /** - * The ID of the collection we're currently sending requests for + * Endpoint dynamically changing depending on what request we're sending */ - private collectionID: string; + private endpoint$: Observable; + + /** + * Is the current endpoint based on a collection? + */ + private collectionEndpoint = false; constructor( protected requestService: RequestService, @@ -48,21 +54,43 @@ class DataServiceImpl extends ItemDataService { } /** - * Get the base endpoint for all requests - * Uses the current collectionID to assemble a request endpoint for the collection's item template + * Set the endpoint to be based on a collection + * @param collectionID The ID of the collection to base the endpoint on */ - protected getEndpoint(): Observable { - return this.collectionService.getIDHrefObs(this.collectionID).pipe( - switchMap((href: string) => this.halService.getEndpoint(this.linkPath, href)) + private setCollectionEndpoint(collectionID: string) { + this.collectionEndpoint = true; + this.endpoint$ = this.collectionService.getIDHrefObs(collectionID).pipe( + switchMap((href: string) => this.halService.getEndpoint(this.collectionLinkPath, href)) ); } /** - * Since the collection ID is included in the base endpoint, simply return the base endpoint + * 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 { + 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 { - return this.getEndpoint(); + if (this.collectionEndpoint) { + return this.getEndpoint(); + } else { + return super.getIDHrefObs(resourceID); + } } /** @@ -70,7 +98,7 @@ class DataServiceImpl extends ItemDataService { * @param collectionID */ findByCollectionID(collectionID: string): Observable> { - this.collectionID = collectionID; + this.setCollectionEndpoint(collectionID); return super.findById(collectionID); } @@ -80,7 +108,7 @@ class DataServiceImpl extends ItemDataService { * @param collectionID */ create(item: Item, collectionID: string): Observable> { - this.collectionID = collectionID; + this.setCollectionEndpoint(collectionID); return super.create(item); } @@ -90,13 +118,13 @@ class DataServiceImpl extends ItemDataService { * @param collectionID */ deleteByCollectionID(item: Item, collectionID: string): Observable { - this.collectionID = collectionID; + this.setRegularEndpoint(); return super.delete(item); } } /** - * A service responsible for fetching/sending data from/to the REST API on a collection's itemtemplate endpoint + * A service responsible for fetching/sending data from/to the REST API on a collection's itemtemplates endpoint */ @Injectable() export class ItemTemplateDataService implements UpdateDataService { From 24fc3e6c763c5cc6760cabe7954981405513df63 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 23 Jan 2020 15:14:25 +0100 Subject: [PATCH 12/84] 65272: TemplateItem object and normalized version --- .../models/normalized-template-item.model.ts | 22 +++++++++++++++++++ src/app/core/core.module.ts | 4 +++- src/app/core/shared/template-item.model.ts | 18 +++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 src/app/core/cache/models/normalized-template-item.model.ts create mode 100644 src/app/core/shared/template-item.model.ts diff --git a/src/app/core/cache/models/normalized-template-item.model.ts b/src/app/core/cache/models/normalized-template-item.model.ts new file mode 100644 index 0000000000..7a3259d74d --- /dev/null +++ b/src/app/core/cache/models/normalized-template-item.model.ts @@ -0,0 +1,22 @@ +import { inheritSerialization, deserialize } from 'cerialize'; + +import { mapsTo, relationship } from '../builders/build-decorators'; +import { TemplateItem } from '../../shared/template-item.model'; +import { NormalizedItem } from './normalized-item.model'; +import { Collection } from '../../shared/collection.model'; + +/** + * Normalized model class for a DSpace Template Item + */ +@mapsTo(TemplateItem) +@inheritSerialization(NormalizedItem) +export class NormalizedTemplateItem extends NormalizedItem { + + /** + * The Collection that this item is a template for + */ + @deserialize + @relationship(Collection, false) + templateItemOf: string; + +} diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 55fe376294..57034a9737 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -141,6 +141,7 @@ import { NormalizedExternalSource } from './cache/models/normalized-external-sou import { NormalizedExternalSourceEntry } from './cache/models/normalized-external-source-entry.model'; import { ExternalSourceService } from './data/external-source.service'; import { LookupRelationService } from './data/lookup-relation.service'; +import { NormalizedTemplateItem } from './cache/models/normalized-template-item.model'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -302,7 +303,8 @@ export const normalizedModels = NormalizedRelationshipType, NormalizedItemType, NormalizedExternalSource, - NormalizedExternalSourceEntry + NormalizedExternalSourceEntry, + NormalizedTemplateItem ]; @NgModule({ diff --git a/src/app/core/shared/template-item.model.ts b/src/app/core/shared/template-item.model.ts new file mode 100644 index 0000000000..a58ec047ce --- /dev/null +++ b/src/app/core/shared/template-item.model.ts @@ -0,0 +1,18 @@ +import { Item } from './item.model'; +import { Observable } from 'rxjs/internal/Observable'; +import { RemoteData } from '../data/remote-data'; +import { Collection } from './collection.model'; +import { ResourceType } from './resource-type'; + +/** + * Class representing a DSpace Template Item + */ +export class TemplateItem extends Item { + static type = new ResourceType('templateItem'); + + /** + * The Collection that this item is a template for + */ + templateItemOf: Observable>; + +} From 4ae5ee21b16aa1b047b7bc0a674839d130944852 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 23 Jan 2020 15:59:47 +0100 Subject: [PATCH 13/84] 65272: Edit template item - Empty metadata alert + alignment fixes --- resources/i18n/en.json5 | 2 + .../item-metadata.component.html | 39 ++++++++++--------- .../item-metadata/item-metadata.component.ts | 7 ++++ 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index 86d93044ac..16f06c9e4e 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -685,6 +685,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", diff --git a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.html b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.html index 496429a3ba..366f6fffe2 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.html +++ b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.html @@ -1,5 +1,5 @@