Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,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 @@ -12,7 +12,7 @@ import { Store } from '@ngrx/store';
import { Item } from '../../../../../core/shared/item.model';
import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model';
import { RelationshipOptions } from '../../models/relationship-options.model';
import { AddRelationshipAction, RemoveRelationshipAction } from './relationship.actions';
import { AddRelationshipAction, RemoveRelationshipAction, ReplaceRelationshipAction } from './relationship.actions';
import { SearchConfigurationService } from '../../../../../core/shared/search/search-configuration.service';
import { PaginatedSearchOptions } from '../../../../search/models/paginated-search-options.model';
import { ExternalSource } from '../../../../../core/shared/external-source.model';
Expand All @@ -32,9 +32,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 @@ -68,11 +70,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$ = observableOf([searchResult1, searchResult2]);
selectableListService = { getSelectableList: () => selection$ };
Expand Down Expand Up @@ -172,13 +176,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 @@ -13,7 +13,7 @@ import { SearchResult } from '../../../../search/models/search-result.model';
import { Item } from '../../../../../core/shared/item.model';
import {
AddRelationshipAction,
RemoveRelationshipAction,
RemoveRelationshipAction, ReplaceRelationshipAction,
UpdateRelationshipNameVariantAction,
} from './relationship.actions';
import { RelationshipDataService } from '../../../../../core/data/relationship-data.service';
Expand Down Expand Up @@ -95,6 +95,17 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy

query: 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 @@ -235,9 +246,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 @@ -260,6 +279,7 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
* @param selectableObjects
*/
deselect(...selectableObjects: SearchResult<Item>[]) {
this.resetReplaceProperties();
this.zone.runOutsideAngular(
() => selectableObjects.forEach((object) => {
this.subMap[object.indexableObject.uuid].unsubscribe();
Expand Down Expand Up @@ -297,6 +317,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 { Relationship } from '../../../../../core/shared/item-relationships/rela

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 @@ -3,7 +3,12 @@ import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
import { provideMockActions } from '@ngrx/effects/testing';
import { Store } from '@ngrx/store';
import { RelationshipEffects } from './relationship.effects';
import { AddRelationshipAction, RelationshipActionTypes, RemoveRelationshipAction } from './relationship.actions';
import {
AddRelationshipAction,
RelationshipActionTypes,
RemoveRelationshipAction,
ReplaceRelationshipAction
} from './relationship.actions';
import { Item } from '../../../../../core/shared/item.model';
import { MetadataValue } from '../../../../../core/shared/metadata.models';
import { RelationshipTypeDataService } from '../../../../../core/data/relationship-type-data.service';
Expand All @@ -23,6 +28,7 @@ import { SelectableListService } from '../../../../object-list/selectable-list/s
import { cold, hot } from 'jasmine-marbles';
import { DEBOUNCE_TIME_OPERATOR } from '../../../../../core/shared/operators';
import { last } from 'rxjs/operators';
import { ItemDataService } from '../../../../../core/data/item-data.service';

describe('RelationshipEffects', () => {
let relationEffects: RelationshipEffects;
Expand Down Expand Up @@ -51,6 +57,7 @@ describe('RelationshipEffects', () => {
let notificationsService;
let translateService;
let selectableListService;
let itemService;

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

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

beforeEach(waitForAsync(() => {
Expand All @@ -118,6 +128,7 @@ describe('RelationshipEffects', () => {
provideMockActions(() => actions),
{ provide: RelationshipTypeDataService, useValue: mockRelationshipTypeService },
{ provide: RelationshipDataService, useValue: mockRelationshipService },
{ provide: ItemDataService, useValue: itemService },
{
provide: SubmissionObjectDataService, useValue: {
findById: () => createSuccessfulRemoteDataObject$(new WorkspaceItem())
Expand All @@ -140,6 +151,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 @@ -210,6 +222,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