From ed724733660df7d66d07f15b57200d0cc1ef9597 Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Wed, 8 Nov 2023 15:35:50 +0100 Subject: [PATCH 1/8] [DSC-1309] Mirador's index.js refactoring and fixes --- src/mirador-viewer/config.default.js | 236 +++++++++++++-------------- 1 file changed, 118 insertions(+), 118 deletions(-) diff --git a/src/mirador-viewer/config.default.js b/src/mirador-viewer/config.default.js index 56f2d5a81b3..12ecea187cd 100644 --- a/src/mirador-viewer/config.default.js +++ b/src/mirador-viewer/config.default.js @@ -26,7 +26,7 @@ const multi = params.get('multi'); const notMobile = params.get('notMobile'); let windowSettings = {}; -let sidbarPanel = 'info'; +let sideBarPanel = 'info'; let defaultView = 'single'; let multipleItems = false; let thumbNavigation = 'off'; @@ -36,7 +36,7 @@ windowSettings.manifestId = manifest; (() => { if (searchOption) { defaultView = 'book'; - sidbarPanel = 'search'; + sideBarPanel = 'search'; multipleItems = true; if (notMobile) { thumbNavigation = 'far-right'; @@ -45,7 +45,7 @@ windowSettings.manifestId = manifest; windowSettings.defaultSearchQuery = query; } } else { - if(multi) { + if (multi) { multipleItems = multi; if (notMobile) { thumbNavigation = 'far-right'; @@ -54,127 +54,127 @@ windowSettings.manifestId = manifest; } })(); -(Mirador.viewer( - { - id: 'mirador', - mainMenuSettings: { - show: true - }, - thumbnailNavigation: { - defaultPosition: thumbNavigation, // Which position for the thumbnail navigation to be be displayed. Other possible values are "far-bottom" or "far-right" - displaySettings: true, // Display the settings for this in WindowTopMenu - height: 120, // height of entire ThumbnailNavigation area when position is "far-bottom" - width: 100, // width of one canvas (doubled for book view) in ThumbnailNavigation area when position is "far-right" - }, - themes: { - light: { - palette: { - type: 'light', - primary: { - main: '#266883', - }, - secondary: { - main: '#b03727', - }, - shades: { // Shades that can be used to offset color areas of the Workspace / Window - dark: '#eeeeee', - main: '#ffffff', - light: '#ffffff', - }, - highlights: { - primary: '#ffff00', - secondary: '#00BFFF', - }, - search: { - default: { fillStyle: '#00BFFF', globalAlpha: 0.3 }, - hovered: { fillStyle: '#00FFFF', globalAlpha: 0.3 }, - selected: { fillStyle: '#ff0900', globalAlpha: 0.3 }, - }, - }, +const miradorConfiguration = { + id: 'mirador', + mainMenuSettings: { + show: true + }, + thumbnailNavigation: { + defaultPosition: thumbNavigation, // Which position for the thumbnail navigation to be be displayed. Other possible values are "far-bottom" or "far-right" + displaySettings: true, // Display the settings for this in WindowTopMenu + height: 120, // height of entire ThumbnailNavigation area when position is "far-bottom" + width: 100, // width of one canvas (doubled for book view) in ThumbnailNavigation area when position is "far-right" + }, + themes: { + light: { + palette: { + type: 'light', + primary: { + main: '#266883', }, - dark: { - palette: { - type: 'dark', - primary: { - main: '#2790b0', - }, - secondary: { - main: '#eeeeee', - }, - highlights: { - primary: '#ffff00', - secondary: '#00BFFF', - }, - }, + secondary: { + main: '#b03727', }, - }, - selectedTheme: 'light', - data: [manifest], - windows: [ - windowSettings - ], - miradorSharePlugin: { - dragAndDropInfoLink: 'https://iiif.io', - embedOption: { - enabled: true, - embedUrlReplacePattern: [ - /.*/, - embedURL - ], - syncIframeDimensions: { - height: {param: 'maxheight'}, - }, + shades: { // Shades that can be used to offset color areas of the Workspace / Window + dark: '#eeeeee', + main: '#ffffff', + light: '#ffffff', }, - shareLink: { - enabled: true, - manifestIdReplacePattern: [ - /\/iiif\/manifest/, - '', - ], + highlights: { + primary: '#ffff00', + secondary: '#00BFFF', + }, + search: { + default: { fillStyle: '#00BFFF', globalAlpha: 0.3 }, + hovered: { fillStyle: '#00FFFF', globalAlpha: 0.3 }, + selected: { fillStyle: '#ff0900', globalAlpha: 0.3 }, }, }, - miradorDownloadPlugin: { - restrictDownloadOnSizeDefinition: false - }, - window: { - allowClose: false, - // sideBarOpenByDefault: false, - allowFullscreen: true, - allowMaximize: false, - defaultView: defaultView, - sideBarOpen: notMobile, - allowTopMenuButton: true, - defaultSidebarPanelWidth: 230, - switchCanvasOnSearch: true, - views: [ - { key: 'single', behaviors: ['individuals'] }, - { key: 'book', behaviors: ['paged'] }, - { key: 'scroll', behaviors: ['continuous'] }, - { key: 'gallery' }, - ], - panels: { - info: true, - attribution: false, - canvas: true, - search: searchOption, - layers: false, + }, + dark: { + palette: { + type: 'dark', + primary: { + main: '#2790b0', + }, + secondary: { + main: '#eeeeee', + }, + highlights: { + primary: '#ffff00', + secondary: '#00BFFF', }, - sideBarPanel: sidbarPanel }, - workspace: { - allowNewWindows: false, - showZoomControls: true, - type: 'mosaic' + }, + }, + selectedTheme: 'light', + data: [manifest], + windows: [ + windowSettings + ], + miradorSharePlugin: { + dragAndDropInfoLink: 'https://iiif.io', + embedOption: { + enabled: true, + embedUrlReplacePattern: [ + /.*/, + embedURL + ], + syncIframeDimensions: { + height: {param: 'maxheight'}, }, - workspaceControlPanel: { - enabled: false - } }, - [ - miradorShareDialogPlugin, - miradorSharePlugin, - miradorDownloadDialog, - miradorDownloadPlugin - ] - ) -)(manifest); + shareLink: { + enabled: true, + manifestIdReplacePattern: [ + /\/iiif\/manifest/, + '', + ], + }, + }, + miradorDownloadPlugin: { + restrictDownloadOnSizeDefinition: false + }, + window: { + allowClose: false, + // sideBarOpenByDefault: false, + allowFullscreen: true, + allowMaximize: false, + defaultView: defaultView, + sideBarOpen: notMobile, + allowTopMenuButton: true, + defaultSidebarPanelWidth: 230, + switchCanvasOnSearch: true, + views: [ + { key: 'single', behaviors: ['individuals'] }, + { key: 'book', behaviors: ['paged'] }, + { key: 'scroll', behaviors: ['continuous'] }, + { key: 'gallery' }, + ], + panels: { + info: true, + attribution: false, + canvas: true, + search: searchOption, + layers: false, + }, + sideBarPanel: sideBarPanel + }, + workspace: { + allowNewWindows: false, + showZoomControls: true, + type: 'mosaic' + }, + workspaceControlPanel: { + enabled: false + } +}; + +const miradorPlugins = [ + miradorShareDialogPlugin, + miradorSharePlugin, + miradorDownloadDialog, + miradorDownloadPlugin +]; + +Mirador.viewer(miradorConfiguration, miradorPlugins); From 2afc37ddd14e69ad9c412e847c3c970c173c231a Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Thu, 9 Nov 2023 12:44:01 +0100 Subject: [PATCH 2/8] [DSC-1309] add `isDownloadPluginEnabled` flag to Mirador configuration --- src/mirador-viewer/config.default.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/mirador-viewer/config.default.js b/src/mirador-viewer/config.default.js index 12ecea187cd..b8b735ec9b2 100644 --- a/src/mirador-viewer/config.default.js +++ b/src/mirador-viewer/config.default.js @@ -25,11 +25,15 @@ const query = params.get('query'); const multi = params.get('multi'); const notMobile = params.get('notMobile'); +let isDownloadPluginEnabled = true; let windowSettings = {}; let sideBarPanel = 'info'; let defaultView = 'single'; let multipleItems = false; let thumbNavigation = 'off'; +let downloadPluginSettings = { + restrictDownloadOnSizeDefinition: false +}; windowSettings.manifestId = manifest; @@ -54,7 +58,7 @@ windowSettings.manifestId = manifest; } })(); -const miradorConfiguration = { +let miradorConfiguration = { id: 'mirador', mainMenuSettings: { show: true @@ -132,9 +136,6 @@ const miradorConfiguration = { ], }, }, - miradorDownloadPlugin: { - restrictDownloadOnSizeDefinition: false - }, window: { allowClose: false, // sideBarOpenByDefault: false, @@ -170,11 +171,17 @@ const miradorConfiguration = { } }; -const miradorPlugins = [ +let miradorPlugins = [ miradorShareDialogPlugin, miradorSharePlugin, miradorDownloadDialog, - miradorDownloadPlugin ]; +(() => { + if (isDownloadPluginEnabled) { + miradorPlugins = [...miradorPlugins, miradorDownloadPlugin]; + miradorConfiguration.miradorDownloadPlugin = downloadPluginSettings; + } +})(); + Mirador.viewer(miradorConfiguration, miradorPlugins); From 51ed9a5f7713cf59f552cb0c26157e73e0d9c1fe Mon Sep 17 00:00:00 2001 From: Davide Negretti Date: Fri, 10 Nov 2023 18:12:14 +0100 Subject: [PATCH 3/8] [DSC-1309] enable Mirador download plugin from configuration --- src/app/item-page/mirador-viewer/mirador-viewer.component.ts | 3 +++ src/config/app-config.interface.ts | 2 ++ src/config/default-app-config.ts | 5 +++++ src/config/mirador-config.interfaces.ts | 5 +++++ src/environments/environment.test.ts | 4 ++++ src/mirador-viewer/config.default.js | 2 +- 6 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 src/config/mirador-config.interfaces.ts diff --git a/src/app/item-page/mirador-viewer/mirador-viewer.component.ts b/src/app/item-page/mirador-viewer/mirador-viewer.component.ts index 15ebfc61bc6..4d71e8190c1 100644 --- a/src/app/item-page/mirador-viewer/mirador-viewer.component.ts +++ b/src/app/item-page/mirador-viewer/mirador-viewer.component.ts @@ -87,6 +87,9 @@ export class MiradorViewerComponent implements OnInit { if (this.notMobile) { viewerPath += '¬Mobile=true'; } + if (environment.mirador.enableDownloadPlugin) { + viewerPath += '&enableDownloadPlugin=true'; + } // TODO: Should the query term be trusted here? return this.sanitizer.bypassSecurityTrustResourceUrl(viewerPath); diff --git a/src/config/app-config.interface.ts b/src/config/app-config.interface.ts index f95a82dd399..3b41ea234fd 100644 --- a/src/config/app-config.interface.ts +++ b/src/config/app-config.interface.ts @@ -31,6 +31,7 @@ import { FollowAuthorityMetadata } from './search-follow-metadata.interface'; import { AdvancedAttachmentRenderingConfig } from './advanced-attachment-rendering.config'; import { AttachmentRenderingConfig } from './attachment-rendering.config'; import { SearchResultConfig } from './search-result-config.interface'; +import { MiradorConfig } from './mirador-config.interfaces'; interface AppConfig extends Config { ui: UIServerConfig; @@ -68,6 +69,7 @@ interface AppConfig extends Config { attachmentRendering: AttachmentRenderingConfig; advancedAttachmentRendering: AdvancedAttachmentRenderingConfig; searchResult: SearchResultConfig; + mirador: MiradorConfig; } /** diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index d8d8b7bb42b..651c53aac9a 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -34,6 +34,7 @@ import { } from './advanced-attachment-rendering.config'; import { AttachmentRenderingConfig } from './attachment-rendering.config'; import { SearchResultConfig } from './search-result-config.interface'; +import { MiradorConfig } from './mirador-config.interfaces'; export class DefaultAppConfig implements AppConfig { production = false; @@ -750,4 +751,8 @@ export class DefaultAppConfig implements AppConfig { additionalMetadataFields: [], authorMetadata: ['dc.contributor.author', 'dc.creator', 'dc.contributor.*'], }; + + mirador: MiradorConfig = { + enableDownloadPlugin: true, + }; } diff --git a/src/config/mirador-config.interfaces.ts b/src/config/mirador-config.interfaces.ts new file mode 100644 index 00000000000..a46554959c5 --- /dev/null +++ b/src/config/mirador-config.interfaces.ts @@ -0,0 +1,5 @@ +import { Config } from './config.interface'; + +export interface MiradorConfig extends Config { + enableDownloadPlugin: boolean; +} diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts index 1d932a8172b..c3f931b44d9 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -568,4 +568,8 @@ export const environment: BuildConfig = { ], authorMetadata: ['dc.contributor.author', 'dc.contributor.editor', 'dc.contributor.contributor', 'dc.creator'], }, + + mirador: { + enableDownloadPlugin: true, + }, }; diff --git a/src/mirador-viewer/config.default.js b/src/mirador-viewer/config.default.js index b8b735ec9b2..43a5651917b 100644 --- a/src/mirador-viewer/config.default.js +++ b/src/mirador-viewer/config.default.js @@ -24,8 +24,8 @@ const searchOption = params.get('searchable'); const query = params.get('query'); const multi = params.get('multi'); const notMobile = params.get('notMobile'); +const isDownloadPluginEnabled = (params.get('enableDownloadPlugin') === 'true'); -let isDownloadPluginEnabled = true; let windowSettings = {}; let sideBarPanel = 'info'; let defaultView = 'single'; From b176435406358c6bc8d0c880c32ce34aa9624ef4 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 26 Feb 2024 18:39:27 +0100 Subject: [PATCH 4/8] [DSC-1543] Fix out of memory error during Angular build --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 0ead78f3d30..a55594240da 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,8 @@ "build:stats": "ng build --stats-json", "build:ci": "ng config cli.cache.environment ci && yarn run build:ssr", "build:prod": "cross-env NODE_ENV=production yarn run build:ssr", - "build:ssr": "npm run ng-high-memory -- build --configuration production && ng run dspace-angular:server:production", - "ng-high-memory": "node --max_old_space_size=8192 node_modules/@angular/cli/bin/ng", + "build:ssr": "npm run ng-high-memory -- build --configuration production && npm run ng-high-memory -- run dspace-angular:server:production", + "ng-high-memory": "node --max-old-space-size=8192 node_modules/@angular/cli/bin/ng", "test": "npm run ng-high-memory -- test --source-map=true --watch=false --configuration test", "test:watch": "nodemon --exec \"npm run ng-high-memory -- test --source-map=true --watch=true --configuration test\"", "test:headless": "npm run ng-high-memory -- test --source-map=true --watch=false --configuration test --browsers=ChromeHeadless --code-coverage", From 9ee498437c8dcd0177e4da21c59db03ba4af6f45 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 26 Feb 2024 22:06:40 +0100 Subject: [PATCH 5/8] [DSC-714] Set authority value only when is provided by the VocabularyEntryDetail object --- .../builder/json-patch-operations-builder.ts | 12 +------ .../models/vocabulary-entry.model.ts | 2 -- .../onebox/dynamic-onebox.component.html | 1 - .../vocabulary-treeview.component.spec.ts | 35 ++++++++++++++++--- .../vocabulary-treeview.component.ts | 2 +- 5 files changed, 33 insertions(+), 19 deletions(-) diff --git a/src/app/core/json-patch/builder/json-patch-operations-builder.ts b/src/app/core/json-patch/builder/json-patch-operations-builder.ts index 029c0579424..c11ff47b779 100644 --- a/src/app/core/json-patch/builder/json-patch-operations-builder.ts +++ b/src/app/core/json-patch/builder/json-patch-operations-builder.ts @@ -157,7 +157,7 @@ export class JsonPatchOperationsBuilder { operationValue = new FormFieldMetadataValueObject(dateToISOFormat(value)); } } else if (value instanceof VocabularyEntry) { - operationValue = this.prepareAuthorityValue(value); + operationValue = new FormFieldMetadataValueObject(value.value, null, value.securityLevel, value.authority); } else if (value instanceof FormFieldLanguageValueObject) { operationValue = new FormFieldMetadataValueObject(value.value, value.language, securityLevel); } else if (value.hasOwnProperty('authority')) { @@ -184,14 +184,4 @@ export class JsonPatchOperationsBuilder { return operationValue; } - protected prepareAuthorityValue(value: any): FormFieldMetadataValueObject { - let operationValue: FormFieldMetadataValueObject; - if (isNotEmpty(value.authority)) { - operationValue = new FormFieldMetadataValueObject(value.value, value.language, value.securityLevel, value.authority); - } else { - operationValue = new FormFieldMetadataValueObject(value.value, value.language, value.securityLevel,); - } - return operationValue; - } - } diff --git a/src/app/core/submission/vocabularies/models/vocabulary-entry.model.ts b/src/app/core/submission/vocabularies/models/vocabulary-entry.model.ts index 440237dd7fb..4fb1093851e 100644 --- a/src/app/core/submission/vocabularies/models/vocabulary-entry.model.ts +++ b/src/app/core/submission/vocabularies/models/vocabulary-entry.model.ts @@ -50,8 +50,6 @@ export class VocabularyEntry extends ListableObject { @autoserialize securityLevel: number; - - /** * A string representing the kind of vocabulary entry */ diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.html index aa5eca1da4f..fe59791e6b9 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.html @@ -79,7 +79,6 @@ [disabled]="model.readOnly" [type]="model.inputType" [value]="currentValue?.display" - [disabled]="model.readOnly" (focus)="onFocus($event)" (change)="onChange($event)" (click)="openTree($event)" diff --git a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.spec.ts b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.spec.ts index 8af5ca3359d..2cc9b500f41 100644 --- a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.spec.ts +++ b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.spec.ts @@ -29,6 +29,13 @@ describe('VocabularyTreeviewComponent test suite', () => { item.id = 'node1'; const item2 = new VocabularyEntryDetail(); item2.id = 'node2'; + const entryWithAuthority = new VocabularyEntryDetail(); + entryWithAuthority.authority = 'entryWithAuthority'; + entryWithAuthority.id = 'entryWithAuthority'; + entryWithAuthority.value = 'test'; + const entryWithoutAuthority = new VocabularyEntryDetail(); + entryWithoutAuthority.id = 'entryWithoutAuthority'; + entryWithoutAuthority.value = 'test2'; const emptyNodeMap = new Map(); const storedNodeMap = new Map().set('test', new TreeviewFlatNode(item2)); const nodeMap = new Map().set('test', new TreeviewFlatNode(item)); @@ -169,11 +176,31 @@ describe('VocabularyTreeviewComponent test suite', () => { expect(vocabularyTreeviewServiceStub.loadMore).toHaveBeenCalledWith(node.item, [], true); }); - it('should emit select event', () => { - spyOn(comp, 'onSelect'); - comp.onSelect(item); + it('should emit proper FormFieldMetadataValueObject when VocabularyEntryDetail has authority', () => { + spyOn(compAsAny, 'getSelectedEntryIds').and.returnValue([]); + spyOn(comp.select, 'emit'); + comp.onSelect(entryWithAuthority); - expect(comp.onSelect).toHaveBeenCalledWith(item); + const expected = new FormFieldMetadataValueObject(entryWithAuthority.value, null, null, entryWithAuthority.authority); + expect(comp.select.emit).toHaveBeenCalledWith(expected); + }); + + it('should emit proper FormFieldMetadataValueObject when VocabularyEntryDetail has no authority', () => { + spyOn(compAsAny, 'getSelectedEntryIds').and.returnValue([]); + spyOn(comp.select, 'emit'); + comp.onSelect(entryWithoutAuthority); + + const expected = new FormFieldMetadataValueObject(entryWithoutAuthority.value); + expect(comp.select.emit).toHaveBeenCalledWith(expected); + }); + + it('should emit deselect when entry is already present', () => { + spyOn(compAsAny, 'getSelectedEntryIds').and.returnValue([entryWithAuthority.id]); + spyOn(comp.select, 'emit'); + spyOn(comp.deselect, 'emit'); + comp.onSelect(entryWithAuthority); + + expect(comp.deselect.emit).toHaveBeenCalled(); }); it('should call searchByQuery function and set storedNodeMap properly', () => { diff --git a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts index a78195a6811..2914464341d 100644 --- a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts +++ b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts @@ -248,7 +248,7 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit, OnChanges onSelect(item: VocabularyEntryDetail) { if (!this.getSelectedEntryIds().includes(this.getEntryId(item))) { this.selectedItems.push(item); - this.select.emit(item); + this.select.emit(new FormFieldMetadataValueObject(item.value, null, item.securityLevel, item.authority, item.display)); } else { this.selectedItems = this.selectedItems.filter((detail: VocabularyTreeItemType) => this.getEntryId(detail) !== this.getEntryId(item)); this.deselect.emit(item); From 5f4c68f0cda6b0fa12c7df9e4fda855ca9f093aa Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Tue, 27 Feb 2024 09:33:10 +0100 Subject: [PATCH 6/8] [DSC-714] Fix indentation and refactoring --- .../builder/json-patch-operations-builder.ts | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/app/core/json-patch/builder/json-patch-operations-builder.ts b/src/app/core/json-patch/builder/json-patch-operations-builder.ts index c11ff47b779..a4886d370d3 100644 --- a/src/app/core/json-patch/builder/json-patch-operations-builder.ts +++ b/src/app/core/json-patch/builder/json-patch-operations-builder.ts @@ -110,27 +110,25 @@ export class JsonPatchOperationsBuilder { operationValue = []; value.forEach((entry) => { if ((typeof entry === 'object')) { - if (securityLevel != null) { + if (isNotEmpty(securityLevel)) { operationValue.push(this.prepareObjectValue(entry, securityLevel)); } else { operationValue.push(this.prepareObjectValue(entry)); } - } else { operationValue.push(new FormFieldMetadataValueObject(entry, null, securityLevel)); } }); } else if (typeof value === 'object') { - if (securityLevel != null) { + if (isNotEmpty(securityLevel)) { operationValue = this.prepareObjectValue(value, securityLevel); } else { operationValue = this.prepareObjectValue(value); } - } else { // add the possibility to add security level when value is string // in this case security level is set on metadata value - if (securityLevel != null) { + if (isNotEmpty(securityLevel)) { operationValue = new FormFieldMetadataValueObject(value, null, securityLevel); } else { operationValue = new FormFieldMetadataValueObject(value, null); @@ -143,16 +141,16 @@ export class JsonPatchOperationsBuilder { } protected prepareObjectValue(value: any, securityLevel = null) { - let operationValue = Object.create({}); + let operationValue = Object.create({}); if (isEmpty(value) || value instanceof FormFieldMetadataValueObject) { - if (securityLevel != null) { - operationValue = {...value, securityLevel: securityLevel}; - } else { + if (isNotEmpty(securityLevel)) { + operationValue = { ...value, securityLevel: securityLevel }; + } else { operationValue = value; } } else if (value instanceof Date) { if (securityLevel != null) { - operationValue = new FormFieldMetadataValueObject(dateToISOFormat(value), null, securityLevel); + operationValue = new FormFieldMetadataValueObject(dateToISOFormat(value), null, securityLevel); } else { operationValue = new FormFieldMetadataValueObject(dateToISOFormat(value)); } @@ -170,11 +168,10 @@ export class JsonPatchOperationsBuilder { Object.keys(value) .forEach((key) => { if (typeof value[key] === 'object') { - if (securityLevel != null) { - operationValue[key] = this.prepareObjectValue(value[key], securityLevel); + if (isNotEmpty(securityLevel)) { + operationValue[key] = this.prepareObjectValue(value[key], securityLevel); } else { - operationValue[key] = this.prepareObjectValue(value[key]); - + operationValue[key] = this.prepareObjectValue(value[key]); } } else { operationValue[key] = value[key]; From 1d6d6ca6c33735b10a929f2bd401cb58af1c1554 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Tue, 27 Feb 2024 14:33:38 +0100 Subject: [PATCH 7/8] [DSC-714] Use hal links to retrieve parent and children entries --- .../models/vocabulary-entry-detail.model.ts | 21 +++++++- .../vocabulary-entry-details.data.service.ts | 18 ++++++- .../vocabularies/vocabulary.service.spec.ts | 40 ++++++++++++++-- .../vocabularies/vocabulary.service.ts | 48 ++++++++++++++----- 4 files changed, 110 insertions(+), 17 deletions(-) diff --git a/src/app/core/submission/vocabularies/models/vocabulary-entry-detail.model.ts b/src/app/core/submission/vocabularies/models/vocabulary-entry-detail.model.ts index 2e066bae950..8dd360218c0 100644 --- a/src/app/core/submission/vocabularies/models/vocabulary-entry-detail.model.ts +++ b/src/app/core/submission/vocabularies/models/vocabulary-entry-detail.model.ts @@ -1,9 +1,12 @@ +import { Observable } from 'rxjs'; import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; import { HALLink } from '../../../shared/hal-link.model'; import { VOCABULARY_ENTRY_DETAIL } from './vocabularies.resource-type'; -import { typedObject } from '../../../cache/builders/build-decorators'; +import { link, typedObject } from '../../../cache/builders/build-decorators'; import { VocabularyEntry } from './vocabulary-entry.model'; +import { RemoteData } from '../../../data/remote-data'; +import { PaginatedList } from '../../../data/paginated-list.model'; /** * Model class for a VocabularyEntryDetail @@ -33,7 +36,21 @@ export class VocabularyEntryDetail extends VocabularyEntry { self: HALLink; vocabulary: HALLink; parent: HALLink; - children + children: HALLink; }; + /** + * The submitter for this SubmissionObject + * Will be undefined unless the submitter {@link HALLink} has been resolved. + */ + @link(VOCABULARY_ENTRY_DETAIL) + parent?: Observable>; + + /** + * The submitter for this SubmissionObject + * Will be undefined unless the submitter {@link HALLink} has been resolved. + */ + @link(VOCABULARY_ENTRY_DETAIL, true) + children?: Observable>>; + } diff --git a/src/app/core/submission/vocabularies/vocabulary-entry-details.data.service.ts b/src/app/core/submission/vocabularies/vocabulary-entry-details.data.service.ts index 184b4c1e97d..a5892770138 100644 --- a/src/app/core/submission/vocabularies/vocabulary-entry-details.data.service.ts +++ b/src/app/core/submission/vocabularies/vocabulary-entry-details.data.service.ts @@ -54,13 +54,29 @@ export class VocabularyEntryDetailsDataService extends IdentifiableDataService>>} + * @return {Observable>>} * Return an observable that emits object list */ public findAll(options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig[]): Observable>> { return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } + /** + * Returns an observable of {@link RemoteData} of an object, based on its ID, with a list of + * {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object + * @param id ID of object we want to retrieve + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which + * {@link HALLink}s should be automatically resolved + */ + findById(id: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { + const href$ = this.getIDHrefObs(id, ...linksToFollow); + return this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } + /** * Make a new FindListRequest with given search method * diff --git a/src/app/core/submission/vocabularies/vocabulary.service.spec.ts b/src/app/core/submission/vocabularies/vocabulary.service.spec.ts index ba51db81518..58bea914789 100644 --- a/src/app/core/submission/vocabularies/vocabulary.service.spec.ts +++ b/src/app/core/submission/vocabularies/vocabulary.service.spec.ts @@ -92,6 +92,33 @@ describe('VocabularyService', () => { type: 'vocabularyEntry' }; + const entryDetailRequestURL = `https://rest.api/rest/api/submission/vocabularyEntryDetails/${hierarchicalVocabulary.id}:testValue`; + const entryDetailParentRequestURL = `https://rest.api/rest/api/submission/vocabularyEntryDetails/${hierarchicalVocabulary.id}:testValue/parent`; + const entryDetailChildrenRequestURL = `https://rest.api/rest/api/submission/vocabularyEntryDetails/${hierarchicalVocabulary.id}:testValue/children`; + + const vocabularyEntryDetail: any = { + authority: 'authorityId', + display: 'test', + value: 'test', + otherInformation: { + id: 'authorityId', + hasChildren: 'true', + note: 'Familjeforskning' + }, + type: 'vocabularyEntryDetail', + _links: { + self: { + href: entryDetailRequestURL + }, + parent: { + href: entryDetailParentRequestURL + }, + children: { + href: entryDetailChildrenRequestURL + } + } + }; + const vocabularyEntryParentDetail: any = { authority: 'authorityId2', display: 'testParent', @@ -164,9 +191,7 @@ describe('VocabularyService', () => { const endpointURL = `https://rest.api/rest/api/submission/vocabularies`; const requestURL = `https://rest.api/rest/api/submission/vocabularies/${vocabulary.id}`; const entryDetailEndpointURL = `https://rest.api/rest/api/submission/vocabularyEntryDetails`; - const entryDetailRequestURL = `https://rest.api/rest/api/submission/vocabularyEntryDetails/${hierarchicalVocabulary.id}:testValue`; - const entryDetailParentRequestURL = `https://rest.api/rest/api/submission/vocabularyEntryDetails/${hierarchicalVocabulary.id}:testValue/parent`; - const entryDetailChildrenRequestURL = `https://rest.api/rest/api/submission/vocabularyEntryDetails/${hierarchicalVocabulary.id}:testValue/children`; + const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a'; const vocabularyId = 'types'; const metadata = 'dc.type'; @@ -193,6 +218,7 @@ describe('VocabularyService', () => { const vocabularyRD = createSuccessfulRemoteDataObject(vocabulary); const vocabularyRD$ = createSuccessfulRemoteDataObject$(vocabulary); const vocabularyEntriesRD = createSuccessfulRemoteDataObject$(paginatedListEntries); + const vocabularyEntryDetailRD$ = createSuccessfulRemoteDataObject$(vocabularyEntryDetail); const vocabularyEntryDetailParentRD = createSuccessfulRemoteDataObject(vocabularyEntryParentDetail); const vocabularyEntryChildrenRD = createSuccessfulRemoteDataObject(childrenPaginatedList); const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList); @@ -485,6 +511,10 @@ describe('VocabularyService', () => { }); describe('getEntryDetailParent', () => { + beforeEach(() => { + (service as any).vocabularyEntryDetailDataService.findById.and.returnValue(vocabularyEntryDetailRD$); + }); + it('should proxy the call to vocabularyDataService.getEntryDetailParent', () => { scheduler.schedule(() => service.getEntryDetailParent('testValue', hierarchicalVocabulary.id).subscribe()); scheduler.flush(); @@ -502,6 +532,10 @@ describe('VocabularyService', () => { }); describe('getEntryDetailChildren', () => { + beforeEach(() => { + (service as any).vocabularyEntryDetailDataService.findById.and.returnValue(vocabularyEntryDetailRD$); + }); + it('should proxy the call to vocabularyDataService.getEntryDetailChildren', () => { const options: VocabularyFindOptions = new VocabularyFindOptions( null, diff --git a/src/app/core/submission/vocabularies/vocabulary.service.ts b/src/app/core/submission/vocabularies/vocabulary.service.ts index a7c808870ec..37eb4bd9b3a 100644 --- a/src/app/core/submission/vocabularies/vocabulary.service.ts +++ b/src/app/core/submission/vocabularies/vocabulary.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; +import { Observable, of } from 'rxjs'; import { first, map, mergeMap, switchMap } from 'rxjs/operators'; import { followLink, FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; import { RequestService } from '../../data/request.service'; @@ -8,7 +8,11 @@ import { PaginatedList } from '../../data/paginated-list.model'; import { Vocabulary } from './models/vocabulary.model'; import { VocabularyEntry } from './models/vocabulary-entry.model'; import { hasValue, isNotEmpty } from '../../../shared/empty.util'; -import { getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteListPayload } from '../../shared/operators'; +import { + getFirstCompletedRemoteData, + getFirstSucceededRemoteDataPayload, + getFirstSucceededRemoteListPayload +} from '../../shared/operators'; import { VocabularyFindOptions } from './models/vocabulary-find-options.model'; import { VocabularyEntryDetail } from './models/vocabulary-entry-detail.model'; import { RequestParam } from '../../cache/models/request-param.model'; @@ -17,6 +21,7 @@ import { PageInfo } from '../../shared/page-info.model'; import { FindListOptions } from '../../data/find-list-options.model'; import { VocabularyEntryDetailsDataService } from './vocabulary-entry-details.data.service'; import { VocabularyDataService } from './vocabulary.data.service'; +import { createFailedRemoteDataObject } from '../../../shared/remote-data.utils'; /** * A service responsible for fetching/sending data from/to the REST API on the vocabularies endpoint @@ -249,7 +254,7 @@ export class VocabularyService { searchVocabularyByMetadataAndCollection(vocabularyOptions: VocabularyOptions, ...linksToFollow: FollowLinkConfig[]): Observable> { const options: VocabularyFindOptions = new VocabularyFindOptions(vocabularyOptions.scope, vocabularyOptions.metadata); - return this.vocabularyDataService.getSearchByHref(this.searchByMetadataAndCollectionMethod, options).pipe( + return this.vocabularyDataService.getSearchByHref(this.searchByMetadataAndCollectionMethod, options, ...linksToFollow).pipe( first((href: string) => hasValue(href)), mergeMap((href: string) => this.vocabularyDataService.findByHref(href)) ); @@ -309,11 +314,20 @@ export class VocabularyService { * Return an observable that emits a PaginatedList of VocabularyEntryDetail */ getEntryDetailParent(value: string, name: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { - const linkPath = `${name}:${value}/parent`; - - return this.vocabularyEntryDetailDataService.getBrowseEndpoint().pipe( - map((href: string) => `${href}/${linkPath}`), - mergeMap((href) => this.vocabularyEntryDetailDataService.findByHref(href, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)) + return this.findEntryDetailById(value, name, useCachedVersionIfAvailable, reRequestOnStale, true, ...linksToFollow).pipe( + getFirstCompletedRemoteData(), + switchMap((entryRD: RemoteData) => { + if (entryRD.hasSucceeded) { + return this.vocabularyEntryDetailDataService.findByHref( + entryRD.payload._links.parent.href, + useCachedVersionIfAvailable, + reRequestOnStale, + ...linksToFollow + ); + } else { + return of(createFailedRemoteDataObject(entryRD.errorMessage)); + } + }) ); } @@ -344,9 +358,21 @@ export class VocabularyService { pageInfo.currentPage ); - return this.vocabularyEntryDetailDataService.getBrowseEndpoint().pipe( - map(href => `${href}/${name}:${value}/children`), - switchMap(href => this.vocabularyEntryDetailDataService.findListByHref(href, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)) + return this.findEntryDetailById(value, name, useCachedVersionIfAvailable, reRequestOnStale, true, ...linksToFollow).pipe( + getFirstCompletedRemoteData(), + switchMap((entryRD: RemoteData) => { + if (entryRD.hasSucceeded) { + return this.vocabularyEntryDetailDataService.findListByHref( + entryRD.payload._links.children.href, + options, + useCachedVersionIfAvailable, + reRequestOnStale, + ...linksToFollow + ); + } else { + return of(createFailedRemoteDataObject>(entryRD.errorMessage)); + } + }) ); } From 8d3b068d0416f89f7d93898d8ab5d43a731a5665 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Tue, 27 Feb 2024 17:23:03 +0100 Subject: [PATCH 8/8] [DSC-1562] Use SearchManager for search components in order to optimize the follow-up requests to embedded objects --- src/app/lucky-search/lucky-search.service.spec.ts | 7 ++++--- src/app/lucky-search/lucky-search.service.ts | 4 ++-- .../browse-most-elements.component.spec.ts | 8 +++++--- .../browse-most-elements.component.ts | 4 ++-- .../counters-section/counters-section.component.spec.ts | 4 ++-- .../counters-section/counters-section.component.ts | 4 ++-- .../multi-column-top-section.component.ts | 4 ++-- 7 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/app/lucky-search/lucky-search.service.spec.ts b/src/app/lucky-search/lucky-search.service.spec.ts index c0007435802..04d408573ea 100644 --- a/src/app/lucky-search/lucky-search.service.spec.ts +++ b/src/app/lucky-search/lucky-search.service.spec.ts @@ -1,13 +1,14 @@ import { TestBed } from '@angular/core/testing'; import { LuckySearchService } from './lucky-search.service'; -import {SearchService} from '../core/shared/search/search.service'; -import {SearchServiceStub} from '../shared/testing/search-service.stub'; +import { SearchManager } from '../core/browse/search-manager'; +import { SearchServiceStub } from '../shared/testing/search-service.stub'; + describe('LuckySearchService', () => { let service: LuckySearchService; beforeEach(() => { TestBed.configureTestingModule({ providers: [ - {provide: SearchService, useValue: new SearchServiceStub('/search')}, + {provide: SearchManager, useValue: new SearchServiceStub('/search')}, ], }); service = TestBed.inject(LuckySearchService); diff --git a/src/app/lucky-search/lucky-search.service.ts b/src/app/lucky-search/lucky-search.service.ts index f202052c115..c65bd648abf 100644 --- a/src/app/lucky-search/lucky-search.service.ts +++ b/src/app/lucky-search/lucky-search.service.ts @@ -5,14 +5,14 @@ import { PaginatedList } from '../core/data/paginated-list.model'; import { SearchResult } from '../shared/search/models/search-result.model'; import { DSpaceObject } from '../core/shared/dspace-object.model'; import { PaginatedSearchOptions } from '../shared/search/models/paginated-search-options.model'; -import { SearchService } from '../core/shared/search/search.service'; +import { SearchManager } from '../core/browse/search-manager'; @Injectable({ providedIn: 'root' }) export class LuckySearchService { - constructor(private searchService: SearchService) { + constructor(private searchService: SearchManager) { } /** diff --git a/src/app/shared/browse-most-elements/browse-most-elements.component.spec.ts b/src/app/shared/browse-most-elements/browse-most-elements.component.spec.ts index 95fad5f05b7..a42da359e41 100644 --- a/src/app/shared/browse-most-elements/browse-most-elements.component.spec.ts +++ b/src/app/shared/browse-most-elements/browse-most-elements.component.spec.ts @@ -1,8 +1,10 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { ChangeDetectorRef, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { BrowseMostElementsComponent } from './browse-most-elements.component'; -import { SearchService } from '../../core/shared/search/search.service'; + import { of } from 'rxjs'; + +import { BrowseMostElementsComponent } from './browse-most-elements.component'; +import { SearchManager } from '../../core/browse/search-manager'; import { PaginatedSearchOptions } from '../search/models/paginated-search-options.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; @@ -68,7 +70,7 @@ describe('BrowseMostElementsComponent', () => { declarations: [BrowseMostElementsComponent], providers: [ { provide: APP_CONFIG, useValue: mockConfig }, - { provide: SearchService, useValue: mockSearchService }, + { provide: SearchManager, useValue: mockSearchService }, { provide: ChangeDetectorRef, useValue: {} }, ], schemas: [CUSTOM_ELEMENTS_SCHEMA], // Ignore unknown Angular elements diff --git a/src/app/shared/browse-most-elements/browse-most-elements.component.ts b/src/app/shared/browse-most-elements/browse-most-elements.component.ts index ed616957ad6..b442ffdfe2b 100644 --- a/src/app/shared/browse-most-elements/browse-most-elements.component.ts +++ b/src/app/shared/browse-most-elements/browse-most-elements.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectorRef, Component, Inject, Input, OnInit, PLATFORM_ID } from '@angular/core'; import { isPlatformServer } from '@angular/common'; -import { SearchService } from '../../core/shared/search/search.service'; +import { SearchManager } from '../../core/browse/search-manager'; import { PaginatedSearchOptions } from '../search/models/paginated-search-options.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { SearchResult } from '../search/models/search-result.model'; @@ -39,7 +39,7 @@ export class BrowseMostElementsComponent implements OnInit { constructor( @Inject(APP_CONFIG) protected appConfig: AppConfig, @Inject(PLATFORM_ID) private platformId: Object, - private searchService: SearchService, + private searchService: SearchManager, private cdr: ChangeDetectorRef) { } diff --git a/src/app/shared/explore/section-component/counters-section/counters-section.component.spec.ts b/src/app/shared/explore/section-component/counters-section/counters-section.component.spec.ts index a40884e3d66..565ccce975d 100644 --- a/src/app/shared/explore/section-component/counters-section/counters-section.component.spec.ts +++ b/src/app/shared/explore/section-component/counters-section/counters-section.component.spec.ts @@ -1,9 +1,9 @@ -import { SearchService } from '../../../../core/shared/search/search.service'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { CountersSectionComponent } from './counters-section.component'; import { NativeWindowService } from '../../../../core/services/window.service'; import { NativeWindowMockFactory } from '../../../mocks/mock-native-window-ref'; +import { SearchManager } from '../../../../core/browse/search-manager'; xdescribe('CountersSectionComponent', () => { let component: CountersSectionComponent; @@ -13,7 +13,7 @@ xdescribe('CountersSectionComponent', () => { TestBed.configureTestingModule({ declarations: [ CountersSectionComponent ], providers: [ - { provide: SearchService, useValue: {} }, + { provide: SearchManager, useValue: {} }, { provide: NativeWindowService, useFactory: NativeWindowMockFactory }, ] }) diff --git a/src/app/shared/explore/section-component/counters-section/counters-section.component.ts b/src/app/shared/explore/section-component/counters-section/counters-section.component.ts index 19273bbb627..4aa6ee2463e 100644 --- a/src/app/shared/explore/section-component/counters-section/counters-section.component.ts +++ b/src/app/shared/explore/section-component/counters-section/counters-section.component.ts @@ -9,7 +9,7 @@ import { SearchObjects } from '../../../search/models/search-objects.model'; import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators'; import { PaginationComponentOptions } from '../../../pagination/pagination-component-options.model'; import { SectionComponent } from '../../../../core/layout/models/section.model'; -import { SearchService } from '../../../../core/shared/search/search.service'; +import { SearchManager } from '../../../../core/browse/search-manager'; import { PaginatedSearchOptions } from '../../../search/models/paginated-search-options.model'; import { UUIDService } from '../../../../core/shared/uuid.service'; import { InternalLinkService } from 'src/app/core/services/internal-link.service'; @@ -40,7 +40,7 @@ export class CountersSectionComponent implements OnInit { constructor( public internalLinkService: InternalLinkService, - private searchService: SearchService, + private searchService: SearchManager, private uuidService: UUIDService, @Inject(PLATFORM_ID) private platformId: Object, ) { diff --git a/src/app/shared/explore/section-component/multi-column-top-section/multi-column-top-section.component.ts b/src/app/shared/explore/section-component/multi-column-top-section/multi-column-top-section.component.ts index d0e1f95a2b4..e59933737ff 100644 --- a/src/app/shared/explore/section-component/multi-column-top-section/multi-column-top-section.component.ts +++ b/src/app/shared/explore/section-component/multi-column-top-section/multi-column-top-section.component.ts @@ -11,7 +11,7 @@ import { SearchObjects } from '../../../search/models/search-objects.model'; import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators'; import { PaginatedSearchOptions } from '../../../search/models/paginated-search-options.model'; import { PaginationComponentOptions } from '../../../pagination/pagination-component-options.model'; -import { SearchService } from '../../../../core/shared/search/search.service'; +import { SearchManager } from '../../../../core/browse/search-manager'; import { SortDirection, SortOptions } from '../../../../core/cache/models/sort-options.model'; import { getItemPageRoute } from '../../../../item-page/item-page-routing-paths'; import { Item } from '../../../../core/shared/item.model'; @@ -30,7 +30,7 @@ export class MultiColumnTopSectionComponent implements OnInit { topObjects: Observable; - constructor(private searchService: SearchService) { + constructor(private searchService: SearchManager) { }