From 101f1a76dd97009b10d30f1bb96994aca6933729 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 12 May 2020 14:54:10 +0200 Subject: [PATCH 01/18] 70834: Refactoring registry-service pt1 - removing response-parsing services and using data-services --- .../metadata-registry.component.ts | 3 +- .../metadata-schema.component.ts | 6 +- src/app/core/cache/response.models.ts | 45 ---- src/app/core/core.module.ts | 10 +- .../core/data/metadata-field-data.service.ts | 48 ++++ .../core/data/metadata-schema-data.service.ts | 26 +- ...amformats-response-parsing.service.spec.ts | 41 ---- ...tstreamformats-response-parsing.service.ts | 25 -- ...atafields-response-parsing.service.spec.ts | 68 ------ ...metadatafields-response-parsing.service.ts | 34 --- ...taschemas-response-parsing.service.spec.ts | 50 ---- ...etadataschemas-response-parsing.service.ts | 29 --- ...egistry-bitstreamformats-response.model.ts | 24 -- .../registry-metadatafields-response.model.ts | 46 ---- ...registry-metadataschemas-response.model.ts | 14 -- src/app/core/registry/registry.service.ts | 224 +++--------------- src/app/shared/pagination/pagination.utils.ts | 14 ++ 17 files changed, 107 insertions(+), 600 deletions(-) create mode 100644 src/app/core/data/metadata-field-data.service.ts delete mode 100644 src/app/core/data/registry-bitstreamformats-response-parsing.service.spec.ts delete mode 100644 src/app/core/data/registry-bitstreamformats-response-parsing.service.ts delete mode 100644 src/app/core/data/registry-metadatafields-response-parsing.service.spec.ts delete mode 100644 src/app/core/data/registry-metadatafields-response-parsing.service.ts delete mode 100644 src/app/core/data/registry-metadataschemas-response-parsing.service.spec.ts delete mode 100644 src/app/core/data/registry-metadataschemas-response-parsing.service.ts delete mode 100644 src/app/core/registry/registry-bitstreamformats-response.model.ts delete mode 100644 src/app/core/registry/registry-metadatafields-response.model.ts delete mode 100644 src/app/core/registry/registry-metadataschemas-response.model.ts create mode 100644 src/app/shared/pagination/pagination.utils.ts diff --git a/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.ts b/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.ts index 302974b5c2..ed5cefeab2 100644 --- a/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.ts +++ b/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.ts @@ -12,6 +12,7 @@ import { NotificationsService } from '../../../shared/notifications/notification import { Route, Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { MetadataSchema } from '../../../core/metadata/metadata-schema.model'; +import { toFindListOptions } from '../../../shared/pagination/pagination.utils'; @Component({ selector: 'ds-metadata-registry', @@ -57,7 +58,7 @@ export class MetadataRegistryComponent { * Update the list of schemas by fetching it from the rest api or cache */ private updateSchemas() { - this.metadataSchemas = this.registryService.getMetadataSchemas(this.config); + this.metadataSchemas = this.registryService.getMetadataSchemas(toFindListOptions(this.config)); } /** diff --git a/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.ts b/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.ts index 2974c1c087..5712cde0e7 100644 --- a/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.ts +++ b/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.ts @@ -13,6 +13,8 @@ import { NotificationsService } from '../../../shared/notifications/notification import { TranslateService } from '@ngx-translate/core'; import { MetadataField } from '../../../core/metadata/metadata-field.model'; import { MetadataSchema } from '../../../core/metadata/metadata-schema.model'; +import { getSucceededRemoteData } from '../../../core/shared/operators'; +import { toFindListOptions } from '../../../shared/pagination/pagination.utils'; @Component({ selector: 'ds-metadata-schema', @@ -85,9 +87,9 @@ export class MetadataSchemaComponent implements OnInit { * Update the list of fields by fetching it from the rest api or cache */ private updateFields() { - this.metadataSchema.subscribe((schemaData) => { + this.metadataSchema.pipe(getSucceededRemoteData()).subscribe((schemaData) => { const schema = schemaData.payload; - this.metadataFields = this.registryService.getMetadataFieldsBySchema(schema, this.config); + this.metadataFields = this.registryService.getMetadataFieldsBySchema(schema, toFindListOptions(this.config)); this.namespace = {namespace: schemaData.payload.namespace}; }); } diff --git a/src/app/core/cache/response.models.ts b/src/app/core/cache/response.models.ts index 3f46ecf647..b40965dd0a 100644 --- a/src/app/core/cache/response.models.ts +++ b/src/app/core/cache/response.models.ts @@ -6,9 +6,6 @@ import { ConfigObject } from '../config/models/config.model'; import { FacetValue } from '../../shared/search/facet-value.model'; import { SearchFilterConfig } from '../../shared/search/search-filter-config.model'; import { IntegrationModel } from '../integration/models/integration.model'; -import { RegistryMetadataschemasResponse } from '../registry/registry-metadataschemas-response.model'; -import { RegistryMetadatafieldsResponse } from '../registry/registry-metadatafields-response.model'; -import { RegistryBitstreamformatsResponse } from '../registry/registry-bitstreamformats-response.model'; import { PaginatedList } from '../data/paginated-list'; import { SubmissionObject } from '../submission/models/submission-object.model'; import { DSpaceObject } from '../shared/dspace-object.model'; @@ -40,48 +37,6 @@ export class DSOSuccessResponse extends RestResponse { } } -/** - * A successful response containing a list of MetadataSchemas wrapped in a RegistryMetadataschemasResponse - */ -export class RegistryMetadataschemasSuccessResponse extends RestResponse { - constructor( - public metadataschemasResponse: RegistryMetadataschemasResponse, - public statusCode: number, - public statusText: string, - public pageInfo?: PageInfo - ) { - super(true, statusCode, statusText); - } -} - -/** - * A successful response containing a list of MetadataFields wrapped in a RegistryMetadatafieldsResponse - */ -export class RegistryMetadatafieldsSuccessResponse extends RestResponse { - constructor( - public metadatafieldsResponse: RegistryMetadatafieldsResponse, - public statusCode: number, - public statusText: string, - public pageInfo?: PageInfo - ) { - super(true, statusCode, statusText); - } -} - -/** - * A successful response containing a list of BitstreamFormats wrapped in a RegistryBitstreamformatsResponse - */ -export class RegistryBitstreamformatsSuccessResponse extends RestResponse { - constructor( - public bitstreamformatsResponse: RegistryBitstreamformatsResponse, - public statusCode: number, - public statusText: string, - public pageInfo?: PageInfo - ) { - super(true, statusCode, statusText); - } -} - /** * A successful response containing exactly one MetadataSchema */ diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 356dad5ed8..9cde79471c 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -73,9 +73,6 @@ import { MetadatafieldParsingService } from './data/metadatafield-parsing.servic import { MetadataschemaParsingService } from './data/metadataschema-parsing.service'; import { MyDSpaceResponseParsingService } from './data/mydspace-response-parsing.service'; import { ObjectUpdatesService } from './data/object-updates/object-updates.service'; -import { RegistryBitstreamformatsResponseParsingService } from './data/registry-bitstreamformats-response-parsing.service'; -import { RegistryMetadatafieldsResponseParsingService } from './data/registry-metadatafields-response-parsing.service'; -import { RegistryMetadataschemasResponseParsingService } from './data/registry-metadataschemas-response-parsing.service'; import { RelationshipTypeService } from './data/relationship-type.service'; import { RelationshipService } from './data/relationship.service'; import { ResourcePolicyService } from './data/resource-policy.service'; @@ -145,6 +142,8 @@ import { Version } from './shared/version.model'; import { VersionHistory } from './shared/version-history.model'; import { WorkflowActionDataService } from './data/workflow-action-data.service'; import { WorkflowAction } from './tasks/models/workflow-action-object.model'; +import { MetadataSchemaDataService } from './data/metadata-schema-data.service'; +import { MetadataFieldDataService } from './data/metadata-field-data.service'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -201,9 +200,6 @@ const PROVIDERS = [ FacetValueResponseParsingService, FacetValueMapResponseParsingService, FacetConfigResponseParsingService, - RegistryMetadataschemasResponseParsingService, - RegistryMetadatafieldsResponseParsingService, - RegistryBitstreamformatsResponseParsingService, MappedCollectionsReponseParsingService, DebugResponseParsingService, SearchResponseParsingService, @@ -264,6 +260,8 @@ const PROVIDERS = [ LicenseDataService, ItemTypeDataService, WorkflowActionDataService, + MetadataSchemaDataService, + MetadataFieldDataService, // register AuthInterceptor as HttpInterceptor { provide: HTTP_INTERCEPTORS, diff --git a/src/app/core/data/metadata-field-data.service.ts b/src/app/core/data/metadata-field-data.service.ts new file mode 100644 index 0000000000..59af99e558 --- /dev/null +++ b/src/app/core/data/metadata-field-data.service.ts @@ -0,0 +1,48 @@ +import { Injectable } from '@angular/core'; +import { dataService } from '../cache/builders/build-decorators'; +import { DataService } from './data.service'; +import { RequestService } from './request.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { Store } from '@ngrx/store'; +import { CoreState } from '../core.reducers'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; +import { HttpClient } from '@angular/common/http'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { METADATA_FIELD } from '../metadata/metadata-field.resource-type'; +import { MetadataField } from '../metadata/metadata-field.model'; +import { MetadataSchema } from '../metadata/metadata-schema.model'; +import { FindListOptions } from './request.models'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { SearchParam } from '../cache/models/search-param.model'; + +/** + * A service responsible for fetching/sending data from/to the REST API on the metadatafields endpoint + */ +@Injectable() +@dataService(METADATA_FIELD) +export class MetadataFieldDataService extends DataService { + protected linkPath = 'metadatafields'; + protected searchBySchemaLinkPath = 'bySchema'; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected halService: HALEndpointService, + protected objectCache: ObjectCacheService, + protected comparator: DefaultChangeAnalyzer, + protected http: HttpClient, + protected notificationsService: NotificationsService) { + super(); + } + + findBySchema(schema: MetadataSchema, options: FindListOptions = {}, ...linksToFollow: Array>) { + const optionsWithSchema = Object.assign(new FindListOptions(), options, { + searchParams: [new SearchParam('schema', schema.prefix)] + }); + return this.searchBy(this.searchBySchemaLinkPath, optionsWithSchema, ...linksToFollow); + } + +} diff --git a/src/app/core/data/metadata-schema-data.service.ts b/src/app/core/data/metadata-schema-data.service.ts index 915f588379..bdb4b9315f 100644 --- a/src/app/core/data/metadata-schema-data.service.ts +++ b/src/app/core/data/metadata-schema-data.service.ts @@ -9,37 +9,17 @@ import { CoreState } from '../core.reducers'; import { MetadataSchema } from '../metadata/metadata-schema.model'; import { METADATA_SCHEMA } from '../metadata/metadata-schema.resource-type'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { ChangeAnalyzer } from './change-analyzer'; - import { DataService } from './data.service'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; import { RequestService } from './request.service'; -/* tslint:disable:max-classes-per-file */ -class DataServiceImpl extends DataService { - protected linkPath = 'metadataschemas'; - - 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) { - super(); - } - -} - /** * A service responsible for fetching/sending data from/to the REST API on the metadataschemas endpoint */ @Injectable() @dataService(METADATA_SCHEMA) -export class MetadataSchemaDataService { - private dataService: DataServiceImpl; +export class MetadataSchemaDataService extends DataService { + protected linkPath = 'metadataschemas'; constructor( protected requestService: RequestService, @@ -50,6 +30,6 @@ export class MetadataSchemaDataService { protected comparator: DefaultChangeAnalyzer, protected http: HttpClient, protected notificationsService: NotificationsService) { - this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator); + super(); } } diff --git a/src/app/core/data/registry-bitstreamformats-response-parsing.service.spec.ts b/src/app/core/data/registry-bitstreamformats-response-parsing.service.spec.ts deleted file mode 100644 index 6cc031f3c9..0000000000 --- a/src/app/core/data/registry-bitstreamformats-response-parsing.service.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { PageInfo } from '../shared/page-info.model'; -import { DSOResponseParsingService } from './dso-response-parsing.service'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { - RegistryBitstreamformatsSuccessResponse -} from '../cache/response.models'; -import { RegistryBitstreamformatsResponseParsingService } from './registry-bitstreamformats-response-parsing.service'; - -describe('RegistryBitstreamformatsResponseParsingService', () => { - let service: RegistryBitstreamformatsResponseParsingService; - - const mockDSOParser = Object.assign({ - processPageInfo: () => new PageInfo() - }) as DSOResponseParsingService; - - const data = Object.assign({ - payload: { - _embedded: { - bitstreamformats: [ - { - uuid: 'uuid-1', - description: 'a description' - }, - { - uuid: 'uuid-2', - description: 'another description' - }, - ] - } - } - }) as DSpaceRESTV2Response; - - beforeEach(() => { - service = new RegistryBitstreamformatsResponseParsingService(mockDSOParser); - }); - - it('should parse the data correctly', () => { - const response = service.parse(null, data); - expect(response.constructor).toBe(RegistryBitstreamformatsSuccessResponse); - }); -}); diff --git a/src/app/core/data/registry-bitstreamformats-response-parsing.service.ts b/src/app/core/data/registry-bitstreamformats-response-parsing.service.ts deleted file mode 100644 index 1cbcf358e3..0000000000 --- a/src/app/core/data/registry-bitstreamformats-response-parsing.service.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Injectable } from '@angular/core'; -import { RegistryBitstreamformatsSuccessResponse, RestResponse } from '../cache/response.models'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; -import { RegistryBitstreamformatsResponse } from '../registry/registry-bitstreamformats-response.model'; -import { DSOResponseParsingService } from './dso-response-parsing.service'; -import { ResponseParsingService } from './parsing.service'; -import { RestRequest } from './request.models'; - -@Injectable() -export class RegistryBitstreamformatsResponseParsingService implements ResponseParsingService { - constructor(private dsoParser: DSOResponseParsingService) { - } - - parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { - const payload = data.payload; - - const bitstreamformats = payload._embedded.bitstreamformats; - payload.bitstreamformats = bitstreamformats; - - const deserialized = new DSpaceSerializer(RegistryBitstreamformatsResponse).deserialize(payload); - return new RegistryBitstreamformatsSuccessResponse(deserialized, data.statusCode, data.statusText, this.dsoParser.processPageInfo(data.payload.page)); - } - -} diff --git a/src/app/core/data/registry-metadatafields-response-parsing.service.spec.ts b/src/app/core/data/registry-metadatafields-response-parsing.service.spec.ts deleted file mode 100644 index 5ede21954a..0000000000 --- a/src/app/core/data/registry-metadatafields-response-parsing.service.spec.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { PageInfo } from '../shared/page-info.model'; -import { DSOResponseParsingService } from './dso-response-parsing.service'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { - RegistryMetadatafieldsSuccessResponse -} from '../cache/response.models'; -import { RegistryMetadatafieldsResponseParsingService } from './registry-metadatafields-response-parsing.service'; - -describe('RegistryMetadatafieldsResponseParsingService', () => { - let service: RegistryMetadatafieldsResponseParsingService; - - const mockDSOParser = Object.assign({ - processPageInfo: () => new PageInfo() - }) as DSOResponseParsingService; - - const data = Object.assign({ - payload: { - _embedded: { - metadatafields: [ - { - id: 1, - element: 'element', - qualifier: 'qualifier', - scopeNote: 'a scope note', - _embedded: { - schema: { - id: 1, - prefix: 'test', - namespace: 'test namespace' - } - } - }, - { - id: 2, - element: 'secondelement', - qualifier: 'secondqualifier', - scopeNote: 'a second scope note', - _embedded: { - schema: { - id: 1, - prefix: 'test', - namespace: 'test namespace' - } - } - }, - ] - } - } - }) as DSpaceRESTV2Response; - - const emptyData = Object.assign({ - payload: {} - }) as DSpaceRESTV2Response; - - beforeEach(() => { - service = new RegistryMetadatafieldsResponseParsingService(mockDSOParser); - }); - - it('should parse the data correctly', () => { - const response = service.parse(null, data); - expect(response.constructor).toBe(RegistryMetadatafieldsSuccessResponse); - }); - - it('should not produce an error and parse the data correctly when the data is empty', () => { - const response = service.parse(null, emptyData); - expect(response.constructor).toBe(RegistryMetadatafieldsSuccessResponse); - }); -}); diff --git a/src/app/core/data/registry-metadatafields-response-parsing.service.ts b/src/app/core/data/registry-metadatafields-response-parsing.service.ts deleted file mode 100644 index cf9484c4c4..0000000000 --- a/src/app/core/data/registry-metadatafields-response-parsing.service.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Injectable } from '@angular/core'; -import { hasValue } from '../../shared/empty.util'; -import { RegistryMetadatafieldsSuccessResponse, RestResponse } from '../cache/response.models'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; -import { RegistryMetadatafieldsResponse } from '../registry/registry-metadatafields-response.model'; -import { DSOResponseParsingService } from './dso-response-parsing.service'; -import { ResponseParsingService } from './parsing.service'; -import { RestRequest } from './request.models'; - -@Injectable() -export class RegistryMetadatafieldsResponseParsingService implements ResponseParsingService { - constructor(private dsoParser: DSOResponseParsingService) { - } - - parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { - const payload = data.payload; - - let metadatafields = []; - - if (hasValue(payload._embedded)) { - metadatafields = payload._embedded.metadatafields; - metadatafields.forEach((field) => { - field.schema = field._embedded.schema; - }); - } - - payload.metadatafields = metadatafields; - - const deserialized = new DSpaceSerializer(RegistryMetadatafieldsResponse).deserialize(payload); - return new RegistryMetadatafieldsSuccessResponse(deserialized, data.statusCode, data.statusText, this.dsoParser.processPageInfo(data.payload)); - } - -} diff --git a/src/app/core/data/registry-metadataschemas-response-parsing.service.spec.ts b/src/app/core/data/registry-metadataschemas-response-parsing.service.spec.ts deleted file mode 100644 index e49305d06a..0000000000 --- a/src/app/core/data/registry-metadataschemas-response-parsing.service.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { RegistryMetadataschemasResponseParsingService } from './registry-metadataschemas-response-parsing.service'; -import { PageInfo } from '../shared/page-info.model'; -import { DSOResponseParsingService } from './dso-response-parsing.service'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { RegistryMetadataschemasSuccessResponse } from '../cache/response.models'; - -describe('RegistryMetadataschemasResponseParsingService', () => { - let service: RegistryMetadataschemasResponseParsingService; - - const mockDSOParser = Object.assign({ - processPageInfo: () => new PageInfo() - }) as DSOResponseParsingService; - - const data = Object.assign({ - payload: { - _embedded: { - metadataschemas: [ - { - id: 1, - prefix: 'test', - namespace: 'test namespace' - }, - { - id: 2, - prefix: 'second', - namespace: 'second test namespace' - } - ] - } - } - }) as DSpaceRESTV2Response; - - const emptyData = Object.assign({ - payload: {} - }) as DSpaceRESTV2Response; - - beforeEach(() => { - service = new RegistryMetadataschemasResponseParsingService(mockDSOParser); - }); - - it('should parse the data correctly', () => { - const response = service.parse(null, data); - expect(response.constructor).toBe(RegistryMetadataschemasSuccessResponse); - }); - - it('should not produce an error and parse the data correctly when the data is empty', () => { - const response = service.parse(null, emptyData); - expect(response.constructor).toBe(RegistryMetadataschemasSuccessResponse); - }); -}); diff --git a/src/app/core/data/registry-metadataschemas-response-parsing.service.ts b/src/app/core/data/registry-metadataschemas-response-parsing.service.ts deleted file mode 100644 index 416ed19dc2..0000000000 --- a/src/app/core/data/registry-metadataschemas-response-parsing.service.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Injectable } from '@angular/core'; -import { hasValue } from '../../shared/empty.util'; -import { RegistryMetadataschemasSuccessResponse, RestResponse } from '../cache/response.models'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; -import { RegistryMetadataschemasResponse } from '../registry/registry-metadataschemas-response.model'; -import { DSOResponseParsingService } from './dso-response-parsing.service'; -import { ResponseParsingService } from './parsing.service'; -import { RestRequest } from './request.models'; - -@Injectable() -export class RegistryMetadataschemasResponseParsingService implements ResponseParsingService { - constructor(private dsoParser: DSOResponseParsingService) { - } - - parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { - const payload = data.payload; - - let metadataschemas = []; - if (hasValue(payload._embedded)) { - metadataschemas = payload._embedded.metadataschemas; - } - payload.metadataschemas = metadataschemas; - - const deserialized = new DSpaceSerializer(RegistryMetadataschemasResponse).deserialize(payload); - return new RegistryMetadataschemasSuccessResponse(deserialized, data.statusCode, data.statusText, this.dsoParser.processPageInfo(data.payload)); - } - -} diff --git a/src/app/core/registry/registry-bitstreamformats-response.model.ts b/src/app/core/registry/registry-bitstreamformats-response.model.ts deleted file mode 100644 index 4da30b4ffc..0000000000 --- a/src/app/core/registry/registry-bitstreamformats-response.model.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { autoserialize, deserialize } from 'cerialize'; -import { BITSTREAM_FORMAT } from '../shared/bitstream-format.resource-type'; -import { HALLink } from '../shared/hal-link.model'; -import { PageInfo } from '../shared/page-info.model'; -import { BitstreamFormat } from '../shared/bitstream-format.model'; -import { link } from '../cache/builders/build-decorators'; - -export class RegistryBitstreamformatsResponse { - @autoserialize - page: PageInfo; - - /** - * The {@link HALLink}s for this RegistryBitstreamformatsResponse - */ - @deserialize - _links: { - self: HALLink; - bitstreamformats: HALLink; - }; - - @link(BITSTREAM_FORMAT) - bitstreamformats?: BitstreamFormat[]; - -} diff --git a/src/app/core/registry/registry-metadatafields-response.model.ts b/src/app/core/registry/registry-metadatafields-response.model.ts deleted file mode 100644 index 5dc492ab0f..0000000000 --- a/src/app/core/registry/registry-metadatafields-response.model.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { autoserialize, deserialize } from 'cerialize'; -import { typedObject } from '../cache/builders/build-decorators'; -import { MetadataField } from '../metadata/metadata-field.model'; -import { METADATA_FIELD } from '../metadata/metadata-field.resource-type'; -import { HALLink } from '../shared/hal-link.model'; -import { PageInfo } from '../shared/page-info.model'; -import { ResourceType } from '../shared/resource-type'; -import { excludeFromEquals } from '../utilities/equals.decorators'; - -/** - * Class that represents a response with a registry's metadata fields - */ -@typedObject -export class RegistryMetadatafieldsResponse { - static type = METADATA_FIELD; - - /** - * The object type - */ - @excludeFromEquals - @autoserialize - type: ResourceType; - - /** - * List of metadata fields in the response - */ - @deserialize - metadatafields: MetadataField[]; - - /** - * Page info of this response - */ - @autoserialize - page: PageInfo; - - /** - * The REST link to this response - */ - @autoserialize - self: string; - - @deserialize - _links: { - self: HALLink, - } -} diff --git a/src/app/core/registry/registry-metadataschemas-response.model.ts b/src/app/core/registry/registry-metadataschemas-response.model.ts deleted file mode 100644 index 7a485d8849..0000000000 --- a/src/app/core/registry/registry-metadataschemas-response.model.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { PageInfo } from '../shared/page-info.model'; -import { autoserialize, deserialize } from 'cerialize'; -import { MetadataSchema } from '../metadata/metadata-schema.model'; - -export class RegistryMetadataschemasResponse { - @deserialize - metadataschemas: MetadataSchema[]; - - @autoserialize - page: PageInfo; - - @autoserialize - self: string; -} diff --git a/src/app/core/registry/registry.service.ts b/src/app/core/registry/registry.service.ts index fbc42b26f4..ab846fa6d1 100644 --- a/src/app/core/registry/registry.service.ts +++ b/src/app/core/registry/registry.service.ts @@ -7,32 +7,20 @@ import { PageInfo } from '../shared/page-info.model'; import { CreateMetadataFieldRequest, CreateMetadataSchemaRequest, - DeleteRequest, - GetRequest, - RestRequest, + DeleteRequest, FindListOptions, UpdateMetadataFieldRequest, UpdateMetadataSchemaRequest } from '../data/request.models'; -import { GenericConstructor } from '../shared/generic-constructor'; -import { ResponseParsingService } from '../data/parsing.service'; -import { RegistryMetadataschemasResponseParsingService } from '../data/registry-metadataschemas-response-parsing.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RequestService } from '../data/request.service'; -import { RegistryMetadataschemasResponse } from './registry-metadataschemas-response.model'; import { MetadatafieldSuccessResponse, MetadataschemaSuccessResponse, - RegistryMetadatafieldsSuccessResponse, - RegistryMetadataschemasSuccessResponse, RestResponse } from '../cache/response.models'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { RegistryMetadatafieldsResponseParsingService } from '../data/registry-metadatafields-response-parsing.service'; -import { RegistryMetadatafieldsResponse } from './registry-metadatafields-response.model'; import { hasNoValue, hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; -import { URLCombiner } from '../url-combiner/url-combiner'; -import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; -import { configureRequest, getResponseFromEntry } from '../shared/operators'; +import { configureRequest, getFirstSucceededRemoteDataPayload, getResponseFromEntry } from '../shared/operators'; import { createSelector, select, Store } from '@ngrx/store'; import { AppState } from '../../app.reducer'; import { MetadataRegistryState } from '../../+admin/admin-registries/metadata-registry/metadata-registry.reducers'; @@ -57,6 +45,9 @@ import { TranslateService } from '@ngx-translate/core'; import { MetadataSchema } from '../metadata/metadata-schema.model'; import { MetadataField } from '../metadata/metadata-field.model'; import { getClassForType } from '../cache/builders/build-decorators'; +import { MetadataSchemaDataService } from '../data/metadata-schema-data.service'; +import { MetadataFieldDataService } from '../data/metadata-field-data.service'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; const metadataRegistryStateSelector = (state: AppState) => state.metadataRegistry; const editMetadataSchemaSelector = createSelector(metadataRegistryStateSelector, (metadataState: MetadataRegistryState) => metadataState.editSchema); @@ -80,211 +71,60 @@ export class RegistryService { private halService: HALEndpointService, private store: Store, private notificationsService: NotificationsService, - private translateService: TranslateService) { + private translateService: TranslateService, + private metadataSchemaService: MetadataSchemaDataService, + private metadataFieldService: MetadataFieldDataService) { } /** * Retrieves all metadata schemas - * @param pagination The pagination info used to retrieve the schemas + * @param options The options used to retrieve the schemas + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved */ - public getMetadataSchemas(pagination: PaginationComponentOptions): Observable>> { - const requestObs = this.getMetadataSchemasRequestObs(pagination); - - const requestEntryObs = requestObs.pipe( - flatMap((request: RestRequest) => this.requestService.getByHref(request.href)) - ); - - const rmrObs: Observable = requestEntryObs.pipe( - getResponseFromEntry(), - map((response: RegistryMetadataschemasSuccessResponse) => response.metadataschemasResponse) - ); - - const metadataschemasObs: Observable = rmrObs.pipe( - map((rmr: RegistryMetadataschemasResponse) => rmr.metadataschemas) - ); - - const pageInfoObs: Observable = requestEntryObs.pipe( - getResponseFromEntry(), - map((response: RegistryMetadataschemasSuccessResponse) => response.pageInfo) - ); - - const payloadObs = observableCombineLatest(metadataschemasObs, pageInfoObs).pipe( - map(([metadataschemas, pageInfo]) => { - return new PaginatedList(pageInfo, metadataschemas); - }) - ); - - return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs); + public getMetadataSchemas(options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { + return this.metadataSchemaService.findAll(options, ...linksToFollow); } /** * Retrieves a metadata schema by its name * @param schemaName The name of the schema to find + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved */ - public getMetadataSchemaByName(schemaName: string): Observable> { - // Temporary pagination to get ALL metadataschemas until there's a rest api endpoint for fetching a specific schema - const pagination: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { - id: 'all-metadatafields-pagination', - pageSize: 10000 + public getMetadataSchemaByName(schemaName: string, ...linksToFollow: Array>): Observable> { + // Temporary options to get ALL metadataschemas until there's a rest api endpoint for fetching a specific schema + const options: FindListOptions = Object.assign(new FindListOptions(), { + elementsPerPage: 10000 }); - const requestObs = this.getMetadataSchemasRequestObs(pagination); - - const requestEntryObs = requestObs.pipe( - flatMap((request: RestRequest) => this.requestService.getByHref(request.href)) + return this.getMetadataSchemas(options).pipe( + getFirstSucceededRemoteDataPayload(), + map((schemas: PaginatedList) => schemas.page.filter((schema) => schema.prefix === schemaName)[0]), + flatMap((schema: MetadataSchema) => this.metadataSchemaService.findById(`${schema.id}`, ...linksToFollow)) ); - - const rmrObs: Observable = requestEntryObs.pipe( - getResponseFromEntry(), - map((response: RegistryMetadataschemasSuccessResponse) => response.metadataschemasResponse) - ); - - const metadataschemaObs: Observable = rmrObs.pipe( - map((rmr: RegistryMetadataschemasResponse) => rmr.metadataschemas), - map((metadataSchemas: MetadataSchema[]) => metadataSchemas.filter((value) => value.prefix === schemaName)[0]) - ); - - return this.rdb.toRemoteDataObservable(requestEntryObs, metadataschemaObs); } /** * retrieves all metadata fields that belong to a certain metadata schema * @param schema The schema to filter by - * @param pagination The pagination info used to retrieve the fields + * @param options The options info used to retrieve the fields + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved */ - public getMetadataFieldsBySchema(schema: MetadataSchema, pagination: PaginationComponentOptions): Observable>> { - const requestObs = this.getMetadataFieldsBySchemaRequestObs(pagination, schema); - - const requestEntryObs = requestObs.pipe( - flatMap((request: RestRequest) => this.requestService.getByHref(request.href)) - ); - - const rmrObs: Observable = requestEntryObs.pipe( - getResponseFromEntry(), - map((response: RegistryMetadatafieldsSuccessResponse) => response.metadatafieldsResponse) - ); - - const metadatafieldsObs: Observable = rmrObs.pipe( - map((rmr: RegistryMetadatafieldsResponse) => rmr.metadatafields) - ); - - const pageInfoObs: Observable = requestEntryObs.pipe( - getResponseFromEntry(), - - map((response: RegistryMetadatafieldsSuccessResponse) => response.pageInfo) - ); - - const payloadObs = observableCombineLatest(metadatafieldsObs, pageInfoObs).pipe( - map(([metadatafields, pageInfo]) => { - return new PaginatedList(pageInfo, metadatafields); - }) - ); - - return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs); + public getMetadataFieldsBySchema(schema: MetadataSchema, options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { + return this.metadataFieldService.findBySchema(schema, options, ...linksToFollow); } /** * Retrieve all existing metadata fields as a paginated list - * @param pagination Pagination options to determine which page of metadata fields should be requested - * When no pagination is provided, all metadata fields are requested in one large page + * @param options Options to determine which page of metadata fields should be requested + * When no options are provided, all metadata fields are requested in one large page + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved * @returns an observable that emits a remote data object with a page of metadata fields */ - public getAllMetadataFields(pagination?: PaginationComponentOptions): Observable>> { - if (hasNoValue(pagination)) { - pagination = {currentPage: 1, pageSize: 10000} as any; + public getAllMetadataFields(options?: FindListOptions, ...linksToFollow: Array>): Observable>> { + if (hasNoValue(options)) { + options = {currentPage: 1, elementsPerPage: 10000} as any; } - const requestObs = this.getMetadataFieldsRequestObs(pagination); - - const requestEntryObs = requestObs.pipe( - flatMap((request: RestRequest) => this.requestService.getByHref(request.href)) - ); - - const rmrObs: Observable = requestEntryObs.pipe( - getResponseFromEntry(), - map((response: RegistryMetadatafieldsSuccessResponse) => response.metadatafieldsResponse) - ); - - const metadatafieldsObs: Observable = rmrObs.pipe( - map((rmr: RegistryMetadatafieldsResponse) => rmr.metadatafields), - /* Make sure to explicitly cast this into a MetadataField object, on first page loads this object comes from the object cache created by the server and its prototype is unknown */ - map((metadataFields: MetadataField[]) => metadataFields.map((metadataField: MetadataField) => Object.assign(new MetadataField(), metadataField))) - ); - - const pageInfoObs: Observable = requestEntryObs.pipe( - getResponseFromEntry(), - - map((response: RegistryMetadatafieldsSuccessResponse) => response.pageInfo) - ); - - const payloadObs = observableCombineLatest(metadatafieldsObs, pageInfoObs).pipe( - map(([metadatafields, pageInfo]) => { - return new PaginatedList(pageInfo, metadatafields); - }) - ); - - return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs); - } - - public getMetadataSchemasRequestObs(pagination: PaginationComponentOptions): Observable { - return this.halService.getEndpoint(this.metadataSchemasPath).pipe( - map((url: string) => { - const args: string[] = []; - args.push(`size=${pagination.pageSize}`); - args.push(`page=${pagination.currentPage - 1}`); - if (isNotEmpty(args)) { - url = new URLCombiner(url, `?${args.join('&')}`).toString(); - } - const request = new GetRequest(this.requestService.generateRequestId(), url); - return Object.assign(request, { - getResponseParser(): GenericConstructor { - return RegistryMetadataschemasResponseParsingService; - } - }); - }), - tap((request: RestRequest) => this.requestService.configure(request)), - ); - } - - private getMetadataFieldsBySchemaRequestObs(pagination: PaginationComponentOptions, schema: MetadataSchema): Observable { - return this.halService.getEndpoint(this.metadataFieldsPath + '/search/bySchema').pipe( - // return this.halService.getEndpoint(this.metadataFieldsPath).pipe( - map((url: string) => { - const args: string[] = []; - args.push(`schema=${schema.prefix}`); - args.push(`size=${pagination.pageSize}`); - args.push(`page=${pagination.currentPage - 1}`); - if (isNotEmpty(args)) { - url = new URLCombiner(url, `?${args.join('&')}`).toString(); - } - const request = new GetRequest(this.requestService.generateRequestId(), url); - return Object.assign(request, { - getResponseParser(): GenericConstructor { - return RegistryMetadatafieldsResponseParsingService; - } - }); - }), - tap((request: RestRequest) => this.requestService.configure(request)), - ); - } - - private getMetadataFieldsRequestObs(pagination: PaginationComponentOptions): Observable { - return this.halService.getEndpoint(this.metadataFieldsPath).pipe( - map((url: string) => { - const args: string[] = []; - args.push(`size=${pagination.pageSize}`); - args.push(`page=${pagination.currentPage - 1}`); - if (isNotEmpty(args)) { - url = new URLCombiner(url, `?${args.join('&')}`).toString(); - } - const request = new GetRequest(this.requestService.generateRequestId(), url); - return Object.assign(request, { - getResponseParser(): GenericConstructor { - return RegistryMetadatafieldsResponseParsingService; - } - }); - }), - tap((request: RestRequest) => this.requestService.configure(request)), - ); + return this.metadataFieldService.findAll(options, ...linksToFollow); } public editMetadataSchema(schema: MetadataSchema) { diff --git a/src/app/shared/pagination/pagination.utils.ts b/src/app/shared/pagination/pagination.utils.ts new file mode 100644 index 0000000000..5701c96b54 --- /dev/null +++ b/src/app/shared/pagination/pagination.utils.ts @@ -0,0 +1,14 @@ +import { PaginationComponentOptions } from './pagination-component-options.model'; +import { FindListOptions } from '../../core/data/request.models'; + +/** + * Transform a PaginationComponentOptions object into a FindListOptions object + * @param pagination The PaginationComponentOptions to transform + * @param original An original FindListOptions object to start from + */ +export function toFindListOptions(pagination: PaginationComponentOptions, original?: FindListOptions): FindListOptions { + return Object.assign(new FindListOptions(), original, { + currentPage: pagination.currentPage, + elementsPerPage: pagination.pageSize + }); +} From 7677a673aae9aaffe5cfd58d67fcfcee85fd38b4 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 12 May 2020 16:36:03 +0200 Subject: [PATCH 02/18] 70834: Intermediate commit --- .../core/data/metadata-field-data.service.ts | 62 +++++++- .../core/data/metadata-schema-data.service.ts | 67 +++++++- src/app/core/registry/registry.service.ts | 149 ++---------------- 3 files changed, 138 insertions(+), 140 deletions(-) diff --git a/src/app/core/data/metadata-field-data.service.ts b/src/app/core/data/metadata-field-data.service.ts index 59af99e558..9d1298dced 100644 --- a/src/app/core/data/metadata-field-data.service.ts +++ b/src/app/core/data/metadata-field-data.service.ts @@ -8,14 +8,21 @@ import { CoreState } from '../core.reducers'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { METADATA_FIELD } from '../metadata/metadata-field.resource-type'; import { MetadataField } from '../metadata/metadata-field.model'; import { MetadataSchema } from '../metadata/metadata-schema.model'; -import { FindListOptions } from './request.models'; +import { CreateMetadataFieldRequest, FindListOptions, UpdateMetadataFieldRequest } from './request.models'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { SearchParam } from '../cache/models/search-param.model'; +import { Observable } from 'rxjs/internal/Observable'; +import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; +import { distinctUntilChanged, map, take, tap } from 'rxjs/operators'; +import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; +import { configureRequest, getResponseFromEntry } from '../shared/operators'; +import { MetadatafieldSuccessResponse, RestResponse } from '../cache/response.models'; +import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; /** * A service responsible for fetching/sending data from/to the REST API on the metadatafields endpoint @@ -45,4 +52,55 @@ export class MetadataFieldDataService extends DataService { return this.searchBy(this.searchBySchemaLinkPath, optionsWithSchema, ...linksToFollow); } + createOrUpdateMetadataField(field: MetadataField): Observable { + const isUpdate = hasValue(field.id); + const requestId = this.requestService.generateRequestId(); + const endpoint$ = this.getBrowseEndpoint().pipe( + isNotEmptyOperator(), + map((endpoint: string) => (isUpdate ? `${endpoint}/${field.id}` : `${endpoint}?schemaId=${field.schema.id}`)), + distinctUntilChanged() + ); + + const request$ = endpoint$.pipe( + take(1), + map((endpoint: string) => { + if (isUpdate) { + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'application/json'); + options.headers = headers; + return new UpdateMetadataFieldRequest(requestId, endpoint, JSON.stringify(field), options); + } else { + return new CreateMetadataFieldRequest(requestId, endpoint, JSON.stringify(field)); + } + }) + ); + + // Execute the post/put request + request$.pipe( + configureRequest(this.requestService) + ).subscribe(); + + // Return response + return this.requestService.getByUUID(requestId).pipe( + getResponseFromEntry(), + map((response: RestResponse) => { + if (!response.isSuccessful) { + if (hasValue((response as any).errorMessage)) { + this.notificationsService.error('Server Error:', (response as any).errorMessage, new NotificationOptions(-1)); + } + } else { + return response; + } + }), + isNotEmptyOperator() + ); + } + + clearRequests(): Observable { + return this.getBrowseEndpoint().pipe( + tap((href: string) => this.requestService.removeByHrefSubstring(href)) + ); + } + } diff --git a/src/app/core/data/metadata-schema-data.service.ts b/src/app/core/data/metadata-schema-data.service.ts index bdb4b9315f..f75f1e453f 100644 --- a/src/app/core/data/metadata-schema-data.service.ts +++ b/src/app/core/data/metadata-schema-data.service.ts @@ -1,8 +1,8 @@ -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { dataService } from '../cache/builders/build-decorators'; +import { dataService, getClassForType } from '../cache/builders/build-decorators'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { CoreState } from '../core.reducers'; @@ -12,6 +12,15 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; import { DataService } from './data.service'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; import { RequestService } from './request.service'; +import { Observable } from 'rxjs/internal/Observable'; +import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; +import { distinctUntilChanged, map, take, tap } from 'rxjs/operators'; +import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; +import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; +import { CreateMetadataSchemaRequest, UpdateMetadataSchemaRequest } from './request.models'; +import { configureRequest, getResponseFromEntry } from '../shared/operators'; +import { MetadataschemaSuccessResponse, RestResponse } from '../cache/response.models'; +import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; /** * A service responsible for fetching/sending data from/to the REST API on the metadataschemas endpoint @@ -32,4 +41,58 @@ export class MetadataSchemaDataService extends DataService { protected notificationsService: NotificationsService) { super(); } + + createOrUpdateMetadataSchema(schema: MetadataSchema): Observable { + const isUpdate = hasValue(schema.id); + const requestId = this.requestService.generateRequestId(); + const endpoint$ = this.getBrowseEndpoint().pipe( + isNotEmptyOperator(), + map((endpoint: string) => (isUpdate ? `${endpoint}/${schema.id}` : endpoint)), + distinctUntilChanged() + ); + + const serializedSchema = new DSpaceSerializer(getClassForType(MetadataSchema.type)).serialize(schema); + + const request$ = endpoint$.pipe( + take(1), + map((endpoint: string) => { + if (isUpdate) { + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'application/json'); + options.headers = headers; + return new UpdateMetadataSchemaRequest(requestId, endpoint, JSON.stringify(serializedSchema), options); + } else { + return new CreateMetadataSchemaRequest(requestId, endpoint, JSON.stringify(serializedSchema)); + } + }) + ); + + // Execute the post/put request + request$.pipe( + configureRequest(this.requestService) + ).subscribe(); + + // Return created/updated schema + return this.requestService.getByUUID(requestId).pipe( + getResponseFromEntry(), + map((response: RestResponse) => { + if (!response.isSuccessful) { + if (hasValue((response as any).errorMessage)) { + this.notificationsService.error('Server Error:', (response as any).errorMessage, new NotificationOptions(-1)); + } + } else { + return response; + } + }), + isNotEmptyOperator() + ); + } + + clearRequests(): Observable { + return this.getBrowseEndpoint().pipe( + tap((href: string) => this.requestService.removeByHrefSubstring(href)) + ); + } + } diff --git a/src/app/core/registry/registry.service.ts b/src/app/core/registry/registry.service.ts index ab846fa6d1..fe1ae63144 100644 --- a/src/app/core/registry/registry.service.ts +++ b/src/app/core/registry/registry.service.ts @@ -2,15 +2,8 @@ import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; import { Injectable } from '@angular/core'; import { RemoteData } from '../data/remote-data'; import { PaginatedList } from '../data/paginated-list'; -import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; import { PageInfo } from '../shared/page-info.model'; -import { - CreateMetadataFieldRequest, - CreateMetadataSchemaRequest, - DeleteRequest, FindListOptions, - UpdateMetadataFieldRequest, - UpdateMetadataSchemaRequest -} from '../data/request.models'; +import { FindListOptions } from '../data/request.models'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RequestService } from '../data/request.service'; import { @@ -19,8 +12,8 @@ import { RestResponse } from '../cache/response.models'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { hasNoValue, hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; -import { configureRequest, getFirstSucceededRemoteDataPayload, getResponseFromEntry } from '../shared/operators'; +import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util'; +import { getFirstSucceededRemoteDataPayload } from '../shared/operators'; import { createSelector, select, Store } from '@ngrx/store'; import { AppState } from '../../app.reducer'; import { MetadataRegistryState } from '../../+admin/admin-registries/metadata-registry/metadata-registry.reducers'; @@ -36,15 +29,11 @@ import { MetadataRegistrySelectFieldAction, MetadataRegistrySelectSchemaAction } from '../../+admin/admin-registries/metadata-registry/metadata-registry.actions'; -import { distinctUntilChanged, flatMap, map, take, tap } from 'rxjs/operators'; +import { flatMap, map, tap } from 'rxjs/operators'; import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; -import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; -import { HttpHeaders } from '@angular/common/http'; import { TranslateService } from '@ngx-translate/core'; import { MetadataSchema } from '../metadata/metadata-schema.model'; import { MetadataField } from '../metadata/metadata-field.model'; -import { getClassForType } from '../cache/builders/build-decorators'; import { MetadataSchemaDataService } from '../data/metadata-schema-data.service'; import { MetadataFieldDataService } from '../data/metadata-field-data.service'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; @@ -61,11 +50,6 @@ const selectedMetadataFieldsSelector = createSelector(metadataRegistryStateSelec @Injectable() export class RegistryService { - private metadataSchemasPath = 'metadataschemas'; - private metadataFieldsPath = 'metadatafields'; - - // private bitstreamFormatsPath = 'bitstreamformats'; - constructor(protected requestService: RequestService, private rdb: RemoteDataBuildService, private halService: HALEndpointService, @@ -232,51 +216,10 @@ export class RegistryService { */ public createOrUpdateMetadataSchema(schema: MetadataSchema): Observable { const isUpdate = hasValue(schema.id); - const requestId = this.requestService.generateRequestId(); - const endpoint$ = this.halService.getEndpoint(this.metadataSchemasPath).pipe( - isNotEmptyOperator(), - map((endpoint: string) => (isUpdate ? `${endpoint}/${schema.id}` : endpoint)), - distinctUntilChanged() - ); - - const serializedSchema = new DSpaceSerializer(getClassForType(MetadataSchema.type)).serialize(schema); - - const request$ = endpoint$.pipe( - take(1), - map((endpoint: string) => { - if (isUpdate) { - const options: HttpOptions = Object.create({}); - let headers = new HttpHeaders(); - headers = headers.append('Content-Type', 'application/json'); - options.headers = headers; - return new UpdateMetadataSchemaRequest(requestId, endpoint, JSON.stringify(serializedSchema), options); - } else { - return new CreateMetadataSchemaRequest(requestId, endpoint, JSON.stringify(serializedSchema)); - } - }) - ); - - // Execute the post/put request - request$.pipe( - configureRequest(this.requestService) - ).subscribe(); - - // Return created/updated schema - return this.requestService.getByUUID(requestId).pipe( - getResponseFromEntry(), - map((response: RestResponse) => { - if (!response.isSuccessful) { - if (hasValue((response as any).errorMessage)) { - this.notificationsService.error('Server Error:', (response as any).errorMessage, new NotificationOptions(-1)); - } - } else { - this.showNotifications(true, isUpdate, false, {prefix: schema.prefix}); - return response; - } - }), - isNotEmptyOperator(), + return this.metadataSchemaService.createOrUpdateMetadataSchema(schema).pipe( map((response: MetadataschemaSuccessResponse) => { if (isNotEmpty(response.metadataschema)) { + this.showNotifications(true, isUpdate, false, {prefix: schema.prefix}); return response.metadataschema; } }) @@ -288,16 +231,14 @@ export class RegistryService { * @param id The id of the metadata schema to delete */ public deleteMetadataSchema(id: number): Observable { - return this.delete(this.metadataSchemasPath, id); + return this.metadataSchemaService.deleteAndReturnResponse(`${id}`); } /** * Method that clears a cached metadata schema request and returns its REST url */ public clearMetadataSchemaRequests(): Observable { - return this.halService.getEndpoint(this.metadataSchemasPath).pipe( - tap((href: string) => this.requestService.removeByHrefSubstring(href)) - ); + return this.metadataSchemaService.clearRequests(); } /** @@ -310,50 +251,11 @@ export class RegistryService { */ public createOrUpdateMetadataField(field: MetadataField): Observable { const isUpdate = hasValue(field.id); - const requestId = this.requestService.generateRequestId(); - const endpoint$ = this.halService.getEndpoint(this.metadataFieldsPath).pipe( - isNotEmptyOperator(), - map((endpoint: string) => (isUpdate ? `${endpoint}/${field.id}` : `${endpoint}?schemaId=${field.schema.id}`)), - distinctUntilChanged() - ); - - const request$ = endpoint$.pipe( - take(1), - map((endpoint: string) => { - if (isUpdate) { - const options: HttpOptions = Object.create({}); - let headers = new HttpHeaders(); - headers = headers.append('Content-Type', 'application/json'); - options.headers = headers; - return new UpdateMetadataFieldRequest(requestId, endpoint, JSON.stringify(field), options); - } else { - return new CreateMetadataFieldRequest(requestId, endpoint, JSON.stringify(field)); - } - }) - ); - - // Execute the post/put request - request$.pipe( - configureRequest(this.requestService) - ).subscribe(); - - // Return created/updated field - return this.requestService.getByUUID(requestId).pipe( - getResponseFromEntry(), - map((response: RestResponse) => { - if (!response.isSuccessful) { - if (hasValue((response as any).errorMessage)) { - this.notificationsService.error('Server Error:', (response as any).errorMessage, new NotificationOptions(-1)); - } - } else { - const fieldString = `${field.schema.prefix}.${field.element}${field.qualifier ? `.${field.qualifier}` : ''}`; - this.showNotifications(true, isUpdate, true, {field: fieldString}); - return response; - } - }), - isNotEmptyOperator(), + return this.metadataFieldService.createOrUpdateMetadataField(field).pipe( map((response: MetadatafieldSuccessResponse) => { if (isNotEmpty(response.metadatafield)) { + const fieldString = `${field.schema.prefix}.${field.element}${field.qualifier ? `.${field.qualifier}` : ''}`; + this.showNotifications(true, isUpdate, true, {field: fieldString}); return response.metadatafield; } }) @@ -365,38 +267,13 @@ export class RegistryService { * @param id The id of the metadata field to delete */ public deleteMetadataField(id: number): Observable { - return this.delete(this.metadataFieldsPath, id); + return this.metadataFieldService.deleteAndReturnResponse(`${id}`); } /** * Method that clears a cached metadata field request and returns its REST url */ public clearMetadataFieldRequests(): Observable { - return this.halService.getEndpoint(this.metadataFieldsPath).pipe( - tap((href: string) => this.requestService.removeByHrefSubstring(href)) - ); - } - - private delete(path: string, id: number): Observable { - const requestId = this.requestService.generateRequestId(); - const endpoint$ = this.halService.getEndpoint(path).pipe( - isNotEmptyOperator(), - map((endpoint: string) => `${endpoint}/${id}`), - distinctUntilChanged() - ); - - const request$ = endpoint$.pipe( - take(1), - map((endpoint: string) => new DeleteRequest(requestId, endpoint)) - ); - - // Execute the delete request - request$.pipe( - configureRequest(this.requestService) - ).subscribe(); - - return this.requestService.getByUUID(requestId).pipe( - getResponseFromEntry() - ); + return this.metadataFieldService.clearRequests(); } private showNotifications(success: boolean, edited: boolean, isField: boolean, options: any) { From cd46f339097cd64d148e6d532767cf3eb3b72f91 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 13 May 2020 13:56:42 +0200 Subject: [PATCH 03/18] 70834: Metadata schema component refactoring and caching issue fix --- .../metadata-field-form.component.ts | 1 + .../metadata-schema.component.html | 62 ++++++++++--------- .../metadata-schema.component.ts | 41 ++++++------ .../core/data/metadata-field-data.service.ts | 22 ++++++- 4 files changed, 76 insertions(+), 50 deletions(-) diff --git a/src/app/+admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.ts b/src/app/+admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.ts index 0811530343..52fee16473 100644 --- a/src/app/+admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.ts +++ b/src/app/+admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.ts @@ -177,6 +177,7 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy { }); } this.clearFields(); + this.registryService.cancelEditMetadataField(); } ); } diff --git a/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.html b/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.html index 4a7a4cf34d..49ef748349 100644 --- a/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.html +++ b/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.html @@ -1,36 +1,37 @@
-
\ No newline at end of file + diff --git a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html index 1181e097eb..4800bb8733 100644 --- a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html +++ b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html @@ -5,6 +5,6 @@ diff --git a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts index 7b5c020f1b..fb49fe084a 100644 --- a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts @@ -63,7 +63,7 @@ describe('DSOSelectorModalWrapperComponent', () => { }); it('should initially set the DSO to the activated route\'s item/collection/community', () => { - component.dsoRD$ + component.dsoRD .pipe(first()) .subscribe((a) => { expect(a).toEqual(itemRD); diff --git a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts index 881476cac6..c13bd2b69d 100644 --- a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts @@ -1,11 +1,12 @@ import { Injectable, Input, OnInit } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router'; import { Observable } from 'rxjs'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { RemoteData } from '../../../core/data/remote-data'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { map } from 'rxjs/operators'; import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model'; +import { hasValue, isNotEmpty } from '../../empty.util'; export enum SelectorActionType { CREATE = 'create', @@ -21,7 +22,7 @@ export abstract class DSOSelectorModalWrapperComponent implements OnInit { /** * The current page's DSO */ - @Input() dsoRD$: Observable>; + @Input() dsoRD: RemoteData; /** * The type of the DSO that's being edited or created @@ -45,10 +46,30 @@ export abstract class DSOSelectorModalWrapperComponent implements OnInit { * Get de current page's DSO based on the selectorType */ ngOnInit(): void { - const typeString = this.selectorType.toString().toLowerCase(); - this.dsoRD$ = this.route.root.firstChild.firstChild.data.pipe(map((data) => data[typeString])); + const matchingRoute = this.findRouteData( + (route: ActivatedRouteSnapshot) => hasValue(route.data.dso), + this.route.root.snapshot + ); + if (hasValue(matchingRoute)) { + this.dsoRD = matchingRoute.data.dso; + } } + findRouteData(predicate: (value: ActivatedRouteSnapshot, index?: number, obj?: ActivatedRouteSnapshot[]) => unknown, ...routes: ActivatedRouteSnapshot[]) { + const result = routes.find(predicate); + if (hasValue(result)) { + return result; + } else { + const nextLevelRoutes = routes + .map((route: ActivatedRouteSnapshot) => route.children) + .reduce((combined: ActivatedRouteSnapshot[], current: ActivatedRouteSnapshot[]) => [...combined, ...current]); + if (isNotEmpty(nextLevelRoutes)) { + return this.findRouteData(predicate, ...nextLevelRoutes) + } else { + return undefined; + } + } + } /** * Method called when an object has been selected * @param dso The selected DSpaceObject From 405815ac5a60ccf047f02db5a44ed1dd193e7ad2 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Mon, 20 Apr 2020 13:36:01 +0200 Subject: [PATCH 12/18] fix test --- .../dso-selector-modal-wrapper.component.spec.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts index fb49fe084a..c51edc5d9c 100644 --- a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts @@ -63,11 +63,7 @@ describe('DSOSelectorModalWrapperComponent', () => { }); it('should initially set the DSO to the activated route\'s item/collection/community', () => { - component.dsoRD - .pipe(first()) - .subscribe((a) => { - expect(a).toEqual(itemRD); - }) + expect(component.dsoRD).toEqual(itemRD); }); describe('selectObject', () => { From 6c1e636f5c257fe83b84e8018a8e442641aa2c82 Mon Sep 17 00:00:00 2001 From: Samuel Date: Wed, 27 May 2020 14:15:46 +0200 Subject: [PATCH 13/18] fix tests --- ...eate-collection-parent-selector.component.spec.ts | 10 +++++++++- ...reate-community-parent-selector.component.spec.ts | 10 +++++++++- .../create-item-parent-selector.component.spec.ts | 10 +++++++++- .../dso-selector-modal-wrapper.component.spec.ts | 12 ++++++++++-- .../edit-collection-selector.component.spec.ts | 10 +++++++++- .../edit-community-selector.component.spec.ts | 10 +++++++++- .../edit-item-selector.component.spec.ts | 10 +++++++++- 7 files changed, 64 insertions(+), 8 deletions(-) diff --git a/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.spec.ts index 480f6ff709..df62534593 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.spec.ts @@ -39,7 +39,15 @@ describe('CreateCollectionParentSelectorComponent', () => { { provide: NgbActiveModal, useValue: modalStub }, { provide: ActivatedRoute, - useValue: { root: { firstChild: { firstChild: { data: observableOf({ community: communityRD }) } } } } + useValue: { + root: { + snapshot: { + data: { + dso: communityRD, + }, + }, + } + }, }, { provide: Router, useValue: router diff --git a/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.spec.ts index b723d3fe98..9c6185199c 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.spec.ts @@ -33,7 +33,15 @@ describe('CreateCommunityParentSelectorComponent', () => { { provide: NgbActiveModal, useValue: modalStub }, { provide: ActivatedRoute, - useValue: { root: { firstChild: { firstChild: { data: observableOf({ community: communityRD }) } } } } + useValue: { + root: { + snapshot: { + data: { + dso: communityRD, + }, + }, + } + }, }, { provide: Router, useValue: router diff --git a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.spec.ts index 854349a47c..e8cd35fb50 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.spec.ts @@ -32,7 +32,15 @@ describe('CreateItemParentSelectorComponent', () => { { provide: NgbActiveModal, useValue: modalStub }, { provide: ActivatedRoute, - useValue: { root: { firstChild: { firstChild: { data: observableOf({ collection: collectionRD }) } } } } + useValue: { + root: { + snapshot: { + data: { + dso: collectionRD, + }, + }, + } + }, }, { provide: Router, useValue: router diff --git a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts index c51edc5d9c..f52ce3fc8f 100644 --- a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.spec.ts @@ -41,8 +41,16 @@ describe('DSOSelectorModalWrapperComponent', () => { { provide: NgbActiveModal, useValue: modalStub }, { provide: ActivatedRoute, - useValue: { root: { firstChild: { firstChild: { data: observableOf({ item: itemRD }) } } } } - } + useValue: { + root: { + snapshot: { + data: { + dso: itemRD, + }, + }, + } + } + }, ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); diff --git a/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.spec.ts index a17d9e4c21..21ff5e846d 100644 --- a/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.spec.ts @@ -33,7 +33,15 @@ describe('EditCollectionSelectorComponent', () => { { provide: NgbActiveModal, useValue: modalStub }, { provide: ActivatedRoute, - useValue: { root: { firstChild: { firstChild: { data: observableOf({ collection: collectionRD }) } } } } + useValue: { + root: { + snapshot: { + data: { + dso: collectionRD, + }, + }, + } + }, }, { provide: Router, useValue: router diff --git a/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.spec.ts index c48d29baa9..b37fa23024 100644 --- a/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.spec.ts @@ -33,7 +33,15 @@ describe('EditCommunitySelectorComponent', () => { { provide: NgbActiveModal, useValue: modalStub }, { provide: ActivatedRoute, - useValue: { root: { firstChild: { firstChild: { data: observableOf({ community: communityRD }) } } } } + useValue: { + root: { + snapshot: { + data: { + dso: communityRD, + }, + }, + } + }, }, { provide: Router, useValue: router diff --git a/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.spec.ts index 582320acae..e310d6ac02 100644 --- a/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.spec.ts @@ -33,7 +33,15 @@ describe('EditItemSelectorComponent', () => { { provide: NgbActiveModal, useValue: modalStub }, { provide: ActivatedRoute, - useValue: { root: { firstChild: { firstChild: { data: observableOf({ item: itemRD }) } } } } + useValue: { + root: { + snapshot: { + data: { + dso: itemRD, + }, + }, + } + }, }, { provide: Router, useValue: router From ee9f5ec7f1d9b5af20f36460f8723cfe6961de50 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 11 Jun 2020 15:48:26 +0200 Subject: [PATCH 14/18] 70834: MetadataField search responseMsToLive fix --- .../metadata-registry.component.html | 1 - .../core/data/metadata-field-data.service.ts | 34 +++++++++++++++++-- src/app/core/registry/registry.service.ts | 6 ++-- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.html b/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.html index a254f20428..42b7558397 100644 --- a/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.html +++ b/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.html @@ -11,7 +11,6 @@ { ); } + /** + * Make a new FindListRequest with given search method + * + * @param searchMethod The search method for the object + * @param options The [[FindListOptions]] object + * @param linksToFollow The array of [[FollowLinkConfig]] + * @return {Observable>} + * Return an observable that emits response from the server + */ + searchBy(searchMethod: string, options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { + 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); + + this.requestService.configure(request); + } + ), + switchMap((href) => this.requestService.getByHref(href)), + skipWhile((requestEntry) => hasValue(requestEntry) && requestEntry.completed), + switchMap((href) => + this.rdbService.buildList(hrefObs, ...linksToFollow) as Observable>> + ) + ); + } + } diff --git a/src/app/core/registry/registry.service.ts b/src/app/core/registry/registry.service.ts index 83e5fc9c64..79b982da8a 100644 --- a/src/app/core/registry/registry.service.ts +++ b/src/app/core/registry/registry.service.ts @@ -12,7 +12,7 @@ import { RestResponse } from '../cache/response.models'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { hasNoValue, hasValue, hasValueOperator, isNotEmpty } from '../../shared/empty.util'; +import { hasNoValue, hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; import { getAllSucceededRemoteDataPayload, getFirstSucceededRemoteDataPayload } from '../shared/operators'; import { createSelector, select, Store } from '@ngrx/store'; import { AppState } from '../../app.reducer'; @@ -79,7 +79,9 @@ export class RegistryService { }); return this.getMetadataSchemas(options).pipe( getFirstSucceededRemoteDataPayload(), - map((schemas: PaginatedList) => schemas.page.filter((schema) => schema.prefix === schemaName)[0]), + map((schemas: PaginatedList) => schemas.page), + isNotEmptyOperator(), + map((schemas: MetadataSchema[]) => schemas.filter((schema) => schema.prefix === schemaName)[0]), flatMap((schema: MetadataSchema) => this.metadataSchemaService.findById(`${schema.id}`, ...linksToFollow)) ); } From 7421eef223c21a0f5f8f1bb984ca81cfd580986a Mon Sep 17 00:00:00 2001 From: Samuel Date: Fri, 12 Jun 2020 09:54:13 +0200 Subject: [PATCH 15/18] Misc edit community and collection bugs --- .../comcol-form/comcol-form.component.spec.ts | 66 +++++++++++-------- .../comcol-form/comcol-form.component.ts | 32 +++++++-- .../comcol-metadata.component.spec.ts | 31 ++++++--- .../comcol-metadata.component.ts | 42 +++++++----- src/assets/i18n/en.json5 | 4 ++ 5 files changed, 115 insertions(+), 60 deletions(-) diff --git a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.spec.ts b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.spec.ts index a1bac46f87..3fcdc280d0 100644 --- a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.spec.ts +++ b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.spec.ts @@ -22,6 +22,7 @@ import { NotificationsService } from '../../notifications/notifications.service' import { NotificationsServiceStub } from '../../testing/notifications-service.stub'; import { VarDirective } from '../../utils/var.directive'; import { ComColFormComponent } from './comcol-form.component'; +import { Operation } from 'fast-json-patch'; describe('ComColFormComponent', () => { let comp: ComColFormComponent; @@ -40,11 +41,8 @@ describe('ComColFormComponent', () => { } }; const dcTitle = 'dc.title'; - const dcRandom = 'dc.random'; const dcAbstract = 'dc.description.abstract'; - const titleMD = { [dcTitle]: [{ value: 'Community Title', language: null }] }; - const randomMD = { [dcRandom]: [{ value: 'Random metadata excluded from form', language: null }] }; const abstractMD = { [dcAbstract]: [{ value: 'Community description', language: null }] }; const newTitleMD = { [dcTitle]: [{ value: 'New Community Title', language: null }] }; const formModel = [ @@ -112,33 +110,47 @@ describe('ComColFormComponent', () => { }); it('should emit the new version of the community', () => { - comp.dso = Object.assign( - new Community(), - { - metadata: { - ...titleMD, - ...randomMD - } - } - ); + comp.dso = new Community(); comp.onSubmit(); + const operations: Operation[] = [ + { + op: 'replace', + path: '/metadata/dc.title', + value: { + value: 'New Community Title', + language: null, + }, + }, + { + op: 'replace', + path: '/metadata/dc.description.abstract', + value: { + value: 'Community description', + language: null, + }, + }, + ]; + expect(comp.submitForm.emit).toHaveBeenCalledWith( { - dso: Object.assign( - {}, - new Community(), - { + dso: Object.assign({}, comp.dso, { metadata: { - ...newTitleMD, - ...randomMD, - ...abstractMD + 'dc.title': [{ + value: 'New Community Title', + language: null, + }], + 'dc.description.abstract': [{ + value: 'Community description', + language: null, + }], }, - type: Community.type - }, + type: Community.type, + } ), uploader: undefined, - deleteLogo: false + deleteLogo: false, + operations: operations, } ); }) @@ -164,11 +176,6 @@ describe('ComColFormComponent', () => { it('should emit finish', () => { expect(comp.finish.emit).toHaveBeenCalled(); }); - - it('should remove the object\'s cache', () => { - expect(requestServiceStub.removeByHrefSubstring).toHaveBeenCalled(); - expect(objectCacheStub.remove).toHaveBeenCalled(); - }); }); describe('onUploadError', () => { @@ -239,6 +246,11 @@ describe('ComColFormComponent', () => { it('should display a success notification', () => { expect(notificationsService.success).toHaveBeenCalled(); }); + + it('should remove the object\'s cache', () => { + expect(requestServiceStub.removeByHrefSubstring).toHaveBeenCalled(); + expect(objectCacheStub.remove).toHaveBeenCalled(); + }); }); describe('when dsoService.deleteLogo returns an error response', () => { diff --git a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts index f8199d2aad..91e896ce6c 100644 --- a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts +++ b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts @@ -25,6 +25,7 @@ import { hasValue, isNotEmpty } from '../../empty.util'; import { NotificationsService } from '../../notifications/notifications.service'; import { UploaderOptions } from '../../uploader/uploader-options.model'; import { UploaderComponent } from '../../uploader/uploader.component'; +import { Operation } from 'fast-json-patch'; /** * A form for creating and editing Communities or Collections @@ -85,7 +86,8 @@ export class ComColFormComponent implements OnInit, OnDe @Output() submitForm: EventEmitter<{ dso: T, uploader: FileUploader, - deleteLogo: boolean + deleteLogo: boolean, + operations: Operation[], }> = new EventEmitter(); /** @@ -189,9 +191,9 @@ export class ComColFormComponent implements OnInit, OnDe const formMetadata = {} as MetadataMap; this.formModel.forEach((fieldModel: DynamicInputModel) => { const value: MetadataValue = { - value: fieldModel.value as string, - language: null - } as any; + value: fieldModel.value as string, + language: null + } as any; if (formMetadata.hasOwnProperty(fieldModel.name)) { formMetadata[fieldModel.name].push(value); } else { @@ -206,10 +208,26 @@ export class ComColFormComponent implements OnInit, OnDe }, type: Community.type }); + + const operations: Operation[] = []; + this.formModel.forEach((fieldModel: DynamicInputModel) => { + if (fieldModel.value !== this.dso.firstMetadataValue(fieldModel.name)) { + operations.push({ + op: 'replace', + path: `/metadata/${fieldModel.name}`, + value: { + value: fieldModel.value, + language: null, + }, + }); + } + }); + this.submitForm.emit({ dso: updatedDSO, uploader: hasValue(this.uploaderComponent) ? this.uploaderComponent.uploader : undefined, - deleteLogo: this.markLogoForDeletion + deleteLogo: this.markLogoForDeletion, + operations: operations, }); } @@ -257,7 +275,9 @@ export class ComColFormComponent implements OnInit, OnDe * The request was successful, display a success notification */ public onCompleteItem() { - this.refreshCache(); + if (hasValue(this.dso.id)) { + this.refreshCache(); + } this.notificationsService.success(null, this.translate.get(this.type.value + '.edit.logo.notifications.add.success')); this.finish.emit(); } diff --git a/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.spec.ts b/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.spec.ts index 414d64cbff..c606f50a71 100644 --- a/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.spec.ts +++ b/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.spec.ts @@ -27,6 +27,7 @@ describe('ComColMetadataComponent', () => { let communityDataServiceStub; let routerStub; let routeStub; + let isSuccessful = true; const logoEndpoint = 'rest/api/logo/endpoint'; @@ -49,6 +50,11 @@ describe('ComColMetadataComponent', () => { communityDataServiceStub = { update: (com, uuid?) => createSuccessfulRemoteDataObject$(newCommunity), + patch: () => { + return observableOf({ + isSuccessful, + }) + }, getLogoEndpoint: () => observableOf(logoEndpoint) }; @@ -95,21 +101,28 @@ describe('ComColMetadataComponent', () => { describe('with an empty queue in the uploader', () => { beforeEach(() => { data = { - dso: Object.assign(new Community(), { - metadata: [{ - key: 'dc.title', - value: 'test' - }] - }), + operations: [ + { + op: 'replace', + path: '/metadata/dc.title', + value: { + value: 'test', + language: null, + }, + }, + ], + dso: new Community(), uploader: { options: { url: '' }, queue: [], /* tslint:disable:no-empty */ - uploadAll: () => {} + uploadAll: () => { + } /* tslint:enable:no-empty */ - } + }, + deleteLogo: false, } }); @@ -121,8 +134,8 @@ describe('ComColMetadataComponent', () => { }); it('should not navigate on failure', () => { + isSuccessful = false; spyOn(router, 'navigate'); - spyOn(dsoDataService, 'update').and.returnValue(createFailedRemoteDataObject$(newCommunity)); comp.onSubmit(data); fixture.detectChanges(); expect(router.navigate).not.toHaveBeenCalled(); diff --git a/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.ts b/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.ts index 1031fead10..02c28d989c 100644 --- a/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.ts +++ b/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.ts @@ -5,8 +5,7 @@ import { RemoteData } from '../../../../core/data/remote-data'; import { ActivatedRoute, Router } from '@angular/router'; import { first, map, take } from 'rxjs/operators'; import { getSucceededRemoteData } from '../../../../core/shared/operators'; -import { hasValue, isNotUndefined } from '../../../empty.util'; -import { DataService } from '../../../../core/data/data.service'; +import { hasValue, isEmpty } from '../../../empty.util'; import { ResourceType } from '../../../../core/shared/resource-type'; import { ComColDataService } from '../../../../core/data/comcol-data.service'; import { NotificationsService } from '../../../notifications/notifications.service'; @@ -49,26 +48,33 @@ export class ComcolMetadataComponent implements On * @param event The event returned by the community/collection form. Contains the new dso and logo uploader */ onSubmit(event) { - const dso = event.dso; + const uploader = event.uploader; const deleteLogo = event.deleteLogo; - this.dsoDataService.update(dso) - .pipe(getSucceededRemoteData()) - .subscribe((dsoRD: RemoteData) => { - if (isNotUndefined(dsoRD)) { - const newUUID = dsoRD.payload.uuid; - if (hasValue(uploader) && uploader.queue.length > 0) { - this.dsoDataService.getLogoEndpoint(newUUID).pipe(take(1)).subscribe((href: string) => { - uploader.options.url = href; - uploader.uploadAll(); - }); - } else if (!deleteLogo) { - this.router.navigate([this.frontendURL + newUUID]); - } - this.notificationsService.success(null, this.translate.get(this.type.value + '.edit.notifications.success')); - } + const newLogo = hasValue(uploader) && uploader.queue.length > 0; + if (newLogo) { + this.dsoDataService.getLogoEndpoint(event.dso.uuid).pipe(take(1)).subscribe((href: string) => { + uploader.options.url = href; + uploader.uploadAll(); }); + } + + if (!isEmpty(event.operations)) { + this.dsoDataService.patch(event.dso, event.operations) + .subscribe(async (response) => { + if (response.isSuccessful) { + if (!newLogo && !deleteLogo) { + await this.router.navigate([this.frontendURL + event.dso.uuid]); + } + this.notificationsService.success(null, this.translate.get(`${this.type.value}.edit.notifications.success`)); + } else if (response.statusCode === 403) { + this.notificationsService.error(null, this.translate.get(`${this.type.value}.edit.notifications.unauthorized`)); + } else { + this.notificationsService.error(null, this.translate.get(`${this.type.value}.edit.notifications.error`)); + } + }); + } } /** diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 4173fa1cf2..aad6cbf7b1 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -767,6 +767,10 @@ "community.edit.notifications.success": "Successfully edited the Community", + "community.edit.notifications.unauthorized": "You do not have privileges to make this change", + + "community.edit.notifications.error": "An error occured while editing the Community", + "community.edit.return": "Return", From b8ab83ce998ed95c3e36a618d5e5bd4a9d1b9edc Mon Sep 17 00:00:00 2001 From: Samuel Date: Fri, 12 Jun 2020 11:11:31 +0200 Subject: [PATCH 16/18] Misc edit community and collection bugs - repair test --- .../comcol-metadata.component.spec.ts | 51 +++++++++++-------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.spec.ts b/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.spec.ts index c606f50a71..b50a1d3ac4 100644 --- a/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.spec.ts +++ b/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.spec.ts @@ -13,13 +13,13 @@ import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; import { NotificationsService } from '../../../notifications/notifications.service'; import { SharedModule } from '../../../shared.module'; import { NotificationsServiceStub } from '../../../testing/notifications-service.stub'; -import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../remote-data.utils'; +import { createSuccessfulRemoteDataObject$ } from '../../../remote-data.utils'; import { ComcolMetadataComponent } from './comcol-metadata.component'; describe('ComColMetadataComponent', () => { let comp: ComcolMetadataComponent; let fixture: ComponentFixture>; - let dsoDataService: CommunityDataService; + let dsoDataService; let router: Router; let community; @@ -27,7 +27,6 @@ describe('ComColMetadataComponent', () => { let communityDataServiceStub; let routerStub; let routeStub; - let isSuccessful = true; const logoEndpoint = 'rest/api/logo/endpoint'; @@ -50,11 +49,7 @@ describe('ComColMetadataComponent', () => { communityDataServiceStub = { update: (com, uuid?) => createSuccessfulRemoteDataObject$(newCommunity), - patch: () => { - return observableOf({ - isSuccessful, - }) - }, + patch: () => null, getLogoEndpoint: () => observableOf(logoEndpoint) }; @@ -123,22 +118,38 @@ describe('ComColMetadataComponent', () => { /* tslint:enable:no-empty */ }, deleteLogo: false, - } + }; + spyOn(router, 'navigate'); }); - it('should navigate when successful', () => { - spyOn(router, 'navigate'); - comp.onSubmit(data); - fixture.detectChanges(); - expect(router.navigate).toHaveBeenCalled(); + describe('when successful', () => { + + beforeEach(() => { + spyOn(dsoDataService, 'patch').and.returnValue(observableOf({ + isSuccessful: true, + })); + }); + + it('should navigate', () => { + comp.onSubmit(data); + fixture.detectChanges(); + expect(router.navigate).toHaveBeenCalled(); + }); }); - it('should not navigate on failure', () => { - isSuccessful = false; - spyOn(router, 'navigate'); - comp.onSubmit(data); - fixture.detectChanges(); - expect(router.navigate).not.toHaveBeenCalled(); + describe('on failure', () => { + + beforeEach(() => { + spyOn(dsoDataService, 'patch').and.returnValue(observableOf({ + isSuccessful: false, + })); + }); + + it('should not navigate', () => { + comp.onSubmit(data); + fixture.detectChanges(); + expect(router.navigate).not.toHaveBeenCalled(); + }); }); }); From 0721b844f5f18528ba39a8865a347ea6c3542fc0 Mon Sep 17 00:00:00 2001 From: Samuel Date: Thu, 18 Jun 2020 12:25:50 +0200 Subject: [PATCH 17/18] Misc edit community and collection bugs - repair create top level community --- .../create-comcol-page/create-comcol-page.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts index a8d6499cbd..4a7cd9afb1 100644 --- a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts +++ b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts @@ -77,7 +77,8 @@ export class CreateComColPageComponent implements const uploader = event.uploader; this.parentUUID$.pipe(take(1)).subscribe((uuid: string) => { - this.dsoDataService.create(dso, new RequestParam('parent', uuid)) + const params = uuid ? [new RequestParam('parent', uuid)] : []; + this.dsoDataService.create(dso, ...params) .pipe(getSucceededRemoteData()) .subscribe((dsoRD: RemoteData) => { if (isNotUndefined(dsoRD)) { From 4c1b5891587a09c7f643438a06653aaf40c78cc6 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 19 Jun 2020 11:09:02 +0200 Subject: [PATCH 18/18] 70834: Use data-service's responseMsToLive for searchBy --- src/app/core/data/data.service.ts | 4 ++- .../core/data/metadata-field-data.service.ts | 29 ------------------- 2 files changed, 3 insertions(+), 30 deletions(-) diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index e31095ca65..7f77d72d3a 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -344,7 +344,9 @@ export abstract class DataService { tap((href: string) => { this.requestService.removeByHrefSubstring(href); const request = new FindListRequest(this.requestService.generateRequestId(), href, options); - request.responseMsToLive = 10 * 1000; + if (hasValue(this.responseMsToLive)) { + request.responseMsToLive = this.responseMsToLive; + } this.requestService.configure(request); } diff --git a/src/app/core/data/metadata-field-data.service.ts b/src/app/core/data/metadata-field-data.service.ts index 34b438cc10..f50be20f13 100644 --- a/src/app/core/data/metadata-field-data.service.ts +++ b/src/app/core/data/metadata-field-data.service.ts @@ -86,33 +86,4 @@ export class MetadataFieldDataService extends DataService { ); } - /** - * Make a new FindListRequest with given search method - * - * @param searchMethod The search method for the object - * @param options The [[FindListOptions]] object - * @param linksToFollow The array of [[FollowLinkConfig]] - * @return {Observable>} - * Return an observable that emits response from the server - */ - searchBy(searchMethod: string, options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { - 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); - - this.requestService.configure(request); - } - ), - switchMap((href) => this.requestService.getByHref(href)), - skipWhile((requestEntry) => hasValue(requestEntry) && requestEntry.completed), - switchMap((href) => - this.rdbService.buildList(hrefObs, ...linksToFollow) as Observable>> - ) - ); - } - }