diff --git a/src/app/admin/admin-notify-dashboard/admin-notify-logs/admin-notify-incoming/admin-notify-incoming.component.spec.ts b/src/app/admin/admin-notify-dashboard/admin-notify-logs/admin-notify-incoming/admin-notify-incoming.component.spec.ts index a2dc89b621..054dac3218 100644 --- a/src/app/admin/admin-notify-dashboard/admin-notify-logs/admin-notify-incoming/admin-notify-incoming.component.spec.ts +++ b/src/app/admin/admin-notify-dashboard/admin-notify-logs/admin-notify-incoming/admin-notify-incoming.component.spec.ts @@ -16,6 +16,7 @@ import { SEARCH_CONFIG_SERVICE } from '../../../../my-dspace-page/my-dspace-conf import { MockActivatedRoute } from '../../../../shared/mocks/active-router.mock'; import { getMockRemoteDataBuildService } from '../../../../shared/mocks/remote-data-build.service.mock'; import { routeServiceStub } from '../../../../shared/testing/route-service.stub'; +import { AdminNotifyLogsResultComponent } from '../admin-notify-logs-result/admin-notify-logs-result.component'; import { AdminNotifyIncomingComponent } from './admin-notify-incoming.component'; describe('AdminNotifyIncomingComponent', () => { @@ -48,6 +49,8 @@ describe('AdminNotifyIncomingComponent', () => { { provide: APP_DATA_SERVICES_MAP, useValue: {} }, provideMockStore({}), ], + }).overrideComponent(AdminNotifyIncomingComponent, { + remove: { imports: [AdminNotifyLogsResultComponent] }, }) .compileComponents(); diff --git a/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.spec.ts b/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.spec.ts index a148881db7..7fa29919f8 100644 --- a/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.spec.ts +++ b/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.spec.ts @@ -12,6 +12,8 @@ import { of as observableOf } from 'rxjs'; import { ItemTemplateDataService } from '../../core/data/item-template-data.service'; import { Collection } from '../../core/shared/collection.model'; +import { DsoEditMetadataComponent } from '../../dso-shared/dso-edit-metadata/dso-edit-metadata.component'; +import { ThemedDsoEditMetadataComponent } from '../../dso-shared/dso-edit-metadata/themed-dso-edit-metadata.component'; import { getMockThemeService } from '../../shared/mocks/theme-service.mock'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { @@ -47,6 +49,10 @@ describe('EditItemTemplatePageComponent', () => { { provide: NotificationsService, useValue: new NotificationsServiceStub() }, ], schemas: [NO_ERRORS_SCHEMA], + }).overrideComponent(EditItemTemplatePageComponent, { + remove: { + imports: [ThemedDsoEditMetadataComponent, DsoEditMetadataComponent], + }, }).compileComponents(); })); diff --git a/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.ts b/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.ts index cc96b46588..eb44ffdc9b 100644 --- a/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.ts +++ b/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.ts @@ -24,6 +24,7 @@ import { RemoteData } from '../../core/data/remote-data'; import { Collection } from '../../core/shared/collection.model'; import { Item } from '../../core/shared/item.model'; import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; +import { DsoEditMetadataComponent } from '../../dso-shared/dso-edit-metadata/dso-edit-metadata.component'; import { ThemedDsoEditMetadataComponent } from '../../dso-shared/dso-edit-metadata/themed-dso-edit-metadata.component'; import { AlertComponent } from '../../shared/alert/alert.component'; import { AlertType } from '../../shared/alert/alert-type'; @@ -36,6 +37,7 @@ import { getCollectionEditRoute } from '../collection-page-routing-paths'; templateUrl: './edit-item-template-page.component.html', imports: [ ThemedDsoEditMetadataComponent, + DsoEditMetadataComponent, RouterLink, AsyncPipe, VarDirective, diff --git a/src/app/core/data-services-map.ts b/src/app/core/data-services-map.ts index 7a4b258abc..12fc91b678 100644 --- a/src/app/core/data-services-map.ts +++ b/src/app/core/data-services-map.ts @@ -27,6 +27,7 @@ import { WORKSPACEITEM } from './eperson/models/workspaceitem.resource-type'; import { FEEDBACK } from './feedback/models/feedback.resource-type'; import { METADATA_FIELD } from './metadata/metadata-field.resource-type'; import { METADATA_SCHEMA } from './metadata/metadata-schema.resource-type'; +import { SUGGESTION } from './notifications/models/suggestion-objects.resource-type'; import { SUGGESTION_SOURCE } from './notifications/models/suggestion-source-object.resource-type'; import { SUGGESTION_TARGET } from './notifications/models/suggestion-target-object.resource-type'; import { QUALITY_ASSURANCE_EVENT_OBJECT } from './notifications/qa/models/quality-assurance-event-object.resource-type'; @@ -130,6 +131,7 @@ export const LAZY_DATA_SERVICES: LazyDataServicesMap = { [QUALITY_ASSURANCE_EVENT_OBJECT.value]: () => import('./notifications/qa/events/quality-assurance-event-data.service').then(m => m.QualityAssuranceEventDataService), [QUALITY_ASSURANCE_SOURCE_OBJECT.value]: () => import('./notifications/qa/source/quality-assurance-source-data.service').then(m => m.QualityAssuranceSourceDataService), [QUALITY_ASSURANCE_TOPIC_OBJECT.value]: () => import('./notifications/qa/topics/quality-assurance-topic-data.service').then(m => m.QualityAssuranceTopicDataService), + [SUGGESTION.value]: () => import('./notifications/suggestions-data.service').then(m => m.SuggestionsDataService), [SUGGESTION_SOURCE.value]: () => import('./notifications/source/suggestion-source-data.service').then(m => m.SuggestionSourceDataService), [SUGGESTION_TARGET.value]: () => import('./notifications/target/suggestion-target-data.service').then(m => m.SuggestionTargetDataService), [DUPLICATE.value]: () => import('./submission/submission-duplicate-data.service').then(m => m.SubmissionDuplicateDataService), diff --git a/src/app/core/data/base/data-service.decorator.spec.ts b/src/app/core/data/base/data-service.decorator.spec.ts deleted file mode 100644 index 296371be69..0000000000 --- a/src/app/core/data/base/data-service.decorator.spec.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* eslint-disable max-classes-per-file */ -/** - * The contents of this file are subject to the license and copyright - * detailed in the LICENSE and NOTICE files at the root of the source - * tree and available online at - * - * http://www.dspace.org/license/ - */ -import { v4 as uuidv4 } from 'uuid'; - -import { ResourceType } from '../../shared/resource-type'; -import { BaseDataService } from './base-data.service'; -import { - dataService, - getDataServiceFor, -} from './data-service.decorator'; -import { HALDataService } from './hal-data-service.interface'; - -class TestService extends BaseDataService { -} - -class AnotherTestService implements HALDataService { - public findListByHref(href$, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow): any { - return undefined; - } - - public findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow): any { - return undefined; - } -} - -let testType; - -describe('@dataService/getDataServiceFor', () => { - beforeEach(() => { - testType = new ResourceType(`testType-${uuidv4()}`); - }); - - it('should register a resourcetype for a dataservice', () => { - dataService(testType)(TestService); - expect(getDataServiceFor(testType)).toBe(TestService); - }); - - describe(`when the resource type isn't specified`, () => { - it(`should throw an error`, () => { - expect(() => { - dataService(undefined)(TestService); - }).toThrow(); - }); - }); - - describe(`when there already is a registered dataservice for a resourcetype`, () => { - it(`should throw an error`, () => { - dataService(testType)(TestService); - expect(() => { - dataService(testType)(AnotherTestService); - }).toThrow(); - }); - }); -}); diff --git a/src/app/core/data/base/data-service.decorator.ts b/src/app/core/data/base/data-service.decorator.ts deleted file mode 100644 index 600fb5e3e3..0000000000 --- a/src/app/core/data/base/data-service.decorator.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * The contents of this file are subject to the license and copyright - * detailed in the LICENSE and NOTICE files at the root of the source - * tree and available online at - * - * http://www.dspace.org/license/ - */ -import { InjectionToken } from '@angular/core'; - -import { - hasNoValue, - hasValue, -} from '../../../shared/empty.util'; -import { CacheableObject } from '../../cache/cacheable-object.model'; -import { GenericConstructor } from '../../shared/generic-constructor'; -import { ResourceType } from '../../shared/resource-type'; -import { HALDataService } from './hal-data-service.interface'; - -export const DATA_SERVICE_FACTORY = new InjectionToken<(resourceType: ResourceType) => GenericConstructor>>('getDataServiceFor', { - providedIn: 'root', - factory: () => getDataServiceFor, -}); -const dataServiceMap = new Map(); - -/** - * A class decorator to indicate that this class is a data service for a given HAL resource type. - * - * In most cases, a data service should extend {@link BaseDataService}. - * At the very least it must implement {@link HALDataService} in order for it to work with {@link LinkService}. - * - * @param resourceType the resource type the class is a dataservice for - */ -export function dataService(resourceType: ResourceType) { - return (target: GenericConstructor>): void => { - if (hasNoValue(resourceType)) { - throw new Error(`Invalid @dataService annotation on ${target}, resourceType needs to be defined`); - } - const existingDataservice = dataServiceMap.get(resourceType.value); - - if (hasValue(existingDataservice)) { - throw new Error(`Multiple dataservices for ${resourceType.value}: ${existingDataservice} and ${target}`); - } - - dataServiceMap.set(resourceType.value, target); - }; -} - -/** - * Return the dataservice matching the given resource type - * - * @param resourceType the resource type you want the matching dataservice for - */ -export function getDataServiceFor(resourceType: ResourceType): GenericConstructor> { - return dataServiceMap.get(resourceType.value); -} diff --git a/src/app/core/notifications/suggestion-data.service.spec.ts b/src/app/core/notifications/suggestions-data.service.spec.ts similarity index 50% rename from src/app/core/notifications/suggestion-data.service.spec.ts rename to src/app/core/notifications/suggestions-data.service.spec.ts index 050f81914f..7d32cb3f9b 100644 --- a/src/app/core/notifications/suggestion-data.service.spec.ts +++ b/src/app/core/notifications/suggestions-data.service.spec.ts @@ -12,21 +12,12 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv import { RequestParam } from '../cache/models/request-param.model'; import { ObjectCacheService } from '../cache/object-cache.service'; import { RestResponse } from '../cache/response.models'; -import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service'; import { RemoteData } from '../data/remote-data'; import { RequestService } from '../data/request.service'; import { RequestEntry } from '../data/request-entry.model'; import { RequestEntryState } from '../data/request-entry-state.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { Suggestion } from './models/suggestion.model'; -import { SuggestionSource } from './models/suggestion-source.model'; -import { SuggestionTarget } from './models/suggestion-target.model'; -import { SuggestionSourceDataService } from './source/suggestion-source-data.service'; -import { - SuggestionDataServiceImpl, - SuggestionsDataService, -} from './suggestions-data.service'; -import { SuggestionTargetDataService } from './target/suggestion-target-data.service'; +import { SuggestionsDataService } from './suggestions-data.service'; describe('SuggestionDataService test', () => { let scheduler: TestScheduler; @@ -37,12 +28,6 @@ describe('SuggestionDataService test', () => { let halService: HALEndpointService; let notificationsService: NotificationsService; let http: HttpClient; - let comparatorSuggestion: DefaultChangeAnalyzer; - let comparatorSuggestionSource: DefaultChangeAnalyzer; - let comparatorSuggestionTarget: DefaultChangeAnalyzer; - let suggestionSourcesDataService: SuggestionSourceDataService; - let suggestionTargetsDataService: SuggestionTargetDataService; - let suggestionsDataService: SuggestionDataServiceImpl; let responseCacheEntry: RequestEntry; @@ -62,9 +47,6 @@ describe('SuggestionDataService test', () => { halService, notificationsService, http, - comparatorSuggestion, - comparatorSuggestionSource, - comparatorSuggestionTarget, ); } @@ -74,9 +56,6 @@ describe('SuggestionDataService test', () => { objectCache = {} as ObjectCacheService; http = {} as HttpClient; notificationsService = {} as NotificationsService; - comparatorSuggestion = {} as DefaultChangeAnalyzer; - comparatorSuggestionTarget = {} as DefaultChangeAnalyzer; - comparatorSuggestionSource = {} as DefaultChangeAnalyzer; responseCacheEntry = new RequestEntry(); responseCacheEntry.request = { href: 'https://rest.api/' } as any; responseCacheEntry.response = new RestResponse(true, 200, 'Success'); @@ -99,75 +78,23 @@ describe('SuggestionDataService test', () => { buildList: cold('a', { a: remoteDataMocks.Success }), }); - - suggestionSourcesDataService = jasmine.createSpyObj('suggestionSourcesDataService', { - getSources: observableOf(null), - }); - - suggestionTargetsDataService = jasmine.createSpyObj('suggestionTargetsDataService', { - getTargets: observableOf(null), - getTargetsByUser: observableOf(null), - findById: observableOf(null), - }); - - suggestionsDataService = jasmine.createSpyObj('suggestionsDataService', { - searchBy: observableOf(null), - delete: observableOf(null), - }); - - service = initTestService(); - /* eslint-disable-next-line @typescript-eslint/dot-notation */ - service['suggestionSourcesDataService'] = suggestionSourcesDataService; - /* eslint-disable-next-line @typescript-eslint/dot-notation */ - service['suggestionTargetsDataService'] = suggestionTargetsDataService; - /* eslint-disable-next-line @typescript-eslint/dot-notation */ - service['suggestionsDataService'] = suggestionsDataService; - }); - - describe('Suggestion targets service', () => { - it('should call suggestionSourcesDataService.getTargets', () => { - const options = { - searchParams: [new RequestParam('source', testSource)], - }; - service.getTargets(testSource); - expect(suggestionTargetsDataService.getTargets).toHaveBeenCalledWith('findBySource', options); - }); - - it('should call suggestionSourcesDataService.getTargetsByUser', () => { - const options = { - searchParams: [new RequestParam('target', testUserId)], - }; - service.getTargetsByUser(testUserId); - expect(suggestionTargetsDataService.getTargetsByUser).toHaveBeenCalledWith(testUserId, options); - }); - - it('should call suggestionSourcesDataService.getTargetById', () => { - service.getTargetById('1'); - expect(suggestionTargetsDataService.findById).toHaveBeenCalledWith('1'); - }); - }); - - - describe('Suggestion sources service', () => { - it('should call suggestionSourcesDataService.getSources', () => { - service.getSources(); - expect(suggestionSourcesDataService.getSources).toHaveBeenCalled(); - }); }); describe('Suggestion service', () => { it('should call suggestionsDataService.searchBy', () => { + spyOn((service as any).searchData, 'searchBy').and.returnValue(observableOf(null)); const options = { searchParams: [new RequestParam('target', testUserId), new RequestParam('source', testSource)], }; service.getSuggestionsByTargetAndSource(testUserId, testSource); - expect(suggestionsDataService.searchBy).toHaveBeenCalledWith('findByTargetAndSource', options, false, true); + expect((service as any).searchData.searchBy).toHaveBeenCalledWith('findByTargetAndSource', options, false, true); }); it('should call suggestionsDataService.delete', () => { + spyOn((service as any).deleteData, 'delete').and.returnValue(observableOf(null)); service.deleteSuggestion('1'); - expect(suggestionsDataService.delete).toHaveBeenCalledWith('1'); + expect((service as any).deleteData.delete).toHaveBeenCalledWith('1'); }); }); diff --git a/src/app/core/notifications/suggestions-data.service.ts b/src/app/core/notifications/suggestions-data.service.ts index d39d270bdf..9d4b3da95d 100644 --- a/src/app/core/notifications/suggestions-data.service.ts +++ b/src/app/core/notifications/suggestions-data.service.ts @@ -1,91 +1,37 @@ -/* eslint-disable max-classes-per-file */ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { dataService } from '../cache/builders/build-decorators'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RequestParam } from '../cache/models/request-param.model'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { CoreState } from '../core-state.model'; -import { ChangeAnalyzer } from '../data/change-analyzer'; -import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service'; +import { + DeleteData, + DeleteDataImpl, +} from '../data/base/delete-data'; +import { IdentifiableDataService } from '../data/base/identifiable-data.service'; +import { SearchDataImpl } from '../data/base/search-data'; import { FindListOptions } from '../data/find-list-options.model'; import { PaginatedList } from '../data/paginated-list.model'; import { RemoteData } from '../data/remote-data'; import { RequestService } from '../data/request.service'; -import { UpdateDataServiceImpl } from '../data/update-data.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { NoContent } from '../shared/NoContent.model'; import { Suggestion } from './models/suggestion.model'; -import { SUGGESTION } from './models/suggestion-objects.resource-type'; -import { SuggestionSource } from './models/suggestion-source.model'; -import { SuggestionTarget } from './models/suggestion-target.model'; -import { SuggestionSourceDataService } from './source/suggestion-source-data.service'; -import { SuggestionTargetDataService } from './target/suggestion-target-data.service'; - -/* tslint:disable:max-classes-per-file */ - -/** - * A private UpdateDataServiceImpl implementation to delegate specific methods to. - */ -export class SuggestionDataServiceImpl extends UpdateDataServiceImpl { - - /** - * Initialize service variables - * @param {RequestService} requestService - * @param {RemoteDataBuildService} rdbService - * @param {Store} store - * @param {ObjectCacheService} objectCache - * @param {HALEndpointService} halService - * @param {NotificationsService} notificationsService - * @param {HttpClient} http - * @param {ChangeAnalyzer} comparator - * @param responseMsToLive - */ - constructor( - protected requestService: RequestService, - protected rdbService: RemoteDataBuildService, - protected store: Store, - protected objectCache: ObjectCacheService, - protected halService: HALEndpointService, - protected notificationsService: NotificationsService, - protected http: HttpClient, - protected comparator: ChangeAnalyzer, - protected responseMsToLive: number, - ) { - super('suggestions', requestService, rdbService, objectCache, halService, notificationsService, comparator ,responseMsToLive); - } -} /** * The service handling all Suggestion Target REST requests. */ @Injectable({ providedIn: 'root' }) -@dataService(SUGGESTION) -export class SuggestionsDataService { +export class SuggestionsDataService extends IdentifiableDataService { + protected searchFindBySourceMethod = 'findBySource'; protected searchFindByTargetAndSourceMethod = 'findByTargetAndSource'; - /** - * A private UpdateDataServiceImpl implementation to delegate specific methods to. - */ - private suggestionsDataService: SuggestionDataServiceImpl; - - /** - * A private UpdateDataServiceImpl implementation to delegate specific methods to. - */ - private suggestionSourcesDataService: SuggestionSourceDataService; - - /** - * A private UpdateDataServiceImpl implementation to delegate specific methods to. - */ - private suggestionTargetsDataService: SuggestionTargetDataService; - - private responseMsToLive = 10 * 1000; + private deleteData: DeleteData; + private searchData: SearchDataImpl; /** * Initialize service variables @@ -95,9 +41,6 @@ export class SuggestionsDataService { * @param {HALEndpointService} halService * @param {NotificationsService} notificationsService * @param {HttpClient} http - * @param {DefaultChangeAnalyzer} comparatorSuggestions - * @param {DefaultChangeAnalyzer} comparatorSources - * @param {DefaultChangeAnalyzer} comparatorTargets */ constructor( protected requestService: RequestService, @@ -106,81 +49,10 @@ export class SuggestionsDataService { protected halService: HALEndpointService, protected notificationsService: NotificationsService, protected http: HttpClient, - protected comparatorSuggestions: DefaultChangeAnalyzer, - protected comparatorSources: DefaultChangeAnalyzer, - protected comparatorTargets: DefaultChangeAnalyzer, ) { - this.suggestionsDataService = new SuggestionDataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparatorSuggestions, this.responseMsToLive); - this.suggestionSourcesDataService = new SuggestionSourceDataService(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparatorSources); - this.suggestionTargetsDataService = new SuggestionTargetDataService(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparatorTargets); - } - - /** - * Return the list of Suggestion Sources - * - * @param options - * Find list options object. - * @return Observable>> - * The list of Suggestion Sources. - */ - public getSources(options: FindListOptions = {}): Observable>> { - return this.suggestionSourcesDataService.getSources(options); - } - - /** - * Return the list of Suggestion Target for a given source - * - * @param source - * The source for which to find targets. - * @param options - * Find list options object. - * @param linksToFollow - * List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved. - * @return Observable>> - * The list of Suggestion Target. - */ - public getTargets( - source: string, - options: FindListOptions = {}, - ...linksToFollow: FollowLinkConfig[] - ): Observable>> { - options.searchParams = [new RequestParam('source', source)]; - - return this.suggestionTargetsDataService.getTargets(this.searchFindBySourceMethod, options, ...linksToFollow); - } - - /** - * Return the list of Suggestion Target for a given user - * - * @param userId - * The user Id for which to find targets. - * @param options - * Find list options object. - * @param linksToFollow - * List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved. - * @return Observable>> - * The list of Suggestion Target. - */ - public getTargetsByUser( - userId: string, - options: FindListOptions = {}, - ...linksToFollow: FollowLinkConfig[] - ): Observable>> { - options.searchParams = [new RequestParam('target', userId)]; - return this.suggestionTargetsDataService.getTargetsByUser(userId, options, ...linksToFollow); - } - - /** - * Return a Suggestion Target for a given id - * - * @param targetId - * The target id to retrieve. - * - * @return Observable> - * The list of Suggestion Target. - */ - public getTargetById(targetId: string): Observable> { - return this.suggestionTargetsDataService.findById(targetId); + super('suggestions', requestService, rdbService, objectCache, halService); + this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint); + this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive); } /** @@ -188,7 +60,7 @@ export class SuggestionsDataService { * @suggestionId */ public deleteSuggestion(suggestionId: string): Observable> { - return this.suggestionsDataService.delete(suggestionId); + return this.deleteData.delete(suggestionId); } /** @@ -216,7 +88,7 @@ export class SuggestionsDataService { new RequestParam('source', source), ]; - return this.suggestionsDataService.searchBy(this.searchFindByTargetAndSourceMethod, options, false, true, ...linksToFollow); + return this.searchData.searchBy(this.searchFindByTargetAndSourceMethod, options, false, true, ...linksToFollow); } /** diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.spec.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.spec.ts index 567572c4fa..c3d82d0795 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.spec.ts +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.spec.ts @@ -1,7 +1,6 @@ +import { CommonModule } from '@angular/common'; import { - ChangeDetectionStrategy, DebugElement, - Injectable, NO_ERRORS_SCHEMA, } from '@angular/core'; import { @@ -9,15 +8,15 @@ import { TestBed, waitForAsync, } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; +import { + BrowserModule, + By, +} from '@angular/platform-browser'; import { RouterTestingModule } from '@angular/router/testing'; import { TranslateModule } from '@ngx-translate/core'; -import { Operation } from 'fast-json-patch'; -import { Observable } from 'rxjs'; +import { APP_DATA_SERVICES_MAP } from '../../../config/app-config.interface'; import { ArrayMoveChangeAnalyzer } from '../../core/data/array-move-change-analyzer.service'; -import { DATA_SERVICE_FACTORY } from '../../core/data/base/data-service.decorator'; -import { RemoteData } from '../../core/data/remote-data'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { Item } from '../../core/shared/item.model'; import { ITEM } from '../../core/shared/item.resource-type'; @@ -25,7 +24,7 @@ import { MetadataValue } from '../../core/shared/metadata.models'; import { AlertComponent } from '../../shared/alert/alert.component'; import { LoadingComponent } from '../../shared/loading/loading.component'; import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { TestDataService } from '../../shared/testing/test-data-service.mock'; import { VarDirective } from '../../shared/utils/var.directive'; import { DsoEditMetadataComponent } from './dso-edit-metadata.component'; import { DsoEditMetadataFieldValuesComponent } from './dso-edit-metadata-field-values/dso-edit-metadata-field-values.component'; @@ -39,12 +38,9 @@ const REINSTATE_BTN = 'reinstate'; const SAVE_BTN = 'save'; const DISCARD_BTN = 'discard'; -@Injectable() -class TestDataService { - patch(object: Item, operations: Operation[]): Observable> { - return createSuccessfulRemoteDataObject$(object); - } -} +const mockDataServiceMap: any = { + [ITEM.value]: () => import('../../shared/testing/test-data-service.mock').then(m => m.TestDataService), +}; describe('DsoEditMetadataComponent', () => { let component: DsoEditMetadataComponent; @@ -92,21 +88,18 @@ describe('DsoEditMetadataComponent', () => { TestBed.configureTestingModule({ imports: [ + CommonModule, + BrowserModule, TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), DsoEditMetadataComponent, VarDirective, ], providers: [ - TestDataService, - { - provide: DATA_SERVICE_FACTORY, - useValue: jasmine - .createSpy('getDataServiceFor') - .and.returnValue(TestDataService), - }, + { provide: APP_DATA_SERVICES_MAP, useValue: mockDataServiceMap }, { provide: NotificationsService, useValue: notificationsService }, ArrayMoveChangeAnalyzer, + TestDataService, ], schemas: [NO_ERRORS_SCHEMA], }) @@ -122,19 +115,16 @@ describe('DsoEditMetadataComponent', () => { LoadingComponent, ], }, - add: { - changeDetection: ChangeDetectionStrategy.Default, - }, }) .compileComponents(); })); - beforeEach(() => { + beforeEach(waitForAsync(() => { fixture = TestBed.createComponent(DsoEditMetadataComponent); component = fixture.componentInstance; component.dso = dso; fixture.detectChanges(); - }); + })); describe('when no changes have been made', () => { assertButton(ADD_BTN, true, false); diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.ts index 517843f3da..6365f1ea99 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.ts +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.ts @@ -4,8 +4,10 @@ import { NgIf, } from '@angular/common'; import { + ChangeDetectorRef, Component, Inject, + InjectionToken, Injector, Input, OnDestroy, @@ -23,18 +25,25 @@ import { import { BehaviorSubject, combineLatest as observableCombineLatest, + EMPTY, Observable, Subscription, } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { + map, + mergeMap, + tap, +} from 'rxjs/operators'; +import { + APP_DATA_SERVICES_MAP, + LazyDataServicesMap, +} from '../../../config/app-config.interface'; import { ArrayMoveChangeAnalyzer } from '../../core/data/array-move-change-analyzer.service'; -import { DATA_SERVICE_FACTORY } from '../../core/data/base/data-service.decorator'; -import { HALDataService } from '../../core/data/base/hal-data-service.interface'; import { RemoteData } from '../../core/data/remote-data'; import { UpdateDataService } from '../../core/data/update-data.service'; +import { lazyService } from '../../core/lazy-service'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; -import { GenericConstructor } from '../../core/shared/generic-constructor'; import { getFirstCompletedRemoteData } from '../../core/shared/operators'; import { ResourceType } from '../../core/shared/resource-type'; import { AlertComponent } from '../../shared/alert/alert.component'; @@ -42,6 +51,7 @@ import { AlertType } from '../../shared/alert/alert-type'; import { hasNoValue, hasValue, + isNotEmpty, } from '../../shared/empty.util'; import { LoadingComponent } from '../../shared/loading/loading.component'; import { NotificationsService } from '../../shared/notifications/notifications.service'; @@ -141,7 +151,8 @@ export class DsoEditMetadataComponent implements OnInit, OnDestroy { protected translateService: TranslateService, protected parentInjector: Injector, protected arrayMoveChangeAnalyser: ArrayMoveChangeAnalyzer, - @Inject(DATA_SERVICE_FACTORY) protected getDataServiceFor: (resourceType: ResourceType) => GenericConstructor>) { + protected cdr: ChangeDetectorRef, + @Inject(APP_DATA_SERVICES_MAP) private dataServiceMap: InjectionToken) { } /** @@ -152,15 +163,18 @@ export class DsoEditMetadataComponent implements OnInit, OnDestroy { if (hasNoValue(this.dso)) { this.dsoUpdateSubscription = observableCombineLatest([this.route.data, this.route.parent.data]).pipe( map(([data, parentData]: [Data, Data]) => Object.assign({}, data, parentData)), - map((data: any) => data.dso), - ).subscribe((rd: RemoteData) => { - this.dso = rd.payload; - this.initDataService(); + tap((data: any) => this.initDSO(data.dso.payload)), + mergeMap(() => this.retrieveDataService()), + ).subscribe((dataService: UpdateDataService) => { + this.initDataService(dataService); this.initForm(); }); } else { - this.initDataService(); - this.initForm(); + this.initDSOType(this.dso); + this.retrieveDataService().subscribe((dataService: UpdateDataService) => { + this.initDataService(dataService); + this.initForm(); + }); } this.savingOrLoadingFieldValidation$ = observableCombineLatest([this.saving$, this.loadingFieldValidation$]).pipe( map(([saving, loading]: [boolean, boolean]) => saving || loading), @@ -168,25 +182,47 @@ export class DsoEditMetadataComponent implements OnInit, OnDestroy { } /** - * Initialise (resolve) the data-service for the current DSpaceObject + * Resolve the data-service for the current DSpaceObject and retrieve its instance */ - initDataService(): void { - let type: ResourceType; - if (typeof this.dso.type === 'string') { - type = new ResourceType(this.dso.type); - } else { - type = this.dso.type; - } + retrieveDataService(): Observable> { if (hasNoValue(this.updateDataService)) { - const provider = this.getDataServiceFor(type); - this.updateDataService = Injector.create({ - providers: [], - parent: this.parentInjector, - }).get(provider); + const lazyProvider$: Observable> = lazyService(this.dataServiceMap[this.dsoType], this.parentInjector); + return lazyProvider$; + } else { + return EMPTY; + } + } + + /** + * Initialise the current DSpaceObject + */ + initDSO(object: DSpaceObject) { + this.dso = object; + this.initDSOType(object); + } + + /** + * Initialise the current DSpaceObject's type + */ + initDSOType(object: DSpaceObject) { + let type: ResourceType; + if (typeof object.type === 'string') { + type = new ResourceType(object.type); + } else { + type = object.type; } this.dsoType = type.value; } + /** + * Initialise the data-service for the current DSpaceObject + */ + initDataService(dataService: UpdateDataService): void { + if (isNotEmpty(dataService)) { + this.updateDataService = dataService; + } + } + /** * Initialise the dynamic form object by passing the DSpaceObject's metadata * Call onValueSaved() to update the form's state properties @@ -194,6 +230,7 @@ export class DsoEditMetadataComponent implements OnInit, OnDestroy { initForm(): void { this.form = new DsoEditMetadataForm(this.dso.metadata); this.onValueSaved(); + this.cdr.detectChanges(); } /** diff --git a/src/app/notifications/suggestions.service.ts b/src/app/notifications/suggestions.service.ts index bb999259ad..f456914e18 100644 --- a/src/app/notifications/suggestions.service.ts +++ b/src/app/notifications/suggestions.service.ts @@ -171,7 +171,7 @@ export class SuggestionsService { if (isNotEmpty(profile) && profile.hasSucceeded && isNotEmpty(profile.payload)) { return this.researcherProfileService.findRelatedItemId(profile.payload).pipe( mergeMap((itemId: string) => { - return this.suggestionsDataService.getTargetsByUser(itemId).pipe( + return this.suggestionTargetDataService.getTargetsByUser(itemId).pipe( getFirstSucceededRemoteListPayload(), ); }), diff --git a/src/app/shared/eperson-group-list/eperson-group-list.component.spec.ts b/src/app/shared/eperson-group-list/eperson-group-list.component.spec.ts index 7833035391..e09fa32526 100644 --- a/src/app/shared/eperson-group-list/eperson-group-list.component.spec.ts +++ b/src/app/shared/eperson-group-list/eperson-group-list.component.spec.ts @@ -6,6 +6,7 @@ import { } from '@angular/core'; import { ComponentFixture, + fakeAsync, inject, TestBed, waitForAsync, @@ -15,19 +16,15 @@ import { TranslateModule } from '@ngx-translate/core'; import { cold } from 'jasmine-marbles'; import uniqueId from 'lodash/uniqueId'; import { of as observableOf } from 'rxjs'; -import { DSONameService } from 'src/app/core/breadcrumbs/dso-name.service'; -import { - dataService, - getDataServiceFor, -} from 'src/app/core/data/base/data-service.decorator'; -import { EPERSON } from 'src/app/core/eperson/models/eperson.resource-type'; -import { GROUP } from 'src/app/core/eperson/models/group.resource-type'; -import { ResourceType } from 'src/app/core/shared/resource-type'; +import { APP_DATA_SERVICES_MAP } from '../../../config/app-config.interface'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { buildPaginatedList } from '../../core/data/paginated-list.model'; import { RequestService } from '../../core/data/request.service'; import { EPersonDataService } from '../../core/eperson/eperson-data.service'; import { GroupDataService } from '../../core/eperson/group-data.service'; +import { EPERSON } from '../../core/eperson/models/eperson.resource-type'; +import { GROUP } from '../../core/eperson/models/group.resource-type'; import { PaginationService } from '../../core/pagination/pagination.service'; import { PageInfo } from '../../core/shared/page-info.model'; import { DSONameServiceMock } from '../mocks/dso-name.service.mock'; @@ -44,6 +41,11 @@ import { SearchEvent } from './eperson-group-list-event-type'; import { EpersonSearchBoxComponent } from './eperson-search-box/eperson-search-box.component'; import { GroupSearchBoxComponent } from './group-search-box/group-search-box.component'; +const mockDataServiceMap: any = { + [EPERSON.value]: () => import('../../core/eperson/eperson-data.service').then(m => m.EPersonDataService), + [GROUP.value]: () => import('../../core/eperson/group-data.service').then(m => m.GroupDataService), +}; + describe('EpersonGroupListComponent test suite', () => { let comp: EpersonGroupListComponent; let compAsAny: any; @@ -59,7 +61,6 @@ describe('EpersonGroupListComponent test suite', () => { const mockEpersonService = jasmine.createSpyObj('epersonService', { - getDataServiceFor: jasmine.createSpy('getDataServiceFor'), findByHref: jasmine.createSpy('findByHref'), findAll: jasmine.createSpy('findAll'), searchByScope: jasmine.createSpy('searchByScope'), @@ -71,7 +72,6 @@ describe('EpersonGroupListComponent test suite', () => { const mockGroupService = jasmine.createSpyObj('groupService', { - getDataServiceFor: jasmine.createSpy('getDataServiceFor'), findByHref: jasmine.createSpy('findByHref'), findAll: jasmine.createSpy('findAll'), searchGroups: jasmine.createSpy('searchGroups'), @@ -106,6 +106,7 @@ describe('EpersonGroupListComponent test suite', () => { { provide: GroupDataService, useValue: mockGroupService }, { provide: RequestService, useValue: getMockRequestService() }, { provide: PaginationService, useValue: paginationService }, + { provide: APP_DATA_SERVICES_MAP, useValue: mockDataServiceMap }, EpersonGroupListComponent, ChangeDetectorRef, Injector, @@ -151,19 +152,14 @@ describe('EpersonGroupListComponent test suite', () => { describe('when is list of eperson', () => { - beforeEach(() => { + beforeEach(waitForAsync(() => { // initTestScheduler(); fixture = TestBed.createComponent(EpersonGroupListComponent); epersonService = TestBed.inject(EPersonDataService); comp = fixture.componentInstance; compAsAny = fixture.componentInstance; comp.isListOfEPerson = true; - const resourceType: ResourceType = (comp.isListOfEPerson) ? EPERSON : GROUP; - const mockDataService = dataService(resourceType); - if (!getDataServiceFor(resourceType)) { - mockDataService(EPersonDataService); - } - }); + })); afterEach(() => { comp = null; @@ -172,33 +168,40 @@ describe('EpersonGroupListComponent test suite', () => { fixture.destroy(); }); - it('should inject EPersonDataService', () => { + it('should inject EPersonDataService', fakeAsync(() => { spyOn(comp, 'updateList'); fixture.detectChanges(); - expect(compAsAny.dataService).toBeDefined(); - expect(comp.updateList).toHaveBeenCalled(); - }); + fixture.whenStable().then(() => { + expect(compAsAny.dataService).toBeDefined(); + expect(comp.updateList).toHaveBeenCalled(); + }); + })); - it('should init entrySelectedId', () => { + it('should init entrySelectedId', fakeAsync(() => { spyOn(comp, 'updateList'); comp.initSelected = EPersonMock.id; fixture.detectChanges(); - expect(compAsAny.entrySelectedId.value).toBe(EPersonMock.id); - }); + fixture.whenStable().then(() => { + expect(compAsAny.entrySelectedId.value).toBe(EPersonMock.id); + }); - it('should init the list of eperson', () => { + })); + + it('should init the list of eperson', fakeAsync(() => { epersonService.searchByScope.and.returnValue(observableOf(epersonPaginatedListRD)); fixture.detectChanges(); - expect(compAsAny.list$.value).toEqual(epersonPaginatedListRD); - expect(comp.getList()).toBeObservable(cold('a', { - a: epersonPaginatedListRD, - })); - }); + fixture.whenStable().then(() => { + expect(compAsAny.list$.value).toEqual(epersonPaginatedListRD); + expect(comp.getList()).toBeObservable(cold('a', { + a: epersonPaginatedListRD, + })); + }); + })); it('should emit select event', () => { spyOn(comp.select, 'emit'); @@ -234,11 +237,6 @@ describe('EpersonGroupListComponent test suite', () => { comp = fixture.componentInstance; compAsAny = fixture.componentInstance; comp.isListOfEPerson = false; - const resourceType: ResourceType = (comp.isListOfEPerson) ? EPERSON : GROUP; - const mockDataService = dataService(resourceType); - if (!getDataServiceFor(resourceType)) { - mockDataService(GroupDataService); - } }); afterEach(() => { @@ -248,32 +246,39 @@ describe('EpersonGroupListComponent test suite', () => { fixture.destroy(); }); - it('should inject GroupDataService', () => { + it('should inject GroupDataService', fakeAsync(() => { spyOn(comp, 'updateList'); fixture.detectChanges(); - expect(compAsAny.dataService).toBeDefined(); - expect(comp.updateList).toHaveBeenCalled(); - }); + fixture.whenStable().then(() => { + expect(compAsAny.dataService).toBeDefined(); + expect(comp.updateList).toHaveBeenCalled(); + }); - it('should init entrySelectedId', () => { + })); + + it('should init entrySelectedId', fakeAsync(() => { spyOn(comp, 'updateList'); comp.initSelected = GroupMock.id; fixture.detectChanges(); - expect(compAsAny.entrySelectedId.value).toBe(GroupMock.id); - }); + fixture.whenStable().then(() => { + expect(compAsAny.entrySelectedId.value).toBe(GroupMock.id); + }); + })); - it('should init the list of group', () => { + it('should init the list of group', fakeAsync(() => { groupService.searchGroups.and.returnValue(observableOf(groupPaginatedListRD)); fixture.detectChanges(); - expect(compAsAny.list$.value).toEqual(groupPaginatedListRD); - expect(comp.getList()).toBeObservable(cold('a', { - a: groupPaginatedListRD, - })); - }); + fixture.whenStable().then(() => { + expect(compAsAny.list$.value).toEqual(groupPaginatedListRD); + expect(comp.getList()).toBeObservable(cold('a', { + a: groupPaginatedListRD, + })); + }); + })); it('should emit select event', () => { spyOn(comp.select, 'emit'); diff --git a/src/app/shared/eperson-group-list/eperson-group-list.component.ts b/src/app/shared/eperson-group-list/eperson-group-list.component.ts index 6605e6c541..0a33767e34 100644 --- a/src/app/shared/eperson-group-list/eperson-group-list.component.ts +++ b/src/app/shared/eperson-group-list/eperson-group-list.component.ts @@ -6,6 +6,8 @@ import { import { Component, EventEmitter, + Inject, + InjectionToken, Injector, Input, OnDestroy, @@ -21,8 +23,11 @@ import { } from 'rxjs'; import { map } from 'rxjs/operators'; +import { + APP_DATA_SERVICES_MAP, + LazyDataServicesMap, +} from '../../../config/app-config.interface'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; -import { getDataServiceFor } from '../../core/data/base/data-service.decorator'; import { FindListOptions } from '../../core/data/find-list-options.model'; import { PaginatedList } from '../../core/data/paginated-list.model'; import { RemoteData } from '../../core/data/remote-data'; @@ -30,6 +35,7 @@ import { EPersonDataService } from '../../core/eperson/eperson-data.service'; import { GroupDataService } from '../../core/eperson/group-data.service'; import { EPERSON } from '../../core/eperson/models/eperson.resource-type'; import { GROUP } from '../../core/eperson/models/group.resource-type'; +import { lazyService } from '../../core/lazy-service'; import { PaginationService } from '../../core/pagination/pagination.service'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { getFirstCompletedRemoteData } from '../../core/shared/operators'; @@ -121,9 +127,13 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy { * * @param {DSONameService} dsoNameService * @param {Injector} parentInjector + * @param {PaginationService} paginationService + * @param {APP_DATA_SERVICES_MAP} dataServiceMap */ - constructor(public dsoNameService: DSONameService, private parentInjector: Injector, - private paginationService: PaginationService) { + constructor(public dsoNameService: DSONameService, + private parentInjector: Injector, + private paginationService: PaginationService, + @Inject(APP_DATA_SERVICES_MAP) private dataServiceMap: InjectionToken) { } /** @@ -131,19 +141,19 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy { */ ngOnInit(): void { const resourceType: ResourceType = (this.isListOfEPerson) ? EPERSON : GROUP; - const provider = getDataServiceFor(resourceType); - this.dataService = Injector.create({ - providers: [], - parent: this.parentInjector, - }).get(provider); - this.paginationOptions.id = uniqueId('egl'); - this.paginationOptions.pageSize = 5; + const lazyProvider$: Observable = lazyService(this.dataServiceMap[resourceType.value], this.parentInjector); + lazyProvider$.subscribe((dataService: EPersonDataService | GroupDataService) => { + this.dataService = dataService; + console.log(dataService); + this.paginationOptions.id = uniqueId('egl'); + this.paginationOptions.pageSize = 5; - if (this.initSelected) { - this.entrySelectedId.next(this.initSelected); - } + if (this.initSelected) { + this.entrySelectedId.next(this.initSelected); + } - this.updateList(this.currentSearchScope, this.currentSearchQuery); + this.updateList(this.currentSearchScope, this.currentSearchQuery); + }); } /** diff --git a/src/app/shared/resource-policies/resolvers/resource-policy-target.resolver.ts b/src/app/shared/resource-policies/resolvers/resource-policy-target.resolver.ts index 2689de0a9d..131824d144 100644 --- a/src/app/shared/resource-policies/resolvers/resource-policy-target.resolver.ts +++ b/src/app/shared/resource-policies/resolvers/resource-policy-target.resolver.ts @@ -1,5 +1,7 @@ import { + Inject, Injectable, + InjectionToken, Injector, } from '@angular/core'; import { @@ -8,10 +10,15 @@ import { RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; -import { getDataServiceFor } from '../../../core/data/base/data-service.decorator'; +import { + APP_DATA_SERVICES_MAP, + LazyDataServicesMap, +} from '../../../../config/app-config.interface'; import { IdentifiableDataService } from '../../../core/data/base/identifiable-data.service'; import { RemoteData } from '../../../core/data/remote-data'; +import { lazyService } from '../../../core/lazy-service'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; import { ResourceType } from '../../../core/shared/resource-type'; @@ -21,13 +28,12 @@ import { isEmpty } from '../../empty.util'; * This class represents a resolver that requests a specific item before the route is activated */ @Injectable({ providedIn: 'root' }) -export class ResourcePolicyTargetResolver { - /** - * The data service used to make request. - */ - private dataService: IdentifiableDataService; +export class ResourcePolicyTargetResolver { - constructor(private parentInjector: Injector, private router: Router) { + constructor( + private parentInjector: Injector, + private router: Router, + @Inject(APP_DATA_SERVICES_MAP) private dataServiceMap: InjectionToken) { } /** @@ -45,13 +51,13 @@ export class ResourcePolicyTargetResolver { this.router.navigateByUrl('/404', { skipLocationChange: true }); } - const provider = getDataServiceFor(new ResourceType(targetType)); - this.dataService = Injector.create({ - providers: [], - parent: this.parentInjector, - }).get(provider); + const resourceType: ResourceType = new ResourceType(targetType); + const lazyProvider$: Observable> = lazyService(this.dataServiceMap[resourceType.value], this.parentInjector); - return this.dataService.findById(policyTargetId).pipe( + return lazyProvider$.pipe( + switchMap((dataService: IdentifiableDataService) => { + return dataService.findById(policyTargetId); + }), getFirstCompletedRemoteData(), ); } diff --git a/src/app/shared/testing/test-data-service.mock.ts b/src/app/shared/testing/test-data-service.mock.ts index dbb216ea57..b5529f4a07 100644 --- a/src/app/shared/testing/test-data-service.mock.ts +++ b/src/app/shared/testing/test-data-service.mock.ts @@ -1,7 +1,14 @@ import { Injectable } from '@angular/core'; -import { of } from 'rxjs'; +import { Operation } from 'fast-json-patch'; +import { + Observable, + of, +} from 'rxjs'; import { FindListOptions } from '../../core/data/find-list-options.model'; +import { RemoteData } from '../../core/data/remote-data'; +import { Item } from '../../core/shared/item.model'; +import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils'; import { FollowLinkConfig } from '../utils/follow-link-config.model'; @Injectable() @@ -13,4 +20,8 @@ export class TestDataService { findByHref(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]) { return of('findByHref'); } + + patch(object: Item, operations: Operation[]): Observable> { + return createSuccessfulRemoteDataObject$(object); + } }