diff --git a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts index 2acfa17c8b..3037744e67 100644 --- a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts +++ b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts @@ -209,7 +209,7 @@ describe('BitstreamFormatsComponent', () => { selectBitstreamFormat: {}, deselectBitstreamFormat: {}, deselectAllBitstreamFormats: {}, - delete: observableOf(true), + delete: observableOf({ isSuccessful: true }), clearBitStreamFormatRequests: observableOf('cleared') }); diff --git a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.ts b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.ts index 52010e0132..80ae56ec93 100644 --- a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.ts +++ b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.ts @@ -11,6 +11,7 @@ import { hasValue } from '../../../shared/empty.util'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; +import { RestResponse } from '../../../core/cache/response.models'; /** * This component renders a list of bitstream formats @@ -64,7 +65,7 @@ export class BitstreamFormatsComponent implements OnInit { const tasks$ = []; for (const format of formats) { if (hasValue(format.id)) { - tasks$.push(this.bitstreamFormatService.delete(format.id)); + tasks$.push(this.bitstreamFormatService.delete(format.id).pipe(map((response: RestResponse) => response.isSuccessful))); } } zip(...tasks$).subscribe((results: boolean[]) => { diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.guard.ts b/src/app/+collection-page/create-collection-page/create-collection-page.guard.ts index 4cd842e926..5bcad1cbd7 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.guard.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.guard.ts @@ -5,8 +5,7 @@ import { hasNoValue, hasValue } from '../../shared/empty.util'; import { CommunityDataService } from '../../core/data/community-data.service'; import { RemoteData } from '../../core/data/remote-data'; import { Community } from '../../core/shared/community.model'; -import { getFinishedRemoteData } from '../../core/shared/operators'; -import { map, tap } from 'rxjs/operators'; +import { map, tap, find } from 'rxjs/operators'; import { Observable, of as observableOf } from 'rxjs'; /** @@ -29,18 +28,15 @@ export class CreateCollectionPageGuard implements CanActivate { this.router.navigate(['/404']); return observableOf(false); } - const parent: Observable> = this.communityService.findById(parentID) + return this.communityService.findById(parentID) .pipe( - getFinishedRemoteData(), - ); - - return parent.pipe( - map((communityRD: RemoteData) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)), - tap((isValid: boolean) => { - if (!isValid) { - this.router.navigate(['/404']); - } - }) + find((communityRD: RemoteData) => hasValue(communityRD.payload) || hasValue(communityRD.error)), + map((communityRD: RemoteData) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)), + tap((isValid: boolean) => { + if (!isValid) { + this.router.navigate(['/404']); + } + }) ); } } diff --git a/src/app/+community-page/create-community-page/create-community-page.guard.ts b/src/app/+community-page/create-community-page/create-community-page.guard.ts index 2ee5cb6064..de7026c887 100644 --- a/src/app/+community-page/create-community-page/create-community-page.guard.ts +++ b/src/app/+community-page/create-community-page/create-community-page.guard.ts @@ -5,8 +5,7 @@ import { hasNoValue, hasValue } from '../../shared/empty.util'; import { CommunityDataService } from '../../core/data/community-data.service'; import { RemoteData } from '../../core/data/remote-data'; import { Community } from '../../core/shared/community.model'; -import { getFinishedRemoteData } from '../../core/shared/operators'; -import { map, tap } from 'rxjs/operators'; +import { map, tap, find } from 'rxjs/operators'; import { Observable, of as observableOf } from 'rxjs'; /** @@ -29,18 +28,16 @@ export class CreateCommunityPageGuard implements CanActivate { return observableOf(true); } - const parent: Observable> = this.communityService.findById(parentID) + return this.communityService.findById(parentID) .pipe( - getFinishedRemoteData(), - ); - - return parent.pipe( - map((communityRD: RemoteData) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)), - tap((isValid: boolean) => { - if (!isValid) { - this.router.navigate(['/404']); + find((communityRD: RemoteData) => hasValue(communityRD.payload) || hasValue(communityRD.error)), + map((communityRD: RemoteData) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)), + tap((isValid: boolean) => { + if (!isValid) { + this.router.navigate(['/404']); + } } - }) + ) ); } } diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts b/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts index 0bdb4be6cb..f86c57d69e 100644 --- a/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts @@ -114,7 +114,7 @@ describe('ItemBitstreamsComponent', () => { } ); bitstreamService = jasmine.createSpyObj('bitstreamService', { - deleteAndReturnResponse: jasmine.createSpy('deleteAndReturnResponse') + delete: jasmine.createSpy('delete') }); objectCache = jasmine.createSpyObj('objectCache', { remove: jasmine.createSpy('remove') @@ -182,12 +182,25 @@ describe('ItemBitstreamsComponent', () => { comp.submit(); }); - it('should call deleteAndReturnResponse on the bitstreamService for the marked field', () => { - expect(bitstreamService.deleteAndReturnResponse).toHaveBeenCalledWith(bitstream2.id); + it('should call delete on the bitstreamService for the marked field', () => { + expect(bitstreamService.delete).toHaveBeenCalledWith(bitstream2.id); }); - it('should not call deleteAndReturnResponse on the bitstreamService for the unmarked field', () => { - expect(bitstreamService.deleteAndReturnResponse).not.toHaveBeenCalledWith(bitstream1.id); + it('should not call delete on the bitstreamService for the unmarked field', () => { + expect(bitstreamService.delete).not.toHaveBeenCalledWith(bitstream1.id); + }); + }); + + describe('when dropBitstream is called', () => { + const event = { + fromIndex: 0, + toIndex: 50, + // tslint:disable-next-line:no-empty + finish: () => {} + }; + + beforeEach(() => { + comp.dropBitstream(bundle, event); }); }); diff --git a/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts b/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts index 45b8e23108..e00f4ae4aa 100644 --- a/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts +++ b/src/app/+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts @@ -165,7 +165,7 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme take(1), switchMap((removedBistreams: Bitstream[]) => { if (isNotEmpty(removedBistreams)) { - return observableZip(...removedBistreams.map((bitstream: Bitstream) => this.bitstreamService.deleteAndReturnResponse(bitstream.id))); + return observableZip(...removedBistreams.map((bitstream: Bitstream) => this.bitstreamService.delete(bitstream.id))); } else { return observableOf(undefined); } diff --git a/src/app/+item-page/edit-item-page/item-delete/item-delete.component.ts b/src/app/+item-page/edit-item-page/item-delete/item-delete.component.ts index 209ddf22a9..933919c572 100644 --- a/src/app/+item-page/edit-item-page/item-delete/item-delete.component.ts +++ b/src/app/+item-page/edit-item-page/item-delete/item-delete.component.ts @@ -21,6 +21,7 @@ import { RelationshipService } from '../../../core/data/relationship.service'; import { EntityTypeService } from '../../../core/data/entity-type.service'; import { LinkService } from '../../../core/cache/builders/link.service'; import { followLink } from '../../../shared/utils/follow-link-config.model'; +import { RestResponse } from '../../../core/cache/response.models'; @Component({ selector: 'ds-item-delete', @@ -327,8 +328,8 @@ export class ItemDeleteComponent ), ).subscribe((types) => { this.itemDataService.delete(this.item.id, types).pipe(first()).subscribe( - (succeeded: boolean) => { - this.notify(succeeded); + (response: RestResponse) => { + this.notify(response.isSuccessful); } ); }); diff --git a/src/app/+item-page/simple/item-types/shared/item-relationships-utils.ts b/src/app/+item-page/simple/item-types/shared/item-relationships-utils.ts index 73e02ca29d..6ef035f1e6 100644 --- a/src/app/+item-page/simple/item-types/shared/item-relationships-utils.ts +++ b/src/app/+item-page/simple/item-types/shared/item-relationships-utils.ts @@ -5,7 +5,7 @@ import { PaginatedList } from '../../../../core/data/paginated-list'; import { RemoteData } from '../../../../core/data/remote-data'; import { Relationship } from '../../../../core/shared/item-relationships/relationship.model'; import { Item } from '../../../../core/shared/item.model'; -import { getFinishedRemoteData, getSucceededRemoteData } from '../../../../core/shared/operators'; +import { getFirstSucceededRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators'; import { hasValue } from '../../../../shared/empty.util'; /** @@ -75,16 +75,19 @@ export const paginatedRelationsToItems = (thisId: string) => getSucceededRemoteData(), switchMap((relationshipsRD: RemoteData>) => { return observableCombineLatest( - ...relationshipsRD.payload.page.map((rel: Relationship) => observableCombineLatest(rel.leftItem.pipe(getFinishedRemoteData()), rel.rightItem.pipe(getFinishedRemoteData()))) - ).pipe( + relationshipsRD.payload.page.map((rel: Relationship) => + observableCombineLatest([ + rel.leftItem.pipe(getFirstSucceededRemoteDataPayload()), + rel.rightItem.pipe(getFirstSucceededRemoteDataPayload())] + ) + )).pipe( map((arr) => arr - .filter(([leftItem, rightItem]) => leftItem.hasSucceeded && rightItem.hasSucceeded) .map(([leftItem, rightItem]) => { - if (leftItem.payload.id === thisId) { - return rightItem.payload; - } else if (rightItem.payload.id === thisId) { - return leftItem.payload; + if (leftItem.id === thisId) { + return rightItem; + } else if (rightItem.id === thisId) { + return leftItem; } }) .filter((item: Item) => hasValue(item)) diff --git a/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts b/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts index 43c3e90152..72a39ab53c 100644 --- a/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts +++ b/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts @@ -7,6 +7,8 @@ import { RouteService } from '../../core/services/route.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; import { RequestService } from '../../core/data/request.service'; +import { map } from 'rxjs/operators'; +import { RestResponse } from '../../core/cache/response.models'; @Component({ selector: 'ds-workflow-item-delete', @@ -39,6 +41,6 @@ export class WorkflowItemDeleteComponent extends WorkflowItemActionPageComponent */ sendRequest(id: string): Observable { this.requestService.removeByHrefSubstring('/discover'); - return this.workflowItemService.delete(id); + return this.workflowItemService.delete(id).pipe(map((response: RestResponse) => response.isSuccessful)); } } diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index 6c9f40888f..ae3b0e4fd1 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -1,13 +1,7 @@ import { Injectable } from '@angular/core'; import { combineLatest as observableCombineLatest, Observable, of as observableOf, race as observableRace } from 'rxjs'; import { distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators'; -import { - hasValue, - hasValueOperator, - isEmpty, - isNotEmpty, - isNotUndefined -} from '../../../shared/empty.util'; +import { hasValue, hasValueOperator, isEmpty, isNotEmpty, isNotUndefined } from '../../../shared/empty.util'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; import { PaginatedList } from '../../data/paginated-list'; @@ -15,12 +9,7 @@ import { RemoteData } from '../../data/remote-data'; import { RemoteDataError } from '../../data/remote-data-error'; import { RequestEntry } from '../../data/request.reducer'; import { RequestService } from '../../data/request.service'; -import { - filterSuccessfulResponses, - getRequestFromRequestHref, - getRequestFromRequestUUID, - getResourceLinksFromResponse -} from '../../shared/operators'; +import { filterSuccessfulResponses, getRequestFromRequestHref, getRequestFromRequestUUID, getResourceLinksFromResponse } from '../../shared/operators'; import { PageInfo } from '../../shared/page-info.model'; import { CacheableObject } from '../object-cache.reducer'; import { ObjectCacheService } from '../object-cache.service'; @@ -98,7 +87,8 @@ export class RemoteDataBuildService { let error: RemoteDataError; const response = reqEntry ? reqEntry.response : undefined; if (hasValue(response)) { - isSuccessful = response.isSuccessful; + isSuccessful = response.statusCode === 204 || + response.statusCode >= 200 && response.statusCode < 300 && hasValue(payload); const errorMessage = isSuccessful === false ? (response as ErrorResponse).errorMessage : undefined; if (hasValue(errorMessage)) { error = new RemoteDataError( @@ -155,7 +145,7 @@ export class RemoteDataBuildService { }) ); - const payload$ = observableCombineLatest(tDomainList$, pageInfo$).pipe( + const payload$ = observableCombineLatest([tDomainList$, pageInfo$]).pipe( map(([tDomainList, pageInfo]) => { return new PaginatedList(pageInfo, tDomainList); }) diff --git a/src/app/core/cache/object-cache.reducer.ts b/src/app/core/cache/object-cache.reducer.ts index a39ceb4e16..38b52ad8c5 100644 --- a/src/app/core/cache/object-cache.reducer.ts +++ b/src/app/core/cache/object-cache.reducer.ts @@ -1,7 +1,5 @@ -import { autoserialize, deserialize } from 'cerialize'; import { HALLink } from '../shared/hal-link.model'; import { HALResource } from '../shared/hal-resource.model'; -import { excludeFromEquals } from '../utilities/equals.decorators'; import { ObjectCacheAction, ObjectCacheActionTypes, @@ -15,12 +13,6 @@ import { CacheEntry } from './cache-entry'; import { ResourceType } from '../shared/resource-type'; import { applyPatch, Operation } from 'fast-json-patch'; -export enum DirtyType { - Created = 'Created', - Updated = 'Updated', - Deleted = 'Deleted' -} - /** * An interface to represent a JsonPatch */ @@ -72,6 +64,7 @@ export class ObjectCacheEntry implements CacheEntry { patches: Patch[] = []; isDirty: boolean; } + /* tslint:enable:max-classes-per-file */ /** diff --git a/src/app/core/cache/server-sync-buffer.reducer.ts b/src/app/core/cache/server-sync-buffer.reducer.ts index d79dd51da4..7b65db0fbb 100644 --- a/src/app/core/cache/server-sync-buffer.reducer.ts +++ b/src/app/core/cache/server-sync-buffer.reducer.ts @@ -9,7 +9,7 @@ import { RestRequestMethod } from '../data/rest-request-method'; /** * An entry in the ServerSyncBufferState - * href: unique href of an ObjectCacheEntry + * href: unique href of an ServerSyncBufferEntry * method: RestRequestMethod type */ export class ServerSyncBufferEntry { @@ -48,6 +48,7 @@ export function serverSyncBufferReducer(state = initialState, action: ServerSync case ServerSyncBufferActionTypes.EMPTY: { return emptyServerSyncQueue(state, action as EmptySSBAction); } + default: { return state; } diff --git a/src/app/core/config/config-response-parsing.service.spec.ts b/src/app/core/config/config-response-parsing.service.spec.ts index c0bc8b3212..3328b48f04 100644 --- a/src/app/core/config/config-response-parsing.service.spec.ts +++ b/src/app/core/config/config-response-parsing.service.spec.ts @@ -177,6 +177,7 @@ describe('ConfigResponseParsingService', () => { Object.assign(new SubmissionDefinitionModel(), { isDefault: true, name: 'traditional', + id: 'traditional', type: 'submissiondefinition', _links: { sections: { href: 'https://rest.api/config/submissiondefinitions/traditional/sections' }, @@ -187,6 +188,7 @@ describe('ConfigResponseParsingService', () => { header: 'submit.progressbar.describe.stepone', mandatory: true, sectionType: 'submission-form', + id: 'traditionalpageone', visibility: { main: null, other: 'READONLY' @@ -201,6 +203,7 @@ describe('ConfigResponseParsingService', () => { header: 'submit.progressbar.describe.steptwo', mandatory: true, sectionType: 'submission-form', + id: 'traditionalpagetwo', visibility: { main: null, other: 'READONLY' @@ -215,6 +218,7 @@ describe('ConfigResponseParsingService', () => { header: 'submit.progressbar.upload', mandatory: false, sectionType: 'upload', + id: 'upload', visibility: { main: null, other: 'READONLY' @@ -229,6 +233,7 @@ describe('ConfigResponseParsingService', () => { header: 'submit.progressbar.license', mandatory: true, sectionType: 'license', + id: 'license', visibility: { main: null, other: 'READONLY' diff --git a/src/app/core/config/models/config.model.ts b/src/app/core/config/models/config.model.ts index fabb16eb23..53250ee045 100644 --- a/src/app/core/config/models/config.model.ts +++ b/src/app/core/config/models/config.model.ts @@ -6,6 +6,12 @@ import { excludeFromEquals } from '../../utilities/equals.decorators'; export abstract class ConfigObject implements CacheableObject { + /** + * The name for this configuration + */ + @autoserialize + public id: string; + /** * The name for this configuration */ diff --git a/src/app/core/data/bitstream-format-data.service.spec.ts b/src/app/core/data/bitstream-format-data.service.spec.ts index 7954416010..99bf4eea18 100644 --- a/src/app/core/data/bitstream-format-data.service.spec.ts +++ b/src/app/core/data/bitstream-format-data.service.spec.ts @@ -281,7 +281,7 @@ describe('BitstreamFormatDataService', () => { format.uuid = 'format-uuid'; format.id = 'format-id'; - const expected = cold('(b|)', {b: true}); + const expected = cold('(b|)', { b: responseCacheEntry.response }); const result = service.delete(format.id); expect(result).toBeObservable(expected); diff --git a/src/app/core/data/bitstream-format-data.service.ts b/src/app/core/data/bitstream-format-data.service.ts index e8cf030a52..52ca07060a 100644 --- a/src/app/core/data/bitstream-format-data.service.ts +++ b/src/app/core/data/bitstream-format-data.service.ts @@ -155,9 +155,9 @@ export class BitstreamFormatDataService extends DataService { /** * Delete an existing DSpace Object on the server * @param formatID The DSpace Object'id to be removed - * Return an observable that emits true when the deletion was successful, false when it failed + * @return the RestResponse as an Observable */ - delete(formatID: string): Observable { + delete(formatID: string): Observable { const requestId = this.requestService.generateRequestId(); const hrefObs = this.halService.getEndpoint(this.linkPath).pipe( @@ -173,7 +173,7 @@ export class BitstreamFormatDataService extends DataService { return this.requestService.getByUUID(requestId).pipe( find((request: RequestEntry) => request.completed), - map((request: RequestEntry) => request.response.isSuccessful) + map((request: RequestEntry) => request.response) ); } diff --git a/src/app/core/data/bundle-data.service.spec.ts b/src/app/core/data/bundle-data.service.spec.ts new file mode 100644 index 0000000000..1e1bf0eb9c --- /dev/null +++ b/src/app/core/data/bundle-data.service.spec.ts @@ -0,0 +1,94 @@ +import { HttpClient } from '@angular/common/http'; +import { Store } from '@ngrx/store'; +import { compare, Operation } from 'fast-json-patch'; +import { Observable, of as observableOf } from 'rxjs'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { followLink } from '../../shared/utils/follow-link-config.model'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { SortDirection, SortOptions } from '../cache/models/sort-options.model'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { CoreState } from '../core.reducers'; +import { DSpaceObject } from '../shared/dspace-object.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { Item } from '../shared/item.model'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { ChangeAnalyzer } from './change-analyzer'; +import { DataService } from './data.service'; +import { FindListOptions, PatchRequest } from './request.models'; +import { RequestService } from './request.service'; +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; +import { BundleDataService } from './bundle-data.service'; +import { HALLink } from '../shared/hal-link.model'; + +class DummyChangeAnalyzer implements ChangeAnalyzer { + diff(object1: Item, object2: Item): Operation[] { + return compare((object1 as any).metadata, (object2 as any).metadata); + } +} + +describe('BundleDataService', () => { + let service: BundleDataService; + let requestService; + let halService; + let rdbService; + let notificationsService; + let http; + let comparator; + let objectCache; + let store; + let item; + let bundleLink; + let bundleHALLink; + + function initTestService(): BundleDataService { + bundleLink = '/items/0fdc0cd7-ff8c-433d-b33c-9b56108abc07/bundles'; + bundleHALLink = new HALLink(); + bundleHALLink.href = bundleLink; + item = new Item(); + item._links = { + bundles: bundleHALLink + }; + requestService = getMockRequestService(); + halService = new HALEndpointServiceStub('url') as any; + rdbService = {} as RemoteDataBuildService; + notificationsService = {} as NotificationsService; + http = {} as HttpClient; + comparator = new DummyChangeAnalyzer() as any; + objectCache = { + + addPatch: () => { + /* empty */ + }, + getObjectBySelfLink: () => { + /* empty */ + } + } as any; + store = {} as Store; + return new BundleDataService( + requestService, + rdbService, + store, + objectCache, + halService, + notificationsService, + http, + comparator, + ); + } + + beforeEach(() => { + service = initTestService(); + }); + + describe('findAllByItem', () => { + beforeEach(() => { + spyOn(service, 'findAllByHref'); + service.findAllByItem(item); + }); + + it('should call findAllByHref with the item\'s bundles link', () => { + expect(service.findAllByHref).toHaveBeenCalledWith(bundleLink, undefined); + }) + }); +}); diff --git a/src/app/core/data/collection-data.service.spec.ts b/src/app/core/data/collection-data.service.spec.ts index 7087655a26..76aad4ad56 100644 --- a/src/app/core/data/collection-data.service.spec.ts +++ b/src/app/core/data/collection-data.service.spec.ts @@ -73,7 +73,12 @@ describe('CollectionDataService', () => { describe('when the requests are successful', () => { beforeEach(() => { - createService(); + createService(observableOf({ + request: { + href: 'https://rest.api/request' + }, + completed: true + })); }); describe('when calling getContentSource', () => { @@ -133,7 +138,7 @@ describe('CollectionDataService', () => { }); it('should return a RemoteData> for the getAuthorizedCollection', () => { - const result = service.getAuthorizedCollection(queryString) + const result = service.getAuthorizedCollection(queryString); const expected = cold('a|', { a: paginatedListRD }); @@ -148,7 +153,7 @@ describe('CollectionDataService', () => { }); it('should return a RemoteData> for the getAuthorizedCollectionByCommunity', () => { - const result = service.getAuthorizedCollectionByCommunity(communityId, queryString) + const result = service.getAuthorizedCollectionByCommunity(communityId, queryString); const expected = cold('a|', { a: paginatedListRD }); diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index b6f6465450..0d818f2030 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -2,19 +2,8 @@ import { HttpClient } from '@angular/common/http'; import { Store } from '@ngrx/store'; import { Operation } from 'fast-json-patch'; import { Observable } from 'rxjs'; -import { - distinctUntilChanged, - filter, - find, - first, - map, - mergeMap, - skipWhile, - switchMap, - take, - tap -} from 'rxjs/operators'; -import { hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; +import { distinctUntilChanged, filter, find, first, map, mergeMap, switchMap, take } from 'rxjs/operators'; +import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; @@ -28,25 +17,12 @@ import { CoreState } from '../core.reducers'; import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; import { DSpaceObject } from '../shared/dspace-object.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { - configureRequest, - getRemoteDataPayload, - getResponseFromEntry, - getSucceededRemoteData -} from '../shared/operators'; +import { configureRequest, getRemoteDataPayload, getResponseFromEntry, getSucceededRemoteData } from '../shared/operators'; import { URLCombiner } from '../url-combiner/url-combiner'; import { ChangeAnalyzer } from './change-analyzer'; import { PaginatedList } from './paginated-list'; import { RemoteData } from './remote-data'; -import { - CreateRequest, - DeleteByIDRequest, - FindByIDRequest, - FindListOptions, - FindListRequest, - GetRequest, - PatchRequest, PutRequest -} from './request.models'; +import { CreateRequest, DeleteByIDRequest, FindByIDRequest, FindListOptions, FindListRequest, GetRequest, PatchRequest, PutRequest } from './request.models'; import { RequestEntry } from './request.reducer'; import { RequestService } from './request.service'; import { RestRequestMethod } from './rest-request-method'; @@ -353,26 +329,24 @@ export abstract class DataService implements UpdateDa * Return an observable that emits response from the server */ searchBy(searchMethod: string, options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { - + const requestId = this.requestService.generateRequestId(); const hrefObs = this.getSearchByHref(searchMethod, options, ...linksToFollow); - return hrefObs.pipe( - find((href: string) => hasValue(href)), - tap((href: string) => { - this.requestService.removeByHrefSubstring(href); - const request = new FindListRequest(this.requestService.generateRequestId(), href, options); - if (hasValue(this.responseMsToLive)) { - request.responseMsToLive = this.responseMsToLive; - } + hrefObs.pipe( + find((href: string) => hasValue(href)) + ).subscribe((href: string) => { + const request = new FindListRequest(requestId, href, options); + if (hasValue(this.responseMsToLive)) { + request.responseMsToLive = this.responseMsToLive; + } + this.requestService.configure(request); + }); - this.requestService.configure(request); - } + return this.requestService.getByUUID(requestId).pipe( + find((requestEntry) => hasValue(requestEntry) && requestEntry.completed), + switchMap((requestEntry) => + this.rdbService.buildList(requestEntry.request.href, ...linksToFollow) ), - switchMap((href) => this.requestService.getByHref(href)), - skipWhile((requestEntry) => hasValue(requestEntry) && requestEntry.completed), - switchMap((href) => - this.rdbService.buildList(hrefObs, ...linksToFollow) as Observable>> - ) ); } @@ -391,6 +365,9 @@ export abstract class DataService implements UpdateDa find((href: string) => hasValue(href)), map((href: string) => { const request = new PatchRequest(requestId, href, operations); + if (hasValue(this.responseMsToLive)) { + request.responseMsToLive = this.responseMsToLive; + } this.requestService.configure(request); }) ).subscribe(); @@ -464,7 +441,13 @@ export abstract class DataService implements UpdateDa const request$ = endpoint$.pipe( take(1), - map((endpoint: string) => new CreateRequest(requestId, endpoint, JSON.stringify(serializedDso))) + map((endpoint: string) => { + const request = new CreateRequest(requestId, endpoint, JSON.stringify(serializedDso)); + if (hasValue(this.responseMsToLive)) { + request.responseMsToLive = this.responseMsToLive; + } + return request + }) ); // Execute the post request @@ -513,7 +496,13 @@ export abstract class DataService implements UpdateDa const request$ = endpoint$.pipe( take(1), - map((endpoint: string) => new CreateRequest(requestId, endpoint, JSON.stringify(serializedDso))) + map((endpoint: string) => { + const request = new CreateRequest(requestId, endpoint, JSON.stringify(serializedDso)); + if (hasValue(this.responseMsToLive)) { + request.responseMsToLive = this.responseMsToLive; + } + return request + }) ); // Execute the post request @@ -542,42 +531,9 @@ export abstract class DataService implements UpdateDa * @param dsoID The DSpace Object' id to be removed * @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual * metadata should be saved as real metadata - * @return an observable that emits true when the deletion was successful, false when it failed + * @return the RestResponse as an Observable */ - delete(dsoID: string, copyVirtualMetadata?: string[]): Observable { - const requestId = this.deleteAndReturnRequestId(dsoID, copyVirtualMetadata); - - return this.requestService.getByUUID(requestId).pipe( - find((request: RequestEntry) => isNotEmpty(request) && request.completed), - map((request: RequestEntry) => request.response.isSuccessful) - ); - } - - /** - * Delete an existing DSpace Object on the server - * @param dsoID The DSpace Object' id to be removed - * @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual - * metadata should be saved as real metadata - * Return an observable of the completed response - */ - deleteAndReturnResponse(dsoID: string, copyVirtualMetadata?: string[]): Observable { - const requestId = this.deleteAndReturnRequestId(dsoID, copyVirtualMetadata); - - return this.requestService.getByUUID(requestId).pipe( - hasValueOperator(), - find((request: RequestEntry) => request.completed), - map((request: RequestEntry) => request.response) - ); - } - - /** - * Delete an existing DSpace Object on the server - * @param dsoID The DSpace Object' id to be removed - * @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual - * metadata should be saved as real metadata - * Return the delete request's ID - */ - private deleteAndReturnRequestId(dsoID: string, copyVirtualMetadata?: string[]): string { + delete(dsoID: string, copyVirtualMetadata?: string[]): Observable { const requestId = this.requestService.generateRequestId(); const hrefObs = this.getIDHrefObs(dsoID); @@ -593,11 +549,17 @@ export abstract class DataService implements UpdateDa ); } const request = new DeleteByIDRequest(requestId, href, dsoID); + if (hasValue(this.responseMsToLive)) { + request.responseMsToLive = this.responseMsToLive; + } this.requestService.configure(request); }) ).subscribe(); - return requestId; + return this.requestService.getByUUID(requestId).pipe( + find((request: RequestEntry) => request.completed), + map((request: RequestEntry) => request.response) + ); } /** @@ -608,4 +570,15 @@ export abstract class DataService implements UpdateDa this.requestService.commit(method); } + /** + * Return the links to traverse from the root of the api to the + * endpoint this DataService represents + * + * e.g. if the api root links to 'foo', and the endpoint at 'foo' + * links to 'bar' the linkPath for the BarDataService would be + * 'foo/bar' + */ + getLinkPath(): string { + return this.linkPath; + } } diff --git a/src/app/core/data/item-template-data.service.ts b/src/app/core/data/item-template-data.service.ts index d157c09a7c..a44d48e9bd 100644 --- a/src/app/core/data/item-template-data.service.ts +++ b/src/app/core/data/item-template-data.service.ts @@ -16,9 +16,10 @@ import { NotificationsService } from '../../shared/notifications/notifications.s import { HttpClient } from '@angular/common/http'; import { BrowseService } from '../browse/browse.service'; import { CollectionDataService } from './collection-data.service'; -import { switchMap } from 'rxjs/operators'; +import { switchMap, map } from 'rxjs/operators'; import { BundleDataService } from './bundle-data.service'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { RestResponse } from '../cache/response.models'; /* tslint:disable:max-classes-per-file */ /** @@ -121,7 +122,7 @@ class DataServiceImpl extends ItemDataService { */ deleteByCollectionID(item: Item, collectionID: string): Observable { this.setRegularEndpoint(); - return super.delete(item.uuid); + return super.delete(item.uuid).pipe(map((response: RestResponse) => response.isSuccessful)); } } diff --git a/src/app/core/data/lookup-relation.service.ts b/src/app/core/data/lookup-relation.service.ts index 395976cbc3..bf0e2469d1 100644 --- a/src/app/core/data/lookup-relation.service.ts +++ b/src/app/core/data/lookup-relation.service.ts @@ -63,6 +63,7 @@ export class LookupRelationService { concat(subject.pipe(take(1))) ) ) as any + , ) as Observable>>>; } diff --git a/src/app/core/data/relationship-type.service.ts b/src/app/core/data/relationship-type.service.ts index eefe663209..bd279dbb0b 100644 --- a/src/app/core/data/relationship-type.service.ts +++ b/src/app/core/data/relationship-type.service.ts @@ -3,7 +3,7 @@ import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { combineLatest as observableCombineLatest } from 'rxjs'; import { Observable } from 'rxjs/internal/Observable'; -import { filter, find, map, switchMap } from 'rxjs/operators'; +import { filter, find, map, mergeMap, switchMap } from 'rxjs/operators'; import { AppState } from '../../app.reducer'; import { isNotUndefined } from '../../shared/empty.util'; import { NotificationsService } from '../../shared/notifications/notifications.service'; @@ -76,7 +76,7 @@ export class RelationshipTypeService extends DataService { getSucceededRemoteData(), /* Flatten the page so we can treat it like an observable */ switchMap((typeListRD: RemoteData>) => typeListRD.payload.page), - switchMap((type: RelationshipType) => { + mergeMap((type: RelationshipType) => { if (type.leftwardType === label) { return this.checkType(type, firstType, secondType); } else if (type.rightwardType === label) { @@ -92,7 +92,7 @@ export class RelationshipTypeService extends DataService { // returns a void observable if there's not match // returns an observable that emits the relationship type when there is a match private checkType(type: RelationshipType, firstType: string, secondType: string): Observable { - const entityTypes = observableCombineLatest(type.leftType.pipe(getSucceededRemoteData()), type.rightType.pipe(getSucceededRemoteData())); + const entityTypes = observableCombineLatest([type.leftType.pipe(getSucceededRemoteData()), type.rightType.pipe(getSucceededRemoteData())]); return entityTypes.pipe( find(([leftTypeRD, rightTypeRD]: [RemoteData, RemoteData]) => leftTypeRD.payload.label === firstType && rightTypeRD.payload.label === secondType), filter((types) => isNotUndefined(types)), diff --git a/src/app/core/data/relationship.service.spec.ts b/src/app/core/data/relationship.service.spec.ts index c8ce2c5c45..9253fb6730 100644 --- a/src/app/core/data/relationship.service.spec.ts +++ b/src/app/core/data/relationship.service.spec.ts @@ -1,11 +1,6 @@ import { Observable } from 'rxjs/internal/Observable'; import { of as observableOf } from 'rxjs/internal/observable/of'; import * as ItemRelationshipsUtils from '../../+item-page/simple/item-types/shared/item-relationships-utils'; -import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/remote-data-build.service.mock'; -import { getMockRequestService } from '../../shared/mocks/request.service.mock'; -import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; -import { spyOnOperator } from '../../shared/testing/utils.test'; import { followLink } from '../../shared/utils/follow-link-config.model'; import { ObjectCacheService } from '../cache/object-cache.service'; import { RelationshipType } from '../shared/item-relationships/relationship-type.model'; @@ -13,11 +8,16 @@ import { Relationship } from '../shared/item-relationships/relationship.model'; import { Item } from '../shared/item.model'; import { PageInfo } from '../shared/page-info.model'; import { PaginatedList } from './paginated-list'; -import { RelationshipService } from './relationship.service'; -import { RemoteData } from './remote-data'; import { DeleteRequest, FindListOptions } from './request.models'; -import { RequestEntry } from './request.reducer'; +import { RelationshipService } from './relationship.service'; import { RequestService } from './request.service'; +import { RemoteData } from './remote-data'; +import { RequestEntry } from './request.reducer'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/remote-data-build.service.mock'; +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; +import { spyOnOperator } from '../../shared/testing/utils.test'; describe('RelationshipService', () => { let service: RelationshipService; @@ -159,8 +159,8 @@ describe('RelationshipService', () => { it('should clear the cache of the related items', () => { expect(objectCache.remove).toHaveBeenCalledWith(relatedItem1._links.self.href); expect(objectCache.remove).toHaveBeenCalledWith(item._links.self.href); - expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(relatedItem1.self); - expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(item.self); + expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(relatedItem1.uuid); + expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(item.uuid); }); }); @@ -175,37 +175,6 @@ describe('RelationshipService', () => { }); }); - describe('getRelatedItems', () => { - let mockItem; - - beforeEach(() => { - mockItem = { uuid: 'someid' } as Item; - - spyOn(service, 'getItemRelationshipsArray').and.returnValue(observableOf(relationships)); - - spyOnOperator(ItemRelationshipsUtils, 'relationsToItems').and.returnValue((v) => v); - }); - - it('should call getItemRelationshipsArray with the correct params', (done) => { - service.getRelatedItems(mockItem).subscribe(() => { - expect(service.getItemRelationshipsArray).toHaveBeenCalledWith( - mockItem, - followLink('leftItem'), - followLink('rightItem'), - followLink('relationshipType') - ); - done(); - }); - }); - - it('should use the relationsToItems operator', (done) => { - service.getRelatedItems(mockItem).subscribe(() => { - expect(ItemRelationshipsUtils.relationsToItems).toHaveBeenCalledWith(mockItem.uuid); - done(); - }); - }); - }); - describe('getRelatedItemsByLabel', () => { let relationsList; let mockItem; @@ -258,7 +227,6 @@ describe('RelationshipService', () => { }); }); }) - }); function getRemotedataObservable(obj: any): Observable> { diff --git a/src/app/core/data/relationship.service.ts b/src/app/core/data/relationship.service.ts index 3d68e70206..6d9b237db0 100644 --- a/src/app/core/data/relationship.service.ts +++ b/src/app/core/data/relationship.service.ts @@ -1,21 +1,14 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { MemoizedSelector, select, Store } from '@ngrx/store'; -import { combineLatest, combineLatest as observableCombineLatest } from 'rxjs'; +import { combineLatest as observableCombineLatest } from 'rxjs'; import { Observable } from 'rxjs/internal/Observable'; import { distinctUntilChanged, filter, map, mergeMap, startWith, switchMap, take, tap } from 'rxjs/operators'; -import { - compareArraysUsingIds, - paginatedRelationsToItems, - relationsToItems -} from '../../+item-page/simple/item-types/shared/item-relationships-utils'; +import { compareArraysUsingIds, paginatedRelationsToItems, relationsToItems } from '../../+item-page/simple/item-types/shared/item-relationships-utils'; import { AppState, keySelector } from '../../app.reducer'; import { hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; import { ReorderableRelationship } from '../../shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component'; -import { - RemoveNameVariantAction, - SetNameVariantAction -} from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.actions'; +import { RemoveNameVariantAction, SetNameVariantAction } from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.actions'; import { NameVariantListState } from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; @@ -31,12 +24,7 @@ import { RelationshipType } from '../shared/item-relationships/relationship-type import { Relationship } from '../shared/item-relationships/relationship.model'; import { RELATIONSHIP } from '../shared/item-relationships/relationship.resource-type'; import { Item } from '../shared/item.model'; -import { - configureRequest, - getRemoteDataPayload, - getResponseFromEntry, - getSucceededRemoteData -} from '../shared/operators'; +import { configureRequest, getFirstSucceededRemoteDataPayload, getRemoteDataPayload, getResponseFromEntry, getSucceededRemoteData } from '../shared/operators'; import { DataService } from './data.service'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; import { ItemDataService } from './item-data.service'; @@ -55,6 +43,19 @@ const relationshipStateSelector = (listID: string, itemID: string): MemoizedSele return keySelector(itemID, relationshipListStateSelector(listID)); }; +/** + * Return true if the Item in the payload of the source observable matches + * the given Item by UUID + * + * @param itemCheck the Item to compare with + */ +const compareItemsByUUID = (itemCheck: Item) => + (source: Observable>): Observable => + source.pipe( + getFirstSucceededRemoteDataPayload(), + map((item: Item) => item.uuid === itemCheck.uuid) + ); + /** * The service handling all relationship requests */ @@ -62,6 +63,7 @@ const relationshipStateSelector = (listID: string, itemID: string): MemoizedSele @dataService(RELATIONSHIP) export class RelationshipService extends DataService { protected linkPath = 'relationships'; + protected responseMsToLive = 15 * 60 * 1000; constructor(protected itemService: ItemDataService, protected requestService: RequestService, @@ -101,11 +103,7 @@ export class RelationshipService extends DataService { configureRequest(this.requestService), switchMap((restRequest: RestRequest) => this.requestService.getByUUID(restRequest.uuid)), getResponseFromEntry(), - switchMap((response) => - this.clearRelatedCache(id).pipe( - map(() => response), - ) - ), + tap(() => this.refreshRelationshipItemsInCacheByRelationship(id)), ); } @@ -132,8 +130,8 @@ export class RelationshipService extends DataService { configureRequest(this.requestService), switchMap((restRequest: RestRequest) => this.requestService.getByUUID(restRequest.uuid)), getResponseFromEntry(), - tap(() => this.removeRelationshipItemsFromCache(item1)), - tap(() => this.removeRelationshipItemsFromCache(item2)) + tap(() => this.refreshRelationshipItemsInCache(item1)), + tap(() => this.refreshRelationshipItemsInCache(item2)) ) as Observable; } @@ -141,19 +139,19 @@ export class RelationshipService extends DataService { * Method to remove two items of a relationship from the cache using the identifier of the relationship * @param relationshipId The identifier of the relationship */ - private removeRelationshipItemsFromCacheByRelationship(relationshipId: string) { - this.findById(relationshipId).pipe( + private refreshRelationshipItemsInCacheByRelationship(relationshipId: string) { + this.findById(relationshipId, followLink('leftItem'), followLink('rightItem')).pipe( getSucceededRemoteData(), getRemoteDataPayload(), - switchMap((rel: Relationship) => combineLatest( + switchMap((rel: Relationship) => observableCombineLatest( rel.leftItem.pipe(getSucceededRemoteData(), getRemoteDataPayload()), rel.rightItem.pipe(getSucceededRemoteData(), getRemoteDataPayload()) ) ), take(1) ).subscribe(([item1, item2]) => { - this.removeRelationshipItemsFromCache(item1); - this.removeRelationshipItemsFromCache(item2); + this.refreshRelationshipItemsInCache(item1); + this.refreshRelationshipItemsInCache(item2); }) } @@ -161,13 +159,13 @@ export class RelationshipService extends DataService { * Method to remove an item that's part of a relationship from the cache * @param item The item to remove from the cache */ - private removeRelationshipItemsFromCache(item) { + public refreshRelationshipItemsInCache(item) { this.objectCache.remove(item._links.self.href); this.requestService.removeByHrefSubstring(item.uuid); - combineLatest( + observableCombineLatest([ this.objectCache.hasBySelfLinkObservable(item._links.self.href), - this.requestService.hasByHrefObservable(item.uuid) - ).pipe( + this.requestService.hasByHrefObservable(item.self) + ]).pipe( filter(([existsInOC, existsInRC]) => !existsInOC && !existsInRC), take(1), switchMap(() => this.itemService.findByHref(item._links.self.href).pipe(take(1))) @@ -176,7 +174,10 @@ export class RelationshipService extends DataService { /** * Get an item's relationships in the form of an array - * @param item + * + * @param item The {@link Item} to get {@link Relationship}s for + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s + * should be automatically resolved */ getItemRelationshipsArray(item: Item, ...linksToFollow: Array>): Observable { return this.findAllByHref(item._links.relationships.href, undefined, ...linksToFollow).pipe( @@ -275,10 +276,10 @@ export class RelationshipService extends DataService { getRelationshipsByRelatedItemIds(item: Item, uuids: string[]): Observable { return this.getItemRelationshipsArray(item, followLink('leftItem'), followLink('rightItem')).pipe( switchMap((relationships: Relationship[]) => { - return observableCombineLatest(...relationships.map((relationship: Relationship) => { + return observableCombineLatest(relationships.map((relationship: Relationship) => { const isLeftItem$ = this.isItemInUUIDArray(relationship.leftItem, uuids); const isRightItem$ = this.isItemInUUIDArray(relationship.rightItem, uuids); - return observableCombineLatest(isLeftItem$, isRightItem$).pipe( + return observableCombineLatest([isLeftItem$, isRightItem$]).pipe( filter(([isLeftItem, isRightItem]) => isLeftItem || isRightItem), map(() => relationship), startWith(undefined) @@ -304,34 +305,31 @@ export class RelationshipService extends DataService { * @param label The rightward or leftward type of the relationship */ getRelationshipByItemsAndLabel(item1: Item, item2: Item, label: string, options?: FindListOptions): Observable { - return this.getItemRelationshipsByLabel(item1, label, options, followLink('relationshipType'), followLink('leftItem'), followLink('rightItem')) - .pipe( + return this.getItemRelationshipsByLabel( + item1, + label, + options, + followLink('relationshipType'), + followLink('leftItem'), + followLink('rightItem') + ).pipe( getSucceededRemoteData(), - isNotEmptyOperator(), - map((relationshipListRD: RemoteData>) => relationshipListRD.payload.page), - mergeMap((relationships: Relationship[]) => { - return observableCombineLatest(...relationships.map((relationship: Relationship) => { - return observableCombineLatest( - this.isItemMatchWithItemRD(relationship.leftItem, item2), - this.isItemMatchWithItemRD(relationship.rightItem, item2) - ).pipe( - map(([isLeftItem, isRightItem]) => isLeftItem || isRightItem), - map((isMatch) => isMatch ? relationship : undefined) - ); - })) + // the mergemap below will emit all elements of the list as separate events + mergeMap((relationshipListRD: RemoteData>) => relationshipListRD.payload.page), + mergeMap((relationship: Relationship) => { + return observableCombineLatest([ + this.itemService.findByHref(relationship._links.leftItem.href).pipe(compareItemsByUUID(item2)), + this.itemService.findByHref(relationship._links.rightItem.href).pipe(compareItemsByUUID(item2)) + ]).pipe( + map(([isLeftItem, isRightItem]) => isLeftItem || isRightItem), + map((isMatch) => isMatch ? relationship : undefined) + ); }), - map((relationships: Relationship[]) => relationships.find(((relationship) => hasValue(relationship)))) + filter((relationship) => hasValue(relationship)), + take(1) ) } - private isItemMatchWithItemRD(itemRD$: Observable>, itemCheck: Item): Observable { - return itemRD$.pipe( - getSucceededRemoteData(), - map((itemRD: RemoteData) => itemRD.payload), - map((item: Item) => item.uuid === itemCheck.uuid) - ); - } - /** * Method to set the name variant for specific list and item * @param listID The list for which to save the name variant @@ -378,7 +376,7 @@ export class RelationshipService extends DataService { * @param nameVariant The name variant to set for the matching relationship */ public updateNameVariant(item1: Item, item2: Item, relationshipLabel: string, nameVariant: string): Observable> { - const update$: Observable> = this.getRelationshipByItemsAndLabel(item1, item2, relationshipLabel) + return this.getRelationshipByItemsAndLabel(item1, item2, relationshipLabel) .pipe( switchMap((relation: Relationship) => relation.relationshipType.pipe( @@ -400,16 +398,6 @@ export class RelationshipService extends DataService { return this.update(updatedRelationship); }), ); - - update$.pipe( - filter((relationshipRD: RemoteData) => relationshipRD.state === RemoteDataState.RequestPending), - take(1), - ).subscribe(() => { - this.removeRelationshipItemsFromCache(item1); - this.removeRelationshipItemsFromCache(item2); - }); - - return update$ } /** @@ -432,7 +420,7 @@ export class RelationshipService extends DataService { take(1), ).subscribe((relationshipRD: RemoteData) => { if (relationshipRD.state === RemoteDataState.ResponsePending) { - this.removeRelationshipItemsFromCacheByRelationship(reoRel.relationship.id); + this.refreshRelationshipItemsInCacheByRelationship(reoRel.relationship.id); } }); @@ -440,18 +428,11 @@ export class RelationshipService extends DataService { } /** - * Clear object and request caches of the items related to a relationship (left and right items) - * @param uuid The uuid of the relationship for which to clear the related items from the cache + * Patch isn't supported on the relationship endpoint, so use put instead. + * + * @param object the {@link Relationship} to update */ - clearRelatedCache(uuid: string): Observable { - return this.findById(uuid).pipe( - getSucceededRemoteData(), - map((rd: RemoteData) => { - this.objectCache.remove(rd.payload._links.leftItem.href); - this.objectCache.remove(rd.payload._links.rightItem.href); - this.requestService.removeByHrefSubstring(rd.payload._links.leftItem.href); - this.requestService.removeByHrefSubstring(rd.payload._links.rightItem.href); - }) - ); + update(object: Relationship): Observable> { + return this.put(object); } } diff --git a/src/app/core/eperson/eperson-data.service.ts b/src/app/core/eperson/eperson-data.service.ts index 379daf5dcd..415977a46f 100644 --- a/src/app/core/eperson/eperson-data.service.ts +++ b/src/app/core/eperson/eperson-data.service.ts @@ -224,7 +224,7 @@ export class EPersonDataService extends DataService { * @param ePerson The EPerson to delete */ public deleteEPerson(ePerson: EPerson): Observable { - return this.delete(ePerson.id); + return this.delete(ePerson.id).pipe(map((response: RestResponse) => response.isSuccessful)); } /** diff --git a/src/app/core/eperson/group-data.service.ts b/src/app/core/eperson/group-data.service.ts index 75f00310ec..a10b46c3d0 100644 --- a/src/app/core/eperson/group-data.service.ts +++ b/src/app/core/eperson/group-data.service.ts @@ -135,7 +135,7 @@ export class GroupDataService extends DataService { * @param id The group id to delete */ public deleteGroup(group: Group): Observable { - return this.delete(group.id); + return this.delete(group.id).pipe(map((response: RestResponse) => response.isSuccessful)); } /** diff --git a/src/app/core/json-patch/builder/json-patch-operations-builder.ts b/src/app/core/json-patch/builder/json-patch-operations-builder.ts index c45183b4ef..eb54265318 100644 --- a/src/app/core/json-patch/builder/json-patch-operations-builder.ts +++ b/src/app/core/json-patch/builder/json-patch-operations-builder.ts @@ -1,13 +1,9 @@ import { Store } from '@ngrx/store'; import { CoreState } from '../../core.reducers'; -import { - NewPatchAddOperationAction, - NewPatchRemoveOperationAction, - NewPatchReplaceOperationAction -} from '../json-patch-operations.actions'; +import { NewPatchAddOperationAction, NewPatchMoveOperationAction, NewPatchRemoveOperationAction, NewPatchReplaceOperationAction } from '../json-patch-operations.actions'; import { JsonPatchOperationPathObject } from './json-patch-operation-path-combiner'; import { Injectable } from '@angular/core'; -import { isEmpty, isNotEmpty } from '../../../shared/empty.util'; +import { hasNoValue, isEmpty, isNotEmpty } from '../../../shared/empty.util'; import { dateToISOFormat } from '../../../shared/date.util'; import { AuthorityValue } from '../../integration/models/authority.value'; import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model'; @@ -53,12 +49,35 @@ export class JsonPatchOperationsBuilder { * a boolean representing if the value to be added is a plain text value */ replace(path: JsonPatchOperationPathObject, value, plain = false) { + if (hasNoValue(value) || (typeof value === 'object' && hasNoValue(value.value))) { + this.remove(path); + } else { + this.store.dispatch( + new NewPatchReplaceOperationAction( + path.rootElement, + path.subRootElement, + path.path, + this.prepareValue(value, plain, false))); + } + } + + /** + * Dispatch a new NewPatchMoveOperationAction + * + * @param path + * the new path tho move to + * @param prevPath + * the original path to move from + */ + move(path: JsonPatchOperationPathObject, prevPath: string) { this.store.dispatch( - new NewPatchReplaceOperationAction( + new NewPatchMoveOperationAction( path.rootElement, path.subRootElement, - path.path, - this.prepareValue(value, plain, false))); + prevPath, + path.path + ) + ); } /** diff --git a/src/app/core/json-patch/json-patch-operations.reducer.ts b/src/app/core/json-patch/json-patch-operations.reducer.ts index 906d5e0331..648221f512 100644 --- a/src/app/core/json-patch/json-patch-operations.reducer.ts +++ b/src/app/core/json-patch/json-patch-operations.reducer.ts @@ -196,7 +196,8 @@ function newOperation(state: JsonPatchOperationsState, action): JsonPatchOperati body, action.type, action.payload.path, - hasValue(action.payload.value) ? action.payload.value : null); + hasValue(action.payload.value) ? action.payload.value : null, + hasValue(action.payload.from) ? action.payload.from : null); if (hasValue(newState[ action.payload.resourceType ]) && hasValue(newState[ action.payload.resourceType ].children)) { @@ -293,7 +294,21 @@ function flushOperation(state: JsonPatchOperationsState, action: FlushPatchOpera } } -function addOperationToList(body: JsonPatchOperationObject[], actionType, targetPath, value?) { +/** + * Add a new operation to a patch + * + * @param body + * The current patch + * @param actionType + * The type of operation to add + * @param targetPath + * The path for the operation + * @param value + * The new value + * @param fromPath + * The previous path (in case of a move operation) + */ +function addOperationToList(body: JsonPatchOperationObject[], actionType, targetPath, value?, fromPath?) { const newBody = Array.from(body); switch (actionType) { case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_ADD_OPERATION: @@ -313,6 +328,9 @@ function addOperationToList(body: JsonPatchOperationObject[], actionType, target case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_REMOVE_OPERATION: newBody.push(makeOperationEntry({ op: JsonPatchOperationType.remove, path: targetPath })); break; + case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_MOVE_OPERATION: + newBody.push(makeOperationEntry({ op: JsonPatchOperationType.move, from: fromPath, path: targetPath })); + break; } return newBody; } diff --git a/src/app/core/json-patch/json-patch-operations.service.spec.ts b/src/app/core/json-patch/json-patch-operations.service.spec.ts index 583b90a01f..fb9e641441 100644 --- a/src/app/core/json-patch/json-patch-operations.service.spec.ts +++ b/src/app/core/json-patch/json-patch-operations.service.spec.ts @@ -21,10 +21,8 @@ import { RollbacktPatchOperationsAction, StartTransactionPatchOperationsAction } from './json-patch-operations.actions'; -import { StoreMock } from '../../shared/testing/store.mock'; import { RequestEntry } from '../data/request.reducer'; import { catchError } from 'rxjs/operators'; -import { storeModuleConfig } from '../../app.reducer'; class TestService extends JsonPatchOperationsService { protected linkPath = ''; @@ -99,27 +97,22 @@ describe('JsonPatchOperationsService test suite', () => { } - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [ - StoreModule.forRoot({}, storeModuleConfig), - ], - providers: [ - { provide: Store, useClass: StoreMock } - ] - }).compileComponents(); - })); + function getStore() { + return jasmine.createSpyObj('store', { + dispatch: {}, + select: observableOf(mockState['json/patch'][testJsonPatchResourceType]), + pipe: observableOf(true) + }); + } beforeEach(() => { - store = TestBed.get(Store); + store = getStore(); requestService = getMockRequestService(getRequestEntry$(true)); rdbService = getMockRemoteDataBuildService(); scheduler = getTestScheduler(); halService = new HALEndpointServiceStub(resourceEndpointURL); service = initTestService(); - spyOn(store, 'select').and.returnValue(observableOf(mockState['json/patch'][testJsonPatchResourceType])); - spyOn(store, 'dispatch').and.callThrough(); spyOn(Date.prototype, 'getTime').and.callFake(() => { return timestamp; }); @@ -164,7 +157,7 @@ describe('JsonPatchOperationsService test suite', () => { describe('when request is not successful', () => { beforeEach(() => { - store = TestBed.get(Store); + store = getStore(); requestService = getMockRequestService(getRequestEntry$(false)); rdbService = getMockRemoteDataBuildService(); scheduler = getTestScheduler(); @@ -227,7 +220,7 @@ describe('JsonPatchOperationsService test suite', () => { describe('when request is not successful', () => { beforeEach(() => { - store = TestBed.get(Store); + store = getStore(); requestService = getMockRequestService(getRequestEntry$(false)); rdbService = getMockRemoteDataBuildService(); scheduler = getTestScheduler(); diff --git a/src/app/core/registry/registry.service.spec.ts b/src/app/core/registry/registry.service.spec.ts index 8e4e191e0f..43725a2d33 100644 --- a/src/app/core/registry/registry.service.spec.ts +++ b/src/app/core/registry/registry.service.spec.ts @@ -127,7 +127,7 @@ describe('RegistryService', () => { findAll: createSuccessfulRemoteDataObject$(createPaginatedList(mockSchemasList)), findById: createSuccessfulRemoteDataObject$(mockSchemasList[0]), createOrUpdateMetadataSchema: createSuccessfulRemoteDataObject$(mockSchemasList[0]), - deleteAndReturnResponse: observableOf(new RestResponse(true, 200, 'OK')), + delete: observableOf(new RestResponse(true, 200, 'OK')), clearRequests: observableOf('href') }); @@ -136,7 +136,7 @@ describe('RegistryService', () => { findById: createSuccessfulRemoteDataObject$(mockFieldsList[0]), create: createSuccessfulRemoteDataObject$(mockFieldsList[0]), put: createSuccessfulRemoteDataObject$(mockFieldsList[0]), - deleteAndReturnResponse: observableOf(new RestResponse(true, 200, 'OK')), + delete: observableOf(new RestResponse(true, 200, 'OK')), clearRequests: observableOf('href') }); } diff --git a/src/app/core/registry/registry.service.ts b/src/app/core/registry/registry.service.ts index cad5478f7a..72de6ec793 100644 --- a/src/app/core/registry/registry.service.ts +++ b/src/app/core/registry/registry.service.ts @@ -223,7 +223,7 @@ export class RegistryService { * @param id The id of the metadata schema to delete */ public deleteMetadataSchema(id: number): Observable { - return this.metadataSchemaService.deleteAndReturnResponse(`${id}`); + return this.metadataSchemaService.delete(`${id}`); } /** @@ -269,7 +269,7 @@ export class RegistryService { * @param id The id of the metadata field to delete */ public deleteMetadataField(id: number): Observable { - return this.metadataFieldService.deleteAndReturnResponse(`${id}`); + return this.metadataFieldService.delete(`${id}`); } /** * Method that clears a cached metadata field request and returns its REST url diff --git a/src/app/core/resource-policy/resource-policy.service.spec.ts b/src/app/core/resource-policy/resource-policy.service.spec.ts index 1c6ac47405..d2823b2dd0 100644 --- a/src/app/core/resource-policy/resource-policy.service.spec.ts +++ b/src/app/core/resource-policy/resource-policy.service.spec.ts @@ -96,6 +96,8 @@ describe('ResourcePolicyService', () => { }); responseCacheEntry = new RequestEntry(); + responseCacheEntry.request = { href: 'https://rest.api/' } as any; + responseCacheEntry.completed = true; responseCacheEntry.response = new RestResponse(true, 200, 'Success'); requestService = jasmine.createSpyObj('requestService', { diff --git a/src/app/core/resource-policy/resource-policy.service.ts b/src/app/core/resource-policy/resource-policy.service.ts index 291920c35a..47b188b5c5 100644 --- a/src/app/core/resource-policy/resource-policy.service.ts +++ b/src/app/core/resource-policy/resource-policy.service.ts @@ -24,6 +24,8 @@ import { PaginatedList } from '../data/paginated-list'; import { ActionType } from './models/action-type.model'; import { RequestParam } from '../cache/models/request-param.model'; import { isNotEmpty } from '../../shared/empty.util'; +import { map } from 'rxjs/operators'; +import { RestResponse } from '../cache/response.models'; /* tslint:disable:max-classes-per-file */ @@ -100,7 +102,7 @@ export class ResourcePolicyService { * @return an observable that emits true when the deletion was successful, false when it failed */ delete(resourcePolicyID: string): Observable { - return this.dataService.delete(resourcePolicyID); + return this.dataService.delete(resourcePolicyID).pipe(map((response: RestResponse) => response.isSuccessful)); } /** diff --git a/src/app/core/shared/context.model.ts b/src/app/core/shared/context.model.ts index ff24b7d090..4699a7977b 100644 --- a/src/app/core/shared/context.model.ts +++ b/src/app/core/shared/context.model.ts @@ -9,7 +9,8 @@ export enum Context { Workflow = 'workflow', Workspace = 'workspace', AdminMenu = 'adminMenu', - SubmissionModal = 'submissionModal', + EntitySearchModalWithNameVariants = 'EntitySearchModalWithNameVariants', + EntitySearchModal = 'EntitySearchModal', AdminSearch = 'adminSearch', AdminWorkflowSearch = 'adminWorkflowSearch', } diff --git a/src/app/core/shared/metadata.models.ts b/src/app/core/shared/metadata.models.ts index 085cdb4504..c29ac3bd2b 100644 --- a/src/app/core/shared/metadata.models.ts +++ b/src/app/core/shared/metadata.models.ts @@ -3,7 +3,7 @@ import { autoserialize, Serialize, Deserialize } from 'cerialize'; import { hasValue } from '../../shared/empty.util'; /* tslint:disable:max-classes-per-file */ -const VIRTUAL_METADATA_PREFIX = 'virtual::'; +export const VIRTUAL_METADATA_PREFIX = 'virtual::'; /** A single metadata value and its properties. */ export interface MetadataValueInterface { diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index 4abb71350b..7516cc6532 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -66,7 +66,7 @@ export const getPaginatedListPayload = () => export const getSucceededRemoteData = () => (source: Observable>): Observable> => - source.pipe(find((rd: RemoteData) => rd.hasSucceeded)); + source.pipe(filter((rd: RemoteData) => rd.hasSucceeded), take(1)); export const getSucceededRemoteWithNotEmptyData = () => (source: Observable>): Observable> => diff --git a/src/app/core/submission/submission-object-data.service.spec.ts b/src/app/core/submission/submission-object-data.service.spec.ts index f46a465edb..931a7ae7d5 100644 --- a/src/app/core/submission/submission-object-data.service.spec.ts +++ b/src/app/core/submission/submission-object-data.service.spec.ts @@ -7,12 +7,14 @@ import { SubmissionObjectDataService } from './submission-object-data.service'; import { SubmissionScopeType } from './submission-scope-type'; import { WorkflowItemDataService } from './workflowitem-data.service'; import { WorkspaceitemDataService } from './workspaceitem-data.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; describe('SubmissionObjectDataService', () => { let service: SubmissionObjectDataService; let submissionService: SubmissionService; let workspaceitemDataService: WorkspaceitemDataService; let workflowItemDataService: WorkflowItemDataService; + let halService: HALEndpointService; const submissionId = '1234'; const wsiResult = 'wsiResult' as any; @@ -25,6 +27,9 @@ describe('SubmissionObjectDataService', () => { workflowItemDataService = jasmine.createSpyObj('WorkflowItemDataService', { findById: wfiResult }); + halService = jasmine.createSpyObj('HALEndpointService', { + getEndpoint: '/workspaceItem' + }); }); describe('findById', () => { @@ -32,7 +37,7 @@ describe('SubmissionObjectDataService', () => { submissionService = jasmine.createSpyObj('SubmissionService', { getSubmissionScope: {} }); - service = new SubmissionObjectDataService(workspaceitemDataService, workflowItemDataService, submissionService); + service = new SubmissionObjectDataService(workspaceitemDataService, workflowItemDataService, submissionService, halService); service.findById(submissionId); expect(submissionService.getSubmissionScope).toHaveBeenCalled(); }); @@ -42,7 +47,7 @@ describe('SubmissionObjectDataService', () => { submissionService = jasmine.createSpyObj('SubmissionService', { getSubmissionScope: SubmissionScopeType.WorkspaceItem }); - service = new SubmissionObjectDataService(workspaceitemDataService, workflowItemDataService, submissionService); + service = new SubmissionObjectDataService(workspaceitemDataService, workflowItemDataService, submissionService, halService); }); it('should forward the result of WorkspaceitemDataService.findByIdAndIDType()', () => { @@ -57,7 +62,7 @@ describe('SubmissionObjectDataService', () => { submissionService = jasmine.createSpyObj('SubmissionService', { getSubmissionScope: SubmissionScopeType.WorkflowItem }); - service = new SubmissionObjectDataService(workspaceitemDataService, workflowItemDataService, submissionService); + service = new SubmissionObjectDataService(workspaceitemDataService, workflowItemDataService, submissionService, halService); }); it('should forward the result of WorkflowItemDataService.findByIdAndIDType()', () => { @@ -72,7 +77,7 @@ describe('SubmissionObjectDataService', () => { submissionService = jasmine.createSpyObj('SubmissionService', { getSubmissionScope: 'Something else' }); - service = new SubmissionObjectDataService(workspaceitemDataService, workflowItemDataService, submissionService); + service = new SubmissionObjectDataService(workspaceitemDataService, workflowItemDataService, submissionService, halService); }); it('shouldn\'t call any data service methods', () => { diff --git a/src/app/core/submission/submission-object-data.service.ts b/src/app/core/submission/submission-object-data.service.ts index 0b6d65c758..502609a032 100644 --- a/src/app/core/submission/submission-object-data.service.ts +++ b/src/app/core/submission/submission-object-data.service.ts @@ -8,6 +8,9 @@ import { SubmissionObject } from './models/submission-object.model'; import { SubmissionScopeType } from './submission-scope-type'; import { WorkflowItemDataService } from './workflowitem-data.service'; import { WorkspaceitemDataService } from './workspaceitem-data.service'; +import { DataService } from '../data/data.service'; +import { map } from 'rxjs/operators'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; /** * A service to retrieve submission objects (WorkspaceItem/WorkflowItem) @@ -20,10 +23,22 @@ export class SubmissionObjectDataService { constructor( private workspaceitemDataService: WorkspaceitemDataService, private workflowItemDataService: WorkflowItemDataService, - private submissionService: SubmissionService + private submissionService: SubmissionService, + private halService: HALEndpointService ) { } + /** + * Create the HREF for a specific object based on its identifier + * @param id The identifier for the object + */ + getHrefByID(id): Observable { + const dataService: DataService = this.submissionService.getSubmissionScope() === SubmissionScopeType.WorkspaceItem ? this.workspaceitemDataService : this.workflowItemDataService; + + return this.halService.getEndpoint(dataService.getLinkPath()).pipe( + map((endpoint: string) => dataService.getIDHref(endpoint, encodeURIComponent(id)))); + } + /** * Retrieve a submission object based on its ID. * diff --git a/src/app/core/submission/submission-response-parsing.service.ts b/src/app/core/submission/submission-response-parsing.service.ts index afabde831a..4bbd93b18d 100644 --- a/src/app/core/submission/submission-response-parsing.service.ts +++ b/src/app/core/submission/submission-response-parsing.service.ts @@ -176,5 +176,4 @@ export class SubmissionResponseParsingService extends BaseResponseParsingService return definition; } - } diff --git a/src/app/core/submission/workflowitem-data.service.ts b/src/app/core/submission/workflowitem-data.service.ts index c82f7bf0b5..9b7555808d 100644 --- a/src/app/core/submission/workflowitem-data.service.ts +++ b/src/app/core/submission/workflowitem-data.service.ts @@ -17,6 +17,7 @@ import { Observable } from 'rxjs'; import { find, map } from 'rxjs/operators'; import { hasValue } from '../../shared/empty.util'; import { RequestEntry } from '../data/request.reducer'; +import { RestResponse } from '../cache/response.models'; /** * A service that provides methods to make REST requests with workflow items endpoint. @@ -44,7 +45,7 @@ export class WorkflowItemDataService extends DataService { * @param id The Workflow Item's id to be removed * @return an observable that emits true when the deletion was successful, false when it failed */ - delete(id: string): Observable { + delete(id: string): Observable { return this.deleteWFI(id, true) } @@ -54,7 +55,7 @@ export class WorkflowItemDataService extends DataService { * @return an observable that emits true when sending back the item was successful, false when it failed */ sendBack(id: string): Observable { - return this.deleteWFI(id, false) + return this.deleteWFI(id, false).pipe(map((response: RestResponse) => response.isSuccessful)); } /** @@ -64,7 +65,7 @@ export class WorkflowItemDataService extends DataService { * When true, the workflow item and its item will be permanently expunged on the server * When false, the workflow item will be removed, but the item will still be available as a workspace item */ - private deleteWFI(id: string, expunge: boolean): Observable { + private deleteWFI(id: string, expunge: boolean): Observable { const requestId = this.requestService.generateRequestId(); const hrefObs = this.halService.getEndpoint(this.linkPath).pipe( @@ -82,7 +83,7 @@ export class WorkflowItemDataService extends DataService { return this.requestService.getByUUID(requestId).pipe( find((request: RequestEntry) => request.completed), - map((request: RequestEntry) => request.response.isSuccessful) + map((request: RequestEntry) => request.response) ); } } diff --git a/src/app/core/submission/workspaceitem-data.service.ts b/src/app/core/submission/workspaceitem-data.service.ts index fcb85cc8b4..224bb64706 100644 --- a/src/app/core/submission/workspaceitem-data.service.ts +++ b/src/app/core/submission/workspaceitem-data.service.ts @@ -21,7 +21,6 @@ import { WorkspaceItem } from './models/workspaceitem.model'; @dataService(WorkspaceItem.type) export class WorkspaceitemDataService extends DataService { protected linkPath = 'workspaceitems'; - protected responseMsToLive = 10 * 1000; constructor( protected comparator: DSOChangeAnalyzer, diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.html index fb69ed92f5..636eac2309 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.html +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.html @@ -1 +1 @@ - \ No newline at end of file + diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.html index 53713b47ee..1faa4ed5db 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.html +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.html @@ -1 +1 @@ - \ No newline at end of file + diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.html index 594a0e0dc1..cb3220bd8e 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.html +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.html @@ -1 +1 @@ - \ No newline at end of file + diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.html index 0f9b5894f9..1ae772a3c3 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.html +++ b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.html @@ -17,7 +17,7 @@
- +

diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.html index 16e2a8b847..75d29781b7 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.html +++ b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.html @@ -17,7 +17,7 @@
- +

diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.html index 4902eec71e..8c7e5c2f44 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.html +++ b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.html @@ -17,7 +17,7 @@
- +

diff --git a/src/app/entity-groups/journal-entities/item-list-elements/journal-issue/journal-issue-list-element.component.html b/src/app/entity-groups/journal-entities/item-list-elements/journal-issue/journal-issue-list-element.component.html index 398feea260..395d6eba20 100644 --- a/src/app/entity-groups/journal-entities/item-list-elements/journal-issue/journal-issue-list-element.component.html +++ b/src/app/entity-groups/journal-entities/item-list-elements/journal-issue/journal-issue-list-element.component.html @@ -1 +1 @@ - \ No newline at end of file + diff --git a/src/app/entity-groups/journal-entities/item-list-elements/journal-volume/journal-volume-list-element.component.html b/src/app/entity-groups/journal-entities/item-list-elements/journal-volume/journal-volume-list-element.component.html index bf967e6e78..a3f604900e 100644 --- a/src/app/entity-groups/journal-entities/item-list-elements/journal-volume/journal-volume-list-element.component.html +++ b/src/app/entity-groups/journal-entities/item-list-elements/journal-volume/journal-volume-list-element.component.html @@ -1 +1 @@ - \ No newline at end of file + diff --git a/src/app/entity-groups/journal-entities/item-list-elements/journal/journal-list-element.component.html b/src/app/entity-groups/journal-entities/item-list-elements/journal/journal-list-element.component.html index 3e4dfb0b48..ca028c1659 100644 --- a/src/app/entity-groups/journal-entities/item-list-elements/journal/journal-list-element.component.html +++ b/src/app/entity-groups/journal-entities/item-list-elements/journal/journal-list-element.component.html @@ -1 +1 @@ - \ No newline at end of file + diff --git a/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-issue/journal-issue-search-result-list-element.component.html b/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-issue/journal-issue-search-result-list-element.component.html index 38094c5c79..45482972ec 100644 --- a/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-issue/journal-issue-search-result-list-element.component.html +++ b/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-issue/journal-issue-search-result-list-element.component.html @@ -1,4 +1,4 @@ - + + + + diff --git a/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.html index a431f5979f..8a53609c55 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.html +++ b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.html @@ -1 +1 @@ - \ No newline at end of file + diff --git a/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.html index 0c87599399..4a240baca9 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.html +++ b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.html @@ -1 +1 @@ - \ No newline at end of file + diff --git a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/org-unit/org-unit-search-result-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/org-unit/org-unit-search-result-grid-element.component.html index 1f64856583..cf4e4a6b52 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/org-unit/org-unit-search-result-grid-element.component.html +++ b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/org-unit/org-unit-search-result-grid-element.component.html @@ -17,7 +17,7 @@
- +

diff --git a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.html index cbe93b2545..1b45c7c4f9 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.html +++ b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.html @@ -17,7 +17,7 @@
- +

diff --git a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.html index 22182d50be..ac3c3ea453 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.html +++ b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.html @@ -17,7 +17,7 @@
- +

diff --git a/src/app/entity-groups/research-entities/item-list-elements/org-unit/org-unit-list-element.component.html b/src/app/entity-groups/research-entities/item-list-elements/org-unit/org-unit-list-element.component.html index 03ef45c7a4..cf2dc3e61c 100644 --- a/src/app/entity-groups/research-entities/item-list-elements/org-unit/org-unit-list-element.component.html +++ b/src/app/entity-groups/research-entities/item-list-elements/org-unit/org-unit-list-element.component.html @@ -1 +1 @@ - \ No newline at end of file + diff --git a/src/app/entity-groups/research-entities/item-list-elements/person/person-list-element.component.html b/src/app/entity-groups/research-entities/item-list-elements/person/person-list-element.component.html index dbc3a42a05..290635ea27 100644 --- a/src/app/entity-groups/research-entities/item-list-elements/person/person-list-element.component.html +++ b/src/app/entity-groups/research-entities/item-list-elements/person/person-list-element.component.html @@ -1 +1 @@ - \ No newline at end of file + diff --git a/src/app/entity-groups/research-entities/item-list-elements/project/project-list-element.component.html b/src/app/entity-groups/research-entities/item-list-elements/project/project-list-element.component.html index 8f74452eaa..6f229c00e0 100644 --- a/src/app/entity-groups/research-entities/item-list-elements/project/project-list-element.component.html +++ b/src/app/entity-groups/research-entities/item-list-elements/project/project-list-element.component.html @@ -1 +1 @@ - \ No newline at end of file + diff --git a/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/org-unit/org-unit-search-result-list-element.component.html b/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/org-unit/org-unit-search-result-list-element.component.html index f08d0fdc11..5f570cb021 100644 --- a/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/org-unit/org-unit-search-result-list-element.component.html +++ b/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/org-unit/org-unit-search-result-list-element.component.html @@ -1,4 +1,4 @@ - +
+ - + diff --git a/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.html b/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.html index d8a4e744e4..de805a64b3 100644 --- a/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.html +++ b/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.html @@ -1,7 +1,6 @@ - + @@ -9,5 +8,5 @@ + [tooltip]="metadataRepresentation.allMetadata(['dc.description']).length > 0 ? descTemplate : null"> diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts index 4612996e91..7d39d4d314 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts @@ -7,7 +7,7 @@ import { Component, Inject, OnInit } from '@angular/core'; import { Metadata } from '../../../../../core/shared/metadata.utils'; import { MetadataValue } from '../../../../../core/shared/metadata.models'; -@listableObjectComponent(ExternalSourceEntry, ViewMode.ListElement, Context.SubmissionModal) +@listableObjectComponent(ExternalSourceEntry, ViewMode.ListElement, Context.EntitySearchModalWithNameVariants) @Component({ selector: 'ds-external-source-entry-list-submission-element', styleUrls: ['./external-source-entry-list-submission-element.component.scss'], diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.html b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.html index 93165c24cd..063e1393cc 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.html +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.html @@ -1,10 +1,15 @@
-
- -
+ + +
- + +
+ diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.ts index 96f28a799b..1ed9d6cead 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.ts +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.ts @@ -20,7 +20,8 @@ import { ItemDataService } from '../../../../../core/data/item-data.service'; import { SelectableListService } from '../../../../../shared/object-list/selectable-list/selectable-list.service'; import { NameVariantModalComponent } from '../../name-variant-modal/name-variant-modal.component'; -@listableObjectComponent('OrgUnitSearchResult', ViewMode.ListElement, Context.SubmissionModal) +@listableObjectComponent('OrgUnitSearchResult', ViewMode.ListElement, Context.EntitySearchModal) +@listableObjectComponent('OrgUnitSearchResult', ViewMode.ListElement, Context.EntitySearchModalWithNameVariants) @Component({ selector: 'ds-person-search-result-list-submission-element', styleUrls: ['./org-unit-search-result-list-submission-element.component.scss'], @@ -34,6 +35,7 @@ export class OrgUnitSearchResultListSubmissionElementComponent extends SearchRes allSuggestions: string[]; selectedName: string; alternativeField = 'dc.title.alternative'; + useNameVariants = false; constructor(protected truncatableService: TruncatableService, private relationshipService: RelationshipService, @@ -48,16 +50,21 @@ export class OrgUnitSearchResultListSubmissionElementComponent extends SearchRes ngOnInit() { super.ngOnInit(); - const defaultValue = this.firstMetadataValue('organization.legalName'); - const alternatives = this.allMetadataValues(this.alternativeField); - this.allSuggestions = [defaultValue, ...alternatives]; - this.relationshipService.getNameVariant(this.listID, this.dso.uuid) - .pipe(take(1)) - .subscribe((nameVariant: string) => { - this.selectedName = nameVariant || defaultValue; - } - ); + this.useNameVariants = this.context === Context.EntitySearchModalWithNameVariants; + + if (this.useNameVariants) { + const defaultValue = this.firstMetadataValue('organization.legalName'); + const alternatives = this.allMetadataValues(this.alternativeField); + this.allSuggestions = [defaultValue, ...alternatives]; + + this.relationshipService.getNameVariant(this.listID, this.dso.uuid) + .pipe(take(1)) + .subscribe((nameVariant: string) => { + this.selectedName = nameVariant || defaultValue; + } + ); + } } select(value) { @@ -75,7 +82,7 @@ export class OrgUnitSearchResultListSubmissionElementComponent extends SearchRes if (!this.allSuggestions.includes(value)) { this.openModal(value) .then(() => { - + // user clicked ok: store the name variant in the item const newName: MetadataValue = new MetadataValue(); newName.value = value; @@ -89,9 +96,12 @@ export class OrgUnitSearchResultListSubmissionElementComponent extends SearchRes }, }); this.itemDataService.update(updatedItem).pipe(take(1)).subscribe(); - }) + }).catch(() => { + // user clicked cancel: use the name variant only for this relation, no further action required + }).finally(() => { + this.select(value); + }) } - this.select(value); } openModal(value): Promise { diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component.scss b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component.scss index 8301e12c5f..20b48c805b 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component.scss +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component.scss @@ -1,6 +1,7 @@ form { z-index: 1; &:before { + pointer-events: none; // prevent the icon from ‘catching‘ the click position: absolute; font-weight: 900; font-family: "Font Awesome 5 Free"; @@ -15,4 +16,4 @@ form { input.suggestion_input { background: transparent; } -} \ No newline at end of file +} diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.html b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.html index 25c091d386..9fe9898c2b 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.html +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.html @@ -1,7 +1,4 @@
-
- -
diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.ts index 83761c6c20..9541ff334c 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.ts +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.ts @@ -20,7 +20,7 @@ import { MetadataValue } from '../../../../../core/shared/metadata.models'; import { ItemDataService } from '../../../../../core/data/item-data.service'; import { SelectableListService } from '../../../../../shared/object-list/selectable-list/selectable-list.service'; -@listableObjectComponent('PersonSearchResult', ViewMode.ListElement, Context.SubmissionModal) +@listableObjectComponent('PersonSearchResult', ViewMode.ListElement, Context.EntitySearchModalWithNameVariants) @Component({ selector: 'ds-person-search-result-list-submission-element', styleUrls: ['./person-search-result-list-submission-element.component.scss'], @@ -55,7 +55,7 @@ export class PersonSearchResultListSubmissionElementComponent extends SearchResu this.relationshipService.getNameVariant(this.listID, this.dso.uuid) .pipe(take(1)) .subscribe((nameVariant: string) => { - this.selectedName = nameVariant || defaultValue; + this.selectedName = nameVariant || defaultValue; } ); } @@ -75,27 +75,32 @@ export class PersonSearchResultListSubmissionElementComponent extends SearchResu if (!this.allSuggestions.includes(value)) { this.openModal(value) .then(() => { + // user clicked ok: store the name variant in the item + const newName: MetadataValue = new MetadataValue(); + newName.value = value; - const newName: MetadataValue = new MetadataValue(); - newName.value = value; - - const existingNames: MetadataValue[] = this.dso.metadata[this.alternativeField] || []; - const alternativeNames = { [this.alternativeField]: [...existingNames, newName] }; - const updatedItem = - Object.assign({}, this.dso, { - metadata: { - ...this.dso.metadata, - ...alternativeNames - }, - }); - this.itemDataService.update(updatedItem).pipe(take(1)).subscribe(); - }) + const existingNames: MetadataValue[] = this.dso.metadata[this.alternativeField] || []; + const alternativeNames = { [this.alternativeField]: [...existingNames, newName] }; + const updatedItem = + Object.assign({}, this.dso, { + metadata: { + ...this.dso.metadata, + ...alternativeNames + }, + }); + this.itemDataService.update(updatedItem).pipe(take(1)).subscribe(); + this.itemDataService.commitUpdates(); + }).catch(() => { + // user clicked cancel: use the name variant only for this relation, no further action required + }).finally(() => { + this.select(value); + }) } - this.select(value); } openModal(value): Promise { const modalRef = this.modalService.open(NameVariantModalComponent, { centered: true }); + const modalComp = modalRef.componentInstance; modalComp.value = value; return modalRef.result; diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-suggestions/person-input-suggestions.component.html b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-suggestions/person-input-suggestions.component.html index e177b2b561..062e68da1f 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-suggestions/person-input-suggestions.component.html +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-suggestions/person-input-suggestions.component.html @@ -1,11 +1,12 @@ -
-
- \ No newline at end of file + diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-suggestions/person-input-suggestions.component.scss b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-suggestions/person-input-suggestions.component.scss index 8301e12c5f..86233c473a 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-suggestions/person-input-suggestions.component.scss +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-suggestions/person-input-suggestions.component.scss @@ -1,6 +1,6 @@ form { - z-index: 1; &:before { + pointer-events: none; // prevent the icon from ‘catching‘ the click position: absolute; font-weight: 900; font-family: "Font Awesome 5 Free"; @@ -9,10 +9,9 @@ form { right: 0; height: 20px; width: 20px; - z-index: -1; } input.suggestion_input { background: transparent; } -} \ No newline at end of file +} diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-suggestions/person-input-suggestions.component.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-suggestions/person-input-suggestions.component.ts index a1802ce1a7..5b4ecd9d2e 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-suggestions/person-input-suggestions.component.ts +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-suggestions/person-input-suggestions.component.ts @@ -33,8 +33,10 @@ export class PersonInputSuggestionsComponent extends InputSuggestionsComponent i } onSubmit(data) { - this.value = data; - this.submitSuggestion.emit(data); + if (data !== this.value) { + this.value = data; + this.submitSuggestion.emit(data); + } } onClickSuggestion(data) { diff --git a/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.spec.ts b/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.spec.ts index a3f6ac0216..e791f41d56 100644 --- a/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.spec.ts +++ b/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.spec.ts @@ -48,7 +48,7 @@ describe('DeleteComColPageComponent', () => { dsoDataService = jasmine.createSpyObj( 'dsoDataService', { - delete: observableOf(true) + delete: observableOf({ isSuccessful: true }) }); routerStub = { @@ -106,7 +106,7 @@ describe('DeleteComColPageComponent', () => { }); it('should show an error notification on failure', () => { - (dsoDataService.delete as any).and.returnValue(observableOf(false)); + (dsoDataService.delete as any).and.returnValue(observableOf({ isSuccessful: false })); spyOn(router, 'navigate'); comp.onConfirm(data2); fixture.detectChanges(); diff --git a/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.ts b/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.ts index f5a1a84af5..d07d7be032 100644 --- a/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.ts +++ b/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.ts @@ -7,6 +7,7 @@ import { DataService } from '../../../core/data/data.service'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { NotificationsService } from '../../notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; +import { RestResponse } from '../../../core/cache/response.models'; /** * Component representing the delete page for communities and collections @@ -45,8 +46,8 @@ export class DeleteComColPageComponent implements onConfirm(dso: TDomain) { this.dsoDataService.delete(dso.id) .pipe(first()) - .subscribe((success: boolean) => { - if (success) { + .subscribe((response: RestResponse) => { + if (response.isSuccessful) { const successMessage = this.translate.instant((dso as any).type + '.delete.notification.success'); this.notifications.success(successMessage) } else { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html index a31171d7ef..028299f760 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html @@ -1,29 +1,25 @@ -
- - - -
+ +
- - -
- {{ message | translate:model.validators }} + {{ message | translate: model.validators }}
- -
+
- -
+
- - + + + + + + - - -
-
    - - -
-
+ + diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts index 4dee6905d2..7b95f2396e 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts @@ -71,6 +71,9 @@ import { Item } from '../../../../core/shared/item.model'; import { WorkspaceItem } from '../../../../core/submission/models/workspaceitem.model'; import { of as observableOf } from 'rxjs'; import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils'; +import { FormService } from '../../form.service'; +import { SubmissionService } from '../../../../submission/submission.service'; +import { FormBuilderService } from '../form-builder.service'; describe('DsDynamicFormControlContainerComponent test suite', () => { @@ -101,15 +104,16 @@ describe('DsDynamicFormControlContainerComponent test suite', () => { new DynamicSwitchModel({ id: 'switch' }), new DynamicTextAreaModel({ id: 'textarea' }), new DynamicTimePickerModel({ id: 'timepicker' }), - new DynamicTypeaheadModel({ id: 'typeahead', metadataFields: [], repeatable: false, submissionId: '1234' }), + new DynamicTypeaheadModel({ id: 'typeahead', metadataFields: [], repeatable: false, submissionId: '1234', hasSelectableMetadata: false }), new DynamicScrollableDropdownModel({ id: 'scrollableDropdown', authorityOptions: authorityOptions, metadataFields: [], repeatable: false, - submissionId: '1234' + submissionId: '1234', + hasSelectableMetadata: false }), - new DynamicTagModel({ id: 'tag', metadataFields: [], repeatable: false, submissionId: '1234' }), + new DynamicTagModel({ id: 'tag', metadataFields: [], repeatable: false, submissionId: '1234', hasSelectableMetadata: false }), new DynamicListCheckboxGroupModel({ id: 'checkboxList', authorityOptions: authorityOptions, @@ -130,11 +134,12 @@ describe('DsDynamicFormControlContainerComponent test suite', () => { scopeUUID: '', submissionScope: '', repeatable: false, - metadataFields: [] + metadataFields: [], + hasSelectableMetadata: false }), new DynamicDsDatePickerModel({ id: 'datepicker' }), - new DynamicLookupModel({ id: 'lookup', metadataFields: [], repeatable: false, submissionId: '1234' }), - new DynamicLookupNameModel({ id: 'lookupName', metadataFields: [], repeatable: false, submissionId: '1234' }), + new DynamicLookupModel({ id: 'lookup', metadataFields: [], repeatable: false, submissionId: '1234', hasSelectableMetadata: false }), + new DynamicLookupNameModel({ id: 'lookupName', metadataFields: [], repeatable: false, submissionId: '1234', hasSelectableMetadata: false}), new DynamicQualdropModel({ id: 'combobox', readOnly: false, required: false }) ]; const testModel = formModel[8]; @@ -175,6 +180,9 @@ describe('DsDynamicFormControlContainerComponent test suite', () => { { provide: Store, useValue: {} }, { provide: RelationshipService, useValue: {} }, { provide: SelectableListService, useValue: {} }, + { provide: FormService, useValue: {} }, + { provide: FormBuilderService, useValue: {} }, + { provide: SubmissionService, useValue: {} }, { provide: SubmissionObjectDataService, useValue: { @@ -220,7 +228,6 @@ describe('DsDynamicFormControlContainerComponent test suite', () => { expect(component.group instanceof FormGroup).toBe(true); expect(component.model instanceof DynamicFormControlModel).toBe(true); expect(component.hasErrorMessaging).toBe(false); - expect(component.asBootstrapFormGroup).toBe(true); expect(component.onControlValueChanges).toBeDefined(); expect(component.onModelDisabledUpdates).toBeDefined(); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts index 2089ce8bca..0064c2e093 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts @@ -1,5 +1,6 @@ import { - ChangeDetectionStrategy, ChangeDetectorRef, + ChangeDetectionStrategy, + ChangeDetectorRef, Component, ComponentFactoryResolver, ContentChildren, @@ -16,7 +17,7 @@ import { ViewChild, ViewContainerRef } from '@angular/core'; -import { FormGroup } from '@angular/forms'; +import { FormArray, FormGroup } from '@angular/forms'; import { DYNAMIC_FORM_CONTROL_TYPE_ARRAY, @@ -29,13 +30,18 @@ import { DYNAMIC_FORM_CONTROL_TYPE_SELECT, DYNAMIC_FORM_CONTROL_TYPE_TEXTAREA, DYNAMIC_FORM_CONTROL_TYPE_TIMEPICKER, - DynamicDatePickerModel, DynamicFormComponentService, + DynamicDatePickerModel, + DynamicFormArrayGroupModel, + DynamicFormArrayModel, + DynamicFormComponentService, DynamicFormControl, DynamicFormControlContainerComponent, DynamicFormControlEvent, + DynamicFormControlEventType, DynamicFormControlModel, DynamicFormLayout, - DynamicFormLayoutService, DynamicFormRelationService, + DynamicFormLayoutService, + DynamicFormRelationService, DynamicFormValidationService, DynamicTemplateDirective, } from '@ng-dynamic-forms/core'; @@ -50,11 +56,7 @@ import { DynamicNGBootstrapTimePickerComponent } from '@ng-dynamic-forms/ui-ng-bootstrap'; import { TranslateService } from '@ngx-translate/core'; -import { followLink } from '../../../utils/follow-link-config.model'; -import { - Reorderable, - ReorderableRelationship -} from './existing-metadata-list-element/existing-metadata-list-element.component'; +import { ReorderableRelationship } from './existing-metadata-list-element/existing-metadata-list-element.component'; import { DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD } from './models/typeahead/dynamic-typeahead.model'; import { DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN } from './models/scrollable-dropdown/dynamic-scrollable-dropdown.model'; @@ -63,7 +65,7 @@ import { DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER } from './models/date-picker/dat import { DYNAMIC_FORM_CONTROL_TYPE_LOOKUP } from './models/lookup/dynamic-lookup.model'; import { DynamicListCheckboxGroupModel } from './models/list/dynamic-list-checkbox-group.model'; import { DynamicListRadioGroupModel } from './models/list/dynamic-list-radio-group.model'; -import { hasValue, isNotEmpty, isNotUndefined } from '../../../empty.util'; +import { hasNoValue, hasValue, isNotEmpty, isNotUndefined } from '../../../empty.util'; import { DYNAMIC_FORM_CONTROL_TYPE_LOOKUP_NAME } from './models/lookup/dynamic-lookup-name.model'; import { DsDynamicTagComponent } from './models/tag/dynamic-tag.component'; import { DsDatePickerComponent } from './models/date-picker/date-picker.component'; @@ -78,8 +80,8 @@ import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './models/relation-grou import { DsDatePickerInlineComponent } from './models/date-picker-inline/dynamic-date-picker-inline.component'; import { DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH } from './models/custom-switch/custom-switch.model'; import { CustomSwitchComponent } from './models/custom-switch/custom-switch.component'; -import { map, startWith, switchMap, find } from 'rxjs/operators'; -import { combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; +import { find, map, startWith, switchMap, take } from 'rxjs/operators'; +import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs'; import { SearchResult } from '../../../search/search-result.model'; import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; @@ -88,7 +90,7 @@ import { SelectableListService } from '../../../object-list/selectable-list/sele import { DsDynamicDisabledComponent } from './models/disabled/dynamic-disabled.component'; import { DYNAMIC_FORM_CONTROL_TYPE_DISABLED } from './models/disabled/dynamic-disabled.model'; import { DsDynamicLookupRelationModalComponent } from './relation-lookup-modal/dynamic-lookup-relation-modal.component'; -import { getAllSucceededRemoteData, getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators'; +import { getAllSucceededRemoteData, getFirstSucceededRemoteDataPayload, getPaginatedListPayload, getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators'; import { RemoteData } from '../../../../core/data/remote-data'; import { Item } from '../../../../core/shared/item.model'; import { ItemDataService } from '../../../../core/data/item-data.service'; @@ -98,9 +100,16 @@ import { SubmissionObjectDataService } from '../../../../core/submission/submiss import { SubmissionObject } from '../../../../core/submission/models/submission-object.model'; import { PaginatedList } from '../../../../core/data/paginated-list'; import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model'; -import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; import { Relationship } from '../../../../core/shared/item-relationships/relationship.model'; import { Collection } from '../../../../core/shared/collection.model'; +import { MetadataValue, VIRTUAL_METADATA_PREFIX } from '../../../../core/shared/metadata.models'; +import { FormService } from '../../form.service'; +import { SelectableListState } from '../../../object-list/selectable-list/selectable-list.reducer'; +import { SubmissionService } from '../../../../submission/submission.service'; +import { followLink } from '../../../utils/follow-link-config.model'; +import { paginatedRelationsToItems } from '../../../../+item-page/simple/item-types/shared/item-relationships-utils'; +import { RelationshipOptions } from '../models/relationship-options.model'; +import { FormBuilderService } from '../form-builder.service'; export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type | null { switch (model.type) { @@ -180,22 +189,22 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo @Input('templates') inputTemplateList: QueryList; @Input() formId: string; - @Input() asBootstrapFormGroup = true; + @Input() asBootstrapFormGroup = false; @Input() bindId = true; @Input() context: any | null = null; @Input() group: FormGroup; @Input() hasErrorMessaging = false; @Input() layout = null as DynamicFormLayout; @Input() model: any; - reorderables$: Observable; - reorderables: ReorderableRelationship[]; - hasRelationLookup: boolean; + relationshipValue$: Observable; + isRelationship: boolean; modalRef: NgbModalRef; item: Item; + item$: Observable; collection: Collection; listId: string; searchConfig: string; - + value: MetadataValue; /** * List of subscriptions to unsubscribe from */ @@ -207,7 +216,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo @Output('dfFocus') focus: EventEmitter = new EventEmitter(); @Output('ngbEvent') customEvent: EventEmitter = new EventEmitter(); /* tslint:enable:no-output-rename */ - @ViewChild('componentViewContainer', { read: ViewContainerRef, static: true}) componentViewContainerRef: ViewContainerRef; + @ViewChild('componentViewContainer', { read: ViewContainerRef, static: true }) componentViewContainerRef: ViewContainerRef; private showErrorMessagesPreviousStage: boolean; @@ -229,9 +238,11 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo private zone: NgZone, private store: Store, private submissionObjectService: SubmissionObjectDataService, - private ref: ChangeDetectorRef + private ref: ChangeDetectorRef, + private formService: FormService, + private formBuilderService: FormBuilderService, + private submissionService: SubmissionService ) { - super(componentFactoryResolver, layoutService, validationService, dynamicFormComponentService, relationService); } @@ -239,62 +250,76 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo * Sets up the necessary variables for when this control can be used to add relationships to the submitted item */ ngOnInit(): void { - this.hasRelationLookup = hasValue(this.model.relationship); - this.reorderables = []; - if (this.hasRelationLookup) { + this.isRelationship = hasValue(this.model.relationship); + const isWrapperAroundRelationshipList = hasValue(this.model.relationshipConfig); - this.listId = 'list-' + this.model.relationship.relationshipType; + if (this.isRelationship || isWrapperAroundRelationshipList) { + const config = this.model.relationshipConfig || this.model.relationship; + const relationshipOptions = Object.assign(new RelationshipOptions(), config); + this.listId = `list-${this.model.submissionId}-${relationshipOptions.relationshipType}`; + this.setItem(); - const submissionObject$ = this.submissionObjectService - .findById(this.model.submissionId, followLink('item'), followLink('collection')).pipe( - getAllSucceededRemoteData(), - getRemoteDataPayload() - ); + if (isWrapperAroundRelationshipList || !this.model.repeatable) { + const subscription = this.selectableListService.getSelectableList(this.listId).pipe( + find((list: SelectableListState) => hasNoValue(list)), + switchMap(() => this.item$.pipe(take(1))), + switchMap((item) => { + const relationshipsRD$ = this.relationshipService.getItemRelationshipsByLabel(item, + relationshipOptions.relationshipType, + undefined, + followLink('leftItem'), + followLink('rightItem'), + followLink('relationshipType') + ); - const item$ = submissionObject$.pipe(switchMap((submissionObject: SubmissionObject) => (submissionObject.item as Observable>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()))); - const collection$ = submissionObject$.pipe(switchMap((submissionObject: SubmissionObject) => (submissionObject.collection as Observable>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()))); + relationshipsRD$.pipe( + getFirstSucceededRemoteDataPayload(), + getPaginatedListPayload() + ).subscribe((relationships: Relationship[]) => { + // set initial namevariants for pre-existing relationships + relationships.forEach((relationship: Relationship) => { + const relationshipMD: MetadataValue = item.firstMetadata(relationshipOptions.metadataField, { authority: `${VIRTUAL_METADATA_PREFIX}${relationship.id}` }); + const nameVariantMD: MetadataValue = item.firstMetadata(this.model.metadataFields, { authority: `${VIRTUAL_METADATA_PREFIX}${relationship.id}` }); + if (hasValue(relationshipMD) && isNotEmpty(relationshipMD.value) && hasValue(nameVariantMD) && isNotEmpty(nameVariantMD.value)) { + this.relationshipService.setNameVariant(this.listId, relationshipMD.value, nameVariantMD.value); + } + }); + }); - this.subs.push(item$.subscribe((item) => this.item = item)); - this.subs.push(collection$.subscribe((collection) => this.collection = collection)); - this.reorderables$ = item$.pipe( - switchMap((item) => this.relationshipService.getItemRelationshipsByLabel(item, this.model.relationship.relationshipType, undefined, followLink('leftItem'), followLink('rightItem'), followLink('relationshipType')) + return relationshipsRD$.pipe( + paginatedRelationsToItems(item.uuid), + getSucceededRemoteData(), + map((items: RemoteData>) => items.payload.page.map((i) => Object.assign(new ItemSearchResult(), { indexableObject: i }))), + ) + }) + ).subscribe((relatedItems: Array>) => this.selectableListService.select(this.listId, relatedItems)); + this.subs.push(subscription); + } + + if (hasValue(this.model.metadataValue)) { + this.value = Object.assign(new MetadataValue(), this.model.metadataValue); + } else { + this.value = Object.assign(new MetadataValue(), this.model.value); + } + + if (hasValue(this.value) && this.value.isVirtual) { + const relationship$ = this.relationshipService.findById(this.value.virtualValue, followLink('leftItem'), followLink('rightItem'), followLink('relationshipType')) .pipe( getAllSucceededRemoteData(), - getRemoteDataPayload(), - map((relationshipList: PaginatedList) => relationshipList.page), - startWith([]), - switchMap((relationships: Relationship[]) => - observableCombineLatest( - relationships.map((relationship: Relationship) => - relationship.leftItem.pipe( - getSucceededRemoteData(), - getRemoteDataPayload(), - map((leftItem: Item) => { - return new ReorderableRelationship(relationship, leftItem.uuid !== this.item.uuid) - }), - ) - ))), - map((relationships: ReorderableRelationship[]) => - relationships - .sort((a: Reorderable, b: Reorderable) => { - return Math.sign(a.getPlace() - b.getPlace()); - }) + getRemoteDataPayload()); + this.relationshipValue$ = observableCombineLatest([this.item$.pipe(take(1)), relationship$]).pipe( + switchMap(([item, relationship]: [Item, Relationship]) => + relationship.leftItem.pipe( + getAllSucceededRemoteData(), + getRemoteDataPayload(), + map((leftItem: Item) => { + return new ReorderableRelationship(relationship, leftItem.uuid !== item.uuid, this.relationshipService, this.store, this.model.submissionId) + }), ) - ) - ) - ); - - this.subs.push(this.reorderables$.subscribe((rs) => { - this.reorderables = rs; - this.ref.detectChanges(); - })); - - item$.pipe( - switchMap((item) => this.relationshipService.getRelatedItemsByLabel(item, this.model.relationship.relationshipType)), - map((items: RemoteData>) => items.payload.page.map((item) => Object.assign(new ItemSearchResult(), { indexableObject: item }))), - ).subscribe((relatedItems: Array>) => { - this.selectableListService.select(this.listId, relatedItems) - }); + ), + startWith(undefined) + ); + } } } @@ -303,7 +328,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo } ngOnChanges(changes: SimpleChanges) { - if (changes) { + if (changes && !this.isRelationship && hasValue(this.group.get(this.model.id))) { super.ngOnChanges(changes); if (this.model && this.model.placeholder) { this.model.placeholder = this.translateService.instant(this.model.placeholder); @@ -351,6 +376,27 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo size: 'lg' }); const modalComp = this.modalRef.componentInstance; + + if (hasValue(this.model.value) && !this.model.readOnly) { + if (typeof this.model.value === 'string') { + modalComp.query = this.model.value; + } else if (typeof this.model.value.value === 'string') { + modalComp.query = this.model.value.value; + } + } + + if (hasValue(this.model.value)) { + this.model.value = ''; + this.onChange({ + $event: { previousIndex: 0 }, + context: { index: 0 }, + control: this.control, + model: this.model, + type: DynamicFormControlEventType.Change + }); + } + this.submissionService.dispatchSave(this.model.submissionId); + modalComp.repeatable = this.model.repeatable; modalComp.listId = this.listId; modalComp.relationshipOptions = this.model.relationship; @@ -358,32 +404,18 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo modalComp.metadataFields = this.model.metadataFields; modalComp.item = this.item; modalComp.collection = this.collection; + modalComp.submissionId = this.model.submissionId; } /** - * Method to move a relationship inside the list of relationships - * This will update the view and update the right or left place field of the relationships in the list - * @param event + * Callback for the remove event, + * remove the current control from its array */ - moveSelection(event: CdkDragDrop) { - this.zone.runOutsideAngular(() => { - moveItemInArray(this.reorderables, event.previousIndex, event.currentIndex); - const reorderables: Reorderable[] = this.reorderables.map((reo: Reorderable, index: number) => { - reo.oldIndex = reo.getPlace(); - reo.newIndex = index; - return reo; - } - ); - observableCombineLatest( - reorderables.map((rel: ReorderableRelationship) => { - if (rel.oldIndex !== rel.newIndex) { - return this.relationshipService.updatePlace(rel); - } else { - return observableOf(undefined) as Observable>; - } - }) - ).subscribe(); - }) + onRemove(): void { + const arrayContext: DynamicFormArrayModel = (this.context as DynamicFormArrayGroupModel).context; + const path = this.formBuilderService.getPath(arrayContext); + const formArrayControl = this.group.root.get(path) as FormArray; + this.formBuilderService.removeFormArrayGroup(this.context.index, formArrayControl, arrayContext); } /** @@ -396,9 +428,20 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo } /** - * Prevent unnecessary rerendering so fields don't lose focus + * Initialize this.item$ based on this.model.submissionId */ - trackReorderable(index, reorderable: Reorderable) { - return hasValue(reorderable) ? reorderable.getId() : undefined; + private setItem() { + const submissionObject$ = this.submissionObjectService + .findById(this.model.submissionId, followLink('item'), followLink('collection')).pipe( + getAllSucceededRemoteData(), + getRemoteDataPayload() + ); + + this.item$ = submissionObject$.pipe(switchMap((submissionObject: SubmissionObject) => (submissionObject.item as Observable>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()))); + const collection$ = submissionObject$.pipe(switchMap((submissionObject: SubmissionObject) => (submissionObject.collection as Observable>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()))); + + this.subs.push(this.item$.subscribe((item) => this.item = item)); + this.subs.push(collection$.subscribe((collection) => this.collection = collection)); + } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.html index 4d8123a4b9..5684f4eac9 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.html @@ -7,6 +7,7 @@ [model]="model" [ngClass]="[getClass(model, 'element', 'host'), getClass(model, 'grid', 'host')]" [templates]="templates" + [asBootstrapFormGroup]="true" (dfBlur)="onEvent($event, 'blur')" (dfChange)="onEvent($event, 'change')" (dfFocus)="onEvent($event, 'focus')"> diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.ts index 490275a03b..ad1c18706d 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.ts @@ -37,5 +37,4 @@ export class DsDynamicFormComponent extends DynamicFormComponent { constructor(protected formService: FormBuilderService, protected layoutService: DynamicFormLayoutService) { super(formService, layoutService); } - } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.html index 960dd78767..57ab7d66d8 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.html @@ -1,11 +1,14 @@ -
  • - - - - - -
  • +
    diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.scss index e69de29bb2..ab63e324bd 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.scss +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.scss @@ -0,0 +1,3 @@ +span.text-contents{ + padding: $btn-padding-y 0; +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.spec.ts index 79a650b597..ff2fd0c798 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.spec.ts @@ -10,6 +10,8 @@ import { RelationshipOptions } from '../../models/relationship-options.model'; import { createSuccessfulRemoteDataObject$ } from '../../../../remote-data.utils'; import { RemoveRelationshipAction } from '../relation-lookup-modal/relationship.actions'; import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model'; +import { of as observableOf } from 'rxjs'; +import { RelationshipService } from '../../../../../core/data/relationship.service'; describe('ExistingMetadataListElementComponent', () => { let component: ExistingMetadataListElementComponent; @@ -28,6 +30,8 @@ describe('ExistingMetadataListElementComponent', () => { let leftItemRD$; let rightItemRD$; let relatedSearchResult; + let submissionId; + let relationshipService; function init() { uuid1 = '91ce578d-2e63-4093-8c73-3faafd716000'; @@ -42,9 +46,13 @@ describe('ExistingMetadataListElementComponent', () => { leftItemRD$ = createSuccessfulRemoteDataObject$(relatedItem); rightItemRD$ = createSuccessfulRemoteDataObject$(submissionItem); relatedSearchResult = Object.assign(new ItemSearchResult(), { indexableObject: relatedItem }); + relationshipService = { + updatePlace:() => observableOf({}) + } as any; relationship = Object.assign(new Relationship(), { leftItem: leftItemRD$, rightItem: rightItemRD$ }); - reoRel = new ReorderableRelationship(relationship, true); + submissionId = '1234'; + reoRel = new ReorderableRelationship(relationship, true, relationshipService, {} as any, submissionId); } beforeEach(async(() => { @@ -68,6 +76,7 @@ describe('ExistingMetadataListElementComponent', () => { component.reoRel = reoRel; component.metadataFields = metadataFields; component.relationshipOptions = relationshipOptions; + component.submissionId = submissionId; fixture.detectChanges(); component.ngOnChanges(); }); @@ -84,9 +93,8 @@ describe('ExistingMetadataListElementComponent', () => { it('should dispatch a RemoveRelationshipAction', () => { component.removeSelection(); - const action = new RemoveRelationshipAction(submissionItem, relatedItem, relationshipOptions.relationshipType); + const action = new RemoveRelationshipAction(submissionItem, relatedItem, relationshipOptions.relationshipType, submissionId); expect(store.dispatch).toHaveBeenCalledWith(action); - }); }) }); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts index 09aaa253c6..d4ce3342e7 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts @@ -1,50 +1,126 @@ -import { Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core'; +import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output } from '@angular/core'; +import { FormControl } from '@angular/forms'; +import { DynamicFormArrayGroupModel } from '@ng-dynamic-forms/core'; +import { Store } from '@ngrx/store'; +import { BehaviorSubject, Subscription } from 'rxjs'; +import { filter } from 'rxjs/operators'; +import { AppState } from '../../../../../app.reducer'; +import { RelationshipService } from '../../../../../core/data/relationship.service'; +import { Relationship } from '../../../../../core/shared/item-relationships/relationship.model'; import { Item } from '../../../../../core/shared/item.model'; +import { ItemMetadataRepresentation } from '../../../../../core/shared/metadata-representation/item/item-metadata-representation.model'; import { MetadataRepresentation } from '../../../../../core/shared/metadata-representation/metadata-representation.model'; +import { MetadataValue } from '../../../../../core/shared/metadata.models'; import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../../core/shared/operators'; import { hasValue, isNotEmpty } from '../../../../empty.util'; -import { Subscription } from 'rxjs'; -import { filter } from 'rxjs/operators'; -import { Relationship } from '../../../../../core/shared/item-relationships/relationship.model'; -import { MetadataValue } from '../../../../../core/shared/metadata.models'; -import { ItemMetadataRepresentation } from '../../../../../core/shared/metadata-representation/item/item-metadata-representation.model'; -import { RelationshipOptions } from '../../models/relationship-options.model'; -import { RemoveRelationshipAction } from '../relation-lookup-modal/relationship.actions'; -import { SelectableListService } from '../../../../object-list/selectable-list/selectable-list.service'; -import { Store } from '@ngrx/store'; -import { AppState } from '../../../../../app.reducer'; import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model'; +import { SelectableListService } from '../../../../object-list/selectable-list/selectable-list.service'; +import { FormFieldMetadataValueObject } from '../../models/form-field-metadata-value.model'; +import { RelationshipOptions } from '../../models/relationship-options.model'; +import { DynamicConcatModel } from '../models/ds-dynamic-concat.model'; +import { RemoveRelationshipAction } from '../relation-lookup-modal/relationship.actions'; // tslint:disable:max-classes-per-file /** * Abstract class that defines objects that can be reordered */ export abstract class Reorderable { + constructor(public oldIndex?: number, public newIndex?: number) { } + /** + * Return the id for this Reorderable + */ abstract getId(): string; + /** + * Return the place metadata for this Reorderable + */ abstract getPlace(): number; + + /** + * Update the Reorderable + */ + update(): void { + this.oldIndex = this.newIndex; + } + + /** + * Returns true if the oldIndex of this Reorderable + * differs from the newIndex + */ + get hasMoved(): boolean { + return this.oldIndex !== this.newIndex + } +} + +/** + * A Reorderable representation of a FormFieldMetadataValue + */ +export class ReorderableFormFieldMetadataValue extends Reorderable { + + constructor( + public metadataValue: FormFieldMetadataValueObject, + public model: DynamicConcatModel, + public control: FormControl, + public group: DynamicFormArrayGroupModel, + oldIndex?: number, + newIndex?: number + ) { + super(oldIndex, newIndex); + this.metadataValue = metadataValue; + } + + /** + * Return the id for this Reorderable + */ + getId(): string { + if (hasValue(this.metadataValue.authority)) { + return this.metadataValue.authority; + } else { + // can't use UUIDs, they're generated client side + return this.metadataValue.value; + } + } + + /** + * Return the place metadata for this Reorderable + */ + getPlace(): number { + return this.metadataValue.place; + } + } /** * Represents a single relationship that can be reordered in a list of multiple relationships */ export class ReorderableRelationship extends Reorderable { - relationship: Relationship; - useLeftItem: boolean; - constructor(relationship: Relationship, useLeftItem: boolean, oldIndex?: number, newIndex?: number) { + constructor( + public relationship: Relationship, + public useLeftItem: boolean, + protected relationshipService: RelationshipService, + protected store: Store, + protected submissionID: string, + oldIndex?: number, + newIndex?: number) { super(oldIndex, newIndex); this.relationship = relationship; this.useLeftItem = useLeftItem; } + /** + * Return the id for this Reorderable + */ getId(): string { return this.relationship.id; } + /** + * Return the place metadata for this Reorderable + */ getPlace(): number { if (this.useLeftItem) { return this.relationship.rightPlace @@ -62,15 +138,16 @@ export class ReorderableRelationship extends Reorderable { templateUrl: './existing-metadata-list-element.component.html', styleUrls: ['./existing-metadata-list-element.component.scss'] }) -export class ExistingMetadataListElementComponent implements OnChanges, OnDestroy { +export class ExistingMetadataListElementComponent implements OnInit, OnChanges, OnDestroy { @Input() listId: string; @Input() submissionItem: Item; @Input() reoRel: ReorderableRelationship; @Input() metadataFields: string[]; @Input() relationshipOptions: RelationshipOptions; - metadataRepresentation: MetadataRepresentation; + @Input() submissionId: string; + metadataRepresentation$: BehaviorSubject = new BehaviorSubject(undefined); relatedItem: Item; - + @Output() remove: EventEmitter = new EventEmitter(); /** * List of subscriptions to unsubscribe from */ @@ -82,24 +159,35 @@ export class ExistingMetadataListElementComponent implements OnChanges, OnDestro ) { } + ngOnInit(): void { + this.ngOnChanges(); + } + + /** + * Change callback for the component + */ ngOnChanges() { - const item$ = this.reoRel.useLeftItem ? - this.reoRel.relationship.leftItem : this.reoRel.relationship.rightItem; - this.subs.push(item$.pipe( - getAllSucceededRemoteData(), - getRemoteDataPayload(), - filter((item: Item) => hasValue(item) && isNotEmpty(item.uuid)) - ).subscribe((item: Item) => { - this.relatedItem = item; - const relationMD: MetadataValue = this.submissionItem.firstMetadata(this.relationshipOptions.metadataField, { value: this.relatedItem.uuid }); - if (hasValue(relationMD)) { - const metadataRepresentationMD: MetadataValue = this.submissionItem.firstMetadata(this.metadataFields, { authority: relationMD.authority }); - this.metadataRepresentation = Object.assign( - new ItemMetadataRepresentation(metadataRepresentationMD), - this.relatedItem - ) - } - })); + if (hasValue(this.reoRel)) { + const item$ = this.reoRel.useLeftItem ? + this.reoRel.relationship.leftItem : this.reoRel.relationship.rightItem; + this.subs.push(item$.pipe( + getAllSucceededRemoteData(), + getRemoteDataPayload(), + filter((item: Item) => hasValue(item) && isNotEmpty(item.uuid)) + ).subscribe((item: Item) => { + this.relatedItem = item; + const relationMD: MetadataValue = this.submissionItem.firstMetadata(this.relationshipOptions.metadataField, { value: this.relatedItem.uuid }); + if (hasValue(relationMD)) { + const metadataRepresentationMD: MetadataValue = this.submissionItem.firstMetadata(this.metadataFields, { authority: relationMD.authority }); + + const nextValue = Object.assign( + new ItemMetadataRepresentation(metadataRepresentationMD), + this.relatedItem + ); + this.metadataRepresentation$.next(nextValue); + } + })); + } } /** @@ -107,7 +195,8 @@ export class ExistingMetadataListElementComponent implements OnChanges, OnDestro */ removeSelection() { this.selectableListService.deselectSingle(this.listId, Object.assign(new ItemSearchResult(), { indexableObject: this.relatedItem })); - this.store.dispatch(new RemoveRelationshipAction(this.submissionItem, this.relatedItem, this.relationshipOptions.relationshipType)) + this.store.dispatch(new RemoveRelationshipAction(this.submissionItem, this.relatedItem, this.relationshipOptions.relationshipType, this.submissionId)); + this.remove.emit(); } /** @@ -120,4 +209,5 @@ export class ExistingMetadataListElementComponent implements OnChanges, OnDestro } } + // tslint:enable:max-classes-per-file diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component.html new file mode 100644 index 0000000000..15087d2553 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component.html @@ -0,0 +1,14 @@ +
    + + + + + + + + + +
    diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component.scss new file mode 100644 index 0000000000..ab63e324bd --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component.scss @@ -0,0 +1,3 @@ +span.text-contents{ + padding: $btn-padding-y 0; +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component.spec.ts new file mode 100644 index 0000000000..6b6c518bb0 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component.spec.ts @@ -0,0 +1,100 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ExistingRelationListElementComponent } from './existing-relation-list-element.component'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { SelectableListService } from '../../../../object-list/selectable-list/selectable-list.service'; +import { select, Store } from '@ngrx/store'; +import { Item } from '../../../../../core/shared/item.model'; +import { Relationship } from '../../../../../core/shared/item-relationships/relationship.model'; +import { RelationshipOptions } from '../../models/relationship-options.model'; +import { RemoveRelationshipAction } from '../relation-lookup-modal/relationship.actions'; +import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model'; +import { of as observableOf } from 'rxjs'; +import { ReorderableRelationship } from '../existing-metadata-list-element/existing-metadata-list-element.component'; +import { createSuccessfulRemoteDataObject$ } from '../../../../remote-data.utils'; + +describe('ExistingRelationListElementComponent', () => { + let component: ExistingRelationListElementComponent; + let fixture: ComponentFixture; + let selectionService; + let store; + let listID; + let submissionItem; + let relationship; + let reoRel; + let metadataFields; + let relationshipOptions; + let uuid1; + let uuid2; + let relatedItem; + let leftItemRD$; + let rightItemRD$; + let relatedSearchResult; + let submissionId; + let relationshipService; + + function init() { + uuid1 = '91ce578d-2e63-4093-8c73-3faafd716000'; + uuid2 = '0e9dba1c-e1c3-4e05-a539-446f08ef57a7'; + selectionService = jasmine.createSpyObj('selectionService', ['deselectSingle']); + store = jasmine.createSpyObj('store', ['dispatch']); + listID = '1234-listID'; + submissionItem = Object.assign(new Item(), { uuid: uuid1 }); + metadataFields = ['dc.contributor.author']; + relationshipOptions = Object.assign(new RelationshipOptions(), { relationshipType: 'isPublicationOfAuthor', filter: 'test.filter', searchConfiguration: 'personConfiguration', nameVariants: true }) + relatedItem = Object.assign(new Item(), { uuid: uuid2 }); + leftItemRD$ = createSuccessfulRemoteDataObject$(relatedItem); + rightItemRD$ = createSuccessfulRemoteDataObject$(submissionItem); + relatedSearchResult = Object.assign(new ItemSearchResult(), { indexableObject: relatedItem }); + relationshipService = { + updatePlace:() => observableOf({}) + } as any; + + relationship = Object.assign(new Relationship(), { leftItem: leftItemRD$, rightItem: rightItemRD$ }); + submissionId = '1234'; + reoRel = new ReorderableRelationship(relationship, true, relationshipService, {} as any, submissionId); + } + + beforeEach(async(() => { + init(); + TestBed.configureTestingModule({ + declarations: [ExistingRelationListElementComponent], + providers: [ + { provide: SelectableListService, useValue: selectionService }, + { provide: Store, useValue: store }, + ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ExistingRelationListElementComponent); + component = fixture.componentInstance; + component.listId = listID; + component.submissionItem = submissionItem; + component.reoRel = reoRel; + component.metadataFields = metadataFields; + component.relationshipOptions = relationshipOptions; + component.submissionId = submissionId; + fixture.detectChanges(); + component.ngOnChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('removeSelection', () => { + it('should deselect the object in the selectable list service', () => { + component.removeSelection(); + expect(selectionService.deselectSingle).toHaveBeenCalledWith(listID, relatedSearchResult); + }); + + it('should dispatch a RemoveRelationshipAction', () => { + component.removeSelection(); + const action = new RemoveRelationshipAction(submissionItem, relatedItem, relationshipOptions.relationshipType, submissionId); + expect(store.dispatch).toHaveBeenCalledWith(action); + }); + }) +}); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component.ts new file mode 100644 index 0000000000..65b3730773 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component.ts @@ -0,0 +1,120 @@ +import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { BehaviorSubject, Observable, Subscription } from 'rxjs'; +import { filter } from 'rxjs/operators'; +import { AppState } from '../../../../../app.reducer'; +import { Item } from '../../../../../core/shared/item.model'; +import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../../core/shared/operators'; +import { hasValue, isNotEmpty } from '../../../../empty.util'; +import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model'; +import { SelectableListService } from '../../../../object-list/selectable-list/selectable-list.service'; +import { RelationshipOptions } from '../../models/relationship-options.model'; +import { RemoveRelationshipAction } from '../relation-lookup-modal/relationship.actions'; +import { ViewMode } from '../../../../../core/shared/view-mode.model'; +import { ReorderableRelationship } from '../existing-metadata-list-element/existing-metadata-list-element.component'; + +// tslint:disable:max-classes-per-file +/** + * Abstract class that defines objects that can be reordered + */ +export abstract class Reorderable { + + constructor(public oldIndex?: number, public newIndex?: number) { + } + + /** + * Return the id for this Reorderable + */ + abstract getId(): string; + + /** + * Return the place metadata for this Reorderable + */ + abstract getPlace(): number; + + /** + * Update the Reorderable + */ + abstract update(): Observable; + + /** + * Returns true if the oldIndex of this Reorderable + * differs from the newIndex + */ + get hasMoved(): boolean { + return this.oldIndex !== this.newIndex + } +} + +/** + * Represents a single existing relationship value as metadata in submission + */ +@Component({ + selector: 'ds-existing-relation-list-element', + templateUrl: './existing-relation-list-element.component.html', + styleUrls: ['./existing-relation-list-element.component.scss'] +}) +export class ExistingRelationListElementComponent implements OnInit, OnChanges, OnDestroy { + @Input() listId: string; + @Input() submissionItem: Item; + @Input() reoRel: ReorderableRelationship; + @Input() metadataFields: string[]; + @Input() relationshipOptions: RelationshipOptions; + @Input() submissionId: string; + relatedItem$: BehaviorSubject = new BehaviorSubject(undefined); + viewType = ViewMode.ListElement; + @Output() remove: EventEmitter = new EventEmitter(); + + /** + * List of subscriptions to unsubscribe from + */ + private subs: Subscription[] = []; + + constructor( + private selectableListService: SelectableListService, + private store: Store + ) { + } + + ngOnInit(): void { + this.ngOnChanges(); + } + + /** + * Change callback for the component + */ + ngOnChanges() { + if (hasValue(this.reoRel)) { + const item$ = this.reoRel.useLeftItem ? + this.reoRel.relationship.leftItem : this.reoRel.relationship.rightItem; + this.subs.push(item$.pipe( + getAllSucceededRemoteData(), + getRemoteDataPayload(), + filter((item: Item) => hasValue(item) && isNotEmpty(item.uuid)) + ).subscribe((item: Item) => { + this.relatedItem$.next(item); + })); + } + + } + + /** + * Removes the selected relationship from the list + */ + removeSelection() { + this.selectableListService.deselectSingle(this.listId, Object.assign(new ItemSearchResult(), { indexableObject: this.relatedItem$.getValue() })); + this.store.dispatch(new RemoveRelationshipAction(this.submissionItem, this.relatedItem$.getValue(), this.relationshipOptions.relationshipType, this.submissionId)); + } + + /** + * Unsubscribe from all subscriptions + */ + ngOnDestroy(): void { + this.subs + .filter((sub) => hasValue(sub)) + .forEach((sub) => sub.unsubscribe()); + } + +} + +// tslint:enable:max-classes-per-file diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html index 75c27b6ca5..ac5ece93d1 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html @@ -1,32 +1,45 @@ -
    - -
    - - - - - - - +
    + + + +
    +
    +
    +
    + + + + + + +
    +
    -
    - + + + + + diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.scss new file mode 100644 index 0000000000..b61bb9232b --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.scss @@ -0,0 +1,52 @@ +@import './../../../../../../../styles/variables'; + +:host { + display: block; +} + +.cdk-drag { + margin-left: -(2 * $spacer); + margin-right: -(0.5 * $spacer); + padding-right: (0.5 * $spacer); + .drag-icon { + visibility: hidden; + width: (2 * $spacer); + color: $gray-600; + margin: $btn-padding-y 0; + line-height: $btn-line-height; + text-indent: 0.5 * $spacer + } + + &:hover, &:focus { + cursor: grab; + .drag-icon { + visibility: visible; + } + } + +} + +.cdk-drop-list-dragging { + .cdk-drag { + cursor: grabbing; + .drag-icon { + visibility: hidden; + } + } +} + +.cdk-drag-preview { + background-color: white; + border-radius: $border-radius-sm; + margin-left: 0; + box-shadow: 0 5px 5px 0px rgba(0, 0, 0, 0.2), + 0 8px 10px 1px rgba(0, 0, 0, 0.14), + 0 3px 14px 2px rgba(0, 0, 0, 0.12); + .drag-icon { + visibility: visible; + } +} + +.cdk-drag-placeholder { + opacity: 0; +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.ts index 1e8fd3b55e..ea6455a138 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.ts @@ -1,25 +1,31 @@ +import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { Component, EventEmitter, Input, Output, QueryList } from '@angular/core'; import { FormGroup } from '@angular/forms'; import { DynamicFormArrayComponent, - DynamicFormArrayModel, - DynamicFormControlCustomEvent, DynamicFormControlEvent, + DynamicFormControlCustomEvent, + DynamicFormControlEvent, + DynamicFormControlEventType, DynamicFormLayout, DynamicFormLayoutService, DynamicFormValidationService, DynamicTemplateDirective } from '@ng-dynamic-forms/core'; +import { Relationship } from '../../../../../../core/shared/item-relationships/relationship.model'; +import { DynamicRowArrayModel } from '../ds-dynamic-row-array-model'; +import { hasValue } from '../../../../../empty.util'; @Component({ - selector: 'ds-dynamic-form-array', - templateUrl: './dynamic-form-array.component.html' + selector: 'ds-dynamic-form-array', + templateUrl: './dynamic-form-array.component.html', + styleUrls: ['./dynamic-form-array.component.scss'] }) export class DsDynamicFormArrayComponent extends DynamicFormArrayComponent { @Input() bindId = true; @Input() group: FormGroup; @Input() layout: DynamicFormLayout; - @Input() model: DynamicFormArrayModel; + @Input() model: DynamicRowArrayModel; @Input() templates: QueryList | undefined; /* tslint:disable:no-output-rename */ @@ -27,12 +33,39 @@ export class DsDynamicFormArrayComponent extends DynamicFormArrayComponent { @Output('dfChange') change: EventEmitter = new EventEmitter(); @Output('dfFocus') focus: EventEmitter = new EventEmitter(); @Output('ngbEvent') customEvent: EventEmitter = new EventEmitter(); + /* tslint:enable:no-output-rename */ constructor(protected layoutService: DynamicFormLayoutService, - protected validationService: DynamicFormValidationService) { - + protected validationService: DynamicFormValidationService, + ) { super(layoutService, validationService); } + moveSelection(event: CdkDragDrop) { + this.model.moveGroup(event.previousIndex, event.currentIndex - event.previousIndex); + const prevIndex = event.previousIndex - 1; + const index = event.currentIndex - 1; + + if (hasValue(this.model.groups[index]) && hasValue((this.control as any).controls[index])) { + const $event = { + $event: { previousIndex: prevIndex }, + context: { index }, + control: (this.control as any).controls[index], + group: this.group, + model: this.model.groups[index].group[0], + type: DynamicFormControlEventType.Change + }; + + this.onChange($event); + } + } + + update(event: any, index: number) { + const $event = Object.assign({}, event, { + context: { index: index - 1} + }); + + this.onChange($event) + } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.model.ts index 995fcbf350..7f7c3e68d5 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.model.ts @@ -17,6 +17,7 @@ export class DynamicDsDatePickerModel extends DynamicDateControlModel { valueUpdates: Subject; malformedDate: boolean; hasLanguages = false; + repeatable = false; constructor(config: DynamicDateControlModelConfig, layout?: DynamicFormControlLayout) { super(config, layout); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component.spec.ts index 8e0c6fc20e..3aaff1339f 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component.spec.ts @@ -15,7 +15,7 @@ describe('DsDynamicDisabledComponent', () => { let model; function init() { - model = new DynamicDisabledModel({ value: 'test', repeatable: false, metadataFields: [], submissionId: '1234', id: '1' }); + model = new DynamicDisabledModel({ value: 'test', repeatable: false, metadataFields: [], submissionId: '1234', id: '1', hasSelectableMetadata: false }); } beforeEach(async(() => { @@ -52,7 +52,6 @@ describe('DsDynamicDisabledComponent', () => { it('should have a disabled input', () => { const input = de.query(By.css('input')); - console.log(input.nativeElement.getAttribute('disabled')); expect(input.nativeElement.getAttribute('disabled')).toEqual(''); }); }); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.model.ts index 0fa2b3e5ed..5eb9aa8dd2 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.model.ts @@ -5,6 +5,7 @@ export const DYNAMIC_FORM_CONTROL_TYPE_DISABLED = 'EMPTY'; export interface DsDynamicDisabledModelConfig extends DsDynamicInputModelConfig { value?: any; + hasSelectableMetadata: boolean; } /** @@ -14,11 +15,14 @@ export class DynamicDisabledModel extends DsDynamicInputModel { @serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_DISABLED; @serializable() value: any; + @serializable() hasSelectableMetadata: boolean; constructor(config: DsDynamicDisabledModelConfig, layout?: DynamicFormControlLayout) { super(config, layout); this.readOnly = true; this.disabled = true; + this.hasSelectableMetadata = config.hasSelectableMetadata; + this.valueUpdates.next(config.value); } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-concat.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-concat.model.ts index af05d5bf35..7d4b58c95d 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-concat.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-concat.model.ts @@ -2,10 +2,11 @@ import { DynamicFormControlLayout, DynamicFormGroupModel, DynamicFormGroupModelC import { Subject } from 'rxjs'; -import { isNotEmpty } from '../../../../empty.util'; +import { hasNoValue, isNotEmpty } from '../../../../empty.util'; import { DsDynamicInputModel } from './ds-dynamic-input.model'; import { FormFieldMetadataValueObject } from '../../models/form-field-metadata-value.model'; import { RelationshipOptions } from '../../models/relationship-options.model'; +import { MetadataValue } from '../../../../../core/shared/metadata.models'; export const CONCAT_GROUP_SUFFIX = '_CONCAT_GROUP'; export const CONCAT_FIRST_INPUT_SUFFIX = '_CONCAT_FIRST_INPUT'; @@ -14,11 +15,14 @@ export const CONCAT_SECOND_INPUT_SUFFIX = '_CONCAT_SECOND_INPUT'; export interface DynamicConcatModelConfig extends DynamicFormGroupModelConfig { separator: string; value?: any; + hint?: string; relationship?: RelationshipOptions; repeatable: boolean; required: boolean; metadataFields: string[]; submissionId: string; + hasSelectableMetadata: boolean; + metadataValue?: MetadataValue; } export class DynamicConcatModel extends DynamicFormGroupModel { @@ -28,8 +32,11 @@ export class DynamicConcatModel extends DynamicFormGroupModel { @serializable() relationship?: RelationshipOptions; @serializable() repeatable?: boolean; @serializable() required?: boolean; + @serializable() hint?: string; @serializable() metadataFields: string[]; @serializable() submissionId: string; + @serializable() hasSelectableMetadata: boolean; + @serializable() metadataValue: MetadataValue; isCustomGroup = true; valueUpdates: Subject; @@ -37,26 +44,30 @@ export class DynamicConcatModel extends DynamicFormGroupModel { constructor(config: DynamicConcatModelConfig, layout?: DynamicFormControlLayout) { super(config, layout); - this.separator = config.separator + ' '; this.relationship = config.relationship; this.repeatable = config.repeatable; this.required = config.required; + this.hint = config.hint; this.metadataFields = config.metadataFields; this.submissionId = config.submissionId; - + this.hasSelectableMetadata = config.hasSelectableMetadata; + this.metadataValue = config.metadataValue; this.valueUpdates = new Subject(); this.valueUpdates.subscribe((value: string) => this.value = value); } get value() { - const firstValue = (this.get(0) as DsDynamicInputModel).value; - const secondValue = (this.get(1) as DsDynamicInputModel).value; - - if (isNotEmpty(firstValue) && isNotEmpty(secondValue)) { - return new FormFieldMetadataValueObject(firstValue + this.separator + secondValue); - } else if (isNotEmpty(firstValue)) { - return new FormFieldMetadataValueObject(firstValue); + const [firstValue, secondValue] = this.group.map((inputModel: DsDynamicInputModel) => + (typeof inputModel.value === 'string') ? + Object.assign(new FormFieldMetadataValueObject(), { value: inputModel.value, display: inputModel.value }) : + (inputModel.value as any)); + if (isNotEmpty(firstValue) && isNotEmpty(firstValue.value) && isNotEmpty(secondValue) && isNotEmpty(secondValue.value)) { + return Object.assign(new FormFieldMetadataValueObject(), firstValue, { value: firstValue.value + this.separator + secondValue.value }); + } else if (isNotEmpty(firstValue) && isNotEmpty(firstValue.value)) { + return Object.assign(new FormFieldMetadataValueObject(), firstValue); + } else if (isNotEmpty(secondValue) && isNotEmpty(secondValue.value)) { + return Object.assign(new FormFieldMetadataValueObject(), secondValue); } else { return null; } @@ -71,18 +82,21 @@ export class DynamicConcatModel extends DynamicFormGroupModel { } else { tempValue = value.value; } - - if (tempValue.includes(this.separator)) { - values = tempValue.split(this.separator); - } else { - values = [tempValue, null]; + if (hasNoValue(tempValue)) { + tempValue = ''; } + values = [...tempValue.split(this.separator), null].map((v) => + Object.assign(new FormFieldMetadataValueObject(), value, { display: v, value: v })); - if (values[0]) { + if (values[0].value) { (this.get(0) as DsDynamicInputModel).valueUpdates.next(values[0]); + } else { + (this.get(0) as DsDynamicInputModel).valueUpdates.next(undefined); } - if (values[1]) { + if (values[1].value) { (this.get(1) as DsDynamicInputModel).valueUpdates.next(values[1]); + } else { + (this.get(1) as DsDynamicInputModel).valueUpdates.next(undefined); } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts index 3827df7be6..7573b67912 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts @@ -6,16 +6,21 @@ import { AuthorityOptions } from '../../../../../core/integration/models/authori import { hasValue } from '../../../../empty.util'; import { FormFieldMetadataValueObject } from '../../models/form-field-metadata-value.model'; import { RelationshipOptions } from '../../models/relationship-options.model'; +import { MetadataValue } from '../../../../../core/shared/metadata.models'; export interface DsDynamicInputModelConfig extends DynamicInputModelConfig { authorityOptions?: AuthorityOptions; languageCodes?: LanguageCode[]; language?: string; + place?: number; value?: any; relationship?: RelationshipOptions; repeatable: boolean; metadataFields: string[]; submissionId: string; + hasSelectableMetadata: boolean; + metadataValue?: MetadataValue; + } export class DsDynamicInputModel extends DynamicInputModel { @@ -28,6 +33,8 @@ export class DsDynamicInputModel extends DynamicInputModel { @serializable() repeatable?: boolean; @serializable() metadataFields: string[]; @serializable() submissionId: string; + @serializable() hasSelectableMetadata: boolean; + @serializable() metadataValue: MetadataValue; constructor(config: DsDynamicInputModelConfig, layout?: DynamicFormControlLayout) { super(config, layout); @@ -38,6 +45,8 @@ export class DsDynamicInputModel extends DynamicInputModel { this.value = config.value; this.relationship = config.relationship; this.submissionId = config.submissionId; + this.hasSelectableMetadata = config.hasSelectableMetadata; + this.metadataValue = config.metadataValue; this.language = config.language; if (!this.language) { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model.ts index 7de319bf56..8925d8fd87 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model.ts @@ -1,18 +1,34 @@ import { DynamicFormArrayModel, DynamicFormArrayModelConfig, DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core'; +import { RelationshipOptions } from '../../models/relationship-options.model'; export interface DynamicRowArrayModelConfig extends DynamicFormArrayModelConfig { notRepeatable: boolean; required: boolean; + submissionId: string; + relationshipConfig: RelationshipOptions; + metadataKey: string; + metadataFields: string[]; + hasSelectableMetadata: boolean; } export class DynamicRowArrayModel extends DynamicFormArrayModel { @serializable() notRepeatable = false; @serializable() required = false; + @serializable() submissionId: string; + @serializable() relationshipConfig: RelationshipOptions; + @serializable() metadataKey: string; + @serializable() metadataFields: string[]; + @serializable() hasSelectableMetadata: boolean; isRowArray = true; constructor(config: DynamicRowArrayModelConfig, layout?: DynamicFormControlLayout) { super(config, layout); this.notRepeatable = config.notRepeatable; this.required = config.required; + this.submissionId = config.submissionId; + this.relationshipConfig = config.relationshipConfig; + this.metadataKey = config.metadataKey; + this.metadataFields = config.metadataFields; + this.hasSelectableMetadata = config.hasSelectableMetadata; } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/form-group/dynamic-form-group.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/form-group/dynamic-form-group.component.html index 897ea4c5e3..c80b0d4e08 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/form-group/dynamic-form-group.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/form-group/dynamic-form-group.component.html @@ -6,7 +6,6 @@ [ngClass]="getClass('element','control')">
    -
    diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts index c77aabfeed..b11aa2cb20 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts @@ -11,7 +11,7 @@ import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstr import { AuthorityService } from '../../../../../../core/integration/authority.service'; import { AuthorityServiceStub } from '../../../../../testing/authority-service.stub'; import { DsDynamicLookupComponent } from './dynamic-lookup.component'; -import { DynamicLookupModel } from './dynamic-lookup.model'; +import { DynamicLookupModel, DynamicLookupModelConfig } from './dynamic-lookup.model'; import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { TranslateModule } from '@ngx-translate/core'; import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model'; @@ -22,7 +22,7 @@ import { DynamicLookupNameModel } from './dynamic-lookup-name.model'; import { AuthorityConfidenceStateDirective } from '../../../../../authority-confidence/authority-confidence-state.directive'; import { ObjNgFor } from '../../../../../utils/object-ngfor.pipe'; -let LOOKUP_TEST_MODEL_CONFIG = { +let LOOKUP_TEST_MODEL_CONFIG: DynamicLookupModelConfig = { authorityOptions: { closed: false, metadata: 'lookup', @@ -39,11 +39,11 @@ let LOOKUP_TEST_MODEL_CONFIG = { readOnly: false, required: true, repeatable: true, - separator: ',', validators: { required: null }, value: undefined, metadataFields: [], - submissionId: '1234' + submissionId: '1234', + hasSelectableMetadata: false }; let LOOKUP_NAME_TEST_MODEL_CONFIG = { @@ -63,11 +63,11 @@ let LOOKUP_NAME_TEST_MODEL_CONFIG = { readOnly: false, required: true, repeatable: true, - separator: ',', validators: { required: null }, value: undefined, metadataFields: [], - submissionId: '1234' + submissionId: '1234', + hasSelectableMetadata: false }; let LOOKUP_TEST_GROUP = new FormGroup({ @@ -94,11 +94,11 @@ describe('Dynamic Lookup component', () => { readOnly: false, required: true, repeatable: true, - separator: ',', validators: { required: null }, value: undefined, metadataFields: [], - submissionId: '1234' + submissionId: '1234', + hasSelectableMetadata: false }; LOOKUP_NAME_TEST_MODEL_CONFIG = { @@ -118,11 +118,11 @@ describe('Dynamic Lookup component', () => { readOnly: false, required: true, repeatable: true, - separator: ',', validators: { required: null }, value: undefined, metadataFields: [], - submissionId: '1234' + submissionId: '1234', + hasSelectableMetadata: false }; LOOKUP_TEST_GROUP = new FormGroup({ diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.spec.ts index bcddb52123..b5cb153db2 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.spec.ts @@ -80,7 +80,8 @@ function init() { submissionScope: undefined, validators: { required: null }, repeatable: false, - metadataFields: [] + metadataFields: [], + hasSelectableMetadata: false } as DynamicRelationGroupModelConfig; FORM_GROUP_TEST_GROUP = new FormGroup({ diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html index cfe50def98..8cb44bc733 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html @@ -1,8 +1,7 @@
    - - diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.spec.ts index 6086444264..21c832d4f8 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.spec.ts @@ -40,7 +40,8 @@ export const SD_TEST_MODEL_CONFIG = { repeatable: false, value: undefined, metadataFields: [], - submissionId: '1234' + submissionId: '1234', + hasSelectableMetadata: false }; describe('Dynamic Dynamic Scrollable Dropdown component', () => { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html index b4a9e4c9c7..909dcb1934 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html @@ -1,5 +1,5 @@
    + { let lookupRelationService; function init() { - relationship = { filter: 'filter', relationshipType: 'isAuthorOfPublication', nameVariants: true } as RelationshipOptions; + relationship = Object.assign(new RelationshipOptions(), { filter: 'filter', relationshipType: 'isAuthorOfPublication', nameVariants: true, searchConfiguration: 'personConfig' }); pSearchOptions = new PaginatedSearchOptions({}); item1 = Object.assign(new Item(), { uuid: 'e1c51c69-896d-42dc-8221-1d5f2ad5516e' }); item2 = Object.assign(new Item(), { uuid: 'c8279647-1acc-41ae-b036-951d5f65649b' }); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts index 9484631610..f851e52537 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts @@ -2,20 +2,18 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angu import { SEARCH_CONFIG_SERVICE } from '../../../../../../+my-dspace-page/my-dspace-page.component'; import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service'; import { Item } from '../../../../../../core/shared/item.model'; -import { PaginatedSearchOptions } from '../../../../../search/paginated-search-options.model'; import { SearchResult } from '../../../../../search/search-result.model'; import { PaginatedList } from '../../../../../../core/data/paginated-list'; import { RemoteData } from '../../../../../../core/data/remote-data'; -import { Observable, ReplaySubject } from 'rxjs'; +import { Observable } from 'rxjs'; import { RelationshipOptions } from '../../../models/relationship-options.model'; import { PaginationComponentOptions } from '../../../../../pagination/pagination-component-options.model'; import { ListableObject } from '../../../../../object-collection/shared/listable-object.model'; import { SearchService } from '../../../../../../core/shared/search/search.service'; import { ActivatedRoute, Router } from '@angular/router'; import { SelectableListService } from '../../../../../object-list/selectable-list/selectable-list.service'; -import { hasValue, isNotEmpty } from '../../../../../empty.util'; -import { concat, map, multicast, switchMap, take, takeWhile, tap } from 'rxjs/operators'; -import { DSpaceObject } from '../../../../../../core/shared/dspace-object.model'; +import { hasValue } from '../../../../../empty.util'; +import { map, startWith, switchMap, take, tap } from 'rxjs/operators'; import { getSucceededRemoteData } from '../../../../../../core/shared/operators'; import { RouteService } from '../../../../../../core/services/route.service'; import { CollectionElementLinkType } from '../../../../../object-collection/collection-element-link.type'; @@ -47,6 +45,7 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest * The ID of the list to add/remove selected items to/from */ @Input() listId: string; + @Input() query: string; /** * Is the selection repeatable? @@ -101,10 +100,10 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest /** * The initial pagination to use */ - initialPagination = Object.assign(new PaginationComponentOptions(), { - id: 'submission-relation-list', + initialPagination = { + page: 1, pageSize: 5 - }); + }; /** * The type of links to display @@ -129,10 +128,8 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest this.resetRoute(); this.routeService.setParameter('fixedFilterQuery', this.relationship.filter); this.routeService.setParameter('configuration', this.relationship.searchConfiguration); - - this.someSelected$ = this.selection$.pipe(map((selection) => isNotEmpty(selection))); this.resultsRD$ = this.searchConfigService.paginatedSearchOptions.pipe( - switchMap((options) => this.lookupRelationService.getLocalResults(this.relationship, options)) + switchMap((options) => this.lookupRelationService.getLocalResults(this.relationship, options).pipe(startWith(undefined))) ); } @@ -141,7 +138,7 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest */ resetRoute() { this.router.navigate([], { - queryParams: Object.assign({}, { pageSize: this.initialPagination.pageSize }, this.route.snapshot.queryParams, { page: 1 }) + queryParams: Object.assign({ query: this.query }, this.route.snapshot.queryParams, this.initialPagination), }); } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.html index 46ee1727fe..cd55553f5b 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.html @@ -8,7 +8,7 @@ {{'submission.sections.describe.relationship-lookup.selection-tab.no-selection' | translate}}
    -

    {{ 'submission.sections.describe.relationship-lookup.selection-tab.title.' + label | translate}}

    +

    {{ 'submission.sections.describe.relationship-lookup.selection-tab.title.' + relationshipType | translate}}

    -
    \ No newline at end of file +
    diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts index a918b51930..b01af2e57b 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts @@ -30,9 +30,9 @@ import { createSuccessfulRemoteDataObject } from '../../../../../remote-data.uti */ export class DsDynamicLookupRelationSelectionTabComponent { /** - * The label to use to display i18n messages (describing the type of relationship) + * A string that describes the type of relationship */ - @Input() label: string; + @Input() relationshipType: string; /** * The ID of the list to add/remove selected items to/from diff --git a/src/app/shared/form/builder/form-builder.service.spec.ts b/src/app/shared/form/builder/form-builder.service.spec.ts index 972abb68b5..400ba0daff 100644 --- a/src/app/shared/form/builder/form-builder.service.spec.ts +++ b/src/app/shared/form/builder/form-builder.service.spec.ts @@ -56,10 +56,10 @@ describe('FormBuilderService test suite', () => { let testFormConfiguration: SubmissionFormsModel; let service: FormBuilderService; - const submissionId = '1234'; + const submissionId = '1234'; function testValidator() { - return {testValidator: {valid: true}}; + return { testValidator: { valid: true } }; } function testAsyncValidator() { @@ -71,10 +71,10 @@ describe('FormBuilderService test suite', () => { TestBed.configureTestingModule({ imports: [ReactiveFormsModule], providers: [ - {provide: FormBuilderService, useClass: FormBuilderService}, - {provide: DynamicFormValidationService, useValue: {}}, - {provide: NG_VALIDATORS, useValue: testValidator, multi: true}, - {provide: NG_ASYNC_VALIDATORS, useValue: testAsyncValidator, multi: true} + { provide: FormBuilderService, useClass: FormBuilderService }, + { provide: DynamicFormValidationService, useValue: {} }, + { provide: NG_VALIDATORS, useValue: testValidator, multi: true }, + { provide: NG_ASYNC_VALIDATORS, useValue: testAsyncValidator, multi: true } ] }); @@ -148,9 +148,9 @@ describe('FormBuilderService test suite', () => { } ), - new DynamicTextAreaModel({id: 'testTextArea'}), + new DynamicTextAreaModel({ id: 'testTextArea' }), - new DynamicCheckboxModel({id: 'testCheckbox'}), + new DynamicCheckboxModel({ id: 'testCheckbox' }), new DynamicFormArrayModel( { @@ -158,10 +158,10 @@ describe('FormBuilderService test suite', () => { initialCount: 5, groupFactory: () => { return [ - new DynamicInputModel({id: 'testFormArrayGroupInput'}), + new DynamicInputModel({ id: 'testFormArrayGroupInput' }), new DynamicFormArrayModel({ id: 'testNestedFormArray', groupFactory: () => [ - new DynamicInputModel({id: 'testNestedFormArrayGroupInput'}) + new DynamicInputModel({ id: 'testNestedFormArrayGroupInput' }) ] }) ]; @@ -173,37 +173,37 @@ describe('FormBuilderService test suite', () => { { id: 'testFormGroup', group: [ - new DynamicInputModel({id: 'nestedTestInput'}), - new DynamicTextAreaModel({id: 'nestedTestTextArea'}) + new DynamicInputModel({ id: 'nestedTestInput' }), + new DynamicTextAreaModel({ id: 'nestedTestTextArea' }) ] } ), - new DynamicSliderModel({id: 'testSlider'}), + new DynamicSliderModel({ id: 'testSlider' }), - new DynamicSwitchModel({id: 'testSwitch'}), + new DynamicSwitchModel({ id: 'testSwitch' }), - new DynamicDatePickerModel({id: 'testDatepicker', value: new Date()}), + new DynamicDatePickerModel({ id: 'testDatepicker', value: new Date() }), - new DynamicFileUploadModel({id: 'testFileUpload'}), + new DynamicFileUploadModel({ id: 'testFileUpload' }), - new DynamicEditorModel({id: 'testEditor'}), + new DynamicEditorModel({ id: 'testEditor' }), - new DynamicTimePickerModel({id: 'testTimePicker'}), + new DynamicTimePickerModel({ id: 'testTimePicker' }), - new DynamicRatingModel({id: 'testRating'}), + new DynamicRatingModel({ id: 'testRating' }), - new DynamicColorPickerModel({id: 'testColorPicker'}), + new DynamicColorPickerModel({ id: 'testColorPicker' }), - new DynamicTypeaheadModel({id: 'testTypeahead', repeatable: false, metadataFields: [], submissionId: '1234'}), + new DynamicTypeaheadModel({ id: 'testTypeahead', repeatable: false, metadataFields: [], submissionId: '1234', hasSelectableMetadata: false }), - new DynamicScrollableDropdownModel({id: 'testScrollableDropdown', authorityOptions: authorityOptions, repeatable: false, metadataFields: [], submissionId: '1234'}), + new DynamicScrollableDropdownModel({ id: 'testScrollableDropdown', authorityOptions: authorityOptions, repeatable: false, metadataFields: [], submissionId: '1234', hasSelectableMetadata: false }), - new DynamicTagModel({id: 'testTag', repeatable: false, metadataFields: [], submissionId: '1234'}), + new DynamicTagModel({ id: 'testTag', repeatable: false, metadataFields: [], submissionId: '1234', hasSelectableMetadata: false }), - new DynamicListCheckboxGroupModel({id: 'testCheckboxList', authorityOptions: authorityOptions, repeatable: true}), + new DynamicListCheckboxGroupModel({ id: 'testCheckboxList', authorityOptions: authorityOptions, repeatable: true }), - new DynamicListRadioGroupModel({id: 'testRadioList', authorityOptions: authorityOptions, repeatable: false}), + new DynamicListRadioGroupModel({ id: 'testRadioList', authorityOptions: authorityOptions, repeatable: false }), new DynamicRelationGroupModel({ submissionId, @@ -211,7 +211,7 @@ describe('FormBuilderService test suite', () => { formConfiguration: [{ fields: [{ hints: 'Enter the name of the author.', - input: {type: 'onebox'}, + input: { type: 'onebox' }, label: 'Authors', languageCodes: [], mandatory: 'true', @@ -221,12 +221,12 @@ describe('FormBuilderService test suite', () => { authority: 'RPAuthority', closed: false, metadata: 'dc.contributor.author' - }], + }] } as FormFieldModel] } as FormRowModel, { fields: [{ hints: 'Enter the affiliation of the author.', - input: {type: 'onebox'}, + input: { type: 'onebox' }, label: 'Affiliation', languageCodes: [], mandatory: 'false', @@ -244,29 +244,35 @@ describe('FormBuilderService test suite', () => { scopeUUID: '', submissionScope: '', repeatable: false, - metadataFields: [] + metadataFields: [], + hasSelectableMetadata: true }), - new DynamicDsDatePickerModel({id: 'testDate'}), + new DynamicDsDatePickerModel({ id: 'testDate' }), - new DynamicLookupModel({id: 'testLookup', repeatable: false, metadataFields: [], submissionId: '1234'}), + new DynamicLookupModel({ id: 'testLookup', repeatable: false, metadataFields: [], submissionId: '1234', hasSelectableMetadata: true }), - new DynamicLookupNameModel({id: 'testLookupName', repeatable: false, metadataFields: [], submissionId: '1234'}), + new DynamicLookupNameModel({ id: 'testLookupName', repeatable: false, metadataFields: [], submissionId: '1234', hasSelectableMetadata: true }), - new DynamicQualdropModel({id: 'testCombobox', readOnly: false, required: false}), + new DynamicQualdropModel({ id: 'testCombobox', readOnly: false, required: false }), new DynamicRowArrayModel( { id: 'testFormRowArray', initialCount: 5, notRepeatable: false, + relationshipConfig: undefined, + submissionId: '1234', groupFactory: () => { return [ - new DynamicInputModel({id: 'testFormRowArrayGroupInput'}) + new DynamicInputModel({ id: 'testFormRowArrayGroupInput' }) ]; }, - required: false - } + required: false, + metadataKey: 'dc.contributor.author', + metadataFields: ['dc.contributor.author'], + hasSelectableMetadata: true + }, ), ]; @@ -628,7 +634,7 @@ describe('FormBuilderService test suite', () => { it('should throw when unknown DynamicFormControlModel id is specified in JSON', () => { - expect(() => service.fromJSON([{id: 'test'}])) + expect(() => service.fromJSON([{ id: 'test' }])) .toThrow(new Error(`unknown form control model type defined on JSON object with id "test"`)); }); @@ -648,8 +654,8 @@ describe('FormBuilderService test suite', () => { const formGroup = service.createFormGroup(testModel); const nestedFormGroup = formGroup.controls.testFormGroup as FormGroup; const nestedFormGroupModel = testModel[7] as DynamicFormGroupModel; - const newModel1 = new DynamicInputModel({id: 'newInput1'}); - const newModel2 = new DynamicInputModel({id: 'newInput2'}); + const newModel1 = new DynamicInputModel({ id: 'newInput1' }); + const newModel2 = new DynamicInputModel({ id: 'newInput2' }); service.addFormGroupControl(formGroup, testModel, newModel1); service.addFormGroupControl(nestedFormGroup, nestedFormGroupModel, newModel2); @@ -666,8 +672,8 @@ describe('FormBuilderService test suite', () => { const formGroup = service.createFormGroup(testModel); const nestedFormGroup = formGroup.controls.testFormGroup as FormGroup; const nestedFormGroupModel = testModel[7] as DynamicFormGroupModel; - const newModel1 = new DynamicInputModel({id: 'newInput1'}); - const newModel2 = new DynamicInputModel({id: 'newInput2'}); + const newModel1 = new DynamicInputModel({ id: 'newInput1' }); + const newModel2 = new DynamicInputModel({ id: 'newInput2' }); service.insertFormGroupControl(4, formGroup, testModel, newModel1); service.insertFormGroupControl(0, nestedFormGroup, nestedFormGroupModel, newModel2); diff --git a/src/app/shared/form/builder/form-builder.service.ts b/src/app/shared/form/builder/form-builder.service.ts index 7e657d97d4..1ca0a2748c 100644 --- a/src/app/shared/form/builder/form-builder.service.ts +++ b/src/app/shared/form/builder/form-builder.service.ts @@ -226,7 +226,7 @@ export class FormBuilderService extends DynamicFormService { } hasArrayGroupValue(model: DynamicFormControlModel): boolean { - return model && (this.isListGroup(model) || model.type === DYNAMIC_FORM_CONTROL_TYPE_TAG); + return model && (this.isListGroup(model) || model.type === DYNAMIC_FORM_CONTROL_TYPE_TAG || model.type === DYNAMIC_FORM_CONTROL_TYPE_ARRAY); } hasMappedGroupValue(model: DynamicFormControlModel): boolean { @@ -283,11 +283,16 @@ export class FormBuilderService extends DynamicFormService { return isNotEmpty(fieldModel) ? formGroup.get(this.getPath(fieldModel)) : null; } + /** + * Note (discovered while debugging) this is not the ID as used in the form, + * but the first part of the path needed in a patch operation: + * e.g. add foo/0 -> the id is 'foo' + */ getId(model: DynamicPathable): string { let tempModel: DynamicFormControlModel; if (this.isArrayGroup(model as DynamicFormControlModel)) { - return model.index.toString(); + return hasValue((model as any).metadataKey) ? (model as any).metadataKey : model.index.toString(); } else if (this.isModelInCustomGroup(model as DynamicFormControlModel)) { tempModel = (model as any).parent; } else { diff --git a/src/app/shared/form/builder/models/form-field-metadata-value.model.ts b/src/app/shared/form/builder/models/form-field-metadata-value.model.ts index 45489e3618..c0bdb338e3 100644 --- a/src/app/shared/form/builder/models/form-field-metadata-value.model.ts +++ b/src/app/shared/form/builder/models/form-field-metadata-value.model.ts @@ -1,7 +1,7 @@ -import { isEmpty, isNotEmpty, isNotNull } from '../../../empty.util'; +import { hasValue, isEmpty, isNotEmpty, isNotNull } from '../../../empty.util'; import { ConfidenceType } from '../../../../core/integration/models/confidence-type'; import { PLACEHOLDER_PARENT_METADATA } from '../ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model'; -import { MetadataValueInterface } from '../../../../core/shared/metadata.models'; +import { MetadataValueInterface, VIRTUAL_METADATA_PREFIX } from '../../../../core/shared/metadata.models'; export interface OtherInformation { [name: string]: string @@ -64,4 +64,12 @@ export class FormFieldMetadataValueObject implements MetadataValueInterface { hasPlaceholder() { return this.hasValue() && this.value === PLACEHOLDER_PARENT_METADATA; } + + get isVirtual(): boolean { + return hasValue(this.authority) && this.authority.startsWith(VIRTUAL_METADATA_PREFIX); + } + + toString() { + return this.display || this.value; + } } diff --git a/src/app/shared/form/builder/models/relationship-options.model.ts b/src/app/shared/form/builder/models/relationship-options.model.ts index f1d3d0ae7a..031f468f25 100644 --- a/src/app/shared/form/builder/models/relationship-options.model.ts +++ b/src/app/shared/form/builder/models/relationship-options.model.ts @@ -1,4 +1,4 @@ -const RELATION_METADATA_PREFIX = 'relation.' +const RELATION_METADATA_PREFIX = 'relation.'; /** * The submission options for fields that can represent relationships @@ -7,7 +7,7 @@ export class RelationshipOptions { relationshipType: string; filter: string; searchConfiguration: string; - nameVariants: boolean; + nameVariants: string; get metadataField() { return RELATION_METADATA_PREFIX + this.relationshipType diff --git a/src/app/shared/form/builder/parsers/concat-field-parser.ts b/src/app/shared/form/builder/parsers/concat-field-parser.ts index 33a92c726d..09a3f53c58 100644 --- a/src/app/shared/form/builder/parsers/concat-field-parser.ts +++ b/src/app/shared/form/builder/parsers/concat-field-parser.ts @@ -13,7 +13,7 @@ import { DynamicConcatModel, DynamicConcatModelConfig } from '../ds-dynamic-form-ui/models/ds-dynamic-concat.model'; -import { isNotEmpty } from '../../../empty.util'; +import { hasNoValue, hasValue, isNotEmpty } from '../../../empty.util'; import { ParserOptions } from './parser-options'; import { CONFIG_DATA, @@ -53,14 +53,18 @@ export class ConcatFieldParser extends FieldParser { }; const groupId = id.replace(/\./g, '_') + CONCAT_GROUP_SUFFIX; - const concatGroup: DynamicConcatModelConfig = this.initModel(groupId, label, false); + const concatGroup: DynamicConcatModelConfig = this.initModel(groupId, label, false, true); concatGroup.group = []; concatGroup.separator = this.separator; const input1ModelConfig: DynamicInputModelConfig = this.initModel(id + CONCAT_FIRST_INPUT_SUFFIX, false, false); const input2ModelConfig: DynamicInputModelConfig = this.initModel(id + CONCAT_SECOND_INPUT_SUFFIX, false, false); - input2ModelConfig.hint = ' '; + + if (hasNoValue(concatGroup.hint) && hasValue(input1ModelConfig.hint) && hasNoValue(input2ModelConfig.hint)) { + concatGroup.hint = input1ModelConfig.hint; + input1ModelConfig.hint = undefined; + } if (this.configData.mandatory) { concatGroup.required = true; diff --git a/src/app/shared/form/builder/parsers/disabled-field-parser.spec.ts b/src/app/shared/form/builder/parsers/disabled-field-parser.spec.ts index c885b737c2..9cd0421ea6 100644 --- a/src/app/shared/form/builder/parsers/disabled-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/disabled-field-parser.spec.ts @@ -51,7 +51,7 @@ describe('DisabledFieldParser test suite', () => { it('should set init value properly', () => { initFormValues = { description: [ - new FormFieldMetadataValueObject('test description'), + 'test description', ], }; const expectedValue ='test description'; @@ -59,7 +59,7 @@ describe('DisabledFieldParser test suite', () => { const parser = new DisabledFieldParser(submissionId, field, initFormValues, parserOptions); const fieldModel = parser.parse(); - expect(fieldModel.value).toEqual(expectedValue); + expect(fieldModel.value.value).toEqual(expectedValue); }); }); diff --git a/src/app/shared/form/builder/parsers/disabled-field-parser.ts b/src/app/shared/form/builder/parsers/disabled-field-parser.ts index 14d7051466..330c288fe9 100644 --- a/src/app/shared/form/builder/parsers/disabled-field-parser.ts +++ b/src/app/shared/form/builder/parsers/disabled-field-parser.ts @@ -9,7 +9,7 @@ export class DisabledFieldParser extends FieldParser { public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any { const emptyModelConfig: DsDynamicDisabledModelConfig = this.initModel(null, label); - this.setValues(emptyModelConfig, fieldValue); + this.setValues(emptyModelConfig, fieldValue, true); return new DynamicDisabledModel(emptyModelConfig) } } diff --git a/src/app/shared/form/builder/parsers/field-parser.ts b/src/app/shared/form/builder/parsers/field-parser.ts index f218d442e1..6fafe9cbc1 100644 --- a/src/app/shared/form/builder/parsers/field-parser.ts +++ b/src/app/shared/form/builder/parsers/field-parser.ts @@ -1,13 +1,10 @@ import { Inject, InjectionToken } from '@angular/core'; -import { hasValue, isNotEmpty, isNotNull, isNotUndefined, isEmpty } from '../../../empty.util'; +import { hasValue, isNotEmpty, isNotNull, isNotUndefined } from '../../../empty.util'; import { FormFieldModel } from '../models/form-field.model'; import { uniqueId } from 'lodash'; import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; -import { - DynamicRowArrayModel, - DynamicRowArrayModelConfig -} from '../ds-dynamic-form-ui/models/ds-dynamic-row-array-model'; +import { DynamicRowArrayModel, DynamicRowArrayModelConfig } from '../ds-dynamic-form-ui/models/ds-dynamic-row-array-model'; import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-form-ui/models/ds-dynamic-input.model'; import { DynamicFormControlLayout } from '@ng-dynamic-forms/core'; import { setLayout } from './parser.utils'; @@ -39,36 +36,51 @@ export abstract class FieldParser { && (this.configData.input.type !== 'list') && (this.configData.input.type !== 'tag') && (this.configData.input.type !== 'group') - && isEmpty(this.configData.selectableRelationship) ) { let arrayCounter = 0; let fieldArrayCounter = 0; + let metadataKey; + + if (Array.isArray(this.configData.selectableMetadata) && this.configData.selectableMetadata.length === 1) { + metadataKey = this.configData.selectableMetadata[0].metadata; + } const config = { id: uniqueId() + '_array', label: this.configData.label, initialCount: this.getInitArrayIndex(), notRepeatable: !this.configData.repeatable, - required: JSON.parse( this.configData.mandatory), + relationshipConfig: this.configData.selectableRelationship, + required: JSON.parse(this.configData.mandatory), + submissionId: this.submissionId, + metadataKey, + metadataFields: this.getAllFieldIds(), + hasSelectableMetadata: isNotEmpty(this.configData.selectableMetadata), groupFactory: () => { let model; if ((arrayCounter === 0)) { model = this.modelFactory(); arrayCounter++; } else { - const fieldArrayOfValueLenght = this.getInitValueCount(arrayCounter - 1); + const fieldArrayOfValueLength = this.getInitValueCount(arrayCounter - 1); let fieldValue = null; - if (fieldArrayOfValueLenght > 0) { - fieldValue = this.getInitFieldValue(arrayCounter - 1, fieldArrayCounter++); - if (fieldArrayCounter === fieldArrayOfValueLenght) { + if (fieldArrayOfValueLength > 0) { + if (fieldArrayCounter === 0) { + fieldValue = ''; + } else { + fieldValue = this.getInitFieldValue(arrayCounter - 1, fieldArrayCounter - 1); + } + fieldArrayCounter++; + if (fieldArrayCounter === fieldArrayOfValueLength + 1) { fieldArrayCounter = 0; arrayCounter++; } } model = this.modelFactory(fieldValue, false); + model.id = `${model.id}_${fieldArrayCounter}`; } setLayout(model, 'element', 'host', 'col'); - if (model.hasLanguages) { + if (model.hasLanguages || isNotEmpty(model.relationship)) { setLayout(model, 'grid', 'control', 'col'); } return [model]; @@ -85,6 +97,7 @@ export abstract class FieldParser { } else { const model = this.modelFactory(this.getInitFieldValue()); + model.submissionId = this.submissionId; if (model.hasLanguages || isNotEmpty(model.relationship)) { setLayout(model, 'grid', 'control', 'col'); } @@ -147,9 +160,10 @@ export abstract class FieldParser { } protected getInitArrayIndex() { + let fieldCount = 0; const fieldIds: any = this.getAllFieldIds(); if (isNotEmpty(this.initFormValues) && isNotNull(fieldIds) && fieldIds.length === 1 && this.initFormValues.hasOwnProperty(fieldIds)) { - return this.initFormValues[fieldIds].length; + fieldCount = this.initFormValues[fieldIds].filter((value) => hasValue(value) && hasValue(value.value)).length; } else if (isNotEmpty(this.initFormValues) && isNotNull(fieldIds) && fieldIds.length > 1) { let counter = 0; fieldIds.forEach((id) => { @@ -157,10 +171,9 @@ export abstract class FieldParser { counter = counter + this.initFormValues[id].length; } }); - return (counter === 0) ? 1 : counter; - } else { - return 1; + fieldCount = counter; } + return (fieldCount === 0) ? 1 : fieldCount + 1 } protected getFieldId(): string { @@ -178,11 +191,11 @@ export abstract class FieldParser { return ids; } } else { - return [this.configData.selectableRelationship.relationshipType]; + return ['relation.' + this.configData.selectableRelationship.relationshipType]; } } - protected initModel(id?: string, label = true, setErrors = true) { + protected initModel(id?: string, label = true, setErrors = true, hint = true) { const controlModel = Object.create(null); @@ -202,16 +215,17 @@ export abstract class FieldParser { controlModel.relationship = Object.assign(new RelationshipOptions(), this.configData.selectableRelationship); } controlModel.repeatable = this.configData.repeatable; - controlModel.metadataFields = isNotEmpty(this.configData.selectableMetadata) ? this.configData.selectableMetadata.map((metadataObject) => metadataObject.metadata) : []; + controlModel.metadataFields = this.getAllFieldIds() || []; + controlModel.hasSelectableMetadata = isNotEmpty(this.configData.selectableMetadata); controlModel.submissionId = this.submissionId; // Set label this.setLabel(controlModel, label); - + if (hint) { + controlModel.hint = this.configData.hints; + } controlModel.placeholder = this.configData.label; - controlModel.hint = this.configData.hints; - if (this.configData.mandatory && setErrors) { this.markAsRequired(controlModel); } @@ -247,7 +261,6 @@ export abstract class FieldParser { {}, controlModel.errorMessages, { pattern: 'error.validation.pattern' }); - } protected markAsRequired(controlModel) { @@ -302,7 +315,9 @@ export abstract class FieldParser { } if (typeof fieldValue === 'object') { + modelConfig.metadataValue = fieldValue; modelConfig.language = fieldValue.language; + modelConfig.place = fieldValue.place; if (forceValueAsObj) { modelConfig.value = fieldValue; } else { @@ -320,7 +335,6 @@ export abstract class FieldParser { } } } - return modelConfig; } diff --git a/src/app/shared/form/builder/parsers/name-field-parser.spec.ts b/src/app/shared/form/builder/parsers/name-field-parser.spec.ts index 1b0c637030..8b75856256 100644 --- a/src/app/shared/form/builder/parsers/name-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/name-field-parser.spec.ts @@ -95,7 +95,7 @@ describe('NameFieldParser test suite', () => { initFormValues = { name: [new FormFieldMetadataValueObject('test, name')], }; - const expectedValue = new FormFieldMetadataValueObject('test, name'); + const expectedValue = new FormFieldMetadataValueObject('test, name', undefined, undefined, 'test'); const parser = new NameFieldParser(submissionId, field1, initFormValues, parserOptions); diff --git a/src/app/shared/form/builder/parsers/series-field-parser.spec.ts b/src/app/shared/form/builder/parsers/series-field-parser.spec.ts index ceb4e96320..4e34acb401 100644 --- a/src/app/shared/form/builder/parsers/series-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/series-field-parser.spec.ts @@ -58,7 +58,7 @@ describe('SeriesFieldParser test suite', () => { initFormValues = { series: [new FormFieldMetadataValueObject('test; series')], }; - const expectedValue = new FormFieldMetadataValueObject('test; series'); + const expectedValue = new FormFieldMetadataValueObject('test; series', undefined, undefined, 'test'); const parser = new SeriesFieldParser(submissionId, field, initFormValues, parserOptions); diff --git a/src/app/shared/form/form.component.html b/src/app/shared/form/form.component.html index 20fb942380..32ccc3b696 100644 --- a/src/app/shared/form/form.component.html +++ b/src/app/shared/form/form.component.html @@ -1,65 +1,57 @@
    -
    + - + + + +
    +
    + +
    +
    - + +
    +
    + +
    +
    +
    - -
    -
    - - -
    -
    +
    - -
    -
    - -
    -
    - - -
    - - +
    -
    -
    - -
    - - +
    +
    +
    + + +
    +
    -
    -
    -
    +
    diff --git a/src/app/shared/form/form.component.scss b/src/app/shared/form/form.component.scss index acdeb792ca..01cf09576f 100644 --- a/src/app/shared/form/form.component.scss +++ b/src/app/shared/form/form.component.scss @@ -42,8 +42,3 @@ .right-addon input { padding-right: $spacer * 2.25; } - -.ds-form-qualdrop-hint { - top: -$spacer; - position: relative; -} diff --git a/src/app/shared/form/form.component.spec.ts b/src/app/shared/form/form.component.spec.ts index 6a8a1229d4..f7a0564191 100644 --- a/src/app/shared/form/form.component.spec.ts +++ b/src/app/shared/form/form.component.spec.ts @@ -92,7 +92,6 @@ function init() { groupFactory: () => { return [ new DynamicInputModel({ - id: 'bootstrapArrayGroupInput', placeholder: 'example array group input', readOnly: false @@ -362,7 +361,7 @@ describe('FormComponent test suite', () => { spyOn((formComp as any).formService, 'validateAllFormFields'); - form.next(formState.testForm) + form.next(formState.testForm); formFixture.detectChanges(); formComp.onSubmit(); @@ -418,7 +417,7 @@ describe('FormComponent test suite', () => { })); it('should dispatch FormChangeAction when an item has been removed from an array', inject([FormBuilderService], (service: FormBuilderService) => { - formComp.removeItem(new Event('click'), formComp.formModel[0] as DynamicFormArrayModel, 0); + formComp.removeItem(new Event('click'), formComp.formModel[0] as DynamicFormArrayModel, 1); expect(store.dispatch).toHaveBeenCalledWith(new FormChangeAction('testFormArray', service.getValueFromModel(formComp.formModel))); })); @@ -426,7 +425,7 @@ describe('FormComponent test suite', () => { it('should emit removeArrayItem Event when an item has been removed from an array', inject([FormBuilderService], (service: FormBuilderService) => { spyOn(formComp.removeArrayItem, 'emit'); - formComp.removeItem(new Event('click'), formComp.formModel[0] as DynamicFormArrayModel, 0); + formComp.removeItem(new Event('click'), formComp.formModel[0] as DynamicFormArrayModel, 1); expect(formComp.removeArrayItem.emit).toHaveBeenCalled(); })); diff --git a/src/app/shared/form/form.component.ts b/src/app/shared/form/form.component.ts index def61cb5b2..dee06c29b2 100644 --- a/src/app/shared/form/form.component.ts +++ b/src/app/shared/form/form.component.ts @@ -1,28 +1,18 @@ import { distinctUntilChanged, filter, map } from 'rxjs/operators'; -import { - ChangeDetectorRef, - Component, - EventEmitter, - Input, - OnDestroy, - OnInit, - Output, ViewEncapsulation -} from '@angular/core'; +import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { AbstractControl, FormArray, FormControl, FormGroup } from '@angular/forms'; -import { - DynamicFormArrayModel, - DynamicFormControlEvent, - DynamicFormControlModel, - DynamicFormGroupModel, - DynamicFormLayout, -} from '@ng-dynamic-forms/core'; +import { DynamicFormArrayModel, DynamicFormControlEvent, DynamicFormControlModel, DynamicFormGroupModel, DynamicFormLayout, } from '@ng-dynamic-forms/core'; import { findIndex } from 'lodash'; import { FormBuilderService } from './builder/form-builder.service'; import { Observable, Subscription } from 'rxjs'; import { hasValue, isNotEmpty, isNotNull, isNull } from '../empty.util'; import { FormService } from './form.service'; import { FormEntry, FormError } from './form.reducer'; +import { DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN } from './builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model'; +import { QUALDROP_GROUP_SUFFIX } from './builder/ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model'; + +const QUALDROP_GROUP_REGEX = new RegExp(`${QUALDROP_GROUP_SUFFIX}_\\d+$`); /** * The default form component. @@ -304,15 +294,49 @@ export class FormComponent implements OnDestroy, OnInit { removeItem($event, arrayContext: DynamicFormArrayModel, index: number): void { const formArrayControl = this.formGroup.get(this.formBuilderService.getPath(arrayContext)) as FormArray; - this.removeArrayItem.emit(this.getEvent($event, arrayContext, index, 'remove')); + this.removeArrayItem.emit(this.getEvent($event, arrayContext, index - 1, 'remove')); this.formBuilderService.removeFormArrayGroup(index, formArrayControl, arrayContext); this.formService.changeForm(this.formId, this.formModel); } insertItem($event, arrayContext: DynamicFormArrayModel, index: number): void { const formArrayControl = this.formGroup.get(this.formBuilderService.getPath(arrayContext)) as FormArray; - this.formBuilderService.insertFormArrayGroup(index, formArrayControl, arrayContext); - this.addArrayItem.emit(this.getEvent($event, arrayContext, index, 'add')); + + // First emit the new value so it can be sent to the server + const value = formArrayControl.controls[0].value; + const event = this.getEvent($event, arrayContext, 0, 'add'); + this.addArrayItem.emit(event); + this.change.emit(event); + + // Next: update the UI so the user sees the changes + // without having to wait for the server's reply + + // add an empty new field at the bottom + this.formBuilderService.addFormArrayGroup(formArrayControl, arrayContext); + + // set that field to the new value + const model = arrayContext.groups[arrayContext.groups.length - 1].group[0] as any; + if (model.type === DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN) { + model.value = Object.values(value)[0]; + } else if (this.formBuilderService.isQualdropGroup(model)) { + const ctrl = formArrayControl.controls[formArrayControl.length - 1]; + const ctrlKey = Object.keys(ctrl.value).find((key: string) => isNotEmpty(key.match(QUALDROP_GROUP_REGEX))); + const valueKey = Object.keys(value).find((key: string) => isNotEmpty(key.match(QUALDROP_GROUP_REGEX))); + if (ctrlKey !== valueKey) { + Object.defineProperty(value, ctrlKey, Object.getOwnPropertyDescriptor(value, valueKey)); + delete value[valueKey]; + } + ctrl.setValue(value); + } else { + formArrayControl.controls[formArrayControl.length - 1].setValue(value); + } + + // Clear the topmost field by removing the filled out version and inserting a new, empty version. + // Doing it this way ensures an empty value of the correct type is added without a bunch of ifs here + this.formBuilderService.removeFormArrayGroup(0, formArrayControl, arrayContext); + this.formBuilderService.insertFormArrayGroup(0, formArrayControl, arrayContext); + + // Tell the formService that it should rerender. this.formService.changeForm(this.formId, this.formModel); } diff --git a/src/app/shared/metadata-representation/metadata-representation-loader.component.ts b/src/app/shared/metadata-representation/metadata-representation-loader.component.ts index 86bede6789..c091534f62 100644 --- a/src/app/shared/metadata-representation/metadata-representation-loader.component.ts +++ b/src/app/shared/metadata-representation/metadata-representation-loader.component.ts @@ -5,6 +5,7 @@ import { Context } from '../../core/shared/context.model'; import { GenericConstructor } from '../../core/shared/generic-constructor'; import { MetadataRepresentationListElementComponent } from '../object-list/metadata-representation-list-element/metadata-representation-list-element.component'; import { MetadataRepresentationDirective } from './metadata-representation.directive'; +import { hasValue } from '../empty.util'; @Component({ selector: 'ds-metadata-representation-loader', @@ -15,10 +16,21 @@ import { MetadataRepresentationDirective } from './metadata-representation.direc * Component for determining what component to use depending on the item's relationship type (relationship.type), its metadata representation and, optionally, its context */ export class MetadataRepresentationLoaderComponent implements OnInit { + private componentRefInstance: MetadataRepresentationListElementComponent; + /** * The item or metadata to determine the component for */ - @Input() mdRepresentation: MetadataRepresentation; + private _mdRepresentation: MetadataRepresentation; + get mdRepresentation(): MetadataRepresentation { + return this._mdRepresentation; + } + @Input() set mdRepresentation(nextValue: MetadataRepresentation) { + this._mdRepresentation = nextValue; + if (hasValue(this.componentRefInstance)) { + this.componentRefInstance.metadataRepresentation = nextValue; + } + } /** * The optional context @@ -43,7 +55,8 @@ export class MetadataRepresentationLoaderComponent implements OnInit { viewContainerRef.clear(); const componentRef = viewContainerRef.createComponent(componentFactory); - (componentRef.instance as MetadataRepresentationListElementComponent).metadataRepresentation = this.mdRepresentation; + this.componentRefInstance = componentRef.instance as MetadataRepresentationListElementComponent; + this.componentRefInstance.metadataRepresentation = this.mdRepresentation; } /** diff --git a/src/app/shared/mocks/form-models.mock.ts b/src/app/shared/mocks/form-models.mock.ts index e4f9ec3131..cd6228417b 100644 --- a/src/app/shared/mocks/form-models.mock.ts +++ b/src/app/shared/mocks/form-models.mock.ts @@ -10,7 +10,6 @@ import { AuthorityValue } from '../../core/integration/models/authority.value'; import { FormFieldMetadataValueObject } from '../form/builder/models/form-field-metadata-value.model'; import { DynamicRowGroupModel } from '../form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-group-model'; import { FormRowModel } from '../../core/config/models/config-submission-form.model'; -import { WorkspaceItem } from '../../core/submission/models/workspaceitem.model'; export const qualdropSelectConfig = { name: 'dc.identifier_QUALDROP_METADATA', @@ -56,7 +55,8 @@ export const qualdropInputConfig = { repeatable: false, value: 'test', submissionId: '1234', - metadataFields: [] + metadataFields: [], + hasSelectableMetadata: false }; export const mockQualdropSelectModel = new DynamicSelectModel(qualdropSelectConfig); @@ -76,10 +76,15 @@ const rowArrayQualdropConfig = { id: 'row_QUALDROP_GROUP', initialCount: 1, notRepeatable: true, + relationshipConfig: undefined, groupFactory: () => { return [MockQualdropModel]; }, - required: false + required: false, + submissionId: '1234', + metadataKey: 'dc.some.key', + metadataFields: ['dc.some.key'], + hasSelectableMetadata: false } as DynamicRowArrayModelConfig; export const MockRowArrayQualdropModel: DynamicRowArrayModel = new DynamicRowArrayModel(rowArrayQualdropConfig); @@ -136,7 +141,8 @@ const relationGroupConfig = { 'issue test 2' ], }, - metadataFields: [] + metadataFields: [], + hasSelectableMetadata: false }; export const MockRelationModel: DynamicRelationGroupModel = new DynamicRelationGroupModel(relationGroupConfig); @@ -165,7 +171,8 @@ export const inputWithLanguageAndAuthorityConfig = { id: 'testWithLanguageAndAuthority', }, submissionId: '1234', - metadataFields: [] + metadataFields: [], + hasSelectableMetadata: false }; export const mockInputWithLanguageAndAuthorityModel = new DsDynamicInputModel(inputWithLanguageAndAuthorityConfig); @@ -189,7 +196,8 @@ export const inputWithLanguageConfig = { repeatable: false, value: 'testWithLanguage', submissionId: '1234', - metadataFields: [] + metadataFields: [], + hasSelectableMetadata: false }; export const mockInputWithLanguageModel = new DsDynamicInputModel(inputWithLanguageConfig); @@ -218,7 +226,8 @@ export const inputWithLanguageAndAuthorityArrayConfig = { id: 'testLanguageAndAuthorityArray', }], submissionId: '1234', - metadataFields: [] + metadataFields: [], + hasSelectableMetadata: false }; export const mockInputWithLanguageAndAuthorityArrayModel = new DsDynamicInputModel(inputWithLanguageAndAuthorityArrayConfig); @@ -231,7 +240,8 @@ export const inputWithFormFieldValueConfig = { repeatable: false, value: new FormFieldMetadataValueObject('testWithFormFieldValue'), submissionId: '1234', - metadataFields: [] + metadataFields: [], + hasSelectableMetadata: false }; export const mockInputWithFormFieldValueModel = new DsDynamicInputModel(inputWithFormFieldValueConfig); @@ -244,7 +254,8 @@ export const inputWithAuthorityValueConfig = { repeatable: false, value: Object.assign({}, new AuthorityValue(), { value: 'testWithAuthorityValue', id: 'testWithAuthorityValue', display: 'testWithAuthorityValue' }), submissionId: '1234', - metadataFields: [] + metadataFields: [], + hasSelectableMetadata: false }; export const mockInputWithAuthorityValueModel = new DsDynamicInputModel(inputWithAuthorityValueConfig); @@ -257,7 +268,8 @@ export const inputWithObjectValueConfig = { repeatable: false, value: { value: 'testWithObjectValue', id: 'testWithObjectValue', display: 'testWithObjectValue' }, submissionId: '1234', - metadataFields: [] + metadataFields: [], + hasSelectableMetadata: false }; export const mockInputWithObjectValueModel = new DsDynamicInputModel(inputWithObjectValueConfig); @@ -274,7 +286,8 @@ export const fileFormEditInputConfig = { disabled: false, repeatable: false, submissionId: '1234', - metadataFields: [] + metadataFields: [], + hasSelectableMetadata: false }; export const mockFileFormEditInputModel = new DsDynamicInputModel(fileFormEditInputConfig); diff --git a/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.spec.ts b/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.spec.ts index 6e7f7bf65b..503af1238a 100644 --- a/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.spec.ts +++ b/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.spec.ts @@ -148,7 +148,7 @@ describe('WorkspaceitemActionsComponent', () => { it('should display a success notification on delete success', async(() => { spyOn((component as any).modalService, 'open').and.returnValue({result: Promise.resolve('ok')}); - mockDataService.delete.and.returnValue(observableOf(true)); + mockDataService.delete.and.returnValue(observableOf({ isSuccessful: true })); spyOn(component, 'reload'); component.confirmDiscard('ok'); diff --git a/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.ts b/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.ts index 27512d899e..e28c6cb11d 100644 --- a/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.ts +++ b/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.ts @@ -11,6 +11,7 @@ import { WorkspaceitemDataService } from '../../../core/submission/workspaceitem import { NotificationsService } from '../../notifications/notifications.service'; import { RequestService } from '../../../core/data/request.service'; import { SearchService } from '../../../core/shared/search/search.service'; +import { RestResponse } from '../../../core/cache/response.models'; /** * This component represents actions related to WorkspaceItem object. @@ -63,9 +64,9 @@ export class WorkspaceitemActionsComponent extends MyDSpaceActionsComponent { + .subscribe((response: RestResponse) => { this.processingDelete$.next(false); - this.handleActionResponse(response); + this.handleActionResponse(response.isSuccessful); }) } } diff --git a/src/app/shared/object-collection/object-collection.component.spec.ts b/src/app/shared/object-collection/object-collection.component.spec.ts index 7ba8328495..4d23600603 100644 --- a/src/app/shared/object-collection/object-collection.component.spec.ts +++ b/src/app/shared/object-collection/object-collection.component.spec.ts @@ -33,8 +33,8 @@ describe('ObjectCollectionComponent', () => { beforeEach(async(() => { fixture = TestBed.createComponent(ObjectCollectionComponent); objectCollectionComponent = fixture.componentInstance; - })); + it('should only show the grid component when the viewmode is set to grid', () => { objectCollectionComponent.currentMode$ = observableOf(ViewMode.GridElement); diff --git a/src/app/shared/object-collection/shared/listable-object/listable-object-component-loader.component.ts b/src/app/shared/object-collection/shared/listable-object/listable-object-component-loader.component.ts index 4e6e206ddd..255f66ac86 100644 --- a/src/app/shared/object-collection/shared/listable-object/listable-object-component-loader.component.ts +++ b/src/app/shared/object-collection/shared/listable-object/listable-object-component-loader.component.ts @@ -46,6 +46,11 @@ export class ListableObjectComponentLoaderComponent implements OnInit { */ @Input() listID: string; + /** + * Whether to show the badge label or not + */ + @Input() showLabel = true; + /** * Directive hook used to place the dynamic child component */ @@ -68,6 +73,9 @@ export class ListableObjectComponentLoaderComponent implements OnInit { (componentRef.instance as any).index = this.index; (componentRef.instance as any).linkType = this.linkType; (componentRef.instance as any).listID = this.listID; + (componentRef.instance as any).showLabel = this.showLabel; + (componentRef.instance as any).context = this.context; + (componentRef.instance as any).viewMode = this.viewMode; } /** diff --git a/src/app/shared/object-collection/shared/object-collection-element/abstract-listable-element.component.ts b/src/app/shared/object-collection/shared/object-collection-element/abstract-listable-element.component.ts index 3602f45ede..402984731c 100644 --- a/src/app/shared/object-collection/shared/object-collection-element/abstract-listable-element.component.ts +++ b/src/app/shared/object-collection/shared/object-collection-element/abstract-listable-element.component.ts @@ -29,6 +29,21 @@ export class AbstractListableElementComponent { */ @Input() index: number; + /** + * Whether to show the badge label or not + */ + @Input() showLabel = true; + + /** + * The context we matched on to get this component + */ + @Input() context: Context; + + /** + * The viewmode we matched on to get this component + */ + @Input() viewMode: ViewMode; + /** * The available link types */ diff --git a/src/app/shared/object-collection/shared/selectable-list-item-control/selectable-list-item-control.component.html b/src/app/shared/object-collection/shared/selectable-list-item-control/selectable-list-item-control.component.html index 92d85d03f4..56a83913a7 100644 --- a/src/app/shared/object-collection/shared/selectable-list-item-control/selectable-list-item-control.component.html +++ b/src/app/shared/object-collection/shared/selectable-list-item-control/selectable-list-item-control.component.html @@ -4,7 +4,7 @@ [id]="'object' + index" [ngModel]="selected$ | async" (ngModelChange)="selectCheckbox($event)"> - + diff --git a/src/app/shared/object-grid/search-result-grid-element/item-search-result/publication/publication-search-result-grid-element.component.html b/src/app/shared/object-grid/search-result-grid-element/item-search-result/publication/publication-search-result-grid-element.component.html index ec0b792e34..3c2d54b003 100644 --- a/src/app/shared/object-grid/search-result-grid-element/item-search-result/publication/publication-search-result-grid-element.component.html +++ b/src/app/shared/object-grid/search-result-grid-element/item-search-result/publication/publication-search-result-grid-element.component.html @@ -15,7 +15,7 @@
    - +

    diff --git a/src/app/shared/object-list/item-list-element/item-types/publication/publication-list-element.component.html b/src/app/shared/object-list/item-list-element/item-types/publication/publication-list-element.component.html index acc3ee4194..dcebcfd56a 100644 --- a/src/app/shared/object-list/item-list-element/item-types/publication/publication-list-element.component.html +++ b/src/app/shared/object-list/item-list-element/item-types/publication/publication-list-element.component.html @@ -1 +1 @@ - \ No newline at end of file + diff --git a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/publication/publication-search-result-list-element.component.html b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/publication/publication-search-result-list-element.component.html index 3d2604585d..bd00e4aff1 100644 --- a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/publication/publication-search-result-list-element.component.html +++ b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/publication/publication-search-result-list-element.component.html @@ -1,4 +1,4 @@ - + = { diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index cc61a6d868..e4114971ab 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -191,6 +191,7 @@ import { MissingTranslationHelper } from './translate/missing-translation.helper import { ItemVersionsNoticeComponent } from './item/item-versions/notice/item-versions-notice.component'; import { FileValidator } from './utils/require-file.validator'; import { FileValueAccessorDirective } from './utils/file-value-accessor.directive'; +import { ExistingRelationListElementComponent } from './form/builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component'; import { ModifyItemOverviewComponent } from '../+item-page/edit-item-page/modify-item-overview/modify-item-overview.component'; import { ClaimedTaskActionsLoaderComponent } from './mydspace-actions/claimed-task/switcher/claimed-task-actions-loader.component'; import { ClaimedTaskActionsDirective } from './mydspace-actions/claimed-task/switcher/claimed-task-actions.directive'; @@ -380,6 +381,7 @@ const COMPONENTS = [ ExternalSourceEntryImportModalComponent, ImportableListItemControlComponent, ExistingMetadataListElementComponent, + ExistingRelationListElementComponent, LogInShibbolethComponent, LogInPasswordComponent, LogInContainerComponent, @@ -459,6 +461,7 @@ const ENTRY_COMPONENTS = [ DsDynamicLookupRelationSelectionTabComponent, DsDynamicLookupRelationExternalSourceTabComponent, ExternalSourceEntryImportModalComponent, + ExistingRelationListElementComponent, LogInPasswordComponent, LogInShibbolethComponent, ItemVersionsComponent, diff --git a/src/app/submission/edit/submission-edit.component.html b/src/app/submission/edit/submission-edit.component.html index dcd8d84edc..19bcd6f079 100644 --- a/src/app/submission/edit/submission-edit.component.html +++ b/src/app/submission/edit/submission-edit.component.html @@ -3,5 +3,6 @@ [sections]="sections" [selfUrl]="selfUrl" [submissionDefinition]="submissionDefinition" + [item]="item" [submissionId]="submissionId">
    diff --git a/src/app/submission/edit/submission-edit.component.ts b/src/app/submission/edit/submission-edit.component.ts index 908f473136..12811173dd 100644 --- a/src/app/submission/edit/submission-edit.component.ts +++ b/src/app/submission/edit/submission-edit.component.ts @@ -13,6 +13,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s import { SubmissionObject } from '../../core/submission/models/submission-object.model'; import { Collection } from '../../core/shared/collection.model'; import { RemoteData } from '../../core/data/remote-data'; +import { Item } from '../../core/shared/item.model'; /** * This component allows to edit an existing workspaceitem/workflowitem. @@ -59,6 +60,7 @@ export class SubmissionEditComponent implements OnDestroy, OnInit { * @type {Array} */ private subs: Subscription[] = []; + public item: Item; /** * Initialize instance variables @@ -96,6 +98,7 @@ export class SubmissionEditComponent implements OnDestroy, OnInit { this.collectionId = (submissionObjectRD.payload.collection as Collection).id; this.selfUrl = submissionObjectRD.payload._links.self.href; this.sections = submissionObjectRD.payload.sections; + this.item = submissionObjectRD.payload.item as Item; this.submissionDefinition = (submissionObjectRD.payload.submissionDefinition as SubmissionDefinitionsModel); this.changeDetectorRef.detectChanges(); } diff --git a/src/app/submission/form/submission-form.component.spec.ts b/src/app/submission/form/submission-form.component.spec.ts index ab5d3ad508..cebc12c987 100644 --- a/src/app/submission/form/submission-form.component.spec.ts +++ b/src/app/submission/form/submission-form.component.spec.ts @@ -20,6 +20,7 @@ import { AuthServiceStub } from '../../shared/testing/auth-service.stub'; import { AuthService } from '../../core/auth/auth.service'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; import { createTestComponent } from '../../shared/testing/utils.test'; +import { Item } from '../../core/shared/item.model'; describe('SubmissionFormComponent Component', () => { @@ -66,7 +67,7 @@ describe('SubmissionFormComponent Component', () => { `; + [submissionId]="submissionId" [item]="item">`; testFixture = createTestComponent(html, TestComponent) as ComponentFixture; testComp = testFixture.componentInstance; @@ -118,6 +119,7 @@ describe('SubmissionFormComponent Component', () => { comp.submissionDefinition = submissionDefinition; comp.selfUrl = selfUrl; comp.sections = sectionsData; + comp.item = new Item(); submissionServiceStub.getSubmissionObject.and.returnValue(observableOf(submissionState)); submissionServiceStub.getSubmissionSections.and.returnValue(observableOf(sectionsList)); @@ -143,6 +145,7 @@ describe('SubmissionFormComponent Component', () => { selfUrl, submissionDefinition, sectionsData, + comp.item, null); expect(submissionServiceStub.startAutoSave).toHaveBeenCalled(); }); @@ -153,6 +156,7 @@ describe('SubmissionFormComponent Component', () => { comp.submissionDefinition = submissionDefinition; comp.selfUrl = selfUrl; comp.sections = sectionsData; + comp.item = new Item(); comp.onCollectionChange(submissionObjectNew); @@ -168,7 +172,9 @@ describe('SubmissionFormComponent Component', () => { submissionId, selfUrl, submissionObjectNew.submissionDefinition, - submissionObjectNew.sections); + submissionObjectNew.sections, + comp.item, + ); }); it('should update only collection id on collection change when submission definition is not changed', () => { @@ -178,6 +184,7 @@ describe('SubmissionFormComponent Component', () => { comp.submissionDefinition = submissionDefinition; comp.selfUrl = selfUrl; comp.sections = sectionsData; + comp.item = new Item(); comp.onCollectionChange({ collection: { diff --git a/src/app/submission/form/submission-form.component.ts b/src/app/submission/form/submission-form.component.ts index 0b8cfce619..9aed2da792 100644 --- a/src/app/submission/form/submission-form.component.ts +++ b/src/app/submission/form/submission-form.component.ts @@ -14,6 +14,7 @@ import { UploaderOptions } from '../../shared/uploader/uploader-options.model'; import { SubmissionObjectEntry } from '../objects/submission-objects.reducer'; import { SectionDataObject } from '../sections/models/section-data.model'; import { SubmissionService } from '../submission.service'; +import { Item } from '../../core/shared/item.model'; /** * This component represents the submission form. @@ -30,6 +31,7 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy { * @type {string} */ @Input() collectionId: string; + @Input() item: Item; /** * The list of submission's sections @@ -150,6 +152,7 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy { this.selfUrl, this.submissionDefinition, this.sections, + this.item, null); this.changeDetectorRef.detectChanges(); }) @@ -191,7 +194,8 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy { this.submissionId, submissionObject._links.self.href, this.submissionDefinition, - this.sections); + this.sections, + this.item); } else { this.changeDetectorRef.detectChanges(); } diff --git a/src/app/submission/objects/submission-objects.actions.ts b/src/app/submission/objects/submission-objects.actions.ts index 57226fc531..1e3e44aba9 100644 --- a/src/app/submission/objects/submission-objects.actions.ts +++ b/src/app/submission/objects/submission-objects.actions.ts @@ -10,6 +10,7 @@ import { import { SubmissionObject } from '../../core/submission/models/submission-object.model'; import { SubmissionDefinitionsModel } from '../../core/config/models/config-submission-definitions.model'; import { SectionsType } from '../sections/sections-type'; +import { Item } from '../../core/shared/item.model'; /** * For each action type in an action group, make a simple @@ -273,6 +274,7 @@ export class InitSubmissionFormAction implements Action { selfUrl: string; submissionDefinition: SubmissionDefinitionsModel; sections: WorkspaceitemSectionsObject; + item: Item; errors: SubmissionSectionError[]; }; @@ -297,8 +299,9 @@ export class InitSubmissionFormAction implements Action { selfUrl: string, submissionDefinition: SubmissionDefinitionsModel, sections: WorkspaceitemSectionsObject, + item: Item, errors: SubmissionSectionError[]) { - this.payload = { collectionId, submissionId, selfUrl, submissionDefinition, sections, errors }; + this.payload = { collectionId, submissionId, selfUrl, submissionDefinition, sections, item, errors }; } } @@ -378,6 +381,7 @@ export class SaveSubmissionFormSuccessAction implements Action { payload: { submissionId: string; submissionObject: SubmissionObject[]; + notify?: boolean }; /** @@ -388,8 +392,8 @@ export class SaveSubmissionFormSuccessAction implements Action { * @param submissionObject * the submission's Object */ - constructor(submissionId: string, submissionObject: SubmissionObject[]) { - this.payload = { submissionId, submissionObject }; + constructor(submissionId: string, submissionObject: SubmissionObject[], notify?: boolean) { + this.payload = { submissionId, submissionObject, notify }; } } @@ -435,6 +439,7 @@ export class SaveSubmissionSectionFormSuccessAction implements Action { payload: { submissionId: string; submissionObject: SubmissionObject[]; + notify?: boolean }; /** @@ -445,8 +450,8 @@ export class SaveSubmissionSectionFormSuccessAction implements Action { * @param submissionObject * the submission's Object */ - constructor(submissionId: string, submissionObject: SubmissionObject[]) { - this.payload = { submissionId, submissionObject }; + constructor(submissionId: string, submissionObject: SubmissionObject[], notify?: boolean) { + this.payload = { submissionId, submissionObject, notify }; } } @@ -475,6 +480,7 @@ export class ResetSubmissionFormAction implements Action { selfUrl: string; sections: WorkspaceitemSectionsObject; submissionDefinition: SubmissionDefinitionsModel; + item: Item; }; /** @@ -491,8 +497,8 @@ export class ResetSubmissionFormAction implements Action { * @param submissionDefinition * the submission's form definition */ - constructor(collectionId: string, submissionId: string, selfUrl: string, sections: WorkspaceitemSectionsObject, submissionDefinition: SubmissionDefinitionsModel) { - this.payload = { collectionId, submissionId, selfUrl, sections, submissionDefinition }; + constructor(collectionId: string, submissionId: string, selfUrl: string, sections: WorkspaceitemSectionsObject, submissionDefinition: SubmissionDefinitionsModel, item: Item) { + this.payload = { collectionId, submissionId, selfUrl, sections, submissionDefinition, item }; } } diff --git a/src/app/submission/objects/submission-objects.effects.spec.ts b/src/app/submission/objects/submission-objects.effects.spec.ts index 6a8475b2d4..6c2e9eefc6 100644 --- a/src/app/submission/objects/submission-objects.effects.spec.ts +++ b/src/app/submission/objects/submission-objects.effects.spec.ts @@ -47,6 +47,10 @@ import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; import { StoreMock } from '../../shared/testing/store.mock'; import { AppState, storeModuleConfig } from '../../app.reducer'; import parseSectionErrors from '../utils/parseSectionErrors'; +import { Item } from '../../core/shared/item.model'; +import { WorkspaceitemDataService } from '../../core/submission/workspaceitem-data.service'; +import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service'; +import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; describe('SubmissionObjectEffects test suite', () => { let submissionObjectEffects: SubmissionObjectEffects; @@ -83,6 +87,10 @@ describe('SubmissionObjectEffects test suite', () => { { provide: SectionsService, useClass: SectionsServiceStub }, { provide: SubmissionService, useValue: submissionServiceStub }, { provide: SubmissionJsonPatchOperationsService, useValue: submissionJsonPatchOperationsServiceStub }, + { provide: WorkspaceitemDataService, useValue: {} }, + { provide: WorkflowItemDataService, useValue: {} }, + { provide: WorkflowItemDataService, useValue: {} }, + { provide: HALEndpointService, useValue: {} }, ], }); @@ -101,6 +109,7 @@ describe('SubmissionObjectEffects test suite', () => { selfUrl: selfUrl, submissionDefinition: submissionDefinition, sections: {}, + item: {metadata: {}}, errors: [], } } @@ -153,6 +162,7 @@ describe('SubmissionObjectEffects test suite', () => { selfUrl: selfUrl, submissionDefinition: submissionDefinition, sections: {}, + item: new Item(), errors: [], } } @@ -165,6 +175,7 @@ describe('SubmissionObjectEffects test suite', () => { selfUrl, submissionDefinition, {}, + new Item(), null ) }); diff --git a/src/app/submission/objects/submission-objects.effects.ts b/src/app/submission/objects/submission-objects.effects.ts index a2a3350c6a..2dfed9ee47 100644 --- a/src/app/submission/objects/submission-objects.effects.ts +++ b/src/app/submission/objects/submission-objects.effects.ts @@ -2,10 +2,10 @@ import { Injectable } from '@angular/core'; import { Actions, Effect, ofType } from '@ngrx/effects'; import { Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; -import { union } from 'lodash'; +import { isEqual, union } from 'lodash'; -import { from as observableFrom, of as observableOf } from 'rxjs'; -import { catchError, map, mergeMap, switchMap, tap, withLatestFrom } from 'rxjs/operators'; +import { from as observableFrom, Observable, of as observableOf } from 'rxjs'; +import { catchError, filter, map, mergeMap, switchMap, take, tap, withLatestFrom } from 'rxjs/operators'; import { SubmissionObject } from '../../core/submission/models/submission-object.model'; import { WorkflowItem } from '../../core/submission/models/workflowitem.model'; import { WorkspaceitemSectionUploadObject } from '../../core/submission/models/workspaceitem-section-upload.model'; @@ -19,7 +19,6 @@ import { SectionsService } from '../sections/sections.service'; import { SubmissionState } from '../submission.reducers'; import { SubmissionService } from '../submission.service'; import parseSectionErrors from '../utils/parseSectionErrors'; - import { CompleteInitSubmissionFormAction, DepositSubmissionAction, @@ -43,7 +42,12 @@ import { SubmissionObjectActionTypes, UpdateSectionDataAction } from './submission-objects.actions'; -import { SubmissionObjectEntry } from './submission-objects.reducer'; +import { SubmissionObjectEntry, SubmissionSectionObject } from './submission-objects.reducer'; +import { Item } from '../../core/shared/item.model'; +import { RemoteData } from '../../core/data/remote-data'; +import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; +import { SubmissionObjectDataService } from '../../core/submission/submission-object-data.service'; +import { followLink } from '../../shared/utils/follow-link-config.model'; @Injectable() export class SubmissionObjectEffects { @@ -61,7 +65,12 @@ export class SubmissionObjectEffects { const sectionId = selfLink.substr(selfLink.lastIndexOf('/') + 1); const config = sectionDefinition._links.config ? (sectionDefinition._links.config.href || sectionDefinition._links.config) : ''; const enabled = (sectionDefinition.mandatory) || (isNotEmpty(action.payload.sections) && action.payload.sections.hasOwnProperty(sectionId)); - const sectionData = (isNotUndefined(action.payload.sections) && isNotUndefined(action.payload.sections[sectionId])) ? action.payload.sections[sectionId] : Object.create(null); + let sectionData; + if (sectionDefinition.sectionType !== SectionsType.SubmissionForm) { + sectionData = (isNotUndefined(action.payload.sections) && isNotUndefined(action.payload.sections[sectionId])) ? action.payload.sections[sectionId] : Object.create(null); + } else { + sectionData = action.payload.item.metadata; + } const sectionErrors = null; mappedActions.push( new InitSectionAction( @@ -78,7 +87,7 @@ export class SubmissionObjectEffects { ) ) }); - return {action: action, definition: definition, mappedActions: mappedActions}; + return { action: action, definition: definition, mappedActions: mappedActions }; }), mergeMap((result) => { return observableFrom( @@ -99,6 +108,7 @@ export class SubmissionObjectEffects { action.payload.selfUrl, action.payload.submissionDefinition, action.payload.sections, + action.payload.item, null ))); @@ -137,7 +147,7 @@ export class SubmissionObjectEffects { ofType(SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_SUCCESS, SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_SUCCESS), withLatestFrom(this.store$), map(([action, currentState]: [SaveSubmissionFormSuccessAction | SaveSubmissionSectionFormSuccessAction, any]) => { - return this.parseSaveResponse((currentState.submission as SubmissionState).objects[action.payload.submissionId], action.payload.submissionObject, action.payload.submissionId); + return this.parseSaveResponse((currentState.submission as SubmissionState).objects[action.payload.submissionId], action.payload.submissionObject, action.payload.submissionId, action.payload.notify); }), mergeMap((actions) => observableFrom(actions))); @@ -159,7 +169,7 @@ export class SubmissionObjectEffects { /** * Show a notification on error */ - @Effect({dispatch: false}) saveError$ = this.actions$.pipe( + @Effect({ dispatch: false }) saveError$ = this.actions$.pipe( ofType(SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_ERROR, SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_ERROR), withLatestFrom(this.store$), tap(() => this.notificationsService.error(null, this.translate.get('submission.sections.general.save_error_notice')))); @@ -201,7 +211,7 @@ export class SubmissionObjectEffects { /** * Show a notification on success and redirect to MyDSpace page */ - @Effect({dispatch: false}) saveForLaterSubmissionSuccess$ = this.actions$.pipe( + @Effect({ dispatch: false }) saveForLaterSubmissionSuccess$ = this.actions$.pipe( ofType(SubmissionObjectActionTypes.SAVE_FOR_LATER_SUBMISSION_FORM_SUCCESS), tap(() => this.notificationsService.success(null, this.translate.get('submission.sections.general.save_success_notice'))), tap(() => this.submissionService.redirectToMyDSpace())); @@ -209,7 +219,7 @@ export class SubmissionObjectEffects { /** * Show a notification on success and redirect to MyDSpace page */ - @Effect({dispatch: false}) depositSubmissionSuccess$ = this.actions$.pipe( + @Effect({ dispatch: false }) depositSubmissionSuccess$ = this.actions$.pipe( ofType(SubmissionObjectActionTypes.DEPOSIT_SUBMISSION_SUCCESS), tap(() => this.notificationsService.success(null, this.translate.get('submission.sections.general.deposit_success_notice'))), tap(() => this.submissionService.redirectToMyDSpace())); @@ -217,7 +227,7 @@ export class SubmissionObjectEffects { /** * Show a notification on error */ - @Effect({dispatch: false}) depositSubmissionError$ = this.actions$.pipe( + @Effect({ dispatch: false }) depositSubmissionError$ = this.actions$.pipe( ofType(SubmissionObjectActionTypes.DEPOSIT_SUBMISSION_ERROR), tap(() => this.notificationsService.error(null, this.translate.get('submission.sections.general.deposit_error_notice')))); @@ -232,10 +242,40 @@ export class SubmissionObjectEffects { catchError(() => observableOf(new DiscardSubmissionErrorAction(action.payload.submissionId)))); })); + /** + * Adds all metadata an item to the SubmissionForm sections of the submission + */ + @Effect() addAllMetadataToSectionData = this.actions$.pipe( + ofType(SubmissionObjectActionTypes.UPLOAD_SECTION_DATA), + switchMap((action: UpdateSectionDataAction) => { + return this.sectionService.getSectionState(action.payload.submissionId, action.payload.sectionId) + .pipe(map((section: SubmissionSectionObject) => [action, section]), take(1)); + }), + filter(([action, section]: [UpdateSectionDataAction, SubmissionSectionObject]) => section.sectionType === SectionsType.SubmissionForm), + switchMap(([action, section]: [UpdateSectionDataAction, SubmissionSectionObject]) => { + const submissionObject$ = this.submissionObjectService + .findById(action.payload.submissionId, followLink('item')).pipe( + getFirstSucceededRemoteDataPayload() + ); + + const item$ = submissionObject$.pipe( + switchMap((submissionObject: SubmissionObject) => (submissionObject.item as Observable>).pipe( + getFirstSucceededRemoteDataPayload(), + ))); + + return item$.pipe( + map((item: Item) => item.metadata), + filter((metadata) => !isEqual(action.payload.data, metadata)), + map((metadata: any) => new UpdateSectionDataAction(action.payload.submissionId, action.payload.sectionId, metadata, action.payload.errors)) + ); + }), + ); + /** * Show a notification on success and redirect to MyDSpace page */ - @Effect({dispatch: false}) discardSubmissionSuccess$ = this.actions$.pipe( + @Effect({ dispatch: false }) + discardSubmissionSuccess$ = this.actions$.pipe( ofType(SubmissionObjectActionTypes.DISCARD_SUBMISSION_SUCCESS), tap(() => this.notificationsService.success(null, this.translate.get('submission.sections.general.discard_success_notice'))), tap(() => this.submissionService.redirectToMyDSpace())); @@ -243,7 +283,7 @@ export class SubmissionObjectEffects { /** * Show a notification on error */ - @Effect({dispatch: false}) discardSubmissionError$ = this.actions$.pipe( + @Effect({ dispatch: false }) discardSubmissionError$ = this.actions$.pipe( ofType(SubmissionObjectActionTypes.DISCARD_SUBMISSION_ERROR), tap(() => this.notificationsService.error(null, this.translate.get('submission.sections.general.discard_error_notice')))); @@ -253,6 +293,7 @@ export class SubmissionObjectEffects { private sectionService: SectionsService, private store$: Store, private submissionService: SubmissionService, + private submissionObjectService: SubmissionObjectDataService, private translate: TranslateService) { } @@ -267,7 +308,7 @@ export class SubmissionObjectEffects { if (isNotEmpty(response)) { response.forEach((item: WorkspaceItem | WorkflowItem) => { - const {errors} = item; + const { errors } = item; if (errors && !isEmpty(errors)) { canDeposit = false; @@ -307,7 +348,7 @@ export class SubmissionObjectEffects { response.forEach((item: WorkspaceItem | WorkflowItem) => { let errorsList = Object.create({}); - const {errors} = item; + const { errors } = item; if (errors && !isEmpty(errors)) { // to avoid dispatching an action for every error, create an array of errors per section @@ -336,11 +377,8 @@ export class SubmissionObjectEffects { } mappedActions.push(new UpdateSectionDataAction(submissionId, sectionId, sectionData, sectionErrors)); } - }); - } - return mappedActions; } diff --git a/src/app/submission/objects/submission-objects.reducer.spec.ts b/src/app/submission/objects/submission-objects.reducer.spec.ts index e7f3a24c89..0c585e4bca 100644 --- a/src/app/submission/objects/submission-objects.reducer.spec.ts +++ b/src/app/submission/objects/submission-objects.reducer.spec.ts @@ -39,6 +39,7 @@ import { mockSubmissionSelfUrl, mockSubmissionState } from '../../shared/mocks/submission.mock'; +import { Item } from '../../core/shared/item.model'; describe('submissionReducer test suite', () => { @@ -67,7 +68,7 @@ describe('submissionReducer test suite', () => { } }; - const action = new InitSubmissionFormAction(collectionId, submissionId, selfUrl, submissionDefinition, {}, []); + const action = new InitSubmissionFormAction(collectionId, submissionId, selfUrl, submissionDefinition, {}, new Item(), []); const newState = submissionObjectReducer({}, action); expect(newState).toEqual(expectedState); @@ -100,7 +101,7 @@ describe('submissionReducer test suite', () => { } }; - const action = new ResetSubmissionFormAction(collectionId, submissionId, selfUrl, {}, submissionDefinition); + const action = new ResetSubmissionFormAction(collectionId, submissionId, selfUrl, {}, submissionDefinition, new Item()); const newState = submissionObjectReducer(initState, action); expect(newState).toEqual(expectedState); @@ -241,7 +242,7 @@ describe('submissionReducer test suite', () => { isValid: false } as any; - let action: any = new InitSubmissionFormAction(collectionId, submissionId, selfUrl, submissionDefinition, {}, []); + let action: any = new InitSubmissionFormAction(collectionId, submissionId, selfUrl, submissionDefinition, {}, new Item(), []); let newState = submissionObjectReducer({}, action); action = new InitSectionAction( diff --git a/src/app/submission/sections/form/section-form-operations.service.spec.ts b/src/app/submission/sections/form/section-form-operations.service.spec.ts index de8e7da7f9..50803c9886 100644 --- a/src/app/submission/sections/form/section-form-operations.service.spec.ts +++ b/src/app/submission/sections/form/section-form-operations.service.spec.ts @@ -665,7 +665,7 @@ describe('SectionFormOperationsService test suite', () => { spyOn(service, 'getFieldPathFromEvent').and.returnValue('path/1'); spyOn(service, 'getFieldPathSegmentedFromChangeEvent').and.returnValue('path'); spyOn(service, 'getFieldValueFromChangeEvent').and.returnValue(new FormFieldMetadataValueObject('test')); - spyOn(service, 'getArrayIndexFromEvent').and.returnValue(1); + spyOn(service, 'getArrayIndexFromEvent').and.returnValue(0); spyOn(serviceAsAny, 'getValueMap'); spyOn(serviceAsAny, 'dispatchOperationsFromMap'); formBuilderService.isQualdropGroup.and.returnValue(false); @@ -676,8 +676,10 @@ describe('SectionFormOperationsService test suite', () => { serviceAsAny.dispatchOperationsFromChangeEvent(pathCombiner, event, previousValue, false); expect(jsonPatchOpBuilder.add).toHaveBeenCalledWith( - pathCombiner.getPath('path/1'), - new FormFieldMetadataValueObject('test')); + pathCombiner.getPath('path'), + new FormFieldMetadataValueObject('test'), + true + ); }); }); diff --git a/src/app/submission/sections/form/section-form-operations.service.ts b/src/app/submission/sections/form/section-form-operations.service.ts index 2d6b1c5477..6e7a35fe26 100644 --- a/src/app/submission/sections/form/section-form-operations.service.ts +++ b/src/app/submission/sections/form/section-form-operations.service.ts @@ -9,7 +9,15 @@ import { DynamicFormControlModel } from '@ng-dynamic-forms/core'; -import { isNotEmpty, isNotNull, isNotUndefined, isNull, isUndefined } from '../../../shared/empty.util'; +import { + hasNoValue, + hasValue, + isNotEmpty, + isNotNull, + isNotUndefined, + isNull, + isUndefined +} from '../../../shared/empty.util'; import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; import { FormFieldPreviousValueObject } from '../../../shared/form/builder/models/form-field-previous-value-object'; import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; @@ -61,6 +69,9 @@ export class SectionFormOperationsService { case 'change': this.dispatchOperationsFromChangeEvent(pathCombiner, event, previousValue, hasStoredValue); break; + case 'add': + this.dispatchOperationsFromAddEvent(pathCombiner, event); + break; default: break; } @@ -173,7 +184,7 @@ export class SectionFormOperationsService { metadataValueMap.set(groupModel.qualdropId, metadataValueList); } if (index === fieldIndex) { - path = groupModel.qualdropId + '/' + (metadataValueMap.get(groupModel.qualdropId).length - 1) + path = groupModel.qualdropId + '/' + (metadataValueList.length - 1) } }); @@ -286,6 +297,42 @@ export class SectionFormOperationsService { } } + /** + * Handle form add operations + * + * @param pathCombiner + * the [[JsonPatchOperationPathCombiner]] object for the specified operation + * @param event + * the [[DynamicFormControlEvent]] for the specified operation + */ + protected dispatchOperationsFromAddEvent( + pathCombiner: JsonPatchOperationPathCombiner, + event: DynamicFormControlEvent + ): void { + const path = this.getFieldPathSegmentedFromChangeEvent(event); + const value = this.getFieldValueFromChangeEvent(event); + if (isNotEmpty(value)) { + value.place = this.getArrayIndexFromEvent(event); + if (hasValue(event.group) && hasValue(event.group.value)) { + const valuesInGroup = event.group.value + .map((g) => Object.values(g)) + .reduce((accumulator, currentValue) => accumulator.concat(currentValue)) + .filter((v) => isNotEmpty(v)); + if (valuesInGroup.length === 1) { + // The first add for a field needs to be a different PATCH operation + // for some reason + this.operationsBuilder.add( + pathCombiner.getPath([path]), + [value], false); + } else { + this.operationsBuilder.add( + pathCombiner.getPath([path, '-']), + value, false); + } + } + } + } + /** * Handle form change operations * @@ -312,14 +359,29 @@ export class SectionFormOperationsService { } else if (this.formBuilder.isRelationGroup(event.model)) { // It's a relation model this.dispatchOperationsFromMap(this.getValueMap(value), pathCombiner, event, previousValue); - } else if (this.formBuilder.hasArrayGroupValue(event.model)) { + } else if (this.formBuilder.hasArrayGroupValue(event.model) && hasNoValue((event.model as any).relationshipConfig)) { // Model has as value an array, so dispatch an add operation with entire block of values this.operationsBuilder.add( pathCombiner.getPath(segmentedPath), value, true); } else if (previousValue.isPathEqual(this.formBuilder.getPath(event.model)) || hasStoredValue) { // Here model has a previous value changed or stored in the server - if (!value.hasValue()) { + if (hasValue(event.$event) && hasValue(event.$event.previousIndex)) { + if (event.$event.previousIndex < 0) { + this.operationsBuilder.add( + pathCombiner.getPath(segmentedPath), + value, true); + } else { + const moveTo = pathCombiner.getPath(path); + const moveFrom = pathCombiner.getPath(segmentedPath + '/' + event.$event.previousIndex); + if (isNotEmpty(moveFrom.path) && isNotEmpty(moveTo.path) && moveFrom.path !== moveTo.path) { + this.operationsBuilder.move( + moveTo, + moveFrom.path + ) + } + } + } else if (!value.hasValue()) { // New value is empty, so dispatch a remove operation if (this.getArrayIndexFromEvent(event) === 0) { this.operationsBuilder.remove(pathCombiner.getPath(segmentedPath)); @@ -333,22 +395,13 @@ export class SectionFormOperationsService { value); } previousValue.delete(); - } else if (value.hasValue()) { - // Here model has no previous value but a new one - if (isUndefined(this.getArrayIndexFromEvent(event)) - || this.getArrayIndexFromEvent(event) === 0) { + } else if (value.hasValue() && (isUndefined(this.getArrayIndexFromEvent(event)) + || this.getArrayIndexFromEvent(event) === 0)) { // Model is single field or is part of an array model but is the first item, // so dispatch an add operation that initialize the values of a specific metadata this.operationsBuilder.add( pathCombiner.getPath(segmentedPath), value, true); - } else { - // Model is part of an array model but is not the first item, - // so dispatch an add operation that add a value to an existent metadata - this.operationsBuilder.add( - pathCombiner.getPath(path), - value); - } } } diff --git a/src/app/submission/sections/form/section-form.component.spec.ts b/src/app/submission/sections/form/section-form.component.spec.ts index c8c0d671ec..b2bbd4e63f 100644 --- a/src/app/submission/sections/form/section-form.component.spec.ts +++ b/src/app/submission/sections/form/section-form.component.spec.ts @@ -41,6 +41,12 @@ import { SubmissionSectionError } from '../../objects/submission-objects.reducer import { DynamicFormControlEvent, DynamicFormControlEventType } from '@ng-dynamic-forms/core'; import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; import { FormRowModel } from '../../../core/config/models/config-submission-form.model'; +import { WorkspaceitemDataService } from '../../../core/submission/workspaceitem-data.service'; +import { RemoteData } from '../../../core/data/remote-data'; +import { WorkspaceItem } from '../../../core/submission/models/workspaceitem.model'; +import { SubmissionObjectDataService } from '../../../core/submission/submission-object-data.service'; +import { ObjectCacheService } from '../../../core/cache/object-cache.service'; +import { RequestService } from '../../../core/data/request.service'; function getMockSubmissionFormsConfigService(): SubmissionFormsConfigService { return jasmine.createSpyObj('FormOperationsService', { @@ -114,11 +120,11 @@ const testFormConfiguration = { const testFormModel = [ new DynamicRowGroupModel({ id: 'df-row-group-config-1', - group: [new DsDynamicInputModel({ id: 'dc.title', metadataFields: [], repeatable: false, submissionId: '1234' })], + group: [new DsDynamicInputModel({ id: 'dc.title', metadataFields: [], repeatable: false, submissionId: '1234', hasSelectableMetadata: false })], }), new DynamicRowGroupModel({ id: 'df-row-group-config-2', - group: [new DsDynamicInputModel({ id: 'dc.contributor', metadataFields: [], repeatable: false, submissionId: '1234' })], + group: [new DsDynamicInputModel({ id: 'dc.contributor', metadataFields: [], repeatable: false, submissionId: '1234', hasSelectableMetadata: false })], }) ]; @@ -173,9 +179,12 @@ describe('SubmissionSectionformComponent test suite', () => { { provide: SectionsService, useClass: SectionsServiceStub }, { provide: SubmissionService, useClass: SubmissionServiceStub }, { provide: TranslateService, useValue: getMockTranslateService() }, + { provide: ObjectCacheService, useValue: { remove: () => {/*do nothing*/}, hasBySelfLinkObservable: () => observableOf(false) } }, + { provide: RequestService, useValue: { removeByHrefSubstring: () => {/*do nothing*/}, hasByHrefObservable: () => observableOf(false) } }, { provide: 'collectionIdProvider', useValue: collectionId }, { provide: 'sectionDataProvider', useValue: sectionObject }, { provide: 'submissionIdProvider', useValue: submissionId }, + { provide: SubmissionObjectDataService, useValue: { getHrefByID: () => observableOf('testUrl'), findById: () => observableOf(new RemoteData(false, false, true, null, new WorkspaceItem())) } }, ChangeDetectorRef, SubmissionSectionformComponent ], @@ -248,7 +257,6 @@ describe('SubmissionSectionformComponent test suite', () => { expect(comp.isLoading).toBeFalsy(); expect(comp.initForm).toHaveBeenCalledWith(sectionData); expect(comp.subscriptions).toHaveBeenCalled(); - }); it('should init form model properly', () => { @@ -311,7 +319,6 @@ describe('SubmissionSectionformComponent test suite', () => { expect(comp.isUpdating).toBeFalsy(); expect(comp.initForm).toHaveBeenCalled(); expect(comp.checksForErrors).toHaveBeenCalled(); - expect(notificationsServiceStub.info).toHaveBeenCalled(); expect(comp.sectionData.data).toEqual(sectionData); }); @@ -328,7 +335,7 @@ describe('SubmissionSectionformComponent test suite', () => { comp.updateForm(sectionData, parsedSectionErrors); - expect(comp.initForm).not.toHaveBeenCalled(); + expect(comp.initForm).toHaveBeenCalled(); expect(comp.checksForErrors).toHaveBeenCalled(); expect(comp.sectionData.data).toEqual(sectionData); }); diff --git a/src/app/submission/sections/form/section-form.component.ts b/src/app/submission/sections/form/section-form.component.ts index 58cda897b0..f4a64c5976 100644 --- a/src/app/submission/sections/form/section-form.component.ts +++ b/src/app/submission/sections/form/section-form.component.ts @@ -1,8 +1,8 @@ import { ChangeDetectorRef, Component, Inject, ViewChild } from '@angular/core'; import { DynamicFormControlEvent, DynamicFormControlModel } from '@ng-dynamic-forms/core'; -import { Observable, Subscription } from 'rxjs'; -import { distinctUntilChanged, filter, find, flatMap, map, take, tap } from 'rxjs/operators'; +import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs'; +import { distinctUntilChanged, filter, find, flatMap, map, switchMap, take, tap } from 'rxjs/operators'; import { TranslateService } from '@ngx-translate/core'; import { isEqual } from 'lodash'; @@ -11,7 +11,7 @@ import { FormComponent } from '../../../shared/form/form.component'; import { FormService } from '../../../shared/form/form.service'; import { SectionModelComponent } from '../models/section.model'; import { SubmissionFormsConfigService } from '../../../core/config/submission-forms-config.service'; -import { hasValue, isNotEmpty, isUndefined } from '../../../shared/empty.util'; +import { hasNoValue, hasValue, isNotEmpty, isUndefined } from '../../../shared/empty.util'; import { ConfigData } from '../../../core/config/config-data'; import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; import { SubmissionFormsModel } from '../../../core/config/models/config-submission-forms.model'; @@ -27,10 +27,13 @@ import { NotificationsService } from '../../../shared/notifications/notification import { SectionsService } from '../sections.service'; import { difference } from '../../../shared/object.util'; import { WorkspaceitemSectionFormObject } from '../../../core/submission/models/workspaceitem-section-form.model'; -import { WorkspaceitemDataService } from '../../../core/submission/workspaceitem-data.service'; -import { combineLatest as combineLatestObservable } from 'rxjs'; -import { getSucceededRemoteData } from '../../../core/shared/operators'; -import { RemoteData } from '../../../core/data/remote-data'; +import { WorkspaceItem } from '../../../core/submission/models/workspaceitem.model'; +import { getRemoteDataPayload, getSucceededRemoteData } from '../../../core/shared/operators'; +import { SubmissionObject } from '../../../core/submission/models/submission-object.model'; +import { SubmissionObjectDataService } from '../../../core/submission/submission-object-data.service'; +import { ObjectCacheService } from '../../../core/cache/object-cache.service'; +import { RequestService } from '../../../core/data/request.service'; +import { followLink } from '../../../shared/utils/follow-link-config.model'; import { environment } from '../../../../environments/environment'; /** @@ -104,6 +107,7 @@ export class SubmissionSectionformComponent extends SectionModelComponent { */ protected subs: Subscription[] = []; + protected workspaceItem: WorkspaceItem; /** * The FormComponent reference */ @@ -121,7 +125,9 @@ export class SubmissionSectionformComponent extends SectionModelComponent { * @param {SectionsService} sectionService * @param {SubmissionService} submissionService * @param {TranslateService} translate - * @param {GlobalConfig} EnvConfig + * @param {SubmissionObjectDataService} submissionObjectService + * @param {ObjectCacheService} objectCache + * @param {RequestService} requestService * @param {string} injectedCollectionId * @param {SectionDataObject} injectedSectionData * @param {string} injectedSubmissionId @@ -135,6 +141,9 @@ export class SubmissionSectionformComponent extends SectionModelComponent { protected sectionService: SectionsService, protected submissionService: SubmissionService, protected translate: TranslateService, + protected submissionObjectService: SubmissionObjectDataService, + protected objectCache: ObjectCacheService, + protected requestService: RequestService, @Inject('collectionIdProvider') public injectedCollectionId: string, @Inject('sectionDataProvider') public injectedSectionData: SectionDataObject, @Inject('submissionIdProvider') public injectedSubmissionId: string) { @@ -150,11 +159,29 @@ export class SubmissionSectionformComponent extends SectionModelComponent { this.formConfigService.getConfigByHref(this.sectionData.config).pipe( map((configData: ConfigData) => configData.payload), tap((config: SubmissionFormsModel) => this.formConfig = config), - flatMap(() => this.sectionService.getSectionData(this.submissionId, this.sectionData.id)), + flatMap(() => + observableCombineLatest( + this.sectionService.getSectionData(this.submissionId, this.sectionData.id), + this.submissionObjectService.getHrefByID(this.submissionId).pipe(take(1)).pipe( + switchMap((href: string) => { + this.objectCache.remove(href); + this.requestService.removeByHrefSubstring(this.submissionId); + return observableCombineLatest( + this.objectCache.hasBySelfLinkObservable(href), + this.requestService.hasByHrefObservable(href) + ).pipe( + filter(([existsInOC, existsInRC]) => !existsInOC && !existsInRC), + take(1), + switchMap(() => this.submissionObjectService.findById(this.submissionId, followLink('item')).pipe(getSucceededRemoteData(), getRemoteDataPayload()) as Observable) + ) + }) + ) + )), take(1)) - .subscribe((sectionData: WorkspaceitemSectionFormObject) => { + .subscribe(([sectionData, workspaceItem]: [WorkspaceitemSectionFormObject, WorkspaceItem]) => { if (isUndefined(this.formModel)) { this.sectionData.errors = []; + this.workspaceItem = workspaceItem; // Is the first loading so init form this.initForm(sectionData); this.sectionData.data = sectionData; @@ -244,23 +271,15 @@ export class SubmissionSectionformComponent extends SectionModelComponent { */ updateForm(sectionData: WorkspaceitemSectionFormObject, errors: SubmissionSectionError[]): void { - if (isNotEmpty(sectionData) && !isEqual(sectionData, this.sectionData.data)) { + if (hasValue(sectionData) && !isEqual(sectionData, this.sectionData.data)) { this.sectionData.data = sectionData; - if (this.hasMetadataEnrichment(sectionData)) { - const msg = this.translate.instant( - 'submission.sections.general.metadata-extracted', - { sectionId: this.sectionData.id }); - this.notificationsService.info(null, msg, null, true); - this.isUpdating = true; - this.formModel = null; - this.cdr.detectChanges(); - this.initForm(sectionData); - this.checksForErrors(errors); - this.isUpdating = false; - this.cdr.detectChanges(); - } else if (isNotEmpty(errors) || isNotEmpty(this.sectionData.errors)) { - this.checksForErrors(errors); - } + this.isUpdating = true; + this.formModel = null; + this.cdr.detectChanges(); + this.initForm(sectionData); + this.checksForErrors(errors); + this.isUpdating = false; + this.cdr.detectChanges(); } else if (isNotEmpty(errors) || isNotEmpty(this.sectionData.errors)) { this.checksForErrors(errors); } @@ -320,16 +339,19 @@ export class SubmissionSectionformComponent extends SectionModelComponent { * the [[DynamicFormControlEvent]] emitted */ onChange(event: DynamicFormControlEvent): void { - this.formOperationsService.dispatchOperationsFromEvent( - this.pathCombiner, - event, - this.previousValue, - this.hasStoredValue(this.formBuilderService.getId(event.model), this.formOperationsService.getArrayIndexFromEvent(event))); - const metadata = this.formOperationsService.getFieldPathSegmentedFromChangeEvent(event); - const value = this.formOperationsService.getFieldValueFromChangeEvent(event); + // don't handle change events for things with an index < 0, those are template rows. + if (hasNoValue(event.context) || hasNoValue(event.context.index) || event.context.index >= 0) { + this.formOperationsService.dispatchOperationsFromEvent( + this.pathCombiner, + event, + this.previousValue, + this.hasStoredValue(this.formBuilderService.getId(event.model), this.formOperationsService.getArrayIndexFromEvent(event))); + const metadata = this.formOperationsService.getFieldPathSegmentedFromChangeEvent(event); + const value = this.formOperationsService.getFieldValueFromChangeEvent(event); - if (environment.submission.autosave.metadata.indexOf(metadata) !== -1 && isNotEmpty(value)) { - this.submissionService.dispatchSave(this.submissionId); + if (environment.submission.autosave.metadata.indexOf(metadata) !== -1 && isNotEmpty(value)) { + this.submissionService.dispatchSave(this.submissionId); + } } } diff --git a/src/app/submission/sections/sections.service.spec.ts b/src/app/submission/sections/sections.service.spec.ts index 1455a66f78..e5cb3ddc09 100644 --- a/src/app/submission/sections/sections.service.spec.ts +++ b/src/app/submission/sections/sections.service.spec.ts @@ -368,7 +368,6 @@ describe('SectionsService test suite', () => { scheduler.schedule(() => service.updateSectionData(submissionId, sectionId, data, [])); scheduler.flush(); - expect(notificationsServiceStub.info).toHaveBeenCalled(); expect(store.dispatch).toHaveBeenCalledWith(new UpdateSectionDataAction(submissionId, sectionId, data, [])); }); }); diff --git a/src/app/submission/sections/sections.service.ts b/src/app/submission/sections/sections.service.ts index f6ad5ef0cf..52ae941893 100644 --- a/src/app/submission/sections/sections.service.ts +++ b/src/app/submission/sections/sections.service.ts @@ -17,17 +17,8 @@ import { SectionStatusChangeAction, UpdateSectionDataAction } from '../objects/submission-objects.actions'; -import { - SubmissionObjectEntry, - SubmissionSectionError, - SubmissionSectionObject -} from '../objects/submission-objects.reducer'; -import { - submissionObjectFromIdSelector, - submissionSectionDataFromIdSelector, - submissionSectionErrorsFromIdSelector, - submissionSectionFromIdSelector -} from '../selectors'; +import { SubmissionObjectEntry, SubmissionSectionError, SubmissionSectionObject } from '../objects/submission-objects.reducer'; +import { submissionObjectFromIdSelector, submissionSectionDataFromIdSelector, submissionSectionErrorsFromIdSelector, submissionSectionFromIdSelector } from '../selectors'; import { SubmissionScopeType } from '../../core/submission/submission-scope-type'; import parseSectionErrorPaths, { SectionErrorPath } from '../utils/parseSectionErrorPaths'; import { FormAddError, FormClearErrorsAction, FormRemoveErrorAction } from '../../shared/form/form.actions'; @@ -175,7 +166,8 @@ export class SectionsService { return this.store.select(submissionSectionFromIdSelector(submissionId, sectionId)).pipe( filter((sectionObj: SubmissionSectionObject) => hasValue(sectionObj)), map((sectionObj: SubmissionSectionObject) => sectionObj), - distinctUntilChanged()); + distinctUntilChanged(), + ); } /** @@ -321,10 +313,6 @@ export class SectionsService { take(1), filter(([available, enabled]: [boolean, boolean]) => available)) .subscribe(([available, enabled]: [boolean, boolean]) => { - if (!enabled) { - const msg = this.translate.instant('submission.sections.general.metadata-extracted-new-section', {sectionId}); - this.notificationsService.info(null, msg, null, true); - } this.store.dispatch(new UpdateSectionDataAction(submissionId, sectionId, data, errors)); }); } diff --git a/src/app/submission/submission.service.spec.ts b/src/app/submission/submission.service.spec.ts index eb7538ec69..4e4d418b0d 100644 --- a/src/app/submission/submission.service.spec.ts +++ b/src/app/submission/submission.service.spec.ts @@ -45,6 +45,7 @@ import { getMockSearchService } from '../shared/mocks/search-service.mock'; import { getMockRequestService } from '../shared/mocks/request.service.mock'; import { RequestService } from '../core/data/request.service'; import { SearchService } from '../core/shared/search/search.service'; +import { Item } from '../core/shared/item.model'; import { storeModuleConfig } from '../app.reducer'; import { environment } from '../../environments/environment'; @@ -439,6 +440,7 @@ describe('SubmissionService test suite', () => { selfUrl, submissionDefinition, {}, + new Item(), [] ); const expected = new InitSubmissionFormAction( @@ -447,6 +449,7 @@ describe('SubmissionService test suite', () => { selfUrl, submissionDefinition, {}, + new Item(), []); expect((service as any).store.dispatch).toHaveBeenCalledWith(expected); @@ -842,14 +845,17 @@ describe('SubmissionService test suite', () => { submissionId, selfUrl, submissionDefinition, - {} - ); + {}, + new Item() + ) + ; const expected = new ResetSubmissionFormAction( collectionId, submissionId, selfUrl, {}, - submissionDefinition + submissionDefinition, + new Item() ); expect((service as any).store.dispatch).toHaveBeenCalledWith(expected); diff --git a/src/app/submission/submission.service.ts b/src/app/submission/submission.service.ts index f06f6ea069..262612d50c 100644 --- a/src/app/submission/submission.service.ts +++ b/src/app/submission/submission.service.ts @@ -45,6 +45,7 @@ import { RemoteDataError } from '../core/data/remote-data-error'; import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject } from '../shared/remote-data.utils'; import { RequestService } from '../core/data/request.service'; import { SearchService } from '../core/shared/search/search.service'; +import { Item } from '../core/shared/item.model'; import { environment } from '../../environments/environment'; /** @@ -163,8 +164,9 @@ export class SubmissionService { selfUrl: string, submissionDefinition: SubmissionDefinitionsModel, sections: WorkspaceitemSectionsObject, + item: Item, errors: SubmissionSectionError[]) { - this.store.dispatch(new InitSubmissionFormAction(collectionId, submissionId, selfUrl, submissionDefinition, sections, errors)); + this.store.dispatch(new InitSubmissionFormAction(collectionId, submissionId, selfUrl, submissionDefinition, sections, item, errors)); } /** @@ -502,9 +504,10 @@ export class SubmissionService { submissionId: string, selfUrl: string, submissionDefinition: SubmissionDefinitionsModel, - sections: WorkspaceitemSectionsObject + sections: WorkspaceitemSectionsObject, + item: Item ) { - this.store.dispatch(new ResetSubmissionFormAction(collectionId, submissionId, selfUrl, sections, submissionDefinition)); + this.store.dispatch(new ResetSubmissionFormAction(collectionId, submissionId, selfUrl, sections, submissionDefinition, item)); } /** diff --git a/src/app/submission/submit/submission-submit.component.html b/src/app/submission/submit/submission-submit.component.html index c9e8c6b51a..2ceaf5a6de 100644 --- a/src/app/submission/submit/submission-submit.component.html +++ b/src/app/submission/submit/submission-submit.component.html @@ -3,6 +3,7 @@ diff --git a/src/app/submission/submit/submission-submit.component.ts b/src/app/submission/submit/submission-submit.component.ts index d3d3ca4e66..deced3ef26 100644 --- a/src/app/submission/submit/submission-submit.component.ts +++ b/src/app/submission/submit/submission-submit.component.ts @@ -10,6 +10,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s import { SubmissionService } from '../submission.service'; import { SubmissionObject } from '../../core/submission/models/submission-object.model'; import { Collection } from '../../core/shared/collection.model'; +import { Item } from '../../core/shared/item.model'; /** * This component allows to submit a new workspaceitem. @@ -26,6 +27,7 @@ export class SubmissionSubmitComponent implements OnDestroy, OnInit { * @type {string} */ public collectionId: string; + public item: Item; /** * The collection id input to create a new submission @@ -98,6 +100,7 @@ export class SubmissionSubmitComponent implements OnDestroy, OnInit { this.selfUrl = submissionObject._links.self.href; this.submissionDefinition = (submissionObject.submissionDefinition as SubmissionDefinitionsModel); this.submissionId = submissionObject.id; + this.item = submissionObject.item as Item; this.changeDetectorRef.detectChanges(); } } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index b35ced7ce9..e602e4cf5c 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -2740,13 +2740,13 @@ "submission.sections.describe.relationship-lookup.selected": "Selected {{ size }} items", - "submission.sections.describe.relationship-lookup.search-tab.tab-title.Author": "Local Authors ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.isAuthorOfPublication": "Local Authors ({{ count }})", - "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal": "Local Journals ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.isJournalOfPublication": "Local Journals ({{ count }})", - "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal Issue": "Local Journal Issues ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.isJournalIssueOfPublication": "Local Journal Issues ({{ count }})", - "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal Volume": "Local Journal Volumes ({{ count }})", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.isJournalVolumeOfPublication": "Local Journal Volumes ({{ count }})", "submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaJournal": "Sherpa Journals ({{ count }})", @@ -2756,23 +2756,27 @@ "submission.sections.describe.relationship-lookup.search-tab.tab-title.lcname": "LC Names ({{ count }})", - "submission.sections.describe.relationship-lookup.search-tab.tab-title.Funding Agency": "Search for Funding Agencies", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.isFundingAgencyOfPublication": "Search for Funding Agencies", - "submission.sections.describe.relationship-lookup.search-tab.tab-title.Funding": "Search for Funding", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.isFundingOfPublication": "Search for Funding", + + "submission.sections.describe.relationship-lookup.search-tab.tab-title.isChildOrgUnitOf": "Search for Organizational Units", "submission.sections.describe.relationship-lookup.selection-tab.tab-title": "Current Selection ({{ count }})", - "submission.sections.describe.relationship-lookup.title.Journal Issue": "Journal Issues", + "submission.sections.describe.relationship-lookup.title.isJournalIssueOfPublication": "Journal Issues", - "submission.sections.describe.relationship-lookup.title.Journal Volume": "Journal Volumes", + "submission.sections.describe.relationship-lookup.title.isJournalVolumeOfPublication": "Journal Volumes", - "submission.sections.describe.relationship-lookup.title.Journal": "Journals", + "submission.sections.describe.relationship-lookup.title.isJournalOfPublication": "Journals", - "submission.sections.describe.relationship-lookup.title.Author": "Authors", + "submission.sections.describe.relationship-lookup.title.isAuthorOfPublication": "Authors", - "submission.sections.describe.relationship-lookup.title.Funding Agency": "Funding Agency", + "submission.sections.describe.relationship-lookup.title.isFundingAgencyOfPublication": "Funding Agency", - "submission.sections.describe.relationship-lookup.title.Funding": "Funding", + "submission.sections.describe.relationship-lookup.title.isFundingOfPublication": "Funding", + + "submission.sections.describe.relationship-lookup.title.isChildOrgUnitOf": "Parent Organizational Unit", "submission.sections.describe.relationship-lookup.search-tab.toggle-dropdown": "Toggle dropdown", @@ -2780,13 +2784,19 @@ "submission.sections.describe.relationship-lookup.selection-tab.no-selection": "Your selection is currently empty.", - "submission.sections.describe.relationship-lookup.selection-tab.title.Author": "Selected Authors", + "submission.sections.describe.relationship-lookup.selection-tab.title.isAuthorOfPublication": "Selected Authors", - "submission.sections.describe.relationship-lookup.selection-tab.title.Journal": "Selected Journals", + "submission.sections.describe.relationship-lookup.selection-tab.title.isJournalOfPublication": "Selected Journals", - "submission.sections.describe.relationship-lookup.selection-tab.title.Journal Volume": "Selected Journal Volume", + "submission.sections.describe.relationship-lookup.selection-tab.title.isJournalVolumeOfPublication": "Selected Journal Volume", - "submission.sections.describe.relationship-lookup.selection-tab.title.Journal Issue": "Selected Issue", + "submission.sections.describe.relationship-lookup.selection-tab.title.isJournalIssueOfPublication": "Selected Issue", + + "submission.sections.describe.relationship-lookup.selection-tab.title.isFundingAgencyOfPublication": "Selected Funding Agency", + + "submission.sections.describe.relationship-lookup.selection-tab.title.isFundingOfPublication": "Selected Funding", + + "submission.sections.describe.relationship-lookup.selection-tab.title.isChildOrgUnitOf": "Selected Organizational Unit", "submission.sections.describe.relationship-lookup.selection-tab.title.sherpaJournal": "Search Results",