Skip to content

Commit 27686dc

Browse files
136580: Submission - Replace plaintext with relationship
1 parent 404ccd9 commit 27686dc

File tree

6 files changed

+283
-35
lines changed

6 files changed

+283
-35
lines changed

src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,11 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
455455
modalComp.query = this.model.value;
456456
} else if (typeof this.model.value.value === 'string') {
457457
modalComp.query = this.model.value.value;
458+
// If the existing value is not virtual, store properties on the modal required to perform a replace operation
459+
if (!this.model.value.isVirtual) {
460+
modalComp.replaceValuePlace = this.model.value.place;
461+
modalComp.replaceValueMetadataField = this.model.name;
462+
}
458463
}
459464
}
460465

src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.spec.ts

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { Store } from '@ngrx/store';
1212
import { Item } from '../../../../../core/shared/item.model';
1313
import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model';
1414
import { RelationshipOptions } from '../../models/relationship-options.model';
15-
import { AddRelationshipAction, RemoveRelationshipAction } from './relationship.actions';
15+
import { AddRelationshipAction, RemoveRelationshipAction, ReplaceRelationshipAction } from './relationship.actions';
1616
import { SearchConfigurationService } from '../../../../../core/shared/search/search-configuration.service';
1717
import { PaginatedSearchOptions } from '../../../../search/models/paginated-search-options.model';
1818
import { ExternalSource } from '../../../../../core/shared/external-source.model';
@@ -32,9 +32,11 @@ describe('DsDynamicLookupRelationModalComponent', () => {
3232
let item;
3333
let item1;
3434
let item2;
35+
let item3;
3536
let testWSI;
3637
let searchResult1;
3738
let searchResult2;
39+
let searchResult3;
3840
let listID;
3941
let selection$;
4042
let selectableListService;
@@ -68,11 +70,13 @@ describe('DsDynamicLookupRelationModalComponent', () => {
6870
item = Object.assign(new Item(), { uuid: '7680ca97-e2bd-4398-bfa7-139a8673dc42', metadata: {} });
6971
item1 = Object.assign(new Item(), { uuid: 'e1c51c69-896d-42dc-8221-1d5f2ad5516e' });
7072
item2 = Object.assign(new Item(), { uuid: 'c8279647-1acc-41ae-b036-951d5f65649b' });
73+
item3 = Object.assign(new Item(), { uuid: '6264b66f-ae25-4221-b72a-8696536c5ebb' });
7174
testWSI = new WorkspaceItem();
7275
testWSI.item = createSuccessfulRemoteDataObject$(item);
7376
testWSI.collection = createSuccessfulRemoteDataObject$(collection);
7477
searchResult1 = Object.assign(new ItemSearchResult(), { indexableObject: item1 });
7578
searchResult2 = Object.assign(new ItemSearchResult(), { indexableObject: item2 });
79+
searchResult3 = Object.assign(new ItemSearchResult(), { indexableObject: item3 });
7680
listID = '6b0c8221-fcb4-47a8-b483-ca32363fffb3';
7781
selection$ = observableOf([searchResult1, searchResult2]);
7882
selectableListService = { getSelectableList: () => selection$ };
@@ -172,13 +176,37 @@ describe('DsDynamicLookupRelationModalComponent', () => {
172176
spyOn((component as any).store, 'dispatch');
173177
});
174178

175-
it('should dispatch an AddRelationshipAction for each selected object', () => {
176-
component.select(searchResult1, searchResult2);
177-
const action = new AddRelationshipAction(component.item, searchResult1.indexableObject, relationship.relationshipType, submissionId, nameVariant);
178-
const action2 = new AddRelationshipAction(component.item, searchResult2.indexableObject, relationship.relationshipType, submissionId, nameVariant);
179+
describe('when replace properties are present', () => {
180+
beforeEach(() => {
181+
component.replaceValuePlace = 3;
182+
component.replaceValueMetadataField = 'dc.subject';
183+
});
179184

180-
expect((component as any).store.dispatch).toHaveBeenCalledWith(action);
181-
expect((component as any).store.dispatch).toHaveBeenCalledWith(action2);
185+
it('should dispatch a ReplaceRelationshipAction for the first selected object and a AddRelationshipAction for every other selected object', () => {
186+
component.select(searchResult1, searchResult2, searchResult3);
187+
const action1 = new ReplaceRelationshipAction(component.item, searchResult1.indexableObject, true, 3, 'dc.subject', relationship.relationshipType, submissionId, nameVariant);
188+
const action2 = new AddRelationshipAction(component.item, searchResult2.indexableObject, relationship.relationshipType, submissionId, nameVariant);
189+
const action3 = new AddRelationshipAction(component.item, searchResult3.indexableObject, relationship.relationshipType, submissionId, nameVariant);
190+
191+
expect((component as any).store.dispatch).toHaveBeenCalledWith(action1);
192+
expect((component as any).store.dispatch).toHaveBeenCalledWith(action2);
193+
expect((component as any).store.dispatch).toHaveBeenCalledWith(action3);
194+
expect(component.replaceValuePlace).toBeUndefined();
195+
expect(component.replaceValueMetadataField).toBeUndefined();
196+
});
197+
});
198+
199+
describe('when replace properties are missing', () => {
200+
it('should dispatch an AddRelationshipAction for each selected object', () => {
201+
component.select(searchResult1, searchResult2, searchResult3);
202+
const action1 = new AddRelationshipAction(component.item, searchResult1.indexableObject, relationship.relationshipType, submissionId, nameVariant);
203+
const action2 = new AddRelationshipAction(component.item, searchResult2.indexableObject, relationship.relationshipType, submissionId, nameVariant);
204+
const action3 = new AddRelationshipAction(component.item, searchResult3.indexableObject, relationship.relationshipType, submissionId, nameVariant);
205+
206+
expect((component as any).store.dispatch).toHaveBeenCalledWith(action1);
207+
expect((component as any).store.dispatch).toHaveBeenCalledWith(action2);
208+
expect((component as any).store.dispatch).toHaveBeenCalledWith(action3);
209+
});
182210
});
183211
});
184212

src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { SearchResult } from '../../../../search/models/search-result.model';
1313
import { Item } from '../../../../../core/shared/item.model';
1414
import {
1515
AddRelationshipAction,
16-
RemoveRelationshipAction,
16+
RemoveRelationshipAction, ReplaceRelationshipAction,
1717
UpdateRelationshipNameVariantAction,
1818
} from './relationship.actions';
1919
import { RelationshipDataService } from '../../../../../core/data/relationship-data.service';
@@ -95,6 +95,17 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
9595

9696
query: string;
9797

98+
/**
99+
* The index of the plain-text value that should be replaced by adding a relationship
100+
*/
101+
replaceValuePlace: number;
102+
103+
/**
104+
* The metadata field of the value to replace with a relationship
105+
* Undefined if no value needs replacing
106+
*/
107+
replaceValueMetadataField: string;
108+
98109
/**
99110
* A map of subscriptions within this component
100111
*/
@@ -235,9 +246,17 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
235246
]);
236247
obs
237248
.subscribe((arr: any[]) => {
238-
return arr.forEach((object: any) => {
239-
const addRelationshipAction = new AddRelationshipAction(this.item, object.item, this.relationshipOptions.relationshipType, this.submissionId, object.nameVariant);
240-
this.store.dispatch(addRelationshipAction);
249+
return arr.forEach((object: any, i: number) => {
250+
let action;
251+
if (i === 0 && hasValue(this.replaceValueMetadataField)) {
252+
// This is the first action this modal performs and "replace" properties are present to replace an existing metadata value
253+
action = new ReplaceRelationshipAction(this.item, object.item, true, this.replaceValuePlace, this.replaceValueMetadataField, this.relationshipOptions.relationshipType, this.submissionId, object.nameVariant);
254+
// Only "replace" once, reset replace properties so future actions become "add"
255+
this.resetReplaceProperties();
256+
} else {
257+
action = new AddRelationshipAction(this.item, object.item, this.relationshipOptions.relationshipType, this.submissionId, object.nameVariant);
258+
}
259+
this.store.dispatch(action);
241260
}
242261
);
243262
});
@@ -260,6 +279,7 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
260279
* @param selectableObjects
261280
*/
262281
deselect(...selectableObjects: SearchResult<Item>[]) {
282+
this.resetReplaceProperties();
263283
this.zone.runOutsideAngular(
264284
() => selectableObjects.forEach((object) => {
265285
this.subMap[object.indexableObject.uuid].unsubscribe();
@@ -297,6 +317,11 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
297317
this.totalInternal$.next(totalPages);
298318
}
299319

320+
private resetReplaceProperties() {
321+
this.replaceValueMetadataField = undefined;
322+
this.replaceValuePlace = undefined;
323+
}
324+
300325
ngOnDestroy() {
301326
this.router.navigate([], {});
302327
Object.values(this.subMap).forEach((subscription) => subscription.unsubscribe());

src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.actions.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Relationship } from '../../../../../core/shared/item-relationships/rela
99

1010
export const RelationshipActionTypes = {
1111
ADD_RELATIONSHIP: type('dspace/relationship/ADD_RELATIONSHIP'),
12+
REPLACE_RELATIONSHIP: type('dspace/relationship/REPLACE_RELATIONSHIP'),
1213
REMOVE_RELATIONSHIP: type('dspace/relationship/REMOVE_RELATIONSHIP'),
1314
UPDATE_NAME_VARIANT: type('dspace/relationship/UPDATE_NAME_VARIANT'),
1415
UPDATE_RELATIONSHIP: type('dspace/relationship/UPDATE_RELATIONSHIP'),
@@ -132,10 +133,53 @@ export class RemoveRelationshipAction implements Action {
132133
}
133134
}
134135

136+
/**
137+
* An ngrx action to replace a plain-text metadata value with a new relationship
138+
*/
139+
export class ReplaceRelationshipAction implements Action {
140+
type = RelationshipActionTypes.REPLACE_RELATIONSHIP;
141+
142+
payload: {
143+
item1: Item;
144+
item2: Item;
145+
replaceLeftSide: boolean;
146+
place: number;
147+
mdField: string;
148+
relationshipType: string;
149+
submissionId: string;
150+
nameVariant: string;
151+
};
152+
153+
/**
154+
* Create a new AddRelationshipAction
155+
*
156+
* @param item1 The first item in the relationship
157+
* @param item2 The second item in the relationship
158+
* @param replaceLeftSide If true, the item on the left side (item1) will have its metadata value replaced
159+
* @param place The index of the metadata value that should be replaced with the new relationship
160+
* @param mdField The metadata field of the value to replace
161+
* @param relationshipType The label of the relationshipType
162+
* @param submissionId The current submissionId
163+
* @param nameVariant The nameVariant of the relationshipType
164+
*/
165+
constructor(
166+
item1: Item,
167+
item2: Item,
168+
replaceLeftSide: boolean,
169+
place: number,
170+
mdField: string,
171+
relationshipType: string,
172+
submissionId: string,
173+
nameVariant?: string,
174+
) {
175+
this.payload = { item1, item2, replaceLeftSide, place, mdField, relationshipType, submissionId, nameVariant };
176+
}
177+
}
135178

136179
/**
137180
* A type to encompass all RelationshipActions
138181
*/
139182
export type RelationshipAction
140183
= AddRelationshipAction
184+
| ReplaceRelationshipAction
141185
| RemoveRelationshipAction;

src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.spec.ts

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
33
import { provideMockActions } from '@ngrx/effects/testing';
44
import { Store } from '@ngrx/store';
55
import { RelationshipEffects } from './relationship.effects';
6-
import { AddRelationshipAction, RelationshipActionTypes, RemoveRelationshipAction } from './relationship.actions';
6+
import {
7+
AddRelationshipAction,
8+
RelationshipActionTypes,
9+
RemoveRelationshipAction,
10+
ReplaceRelationshipAction
11+
} from './relationship.actions';
712
import { Item } from '../../../../../core/shared/item.model';
813
import { MetadataValue } from '../../../../../core/shared/metadata.models';
914
import { RelationshipTypeDataService } from '../../../../../core/data/relationship-type-data.service';
@@ -23,6 +28,7 @@ import { SelectableListService } from '../../../../object-list/selectable-list/s
2328
import { cold, hot } from 'jasmine-marbles';
2429
import { DEBOUNCE_TIME_OPERATOR } from '../../../../../core/shared/operators';
2530
import { last } from 'rxjs/operators';
31+
import { ItemDataService } from '../../../../../core/data/item-data.service';
2632

2733
describe('RelationshipEffects', () => {
2834
let relationEffects: RelationshipEffects;
@@ -51,6 +57,7 @@ describe('RelationshipEffects', () => {
5157
let notificationsService;
5258
let translateService;
5359
let selectableListService;
60+
let itemService;
5461

5562
function init() {
5663
testUUID1 = '20e24c2f-a00a-467c-bdee-c929e79bf08d';
@@ -93,8 +100,8 @@ describe('RelationshipEffects', () => {
93100
getRelationshipByItemsAndLabel:
94101
() => observableOf(relationship),
95102
deleteRelationship: () => observableOf(new RestResponse(true, 200, 'OK')),
96-
addRelationship: () => observableOf(new RestResponse(true, 200, 'OK'))
97-
103+
addRelationship: () => createSuccessfulRemoteDataObject$(new Relationship()),
104+
update: () => createSuccessfulRemoteDataObject$(new Relationship()),
98105
};
99106
mockRelationshipTypeService = {
100107
getRelationshipTypeByLabelAndTypes:
@@ -108,6 +115,9 @@ describe('RelationshipEffects', () => {
108115
findSelectedByCondition: observableOf({}),
109116
deselectSingle: {}
110117
});
118+
itemService = jasmine.createSpyObj('itemService', {
119+
patch: createSuccessfulRemoteDataObject$(new Item()),
120+
});
111121
}
112122

113123
beforeEach(waitForAsync(() => {
@@ -118,6 +128,7 @@ describe('RelationshipEffects', () => {
118128
provideMockActions(() => actions),
119129
{ provide: RelationshipTypeDataService, useValue: mockRelationshipTypeService },
120130
{ provide: RelationshipDataService, useValue: mockRelationshipService },
131+
{ provide: ItemDataService, useValue: itemService },
121132
{
122133
provide: SubmissionObjectDataService, useValue: {
123134
findById: () => createSuccessfulRemoteDataObject$(new WorkspaceItem())
@@ -140,6 +151,7 @@ describe('RelationshipEffects', () => {
140151
identifier = (relationEffects as any).createIdentifier(leftItem, rightItem, relationshipType.leftwardType);
141152
spyOn((relationEffects as any), 'addRelationship').and.stub();
142153
spyOn((relationEffects as any), 'removeRelationship').and.stub();
154+
spyOn((relationEffects as any), 'replaceRelationship').and.stub();
143155
});
144156

145157
describe('mapLastActions$', () => {
@@ -208,6 +220,73 @@ describe('RelationshipEffects', () => {
208220
});
209221
});
210222

223+
describe('When a REPLACE_RELATIONSHIP action is triggered', () => {
224+
describe('When it\'s the first time for this identifier', () => {
225+
let action;
226+
227+
it('should set the current value debounceMap and the value of the initialActionMap to REPLACE_RELATIONSHIP', () => {
228+
action = new ReplaceRelationshipAction(leftItem, rightItem, true, 0, 'dc.subject', relationshipType.leftwardType, '1234');
229+
actions = hot('--a-|', { a: action });
230+
const expected = cold('--b-|', { b: undefined });
231+
expect(relationEffects.mapLastActions$).toBeObservable(expected);
232+
233+
expect((relationEffects as any).initialActionMap[identifier]).toBe(action.type);
234+
expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type);
235+
});
236+
});
237+
238+
describe('When it\'s not the first time for this identifier', () => {
239+
let action;
240+
const testActionType = 'TEST_TYPE';
241+
beforeEach(() => {
242+
(relationEffects as any).initialActionMap[identifier] = testActionType;
243+
(relationEffects as any).debounceMap[identifier] = new BehaviorSubject<string>(testActionType);
244+
});
245+
246+
it('should set the current value debounceMap to REPLACE_RELATIONSHIP but not change the value of the initialActionMap', () => {
247+
action = new ReplaceRelationshipAction(leftItem, rightItem, true, 0, 'dc.subject', relationshipType.leftwardType, '1234');
248+
actions = hot('--a-|', { a: action });
249+
250+
const expected = cold('--b-|', { b: undefined });
251+
expect(relationEffects.mapLastActions$).toBeObservable(expected);
252+
253+
expect((relationEffects as any).initialActionMap[identifier]).toBe(testActionType);
254+
expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type);
255+
});
256+
});
257+
258+
describe('When the initialActionMap contains a REPLACE_RELATIONSHIP action', () => {
259+
let action;
260+
describe('When the last value in the debounceMap is also a REPLACE_RELATIONSHIP action', () => {
261+
beforeEach(() => {
262+
spyOn((relationEffects as any).relationshipService, 'update').and.callThrough();
263+
((relationEffects as any).debounceTime as jasmine.Spy).and.returnValue((v) => v);
264+
(relationEffects as any).initialActionMap[identifier] = RelationshipActionTypes.REPLACE_RELATIONSHIP;
265+
});
266+
267+
it('should call replaceRelationship on the effect', () => {
268+
action = new ReplaceRelationshipAction(leftItem, rightItem, true, 0, 'dc.subject', relationshipType.leftwardType, '1234');
269+
actions = hot('--a-|', { a: action });
270+
const expected = cold('--b-|', { b: undefined });
271+
expect(relationEffects.mapLastActions$).toBeObservable(expected);
272+
expect((relationEffects as any).replaceRelationship).toHaveBeenCalledWith(leftItem, rightItem, true, 0, 'dc.subject', relationshipType.leftwardType, '1234', undefined);
273+
});
274+
});
275+
276+
describe('When the last value in the debounceMap is instead a REMOVE_RELATIONSHIP action', () => {
277+
it('should <b>not</b> call removeRelationship or replaceRelationship on the effect', () => {
278+
const actiona = new ReplaceRelationshipAction(leftItem, rightItem, true, 0, 'dc.subject', relationshipType.leftwardType, '1234');
279+
const actionb = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
280+
actions = hot('--ab-|', { a: actiona, b: actionb });
281+
const expected = cold('--bb-|', { b: undefined });
282+
expect(relationEffects.mapLastActions$).toBeObservable(expected);
283+
expect((relationEffects as any).replaceRelationship).not.toHaveBeenCalled();
284+
expect((relationEffects as any).removeRelationship).not.toHaveBeenCalled();
285+
});
286+
});
287+
});
288+
});
289+
211290
describe('When an REMOVE_RELATIONSHIP action is triggered', () => {
212291
describe('When it\'s the first time for this identifier', () => {
213292
let action;

0 commit comments

Comments
 (0)