mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge pull request #541 from atmire/metadata-and-relationships-combined-in-submission
Combining relationships and metadata during submission
This commit is contained in:
@@ -209,7 +209,7 @@ describe('BitstreamFormatsComponent', () => {
|
||||
selectBitstreamFormat: {},
|
||||
deselectBitstreamFormat: {},
|
||||
deselectAllBitstreamFormats: {},
|
||||
delete: observableOf(true),
|
||||
delete: observableOf({ isSuccessful: true }),
|
||||
clearBitStreamFormatRequests: observableOf('cleared')
|
||||
});
|
||||
|
||||
|
@@ -11,6 +11,7 @@ import { hasValue } from '../../../shared/empty.util';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { RestResponse } from '../../../core/cache/response.models';
|
||||
|
||||
/**
|
||||
* This component renders a list of bitstream formats
|
||||
@@ -64,7 +65,7 @@ export class BitstreamFormatsComponent implements OnInit {
|
||||
const tasks$ = [];
|
||||
for (const format of formats) {
|
||||
if (hasValue(format.id)) {
|
||||
tasks$.push(this.bitstreamFormatService.delete(format.id));
|
||||
tasks$.push(this.bitstreamFormatService.delete(format.id).pipe(map((response: RestResponse) => response.isSuccessful)));
|
||||
}
|
||||
}
|
||||
zip(...tasks$).subscribe((results: boolean[]) => {
|
||||
|
@@ -5,8 +5,7 @@ import { hasNoValue, hasValue } from '../../shared/empty.util';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { getFinishedRemoteData } from '../../core/shared/operators';
|
||||
import { map, tap } from 'rxjs/operators';
|
||||
import { map, tap, find } from 'rxjs/operators';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
|
||||
/**
|
||||
@@ -29,18 +28,15 @@ export class CreateCollectionPageGuard implements CanActivate {
|
||||
this.router.navigate(['/404']);
|
||||
return observableOf(false);
|
||||
}
|
||||
const parent: Observable<RemoteData<Community>> = this.communityService.findById(parentID)
|
||||
return this.communityService.findById(parentID)
|
||||
.pipe(
|
||||
getFinishedRemoteData(),
|
||||
);
|
||||
|
||||
return parent.pipe(
|
||||
map((communityRD: RemoteData<Community>) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)),
|
||||
tap((isValid: boolean) => {
|
||||
if (!isValid) {
|
||||
this.router.navigate(['/404']);
|
||||
}
|
||||
})
|
||||
find((communityRD: RemoteData<Community>) => hasValue(communityRD.payload) || hasValue(communityRD.error)),
|
||||
map((communityRD: RemoteData<Community>) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)),
|
||||
tap((isValid: boolean) => {
|
||||
if (!isValid) {
|
||||
this.router.navigate(['/404']);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -5,8 +5,7 @@ import { hasNoValue, hasValue } from '../../shared/empty.util';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { getFinishedRemoteData } from '../../core/shared/operators';
|
||||
import { map, tap } from 'rxjs/operators';
|
||||
import { map, tap, find } from 'rxjs/operators';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
|
||||
/**
|
||||
@@ -29,18 +28,16 @@ export class CreateCommunityPageGuard implements CanActivate {
|
||||
return observableOf(true);
|
||||
}
|
||||
|
||||
const parent: Observable<RemoteData<Community>> = this.communityService.findById(parentID)
|
||||
return this.communityService.findById(parentID)
|
||||
.pipe(
|
||||
getFinishedRemoteData(),
|
||||
);
|
||||
|
||||
return parent.pipe(
|
||||
map((communityRD: RemoteData<Community>) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)),
|
||||
tap((isValid: boolean) => {
|
||||
if (!isValid) {
|
||||
this.router.navigate(['/404']);
|
||||
find((communityRD: RemoteData<Community>) => hasValue(communityRD.payload) || hasValue(communityRD.error)),
|
||||
map((communityRD: RemoteData<Community>) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)),
|
||||
tap((isValid: boolean) => {
|
||||
if (!isValid) {
|
||||
this.router.navigate(['/404']);
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -114,7 +114,7 @@ describe('ItemBitstreamsComponent', () => {
|
||||
}
|
||||
);
|
||||
bitstreamService = jasmine.createSpyObj('bitstreamService', {
|
||||
deleteAndReturnResponse: jasmine.createSpy('deleteAndReturnResponse')
|
||||
delete: jasmine.createSpy('delete')
|
||||
});
|
||||
objectCache = jasmine.createSpyObj('objectCache', {
|
||||
remove: jasmine.createSpy('remove')
|
||||
@@ -182,12 +182,25 @@ describe('ItemBitstreamsComponent', () => {
|
||||
comp.submit();
|
||||
});
|
||||
|
||||
it('should call deleteAndReturnResponse on the bitstreamService for the marked field', () => {
|
||||
expect(bitstreamService.deleteAndReturnResponse).toHaveBeenCalledWith(bitstream2.id);
|
||||
it('should call delete on the bitstreamService for the marked field', () => {
|
||||
expect(bitstreamService.delete).toHaveBeenCalledWith(bitstream2.id);
|
||||
});
|
||||
|
||||
it('should not call deleteAndReturnResponse on the bitstreamService for the unmarked field', () => {
|
||||
expect(bitstreamService.deleteAndReturnResponse).not.toHaveBeenCalledWith(bitstream1.id);
|
||||
it('should not call delete on the bitstreamService for the unmarked field', () => {
|
||||
expect(bitstreamService.delete).not.toHaveBeenCalledWith(bitstream1.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when dropBitstream is called', () => {
|
||||
const event = {
|
||||
fromIndex: 0,
|
||||
toIndex: 50,
|
||||
// tslint:disable-next-line:no-empty
|
||||
finish: () => {}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
comp.dropBitstream(bundle, event);
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -165,7 +165,7 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
||||
take(1),
|
||||
switchMap((removedBistreams: Bitstream[]) => {
|
||||
if (isNotEmpty(removedBistreams)) {
|
||||
return observableZip(...removedBistreams.map((bitstream: Bitstream) => this.bitstreamService.deleteAndReturnResponse(bitstream.id)));
|
||||
return observableZip(...removedBistreams.map((bitstream: Bitstream) => this.bitstreamService.delete(bitstream.id)));
|
||||
} else {
|
||||
return observableOf(undefined);
|
||||
}
|
||||
|
@@ -21,6 +21,7 @@ import { RelationshipService } from '../../../core/data/relationship.service';
|
||||
import { EntityTypeService } from '../../../core/data/entity-type.service';
|
||||
import { LinkService } from '../../../core/cache/builders/link.service';
|
||||
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||
import { RestResponse } from '../../../core/cache/response.models';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-item-delete',
|
||||
@@ -327,8 +328,8 @@ export class ItemDeleteComponent
|
||||
),
|
||||
).subscribe((types) => {
|
||||
this.itemDataService.delete(this.item.id, types).pipe(first()).subscribe(
|
||||
(succeeded: boolean) => {
|
||||
this.notify(succeeded);
|
||||
(response: RestResponse) => {
|
||||
this.notify(response.isSuccessful);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@@ -5,7 +5,7 @@ import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { getFinishedRemoteData, getSucceededRemoteData } from '../../../../core/shared/operators';
|
||||
import { getFirstSucceededRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
|
||||
import { hasValue } from '../../../../shared/empty.util';
|
||||
|
||||
/**
|
||||
@@ -75,16 +75,19 @@ export const paginatedRelationsToItems = (thisId: string) =>
|
||||
getSucceededRemoteData(),
|
||||
switchMap((relationshipsRD: RemoteData<PaginatedList<Relationship>>) => {
|
||||
return observableCombineLatest(
|
||||
...relationshipsRD.payload.page.map((rel: Relationship) => observableCombineLatest(rel.leftItem.pipe(getFinishedRemoteData()), rel.rightItem.pipe(getFinishedRemoteData())))
|
||||
).pipe(
|
||||
relationshipsRD.payload.page.map((rel: Relationship) =>
|
||||
observableCombineLatest([
|
||||
rel.leftItem.pipe(getFirstSucceededRemoteDataPayload()),
|
||||
rel.rightItem.pipe(getFirstSucceededRemoteDataPayload())]
|
||||
)
|
||||
)).pipe(
|
||||
map((arr) =>
|
||||
arr
|
||||
.filter(([leftItem, rightItem]) => leftItem.hasSucceeded && rightItem.hasSucceeded)
|
||||
.map(([leftItem, rightItem]) => {
|
||||
if (leftItem.payload.id === thisId) {
|
||||
return rightItem.payload;
|
||||
} else if (rightItem.payload.id === thisId) {
|
||||
return leftItem.payload;
|
||||
if (leftItem.id === thisId) {
|
||||
return rightItem;
|
||||
} else if (rightItem.id === thisId) {
|
||||
return leftItem;
|
||||
}
|
||||
})
|
||||
.filter((item: Item) => hasValue(item))
|
||||
|
@@ -7,6 +7,8 @@ import { RouteService } from '../../core/services/route.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { RequestService } from '../../core/data/request.service';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { RestResponse } from '../../core/cache/response.models';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-workflow-item-delete',
|
||||
@@ -39,6 +41,6 @@ export class WorkflowItemDeleteComponent extends WorkflowItemActionPageComponent
|
||||
*/
|
||||
sendRequest(id: string): Observable<boolean> {
|
||||
this.requestService.removeByHrefSubstring('/discover');
|
||||
return this.workflowItemService.delete(id);
|
||||
return this.workflowItemService.delete(id).pipe(map((response: RestResponse) => response.isSuccessful));
|
||||
}
|
||||
}
|
||||
|
@@ -1,13 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf, race as observableRace } from 'rxjs';
|
||||
import { distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators';
|
||||
import {
|
||||
hasValue,
|
||||
hasValueOperator,
|
||||
isEmpty,
|
||||
isNotEmpty,
|
||||
isNotUndefined
|
||||
} from '../../../shared/empty.util';
|
||||
import { hasValue, hasValueOperator, isEmpty, isNotEmpty, isNotUndefined } from '../../../shared/empty.util';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
||||
import { PaginatedList } from '../../data/paginated-list';
|
||||
@@ -15,12 +9,7 @@ import { RemoteData } from '../../data/remote-data';
|
||||
import { RemoteDataError } from '../../data/remote-data-error';
|
||||
import { RequestEntry } from '../../data/request.reducer';
|
||||
import { RequestService } from '../../data/request.service';
|
||||
import {
|
||||
filterSuccessfulResponses,
|
||||
getRequestFromRequestHref,
|
||||
getRequestFromRequestUUID,
|
||||
getResourceLinksFromResponse
|
||||
} from '../../shared/operators';
|
||||
import { filterSuccessfulResponses, getRequestFromRequestHref, getRequestFromRequestUUID, getResourceLinksFromResponse } from '../../shared/operators';
|
||||
import { PageInfo } from '../../shared/page-info.model';
|
||||
import { CacheableObject } from '../object-cache.reducer';
|
||||
import { ObjectCacheService } from '../object-cache.service';
|
||||
@@ -98,7 +87,8 @@ export class RemoteDataBuildService {
|
||||
let error: RemoteDataError;
|
||||
const response = reqEntry ? reqEntry.response : undefined;
|
||||
if (hasValue(response)) {
|
||||
isSuccessful = response.isSuccessful;
|
||||
isSuccessful = response.statusCode === 204 ||
|
||||
response.statusCode >= 200 && response.statusCode < 300 && hasValue(payload);
|
||||
const errorMessage = isSuccessful === false ? (response as ErrorResponse).errorMessage : undefined;
|
||||
if (hasValue(errorMessage)) {
|
||||
error = new RemoteDataError(
|
||||
@@ -155,7 +145,7 @@ export class RemoteDataBuildService {
|
||||
})
|
||||
);
|
||||
|
||||
const payload$ = observableCombineLatest(tDomainList$, pageInfo$).pipe(
|
||||
const payload$ = observableCombineLatest([tDomainList$, pageInfo$]).pipe(
|
||||
map(([tDomainList, pageInfo]) => {
|
||||
return new PaginatedList(pageInfo, tDomainList);
|
||||
})
|
||||
|
9
src/app/core/cache/object-cache.reducer.ts
vendored
9
src/app/core/cache/object-cache.reducer.ts
vendored
@@ -1,7 +1,5 @@
|
||||
import { autoserialize, deserialize } from 'cerialize';
|
||||
import { HALLink } from '../shared/hal-link.model';
|
||||
import { HALResource } from '../shared/hal-resource.model';
|
||||
import { excludeFromEquals } from '../utilities/equals.decorators';
|
||||
import {
|
||||
ObjectCacheAction,
|
||||
ObjectCacheActionTypes,
|
||||
@@ -15,12 +13,6 @@ import { CacheEntry } from './cache-entry';
|
||||
import { ResourceType } from '../shared/resource-type';
|
||||
import { applyPatch, Operation } from 'fast-json-patch';
|
||||
|
||||
export enum DirtyType {
|
||||
Created = 'Created',
|
||||
Updated = 'Updated',
|
||||
Deleted = 'Deleted'
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface to represent a JsonPatch
|
||||
*/
|
||||
@@ -72,6 +64,7 @@ export class ObjectCacheEntry implements CacheEntry {
|
||||
patches: Patch[] = [];
|
||||
isDirty: boolean;
|
||||
}
|
||||
|
||||
/* tslint:enable:max-classes-per-file */
|
||||
|
||||
/**
|
||||
|
@@ -9,7 +9,7 @@ import { RestRequestMethod } from '../data/rest-request-method';
|
||||
|
||||
/**
|
||||
* An entry in the ServerSyncBufferState
|
||||
* href: unique href of an ObjectCacheEntry
|
||||
* href: unique href of an ServerSyncBufferEntry
|
||||
* method: RestRequestMethod type
|
||||
*/
|
||||
export class ServerSyncBufferEntry {
|
||||
@@ -48,6 +48,7 @@ export function serverSyncBufferReducer(state = initialState, action: ServerSync
|
||||
case ServerSyncBufferActionTypes.EMPTY: {
|
||||
return emptyServerSyncQueue(state, action as EmptySSBAction);
|
||||
}
|
||||
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
|
@@ -177,6 +177,7 @@ describe('ConfigResponseParsingService', () => {
|
||||
Object.assign(new SubmissionDefinitionModel(), {
|
||||
isDefault: true,
|
||||
name: 'traditional',
|
||||
id: 'traditional',
|
||||
type: 'submissiondefinition',
|
||||
_links: {
|
||||
sections: { href: 'https://rest.api/config/submissiondefinitions/traditional/sections' },
|
||||
@@ -187,6 +188,7 @@ describe('ConfigResponseParsingService', () => {
|
||||
header: 'submit.progressbar.describe.stepone',
|
||||
mandatory: true,
|
||||
sectionType: 'submission-form',
|
||||
id: 'traditionalpageone',
|
||||
visibility: {
|
||||
main: null,
|
||||
other: 'READONLY'
|
||||
@@ -201,6 +203,7 @@ describe('ConfigResponseParsingService', () => {
|
||||
header: 'submit.progressbar.describe.steptwo',
|
||||
mandatory: true,
|
||||
sectionType: 'submission-form',
|
||||
id: 'traditionalpagetwo',
|
||||
visibility: {
|
||||
main: null,
|
||||
other: 'READONLY'
|
||||
@@ -215,6 +218,7 @@ describe('ConfigResponseParsingService', () => {
|
||||
header: 'submit.progressbar.upload',
|
||||
mandatory: false,
|
||||
sectionType: 'upload',
|
||||
id: 'upload',
|
||||
visibility: {
|
||||
main: null,
|
||||
other: 'READONLY'
|
||||
@@ -229,6 +233,7 @@ describe('ConfigResponseParsingService', () => {
|
||||
header: 'submit.progressbar.license',
|
||||
mandatory: true,
|
||||
sectionType: 'license',
|
||||
id: 'license',
|
||||
visibility: {
|
||||
main: null,
|
||||
other: 'READONLY'
|
||||
|
@@ -6,6 +6,12 @@ import { excludeFromEquals } from '../../utilities/equals.decorators';
|
||||
|
||||
export abstract class ConfigObject implements CacheableObject {
|
||||
|
||||
/**
|
||||
* The name for this configuration
|
||||
*/
|
||||
@autoserialize
|
||||
public id: string;
|
||||
|
||||
/**
|
||||
* The name for this configuration
|
||||
*/
|
||||
|
@@ -281,7 +281,7 @@ describe('BitstreamFormatDataService', () => {
|
||||
format.uuid = 'format-uuid';
|
||||
format.id = 'format-id';
|
||||
|
||||
const expected = cold('(b|)', {b: true});
|
||||
const expected = cold('(b|)', { b: responseCacheEntry.response });
|
||||
const result = service.delete(format.id);
|
||||
|
||||
expect(result).toBeObservable(expected);
|
||||
|
@@ -155,9 +155,9 @@ export class BitstreamFormatDataService extends DataService<BitstreamFormat> {
|
||||
/**
|
||||
* Delete an existing DSpace Object on the server
|
||||
* @param formatID The DSpace Object'id to be removed
|
||||
* Return an observable that emits true when the deletion was successful, false when it failed
|
||||
* @return the RestResponse as an Observable
|
||||
*/
|
||||
delete(formatID: string): Observable<boolean> {
|
||||
delete(formatID: string): Observable<RestResponse> {
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
|
||||
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
|
||||
@@ -173,7 +173,7 @@ export class BitstreamFormatDataService extends DataService<BitstreamFormat> {
|
||||
|
||||
return this.requestService.getByUUID(requestId).pipe(
|
||||
find((request: RequestEntry) => request.completed),
|
||||
map((request: RequestEntry) => request.response.isSuccessful)
|
||||
map((request: RequestEntry) => request.response)
|
||||
);
|
||||
}
|
||||
|
||||
|
94
src/app/core/data/bundle-data.service.spec.ts
Normal file
94
src/app/core/data/bundle-data.service.spec.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { compare, Operation } from 'fast-json-patch';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { SortDirection, SortOptions } from '../cache/models/sort-options.model';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { Item } from '../shared/item.model';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { ChangeAnalyzer } from './change-analyzer';
|
||||
import { DataService } from './data.service';
|
||||
import { FindListOptions, PatchRequest } from './request.models';
|
||||
import { RequestService } from './request.service';
|
||||
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
||||
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
||||
import { BundleDataService } from './bundle-data.service';
|
||||
import { HALLink } from '../shared/hal-link.model';
|
||||
|
||||
class DummyChangeAnalyzer implements ChangeAnalyzer<Item> {
|
||||
diff(object1: Item, object2: Item): Operation[] {
|
||||
return compare((object1 as any).metadata, (object2 as any).metadata);
|
||||
}
|
||||
}
|
||||
|
||||
describe('BundleDataService', () => {
|
||||
let service: BundleDataService;
|
||||
let requestService;
|
||||
let halService;
|
||||
let rdbService;
|
||||
let notificationsService;
|
||||
let http;
|
||||
let comparator;
|
||||
let objectCache;
|
||||
let store;
|
||||
let item;
|
||||
let bundleLink;
|
||||
let bundleHALLink;
|
||||
|
||||
function initTestService(): BundleDataService {
|
||||
bundleLink = '/items/0fdc0cd7-ff8c-433d-b33c-9b56108abc07/bundles';
|
||||
bundleHALLink = new HALLink();
|
||||
bundleHALLink.href = bundleLink;
|
||||
item = new Item();
|
||||
item._links = {
|
||||
bundles: bundleHALLink
|
||||
};
|
||||
requestService = getMockRequestService();
|
||||
halService = new HALEndpointServiceStub('url') as any;
|
||||
rdbService = {} as RemoteDataBuildService;
|
||||
notificationsService = {} as NotificationsService;
|
||||
http = {} as HttpClient;
|
||||
comparator = new DummyChangeAnalyzer() as any;
|
||||
objectCache = {
|
||||
|
||||
addPatch: () => {
|
||||
/* empty */
|
||||
},
|
||||
getObjectBySelfLink: () => {
|
||||
/* empty */
|
||||
}
|
||||
} as any;
|
||||
store = {} as Store<CoreState>;
|
||||
return new BundleDataService(
|
||||
requestService,
|
||||
rdbService,
|
||||
store,
|
||||
objectCache,
|
||||
halService,
|
||||
notificationsService,
|
||||
http,
|
||||
comparator,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
service = initTestService();
|
||||
});
|
||||
|
||||
describe('findAllByItem', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(service, 'findAllByHref');
|
||||
service.findAllByItem(item);
|
||||
});
|
||||
|
||||
it('should call findAllByHref with the item\'s bundles link', () => {
|
||||
expect(service.findAllByHref).toHaveBeenCalledWith(bundleLink, undefined);
|
||||
})
|
||||
});
|
||||
});
|
@@ -73,7 +73,12 @@ describe('CollectionDataService', () => {
|
||||
|
||||
describe('when the requests are successful', () => {
|
||||
beforeEach(() => {
|
||||
createService();
|
||||
createService(observableOf({
|
||||
request: {
|
||||
href: 'https://rest.api/request'
|
||||
},
|
||||
completed: true
|
||||
}));
|
||||
});
|
||||
|
||||
describe('when calling getContentSource', () => {
|
||||
@@ -133,7 +138,7 @@ describe('CollectionDataService', () => {
|
||||
});
|
||||
|
||||
it('should return a RemoteData<PaginatedList<Colletion>> for the getAuthorizedCollection', () => {
|
||||
const result = service.getAuthorizedCollection(queryString)
|
||||
const result = service.getAuthorizedCollection(queryString);
|
||||
const expected = cold('a|', {
|
||||
a: paginatedListRD
|
||||
});
|
||||
@@ -148,7 +153,7 @@ describe('CollectionDataService', () => {
|
||||
});
|
||||
|
||||
it('should return a RemoteData<PaginatedList<Colletion>> for the getAuthorizedCollectionByCommunity', () => {
|
||||
const result = service.getAuthorizedCollectionByCommunity(communityId, queryString)
|
||||
const result = service.getAuthorizedCollectionByCommunity(communityId, queryString);
|
||||
const expected = cold('a|', {
|
||||
a: paginatedListRD
|
||||
});
|
||||
|
@@ -2,19 +2,8 @@ import { HttpClient } from '@angular/common/http';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import { Observable } from 'rxjs';
|
||||
import {
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
find,
|
||||
first,
|
||||
map,
|
||||
mergeMap,
|
||||
skipWhile,
|
||||
switchMap,
|
||||
take,
|
||||
tap
|
||||
} from 'rxjs/operators';
|
||||
import { hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
|
||||
import { distinctUntilChanged, filter, find, first, map, mergeMap, switchMap, take } from 'rxjs/operators';
|
||||
import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
|
||||
import { NotificationOptions } from '../../shared/notifications/models/notification-options.model';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
@@ -28,25 +17,12 @@ import { CoreState } from '../core.reducers';
|
||||
import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer';
|
||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import {
|
||||
configureRequest,
|
||||
getRemoteDataPayload,
|
||||
getResponseFromEntry,
|
||||
getSucceededRemoteData
|
||||
} from '../shared/operators';
|
||||
import { configureRequest, getRemoteDataPayload, getResponseFromEntry, getSucceededRemoteData } from '../shared/operators';
|
||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||
import { ChangeAnalyzer } from './change-analyzer';
|
||||
import { PaginatedList } from './paginated-list';
|
||||
import { RemoteData } from './remote-data';
|
||||
import {
|
||||
CreateRequest,
|
||||
DeleteByIDRequest,
|
||||
FindByIDRequest,
|
||||
FindListOptions,
|
||||
FindListRequest,
|
||||
GetRequest,
|
||||
PatchRequest, PutRequest
|
||||
} from './request.models';
|
||||
import { CreateRequest, DeleteByIDRequest, FindByIDRequest, FindListOptions, FindListRequest, GetRequest, PatchRequest, PutRequest } from './request.models';
|
||||
import { RequestEntry } from './request.reducer';
|
||||
import { RequestService } from './request.service';
|
||||
import { RestRequestMethod } from './rest-request-method';
|
||||
@@ -353,26 +329,24 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
|
||||
* Return an observable that emits response from the server
|
||||
*/
|
||||
searchBy(searchMethod: string, options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<T>>): Observable<RemoteData<PaginatedList<T>>> {
|
||||
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
const hrefObs = this.getSearchByHref(searchMethod, options, ...linksToFollow);
|
||||
|
||||
return hrefObs.pipe(
|
||||
find((href: string) => hasValue(href)),
|
||||
tap((href: string) => {
|
||||
this.requestService.removeByHrefSubstring(href);
|
||||
const request = new FindListRequest(this.requestService.generateRequestId(), href, options);
|
||||
if (hasValue(this.responseMsToLive)) {
|
||||
request.responseMsToLive = this.responseMsToLive;
|
||||
}
|
||||
hrefObs.pipe(
|
||||
find((href: string) => hasValue(href))
|
||||
).subscribe((href: string) => {
|
||||
const request = new FindListRequest(requestId, href, options);
|
||||
if (hasValue(this.responseMsToLive)) {
|
||||
request.responseMsToLive = this.responseMsToLive;
|
||||
}
|
||||
this.requestService.configure(request);
|
||||
});
|
||||
|
||||
this.requestService.configure(request);
|
||||
}
|
||||
return this.requestService.getByUUID(requestId).pipe(
|
||||
find((requestEntry) => hasValue(requestEntry) && requestEntry.completed),
|
||||
switchMap((requestEntry) =>
|
||||
this.rdbService.buildList<T>(requestEntry.request.href, ...linksToFollow)
|
||||
),
|
||||
switchMap((href) => this.requestService.getByHref(href)),
|
||||
skipWhile((requestEntry) => hasValue(requestEntry) && requestEntry.completed),
|
||||
switchMap((href) =>
|
||||
this.rdbService.buildList<T>(hrefObs, ...linksToFollow) as Observable<RemoteData<PaginatedList<T>>>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -391,6 +365,9 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
|
||||
find((href: string) => hasValue(href)),
|
||||
map((href: string) => {
|
||||
const request = new PatchRequest(requestId, href, operations);
|
||||
if (hasValue(this.responseMsToLive)) {
|
||||
request.responseMsToLive = this.responseMsToLive;
|
||||
}
|
||||
this.requestService.configure(request);
|
||||
})
|
||||
).subscribe();
|
||||
@@ -464,7 +441,13 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
|
||||
|
||||
const request$ = endpoint$.pipe(
|
||||
take(1),
|
||||
map((endpoint: string) => new CreateRequest(requestId, endpoint, JSON.stringify(serializedDso)))
|
||||
map((endpoint: string) => {
|
||||
const request = new CreateRequest(requestId, endpoint, JSON.stringify(serializedDso));
|
||||
if (hasValue(this.responseMsToLive)) {
|
||||
request.responseMsToLive = this.responseMsToLive;
|
||||
}
|
||||
return request
|
||||
})
|
||||
);
|
||||
|
||||
// Execute the post request
|
||||
@@ -513,7 +496,13 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
|
||||
|
||||
const request$ = endpoint$.pipe(
|
||||
take(1),
|
||||
map((endpoint: string) => new CreateRequest(requestId, endpoint, JSON.stringify(serializedDso)))
|
||||
map((endpoint: string) => {
|
||||
const request = new CreateRequest(requestId, endpoint, JSON.stringify(serializedDso));
|
||||
if (hasValue(this.responseMsToLive)) {
|
||||
request.responseMsToLive = this.responseMsToLive;
|
||||
}
|
||||
return request
|
||||
})
|
||||
);
|
||||
|
||||
// Execute the post request
|
||||
@@ -542,42 +531,9 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
|
||||
* @param dsoID The DSpace Object' id to be removed
|
||||
* @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual
|
||||
* metadata should be saved as real metadata
|
||||
* @return an observable that emits true when the deletion was successful, false when it failed
|
||||
* @return the RestResponse as an Observable
|
||||
*/
|
||||
delete(dsoID: string, copyVirtualMetadata?: string[]): Observable<boolean> {
|
||||
const requestId = this.deleteAndReturnRequestId(dsoID, copyVirtualMetadata);
|
||||
|
||||
return this.requestService.getByUUID(requestId).pipe(
|
||||
find((request: RequestEntry) => isNotEmpty(request) && request.completed),
|
||||
map((request: RequestEntry) => request.response.isSuccessful)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an existing DSpace Object on the server
|
||||
* @param dsoID The DSpace Object' id to be removed
|
||||
* @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual
|
||||
* metadata should be saved as real metadata
|
||||
* Return an observable of the completed response
|
||||
*/
|
||||
deleteAndReturnResponse(dsoID: string, copyVirtualMetadata?: string[]): Observable<RestResponse> {
|
||||
const requestId = this.deleteAndReturnRequestId(dsoID, copyVirtualMetadata);
|
||||
|
||||
return this.requestService.getByUUID(requestId).pipe(
|
||||
hasValueOperator(),
|
||||
find((request: RequestEntry) => request.completed),
|
||||
map((request: RequestEntry) => request.response)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an existing DSpace Object on the server
|
||||
* @param dsoID The DSpace Object' id to be removed
|
||||
* @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual
|
||||
* metadata should be saved as real metadata
|
||||
* Return the delete request's ID
|
||||
*/
|
||||
private deleteAndReturnRequestId(dsoID: string, copyVirtualMetadata?: string[]): string {
|
||||
delete(dsoID: string, copyVirtualMetadata?: string[]): Observable<RestResponse> {
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
|
||||
const hrefObs = this.getIDHrefObs(dsoID);
|
||||
@@ -593,11 +549,17 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
|
||||
);
|
||||
}
|
||||
const request = new DeleteByIDRequest(requestId, href, dsoID);
|
||||
if (hasValue(this.responseMsToLive)) {
|
||||
request.responseMsToLive = this.responseMsToLive;
|
||||
}
|
||||
this.requestService.configure(request);
|
||||
})
|
||||
).subscribe();
|
||||
|
||||
return requestId;
|
||||
return this.requestService.getByUUID(requestId).pipe(
|
||||
find((request: RequestEntry) => request.completed),
|
||||
map((request: RequestEntry) => request.response)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -608,4 +570,15 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
|
||||
this.requestService.commit(method);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the links to traverse from the root of the api to the
|
||||
* endpoint this DataService represents
|
||||
*
|
||||
* e.g. if the api root links to 'foo', and the endpoint at 'foo'
|
||||
* links to 'bar' the linkPath for the BarDataService would be
|
||||
* 'foo/bar'
|
||||
*/
|
||||
getLinkPath(): string {
|
||||
return this.linkPath;
|
||||
}
|
||||
}
|
||||
|
@@ -16,9 +16,10 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { BrowseService } from '../browse/browse.service';
|
||||
import { CollectionDataService } from './collection-data.service';
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
import { switchMap, map } from 'rxjs/operators';
|
||||
import { BundleDataService } from './bundle-data.service';
|
||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
import { RestResponse } from '../cache/response.models';
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
/**
|
||||
@@ -121,7 +122,7 @@ class DataServiceImpl extends ItemDataService {
|
||||
*/
|
||||
deleteByCollectionID(item: Item, collectionID: string): Observable<boolean> {
|
||||
this.setRegularEndpoint();
|
||||
return super.delete(item.uuid);
|
||||
return super.delete(item.uuid).pipe(map((response: RestResponse) => response.isSuccessful));
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -63,6 +63,7 @@ export class LookupRelationService {
|
||||
concat(subject.pipe(take(1)))
|
||||
)
|
||||
) as any
|
||||
,
|
||||
) as Observable<RemoteData<PaginatedList<SearchResult<Item>>>>;
|
||||
}
|
||||
|
||||
|
@@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { combineLatest as observableCombineLatest } from 'rxjs';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { filter, find, map, switchMap } from 'rxjs/operators';
|
||||
import { filter, find, map, mergeMap, switchMap } from 'rxjs/operators';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { isNotUndefined } from '../../shared/empty.util';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
@@ -76,7 +76,7 @@ export class RelationshipTypeService extends DataService<RelationshipType> {
|
||||
getSucceededRemoteData(),
|
||||
/* Flatten the page so we can treat it like an observable */
|
||||
switchMap((typeListRD: RemoteData<PaginatedList<RelationshipType>>) => typeListRD.payload.page),
|
||||
switchMap((type: RelationshipType) => {
|
||||
mergeMap((type: RelationshipType) => {
|
||||
if (type.leftwardType === label) {
|
||||
return this.checkType(type, firstType, secondType);
|
||||
} else if (type.rightwardType === label) {
|
||||
@@ -92,7 +92,7 @@ export class RelationshipTypeService extends DataService<RelationshipType> {
|
||||
// returns a void observable if there's not match
|
||||
// returns an observable that emits the relationship type when there is a match
|
||||
private checkType(type: RelationshipType, firstType: string, secondType: string): Observable<RelationshipType> {
|
||||
const entityTypes = observableCombineLatest(type.leftType.pipe(getSucceededRemoteData()), type.rightType.pipe(getSucceededRemoteData()));
|
||||
const entityTypes = observableCombineLatest([type.leftType.pipe(getSucceededRemoteData()), type.rightType.pipe(getSucceededRemoteData())]);
|
||||
return entityTypes.pipe(
|
||||
find(([leftTypeRD, rightTypeRD]: [RemoteData<ItemType>, RemoteData<ItemType>]) => leftTypeRD.payload.label === firstType && rightTypeRD.payload.label === secondType),
|
||||
filter((types) => isNotUndefined(types)),
|
||||
|
@@ -1,11 +1,6 @@
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||
import * as ItemRelationshipsUtils from '../../+item-page/simple/item-types/shared/item-relationships-utils';
|
||||
import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/remote-data-build.service.mock';
|
||||
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
||||
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { spyOnOperator } from '../../shared/testing/utils.test';
|
||||
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { RelationshipType } from '../shared/item-relationships/relationship-type.model';
|
||||
@@ -13,11 +8,16 @@ import { Relationship } from '../shared/item-relationships/relationship.model';
|
||||
import { Item } from '../shared/item.model';
|
||||
import { PageInfo } from '../shared/page-info.model';
|
||||
import { PaginatedList } from './paginated-list';
|
||||
import { RelationshipService } from './relationship.service';
|
||||
import { RemoteData } from './remote-data';
|
||||
import { DeleteRequest, FindListOptions } from './request.models';
|
||||
import { RequestEntry } from './request.reducer';
|
||||
import { RelationshipService } from './relationship.service';
|
||||
import { RequestService } from './request.service';
|
||||
import { RemoteData } from './remote-data';
|
||||
import { RequestEntry } from './request.reducer';
|
||||
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/remote-data-build.service.mock';
|
||||
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
||||
import { spyOnOperator } from '../../shared/testing/utils.test';
|
||||
|
||||
describe('RelationshipService', () => {
|
||||
let service: RelationshipService;
|
||||
@@ -159,8 +159,8 @@ describe('RelationshipService', () => {
|
||||
it('should clear the cache of the related items', () => {
|
||||
expect(objectCache.remove).toHaveBeenCalledWith(relatedItem1._links.self.href);
|
||||
expect(objectCache.remove).toHaveBeenCalledWith(item._links.self.href);
|
||||
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(relatedItem1.self);
|
||||
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(item.self);
|
||||
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(relatedItem1.uuid);
|
||||
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(item.uuid);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -175,37 +175,6 @@ describe('RelationshipService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRelatedItems', () => {
|
||||
let mockItem;
|
||||
|
||||
beforeEach(() => {
|
||||
mockItem = { uuid: 'someid' } as Item;
|
||||
|
||||
spyOn(service, 'getItemRelationshipsArray').and.returnValue(observableOf(relationships));
|
||||
|
||||
spyOnOperator(ItemRelationshipsUtils, 'relationsToItems').and.returnValue((v) => v);
|
||||
});
|
||||
|
||||
it('should call getItemRelationshipsArray with the correct params', (done) => {
|
||||
service.getRelatedItems(mockItem).subscribe(() => {
|
||||
expect(service.getItemRelationshipsArray).toHaveBeenCalledWith(
|
||||
mockItem,
|
||||
followLink('leftItem'),
|
||||
followLink('rightItem'),
|
||||
followLink('relationshipType')
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should use the relationsToItems operator', (done) => {
|
||||
service.getRelatedItems(mockItem).subscribe(() => {
|
||||
expect(ItemRelationshipsUtils.relationsToItems).toHaveBeenCalledWith(mockItem.uuid);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRelatedItemsByLabel', () => {
|
||||
let relationsList;
|
||||
let mockItem;
|
||||
@@ -258,7 +227,6 @@ describe('RelationshipService', () => {
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
function getRemotedataObservable(obj: any): Observable<RemoteData<any>> {
|
||||
|
@@ -1,21 +1,14 @@
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MemoizedSelector, select, Store } from '@ngrx/store';
|
||||
import { combineLatest, combineLatest as observableCombineLatest } from 'rxjs';
|
||||
import { combineLatest as observableCombineLatest } from 'rxjs';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { distinctUntilChanged, filter, map, mergeMap, startWith, switchMap, take, tap } from 'rxjs/operators';
|
||||
import {
|
||||
compareArraysUsingIds,
|
||||
paginatedRelationsToItems,
|
||||
relationsToItems
|
||||
} from '../../+item-page/simple/item-types/shared/item-relationships-utils';
|
||||
import { compareArraysUsingIds, paginatedRelationsToItems, relationsToItems } from '../../+item-page/simple/item-types/shared/item-relationships-utils';
|
||||
import { AppState, keySelector } from '../../app.reducer';
|
||||
import { hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
|
||||
import { ReorderableRelationship } from '../../shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component';
|
||||
import {
|
||||
RemoveNameVariantAction,
|
||||
SetNameVariantAction
|
||||
} from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.actions';
|
||||
import { RemoveNameVariantAction, SetNameVariantAction } from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.actions';
|
||||
import { NameVariantListState } from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
@@ -31,12 +24,7 @@ import { RelationshipType } from '../shared/item-relationships/relationship-type
|
||||
import { Relationship } from '../shared/item-relationships/relationship.model';
|
||||
import { RELATIONSHIP } from '../shared/item-relationships/relationship.resource-type';
|
||||
import { Item } from '../shared/item.model';
|
||||
import {
|
||||
configureRequest,
|
||||
getRemoteDataPayload,
|
||||
getResponseFromEntry,
|
||||
getSucceededRemoteData
|
||||
} from '../shared/operators';
|
||||
import { configureRequest, getFirstSucceededRemoteDataPayload, getRemoteDataPayload, getResponseFromEntry, getSucceededRemoteData } from '../shared/operators';
|
||||
import { DataService } from './data.service';
|
||||
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||
import { ItemDataService } from './item-data.service';
|
||||
@@ -55,6 +43,19 @@ const relationshipStateSelector = (listID: string, itemID: string): MemoizedSele
|
||||
return keySelector<string>(itemID, relationshipListStateSelector(listID));
|
||||
};
|
||||
|
||||
/**
|
||||
* Return true if the Item in the payload of the source observable matches
|
||||
* the given Item by UUID
|
||||
*
|
||||
* @param itemCheck the Item to compare with
|
||||
*/
|
||||
const compareItemsByUUID = (itemCheck: Item) =>
|
||||
(source: Observable<RemoteData<Item>>): Observable<boolean> =>
|
||||
source.pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
map((item: Item) => item.uuid === itemCheck.uuid)
|
||||
);
|
||||
|
||||
/**
|
||||
* The service handling all relationship requests
|
||||
*/
|
||||
@@ -62,6 +63,7 @@ const relationshipStateSelector = (listID: string, itemID: string): MemoizedSele
|
||||
@dataService(RELATIONSHIP)
|
||||
export class RelationshipService extends DataService<Relationship> {
|
||||
protected linkPath = 'relationships';
|
||||
protected responseMsToLive = 15 * 60 * 1000;
|
||||
|
||||
constructor(protected itemService: ItemDataService,
|
||||
protected requestService: RequestService,
|
||||
@@ -101,11 +103,7 @@ export class RelationshipService extends DataService<Relationship> {
|
||||
configureRequest(this.requestService),
|
||||
switchMap((restRequest: RestRequest) => this.requestService.getByUUID(restRequest.uuid)),
|
||||
getResponseFromEntry(),
|
||||
switchMap((response) =>
|
||||
this.clearRelatedCache(id).pipe(
|
||||
map(() => response),
|
||||
)
|
||||
),
|
||||
tap(() => this.refreshRelationshipItemsInCacheByRelationship(id)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -132,8 +130,8 @@ export class RelationshipService extends DataService<Relationship> {
|
||||
configureRequest(this.requestService),
|
||||
switchMap((restRequest: RestRequest) => this.requestService.getByUUID(restRequest.uuid)),
|
||||
getResponseFromEntry(),
|
||||
tap(() => this.removeRelationshipItemsFromCache(item1)),
|
||||
tap(() => this.removeRelationshipItemsFromCache(item2))
|
||||
tap(() => this.refreshRelationshipItemsInCache(item1)),
|
||||
tap(() => this.refreshRelationshipItemsInCache(item2))
|
||||
) as Observable<RestResponse>;
|
||||
}
|
||||
|
||||
@@ -141,19 +139,19 @@ export class RelationshipService extends DataService<Relationship> {
|
||||
* Method to remove two items of a relationship from the cache using the identifier of the relationship
|
||||
* @param relationshipId The identifier of the relationship
|
||||
*/
|
||||
private removeRelationshipItemsFromCacheByRelationship(relationshipId: string) {
|
||||
this.findById(relationshipId).pipe(
|
||||
private refreshRelationshipItemsInCacheByRelationship(relationshipId: string) {
|
||||
this.findById(relationshipId, followLink('leftItem'), followLink('rightItem')).pipe(
|
||||
getSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
switchMap((rel: Relationship) => combineLatest(
|
||||
switchMap((rel: Relationship) => observableCombineLatest(
|
||||
rel.leftItem.pipe(getSucceededRemoteData(), getRemoteDataPayload()),
|
||||
rel.rightItem.pipe(getSucceededRemoteData(), getRemoteDataPayload())
|
||||
)
|
||||
),
|
||||
take(1)
|
||||
).subscribe(([item1, item2]) => {
|
||||
this.removeRelationshipItemsFromCache(item1);
|
||||
this.removeRelationshipItemsFromCache(item2);
|
||||
this.refreshRelationshipItemsInCache(item1);
|
||||
this.refreshRelationshipItemsInCache(item2);
|
||||
})
|
||||
}
|
||||
|
||||
@@ -161,13 +159,13 @@ export class RelationshipService extends DataService<Relationship> {
|
||||
* Method to remove an item that's part of a relationship from the cache
|
||||
* @param item The item to remove from the cache
|
||||
*/
|
||||
private removeRelationshipItemsFromCache(item) {
|
||||
public refreshRelationshipItemsInCache(item) {
|
||||
this.objectCache.remove(item._links.self.href);
|
||||
this.requestService.removeByHrefSubstring(item.uuid);
|
||||
combineLatest(
|
||||
observableCombineLatest([
|
||||
this.objectCache.hasBySelfLinkObservable(item._links.self.href),
|
||||
this.requestService.hasByHrefObservable(item.uuid)
|
||||
).pipe(
|
||||
this.requestService.hasByHrefObservable(item.self)
|
||||
]).pipe(
|
||||
filter(([existsInOC, existsInRC]) => !existsInOC && !existsInRC),
|
||||
take(1),
|
||||
switchMap(() => this.itemService.findByHref(item._links.self.href).pipe(take(1)))
|
||||
@@ -176,7 +174,10 @@ export class RelationshipService extends DataService<Relationship> {
|
||||
|
||||
/**
|
||||
* Get an item's relationships in the form of an array
|
||||
* @param item
|
||||
*
|
||||
* @param item The {@link Item} to get {@link Relationship}s for
|
||||
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s
|
||||
* should be automatically resolved
|
||||
*/
|
||||
getItemRelationshipsArray(item: Item, ...linksToFollow: Array<FollowLinkConfig<Relationship>>): Observable<Relationship[]> {
|
||||
return this.findAllByHref(item._links.relationships.href, undefined, ...linksToFollow).pipe(
|
||||
@@ -275,10 +276,10 @@ export class RelationshipService extends DataService<Relationship> {
|
||||
getRelationshipsByRelatedItemIds(item: Item, uuids: string[]): Observable<Relationship[]> {
|
||||
return this.getItemRelationshipsArray(item, followLink('leftItem'), followLink('rightItem')).pipe(
|
||||
switchMap((relationships: Relationship[]) => {
|
||||
return observableCombineLatest(...relationships.map((relationship: Relationship) => {
|
||||
return observableCombineLatest(relationships.map((relationship: Relationship) => {
|
||||
const isLeftItem$ = this.isItemInUUIDArray(relationship.leftItem, uuids);
|
||||
const isRightItem$ = this.isItemInUUIDArray(relationship.rightItem, uuids);
|
||||
return observableCombineLatest(isLeftItem$, isRightItem$).pipe(
|
||||
return observableCombineLatest([isLeftItem$, isRightItem$]).pipe(
|
||||
filter(([isLeftItem, isRightItem]) => isLeftItem || isRightItem),
|
||||
map(() => relationship),
|
||||
startWith(undefined)
|
||||
@@ -304,34 +305,31 @@ export class RelationshipService extends DataService<Relationship> {
|
||||
* @param label The rightward or leftward type of the relationship
|
||||
*/
|
||||
getRelationshipByItemsAndLabel(item1: Item, item2: Item, label: string, options?: FindListOptions): Observable<Relationship> {
|
||||
return this.getItemRelationshipsByLabel(item1, label, options, followLink('relationshipType'), followLink('leftItem'), followLink('rightItem'))
|
||||
.pipe(
|
||||
return this.getItemRelationshipsByLabel(
|
||||
item1,
|
||||
label,
|
||||
options,
|
||||
followLink('relationshipType'),
|
||||
followLink('leftItem'),
|
||||
followLink('rightItem')
|
||||
).pipe(
|
||||
getSucceededRemoteData(),
|
||||
isNotEmptyOperator(),
|
||||
map((relationshipListRD: RemoteData<PaginatedList<Relationship>>) => relationshipListRD.payload.page),
|
||||
mergeMap((relationships: Relationship[]) => {
|
||||
return observableCombineLatest(...relationships.map((relationship: Relationship) => {
|
||||
return observableCombineLatest(
|
||||
this.isItemMatchWithItemRD(relationship.leftItem, item2),
|
||||
this.isItemMatchWithItemRD(relationship.rightItem, item2)
|
||||
).pipe(
|
||||
map(([isLeftItem, isRightItem]) => isLeftItem || isRightItem),
|
||||
map((isMatch) => isMatch ? relationship : undefined)
|
||||
);
|
||||
}))
|
||||
// the mergemap below will emit all elements of the list as separate events
|
||||
mergeMap((relationshipListRD: RemoteData<PaginatedList<Relationship>>) => relationshipListRD.payload.page),
|
||||
mergeMap((relationship: Relationship) => {
|
||||
return observableCombineLatest([
|
||||
this.itemService.findByHref(relationship._links.leftItem.href).pipe(compareItemsByUUID(item2)),
|
||||
this.itemService.findByHref(relationship._links.rightItem.href).pipe(compareItemsByUUID(item2))
|
||||
]).pipe(
|
||||
map(([isLeftItem, isRightItem]) => isLeftItem || isRightItem),
|
||||
map((isMatch) => isMatch ? relationship : undefined)
|
||||
);
|
||||
}),
|
||||
map((relationships: Relationship[]) => relationships.find(((relationship) => hasValue(relationship))))
|
||||
filter((relationship) => hasValue(relationship)),
|
||||
take(1)
|
||||
)
|
||||
}
|
||||
|
||||
private isItemMatchWithItemRD(itemRD$: Observable<RemoteData<Item>>, itemCheck: Item): Observable<boolean> {
|
||||
return itemRD$.pipe(
|
||||
getSucceededRemoteData(),
|
||||
map((itemRD: RemoteData<Item>) => itemRD.payload),
|
||||
map((item: Item) => item.uuid === itemCheck.uuid)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to set the name variant for specific list and item
|
||||
* @param listID The list for which to save the name variant
|
||||
@@ -378,7 +376,7 @@ export class RelationshipService extends DataService<Relationship> {
|
||||
* @param nameVariant The name variant to set for the matching relationship
|
||||
*/
|
||||
public updateNameVariant(item1: Item, item2: Item, relationshipLabel: string, nameVariant: string): Observable<RemoteData<Relationship>> {
|
||||
const update$: Observable<RemoteData<Relationship>> = this.getRelationshipByItemsAndLabel(item1, item2, relationshipLabel)
|
||||
return this.getRelationshipByItemsAndLabel(item1, item2, relationshipLabel)
|
||||
.pipe(
|
||||
switchMap((relation: Relationship) =>
|
||||
relation.relationshipType.pipe(
|
||||
@@ -400,16 +398,6 @@ export class RelationshipService extends DataService<Relationship> {
|
||||
return this.update(updatedRelationship);
|
||||
}),
|
||||
);
|
||||
|
||||
update$.pipe(
|
||||
filter((relationshipRD: RemoteData<Relationship>) => relationshipRD.state === RemoteDataState.RequestPending),
|
||||
take(1),
|
||||
).subscribe(() => {
|
||||
this.removeRelationshipItemsFromCache(item1);
|
||||
this.removeRelationshipItemsFromCache(item2);
|
||||
});
|
||||
|
||||
return update$
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -432,7 +420,7 @@ export class RelationshipService extends DataService<Relationship> {
|
||||
take(1),
|
||||
).subscribe((relationshipRD: RemoteData<Relationship>) => {
|
||||
if (relationshipRD.state === RemoteDataState.ResponsePending) {
|
||||
this.removeRelationshipItemsFromCacheByRelationship(reoRel.relationship.id);
|
||||
this.refreshRelationshipItemsInCacheByRelationship(reoRel.relationship.id);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -440,18 +428,11 @@ export class RelationshipService extends DataService<Relationship> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear object and request caches of the items related to a relationship (left and right items)
|
||||
* @param uuid The uuid of the relationship for which to clear the related items from the cache
|
||||
* Patch isn't supported on the relationship endpoint, so use put instead.
|
||||
*
|
||||
* @param object the {@link Relationship} to update
|
||||
*/
|
||||
clearRelatedCache(uuid: string): Observable<void> {
|
||||
return this.findById(uuid).pipe(
|
||||
getSucceededRemoteData(),
|
||||
map((rd: RemoteData<Relationship>) => {
|
||||
this.objectCache.remove(rd.payload._links.leftItem.href);
|
||||
this.objectCache.remove(rd.payload._links.rightItem.href);
|
||||
this.requestService.removeByHrefSubstring(rd.payload._links.leftItem.href);
|
||||
this.requestService.removeByHrefSubstring(rd.payload._links.rightItem.href);
|
||||
})
|
||||
);
|
||||
update(object: Relationship): Observable<RemoteData<Relationship>> {
|
||||
return this.put(object);
|
||||
}
|
||||
}
|
||||
|
@@ -224,7 +224,7 @@ export class EPersonDataService extends DataService<EPerson> {
|
||||
* @param ePerson The EPerson to delete
|
||||
*/
|
||||
public deleteEPerson(ePerson: EPerson): Observable<boolean> {
|
||||
return this.delete(ePerson.id);
|
||||
return this.delete(ePerson.id).pipe(map((response: RestResponse) => response.isSuccessful));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -135,7 +135,7 @@ export class GroupDataService extends DataService<Group> {
|
||||
* @param id The group id to delete
|
||||
*/
|
||||
public deleteGroup(group: Group): Observable<boolean> {
|
||||
return this.delete(group.id);
|
||||
return this.delete(group.id).pipe(map((response: RestResponse) => response.isSuccessful));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,13 +1,9 @@
|
||||
import { Store } from '@ngrx/store';
|
||||
import { CoreState } from '../../core.reducers';
|
||||
import {
|
||||
NewPatchAddOperationAction,
|
||||
NewPatchRemoveOperationAction,
|
||||
NewPatchReplaceOperationAction
|
||||
} from '../json-patch-operations.actions';
|
||||
import { NewPatchAddOperationAction, NewPatchMoveOperationAction, NewPatchRemoveOperationAction, NewPatchReplaceOperationAction } from '../json-patch-operations.actions';
|
||||
import { JsonPatchOperationPathObject } from './json-patch-operation-path-combiner';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
||||
import { hasNoValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
||||
import { dateToISOFormat } from '../../../shared/date.util';
|
||||
import { AuthorityValue } from '../../integration/models/authority.value';
|
||||
import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model';
|
||||
@@ -53,12 +49,35 @@ export class JsonPatchOperationsBuilder {
|
||||
* a boolean representing if the value to be added is a plain text value
|
||||
*/
|
||||
replace(path: JsonPatchOperationPathObject, value, plain = false) {
|
||||
if (hasNoValue(value) || (typeof value === 'object' && hasNoValue(value.value))) {
|
||||
this.remove(path);
|
||||
} else {
|
||||
this.store.dispatch(
|
||||
new NewPatchReplaceOperationAction(
|
||||
path.rootElement,
|
||||
path.subRootElement,
|
||||
path.path,
|
||||
this.prepareValue(value, plain, false)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a new NewPatchMoveOperationAction
|
||||
*
|
||||
* @param path
|
||||
* the new path tho move to
|
||||
* @param prevPath
|
||||
* the original path to move from
|
||||
*/
|
||||
move(path: JsonPatchOperationPathObject, prevPath: string) {
|
||||
this.store.dispatch(
|
||||
new NewPatchReplaceOperationAction(
|
||||
new NewPatchMoveOperationAction(
|
||||
path.rootElement,
|
||||
path.subRootElement,
|
||||
path.path,
|
||||
this.prepareValue(value, plain, false)));
|
||||
prevPath,
|
||||
path.path
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -196,7 +196,8 @@ function newOperation(state: JsonPatchOperationsState, action): JsonPatchOperati
|
||||
body,
|
||||
action.type,
|
||||
action.payload.path,
|
||||
hasValue(action.payload.value) ? action.payload.value : null);
|
||||
hasValue(action.payload.value) ? action.payload.value : null,
|
||||
hasValue(action.payload.from) ? action.payload.from : null);
|
||||
|
||||
if (hasValue(newState[ action.payload.resourceType ])
|
||||
&& hasValue(newState[ action.payload.resourceType ].children)) {
|
||||
@@ -293,7 +294,21 @@ function flushOperation(state: JsonPatchOperationsState, action: FlushPatchOpera
|
||||
}
|
||||
}
|
||||
|
||||
function addOperationToList(body: JsonPatchOperationObject[], actionType, targetPath, value?) {
|
||||
/**
|
||||
* Add a new operation to a patch
|
||||
*
|
||||
* @param body
|
||||
* The current patch
|
||||
* @param actionType
|
||||
* The type of operation to add
|
||||
* @param targetPath
|
||||
* The path for the operation
|
||||
* @param value
|
||||
* The new value
|
||||
* @param fromPath
|
||||
* The previous path (in case of a move operation)
|
||||
*/
|
||||
function addOperationToList(body: JsonPatchOperationObject[], actionType, targetPath, value?, fromPath?) {
|
||||
const newBody = Array.from(body);
|
||||
switch (actionType) {
|
||||
case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_ADD_OPERATION:
|
||||
@@ -313,6 +328,9 @@ function addOperationToList(body: JsonPatchOperationObject[], actionType, target
|
||||
case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_REMOVE_OPERATION:
|
||||
newBody.push(makeOperationEntry({ op: JsonPatchOperationType.remove, path: targetPath }));
|
||||
break;
|
||||
case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_MOVE_OPERATION:
|
||||
newBody.push(makeOperationEntry({ op: JsonPatchOperationType.move, from: fromPath, path: targetPath }));
|
||||
break;
|
||||
}
|
||||
return newBody;
|
||||
}
|
||||
|
@@ -21,10 +21,8 @@ import {
|
||||
RollbacktPatchOperationsAction,
|
||||
StartTransactionPatchOperationsAction
|
||||
} from './json-patch-operations.actions';
|
||||
import { StoreMock } from '../../shared/testing/store.mock';
|
||||
import { RequestEntry } from '../data/request.reducer';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { storeModuleConfig } from '../../app.reducer';
|
||||
|
||||
class TestService extends JsonPatchOperationsService<SubmitDataResponseDefinitionObject, SubmissionPatchRequest> {
|
||||
protected linkPath = '';
|
||||
@@ -99,27 +97,22 @@ describe('JsonPatchOperationsService test suite', () => {
|
||||
|
||||
}
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
StoreModule.forRoot({}, storeModuleConfig),
|
||||
],
|
||||
providers: [
|
||||
{ provide: Store, useClass: StoreMock }
|
||||
]
|
||||
}).compileComponents();
|
||||
}));
|
||||
function getStore() {
|
||||
return jasmine.createSpyObj('store', {
|
||||
dispatch: {},
|
||||
select: observableOf(mockState['json/patch'][testJsonPatchResourceType]),
|
||||
pipe: observableOf(true)
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
store = TestBed.get(Store);
|
||||
store = getStore();
|
||||
requestService = getMockRequestService(getRequestEntry$(true));
|
||||
rdbService = getMockRemoteDataBuildService();
|
||||
scheduler = getTestScheduler();
|
||||
halService = new HALEndpointServiceStub(resourceEndpointURL);
|
||||
service = initTestService();
|
||||
|
||||
spyOn(store, 'select').and.returnValue(observableOf(mockState['json/patch'][testJsonPatchResourceType]));
|
||||
spyOn(store, 'dispatch').and.callThrough();
|
||||
spyOn(Date.prototype, 'getTime').and.callFake(() => {
|
||||
return timestamp;
|
||||
});
|
||||
@@ -164,7 +157,7 @@ describe('JsonPatchOperationsService test suite', () => {
|
||||
|
||||
describe('when request is not successful', () => {
|
||||
beforeEach(() => {
|
||||
store = TestBed.get(Store);
|
||||
store = getStore();
|
||||
requestService = getMockRequestService(getRequestEntry$(false));
|
||||
rdbService = getMockRemoteDataBuildService();
|
||||
scheduler = getTestScheduler();
|
||||
@@ -227,7 +220,7 @@ describe('JsonPatchOperationsService test suite', () => {
|
||||
|
||||
describe('when request is not successful', () => {
|
||||
beforeEach(() => {
|
||||
store = TestBed.get(Store);
|
||||
store = getStore();
|
||||
requestService = getMockRequestService(getRequestEntry$(false));
|
||||
rdbService = getMockRemoteDataBuildService();
|
||||
scheduler = getTestScheduler();
|
||||
|
@@ -127,7 +127,7 @@ describe('RegistryService', () => {
|
||||
findAll: createSuccessfulRemoteDataObject$(createPaginatedList(mockSchemasList)),
|
||||
findById: createSuccessfulRemoteDataObject$(mockSchemasList[0]),
|
||||
createOrUpdateMetadataSchema: createSuccessfulRemoteDataObject$(mockSchemasList[0]),
|
||||
deleteAndReturnResponse: observableOf(new RestResponse(true, 200, 'OK')),
|
||||
delete: observableOf(new RestResponse(true, 200, 'OK')),
|
||||
clearRequests: observableOf('href')
|
||||
});
|
||||
|
||||
@@ -136,7 +136,7 @@ describe('RegistryService', () => {
|
||||
findById: createSuccessfulRemoteDataObject$(mockFieldsList[0]),
|
||||
create: createSuccessfulRemoteDataObject$(mockFieldsList[0]),
|
||||
put: createSuccessfulRemoteDataObject$(mockFieldsList[0]),
|
||||
deleteAndReturnResponse: observableOf(new RestResponse(true, 200, 'OK')),
|
||||
delete: observableOf(new RestResponse(true, 200, 'OK')),
|
||||
clearRequests: observableOf('href')
|
||||
});
|
||||
}
|
||||
|
@@ -223,7 +223,7 @@ export class RegistryService {
|
||||
* @param id The id of the metadata schema to delete
|
||||
*/
|
||||
public deleteMetadataSchema(id: number): Observable<RestResponse> {
|
||||
return this.metadataSchemaService.deleteAndReturnResponse(`${id}`);
|
||||
return this.metadataSchemaService.delete(`${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -269,7 +269,7 @@ export class RegistryService {
|
||||
* @param id The id of the metadata field to delete
|
||||
*/
|
||||
public deleteMetadataField(id: number): Observable<RestResponse> {
|
||||
return this.metadataFieldService.deleteAndReturnResponse(`${id}`);
|
||||
return this.metadataFieldService.delete(`${id}`);
|
||||
}
|
||||
/**
|
||||
* Method that clears a cached metadata field request and returns its REST url
|
||||
|
@@ -96,6 +96,8 @@ describe('ResourcePolicyService', () => {
|
||||
});
|
||||
|
||||
responseCacheEntry = new RequestEntry();
|
||||
responseCacheEntry.request = { href: 'https://rest.api/' } as any;
|
||||
responseCacheEntry.completed = true;
|
||||
responseCacheEntry.response = new RestResponse(true, 200, 'Success');
|
||||
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
|
@@ -24,6 +24,8 @@ import { PaginatedList } from '../data/paginated-list';
|
||||
import { ActionType } from './models/action-type.model';
|
||||
import { RequestParam } from '../cache/models/request-param.model';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { RestResponse } from '../cache/response.models';
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
|
||||
@@ -100,7 +102,7 @@ export class ResourcePolicyService {
|
||||
* @return an observable that emits true when the deletion was successful, false when it failed
|
||||
*/
|
||||
delete(resourcePolicyID: string): Observable<boolean> {
|
||||
return this.dataService.delete(resourcePolicyID);
|
||||
return this.dataService.delete(resourcePolicyID).pipe(map((response: RestResponse) => response.isSuccessful));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -9,7 +9,8 @@ export enum Context {
|
||||
Workflow = 'workflow',
|
||||
Workspace = 'workspace',
|
||||
AdminMenu = 'adminMenu',
|
||||
SubmissionModal = 'submissionModal',
|
||||
EntitySearchModalWithNameVariants = 'EntitySearchModalWithNameVariants',
|
||||
EntitySearchModal = 'EntitySearchModal',
|
||||
AdminSearch = 'adminSearch',
|
||||
AdminWorkflowSearch = 'adminWorkflowSearch',
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@ import { autoserialize, Serialize, Deserialize } from 'cerialize';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
|
||||
const VIRTUAL_METADATA_PREFIX = 'virtual::';
|
||||
export const VIRTUAL_METADATA_PREFIX = 'virtual::';
|
||||
|
||||
/** A single metadata value and its properties. */
|
||||
export interface MetadataValueInterface {
|
||||
|
@@ -66,7 +66,7 @@ export const getPaginatedListPayload = () =>
|
||||
|
||||
export const getSucceededRemoteData = () =>
|
||||
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
||||
source.pipe(find((rd: RemoteData<T>) => rd.hasSucceeded));
|
||||
source.pipe(filter((rd: RemoteData<T>) => rd.hasSucceeded), take(1));
|
||||
|
||||
export const getSucceededRemoteWithNotEmptyData = () =>
|
||||
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
||||
|
@@ -7,12 +7,14 @@ import { SubmissionObjectDataService } from './submission-object-data.service';
|
||||
import { SubmissionScopeType } from './submission-scope-type';
|
||||
import { WorkflowItemDataService } from './workflowitem-data.service';
|
||||
import { WorkspaceitemDataService } from './workspaceitem-data.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
|
||||
describe('SubmissionObjectDataService', () => {
|
||||
let service: SubmissionObjectDataService;
|
||||
let submissionService: SubmissionService;
|
||||
let workspaceitemDataService: WorkspaceitemDataService;
|
||||
let workflowItemDataService: WorkflowItemDataService;
|
||||
let halService: HALEndpointService;
|
||||
|
||||
const submissionId = '1234';
|
||||
const wsiResult = 'wsiResult' as any;
|
||||
@@ -25,6 +27,9 @@ describe('SubmissionObjectDataService', () => {
|
||||
workflowItemDataService = jasmine.createSpyObj('WorkflowItemDataService', {
|
||||
findById: wfiResult
|
||||
});
|
||||
halService = jasmine.createSpyObj('HALEndpointService', {
|
||||
getEndpoint: '/workspaceItem'
|
||||
});
|
||||
});
|
||||
|
||||
describe('findById', () => {
|
||||
@@ -32,7 +37,7 @@ describe('SubmissionObjectDataService', () => {
|
||||
submissionService = jasmine.createSpyObj('SubmissionService', {
|
||||
getSubmissionScope: {}
|
||||
});
|
||||
service = new SubmissionObjectDataService(workspaceitemDataService, workflowItemDataService, submissionService);
|
||||
service = new SubmissionObjectDataService(workspaceitemDataService, workflowItemDataService, submissionService, halService);
|
||||
service.findById(submissionId);
|
||||
expect(submissionService.getSubmissionScope).toHaveBeenCalled();
|
||||
});
|
||||
@@ -42,7 +47,7 @@ describe('SubmissionObjectDataService', () => {
|
||||
submissionService = jasmine.createSpyObj('SubmissionService', {
|
||||
getSubmissionScope: SubmissionScopeType.WorkspaceItem
|
||||
});
|
||||
service = new SubmissionObjectDataService(workspaceitemDataService, workflowItemDataService, submissionService);
|
||||
service = new SubmissionObjectDataService(workspaceitemDataService, workflowItemDataService, submissionService, halService);
|
||||
});
|
||||
|
||||
it('should forward the result of WorkspaceitemDataService.findByIdAndIDType()', () => {
|
||||
@@ -57,7 +62,7 @@ describe('SubmissionObjectDataService', () => {
|
||||
submissionService = jasmine.createSpyObj('SubmissionService', {
|
||||
getSubmissionScope: SubmissionScopeType.WorkflowItem
|
||||
});
|
||||
service = new SubmissionObjectDataService(workspaceitemDataService, workflowItemDataService, submissionService);
|
||||
service = new SubmissionObjectDataService(workspaceitemDataService, workflowItemDataService, submissionService, halService);
|
||||
});
|
||||
|
||||
it('should forward the result of WorkflowItemDataService.findByIdAndIDType()', () => {
|
||||
@@ -72,7 +77,7 @@ describe('SubmissionObjectDataService', () => {
|
||||
submissionService = jasmine.createSpyObj('SubmissionService', {
|
||||
getSubmissionScope: 'Something else'
|
||||
});
|
||||
service = new SubmissionObjectDataService(workspaceitemDataService, workflowItemDataService, submissionService);
|
||||
service = new SubmissionObjectDataService(workspaceitemDataService, workflowItemDataService, submissionService, halService);
|
||||
});
|
||||
|
||||
it('shouldn\'t call any data service methods', () => {
|
||||
|
@@ -8,6 +8,9 @@ import { SubmissionObject } from './models/submission-object.model';
|
||||
import { SubmissionScopeType } from './submission-scope-type';
|
||||
import { WorkflowItemDataService } from './workflowitem-data.service';
|
||||
import { WorkspaceitemDataService } from './workspaceitem-data.service';
|
||||
import { DataService } from '../data/data.service';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
|
||||
/**
|
||||
* A service to retrieve submission objects (WorkspaceItem/WorkflowItem)
|
||||
@@ -20,10 +23,22 @@ export class SubmissionObjectDataService {
|
||||
constructor(
|
||||
private workspaceitemDataService: WorkspaceitemDataService,
|
||||
private workflowItemDataService: WorkflowItemDataService,
|
||||
private submissionService: SubmissionService
|
||||
private submissionService: SubmissionService,
|
||||
private halService: HALEndpointService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the HREF for a specific object based on its identifier
|
||||
* @param id The identifier for the object
|
||||
*/
|
||||
getHrefByID(id): Observable<string> {
|
||||
const dataService: DataService<SubmissionObject> = this.submissionService.getSubmissionScope() === SubmissionScopeType.WorkspaceItem ? this.workspaceitemDataService : this.workflowItemDataService;
|
||||
|
||||
return this.halService.getEndpoint(dataService.getLinkPath()).pipe(
|
||||
map((endpoint: string) => dataService.getIDHref(endpoint, encodeURIComponent(id))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a submission object based on its ID.
|
||||
*
|
||||
|
@@ -176,5 +176,4 @@ export class SubmissionResponseParsingService extends BaseResponseParsingService
|
||||
|
||||
return definition;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -17,6 +17,7 @@ import { Observable } from 'rxjs';
|
||||
import { find, map } from 'rxjs/operators';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { RequestEntry } from '../data/request.reducer';
|
||||
import { RestResponse } from '../cache/response.models';
|
||||
|
||||
/**
|
||||
* A service that provides methods to make REST requests with workflow items endpoint.
|
||||
@@ -44,7 +45,7 @@ export class WorkflowItemDataService extends DataService<WorkflowItem> {
|
||||
* @param id The Workflow Item's id to be removed
|
||||
* @return an observable that emits true when the deletion was successful, false when it failed
|
||||
*/
|
||||
delete(id: string): Observable<boolean> {
|
||||
delete(id: string): Observable<RestResponse> {
|
||||
return this.deleteWFI(id, true)
|
||||
}
|
||||
|
||||
@@ -54,7 +55,7 @@ export class WorkflowItemDataService extends DataService<WorkflowItem> {
|
||||
* @return an observable that emits true when sending back the item was successful, false when it failed
|
||||
*/
|
||||
sendBack(id: string): Observable<boolean> {
|
||||
return this.deleteWFI(id, false)
|
||||
return this.deleteWFI(id, false).pipe(map((response: RestResponse) => response.isSuccessful));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,7 +65,7 @@ export class WorkflowItemDataService extends DataService<WorkflowItem> {
|
||||
* When true, the workflow item and its item will be permanently expunged on the server
|
||||
* When false, the workflow item will be removed, but the item will still be available as a workspace item
|
||||
*/
|
||||
private deleteWFI(id: string, expunge: boolean): Observable<boolean> {
|
||||
private deleteWFI(id: string, expunge: boolean): Observable<RestResponse> {
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
|
||||
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
|
||||
@@ -82,7 +83,7 @@ export class WorkflowItemDataService extends DataService<WorkflowItem> {
|
||||
|
||||
return this.requestService.getByUUID(requestId).pipe(
|
||||
find((request: RequestEntry) => request.completed),
|
||||
map((request: RequestEntry) => request.response.isSuccessful)
|
||||
map((request: RequestEntry) => request.response)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -21,7 +21,6 @@ import { WorkspaceItem } from './models/workspaceitem.model';
|
||||
@dataService(WorkspaceItem.type)
|
||||
export class WorkspaceitemDataService extends DataService<WorkspaceItem> {
|
||||
protected linkPath = 'workspaceitems';
|
||||
protected responseMsToLive = 10 * 1000;
|
||||
|
||||
constructor(
|
||||
protected comparator: DSOChangeAnalyzer<WorkspaceItem>,
|
||||
|
@@ -1 +1 @@
|
||||
<ds-journal-issue-search-result-grid-element [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-journal-issue-search-result-grid-element>
|
||||
<ds-journal-issue-search-result-grid-element [showLabel]="showLabel" [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-journal-issue-search-result-grid-element>
|
||||
|
@@ -1 +1 @@
|
||||
<ds-journal-volume-search-result-grid-element [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-journal-volume-search-result-grid-element>
|
||||
<ds-journal-volume-search-result-grid-element [showLabel]="showLabel" [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-journal-volume-search-result-grid-element>
|
||||
|
@@ -1 +1 @@
|
||||
<ds-journal-search-result-grid-element [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-journal-search-result-grid-element>
|
||||
<ds-journal-search-result-grid-element [showLabel]="showLabel" [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-journal-search-result-grid-element>
|
||||
|
@@ -17,7 +17,7 @@
|
||||
</div>
|
||||
</span>
|
||||
<div class="card-body">
|
||||
<ds-item-type-badge [object]="dso"></ds-item-type-badge>
|
||||
<ds-item-type-badge *ngIf="showLabel" [object]="dso"></ds-item-type-badge>
|
||||
<ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4">
|
||||
<h4 class="card-title" [innerHTML]="firstMetadataValue('dc.title')"></h4>
|
||||
</ds-truncatable-part>
|
||||
|
@@ -17,7 +17,7 @@
|
||||
</div>
|
||||
</span>
|
||||
<div class="card-body">
|
||||
<ds-item-type-badge [object]="dso"></ds-item-type-badge>
|
||||
<ds-item-type-badge *ngIf="showLabel" [object]="dso"></ds-item-type-badge>
|
||||
<ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4">
|
||||
<h4 class="card-title" [innerHTML]="dso.firstMetadataValue('dc.title')"></h4>
|
||||
</ds-truncatable-part>
|
||||
|
@@ -17,7 +17,7 @@
|
||||
</div>
|
||||
</span>
|
||||
<div class="card-body">
|
||||
<ds-item-type-badge [object]="dso"></ds-item-type-badge>
|
||||
<ds-item-type-badge *ngIf="showLabel" [object]="dso"></ds-item-type-badge>
|
||||
<ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4">
|
||||
<h4 class="card-title" [innerHTML]="firstMetadataValue('dc.title')"></h4>
|
||||
</ds-truncatable-part>
|
||||
|
@@ -1 +1 @@
|
||||
<ds-journal-issue-search-result-list-element [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-journal-issue-search-result-list-element>
|
||||
<ds-journal-issue-search-result-list-element [showLabel]="showLabel" [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-journal-issue-search-result-list-element>
|
||||
|
@@ -1 +1 @@
|
||||
<ds-journal-volume-search-result-list-element [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-journal-volume-search-result-list-element>
|
||||
<ds-journal-volume-search-result-list-element [showLabel]="showLabel" [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-journal-volume-search-result-list-element>
|
||||
|
@@ -1 +1 @@
|
||||
<ds-journal-search-result-list-element [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-journal-search-result-list-element>
|
||||
<ds-journal-search-result-list-element [showLabel]="showLabel" [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-journal-search-result-list-element>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<ds-item-type-badge [object]="dso"></ds-item-type-badge>
|
||||
<ds-item-type-badge *ngIf="showLabel" [object]="dso"></ds-item-type-badge>
|
||||
<ds-truncatable [id]="dso.id">
|
||||
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer"
|
||||
[routerLink]="['/items/' + dso.id]" class="lead"
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<ds-item-type-badge [object]="dso"></ds-item-type-badge>
|
||||
<ds-item-type-badge *ngIf="showLabel" [object]="dso"></ds-item-type-badge>
|
||||
<ds-truncatable [id]="dso.id">
|
||||
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer"
|
||||
[routerLink]="['/items/' + dso.id]" class="lead"
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<ds-item-type-badge [object]="dso"></ds-item-type-badge>
|
||||
<ds-item-type-badge *ngIf="showLabel" [object]="dso"></ds-item-type-badge>
|
||||
<ds-truncatable [id]="dso.id">
|
||||
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer"
|
||||
[routerLink]="['/items/' + dso.id]" class="lead"
|
||||
|
@@ -1 +1 @@
|
||||
<ds-org-unit-search-result-grid-element [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-org-unit-search-result-grid-element>
|
||||
<ds-org-unit-search-result-grid-element [showLabel]="showLabel" [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-org-unit-search-result-grid-element>
|
||||
|
@@ -1 +1 @@
|
||||
<ds-person-search-result-grid-element [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-person-search-result-grid-element>
|
||||
<ds-person-search-result-grid-element [showLabel]="showLabel" [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-person-search-result-grid-element>
|
||||
|
@@ -1 +1 @@
|
||||
<ds-project-search-result-grid-element [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-project-search-result-grid-element>
|
||||
<ds-project-search-result-grid-element [showLabel]="showLabel" [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-project-search-result-grid-element>
|
||||
|
@@ -17,7 +17,7 @@
|
||||
</div>
|
||||
</span>
|
||||
<div class="card-body">
|
||||
<ds-item-type-badge [object]="dso"></ds-item-type-badge>
|
||||
<ds-item-type-badge *ngIf="showLabel" [object]="dso"></ds-item-type-badge>
|
||||
<ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4">
|
||||
<h4 class="card-title" [innerHTML]="firstMetadataValue('organization.legalName')"></h4>
|
||||
</ds-truncatable-part>
|
||||
|
@@ -17,7 +17,7 @@
|
||||
</div>
|
||||
</span>
|
||||
<div class="card-body">
|
||||
<ds-item-type-badge [object]="dso"></ds-item-type-badge>
|
||||
<ds-item-type-badge *ngIf="showLabel" [object]="dso"></ds-item-type-badge>
|
||||
<ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4">
|
||||
<h4 class="card-title"
|
||||
[innerHTML]="firstMetadataValue('person.familyName') + ', ' + firstMetadataValue('person.givenName')"></h4>
|
||||
|
@@ -17,7 +17,7 @@
|
||||
</div>
|
||||
</span>
|
||||
<div class="card-body">
|
||||
<ds-item-type-badge [object]="dso"></ds-item-type-badge>
|
||||
<ds-item-type-badge *ngIf="showLabel" [object]="dso"></ds-item-type-badge>
|
||||
<ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4">
|
||||
<h4 class="card-title" [innerHTML]="firstMetadataValue('dc.title')"></h4>
|
||||
</ds-truncatable-part>
|
||||
|
@@ -1 +1 @@
|
||||
<ds-org-unit-search-result-list-element [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-org-unit-search-result-list-element>
|
||||
<ds-org-unit-search-result-list-element [showLabel]="showLabel" [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-org-unit-search-result-list-element>
|
||||
|
@@ -1 +1 @@
|
||||
<ds-person-search-result-list-element [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-person-search-result-list-element>
|
||||
<ds-person-search-result-list-element [showLabel]="showLabel" [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-person-search-result-list-element>
|
||||
|
@@ -1 +1 @@
|
||||
<ds-project-search-result-list-element [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-project-search-result-list-element>
|
||||
<ds-project-search-result-list-element [showLabel]="showLabel" [object]="{ indexableObject: object, hitHighlights: {} }" [linkType]="linkType"></ds-project-search-result-list-element>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<ds-item-type-badge [object]="dso"></ds-item-type-badge>
|
||||
<ds-item-type-badge *ngIf="showLabel" [object]="dso"></ds-item-type-badge>
|
||||
<ds-truncatable [id]="dso.id">
|
||||
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer"
|
||||
[routerLink]="['/items/' + dso.id]" class="lead"
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<ds-item-type-badge [object]="dso"></ds-item-type-badge>
|
||||
<ds-item-type-badge *ngIf="showLabel" [object]="dso"></ds-item-type-badge>
|
||||
<ds-truncatable [id]="dso.id">
|
||||
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer"
|
||||
[routerLink]="['/items/' + dso.id]" class="lead"
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<ds-truncatable [id]="dso.id">
|
||||
<ds-item-type-badge [object]="dso"></ds-item-type-badge>
|
||||
<ds-item-type-badge *ngIf="showLabel" [object]="dso"></ds-item-type-badge>
|
||||
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer"
|
||||
[routerLink]="['/items/' + dso.id]" class="lead"
|
||||
[innerHTML]="firstMetadataValue('dc.title')"></a>
|
||||
|
@@ -1,7 +1,6 @@
|
||||
<ng-template #descTemplate>
|
||||
<span class="text-muted">
|
||||
<span *ngIf="metadataRepresentation.allMetadata(['dc.description']).length > 0"
|
||||
class="item-list-job-title">
|
||||
<span class="item-list-job-title">
|
||||
<span [innerHTML]="metadataRepresentation.firstMetadataValue(['dc.description'])"></span>
|
||||
</span>
|
||||
</span>
|
||||
@@ -9,5 +8,5 @@
|
||||
<ds-truncatable [id]="metadataRepresentation.id">
|
||||
<a [routerLink]="['/items/' + metadataRepresentation.id]"
|
||||
[innerHTML]="metadataRepresentation.getValue()"
|
||||
[tooltip]="metadataRepresentation.allMetadata(['organization.legalName']).length > 0 ? descTemplate : null"></a>
|
||||
[tooltip]="metadataRepresentation.allMetadata(['dc.description']).length > 0 ? descTemplate : null"></a>
|
||||
</ds-truncatable>
|
||||
|
@@ -7,7 +7,7 @@ import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { Metadata } from '../../../../../core/shared/metadata.utils';
|
||||
import { MetadataValue } from '../../../../../core/shared/metadata.models';
|
||||
|
||||
@listableObjectComponent(ExternalSourceEntry, ViewMode.ListElement, Context.SubmissionModal)
|
||||
@listableObjectComponent(ExternalSourceEntry, ViewMode.ListElement, Context.EntitySearchModalWithNameVariants)
|
||||
@Component({
|
||||
selector: 'ds-external-source-entry-list-submission-element',
|
||||
styleUrls: ['./external-source-entry-list-submission-element.component.scss'],
|
||||
|
@@ -1,10 +1,15 @@
|
||||
<div class="d-flex">
|
||||
<div class="person-thumbnail pr-2">
|
||||
<ds-thumbnail [thumbnail]="getThumbnail() | async" [defaultImage]="'assets/images/orgunit-placeholder.svg'"></ds-thumbnail>
|
||||
</div>
|
||||
<!-- <div class="person-thumbnail pr-2">-->
|
||||
<!-- <ds-thumbnail [thumbnail]="getThumbnail() | async" [defaultImage]="'assets/images/orgunit-placeholder.svg'"></ds-thumbnail>-->
|
||||
<!-- </div>-->
|
||||
<div class="flex-grow-1">
|
||||
<ds-org-unit-input-suggestions [suggestions]="allSuggestions" [(ngModel)]="selectedName" (clickSuggestion)="select($event)"
|
||||
<ds-org-unit-input-suggestions *ngIf="useNameVariants" [suggestions]="allSuggestions" [(ngModel)]="selectedName" (clickSuggestion)="select($event)"
|
||||
(submitSuggestion)="selectCustom($event)"></ds-org-unit-input-suggestions>
|
||||
|
||||
<div *ngIf="!useNameVariants"
|
||||
class="lead"
|
||||
[innerHTML]="firstMetadataValue('organization.legalName')"></div>
|
||||
|
||||
<span class="text-muted">
|
||||
<span *ngIf="dso.allMetadata('organization.address.addressLocality').length > 0"
|
||||
class="item-list-address-locality">
|
||||
|
@@ -20,7 +20,8 @@ import { ItemDataService } from '../../../../../core/data/item-data.service';
|
||||
import { SelectableListService } from '../../../../../shared/object-list/selectable-list/selectable-list.service';
|
||||
import { NameVariantModalComponent } from '../../name-variant-modal/name-variant-modal.component';
|
||||
|
||||
@listableObjectComponent('OrgUnitSearchResult', ViewMode.ListElement, Context.SubmissionModal)
|
||||
@listableObjectComponent('OrgUnitSearchResult', ViewMode.ListElement, Context.EntitySearchModal)
|
||||
@listableObjectComponent('OrgUnitSearchResult', ViewMode.ListElement, Context.EntitySearchModalWithNameVariants)
|
||||
@Component({
|
||||
selector: 'ds-person-search-result-list-submission-element',
|
||||
styleUrls: ['./org-unit-search-result-list-submission-element.component.scss'],
|
||||
@@ -34,6 +35,7 @@ export class OrgUnitSearchResultListSubmissionElementComponent extends SearchRes
|
||||
allSuggestions: string[];
|
||||
selectedName: string;
|
||||
alternativeField = 'dc.title.alternative';
|
||||
useNameVariants = false;
|
||||
|
||||
constructor(protected truncatableService: TruncatableService,
|
||||
private relationshipService: RelationshipService,
|
||||
@@ -48,16 +50,21 @@ export class OrgUnitSearchResultListSubmissionElementComponent extends SearchRes
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit();
|
||||
const defaultValue = this.firstMetadataValue('organization.legalName');
|
||||
const alternatives = this.allMetadataValues(this.alternativeField);
|
||||
this.allSuggestions = [defaultValue, ...alternatives];
|
||||
|
||||
this.relationshipService.getNameVariant(this.listID, this.dso.uuid)
|
||||
.pipe(take(1))
|
||||
.subscribe((nameVariant: string) => {
|
||||
this.selectedName = nameVariant || defaultValue;
|
||||
}
|
||||
);
|
||||
this.useNameVariants = this.context === Context.EntitySearchModalWithNameVariants;
|
||||
|
||||
if (this.useNameVariants) {
|
||||
const defaultValue = this.firstMetadataValue('organization.legalName');
|
||||
const alternatives = this.allMetadataValues(this.alternativeField);
|
||||
this.allSuggestions = [defaultValue, ...alternatives];
|
||||
|
||||
this.relationshipService.getNameVariant(this.listID, this.dso.uuid)
|
||||
.pipe(take(1))
|
||||
.subscribe((nameVariant: string) => {
|
||||
this.selectedName = nameVariant || defaultValue;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
select(value) {
|
||||
@@ -75,7 +82,7 @@ export class OrgUnitSearchResultListSubmissionElementComponent extends SearchRes
|
||||
if (!this.allSuggestions.includes(value)) {
|
||||
this.openModal(value)
|
||||
.then(() => {
|
||||
|
||||
// user clicked ok: store the name variant in the item
|
||||
const newName: MetadataValue = new MetadataValue();
|
||||
newName.value = value;
|
||||
|
||||
@@ -89,9 +96,12 @@ export class OrgUnitSearchResultListSubmissionElementComponent extends SearchRes
|
||||
},
|
||||
});
|
||||
this.itemDataService.update(updatedItem).pipe(take(1)).subscribe();
|
||||
})
|
||||
}).catch(() => {
|
||||
// user clicked cancel: use the name variant only for this relation, no further action required
|
||||
}).finally(() => {
|
||||
this.select(value);
|
||||
})
|
||||
}
|
||||
this.select(value);
|
||||
}
|
||||
|
||||
openModal(value): Promise<any> {
|
||||
|
@@ -1,6 +1,7 @@
|
||||
form {
|
||||
z-index: 1;
|
||||
&:before {
|
||||
pointer-events: none; // prevent the icon from ‘catching‘ the click
|
||||
position: absolute;
|
||||
font-weight: 900;
|
||||
font-family: "Font Awesome 5 Free";
|
||||
@@ -15,4 +16,4 @@ form {
|
||||
input.suggestion_input {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,4 @@
|
||||
<div class="d-flex">
|
||||
<div class="person-thumbnail pr-2">
|
||||
<ds-thumbnail [thumbnail]="getThumbnail() | async" [defaultImage]="'assets/images/person-placeholder.svg'"></ds-thumbnail>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<ds-person-input-suggestions [suggestions]="allSuggestions" [(ngModel)]="selectedName" (clickSuggestion)="select($event)" (submitSuggestion)="selectCustom($event)"></ds-person-input-suggestions>
|
||||
<span class="text-muted">
|
||||
|
@@ -20,7 +20,7 @@ import { MetadataValue } from '../../../../../core/shared/metadata.models';
|
||||
import { ItemDataService } from '../../../../../core/data/item-data.service';
|
||||
import { SelectableListService } from '../../../../../shared/object-list/selectable-list/selectable-list.service';
|
||||
|
||||
@listableObjectComponent('PersonSearchResult', ViewMode.ListElement, Context.SubmissionModal)
|
||||
@listableObjectComponent('PersonSearchResult', ViewMode.ListElement, Context.EntitySearchModalWithNameVariants)
|
||||
@Component({
|
||||
selector: 'ds-person-search-result-list-submission-element',
|
||||
styleUrls: ['./person-search-result-list-submission-element.component.scss'],
|
||||
@@ -55,7 +55,7 @@ export class PersonSearchResultListSubmissionElementComponent extends SearchResu
|
||||
this.relationshipService.getNameVariant(this.listID, this.dso.uuid)
|
||||
.pipe(take(1))
|
||||
.subscribe((nameVariant: string) => {
|
||||
this.selectedName = nameVariant || defaultValue;
|
||||
this.selectedName = nameVariant || defaultValue;
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -75,27 +75,32 @@ export class PersonSearchResultListSubmissionElementComponent extends SearchResu
|
||||
if (!this.allSuggestions.includes(value)) {
|
||||
this.openModal(value)
|
||||
.then(() => {
|
||||
// user clicked ok: store the name variant in the item
|
||||
const newName: MetadataValue = new MetadataValue();
|
||||
newName.value = value;
|
||||
|
||||
const newName: MetadataValue = new MetadataValue();
|
||||
newName.value = value;
|
||||
|
||||
const existingNames: MetadataValue[] = this.dso.metadata[this.alternativeField] || [];
|
||||
const alternativeNames = { [this.alternativeField]: [...existingNames, newName] };
|
||||
const updatedItem =
|
||||
Object.assign({}, this.dso, {
|
||||
metadata: {
|
||||
...this.dso.metadata,
|
||||
...alternativeNames
|
||||
},
|
||||
});
|
||||
this.itemDataService.update(updatedItem).pipe(take(1)).subscribe();
|
||||
})
|
||||
const existingNames: MetadataValue[] = this.dso.metadata[this.alternativeField] || [];
|
||||
const alternativeNames = { [this.alternativeField]: [...existingNames, newName] };
|
||||
const updatedItem =
|
||||
Object.assign({}, this.dso, {
|
||||
metadata: {
|
||||
...this.dso.metadata,
|
||||
...alternativeNames
|
||||
},
|
||||
});
|
||||
this.itemDataService.update(updatedItem).pipe(take(1)).subscribe();
|
||||
this.itemDataService.commitUpdates();
|
||||
}).catch(() => {
|
||||
// user clicked cancel: use the name variant only for this relation, no further action required
|
||||
}).finally(() => {
|
||||
this.select(value);
|
||||
})
|
||||
}
|
||||
this.select(value);
|
||||
}
|
||||
|
||||
openModal(value): Promise<any> {
|
||||
const modalRef = this.modalService.open(NameVariantModalComponent, { centered: true });
|
||||
|
||||
const modalComp = modalRef.componentInstance;
|
||||
modalComp.value = value;
|
||||
return modalRef.result;
|
||||
|
@@ -1,11 +1,12 @@
|
||||
<form #form="ngForm" (ngSubmit)="onSubmit(value)"
|
||||
<form #form="ngForm" (ngSubmit)="onSubmit(inputField.value)"
|
||||
[action]="action" (keydown)="onKeydown($event)"
|
||||
(keydown.arrowdown)="shiftFocusDown($event)"
|
||||
(keydown.arrowup)="shiftFocusUp($event)" (keydown.esc)="close()"
|
||||
(dsClickOutside)="close();">
|
||||
<input #inputField type="text" [(ngModel)]="value" [name]="name"
|
||||
<input #inputField type="text" [ngModel]="value" [name]="name"
|
||||
class="form-control suggestion_input"
|
||||
(focus)="open()"
|
||||
(blur)="onSubmit(inputField.value)"
|
||||
(click)="open()"
|
||||
[ngClass]="{'is-invalid': !valid}"
|
||||
[dsDebounce]="debounceTime" (onDebounce)="find($event)"
|
||||
@@ -21,4 +22,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
form {
|
||||
z-index: 1;
|
||||
&:before {
|
||||
pointer-events: none; // prevent the icon from ‘catching‘ the click
|
||||
position: absolute;
|
||||
font-weight: 900;
|
||||
font-family: "Font Awesome 5 Free";
|
||||
@@ -9,10 +9,9 @@ form {
|
||||
right: 0;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
input.suggestion_input {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -33,8 +33,10 @@ export class PersonInputSuggestionsComponent extends InputSuggestionsComponent i
|
||||
}
|
||||
|
||||
onSubmit(data) {
|
||||
this.value = data;
|
||||
this.submitSuggestion.emit(data);
|
||||
if (data !== this.value) {
|
||||
this.value = data;
|
||||
this.submitSuggestion.emit(data);
|
||||
}
|
||||
}
|
||||
|
||||
onClickSuggestion(data) {
|
||||
|
@@ -48,7 +48,7 @@ describe('DeleteComColPageComponent', () => {
|
||||
dsoDataService = jasmine.createSpyObj(
|
||||
'dsoDataService',
|
||||
{
|
||||
delete: observableOf(true)
|
||||
delete: observableOf({ isSuccessful: true })
|
||||
});
|
||||
|
||||
routerStub = {
|
||||
@@ -106,7 +106,7 @@ describe('DeleteComColPageComponent', () => {
|
||||
});
|
||||
|
||||
it('should show an error notification on failure', () => {
|
||||
(dsoDataService.delete as any).and.returnValue(observableOf(false));
|
||||
(dsoDataService.delete as any).and.returnValue(observableOf({ isSuccessful: false }));
|
||||
spyOn(router, 'navigate');
|
||||
comp.onConfirm(data2);
|
||||
fixture.detectChanges();
|
||||
|
@@ -7,6 +7,7 @@ import { DataService } from '../../../core/data/data.service';
|
||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||
import { NotificationsService } from '../../notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { RestResponse } from '../../../core/cache/response.models';
|
||||
|
||||
/**
|
||||
* Component representing the delete page for communities and collections
|
||||
@@ -45,8 +46,8 @@ export class DeleteComColPageComponent<TDomain extends DSpaceObject> implements
|
||||
onConfirm(dso: TDomain) {
|
||||
this.dsoDataService.delete(dso.id)
|
||||
.pipe(first())
|
||||
.subscribe((success: boolean) => {
|
||||
if (success) {
|
||||
.subscribe((response: RestResponse) => {
|
||||
if (response.isSuccessful) {
|
||||
const successMessage = this.translate.instant((dso as any).type + '.delete.notification.success');
|
||||
this.notifications.success(successMessage)
|
||||
} else {
|
||||
|
@@ -1,29 +1,25 @@
|
||||
<div [class.form-group]="(model.type !== 'GROUP' && asBootstrapFormGroup) || getClass('element', 'container').includes('form-group')"
|
||||
<div [class.form-group]="asBootstrapFormGroup"
|
||||
[formGroup]="group"
|
||||
[ngClass]="[getClass('element', 'container'), getClass('grid', 'container')]">
|
||||
|
||||
<label *ngIf="!isCheckbox && hasLabel"
|
||||
[for]="model.id"
|
||||
[innerHTML]="(model.required && model.label) ? (model.label | translate) + ' *' : (model.label | translate)"
|
||||
[ngClass]="[getClass('element', 'label'), getClass('grid', 'label')]"></label>
|
||||
|
||||
<ng-container *ngTemplateOutlet="startTemplate?.templateRef; context: model"></ng-container>
|
||||
|
||||
<div [ngClass]="{'form-row': model.hasLanguages || hasRelationLookup }">
|
||||
<!-- Should be *ngIf instead of class d-none, but that breaks the #componentViewContainer reference-->
|
||||
<div [ngClass]="{'form-row': model.hasLanguages || isRelationship,
|
||||
'd-none': value?.isVirtual && (model.hasSelectableMetadata || context?.index > 0)}">
|
||||
<div [ngClass]="getClass('grid', 'control')">
|
||||
|
||||
<ng-container #componentViewContainer></ng-container>
|
||||
|
||||
<small *ngIf="hasHint && (!showErrorMessages || errorMessages.length === 0)"
|
||||
<small *ngIf="hasHint && (model.repeatable === false || context?.index === 0) && (!showErrorMessages || errorMessages.length === 0)"
|
||||
class="text-muted" [innerHTML]="model.hint | translate" [ngClass]="getClass('element', 'hint')"></small>
|
||||
|
||||
<div *ngIf="showErrorMessages" [ngClass]="[getClass('element', 'errors'), getClass('grid', 'errors')]">
|
||||
<small *ngFor="let message of errorMessages" class="invalid-feedback d-block">{{ message | translate:model.validators }}</small>
|
||||
<small *ngFor="let message of errorMessages" class="invalid-feedback d-block">{{ message | translate: model.validators }}</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div *ngIf="model.languageCodes && model.languageCodes.length > 0" class="col-xs-2">
|
||||
<div *ngIf="model.languageCodes && model.languageCodes.length > 0" class="col-xs-2" >
|
||||
<select
|
||||
#language="ngModel"
|
||||
[disabled]="model.readOnly"
|
||||
@@ -36,32 +32,41 @@
|
||||
<option *ngFor="let lang of model.languageCodes" [value]="lang.code">{{lang.display}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div *ngIf="hasRelationLookup" class="col-auto text-center">
|
||||
<div *ngIf="isRelationship" class="col-auto text-center" [class.invisible]="context?.index > 0">
|
||||
<button class="btn btn-secondary"
|
||||
type="submit"
|
||||
type="button"
|
||||
ngbTooltip="{{'form.lookup-help' | translate}}"
|
||||
placement="top"
|
||||
(click)="openLookup(); $event.stopPropagation();">{{'form.lookup' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngTemplateOutlet="endTemplate?.templateRef; context: model"></ng-container>
|
||||
|
||||
<ng-container *ngIf="value?.isVirtual">
|
||||
<ds-existing-metadata-list-element
|
||||
*ngIf="model.hasSelectableMetadata"
|
||||
[reoRel]="relationshipValue$ | async"
|
||||
[submissionItem]="item$ | async"
|
||||
[listId]="listId"
|
||||
[metadataFields]="model.metadataFields"
|
||||
[submissionId]="model.submissionId"
|
||||
[relationshipOptions]="model.relationship"
|
||||
(remove)="onRemove()"
|
||||
>
|
||||
</ds-existing-metadata-list-element>
|
||||
<ds-existing-relation-list-element
|
||||
*ngIf="!model.hasSelectableMetadata"
|
||||
[ngClass]="{'d-block pb-2 pt-2': !context?.index}"
|
||||
[reoRel]="relationshipValue$ | async"
|
||||
[submissionItem]="item$ | async"
|
||||
[listId]="listId"
|
||||
[metadataFields]="model.metadataFields"
|
||||
[submissionId]="model.submissionId"
|
||||
[relationshipOptions]="model.relationship"
|
||||
>
|
||||
</ds-existing-relation-list-element>
|
||||
</ng-container>
|
||||
<ng-content></ng-content>
|
||||
|
||||
|
||||
<div *ngIf="hasRelationLookup" class="mt-3">
|
||||
<ul class="list-unstyled" cdkDropList (cdkDropListDropped)="moveSelection($event)">
|
||||
<ds-existing-metadata-list-element cdkDrag
|
||||
*ngFor="let reorderable of reorderables; trackBy: trackReorderable"
|
||||
[reoRel]="reorderable"
|
||||
[submissionItem]="item"
|
||||
[listId]="listId"
|
||||
[metadataFields]="model.metadataFields"
|
||||
[relationshipOptions]="model.relationship">
|
||||
</ds-existing-metadata-list-element>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
@@ -71,6 +71,9 @@ import { Item } from '../../../../core/shared/item.model';
|
||||
import { WorkspaceItem } from '../../../../core/submission/models/workspaceitem.model';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils';
|
||||
import { FormService } from '../../form.service';
|
||||
import { SubmissionService } from '../../../../submission/submission.service';
|
||||
import { FormBuilderService } from '../form-builder.service';
|
||||
|
||||
describe('DsDynamicFormControlContainerComponent test suite', () => {
|
||||
|
||||
@@ -101,15 +104,16 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
|
||||
new DynamicSwitchModel({ id: 'switch' }),
|
||||
new DynamicTextAreaModel({ id: 'textarea' }),
|
||||
new DynamicTimePickerModel({ id: 'timepicker' }),
|
||||
new DynamicTypeaheadModel({ id: 'typeahead', metadataFields: [], repeatable: false, submissionId: '1234' }),
|
||||
new DynamicTypeaheadModel({ id: 'typeahead', metadataFields: [], repeatable: false, submissionId: '1234', hasSelectableMetadata: false }),
|
||||
new DynamicScrollableDropdownModel({
|
||||
id: 'scrollableDropdown',
|
||||
authorityOptions: authorityOptions,
|
||||
metadataFields: [],
|
||||
repeatable: false,
|
||||
submissionId: '1234'
|
||||
submissionId: '1234',
|
||||
hasSelectableMetadata: false
|
||||
}),
|
||||
new DynamicTagModel({ id: 'tag', metadataFields: [], repeatable: false, submissionId: '1234' }),
|
||||
new DynamicTagModel({ id: 'tag', metadataFields: [], repeatable: false, submissionId: '1234', hasSelectableMetadata: false }),
|
||||
new DynamicListCheckboxGroupModel({
|
||||
id: 'checkboxList',
|
||||
authorityOptions: authorityOptions,
|
||||
@@ -130,11 +134,12 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
|
||||
scopeUUID: '',
|
||||
submissionScope: '',
|
||||
repeatable: false,
|
||||
metadataFields: []
|
||||
metadataFields: [],
|
||||
hasSelectableMetadata: false
|
||||
}),
|
||||
new DynamicDsDatePickerModel({ id: 'datepicker' }),
|
||||
new DynamicLookupModel({ id: 'lookup', metadataFields: [], repeatable: false, submissionId: '1234' }),
|
||||
new DynamicLookupNameModel({ id: 'lookupName', metadataFields: [], repeatable: false, submissionId: '1234' }),
|
||||
new DynamicLookupModel({ id: 'lookup', metadataFields: [], repeatable: false, submissionId: '1234', hasSelectableMetadata: false }),
|
||||
new DynamicLookupNameModel({ id: 'lookupName', metadataFields: [], repeatable: false, submissionId: '1234', hasSelectableMetadata: false}),
|
||||
new DynamicQualdropModel({ id: 'combobox', readOnly: false, required: false })
|
||||
];
|
||||
const testModel = formModel[8];
|
||||
@@ -175,6 +180,9 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
|
||||
{ provide: Store, useValue: {} },
|
||||
{ provide: RelationshipService, useValue: {} },
|
||||
{ provide: SelectableListService, useValue: {} },
|
||||
{ provide: FormService, useValue: {} },
|
||||
{ provide: FormBuilderService, useValue: {} },
|
||||
{ provide: SubmissionService, useValue: {} },
|
||||
{
|
||||
provide: SubmissionObjectDataService,
|
||||
useValue: {
|
||||
@@ -220,7 +228,6 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
|
||||
expect(component.group instanceof FormGroup).toBe(true);
|
||||
expect(component.model instanceof DynamicFormControlModel).toBe(true);
|
||||
expect(component.hasErrorMessaging).toBe(false);
|
||||
expect(component.asBootstrapFormGroup).toBe(true);
|
||||
|
||||
expect(component.onControlValueChanges).toBeDefined();
|
||||
expect(component.onModelDisabledUpdates).toBeDefined();
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
ChangeDetectionStrategy, ChangeDetectorRef,
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
ContentChildren,
|
||||
@@ -16,7 +17,7 @@ import {
|
||||
ViewChild,
|
||||
ViewContainerRef
|
||||
} from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { FormArray, FormGroup } from '@angular/forms';
|
||||
|
||||
import {
|
||||
DYNAMIC_FORM_CONTROL_TYPE_ARRAY,
|
||||
@@ -29,13 +30,18 @@ import {
|
||||
DYNAMIC_FORM_CONTROL_TYPE_SELECT,
|
||||
DYNAMIC_FORM_CONTROL_TYPE_TEXTAREA,
|
||||
DYNAMIC_FORM_CONTROL_TYPE_TIMEPICKER,
|
||||
DynamicDatePickerModel, DynamicFormComponentService,
|
||||
DynamicDatePickerModel,
|
||||
DynamicFormArrayGroupModel,
|
||||
DynamicFormArrayModel,
|
||||
DynamicFormComponentService,
|
||||
DynamicFormControl,
|
||||
DynamicFormControlContainerComponent,
|
||||
DynamicFormControlEvent,
|
||||
DynamicFormControlEventType,
|
||||
DynamicFormControlModel,
|
||||
DynamicFormLayout,
|
||||
DynamicFormLayoutService, DynamicFormRelationService,
|
||||
DynamicFormLayoutService,
|
||||
DynamicFormRelationService,
|
||||
DynamicFormValidationService,
|
||||
DynamicTemplateDirective,
|
||||
} from '@ng-dynamic-forms/core';
|
||||
@@ -50,11 +56,7 @@ import {
|
||||
DynamicNGBootstrapTimePickerComponent
|
||||
} from '@ng-dynamic-forms/ui-ng-bootstrap';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { followLink } from '../../../utils/follow-link-config.model';
|
||||
import {
|
||||
Reorderable,
|
||||
ReorderableRelationship
|
||||
} from './existing-metadata-list-element/existing-metadata-list-element.component';
|
||||
import { ReorderableRelationship } from './existing-metadata-list-element/existing-metadata-list-element.component';
|
||||
|
||||
import { DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD } from './models/typeahead/dynamic-typeahead.model';
|
||||
import { DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN } from './models/scrollable-dropdown/dynamic-scrollable-dropdown.model';
|
||||
@@ -63,7 +65,7 @@ import { DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER } from './models/date-picker/dat
|
||||
import { DYNAMIC_FORM_CONTROL_TYPE_LOOKUP } from './models/lookup/dynamic-lookup.model';
|
||||
import { DynamicListCheckboxGroupModel } from './models/list/dynamic-list-checkbox-group.model';
|
||||
import { DynamicListRadioGroupModel } from './models/list/dynamic-list-radio-group.model';
|
||||
import { hasValue, isNotEmpty, isNotUndefined } from '../../../empty.util';
|
||||
import { hasNoValue, hasValue, isNotEmpty, isNotUndefined } from '../../../empty.util';
|
||||
import { DYNAMIC_FORM_CONTROL_TYPE_LOOKUP_NAME } from './models/lookup/dynamic-lookup-name.model';
|
||||
import { DsDynamicTagComponent } from './models/tag/dynamic-tag.component';
|
||||
import { DsDatePickerComponent } from './models/date-picker/date-picker.component';
|
||||
@@ -78,8 +80,8 @@ import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './models/relation-grou
|
||||
import { DsDatePickerInlineComponent } from './models/date-picker-inline/dynamic-date-picker-inline.component';
|
||||
import { DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH } from './models/custom-switch/custom-switch.model';
|
||||
import { CustomSwitchComponent } from './models/custom-switch/custom-switch.component';
|
||||
import { map, startWith, switchMap, find } from 'rxjs/operators';
|
||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs';
|
||||
import { find, map, startWith, switchMap, take } from 'rxjs/operators';
|
||||
import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs';
|
||||
import { SearchResult } from '../../../search/search-result.model';
|
||||
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
|
||||
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||
@@ -88,7 +90,7 @@ import { SelectableListService } from '../../../object-list/selectable-list/sele
|
||||
import { DsDynamicDisabledComponent } from './models/disabled/dynamic-disabled.component';
|
||||
import { DYNAMIC_FORM_CONTROL_TYPE_DISABLED } from './models/disabled/dynamic-disabled.model';
|
||||
import { DsDynamicLookupRelationModalComponent } from './relation-lookup-modal/dynamic-lookup-relation-modal.component';
|
||||
import { getAllSucceededRemoteData, getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
|
||||
import { getAllSucceededRemoteData, getFirstSucceededRemoteDataPayload, getPaginatedListPayload, getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { ItemDataService } from '../../../../core/data/item-data.service';
|
||||
@@ -98,9 +100,16 @@ import { SubmissionObjectDataService } from '../../../../core/submission/submiss
|
||||
import { SubmissionObject } from '../../../../core/submission/models/submission-object.model';
|
||||
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||
import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model';
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
|
||||
import { Collection } from '../../../../core/shared/collection.model';
|
||||
import { MetadataValue, VIRTUAL_METADATA_PREFIX } from '../../../../core/shared/metadata.models';
|
||||
import { FormService } from '../../form.service';
|
||||
import { SelectableListState } from '../../../object-list/selectable-list/selectable-list.reducer';
|
||||
import { SubmissionService } from '../../../../submission/submission.service';
|
||||
import { followLink } from '../../../utils/follow-link-config.model';
|
||||
import { paginatedRelationsToItems } from '../../../../+item-page/simple/item-types/shared/item-relationships-utils';
|
||||
import { RelationshipOptions } from '../models/relationship-options.model';
|
||||
import { FormBuilderService } from '../form-builder.service';
|
||||
|
||||
export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type<DynamicFormControl> | null {
|
||||
switch (model.type) {
|
||||
@@ -180,22 +189,22 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
|
||||
@Input('templates') inputTemplateList: QueryList<DynamicTemplateDirective>;
|
||||
|
||||
@Input() formId: string;
|
||||
@Input() asBootstrapFormGroup = true;
|
||||
@Input() asBootstrapFormGroup = false;
|
||||
@Input() bindId = true;
|
||||
@Input() context: any | null = null;
|
||||
@Input() group: FormGroup;
|
||||
@Input() hasErrorMessaging = false;
|
||||
@Input() layout = null as DynamicFormLayout;
|
||||
@Input() model: any;
|
||||
reorderables$: Observable<ReorderableRelationship[]>;
|
||||
reorderables: ReorderableRelationship[];
|
||||
hasRelationLookup: boolean;
|
||||
relationshipValue$: Observable<ReorderableRelationship>;
|
||||
isRelationship: boolean;
|
||||
modalRef: NgbModalRef;
|
||||
item: Item;
|
||||
item$: Observable<Item>;
|
||||
collection: Collection;
|
||||
listId: string;
|
||||
searchConfig: string;
|
||||
|
||||
value: MetadataValue;
|
||||
/**
|
||||
* List of subscriptions to unsubscribe from
|
||||
*/
|
||||
@@ -207,7 +216,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
|
||||
@Output('dfFocus') focus: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
|
||||
@Output('ngbEvent') customEvent: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
|
||||
/* tslint:enable:no-output-rename */
|
||||
@ViewChild('componentViewContainer', { read: ViewContainerRef, static: true}) componentViewContainerRef: ViewContainerRef;
|
||||
@ViewChild('componentViewContainer', { read: ViewContainerRef, static: true }) componentViewContainerRef: ViewContainerRef;
|
||||
|
||||
private showErrorMessagesPreviousStage: boolean;
|
||||
|
||||
@@ -229,9 +238,11 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
|
||||
private zone: NgZone,
|
||||
private store: Store<AppState>,
|
||||
private submissionObjectService: SubmissionObjectDataService,
|
||||
private ref: ChangeDetectorRef
|
||||
private ref: ChangeDetectorRef,
|
||||
private formService: FormService,
|
||||
private formBuilderService: FormBuilderService,
|
||||
private submissionService: SubmissionService
|
||||
) {
|
||||
|
||||
super(componentFactoryResolver, layoutService, validationService, dynamicFormComponentService, relationService);
|
||||
}
|
||||
|
||||
@@ -239,62 +250,76 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
|
||||
* Sets up the necessary variables for when this control can be used to add relationships to the submitted item
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.hasRelationLookup = hasValue(this.model.relationship);
|
||||
this.reorderables = [];
|
||||
if (this.hasRelationLookup) {
|
||||
this.isRelationship = hasValue(this.model.relationship);
|
||||
const isWrapperAroundRelationshipList = hasValue(this.model.relationshipConfig);
|
||||
|
||||
this.listId = 'list-' + this.model.relationship.relationshipType;
|
||||
if (this.isRelationship || isWrapperAroundRelationshipList) {
|
||||
const config = this.model.relationshipConfig || this.model.relationship;
|
||||
const relationshipOptions = Object.assign(new RelationshipOptions(), config);
|
||||
this.listId = `list-${this.model.submissionId}-${relationshipOptions.relationshipType}`;
|
||||
this.setItem();
|
||||
|
||||
const submissionObject$ = this.submissionObjectService
|
||||
.findById(this.model.submissionId, followLink('item'), followLink('collection')).pipe(
|
||||
getAllSucceededRemoteData(),
|
||||
getRemoteDataPayload()
|
||||
);
|
||||
if (isWrapperAroundRelationshipList || !this.model.repeatable) {
|
||||
const subscription = this.selectableListService.getSelectableList(this.listId).pipe(
|
||||
find((list: SelectableListState) => hasNoValue(list)),
|
||||
switchMap(() => this.item$.pipe(take(1))),
|
||||
switchMap((item) => {
|
||||
const relationshipsRD$ = this.relationshipService.getItemRelationshipsByLabel(item,
|
||||
relationshipOptions.relationshipType,
|
||||
undefined,
|
||||
followLink('leftItem'),
|
||||
followLink('rightItem'),
|
||||
followLink('relationshipType')
|
||||
);
|
||||
|
||||
const item$ = submissionObject$.pipe(switchMap((submissionObject: SubmissionObject) => (submissionObject.item as Observable<RemoteData<Item>>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload())));
|
||||
const collection$ = submissionObject$.pipe(switchMap((submissionObject: SubmissionObject) => (submissionObject.collection as Observable<RemoteData<Collection>>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload())));
|
||||
relationshipsRD$.pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
getPaginatedListPayload()
|
||||
).subscribe((relationships: Relationship[]) => {
|
||||
// set initial namevariants for pre-existing relationships
|
||||
relationships.forEach((relationship: Relationship) => {
|
||||
const relationshipMD: MetadataValue = item.firstMetadata(relationshipOptions.metadataField, { authority: `${VIRTUAL_METADATA_PREFIX}${relationship.id}` });
|
||||
const nameVariantMD: MetadataValue = item.firstMetadata(this.model.metadataFields, { authority: `${VIRTUAL_METADATA_PREFIX}${relationship.id}` });
|
||||
if (hasValue(relationshipMD) && isNotEmpty(relationshipMD.value) && hasValue(nameVariantMD) && isNotEmpty(nameVariantMD.value)) {
|
||||
this.relationshipService.setNameVariant(this.listId, relationshipMD.value, nameVariantMD.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.subs.push(item$.subscribe((item) => this.item = item));
|
||||
this.subs.push(collection$.subscribe((collection) => this.collection = collection));
|
||||
this.reorderables$ = item$.pipe(
|
||||
switchMap((item) => this.relationshipService.getItemRelationshipsByLabel(item, this.model.relationship.relationshipType, undefined, followLink('leftItem'), followLink('rightItem'), followLink('relationshipType'))
|
||||
return relationshipsRD$.pipe(
|
||||
paginatedRelationsToItems(item.uuid),
|
||||
getSucceededRemoteData(),
|
||||
map((items: RemoteData<PaginatedList<Item>>) => items.payload.page.map((i) => Object.assign(new ItemSearchResult(), { indexableObject: i }))),
|
||||
)
|
||||
})
|
||||
).subscribe((relatedItems: Array<SearchResult<Item>>) => this.selectableListService.select(this.listId, relatedItems));
|
||||
this.subs.push(subscription);
|
||||
}
|
||||
|
||||
if (hasValue(this.model.metadataValue)) {
|
||||
this.value = Object.assign(new MetadataValue(), this.model.metadataValue);
|
||||
} else {
|
||||
this.value = Object.assign(new MetadataValue(), this.model.value);
|
||||
}
|
||||
|
||||
if (hasValue(this.value) && this.value.isVirtual) {
|
||||
const relationship$ = this.relationshipService.findById(this.value.virtualValue, followLink('leftItem'), followLink('rightItem'), followLink('relationshipType'))
|
||||
.pipe(
|
||||
getAllSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
map((relationshipList: PaginatedList<Relationship>) => relationshipList.page),
|
||||
startWith([]),
|
||||
switchMap((relationships: Relationship[]) =>
|
||||
observableCombineLatest(
|
||||
relationships.map((relationship: Relationship) =>
|
||||
relationship.leftItem.pipe(
|
||||
getSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
map((leftItem: Item) => {
|
||||
return new ReorderableRelationship(relationship, leftItem.uuid !== this.item.uuid)
|
||||
}),
|
||||
)
|
||||
))),
|
||||
map((relationships: ReorderableRelationship[]) =>
|
||||
relationships
|
||||
.sort((a: Reorderable, b: Reorderable) => {
|
||||
return Math.sign(a.getPlace() - b.getPlace());
|
||||
})
|
||||
getRemoteDataPayload());
|
||||
this.relationshipValue$ = observableCombineLatest([this.item$.pipe(take(1)), relationship$]).pipe(
|
||||
switchMap(([item, relationship]: [Item, Relationship]) =>
|
||||
relationship.leftItem.pipe(
|
||||
getAllSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
map((leftItem: Item) => {
|
||||
return new ReorderableRelationship(relationship, leftItem.uuid !== item.uuid, this.relationshipService, this.store, this.model.submissionId)
|
||||
}),
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
this.subs.push(this.reorderables$.subscribe((rs) => {
|
||||
this.reorderables = rs;
|
||||
this.ref.detectChanges();
|
||||
}));
|
||||
|
||||
item$.pipe(
|
||||
switchMap((item) => this.relationshipService.getRelatedItemsByLabel(item, this.model.relationship.relationshipType)),
|
||||
map((items: RemoteData<PaginatedList<Item>>) => items.payload.page.map((item) => Object.assign(new ItemSearchResult(), { indexableObject: item }))),
|
||||
).subscribe((relatedItems: Array<SearchResult<Item>>) => {
|
||||
this.selectableListService.select(this.listId, relatedItems)
|
||||
});
|
||||
),
|
||||
startWith(undefined)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,7 +328,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (changes) {
|
||||
if (changes && !this.isRelationship && hasValue(this.group.get(this.model.id))) {
|
||||
super.ngOnChanges(changes);
|
||||
if (this.model && this.model.placeholder) {
|
||||
this.model.placeholder = this.translateService.instant(this.model.placeholder);
|
||||
@@ -351,6 +376,27 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
|
||||
size: 'lg'
|
||||
});
|
||||
const modalComp = this.modalRef.componentInstance;
|
||||
|
||||
if (hasValue(this.model.value) && !this.model.readOnly) {
|
||||
if (typeof this.model.value === 'string') {
|
||||
modalComp.query = this.model.value;
|
||||
} else if (typeof this.model.value.value === 'string') {
|
||||
modalComp.query = this.model.value.value;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasValue(this.model.value)) {
|
||||
this.model.value = '';
|
||||
this.onChange({
|
||||
$event: { previousIndex: 0 },
|
||||
context: { index: 0 },
|
||||
control: this.control,
|
||||
model: this.model,
|
||||
type: DynamicFormControlEventType.Change
|
||||
});
|
||||
}
|
||||
this.submissionService.dispatchSave(this.model.submissionId);
|
||||
|
||||
modalComp.repeatable = this.model.repeatable;
|
||||
modalComp.listId = this.listId;
|
||||
modalComp.relationshipOptions = this.model.relationship;
|
||||
@@ -358,32 +404,18 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
|
||||
modalComp.metadataFields = this.model.metadataFields;
|
||||
modalComp.item = this.item;
|
||||
modalComp.collection = this.collection;
|
||||
modalComp.submissionId = this.model.submissionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to move a relationship inside the list of relationships
|
||||
* This will update the view and update the right or left place field of the relationships in the list
|
||||
* @param event
|
||||
* Callback for the remove event,
|
||||
* remove the current control from its array
|
||||
*/
|
||||
moveSelection(event: CdkDragDrop<Relationship>) {
|
||||
this.zone.runOutsideAngular(() => {
|
||||
moveItemInArray(this.reorderables, event.previousIndex, event.currentIndex);
|
||||
const reorderables: Reorderable[] = this.reorderables.map((reo: Reorderable, index: number) => {
|
||||
reo.oldIndex = reo.getPlace();
|
||||
reo.newIndex = index;
|
||||
return reo;
|
||||
}
|
||||
);
|
||||
observableCombineLatest(
|
||||
reorderables.map((rel: ReorderableRelationship) => {
|
||||
if (rel.oldIndex !== rel.newIndex) {
|
||||
return this.relationshipService.updatePlace(rel);
|
||||
} else {
|
||||
return observableOf(undefined) as Observable<RemoteData<Relationship>>;
|
||||
}
|
||||
})
|
||||
).subscribe();
|
||||
})
|
||||
onRemove(): void {
|
||||
const arrayContext: DynamicFormArrayModel = (this.context as DynamicFormArrayGroupModel).context;
|
||||
const path = this.formBuilderService.getPath(arrayContext);
|
||||
const formArrayControl = this.group.root.get(path) as FormArray;
|
||||
this.formBuilderService.removeFormArrayGroup(this.context.index, formArrayControl, arrayContext);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -396,9 +428,20 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent unnecessary rerendering so fields don't lose focus
|
||||
* Initialize this.item$ based on this.model.submissionId
|
||||
*/
|
||||
trackReorderable(index, reorderable: Reorderable) {
|
||||
return hasValue(reorderable) ? reorderable.getId() : undefined;
|
||||
private setItem() {
|
||||
const submissionObject$ = this.submissionObjectService
|
||||
.findById(this.model.submissionId, followLink('item'), followLink('collection')).pipe(
|
||||
getAllSucceededRemoteData(),
|
||||
getRemoteDataPayload()
|
||||
);
|
||||
|
||||
this.item$ = submissionObject$.pipe(switchMap((submissionObject: SubmissionObject) => (submissionObject.item as Observable<RemoteData<Item>>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload())));
|
||||
const collection$ = submissionObject$.pipe(switchMap((submissionObject: SubmissionObject) => (submissionObject.collection as Observable<RemoteData<Collection>>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload())));
|
||||
|
||||
this.subs.push(this.item$.subscribe((item) => this.item = item));
|
||||
this.subs.push(collection$.subscribe((collection) => this.collection = collection));
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@
|
||||
[model]="model"
|
||||
[ngClass]="[getClass(model, 'element', 'host'), getClass(model, 'grid', 'host')]"
|
||||
[templates]="templates"
|
||||
[asBootstrapFormGroup]="true"
|
||||
(dfBlur)="onEvent($event, 'blur')"
|
||||
(dfChange)="onEvent($event, 'change')"
|
||||
(dfFocus)="onEvent($event, 'focus')"></ds-dynamic-form-control-container>
|
||||
|
@@ -37,5 +37,4 @@ export class DsDynamicFormComponent extends DynamicFormComponent {
|
||||
constructor(protected formService: FormBuilderService, protected layoutService: DynamicFormLayoutService) {
|
||||
super(formService, layoutService);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,11 +1,14 @@
|
||||
<li *ngIf="metadataRepresentation">
|
||||
<button type="button" class="close float-left" aria-label="Move button" cdkDragHandle>
|
||||
<i aria-hidden="true" class="fas fa-arrows-alt fa-xs"></i>
|
||||
<div class="d-flex">
|
||||
<span class="mr-auto text-contents">
|
||||
<ng-container *ngIf="!(metadataRepresentation$ | async)">
|
||||
<ds-loading [showMessage]="false"></ds-loading>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="(metadataRepresentation$ | async)">
|
||||
<ds-metadata-representation-loader [mdRepresentation]="metadataRepresentation$ | async"></ds-metadata-representation-loader>
|
||||
</ng-container>
|
||||
</span>
|
||||
<button type="button" class="btn btn-secondary"
|
||||
(click)="removeSelection()">
|
||||
<i class="fas fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button type="button" class="close float-left" aria-label="Close button" (click)="removeSelection()">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<span class="d-inline-block align-middle ml-1">
|
||||
<ds-metadata-representation-loader [mdRepresentation]="metadataRepresentation"></ds-metadata-representation-loader>
|
||||
</span>
|
||||
</li>
|
||||
</div>
|
||||
|
@@ -0,0 +1,3 @@
|
||||
span.text-contents{
|
||||
padding: $btn-padding-y 0;
|
||||
}
|
||||
|
@@ -10,6 +10,8 @@ import { RelationshipOptions } from '../../models/relationship-options.model';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../../remote-data.utils';
|
||||
import { RemoveRelationshipAction } from '../relation-lookup-modal/relationship.actions';
|
||||
import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { RelationshipService } from '../../../../../core/data/relationship.service';
|
||||
|
||||
describe('ExistingMetadataListElementComponent', () => {
|
||||
let component: ExistingMetadataListElementComponent;
|
||||
@@ -28,6 +30,8 @@ describe('ExistingMetadataListElementComponent', () => {
|
||||
let leftItemRD$;
|
||||
let rightItemRD$;
|
||||
let relatedSearchResult;
|
||||
let submissionId;
|
||||
let relationshipService;
|
||||
|
||||
function init() {
|
||||
uuid1 = '91ce578d-2e63-4093-8c73-3faafd716000';
|
||||
@@ -42,9 +46,13 @@ describe('ExistingMetadataListElementComponent', () => {
|
||||
leftItemRD$ = createSuccessfulRemoteDataObject$(relatedItem);
|
||||
rightItemRD$ = createSuccessfulRemoteDataObject$(submissionItem);
|
||||
relatedSearchResult = Object.assign(new ItemSearchResult(), { indexableObject: relatedItem });
|
||||
relationshipService = {
|
||||
updatePlace:() => observableOf({})
|
||||
} as any;
|
||||
|
||||
relationship = Object.assign(new Relationship(), { leftItem: leftItemRD$, rightItem: rightItemRD$ });
|
||||
reoRel = new ReorderableRelationship(relationship, true);
|
||||
submissionId = '1234';
|
||||
reoRel = new ReorderableRelationship(relationship, true, relationshipService, {} as any, submissionId);
|
||||
}
|
||||
|
||||
beforeEach(async(() => {
|
||||
@@ -68,6 +76,7 @@ describe('ExistingMetadataListElementComponent', () => {
|
||||
component.reoRel = reoRel;
|
||||
component.metadataFields = metadataFields;
|
||||
component.relationshipOptions = relationshipOptions;
|
||||
component.submissionId = submissionId;
|
||||
fixture.detectChanges();
|
||||
component.ngOnChanges();
|
||||
});
|
||||
@@ -84,9 +93,8 @@ describe('ExistingMetadataListElementComponent', () => {
|
||||
|
||||
it('should dispatch a RemoveRelationshipAction', () => {
|
||||
component.removeSelection();
|
||||
const action = new RemoveRelationshipAction(submissionItem, relatedItem, relationshipOptions.relationshipType);
|
||||
const action = new RemoveRelationshipAction(submissionItem, relatedItem, relationshipOptions.relationshipType, submissionId);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(action);
|
||||
|
||||
});
|
||||
})
|
||||
});
|
||||
|
@@ -1,50 +1,126 @@
|
||||
import { Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { DynamicFormArrayGroupModel } from '@ng-dynamic-forms/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { BehaviorSubject, Subscription } from 'rxjs';
|
||||
import { filter } from 'rxjs/operators';
|
||||
import { AppState } from '../../../../../app.reducer';
|
||||
import { RelationshipService } from '../../../../../core/data/relationship.service';
|
||||
import { Relationship } from '../../../../../core/shared/item-relationships/relationship.model';
|
||||
import { Item } from '../../../../../core/shared/item.model';
|
||||
import { ItemMetadataRepresentation } from '../../../../../core/shared/metadata-representation/item/item-metadata-representation.model';
|
||||
import { MetadataRepresentation } from '../../../../../core/shared/metadata-representation/metadata-representation.model';
|
||||
import { MetadataValue } from '../../../../../core/shared/metadata.models';
|
||||
import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../../core/shared/operators';
|
||||
import { hasValue, isNotEmpty } from '../../../../empty.util';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { filter } from 'rxjs/operators';
|
||||
import { Relationship } from '../../../../../core/shared/item-relationships/relationship.model';
|
||||
import { MetadataValue } from '../../../../../core/shared/metadata.models';
|
||||
import { ItemMetadataRepresentation } from '../../../../../core/shared/metadata-representation/item/item-metadata-representation.model';
|
||||
import { RelationshipOptions } from '../../models/relationship-options.model';
|
||||
import { RemoveRelationshipAction } from '../relation-lookup-modal/relationship.actions';
|
||||
import { SelectableListService } from '../../../../object-list/selectable-list/selectable-list.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppState } from '../../../../../app.reducer';
|
||||
import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model';
|
||||
import { SelectableListService } from '../../../../object-list/selectable-list/selectable-list.service';
|
||||
import { FormFieldMetadataValueObject } from '../../models/form-field-metadata-value.model';
|
||||
import { RelationshipOptions } from '../../models/relationship-options.model';
|
||||
import { DynamicConcatModel } from '../models/ds-dynamic-concat.model';
|
||||
import { RemoveRelationshipAction } from '../relation-lookup-modal/relationship.actions';
|
||||
|
||||
// tslint:disable:max-classes-per-file
|
||||
/**
|
||||
* Abstract class that defines objects that can be reordered
|
||||
*/
|
||||
export abstract class Reorderable {
|
||||
|
||||
constructor(public oldIndex?: number, public newIndex?: number) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the id for this Reorderable
|
||||
*/
|
||||
abstract getId(): string;
|
||||
|
||||
/**
|
||||
* Return the place metadata for this Reorderable
|
||||
*/
|
||||
abstract getPlace(): number;
|
||||
|
||||
/**
|
||||
* Update the Reorderable
|
||||
*/
|
||||
update(): void {
|
||||
this.oldIndex = this.newIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the oldIndex of this Reorderable
|
||||
* differs from the newIndex
|
||||
*/
|
||||
get hasMoved(): boolean {
|
||||
return this.oldIndex !== this.newIndex
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A Reorderable representation of a FormFieldMetadataValue
|
||||
*/
|
||||
export class ReorderableFormFieldMetadataValue extends Reorderable {
|
||||
|
||||
constructor(
|
||||
public metadataValue: FormFieldMetadataValueObject,
|
||||
public model: DynamicConcatModel,
|
||||
public control: FormControl,
|
||||
public group: DynamicFormArrayGroupModel,
|
||||
oldIndex?: number,
|
||||
newIndex?: number
|
||||
) {
|
||||
super(oldIndex, newIndex);
|
||||
this.metadataValue = metadataValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the id for this Reorderable
|
||||
*/
|
||||
getId(): string {
|
||||
if (hasValue(this.metadataValue.authority)) {
|
||||
return this.metadataValue.authority;
|
||||
} else {
|
||||
// can't use UUIDs, they're generated client side
|
||||
return this.metadataValue.value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the place metadata for this Reorderable
|
||||
*/
|
||||
getPlace(): number {
|
||||
return this.metadataValue.place;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a single relationship that can be reordered in a list of multiple relationships
|
||||
*/
|
||||
export class ReorderableRelationship extends Reorderable {
|
||||
relationship: Relationship;
|
||||
useLeftItem: boolean;
|
||||
|
||||
constructor(relationship: Relationship, useLeftItem: boolean, oldIndex?: number, newIndex?: number) {
|
||||
constructor(
|
||||
public relationship: Relationship,
|
||||
public useLeftItem: boolean,
|
||||
protected relationshipService: RelationshipService,
|
||||
protected store: Store<AppState>,
|
||||
protected submissionID: string,
|
||||
oldIndex?: number,
|
||||
newIndex?: number) {
|
||||
super(oldIndex, newIndex);
|
||||
this.relationship = relationship;
|
||||
this.useLeftItem = useLeftItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the id for this Reorderable
|
||||
*/
|
||||
getId(): string {
|
||||
return this.relationship.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the place metadata for this Reorderable
|
||||
*/
|
||||
getPlace(): number {
|
||||
if (this.useLeftItem) {
|
||||
return this.relationship.rightPlace
|
||||
@@ -62,15 +138,16 @@ export class ReorderableRelationship extends Reorderable {
|
||||
templateUrl: './existing-metadata-list-element.component.html',
|
||||
styleUrls: ['./existing-metadata-list-element.component.scss']
|
||||
})
|
||||
export class ExistingMetadataListElementComponent implements OnChanges, OnDestroy {
|
||||
export class ExistingMetadataListElementComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() listId: string;
|
||||
@Input() submissionItem: Item;
|
||||
@Input() reoRel: ReorderableRelationship;
|
||||
@Input() metadataFields: string[];
|
||||
@Input() relationshipOptions: RelationshipOptions;
|
||||
metadataRepresentation: MetadataRepresentation;
|
||||
@Input() submissionId: string;
|
||||
metadataRepresentation$: BehaviorSubject<MetadataRepresentation> = new BehaviorSubject<MetadataRepresentation>(undefined);
|
||||
relatedItem: Item;
|
||||
|
||||
@Output() remove: EventEmitter<any> = new EventEmitter();
|
||||
/**
|
||||
* List of subscriptions to unsubscribe from
|
||||
*/
|
||||
@@ -82,24 +159,35 @@ export class ExistingMetadataListElementComponent implements OnChanges, OnDestro
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.ngOnChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change callback for the component
|
||||
*/
|
||||
ngOnChanges() {
|
||||
const item$ = this.reoRel.useLeftItem ?
|
||||
this.reoRel.relationship.leftItem : this.reoRel.relationship.rightItem;
|
||||
this.subs.push(item$.pipe(
|
||||
getAllSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
filter((item: Item) => hasValue(item) && isNotEmpty(item.uuid))
|
||||
).subscribe((item: Item) => {
|
||||
this.relatedItem = item;
|
||||
const relationMD: MetadataValue = this.submissionItem.firstMetadata(this.relationshipOptions.metadataField, { value: this.relatedItem.uuid });
|
||||
if (hasValue(relationMD)) {
|
||||
const metadataRepresentationMD: MetadataValue = this.submissionItem.firstMetadata(this.metadataFields, { authority: relationMD.authority });
|
||||
this.metadataRepresentation = Object.assign(
|
||||
new ItemMetadataRepresentation(metadataRepresentationMD),
|
||||
this.relatedItem
|
||||
)
|
||||
}
|
||||
}));
|
||||
if (hasValue(this.reoRel)) {
|
||||
const item$ = this.reoRel.useLeftItem ?
|
||||
this.reoRel.relationship.leftItem : this.reoRel.relationship.rightItem;
|
||||
this.subs.push(item$.pipe(
|
||||
getAllSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
filter((item: Item) => hasValue(item) && isNotEmpty(item.uuid))
|
||||
).subscribe((item: Item) => {
|
||||
this.relatedItem = item;
|
||||
const relationMD: MetadataValue = this.submissionItem.firstMetadata(this.relationshipOptions.metadataField, { value: this.relatedItem.uuid });
|
||||
if (hasValue(relationMD)) {
|
||||
const metadataRepresentationMD: MetadataValue = this.submissionItem.firstMetadata(this.metadataFields, { authority: relationMD.authority });
|
||||
|
||||
const nextValue = Object.assign(
|
||||
new ItemMetadataRepresentation(metadataRepresentationMD),
|
||||
this.relatedItem
|
||||
);
|
||||
this.metadataRepresentation$.next(nextValue);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,7 +195,8 @@ export class ExistingMetadataListElementComponent implements OnChanges, OnDestro
|
||||
*/
|
||||
removeSelection() {
|
||||
this.selectableListService.deselectSingle(this.listId, Object.assign(new ItemSearchResult(), { indexableObject: this.relatedItem }));
|
||||
this.store.dispatch(new RemoveRelationshipAction(this.submissionItem, this.relatedItem, this.relationshipOptions.relationshipType))
|
||||
this.store.dispatch(new RemoveRelationshipAction(this.submissionItem, this.relatedItem, this.relationshipOptions.relationshipType, this.submissionId));
|
||||
this.remove.emit();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,4 +209,5 @@ export class ExistingMetadataListElementComponent implements OnChanges, OnDestro
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// tslint:enable:max-classes-per-file
|
||||
|
@@ -0,0 +1,14 @@
|
||||
<div class="d-flex">
|
||||
<span class="mr-auto text-contents">
|
||||
<ng-container *ngIf="!(relatedItem$ | async)">
|
||||
<ds-loading [showMessage]="false"></ds-loading>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="(relatedItem$ | async)">
|
||||
<ds-listable-object-component-loader [showLabel]="false" [viewMode]="viewType" [object]="(relatedItem$ | async)"></ds-listable-object-component-loader>
|
||||
</ng-container>
|
||||
</span>
|
||||
<button type="button" class="btn btn-secondary"
|
||||
(click)="removeSelection()">
|
||||
<i class="fas fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
@@ -0,0 +1,3 @@
|
||||
span.text-contents{
|
||||
padding: $btn-padding-y 0;
|
||||
}
|
@@ -0,0 +1,100 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ExistingRelationListElementComponent } from './existing-relation-list-element.component';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { SelectableListService } from '../../../../object-list/selectable-list/selectable-list.service';
|
||||
import { select, Store } from '@ngrx/store';
|
||||
import { Item } from '../../../../../core/shared/item.model';
|
||||
import { Relationship } from '../../../../../core/shared/item-relationships/relationship.model';
|
||||
import { RelationshipOptions } from '../../models/relationship-options.model';
|
||||
import { RemoveRelationshipAction } from '../relation-lookup-modal/relationship.actions';
|
||||
import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { ReorderableRelationship } from '../existing-metadata-list-element/existing-metadata-list-element.component';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../../remote-data.utils';
|
||||
|
||||
describe('ExistingRelationListElementComponent', () => {
|
||||
let component: ExistingRelationListElementComponent;
|
||||
let fixture: ComponentFixture<ExistingRelationListElementComponent>;
|
||||
let selectionService;
|
||||
let store;
|
||||
let listID;
|
||||
let submissionItem;
|
||||
let relationship;
|
||||
let reoRel;
|
||||
let metadataFields;
|
||||
let relationshipOptions;
|
||||
let uuid1;
|
||||
let uuid2;
|
||||
let relatedItem;
|
||||
let leftItemRD$;
|
||||
let rightItemRD$;
|
||||
let relatedSearchResult;
|
||||
let submissionId;
|
||||
let relationshipService;
|
||||
|
||||
function init() {
|
||||
uuid1 = '91ce578d-2e63-4093-8c73-3faafd716000';
|
||||
uuid2 = '0e9dba1c-e1c3-4e05-a539-446f08ef57a7';
|
||||
selectionService = jasmine.createSpyObj('selectionService', ['deselectSingle']);
|
||||
store = jasmine.createSpyObj('store', ['dispatch']);
|
||||
listID = '1234-listID';
|
||||
submissionItem = Object.assign(new Item(), { uuid: uuid1 });
|
||||
metadataFields = ['dc.contributor.author'];
|
||||
relationshipOptions = Object.assign(new RelationshipOptions(), { relationshipType: 'isPublicationOfAuthor', filter: 'test.filter', searchConfiguration: 'personConfiguration', nameVariants: true })
|
||||
relatedItem = Object.assign(new Item(), { uuid: uuid2 });
|
||||
leftItemRD$ = createSuccessfulRemoteDataObject$(relatedItem);
|
||||
rightItemRD$ = createSuccessfulRemoteDataObject$(submissionItem);
|
||||
relatedSearchResult = Object.assign(new ItemSearchResult(), { indexableObject: relatedItem });
|
||||
relationshipService = {
|
||||
updatePlace:() => observableOf({})
|
||||
} as any;
|
||||
|
||||
relationship = Object.assign(new Relationship(), { leftItem: leftItemRD$, rightItem: rightItemRD$ });
|
||||
submissionId = '1234';
|
||||
reoRel = new ReorderableRelationship(relationship, true, relationshipService, {} as any, submissionId);
|
||||
}
|
||||
|
||||
beforeEach(async(() => {
|
||||
init();
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ExistingRelationListElementComponent],
|
||||
providers: [
|
||||
{ provide: SelectableListService, useValue: selectionService },
|
||||
{ provide: Store, useValue: store },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ExistingRelationListElementComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.listId = listID;
|
||||
component.submissionItem = submissionItem;
|
||||
component.reoRel = reoRel;
|
||||
component.metadataFields = metadataFields;
|
||||
component.relationshipOptions = relationshipOptions;
|
||||
component.submissionId = submissionId;
|
||||
fixture.detectChanges();
|
||||
component.ngOnChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('removeSelection', () => {
|
||||
it('should deselect the object in the selectable list service', () => {
|
||||
component.removeSelection();
|
||||
expect(selectionService.deselectSingle).toHaveBeenCalledWith(listID, relatedSearchResult);
|
||||
});
|
||||
|
||||
it('should dispatch a RemoveRelationshipAction', () => {
|
||||
component.removeSelection();
|
||||
const action = new RemoveRelationshipAction(submissionItem, relatedItem, relationshipOptions.relationshipType, submissionId);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(action);
|
||||
});
|
||||
})
|
||||
});
|
@@ -0,0 +1,120 @@
|
||||
import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
||||
import { filter } from 'rxjs/operators';
|
||||
import { AppState } from '../../../../../app.reducer';
|
||||
import { Item } from '../../../../../core/shared/item.model';
|
||||
import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../../core/shared/operators';
|
||||
import { hasValue, isNotEmpty } from '../../../../empty.util';
|
||||
import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model';
|
||||
import { SelectableListService } from '../../../../object-list/selectable-list/selectable-list.service';
|
||||
import { RelationshipOptions } from '../../models/relationship-options.model';
|
||||
import { RemoveRelationshipAction } from '../relation-lookup-modal/relationship.actions';
|
||||
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||
import { ReorderableRelationship } from '../existing-metadata-list-element/existing-metadata-list-element.component';
|
||||
|
||||
// tslint:disable:max-classes-per-file
|
||||
/**
|
||||
* Abstract class that defines objects that can be reordered
|
||||
*/
|
||||
export abstract class Reorderable {
|
||||
|
||||
constructor(public oldIndex?: number, public newIndex?: number) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the id for this Reorderable
|
||||
*/
|
||||
abstract getId(): string;
|
||||
|
||||
/**
|
||||
* Return the place metadata for this Reorderable
|
||||
*/
|
||||
abstract getPlace(): number;
|
||||
|
||||
/**
|
||||
* Update the Reorderable
|
||||
*/
|
||||
abstract update(): Observable<any>;
|
||||
|
||||
/**
|
||||
* Returns true if the oldIndex of this Reorderable
|
||||
* differs from the newIndex
|
||||
*/
|
||||
get hasMoved(): boolean {
|
||||
return this.oldIndex !== this.newIndex
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a single existing relationship value as metadata in submission
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-existing-relation-list-element',
|
||||
templateUrl: './existing-relation-list-element.component.html',
|
||||
styleUrls: ['./existing-relation-list-element.component.scss']
|
||||
})
|
||||
export class ExistingRelationListElementComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() listId: string;
|
||||
@Input() submissionItem: Item;
|
||||
@Input() reoRel: ReorderableRelationship;
|
||||
@Input() metadataFields: string[];
|
||||
@Input() relationshipOptions: RelationshipOptions;
|
||||
@Input() submissionId: string;
|
||||
relatedItem$: BehaviorSubject<Item> = new BehaviorSubject<Item>(undefined);
|
||||
viewType = ViewMode.ListElement;
|
||||
@Output() remove: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
/**
|
||||
* List of subscriptions to unsubscribe from
|
||||
*/
|
||||
private subs: Subscription[] = [];
|
||||
|
||||
constructor(
|
||||
private selectableListService: SelectableListService,
|
||||
private store: Store<AppState>
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.ngOnChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change callback for the component
|
||||
*/
|
||||
ngOnChanges() {
|
||||
if (hasValue(this.reoRel)) {
|
||||
const item$ = this.reoRel.useLeftItem ?
|
||||
this.reoRel.relationship.leftItem : this.reoRel.relationship.rightItem;
|
||||
this.subs.push(item$.pipe(
|
||||
getAllSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
filter((item: Item) => hasValue(item) && isNotEmpty(item.uuid))
|
||||
).subscribe((item: Item) => {
|
||||
this.relatedItem$.next(item);
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the selected relationship from the list
|
||||
*/
|
||||
removeSelection() {
|
||||
this.selectableListService.deselectSingle(this.listId, Object.assign(new ItemSearchResult(), { indexableObject: this.relatedItem$.getValue() }));
|
||||
this.store.dispatch(new RemoveRelationshipAction(this.submissionItem, this.relatedItem$.getValue(), this.relationshipOptions.relationshipType, this.submissionId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from all subscriptions
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.subs
|
||||
.filter((sub) => hasValue(sub))
|
||||
.forEach((sub) => sub.unsubscribe());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// tslint:enable:max-classes-per-file
|
@@ -1,32 +1,45 @@
|
||||
<ng-container [formGroup]="group">
|
||||
|
||||
<div [dynamicId]="bindId && model.id"
|
||||
[formArrayName]="model.id"
|
||||
[ngClass]="getClass('element', 'control')">
|
||||
|
||||
<div *ngFor="let groupModel of model.groups; let idx = index" role="group"
|
||||
[formGroupName]="idx" [ngClass]="[getClass('element', 'group'), getClass('grid', 'group')]">
|
||||
|
||||
<ng-container *ngTemplateOutlet="startTemplate?.templateRef; context: groupModel"></ng-container>
|
||||
|
||||
<ds-dynamic-form-control-container *ngFor="let _model of groupModel.group"
|
||||
[bindId]="false"
|
||||
[context]="groupModel"
|
||||
[group]="control.get([idx])"
|
||||
[hidden]="_model.hidden"
|
||||
[layout]="layout"
|
||||
[model]="_model"
|
||||
[templates]="templates"
|
||||
[ngClass]="[getClass('element', 'host', _model), getClass('grid', 'host', _model)]"
|
||||
(dfBlur)="onBlur($event)"
|
||||
(dfChange)="onChange($event)"
|
||||
(dfFocus)="onFocus($event)"
|
||||
(ngbEvent)="onCustomEvent($event, null, true)"></ds-dynamic-form-control-container>
|
||||
|
||||
<ng-container *ngTemplateOutlet="endTemplate?.templateRef; context: groupModel"></ng-container>
|
||||
|
||||
<div role="group"
|
||||
formGroupName="0" [ngClass]="[getClass('element', 'group'), getClass('grid', 'group')]">
|
||||
<ng-container *ngTemplateOutlet="startTemplate?.templateRef; context: model.groups[0]"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="controlContainer; context: {$implicit: 0}"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="endTemplate?.templateRef; context: model.groups[0]"></ng-container>
|
||||
</div>
|
||||
<div cdkDropList cdkDropListLockAxis="y" (cdkDropListDropped)="moveSelection($event)">
|
||||
<div *ngFor="let groupModel of model.groups; let idx = index"
|
||||
[ngClass]="{'pt-2 pb-2': idx > 0}" cdkDrag cdkDragHandle>
|
||||
<div [formGroupName]="idx"
|
||||
[class]="getClass('element', 'group') + ' ' + getClass('grid', 'group')"
|
||||
[ngClass]="{'d-flex align-items-center': idx > 0}"
|
||||
>
|
||||
<ng-container *ngIf="idx > 0">
|
||||
<i class="drag-icon fas fa-grip-vertical fa-fw"></i>
|
||||
<ng-container *ngTemplateOutlet="startTemplate?.templateRef; context: groupModel"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="controlContainer; context: {$implicit: idx}"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="endTemplate?.templateRef; context: groupModel"></ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
||||
|
||||
<ng-template #controlContainer let-idx>
|
||||
<ds-dynamic-form-control-container *ngFor="let _model of model.groups[idx].group"
|
||||
[bindId]="false"
|
||||
[context]="model.groups[idx]"
|
||||
[group]="control.get([idx])"
|
||||
[hidden]="_model.hidden"
|
||||
[layout]="layout"
|
||||
[model]="_model"
|
||||
[templates]="templates"
|
||||
[ngClass]="[getClass('element', 'host', _model), getClass('grid', 'host', _model)]"
|
||||
(dfBlur)="update($event, idx)"
|
||||
(dfChange)="update($event, idx)"
|
||||
(dfFocus)="onFocus($event)"
|
||||
(ngbEvent)="onCustomEvent($event, null, true)"></ds-dynamic-form-control-container>
|
||||
</ng-template>
|
||||
|
@@ -0,0 +1,52 @@
|
||||
@import './../../../../../../../styles/variables';
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cdk-drag {
|
||||
margin-left: -(2 * $spacer);
|
||||
margin-right: -(0.5 * $spacer);
|
||||
padding-right: (0.5 * $spacer);
|
||||
.drag-icon {
|
||||
visibility: hidden;
|
||||
width: (2 * $spacer);
|
||||
color: $gray-600;
|
||||
margin: $btn-padding-y 0;
|
||||
line-height: $btn-line-height;
|
||||
text-indent: 0.5 * $spacer
|
||||
}
|
||||
|
||||
&:hover, &:focus {
|
||||
cursor: grab;
|
||||
.drag-icon {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.cdk-drop-list-dragging {
|
||||
.cdk-drag {
|
||||
cursor: grabbing;
|
||||
.drag-icon {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cdk-drag-preview {
|
||||
background-color: white;
|
||||
border-radius: $border-radius-sm;
|
||||
margin-left: 0;
|
||||
box-shadow: 0 5px 5px 0px rgba(0, 0, 0, 0.2),
|
||||
0 8px 10px 1px rgba(0, 0, 0, 0.14),
|
||||
0 3px 14px 2px rgba(0, 0, 0, 0.12);
|
||||
.drag-icon {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.cdk-drag-placeholder {
|
||||
opacity: 0;
|
||||
}
|
@@ -1,25 +1,31 @@
|
||||
import { CdkDragDrop } from '@angular/cdk/drag-drop';
|
||||
import { Component, EventEmitter, Input, Output, QueryList } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import {
|
||||
DynamicFormArrayComponent,
|
||||
DynamicFormArrayModel,
|
||||
DynamicFormControlCustomEvent, DynamicFormControlEvent,
|
||||
DynamicFormControlCustomEvent,
|
||||
DynamicFormControlEvent,
|
||||
DynamicFormControlEventType,
|
||||
DynamicFormLayout,
|
||||
DynamicFormLayoutService,
|
||||
DynamicFormValidationService,
|
||||
DynamicTemplateDirective
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { Relationship } from '../../../../../../core/shared/item-relationships/relationship.model';
|
||||
import { DynamicRowArrayModel } from '../ds-dynamic-row-array-model';
|
||||
import { hasValue } from '../../../../../empty.util';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-dynamic-form-array',
|
||||
templateUrl: './dynamic-form-array.component.html'
|
||||
selector: 'ds-dynamic-form-array',
|
||||
templateUrl: './dynamic-form-array.component.html',
|
||||
styleUrls: ['./dynamic-form-array.component.scss']
|
||||
})
|
||||
export class DsDynamicFormArrayComponent extends DynamicFormArrayComponent {
|
||||
|
||||
@Input() bindId = true;
|
||||
@Input() group: FormGroup;
|
||||
@Input() layout: DynamicFormLayout;
|
||||
@Input() model: DynamicFormArrayModel;
|
||||
@Input() model: DynamicRowArrayModel;
|
||||
@Input() templates: QueryList<DynamicTemplateDirective> | undefined;
|
||||
|
||||
/* tslint:disable:no-output-rename */
|
||||
@@ -27,12 +33,39 @@ export class DsDynamicFormArrayComponent extends DynamicFormArrayComponent {
|
||||
@Output('dfChange') change: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
|
||||
@Output('dfFocus') focus: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
|
||||
@Output('ngbEvent') customEvent: EventEmitter<DynamicFormControlCustomEvent> = new EventEmitter();
|
||||
|
||||
/* tslint:enable:no-output-rename */
|
||||
|
||||
constructor(protected layoutService: DynamicFormLayoutService,
|
||||
protected validationService: DynamicFormValidationService) {
|
||||
|
||||
protected validationService: DynamicFormValidationService,
|
||||
) {
|
||||
super(layoutService, validationService);
|
||||
}
|
||||
|
||||
moveSelection(event: CdkDragDrop<Relationship>) {
|
||||
this.model.moveGroup(event.previousIndex, event.currentIndex - event.previousIndex);
|
||||
const prevIndex = event.previousIndex - 1;
|
||||
const index = event.currentIndex - 1;
|
||||
|
||||
if (hasValue(this.model.groups[index]) && hasValue((this.control as any).controls[index])) {
|
||||
const $event = {
|
||||
$event: { previousIndex: prevIndex },
|
||||
context: { index },
|
||||
control: (this.control as any).controls[index],
|
||||
group: this.group,
|
||||
model: this.model.groups[index].group[0],
|
||||
type: DynamicFormControlEventType.Change
|
||||
};
|
||||
|
||||
this.onChange($event);
|
||||
}
|
||||
}
|
||||
|
||||
update(event: any, index: number) {
|
||||
const $event = Object.assign({}, event, {
|
||||
context: { index: index - 1}
|
||||
});
|
||||
|
||||
this.onChange($event)
|
||||
}
|
||||
}
|
||||
|
@@ -17,6 +17,7 @@ export class DynamicDsDatePickerModel extends DynamicDateControlModel {
|
||||
valueUpdates: Subject<any>;
|
||||
malformedDate: boolean;
|
||||
hasLanguages = false;
|
||||
repeatable = false;
|
||||
|
||||
constructor(config: DynamicDateControlModelConfig, layout?: DynamicFormControlLayout) {
|
||||
super(config, layout);
|
||||
|
@@ -15,7 +15,7 @@ describe('DsDynamicDisabledComponent', () => {
|
||||
let model;
|
||||
|
||||
function init() {
|
||||
model = new DynamicDisabledModel({ value: 'test', repeatable: false, metadataFields: [], submissionId: '1234', id: '1' });
|
||||
model = new DynamicDisabledModel({ value: 'test', repeatable: false, metadataFields: [], submissionId: '1234', id: '1', hasSelectableMetadata: false });
|
||||
}
|
||||
|
||||
beforeEach(async(() => {
|
||||
@@ -52,7 +52,6 @@ describe('DsDynamicDisabledComponent', () => {
|
||||
|
||||
it('should have a disabled input', () => {
|
||||
const input = de.query(By.css('input'));
|
||||
console.log(input.nativeElement.getAttribute('disabled'));
|
||||
expect(input.nativeElement.getAttribute('disabled')).toEqual('');
|
||||
});
|
||||
});
|
||||
|
@@ -5,6 +5,7 @@ export const DYNAMIC_FORM_CONTROL_TYPE_DISABLED = 'EMPTY';
|
||||
|
||||
export interface DsDynamicDisabledModelConfig extends DsDynamicInputModelConfig {
|
||||
value?: any;
|
||||
hasSelectableMetadata: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -14,11 +15,14 @@ export class DynamicDisabledModel extends DsDynamicInputModel {
|
||||
|
||||
@serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_DISABLED;
|
||||
@serializable() value: any;
|
||||
@serializable() hasSelectableMetadata: boolean;
|
||||
|
||||
constructor(config: DsDynamicDisabledModelConfig, layout?: DynamicFormControlLayout) {
|
||||
super(config, layout);
|
||||
this.readOnly = true;
|
||||
this.disabled = true;
|
||||
this.hasSelectableMetadata = config.hasSelectableMetadata;
|
||||
|
||||
this.valueUpdates.next(config.value);
|
||||
}
|
||||
}
|
||||
|
@@ -2,10 +2,11 @@ import { DynamicFormControlLayout, DynamicFormGroupModel, DynamicFormGroupModelC
|
||||
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
import { isNotEmpty } from '../../../../empty.util';
|
||||
import { hasNoValue, isNotEmpty } from '../../../../empty.util';
|
||||
import { DsDynamicInputModel } from './ds-dynamic-input.model';
|
||||
import { FormFieldMetadataValueObject } from '../../models/form-field-metadata-value.model';
|
||||
import { RelationshipOptions } from '../../models/relationship-options.model';
|
||||
import { MetadataValue } from '../../../../../core/shared/metadata.models';
|
||||
|
||||
export const CONCAT_GROUP_SUFFIX = '_CONCAT_GROUP';
|
||||
export const CONCAT_FIRST_INPUT_SUFFIX = '_CONCAT_FIRST_INPUT';
|
||||
@@ -14,11 +15,14 @@ export const CONCAT_SECOND_INPUT_SUFFIX = '_CONCAT_SECOND_INPUT';
|
||||
export interface DynamicConcatModelConfig extends DynamicFormGroupModelConfig {
|
||||
separator: string;
|
||||
value?: any;
|
||||
hint?: string;
|
||||
relationship?: RelationshipOptions;
|
||||
repeatable: boolean;
|
||||
required: boolean;
|
||||
metadataFields: string[];
|
||||
submissionId: string;
|
||||
hasSelectableMetadata: boolean;
|
||||
metadataValue?: MetadataValue;
|
||||
}
|
||||
|
||||
export class DynamicConcatModel extends DynamicFormGroupModel {
|
||||
@@ -28,8 +32,11 @@ export class DynamicConcatModel extends DynamicFormGroupModel {
|
||||
@serializable() relationship?: RelationshipOptions;
|
||||
@serializable() repeatable?: boolean;
|
||||
@serializable() required?: boolean;
|
||||
@serializable() hint?: string;
|
||||
@serializable() metadataFields: string[];
|
||||
@serializable() submissionId: string;
|
||||
@serializable() hasSelectableMetadata: boolean;
|
||||
@serializable() metadataValue: MetadataValue;
|
||||
|
||||
isCustomGroup = true;
|
||||
valueUpdates: Subject<string>;
|
||||
@@ -37,26 +44,30 @@ export class DynamicConcatModel extends DynamicFormGroupModel {
|
||||
constructor(config: DynamicConcatModelConfig, layout?: DynamicFormControlLayout) {
|
||||
|
||||
super(config, layout);
|
||||
|
||||
this.separator = config.separator + ' ';
|
||||
this.relationship = config.relationship;
|
||||
this.repeatable = config.repeatable;
|
||||
this.required = config.required;
|
||||
this.hint = config.hint;
|
||||
this.metadataFields = config.metadataFields;
|
||||
this.submissionId = config.submissionId;
|
||||
|
||||
this.hasSelectableMetadata = config.hasSelectableMetadata;
|
||||
this.metadataValue = config.metadataValue;
|
||||
this.valueUpdates = new Subject<string>();
|
||||
this.valueUpdates.subscribe((value: string) => this.value = value);
|
||||
}
|
||||
|
||||
get value() {
|
||||
const firstValue = (this.get(0) as DsDynamicInputModel).value;
|
||||
const secondValue = (this.get(1) as DsDynamicInputModel).value;
|
||||
|
||||
if (isNotEmpty(firstValue) && isNotEmpty(secondValue)) {
|
||||
return new FormFieldMetadataValueObject(firstValue + this.separator + secondValue);
|
||||
} else if (isNotEmpty(firstValue)) {
|
||||
return new FormFieldMetadataValueObject(firstValue);
|
||||
const [firstValue, secondValue] = this.group.map((inputModel: DsDynamicInputModel) =>
|
||||
(typeof inputModel.value === 'string') ?
|
||||
Object.assign(new FormFieldMetadataValueObject(), { value: inputModel.value, display: inputModel.value }) :
|
||||
(inputModel.value as any));
|
||||
if (isNotEmpty(firstValue) && isNotEmpty(firstValue.value) && isNotEmpty(secondValue) && isNotEmpty(secondValue.value)) {
|
||||
return Object.assign(new FormFieldMetadataValueObject(), firstValue, { value: firstValue.value + this.separator + secondValue.value });
|
||||
} else if (isNotEmpty(firstValue) && isNotEmpty(firstValue.value)) {
|
||||
return Object.assign(new FormFieldMetadataValueObject(), firstValue);
|
||||
} else if (isNotEmpty(secondValue) && isNotEmpty(secondValue.value)) {
|
||||
return Object.assign(new FormFieldMetadataValueObject(), secondValue);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
@@ -71,18 +82,21 @@ export class DynamicConcatModel extends DynamicFormGroupModel {
|
||||
} else {
|
||||
tempValue = value.value;
|
||||
}
|
||||
|
||||
if (tempValue.includes(this.separator)) {
|
||||
values = tempValue.split(this.separator);
|
||||
} else {
|
||||
values = [tempValue, null];
|
||||
if (hasNoValue(tempValue)) {
|
||||
tempValue = '';
|
||||
}
|
||||
values = [...tempValue.split(this.separator), null].map((v) =>
|
||||
Object.assign(new FormFieldMetadataValueObject(), value, { display: v, value: v }));
|
||||
|
||||
if (values[0]) {
|
||||
if (values[0].value) {
|
||||
(this.get(0) as DsDynamicInputModel).valueUpdates.next(values[0]);
|
||||
} else {
|
||||
(this.get(0) as DsDynamicInputModel).valueUpdates.next(undefined);
|
||||
}
|
||||
if (values[1]) {
|
||||
if (values[1].value) {
|
||||
(this.get(1) as DsDynamicInputModel).valueUpdates.next(values[1]);
|
||||
} else {
|
||||
(this.get(1) as DsDynamicInputModel).valueUpdates.next(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -6,16 +6,21 @@ import { AuthorityOptions } from '../../../../../core/integration/models/authori
|
||||
import { hasValue } from '../../../../empty.util';
|
||||
import { FormFieldMetadataValueObject } from '../../models/form-field-metadata-value.model';
|
||||
import { RelationshipOptions } from '../../models/relationship-options.model';
|
||||
import { MetadataValue } from '../../../../../core/shared/metadata.models';
|
||||
|
||||
export interface DsDynamicInputModelConfig extends DynamicInputModelConfig {
|
||||
authorityOptions?: AuthorityOptions;
|
||||
languageCodes?: LanguageCode[];
|
||||
language?: string;
|
||||
place?: number;
|
||||
value?: any;
|
||||
relationship?: RelationshipOptions;
|
||||
repeatable: boolean;
|
||||
metadataFields: string[];
|
||||
submissionId: string;
|
||||
hasSelectableMetadata: boolean;
|
||||
metadataValue?: MetadataValue;
|
||||
|
||||
}
|
||||
|
||||
export class DsDynamicInputModel extends DynamicInputModel {
|
||||
@@ -28,6 +33,8 @@ export class DsDynamicInputModel extends DynamicInputModel {
|
||||
@serializable() repeatable?: boolean;
|
||||
@serializable() metadataFields: string[];
|
||||
@serializable() submissionId: string;
|
||||
@serializable() hasSelectableMetadata: boolean;
|
||||
@serializable() metadataValue: MetadataValue;
|
||||
|
||||
constructor(config: DsDynamicInputModelConfig, layout?: DynamicFormControlLayout) {
|
||||
super(config, layout);
|
||||
@@ -38,6 +45,8 @@ export class DsDynamicInputModel extends DynamicInputModel {
|
||||
this.value = config.value;
|
||||
this.relationship = config.relationship;
|
||||
this.submissionId = config.submissionId;
|
||||
this.hasSelectableMetadata = config.hasSelectableMetadata;
|
||||
this.metadataValue = config.metadataValue;
|
||||
|
||||
this.language = config.language;
|
||||
if (!this.language) {
|
||||
|
@@ -1,18 +1,34 @@
|
||||
import { DynamicFormArrayModel, DynamicFormArrayModelConfig, DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core';
|
||||
import { RelationshipOptions } from '../../models/relationship-options.model';
|
||||
|
||||
export interface DynamicRowArrayModelConfig extends DynamicFormArrayModelConfig {
|
||||
notRepeatable: boolean;
|
||||
required: boolean;
|
||||
submissionId: string;
|
||||
relationshipConfig: RelationshipOptions;
|
||||
metadataKey: string;
|
||||
metadataFields: string[];
|
||||
hasSelectableMetadata: boolean;
|
||||
}
|
||||
|
||||
export class DynamicRowArrayModel extends DynamicFormArrayModel {
|
||||
@serializable() notRepeatable = false;
|
||||
@serializable() required = false;
|
||||
@serializable() submissionId: string;
|
||||
@serializable() relationshipConfig: RelationshipOptions;
|
||||
@serializable() metadataKey: string;
|
||||
@serializable() metadataFields: string[];
|
||||
@serializable() hasSelectableMetadata: boolean;
|
||||
isRowArray = true;
|
||||
|
||||
constructor(config: DynamicRowArrayModelConfig, layout?: DynamicFormControlLayout) {
|
||||
super(config, layout);
|
||||
this.notRepeatable = config.notRepeatable;
|
||||
this.required = config.required;
|
||||
this.submissionId = config.submissionId;
|
||||
this.relationshipConfig = config.relationshipConfig;
|
||||
this.metadataKey = config.metadataKey;
|
||||
this.metadataFields = config.metadataFields;
|
||||
this.hasSelectableMetadata = config.hasSelectableMetadata;
|
||||
}
|
||||
}
|
||||
|
@@ -6,7 +6,6 @@
|
||||
[ngClass]="getClass('element','control')">
|
||||
|
||||
<ds-dynamic-form-control-container *ngFor="let _model of model.group"
|
||||
[asBootstrapFormGroup]="true"
|
||||
[group]="control"
|
||||
[hasErrorMessaging]="model.hasErrorMessages"
|
||||
[hidden]="_model.hidden"
|
||||
@@ -19,5 +18,4 @@
|
||||
(dfFocus)="onFocus($event)"
|
||||
(ngbEvent)="onCustomEvent($event, null, true)"></ds-dynamic-form-control-container>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user