Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
27686dc
136580: Submission - Replace plaintext with relationship
Atmire-Kristof Dec 3, 2025
73eb506
Merge remote-tracking branch 'atmire/w2p-112198_add-relationship-effe…
Atmire-Kristof Dec 3, 2025
9d45fd0
136580: replace relationship queue post-merge
Atmire-Kristof Dec 3, 2025
80070a8
136580: Lint fix
Atmire-Kristof Dec 3, 2025
7e925da
Merge remote-tracking branch 'dspace/main' into w2p-136580_Replace-pl…
Atmire-Kristof Dec 3, 2025
b4c244e
136580: Fixed selectObject being called twice for non-repeatable fields
alexandrevryghem Dec 4, 2025
df0277b
136580: Submission select multiple relationships fix
Atmire-Kristof Dec 10, 2025
0fff8f5
Merge branch 'w2p-136580_Replace-plaintext-relationship-7.6' into w2p…
Atmire-Kristof Dec 10, 2025
00f3a38
138978: Replace plaintext relationship submission object patch fix
Atmire-Kristof Mar 6, 2026
1a643f5
Merge remote-tracking branch 'dspace/main' into w2p-136580_Replace-pl…
Atmire-Kristof Mar 9, 2026
9c93729
Merge branch 'w2p-136580_Replace-plaintext-relationship-7.6' into w2p…
Atmire-Kristof Mar 9, 2026
ccb6d03
138978: Optional sectionDataProvider
Atmire-Kristof Mar 9, 2026
19f40d7
Merge branch 'w2p-136580_Replace-plaintext-relationship-7.6' into w2p…
Atmire-Kristof Mar 9, 2026
e4dc497
141370: Fix lookup replace for non-repeatable and fresh values
Atmire-Kristof May 15, 2026
b09fef7
Merge branch 'w2p-136580_Replace-plaintext-relationship-7.6' into w2p…
Atmire-Kristof May 15, 2026
558a11f
Merge remote-tracking branch 'dspace/main' into w2p-136580_Replace-pl…
Atmire-Kristof May 15, 2026
1addfc6
136580: Fix change-detection to detect authority changes
Atmire-Kristof May 27, 2026
acabb5a
Merge branch 'w2p-136580_Replace-plaintext-relationship-7.6' into w2p…
Atmire-Kristof May 27, 2026
b7f47bc
141370: Prevent race condition between requests on relationship lookup
Atmire-Kristof Jun 1, 2026
c963c69
Merge branch 'w2p-136580_Replace-plaintext-relationship-7.6' into w2p…
Atmire-Kristof Jun 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/app/core/submission/workflowitem-data.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv
import { ObjectCacheService } from '../cache/object-cache.service';
import { RestResponse } from '../cache/response.models';
import { CoreState } from '../core-state.model';
import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service';
import { HrefOnlyDataService } from '../data/href-only-data.service';
import { RequestService } from '../data/request.service';
import { RequestEntry } from '../data/request-entry.model';
Expand Down Expand Up @@ -91,6 +92,7 @@ describe('WorkflowItemDataService test', () => {
objectCache,
halService,
notificationsService,
new DefaultChangeAnalyzer(),
);
}

