Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,11 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
modalComp.query = this.model.value;
} 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 (!this.model.value.isVirtual) {
modalComp.replaceValuePlace = this.model.value.place;
modalComp.replaceValueMetadataField = this.model.name;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { NameVariantService } from './name-variant.service';
import {
AddRelationshipAction,
RemoveRelationshipAction,
ReplaceRelationshipAction,
} from './relationship.actions';

describe('DsDynamicLookupRelationModalComponent', () => {
Expand All @@ -54,9 +55,11 @@ describe('DsDynamicLookupRelationModalComponent', () => {
let item;
let item1;
let item2;
let item3;
let testWSI;
let searchResult1;
let searchResult2;
let searchResult3;
let listID;
let selection$;
let selectableListService;
Expand Down Expand Up @@ -90,11 +93,13 @@ describe('DsDynamicLookupRelationModalComponent', () => {
item = Object.assign(new Item(), { uuid: '7680ca97-e2bd-4398-bfa7-139a8673dc42', metadata: {} });
item1 = Object.assign(new Item(), { uuid: 'e1c51c69-896d-42dc-8221-1d5f2ad5516e' });
item2 = Object.assign(new Item(), { uuid: 'c8279647-1acc-41ae-b036-951d5f65649b' });
item3 = Object.assign(new Item(), { uuid: '6264b66f-ae25-4221-b72a-8696536c5ebb' });
testWSI = new WorkspaceItem();
testWSI.item = createSuccessfulRemoteDataObject$(item);
testWSI.collection = createSuccessfulRemoteDataObject$(collection);
searchResult1 = Object.assign(new ItemSearchResult(), { indexableObject: item1 });
searchResult2 = Object.assign(new ItemSearchResult(), { indexableObject: item2 });
searchResult3 = Object.assign(new ItemSearchResult(), { indexableObject: item3 });
listID = '6b0c8221-fcb4-47a8-b483-ca32363fffb3';
selection$ = of([searchResult1, searchResult2]);
selectableListService = { getSelectableList: () => selection$ };
Expand Down Expand Up @@ -197,13 +202,37 @@ describe('DsDynamicLookupRelationModalComponent', () => {
spyOn((component as any).store, 'dispatch');
});

it('should dispatch an AddRelationshipAction for each selected object', () => {
component.select(searchResult1, searchResult2);
const action = new AddRelationshipAction(component.item, searchResult1.indexableObject, relationship.relationshipType, submissionId, nameVariant);
const action2 = new AddRelationshipAction(component.item, searchResult2.indexableObject, relationship.relationshipType, submissionId, nameVariant);
describe('when replace properties are present', () => {
beforeEach(() => {
component.replaceValuePlace = 3;
component.replaceValueMetadataField = 'dc.subject';
});

expect((component as any).store.dispatch).toHaveBeenCalledWith(action);
expect((component as any).store.dispatch).toHaveBeenCalledWith(action2);
it('should dispatch a ReplaceRelationshipAction for the first selected object and a AddRelationshipAction for every other selected object', () => {
component.select(searchResult1, searchResult2, searchResult3);
const action1 = new ReplaceRelationshipAction(component.item, searchResult1.indexableObject, true, 3, 'dc.subject', relationship.relationshipType, submissionId, nameVariant);
const action2 = new AddRelationshipAction(component.item, searchResult2.indexableObject, relationship.relationshipType, submissionId, nameVariant);
const action3 = new AddRelationshipAction(component.item, searchResult3.indexableObject, relationship.relationshipType, submissionId, nameVariant);

expect((component as any).store.dispatch).toHaveBeenCalledWith(action1);
expect((component as any).store.dispatch).toHaveBeenCalledWith(action2);
expect((component as any).store.dispatch).toHaveBeenCalledWith(action3);
expect(component.replaceValuePlace).toBeUndefined();
expect(component.replaceValueMetadataField).toBeUndefined();
});
});

describe('when replace properties are missing', () => {
it('should dispatch an AddRelationshipAction for each selected object', () => {
component.select(searchResult1, searchResult2, searchResult3);
const action1 = new AddRelationshipAction(component.item, searchResult1.indexableObject, relationship.relationshipType, submissionId, nameVariant);
const action2 = new AddRelationshipAction(component.item, searchResult2.indexableObject, relationship.relationshipType, submissionId, nameVariant);
const action3 = new AddRelationshipAction(component.item, searchResult3.indexableObject, relationship.relationshipType, submissionId, nameVariant);

expect((component as any).store.dispatch).toHaveBeenCalledWith(action1);
expect((component as any).store.dispatch).toHaveBeenCalledWith(action2);
expect((component as any).store.dispatch).toHaveBeenCalledWith(action3);
});
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import { NameVariantService } from './name-variant.service';
import {
AddRelationshipAction,
RemoveRelationshipAction,
ReplaceRelationshipAction,
UpdateRelationshipNameVariantAction,
} from './relationship.actions';
import { ThemedDynamicLookupRelationSearchTabComponent } from './search-tab/themed-dynamic-lookup-relation-search-tab.component';
Expand Down Expand Up @@ -148,6 +149,17 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
*/
hiddenQuery: string;

/**
* The index of the plain-text value that should be replaced by adding a relationship
*/
replaceValuePlace: number;

/**
* The metadata field of the value to replace with a relationship
* Undefined if no value needs replacing
*/
replaceValueMetadataField: string;

/**
* A map of subscriptions within this component
*/
Expand Down Expand Up @@ -302,9 +314,17 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
]);
obs
.subscribe((arr: any[]) => {
return arr.forEach((object: any) => {
const addRelationshipAction = new AddRelationshipAction(this.item, object.item, this.relationshipOptions.relationshipType, this.submissionId, object.nameVariant);
this.store.dispatch(addRelationshipAction);
return arr.forEach((object: any, i: number) => {
let action;
if (i === 0 && hasValue(this.replaceValueMetadataField)) {
// This is the first action this modal performs and "replace" properties are present to replace an existing metadata value
action = new ReplaceRelationshipAction(this.item, object.item, true, this.replaceValuePlace, this.replaceValueMetadataField, this.relationshipOptions.relationshipType, this.submissionId, object.nameVariant);
// Only "replace" once, reset replace properties so future actions become "add"
this.resetReplaceProperties();
} else {
action = new AddRelationshipAction(this.item, object.item, this.relationshipOptions.relationshipType, this.submissionId, object.nameVariant);
}
this.store.dispatch(action);
},
);
});
Expand All @@ -327,6 +347,7 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
* @param selectableObjects
*/
deselect(...selectableObjects: SearchResult<DSpaceObject>[]) {
this.resetReplaceProperties();
this.zone.runOutsideAngular(
() => selectableObjects.forEach((object) => {
this.subMap[object.indexableObject.uuid].unsubscribe();
Expand Down Expand Up @@ -364,6 +385,11 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
this.totalInternal$.next(totalPages);
}

private resetReplaceProperties() {
this.replaceValueMetadataField = undefined;
this.replaceValuePlace = undefined;
}

ngOnDestroy() {
this.router.navigate([], {});
Object.values(this.subMap).forEach((subscription) => subscription.unsubscribe());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Action } from '@ngrx/store';

export const RelationshipActionTypes = {
ADD_RELATIONSHIP: type('dspace/relationship/ADD_RELATIONSHIP'),
REPLACE_RELATIONSHIP: type('dspace/relationship/REPLACE_RELATIONSHIP'),
REMOVE_RELATIONSHIP: type('dspace/relationship/REMOVE_RELATIONSHIP'),
UPDATE_NAME_VARIANT: type('dspace/relationship/UPDATE_NAME_VARIANT'),
UPDATE_RELATIONSHIP: type('dspace/relationship/UPDATE_RELATIONSHIP'),
Expand Down Expand Up @@ -132,10 +133,53 @@ export class RemoveRelationshipAction implements Action {
}
}

/**
* An ngrx action to replace a plain-text metadata value with a new relationship
*/
export class ReplaceRelationshipAction implements Action {
type = RelationshipActionTypes.REPLACE_RELATIONSHIP;

payload: {
item1: Item;
item2: Item;
replaceLeftSide: boolean;
place: number;
mdField: string;
relationshipType: string;
submissionId: string;
nameVariant: string;
};

/**
* Create a new AddRelationshipAction
*
* @param item1 The first item in the relationship
* @param item2 The second item in the relationship
* @param replaceLeftSide If true, the item on the left side (item1) will have its metadata value replaced
* @param place The index of the metadata value that should be replaced with the new relationship
* @param mdField The metadata field of the value to replace
* @param relationshipType The label of the relationshipType
* @param submissionId The current submissionId
* @param nameVariant The nameVariant of the relationshipType
*/
constructor(
item1: Item,
item2: Item,
replaceLeftSide: boolean,
place: number,
mdField: string,
relationshipType: string,
submissionId: string,
nameVariant?: string,
) {
this.payload = { item1, item2, replaceLeftSide, place, mdField, relationshipType, submissionId, nameVariant };
}
}

/**
* A type to encompass all RelationshipActions
*/
export type RelationshipAction
= AddRelationshipAction
| ReplaceRelationshipAction
| RemoveRelationshipAction;
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@ import {
} from 'rxjs';
import { last } from 'rxjs/operators';

import { ItemDataService } from '../../../../../core/data/item-data.service';
import { SubmissionObjectService } from '../../../../../submission/submission-object.service';
import { SelectableListService } from '../../../../object-list/selectable-list/selectable-list.service';
import {
AddRelationshipAction,
RelationshipActionTypes,
RemoveRelationshipAction,
ReplaceRelationshipAction,
} from './relationship.actions';
import { RelationshipEffects } from './relationship.effects';

Expand Down Expand Up @@ -66,6 +68,7 @@ describe('RelationshipEffects', () => {
let notificationsService;
let translateService;
let selectableListService;
let itemService;

function init() {
testUUID1 = '20e24c2f-a00a-467c-bdee-c929e79bf08d';
Expand Down Expand Up @@ -108,8 +111,8 @@ describe('RelationshipEffects', () => {
getRelationshipByItemsAndLabel:
() => of(relationship),
deleteRelationship: () => of(new RestResponse(true, 200, 'OK')),
addRelationship: () => of(new RestResponse(true, 200, 'OK')),

addRelationship: () => createSuccessfulRemoteDataObject$(new Relationship()),
update: () => createSuccessfulRemoteDataObject$(new Relationship()),
};
mockRelationshipTypeService = {
getRelationshipTypeByLabelAndTypes:
Expand All @@ -123,6 +126,9 @@ describe('RelationshipEffects', () => {
findSelectedByCondition: of({}),
deselectSingle: {},
});
itemService = jasmine.createSpyObj('itemService', {
patch: createSuccessfulRemoteDataObject$(new Item()),
});
}

beforeEach(waitForAsync(() => {
Expand All @@ -133,6 +139,7 @@ describe('RelationshipEffects', () => {
provideMockActions(() => actions),
{ provide: RelationshipTypeDataService, useValue: mockRelationshipTypeService },
{ provide: RelationshipDataService, useValue: mockRelationshipService },
{ provide: ItemDataService, useValue: itemService },
{
provide: SubmissionObjectService, useValue: {
findById: () => createSuccessfulRemoteDataObject$(new WorkspaceItem()),
Expand All @@ -155,6 +162,7 @@ describe('RelationshipEffects', () => {
identifier = (relationEffects as any).createIdentifier(leftItem, rightItem, relationshipType.leftwardType);
spyOn((relationEffects as any), 'addRelationship').and.stub();
spyOn((relationEffects as any), 'removeRelationship').and.stub();
spyOn((relationEffects as any), 'replaceRelationship').and.stub();
});

describe('mapLastActions$', () => {
Expand Down Expand Up @@ -225,6 +233,75 @@ describe('RelationshipEffects', () => {
});
});

describe('When a REPLACE_RELATIONSHIP action is triggered', () => {
describe('When it\'s the first time for this identifier', () => {
let action;

it('should set the current value debounceMap and the value of the initialActionMap to REPLACE_RELATIONSHIP', () => {
action = new ReplaceRelationshipAction(leftItem, rightItem, true, 0, 'dc.subject', relationshipType.leftwardType, '1234');
actions = hot('--a-|', { a: action });
const expected = cold('--b-|', { b: undefined });
expect(relationEffects.mapLastActions$).toBeObservable(expected);

expect((relationEffects as any).initialActionMap[identifier]).toBe(action.type);
expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type);
});
});

describe('When it\'s not the first time for this identifier', () => {
let action;
const testActionType = 'TEST_TYPE';
beforeEach(() => {
(relationEffects as any).initialActionMap[identifier] = testActionType;
(relationEffects as any).debounceMap[identifier] = new BehaviorSubject<string>(testActionType);
});

it('should set the current value debounceMap to REPLACE_RELATIONSHIP but not change the value of the initialActionMap', () => {
action = new ReplaceRelationshipAction(leftItem, rightItem, true, 0, 'dc.subject', relationshipType.leftwardType, '1234');
actions = hot('--a-|', { a: action });

const expected = cold('--b-|', { b: undefined });
expect(relationEffects.mapLastActions$).toBeObservable(expected);

expect((relationEffects as any).initialActionMap[identifier]).toBe(testActionType);
expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type);
});
});

describe('When the initialActionMap contains a REPLACE_RELATIONSHIP action', () => {
let action;
describe('When the last value in the debounceMap is also a REPLACE_RELATIONSHIP action', () => {
beforeEach(() => {
jasmine.getEnv().allowRespy(true);
spyOn((relationEffects as any), 'replaceRelationship').and.returnValue(createSuccessfulRemoteDataObject$(relationship));
spyOn((relationEffects as any).relationshipService, 'update').and.callThrough();
((relationEffects as any).debounceTime as jasmine.Spy).and.returnValue((v) => v);
(relationEffects as any).initialActionMap[identifier] = RelationshipActionTypes.REPLACE_RELATIONSHIP;
});

it('should call replaceRelationship on the effect', () => {
action = new ReplaceRelationshipAction(leftItem, rightItem, true, 0, 'dc.subject', relationshipType.leftwardType, '1234');
actions = hot('--a-|', { a: action });
const expected = cold('--b-|', { b: undefined });
expect(relationEffects.mapLastActions$).toBeObservable(expected);
expect((relationEffects as any).replaceRelationship).toHaveBeenCalledWith(leftItem, rightItem, true, 0, 'dc.subject', relationshipType.leftwardType, '1234', undefined);
});
});

describe('When the last value in the debounceMap is instead a REMOVE_RELATIONSHIP action', () => {
it('should <b>not</b> call removeRelationship or replaceRelationship on the effect', () => {
const actiona = new ReplaceRelationshipAction(leftItem, rightItem, true, 0, 'dc.subject', relationshipType.leftwardType, '1234');
const actionb = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234');
actions = hot('--ab-|', { a: actiona, b: actionb });
const expected = cold('--bb-|', { b: undefined });
expect(relationEffects.mapLastActions$).toBeObservable(expected);
expect((relationEffects as any).replaceRelationship).not.toHaveBeenCalled();
expect((relationEffects as any).removeRelationship).not.toHaveBeenCalled();
});
});
});
});

describe('When an REMOVE_RELATIONSHIP action is triggered', () => {
describe('When it\'s the first time for this identifier', () => {
let action;
Expand Down
Loading
Loading