diff --git a/src/app/core/submission/submission-object-data.service.ts b/src/app/core/submission/submission-object-data.service.ts index 18816fbfd20..2dccee7d521 100644 --- a/src/app/core/submission/submission-object-data.service.ts +++ b/src/app/core/submission/submission-object-data.service.ts @@ -12,6 +12,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; import { environment } from '../../../environments/environment'; import { RequestEntryState } from '../data/request-entry-state.model'; import { IdentifiableDataService } from '../data/base/identifiable-data.service'; +import { Operation } from 'fast-json-patch'; /** * A service to retrieve submission objects (WorkspaceItem/WorkflowItem) @@ -70,4 +71,24 @@ export class SubmissionObjectDataService { )); } } + + patch(object: SubmissionObject, operations: Operation[]): Observable> { + switch (this.submissionService.getSubmissionScope()) { + case SubmissionScopeType.WorkspaceItem: + return this.workspaceitemDataService.patch(object, operations); + case SubmissionScopeType.WorkflowItem: + return this.workflowItemDataService.patch(object, operations); + default: + const now = new Date().getTime(); + return observableOf(new RemoteData( + now, + environment.cache.msToLive.default, + now, + RequestEntryState.Error, + 'The request couldn\'t be sent. Unable to determine the type of submission object', + undefined, + 400 + )); + } + } } diff --git a/src/app/core/submission/workflowitem-data.service.spec.ts b/src/app/core/submission/workflowitem-data.service.spec.ts index 3f6ec54fdad..8a8c67ce1a3 100644 --- a/src/app/core/submission/workflowitem-data.service.spec.ts +++ b/src/app/core/submission/workflowitem-data.service.spec.ts @@ -19,6 +19,7 @@ import { WorkflowItemDataService } from './workflowitem-data.service'; import { WorkflowItem } from './models/workflowitem.model'; import { CoreState } from '../core-state.model'; import { RequestEntry } from '../data/request-entry.model'; +import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service'; describe('WorkflowItemDataService test', () => { let scheduler: TestScheduler; @@ -87,6 +88,7 @@ describe('WorkflowItemDataService test', () => { objectCache, halService, notificationsService, + new DefaultChangeAnalyzer(), ); } diff --git a/src/app/core/submission/workflowitem-data.service.ts b/src/app/core/submission/workflowitem-data.service.ts index 47d18470d39..fc54e396ac6 100644 --- a/src/app/core/submission/workflowitem-data.service.ts +++ b/src/app/core/submission/workflowitem-data.service.ts @@ -21,19 +21,34 @@ import { SearchData, SearchDataImpl } from '../data/base/search-data'; import { DeleteData, DeleteDataImpl } from '../data/base/delete-data'; import { PaginatedList } from '../data/paginated-list.model'; import { dataService } from '../data/base/data-service.decorator'; +import { PatchData, PatchDataImpl } from '../data/base/patch-data'; +import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service'; +import { RestRequestMethod } from '../data/rest-request-method'; +import { Operation } from 'fast-json-patch'; + +/** + * Constructs an endpoint taking into account that the WorkflowItem's "uuid" has a prefix by removing that prefix from the ID + * @param endpoint Endpoint to append ID to + * @param resourceID WorkflowItem's "uuid" including the prefix to remove + */ +const constructWorkflowItemIdEndpoint = (endpoint, resourceID) => { + const regex = new RegExp(`^${WorkflowItem.type.value}-`); + return `${endpoint}/${resourceID.replace(regex, '')}`; +}; /** * A service that provides methods to make REST requests with workflow items endpoint. */ @Injectable() @dataService(WorkflowItem.type) -export class WorkflowItemDataService extends IdentifiableDataService implements SearchData, DeleteData { +export class WorkflowItemDataService extends IdentifiableDataService implements SearchData, DeleteData, PatchData { protected linkPath = 'workflowitems'; protected searchByItemLinkPath = 'item'; protected responseMsToLive = 10 * 1000; private searchData: SearchDataImpl; private deleteData: DeleteDataImpl; + private patchData: PatchDataImpl; constructor( protected requestService: RequestService, @@ -41,11 +56,13 @@ export class WorkflowItemDataService extends IdentifiableDataService, ) { super('workspaceitems', requestService, rdbService, objectCache, halService); this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive); this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint); + this.patchData = new PatchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.comparator, this.responseMsToLive, constructWorkflowItemIdEndpoint); } /** @@ -142,4 +159,20 @@ export class WorkflowItemDataService extends IdentifiableDataService> { return this.deleteData.deleteByHref(href, copyVirtualMetadata); } + + commitUpdates(method?: RestRequestMethod): void { + this.patchData.commitUpdates(method); + } + + createPatchFromCache(object: WorkflowItem): Observable { + return this.patchData.createPatchFromCache(object); + } + + patch(object: WorkflowItem, operations: Operation[]): Observable> { + return this.patchData.patch(object, operations); + } + + update(object: WorkflowItem): Observable> { + return this.patchData.update(object); + } } diff --git a/src/app/core/submission/workspaceitem-data.service.spec.ts b/src/app/core/submission/workspaceitem-data.service.spec.ts index e766a6a039c..02028109a7c 100644 --- a/src/app/core/submission/workspaceitem-data.service.spec.ts +++ b/src/app/core/submission/workspaceitem-data.service.spec.ts @@ -21,6 +21,7 @@ import { RequestEntry } from '../data/request-entry.model'; import { CoreState } from '../core-state.model'; import { testSearchDataImplementation } from '../data/base/search-data.spec'; import { testDeleteDataImplementation } from '../data/base/delete-data.spec'; +import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service'; describe('WorkspaceitemDataService test', () => { let scheduler: TestScheduler; @@ -89,11 +90,12 @@ describe('WorkspaceitemDataService test', () => { objectCache, halService, notificationsService, + new DefaultChangeAnalyzer(), ); } describe('composition', () => { - const initService = () => new WorkspaceitemDataService(null, null, null, null, null); + const initService = () => new WorkspaceitemDataService(null, null, null, null, null, null); testSearchDataImplementation(initService); testDeleteDataImplementation(initService); }); diff --git a/src/app/core/submission/workspaceitem-data.service.ts b/src/app/core/submission/workspaceitem-data.service.ts index f285fb6eca5..7acf18ea611 100644 --- a/src/app/core/submission/workspaceitem-data.service.ts +++ b/src/app/core/submission/workspaceitem-data.service.ts @@ -16,17 +16,32 @@ import { PaginatedList } from '../data/paginated-list.model'; import { DeleteData, DeleteDataImpl } from '../data/base/delete-data'; import { NoContent } from '../shared/NoContent.model'; import { dataService } from '../data/base/data-service.decorator'; +import { PatchData, PatchDataImpl } from '../data/base/patch-data'; +import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service'; +import { RestRequestMethod } from '../data/rest-request-method'; +import { Operation } from 'fast-json-patch'; + +/** + * Constructs an endpoint taking into account that the WorkspaceItem's "uuid" has a prefix by removing that prefix from the ID + * @param endpoint Endpoint to append ID to + * @param resourceID WorkspaceItem's "uuid" including the prefix to remove + */ +const constructWorkspaceItemIdEndpoint = (endpoint, resourceID) => { + const regex = new RegExp(`^${WorkspaceItem.type.value}-`); + return `${endpoint}/${resourceID.replace(regex, '')}`; +}; /** * A service that provides methods to make REST requests with workspaceitems endpoint. */ @Injectable() @dataService(WorkspaceItem.type) -export class WorkspaceitemDataService extends IdentifiableDataService implements SearchData, DeleteData { +export class WorkspaceitemDataService extends IdentifiableDataService implements SearchData, DeleteData, PatchData { protected searchByItemLinkPath = 'item'; private searchData: SearchDataImpl; private deleteData: DeleteDataImpl; + private patchData: PatchDataImpl; constructor( protected requestService: RequestService, @@ -34,11 +49,13 @@ export class WorkspaceitemDataService extends IdentifiableDataService, ) { super('workspaceitems', requestService, rdbService, objectCache, halService); this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive); this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint); + this.patchData = new PatchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.comparator, this.responseMsToLive, constructWorkspaceItemIdEndpoint); } /** @@ -115,4 +132,20 @@ export class WorkspaceitemDataService extends IdentifiableDataService { + return this.patchData.createPatchFromCache(object); + } + + patch(object: WorkspaceItem, operations: Operation[]): Observable> { + return this.patchData.patch(object, operations); + } + + update(object: WorkspaceItem): Observable> { + return this.patchData.update(object); + } + } 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 b77ee9950ca..636c2bb54c9 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 @@ -235,7 +235,8 @@ describe('DsDynamicFormControlContainerComponent test suite', () => { } }, { provide: NgZone, useValue: new NgZone({}) }, - { provide: APP_CONFIG, useValue: environment } + { provide: APP_CONFIG, useValue: environment }, + { provide: 'sectionDataProvider', useValue: { id: 'mock-section-id' } }, ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }).compileComponents().then(() => { 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 ff5a119b6fc..af6f2e4fad0 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 @@ -10,6 +10,7 @@ import { OnChanges, OnDestroy, OnInit, + Optional, Output, QueryList, SimpleChanges, @@ -120,6 +121,7 @@ import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './ds-dynamic-form-cons import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; import { APP_CONFIG, AppConfig } from '../../../../../config/app-config.interface'; import { itemLinksToFollow } from '../../../utils/relation-query.utils'; +import { SectionDataObject } from '../../../../submission/sections/models/section-data.model'; export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type | null { switch (model.type) { @@ -209,6 +211,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo @Input() hasErrorMessaging = false; @Input() layout = null as DynamicFormLayout; @Input() model: any; + @Input() arrayIndex: number; relationshipValue$: Observable; isRelationship: boolean; modalRef: NgbModalRef; @@ -262,6 +265,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo public formBuilderService: FormBuilderService, private submissionService: SubmissionService, @Inject(APP_CONFIG) protected appConfig: AppConfig, + @Optional() @Inject('sectionDataProvider') public sectionData: SectionDataObject, ) { super(ref, componentFactoryResolver, layoutService, validationService, dynamicFormComponentService, relationService); this.fetchThumbnail = this.appConfig.browseBy.showThumbnails; @@ -456,6 +460,13 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo } else if (typeof this.model.value.value === 'string') { modalComp.query = this.model.value.value; } + + // If the existing value is not virtual, store properties on the modal required to perform a replace operation + if (hasValue(this.sectionData) && !this.model.value.isVirtual) { + modalComp.replaceValuePlace = this.arrayIndex || 0; + modalComp.replaceValueMetadataField = this.model.name; + modalComp.replaceValueSection = this.sectionData?.id; + } } modalComp.repeatable = this.model.repeatable; 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 dd19e6158df..df9ec90ca6e 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 @@ -30,6 +30,7 @@ [class.d-none]="_model.hidden" [layout]="formLayout" [model]="_model" + [arrayIndex]="idx" [templates]="templates" [ngClass]="[getClass('element', 'host', _model), getClass('grid', 'host', _model)]" (dfBlur)="onBlur($event)" 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 b72a8722aec..5bb06443c50 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 @@ -6,8 +6,8 @@