Expand Down
38 changes: 37 additions & 1 deletion src/app/core/submission/workflowitem-data.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Injectable } from '@angular/core';
import { RestRequestMethod } from '@dspace/config/rest-request-method';
import { hasValue } from '@dspace/shared/utils/empty.util';
import { Operation } from 'fast-json-patch';
import { Observable } from 'rxjs';
import {
find,
Expand All @@ -14,10 +16,15 @@ import {
DeleteDataImpl,
} from '../data/base/delete-data';
import { IdentifiableDataService } from '../data/base/identifiable-data.service';
import {
PatchData,
PatchDataImpl,
} from '../data/base/patch-data';
import {
SearchData,
SearchDataImpl,
} from '../data/base/search-data';
import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service';
import { FindListOptions } from '../data/find-list-options.model';
import { PaginatedList } from '../data/paginated-list.model';
import { RemoteData } from '../data/remote-data';
Expand All @@ -30,28 +37,41 @@ import { NoContent } from '../shared/NoContent.model';
import { getFirstCompletedRemoteData } from '../shared/operators';
import { WorkflowItem } from './models/workflowitem.model';

/**
* 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({ providedIn: 'root' })
export class WorkflowItemDataService extends IdentifiableDataService<WorkflowItem> implements SearchData<WorkflowItem>, DeleteData<WorkflowItem> {
export class WorkflowItemDataService extends IdentifiableDataService<WorkflowItem> implements SearchData<WorkflowItem>, DeleteData<WorkflowItem>, PatchData<WorkflowItem> {
protected searchByItemLinkPath = 'item';
protected responseMsToLive = 10 * 1000;

private searchData: SearchDataImpl<WorkflowItem>;
private deleteData: DeleteDataImpl<WorkflowItem>;
private patchData: PatchDataImpl<WorkflowItem>;

constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected comparator: DefaultChangeAnalyzer<WorkflowItem>,
) {
super('workflowitems', 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);
}

/**
Expand Down Expand Up @@ -148,4 +168,20 @@ export class WorkflowItemDataService extends IdentifiableDataService<WorkflowIte
public deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
return this.deleteData.deleteByHref(href, copyVirtualMetadata);
}

commitUpdates(method?: RestRequestMethod): void {
this.patchData.commitUpdates(method);
}

createPatchFromCache(object: WorkflowItem): Observable<Operation[]> {
return this.patchData.createPatchFromCache(object);
}

patch(object: WorkflowItem, operations: Operation[]): Observable<RemoteData<WorkflowItem>> {
return this.patchData.patch(object, operations);
}

update(object: WorkflowItem): Observable<RemoteData<WorkflowItem>> {
return this.patchData.update(object);
}
}
40 changes: 38 additions & 2 deletions src/app/core/submission/workspaceitem-data.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import {
HttpHeaders,
} from '@angular/common/http';
import { Injectable } from '@angular/core';
import { RestRequestMethod } from '@dspace/config/rest-request-method';
import { hasValue } from '@dspace/shared/utils/empty.util';
import { Store } from '@ngrx/store';
import { Operation } from 'fast-json-patch';
import { Observable } from 'rxjs';
import {
find,
Expand All @@ -20,6 +22,10 @@ import {
DeleteDataImpl,
} from '../data/base/delete-data';
import { IdentifiableDataService } from '../data/base/identifiable-data.service';
import {
PatchData,
PatchDataImpl,
} from '../data/base/patch-data';
import {
SearchData,
SearchDataImpl,
Expand All @@ -37,15 +43,26 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
import { NoContent } from '../shared/NoContent.model';
import { WorkspaceItem } from './models/workspaceitem.model';

/**
* 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({ providedIn: 'root' })
export class WorkspaceitemDataService extends IdentifiableDataService<WorkspaceItem> implements DeleteData<WorkspaceItem>, SearchData<WorkspaceItem>{
export class WorkspaceitemDataService extends IdentifiableDataService<WorkspaceItem> implements DeleteData<WorkspaceItem>, SearchData<WorkspaceItem>, PatchData<WorkspaceItem> {
protected linkPath = 'workspaceitems';
protected searchByItemLinkPath = 'item';
private deleteData: DeleteData<WorkspaceItem>;
private searchData: SearchDataImpl<WorkspaceItem>;
private patchData: PatchDataImpl<WorkspaceItem>;

constructor(
protected comparator: DSOChangeAnalyzer<WorkspaceItem>,
Expand All @@ -55,10 +72,12 @@ export class WorkspaceitemDataService extends IdentifiableDataService<WorkspaceI
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected objectCache: ObjectCacheService,
protected store: Store<CoreState>) {
protected store: Store<CoreState>,
) {
super('workspaceitems', requestService, rdbService, objectCache, halService);
this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint);
this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
this.patchData = new PatchDataImpl<WorkspaceItem>(this.linkPath, requestService, rdbService, objectCache, halService, this.comparator, this.responseMsToLive, constructWorkspaceItemIdEndpoint);
}
public delete(objectId: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
return this.deleteData.delete(objectId, copyVirtualMetadata);
Expand Down Expand Up @@ -137,4 +156,21 @@ export class WorkspaceitemDataService extends IdentifiableDataService<WorkspaceI
searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig<WorkspaceItem>[]): Observable<RemoteData<PaginatedList<WorkspaceItem>>> {
return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}

commitUpdates(method?: RestRequestMethod): void {
this.patchData.commitUpdates(method);
}

createPatchFromCache(object: WorkspaceItem): Observable<Operation[]> {
return this.patchData.createPatchFromCache(object);
}

patch(object: WorkspaceItem, operations: Operation[]): Observable<RemoteData<WorkspaceItem>> {
return this.patchData.patch(object, operations);
}

update(object: WorkspaceItem): Observable<RemoteData<WorkspaceItem>> {
return this.patchData.update(object);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
{ provide: DYNAMIC_FORM_CONTROL_MAP_FN, useValue: dsDynamicFormControlMapFn },
{ provide: LiveRegionService, useValue: getLiveRegionServiceStub() },
{ provide: Actions, useValue: actions$ },
{ provide: 'sectionDataProvider', useValue: { id: 'mock-section-id' } },
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
}).compileComponents().then(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
OnChanges,
OnDestroy,
OnInit,
Optional,
Output,
PLATFORM_ID,
QueryList,
Expand Down Expand Up @@ -124,6 +125,7 @@ import {
import { AppState } from '../../../../app.reducer';
import { EditMetadataSecurityComponent } from '../../../../item-page/edit-item-page/edit-metadata-security/edit-metadata-security.component';
import { SubmissionObjectActionTypes } from '../../../../submission/objects/submission-objects.actions';
import { SectionDataObject } from '../../../../submission/sections/models/section-data.model';
import { SubmissionService } from '../../../../submission/submission.service';
import { SubmissionObjectService } from '../../../../submission/submission-object.service';
import { LiveRegionService } from '../../../live-region/live-region.service';
Expand Down Expand Up @@ -174,6 +176,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
@Input() hasErrorMessaging = false;
@Input() layout = null as DynamicFormLayout;
@Input() model: any;
@Input() arrayIndex: number;
securityLevel: number;
relationshipValue$: Observable<ReorderableRelationship>;
isRelationship: boolean;
Expand Down Expand Up @@ -235,6 +238,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
private actions$: Actions,
protected renderer: Renderer2,
@Inject(PLATFORM_ID) protected platformId: string,
@Optional() @Inject('sectionDataProvider') public sectionData: SectionDataObject,
) {
super(ref, componentFactoryResolver, layoutService, validationService, dynamicFormComponentService, relationService);
this.fetchThumbnail = this.appConfig.browseBy.showThumbnails;
Expand Down Expand Up @@ -485,6 +489,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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,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)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ describe('DsDynamicFormArrayComponent', () => {
{ provide: APP_CONFIG, useValue: environment },
{ provide: DYNAMIC_FORM_CONTROL_MAP_FN, useValue: dsDynamicFormControlMapFn },
{ provide: LiveRegionService, useValue: getLiveRegionServiceStub() },
{ provide: 'sectionDataProvider', useValue: { id: 'mock-section-id' } },
],
}).overrideComponent(DsDynamicFormArrayComponent, {
remove: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ describe('DsDynamicRelationGroupComponent test suite', () => {
{ provide: APP_DATA_SERVICES_MAP, useValue: {} },
{ provide: DYNAMIC_FORM_CONTROL_MAP_FN, useValue: dsDynamicFormControlMapFn },
{ provide: LiveRegionService, useValue: getLiveRegionServiceStub() },
{ provide: 'sectionDataProvider', useValue: { id: 'mock-section-id' } },
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ <h4 class="modal-title" id="modal-title">{{ ('submission.sections.describe.relat
</button>
</div>
<div class="modal-body">
@if (!item || !collection) {
@if (!item || !collection || isSubmitting) {
<ds-loading></ds-loading>
}
@if (item && collection) {
@if (item && collection && !isSubmitting) {
<ul ngbNav #nav="ngbNav" class="nav-tabs">
<li ngbNavItem role="presentation">
<a ngbNavLink>{{'submission.sections.describe.relationship-lookup.search-tab.tab-title.' + relationshipOptions.relationshipType | translate : { count: (totalInternal$ | async)} }}</a>
Expand Down Expand Up @@ -78,18 +78,18 @@ <h4 class="modal-title" id="modal-title">{{ ('submission.sections.describe.relat
<small>{{ ('submission.sections.describe.relationship-lookup.selected' | translate: {size: (selection$ | async)?.length || 0}) }}</small>
<div class="d-flex float-end space-children-mr">
<div class="close-button">
<button type="button" [dsBtnDisabled]="isPending" class="btn btn-outline-secondary" (click)="close()">
<button type="button" [dsBtnDisabled]="isPending || isSubmitting" class="btn btn-outline-secondary" (click)="close()">
{{ ('submission.sections.describe.relationship-lookup.close' | translate) }}</button>
</div>
@if (isEditRelationship) {
<button class="btn btn-danger discard"
[dsBtnDisabled]="(toAdd.length === 0 && toRemove.length === 0) || isPending"
[dsBtnDisabled]="(toAdd.length === 0 && toRemove.length === 0) || isPending || isSubmitting"
(click)="discardEv()">
<i class="fas fa-times"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.metadata.discard-button" | translate}}</span>
</button>
<button class="btn btn-primary submit"
[dsBtnDisabled]="(toAdd.length === 0 && toRemove.length === 0) || isPending"
[dsBtnDisabled]="(toAdd.length === 0 && toRemove.length === 0) || isPending || isSubmitting"
(click)="submitEv()">
@if (isPending) {
<span class="spinner-border spinner-border-sm me-1" role="status"
Expand Down
Loading
Loading