From be6dbdec66e6caf000c966a2e1e30f3acf199458 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 2 Oct 2023 13:31:25 +0200 Subject: [PATCH 001/720] 107155: Allow caching null objects --- src/app/core/cache/object-cache.reducer.ts | 31 +++++++++++-------- src/app/core/cache/object-cache.service.ts | 16 +++++++--- .../dspace-rest-response-parsing.service.ts | 7 +++-- src/app/core/index/index.effects.ts | 4 +-- 4 files changed, 36 insertions(+), 22 deletions(-) diff --git a/src/app/core/cache/object-cache.reducer.ts b/src/app/core/cache/object-cache.reducer.ts index dc3f50db68..7f389344fb 100644 --- a/src/app/core/cache/object-cache.reducer.ts +++ b/src/app/core/cache/object-cache.reducer.ts @@ -166,20 +166,25 @@ export function objectCacheReducer(state = initialState, action: ObjectCacheActi * the new state, with the object added, or overwritten. */ function addToObjectCache(state: ObjectCacheState, action: AddToObjectCacheAction): ObjectCacheState { - const existing = state[action.payload.objectToCache._links.self.href] || {} as any; + const cacheLink = hasValue(action.payload.objectToCache) ? action.payload.objectToCache._links.self.href : action.payload.alternativeLink; + const existing = state[cacheLink] || {} as any; const newAltLinks = hasValue(action.payload.alternativeLink) ? [action.payload.alternativeLink] : []; - return Object.assign({}, state, { - [action.payload.objectToCache._links.self.href]: { - data: action.payload.objectToCache, - timeCompleted: action.payload.timeCompleted, - msToLive: action.payload.msToLive, - requestUUIDs: [action.payload.requestUUID, ...(existing.requestUUIDs || [])], - dependentRequestUUIDs: existing.dependentRequestUUIDs || [], - isDirty: isNotEmpty(existing.patches), - patches: existing.patches || [], - alternativeLinks: [...(existing.alternativeLinks || []), ...newAltLinks] - } as ObjectCacheEntry - }); + if (hasValue(cacheLink)) { + return Object.assign({}, state, { + [cacheLink]: { + data: action.payload.objectToCache, + timeCompleted: action.payload.timeCompleted, + msToLive: action.payload.msToLive, + requestUUIDs: [action.payload.requestUUID, ...(existing.requestUUIDs || [])], + dependentRequestUUIDs: existing.dependentRequestUUIDs || [], + isDirty: isNotEmpty(existing.patches), + patches: existing.patches || [], + alternativeLinks: [...(existing.alternativeLinks || []), ...newAltLinks] + } as ObjectCacheEntry + }); + } else { + return state; + } } /** diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index 9ca0216210..0330a03f02 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -63,7 +63,9 @@ export class ObjectCacheService { * An optional alternative link to this object */ add(object: CacheableObject, msToLive: number, requestUUID: string, alternativeLink?: string): void { - object = this.linkService.removeResolvedLinks(object); // Ensure the object we're storing has no resolved links + if (hasValue(object)) { + object = this.linkService.removeResolvedLinks(object); // Ensure the object we're storing has no resolved links + } this.store.dispatch(new AddToObjectCacheAction(object, new Date().getTime(), msToLive, requestUUID, alternativeLink)); } @@ -139,11 +141,15 @@ export class ObjectCacheService { } ), map((entry: ObjectCacheEntry) => { - const type: GenericConstructor = getClassForType((entry.data as any).type); - if (typeof type !== 'function') { - throw new Error(`${type} is not a valid constructor for ${JSON.stringify(entry.data)}`); + if (hasValue(entry.data)) { + const type: GenericConstructor = getClassForType((entry.data as any).type); + if (typeof type !== 'function') { + throw new Error(`${type} is not a valid constructor for ${JSON.stringify(entry.data)}`); + } + return Object.assign(new type(), entry.data) as T; + } else { + return null; } - return Object.assign(new type(), entry.data) as T; }) ); } diff --git a/src/app/core/data/dspace-rest-response-parsing.service.ts b/src/app/core/data/dspace-rest-response-parsing.service.ts index 500afc4aff..2f79edd129 100644 --- a/src/app/core/data/dspace-rest-response-parsing.service.ts +++ b/src/app/core/data/dspace-rest-response-parsing.service.ts @@ -109,6 +109,9 @@ export class DspaceRestResponseParsingService implements ResponseParsingService if (hasValue(match)) { embedAltUrl = new URLCombiner(embedAltUrl, `?size=${match.size}`).toString(); } + if (data._embedded[property] == null) { + this.addToObjectCache(null, request, data, embedAltUrl); + } this.process(data._embedded[property], request, embedAltUrl); }); } @@ -226,7 +229,7 @@ export class DspaceRestResponseParsingService implements ResponseParsingService * @param alternativeURL an alternative url that can be used to retrieve the object */ addToObjectCache(co: CacheableObject, request: RestRequest, data: any, alternativeURL?: string): void { - if (!isCacheableObject(co)) { + if (hasValue(co) && !isCacheableObject(co)) { const type = hasValue(data) && hasValue(data.type) ? data.type : 'object'; let dataJSON: string; if (hasValue(data._embedded)) { @@ -240,7 +243,7 @@ export class DspaceRestResponseParsingService implements ResponseParsingService return; } - if (alternativeURL === co._links.self.href) { + if (hasValue(co) && alternativeURL === co._links.self.href) { alternativeURL = undefined; } diff --git a/src/app/core/index/index.effects.ts b/src/app/core/index/index.effects.ts index 18d639023f..9ec013813d 100644 --- a/src/app/core/index/index.effects.ts +++ b/src/app/core/index/index.effects.ts @@ -27,7 +27,7 @@ export class UUIDIndexEffects { addObject$ = createEffect(() => this.actions$ .pipe( ofType(ObjectCacheActionTypes.ADD), - filter((action: AddToObjectCacheAction) => hasValue(action.payload.objectToCache.uuid)), + filter((action: AddToObjectCacheAction) => hasValue(action.payload.objectToCache) && hasValue(action.payload.objectToCache.uuid)), map((action: AddToObjectCacheAction) => { return new AddToIndexAction( IndexName.OBJECT, @@ -46,7 +46,7 @@ export class UUIDIndexEffects { ofType(ObjectCacheActionTypes.ADD), map((action: AddToObjectCacheAction) => { const alternativeLink = action.payload.alternativeLink; - const selfLink = action.payload.objectToCache._links.self.href; + const selfLink = hasValue(action.payload.objectToCache) ? action.payload.objectToCache._links.self.href : alternativeLink; if (hasValue(alternativeLink) && alternativeLink !== selfLink) { return new AddToIndexAction( IndexName.ALTERNATIVE_OBJECT_LINK, From 984c9bfc2a2271e65190e02902ea0589dcfe6c4f Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 26 Oct 2023 16:33:14 +0200 Subject: [PATCH 002/720] 107155: Allow caching of embedded objects without selflink --- src/app/core/cache/object-cache.reducer.ts | 2 +- src/app/core/data/dspace-rest-response-parsing.service.ts | 4 ++++ src/app/core/index/index.effects.ts | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/app/core/cache/object-cache.reducer.ts b/src/app/core/cache/object-cache.reducer.ts index 7f389344fb..21dc729f1b 100644 --- a/src/app/core/cache/object-cache.reducer.ts +++ b/src/app/core/cache/object-cache.reducer.ts @@ -166,7 +166,7 @@ export function objectCacheReducer(state = initialState, action: ObjectCacheActi * the new state, with the object added, or overwritten. */ function addToObjectCache(state: ObjectCacheState, action: AddToObjectCacheAction): ObjectCacheState { - const cacheLink = hasValue(action.payload.objectToCache) ? action.payload.objectToCache._links.self.href : action.payload.alternativeLink; + const cacheLink = hasValue(action.payload.objectToCache?._links?.self) ? action.payload.objectToCache._links.self.href : action.payload.alternativeLink; const existing = state[cacheLink] || {} as any; const newAltLinks = hasValue(action.payload.alternativeLink) ? [action.payload.alternativeLink] : []; if (hasValue(cacheLink)) { diff --git a/src/app/core/data/dspace-rest-response-parsing.service.ts b/src/app/core/data/dspace-rest-response-parsing.service.ts index 2f79edd129..c0e1c70cae 100644 --- a/src/app/core/data/dspace-rest-response-parsing.service.ts +++ b/src/app/core/data/dspace-rest-response-parsing.service.ts @@ -110,7 +110,11 @@ export class DspaceRestResponseParsingService implements ResponseParsingService embedAltUrl = new URLCombiner(embedAltUrl, `?size=${match.size}`).toString(); } if (data._embedded[property] == null) { + // Embedded object is null, meaning it exists (not undefined), but had an empty response (204) -> cache it as null this.addToObjectCache(null, request, data, embedAltUrl); + } else if (!isCacheableObject(data._embedded[property])) { + // Embedded object exists, but doesn't contain a self link -> cache it using the alternative link instead + this.objectCache.add(data._embedded[property], hasValue(request.responseMsToLive) ? request.responseMsToLive : environment.cache.msToLive.default, request.uuid, embedAltUrl); } this.process(data._embedded[property], request, embedAltUrl); }); diff --git a/src/app/core/index/index.effects.ts b/src/app/core/index/index.effects.ts index 9ec013813d..65aa45e571 100644 --- a/src/app/core/index/index.effects.ts +++ b/src/app/core/index/index.effects.ts @@ -46,7 +46,7 @@ export class UUIDIndexEffects { ofType(ObjectCacheActionTypes.ADD), map((action: AddToObjectCacheAction) => { const alternativeLink = action.payload.alternativeLink; - const selfLink = hasValue(action.payload.objectToCache) ? action.payload.objectToCache._links.self.href : alternativeLink; + const selfLink = hasValue(action.payload.objectToCache?._links?.self) ? action.payload.objectToCache._links.self.href : alternativeLink; if (hasValue(alternativeLink) && alternativeLink !== selfLink) { return new AddToIndexAction( IndexName.ALTERNATIVE_OBJECT_LINK, From 36c95db7bf19b83453691ef4e0dbc52608c08ee5 Mon Sep 17 00:00:00 2001 From: Victor Hugo Duran Santiago Date: Thu, 9 May 2024 20:50:51 -0600 Subject: [PATCH 003/720] Set color black on filter section for mobile --- .../search-filters/search-filter/search-filter.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/search/search-filters/search-filter/search-filter.component.html b/src/app/shared/search/search-filters/search-filter/search-filter.component.html index 97809ef854..6c3873d296 100644 --- a/src/app/shared/search/search-filters/search-filter/search-filter.component.html +++ b/src/app/shared/search/search-filters/search-filter/search-filter.component.html @@ -6,7 +6,7 @@ [attr.aria-label]="(((collapsed$ | async) ? 'search.filters.filter.expand' : 'search.filters.filter.collapse') | translate) + ' ' + (('search.filters.filter.' + filter.name + '.head') | translate | lowercase)" [attr.data-test]="'filter-toggle' | dsBrowserOnly" > - + {{'search.filters.filter.' + filter.name + '.head'| translate}} Date: Tue, 21 May 2024 12:02:15 +0200 Subject: [PATCH 004/720] 115051: Created ThemedAdminSearchPageComponent --- src/app/admin/admin-routing.module.ts | 4 +-- .../admin-search-page/admin-search.module.ts | 2 ++ .../themed-admin-search-page.component.ts | 26 +++++++++++++++++++ .../admin-search-page.component.html | 0 .../admin-search-page.component.scss | 0 .../admin-search-page.component.ts | 12 +++++++++ src/themes/custom/lazy-theme.module.ts | 4 +-- 7 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 src/app/admin/admin-search-page/themed-admin-search-page.component.ts create mode 100644 src/themes/custom/app/admin/admin-search-page/admin-search-page.component.html create mode 100644 src/themes/custom/app/admin/admin-search-page/admin-search-page.component.scss create mode 100644 src/themes/custom/app/admin/admin-search-page/admin-search-page.component.ts diff --git a/src/app/admin/admin-routing.module.ts b/src/app/admin/admin-routing.module.ts index 1ea20bc9a0..3e3a8924ac 100644 --- a/src/app/admin/admin-routing.module.ts +++ b/src/app/admin/admin-routing.module.ts @@ -1,7 +1,7 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { MetadataImportPageComponent } from './admin-import-metadata-page/metadata-import-page.component'; -import { AdminSearchPageComponent } from './admin-search-page/admin-search-page.component'; +import { ThemedAdminSearchPageComponent } from './admin-search-page/themed-admin-search-page.component'; import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component'; import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service'; @@ -20,7 +20,7 @@ import { BatchImportPageComponent } from './admin-import-batch-page/batch-import { path: 'search', resolve: { breadcrumb: I18nBreadcrumbResolver }, - component: AdminSearchPageComponent, + component: ThemedAdminSearchPageComponent, data: { title: 'admin.search.title', breadcrumbKey: 'admin.search' } }, { diff --git a/src/app/admin/admin-search-page/admin-search.module.ts b/src/app/admin/admin-search-page/admin-search.module.ts index 353d6dd498..b45eca15c4 100644 --- a/src/app/admin/admin-search-page/admin-search.module.ts +++ b/src/app/admin/admin-search-page/admin-search.module.ts @@ -1,5 +1,6 @@ import { NgModule } from '@angular/core'; import { SharedModule } from '../../shared/shared.module'; +import { ThemedAdminSearchPageComponent } from './themed-admin-search-page.component'; import { AdminSearchPageComponent } from './admin-search-page.component'; import { ItemAdminSearchResultListElementComponent } from './admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component'; import { CommunityAdminSearchResultListElementComponent } from './admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component'; @@ -31,6 +32,7 @@ const ENTRY_COMPONENTS = [ ResearchEntitiesModule.withEntryComponents() ], declarations: [ + ThemedAdminSearchPageComponent, AdminSearchPageComponent, ...ENTRY_COMPONENTS ] diff --git a/src/app/admin/admin-search-page/themed-admin-search-page.component.ts b/src/app/admin/admin-search-page/themed-admin-search-page.component.ts new file mode 100644 index 0000000000..741a3b04f9 --- /dev/null +++ b/src/app/admin/admin-search-page/themed-admin-search-page.component.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { ThemedComponent } from '../../shared/theme-support/themed.component'; +import { AdminSearchPageComponent } from './admin-search-page.component'; + +/** + * Themed wrapper for {@link AdminSearchPageComponent} + */ +@Component({ + selector: 'ds-themed-admin-search-page', + templateUrl: '../../shared/theme-support/themed.component.html', +}) +export class ThemedAdminSearchPageComponent extends ThemedComponent { + + protected getComponentName(): string { + return 'AdminSearchPageComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../themes/${themeName}/app/admin/admin-search-page/admin-search-page.component`); + } + + protected importUnthemedComponent(): Promise { + return import('./admin-search-page.component'); + } + +} diff --git a/src/themes/custom/app/admin/admin-search-page/admin-search-page.component.html b/src/themes/custom/app/admin/admin-search-page/admin-search-page.component.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/themes/custom/app/admin/admin-search-page/admin-search-page.component.scss b/src/themes/custom/app/admin/admin-search-page/admin-search-page.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/themes/custom/app/admin/admin-search-page/admin-search-page.component.ts b/src/themes/custom/app/admin/admin-search-page/admin-search-page.component.ts new file mode 100644 index 0000000000..358f11f0d1 --- /dev/null +++ b/src/themes/custom/app/admin/admin-search-page/admin-search-page.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { AdminSearchPageComponent as BaseComponent } from '../../../../../app/admin/admin-search-page/admin-search-page.component'; + +@Component({ + selector: 'ds-admin-search-page', + // styleUrls: ['./admin-search-page.component.scss'], + styleUrls: ['../../../../../app/admin/admin-search-page/admin-search-page.component.scss'], + // templateUrl: './admin-search-page.component.html', + templateUrl: '../../../../../app/admin/admin-search-page/admin-search-page.component.html', +}) +export class AdminSearchPageComponent extends BaseComponent { +} diff --git a/src/themes/custom/lazy-theme.module.ts b/src/themes/custom/lazy-theme.module.ts index d2ac0ae787..4f35854e5e 100644 --- a/src/themes/custom/lazy-theme.module.ts +++ b/src/themes/custom/lazy-theme.module.ts @@ -114,6 +114,7 @@ import { ObjectListComponent } from './app/shared/object-list/object-list.compon import { BrowseByMetadataPageComponent } from './app/browse-by/browse-by-metadata-page/browse-by-metadata-page.component'; import { BrowseByDatePageComponent } from './app/browse-by/browse-by-date-page/browse-by-date-page.component'; import { BrowseByTitlePageComponent } from './app/browse-by/browse-by-title-page/browse-by-title-page.component'; +import { AdminSearchPageComponent } from './app/admin/admin-search-page/admin-search-page.component'; const DECLARATIONS = [ FileSectionComponent, @@ -168,8 +169,7 @@ const DECLARATIONS = [ BrowseByMetadataPageComponent, BrowseByDatePageComponent, BrowseByTitlePageComponent, - - + AdminSearchPageComponent, ]; @NgModule({ From 970b19bf0104a9f361717d148bfc66a60e70e239 Mon Sep 17 00:00:00 2001 From: Alexandre Vryghem Date: Wed, 22 May 2024 14:02:41 +0200 Subject: [PATCH 005/720] 115051: Created ThemedAdminWorkflowPageComponent --- src/app/admin/admin-routing.module.ts | 3 ++- .../admin-workflow.module.ts | 2 ++ .../themed-admin-workflow-page.component.ts | 26 +++++++++++++++++++ .../admin-workflow-page.component.html | 0 .../admin-workflow-page.component.scss | 0 .../admin-workflow-page.component.ts | 12 +++++++++ src/themes/custom/lazy-theme.module.ts | 2 ++ 7 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 src/app/admin/admin-workflow-page/themed-admin-workflow-page.component.ts create mode 100644 src/themes/custom/app/admin/admin-workflow-page/admin-workflow-page.component.html create mode 100644 src/themes/custom/app/admin/admin-workflow-page/admin-workflow-page.component.scss create mode 100644 src/themes/custom/app/admin/admin-workflow-page/admin-workflow-page.component.ts diff --git a/src/app/admin/admin-routing.module.ts b/src/app/admin/admin-routing.module.ts index 3e3a8924ac..58ccd54c76 100644 --- a/src/app/admin/admin-routing.module.ts +++ b/src/app/admin/admin-routing.module.ts @@ -8,6 +8,7 @@ import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.ser import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component'; import { REGISTRIES_MODULE_PATH } from './admin-routing-paths'; import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component'; +import { ThemedAdminWorkflowPageComponent } from './admin-workflow-page/themed-admin-workflow-page.component'; @NgModule({ imports: [ @@ -26,7 +27,7 @@ import { BatchImportPageComponent } from './admin-import-batch-page/batch-import { path: 'workflow', resolve: { breadcrumb: I18nBreadcrumbResolver }, - component: AdminWorkflowPageComponent, + component: ThemedAdminWorkflowPageComponent, data: { title: 'admin.workflow.title', breadcrumbKey: 'admin.workflow' } }, { diff --git a/src/app/admin/admin-workflow-page/admin-workflow.module.ts b/src/app/admin/admin-workflow-page/admin-workflow.module.ts index 85e8f00a46..1622a3b290 100644 --- a/src/app/admin/admin-workflow-page/admin-workflow.module.ts +++ b/src/app/admin/admin-workflow-page/admin-workflow.module.ts @@ -6,6 +6,7 @@ import { WorkflowItemAdminWorkflowActionsComponent } from './admin-workflow-sear import { WorkflowItemSearchResultAdminWorkflowListElementComponent } from './admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component'; import { AdminWorkflowPageComponent } from './admin-workflow-page.component'; import { SearchModule } from '../../shared/search/search.module'; +import { ThemedAdminWorkflowPageComponent } from './themed-admin-workflow-page.component'; const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator @@ -19,6 +20,7 @@ const ENTRY_COMPONENTS = [ SharedModule.withEntryComponents() ], declarations: [ + ThemedAdminWorkflowPageComponent, AdminWorkflowPageComponent, WorkflowItemAdminWorkflowActionsComponent, ...ENTRY_COMPONENTS diff --git a/src/app/admin/admin-workflow-page/themed-admin-workflow-page.component.ts b/src/app/admin/admin-workflow-page/themed-admin-workflow-page.component.ts new file mode 100644 index 0000000000..fe84c44d0e --- /dev/null +++ b/src/app/admin/admin-workflow-page/themed-admin-workflow-page.component.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { ThemedComponent } from '../../shared/theme-support/themed.component'; +import { AdminWorkflowPageComponent } from './admin-workflow-page.component'; + +/** + * Themed wrapper for {@link AdminWorkflowPageComponent} + */ +@Component({ + selector: 'ds-themed-admin-workflow-page', + templateUrl: '../../shared/theme-support/themed.component.html', +}) +export class ThemedAdminWorkflowPageComponent extends ThemedComponent { + + protected getComponentName(): string { + return 'AdminWorkflowPageComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../themes/${themeName}/app/admin/admin-workflow-page/admin-workflow-page.component`); + } + + protected importUnthemedComponent(): Promise { + return import('./admin-workflow-page.component'); + } + +} diff --git a/src/themes/custom/app/admin/admin-workflow-page/admin-workflow-page.component.html b/src/themes/custom/app/admin/admin-workflow-page/admin-workflow-page.component.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/themes/custom/app/admin/admin-workflow-page/admin-workflow-page.component.scss b/src/themes/custom/app/admin/admin-workflow-page/admin-workflow-page.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/themes/custom/app/admin/admin-workflow-page/admin-workflow-page.component.ts b/src/themes/custom/app/admin/admin-workflow-page/admin-workflow-page.component.ts new file mode 100644 index 0000000000..27c7e57fed --- /dev/null +++ b/src/themes/custom/app/admin/admin-workflow-page/admin-workflow-page.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { AdminWorkflowPageComponent as BaseComponent } from '../../../../../app/admin/admin-workflow-page/admin-workflow-page.component'; + +@Component({ + selector: 'ds-admin-workflow-page', + // styleUrls: ['./admin-workflow-page.component.scss'], + styleUrls: ['../../../../../app/admin/admin-workflow-page/admin-workflow-page.component.scss'], + // templateUrl: './admin-workflow-page.component.html', + templateUrl: '../../../../../app/admin/admin-workflow-page/admin-workflow-page.component.html', +}) +export class AdminWorkflowPageComponent extends BaseComponent { +} diff --git a/src/themes/custom/lazy-theme.module.ts b/src/themes/custom/lazy-theme.module.ts index 4f35854e5e..1939483c4f 100644 --- a/src/themes/custom/lazy-theme.module.ts +++ b/src/themes/custom/lazy-theme.module.ts @@ -115,6 +115,7 @@ import { BrowseByMetadataPageComponent } from './app/browse-by/browse-by-metadat import { BrowseByDatePageComponent } from './app/browse-by/browse-by-date-page/browse-by-date-page.component'; import { BrowseByTitlePageComponent } from './app/browse-by/browse-by-title-page/browse-by-title-page.component'; import { AdminSearchPageComponent } from './app/admin/admin-search-page/admin-search-page.component'; +import { AdminWorkflowPageComponent } from './app/admin/admin-workflow-page/admin-workflow-page.component'; const DECLARATIONS = [ FileSectionComponent, @@ -170,6 +171,7 @@ const DECLARATIONS = [ BrowseByDatePageComponent, BrowseByTitlePageComponent, AdminSearchPageComponent, + AdminWorkflowPageComponent, ]; @NgModule({ From 087c203f725a0748042fd2b2dc30993641e15e55 Mon Sep 17 00:00:00 2001 From: lotte Date: Tue, 25 Jun 2024 18:28:56 +0200 Subject: [PATCH 006/720] 116132: Fixed cookie issue --- src/app/shared/cookies/klaro-configuration.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/shared/cookies/klaro-configuration.ts b/src/app/shared/cookies/klaro-configuration.ts index a41b641dec..a09db759a6 100644 --- a/src/app/shared/cookies/klaro-configuration.ts +++ b/src/app/shared/cookies/klaro-configuration.ts @@ -183,7 +183,6 @@ export const klaroConfiguration: any = { purposes: ['registration-password-recovery'], required: false, cookies: [ - [/^klaro-.+$/], CAPTCHA_COOKIE ], onAccept: `window.refreshCaptchaScript?.call()`, From a23cdfbc2b1dd6dd4a494432c992fdac3a38fc70 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Wed, 26 Jun 2024 08:32:43 +0200 Subject: [PATCH 007/720] 115284: Add repeatable based on relationship max cardinality --- .../edit-relationship-list.component.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts index 6cca52ba96..835ee4ad7a 100644 --- a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts +++ b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts @@ -232,6 +232,22 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy { return update && update.field ? update.field.uuid : undefined; } + /** + * Check whether the current entity can have multiple relationships of this type + * This is based on the max cardinality of the relationship + * @private + */ + private isRepeatable(): boolean { + const isLeft = this.currentItemIsLeftItem$.getValue(); + if (isLeft) { + const leftMaxCardinality = this.relationshipType.leftMaxCardinality; + return hasNoValue(leftMaxCardinality) || leftMaxCardinality > 1; + } else { + const rightMaxCardinality = this.relationshipType.rightMaxCardinality; + return hasNoValue(rightMaxCardinality) || rightMaxCardinality > 1; + } + } + /** * Open the dynamic lookup modal to search for items to add as relationships */ @@ -249,6 +265,7 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy { modalComp.toAdd = []; modalComp.toRemove = []; modalComp.isPending = false; + modalComp.repeatable = this.isRepeatable(); modalComp.hiddenQuery = '-search.resourceid:' + this.item.uuid; this.item.owningCollection.pipe( From 1d8d3b3c273fb7f39e2c26ad0bd620b865ce84c8 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Tue, 9 Jul 2024 14:34:17 -0500 Subject: [PATCH 008/720] Update version tag for development of next release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 664bef37ea..ae3cf5ac51 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dspace-angular", - "version": "7.6.2", + "version": "7.6.3-next", "scripts": { "ng": "ng", "config:watch": "nodemon", From bb770ba65b7941f936d8e657771a88cb11994aeb Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Wed, 10 Jul 2024 11:34:18 +0200 Subject: [PATCH 009/720] [CST-14903] Orcid Synchronization improvements feat: - Introduces reactive states derived from item inside orcid-sync page - Removes unnecessary navigation ref: - Introduces catchError operator and handles failures with error messages --- .../orcid-auth/orcid-auth.component.ts | 15 +- .../orcid-page/orcid-page.component.ts | 18 +- .../orcid-sync-settings.component.spec.ts | 10 +- .../orcid-sync-settings.component.ts | 165 ++++++++++++------ 4 files changed, 144 insertions(+), 64 deletions(-) diff --git a/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts index ea970e7d31..73b4a7b4e1 100644 --- a/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts +++ b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts @@ -1,8 +1,8 @@ import { Component, EventEmitter, Inject, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import { BehaviorSubject, Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { BehaviorSubject, Observable, of } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; import { NativeWindowRef, NativeWindowService } from '../../../core/services/window.service'; import { Item } from '../../../core/shared/item.model'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; @@ -10,6 +10,8 @@ import { RemoteData } from '../../../core/data/remote-data'; import { ResearcherProfile } from '../../../core/profile/model/researcher-profile.model'; import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; import { OrcidAuthService } from '../../../core/orcid/orcid-auth.service'; +import { createFailedRemoteDataObject } from '../../../shared/remote-data.utils'; +import { HttpErrorResponse } from '@angular/common/http'; @Component({ selector: 'ds-orcid-auth', @@ -170,14 +172,15 @@ export class OrcidAuthComponent implements OnInit, OnChanges { unlinkOrcid(): void { this.unlinkProcessing.next(true); this.orcidAuthService.unlinkOrcidByItem(this.item).pipe( - getFirstCompletedRemoteData() + getFirstCompletedRemoteData(), + catchError((err: HttpErrorResponse) => of(createFailedRemoteDataObject(err.message, err.status))) ).subscribe((remoteData: RemoteData) => { this.unlinkProcessing.next(false); - if (remoteData.isSuccess) { + if (remoteData.hasFailed) { + this.notificationsService.error(this.translateService.get('person.page.orcid.unlink.error')); + } else { this.notificationsService.success(this.translateService.get('person.page.orcid.unlink.success')); this.unlink.emit(); - } else { - this.notificationsService.error(this.translateService.get('person.page.orcid.unlink.error')); } }); } diff --git a/src/app/item-page/orcid-page/orcid-page.component.ts b/src/app/item-page/orcid-page/orcid-page.component.ts index f3dbb569d9..1d62c9691c 100644 --- a/src/app/item-page/orcid-page/orcid-page.component.ts +++ b/src/app/item-page/orcid-page/orcid-page.component.ts @@ -3,7 +3,7 @@ import { ActivatedRoute, ParamMap, Router } from '@angular/router'; import { isPlatformBrowser } from '@angular/common'; import { BehaviorSubject, combineLatest } from 'rxjs'; -import { map, take } from 'rxjs/operators'; +import { filter, map, take } from 'rxjs/operators'; import { OrcidAuthService } from '../../core/orcid/orcid-auth.service'; import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; @@ -147,7 +147,19 @@ export class OrcidPageComponent implements OnInit { */ private clearRouteParams(): void { // update route removing the code from query params - const redirectUrl = this.router.url.split('?')[0]; - this.router.navigate([redirectUrl]); + this.route.queryParamMap + .pipe( + filter((paramMap: ParamMap) => isNotEmpty(paramMap.keys)), + map(_ => Object.assign({})), + take(1), + ).subscribe(queryParams => + this.router.navigate( + [], + { + relativeTo: this.route, + queryParams + } + ) + ); } } diff --git a/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.spec.ts b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.spec.ts index f2fa9d2440..38a6df909e 100644 --- a/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.spec.ts +++ b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.spec.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { UntypedFormControl, UntypedFormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { FormsModule, ReactiveFormsModule, UntypedFormControl, UntypedFormGroup } from '@angular/forms'; import { RouterTestingModule } from '@angular/router/testing'; import { By } from '@angular/platform-browser'; @@ -24,8 +24,8 @@ describe('OrcidSyncSettingsComponent test suite', () => { let comp: OrcidSyncSettingsComponent; let fixture: ComponentFixture; let scheduler: TestScheduler; - let researcherProfileService: jasmine.SpyObj; let notificationsService; + let researcherProfileService: jasmine.SpyObj; let formGroup: UntypedFormGroup; const mockResearcherProfile: ResearcherProfile = Object.assign(new ResearcherProfile(), { @@ -161,6 +161,7 @@ describe('OrcidSyncSettingsComponent test suite', () => { scheduler = getTestScheduler(); fixture = TestBed.createComponent(OrcidSyncSettingsComponent); comp = fixture.componentInstance; + researcherProfileService.findByRelatedItem.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile)); comp.item = mockItemLinkedToOrcid; fixture.detectChanges(); })); @@ -197,7 +198,6 @@ describe('OrcidSyncSettingsComponent test suite', () => { }); it('should call updateByOrcidOperations properly', () => { - researcherProfileService.findByRelatedItem.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile)); researcherProfileService.patch.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile)); const expectedOps: Operation[] = [ { @@ -226,7 +226,6 @@ describe('OrcidSyncSettingsComponent test suite', () => { }); it('should show notification on success', () => { - researcherProfileService.findByRelatedItem.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile)); researcherProfileService.patch.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile)); scheduler.schedule(() => comp.onSubmit(formGroup)); @@ -238,6 +237,8 @@ describe('OrcidSyncSettingsComponent test suite', () => { it('should show notification on error', () => { researcherProfileService.findByRelatedItem.and.returnValue(createFailedRemoteDataObject$()); + comp.item = mockItemLinkedToOrcid; + fixture.detectChanges(); scheduler.schedule(() => comp.onSubmit(formGroup)); scheduler.flush(); @@ -247,7 +248,6 @@ describe('OrcidSyncSettingsComponent test suite', () => { }); it('should show notification on error', () => { - researcherProfileService.findByRelatedItem.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile)); researcherProfileService.patch.and.returnValue(createFailedRemoteDataObject$()); scheduler.schedule(() => comp.onSubmit(formGroup)); diff --git a/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.ts b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.ts index 0bcbc295ac..422041d340 100644 --- a/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.ts +++ b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.ts @@ -1,80 +1,97 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { UntypedFormGroup } from '@angular/forms'; import { TranslateService } from '@ngx-translate/core'; import { Operation } from 'fast-json-patch'; -import { of } from 'rxjs'; -import { switchMap } from 'rxjs/operators'; +import { BehaviorSubject, Observable, of } from 'rxjs'; +import { catchError, filter, map, switchMap, take, takeUntil } from 'rxjs/operators'; import { RemoteData } from '../../../core/data/remote-data'; import { ResearcherProfileDataService } from '../../../core/profile/researcher-profile-data.service'; import { Item } from '../../../core/shared/item.model'; -import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; +import { getFirstCompletedRemoteData, getRemoteDataPayload } from '../../../core/shared/operators'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { ResearcherProfile } from '../../../core/profile/model/researcher-profile.model'; +import { hasValue } from '../../../shared/empty.util'; +import { HttpErrorResponse } from '@angular/common/http'; +import { createFailedRemoteDataObject } from '../../../shared/remote-data.utils'; @Component({ selector: 'ds-orcid-sync-setting', templateUrl: './orcid-sync-settings.component.html', styleUrls: ['./orcid-sync-settings.component.scss'] }) -export class OrcidSyncSettingsComponent implements OnInit { - - /** - * The item for which showing the orcid settings - */ - @Input() item: Item; +export class OrcidSyncSettingsComponent implements OnInit, OnDestroy { /** * The prefix used for i18n keys */ messagePrefix = 'person.page.orcid'; - /** * The current synchronization mode */ currentSyncMode: string; - /** * The current synchronization mode for publications */ currentSyncPublications: string; - /** * The current synchronization mode for funding */ currentSyncFunding: string; - /** * The synchronization options */ syncModes: { value: string, label: string }[]; - /** * The synchronization options for publications */ syncPublicationOptions: { value: string, label: string }[]; - /** * The synchronization options for funding */ syncFundingOptions: { value: string, label: string }[]; - /** * The profile synchronization options */ syncProfileOptions: { value: string, label: string, checked: boolean }[]; - /** * An event emitted when settings are updated */ @Output() settingsUpdated: EventEmitter = new EventEmitter(); + /** + * Emitter that triggers onDestroy lifecycle + * @private + */ + readonly #destroy$ = new EventEmitter(); + /** + * {@link BehaviorSubject} that reflects {@link item} input changes + * @private + */ + readonly #item$ = new BehaviorSubject(null); + /** + * {@link Observable} that contains {@link ResearcherProfile} linked to the {@link #item$} + * @private + */ + #researcherProfile$: Observable; constructor(private researcherProfileService: ResearcherProfileDataService, private notificationsService: NotificationsService, private translateService: TranslateService) { } + /** + * The item for which showing the orcid settings + */ + @Input() + set item(item: Item) { + this.#item$.next(item); + } + + ngOnDestroy(): void { + this.#destroy$.next(); + } + /** * Init orcid settings form */ @@ -106,20 +123,21 @@ export class OrcidSyncSettingsComponent implements OnInit { }; }); - const syncProfilePreferences = this.item.allMetadataValues('dspace.orcid.sync-profile'); + this.updateSyncProfileOptions(this.#item$.asObservable()); + this.updateSyncPreferences(this.#item$.asObservable()); - this.syncProfileOptions = ['BIOGRAPHICAL', 'IDENTIFIERS'] - .map((value) => { - return { - label: this.messagePrefix + '.sync-profile.' + value.toLowerCase(), - value: value, - checked: syncProfilePreferences.includes(value) - }; - }); - - this.currentSyncMode = this.getCurrentPreference('dspace.orcid.sync-mode', ['BATCH', 'MANUAL'], 'MANUAL'); - this.currentSyncPublications = this.getCurrentPreference('dspace.orcid.sync-publications', ['DISABLED', 'ALL'], 'DISABLED'); - this.currentSyncFunding = this.getCurrentPreference('dspace.orcid.sync-fundings', ['DISABLED', 'ALL'], 'DISABLED'); + this.#researcherProfile$ = + this.#item$.pipe( + switchMap(item => + this.researcherProfileService.findByRelatedItem(item) + .pipe( + getFirstCompletedRemoteData(), + catchError((err: HttpErrorResponse) => of(createFailedRemoteDataObject(err.message, err.status))), + getRemoteDataPayload(), + ) + ), + takeUntil(this.#destroy$) + ); } /** @@ -144,37 +162,84 @@ export class OrcidSyncSettingsComponent implements OnInit { return; } - this.researcherProfileService.findByRelatedItem(this.item).pipe( - getFirstCompletedRemoteData(), - switchMap((profileRD: RemoteData) => { - if (profileRD.hasSucceeded) { - return this.researcherProfileService.patch(profileRD.payload, operations).pipe( - getFirstCompletedRemoteData(), - ); + this.#researcherProfile$ + .pipe( + switchMap(researcherProfile => this.researcherProfileService.patch(researcherProfile, operations)), + getFirstCompletedRemoteData(), + catchError((err: HttpErrorResponse) => of(createFailedRemoteDataObject(err.message, err.status))), + take(1) + ) + .subscribe((remoteData: RemoteData) => { + if (remoteData.hasFailed) { + this.notificationsService.error(this.translateService.get(this.messagePrefix + '.synchronization-settings-update.error')); } else { - return of(profileRD); + this.notificationsService.success(this.translateService.get(this.messagePrefix + '.synchronization-settings-update.success')); + this.settingsUpdated.emit(); } - }), - ).subscribe((remoteData: RemoteData) => { - if (remoteData.isSuccess) { - this.notificationsService.success(this.translateService.get(this.messagePrefix + '.synchronization-settings-update.success')); - this.settingsUpdated.emit(); - } else { - this.notificationsService.error(this.translateService.get(this.messagePrefix + '.synchronization-settings-update.error')); - } - }); + }); + } + + /** + * + * Handles subscriptions to populate sync preferences + * + * @param item observable that emits update on item changes + * @private + */ + private updateSyncPreferences(item: Observable) { + item.pipe( + filter(hasValue), + map(i => this.getCurrentPreference(i, 'dspace.orcid.sync-mode', ['BATCH', 'MANUAL'], 'MANUAL')), + takeUntil(this.#destroy$) + ).subscribe(val => this.currentSyncMode = val); + item.pipe( + filter(hasValue), + map(i => this.getCurrentPreference(i, 'dspace.orcid.sync-publications', ['DISABLED', 'ALL'], 'DISABLED')), + takeUntil(this.#destroy$) + ).subscribe(val => this.currentSyncPublications = val); + item.pipe( + filter(hasValue), + map(i => this.getCurrentPreference(i, 'dspace.orcid.sync-fundings', ['DISABLED', 'ALL'], 'DISABLED')), + takeUntil(this.#destroy$) + ).subscribe(val => this.currentSyncFunding = val); + } + + /** + * Handles subscription to populate the {@link syncProfileOptions} field + * + * @param item observable that emits update on item changes + * @private + */ + private updateSyncProfileOptions(item: Observable) { + item.pipe( + filter(hasValue), + map(i => i.allMetadataValues('dspace.orcid.sync-profile')), + map(metadata => + ['BIOGRAPHICAL', 'IDENTIFIERS'] + .map((value) => { + return { + label: this.messagePrefix + '.sync-profile.' + value.toLowerCase(), + value: value, + checked: metadata.includes(value) + }; + }) + ), + takeUntil(this.#destroy$) + ) + .subscribe(value => this.syncProfileOptions = value); } /** * Retrieve setting saved in the item's metadata * + * @param item The item from which retrieve settings * @param metadataField The metadata name that contains setting * @param allowedValues The allowed values * @param defaultValue The default value * @private */ - private getCurrentPreference(metadataField: string, allowedValues: string[], defaultValue: string): string { - const currentPreference = this.item.firstMetadataValue(metadataField); + private getCurrentPreference(item: Item, metadataField: string, allowedValues: string[], defaultValue: string): string { + const currentPreference = item.firstMetadataValue(metadataField); return (currentPreference && allowedValues.includes(currentPreference)) ? currentPreference : defaultValue; } From bdea9a6d716eefd6858787fd1bab69f5f8c2ad2a Mon Sep 17 00:00:00 2001 From: root Date: Mon, 1 Jul 2024 15:00:07 -0300 Subject: [PATCH 010/720] Resolution of issue #1193 - Addition of the aria-label attribute to the add, save, discard and undo buttons on the metadata editing page (cherry picked from commit 4e783e76d167e642c9205b440232211bfd4bc290) --- .../dso-edit-metadata/dso-edit-metadata.component.html | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html index 8fb676a724..09868c6865 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html @@ -1,21 +1,25 @@ diff --git a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.ts b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.ts index a6eaf436c9..1528acc0c6 100644 --- a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.ts +++ b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.ts @@ -81,6 +81,11 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple */ range; + /** + * The range currently selected by the slider + */ + sliderRange: [number | undefined, number | undefined]; + /** * Subscription to unsubscribe from */ @@ -138,6 +143,15 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple }; } + /** + * Updates the sliderRange property with the current slider range. + * This method is called whenever the slider value changes, but it does not immediately apply the changes. + * @param range - The current range selected by the slider + */ + onSliderChange(range: [number | undefined, number | undefined]): void { + this.sliderRange = range; + } + /** * Submits new custom range values to the range filter from the widget */ From a0515c412140184c23cface4febb5d48f0d430eb Mon Sep 17 00:00:00 2001 From: Alexandre Vryghem Date: Tue, 13 Aug 2024 11:43:36 +0200 Subject: [PATCH 018/720] 110615: Fixed related entities ay11 tab issue & removed potential duplicate ID issue by removing the unused #browseDropdown ID --- .../tabbed-related-entities-search.component.html | 8 +++++--- .../expandable-navbar-section.component.html | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/app/item-page/simple/related-entities/tabbed-related-entities-search/tabbed-related-entities-search.component.html b/src/app/item-page/simple/related-entities/tabbed-related-entities-search/tabbed-related-entities-search.component.html index 223d4a6ed0..3ce96b2ce3 100644 --- a/src/app/item-page/simple/related-entities/tabbed-related-entities-search/tabbed-related-entities-search.component.html +++ b/src/app/item-page/simple/related-entities/tabbed-related-entities-search/tabbed-related-entities-search.component.html @@ -1,7 +1,9 @@ - - + diff --git a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.html b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.html index 1ce811ce66..b529d9aee4 100644 --- a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.html +++ b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.html @@ -1,7 +1,7 @@
diff --git a/src/app/submission/sections/upload/file/section-upload-file.component.html b/src/app/submission/sections/upload/file/section-upload-file.component.html index 589e7b6ebf..3567ef5c64 100644 --- a/src/app/submission/sections/upload/file/section-upload-file.component.html +++ b/src/app/submission/sections/upload/file/section-upload-file.component.html @@ -19,7 +19,7 @@

{{fileName}} ({{fileData?.sizeBytes | dsFileSize}}) -

From 4a7ebeea16cdd902f99da11ba2dc9e5ab155620e Mon Sep 17 00:00:00 2001 From: Jens Vannerum Date: Mon, 16 Sep 2024 16:09:41 +0200 Subject: [PATCH 040/720] 117544: fix remaining bug --- src/app/shared/btn-disabled.directive.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/btn-disabled.directive.ts b/src/app/shared/btn-disabled.directive.ts index ab37643c02..512aa87ede 100644 --- a/src/app/shared/btn-disabled.directive.ts +++ b/src/app/shared/btn-disabled.directive.ts @@ -12,7 +12,7 @@ import { Directive, Input, HostBinding, HostListener } from '@angular/core'; */ export class BtnDisabledDirective { - @Input() set dsDisabled(value: boolean) { + @Input() set dsBtnDisabled(value: boolean) { this.isDisabled = !!value; } From 83a44ba924fb20065f0bb62fc2a09a7aaec391bc Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Mon, 16 Sep 2024 15:46:07 +0200 Subject: [PATCH 041/720] 118220: Add live-region service and component --- config/config.example.yml | 16 ++- .../live-region/live-region.component.html | 3 + .../live-region/live-region.component.scss | 13 ++ .../live-region/live-region.component.ts | 25 ++++ .../shared/live-region/live-region.config.ts | 9 ++ .../shared/live-region/live-region.service.ts | 118 ++++++++++++++++++ src/app/shared/shared.module.ts | 4 +- src/config/app-config.interface.ts | 2 + src/config/default-app-config.ts | 7 ++ src/environments/environment.test.ts | 7 +- src/styles/_custom_variables.scss | 1 + 11 files changed, 202 insertions(+), 3 deletions(-) create mode 100644 src/app/shared/live-region/live-region.component.html create mode 100644 src/app/shared/live-region/live-region.component.scss create mode 100644 src/app/shared/live-region/live-region.component.ts create mode 100644 src/app/shared/live-region/live-region.config.ts create mode 100644 src/app/shared/live-region/live-region.service.ts diff --git a/config/config.example.yml b/config/config.example.yml index ea38303fa3..58eb6ff33d 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -379,4 +379,18 @@ vocabularies: # Default collection/community sorting order at Advanced search, Create/update community and collection when there are not a query. comcolSelectionSort: sortField: 'dc.title' - sortDirection: 'ASC' \ No newline at end of file + sortDirection: 'ASC' + +# Live Region configuration +# Live Region as defined by w3c, https://www.w3.org/TR/wai-aria-1.1/#terms: +# Live regions are perceivable regions of a web page that are typically updated as a +# result of an external event when user focus may be elsewhere. +# +# The DSpace live region is a component present at the bottom of all pages that is invisible by default, but is useful +# for screen readers. Any message pushed to the live region will be announced by the screen reader. These messages +# usually contain information about changes on the page that might not be in focus. +liveRegion: + # The duration after which messages disappear from the live region in milliseconds + messageTimeOutDurationMs: 30000 + # The visibility of the live region. Setting this to true is only useful for debugging purposes. + isVisible: false diff --git a/src/app/shared/live-region/live-region.component.html b/src/app/shared/live-region/live-region.component.html new file mode 100644 index 0000000000..a48f3ad52e --- /dev/null +++ b/src/app/shared/live-region/live-region.component.html @@ -0,0 +1,3 @@ +
+
{{ message }}
+
diff --git a/src/app/shared/live-region/live-region.component.scss b/src/app/shared/live-region/live-region.component.scss new file mode 100644 index 0000000000..69844a93e1 --- /dev/null +++ b/src/app/shared/live-region/live-region.component.scss @@ -0,0 +1,13 @@ +.live-region { + position: fixed; + bottom: 0; + left: 0; + right: 0; + padding-left: 60px; + height: 90px; + line-height: 18px; + color: var(--bs-white); + background-color: var(--bs-dark); + opacity: 0.94; + z-index: var(--ds-live-region-z-index); +} diff --git a/src/app/shared/live-region/live-region.component.ts b/src/app/shared/live-region/live-region.component.ts new file mode 100644 index 0000000000..d7bd5eb806 --- /dev/null +++ b/src/app/shared/live-region/live-region.component.ts @@ -0,0 +1,25 @@ +import { Component, OnInit } from '@angular/core'; +import { LiveRegionService } from './live-region.service'; +import { Observable } from 'rxjs'; + +@Component({ + selector: `ds-live-region`, + templateUrl: './live-region.component.html', + styleUrls: ['./live-region.component.scss'], +}) +export class LiveRegionComponent implements OnInit { + + protected isVisible: boolean; + + protected messages$: Observable; + + constructor( + protected liveRegionService: LiveRegionService, + ) { + } + + ngOnInit() { + this.isVisible = this.liveRegionService.getLiveRegionVisibility(); + this.messages$ = this.liveRegionService.getMessages$(); + } +} diff --git a/src/app/shared/live-region/live-region.config.ts b/src/app/shared/live-region/live-region.config.ts new file mode 100644 index 0000000000..e545bfd254 --- /dev/null +++ b/src/app/shared/live-region/live-region.config.ts @@ -0,0 +1,9 @@ +import { Config } from '../../../config/config.interface'; + +/** + * Configuration interface used by the LiveRegionService + */ +export class LiveRegionConfig implements Config { + messageTimeOutDurationMs: number; + isVisible: boolean; +} diff --git a/src/app/shared/live-region/live-region.service.ts b/src/app/shared/live-region/live-region.service.ts new file mode 100644 index 0000000000..482d1ca1bb --- /dev/null +++ b/src/app/shared/live-region/live-region.service.ts @@ -0,0 +1,118 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { environment } from '../../../environments/environment'; + +@Injectable({ + providedIn: 'root', +}) +export class LiveRegionService { + + /** + * The duration after which the messages disappear in milliseconds + * @protected + */ + protected messageTimeOutDurationMs: number = environment.liveRegion.messageTimeOutDurationMs; + + /** + * Array containing the messages that should be shown in the live region + * @protected + */ + protected messages: string[] = []; + + /** + * BehaviorSubject emitting the array with messages every time the array updates + * @protected + */ + protected messages$: BehaviorSubject = new BehaviorSubject([]); + + /** + * Whether the live region should be visible + * @protected + */ + protected liveRegionIsVisible: boolean = environment.liveRegion.isVisible; + + /** + * Returns a copy of the array with the current live region messages + */ + getMessages() { + return [...this.messages]; + } + + /** + * Returns the BehaviorSubject emitting the array with messages every time the array updates + */ + getMessages$() { + return this.messages$; + } + + /** + * Adds a message to the live-region messages array + * @param message + */ + addMessage(message: string) { + this.messages.push(message); + this.emitCurrentMessages(); + + // Clear the message once the timeOut has passed + setTimeout(() => this.pop(), this.messageTimeOutDurationMs); + } + + /** + * Clears the live-region messages array + */ + clear() { + this.messages = []; + this.emitCurrentMessages(); + } + + /** + * Removes the longest living message from the array. + * @protected + */ + protected pop() { + if (this.messages.length > 0) { + this.messages.shift(); + this.emitCurrentMessages(); + } + } + + /** + * Makes the messages$ BehaviorSubject emit the current messages array + * @protected + */ + protected emitCurrentMessages() { + this.messages$.next(this.getMessages()); + } + + /** + * Returns a boolean specifying whether the live region should be visible. + * Returns 'true' if the region should be visible and false otherwise. + */ + getLiveRegionVisibility(): boolean { + return this.liveRegionIsVisible; + } + + /** + * Sets the visibility of the live region. + * Setting this to true will make the live region visible which is useful for debugging purposes. + * @param isVisible + */ + setLiveRegionVisibility(isVisible: boolean) { + this.liveRegionIsVisible = isVisible; + } + + /** + * Gets the current message timeOut duration in milliseconds + */ + getMessageTimeOutMs(): number { + return this.messageTimeOutDurationMs; + } + + /** + * Sets the message timeOut duration + * @param timeOutMs the message timeOut duration in milliseconds + */ + setMessageTimeOutMs(timeOutMs: number) { + this.messageTimeOutDurationMs = timeOutMs; + } +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 0f7871f7f9..db5b778722 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -284,6 +284,7 @@ import { } from '../item-page/simple/field-components/specific-field/title/themed-item-page-field.component'; import { BitstreamListItemComponent } from './object-list/bitstream-list-item/bitstream-list-item.component'; import { NgxPaginationModule } from 'ngx-pagination'; +import { LiveRegionComponent } from './live-region/live-region.component'; const MODULES = [ CommonModule, @@ -465,7 +466,8 @@ const ENTRY_COMPONENTS = [ AdvancedClaimedTaskActionRatingComponent, EpersonGroupListComponent, EpersonSearchBoxComponent, - GroupSearchBoxComponent + GroupSearchBoxComponent, + LiveRegionComponent, ]; const PROVIDERS = [ diff --git a/src/config/app-config.interface.ts b/src/config/app-config.interface.ts index 84a30549a7..aa3033ecec 100644 --- a/src/config/app-config.interface.ts +++ b/src/config/app-config.interface.ts @@ -22,6 +22,7 @@ import { HomeConfig } from './homepage-config.interface'; import { MarkdownConfig } from './markdown-config.interface'; import { FilterVocabularyConfig } from './filter-vocabulary-config'; import { DiscoverySortConfig } from './discovery-sort.config'; +import { LiveRegionConfig } from '../app/shared/live-region/live-region.config'; interface AppConfig extends Config { ui: UIServerConfig; @@ -48,6 +49,7 @@ interface AppConfig extends Config { markdown: MarkdownConfig; vocabularies: FilterVocabularyConfig[]; comcolSelectionSort: DiscoverySortConfig; + liveRegion: LiveRegionConfig; } /** diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index a6e9e092e4..1c0f88cf47 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -22,6 +22,7 @@ import { HomeConfig } from './homepage-config.interface'; import { MarkdownConfig } from './markdown-config.interface'; import { FilterVocabularyConfig } from './filter-vocabulary-config'; import { DiscoverySortConfig } from './discovery-sort.config'; +import { LiveRegionConfig } from '../app/shared/live-region/live-region.config'; export class DefaultAppConfig implements AppConfig { production = false; @@ -432,4 +433,10 @@ export class DefaultAppConfig implements AppConfig { sortField:'dc.title', sortDirection:'ASC', }; + + // Live Region configuration, used by the LiveRegionService + liveRegion: LiveRegionConfig = { + messageTimeOutDurationMs: 30000, + isVisible: false, + }; } diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts index cb9d2c7130..498799a454 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -313,5 +313,10 @@ export const environment: BuildConfig = { vocabulary: 'srsc', enabled: true } - ] + ], + + liveRegion: { + messageTimeOutDurationMs: 30000, + isVisible: false, + }, }; diff --git a/src/styles/_custom_variables.scss b/src/styles/_custom_variables.scss index ddf490c7a7..09267d15ae 100644 --- a/src/styles/_custom_variables.scss +++ b/src/styles/_custom_variables.scss @@ -13,6 +13,7 @@ --ds-login-logo-width:72px; --ds-submission-header-z-index: 1001; --ds-submission-footer-z-index: 999; + --ds-live-region-z-index: 1030; --ds-main-z-index: 1; --ds-nav-z-index: 10; From e987c35450f2a03e73fe5a9951ec412fb9675e33 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Tue, 17 Sep 2024 09:07:02 +0200 Subject: [PATCH 042/720] 118220: Add liveRegionComponent & Service tests --- .../live-region/live-region.component.spec.ts | 57 +++++++ .../live-region/live-region.service.spec.ts | 140 ++++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 src/app/shared/live-region/live-region.component.spec.ts create mode 100644 src/app/shared/live-region/live-region.service.spec.ts diff --git a/src/app/shared/live-region/live-region.component.spec.ts b/src/app/shared/live-region/live-region.component.spec.ts new file mode 100644 index 0000000000..2e9797f9c3 --- /dev/null +++ b/src/app/shared/live-region/live-region.component.spec.ts @@ -0,0 +1,57 @@ +import { LiveRegionComponent } from './live-region.component'; +import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { LiveRegionService } from './live-region.service'; +import { of } from 'rxjs'; +import { By } from '@angular/platform-browser'; + +describe('liveRegionComponent', () => { + let fixture: ComponentFixture; + let liveRegionService: LiveRegionService; + + beforeEach(waitForAsync(() => { + liveRegionService = jasmine.createSpyObj('liveRegionService', { + getMessages$: of(['message1', 'message2']), + getLiveRegionVisibility: false, + setLiveRegionVisibility: undefined, + }); + + void TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + ], + declarations: [LiveRegionComponent], + providers: [ + { provide: LiveRegionService, useValue: liveRegionService }, + ], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LiveRegionComponent); + fixture.detectChanges(); + }); + + it('should contain the current live region messages', () => { + const messages = fixture.debugElement.queryAll(By.css('.live-region-message')); + + expect(messages.length).toEqual(2); + expect(messages[0].nativeElement.textContent).toEqual('message1'); + expect(messages[1].nativeElement.textContent).toEqual('message2'); + }); + + it('should respect the live region visibility', () => { + const liveRegion = fixture.debugElement.query(By.css('.live-region')); + expect(liveRegion).toBeDefined(); + + const liveRegionHidden = fixture.debugElement.query(By.css('.visually-hidden')); + expect(liveRegionHidden).toBeDefined(); + + liveRegionService.getLiveRegionVisibility = jasmine.createSpy('getLiveRegionVisibility').and.returnValue(true); + fixture = TestBed.createComponent(LiveRegionComponent); + fixture.detectChanges(); + + const liveRegionVisible = fixture.debugElement.query(By.css('.visually-hidden')); + expect(liveRegionVisible).toBeNull(); + }); +}); diff --git a/src/app/shared/live-region/live-region.service.spec.ts b/src/app/shared/live-region/live-region.service.spec.ts new file mode 100644 index 0000000000..fe5e8b8d8c --- /dev/null +++ b/src/app/shared/live-region/live-region.service.spec.ts @@ -0,0 +1,140 @@ +import { LiveRegionService } from './live-region.service'; +import { fakeAsync, tick, flush } from '@angular/core/testing'; + +describe('liveRegionService', () => { + let service: LiveRegionService; + + + beforeEach(() => { + service = new LiveRegionService(); + }); + + describe('addMessage', () => { + it('should correctly add messages', () => { + expect(service.getMessages().length).toEqual(0); + + service.addMessage('Message One'); + expect(service.getMessages().length).toEqual(1); + expect(service.getMessages()[0]).toEqual('Message One'); + + service.addMessage('Message Two'); + expect(service.getMessages().length).toEqual(2); + expect(service.getMessages()[1]).toEqual('Message Two'); + }); + }); + + describe('clearMessages', () => { + it('should clear the messages', () => { + expect(service.getMessages().length).toEqual(0); + + service.addMessage('Message One'); + service.addMessage('Message Two'); + expect(service.getMessages().length).toEqual(2); + + service.clear(); + expect(service.getMessages().length).toEqual(0); + }); + }); + + describe('messages$', () => { + it('should emit when a message is added and when a message is removed after the timeOut', fakeAsync(() => { + const results: string[][] = []; + + service.getMessages$().subscribe((messages) => { + results.push(messages); + }); + + expect(results.length).toEqual(1); + expect(results[0]).toEqual([]); + + service.addMessage('message'); + + tick(); + + expect(results.length).toEqual(2); + expect(results[1]).toEqual(['message']); + + tick(service.getMessageTimeOutMs()); + + expect(results.length).toEqual(3); + expect(results[2]).toEqual([]); + })); + + it('should only emit once when the messages are cleared', fakeAsync(() => { + const results: string[][] = []; + + service.getMessages$().subscribe((messages) => { + results.push(messages); + }); + + expect(results.length).toEqual(1); + expect(results[0]).toEqual([]); + + service.addMessage('Message One'); + service.addMessage('Message Two'); + + tick(); + + expect(results.length).toEqual(3); + expect(results[2]).toEqual(['Message One', 'Message Two']); + + service.clear(); + flush(); + + expect(results.length).toEqual(4); + expect(results[3]).toEqual([]); + })); + + it('should respect configured timeOut', fakeAsync(() => { + const results: string[][] = []; + + service.getMessages$().subscribe((messages) => { + results.push(messages); + }); + + expect(results.length).toEqual(1); + expect(results[0]).toEqual([]); + + const timeOutMs = 500; + service.setMessageTimeOutMs(timeOutMs); + + service.addMessage('Message One'); + tick(timeOutMs - 1); + + expect(results.length).toEqual(2); + expect(results[1]).toEqual(['Message One']); + + tick(1); + + expect(results.length).toEqual(3); + expect(results[2]).toEqual([]); + + const timeOutMsTwo = 50000; + service.setMessageTimeOutMs(timeOutMsTwo); + + service.addMessage('Message Two'); + tick(timeOutMsTwo - 1); + + expect(results.length).toEqual(4); + expect(results[3]).toEqual(['Message Two']); + + tick(1); + + expect(results.length).toEqual(5); + expect(results[4]).toEqual([]); + })); + }); + + describe('liveRegionVisibility', () => { + it('should be false by default', () => { + expect(service.getLiveRegionVisibility()).toBeFalse(); + }); + + it('should correctly update', () => { + service.setLiveRegionVisibility(true); + expect(service.getLiveRegionVisibility()).toBeTrue(); + service.setLiveRegionVisibility(false); + expect(service.getLiveRegionVisibility()).toBeFalse(); + }); + }); +}); From 35d29c84258e1656ed17c53e6e7054c69b7878d1 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Fri, 20 Sep 2024 09:38:31 +0200 Subject: [PATCH 043/720] 118220: Add LiveRegion to RootComponent --- src/app/root/root.component.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/root/root.component.html b/src/app/root/root.component.html index bf49e507c0..d59bea1db4 100644 --- a/src/app/root/root.component.html +++ b/src/app/root/root.component.html @@ -27,3 +27,5 @@
+ + From cd51baa5f1ee5d8fa66e8ee94beb3168f93c5c01 Mon Sep 17 00:00:00 2001 From: Andrea Barbasso <´andrea.barbasso@4science.com´> Date: Mon, 23 Sep 2024 18:15:39 +0200 Subject: [PATCH 044/720] [CST-14904] add orcid icon with tooltip --- ...-item-metadata-list-element.component.html | 5 ++ .../orcid-badge-and-tooltip.component.html | 11 +++ .../orcid-badge-and-tooltip.component.scss | 11 +++ .../orcid-badge-and-tooltip.component.spec.ts | 71 +++++++++++++++++++ .../orcid-badge-and-tooltip.component.ts | 56 +++++++++++++++ src/app/shared/shared.module.ts | 2 + src/assets/i18n/en.json5 | 4 ++ 7 files changed, 160 insertions(+) create mode 100644 src/app/shared/orcid-badge-and-tooltip/orcid-badge-and-tooltip.component.html create mode 100644 src/app/shared/orcid-badge-and-tooltip/orcid-badge-and-tooltip.component.scss create mode 100644 src/app/shared/orcid-badge-and-tooltip/orcid-badge-and-tooltip.component.spec.ts create mode 100644 src/app/shared/orcid-badge-and-tooltip/orcid-badge-and-tooltip.component.ts diff --git a/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.html b/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.html index 6f56056781..f61c14d3ba 100644 --- a/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.html +++ b/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.html @@ -12,4 +12,9 @@ + + diff --git a/src/app/shared/orcid-badge-and-tooltip/orcid-badge-and-tooltip.component.html b/src/app/shared/orcid-badge-and-tooltip/orcid-badge-and-tooltip.component.html new file mode 100644 index 0000000000..fc34aee970 --- /dev/null +++ b/src/app/shared/orcid-badge-and-tooltip/orcid-badge-and-tooltip.component.html @@ -0,0 +1,11 @@ +orcid-logo + + + {{ orcidTooltip }} + diff --git a/src/app/shared/orcid-badge-and-tooltip/orcid-badge-and-tooltip.component.scss b/src/app/shared/orcid-badge-and-tooltip/orcid-badge-and-tooltip.component.scss new file mode 100644 index 0000000000..6a1c259e18 --- /dev/null +++ b/src/app/shared/orcid-badge-and-tooltip/orcid-badge-and-tooltip.component.scss @@ -0,0 +1,11 @@ +:host { + display: inline-block; +} + +.orcid-icon { + height: 1.2rem; + + &.not-authenticated { + filter: grayscale(100%); + } +} diff --git a/src/app/shared/orcid-badge-and-tooltip/orcid-badge-and-tooltip.component.spec.ts b/src/app/shared/orcid-badge-and-tooltip/orcid-badge-and-tooltip.component.spec.ts new file mode 100644 index 0000000000..adb3c91f94 --- /dev/null +++ b/src/app/shared/orcid-badge-and-tooltip/orcid-badge-and-tooltip.component.spec.ts @@ -0,0 +1,71 @@ +import { + NgClass, + NgIf, +} from '@angular/common'; +import { + ComponentFixture, + TestBed, +} from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateService } from '@ngx-translate/core'; + +import { MetadataValue } from '../../core/shared/metadata.models'; +import { OrcidBadgeAndTooltipComponent } from './orcid-badge-and-tooltip.component'; + +describe('OrcidBadgeAndTooltipComponent', () => { + let component: OrcidBadgeAndTooltipComponent; + let fixture: ComponentFixture; + let translateService: TranslateService; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [OrcidBadgeAndTooltipComponent], + imports: [ + NgbTooltipModule, + NgClass, + NgIf, + ], + providers: [ + { provide: TranslateService, useValue: { instant: (key: string) => key } }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(OrcidBadgeAndTooltipComponent); + component = fixture.componentInstance; + translateService = TestBed.inject(TranslateService); + + component.orcid = { value: '0000-0002-1825-0097' } as MetadataValue; + component.authenticatedTimestamp = { value: '2023-10-01' } as MetadataValue; + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should set orcidTooltip when authenticatedTimestamp is provided', () => { + component.ngOnInit(); + expect(component.orcidTooltip).toBe('person.orcid-tooltip.authenticated'); + }); + + it('should set orcidTooltip when authenticatedTimestamp is not provided', () => { + component.authenticatedTimestamp = null; + component.ngOnInit(); + expect(component.orcidTooltip).toBe('person.orcid-tooltip.not-authenticated'); + }); + + it('should display the ORCID icon', () => { + const badgeIcon = fixture.debugElement.query(By.css('img[data-test="orcidIcon"]')); + expect(badgeIcon).toBeTruthy(); + }); + + it('should display the ORCID icon in greyscale if there is no authenticated timestamp', () => { + component.authenticatedTimestamp = null; + fixture.detectChanges(); + const badgeIcon = fixture.debugElement.query(By.css('img[data-test="orcidIcon"]')); + expect(badgeIcon.nativeElement.classList).toContain('not-authenticated'); + }); + +}); diff --git a/src/app/shared/orcid-badge-and-tooltip/orcid-badge-and-tooltip.component.ts b/src/app/shared/orcid-badge-and-tooltip/orcid-badge-and-tooltip.component.ts new file mode 100644 index 0000000000..1939bad57f --- /dev/null +++ b/src/app/shared/orcid-badge-and-tooltip/orcid-badge-and-tooltip.component.ts @@ -0,0 +1,56 @@ + + +import { + Component, + Input, + OnInit, +} from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; + +import { MetadataValue } from '../../core/shared/metadata.models'; + +/** + * Component to display an ORCID badge with a tooltip. + * The tooltip text changes based on whether the ORCID is authenticated. + */ +@Component({ + selector: 'ds-orcid-badge-and-tooltip', + templateUrl: './orcid-badge-and-tooltip.component.html', + styleUrls: ['./orcid-badge-and-tooltip.component.scss'], +}) +export class OrcidBadgeAndTooltipComponent implements OnInit { + + /** + * The ORCID value to be displayed. + */ + @Input() orcid: MetadataValue; + + /** + * The timestamp indicating when the ORCID was authenticated. + */ + @Input() authenticatedTimestamp: MetadataValue; + + /** + * The tooltip text to be displayed. + */ + orcidTooltip: string; + + /** + * Constructor to inject the TranslateService. + * @param translateService - Service for translation. + */ + constructor( + private translateService: TranslateService, + ) { } + + /** + * Initializes the component. + * Sets the tooltip text based on the presence of the authenticated timestamp. + */ + ngOnInit() { + this.orcidTooltip = this.authenticatedTimestamp ? + this.translateService.instant('person.orcid-tooltip.authenticated', { orcid: this.orcid.value }) : + this.translateService.instant('person.orcid-tooltip.not-authenticated', { orcid: this.orcid.value }); + } + +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 9f05b1d370..d6b6e861ce 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -284,6 +284,7 @@ import { BitstreamListItemComponent } from './object-list/bitstream-list-item/bi import { NgxPaginationModule } from 'ngx-pagination'; import { ThemedLangSwitchComponent } from './lang-switch/themed-lang-switch.component'; import {ThemedUserMenuComponent} from './auth-nav-menu/user-menu/themed-user-menu.component'; +import { OrcidBadgeAndTooltipComponent } from './orcid-badge-and-tooltip/orcid-badge-and-tooltip.component'; const MODULES = [ CommonModule, @@ -404,6 +405,7 @@ const COMPONENTS = [ EpersonSearchBoxComponent, GroupSearchBoxComponent, ThemedItemPageTitleFieldComponent, + OrcidBadgeAndTooltipComponent, ]; const ENTRY_COMPONENTS = [ diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 0a1804fa5c..3f85e8b687 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -5248,6 +5248,10 @@ "person.orcid.registry.auth": "ORCID Authorizations", + "person.orcid-tooltip.authenticated": "{{orcid}}", + + "person.orcid-tooltip.not-authenticated": "{{orcid}} (unconfirmed)", + "home.recent-submissions.head": "Recent Submissions", "listable-notification-object.default-message": "This object couldn't be retrieved", From 35946dcf7cefdaafb70a2ee5ee02a2b53c7e8817 Mon Sep 17 00:00:00 2001 From: Alan Orth Date: Wed, 25 Sep 2024 08:13:21 +0300 Subject: [PATCH 045/720] Update isbot dependency Note that the minimum supported Node.js version is now v18, as v16 is now end of life. --- package.json | 2 +- server.ts | 2 +- yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 86959196d0..78f0cc9887 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "filesize": "^6.1.0", "http-proxy-middleware": "^1.0.5", "http-terminator": "^3.2.0", - "isbot": "^3.6.10", + "isbot": "^5.1.17", "js-cookie": "2.2.1", "js-yaml": "^4.1.0", "json5": "^2.2.3", diff --git a/server.ts b/server.ts index 93f3e86876..23d29723c6 100644 --- a/server.ts +++ b/server.ts @@ -28,7 +28,7 @@ import * as expressStaticGzip from 'express-static-gzip'; /* eslint-enable import/no-namespace */ import axios from 'axios'; import LRU from 'lru-cache'; -import isbot from 'isbot'; +import { isbot } from 'isbot'; import { createCertificate } from 'pem'; import { createServer } from 'https'; import { json } from 'body-parser'; diff --git a/yarn.lock b/yarn.lock index efc65983c6..2a4fd40b5a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7188,10 +7188,10 @@ isbinaryfile@^4.0.8: resolved "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz" integrity sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw== -isbot@^3.6.10: - version "3.6.10" - resolved "https://registry.yarnpkg.com/isbot/-/isbot-3.6.10.tgz#7b66334e81794f0461794debb567975cf08eaf2b" - integrity sha512-+I+2998oyP4oW9+OTQD8TS1r9P6wv10yejukj+Ksj3+UR5pUhsZN3f8W7ysq0p1qxpOVNbl5mCuv0bCaF8y5iQ== +isbot@^5.1.17: + version "5.1.17" + resolved "https://registry.yarnpkg.com/isbot/-/isbot-5.1.17.tgz#ad7da5690a61bbb19056a069975c9a73182682a0" + integrity sha512-/wch8pRKZE+aoVhRX/hYPY1C7dMCeeMyhkQLNLNlYAbGQn9bkvMB8fOUXNnk5I0m4vDYbBJ9ciVtkr9zfBJ7qA== isexe@^2.0.0: version "2.0.0" From 29c1b510733a6081fb5bfa09a5c8f7c0c25de300 Mon Sep 17 00:00:00 2001 From: Alan Orth Date: Wed, 25 Sep 2024 08:34:04 +0300 Subject: [PATCH 046/720] .github/workflows/build.yml: use Node.js v18 and v20 Node.js v16 LTS is end of life since October, 2023. See: https://nodejs.org/en/about/previous-releases --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 50b260b59e..f6ffa5e004 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,7 +38,7 @@ jobs: strategy: # Create a matrix of Node versions to test against (in parallel) matrix: - node-version: [16.x, 18.x] + node-version: [18.x, 20.x] # Do NOT exit immediately if one matrix job fails fail-fast: false # These are the actual CI steps to perform per job From c1fa52ee64e7c50f38c0e671aa13757bc7a4c024 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Wed, 25 Sep 2024 09:50:33 +0200 Subject: [PATCH 047/720] 118220: Store messages with ID so clears can be targeted --- .../live-region/live-region.service.spec.ts | 34 +++++++++++++++- .../shared/live-region/live-region.service.ts | 39 ++++++++++++------- 2 files changed, 57 insertions(+), 16 deletions(-) diff --git a/src/app/shared/live-region/live-region.service.spec.ts b/src/app/shared/live-region/live-region.service.spec.ts index fe5e8b8d8c..858ef88313 100644 --- a/src/app/shared/live-region/live-region.service.spec.ts +++ b/src/app/shared/live-region/live-region.service.spec.ts @@ -1,12 +1,14 @@ import { LiveRegionService } from './live-region.service'; import { fakeAsync, tick, flush } from '@angular/core/testing'; +import { UUIDService } from '../../core/shared/uuid.service'; describe('liveRegionService', () => { let service: LiveRegionService; - beforeEach(() => { - service = new LiveRegionService(); + service = new LiveRegionService( + new UUIDService(), + ); }); describe('addMessage', () => { @@ -85,6 +87,34 @@ describe('liveRegionService', () => { expect(results[3]).toEqual([]); })); + it('should not pop messages added after clearing within timeOut period', fakeAsync(() => { + const results: string[][] = []; + + service.getMessages$().subscribe((messages) => { + results.push(messages); + }); + + expect(results.length).toEqual(1); + expect(results[0]).toEqual([]); + + service.addMessage('Message One'); + tick(10000); + service.clear(); + tick(15000); + service.addMessage('Message Two'); + + // Message Two should not be cleared after 5 more seconds + tick(5000); + + expect(results.length).toEqual(4); + expect(results[3]).toEqual(['Message Two']); + + // But should be cleared 30 seconds after it was added + tick(25000); + expect(results.length).toEqual(5); + expect(results[4]).toEqual([]); + })); + it('should respect configured timeOut', fakeAsync(() => { const results: string[][] = []; diff --git a/src/app/shared/live-region/live-region.service.ts b/src/app/shared/live-region/live-region.service.ts index 482d1ca1bb..d89d72a967 100644 --- a/src/app/shared/live-region/live-region.service.ts +++ b/src/app/shared/live-region/live-region.service.ts @@ -1,12 +1,18 @@ import { Injectable } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; import { environment } from '../../../environments/environment'; +import { UUIDService } from '../../core/shared/uuid.service'; @Injectable({ providedIn: 'root', }) export class LiveRegionService { + constructor( + protected uuidService: UUIDService, + ) { + } + /** * The duration after which the messages disappear in milliseconds * @protected @@ -14,10 +20,11 @@ export class LiveRegionService { protected messageTimeOutDurationMs: number = environment.liveRegion.messageTimeOutDurationMs; /** - * Array containing the messages that should be shown in the live region + * Array containing the messages that should be shown in the live region, + * together with a uuid, so they can be uniquely identified * @protected */ - protected messages: string[] = []; + protected messages: { message: string, uuid: string }[] = []; /** * BehaviorSubject emitting the array with messages every time the array updates @@ -34,27 +41,28 @@ export class LiveRegionService { /** * Returns a copy of the array with the current live region messages */ - getMessages() { - return [...this.messages]; + getMessages(): string[] { + return this.messages.map(messageObj => messageObj.message); } /** * Returns the BehaviorSubject emitting the array with messages every time the array updates */ - getMessages$() { + getMessages$(): BehaviorSubject { return this.messages$; } /** * Adds a message to the live-region messages array * @param message + * @return The uuid of the message */ - addMessage(message: string) { - this.messages.push(message); + addMessage(message: string): string { + const uuid = this.uuidService.generate(); + this.messages.push({ message, uuid }); + setTimeout(() => this.clearMessageByUUID(uuid), this.messageTimeOutDurationMs); this.emitCurrentMessages(); - - // Clear the message once the timeOut has passed - setTimeout(() => this.pop(), this.messageTimeOutDurationMs); + return uuid; } /** @@ -66,12 +74,15 @@ export class LiveRegionService { } /** - * Removes the longest living message from the array. + * Removes the message with the given UUID from the messages array + * @param uuid The uuid of the message to clear * @protected */ - protected pop() { - if (this.messages.length > 0) { - this.messages.shift(); + clearMessageByUUID(uuid: string) { + const index = this.messages.findIndex(messageObj => messageObj.uuid === uuid); + + if (index !== -1) { + this.messages.splice(index, 1); this.emitCurrentMessages(); } } From aed97c9dccf5460897f5c24ea9473fa642468c0c Mon Sep 17 00:00:00 2001 From: Sascha Szott Date: Wed, 26 Jun 2024 14:46:13 +0200 Subject: [PATCH 048/720] added missing German translations (500 error) (cherry picked from commit 5f6b6ef9849e27eaf87e5b8b0f94ccde271d8043) --- src/assets/i18n/de.json5 | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/assets/i18n/de.json5 b/src/assets/i18n/de.json5 index 8efda0390e..d1ac01fe2a 100644 --- a/src/assets/i18n/de.json5 +++ b/src/assets/i18n/de.json5 @@ -9,8 +9,6 @@ // "401.unauthorized": "unauthorized", "401.unauthorized": "unautorisiert", - - // "403.help": "You don't have permission to access this page. You can use the button below to get back to the home page.", "403.help": "Sie sind nicht berechtigt, auf diese Seite zuzugreifen. Über den Button unten auf der Seite gelangen Sie zurück zur Startseite.", @@ -20,8 +18,6 @@ // "403.forbidden": "forbidden", "403.forbidden": "verboten", - - // "404.help": "We can't find the page you're looking for. The page may have been moved or deleted. You can use the button below to get back to the home page. ", "404.help": "Die Seite konnte nicht gefunden werden. Eventuell wurde sie verschoben oder gelöscht. Über den Button unten auf der Seite gelangen Sie zurück zur Startseite.", @@ -31,6 +27,16 @@ // "404.page-not-found": "page not found", "404.page-not-found": "Seite nicht gefunden", + // "500.page-internal-server-error": "Service unavailable", + "500.page-internal-server-error": "Dienst nicht verfügbar", + + // "500.help": "The server is temporarily unable to service your request due to maintenance downtime or capacity problems. Please try again later.", + "500.help": "Der Dienst steht momentan nicht zur Verfügung. Bitte versuchen Sie es später noch einmal.", + + // "500.link.home-page": "Take me to the home page", + "500.link.home-page": "Zur Startseite", + + // "admin.access-control.epeople.breadcrumbs": "EPeople", "admin.access-control.epeople.breadcrumbs": "Personen suchen", From 48ec0cdc340ecf472688c6d606c3fb3643456663 Mon Sep 17 00:00:00 2001 From: Sascha Szott Date: Tue, 9 Jul 2024 17:42:49 +0200 Subject: [PATCH 049/720] minor change: added missing periods in German translations (cherry picked from commit e26ab86dd33981ea4a9969b6043449619948a2b8) --- src/assets/i18n/de.json5 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/assets/i18n/de.json5 b/src/assets/i18n/de.json5 index 8efda0390e..c154ae91c9 100644 --- a/src/assets/i18n/de.json5 +++ b/src/assets/i18n/de.json5 @@ -5143,10 +5143,10 @@ "submission.sections.general.deposit_error_notice": "Beim Einreichen des Items ist ein Fehler aufgetreten. Bitte versuchen Sie es später noch einmal.", // "submission.sections.general.deposit_success_notice": "Submission deposited successfully.", - "submission.sections.general.deposit_success_notice": "Veröffentlichung erfolgreich eingereicht", + "submission.sections.general.deposit_success_notice": "Veröffentlichung erfolgreich eingereicht.", // "submission.sections.general.discard_error_notice": "There was an issue when discarding the item, please try again later.", - "submission.sections.general.discard_error_notice": "Beim Verwerfen der Einreichung ist ein Fehler aufgetreten. Bitte versuchen Sie es später noch einmal", + "submission.sections.general.discard_error_notice": "Beim Verwerfen der Einreichung ist ein Fehler aufgetreten. Bitte versuchen Sie es später noch einmal.", // "submission.sections.general.discard_success_notice": "Submission discarded successfully.", "submission.sections.general.discard_success_notice": "Einreichung erfolgreich verworfen.", From edc738ae3964fada0c6628ce1732f047ae24a603 Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 23 Sep 2024 15:53:55 -0400 Subject: [PATCH 050/720] Update en.json5 - fixed typo for occurred (cherry picked from commit ed5ac47f886fb0e2ad7ef29c2152abcbe414cca1) --- src/assets/i18n/en.json5 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 0a1804fa5c..d00e16caa0 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1240,7 +1240,7 @@ "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.notifications.error": "An error occurred while editing the community", "community.edit.return": "Back", @@ -1448,7 +1448,7 @@ "curation.form.submit.error.head": "Running the curation task failed", - "curation.form.submit.error.content": "An error occured when trying to start the curation task.", + "curation.form.submit.error.content": "An error occurred when trying to start the curation task.", "curation.form.submit.error.invalid-handle": "Couldn't determine the handle for this object", @@ -1700,7 +1700,7 @@ "forgot-email.form.error.head": "Error when trying to reset password", - "forgot-email.form.error.content": "An error occured when attempting to reset the password for the account associated with the following email address: {{ email }}", + "forgot-email.form.error.content": "An error occurred when attempting to reset the password for the account associated with the following email address: {{ email }}", "forgot-password.title": "Forgot Password", @@ -3518,7 +3518,7 @@ "register-page.registration.error.head": "Error when trying to register email", - "register-page.registration.error.content": "An error occured when registering the following email address: {{ email }}", + "register-page.registration.error.content": "An error occurred when registering the following email address: {{ email }}", "register-page.registration.error.recaptcha": "Error when trying to authenticate with recaptcha", From fe7d2a8a7e05bcdb3b63943261ab84cfdd9614d5 Mon Sep 17 00:00:00 2001 From: Elvi Nemiz Date: Sat, 29 Jun 2024 08:14:10 +0800 Subject: [PATCH 051/720] Fix to Mobile navbar hamburger menu for base (custom) theme https://github.com/DSpace/dspace-angular/pull/2444 only fixes the DSpace theme, not the base (and custom) theme. (cherry picked from commit a3b6aef66a81e93f537bf35647be97d14f5b1472) --- src/app/navbar/navbar.component.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/navbar/navbar.component.scss b/src/app/navbar/navbar.component.scss index b74408f0f5..bbf5feec15 100644 --- a/src/app/navbar/navbar.component.scss +++ b/src/app/navbar/navbar.component.scss @@ -10,7 +10,7 @@ /** Mobile menu styling **/ @media screen and (max-width: map-get($grid-breakpoints, md)-0.02) { .navbar { - width: 100%; + width: 100vw; background-color: var(--bs-white); position: absolute; overflow: hidden; From 751d689ff65b15fa4f8b2f898d43ecaa682e8085 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Fri, 27 Sep 2024 10:02:26 +0200 Subject: [PATCH 052/720] 118220: Add additional TypeDocs --- src/app/shared/live-region/live-region.component.ts | 6 ++++++ src/app/shared/live-region/live-region.service.ts | 5 ++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/app/shared/live-region/live-region.component.ts b/src/app/shared/live-region/live-region.component.ts index d7bd5eb806..8a25c4657c 100644 --- a/src/app/shared/live-region/live-region.component.ts +++ b/src/app/shared/live-region/live-region.component.ts @@ -2,6 +2,12 @@ import { Component, OnInit } from '@angular/core'; import { LiveRegionService } from './live-region.service'; import { Observable } from 'rxjs'; +/** + * The Live Region Component is an accessibility tool for screenreaders. When a change occurs on a page when the changed + * section is not in focus, a message should be displayed by this component so it can be announced by a screen reader. + * + * This component should not be used directly. Use the {@link LiveRegionService} to add messages. + */ @Component({ selector: `ds-live-region`, templateUrl: './live-region.component.html', diff --git a/src/app/shared/live-region/live-region.service.ts b/src/app/shared/live-region/live-region.service.ts index d89d72a967..72940c1a0e 100644 --- a/src/app/shared/live-region/live-region.service.ts +++ b/src/app/shared/live-region/live-region.service.ts @@ -3,6 +3,10 @@ import { BehaviorSubject } from 'rxjs'; import { environment } from '../../../environments/environment'; import { UUIDService } from '../../core/shared/uuid.service'; +/** + * The LiveRegionService is responsible for handling the messages that are shown by the {@link LiveRegionComponent}. + * Use this service to add or remove messages to the Live Region. + */ @Injectable({ providedIn: 'root', }) @@ -76,7 +80,6 @@ export class LiveRegionService { /** * Removes the message with the given UUID from the messages array * @param uuid The uuid of the message to clear - * @protected */ clearMessageByUUID(uuid: string) { const index = this.messages.findIndex(messageObj => messageObj.uuid === uuid); From cf54af2c22a8344d67573d7185401ade04cf5588 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Tue, 3 Sep 2024 13:43:02 +0200 Subject: [PATCH 053/720] 117803: Refactor Item Edit Bitstreams page to use HTML Table elements --- .../item-bitstreams.component.html | 11 +- .../item-bitstreams.component.scss | 20 +- .../item-edit-bitstream-bundle.component.html | 109 +++++++-- .../item-edit-bitstream-bundle.component.scss | 20 ++ .../item-edit-bitstream-bundle.component.ts | 216 +++++++++++++++++- ...-drag-and-drop-bitstream-list.component.ts | 4 + src/assets/i18n/en.json5 | 2 + 7 files changed, 336 insertions(+), 46 deletions(-) create mode 100644 src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html index 4cb9577fcb..70f6ca55e8 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html @@ -23,16 +23,7 @@ -
-
-
- - {{'item.edit.bitstreams.headers.name' | translate}} -
-
{{'item.edit.bitstreams.headers.description' | translate}}
-
{{'item.edit.bitstreams.headers.format' | translate}}
-
{{'item.edit.bitstreams.headers.actions' | translate}}
-
+
-
-
- -
- {{'item.edit.bitstreams.bundle.name' | translate:{ name: dsoNameService.getName(bundle) } }} -
-
-
-
- -
-
-
- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ {{'item.edit.bitstreams.headers.name' | translate}} + + {{'item.edit.bitstreams.headers.description' | translate}} + + {{'item.edit.bitstreams.headers.format' | translate}} + + {{'item.edit.bitstreams.headers.actions' | translate}} +
+ {{'item.edit.bitstreams.bundle.name' | translate:{ name: bundleName } }} + + +
+ {{ entry.name }} + + {{ entry.description }} + + {{ (entry.format | async)?.shortDescription }} + +
+
+ + + + + + +
+
+
+ +
+
+ diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss new file mode 100644 index 0000000000..d344b1657a --- /dev/null +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss @@ -0,0 +1,20 @@ +.header-row { + color: var(--bs-table-dark-color); + background-color: var(--bs-table-dark-bg); + border-color: var(--bs-table-dark-border-color); +} + +.bundle-row { + color: var(--bs-table-head-color); + background-color: var(--bs-table-head-bg); + border-color: var(--bs-table-border-color); +} + +.row-element { + padding: 0.75em; + border-bottom: var(--bs-table-border-width) solid var(--bs-table-border-color); +} + +.bitstream-name { + font-weight: normal; +} diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts index 70f4b63217..ad009e7cb7 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts @@ -5,10 +5,65 @@ import { ResponsiveColumnSizes } from '../../../../shared/responsive-table-sizes import { ResponsiveTableSizes } from '../../../../shared/responsive-table-sizes/responsive-table-sizes'; import { getItemPageRoute } from '../../../item-page-routing-paths'; import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; +import { RemoteData } from 'src/app/core/data/remote-data'; +import { PaginatedList } from 'src/app/core/data/paginated-list.model'; +import { Bitstream } from 'src/app/core/shared/bitstream.model'; +import { Observable, BehaviorSubject, switchMap } from 'rxjs'; +import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; +import { FieldUpdates } from '../../../../core/data/object-updates/field-updates.model'; +import { PaginatedSearchOptions } from '../../../../shared/search/models/paginated-search-options.model'; +import { BundleDataService } from '../../../../core/data/bundle-data.service'; +import { followLink } from '../../../../shared/utils/follow-link-config.model'; +import { + getAllSucceededRemoteData, + paginatedListToArray, + getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteData +} from '../../../../core/shared/operators'; +import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; +import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model'; +import { map } from 'rxjs/operators'; +import { getBitstreamDownloadRoute } from '../../../../app-routing-paths'; +import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model'; +import { FieldUpdate } from '../../../../core/data/object-updates/field-update.model'; +import { PaginationService } from '../../../../core/pagination/pagination.service'; + +/** + * Interface storing all the information necessary to create a row in the bitstream edit table + */ +export interface BitstreamTableEntry { + /** + * The bitstream + */ + bitstream: Bitstream, + /** + * The uuid of the Bitstream + */ + id: string, + /** + * The name of the Bitstream + */ + name: string, + /** + * The name of the Bitstream with all whitespace removed + */ + nameStripped: string, + /** + * The description of the Bitstream + */ + description: string, + /** + * Observable emitting the Format of the Bitstream + */ + format: Observable, + /** + * The download url of the Bitstream + */ + downloadUrl: string, +} @Component({ selector: 'ds-item-edit-bitstream-bundle', - styleUrls: ['../item-bitstreams.component.scss'], + styleUrls: ['../item-bitstreams.component.scss', './item-edit-bitstream-bundle.component.scss'], templateUrl: './item-edit-bitstream-bundle.component.html', }) /** @@ -17,6 +72,7 @@ import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; * (which means it'll be added to the parents html without a wrapping ds-item-edit-bitstream-bundle element) */ export class ItemEditBitstreamBundleComponent implements OnInit { + protected readonly FieldChangeType = FieldChangeType; /** * The view on the bundle information and bitstreams @@ -56,9 +112,48 @@ export class ItemEditBitstreamBundleComponent implements OnInit { */ itemPageRoute: string; + /** + * The name of the bundle + */ + bundleName: string; + + /** + * The bitstreams to show in the table + */ + bitstreamsRD$: Observable>>; + + /** + * The data to show in the table + */ + tableEntries$: Observable; + + /** + * The initial page options to use for fetching the bitstreams + */ + paginationOptions: PaginationComponentOptions; + + /** + * The current page options + */ + currentPaginationOptions$: BehaviorSubject; + + /** + * The self url of the bundle, also used when retrieving fieldUpdates + */ + bundleUrl: string; + + /** + * The updates to the current bitstreams + */ + updates$: Observable; + + constructor( protected viewContainerRef: ViewContainerRef, public dsoNameService: DSONameService, + protected bundleService: BundleDataService, + protected objectUpdatesService: ObjectUpdatesService, + protected paginationService: PaginationService, ) { } @@ -66,5 +161,124 @@ export class ItemEditBitstreamBundleComponent implements OnInit { this.bundleNameColumn = this.columnSizes.combineColumns(0, 2); this.viewContainerRef.createEmbeddedView(this.bundleView); this.itemPageRoute = getItemPageRoute(this.item); + this.bundleName = this.dsoNameService.getName(this.bundle); + this.bundleUrl = this.bundle.self; + + this.initializePagination(); + this.initializeBitstreams(); + + // this.bitstreamsRD = this. + } + + protected initializePagination() { + this.paginationOptions = Object.assign(new PaginationComponentOptions(),{ + id: this.bundleName, // This might behave unexpectedly if the item contains two bundles with the same name + currentPage: 1, + pageSize: 10 + }); + + this.currentPaginationOptions$ = new BehaviorSubject(this.paginationOptions); + + this.paginationService.getCurrentPagination(this.paginationOptions.id, this.paginationOptions) + .subscribe((pagination) => { + this.currentPaginationOptions$.next(pagination); + }); + } + + protected initializeBitstreams() { + this.bitstreamsRD$ = this.currentPaginationOptions$.pipe( + switchMap((page: PaginationComponentOptions) => { + const paginatedOptions = new PaginatedSearchOptions({ pagination: Object.assign({}, page) }); + return this.bundleService.getBitstreams(this.bundle.id, paginatedOptions, followLink('format')); + }), + ); + + this.bitstreamsRD$.pipe( + getFirstSucceededRemoteData(), + paginatedListToArray(), + ).subscribe((bitstreams) => { + this.objectUpdatesService.initialize(this.bundleUrl, bitstreams, new Date()); + }); + + this.updates$ = this.bitstreamsRD$.pipe( + getAllSucceededRemoteData(), + paginatedListToArray(), + switchMap((bitstreams) => this.objectUpdatesService.getFieldUpdatesExclusive(this.bundleUrl, bitstreams)) + ); + + this.tableEntries$ = this.bitstreamsRD$.pipe( + getAllSucceededRemoteData(), + paginatedListToArray(), + map((bitstreams) => { + return bitstreams.map((bitstream) => { + const name = this.dsoNameService.getName(bitstream); + + return { + bitstream: bitstream, + id: bitstream.uuid, + name: name, + nameStripped: this.stripWhiteSpace(name), + description: bitstream.firstMetadataValue('dc.description'), + format: bitstream.format.pipe(getFirstSucceededRemoteDataPayload()), + downloadUrl: getBitstreamDownloadRoute(bitstream), + }; + }); + }), + ); + } + + /** + * Check if a user should be allowed to remove this field + */ + canRemove(fieldUpdate: FieldUpdate): boolean { + return fieldUpdate.changeType !== FieldChangeType.REMOVE; + } + + /** + * Check if a user should be allowed to cancel the update to this field + */ + canUndo(fieldUpdate: FieldUpdate): boolean { + return fieldUpdate.changeType >= 0; + } + + /** + * Sends a new remove update for this field to the object updates service + */ + remove(bitstream: Bitstream): void { + this.objectUpdatesService.saveRemoveFieldUpdate(this.bundleUrl, bitstream); + } + + /** + * Cancels the current update for this field in the object updates service + */ + undo(bitstream: Bitstream): void { + this.objectUpdatesService.removeSingleFieldUpdate(this.bundleUrl, bitstream.uuid); + } + + getRowClass(update: FieldUpdate): string { + switch (update.changeType) { + case FieldChangeType.UPDATE: + return 'table-warning'; + case FieldChangeType.ADD: + return 'table-success'; + case FieldChangeType.REMOVE: + return 'table-danger'; + default: + return 'bg-white'; + } + } + + /** + * Returns a string equal to the input string with all whitespace removed. + * @param str + */ + // Whitespace is stripped from the Bitstream names for accessibility reasons. + // To make it clear which headers are relevant for a specific field in the table, the 'headers' attribute is used to + // refer to specific headers. The Bitstream's name is used as header ID for the row containing information regarding + // that bitstream. As the 'headers' attribute contains a space-separated string of header IDs, the Bitstream's header + // ID can not contain strings itself. + stripWhiteSpace(str: string): string { + // '/\s+/g' matches all occurrences of any amount of whitespace characters + return str.replace(/\s+/g, ''); } } diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.ts index 2c81a4e2cb..d5bb9eceea 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.ts @@ -24,6 +24,10 @@ import { PaginationComponentOptions } from '../../../../../shared/pagination/pag * bitstreams within the paginated list. To drag and drop a bitstream between two pages, drag the row on top of the * page number you want the bitstream to end up at. Doing so will add the bitstream to the top of that page. */ +// NOTE: +// This component was used by the item-edit-bitstream-bundle.component, but this is no longer the case. It is left here +// as a reference for the drag-and-drop functionality. This component (and the abstract version it extends) should be +// removed once this reference is no longer useful. export class PaginatedDragAndDropBitstreamListComponent extends AbstractPaginatedDragAndDropListComponent implements OnInit { /** * The bundle to display bitstreams for diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 6c91bae4c1..4d5ef9ee1b 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1934,6 +1934,8 @@ "item.edit.bitstreams.bundle.name": "BUNDLE: {{ name }}", + "item.edit.bitstreams.bundle.table.aria-label": "Bitstreams in the {{ bundle }} Bundle", + "item.edit.bitstreams.discard-button": "Discard", "item.edit.bitstreams.edit.buttons.download": "Download", From a11bfc80ad5b087aab9c6e283a08f0db2f87d5ed Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Tue, 3 Sep 2024 14:24:31 +0200 Subject: [PATCH 054/720] 117803: Hide table headers for subsequent bundle tables --- .../item-bitstreams/item-bitstreams.component.html | 3 ++- .../item-edit-bitstream-bundle.component.html | 14 +++++++++----- .../item-edit-bitstream-bundle.component.ts | 5 +++++ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html index 70f6ca55e8..f22dbe6a0e 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html @@ -24,10 +24,11 @@
-
diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html index c11035e305..a95a7921a4 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html @@ -10,7 +10,7 @@ - + - - - - diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss index d344b1657a..725d329936 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss @@ -18,3 +18,12 @@ .bitstream-name { font-weight: normal; } + +.pagination-control-container { + display: flex; +} + +.pagination-control { + padding: 0 0.1rem; + vertical-align: center; +} diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts index ae6bc0876c..2a84c00c02 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts @@ -26,6 +26,8 @@ import { getBitstreamDownloadRoute } from '../../../../app-routing-paths'; import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model'; import { FieldUpdate } from '../../../../core/data/object-updates/field-update.model'; import { PaginationService } from '../../../../core/pagination/pagination.service'; +import { SortDirection } from '../../../../core/cache/models/sort-options.model'; +import { PaginationComponent } from '../../../../shared/pagination/pagination.component'; /** * Interface storing all the information necessary to create a row in the bitstream edit table @@ -79,6 +81,8 @@ export class ItemEditBitstreamBundleComponent implements OnInit { */ @ViewChild('bundleView', {static: true}) bundleView; + @ViewChild(PaginationComponent) paginationComponent: PaginationComponent; + /** * The bundle to display bitstreams for */ @@ -142,6 +146,16 @@ export class ItemEditBitstreamBundleComponent implements OnInit { */ currentPaginationOptions$: BehaviorSubject; + /** + * The available page size options + */ + pageSizeOptions: number[]; + + /** + * The currently selected page size + */ + pageSize$: BehaviorSubject; + /** * The self url of the bundle, also used when retrieving fieldUpdates */ @@ -182,11 +196,15 @@ export class ItemEditBitstreamBundleComponent implements OnInit { pageSize: 10 }); + this.pageSizeOptions = this.paginationOptions.pageSizeOptions; + this.currentPaginationOptions$ = new BehaviorSubject(this.paginationOptions); + this.pageSize$ = new BehaviorSubject(this.paginationOptions.pageSize); this.paginationService.getCurrentPagination(this.paginationOptions.id, this.paginationOptions) .subscribe((pagination) => { this.currentPaginationOptions$.next(pagination); + this.pageSize$.next(pagination.pageSize); }); } @@ -286,4 +304,9 @@ export class ItemEditBitstreamBundleComponent implements OnInit { // '/\s+/g' matches all occurrences of any amount of whitespace characters return str.replace(/\s+/g, ''); } + + public doPageSizeChange(pageSize: number) { + this.paginationComponent.doPageSizeChange(pageSize); + } + } From d85124c121db1ef88f3015cfdc333bfe1b8c9ce3 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Wed, 4 Sep 2024 14:32:29 +0200 Subject: [PATCH 056/720] 117803: Fix deleted bitstreams not being removed from list --- .../item-edit-bitstream-bundle.component.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts index 2a84c00c02..6ef15397b0 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts @@ -26,8 +26,8 @@ import { getBitstreamDownloadRoute } from '../../../../app-routing-paths'; import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model'; import { FieldUpdate } from '../../../../core/data/object-updates/field-update.model'; import { PaginationService } from '../../../../core/pagination/pagination.service'; -import { SortDirection } from '../../../../core/cache/models/sort-options.model'; import { PaginationComponent } from '../../../../shared/pagination/pagination.component'; +import { RequestService } from '../../../../core/data/request.service'; /** * Interface storing all the information necessary to create a row in the bitstream edit table @@ -173,6 +173,7 @@ export class ItemEditBitstreamBundleComponent implements OnInit { protected bundleService: BundleDataService, protected objectUpdatesService: ObjectUpdatesService, protected paginationService: PaginationService, + protected requestService: RequestService, ) { } @@ -212,7 +213,14 @@ export class ItemEditBitstreamBundleComponent implements OnInit { this.bitstreamsRD$ = this.currentPaginationOptions$.pipe( switchMap((page: PaginationComponentOptions) => { const paginatedOptions = new PaginatedSearchOptions({ pagination: Object.assign({}, page) }); - return this.bundleService.getBitstreams(this.bundle.id, paginatedOptions, followLink('format')); + return this.bundleService.getBitstreamsEndpoint(this.bundle.id, paginatedOptions).pipe( + switchMap((href) => this.requestService.hasByHref$(href)), + switchMap(() => this.bundleService.getBitstreams( + this.bundle.id, + paginatedOptions, + followLink('format') + )) + ); }), ); From 374a9ae14eb036c1bf91eec2891aa722722545ed Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Wed, 4 Sep 2024 15:46:04 +0200 Subject: [PATCH 057/720] 117803: Add item-bitstream service --- .../item-bitstreams.component.ts | 82 ++------- .../item-bitstreams.service.ts | 158 ++++++++++++++++++ .../item-edit-bitstream-bundle.component.ts | 41 +---- 3 files changed, 176 insertions(+), 105 deletions(-) create mode 100644 src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts index ee53bd919c..9f27cf11b3 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts @@ -8,23 +8,19 @@ import { ActivatedRoute, Router } from '@angular/router'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; import { BitstreamDataService } from '../../../core/data/bitstream-data.service'; -import { hasValue, isNotEmpty } from '../../../shared/empty.util'; +import { hasValue } from '../../../shared/empty.util'; import { ObjectCacheService } from '../../../core/cache/object-cache.service'; import { RequestService } from '../../../core/data/request.service'; import { getFirstSucceededRemoteData, getRemoteDataPayload } from '../../../core/shared/operators'; import { RemoteData } from '../../../core/data/remote-data'; import { PaginatedList } from '../../../core/data/paginated-list.model'; import { Bundle } from '../../../core/shared/bundle.model'; -import { Bitstream } from '../../../core/shared/bitstream.model'; import { BundleDataService } from '../../../core/data/bundle-data.service'; import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-search-options.model'; -import { ResponsiveColumnSizes } from '../../../shared/responsive-table-sizes/responsive-column-sizes'; import { ResponsiveTableSizes } from '../../../shared/responsive-table-sizes/responsive-table-sizes'; import { NoContent } from '../../../core/shared/NoContent.model'; import { Operation } from 'fast-json-patch'; -import { FieldUpdate } from '../../../core/data/object-updates/field-update.model'; -import { FieldUpdates } from '../../../core/data/object-updates/field-updates.model'; -import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model'; +import { ItemBitstreamsService } from './item-bitstreams.service'; @Component({ selector: 'ds-item-bitstreams', @@ -41,28 +37,10 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme */ bundles$: Observable; - /** - * The page options to use for fetching the bundles - */ - bundlesOptions = { - id: 'bundles-pagination-options', - currentPage: 1, - pageSize: 9999 - } as any; - /** * The bootstrap sizes used for the columns within this table */ - columnSizes = new ResponsiveTableSizes([ - // Name column - new ResponsiveColumnSizes(2, 2, 3, 4, 4), - // Description column - new ResponsiveColumnSizes(2, 3, 3, 3, 3), - // Format column - new ResponsiveColumnSizes(2, 2, 2, 2, 2), - // Actions column - new ResponsiveColumnSizes(6, 5, 4, 3, 3) - ]); + columnSizes: ResponsiveTableSizes; /** * Are we currently submitting the changes? @@ -88,16 +66,21 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme public requestService: RequestService, public cdRef: ChangeDetectorRef, public bundleService: BundleDataService, - public zone: NgZone + public zone: NgZone, + public itemBitstreamsService: ItemBitstreamsService, ) { super(itemService, objectUpdatesService, router, notificationsService, translateService, route); + + this.columnSizes = this.itemBitstreamsService.getColumnSizes(); } /** * Actions to perform after the item has been initialized */ postItemInit(): void { - this.bundles$ = this.itemService.getBundles(this.item.id, new PaginatedSearchOptions({pagination: this.bundlesOptions})).pipe( + const bundlesOptions = this.itemBitstreamsService.getInitialBundlesPaginationOptions(); + + this. bundles$ = this.itemService.getBundles(this.item.id, new PaginatedSearchOptions({pagination: bundlesOptions})).pipe( getFirstSucceededRemoteData(), getRemoteDataPayload(), map((bundlePage: PaginatedList) => bundlePage.page) @@ -119,30 +102,12 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme */ submit() { this.submitting = true; - const bundlesOnce$ = this.bundles$.pipe(take(1)); - - // Fetch all removed bitstreams from the object update service - const removedBitstreams$ = bundlesOnce$.pipe( - switchMap((bundles: Bundle[]) => observableZip( - ...bundles.map((bundle: Bundle) => this.objectUpdatesService.getFieldUpdates(bundle.self, [], true)) - )), - map((fieldUpdates: FieldUpdates[]) => ([] as FieldUpdate[]).concat( - ...fieldUpdates.map((updates: FieldUpdates) => Object.values(updates).filter((fieldUpdate: FieldUpdate) => fieldUpdate.changeType === FieldChangeType.REMOVE)) - )), - map((fieldUpdates: FieldUpdate[]) => fieldUpdates.map((fieldUpdate: FieldUpdate) => fieldUpdate.field)) - ); - // Send out delete requests for all deleted bitstreams - const removedResponses$: Observable> = removedBitstreams$.pipe( - take(1), - switchMap((removedBitstreams: Bitstream[]) => { - return this.bitstreamService.removeMultiple(removedBitstreams); - }) - ); + const removedResponses$ = this.itemBitstreamsService.removeMarkedBitstreams(this.bundles$); // Perform the setup actions from above in order and display notifications removedResponses$.subscribe((responses: RemoteData) => { - this.displayNotifications('item.edit.bitstreams.notifications.remove', [responses]); + this.itemBitstreamsService.displayNotifications('item.edit.bitstreams.notifications.remove', [responses]); this.submitting = false; }); } @@ -164,7 +129,7 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme } as Operation; this.bundleService.patch(bundle, [moveOperation]).pipe(take(1)).subscribe((response: RemoteData) => { this.zone.run(() => { - this.displayNotifications('item.edit.bitstreams.notifications.move', [response]); + this.itemBitstreamsService.displayNotifications('item.edit.bitstreams.notifications.move', [response]); // Remove all cached requests from this bundle and call the event's callback when the requests are cleared this.requestService.removeByHrefSubstring(bundle.self).pipe( filter((isCached) => isCached), @@ -176,27 +141,6 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme }); } - /** - * Display notifications - * - Error notification for each failed response with their message - * - Success notification in case there's at least one successful response - * @param key The i18n key for the notification messages - * @param responses The returned responses to display notifications for - */ - displayNotifications(key: string, responses: RemoteData[]) { - if (isNotEmpty(responses)) { - const failedResponses = responses.filter((response: RemoteData) => hasValue(response) && response.hasFailed); - const successfulResponses = responses.filter((response: RemoteData) => hasValue(response) && response.hasSucceeded); - - failedResponses.forEach((response: RemoteData) => { - this.notificationsService.error(this.translateService.instant(`${key}.failed.title`), response.errorMessage); - }); - if (successfulResponses.length > 0) { - this.notificationsService.success(this.translateService.instant(`${key}.saved.title`), this.translateService.instant(`${key}.saved.content`)); - } - } - } - /** * Request the object updates service to discard all current changes to this item * Shows a notification to remind the user that they can undo this diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts new file mode 100644 index 0000000000..487df77b28 --- /dev/null +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts @@ -0,0 +1,158 @@ +import { Injectable } from '@angular/core'; +import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; +import { ResponsiveTableSizes } from '../../../shared/responsive-table-sizes/responsive-table-sizes'; +import { ResponsiveColumnSizes } from '../../../shared/responsive-table-sizes/responsive-column-sizes'; +import { RemoteData } from '../../../core/data/remote-data'; +import { isNotEmpty, hasValue } from '../../../shared/empty.util'; +import { Bundle } from '../../../core/shared/bundle.model'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; +import { Observable, zip as observableZip } from 'rxjs'; +import { NoContent } from '../../../core/shared/NoContent.model'; +import { take, switchMap, map } from 'rxjs/operators'; +import { FieldUpdates } from '../../../core/data/object-updates/field-updates.model'; +import { FieldUpdate } from '../../../core/data/object-updates/field-update.model'; +import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model'; +import { Bitstream } from '../../../core/shared/bitstream.model'; +import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; +import { BitstreamDataService } from '../../../core/data/bitstream-data.service'; +import { BitstreamTableEntry } from './item-edit-bitstream-bundle/item-edit-bitstream-bundle.component'; +import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; +import { getBitstreamDownloadRoute } from '../../../app-routing-paths'; +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; + +@Injectable( + { providedIn: 'root' }, +) +export class ItemBitstreamsService { + + constructor( + protected notificationsService: NotificationsService, + protected translateService: TranslateService, + protected objectUpdatesService: ObjectUpdatesService, + protected bitstreamService: BitstreamDataService, + protected dsoNameService: DSONameService, + ) { + } + + /** + * Returns the pagination options to use when fetching the bundles + */ + getInitialBundlesPaginationOptions(): PaginationComponentOptions { + return Object.assign(new PaginationComponentOptions(), { + id: 'bundles-pagination-options', + currentPage: 1, + pageSize: 9999 + }); + } + + getInitialBitstreamsPaginationOptions(bundleName: string): PaginationComponentOptions { + return Object.assign(new PaginationComponentOptions(),{ + id: bundleName, // This might behave unexpectedly if the item contains two bundles with the same name + currentPage: 1, + pageSize: 10 + }); + } + + /** + * Returns the {@link ResponsiveTableSizes} for use in the columns of the edit bitstreams table + */ + getColumnSizes(): ResponsiveTableSizes { + return new ResponsiveTableSizes([ + // Name column + new ResponsiveColumnSizes(2, 2, 3, 4, 4), + // Description column + new ResponsiveColumnSizes(2, 3, 3, 3, 3), + // Format column + new ResponsiveColumnSizes(2, 2, 2, 2, 2), + // Actions column + new ResponsiveColumnSizes(6, 5, 4, 3, 3) + ]); + } + + /** + * Display notifications + * - Error notification for each failed response with their message + * - Success notification in case there's at least one successful response + * @param key The i18n key for the notification messages + * @param responses The returned responses to display notifications for + */ + displayNotifications(key: string, responses: RemoteData[]) { + if (isNotEmpty(responses)) { + const failedResponses = responses.filter((response: RemoteData) => hasValue(response) && response.hasFailed); + const successfulResponses = responses.filter((response: RemoteData) => hasValue(response) && response.hasSucceeded); + + failedResponses.forEach((response: RemoteData) => { + this.notificationsService.error(this.translateService.instant(`${key}.failed.title`), response.errorMessage); + }); + if (successfulResponses.length > 0) { + this.notificationsService.success(this.translateService.instant(`${key}.saved.title`), this.translateService.instant(`${key}.saved.content`)); + } + } + } + + /** + * Removes the bitstreams marked for deletion from the Bundles emitted by the provided observable. + * @param bundles$ + */ + removeMarkedBitstreams(bundles$: Observable): Observable> { + const bundlesOnce$ = bundles$.pipe(take(1)); + + // Fetch all removed bitstreams from the object update service + const removedBitstreams$ = bundlesOnce$.pipe( + switchMap((bundles: Bundle[]) => observableZip( + ...bundles.map((bundle: Bundle) => this.objectUpdatesService.getFieldUpdates(bundle.self, [], true)) + )), + map((fieldUpdates: FieldUpdates[]) => ([] as FieldUpdate[]).concat( + ...fieldUpdates.map((updates: FieldUpdates) => Object.values(updates).filter((fieldUpdate: FieldUpdate) => fieldUpdate.changeType === FieldChangeType.REMOVE)) + )), + map((fieldUpdates: FieldUpdate[]) => fieldUpdates.map((fieldUpdate: FieldUpdate) => fieldUpdate.field)) + ); + + // Send out delete requests for all deleted bitstreams + return removedBitstreams$.pipe( + take(1), + switchMap((removedBitstreams: Bitstream[]) => { + return this.bitstreamService.removeMultiple(removedBitstreams); + }) + ); + } + + mapBitstreamsToTableEntries(bitstreams: Bitstream[]): BitstreamTableEntry[] { + return bitstreams.map((bitstream) => { + const name = this.dsoNameService.getName(bitstream); + + return { + bitstream: bitstream, + id: bitstream.uuid, + name: name, + nameStripped: this.nameToHeader(name), + description: bitstream.firstMetadataValue('dc.description'), + format: bitstream.format.pipe(getFirstSucceededRemoteDataPayload()), + downloadUrl: getBitstreamDownloadRoute(bitstream), + }; + }); + } + + /** + * Returns a string appropriate to be used as header ID + * @param name + */ + nameToHeader(name: string): string { + // Whitespace is stripped from the Bitstream names for accessibility reasons. + // To make it clear which headers are relevant for a specific field in the table, the 'headers' attribute is used to + // refer to specific headers. The Bitstream's name is used as header ID for the row containing information regarding + // that bitstream. As the 'headers' attribute contains a space-separated string of header IDs, the Bitstream's header + // ID can not contain strings itself. + return this.stripWhiteSpace(name); + } + + /** + * Returns a string equal to the input string with all whitespace removed. + * @param str + */ + stripWhiteSpace(str: string): string { + // '/\s+/g' matches all occurrences of any amount of whitespace characters + return str.replace(/\s+/g, ''); + } +} diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts index 6ef15397b0..9c62fe06e7 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts @@ -17,17 +17,17 @@ import { followLink } from '../../../../shared/utils/follow-link-config.model'; import { getAllSucceededRemoteData, paginatedListToArray, - getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteData + getFirstSucceededRemoteData } from '../../../../core/shared/operators'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model'; import { map } from 'rxjs/operators'; -import { getBitstreamDownloadRoute } from '../../../../app-routing-paths'; import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model'; import { FieldUpdate } from '../../../../core/data/object-updates/field-update.model'; import { PaginationService } from '../../../../core/pagination/pagination.service'; import { PaginationComponent } from '../../../../shared/pagination/pagination.component'; import { RequestService } from '../../../../core/data/request.service'; +import { ItemBitstreamsService } from '../item-bitstreams.service'; /** * Interface storing all the information necessary to create a row in the bitstream edit table @@ -174,6 +174,7 @@ export class ItemEditBitstreamBundleComponent implements OnInit { protected objectUpdatesService: ObjectUpdatesService, protected paginationService: PaginationService, protected requestService: RequestService, + protected itemBitstreamsService: ItemBitstreamsService, ) { } @@ -191,11 +192,7 @@ export class ItemEditBitstreamBundleComponent implements OnInit { } protected initializePagination() { - this.paginationOptions = Object.assign(new PaginationComponentOptions(),{ - id: this.bundleName, // This might behave unexpectedly if the item contains two bundles with the same name - currentPage: 1, - pageSize: 10 - }); + this.paginationOptions = this.itemBitstreamsService.getInitialBitstreamsPaginationOptions(this.bundleName); this.pageSizeOptions = this.paginationOptions.pageSizeOptions; @@ -240,21 +237,7 @@ export class ItemEditBitstreamBundleComponent implements OnInit { this.tableEntries$ = this.bitstreamsRD$.pipe( getAllSucceededRemoteData(), paginatedListToArray(), - map((bitstreams) => { - return bitstreams.map((bitstream) => { - const name = this.dsoNameService.getName(bitstream); - - return { - bitstream: bitstream, - id: bitstream.uuid, - name: name, - nameStripped: this.stripWhiteSpace(name), - description: bitstream.firstMetadataValue('dc.description'), - format: bitstream.format.pipe(getFirstSucceededRemoteDataPayload()), - downloadUrl: getBitstreamDownloadRoute(bitstream), - }; - }); - }), + map((bitstreams) => this.itemBitstreamsService.mapBitstreamsToTableEntries(bitstreams)), ); } @@ -299,20 +282,6 @@ export class ItemEditBitstreamBundleComponent implements OnInit { } } - /** - * Returns a string equal to the input string with all whitespace removed. - * @param str - */ - // Whitespace is stripped from the Bitstream names for accessibility reasons. - // To make it clear which headers are relevant for a specific field in the table, the 'headers' attribute is used to - // refer to specific headers. The Bitstream's name is used as header ID for the row containing information regarding - // that bitstream. As the 'headers' attribute contains a space-separated string of header IDs, the Bitstream's header - // ID can not contain strings itself. - stripWhiteSpace(str: string): string { - // '/\s+/g' matches all occurrences of any amount of whitespace characters - return str.replace(/\s+/g, ''); - } - public doPageSizeChange(pageSize: number) { this.paginationComponent.doPageSizeChange(pageSize); } From 3f4bf7ce0fdf73494bcb28045ef6d7cb8a4634dd Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Wed, 4 Sep 2024 16:32:30 +0200 Subject: [PATCH 058/720] 117803: Fix existing tests --- ...em-edit-bitstream-bundle.component.spec.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts index c26f99eb8f..1502ad2311 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts @@ -6,6 +6,15 @@ import { Item } from '../../../../core/shared/item.model'; import { Bundle } from '../../../../core/shared/bundle.model'; import { ResponsiveTableSizes } from '../../../../shared/responsive-table-sizes/responsive-table-sizes'; import { ResponsiveColumnSizes } from '../../../../shared/responsive-table-sizes/responsive-column-sizes'; +import { BundleDataService } from '../../../../core/data/bundle-data.service'; +import { of as observableOf } from 'rxjs'; +import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; +import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub'; +import { RequestService } from '../../../../core/data/request.service'; +import { getMockRequestService } from '../../../../shared/mocks/request.service.mock'; +import { ItemBitstreamsService } from '../item-bitstreams.service'; +import { PaginationService } from '../../../../core/pagination/pagination.service'; +import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; describe('ItemEditBitstreamBundleComponent', () => { let comp: ItemEditBitstreamBundleComponent; @@ -31,10 +40,37 @@ describe('ItemEditBitstreamBundleComponent', () => { } }); + const restEndpoint = 'fake-rest-endpoint'; + const bundleService = jasmine.createSpyObj('bundleService', { + getBitstreamsEndpoint: observableOf(restEndpoint), + getBitstreams: null, + }); + + const objectUpdatesService = { + initialize: () => { + // do nothing + }, + }; + + const itemBitstreamsService = jasmine.createSpyObj('itemBitstreamsService', { + getInitialBitstreamsPaginationOptions: Object.assign(new PaginationComponentOptions(), { + id: 'bundles-pagination-options', + currentPage: 1, + pageSize: 9999 + }), + }); + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [TranslateModule.forRoot()], declarations: [ItemEditBitstreamBundleComponent], + providers: [ + { provide: BundleDataService, useValue: bundleService }, + { provide: ObjectUpdatesService, useValue: objectUpdatesService }, + { provide: PaginationService, useValue: new PaginationServiceStub() }, + { provide: RequestService, useValue: getMockRequestService() }, + { provide: ItemBitstreamsService, useValue: itemBitstreamsService }, + ], schemas: [ NO_ERRORS_SCHEMA ] From d8b426d7458da47320447a85e3c2fb4c7028f20c Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Thu, 5 Sep 2024 10:52:02 +0200 Subject: [PATCH 059/720] 117803: Add ItemBitsreamsService tests --- .../object-updates.service.stub.ts | 28 ++++ .../item-bitstreams.service.spec.ts | 129 ++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 src/app/core/data/object-updates/object-updates.service.stub.ts create mode 100644 src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts diff --git a/src/app/core/data/object-updates/object-updates.service.stub.ts b/src/app/core/data/object-updates/object-updates.service.stub.ts new file mode 100644 index 0000000000..c41728a338 --- /dev/null +++ b/src/app/core/data/object-updates/object-updates.service.stub.ts @@ -0,0 +1,28 @@ +export class ObjectUpdatesServiceStub { + + initialize = jasmine.createSpy('initialize'); + saveFieldUpdate = jasmine.createSpy('saveFieldUpdate'); + getObjectEntry = jasmine.createSpy('getObjectEntry'); + getFieldState = jasmine.createSpy('getFieldState'); + getFieldUpdates = jasmine.createSpy('getFieldUpdates'); + getFieldUpdatesExclusive = jasmine.createSpy('getFieldUpdatesExclusive'); + isValid = jasmine.createSpy('isValid'); + isValidPage = jasmine.createSpy('isValidPage'); + saveAddFieldUpdate = jasmine.createSpy('saveAddFieldUpdate'); + saveRemoveFieldUpdate = jasmine.createSpy('saveRemoveFieldUpdate'); + saveChangeFieldUpdate = jasmine.createSpy('saveChangeFieldUpdate'); + isSelectedVirtualMetadata = jasmine.createSpy('isSelectedVirtualMetadata'); + setSelectedVirtualMetadata = jasmine.createSpy('setSelectedVirtualMetadata'); + setEditableFieldUpdate = jasmine.createSpy('setEditableFieldUpdate'); + setValidFieldUpdate = jasmine.createSpy('setValidFieldUpdate'); + discardFieldUpdates = jasmine.createSpy('discardFieldUpdates'); + discardAllFieldUpdates = jasmine.createSpy('discardAllFieldUpdates'); + reinstateFieldUpdates = jasmine.createSpy('reinstateFieldUpdates'); + removeSingleFieldUpdate = jasmine.createSpy('removeSingleFieldUpdate'); + getUpdateFields = jasmine.createSpy('getUpdateFields'); + hasUpdates = jasmine.createSpy('hasUpdates'); + isReinstatable = jasmine.createSpy('isReinstatable'); + getLastModified = jasmine.createSpy('getLastModified'); + createPatch = jasmine.createSpy('getPatch'); + +} diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts new file mode 100644 index 0000000000..89ecfb518f --- /dev/null +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts @@ -0,0 +1,129 @@ +import { ItemBitstreamsService } from './item-bitstreams.service'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; +import { getMockTranslateService } from '../../../shared/mocks/translate.service.mock'; +import { ObjectUpdatesServiceStub } from '../../../core/data/object-updates/object-updates.service.stub'; +import { BitstreamDataServiceStub } from '../../../shared/testing/bitstream-data-service.stub'; +import { DSONameServiceMock } from '../../../shared/mocks/dso-name.service.mock'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; +import { TranslateService } from '@ngx-translate/core'; +import { BitstreamDataService } from '../../../core/data/bitstream-data.service'; +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; +import { Bitstream } from '../../../core/shared/bitstream.model'; +import { BitstreamFormat } from '../../../core/shared/bitstream-format.model'; +import { + createSuccessfulRemoteDataObject$, + createFailedRemoteDataObject, + createSuccessfulRemoteDataObject +} from '../../../shared/remote-data.utils'; + +describe('ItemBitstreamsService', () => { + let service: ItemBitstreamsService; + let notificationsService: NotificationsService; + let translateService: TranslateService; + let objectUpdatesService: ObjectUpdatesService; + let bitstreamDataService: BitstreamDataService; + let dsoNameService: DSONameService; + + beforeEach(() => { + notificationsService = new NotificationsServiceStub() as any; + translateService = getMockTranslateService(); + objectUpdatesService = new ObjectUpdatesServiceStub() as any; + bitstreamDataService = new BitstreamDataServiceStub() as any; + dsoNameService = new DSONameServiceMock() as any; + + service = new ItemBitstreamsService( + notificationsService, + translateService, + objectUpdatesService, + bitstreamDataService, + dsoNameService, + ); + }); + + describe('displayNotifications', () => { + it('should display an error notification if a response failed', () => { + const responses = [ + createFailedRemoteDataObject(), + ]; + + const key = 'some.key'; + + service.displayNotifications(key, responses); + + expect(notificationsService.success).not.toHaveBeenCalled(); + expect(notificationsService.error).toHaveBeenCalled(); + expect(translateService.instant).toHaveBeenCalledWith('some.key.failed.title'); + }); + + it('should display a success notification if a response succeeded', () => { + const responses = [ + createSuccessfulRemoteDataObject(undefined), + ]; + + const key = 'some.key'; + + service.displayNotifications(key, responses); + + expect(notificationsService.success).toHaveBeenCalled(); + expect(notificationsService.error).not.toHaveBeenCalled(); + expect(translateService.instant).toHaveBeenCalledWith('some.key.saved.title'); + }); + + it('should display both notifications if some failed and some succeeded', () => { + const responses = [ + createFailedRemoteDataObject(), + createSuccessfulRemoteDataObject(undefined), + ]; + + const key = 'some.key'; + + service.displayNotifications(key, responses); + + expect(notificationsService.success).toHaveBeenCalled(); + expect(notificationsService.error).toHaveBeenCalled(); + expect(translateService.instant).toHaveBeenCalledWith('some.key.saved.title'); + expect(translateService.instant).toHaveBeenCalledWith('some.key.saved.title'); + }); + }); + + describe('mapBitstreamsToTableEntries', () => { + it('should correctly map a Bitstream to a BitstreamTableEntry', () => { + const format: BitstreamFormat = new BitstreamFormat(); + + const bitstream: Bitstream = Object.assign(new Bitstream(), { + uuid: 'testUUID', + format: createSuccessfulRemoteDataObject$(format), + }); + + spyOn(dsoNameService, 'getName').and.returnValue('Test Name'); + spyOn(bitstream, 'firstMetadataValue').and.returnValue('description'); + + const tableEntry = service.mapBitstreamsToTableEntries([bitstream])[0]; + + expect(tableEntry.name).toEqual('Test Name'); + expect(tableEntry.nameStripped).toEqual('TestName'); + expect(tableEntry.bitstream).toBe(bitstream); + expect(tableEntry.id).toEqual('testUUID'); + expect(tableEntry.description).toEqual('description'); + expect(tableEntry.downloadUrl).toEqual('/bitstreams/testUUID/download'); + }); + }); + + describe('nameToHeader', () => { + it('should correctly transform a string to an appropriate header ID', () => { + const stringA = 'Test String'; + const stringAResult = 'TestString'; + expect(service.nameToHeader(stringA)).toEqual(stringAResult); + + const stringB = 'Test String Two'; + const stringBResult = 'TestStringTwo'; + expect(service.nameToHeader(stringB)).toEqual(stringBResult); + + const stringC = 'Test String Three'; + const stringCResult = 'TestStringThree'; + expect(service.nameToHeader(stringC)).toEqual(stringCResult); + }); + }); + +}); From cc5b841a65ee396a2622ca3eceb1f2d9bbce07c7 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Tue, 10 Sep 2024 13:36:27 +0200 Subject: [PATCH 060/720] 117803: Only visually hide header rows --- .../item-edit-bitstream-bundle.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html index 6a505b9274..2eb1a151b7 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html @@ -10,7 +10,7 @@
{{'item.edit.bitstreams.headers.name' | translate}} @@ -45,16 +45,20 @@
+ {{ entry.name }} + {{ entry.description }} + {{ (entry.format | async)?.shortDescription }} + - + +
+ +
+ + + +
+
+
- +
{{'item.edit.bitstreams.headers.name' | translate}} From 8481604b1eb63acdc152fc9b7061bb9cd464294e Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Tue, 10 Sep 2024 14:14:58 +0200 Subject: [PATCH 061/720] 117803: Fix table & pagination margin --- .../item-bitstreams/item-bitstreams.component.scss | 4 ++++ .../item-edit-bitstream-bundle.component.scss | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss index 480bf56cc7..662c999461 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss @@ -42,3 +42,7 @@ .table-border { border: 1px solid #dee2e6; } + +:host ::ng-deep .pagination { + padding-top: 0.5rem; +} diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss index 725d329936..ae4eac8d52 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss @@ -27,3 +27,7 @@ padding: 0 0.1rem; vertical-align: center; } + +.table { + margin-bottom: 0; +} From 79f3a3116e2e358d5d7b778324d15c4977c7d55f Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Tue, 10 Sep 2024 14:35:05 +0200 Subject: [PATCH 062/720] 117803: Set header border to background color --- .../item-edit-bitstream-bundle.component.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss index ae4eac8d52..7088c3978e 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss @@ -1,7 +1,7 @@ .header-row { color: var(--bs-table-dark-color); background-color: var(--bs-table-dark-bg); - border-color: var(--bs-table-dark-border-color); + border-color: var(--bs-table-dark-bg); } .bundle-row { From 6a8095d456167a139d320c4ad5c3a3d8a20a365e Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Tue, 10 Sep 2024 15:00:18 +0200 Subject: [PATCH 063/720] 117803: Change pagination settings styling --- .../item-edit-bitstream-bundle.component.html | 4 ++-- .../item-edit-bitstream-bundle.component.scss | 9 --------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html index 2eb1a151b7..f434bf0f8f 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html @@ -41,8 +41,8 @@ title="{{'item.edit.bitstreams.bundle.edit.buttons.upload' | translate}}"> -
- diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss index 7088c3978e..bbd4e1e75c 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss @@ -19,15 +19,6 @@ font-weight: normal; } -.pagination-control-container { - display: flex; -} - -.pagination-control { - padding: 0 0.1rem; - vertical-align: center; -} - .table { margin-bottom: 0; } From a230eee76d931a5b55046587c0fe06fcdd383a90 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Tue, 10 Sep 2024 16:06:44 +0200 Subject: [PATCH 064/720] 117803: Add negative top-margin to subsequent tables --- .../item-bitstreams/item-bitstreams.component.html | 2 +- .../item-edit-bitstream-bundle.component.html | 4 ++-- .../item-edit-bitstream-bundle.component.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html index f22dbe6a0e..3527f2f5b8 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html @@ -28,7 +28,7 @@ [bundle]="bundle" [item]="item" [columnSizes]="columnSizes" - [hideHeader]="!isFirst" + [isFirstTable]="isFirst" (dropObject)="dropBitstream(bundle, $event)">
diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html index f434bf0f8f..3d6ee4fa47 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html @@ -8,9 +8,9 @@ [collectionSize]="bitstreamsList.totalElements"> - - + - + - + - + - + + (cdkDragStarted)="dragStart(entry.name)" (cdkDragEnded)="dragEnd(entry.name)">
{{'item.edit.bitstreams.headers.name' | translate}} diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts index 9c62fe06e7..0974da3f8c 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts @@ -99,9 +99,9 @@ export class ItemEditBitstreamBundleComponent implements OnInit { @Input() columnSizes: ResponsiveTableSizes; /** - * Whether to hide the table headers + * Whether this is the first in a series of bundle tables */ - @Input() hideHeader = false; + @Input() isFirstTable = false; /** * Send an event when the user drops an object on the pagination From be99cc5c23763240869697798b95608e4ff1e319 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Wed, 11 Sep 2024 10:09:03 +0200 Subject: [PATCH 065/720] 118219: Allow dragging of table rows --- .../item-bitstreams/item-bitstreams.component.scss | 7 ------- .../item-edit-bitstream-bundle.component.html | 7 +++++-- .../item-edit-bitstream-bundle.component.ts | 5 +++++ 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss index 662c999461..0ee56fe67e 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss @@ -1,7 +1,4 @@ - - .drag-handle { - visibility: hidden; &:hover { cursor: move; } @@ -11,10 +8,6 @@ cursor: move; } -:host ::ng-deep .bitstream-row:hover .drag-handle, :host ::ng-deep .bitstream-row-drag-handle:focus .drag-handle { - visibility: visible !important; -} - .cdk-drag-preview { margin-left: 0; box-sizing: border-box; diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html index 3d6ee4fa47..b79518a763 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html @@ -27,7 +27,7 @@
{{'item.edit.bitstreams.bundle.name' | translate:{ name: bundleName } }} @@ -65,10 +65,13 @@
+
+ +
{{ entry.name }}
) { + console.log('dropEvent:', event); + } + } From eadbcdbe14108b6c47f9e623e8b99c10d03ff5bd Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Wed, 11 Sep 2024 16:25:42 +0200 Subject: [PATCH 066/720] 118219: Store result of drag & dropping bitstream --- .../item-bitstreams.component.ts | 15 +++-- .../item-edit-bitstream-bundle.component.ts | 60 ++++++++++++++++--- 2 files changed, 62 insertions(+), 13 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts index 9f27cf11b3..e7c846b5da 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectorRef, Component, NgZone, OnDestroy } from '@angular/core'; import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component'; -import { filter, map, switchMap, take } from 'rxjs/operators'; +import { map, switchMap, take } from 'rxjs/operators'; import { Observable, Subscription, zip as observableZip } from 'rxjs'; import { ItemDataService } from '../../../core/data/item-data.service'; import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; @@ -11,7 +11,11 @@ import { BitstreamDataService } from '../../../core/data/bitstream-data.service' import { hasValue } from '../../../shared/empty.util'; import { ObjectCacheService } from '../../../core/cache/object-cache.service'; import { RequestService } from '../../../core/data/request.service'; -import { getFirstSucceededRemoteData, getRemoteDataPayload } from '../../../core/shared/operators'; +import { + getFirstSucceededRemoteData, + getRemoteDataPayload, + getFirstCompletedRemoteData +} from '../../../core/shared/operators'; import { RemoteData } from '../../../core/data/remote-data'; import { PaginatedList } from '../../../core/data/paginated-list.model'; import { Bundle } from '../../../core/shared/bundle.model'; @@ -127,12 +131,13 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme from: `/_links/bitstreams/${event.fromIndex}/href`, path: `/_links/bitstreams/${event.toIndex}/href` } as Operation; - this.bundleService.patch(bundle, [moveOperation]).pipe(take(1)).subscribe((response: RemoteData) => { + this.bundleService.patch(bundle, [moveOperation]).pipe( + getFirstCompletedRemoteData(), + ).subscribe((response: RemoteData) => { this.zone.run(() => { this.itemBitstreamsService.displayNotifications('item.edit.bitstreams.notifications.move', [response]); // Remove all cached requests from this bundle and call the event's callback when the requests are cleared - this.requestService.removeByHrefSubstring(bundle.self).pipe( - filter((isCached) => isCached), + this.requestService.setStaleByHrefSubstring(bundle.self).pipe( take(1) ).subscribe(() => event.finish()); }); diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts index c0c4e30231..0293a1daea 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts @@ -28,7 +28,8 @@ import { PaginationService } from '../../../../core/pagination/pagination.servic import { PaginationComponent } from '../../../../shared/pagination/pagination.component'; import { RequestService } from '../../../../core/data/request.service'; import { ItemBitstreamsService } from '../item-bitstreams.service'; -import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; +import { hasValue } from '../../../../shared/empty.util'; /** * Interface storing all the information necessary to create a row in the bitstream edit table @@ -135,7 +136,7 @@ export class ItemEditBitstreamBundleComponent implements OnInit { /** * The data to show in the table */ - tableEntries$: Observable; + tableEntries$: BehaviorSubject = new BehaviorSubject(null); /** * The initial page options to use for fetching the bitstreams @@ -165,7 +166,7 @@ export class ItemEditBitstreamBundleComponent implements OnInit { /** * The updates to the current bitstreams */ - updates$: Observable; + updates$: BehaviorSubject = new BehaviorSubject(null); constructor( @@ -229,17 +230,17 @@ export class ItemEditBitstreamBundleComponent implements OnInit { this.objectUpdatesService.initialize(this.bundleUrl, bitstreams, new Date()); }); - this.updates$ = this.bitstreamsRD$.pipe( + this.bitstreamsRD$.pipe( getAllSucceededRemoteData(), paginatedListToArray(), switchMap((bitstreams) => this.objectUpdatesService.getFieldUpdatesExclusive(this.bundleUrl, bitstreams)) - ); + ).subscribe((updates) => this.updates$.next(updates)); - this.tableEntries$ = this.bitstreamsRD$.pipe( + this.bitstreamsRD$.pipe( getAllSucceededRemoteData(), paginatedListToArray(), map((bitstreams) => this.itemBitstreamsService.mapBitstreamsToTableEntries(bitstreams)), - ); + ).subscribe((tableEntries) => this.tableEntries$.next(tableEntries)); } /** @@ -288,7 +289,50 @@ export class ItemEditBitstreamBundleComponent implements OnInit { } drop(event: CdkDragDrop) { - console.log('dropEvent:', event); + const dragIndex = event.previousIndex; + let dropIndex = event.currentIndex; + const dragPage = this.currentPaginationOptions$.value.currentPage - 1; + let dropPage = this.currentPaginationOptions$.value.currentPage - 1; + + // Check if the user is hovering over any of the pagination's pages at the time of dropping the object + const droppedOnElement = document.elementFromPoint(event.dropPoint.x, event.dropPoint.y); + if (hasValue(droppedOnElement) && hasValue(droppedOnElement.textContent) && droppedOnElement.classList.contains('page-link')) { + // The user is hovering over a page, fetch the page's number from the element + const droppedPage = Number(droppedOnElement.textContent); + if (hasValue(droppedPage) && !Number.isNaN(droppedPage)) { + dropPage = droppedPage - 1; + dropIndex = 0; + } + } + + const isNewPage = dragPage !== dropPage; + // Move the object in the custom order array if the drop happened within the same page + // This allows us to instantly display a change in the order, instead of waiting for the REST API's response first + if (!isNewPage && dragIndex !== dropIndex) { + const currentEntries = [...this.tableEntries$.value]; + moveItemInArray(currentEntries, dragIndex, dropIndex); + this.tableEntries$.next(currentEntries); + } + + const pageSize = this.currentPaginationOptions$.value.pageSize; + const redirectPage = dropPage + 1; + const fromIndex = (dragPage * pageSize) + dragIndex; + const toIndex = (dropPage * pageSize) + dropIndex; + // Send out a drop event (and navigate to the new page) when the "from" and "to" indexes are different from each other + if (fromIndex !== toIndex) { + // if (isNewPage) { + // this.loading$.next(true); + // } + this.dropObject.emit(Object.assign({ + fromIndex, + toIndex, + finish: () => { + if (isNewPage) { + this.paginationComponent.doPageChange(redirectPage); + } + } + })); + } } } From 6a2c7d09d69e6275d5e29d06a2672ce9b79e52fb Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Thu, 12 Sep 2024 10:01:55 +0200 Subject: [PATCH 067/720] 118219: Add dragging tooltip explaining how to drag to other page --- .../item-edit-bitstream-bundle.component.html | 7 +++-- .../item-edit-bitstream-bundle.component.ts | 30 ++++++++++++++++++- src/assets/i18n/en.json5 | 2 ++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html index b79518a763..0338055df5 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html @@ -27,7 +27,9 @@
{{'item.edit.bitstreams.bundle.name' | translate:{ name: bundleName } }} @@ -65,7 +67,8 @@
diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts index 0293a1daea..bd99fc1a09 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts @@ -21,7 +21,7 @@ import { } from '../../../../core/shared/operators'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model'; -import { map } from 'rxjs/operators'; +import { map, take, filter } from 'rxjs/operators'; import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model'; import { FieldUpdate } from '../../../../core/data/object-updates/field-update.model'; import { PaginationService } from '../../../../core/pagination/pagination.service'; @@ -83,8 +83,16 @@ export class ItemEditBitstreamBundleComponent implements OnInit { */ @ViewChild('bundleView', {static: true}) bundleView; + /** + * The view on the pagination component + */ @ViewChild(PaginationComponent) paginationComponent: PaginationComponent; + /** + * The view on the drag tooltip + */ + @ViewChild('dragTooltip') dragTooltip; + /** * The bundle to display bitstreams for */ @@ -158,6 +166,11 @@ export class ItemEditBitstreamBundleComponent implements OnInit { */ pageSize$: BehaviorSubject; + /** + * Whether the table has multiple pages + */ + hasMultiplePages = false; + /** * The self url of the bundle, also used when retrieving fieldUpdates */ @@ -288,6 +301,21 @@ export class ItemEditBitstreamBundleComponent implements OnInit { this.paginationComponent.doPageSizeChange(pageSize); } + dragStart() { + // Only open the drag tooltip when there are multiple pages + this.paginationComponent.shouldShowBottomPager.pipe( + take(1), + filter((hasMultiplePages) => hasMultiplePages), + ).subscribe(() => { + this.dragTooltip.open(); + }); + } + + dragEnd() { + this.dragTooltip.close(); + } + + drop(event: CdkDragDrop) { const dragIndex = event.previousIndex; let dropIndex = event.currentIndex; diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 4d5ef9ee1b..f2dbf9b2d1 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1936,6 +1936,8 @@ "item.edit.bitstreams.bundle.table.aria-label": "Bitstreams in the {{ bundle }} Bundle", + "item.edit.bitstreams.bundle.tooltip": "You can move a bitstream to a different page by dropping it on the page number.", + "item.edit.bitstreams.discard-button": "Discard", "item.edit.bitstreams.edit.buttons.download": "Download", From 8a16597b6958f99f949a413028ee2d5dee2905dc Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Thu, 12 Sep 2024 10:33:21 +0200 Subject: [PATCH 068/720] 118219: Fix tests --- .../item-bitstreams.component.spec.ts | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts index 10e1812131..6ce7394473 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts @@ -18,7 +18,6 @@ import { ObjectValuesPipe } from '../../../shared/utils/object-values-pipe'; import { VarDirective } from '../../../shared/utils/var.directive'; import { BundleDataService } from '../../../core/data/bundle-data.service'; import { Bundle } from '../../../core/shared/bundle.model'; -import { RestResponse } from '../../../core/cache/response.models'; import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service'; import { RouterStub } from '../../../shared/testing/router.stub'; import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; @@ -145,7 +144,7 @@ describe('ItemBitstreamsComponent', () => { url: url }); bundleService = jasmine.createSpyObj('bundleService', { - patch: observableOf(new RestResponse(true, 200, 'OK')) + patch: createSuccessfulRemoteDataObject$({}), }); TestBed.configureTestingModule({ @@ -191,20 +190,6 @@ describe('ItemBitstreamsComponent', () => { }); }); - describe('when dropBitstream is called', () => { - const event = { - fromIndex: 0, - toIndex: 50, - // eslint-disable-next-line no-empty,@typescript-eslint/no-empty-function - finish: () => { - } - }; - - beforeEach(() => { - comp.dropBitstream(bundle, event); - }); - }); - describe('when dropBitstream is called', () => { beforeEach((done) => { comp.dropBitstream(bundle, { From a207fb51e9159c0076bb8a76cd5a73944a11655b Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Thu, 12 Sep 2024 10:38:38 +0200 Subject: [PATCH 069/720] 118219: Remove unused paginated-drag-and-drop components --- .../edit-item-page/edit-item-page.module.ts | 4 - ...rag-and-drop-bitstream-list.component.html | 33 --- ...-and-drop-bitstream-list.component.spec.ts | 150 ----------- ...-drag-and-drop-bitstream-list.component.ts | 80 ------ ...-edit-bitstream-drag-handle.component.html | 5 - ...em-edit-bitstream-drag-handle.component.ts | 26 -- ...nated-drag-and-drop-list.component.spec.ts | 136 ---------- ...-paginated-drag-and-drop-list.component.ts | 239 ------------------ 8 files changed, 673 deletions(-) delete mode 100644 src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.html delete mode 100644 src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.spec.ts delete mode 100644 src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.ts delete mode 100644 src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component.html delete mode 100644 src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component.ts delete mode 100644 src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.spec.ts delete mode 100644 src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.ts diff --git a/src/app/item-page/edit-item-page/edit-item-page.module.ts b/src/app/item-page/edit-item-page/edit-item-page.module.ts index 0a75394ddd..4ae5ebe666 100644 --- a/src/app/item-page/edit-item-page/edit-item-page.module.ts +++ b/src/app/item-page/edit-item-page/edit-item-page.module.ts @@ -26,8 +26,6 @@ import { ItemMoveComponent } from './item-move/item-move.component'; import { ItemEditBitstreamBundleComponent } from './item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component'; import { BundleDataService } from '../../core/data/bundle-data.service'; import { DragDropModule } from '@angular/cdk/drag-drop'; -import { ItemEditBitstreamDragHandleComponent } from './item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component'; -import { PaginatedDragAndDropBitstreamListComponent } from './item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component'; import { VirtualMetadataComponent } from './virtual-metadata/virtual-metadata.component'; import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component'; import { ItemAuthorizationsComponent } from './item-authorizations/item-authorizations.component'; @@ -82,12 +80,10 @@ import { ItemVersionHistoryComponent, ItemEditBitstreamComponent, ItemEditBitstreamBundleComponent, - PaginatedDragAndDropBitstreamListComponent, EditRelationshipComponent, EditRelationshipListComponent, ItemCollectionMapperComponent, ItemMoveComponent, - ItemEditBitstreamDragHandleComponent, VirtualMetadataComponent, ItemAuthorizationsComponent, IdentifierDataComponent, diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.html deleted file mode 100644 index f54aa73597..0000000000 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.html +++ /dev/null @@ -1,33 +0,0 @@ - - -
- -
- -
- -
-
-
-
-
-
- -
diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.spec.ts deleted file mode 100644 index 7317eb93be..0000000000 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.spec.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { Bundle } from '../../../../../core/shared/bundle.model'; -import { TranslateModule } from '@ngx-translate/core'; -import { PaginatedDragAndDropBitstreamListComponent } from './paginated-drag-and-drop-bitstream-list.component'; -import { VarDirective } from '../../../../../shared/utils/var.directive'; -import { ObjectValuesPipe } from '../../../../../shared/utils/object-values-pipe'; -import { ObjectUpdatesService } from '../../../../../core/data/object-updates/object-updates.service'; -import { BundleDataService } from '../../../../../core/data/bundle-data.service'; -import { Bitstream } from '../../../../../core/shared/bitstream.model'; -import { BitstreamFormat } from '../../../../../core/shared/bitstream-format.model'; -import { of as observableOf } from 'rxjs'; -import { take } from 'rxjs/operators'; -import { ResponsiveTableSizes } from '../../../../../shared/responsive-table-sizes/responsive-table-sizes'; -import { ResponsiveColumnSizes } from '../../../../../shared/responsive-table-sizes/responsive-column-sizes'; -import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils'; -import { createPaginatedList } from '../../../../../shared/testing/utils.test'; -import { RequestService } from '../../../../../core/data/request.service'; -import { PaginationService } from '../../../../../core/pagination/pagination.service'; -import { PaginationServiceStub } from '../../../../../shared/testing/pagination-service.stub'; - -describe('PaginatedDragAndDropBitstreamListComponent', () => { - let comp: PaginatedDragAndDropBitstreamListComponent; - let fixture: ComponentFixture; - let objectUpdatesService: ObjectUpdatesService; - let bundleService: BundleDataService; - let objectValuesPipe: ObjectValuesPipe; - let requestService: RequestService; - let paginationService; - - const columnSizes = new ResponsiveTableSizes([ - new ResponsiveColumnSizes(2, 2, 3, 4, 4), - new ResponsiveColumnSizes(2, 3, 3, 3, 3), - new ResponsiveColumnSizes(2, 2, 2, 2, 2), - new ResponsiveColumnSizes(6, 5, 4, 3, 3) - ]); - - const bundle = Object.assign(new Bundle(), { - id: 'bundle-1', - uuid: 'bundle-1', - _links: { - self: { href: 'bundle-1-selflink' } - } - }); - const date = new Date(); - const format = Object.assign(new BitstreamFormat(), { - shortDescription: 'PDF' - }); - const bitstream1 = Object.assign(new Bitstream(), { - uuid: 'bitstreamUUID1', - name: 'Fake Bitstream 1', - bundleName: 'ORIGINAL', - description: 'Description', - format: createSuccessfulRemoteDataObject$(format) - }); - const fieldUpdate1 = { - field: bitstream1, - changeType: undefined - }; - const bitstream2 = Object.assign(new Bitstream(), { - uuid: 'bitstreamUUID2', - name: 'Fake Bitstream 2', - bundleName: 'ORIGINAL', - description: 'Description', - format: createSuccessfulRemoteDataObject$(format) - }); - const fieldUpdate2 = { - field: bitstream2, - changeType: undefined - }; - - beforeEach(waitForAsync(() => { - objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', - { - getFieldUpdates: observableOf({ - [bitstream1.uuid]: fieldUpdate1, - [bitstream2.uuid]: fieldUpdate2, - }), - getFieldUpdatesExclusive: observableOf({ - [bitstream1.uuid]: fieldUpdate1, - [bitstream2.uuid]: fieldUpdate2, - }), - getFieldUpdatesByCustomOrder: observableOf({ - [bitstream1.uuid]: fieldUpdate1, - [bitstream2.uuid]: fieldUpdate2, - }), - saveMoveFieldUpdate: {}, - saveRemoveFieldUpdate: {}, - removeSingleFieldUpdate: {}, - saveAddFieldUpdate: {}, - discardFieldUpdates: {}, - reinstateFieldUpdates: observableOf(true), - initialize: {}, - getUpdatedFields: observableOf([bitstream1, bitstream2]), - getLastModified: observableOf(date), - hasUpdates: observableOf(true), - isReinstatable: observableOf(false), - isValidPage: observableOf(true), - initializeWithCustomOrder: {}, - addPageToCustomOrder: {} - } - ); - - bundleService = jasmine.createSpyObj('bundleService', { - getBitstreams: createSuccessfulRemoteDataObject$(createPaginatedList([bitstream1, bitstream2])), - getBitstreamsEndpoint: observableOf('') - }); - - objectValuesPipe = new ObjectValuesPipe(); - - requestService = jasmine.createSpyObj('requestService', { - hasByHref$: observableOf(true) - }); - - paginationService = new PaginationServiceStub(); - - TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot()], - declarations: [PaginatedDragAndDropBitstreamListComponent, VarDirective], - providers: [ - { provide: ObjectUpdatesService, useValue: objectUpdatesService }, - { provide: BundleDataService, useValue: bundleService }, - { provide: ObjectValuesPipe, useValue: objectValuesPipe }, - { provide: RequestService, useValue: requestService }, - { provide: PaginationService, useValue: paginationService } - ], schemas: [ - NO_ERRORS_SCHEMA - ] - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(PaginatedDragAndDropBitstreamListComponent); - comp = fixture.componentInstance; - comp.bundle = bundle; - comp.columnSizes = columnSizes; - fixture.detectChanges(); - }); - - it('should initialize the objectsRD$', (done) => { - comp.objectsRD$.pipe(take(1)).subscribe((objects) => { - expect(objects.payload.page).toEqual([bitstream1, bitstream2]); - done(); - }); - }); - - it('should initialize the URL', () => { - expect(comp.url).toEqual(bundle.self); - }); -}); diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.ts deleted file mode 100644 index d5bb9eceea..0000000000 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { AbstractPaginatedDragAndDropListComponent } from '../../../../../shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component'; -import { Component, ElementRef, Input, OnInit } from '@angular/core'; -import { Bundle } from '../../../../../core/shared/bundle.model'; -import { Bitstream } from '../../../../../core/shared/bitstream.model'; -import { ObjectUpdatesService } from '../../../../../core/data/object-updates/object-updates.service'; -import { BundleDataService } from '../../../../../core/data/bundle-data.service'; -import { switchMap } from 'rxjs/operators'; -import { PaginatedSearchOptions } from '../../../../../shared/search/models/paginated-search-options.model'; -import { ResponsiveTableSizes } from '../../../../../shared/responsive-table-sizes/responsive-table-sizes'; -import { followLink } from '../../../../../shared/utils/follow-link-config.model'; -import { ObjectValuesPipe } from '../../../../../shared/utils/object-values-pipe'; -import { RequestService } from '../../../../../core/data/request.service'; -import { PaginationService } from '../../../../../core/pagination/pagination.service'; -import { PaginationComponentOptions } from '../../../../../shared/pagination/pagination-component-options.model'; - -@Component({ - selector: 'ds-paginated-drag-and-drop-bitstream-list', - styleUrls: ['../../item-bitstreams.component.scss'], - templateUrl: './paginated-drag-and-drop-bitstream-list.component.html', -}) -/** - * A component listing edit-bitstream rows for each bitstream within the given bundle. - * This component makes use of the AbstractPaginatedDragAndDropListComponent, allowing for users to drag and drop - * bitstreams within the paginated list. To drag and drop a bitstream between two pages, drag the row on top of the - * page number you want the bitstream to end up at. Doing so will add the bitstream to the top of that page. - */ -// NOTE: -// This component was used by the item-edit-bitstream-bundle.component, but this is no longer the case. It is left here -// as a reference for the drag-and-drop functionality. This component (and the abstract version it extends) should be -// removed once this reference is no longer useful. -export class PaginatedDragAndDropBitstreamListComponent extends AbstractPaginatedDragAndDropListComponent implements OnInit { - /** - * The bundle to display bitstreams for - */ - @Input() bundle: Bundle; - - /** - * The bootstrap sizes used for the columns within this table - */ - @Input() columnSizes: ResponsiveTableSizes; - - constructor(protected objectUpdatesService: ObjectUpdatesService, - protected elRef: ElementRef, - protected objectValuesPipe: ObjectValuesPipe, - protected bundleService: BundleDataService, - protected paginationService: PaginationService, - protected requestService: RequestService) { - super(objectUpdatesService, elRef, objectValuesPipe, paginationService); - } - - ngOnInit() { - super.ngOnInit(); - } - - /** - * Initialize the bitstreams observable depending on currentPage$ - */ - initializeObjectsRD(): void { - this.objectsRD$ = this.currentPage$.pipe( - switchMap((page: PaginationComponentOptions) => { - const paginatedOptions = new PaginatedSearchOptions({pagination: Object.assign({}, page)}); - return this.bundleService.getBitstreamsEndpoint(this.bundle.id, paginatedOptions).pipe( - switchMap((href) => this.requestService.hasByHref$(href)), - switchMap(() => this.bundleService.getBitstreams( - this.bundle.id, - paginatedOptions, - followLink('format') - )) - ); - }) - ); - } - - /** - * Initialize the URL used for the field-update store, in this case the bundle's self-link - */ - initializeURL(): void { - this.url = this.bundle.self; - } -} diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component.html deleted file mode 100644 index 1bce8667ee..0000000000 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component.html +++ /dev/null @@ -1,5 +0,0 @@ - -
- -
-
diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component.ts deleted file mode 100644 index e5cb9ba403..0000000000 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Component, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; - -@Component({ - selector: 'ds-item-edit-bitstream-drag-handle', - styleUrls: ['../item-bitstreams.component.scss'], - templateUrl: './item-edit-bitstream-drag-handle.component.html', -}) -/** - * Component displaying a drag handle for the item-edit-bitstream page - * Creates an embedded view of the contents - * (which means it'll be added to the parents html without a wrapping ds-item-edit-bitstream-drag-handle element) - */ -export class ItemEditBitstreamDragHandleComponent implements OnInit { - /** - * The view on the drag-handle - */ - @ViewChild('handleView', {static: true}) handleView; - - constructor(private viewContainerRef: ViewContainerRef) { - } - - ngOnInit(): void { - this.viewContainerRef.createEmbeddedView(this.handleView); - } - -} diff --git a/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.spec.ts b/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.spec.ts deleted file mode 100644 index bac6b89583..0000000000 --- a/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.spec.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { AbstractPaginatedDragAndDropListComponent } from './abstract-paginated-drag-and-drop-list.component'; -import { DSpaceObject } from '../../core/shared/dspace-object.model'; -import { ObjectUpdatesService } from '../../core/data/object-updates/object-updates.service'; -import { Component, ElementRef } from '@angular/core'; -import { BehaviorSubject, Observable, of as observableOf } from 'rxjs'; -import { PaginatedList } from '../../core/data/paginated-list.model'; -import { RemoteData } from '../../core/data/remote-data'; -import { take } from 'rxjs/operators'; -import { PaginationComponent } from '../pagination/pagination.component'; -import { createSuccessfulRemoteDataObject } from '../remote-data.utils'; -import { createPaginatedList } from '../testing/utils.test'; -import { ObjectValuesPipe } from '../utils/object-values-pipe'; -import { PaginationService } from '../../core/pagination/pagination.service'; -import { PaginationServiceStub } from '../testing/pagination-service.stub'; -import { FieldUpdates } from '../../core/data/object-updates/field-updates.model'; - -@Component({ - selector: 'ds-mock-paginated-drag-drop-abstract', - template: '' -}) -class MockAbstractPaginatedDragAndDropListComponent extends AbstractPaginatedDragAndDropListComponent { - - constructor(protected objectUpdatesService: ObjectUpdatesService, - protected elRef: ElementRef, - protected objectValuesPipe: ObjectValuesPipe, - protected mockUrl: string, - protected paginationService: PaginationService, - protected mockObjectsRD$: Observable>>) { - super(objectUpdatesService, elRef, objectValuesPipe, paginationService); - } - - initializeObjectsRD(): void { - this.objectsRD$ = this.mockObjectsRD$; - } - - initializeURL(): void { - this.url = this.mockUrl; - } -} - -describe('AbstractPaginatedDragAndDropListComponent', () => { - let component: MockAbstractPaginatedDragAndDropListComponent; - let objectUpdatesService: ObjectUpdatesService; - let elRef: ElementRef; - let objectValuesPipe: ObjectValuesPipe; - - const url = 'mock-abstract-paginated-drag-and-drop-list-component'; - - - const object1 = Object.assign(new DSpaceObject(), { uuid: 'object-1' }); - const object2 = Object.assign(new DSpaceObject(), { uuid: 'object-2' }); - const objectsRD = createSuccessfulRemoteDataObject(createPaginatedList([object1, object2])); - let objectsRD$: BehaviorSubject>>; - let paginationService; - - const updates = { - [object1.uuid]: { field: object1, changeType: undefined }, - [object2.uuid]: { field: object2, changeType: undefined } - } as FieldUpdates; - - let paginationComponent: PaginationComponent; - - beforeEach(() => { - objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', { - initialize: {}, - getFieldUpdatesExclusive: observableOf(updates) - }); - elRef = { - nativeElement: jasmine.createSpyObj('nativeElement', { - querySelector: {} - }) - }; - objectValuesPipe = new ObjectValuesPipe(); - paginationComponent = jasmine.createSpyObj('paginationComponent', { - doPageChange: {} - }); - paginationService = new PaginationServiceStub(); - objectsRD$ = new BehaviorSubject(objectsRD); - component = new MockAbstractPaginatedDragAndDropListComponent(objectUpdatesService, elRef, objectValuesPipe, url, paginationService, objectsRD$); - component.paginationComponent = paginationComponent; - component.ngOnInit(); - }); - - it('should call initialize to initialize the objects in the store', () => { - expect(objectUpdatesService.initialize).toHaveBeenCalled(); - }); - - it('should initialize the updates correctly', (done) => { - component.updates$.pipe(take(1)).subscribe((fieldUpdates) => { - expect(fieldUpdates).toEqual(updates); - done(); - }); - }); - - describe('drop', () => { - const event = { - previousIndex: 0, - currentIndex: 1, - item: { element: { nativeElement: { id: object1.uuid } } } - } as any; - - describe('when the user is hovering over a new page', () => { - const hoverPage = 3; - const hoverElement = { textContent: '' + hoverPage }; - - beforeEach(() => { - elRef.nativeElement.querySelector.and.returnValue(hoverElement); - spyOn(component.dropObject, 'emit'); - component.drop(event); - }); - - it('should send out a dropObject event with the expected processed paginated indexes', () => { - expect(component.dropObject.emit).toHaveBeenCalledWith(Object.assign({ - fromIndex: ((component.currentPage$.value.currentPage - 1) * component.pageSize) + event.previousIndex, - toIndex: ((hoverPage - 1) * component.pageSize), - finish: jasmine.anything() - })); - }); - }); - - describe('when the user is not hovering over a new page', () => { - beforeEach(() => { - spyOn(component.dropObject, 'emit'); - component.drop(event); - }); - - it('should send out a dropObject event with the expected properties', () => { - expect(component.dropObject.emit).toHaveBeenCalledWith(Object.assign({ - fromIndex: event.previousIndex, - toIndex: event.currentIndex, - finish: jasmine.anything() - })); - }); - }); - }); -}); diff --git a/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.ts b/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.ts deleted file mode 100644 index 8dba47566f..0000000000 --- a/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { BehaviorSubject, Observable, Subscription } from 'rxjs'; -import { RemoteData } from '../../core/data/remote-data'; -import { PaginatedList } from '../../core/data/paginated-list.model'; -import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; -import { ObjectUpdatesService } from '../../core/data/object-updates/object-updates.service'; -import { distinctUntilChanged, map, switchMap } from 'rxjs/operators'; -import { hasValue } from '../empty.util'; -import { - paginatedListToArray, - getFirstSucceededRemoteData, - getAllSucceededRemoteData -} from '../../core/shared/operators'; -import { DSpaceObject } from '../../core/shared/dspace-object.model'; -import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; -import { Component, ElementRef, EventEmitter, OnDestroy, Output, ViewChild } from '@angular/core'; -import { PaginationComponent } from '../pagination/pagination.component'; -import { ObjectValuesPipe } from '../utils/object-values-pipe'; -import { compareArraysUsing } from '../../item-page/simple/item-types/shared/item-relationships-utils'; -import { PaginationService } from '../../core/pagination/pagination.service'; -import { FieldUpdate } from '../../core/data/object-updates/field-update.model'; -import { FieldUpdates } from '../../core/data/object-updates/field-updates.model'; - -/** - * Operator used for comparing {@link FieldUpdate}s by their field's UUID - */ -export const compareArraysUsingFieldUuids = () => - compareArraysUsing((fieldUpdate: FieldUpdate) => (hasValue(fieldUpdate) && hasValue(fieldUpdate.field)) ? fieldUpdate.field.uuid : undefined); - -/** - * An abstract component containing general methods and logic to be able to drag and drop objects within a paginated - * list. This implementation supports being able to drag and drop objects between pages. - * Dragging an object on top of a page number will automatically detect the page it's being dropped on and send a - * dropObject event to the parent component containing detailed information about the indexes the object was dropped from - * and to. - * - * To extend this component, it is important to make sure to: - * - Initialize objectsRD$ within the initializeObjectsRD() method - * - Initialize a unique URL for this component/page within the initializeURL() method - * - Add (cdkDropListDropped)="drop($event)" to the cdkDropList element in your template - * - Add (pageChange)="switchPage($event)" to the ds-pagination element in your template - * - Use the updates$ observable for building your list of cdkDrag elements in your template - * - * An example component extending from this abstract component: PaginatedDragAndDropBitstreamListComponent - */ -@Component({ - selector: 'ds-paginated-drag-drop-abstract', - template: '' -}) -export abstract class AbstractPaginatedDragAndDropListComponent implements OnDestroy { - /** - * A view on the child pagination component - */ - @ViewChild(PaginationComponent) paginationComponent: PaginationComponent; - - /** - * Send an event when the user drops an object on the pagination - * The event contains details about the index the object came from and is dropped to (across the entirety of the list, - * not just within a single page) - */ - @Output() dropObject: EventEmitter = new EventEmitter(); - - /** - * The URL to use for accessing the object updates from this list - */ - url: string; - - /** - * The objects to retrieve data for and transform into field updates - */ - objectsRD$: Observable>>; - - /** - * The updates to the current list - */ - updates$: Observable; - - /** - * A list of object UUIDs - * This is the order the objects will be displayed in - */ - customOrder: string[]; - - /** - * The amount of objects to display per page - */ - pageSize = 10; - - /** - * The page options to use for fetching the objects - * Start at page 1 and always use the set page size - */ - options = Object.assign(new PaginationComponentOptions(),{ - id: 'dad', - currentPage: 1, - pageSize: this.pageSize - }); - - /** - * The current page being displayed - */ - currentPage$ = new BehaviorSubject(this.options); - - /** - * Whether or not we should display a loading animation - * This is used to display a loading page when the user drops a bitstream onto a new page. The loading animation - * should stop once the bitstream has moved to the new page and the new page's response has loaded and contains the - * dropped object on top (see this.stopLoadingWhenFirstIs below) - */ - loading$: BehaviorSubject = new BehaviorSubject(false); - - /** - * List of subscriptions - */ - subs: Subscription[] = []; - - protected constructor(protected objectUpdatesService: ObjectUpdatesService, - protected elRef: ElementRef, - protected objectValuesPipe: ObjectValuesPipe, - protected paginationService: PaginationService - ) { - } - - /** - * Initialize the observables - */ - ngOnInit() { - this.initializeObjectsRD(); - this.initializeURL(); - this.initializeUpdates(); - this.initializePagination(); - } - - /** - * Overwrite this method to define how the list of objects is initialized and updated - */ - abstract initializeObjectsRD(): void; - - /** - * Overwrite this method to define how the URL is set - */ - abstract initializeURL(): void; - - /** - * Initialize the current pagination retrieval from the paginationService and push to the currentPage$ - */ - initializePagination() { - this.paginationService.getCurrentPagination(this.options.id, this.options).subscribe((currentPagination) => { - this.currentPage$.next(currentPagination); - }); - } - - /** - * Initialize the field-updates in the store - */ - initializeUpdates(): void { - this.objectsRD$.pipe( - getFirstSucceededRemoteData(), - paginatedListToArray(), - ).subscribe((objects: T[]) => { - this.objectUpdatesService.initialize(this.url, objects, new Date()); - }); - this.updates$ = this.objectsRD$.pipe( - getAllSucceededRemoteData(), - paginatedListToArray(), - switchMap((objects: T[]) => this.objectUpdatesService.getFieldUpdatesExclusive(this.url, objects)) - ); - this.subs.push( - this.updates$.pipe( - map((fieldUpdates) => this.objectValuesPipe.transform(fieldUpdates)), - distinctUntilChanged(compareArraysUsingFieldUuids()) - ).subscribe((updateValues) => { - this.customOrder = updateValues.map((fieldUpdate) => fieldUpdate.field.uuid); - // We received new values, stop displaying a loading indicator if it's present - this.loading$.next(false); - }), - // Disable the pagination when objects are loading - this.loading$.subscribe((loading) => this.options.disabled = loading) - ); - } - - /** - * An object was moved, send updates to the dropObject EventEmitter - * When the object is dropped on a page within the pagination of this component, the object moves to the top of that - * page and the pagination automatically loads and switches the view to that page (this is done by calling the event's - * finish() method after sending patch requests to the REST API) - * @param event - */ - drop(event: CdkDragDrop) { - const dragIndex = event.previousIndex; - let dropIndex = event.currentIndex; - const dragPage = this.currentPage$.value.currentPage - 1; - let dropPage = this.currentPage$.value.currentPage - 1; - - // Check if the user is hovering over any of the pagination's pages at the time of dropping the object - const droppedOnElement = this.elRef.nativeElement.querySelector('.page-item:hover'); - if (hasValue(droppedOnElement) && hasValue(droppedOnElement.textContent)) { - // The user is hovering over a page, fetch the page's number from the element - const droppedPage = Number(droppedOnElement.textContent); - if (hasValue(droppedPage) && !Number.isNaN(droppedPage)) { - dropPage = droppedPage - 1; - dropIndex = 0; - } - } - - const isNewPage = dragPage !== dropPage; - // Move the object in the custom order array if the drop happened within the same page - // This allows us to instantly display a change in the order, instead of waiting for the REST API's response first - if (!isNewPage && dragIndex !== dropIndex) { - moveItemInArray(this.customOrder, dragIndex, dropIndex); - } - - const redirectPage = dropPage + 1; - const fromIndex = (dragPage * this.pageSize) + dragIndex; - const toIndex = (dropPage * this.pageSize) + dropIndex; - // Send out a drop event (and navigate to the new page) when the "from" and "to" indexes are different from each other - if (fromIndex !== toIndex) { - if (isNewPage) { - this.loading$.next(true); - } - this.dropObject.emit(Object.assign({ - fromIndex, - toIndex, - finish: () => { - if (isNewPage) { - this.paginationComponent.doPageChange(redirectPage); - } - } - })); - } - } - - /** - * unsub all subscriptions - */ - ngOnDestroy(): void { - this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); - this.paginationService.clearPagination(this.options.id); - } -} From 876d94e124cf11477a5548f11f095bc129c4b313 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Wed, 2 Oct 2024 09:23:26 +0200 Subject: [PATCH 070/720] 115284: Add tests for isRepeatable --- .../edit-relationship-list.component.spec.ts | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts index 312f2936ac..69a2340fd5 100644 --- a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts @@ -78,7 +78,7 @@ describe('EditRelationshipListComponent', () => { fixture.detectChanges(); }; - function init(leftType: string, rightType: string): void { + function init(leftType: string, rightType: string, leftMaxCardinality?: number, rightMaxCardinality?: number): void { entityTypeLeft = Object.assign(new ItemType(), { id: leftType, uuid: leftType, @@ -98,6 +98,8 @@ describe('EditRelationshipListComponent', () => { rightType: createSuccessfulRemoteDataObject$(entityTypeRight), leftwardType: `is${rightType}Of${leftType}`, rightwardType: `is${leftType}Of${rightType}`, + leftMaxCardinality: leftMaxCardinality, + rightMaxCardinality: rightMaxCardinality, }); paginationOptions = Object.assign(new PaginationComponentOptions(), { @@ -367,4 +369,31 @@ describe('EditRelationshipListComponent', () => { })); }); }); + + describe('Is repeatable relationship', () => { + beforeEach(waitForAsync(() => { + currentItemIsLeftItem$ = new BehaviorSubject(true); + })); + describe('when max cardinality is 1', () => { + beforeEach(waitForAsync(() => init('Publication', 'OrgUnit', 1, undefined))); + it('should return false', () => { + const result = (comp as any).isRepeatable(); + expect(result).toBeFalse(); + }); + }); + describe('when max cardinality is 2', () => { + beforeEach(waitForAsync(() => init('Publication', 'OrgUnit', 2, undefined))); + it('should return true', () => { + const result = (comp as any).isRepeatable(); + expect(result).toBeTrue(); + }); + }); + describe('when max cardinality is undefined', () => { + beforeEach(waitForAsync(() => init('Publication', 'OrgUnit', undefined, undefined))); + it('should return true', () => { + const result = (comp as any).isRepeatable(); + expect(result).toBeTrue(); + }); + }); + }); }); From 4bd607107146f4149ce004aba749dbccef7eba71 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 2 Oct 2024 17:02:12 -0500 Subject: [PATCH 071/720] Squashed commit of changes in #3148 from @Andrea-Guevara Co-authored-by: @Andrea-Guevara --- .../item-operation/item-operation.component.html | 6 +++--- .../item-status/item-status.component.html | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-operation/item-operation.component.html b/src/app/item-page/edit-item-page/item-operation/item-operation.component.html index 85c6a2cca1..5364d5b9aa 100644 --- a/src/app/item-page/edit-item-page/item-operation/item-operation.component.html +++ b/src/app/item-page/edit-item-page/item-operation/item-operation.component.html @@ -1,9 +1,9 @@ -
- +
+ {{'item.edit.tabs.status.buttons.' + operation.operationKey + '.label' | translate}}
-
+
diff --git a/src/styles/_global-styles.scss b/src/styles/_global-styles.scss index 765b50ae86..b3120c08cd 100644 --- a/src/styles/_global-styles.scss +++ b/src/styles/_global-styles.scss @@ -466,3 +466,11 @@ ngb-accordion { .mt-ncs { margin-top: calc(var(--ds-content-spacing) * -1); } .mb-ncs { margin-bottom: calc(var(--ds-content-spacing) * -1); } .my-ncs { margin-top: calc(var(--ds-content-spacing) * -1); margin-bottom: calc(var(--ds-content-spacing) * -1); } + + +.link-contrast { + // Rules for accessibility to meet minimum contrast and have an identifiable link between other texts + color: darken($link-color, 5%); + // We use underline to discern link from text as we can't make color lighter on a white bg + text-decoration: underline; +} From db891f2f16103203a8b1cdab4bf363028bd4a820 Mon Sep 17 00:00:00 2001 From: FrancescoMolinaro Date: Tue, 23 Jul 2024 17:13:20 +0200 Subject: [PATCH 087/720] [CST-15592] improve tests, add attributes for testing, fix wrong references --- cypress/e2e/admin-add-new-modals.cy.ts | 6 ++-- cypress/e2e/admin-edit-modals.cy.ts | 6 ++-- cypress/e2e/admin-export-modals.cy.ts | 4 +-- cypress/e2e/admin-search-page.cy.ts | 3 ++ cypress/e2e/admin-workflow-page.cy.ts | 3 ++ cypress/e2e/bulk-access.cy.ts | 7 +++++ cypress/e2e/health-page.cy.ts | 29 +++++++++++++++---- .../health-page/health-page.component.html | 4 +-- .../onclick-menu-item.component.html | 3 +- 9 files changed, 49 insertions(+), 16 deletions(-) diff --git a/cypress/e2e/admin-add-new-modals.cy.ts b/cypress/e2e/admin-add-new-modals.cy.ts index 6e2e8e6970..565ae154f1 100644 --- a/cypress/e2e/admin-add-new-modals.cy.ts +++ b/cypress/e2e/admin-add-new-modals.cy.ts @@ -14,7 +14,7 @@ describe('Admin Add New Modals', () => { // Click on entry of menu cy.get('#admin-menu-section-new-title').click(); - cy.get('a[title="menu.section.new_community"]').click(); + cy.get('a[data-test="menu.section.new_community"]').click(); // Analyze for accessibility testA11y('ds-create-community-parent-selector'); @@ -27,7 +27,7 @@ describe('Admin Add New Modals', () => { // Click on entry of menu cy.get('#admin-menu-section-new-title').click(); - cy.get('a[title="menu.section.new_collection"]').click(); + cy.get('a[data-test="menu.section.new_collection"]').click(); // Analyze for accessibility testA11y('ds-create-collection-parent-selector'); @@ -40,7 +40,7 @@ describe('Admin Add New Modals', () => { // Click on entry of menu cy.get('#admin-menu-section-new-title').click(); - cy.get('a[title="menu.section.new_item"]').click(); + cy.get('a[data-test="menu.section.new_item"]').click(); // Analyze for accessibility testA11y('ds-create-item-parent-selector'); diff --git a/cypress/e2e/admin-edit-modals.cy.ts b/cypress/e2e/admin-edit-modals.cy.ts index 256a6d89cb..e96d6ce898 100644 --- a/cypress/e2e/admin-edit-modals.cy.ts +++ b/cypress/e2e/admin-edit-modals.cy.ts @@ -14,7 +14,7 @@ describe('Admin Edit Modals', () => { // Click on entry of menu cy.get('#admin-menu-section-edit-title').click(); - cy.get('a[title="menu.section.edit_community"]').click(); + cy.get('a[data-test="menu.section.edit_community"]').click(); // Analyze for accessibility testA11y('ds-edit-community-selector'); @@ -27,7 +27,7 @@ describe('Admin Edit Modals', () => { // Click on entry of menu cy.get('#admin-menu-section-edit-title').click(); - cy.get('a[title="menu.section.edit_collection"]').click(); + cy.get('a[data-test="menu.section.edit_collection"]').click(); // Analyze for accessibility testA11y('ds-edit-collection-selector'); @@ -40,7 +40,7 @@ describe('Admin Edit Modals', () => { // Click on entry of menu cy.get('#admin-menu-section-edit-title').click(); - cy.get('a[title="menu.section.edit_item"]').click(); + cy.get('a[data-test="menu.section.edit_item"]').click(); // Analyze for accessibility testA11y('ds-edit-item-selector'); diff --git a/cypress/e2e/admin-export-modals.cy.ts b/cypress/e2e/admin-export-modals.cy.ts index b611bb8fd5..9f69764d19 100644 --- a/cypress/e2e/admin-export-modals.cy.ts +++ b/cypress/e2e/admin-export-modals.cy.ts @@ -14,7 +14,7 @@ describe('Admin Export Modals', () => { // Click on entry of menu cy.get('#admin-menu-section-export-title').click(); - cy.get('a[title="menu.section.export_metadata"]').click(); + cy.get('a[data-test="menu.section.export_metadata"]').click(); // Analyze for accessibility testA11y('ds-export-metadata-selector'); @@ -27,7 +27,7 @@ describe('Admin Export Modals', () => { // Click on entry of menu cy.get('#admin-menu-section-export-title').click(); - cy.get('a[title="menu.section.export_batch"]').click(); + cy.get('a[data-test="menu.section.export_batch"]').click(); // Analyze for accessibility testA11y('ds-export-batch-selector'); diff --git a/cypress/e2e/admin-search-page.cy.ts b/cypress/e2e/admin-search-page.cy.ts index 2e1d13aa13..4fbf8939fe 100644 --- a/cypress/e2e/admin-search-page.cy.ts +++ b/cypress/e2e/admin-search-page.cy.ts @@ -12,6 +12,9 @@ describe('Admin Search Page', () => { cy.get('ds-admin-search-page').should('be.visible'); // At least one search result should be displayed cy.get('[data-test="list-object"]').should('be.visible'); + // Click each filter toggle to open *every* filter + // (As we want to scan filter section for accessibility issues as well) + cy.get('[data-test="filter-toggle"]').click({ multiple: true }); // Analyze for accessibility issues testA11y('ds-admin-search-page'); }); diff --git a/cypress/e2e/admin-workflow-page.cy.ts b/cypress/e2e/admin-workflow-page.cy.ts index cd2275f584..c3c235e346 100644 --- a/cypress/e2e/admin-workflow-page.cy.ts +++ b/cypress/e2e/admin-workflow-page.cy.ts @@ -12,6 +12,9 @@ describe('Admin Workflow Page', () => { cy.get('ds-admin-workflow-page').should('be.visible'); // At least one search result should be displayed cy.get('[data-test="list-object"]').should('be.visible'); + // Click each filter toggle to open *every* filter + // (As we want to scan filter section for accessibility issues as well) + cy.get('[data-test="filter-toggle"]').click({ multiple: true }); // Analyze for accessibility issues testA11y('ds-admin-workflow-page'); }); diff --git a/cypress/e2e/bulk-access.cy.ts b/cypress/e2e/bulk-access.cy.ts index 4d199f53f9..87033e13e4 100644 --- a/cypress/e2e/bulk-access.cy.ts +++ b/cypress/e2e/bulk-access.cy.ts @@ -11,6 +11,11 @@ describe('Bulk Access', () => { it('should pass accessibility tests', () => { // Page must first be visible cy.get('ds-bulk-access').should('be.visible'); + // At least one search result should be displayed + cy.get('[data-test="list-object"]').should('be.visible'); + // Click each filter toggle to open *every* filter + // (As we want to scan filter section for accessibility issues as well) + cy.get('[data-test="filter-toggle"]').click({ multiple: true }); // Analyze for accessibility issues testA11y('ds-bulk-access', { rules: { @@ -18,6 +23,8 @@ describe('Bulk Access', () => { // Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216 'aria-required-children': { enabled: false }, 'nested-interactive': { enabled: false }, + // Card titles fail this test currently + 'heading-order': { enabled: false }, }, } as Options); }); diff --git a/cypress/e2e/health-page.cy.ts b/cypress/e2e/health-page.cy.ts index 91c68638ea..79ebf4bc04 100644 --- a/cypress/e2e/health-page.cy.ts +++ b/cypress/e2e/health-page.cy.ts @@ -1,16 +1,35 @@ import { testA11y } from 'cypress/support/utils'; import { Options } from 'cypress-axe'; -describe('Health Page', () => { - beforeEach(() => { - // Must login as an Admin to see the page - cy.visit('/health'); - cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + +beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/health'); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); +}); + +describe('Health Page > Status Tab', () => { + it('should pass accessibility tests', () => { + // Page must first be visible + cy.get('ds-health-page').should('be.visible'); + // Analyze for accessibility issues + testA11y('ds-health-page', { + rules: { + // All panels are accordians & fail "aria-required-children" and "nested-interactive". + // Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216 + 'aria-required-children': { enabled: false }, + 'nested-interactive': { enabled: false }, + }, + } as Options); }); +}); +describe('Health Page > Info Tab', () => { it('should pass accessibility tests', () => { // Page must first be visible cy.get('ds-health-page').should('be.visible'); + cy.get('a[data-test="health-page.info-tab"]').click(); + // Analyze for accessibility issues testA11y('ds-health-page', { rules: { diff --git a/src/app/health-page/health-page.component.html b/src/app/health-page/health-page.component.html index 14c577a8ee..209fd230ae 100644 --- a/src/app/health-page/health-page.component.html +++ b/src/app/health-page/health-page.component.html @@ -3,7 +3,7 @@

{{'health-page.heading' | translate}}

diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts index bd99fc1a09..24319f4a83 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts @@ -30,6 +30,8 @@ import { RequestService } from '../../../../core/data/request.service'; import { ItemBitstreamsService } from '../item-bitstreams.service'; import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; import { hasValue } from '../../../../shared/empty.util'; +import { LiveRegionService } from '../../../../shared/live-region/live-region.service'; +import { TranslateService } from '@ngx-translate/core'; /** * Interface storing all the information necessary to create a row in the bitstream edit table @@ -166,11 +168,6 @@ export class ItemEditBitstreamBundleComponent implements OnInit { */ pageSize$: BehaviorSubject; - /** - * Whether the table has multiple pages - */ - hasMultiplePages = false; - /** * The self url of the bundle, also used when retrieving fieldUpdates */ @@ -190,6 +187,8 @@ export class ItemEditBitstreamBundleComponent implements OnInit { protected paginationService: PaginationService, protected requestService: RequestService, protected itemBitstreamsService: ItemBitstreamsService, + protected liveRegionService: LiveRegionService, + protected translateService: TranslateService, ) { } @@ -301,7 +300,7 @@ export class ItemEditBitstreamBundleComponent implements OnInit { this.paginationComponent.doPageSizeChange(pageSize); } - dragStart() { + dragStart(bitstreamName: string) { // Only open the drag tooltip when there are multiple pages this.paginationComponent.shouldShowBottomPager.pipe( take(1), @@ -309,10 +308,18 @@ export class ItemEditBitstreamBundleComponent implements OnInit { ).subscribe(() => { this.dragTooltip.open(); }); + + const message = this.translateService.instant('item.edit.bitstreams.edit.live.drag', + { bitstream: bitstreamName }); + this.liveRegionService.addMessage(message); } - dragEnd() { + dragEnd(bitstreamName: string) { this.dragTooltip.close(); + + const message = this.translateService.instant('item.edit.bitstreams.edit.live.drop', + { bitstream: bitstreamName }); + this.liveRegionService.addMessage(message); } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index f2dbf9b2d1..48fd581bdf 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1950,6 +1950,10 @@ "item.edit.bitstreams.edit.buttons.undo": "Undo changes", + "item.edit.bitstreams.edit.live.drag": "{{ bitstream }} grabbed", + + "item.edit.bitstreams.edit.live.drop": "{{ bitstream }} dropped", + "item.edit.bitstreams.empty": "This item doesn't contain any bitstreams. Click the upload button to create one.", "item.edit.bitstreams.headers.actions": "Actions", From 1f909dc6ea0d84859fbeef690c9bd212417c7a99 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Tue, 17 Sep 2024 13:34:08 +0200 Subject: [PATCH 108/720] 118223: Add instructive alert to item-bitstreams edit page --- .../item-bitstreams/item-bitstreams.component.html | 7 ++++++- .../item-bitstreams/item-bitstreams.component.ts | 4 ++++ src/assets/i18n/en.json5 | 2 ++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html index 3527f2f5b8..1c13154bfa 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html @@ -1,4 +1,8 @@
+
+ +
+
Date: Wed, 18 Sep 2024 11:57:41 +0200 Subject: [PATCH 109/720] 118223: Implement bitstream reordering with keyboard --- .../item-bitstreams.component.html | 1 - .../item-bitstreams.component.spec.ts | 43 +- .../item-bitstreams.component.ts | 87 ++-- .../item-bitstreams.service.spec.ts | 19 + .../item-bitstreams.service.ts | 303 ++++++++++++- .../item-edit-bitstream-bundle.component.html | 33 +- ...em-edit-bitstream-bundle.component.spec.ts | 1 + .../item-edit-bitstream-bundle.component.ts | 405 ++++++++++++------ .../live-region/live-region.service.stub.ts | 30 ++ src/assets/i18n/en.json5 | 8 +- 10 files changed, 727 insertions(+), 203 deletions(-) create mode 100644 src/app/shared/live-region/live-region.service.stub.ts diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html index 1c13154bfa..b9af2a7d18 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html @@ -33,7 +33,6 @@ [item]="item" [columnSizes]="columnSizes" [isFirstTable]="isFirst" - (dropObject)="dropBitstream(bundle, $event)" aria-describedby="reorder-description">
diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts index 6ce7394473..a5549a6ba0 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts @@ -25,6 +25,10 @@ import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } f import { createPaginatedList } from '../../../shared/testing/utils.test'; import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model'; import { BitstreamDataServiceStub } from '../../../shared/testing/bitstream-data-service.stub'; +import { ItemBitstreamsService } from './item-bitstreams.service'; +import { ResponsiveTableSizes } from '../../../shared/responsive-table-sizes/responsive-table-sizes'; +import { ResponsiveColumnSizes } from '../../../shared/responsive-table-sizes/responsive-column-sizes'; +import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; let comp: ItemBitstreamsComponent; let fixture: ComponentFixture; @@ -76,6 +80,7 @@ let objectCache: ObjectCacheService; let requestService: RequestService; let searchConfig: SearchConfigurationService; let bundleService: BundleDataService; +let itemBitstreamsService: ItemBitstreamsService; describe('ItemBitstreamsComponent', () => { beforeEach(waitForAsync(() => { @@ -147,6 +152,19 @@ describe('ItemBitstreamsComponent', () => { patch: createSuccessfulRemoteDataObject$({}), }); + itemBitstreamsService = jasmine.createSpyObj('itemBitstreamsService', { + getColumnSizes: new ResponsiveTableSizes([ + new ResponsiveColumnSizes(2, 2, 3, 4, 4), + new ResponsiveColumnSizes(2, 3, 3, 3, 3), + new ResponsiveColumnSizes(2, 2, 2, 2, 2), + new ResponsiveColumnSizes(6, 5, 4, 3, 3) + ]), + getSelectedBitstream$: observableOf({}), + getInitialBundlesPaginationOptions: new PaginationComponentOptions(), + removeMarkedBitstreams: createSuccessfulRemoteDataObject$({}), + displayNotifications: undefined, + }); + TestBed.configureTestingModule({ imports: [TranslateModule.forRoot()], declarations: [ItemBitstreamsComponent, ObjectValuesPipe, VarDirective], @@ -161,6 +179,7 @@ describe('ItemBitstreamsComponent', () => { { provide: RequestService, useValue: requestService }, { provide: SearchConfigurationService, useValue: searchConfig }, { provide: BundleDataService, useValue: bundleService }, + { provide: ItemBitstreamsService, useValue: itemBitstreamsService }, ChangeDetectorRef ], schemas: [ NO_ERRORS_SCHEMA @@ -181,28 +200,8 @@ describe('ItemBitstreamsComponent', () => { comp.submit(); }); - it('should call removeMultiple on the bitstreamService for the marked field', () => { - expect(bitstreamService.removeMultiple).toHaveBeenCalledWith([bitstream2]); - }); - - it('should not call removeMultiple on the bitstreamService for the unmarked field', () => { - expect(bitstreamService.removeMultiple).not.toHaveBeenCalledWith([bitstream1]); - }); - }); - - describe('when dropBitstream is called', () => { - beforeEach((done) => { - comp.dropBitstream(bundle, { - fromIndex: 0, - toIndex: 50, - finish: () => { - done(); - } - }); - }); - - it('should send out a patch for the move operation', () => { - expect(bundleService.patch).toHaveBeenCalled(); + it('should call removeMarkedBitstreams on the itemBitstreamsService', () => { + expect(itemBitstreamsService.removeMarkedBitstreams).toHaveBeenCalled(); }); }); diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts index 7757170f4e..4ced3dd649 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectorRef, Component, NgZone, OnDestroy } from '@angular/core'; +import { ChangeDetectorRef, Component, NgZone, OnDestroy, HostListener } from '@angular/core'; import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component'; import { map, switchMap, take } from 'rxjs/operators'; import { Observable, Subscription, zip as observableZip } from 'rxjs'; @@ -8,13 +8,11 @@ import { ActivatedRoute, Router } from '@angular/router'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; import { BitstreamDataService } from '../../../core/data/bitstream-data.service'; -import { hasValue } from '../../../shared/empty.util'; import { ObjectCacheService } from '../../../core/cache/object-cache.service'; import { RequestService } from '../../../core/data/request.service'; import { getFirstSucceededRemoteData, getRemoteDataPayload, - getFirstCompletedRemoteData } from '../../../core/shared/operators'; import { RemoteData } from '../../../core/data/remote-data'; import { PaginatedList } from '../../../core/data/paginated-list.model'; @@ -23,7 +21,6 @@ import { BundleDataService } from '../../../core/data/bundle-data.service'; import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-search-options.model'; import { ResponsiveTableSizes } from '../../../shared/responsive-table-sizes/responsive-table-sizes'; import { NoContent } from '../../../core/shared/NoContent.model'; -import { Operation } from 'fast-json-patch'; import { ItemBitstreamsService } from './item-bitstreams.service'; import { AlertType } from '../../../shared/alert/aletr-type'; @@ -88,13 +85,63 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme postItemInit(): void { const bundlesOptions = this.itemBitstreamsService.getInitialBundlesPaginationOptions(); - this. bundles$ = this.itemService.getBundles(this.item.id, new PaginatedSearchOptions({pagination: bundlesOptions})).pipe( + this.bundles$ = this.itemService.getBundles(this.item.id, new PaginatedSearchOptions({pagination: bundlesOptions})).pipe( getFirstSucceededRemoteData(), getRemoteDataPayload(), map((bundlePage: PaginatedList) => bundlePage.page) ); } + /** + * Handles keyboard events that should move the currently selected bitstream up + */ + @HostListener('document:keydown.arrowUp', ['$event']) + moveUp(event: KeyboardEvent) { + if (this.itemBitstreamsService.hasSelectedBitstream()) { + event.preventDefault(); + this.itemBitstreamsService.moveSelectedBitstreamUp(); + } + } + + /** + * Handles keyboard events that should move the currently selected bitstream down + */ + @HostListener('document:keydown.arrowDown', ['$event']) + moveDown(event: KeyboardEvent) { + if (this.itemBitstreamsService.hasSelectedBitstream()) { + event.preventDefault(); + this.itemBitstreamsService.moveSelectedBitstreamDown(); + } + } + + /** + * Handles keyboard events that should cancel the currently selected bitstream. + * A cancel means that the selected bitstream is returned to its original position and is no longer selected. + * @param event + */ + @HostListener('document:keyup.escape', ['$event']) + cancelSelection(event: KeyboardEvent) { + if (this.itemBitstreamsService.hasSelectedBitstream()) { + event.preventDefault(); + this.itemBitstreamsService.cancelSelection(); + } + } + + /** + * Handles keyboard events that should clear the currently selected bitstream. + * A clear means that the selected bitstream remains in its current position but is no longer selected. + */ + @HostListener('document:keydown.enter', ['$event']) + @HostListener('document:keydown.space', ['$event']) + clearSelection(event: KeyboardEvent) { + // Only when no specific element is in focus do we want to clear the currently selected bitstream + // Otherwise we might clear the selection when a different action was intended, e.g. clicking a button or selecting + // a different bitstream. + if (event.target instanceof Element && event.target.tagName === 'BODY') { + this.itemBitstreamsService.clearSelection(); + } + } + /** * Initialize the notification messages prefix */ @@ -120,36 +167,6 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme }); } - /** - * A bitstream was dropped in a new location. Send out a Move Patch request to the REST API, display notifications, - * refresh the bundle's cache (so the lists can properly reload) and call the event's callback function (which will - * navigate the user to the correct page) - * @param bundle The bundle to send patch requests to - * @param event The event containing the index the bitstream came from and was dropped to - */ - dropBitstream(bundle: Bundle, event: any) { - this.zone.runOutsideAngular(() => { - if (hasValue(event) && hasValue(event.fromIndex) && hasValue(event.toIndex) && hasValue(event.finish)) { - const moveOperation = { - op: 'move', - from: `/_links/bitstreams/${event.fromIndex}/href`, - path: `/_links/bitstreams/${event.toIndex}/href` - } as Operation; - this.bundleService.patch(bundle, [moveOperation]).pipe( - getFirstCompletedRemoteData(), - ).subscribe((response: RemoteData) => { - this.zone.run(() => { - this.itemBitstreamsService.displayNotifications('item.edit.bitstreams.notifications.move', [response]); - // Remove all cached requests from this bundle and call the event's callback when the requests are cleared - this.requestService.setStaleByHrefSubstring(bundle.self).pipe( - take(1) - ).subscribe(() => event.finish()); - }); - }); - } - }); - } - /** * Request the object updates service to discard all current changes to this item * Shows a notification to remind the user that they can undo this diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts index 89ecfb518f..e144e81ec7 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts @@ -16,6 +16,12 @@ import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; +import { BundleDataService } from '../../../core/data/bundle-data.service'; +import { RequestService } from '../../../core/data/request.service'; +import { LiveRegionService } from '../../../shared/live-region/live-region.service'; +import { Bundle } from '../../../core/shared/bundle.model'; +import { of } from 'rxjs'; +import { getLiveRegionServiceStub } from '../../../shared/live-region/live-region.service.stub'; describe('ItemBitstreamsService', () => { let service: ItemBitstreamsService; @@ -23,21 +29,34 @@ describe('ItemBitstreamsService', () => { let translateService: TranslateService; let objectUpdatesService: ObjectUpdatesService; let bitstreamDataService: BitstreamDataService; + let bundleDataService: BundleDataService; let dsoNameService: DSONameService; + let requestService: RequestService; + let liveRegionService: LiveRegionService; beforeEach(() => { notificationsService = new NotificationsServiceStub() as any; translateService = getMockTranslateService(); objectUpdatesService = new ObjectUpdatesServiceStub() as any; bitstreamDataService = new BitstreamDataServiceStub() as any; + bundleDataService = jasmine.createSpyObj('bundleDataService', { + patch: createSuccessfulRemoteDataObject$(new Bundle()), + }); dsoNameService = new DSONameServiceMock() as any; + requestService = jasmine.createSpyObj('requestService', { + setStaleByHrefSubstring: of(true), + }); + liveRegionService = getLiveRegionServiceStub(); service = new ItemBitstreamsService( notificationsService, translateService, objectUpdatesService, bitstreamDataService, + bundleDataService, dsoNameService, + requestService, + liveRegionService, ); }); diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts index 487df77b28..21dc415198 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts @@ -3,38 +3,277 @@ import { PaginationComponentOptions } from '../../../shared/pagination/paginatio import { ResponsiveTableSizes } from '../../../shared/responsive-table-sizes/responsive-table-sizes'; import { ResponsiveColumnSizes } from '../../../shared/responsive-table-sizes/responsive-column-sizes'; import { RemoteData } from '../../../core/data/remote-data'; -import { isNotEmpty, hasValue } from '../../../shared/empty.util'; +import { isNotEmpty, hasValue, hasNoValue } from '../../../shared/empty.util'; import { Bundle } from '../../../core/shared/bundle.model'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; -import { Observable, zip as observableZip } from 'rxjs'; +import { Observable, zip as observableZip, BehaviorSubject } from 'rxjs'; import { NoContent } from '../../../core/shared/NoContent.model'; -import { take, switchMap, map } from 'rxjs/operators'; +import { take, switchMap, map, tap } from 'rxjs/operators'; import { FieldUpdates } from '../../../core/data/object-updates/field-updates.model'; import { FieldUpdate } from '../../../core/data/object-updates/field-update.model'; import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model'; import { Bitstream } from '../../../core/shared/bitstream.model'; import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; import { BitstreamDataService } from '../../../core/data/bitstream-data.service'; -import { BitstreamTableEntry } from './item-edit-bitstream-bundle/item-edit-bitstream-bundle.component'; -import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; +import { getFirstSucceededRemoteDataPayload, getFirstCompletedRemoteData } from '../../../core/shared/operators'; import { getBitstreamDownloadRoute } from '../../../app-routing-paths'; import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; +import { BitstreamFormat } from '../../../core/shared/bitstream-format.model'; +import { MoveOperation } from 'fast-json-patch'; +import { BundleDataService } from '../../../core/data/bundle-data.service'; +import { RequestService } from '../../../core/data/request.service'; +import { LiveRegionService } from '../../../shared/live-region/live-region.service'; +/** + * Interface storing all the information necessary to create a row in the bitstream edit table + */ +export interface BitstreamTableEntry { + /** + * The bitstream + */ + bitstream: Bitstream, + /** + * The uuid of the Bitstream + */ + id: string, + /** + * The name of the Bitstream + */ + name: string, + /** + * The name of the Bitstream with all whitespace removed + */ + nameStripped: string, + /** + * The description of the Bitstream + */ + description: string, + /** + * Observable emitting the Format of the Bitstream + */ + format: Observable, + /** + * The download url of the Bitstream + */ + downloadUrl: string, +} + +/** + * Interface storing information necessary to highlight and reorder the selected bitstream entry + */ +export interface SelectedBitstreamTableEntry { + /** + * The selected entry + */ + bitstream: BitstreamTableEntry, + /** + * The bundle the bitstream belongs to + */ + bundle: Bundle, + /** + * The total number of bitstreams in the bundle + */ + bundleSize: number, + /** + * The original position of the bitstream within the bundle. + */ + originalPosition: number, + /** + * The current position of the bitstream within the bundle. + */ + currentPosition: number, +} + +/** + * This service handles the selection and updating of the bitstreams and their order on the + * 'Edit Item' -> 'Bitstreams' page. + */ @Injectable( { providedIn: 'root' }, ) export class ItemBitstreamsService { + /** + * BehaviorSubject which emits every time the selected bitstream changes. + */ + protected selectedBitstream$: BehaviorSubject = new BehaviorSubject(null); + + protected isPerformingMoveRequest = false; + constructor( protected notificationsService: NotificationsService, protected translateService: TranslateService, protected objectUpdatesService: ObjectUpdatesService, protected bitstreamService: BitstreamDataService, + protected bundleService: BundleDataService, protected dsoNameService: DSONameService, + protected requestService: RequestService, + protected liveRegionService: LiveRegionService, ) { } + /** + * Returns the observable emitting the currently selected bitstream + */ + getSelectedBitstream$(): Observable { + return this.selectedBitstream$; + } + + /** + * Returns a copy of the currently selected bitstream + */ + getSelectedBitstream(): SelectedBitstreamTableEntry { + const selected = this.selectedBitstream$.getValue(); + + if (hasNoValue(selected)) { + return selected; + } + + return Object.assign({}, selected); + } + + hasSelectedBitstream(): boolean { + return hasValue(this.getSelectedBitstream()); + } + + /** + * Select the provided entry + */ + selectBitstreamEntry(entry: SelectedBitstreamTableEntry) { + if (entry !== this.selectedBitstream$.getValue()) { + this.announceSelect(entry.bitstream.name); + this.updateSelectedBitstream(entry); + } + } + + /** + * Makes the {@link selectedBitstream$} observable emit the provided {@link SelectedBitstreamTableEntry}. + * @protected + */ + protected updateSelectedBitstream(entry: SelectedBitstreamTableEntry) { + this.selectedBitstream$.next(entry); + } + + /** + * Unselects the selected bitstream. Does nothing if no bitstream is selected. + */ + clearSelection() { + const selected = this.getSelectedBitstream(); + + if (hasValue(selected)) { + this.updateSelectedBitstream(null); + this.announceClear(selected.bitstream.name); + } + } + + /** + * Returns the currently selected bitstream to its original position and unselects the bitstream. + * Does nothing if no bitstream is selected. + */ + cancelSelection() { + const selected = this.getSelectedBitstream(); + + if (hasNoValue(selected) || this.isPerformingMoveRequest) { + return; + } + + this.selectedBitstream$.next(null); + + const originalPosition = selected.originalPosition; + const currentPosition = selected.currentPosition; + + // If the selected bitstream has not moved, there is no need to return it to its original position + if (currentPosition === originalPosition) { + this.announceClear(selected.bitstream.name); + } else { + this.announceCancel(selected.bitstream.name, originalPosition); + this.performBitstreamMoveRequest(selected.bundle, currentPosition, originalPosition); + } + } + + /** + * Moves the selected bitstream one position up in the bundle. Does nothing if no bitstream is selected or the + * selected bitstream already is at the beginning of the bundle. + */ + moveSelectedBitstreamUp() { + const selected = this.getSelectedBitstream(); + + if (hasNoValue(selected) || this.isPerformingMoveRequest) { + return; + } + + const originalPosition = selected.currentPosition; + if (originalPosition > 0) { + const newPosition = originalPosition - 1; + selected.currentPosition = newPosition; + + const onRequestCompleted = () => { + this.announceMove(selected.bitstream.name, newPosition); + }; + + this.performBitstreamMoveRequest(selected.bundle, originalPosition, newPosition, onRequestCompleted); + this.updateSelectedBitstream(selected); + } + } + + /** + * Moves the selected bitstream one position down in the bundle. Does nothing if no bitstream is selected or the + * selected bitstream already is at the end of the bundle. + */ + moveSelectedBitstreamDown() { + const selected = this.getSelectedBitstream(); + + if (hasNoValue(selected) || this.isPerformingMoveRequest) { + return; + } + + const originalPosition = selected.currentPosition; + if (originalPosition < selected.bundleSize - 1) { + const newPosition = originalPosition + 1; + selected.currentPosition = newPosition; + + const onRequestCompleted = () => { + this.announceMove(selected.bitstream.name, newPosition); + }; + + this.performBitstreamMoveRequest(selected.bundle, originalPosition, newPosition, onRequestCompleted); + this.updateSelectedBitstream(selected); + } + } + + /** + * Sends out a Move Patch request to the REST API, display notifications, + * refresh the bundle's cache (so the lists can properly reload) + * @param bundle The bundle to send patch requests to + * @param fromIndex The index to move from + * @param toIndex The index to move to + * @param finish Optional: Function to execute once the response has been received + */ + performBitstreamMoveRequest(bundle: Bundle, fromIndex: number, toIndex: number, finish?: () => void) { + if (this.isPerformingMoveRequest) { + console.warn('Attempted to perform move request while previous request has not completed yet'); + return; + } + + const moveOperation: MoveOperation = { + op: 'move', + from: `/_links/bitstreams/${fromIndex}/href`, + path: `/_links/bitstreams/${toIndex}/href`, + }; + + this.isPerformingMoveRequest = true; + this.bundleService.patch(bundle, [moveOperation]).pipe( + getFirstCompletedRemoteData(), + tap((response: RemoteData) => this.displayNotifications('item.edit.bitstreams.notifications.move', [response])), + switchMap(() => this.requestService.setStaleByHrefSubstring(bundle.self)), + take(1), + ).subscribe(() => { + this.isPerformingMoveRequest = false; + finish?.(); + }); + } + /** * Returns the pagination options to use when fetching the bundles */ @@ -46,6 +285,10 @@ export class ItemBitstreamsService { }); } + /** + * Returns the initial pagination options to use when fetching the bitstreams + * @param bundleName The name of the bundle, will be as pagination id. + */ getInitialBitstreamsPaginationOptions(bundleName: string): PaginationComponentOptions { return Object.assign(new PaginationComponentOptions(),{ id: bundleName, // This might behave unexpectedly if the item contains two bundles with the same name @@ -118,6 +361,10 @@ export class ItemBitstreamsService { ); } + /** + * Creates an array of {@link BitstreamTableEntry}s from an array of {@link Bitstream}s + * @param bitstreams The bitstreams array to map to table entries + */ mapBitstreamsToTableEntries(bitstreams: Bitstream[]): BitstreamTableEntry[] { return bitstreams.map((bitstream) => { const name = this.dsoNameService.getName(bitstream); @@ -143,7 +390,7 @@ export class ItemBitstreamsService { // To make it clear which headers are relevant for a specific field in the table, the 'headers' attribute is used to // refer to specific headers. The Bitstream's name is used as header ID for the row containing information regarding // that bitstream. As the 'headers' attribute contains a space-separated string of header IDs, the Bitstream's header - // ID can not contain strings itself. + // ID can not contain spaces itself. return this.stripWhiteSpace(name); } @@ -155,4 +402,48 @@ export class ItemBitstreamsService { // '/\s+/g' matches all occurrences of any amount of whitespace characters return str.replace(/\s+/g, ''); } + + /** + * Adds a message to the live region mentioning that the bitstream with the provided name was selected. + * @param bitstreamName The name of the bitstream that was selected. + */ + announceSelect(bitstreamName: string) { + const message = this.translateService.instant('item.edit.bitstreams.edit.live.select', + { bitstream: bitstreamName }); + this.liveRegionService.addMessage(message); + } + + /** + * Adds a message to the live region mentioning that the bitstream with the provided name was moved to the provided + * position. + * @param bitstreamName The name of the bitstream that moved. + * @param toPosition The zero-indexed position that the bitstream moved to. + */ + announceMove(bitstreamName: string, toPosition: number) { + const message = this.translateService.instant('item.edit.bitstreams.edit.live.move', + { bitstream: bitstreamName, toIndex: toPosition + 1 }); + this.liveRegionService.addMessage(message); + } + + /** + * Adds a message to the live region mentioning that the bitstream with the provided name is no longer selected and + * was returned to the provided position. + * @param bitstreamName The name of the bitstream that is no longer selected + * @param toPosition The zero-indexed position the bitstream returned to. + */ + announceCancel(bitstreamName: string, toPosition: number) { + const message = this.translateService.instant('item.edit.bitstreams.edit.live.cancel', + { bitstream: bitstreamName, toIndex: toPosition + 1 }); + this.liveRegionService.addMessage(message); + } + + /** + * Adds a message to the live region mentioning that the bitstream with the provided name is no longer selected. + * @param bitstreamName The name of the bitstream that is no longer selected. + */ + announceClear(bitstreamName: string) { + const message = this.translateService.instant('item.edit.bitstreams.edit.live.clear', + { bitstream: bitstreamName }); + this.liveRegionService.addMessage(message); + } } diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html index d530fb38d1..06fb571ce4 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html @@ -5,7 +5,8 @@ [hidePagerWhenSinglePage]="true" [hidePaginationDetail]="true" [paginationOptions]="paginationOptions" - [collectionSize]="bitstreamsList.totalElements"> + [collectionSize]="bitstreamsList.totalElements" + [retainScrollPosition]="true"> -
- -
+ diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts index 1502ad2311..25274b8941 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts @@ -58,6 +58,7 @@ describe('ItemEditBitstreamBundleComponent', () => { currentPage: 1, pageSize: 9999 }), + getSelectedBitstream$: observableOf({}), }); beforeEach(waitForAsync(() => { diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts index 24319f4a83..7d2a519baf 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts @@ -1,4 +1,10 @@ -import { Component, EventEmitter, Input, OnInit, Output, ViewChild, ViewContainerRef } from '@angular/core'; +import { + Component, + Input, + OnInit, + ViewChild, + ViewContainerRef, OnDestroy, +} from '@angular/core'; import { Bundle } from '../../../../core/shared/bundle.model'; import { Item } from '../../../../core/shared/item.model'; import { ResponsiveColumnSizes } from '../../../../shared/responsive-table-sizes/responsive-column-sizes'; @@ -8,7 +14,7 @@ import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; import { RemoteData } from 'src/app/core/data/remote-data'; import { PaginatedList } from 'src/app/core/data/paginated-list.model'; import { Bitstream } from 'src/app/core/shared/bitstream.model'; -import { Observable, BehaviorSubject, switchMap } from 'rxjs'; +import { Observable, BehaviorSubject, switchMap, shareReplay, Subscription } from 'rxjs'; import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; import { FieldUpdates } from '../../../../core/data/object-updates/field-updates.model'; import { PaginatedSearchOptions } from '../../../../shared/search/models/paginated-search-options.model'; @@ -17,55 +23,17 @@ import { followLink } from '../../../../shared/utils/follow-link-config.model'; import { getAllSucceededRemoteData, paginatedListToArray, - getFirstSucceededRemoteData } from '../../../../core/shared/operators'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; -import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model'; -import { map, take, filter } from 'rxjs/operators'; +import { map, take, filter, tap, pairwise } from 'rxjs/operators'; import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model'; import { FieldUpdate } from '../../../../core/data/object-updates/field-update.model'; import { PaginationService } from '../../../../core/pagination/pagination.service'; import { PaginationComponent } from '../../../../shared/pagination/pagination.component'; import { RequestService } from '../../../../core/data/request.service'; -import { ItemBitstreamsService } from '../item-bitstreams.service'; -import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; -import { hasValue } from '../../../../shared/empty.util'; -import { LiveRegionService } from '../../../../shared/live-region/live-region.service'; -import { TranslateService } from '@ngx-translate/core'; - -/** - * Interface storing all the information necessary to create a row in the bitstream edit table - */ -export interface BitstreamTableEntry { - /** - * The bitstream - */ - bitstream: Bitstream, - /** - * The uuid of the Bitstream - */ - id: string, - /** - * The name of the Bitstream - */ - name: string, - /** - * The name of the Bitstream with all whitespace removed - */ - nameStripped: string, - /** - * The description of the Bitstream - */ - description: string, - /** - * Observable emitting the Format of the Bitstream - */ - format: Observable, - /** - * The download url of the Bitstream - */ - downloadUrl: string, -} +import { ItemBitstreamsService, BitstreamTableEntry, SelectedBitstreamTableEntry } from '../item-bitstreams.service'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import { hasValue, hasNoValue } from '../../../../shared/empty.util'; @Component({ selector: 'ds-item-edit-bitstream-bundle', @@ -77,7 +45,7 @@ export interface BitstreamTableEntry { * Creates an embedded view of the contents. This is to ensure the table structure won't break. * (which means it'll be added to the parents html without a wrapping ds-item-edit-bitstream-bundle element) */ -export class ItemEditBitstreamBundleComponent implements OnInit { +export class ItemEditBitstreamBundleComponent implements OnInit, OnDestroy { protected readonly FieldChangeType = FieldChangeType; /** @@ -115,13 +83,6 @@ export class ItemEditBitstreamBundleComponent implements OnInit { */ @Input() isFirstTable = false; - /** - * Send an event when the user drops an object on the pagination - * The event contains details about the index the object came from and is dropped to (across the entirety of the list, - * not just within a single page) - */ - @Output() dropObject: EventEmitter = new EventEmitter(); - /** * The bootstrap sizes used for the Bundle Name column * This column stretches over the first 3 columns and thus is a combination of their sizes processed in ngOnInit @@ -138,6 +99,11 @@ export class ItemEditBitstreamBundleComponent implements OnInit { */ bundleName: string; + /** + * The number of bitstreams in the bundle + */ + bundleSize: number; + /** * The bitstreams to show in the table */ @@ -146,7 +112,7 @@ export class ItemEditBitstreamBundleComponent implements OnInit { /** * The data to show in the table */ - tableEntries$: BehaviorSubject = new BehaviorSubject(null); + tableEntries$: BehaviorSubject = new BehaviorSubject([]); /** * The initial page options to use for fetching the bitstreams @@ -158,11 +124,6 @@ export class ItemEditBitstreamBundleComponent implements OnInit { */ currentPaginationOptions$: BehaviorSubject; - /** - * The available page size options - */ - pageSizeOptions: number[]; - /** * The currently selected page size */ @@ -178,6 +139,11 @@ export class ItemEditBitstreamBundleComponent implements OnInit { */ updates$: BehaviorSubject = new BehaviorSubject(null); + /** + * Array containing all subscriptions created by this component + */ + subscriptions: Subscription[] = []; + constructor( protected viewContainerRef: ViewContainerRef, @@ -187,8 +153,6 @@ export class ItemEditBitstreamBundleComponent implements OnInit { protected paginationService: PaginationService, protected requestService: RequestService, protected itemBitstreamsService: ItemBitstreamsService, - protected liveRegionService: LiveRegionService, - protected translateService: TranslateService, ) { } @@ -201,23 +165,27 @@ export class ItemEditBitstreamBundleComponent implements OnInit { this.initializePagination(); this.initializeBitstreams(); + this.initializeSelectionActions(); + } - // this.bitstreamsRD = this. + ngOnDestroy() { + this.subscriptions.forEach(sub => sub?.unsubscribe()); } protected initializePagination() { this.paginationOptions = this.itemBitstreamsService.getInitialBitstreamsPaginationOptions(this.bundleName); - this.pageSizeOptions = this.paginationOptions.pageSizeOptions; - this.currentPaginationOptions$ = new BehaviorSubject(this.paginationOptions); this.pageSize$ = new BehaviorSubject(this.paginationOptions.pageSize); - this.paginationService.getCurrentPagination(this.paginationOptions.id, this.paginationOptions) - .subscribe((pagination) => { - this.currentPaginationOptions$.next(pagination); - this.pageSize$.next(pagination.pageSize); - }); + this.subscriptions.push( + this.paginationService.getCurrentPagination(this.paginationOptions.id, this.paginationOptions) + .subscribe((pagination) => { + this.currentPaginationOptions$.next(pagination); + this.pageSize$.next(pagination.pageSize); + }) + ); + } protected initializeBitstreams() { @@ -233,26 +201,88 @@ export class ItemEditBitstreamBundleComponent implements OnInit { )) ); }), + getAllSucceededRemoteData(), + shareReplay(1), ); - this.bitstreamsRD$.pipe( - getFirstSucceededRemoteData(), - paginatedListToArray(), - ).subscribe((bitstreams) => { - this.objectUpdatesService.initialize(this.bundleUrl, bitstreams, new Date()); - }); + this.subscriptions.push( + this.bitstreamsRD$.pipe( + take(1), + tap(bitstreamsRD => this.bundleSize = bitstreamsRD.payload.totalElements), + paginatedListToArray(), + ).subscribe((bitstreams) => { + this.objectUpdatesService.initialize(this.bundleUrl, bitstreams, new Date()); + }), - this.bitstreamsRD$.pipe( - getAllSucceededRemoteData(), - paginatedListToArray(), - switchMap((bitstreams) => this.objectUpdatesService.getFieldUpdatesExclusive(this.bundleUrl, bitstreams)) - ).subscribe((updates) => this.updates$.next(updates)); + this.bitstreamsRD$.pipe( + paginatedListToArray(), + switchMap((bitstreams) => this.objectUpdatesService.getFieldUpdatesExclusive(this.bundleUrl, bitstreams)) + ).subscribe((updates) => this.updates$.next(updates)), - this.bitstreamsRD$.pipe( - getAllSucceededRemoteData(), - paginatedListToArray(), - map((bitstreams) => this.itemBitstreamsService.mapBitstreamsToTableEntries(bitstreams)), - ).subscribe((tableEntries) => this.tableEntries$.next(tableEntries)); + this.bitstreamsRD$.pipe( + paginatedListToArray(), + map((bitstreams) => this.itemBitstreamsService.mapBitstreamsToTableEntries(bitstreams)), + ).subscribe((tableEntries) => this.tableEntries$.next(tableEntries)), + ); + } + + protected initializeSelectionActions() { + this.subscriptions.push( + this.itemBitstreamsService.getSelectedBitstream$().pipe(pairwise()).subscribe( + ([previousSelection, currentSelection]) => + this.handleSelectedEntryChange(previousSelection, currentSelection)) + ); + } + + /** + * Handles a change in selected bitstream by changing the pagination if the change happened on a different page + * @param previousSelectedEntry The previously selected entry + * @param currentSelectedEntry The currently selected entry + * @protected + */ + protected handleSelectedEntryChange( + previousSelectedEntry: SelectedBitstreamTableEntry, + currentSelectedEntry: SelectedBitstreamTableEntry + ) { + if (hasValue(currentSelectedEntry) && currentSelectedEntry.bundle === this.bundle) { + // If the currently selected bitstream belongs to this bundle, it has possibly moved to a different page. + // In that case we want to change the pagination to the new page. + this.redirectToCurrentPage(currentSelectedEntry); + } + + // If the selection is cancelled or cleared, it is possible the selected bitstream is currently on a different page + // In that case we want to change the pagination to the place where the bitstream was returned to + if (hasNoValue(currentSelectedEntry) && hasValue(previousSelectedEntry) && previousSelectedEntry.bundle === this.bundle) { + this.redirectToOriginalPage(previousSelectedEntry); + } + } + + /** + * Redirect the user to the current page of the provided bitstream if it is on a different page. + * @param bitstreamEntry The entry that the current position will be taken from to determine the page to move to + * @protected + */ + protected redirectToCurrentPage(bitstreamEntry: SelectedBitstreamTableEntry) { + const currentPage = this.getCurrentPage(); + const selectedEntryPage = this.bundleIndexToPage(bitstreamEntry.currentPosition); + + if (currentPage !== selectedEntryPage) { + this.changeToPage(selectedEntryPage); + } + } + + /** + * Redirect the user to the original page of the provided bitstream if it is on a different page. + * @param bitstreamEntry The entry that the original position will be taken from to determine the page to move to + * @protected + */ + protected redirectToOriginalPage(bitstreamEntry: SelectedBitstreamTableEntry) { + const currentPage = this.getCurrentPage(); + const originPage = this.bundleIndexToPage(bitstreamEntry.originalPosition); + + if (currentPage !== originPage) { + this.changeToPage(originPage); + } } /** @@ -283,7 +313,18 @@ export class ItemEditBitstreamBundleComponent implements OnInit { this.objectUpdatesService.removeSingleFieldUpdate(this.bundleUrl, bitstream.uuid); } - getRowClass(update: FieldUpdate): string { + /** + * Returns the css class for a table row depending on the state of the table entry. + * @param update + * @param bitstream + */ + getRowClass(update: FieldUpdate, bitstream: BitstreamTableEntry): string { + const selected = this.itemBitstreamsService.getSelectedBitstream(); + + if (hasValue(selected) && bitstream.id === selected.bitstream.id) { + return 'table-info'; + } + switch (update.changeType) { case FieldChangeType.UPDATE: return 'table-warning'; @@ -296,11 +337,19 @@ export class ItemEditBitstreamBundleComponent implements OnInit { } } + /** + * Changes the page size to the provided page size. + * @param pageSize + */ public doPageSizeChange(pageSize: number) { this.paginationComponent.doPageSizeChange(pageSize); } - dragStart(bitstreamName: string) { + /** + * Handles start of dragging by opening the tooltip mentioning that it is possible to drag a bitstream to a different + * page by dropping it on the page number, only if there are multiple pages. + */ + dragStart() { // Only open the drag tooltip when there are multiple pages this.paginationComponent.shouldShowBottomPager.pipe( take(1), @@ -308,66 +357,170 @@ export class ItemEditBitstreamBundleComponent implements OnInit { ).subscribe(() => { this.dragTooltip.open(); }); - - const message = this.translateService.instant('item.edit.bitstreams.edit.live.drag', - { bitstream: bitstreamName }); - this.liveRegionService.addMessage(message); } - dragEnd(bitstreamName: string) { + /** + * Handles end of dragging by closing the tooltip. + */ + dragEnd() { this.dragTooltip.close(); - - const message = this.translateService.instant('item.edit.bitstreams.edit.live.drop', - { bitstream: bitstreamName }); - this.liveRegionService.addMessage(message); } - + /** + * Handles dropping by calculation the target position, and changing the page if the bitstream was dropped on a + * different page. + * @param event + */ drop(event: CdkDragDrop) { const dragIndex = event.previousIndex; let dropIndex = event.currentIndex; - const dragPage = this.currentPaginationOptions$.value.currentPage - 1; - let dropPage = this.currentPaginationOptions$.value.currentPage - 1; + const dragPage = this.getCurrentPage(); + let dropPage = this.getCurrentPage(); // Check if the user is hovering over any of the pagination's pages at the time of dropping the object const droppedOnElement = document.elementFromPoint(event.dropPoint.x, event.dropPoint.y); if (hasValue(droppedOnElement) && hasValue(droppedOnElement.textContent) && droppedOnElement.classList.contains('page-link')) { // The user is hovering over a page, fetch the page's number from the element - const droppedPage = Number(droppedOnElement.textContent); + let droppedPage = Number(droppedOnElement.textContent); if (hasValue(droppedPage) && !Number.isNaN(droppedPage)) { - dropPage = droppedPage - 1; - dropIndex = 0; + droppedPage -= 1; + + if (droppedPage !== dragPage) { + dropPage = droppedPage; + + if (dropPage > dragPage) { + // When moving to later page, place bitstream at the top + dropIndex = 0; + } else { + // When moving to earlier page, place bitstream at the bottom + dropIndex = this.getCurrentPageSize() - 1; + } + } } } - const isNewPage = dragPage !== dropPage; - // Move the object in the custom order array if the drop happened within the same page - // This allows us to instantly display a change in the order, instead of waiting for the REST API's response first - if (!isNewPage && dragIndex !== dropIndex) { - const currentEntries = [...this.tableEntries$.value]; - moveItemInArray(currentEntries, dragIndex, dropIndex); - this.tableEntries$.next(currentEntries); + const fromIndex = this.pageIndexToBundleIndex(dragIndex, dragPage); + const toIndex = this.pageIndexToBundleIndex(dropIndex, dropPage); + + if (fromIndex === toIndex) { + return; } - const pageSize = this.currentPaginationOptions$.value.pageSize; - const redirectPage = dropPage + 1; - const fromIndex = (dragPage * pageSize) + dragIndex; - const toIndex = (dropPage * pageSize) + dropIndex; - // Send out a drop event (and navigate to the new page) when the "from" and "to" indexes are different from each other - if (fromIndex !== toIndex) { - // if (isNewPage) { - // this.loading$.next(true); - // } - this.dropObject.emit(Object.assign({ - fromIndex, - toIndex, - finish: () => { - if (isNewPage) { - this.paginationComponent.doPageChange(redirectPage); - } - } - })); + const selectedBitstream = this.tableEntries$.value[dragIndex]; + + const finish = () => { + this.itemBitstreamsService.announceMove(selectedBitstream.name, toIndex); + + if (dropPage !== this.getCurrentPage()) { + this.changeToPage(dropPage); + } + }; + + this.itemBitstreamsService.performBitstreamMoveRequest(this.bundle, fromIndex, toIndex, finish); + } + + /** + * Handles a select action for the provided bitstream entry. + * If the selected bitstream is currently selected, the selection is cleared. + * If no, or a different bitstream, is selected, the provided bitstream becomes the selected bitstream. + * @param bitstream + */ + select(bitstream: BitstreamTableEntry) { + const selectedBitstream = this.itemBitstreamsService.getSelectedBitstream(); + + if (hasValue(selectedBitstream) && selectedBitstream.bitstream === bitstream) { + this.itemBitstreamsService.cancelSelection(); + } else { + const selectionObject = this.createBitstreamSelectionObject(bitstream); + + if (hasNoValue(selectionObject)) { + console.warn('Failed to create selection object'); + return; + } + + this.itemBitstreamsService.selectBitstreamEntry(selectionObject); + } + } + + /** + * Creates a {@link SelectedBitstreamTableEntry} from the provided {@link BitstreamTableEntry} so it can be given + * to the {@link ItemBitstreamsService} to select the table entry. + * @param bitstream The table entry to create the selection object from. + * @protected + */ + protected createBitstreamSelectionObject(bitstream: BitstreamTableEntry): SelectedBitstreamTableEntry { + const pageIndex = this.findBitstreamPageIndex(bitstream); + + if (pageIndex === -1) { + return null; } + + const position = this.pageIndexToBundleIndex(pageIndex, this.getCurrentPage()); + + return { + bitstream: bitstream, + bundle: this.bundle, + bundleSize: this.bundleSize, + currentPosition: position, + originalPosition: position, + }; + } + + /** + * Returns the index of the provided {@link BitstreamTableEntry} relative to the current page + * If the current page size is 10, it will return a value from 0 to 9 (inclusive) + * Returns -1 if the provided bitstream could not be found + * @protected + */ + protected findBitstreamPageIndex(bitstream: BitstreamTableEntry): number { + const entries = this.tableEntries$.value; + return entries.findIndex(entry => entry === bitstream); + } + + /** + * Returns the current zero-indexed page + * @protected + */ + protected getCurrentPage(): number { + // The pagination component uses one-based numbering while zero-based numbering is more convenient for calculations + return this.currentPaginationOptions$.value.currentPage - 1; + } + + /** + * Returns the current page size + * @protected + */ + protected getCurrentPageSize(): number { + return this.currentPaginationOptions$.value.pageSize; + } + + /** + * Converts an index relative to the page to an index relative to the bundle + * @param index The index relative to the page + * @param page The zero-indexed page number + * @protected + */ + protected pageIndexToBundleIndex(index: number, page: number) { + return page * this.getCurrentPageSize() + index; + } + + /** + * Calculates the zero-indexed page number from the index relative to the bundle + * @param index The index relative to the bundle + * @protected + */ + protected bundleIndexToPage(index: number) { + return Math.floor(index / this.getCurrentPageSize()); } + /** + * Change the pagination for this bundle to the provided zero-indexed page + * @param page The zero-indexed page to change to + * @protected + */ + protected changeToPage(page: number) { + // Increments page by one because zero-indexing is way easier for calculations but the pagination component + // uses one-indexing. + this.paginationComponent.doPageChange(page + 1); + } } diff --git a/src/app/shared/live-region/live-region.service.stub.ts b/src/app/shared/live-region/live-region.service.stub.ts new file mode 100644 index 0000000000..4f10b46a4c --- /dev/null +++ b/src/app/shared/live-region/live-region.service.stub.ts @@ -0,0 +1,30 @@ +import { of } from 'rxjs'; +import { LiveRegionService } from './live-region.service'; + +export function getLiveRegionServiceStub(): LiveRegionService { + return new LiveRegionServiceStub() as unknown as LiveRegionService; +} + +export class LiveRegionServiceStub { + getMessages = jasmine.createSpy('getMessages').and.returnValue( + ['Message One', 'Message Two'] + ); + + getMessages$ = jasmine.createSpy('getMessages$').and.returnValue( + of(['Message One', 'Message Two']) + ); + + addMessage = jasmine.createSpy('addMessage').and.returnValue('messageId'); + + clear = jasmine.createSpy('clear'); + + clearMessageByUUID = jasmine.createSpy('clearMessageByUUID'); + + getLiveRegionVisibility = jasmine.createSpy('getLiveRegionVisibility').and.returnValue(false); + + setLiveRegionVisibility = jasmine.createSpy('setLiveRegionVisibility'); + + getMessageTimeOutMs = jasmine.createSpy('getMessageTimeOutMs').and.returnValue(30000); + + setMessageTimeOutMs = jasmine.createSpy('setMessageTimeOutMs'); +} diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index ab6f3792ca..519189ed69 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1950,9 +1950,13 @@ "item.edit.bitstreams.edit.buttons.undo": "Undo changes", - "item.edit.bitstreams.edit.live.drag": "{{ bitstream }} grabbed", + "item.edit.bitstreams.edit.live.cancel": "{{ bitstream }} was returned to position {{ toIndex }} and is no longer selected.", - "item.edit.bitstreams.edit.live.drop": "{{ bitstream }} dropped", + "item.edit.bitstreams.edit.live.clear": "{{ bitstream }} is no longer selected.", + + "item.edit.bitstreams.edit.live.select": "{{ bitstream }} is selected.", + + "item.edit.bitstreams.edit.live.move": "{{ bitstream }} is now in position {{ toIndex }}.", "item.edit.bitstreams.empty": "This item doesn't contain any bitstreams. Click the upload button to create one.", From 2e1b1489b65c6c6715774ef482242e8f14d990f0 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Fri, 4 Oct 2024 09:40:20 +0200 Subject: [PATCH 110/720] 118223: Stop space from scrolling down page --- .../item-bitstreams/item-bitstreams.component.ts | 1 + .../item-edit-bitstream-bundle.component.html | 2 +- .../item-edit-bitstream-bundle.component.ts | 12 ++++++++++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts index 4ced3dd649..f77eda02fb 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts @@ -138,6 +138,7 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme // Otherwise we might clear the selection when a different action was intended, e.g. clicking a button or selecting // a different bitstream. if (event.target instanceof Element && event.target.tagName === 'BODY') { + event.preventDefault(); this.itemBitstreamsService.clearSelection(); } } diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html index 06fb571ce4..9afdd2d41c 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html @@ -82,7 +82,7 @@
-
- +
+
{{ entry.name }}
+ (keydown.enter)="select($event, entry)" (keydown.space)="select($event, entry)" (click)="select($event, entry)">
diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts index 7d2a519baf..e2dff2f018 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts @@ -423,9 +423,17 @@ export class ItemEditBitstreamBundleComponent implements OnInit, OnDestroy { * Handles a select action for the provided bitstream entry. * If the selected bitstream is currently selected, the selection is cleared. * If no, or a different bitstream, is selected, the provided bitstream becomes the selected bitstream. - * @param bitstream + * @param event The event that triggered the select action + * @param bitstream The bitstream that is the target of the select action */ - select(bitstream: BitstreamTableEntry) { + select(event: UIEvent, bitstream: BitstreamTableEntry) { + event.preventDefault(); + + if (event instanceof KeyboardEvent && event.repeat) { + // Don't handle hold events, otherwise it would change rapidly between being selected and unselected + return; + } + const selectedBitstream = this.itemBitstreamsService.getSelectedBitstream(); if (hasValue(selectedBitstream) && selectedBitstream.bitstream === bitstream) { From 0920a218762a4aa547c8b9bf72e795e8321e73ff Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Fri, 4 Oct 2024 10:53:42 +0200 Subject: [PATCH 111/720] 118223: Stop sending success notificiations on every move --- .../item-bitstreams.service.ts | 62 +++++++++++++++---- .../item-edit-bitstream-bundle.component.ts | 9 ++- 2 files changed, 58 insertions(+), 13 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts index 21dc415198..f8091d616a 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts @@ -3,7 +3,7 @@ import { PaginationComponentOptions } from '../../../shared/pagination/paginatio import { ResponsiveTableSizes } from '../../../shared/responsive-table-sizes/responsive-table-sizes'; import { ResponsiveColumnSizes } from '../../../shared/responsive-table-sizes/responsive-column-sizes'; import { RemoteData } from '../../../core/data/remote-data'; -import { isNotEmpty, hasValue, hasNoValue } from '../../../shared/empty.util'; +import { hasValue, hasNoValue } from '../../../shared/empty.util'; import { Bundle } from '../../../core/shared/bundle.model'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; @@ -25,6 +25,8 @@ import { BundleDataService } from '../../../core/data/bundle-data.service'; import { RequestService } from '../../../core/data/request.service'; import { LiveRegionService } from '../../../shared/live-region/live-region.service'; +export const MOVE_KEY = 'item.edit.bitstreams.notifications.move'; + /** * Interface storing all the information necessary to create a row in the bitstream edit table */ @@ -164,6 +166,10 @@ export class ItemBitstreamsService { if (hasValue(selected)) { this.updateSelectedBitstream(null); this.announceClear(selected.bitstream.name); + + if (selected.currentPosition !== selected.originalPosition) { + this.displaySuccessNotification(MOVE_KEY); + } } } @@ -265,7 +271,7 @@ export class ItemBitstreamsService { this.isPerformingMoveRequest = true; this.bundleService.patch(bundle, [moveOperation]).pipe( getFirstCompletedRemoteData(), - tap((response: RemoteData) => this.displayNotifications('item.edit.bitstreams.notifications.move', [response])), + tap((response: RemoteData) => this.displayFailedResponseNotifications(MOVE_KEY, [response])), switchMap(() => this.requestService.setStaleByHrefSubstring(bundle.self)), take(1), ).subscribe(() => { @@ -321,19 +327,51 @@ export class ItemBitstreamsService { * @param responses The returned responses to display notifications for */ displayNotifications(key: string, responses: RemoteData[]) { - if (isNotEmpty(responses)) { - const failedResponses = responses.filter((response: RemoteData) => hasValue(response) && response.hasFailed); - const successfulResponses = responses.filter((response: RemoteData) => hasValue(response) && response.hasSucceeded); - - failedResponses.forEach((response: RemoteData) => { - this.notificationsService.error(this.translateService.instant(`${key}.failed.title`), response.errorMessage); - }); - if (successfulResponses.length > 0) { - this.notificationsService.success(this.translateService.instant(`${key}.saved.title`), this.translateService.instant(`${key}.saved.content`)); - } + this.displayFailedResponseNotifications(key, responses); + this.displaySuccessFulResponseNotifications(key, responses); + } + + /** + * Display an error notification for each failed response with their message + * @param key The i18n key for the notification messages + * @param responses The returned responses to display notifications for + */ + displayFailedResponseNotifications(key: string, responses: RemoteData[]) { + const failedResponses = responses.filter((response: RemoteData) => hasValue(response) && response.hasFailed); + failedResponses.forEach((response: RemoteData) => { + this.displayErrorNotification(key, response.errorMessage); + }); + } + + /** + * Display an error notification with the provided key and message + * @param key The i18n key for the notification messages + * @param errorMessage The error message to display + */ + displayErrorNotification(key: string, errorMessage: string) { + this.notificationsService.error(this.translateService.instant(`${key}.failed.title`), errorMessage); + } + + /** + * Display a success notification in case there's at least one successful response + * @param key The i18n key for the notification messages + * @param responses The returned responses to display notifications for + */ + displaySuccessFulResponseNotifications(key: string, responses: RemoteData[]) { + const successfulResponses = responses.filter((response: RemoteData) => hasValue(response) && response.hasSucceeded); + if (successfulResponses.length > 0) { + this.displaySuccessNotification(key); } } + /** + * Display a success notification with the provided key + * @param key The i18n key for the notification messages + */ + displaySuccessNotification(key: string) { + this.notificationsService.success(this.translateService.instant(`${key}.saved.title`), this.translateService.instant(`${key}.saved.content`)); + } + /** * Removes the bitstreams marked for deletion from the Bundles emitted by the provided observable. * @param bundles$ diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts index e2dff2f018..4079ad225b 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts @@ -31,7 +31,12 @@ import { FieldUpdate } from '../../../../core/data/object-updates/field-update.m import { PaginationService } from '../../../../core/pagination/pagination.service'; import { PaginationComponent } from '../../../../shared/pagination/pagination.component'; import { RequestService } from '../../../../core/data/request.service'; -import { ItemBitstreamsService, BitstreamTableEntry, SelectedBitstreamTableEntry } from '../item-bitstreams.service'; +import { + ItemBitstreamsService, + BitstreamTableEntry, + SelectedBitstreamTableEntry, + MOVE_KEY +} from '../item-bitstreams.service'; import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { hasValue, hasNoValue } from '../../../../shared/empty.util'; @@ -414,6 +419,8 @@ export class ItemEditBitstreamBundleComponent implements OnInit, OnDestroy { if (dropPage !== this.getCurrentPage()) { this.changeToPage(dropPage); } + + this.itemBitstreamsService.displaySuccessNotification(MOVE_KEY); }; this.itemBitstreamsService.performBitstreamMoveRequest(this.bundle, fromIndex, toIndex, finish); From b158c5c2a275ff64108229c37083da7e7f78720e Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Fri, 4 Oct 2024 10:58:52 +0200 Subject: [PATCH 112/720] 118223: Move drag tooltip to center of pagination numbers --- .../item-edit-bitstream-bundle.component.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html index 9afdd2d41c..efbdd8c69b 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html @@ -6,7 +6,9 @@ [hidePaginationDetail]="true" [paginationOptions]="paginationOptions" [collectionSize]="bitstreamsList.totalElements" - [retainScrollPosition]="true"> + [retainScrollPosition]="true" + [ngbTooltip]="'item.edit.bitstreams.bundle.tooltip' | translate" placement="bottom" + [autoClose]="false" triggers="manual" #dragTooltip="ngbTooltip"> - + +
{{'item.edit.bitstreams.bundle.name' | translate:{ name: bundleName } }} From 1dcc5d1ec5a21667e90322fb2a6dc4528d8f5c0b Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Fri, 4 Oct 2024 15:16:16 +0200 Subject: [PATCH 113/720] 118223: Add ItemBitstreams service tests --- .../item-bitstreams.service.spec.ts | 443 +++++++++++++++++- .../item-bitstreams.service.ts | 4 +- 2 files changed, 444 insertions(+), 3 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts index e144e81ec7..94adb5f23a 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts @@ -1,4 +1,4 @@ -import { ItemBitstreamsService } from './item-bitstreams.service'; +import { ItemBitstreamsService, SelectedBitstreamTableEntry } from './item-bitstreams.service'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { getMockTranslateService } from '../../../shared/mocks/translate.service.mock'; import { ObjectUpdatesServiceStub } from '../../../core/data/object-updates/object-updates.service.stub'; @@ -22,6 +22,9 @@ import { LiveRegionService } from '../../../shared/live-region/live-region.servi import { Bundle } from '../../../core/shared/bundle.model'; import { of } from 'rxjs'; import { getLiveRegionServiceStub } from '../../../shared/live-region/live-region.service.stub'; +import { fakeAsync, tick } from '@angular/core/testing'; +import createSpy = jasmine.createSpy; +import { MoveOperation } from 'fast-json-patch'; describe('ItemBitstreamsService', () => { let service: ItemBitstreamsService; @@ -60,6 +63,444 @@ describe('ItemBitstreamsService', () => { ); }); + const defaultEntry: SelectedBitstreamTableEntry = { + bitstream: { + name: 'bitstream name', + } as any, + bundle: Object.assign(new Bundle(), { + _links: { self: { href: 'self_link' }}, + }), + bundleSize: 10, + currentPosition: 0, + originalPosition: 0, + }; + + describe('selectBitstreamEntry', () => { + it('should correctly make getSelectedBitstream$ emit', fakeAsync(() => { + const emittedEntries = []; + + service.getSelectedBitstream$().subscribe(selected => emittedEntries.push(selected)); + + expect(emittedEntries.length).toBe(1); + expect(emittedEntries[0]).toBeNull(); + + const entry = Object.assign({}, defaultEntry); + + service.selectBitstreamEntry(entry); + tick(); + + expect(emittedEntries.length).toBe(2); + expect(emittedEntries[1]).toEqual(entry); + })); + + it('should correctly make getSelectedBitstream return the bitstream', () => { + expect(service.getSelectedBitstream()).toBeNull(); + + const entry = Object.assign({}, defaultEntry); + + service.selectBitstreamEntry(entry); + expect(service.getSelectedBitstream()).toEqual(entry); + }); + + it('should correctly make hasSelectedBitstream return', () => { + expect(service.hasSelectedBitstream()).toBeFalse(); + + const entry = Object.assign({}, defaultEntry); + + service.selectBitstreamEntry(entry); + expect(service.hasSelectedBitstream()).toBeTrue(); + }); + + it('should do nothing if no entry was provided', fakeAsync(() => { + const emittedEntries = []; + + service.getSelectedBitstream$().subscribe(selected => emittedEntries.push(selected)); + + expect(emittedEntries.length).toBe(1); + expect(emittedEntries[0]).toBeNull(); + + const entry = Object.assign({}, defaultEntry); + + service.selectBitstreamEntry(entry); + tick(); + + expect(emittedEntries.length).toBe(2); + expect(emittedEntries[1]).toEqual(entry); + + service.selectBitstreamEntry(null); + tick(); + + expect(emittedEntries.length).toBe(2); + expect(emittedEntries[1]).toEqual(entry); + })); + + it('should announce the selected bitstream', () => { + const entry = Object.assign({}, defaultEntry); + + spyOn(service, 'announceSelect'); + + service.selectBitstreamEntry(entry); + expect(service.announceSelect).toHaveBeenCalledWith(entry.bitstream.name); + }); + }); + + describe('clearSelection', () => { + it('should clear the selected bitstream', fakeAsync(() => { + const emittedEntries = []; + + service.getSelectedBitstream$().subscribe(selected => emittedEntries.push(selected)); + + expect(emittedEntries.length).toBe(1); + expect(emittedEntries[0]).toBeNull(); + + const entry = Object.assign({}, defaultEntry); + + service.selectBitstreamEntry(entry); + tick(); + + expect(emittedEntries.length).toBe(2); + expect(emittedEntries[1]).toEqual(entry); + + service.clearSelection(); + tick(); + + expect(emittedEntries.length).toBe(3); + expect(emittedEntries[2]).toBeNull(); + })); + + it('should not do anything if there is no selected bitstream', fakeAsync(() => { + const emittedEntries = []; + + service.getSelectedBitstream$().subscribe(selected => emittedEntries.push(selected)); + + expect(emittedEntries.length).toBe(1); + expect(emittedEntries[0]).toBeNull(); + + service.clearSelection(); + tick(); + + expect(emittedEntries.length).toBe(1); + expect(emittedEntries[0]).toBeNull(); + })); + + it('should announce the cleared bitstream', () => { + const entry = Object.assign({}, defaultEntry); + + spyOn(service, 'announceClear'); + service.selectBitstreamEntry(entry); + service.clearSelection(); + + expect(service.announceClear).toHaveBeenCalledWith(entry.bitstream.name); + }); + + it('should display a notification if the selected bitstream was moved', () => { + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: 7, + } + ); + + spyOn(service, 'displaySuccessNotification'); + service.selectBitstreamEntry(entry); + service.clearSelection(); + + expect(service.displaySuccessNotification).toHaveBeenCalled(); + }); + + it('should not display a notification if the selected bitstream is in its original position', () => { + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 7, + currentPosition: 7, + } + ); + + spyOn(service, 'displaySuccessNotification'); + service.selectBitstreamEntry(entry); + service.clearSelection(); + + expect(service.displaySuccessNotification).not.toHaveBeenCalled(); + }); + }); + + describe('cancelSelection', () => { + it('should clear the selected bitstream', fakeAsync(() => { + const emittedEntries = []; + + service.getSelectedBitstream$().subscribe(selected => emittedEntries.push(selected)); + + expect(emittedEntries.length).toBe(1); + expect(emittedEntries[0]).toBeNull(); + + const entry = Object.assign({}, defaultEntry); + + service.selectBitstreamEntry(entry); + tick(); + + expect(emittedEntries.length).toBe(2); + expect(emittedEntries[1]).toEqual(entry); + + service.cancelSelection(); + tick(); + + expect(emittedEntries.length).toBe(3); + expect(emittedEntries[2]).toBeNull(); + })); + + it('should announce a clear if the bitstream has not moved', () => { + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 7, + currentPosition: 7, + } + ); + + spyOn(service, 'announceClear'); + spyOn(service, 'announceCancel'); + + service.selectBitstreamEntry(entry); + service.cancelSelection(); + + expect(service.announceClear).toHaveBeenCalledWith(entry.bitstream.name); + expect(service.announceCancel).not.toHaveBeenCalled(); + }); + + it('should announce a cancel if the bitstream has moved', () => { + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: 7, + } + ); + + spyOn(service, 'announceClear'); + spyOn(service, 'announceCancel'); + + service.selectBitstreamEntry(entry); + service.cancelSelection(); + + expect(service.announceClear).not.toHaveBeenCalled(); + expect(service.announceCancel).toHaveBeenCalledWith(entry.bitstream.name, entry.originalPosition); + }); + + it('should return the bitstream to its original position if it has moved', () => { + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: 7, + } + ); + + spyOn(service, 'performBitstreamMoveRequest'); + + service.selectBitstreamEntry(entry); + service.cancelSelection(); + + expect(service.performBitstreamMoveRequest).toHaveBeenCalledWith(entry.bundle, entry.currentPosition, entry.originalPosition); + }); + + it('should not move the bitstream if it has not moved', () => { + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 7, + currentPosition: 7, + } + ); + + spyOn(service, 'performBitstreamMoveRequest'); + + service.selectBitstreamEntry(entry); + service.cancelSelection(); + + expect(service.performBitstreamMoveRequest).not.toHaveBeenCalled(); + }); + + it('should not do anything if there is no selected bitstream', () => { + spyOn(service, 'announceClear'); + spyOn(service, 'announceCancel'); + spyOn(service, 'performBitstreamMoveRequest'); + + service.cancelSelection(); + + expect(service.announceClear).not.toHaveBeenCalled(); + expect(service.announceCancel).not.toHaveBeenCalled(); + expect(service.performBitstreamMoveRequest).not.toHaveBeenCalled(); + }); + }); + + describe('moveSelectedBitstream', () => { + beforeEach(() => { + spyOn(service, 'performBitstreamMoveRequest').and.callThrough(); + }); + + describe('up', () => { + it('should move the selected bitstream one position up', () => { + const startPosition = 7; + const endPosition = startPosition - 1; + + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: startPosition, + } + ); + + const movedEntry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: endPosition, + } + ); + + service.selectBitstreamEntry(entry); + service.moveSelectedBitstreamUp(); + expect(service.performBitstreamMoveRequest).toHaveBeenCalledWith(entry.bundle, startPosition, endPosition, jasmine.any(Function)); + expect(service.getSelectedBitstream()).toEqual(movedEntry); + }); + + it('should announce the move', () => { + const startPosition = 7; + const endPosition = startPosition - 1; + + spyOn(service, 'announceMove'); + + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: startPosition, + } + ); + + service.selectBitstreamEntry(entry); + service.moveSelectedBitstreamUp(); + + expect(service.announceMove).toHaveBeenCalledWith(entry.bitstream.name, endPosition); + }); + + it('should not do anything if the bitstream is already at the top', () => { + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: 0, + } + ); + + service.selectBitstreamEntry(entry); + service.moveSelectedBitstreamUp(); + + expect(service.performBitstreamMoveRequest).not.toHaveBeenCalled(); + }); + + it('should not do anything if there is no selected bitstream', () => { + service.moveSelectedBitstreamUp(); + + expect(service.performBitstreamMoveRequest).not.toHaveBeenCalled(); + }); + }); + + describe('down', () => { + it('should move the selected bitstream one position down', () => { + const startPosition = 7; + const endPosition = startPosition + 1; + + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: startPosition, + } + ); + + const movedEntry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: endPosition, + } + ); + + service.selectBitstreamEntry(entry); + service.moveSelectedBitstreamDown(); + expect(service.performBitstreamMoveRequest).toHaveBeenCalledWith(entry.bundle, startPosition, endPosition, jasmine.any(Function)); + expect(service.getSelectedBitstream()).toEqual(movedEntry); + }); + + it('should announce the move', () => { + const startPosition = 7; + const endPosition = startPosition + 1; + + spyOn(service, 'announceMove'); + + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: startPosition, + } + ); + + service.selectBitstreamEntry(entry); + service.moveSelectedBitstreamDown(); + + expect(service.announceMove).toHaveBeenCalledWith(entry.bitstream.name, endPosition); + }); + + it('should not do anything if the bitstream is already at the bottom of the bundle', () => { + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: 9, + } + ); + + service.selectBitstreamEntry(entry); + service.moveSelectedBitstreamDown(); + + expect(service.performBitstreamMoveRequest).not.toHaveBeenCalled(); + }); + + it('should not do anything if there is no selected bitstream', () => { + service.moveSelectedBitstreamDown(); + + expect(service.performBitstreamMoveRequest).not.toHaveBeenCalled(); + }); + }); + }); + + describe('performBitstreamMoveRequest', () => { + const bundle: Bundle = defaultEntry.bundle; + const from = 5; + const to = 7; + const callback = createSpy('callbackFunction'); + + console.log('bundle:', bundle); + + it('should correctly create the Move request', () => { + const expectedOperation: MoveOperation = { + op: 'move', + from: `/_links/bitstreams/${from}/href`, + path: `/_links/bitstreams/${to}/href`, + }; + + service.performBitstreamMoveRequest(bundle, from, to, callback); + expect(bundleDataService.patch).toHaveBeenCalledWith(bundle, [expectedOperation]); + }); + + it('should correctly make the bundle\'s self link stale', () => { + service.performBitstreamMoveRequest(bundle, from, to, callback); + expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith(bundle._links.self.href); + }); + + it('should attempt to show a message should the request have failed', () => { + spyOn(service, 'displayFailedResponseNotifications'); + service.performBitstreamMoveRequest(bundle, from, to, callback); + expect(service.displayFailedResponseNotifications).toHaveBeenCalled(); + }); + + it('should correctly call the provided function once the request has finished', () => { + service.performBitstreamMoveRequest(bundle, from, to, callback); + expect(callback).toHaveBeenCalled(); + }); + }); + describe('displayNotifications', () => { it('should display an error notification if a response failed', () => { const responses = [ diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts index f8091d616a..5b5fb7d63c 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts @@ -143,7 +143,7 @@ export class ItemBitstreamsService { * Select the provided entry */ selectBitstreamEntry(entry: SelectedBitstreamTableEntry) { - if (entry !== this.selectedBitstream$.getValue()) { + if (hasValue(entry) && entry !== this.selectedBitstream$.getValue()) { this.announceSelect(entry.bitstream.name); this.updateSelectedBitstream(entry); } @@ -184,7 +184,7 @@ export class ItemBitstreamsService { return; } - this.selectedBitstream$.next(null); + this.updateSelectedBitstream(null); const originalPosition = selected.originalPosition; const currentPosition = selected.currentPosition; From 0bdb5742e064ca35325aa03530506012e36b1dc4 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Fri, 4 Oct 2024 15:22:43 +0200 Subject: [PATCH 114/720] 118223: Remove unused item-edit-bitstream component --- .../edit-item-page/edit-item-page.module.ts | 2 - .../item-edit-bitstream.component.html | 51 ------ .../item-edit-bitstream.component.spec.ts | 145 ------------------ .../item-edit-bitstream.component.ts | 117 -------------- 4 files changed, 315 deletions(-) delete mode 100644 src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.html delete mode 100644 src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.spec.ts delete mode 100644 src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.ts diff --git a/src/app/item-page/edit-item-page/edit-item-page.module.ts b/src/app/item-page/edit-item-page/edit-item-page.module.ts index 4ae5ebe666..c38b480622 100644 --- a/src/app/item-page/edit-item-page/edit-item-page.module.ts +++ b/src/app/item-page/edit-item-page/edit-item-page.module.ts @@ -15,7 +15,6 @@ import { ItemPrivateComponent } from './item-private/item-private.component'; import { ItemPublicComponent } from './item-public/item-public.component'; import { ItemDeleteComponent } from './item-delete/item-delete.component'; import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.component'; -import { ItemEditBitstreamComponent } from './item-bitstreams/item-edit-bitstream/item-edit-bitstream.component'; import { SearchPageModule } from '../../search-page/search-page.module'; import { ItemCollectionMapperComponent } from './item-collection-mapper/item-collection-mapper.component'; import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component'; @@ -78,7 +77,6 @@ import { ItemRelationshipsComponent, ItemBitstreamsComponent, ItemVersionHistoryComponent, - ItemEditBitstreamComponent, ItemEditBitstreamBundleComponent, EditRelationshipComponent, EditRelationshipListComponent, diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.html deleted file mode 100644 index 0f0fad2199..0000000000 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.html +++ /dev/null @@ -1,51 +0,0 @@ - -
- -
- - {{ bitstreamName }} - -
-
-
-
-
- {{ bitstream?.firstMetadataValue('dc.description') }} -
-
-
-
-
- - {{ (format$ | async)?.shortDescription }} - -
-
-
-
-
- - - - - - -
-
-
-
diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.spec.ts deleted file mode 100644 index aafa5a4fe4..0000000000 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.spec.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { ItemEditBitstreamComponent } from './item-edit-bitstream.component'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; -import { of as observableOf } from 'rxjs'; -import { Bitstream } from '../../../../core/shared/bitstream.model'; -import { TranslateModule } from '@ngx-translate/core'; -import { VarDirective } from '../../../../shared/utils/var.directive'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model'; -import { ResponsiveTableSizes } from '../../../../shared/responsive-table-sizes/responsive-table-sizes'; -import { ResponsiveColumnSizes } from '../../../../shared/responsive-table-sizes/responsive-column-sizes'; -import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; -import { getBitstreamDownloadRoute } from '../../../../app-routing-paths'; -import { By } from '@angular/platform-browser'; -import { BrowserOnlyMockPipe } from '../../../../shared/testing/browser-only-mock.pipe'; - -let comp: ItemEditBitstreamComponent; -let fixture: ComponentFixture; - -const columnSizes = new ResponsiveTableSizes([ - new ResponsiveColumnSizes(2, 2, 3, 4, 4), - new ResponsiveColumnSizes(2, 3, 3, 3, 3), - new ResponsiveColumnSizes(2, 2, 2, 2, 2), - new ResponsiveColumnSizes(6, 5, 4, 3, 3) -]); - -const format = Object.assign(new BitstreamFormat(), { - shortDescription: 'PDF' -}); -const bitstream = Object.assign(new Bitstream(), { - uuid: 'bitstreamUUID', - name: 'Fake Bitstream', - bundleName: 'ORIGINAL', - description: 'Description', - _links: { - content: { href: 'content-link' } - }, - - format: createSuccessfulRemoteDataObject$(format) -}); -const fieldUpdate = { - field: bitstream, - changeType: undefined -}; -const date = new Date(); -const url = 'thisUrl'; - -let objectUpdatesService: ObjectUpdatesService; - -describe('ItemEditBitstreamComponent', () => { - beforeEach(waitForAsync(() => { - objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', - { - getFieldUpdates: observableOf({ - [bitstream.uuid]: fieldUpdate, - }), - getFieldUpdatesExclusive: observableOf({ - [bitstream.uuid]: fieldUpdate, - }), - saveRemoveFieldUpdate: {}, - removeSingleFieldUpdate: {}, - saveAddFieldUpdate: {}, - discardFieldUpdates: {}, - reinstateFieldUpdates: observableOf(true), - initialize: {}, - getUpdatedFields: observableOf([bitstream]), - getLastModified: observableOf(date), - hasUpdates: observableOf(true), - isReinstatable: observableOf(false), - isValidPage: observableOf(true) - } - ); - - TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot()], - declarations: [ - ItemEditBitstreamComponent, - VarDirective, - BrowserOnlyMockPipe, - ], - providers: [ - { provide: ObjectUpdatesService, useValue: objectUpdatesService } - ], schemas: [ - NO_ERRORS_SCHEMA - ] - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(ItemEditBitstreamComponent); - comp = fixture.componentInstance; - comp.fieldUpdate = fieldUpdate; - comp.bundleUrl = url; - comp.columnSizes = columnSizes; - comp.ngOnChanges(undefined); - fixture.detectChanges(); - }); - - describe('when remove is called', () => { - beforeEach(() => { - comp.remove(); - }); - - it('should call saveRemoveFieldUpdate on objectUpdatesService', () => { - expect(objectUpdatesService.saveRemoveFieldUpdate).toHaveBeenCalledWith(url, bitstream); - }); - }); - - describe('when undo is called', () => { - beforeEach(() => { - comp.undo(); - }); - - it('should call removeSingleFieldUpdate on objectUpdatesService', () => { - expect(objectUpdatesService.removeSingleFieldUpdate).toHaveBeenCalledWith(url, bitstream.uuid); - }); - }); - - describe('when canRemove is called', () => { - it('should return true', () => { - expect(comp.canRemove()).toEqual(true); - }); - }); - - describe('when canUndo is called', () => { - it('should return false', () => { - expect(comp.canUndo()).toEqual(false); - }); - }); - - describe('when the component loads', () => { - it('should contain download button with a valid link to the bitstreams download page', () => { - fixture.detectChanges(); - const downloadBtnHref = fixture.debugElement.query(By.css('[data-test="download-button"]')).nativeElement.getAttribute('href'); - expect(downloadBtnHref).toEqual(comp.bitstreamDownloadUrl); - }); - }); - - describe('when the bitstreamDownloadUrl property gets populated', () => { - it('should contain the bitstream download page route', () => { - expect(comp.bitstreamDownloadUrl).not.toEqual(bitstream._links.content.href); - expect(comp.bitstreamDownloadUrl).toEqual(getBitstreamDownloadRoute(bitstream)); - }); - }); -}); diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.ts deleted file mode 100644 index fcb5c706ac..0000000000 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { Component, Input, OnChanges, OnInit, SimpleChanges, ViewChild, ViewContainerRef } from '@angular/core'; -import { Bitstream } from '../../../../core/shared/bitstream.model'; -import cloneDeep from 'lodash/cloneDeep'; -import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; -import { Observable } from 'rxjs'; -import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model'; -import { getRemoteDataPayload, getFirstSucceededRemoteData } from '../../../../core/shared/operators'; -import { ResponsiveTableSizes } from '../../../../shared/responsive-table-sizes/responsive-table-sizes'; -import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; -import { FieldUpdate } from '../../../../core/data/object-updates/field-update.model'; -import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model'; -import { getBitstreamDownloadRoute } from '../../../../app-routing-paths'; - -@Component({ - selector: 'ds-item-edit-bitstream', - styleUrls: ['../item-bitstreams.component.scss'], - templateUrl: './item-edit-bitstream.component.html', -}) -/** - * Component that displays a single bitstream of an item on the edit page - * Creates an embedded view of the contents - * (which means it'll be added to the parents html without a wrapping ds-item-edit-bitstream element) - */ -export class ItemEditBitstreamComponent implements OnChanges, OnInit { - - /** - * The view on the bitstream - */ - @ViewChild('bitstreamView', {static: true}) bitstreamView; - - /** - * The current field, value and state of the bitstream - */ - @Input() fieldUpdate: FieldUpdate; - - /** - * The url of the bundle - */ - @Input() bundleUrl: string; - - /** - * The bootstrap sizes used for the columns within this table - */ - @Input() columnSizes: ResponsiveTableSizes; - - /** - * The bitstream of this field - */ - bitstream: Bitstream; - - /** - * The bitstream's name - */ - bitstreamName: string; - - /** - * The bitstream's download url - */ - bitstreamDownloadUrl: string; - - /** - * The format of the bitstream - */ - format$: Observable; - - constructor(private objectUpdatesService: ObjectUpdatesService, - private dsoNameService: DSONameService, - private viewContainerRef: ViewContainerRef) { - } - - ngOnInit(): void { - this.viewContainerRef.createEmbeddedView(this.bitstreamView); - } - - /** - * Update the current bitstream and its format on changes - * @param changes - */ - ngOnChanges(changes: SimpleChanges): void { - this.bitstream = cloneDeep(this.fieldUpdate.field) as Bitstream; - this.bitstreamName = this.dsoNameService.getName(this.bitstream); - this.bitstreamDownloadUrl = getBitstreamDownloadRoute(this.bitstream); - this.format$ = this.bitstream.format.pipe( - getFirstSucceededRemoteData(), - getRemoteDataPayload() - ); - } - - /** - * Sends a new remove update for this field to the object updates service - */ - remove(): void { - this.objectUpdatesService.saveRemoveFieldUpdate(this.bundleUrl, this.bitstream); - } - - /** - * Cancels the current update for this field in the object updates service - */ - undo(): void { - this.objectUpdatesService.removeSingleFieldUpdate(this.bundleUrl, this.bitstream.uuid); - } - - /** - * Check if a user should be allowed to remove this field - */ - canRemove(): boolean { - return this.fieldUpdate.changeType !== FieldChangeType.REMOVE; - } - - /** - * Check if a user should be allowed to cancel the update to this field - */ - canUndo(): boolean { - return this.fieldUpdate.changeType >= 0; - } - -} From e8379db987317c5cb892989a21297ab5edce8669 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Mon, 7 Oct 2024 11:49:54 +0200 Subject: [PATCH 115/720] 118223: Add item-bitstreams component tests --- .../item-bitstreams.component.spec.ts | 129 +++++++++++++++--- .../item-bitstreams.component.ts | 6 +- .../item-bitstreams.service.stub.ts | 74 ++++++++++ 3 files changed, 192 insertions(+), 17 deletions(-) create mode 100644 src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.stub.ts diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts index a5549a6ba0..d26f815316 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts @@ -26,9 +26,7 @@ import { createPaginatedList } from '../../../shared/testing/utils.test'; import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model'; import { BitstreamDataServiceStub } from '../../../shared/testing/bitstream-data-service.stub'; import { ItemBitstreamsService } from './item-bitstreams.service'; -import { ResponsiveTableSizes } from '../../../shared/responsive-table-sizes/responsive-table-sizes'; -import { ResponsiveColumnSizes } from '../../../shared/responsive-table-sizes/responsive-column-sizes'; -import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; +import { getItemBitstreamsServiceStub, ItemBitstreamsServiceStub } from './item-bitstreams.service.stub'; let comp: ItemBitstreamsComponent; let fixture: ComponentFixture; @@ -80,7 +78,7 @@ let objectCache: ObjectCacheService; let requestService: RequestService; let searchConfig: SearchConfigurationService; let bundleService: BundleDataService; -let itemBitstreamsService: ItemBitstreamsService; +let itemBitstreamsService: ItemBitstreamsServiceStub; describe('ItemBitstreamsComponent', () => { beforeEach(waitForAsync(() => { @@ -152,18 +150,7 @@ describe('ItemBitstreamsComponent', () => { patch: createSuccessfulRemoteDataObject$({}), }); - itemBitstreamsService = jasmine.createSpyObj('itemBitstreamsService', { - getColumnSizes: new ResponsiveTableSizes([ - new ResponsiveColumnSizes(2, 2, 3, 4, 4), - new ResponsiveColumnSizes(2, 3, 3, 3, 3), - new ResponsiveColumnSizes(2, 2, 2, 2, 2), - new ResponsiveColumnSizes(6, 5, 4, 3, 3) - ]), - getSelectedBitstream$: observableOf({}), - getInitialBundlesPaginationOptions: new PaginationComponentOptions(), - removeMarkedBitstreams: createSuccessfulRemoteDataObject$({}), - displayNotifications: undefined, - }); + itemBitstreamsService = getItemBitstreamsServiceStub(); TestBed.configureTestingModule({ imports: [TranslateModule.forRoot()], @@ -218,4 +205,114 @@ describe('ItemBitstreamsComponent', () => { expect(objectUpdatesService.reinstateFieldUpdates).toHaveBeenCalledWith(bundle.self); }); }); + + describe('moveUp', () => { + it('should move the selected bitstream up', () => { + itemBitstreamsService.hasSelectedBitstream.and.returnValue(true); + + const event = { + preventDefault: () => {/* Intentionally empty */}, + } as KeyboardEvent; + comp.moveUp(event); + + expect(itemBitstreamsService.moveSelectedBitstreamUp).toHaveBeenCalled(); + }); + + it('should not do anything if no bitstream is selected', () => { + itemBitstreamsService.hasSelectedBitstream.and.returnValue(false); + + const event = { + preventDefault: () => {/* Intentionally empty */}, + } as KeyboardEvent; + comp.moveUp(event); + + expect(itemBitstreamsService.moveSelectedBitstreamUp).not.toHaveBeenCalled(); + }); + }); + + describe('moveDown', () => { + it('should move the selected bitstream down', () => { + itemBitstreamsService.hasSelectedBitstream.and.returnValue(true); + + const event = { + preventDefault: () => {/* Intentionally empty */}, + } as KeyboardEvent; + comp.moveDown(event); + + expect(itemBitstreamsService.moveSelectedBitstreamDown).toHaveBeenCalled(); + }); + + it('should not do anything if no bitstream is selected', () => { + itemBitstreamsService.hasSelectedBitstream.and.returnValue(false); + + const event = { + preventDefault: () => {/* Intentionally empty */}, + } as KeyboardEvent; + comp.moveDown(event); + + expect(itemBitstreamsService.moveSelectedBitstreamDown).not.toHaveBeenCalled(); + }); + }); + + describe('cancelSelection', () => { + it('should cancel the selection', () => { + itemBitstreamsService.hasSelectedBitstream.and.returnValue(true); + + const event = { + preventDefault: () => {/* Intentionally empty */}, + } as KeyboardEvent; + comp.cancelSelection(event); + + expect(itemBitstreamsService.cancelSelection).toHaveBeenCalled(); + }); + + it('should not do anything if no bitstream is selected', () => { + itemBitstreamsService.hasSelectedBitstream.and.returnValue(false); + + const event = { + preventDefault: () => {/* Intentionally empty */}, + } as KeyboardEvent; + comp.cancelSelection(event); + + expect(itemBitstreamsService.cancelSelection).not.toHaveBeenCalled(); + }); + }); + + describe('clearSelection', () => { + it('should clear the selection', () => { + itemBitstreamsService.hasSelectedBitstream.and.returnValue(true); + + const event = { + target: document.createElement('BODY'), + preventDefault: () => {/* Intentionally empty */}, + } as unknown as KeyboardEvent; + comp.clearSelection(event); + + expect(itemBitstreamsService.clearSelection).toHaveBeenCalled(); + }); + + it('should not do anything if no bitstream is selected', () => { + itemBitstreamsService.hasSelectedBitstream.and.returnValue(false); + + const event = { + target: document.createElement('BODY'), + preventDefault: () => {/* Intentionally empty */}, + } as unknown as KeyboardEvent; + comp.clearSelection(event); + + expect(itemBitstreamsService.clearSelection).not.toHaveBeenCalled(); + }); + + it('should not do anything if the event target is not \'BODY\'', () => { + itemBitstreamsService.hasSelectedBitstream.and.returnValue(true); + + const event = { + target: document.createElement('NOT-BODY'), + preventDefault: () => {/* Intentionally empty */}, + } as unknown as KeyboardEvent; + comp.clearSelection(event); + + expect(itemBitstreamsService.clearSelection).not.toHaveBeenCalled(); + }); + }); }); diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts index f77eda02fb..6ee5dcb545 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts @@ -137,7 +137,11 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme // Only when no specific element is in focus do we want to clear the currently selected bitstream // Otherwise we might clear the selection when a different action was intended, e.g. clicking a button or selecting // a different bitstream. - if (event.target instanceof Element && event.target.tagName === 'BODY') { + if ( + this.itemBitstreamsService.hasSelectedBitstream() && + event.target instanceof Element && + event.target.tagName === 'BODY' + ) { event.preventDefault(); this.itemBitstreamsService.clearSelection(); } diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.stub.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.stub.ts new file mode 100644 index 0000000000..0521bf47f6 --- /dev/null +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.stub.ts @@ -0,0 +1,74 @@ +import { of } from 'rxjs'; +import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; +import { ResponsiveTableSizes } from '../../../shared/responsive-table-sizes/responsive-table-sizes'; +import { ResponsiveColumnSizes } from '../../../shared/responsive-table-sizes/responsive-column-sizes'; +import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; + +export function getItemBitstreamsServiceStub(): ItemBitstreamsServiceStub { + return new ItemBitstreamsServiceStub(); +} + +export class ItemBitstreamsServiceStub { + getSelectedBitstream$ = jasmine.createSpy('getSelectedBitstream$').and + .returnValue(of(null)); + + getSelectedBitstream = jasmine.createSpy('getSelectedBitstream').and + .returnValue(null); + + hasSelectedBitstream = jasmine.createSpy('hasSelectedBitstream').and + .returnValue(false); + + selectBitstreamEntry = jasmine.createSpy('selectBitstreamEntry'); + + clearSelection = jasmine.createSpy('clearSelection'); + + cancelSelection = jasmine.createSpy('cancelSelection'); + + moveSelectedBitstreamUp = jasmine.createSpy('moveSelectedBitstreamUp'); + + moveSelectedBitstreamDown = jasmine.createSpy('moveSelectedBitstreamDown'); + + performBitstreamMoveRequest = jasmine.createSpy('performBitstreamMoveRequest'); + + getInitialBundlesPaginationOptions = jasmine.createSpy('getInitialBundlesPaginationOptions').and + .returnValue(new PaginationComponentOptions()); + + getInitialBitstreamsPaginationOptions = jasmine.createSpy('getInitialBitstreamsPaginationOptions').and + .returnValue(new PaginationComponentOptions()); + + getColumnSizes = jasmine.createSpy('getColumnSizes').and + .returnValue( + new ResponsiveTableSizes([ + new ResponsiveColumnSizes(2, 2, 3, 4, 4), + new ResponsiveColumnSizes(2, 3, 3, 3, 3), + new ResponsiveColumnSizes(2, 2, 2, 2, 2), + new ResponsiveColumnSizes(6, 5, 4, 3, 3) + ]) + ); + + displayNotifications = jasmine.createSpy('displayNotifications'); + + displayFailedResponseNotifications = jasmine.createSpy('displayFailedResponseNotifications'); + + displayErrorNotification = jasmine.createSpy('displayErrorNotification'); + + displaySuccessFulResponseNotifications = jasmine.createSpy('displaySuccessFulResponseNotifications'); + + displaySuccessNotification = jasmine.createSpy('displaySuccessNotification'); + + removeMarkedBitstreams = jasmine.createSpy('removeMarkedBitstreams').and + .returnValue(createSuccessfulRemoteDataObject$({})); + + mapBitstreamsToTableEntries = jasmine.createSpy('mapBitstreamsToTableEntries').and + .returnValue([]); + + nameToHeader = jasmine.createSpy('nameToHeader').and.returnValue('header'); + + stripWhiteSpace = jasmine.createSpy('stripWhiteSpace').and.returnValue('string'); + + announceSelect = jasmine.createSpy('announceSelect'); + + announceMove = jasmine.createSpy('announceMove'); + + announceCancel = jasmine.createSpy('announceCancel'); +} From 7fb4755abaa1759f175ea2d2cff6d93af00d946e Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Tue, 8 Oct 2024 15:51:00 +0200 Subject: [PATCH 116/720] 118223: Add item-edit-bitstream-bundle component tests --- ...em-edit-bitstream-bundle.component.spec.ts | 302 ++++++++++++++++-- .../item-edit-bitstream-bundle.component.ts | 3 +- 2 files changed, 285 insertions(+), 20 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts index 25274b8941..6008b5431f 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts @@ -7,14 +7,19 @@ import { Bundle } from '../../../../core/shared/bundle.model'; import { ResponsiveTableSizes } from '../../../../shared/responsive-table-sizes/responsive-table-sizes'; import { ResponsiveColumnSizes } from '../../../../shared/responsive-table-sizes/responsive-column-sizes'; import { BundleDataService } from '../../../../core/data/bundle-data.service'; -import { of as observableOf } from 'rxjs'; +import { of as observableOf, of, Subject } from 'rxjs'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub'; import { RequestService } from '../../../../core/data/request.service'; import { getMockRequestService } from '../../../../shared/mocks/request.service.mock'; -import { ItemBitstreamsService } from '../item-bitstreams.service'; +import { ItemBitstreamsService, BitstreamTableEntry, SelectedBitstreamTableEntry } from '../item-bitstreams.service'; import { PaginationService } from '../../../../core/pagination/pagination.service'; -import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; +import { getItemBitstreamsServiceStub, ItemBitstreamsServiceStub } from '../item-bitstreams.service.stub'; +import { FieldUpdate } from '../../../../core/data/object-updates/field-update.model'; +import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; +import { createPaginatedList } from '../../../../shared/testing/utils.test'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; describe('ItemEditBitstreamBundleComponent', () => { let comp: ItemEditBitstreamBundleComponent; @@ -43,25 +48,20 @@ describe('ItemEditBitstreamBundleComponent', () => { const restEndpoint = 'fake-rest-endpoint'; const bundleService = jasmine.createSpyObj('bundleService', { getBitstreamsEndpoint: observableOf(restEndpoint), - getBitstreams: null, + getBitstreams: createSuccessfulRemoteDataObject$(createPaginatedList([])), }); - const objectUpdatesService = { - initialize: () => { - // do nothing - }, - }; - - const itemBitstreamsService = jasmine.createSpyObj('itemBitstreamsService', { - getInitialBitstreamsPaginationOptions: Object.assign(new PaginationComponentOptions(), { - id: 'bundles-pagination-options', - currentPage: 1, - pageSize: 9999 - }), - getSelectedBitstream$: observableOf({}), - }); + let objectUpdatesService: any; + let itemBitstreamsService: ItemBitstreamsServiceStub; beforeEach(waitForAsync(() => { + objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', { + initialize: undefined, + getFieldUpdatesExclusive: of(null), + }); + + itemBitstreamsService = getItemBitstreamsServiceStub(); + TestBed.configureTestingModule({ imports: [TranslateModule.forRoot()], declarations: [ItemEditBitstreamBundleComponent], @@ -92,4 +92,270 @@ describe('ItemEditBitstreamBundleComponent', () => { it('should create an embedded view of the component', () => { expect(viewContainerRef.createEmbeddedView).toHaveBeenCalled(); }); + + describe('on selected entry change', () => { + let paginationComponent: any; + let testSubject: Subject = new Subject(); + + beforeEach(() => { + paginationComponent = jasmine.createSpyObj('paginationComponent', { + doPageChange: undefined, + }); + comp.paginationComponent = paginationComponent; + + spyOn(comp, 'getCurrentPageSize').and.returnValue(2); + }); + + it('should move to the page the selected entry is on if were not on that page', () => { + const selectedA: SelectedBitstreamTableEntry = { + bitstream: null, + bundle: bundle, + bundleSize: 5, + originalPosition: 1, + currentPosition: 1, + }; + + const selectedB: SelectedBitstreamTableEntry = { + bitstream: null, + bundle: bundle, + bundleSize: 5, + originalPosition: 1, + currentPosition: 2, + }; + + comp.handleSelectedEntryChange(selectedA, selectedB); + expect(paginationComponent.doPageChange).toHaveBeenCalledWith(2); + }); + + it('should not change page when we are already on the correct page', () => { + const selectedA: SelectedBitstreamTableEntry = { + bitstream: null, + bundle: bundle, + bundleSize: 5, + originalPosition: 0, + currentPosition: 0, + }; + + const selectedB: SelectedBitstreamTableEntry = { + bitstream: null, + bundle: bundle, + bundleSize: 5, + originalPosition: 0, + currentPosition: 1, + }; + + comp.handleSelectedEntryChange(selectedA, selectedB); + expect(paginationComponent.doPageChange).not.toHaveBeenCalled(); + }); + + it('should change to the original page when cancelling', () => { + const selectedA: SelectedBitstreamTableEntry = { + bitstream: null, + bundle: bundle, + bundleSize: 5, + originalPosition: 3, + currentPosition: 0, + }; + + const selectedB = null; + + comp.handleSelectedEntryChange(selectedA, selectedB); + expect(paginationComponent.doPageChange).toHaveBeenCalledWith(2); + }); + + it('should not change page when we are already on the correct page when cancelling', () => { + const selectedA: SelectedBitstreamTableEntry = { + bitstream: null, + bundle: bundle, + bundleSize: 5, + originalPosition: 0, + currentPosition: 3, + }; + + const selectedB = null; + + comp.handleSelectedEntryChange(selectedA, selectedB); + expect(paginationComponent.doPageChange).not.toHaveBeenCalled(); + }); + }); + + describe('getRowClass', () => { + it('should return \'table-info\' when the bitstream is the selected bitstream', () => { + itemBitstreamsService.getSelectedBitstream.and.returnValue({ + bitstream: { id: 'bitstream-id'} + }); + + const bitstreamEntry = { + id: 'bitstream-id', + } as BitstreamTableEntry; + + expect(comp.getRowClass(undefined, bitstreamEntry)).toEqual('table-info'); + }); + + it('should return \'table-warning\' when the update is of type \'UPDATE\'', () => { + const update = { + changeType: FieldChangeType.UPDATE, + } as FieldUpdate; + + expect(comp.getRowClass(update, undefined)).toEqual('table-warning'); + }); + + it('should return \'table-success\' when the update is of type \'ADD\'', () => { + const update = { + changeType: FieldChangeType.ADD, + } as FieldUpdate; + + expect(comp.getRowClass(update, undefined)).toEqual('table-success'); + }); + + it('should return \'table-danger\' when the update is of type \'REMOVE\'', () => { + const update = { + changeType: FieldChangeType.REMOVE, + } as FieldUpdate; + + expect(comp.getRowClass(update, undefined)).toEqual('table-danger'); + }); + + it('should return \'bg-white\' in any other case', () => { + const update = { + changeType: undefined, + } as FieldUpdate; + + expect(comp.getRowClass(update, undefined)).toEqual('bg-white'); + }); + }); + + describe('drag', () => { + let dragTooltip; + let paginationComponent; + + beforeEach(() => { + dragTooltip = jasmine.createSpyObj('dragTooltip', { + open: undefined, + close: undefined, + }); + comp.dragTooltip = dragTooltip; + }); + + describe('Start', () => { + it('should open the tooltip when there are multiple pages', () => { + paginationComponent = jasmine.createSpyObj('paginationComponent', { + doPageChange: undefined, + }, { + shouldShowBottomPager: of(true), + }); + comp.paginationComponent = paginationComponent; + + comp.dragStart(); + expect(dragTooltip.open).toHaveBeenCalled(); + }); + + it('should not open the tooltip when there is only a single page', () => { + paginationComponent = jasmine.createSpyObj('paginationComponent', { + doPageChange: undefined, + }, { + shouldShowBottomPager: of(false), + }); + comp.paginationComponent = paginationComponent; + + comp.dragStart(); + expect(dragTooltip.open).not.toHaveBeenCalled(); + }); + }); + + describe('end', () => { + it('should always close the tooltip', () => { + paginationComponent = jasmine.createSpyObj('paginationComponent', { + doPageChange: undefined, + }, { + shouldShowBottomPager: of(false), + }); + comp.paginationComponent = paginationComponent; + + comp.dragEnd(); + expect(dragTooltip.close).toHaveBeenCalled(); + }); + }); + }); + + describe('drop', () => { + it('should correctly move the bitstream on drop', () => { + const event = { + previousIndex: 1, + currentIndex: 8, + dropPoint: { x: 100, y: 200 }, + } as CdkDragDrop; + + comp.drop(event); + expect(itemBitstreamsService.performBitstreamMoveRequest).toHaveBeenCalledWith(jasmine.any(Bundle), 1, 8, jasmine.any(Function)); + }); + + it('should not move the bitstream if dropped in the same place', () => { + const event = { + previousIndex: 1, + currentIndex: 1, + dropPoint: { x: 100, y: 200 }, + } as CdkDragDrop; + + comp.drop(event); + expect(itemBitstreamsService.performBitstreamMoveRequest).not.toHaveBeenCalled(); + }); + + it('should move to a different page if dropped on a page number', () => { + spyOn(document, 'elementFromPoint').and.returnValue({ + textContent: '2', + classList: { contains: (token: string) => true }, + } as Element); + + const event = { + previousIndex: 1, + currentIndex: 1, + dropPoint: { x: 100, y: 200 }, + } as CdkDragDrop; + + comp.drop(event); + expect(itemBitstreamsService.performBitstreamMoveRequest).toHaveBeenCalledWith(jasmine.any(Bundle), 1, 20, jasmine.any(Function)); + }); + }); + + describe('select', () => { + it('should select the bitstream', () => { + const event = new KeyboardEvent('keydown'); + spyOnProperty(event, 'repeat', 'get').and.returnValue(false); + + const entry = { } as BitstreamTableEntry; + comp.tableEntries$.next([entry]); + + comp.select(event, entry); + expect(itemBitstreamsService.selectBitstreamEntry).toHaveBeenCalledWith(jasmine.objectContaining({ bitstream: entry })); + }); + + it('should cancel the selection if the bitstream already is selected', () => { + const event = new KeyboardEvent('keydown'); + spyOnProperty(event, 'repeat', 'get').and.returnValue(false); + + const entry = { } as BitstreamTableEntry; + comp.tableEntries$.next([entry]); + + itemBitstreamsService.getSelectedBitstream.and.returnValue({ bitstream: entry }); + + comp.select(event, entry); + expect(itemBitstreamsService.selectBitstreamEntry).not.toHaveBeenCalled(); + expect(itemBitstreamsService.cancelSelection).toHaveBeenCalled(); + }); + + it('should not do anything if the user is holding down the select key', () => { + const event = new KeyboardEvent('keydown'); + spyOnProperty(event, 'repeat', 'get').and.returnValue(true); + + const entry = { } as BitstreamTableEntry; + comp.tableEntries$.next([entry]); + + itemBitstreamsService.getSelectedBitstream.and.returnValue({ bitstream: entry }); + + comp.select(event, entry); + expect(itemBitstreamsService.selectBitstreamEntry).not.toHaveBeenCalled(); + expect(itemBitstreamsService.cancelSelection).not.toHaveBeenCalled(); + }); + }); }); diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts index 4079ad225b..7a70ba80dd 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts @@ -243,9 +243,8 @@ export class ItemEditBitstreamBundleComponent implements OnInit, OnDestroy { * Handles a change in selected bitstream by changing the pagination if the change happened on a different page * @param previousSelectedEntry The previously selected entry * @param currentSelectedEntry The currently selected entry - * @protected */ - protected handleSelectedEntryChange( + handleSelectedEntryChange( previousSelectedEntry: SelectedBitstreamTableEntry, currentSelectedEntry: SelectedBitstreamTableEntry ) { From 2b1b9d83d7d2e58a8f26b9843202f1705110fea5 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Wed, 9 Oct 2024 11:14:28 +0200 Subject: [PATCH 117/720] 118223: Include selection action with selection --- .../item-bitstreams.service.spec.ts | 176 ++++++++++++++---- .../item-bitstreams.service.stub.ts | 2 +- .../item-bitstreams.service.ts | 82 ++++++-- ...em-edit-bitstream-bundle.component.spec.ts | 35 +--- .../item-edit-bitstream-bundle.component.ts | 40 ++-- 5 files changed, 233 insertions(+), 102 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts index 94adb5f23a..f2af25f22f 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts @@ -77,20 +77,20 @@ describe('ItemBitstreamsService', () => { describe('selectBitstreamEntry', () => { it('should correctly make getSelectedBitstream$ emit', fakeAsync(() => { - const emittedEntries = []; + const emittedActions = []; - service.getSelectedBitstream$().subscribe(selected => emittedEntries.push(selected)); + service.getSelectionAction$().subscribe(selected => emittedActions.push(selected)); - expect(emittedEntries.length).toBe(1); - expect(emittedEntries[0]).toBeNull(); + expect(emittedActions.length).toBe(1); + expect(emittedActions[0]).toBeNull(); const entry = Object.assign({}, defaultEntry); service.selectBitstreamEntry(entry); tick(); - expect(emittedEntries.length).toBe(2); - expect(emittedEntries[1]).toEqual(entry); + expect(emittedActions.length).toBe(2); + expect(emittedActions[1]).toEqual({ action: 'Selected', selectedEntry: entry }); })); it('should correctly make getSelectedBitstream return the bitstream', () => { @@ -112,26 +112,26 @@ describe('ItemBitstreamsService', () => { }); it('should do nothing if no entry was provided', fakeAsync(() => { - const emittedEntries = []; + const emittedActions = []; - service.getSelectedBitstream$().subscribe(selected => emittedEntries.push(selected)); + service.getSelectionAction$().subscribe(selected => emittedActions.push(selected)); - expect(emittedEntries.length).toBe(1); - expect(emittedEntries[0]).toBeNull(); + expect(emittedActions.length).toBe(1); + expect(emittedActions[0]).toBeNull(); const entry = Object.assign({}, defaultEntry); service.selectBitstreamEntry(entry); tick(); - expect(emittedEntries.length).toBe(2); - expect(emittedEntries[1]).toEqual(entry); + expect(emittedActions.length).toBe(2); + expect(emittedActions[1]).toEqual({ action: 'Selected', selectedEntry: entry }); service.selectBitstreamEntry(null); tick(); - expect(emittedEntries.length).toBe(2); - expect(emittedEntries[1]).toEqual(entry); + expect(emittedActions.length).toBe(2); + expect(emittedActions[1]).toEqual({ action: 'Selected', selectedEntry: entry }); })); it('should announce the selected bitstream', () => { @@ -146,41 +146,41 @@ describe('ItemBitstreamsService', () => { describe('clearSelection', () => { it('should clear the selected bitstream', fakeAsync(() => { - const emittedEntries = []; + const emittedActions = []; - service.getSelectedBitstream$().subscribe(selected => emittedEntries.push(selected)); + service.getSelectionAction$().subscribe(selected => emittedActions.push(selected)); - expect(emittedEntries.length).toBe(1); - expect(emittedEntries[0]).toBeNull(); + expect(emittedActions.length).toBe(1); + expect(emittedActions[0]).toBeNull(); const entry = Object.assign({}, defaultEntry); service.selectBitstreamEntry(entry); tick(); - expect(emittedEntries.length).toBe(2); - expect(emittedEntries[1]).toEqual(entry); + expect(emittedActions.length).toBe(2); + expect(emittedActions[1]).toEqual({ action: 'Selected', selectedEntry: entry }); service.clearSelection(); tick(); - expect(emittedEntries.length).toBe(3); - expect(emittedEntries[2]).toBeNull(); + expect(emittedActions.length).toBe(3); + expect(emittedActions[2]).toEqual({ action: 'Cleared', selectedEntry: entry }); })); it('should not do anything if there is no selected bitstream', fakeAsync(() => { - const emittedEntries = []; + const emittedActions = []; - service.getSelectedBitstream$().subscribe(selected => emittedEntries.push(selected)); + service.getSelectionAction$().subscribe(selected => emittedActions.push(selected)); - expect(emittedEntries.length).toBe(1); - expect(emittedEntries[0]).toBeNull(); + expect(emittedActions.length).toBe(1); + expect(emittedActions[0]).toBeNull(); service.clearSelection(); tick(); - expect(emittedEntries.length).toBe(1); - expect(emittedEntries[0]).toBeNull(); + expect(emittedActions.length).toBe(1); + expect(emittedActions[0]).toBeNull(); })); it('should announce the cleared bitstream', () => { @@ -225,27 +225,53 @@ describe('ItemBitstreamsService', () => { }); describe('cancelSelection', () => { - it('should clear the selected bitstream', fakeAsync(() => { - const emittedEntries = []; + it('should clear the selected bitstream if it has not moved', fakeAsync(() => { + const emittedActions = []; - service.getSelectedBitstream$().subscribe(selected => emittedEntries.push(selected)); + service.getSelectionAction$().subscribe(selected => emittedActions.push(selected)); - expect(emittedEntries.length).toBe(1); - expect(emittedEntries[0]).toBeNull(); + expect(emittedActions.length).toBe(1); + expect(emittedActions[0]).toBeNull(); const entry = Object.assign({}, defaultEntry); service.selectBitstreamEntry(entry); tick(); - expect(emittedEntries.length).toBe(2); - expect(emittedEntries[1]).toEqual(entry); + expect(emittedActions.length).toBe(2); + expect(emittedActions[1]).toEqual({ action: 'Selected', selectedEntry: entry }); service.cancelSelection(); tick(); - expect(emittedEntries.length).toBe(3); - expect(emittedEntries[2]).toBeNull(); + expect(emittedActions.length).toBe(3); + expect(emittedActions[2]).toEqual({ action: 'Cleared', selectedEntry: entry }); + })); + + it('should cancel the selected bitstream if it has moved', fakeAsync(() => { + const emittedActions = []; + + service.getSelectionAction$().subscribe(selected => emittedActions.push(selected)); + + expect(emittedActions.length).toBe(1); + expect(emittedActions[0]).toBeNull(); + + const entry = Object.assign({}, defaultEntry, { + originalPosition: 0, + currentPosition: 3, + }); + + service.selectBitstreamEntry(entry); + tick(); + + expect(emittedActions.length).toBe(2); + expect(emittedActions[1]).toEqual({ action: 'Selected', selectedEntry: entry }); + + service.cancelSelection(); + tick(); + + expect(emittedActions.length).toBe(3); + expect(emittedActions[2]).toEqual({ action: 'Cancelled', selectedEntry: entry }); })); it('should announce a clear if the bitstream has not moved', () => { @@ -359,6 +385,44 @@ describe('ItemBitstreamsService', () => { expect(service.getSelectedBitstream()).toEqual(movedEntry); }); + it('should emit the move', fakeAsync(() => { + const emittedActions = []; + + service.getSelectionAction$().subscribe(selected => emittedActions.push(selected)); + + expect(emittedActions.length).toBe(1); + expect(emittedActions[0]).toBeNull(); + + const startPosition = 7; + const endPosition = startPosition - 1; + + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: startPosition, + } + ); + + const movedEntry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: endPosition, + } + ); + + service.selectBitstreamEntry(entry); + tick(); + + expect(emittedActions.length).toBe(2); + expect(emittedActions[1]).toEqual({ action: 'Selected', selectedEntry: entry }); + + service.moveSelectedBitstreamUp(); + tick(); + + expect(emittedActions.length).toBe(3); + expect(emittedActions[2]).toEqual({ action: 'Moved', selectedEntry: movedEntry }); + })); + it('should announce the move', () => { const startPosition = 7; const endPosition = startPosition - 1; @@ -424,6 +488,44 @@ describe('ItemBitstreamsService', () => { expect(service.getSelectedBitstream()).toEqual(movedEntry); }); + it('should emit the move', fakeAsync(() => { + const emittedActions = []; + + service.getSelectionAction$().subscribe(selected => emittedActions.push(selected)); + + expect(emittedActions.length).toBe(1); + expect(emittedActions[0]).toBeNull(); + + const startPosition = 7; + const endPosition = startPosition + 1; + + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: startPosition, + } + ); + + const movedEntry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: endPosition, + } + ); + + service.selectBitstreamEntry(entry); + tick(); + + expect(emittedActions.length).toBe(2); + expect(emittedActions[1]).toEqual({ action: 'Selected', selectedEntry: entry }); + + service.moveSelectedBitstreamDown(); + tick(); + + expect(emittedActions.length).toBe(3); + expect(emittedActions[2]).toEqual({ action: 'Moved', selectedEntry: movedEntry }); + })); + it('should announce the move', () => { const startPosition = 7; const endPosition = startPosition + 1; diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.stub.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.stub.ts index 0521bf47f6..7aac79fe69 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.stub.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.stub.ts @@ -9,7 +9,7 @@ export function getItemBitstreamsServiceStub(): ItemBitstreamsServiceStub { } export class ItemBitstreamsServiceStub { - getSelectedBitstream$ = jasmine.createSpy('getSelectedBitstream$').and + getSelectionAction$ = jasmine.createSpy('getSelectedBitstream$').and .returnValue(of(null)); getSelectedBitstream = jasmine.createSpy('getSelectedBitstream').and diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts index 5b5fb7d63c..2329107c29 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts @@ -87,6 +87,24 @@ export interface SelectedBitstreamTableEntry { currentPosition: number, } +/** + * Interface storing data regarding a change in selected bitstream + */ +export interface SelectionAction { + /** + * The different types of actions: + * - Selected: Bitstream was selected + * - Moved: Bitstream was moved + * - Cleared: Selection was cleared, bitstream remains at its current position + * - Cancelled: Selection was cancelled, bitstream returns to its original position + */ + action: 'Selected' | 'Moved' | 'Cleared' | 'Cancelled' + /** + * The table entry to which the selection action applies + */ + selectedEntry: SelectedBitstreamTableEntry, +} + /** * This service handles the selection and updating of the bitstreams and their order on the * 'Edit Item' -> 'Bitstreams' page. @@ -99,7 +117,7 @@ export class ItemBitstreamsService { /** * BehaviorSubject which emits every time the selected bitstream changes. */ - protected selectedBitstream$: BehaviorSubject = new BehaviorSubject(null); + protected selectionAction$: BehaviorSubject = new BehaviorSubject(null); protected isPerformingMoveRequest = false; @@ -116,45 +134,68 @@ export class ItemBitstreamsService { } /** - * Returns the observable emitting the currently selected bitstream + * Returns the observable emitting the selection actions */ - getSelectedBitstream$(): Observable { - return this.selectedBitstream$; + getSelectionAction$(): Observable { + return this.selectionAction$; } /** - * Returns a copy of the currently selected bitstream + * Returns the latest selection action */ - getSelectedBitstream(): SelectedBitstreamTableEntry { - const selected = this.selectedBitstream$.getValue(); + getSelectionAction(): SelectionAction { + const action = this.selectionAction$.value; - if (hasNoValue(selected)) { - return selected; + if (hasNoValue(action)) { + return null; } - return Object.assign({}, selected); + return Object.assign({}, action); } + /** + * Returns true if there currently is a selected bitstream + */ hasSelectedBitstream(): boolean { - return hasValue(this.getSelectedBitstream()); + const selectionAction = this.getSelectionAction(); + + if (hasNoValue(selectionAction)) { + return false; + } + + const action = selectionAction.action; + + return action === 'Selected' || action === 'Moved'; + } + + /** + * Returns a copy of the currently selected bitstream + */ + getSelectedBitstream(): SelectedBitstreamTableEntry { + if (!this.hasSelectedBitstream()) { + return null; + } + + const selectionAction = this.getSelectionAction(); + return Object.assign({}, selectionAction.selectedEntry); } /** * Select the provided entry */ selectBitstreamEntry(entry: SelectedBitstreamTableEntry) { - if (hasValue(entry) && entry !== this.selectedBitstream$.getValue()) { + if (hasValue(entry) && entry.bitstream !== this.getSelectedBitstream()?.bitstream) { this.announceSelect(entry.bitstream.name); - this.updateSelectedBitstream(entry); + this.updateSelectionAction({ action: 'Selected', selectedEntry: entry }); } } /** - * Makes the {@link selectedBitstream$} observable emit the provided {@link SelectedBitstreamTableEntry}. + * Makes the {@link selectionAction$} observable emit the provided {@link SelectedBitstreamTableEntry}. * @protected */ - protected updateSelectedBitstream(entry: SelectedBitstreamTableEntry) { - this.selectedBitstream$.next(entry); + protected updateSelectionAction(action: SelectionAction) { + this.selectionAction$.next(action); } /** @@ -164,7 +205,7 @@ export class ItemBitstreamsService { const selected = this.getSelectedBitstream(); if (hasValue(selected)) { - this.updateSelectedBitstream(null); + this.updateSelectionAction({ action: 'Cleared', selectedEntry: selected }); this.announceClear(selected.bitstream.name); if (selected.currentPosition !== selected.originalPosition) { @@ -184,7 +225,6 @@ export class ItemBitstreamsService { return; } - this.updateSelectedBitstream(null); const originalPosition = selected.originalPosition; const currentPosition = selected.currentPosition; @@ -192,9 +232,11 @@ export class ItemBitstreamsService { // If the selected bitstream has not moved, there is no need to return it to its original position if (currentPosition === originalPosition) { this.announceClear(selected.bitstream.name); + this.updateSelectionAction({ action: 'Cleared', selectedEntry: selected }); } else { this.announceCancel(selected.bitstream.name, originalPosition); this.performBitstreamMoveRequest(selected.bundle, currentPosition, originalPosition); + this.updateSelectionAction({ action: 'Cancelled', selectedEntry: selected }); } } @@ -219,7 +261,7 @@ export class ItemBitstreamsService { }; this.performBitstreamMoveRequest(selected.bundle, originalPosition, newPosition, onRequestCompleted); - this.updateSelectedBitstream(selected); + this.updateSelectionAction({ action: 'Moved', selectedEntry: selected }); } } @@ -244,7 +286,7 @@ export class ItemBitstreamsService { }; this.performBitstreamMoveRequest(selected.bundle, originalPosition, newPosition, onRequestCompleted); - this.updateSelectedBitstream(selected); + this.updateSelectionAction({ action: 'Moved', selectedEntry: selected }); } } diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts index 6008b5431f..26a1b0e913 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts @@ -107,15 +107,8 @@ describe('ItemEditBitstreamBundleComponent', () => { }); it('should move to the page the selected entry is on if were not on that page', () => { - const selectedA: SelectedBitstreamTableEntry = { - bitstream: null, - bundle: bundle, - bundleSize: 5, - originalPosition: 1, - currentPosition: 1, - }; - const selectedB: SelectedBitstreamTableEntry = { + const entry: SelectedBitstreamTableEntry = { bitstream: null, bundle: bundle, bundleSize: 5, @@ -123,20 +116,12 @@ describe('ItemEditBitstreamBundleComponent', () => { currentPosition: 2, }; - comp.handleSelectedEntryChange(selectedA, selectedB); + comp.handleSelectionAction({ action: 'Moved', selectedEntry: entry }); expect(paginationComponent.doPageChange).toHaveBeenCalledWith(2); }); it('should not change page when we are already on the correct page', () => { - const selectedA: SelectedBitstreamTableEntry = { - bitstream: null, - bundle: bundle, - bundleSize: 5, - originalPosition: 0, - currentPosition: 0, - }; - - const selectedB: SelectedBitstreamTableEntry = { + const entry: SelectedBitstreamTableEntry = { bitstream: null, bundle: bundle, bundleSize: 5, @@ -144,12 +129,12 @@ describe('ItemEditBitstreamBundleComponent', () => { currentPosition: 1, }; - comp.handleSelectedEntryChange(selectedA, selectedB); + comp.handleSelectionAction({ action: 'Moved', selectedEntry: entry }); expect(paginationComponent.doPageChange).not.toHaveBeenCalled(); }); it('should change to the original page when cancelling', () => { - const selectedA: SelectedBitstreamTableEntry = { + const entry: SelectedBitstreamTableEntry = { bitstream: null, bundle: bundle, bundleSize: 5, @@ -157,14 +142,12 @@ describe('ItemEditBitstreamBundleComponent', () => { currentPosition: 0, }; - const selectedB = null; - - comp.handleSelectedEntryChange(selectedA, selectedB); + comp.handleSelectionAction({ action: 'Cancelled', selectedEntry: entry }); expect(paginationComponent.doPageChange).toHaveBeenCalledWith(2); }); it('should not change page when we are already on the correct page when cancelling', () => { - const selectedA: SelectedBitstreamTableEntry = { + const entry: SelectedBitstreamTableEntry = { bitstream: null, bundle: bundle, bundleSize: 5, @@ -172,9 +155,7 @@ describe('ItemEditBitstreamBundleComponent', () => { currentPosition: 3, }; - const selectedB = null; - - comp.handleSelectedEntryChange(selectedA, selectedB); + comp.handleSelectionAction({ action: 'Cancelled', selectedEntry: entry }); expect(paginationComponent.doPageChange).not.toHaveBeenCalled(); }); }); diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts index 7a70ba80dd..2c7d8ca60f 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts @@ -25,7 +25,7 @@ import { paginatedListToArray, } from '../../../../core/shared/operators'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; -import { map, take, filter, tap, pairwise } from 'rxjs/operators'; +import { map, take, filter, tap } from 'rxjs/operators'; import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model'; import { FieldUpdate } from '../../../../core/data/object-updates/field-update.model'; import { PaginationService } from '../../../../core/pagination/pagination.service'; @@ -35,7 +35,7 @@ import { ItemBitstreamsService, BitstreamTableEntry, SelectedBitstreamTableEntry, - MOVE_KEY + MOVE_KEY, SelectionAction } from '../item-bitstreams.service'; import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { hasValue, hasNoValue } from '../../../../shared/empty.util'; @@ -233,31 +233,37 @@ export class ItemEditBitstreamBundleComponent implements OnInit, OnDestroy { protected initializeSelectionActions() { this.subscriptions.push( - this.itemBitstreamsService.getSelectedBitstream$().pipe(pairwise()).subscribe( - ([previousSelection, currentSelection]) => - this.handleSelectedEntryChange(previousSelection, currentSelection)) + this.itemBitstreamsService.getSelectionAction$().subscribe( + selectionAction => this.handleSelectionAction(selectionAction)) ); } /** * Handles a change in selected bitstream by changing the pagination if the change happened on a different page - * @param previousSelectedEntry The previously selected entry - * @param currentSelectedEntry The currently selected entry + * @param selectionAction */ - handleSelectedEntryChange( - previousSelectedEntry: SelectedBitstreamTableEntry, - currentSelectedEntry: SelectedBitstreamTableEntry - ) { - if (hasValue(currentSelectedEntry) && currentSelectedEntry.bundle === this.bundle) { + handleSelectionAction(selectionAction: SelectionAction) { + if (hasNoValue(selectionAction) || selectionAction.selectedEntry.bundle !== this.bundle) { + return; + } + + if (selectionAction.action === 'Moved') { // If the currently selected bitstream belongs to this bundle, it has possibly moved to a different page. // In that case we want to change the pagination to the new page. - this.redirectToCurrentPage(currentSelectedEntry); + this.redirectToCurrentPage(selectionAction.selectedEntry); + } + + if (selectionAction.action === 'Cancelled') { + // If the selection is cancelled (and returned to its original position), it is possible the previously selected + // bitstream is returned to a different page. In that case we want to change the pagination to the place where + // the bitstream was returned to. + this.redirectToOriginalPage(selectionAction.selectedEntry); } - // If the selection is cancelled or cleared, it is possible the selected bitstream is currently on a different page - // In that case we want to change the pagination to the place where the bitstream was returned to - if (hasNoValue(currentSelectedEntry) && hasValue(previousSelectedEntry) && previousSelectedEntry.bundle === this.bundle) { - this.redirectToOriginalPage(previousSelectedEntry); + if (selectionAction.action === 'Cleared') { + // If the selection is cleared, it is possible the previously selected bitstream is on a different page. In that + // case we want to change the pagination to the place where the bitstream is. + this.redirectToCurrentPage(selectionAction.selectedEntry); } } From 8d93f22767edbf18396edc49b16629967f04c287 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Wed, 9 Oct 2024 12:01:33 +0200 Subject: [PATCH 118/720] 119176: Make table horizontally scrollable For most screen sizes, the ResponsiveTableSizes is enough to resize the table columns. On very small screens, or when zoomed in a lot, even the smallest column sizes are too big. To make it possible to view the rest of the content even in these situations, the ability to scroll horizontally is added. --- .../item-bitstreams/item-bitstreams.component.html | 2 +- .../item-bitstreams/item-bitstreams.component.scss | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html index b9af2a7d18..7789b68278 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html @@ -27,7 +27,7 @@ -
+
Date: Mon, 14 Oct 2024 11:30:48 +0200 Subject: [PATCH 119/720] 119176: Announce notification content in live region --- .../models/notification-options.model.ts | 12 +++- .../notifications-board.component.spec.ts | 49 ++++++++++++++- .../notifications-board.component.ts | 60 +++++++++++++------ 3 files changed, 97 insertions(+), 24 deletions(-) diff --git a/src/app/shared/notifications/models/notification-options.model.ts b/src/app/shared/notifications/models/notification-options.model.ts index 65011496b3..c891781d9d 100644 --- a/src/app/shared/notifications/models/notification-options.model.ts +++ b/src/app/shared/notifications/models/notification-options.model.ts @@ -4,19 +4,25 @@ export interface INotificationOptions { timeOut: number; clickToClose: boolean; animate: NotificationAnimationsType | string; + announceContentInLiveRegion: boolean; } export class NotificationOptions implements INotificationOptions { public timeOut: number; public clickToClose: boolean; public animate: any; + public announceContentInLiveRegion: boolean; - constructor(timeOut = 5000, - clickToClose = true, - animate: NotificationAnimationsType | string = NotificationAnimationsType.Scale) { + constructor( + timeOut = 5000, + clickToClose = true, + animate: NotificationAnimationsType | string = NotificationAnimationsType.Scale, + announceContentInLiveRegion: boolean = true, + ) { this.timeOut = timeOut; this.clickToClose = clickToClose; this.animate = animate; + this.announceContentInLiveRegion = announceContentInLiveRegion; } } diff --git a/src/app/shared/notifications/notifications-board/notifications-board.component.spec.ts b/src/app/shared/notifications/notifications-board/notifications-board.component.spec.ts index 08b9585a8c..73f4e6b1b1 100644 --- a/src/app/shared/notifications/notifications-board/notifications-board.component.spec.ts +++ b/src/app/shared/notifications/notifications-board/notifications-board.component.spec.ts @@ -1,4 +1,4 @@ -import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, inject, TestBed, waitForAsync, fakeAsync, flush } from '@angular/core/testing'; import { BrowserModule, By } from '@angular/platform-browser'; import { ChangeDetectorRef } from '@angular/core'; @@ -15,14 +15,20 @@ import uniqueId from 'lodash/uniqueId'; import { INotificationBoardOptions } from '../../../../config/notifications-config.interfaces'; import { NotificationsServiceStub } from '../../testing/notifications-service.stub'; import { cold } from 'jasmine-marbles'; +import { LiveRegionService } from '../../live-region/live-region.service'; +import { LiveRegionServiceStub } from '../../live-region/live-region.service.stub'; +import { NotificationOptions } from '../models/notification-options.model'; export const bools = { f: false, t: true }; describe('NotificationsBoardComponent', () => { let comp: NotificationsBoardComponent; let fixture: ComponentFixture; + let liveRegionService: LiveRegionServiceStub; beforeEach(waitForAsync(() => { + liveRegionService = new LiveRegionServiceStub(); + TestBed.configureTestingModule({ imports: [ BrowserModule, @@ -36,7 +42,9 @@ describe('NotificationsBoardComponent', () => { declarations: [NotificationsBoardComponent, NotificationComponent], // declare the test component providers: [ { provide: NotificationsService, useClass: NotificationsServiceStub }, - ChangeDetectorRef] + { provide: LiveRegionService, useValue: liveRegionService }, + ChangeDetectorRef, + ] }).compileComponents(); // compile template and css })); @@ -106,5 +114,42 @@ describe('NotificationsBoardComponent', () => { }); }); + describe('add', () => { + beforeEach(() => { + liveRegionService.addMessage.calls.reset(); + }); + + it('should announce content to the live region', fakeAsync(() => { + const notification = new Notification('id', NotificationType.Info, 'title', 'content'); + comp.add(notification); + + flush(); + + expect(liveRegionService.addMessage).toHaveBeenCalledWith('content'); + })); + + it('should not announce anything if there is no content', fakeAsync(() => { + const notification = new Notification('id', NotificationType.Info, 'title'); + comp.add(notification); + + flush(); + + expect(liveRegionService.addMessage).not.toHaveBeenCalled(); + })); + + it('should not announce the content if disabled', fakeAsync(() => { + const options = new NotificationOptions(); + options.announceContentInLiveRegion = false; + + const notification = new Notification('id', NotificationType.Info, 'title', 'content'); + notification.options = options; + comp.add(notification); + + flush(); + + expect(liveRegionService.addMessage).not.toHaveBeenCalled(); + })); + }); + }) ; diff --git a/src/app/shared/notifications/notifications-board/notifications-board.component.ts b/src/app/shared/notifications/notifications-board/notifications-board.component.ts index 97ae09c1a6..eaba659678 100644 --- a/src/app/shared/notifications/notifications-board/notifications-board.component.ts +++ b/src/app/shared/notifications/notifications-board/notifications-board.component.ts @@ -9,7 +9,7 @@ import { } from '@angular/core'; import { select, Store } from '@ngrx/store'; -import { BehaviorSubject, Subscription } from 'rxjs'; +import { BehaviorSubject, Subscription, of as observableOf } from 'rxjs'; import difference from 'lodash/difference'; import { NotificationsService } from '../notifications.service'; @@ -18,6 +18,9 @@ import { notificationsStateSelector } from '../selectors'; import { INotification } from '../models/notification.model'; import { NotificationsState } from '../notifications.reducers'; import { INotificationBoardOptions } from '../../../../config/notifications-config.interfaces'; +import { LiveRegionService } from '../../live-region/live-region.service'; +import { hasNoValue, isNotEmptyOperator } from '../../empty.util'; +import { take } from 'rxjs/operators'; @Component({ selector: 'ds-notifications-board', @@ -49,9 +52,12 @@ export class NotificationsBoardComponent implements OnInit, OnDestroy { */ public isPaused$: BehaviorSubject = new BehaviorSubject(false); - constructor(private service: NotificationsService, - private store: Store, - private cdr: ChangeDetectorRef) { + constructor( + private service: NotificationsService, + private store: Store, + private cdr: ChangeDetectorRef, + protected liveRegionService: LiveRegionService, + ) { } ngOnInit(): void { @@ -85,6 +91,7 @@ export class NotificationsBoardComponent implements OnInit, OnDestroy { this.notifications.splice(this.notifications.length - 1, 1); } this.notifications.splice(0, 0, item); + this.addContentToLiveRegion(item); } else { // Remove the notification from the store // This notification was in the store, but not in this.notifications @@ -93,29 +100,44 @@ export class NotificationsBoardComponent implements OnInit, OnDestroy { } } + /** + * Adds the content of the notification (if any) to the live region, so it can be announced by screen readers. + */ + private addContentToLiveRegion(item: INotification) { + let content = item.content; + + if (!item.options.announceContentInLiveRegion || hasNoValue(content)) { + return; + } + + if (typeof content === 'string') { + content = observableOf(content); + } + + content.pipe( + isNotEmptyOperator(), + take(1), + ).subscribe(contentStr => this.liveRegionService.addMessage(contentStr)); + } + + /** + * Whether to block the provided item because a duplicate notification with the exact same information already + * exists within the notifications array. + * @param item The item to check + * @return true if the notifications array already contains a notification with the exact same information as the + * provided item. false otherwise. + * @private + */ private block(item: INotification): boolean { const toCheck = item.html ? this.checkHtml : this.checkStandard; + this.notifications.forEach((notification) => { if (toCheck(notification, item)) { return true; } }); - if (this.notifications.length > 0) { - this.notifications.forEach((notification) => { - if (toCheck(notification, item)) { - return true; - } - }); - } - - let comp: INotification; - if (this.notifications.length > 0) { - comp = this.notifications[0]; - } else { - return false; - } - return toCheck(comp, item); + return false; } private checkStandard(checker: INotification, item: INotification): boolean { From 93f9341387755efc14d3272f7f9cca452ab91996 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Wed, 16 Oct 2024 14:11:01 +0200 Subject: [PATCH 120/720] 119176: Add aria-labels to buttons --- .../item-edit-bitstream-bundle.component.html | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html index efbdd8c69b..06201b1cbe 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html @@ -40,6 +40,7 @@
-
+
+ + diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss index 985516ab12..7fd1f4b31e 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss @@ -43,3 +43,13 @@ .scrollable-table { overflow-x: auto; } + +.disabled-overlay { + opacity: 0.6; +} + +.loading-overlay { + position: fixed; + top: 50%; + left: 50%; +} diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts index 6ee5dcb545..72f85675c9 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts @@ -59,6 +59,11 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme */ itemUpdateSubscription: Subscription; + /** + * An observable which emits a boolean which represents whether the service is currently handling a 'move' request + */ + isProcessingMoveRequest: Observable; + constructor( public itemService: ItemDataService, public objectUpdatesService: ObjectUpdatesService, @@ -84,6 +89,7 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme */ postItemInit(): void { const bundlesOptions = this.itemBitstreamsService.getInitialBundlesPaginationOptions(); + this.isProcessingMoveRequest = this.itemBitstreamsService.getPerformingMoveRequest$(); this.bundles$ = this.itemService.getBundles(this.item.id, new PaginatedSearchOptions({pagination: bundlesOptions})).pipe( getFirstSucceededRemoteData(), diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts index f2af25f22f..a0277ef064 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts @@ -573,8 +573,6 @@ describe('ItemBitstreamsService', () => { const to = 7; const callback = createSpy('callbackFunction'); - console.log('bundle:', bundle); - it('should correctly create the Move request', () => { const expectedOperation: MoveOperation = { op: 'move', @@ -601,6 +599,22 @@ describe('ItemBitstreamsService', () => { service.performBitstreamMoveRequest(bundle, from, to, callback); expect(callback).toHaveBeenCalled(); }); + + it('should emit at the start and end of the request', fakeAsync(() => { + const emittedActions = []; + + service.getPerformingMoveRequest$().subscribe(selected => emittedActions.push(selected)); + + expect(emittedActions.length).toBe(1); + expect(emittedActions[0]).toBeFalse(); + + service.performBitstreamMoveRequest(bundle, from, to, callback); + tick(); + + expect(emittedActions.length).toBe(3); + expect(emittedActions[1]).toBeTrue(); + expect(emittedActions[2]).toBeFalse(); + })); }); describe('displayNotifications', () => { diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.stub.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.stub.ts index 7aac79fe69..f60693f726 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.stub.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.stub.ts @@ -30,6 +30,10 @@ export class ItemBitstreamsServiceStub { performBitstreamMoveRequest = jasmine.createSpy('performBitstreamMoveRequest'); + getPerformingMoveRequest = jasmine.createSpy('getPerformingMoveRequest').and.returnValue(false); + + getPerformingMoveRequest$ = jasmine.createSpy('getPerformingMoveRequest$').and.returnValue(of(false)); + getInitialBundlesPaginationOptions = jasmine.createSpy('getInitialBundlesPaginationOptions').and .returnValue(new PaginationComponentOptions()); diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts index 2329107c29..9bbf380487 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts @@ -119,7 +119,7 @@ export class ItemBitstreamsService { */ protected selectionAction$: BehaviorSubject = new BehaviorSubject(null); - protected isPerformingMoveRequest = false; + protected isPerformingMoveRequest: BehaviorSubject = new BehaviorSubject(false); constructor( protected notificationsService: NotificationsService, @@ -221,7 +221,7 @@ export class ItemBitstreamsService { cancelSelection() { const selected = this.getSelectedBitstream(); - if (hasNoValue(selected) || this.isPerformingMoveRequest) { + if (hasNoValue(selected) || this.getPerformingMoveRequest()) { return; } @@ -247,7 +247,7 @@ export class ItemBitstreamsService { moveSelectedBitstreamUp() { const selected = this.getSelectedBitstream(); - if (hasNoValue(selected) || this.isPerformingMoveRequest) { + if (hasNoValue(selected) || this.getPerformingMoveRequest()) { return; } @@ -272,7 +272,7 @@ export class ItemBitstreamsService { moveSelectedBitstreamDown() { const selected = this.getSelectedBitstream(); - if (hasNoValue(selected) || this.isPerformingMoveRequest) { + if (hasNoValue(selected) || this.getPerformingMoveRequest()) { return; } @@ -299,7 +299,7 @@ export class ItemBitstreamsService { * @param finish Optional: Function to execute once the response has been received */ performBitstreamMoveRequest(bundle: Bundle, fromIndex: number, toIndex: number, finish?: () => void) { - if (this.isPerformingMoveRequest) { + if (this.getPerformingMoveRequest()) { console.warn('Attempted to perform move request while previous request has not completed yet'); return; } @@ -310,18 +310,34 @@ export class ItemBitstreamsService { path: `/_links/bitstreams/${toIndex}/href`, }; - this.isPerformingMoveRequest = true; + this.announceLoading(); + this.isPerformingMoveRequest.next(true); this.bundleService.patch(bundle, [moveOperation]).pipe( getFirstCompletedRemoteData(), tap((response: RemoteData) => this.displayFailedResponseNotifications(MOVE_KEY, [response])), switchMap(() => this.requestService.setStaleByHrefSubstring(bundle.self)), take(1), ).subscribe(() => { - this.isPerformingMoveRequest = false; + console.log('got here!'); + this.isPerformingMoveRequest.next(false); finish?.(); }); } + /** + * Whether the service currently is processing a 'move' request + */ + getPerformingMoveRequest(): boolean { + return this.isPerformingMoveRequest.value; + } + + /** + * Returns an observable which emits when the service starts, or ends, processing a 'move' request + */ + getPerformingMoveRequest$(): Observable { + return this.isPerformingMoveRequest; + } + /** * Returns the pagination options to use when fetching the bundles */ @@ -526,4 +542,12 @@ export class ItemBitstreamsService { { bitstream: bitstreamName }); this.liveRegionService.addMessage(message); } + + /** + * Adds a message to the live region mentioning that the + */ + announceLoading() { + const message = this.translateService.instant('item.edit.bitstreams.edit.live.loading'); + this.liveRegionService.addMessage(message); + } } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 519189ed69..9007982a72 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1954,6 +1954,8 @@ "item.edit.bitstreams.edit.live.clear": "{{ bitstream }} is no longer selected.", + "item.edit.bitstreams.edit.live.loading": "Waiting for move to complete.", + "item.edit.bitstreams.edit.live.select": "{{ bitstream }} is selected.", "item.edit.bitstreams.edit.live.move": "{{ bitstream }} is now in position {{ toIndex }}.", From 9486ab5fa1d3d0fc11ceb9f5063bbda61a078924 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 18 Oct 2024 14:26:48 -0500 Subject: [PATCH 154/720] Fix code scanning alert no. 6: Incomplete string escaping or encoding Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> (cherry picked from commit 372444c50ac28a6c7f68b20695bea616a3ab8b7f) --- src/app/core/shared/metadata.utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/core/shared/metadata.utils.ts b/src/app/core/shared/metadata.utils.ts index 87a90b53a3..e48b2b0c44 100644 --- a/src/app/core/shared/metadata.utils.ts +++ b/src/app/core/shared/metadata.utils.ts @@ -157,7 +157,7 @@ export class Metadata { const outputKeys: string[] = []; for (const inputKey of inputKeys) { if (inputKey.includes('*')) { - const inputKeyRegex = new RegExp('^' + inputKey.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$'); + const inputKeyRegex = new RegExp('^' + inputKey.replace(/\\/g, '\\\\').replace(/\./g, '\\.').replace(/\*/g, '.*') + '$'); for (const mapKey of Object.keys(mdMap)) { if (!outputKeys.includes(mapKey) && inputKeyRegex.test(mapKey)) { outputKeys.push(mapKey); From 5c877f56e92e07633efecab036a1493b8a06c465 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 23 Oct 2024 14:11:53 -0500 Subject: [PATCH 155/720] Remove unused/unmaintained postcss-apply --- package.json | 1 - postcss.config.js | 1 - yarn.lock | 21 --------------------- 3 files changed, 23 deletions(-) diff --git a/package.json b/package.json index 5197b720dc..b3988ad5d0 100644 --- a/package.json +++ b/package.json @@ -185,7 +185,6 @@ "ngx-mask": "^13.1.7", "nodemon": "^2.0.22", "postcss": "^8.4", - "postcss-apply": "0.12.0", "postcss-import": "^14.0.0", "postcss-loader": "^4.0.3", "postcss-preset-env": "^7.4.2", diff --git a/postcss.config.js b/postcss.config.js index df092d1d39..14013bd4f8 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -2,7 +2,6 @@ module.exports = { plugins: [ require('postcss-import')(), require('postcss-preset-env')(), - require('postcss-apply')(), require('postcss-responsive-type')() ] }; diff --git a/yarn.lock b/yarn.lock index 3f6a555df3..3a1f482a96 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9279,11 +9279,6 @@ performance-now@^2.1.0: resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== -picocolors@^0.2.1: - version "0.2.1" - resolved "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz" - integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA== - picocolors@^1.0.0, picocolors@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" @@ -9352,14 +9347,6 @@ possible-typed-array-names@^1.0.0: resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== -postcss-apply@0.12.0: - version "0.12.0" - resolved "https://registry.npmjs.org/postcss-apply/-/postcss-apply-0.12.0.tgz" - integrity sha512-u8qZLyA9P86cD08IhqjSVV8tf1eGiKQ4fPvjcG3Ic/eOU65EAkDQClp8We7d15TG+RIWRVPSy9v7cJ2D9OReqw== - dependencies: - balanced-match "^1.0.0" - postcss "^7.0.14" - postcss-attribute-case-insensitive@^5.0.2: version "5.0.2" resolved "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz" @@ -9693,14 +9680,6 @@ postcss@^6.0.6: source-map "^0.6.1" supports-color "^5.4.0" -postcss@^7.0.14: - version "7.0.39" - resolved "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz" - integrity sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA== - dependencies: - picocolors "^0.2.1" - source-map "^0.6.1" - postcss@^8.2.14, postcss@^8.3.11, postcss@^8.3.7, postcss@^8.4, postcss@^8.4.19: version "8.4.47" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.47.tgz#5bf6c9a010f3e724c503bf03ef7947dcb0fea365" From 425078dc4e60aac89151c5ab516597faca2271f6 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 23 Oct 2024 14:13:33 -0500 Subject: [PATCH 156/720] Remove unused postcss-responsive-type --- package.json | 1 - postcss.config.js | 3 +-- yarn.lock | 20 ++------------------ 3 files changed, 3 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index b3988ad5d0..aaf3c17656 100644 --- a/package.json +++ b/package.json @@ -188,7 +188,6 @@ "postcss-import": "^14.0.0", "postcss-loader": "^4.0.3", "postcss-preset-env": "^7.4.2", - "postcss-responsive-type": "1.0.0", "react": "^16.14.0", "react-dom": "^16.14.0", "rimraf": "^3.0.2", diff --git a/postcss.config.js b/postcss.config.js index 14013bd4f8..f8b9666b31 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,7 +1,6 @@ module.exports = { plugins: [ require('postcss-import')(), - require('postcss-preset-env')(), - require('postcss-responsive-type')() + require('postcss-preset-env')() ] }; diff --git a/yarn.lock b/yarn.lock index 3a1f482a96..b05cebf7cd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3937,7 +3937,7 @@ chalk@^1.1.1: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2: +chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.2: version "2.4.2" resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -9635,13 +9635,6 @@ postcss-replace-overflow-wrap@^4.0.0: resolved "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz" integrity sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw== -postcss-responsive-type@1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/postcss-responsive-type/-/postcss-responsive-type-1.0.0.tgz" - integrity sha512-O4kAKbc4RLnSkzcguJ6ojW67uOfeILaj+8xjsO0quLU94d8BKCqYwwFEUVRNbj0YcXA6d3uF/byhbaEATMRVig== - dependencies: - postcss "^6.0.6" - postcss-selector-not@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-6.0.1.tgz" @@ -9671,15 +9664,6 @@ postcss@8.4.31: picocolors "^1.0.0" source-map-js "^1.0.2" -postcss@^6.0.6: - version "6.0.23" - resolved "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz" - integrity sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag== - dependencies: - chalk "^2.4.1" - source-map "^0.6.1" - supports-color "^5.4.0" - postcss@^8.2.14, postcss@^8.3.11, postcss@^8.3.7, postcss@^8.4, postcss@^8.4.19: version "8.4.47" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.47.tgz#5bf6c9a010f3e724c503bf03ef7947dcb0fea365" @@ -11315,7 +11299,7 @@ supports-color@^2.0.0: resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" integrity sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g== -supports-color@^5.3.0, supports-color@^5.4.0, supports-color@^5.5.0: +supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== From 0ed8c05e1c40824d8d428a96c9fb5a0fddd5131a Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 23 Oct 2024 15:01:54 -0500 Subject: [PATCH 157/720] Bump http-proxy-middleware from 1.0.5 to 2.0.7 --- package.json | 2 +- yarn.lock | 21 +++++---------------- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 5197b720dc..d9251da2b9 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "express-rate-limit": "^5.1.3", "fast-json-patch": "^3.1.1", "filesize": "^6.1.0", - "http-proxy-middleware": "^1.0.5", + "http-proxy-middleware": "^2.0.7", "http-terminator": "^3.2.0", "isbot": "^5.1.17", "js-cookie": "2.2.1", diff --git a/yarn.lock b/yarn.lock index 3f6a555df3..dd61f43ddb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2522,7 +2522,7 @@ "@types/react" "*" hoist-non-react-statics "^3.3.0" -"@types/http-proxy@^1.17.5", "@types/http-proxy@^1.17.8": +"@types/http-proxy@^1.17.8": version "1.17.10" resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.10.tgz" integrity sha512-Qs5aULi+zV1bwKAg5z1PWnDXWmsn+LxIvUGv6E2+OOMYhclZMO+OXd9pYVf2gLykf2I7IV2u7oTHwChPNsvJ7g== @@ -6629,21 +6629,10 @@ http-proxy-agent@^5.0.0: agent-base "6" debug "4" -http-proxy-middleware@^1.0.5: - version "1.3.1" - resolved "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-1.3.1.tgz" - integrity sha512-13eVVDYS4z79w7f1+NPllJtOQFx/FdUW4btIvVRMaRlUY9VGstAbo5MOhLEuUgZFRHn3x50ufn25zkj/boZnEg== - dependencies: - "@types/http-proxy" "^1.17.5" - http-proxy "^1.18.1" - is-glob "^4.0.1" - is-plain-obj "^3.0.0" - micromatch "^4.0.2" - -http-proxy-middleware@^2.0.3, http-proxy-middleware@^2.0.6: - version "2.0.6" - resolved "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz" - integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== +http-proxy-middleware@^2.0.3, http-proxy-middleware@^2.0.6, http-proxy-middleware@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz#915f236d92ae98ef48278a95dedf17e991936ec6" + integrity sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA== dependencies: "@types/http-proxy" "^1.17.8" http-proxy "^1.18.1" From d4bb79ca18d20111786015bd4657c468a29985f2 Mon Sep 17 00:00:00 2001 From: andreaNeki Date: Tue, 22 Oct 2024 13:29:05 -0300 Subject: [PATCH 158/720] Issue 3426 - Aligning the browse button (cherry picked from commit ddafda33b8c4730a732a133147451390ee2c7ab7) --- src/app/shared/upload/uploader/uploader.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/shared/upload/uploader/uploader.component.html b/src/app/shared/upload/uploader/uploader.component.html index b1fd8199d8..4c1db13d13 100644 --- a/src/app/shared/upload/uploader/uploader.component.html +++ b/src/app/shared/upload/uploader/uploader.component.html @@ -19,8 +19,8 @@ (fileOver)="fileOverBase($event)" class="well ds-base-drop-zone mt-1 mb-3 text-muted">
- - + + {{dropMsg | translate}}{{'uploader.or' | translate}}
{{ dsoNameService.getName(undefined) }} + {{ dsoNameService.getName((group.object | async)?.payload) }} +
diff --git a/src/app/access-control/group-registry/group-form/group-form.component.ts b/src/app/access-control/group-registry/group-form/group-form.component.ts index 37ce30473f..c0ea034fbb 100644 --- a/src/app/access-control/group-registry/group-form/group-form.component.ts +++ b/src/app/access-control/group-registry/group-form/group-form.component.ts @@ -13,7 +13,7 @@ import { Observable, Subscription, combineLatest, } from 'rxjs'; -import { map, switchMap, take, debounceTime, startWith, filter } from 'rxjs/operators'; +import { map, switchMap, take, debounceTime } from 'rxjs/operators'; import { getCollectionEditRolesRoute } from '../../../collection-page/collection-page-routing-paths'; import { getCommunityEditRolesRoute } from '../../../community-page/community-page-routing-paths'; import { DSpaceObjectDataService } from '../../../core/data/dspace-object-data.service'; @@ -35,7 +35,7 @@ import { } from '../../../core/shared/operators'; import { AlertType } from '../../../shared/alert/aletr-type'; import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component'; -import { hasValue, isNotEmpty, hasValueOperator, hasNoValue } from '../../../shared/empty.util'; +import { hasValue, isNotEmpty, hasValueOperator } from '../../../shared/empty.util'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { followLink } from '../../../shared/utils/follow-link-config.model'; @@ -164,11 +164,16 @@ export class GroupFormComponent implements OnInit, OnDestroy { this.activeGroupLinkedDSO$ = this.getActiveGroupLinkedDSO(); this.linkedEditRolesRoute$ = this.getLinkedEditRolesRoute(); this.canEdit$ = this.activeGroupLinkedDSO$.pipe( - filter((dso: DSpaceObject) => hasNoValue(dso)), - switchMap(() => this.activeGroup$), - hasValueOperator(), - switchMap((group: Group) => this.authorizationService.isAuthorized(FeatureID.CanDelete, group.self)), - startWith(false), + switchMap((dso: DSpaceObject) => { + if (hasValue(dso)) { + return [false]; + } else { + return this.activeGroup$.pipe( + hasValueOperator(), + switchMap((group: Group) => this.authorizationService.isAuthorized(FeatureID.CanDelete, group.self)), + ); + } + }), ); this.initialisePage(); } @@ -216,7 +221,7 @@ export class GroupFormComponent implements OnInit, OnDestroy { combineLatest([ this.activeGroup$, this.canEdit$, - this.activeGroupLinkedDSO$.pipe(take(1)), + this.activeGroupLinkedDSO$, ]).subscribe(([activeGroup, canEdit, linkedObject]) => { if (activeGroup != null) { @@ -224,25 +229,31 @@ export class GroupFormComponent implements OnInit, OnDestroy { // Disable group name exists validator this.formGroup.controls.groupName.clearAsyncValidators(); - if (linkedObject?.name) { - this.formBuilderService.insertFormGroupControl(1, this.formGroup, this.formModel, groupCommunityModel); - this.groupDescription = this.formGroup.get('groupCommunity'); + if (isNotEmpty(linkedObject?.name)) { + if (!this.formGroup.controls.groupCommunity) { + this.formBuilderService.insertFormGroupControl(1, this.formGroup, this.formModel, groupCommunityModel); + this.groupDescription = this.formGroup.get('groupCommunity'); + } this.formGroup.patchValue({ groupName: activeGroup.name, groupCommunity: linkedObject?.name ?? '', groupDescription: activeGroup.firstMetadataValue('dc.description'), }); } else { + this.formModel = [ + groupNameModel, + groupDescriptionModel, + ]; this.formGroup.patchValue({ groupName: activeGroup.name, groupDescription: activeGroup.firstMetadataValue('dc.description'), }); } - setTimeout(() => { - if (!canEdit || activeGroup.permanent) { - this.formGroup.disable(); - } - }, 200); + if (!canEdit || activeGroup.permanent) { + this.formGroup.disable(); + } else { + this.formGroup.enable(); + } } }) ); @@ -471,6 +482,7 @@ export class GroupFormComponent implements OnInit, OnDestroy { */ getLinkedEditRolesRoute(): Observable { return this.activeGroupLinkedDSO$.pipe( + hasValueOperator(), map((dso: DSpaceObject) => { switch ((dso as any).type) { case Community.type.value: @@ -478,7 +490,7 @@ export class GroupFormComponent implements OnInit, OnDestroy { case Collection.type.value: return getCollectionEditRolesRoute(dso.id); } - }) + }), ); } } From 585bbec5d53ebc8dedbafaf37f89c095addb883a Mon Sep 17 00:00:00 2001 From: andreaNeki Date: Tue, 27 Aug 2024 17:02:12 -0300 Subject: [PATCH 171/720] Improving accessibility on the new user registration page (cherry picked from commit 0eb2d5ce587ece5d3573cad7534bcbb8df5c6ed4) --- .../register-email-form.component.html | 9 ++++++--- src/assets/i18n/en.json5 | 2 ++ src/assets/i18n/es.json5 | 3 +++ src/assets/i18n/pt-BR.json5 | 3 +++ 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/app/register-email-form/register-email-form.component.html b/src/app/register-email-form/register-email-form.component.html index d4cf75b563..b47790d1cb 100644 --- a/src/app/register-email-form/register-email-form.component.html +++ b/src/app/register-email-form/register-email-form.component.html @@ -14,13 +14,16 @@

{{MESSAGE_PREFIX + '.header'|translate}}

+ type="text" id="email" formControlName="email" + [attr.aria-label]="'register-email.aria.label'|translate" + aria-describedby="email-errors-required email-error-not-valid" + [attr.aria-invalid]="form.get('email')?.invalid"/>
- + {{ MESSAGE_PREFIX + '.email.error.required' | translate }} - + {{ MESSAGE_PREFIX + '.email.error.not-email-form' | translate }} {{ MESSAGE_PREFIX + '.email.error.not-valid-domain' | translate: { domains: validMailDomains.join(', ') } }} diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 475e6437d6..84fbb5db96 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -5369,4 +5369,6 @@ "process.overview.unknown.user": "Unknown", "browse.search-form.placeholder": "Search the repository", + + "register-email.aria.label": "Enter your e-mail address", } diff --git a/src/assets/i18n/es.json5 b/src/assets/i18n/es.json5 index 3bc98a9502..6373f91652 100644 --- a/src/assets/i18n/es.json5 +++ b/src/assets/i18n/es.json5 @@ -7831,5 +7831,8 @@ //"browse.search-form.placeholder": "Search the repository", "browse.search-form.placeholder": "Buscar en el repositorio", + // "register-email.aria.label": "Enter your e-mail address", + "register-email.aria.label": "Introduzca su dirección de correo electrónico", + } diff --git a/src/assets/i18n/pt-BR.json5 b/src/assets/i18n/pt-BR.json5 index aae51b3671..09c6cbf630 100644 --- a/src/assets/i18n/pt-BR.json5 +++ b/src/assets/i18n/pt-BR.json5 @@ -7857,4 +7857,7 @@ //"browse.search-form.placeholder": "Search the repository", "browse.search-form.placeholder": "Buscar no repositório", + + // "register-email.aria.label": "Enter your e-mail address", + "register-email.aria.label": "Digite seu e-mail", } From a6b45f777cbfdafd4bf104cff81c9ed8f3a7460c Mon Sep 17 00:00:00 2001 From: andreaNeki Date: Thu, 5 Sep 2024 09:35:55 -0300 Subject: [PATCH 172/720] Code refactoring - Accessibility on the new user registration and forgotten password forms (cherry picked from commit f00eae67602c3b1d12c5f8fcf43db774a7a70e11) --- .../register-email-form/register-email-form.component.html | 4 ++-- src/assets/i18n/en.json5 | 4 +++- src/assets/i18n/es.json5 | 7 +++++-- src/assets/i18n/pt-BR.json5 | 7 +++++-- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/app/register-email-form/register-email-form.component.html b/src/app/register-email-form/register-email-form.component.html index b47790d1cb..9192226b1e 100644 --- a/src/app/register-email-form/register-email-form.component.html +++ b/src/app/register-email-form/register-email-form.component.html @@ -15,9 +15,9 @@

{{MESSAGE_PREFIX + '.header'|translate}}

for="email">{{MESSAGE_PREFIX + '.email' | translate}} + [attr.aria-invalid]="email.invalid"/>
diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 84fbb5db96..56439dee43 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -5370,5 +5370,7 @@ "browse.search-form.placeholder": "Search the repository", - "register-email.aria.label": "Enter your e-mail address", + "register-page.registration.aria.label": "Enter your e-mail address", + + "forgot-email.form.aria.label": "Enter your e-mail address", } diff --git a/src/assets/i18n/es.json5 b/src/assets/i18n/es.json5 index 6373f91652..dfdf4ca628 100644 --- a/src/assets/i18n/es.json5 +++ b/src/assets/i18n/es.json5 @@ -7831,8 +7831,11 @@ //"browse.search-form.placeholder": "Search the repository", "browse.search-form.placeholder": "Buscar en el repositorio", - // "register-email.aria.label": "Enter your e-mail address", - "register-email.aria.label": "Introduzca su dirección de correo electrónico", + // "register-page.registration.aria.label": "Enter your e-mail address", + "register-page.registration.aria.label": "Introduzca su dirección de correo electrónico", + + // "forgot-email.form.aria.label": "Enter your e-mail address", + "forgot-email.form.aria.label": "Introduzca su dirección de correo electrónico", } diff --git a/src/assets/i18n/pt-BR.json5 b/src/assets/i18n/pt-BR.json5 index 09c6cbf630..eebf896889 100644 --- a/src/assets/i18n/pt-BR.json5 +++ b/src/assets/i18n/pt-BR.json5 @@ -7858,6 +7858,9 @@ //"browse.search-form.placeholder": "Search the repository", "browse.search-form.placeholder": "Buscar no repositório", - // "register-email.aria.label": "Enter your e-mail address", - "register-email.aria.label": "Digite seu e-mail", + // "register-page.registration.aria.label": "Enter your e-mail address", + "register-page.registration.aria.label": "Digite seu e-mail", + + // "forgot-email.form.aria.label": "Enter your e-mail address", + "forgot-email.form.aria.label": "Digite seu e-mail", } From 3b3fa4f643ca96e81216963018b826d8989545ec Mon Sep 17 00:00:00 2001 From: andreaNeki Date: Thu, 31 Oct 2024 11:27:10 -0300 Subject: [PATCH 173/720] Dynamic aria-describedby attribute (cherry picked from commit e629d9edf0d2177953950f5e145bf49ff1203f88) --- .../register-email-form.component.html | 2 +- .../register-email-form.component.spec.ts | 35 +++++++++++++++++++ .../register-email-form.component.ts | 26 ++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/app/register-email-form/register-email-form.component.html b/src/app/register-email-form/register-email-form.component.html index 9192226b1e..a3ccc4bce8 100644 --- a/src/app/register-email-form/register-email-form.component.html +++ b/src/app/register-email-form/register-email-form.component.html @@ -16,7 +16,7 @@

{{MESSAGE_PREFIX + '.header'|translate}}

diff --git a/src/app/register-email-form/register-email-form.component.spec.ts b/src/app/register-email-form/register-email-form.component.spec.ts index 9e852d9491..67b87a974c 100644 --- a/src/app/register-email-form/register-email-form.component.spec.ts +++ b/src/app/register-email-form/register-email-form.component.spec.ts @@ -192,4 +192,39 @@ describe('RegisterEmailFormComponent', () => { expect(router.navigate).not.toHaveBeenCalled(); })); }); + describe('ariaDescribedby', () => { + it('should have required error message when email is empty', () => { + comp.form.patchValue({ email: '' }); + comp.checkEmailValidity(); + + expect(comp.ariaDescribedby).toContain('email-errors-required'); + }); + + it('should have invalid email error message when email is invalid', () => { + comp.form.patchValue({ email: 'invalid-email' }); + comp.checkEmailValidity(); + + expect(comp.ariaDescribedby).toContain('email-error-not-valid'); + }); + + it('should clear ariaDescribedby when email is valid', () => { + comp.form.patchValue({ email: 'valid@email.com' }); + comp.checkEmailValidity(); + + expect(comp.ariaDescribedby).toBe(''); + }); + + it('should update ariaDescribedby on value changes', () => { + spyOn(comp, 'checkEmailValidity').and.callThrough(); + + comp.form.patchValue({ email: '' }); + expect(comp.ariaDescribedby).toContain('email-errors-required'); + + comp.form.patchValue({ email: 'invalid-email' }); + expect(comp.ariaDescribedby).toContain('email-error-not-valid'); + + comp.form.patchValue({ email: 'valid@email.com' }); + expect(comp.ariaDescribedby).toBe(''); + }); + }); }); diff --git a/src/app/register-email-form/register-email-form.component.ts b/src/app/register-email-form/register-email-form.component.ts index df7e9bea5e..0ba37a9090 100644 --- a/src/app/register-email-form/register-email-form.component.ts +++ b/src/app/register-email-form/register-email-form.component.ts @@ -66,6 +66,11 @@ export class RegisterEmailFormComponent implements OnDestroy, OnInit { subscriptions: Subscription[] = []; + /** + * Stores error messages related to the email field + */ + ariaDescribedby: string = ''; + captchaVersion(): Observable { return this.googleRecaptchaService.captchaVersion(); } @@ -135,6 +140,13 @@ export class RegisterEmailFormComponent implements OnDestroy, OnInit { this.disableUntilChecked = res; this.changeDetectorRef.detectChanges(); })); + + /** + * Subscription to email field value changes + */ + this.subscriptions.push(this.email.valueChanges.subscribe(() => { + this.checkEmailValidity(); + })); } /** @@ -248,4 +260,18 @@ export class RegisterEmailFormComponent implements OnDestroy, OnInit { } } + checkEmailValidity() { + const descriptions = []; + + if (this.email.errors?.required) { + descriptions.push('email-errors-required'); + } + + if (this.email.errors?.pattern || this.email.errors?.email) { + descriptions.push('email-error-not-valid'); + } + + this.ariaDescribedby = descriptions.join(' '); + } + } From c330e83095c844c3db61e4be47499f793c0c1773 Mon Sep 17 00:00:00 2001 From: andreaNeki Date: Thu, 31 Oct 2024 11:39:34 -0300 Subject: [PATCH 174/720] Resolving a wrongly declared variable error (cherry picked from commit 7b72a5f0c26514eb07f58faf3470c89fe9729d6f) --- src/app/register-email-form/register-email-form.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/register-email-form/register-email-form.component.ts b/src/app/register-email-form/register-email-form.component.ts index 0ba37a9090..1d8a6d3474 100644 --- a/src/app/register-email-form/register-email-form.component.ts +++ b/src/app/register-email-form/register-email-form.component.ts @@ -69,7 +69,7 @@ export class RegisterEmailFormComponent implements OnDestroy, OnInit { /** * Stores error messages related to the email field */ - ariaDescribedby: string = ''; + ariaDescribedby = ''; captchaVersion(): Observable { return this.googleRecaptchaService.captchaVersion(); From 1fad3cffc7c78dd52380ca21006ea2aa17ada847 Mon Sep 17 00:00:00 2001 From: andreaNeki Date: Thu, 31 Oct 2024 15:01:54 -0300 Subject: [PATCH 175/720] Simplifying the implementation of dynamic aria-describedby (cherry picked from commit fa6e85d6db5a671038e8e27968701b46520154df) --- .../register-email-form.component.html | 2 +- .../register-email-form.component.spec.ts | 35 ------------------- .../register-email-form.component.ts | 28 +-------------- 3 files changed, 2 insertions(+), 63 deletions(-) diff --git a/src/app/register-email-form/register-email-form.component.html b/src/app/register-email-form/register-email-form.component.html index a3ccc4bce8..f6f4880ab6 100644 --- a/src/app/register-email-form/register-email-form.component.html +++ b/src/app/register-email-form/register-email-form.component.html @@ -16,7 +16,7 @@

{{MESSAGE_PREFIX + '.header'|translate}}

diff --git a/src/app/register-email-form/register-email-form.component.spec.ts b/src/app/register-email-form/register-email-form.component.spec.ts index 67b87a974c..9e852d9491 100644 --- a/src/app/register-email-form/register-email-form.component.spec.ts +++ b/src/app/register-email-form/register-email-form.component.spec.ts @@ -192,39 +192,4 @@ describe('RegisterEmailFormComponent', () => { expect(router.navigate).not.toHaveBeenCalled(); })); }); - describe('ariaDescribedby', () => { - it('should have required error message when email is empty', () => { - comp.form.patchValue({ email: '' }); - comp.checkEmailValidity(); - - expect(comp.ariaDescribedby).toContain('email-errors-required'); - }); - - it('should have invalid email error message when email is invalid', () => { - comp.form.patchValue({ email: 'invalid-email' }); - comp.checkEmailValidity(); - - expect(comp.ariaDescribedby).toContain('email-error-not-valid'); - }); - - it('should clear ariaDescribedby when email is valid', () => { - comp.form.patchValue({ email: 'valid@email.com' }); - comp.checkEmailValidity(); - - expect(comp.ariaDescribedby).toBe(''); - }); - - it('should update ariaDescribedby on value changes', () => { - spyOn(comp, 'checkEmailValidity').and.callThrough(); - - comp.form.patchValue({ email: '' }); - expect(comp.ariaDescribedby).toContain('email-errors-required'); - - comp.form.patchValue({ email: 'invalid-email' }); - expect(comp.ariaDescribedby).toContain('email-error-not-valid'); - - comp.form.patchValue({ email: 'valid@email.com' }); - expect(comp.ariaDescribedby).toBe(''); - }); - }); }); diff --git a/src/app/register-email-form/register-email-form.component.ts b/src/app/register-email-form/register-email-form.component.ts index 1d8a6d3474..42be43aab3 100644 --- a/src/app/register-email-form/register-email-form.component.ts +++ b/src/app/register-email-form/register-email-form.component.ts @@ -66,11 +66,6 @@ export class RegisterEmailFormComponent implements OnDestroy, OnInit { subscriptions: Subscription[] = []; - /** - * Stores error messages related to the email field - */ - ariaDescribedby = ''; - captchaVersion(): Observable { return this.googleRecaptchaService.captchaVersion(); } @@ -140,13 +135,6 @@ export class RegisterEmailFormComponent implements OnDestroy, OnInit { this.disableUntilChecked = res; this.changeDetectorRef.detectChanges(); })); - - /** - * Subscription to email field value changes - */ - this.subscriptions.push(this.email.valueChanges.subscribe(() => { - this.checkEmailValidity(); - })); } /** @@ -259,19 +247,5 @@ export class RegisterEmailFormComponent implements OnDestroy, OnInit { console.warn(`Unimplemented notification '${key}' from reCaptcha service`); } } - - checkEmailValidity() { - const descriptions = []; - - if (this.email.errors?.required) { - descriptions.push('email-errors-required'); - } - - if (this.email.errors?.pattern || this.email.errors?.email) { - descriptions.push('email-error-not-valid'); - } - - this.ariaDescribedby = descriptions.join(' '); - } - + } From 1011468273f2887b21e77bf687285acb240d3e53 Mon Sep 17 00:00:00 2001 From: andreaNeki Date: Thu, 31 Oct 2024 15:07:22 -0300 Subject: [PATCH 176/720] Adjusting spaces in ts (cherry picked from commit 009da08177f31d049c2094151d8485c157ee0ede) --- src/app/register-email-form/register-email-form.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/register-email-form/register-email-form.component.ts b/src/app/register-email-form/register-email-form.component.ts index 42be43aab3..c8ca0cc710 100644 --- a/src/app/register-email-form/register-email-form.component.ts +++ b/src/app/register-email-form/register-email-form.component.ts @@ -247,5 +247,4 @@ export class RegisterEmailFormComponent implements OnDestroy, OnInit { console.warn(`Unimplemented notification '${key}' from reCaptcha service`); } } - } From 26f4d1d329d21f1661b6e10bd06362897c7521d1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 02:46:18 +0000 Subject: [PATCH 177/720] Bump sass from 1.80.4 to 1.80.6 in the sass group Bumps the sass group with 1 update: [sass](https://github.com/sass/dart-sass). Updates `sass` from 1.80.4 to 1.80.6 - [Release notes](https://github.com/sass/dart-sass/releases) - [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md) - [Commits](https://github.com/sass/dart-sass/compare/1.80.4...1.80.6) --- updated-dependencies: - dependency-name: sass dependency-type: direct:development update-type: version-update:semver-patch dependency-group: sass ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index e2923712c0..67e416e6e1 100644 --- a/package.json +++ b/package.json @@ -186,7 +186,7 @@ "react-copy-to-clipboard": "^5.1.0", "react-dom": "^16.14.0", "rimraf": "^3.0.2", - "sass": "~1.80.4", + "sass": "~1.80.6", "sass-loader": "^12.6.0", "sass-resources-loader": "^2.2.5", "ts-node": "^8.10.2", diff --git a/yarn.lock b/yarn.lock index 21dcdd6fa5..69b50f026e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10234,15 +10234,16 @@ sass@1.58.1: immutable "^4.0.0" source-map-js ">=0.6.2 <2.0.0" -sass@^1.25.0, sass@~1.80.4: - version "1.80.4" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.80.4.tgz#bc0418fd796cad2f1a1309d8b4d7fe44b7027de0" - integrity sha512-rhMQ2tSF5CsuuspvC94nPM9rToiAFw2h3JTrLlgmNw1MH79v8Cr3DH6KF6o6r+8oofY3iYVPUf66KzC8yuVN1w== +sass@^1.25.0, sass@~1.80.6: + version "1.80.6" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.80.6.tgz#5d0aa55763984effe41e40019c9571ab73e6851f" + integrity sha512-ccZgdHNiBF1NHBsWvacvT5rju3y1d/Eu+8Ex6c21nHp2lZGLBEtuwc415QfiI1PJa1TpCo3iXwwSRjRpn2Ckjg== dependencies: - "@parcel/watcher" "^2.4.1" chokidar "^4.0.0" immutable "^4.0.0" source-map-js ">=0.6.2 <2.0.0" + optionalDependencies: + "@parcel/watcher" "^2.4.1" sax@^1.2.4: version "1.2.4" From 2eb120909b6b917d3f82852382429e391ace7bfd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 02:46:50 +0000 Subject: [PATCH 178/720] Bump compression from 1.7.4 to 1.7.5 Bumps [compression](https://github.com/expressjs/compression) from 1.7.4 to 1.7.5. - [Release notes](https://github.com/expressjs/compression/releases) - [Changelog](https://github.com/expressjs/compression/blob/master/HISTORY.md) - [Commits](https://github.com/expressjs/compression/compare/1.7.4...1.7.5) --- updated-dependencies: - dependency-name: compression dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 36 ++++++++++++++++++------------------ 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index e2923712c0..24c674d8d6 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "cerialize": "0.1.18", "cli-progress": "^3.12.0", "colors": "^1.4.0", - "compression": "^1.7.4", + "compression": "^1.7.5", "cookie-parser": "1.4.7", "core-js": "^3.38.1", "date-fns": "^2.30.0", diff --git a/yarn.lock b/yarn.lock index 21dcdd6fa5..0208ffe1a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2983,7 +2983,7 @@ abbrev@1, abbrev@^1.0.0: resolved "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== -accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: +accepts@~1.3.4, accepts@~1.3.8: version "1.3.8" resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz" integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== @@ -3710,11 +3710,6 @@ builtins@^5.0.0: dependencies: semver "^7.0.0" -bytes@3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz" - integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== - bytes@3.1.2: version "3.1.2" resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz" @@ -4119,7 +4114,7 @@ commondir@^1.0.1: resolved "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz" integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== -compressible@~2.0.16: +compressible@~2.0.18: version "2.0.18" resolved "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz" integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== @@ -4134,17 +4129,17 @@ compression-webpack-plugin@^9.2.0: schema-utils "^4.0.0" serialize-javascript "^6.0.0" -compression@^1.7.4: - version "1.7.4" - resolved "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz" - integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== +compression@^1.7.4, compression@^1.7.5: + version "1.7.5" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.5.tgz#fdd256c0a642e39e314c478f6c2cd654edd74c93" + integrity sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q== dependencies: - accepts "~1.3.5" - bytes "3.0.0" - compressible "~2.0.16" + bytes "3.1.2" + compressible "~2.0.18" debug "2.6.9" + negotiator "~0.6.4" on-headers "~1.0.2" - safe-buffer "5.1.2" + safe-buffer "5.2.1" vary "~1.1.2" concat-map@0.0.1: @@ -8351,11 +8346,16 @@ needle@^3.1.0: iconv-lite "^0.6.3" sax "^1.2.4" -negotiator@0.6.3, negotiator@^0.6.3: +negotiator@0.6.3: version "0.6.3" resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== +negotiator@^0.6.3, negotiator@~0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.4.tgz#777948e2452651c570b712dd01c23e262713fff7" + integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w== + neo-async@^2.6.2: version "2.6.2" resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" @@ -10158,12 +10158,12 @@ safe-array-concat@^1.1.2: has-symbols "^1.0.3" isarray "^2.0.5" -safe-buffer@5.1.2, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.2.1, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== From 1b77530a3187f5e991a686876053b72a16d9ddcc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 02:47:05 +0000 Subject: [PATCH 179/720] Bump @types/lodash from 4.17.12 to 4.17.13 Bumps [@types/lodash](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/lodash) from 4.17.12 to 4.17.13. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/lodash) --- updated-dependencies: - dependency-name: "@types/lodash" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index e2923712c0..6aef716bd5 100644 --- a/package.json +++ b/package.json @@ -145,7 +145,7 @@ "@types/grecaptcha": "^3.0.9", "@types/jasmine": "~3.6.0", "@types/js-cookie": "2.2.6", - "@types/lodash": "^4.17.12", + "@types/lodash": "^4.17.13", "@types/node": "^14.18.63", "@types/sanitize-html": "^2.13.0", "@typescript-eslint/eslint-plugin": "^5.62.0", diff --git a/yarn.lock b/yarn.lock index 21dcdd6fa5..a44db790f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2539,10 +2539,10 @@ resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/lodash@^4.17.12": - version "4.17.12" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.12.tgz#25d71312bf66512105d71e55d42e22c36bcfc689" - integrity sha512-sviUmCE8AYdaF/KIHLDJBQgeYzPBI0vf/17NaYehBJfYD1j6/L95Slh07NlyK2iNyBNaEkb3En2jRt+a8y3xZQ== +"@types/lodash@^4.17.13": + version "4.17.13" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.13.tgz#786e2d67cfd95e32862143abe7463a7f90c300eb" + integrity sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg== "@types/mime@*": version "3.0.1" From 2aaa32ae5f27594aaea2146c809352c7a8a02cc6 Mon Sep 17 00:00:00 2001 From: Pierre Lasou Date: Thu, 31 Oct 2024 11:12:29 -0400 Subject: [PATCH 180/720] Complete tag translation in french for ORCID Contains all french translations for ORCID and Researcher Profile. (cherry picked from commit ac720033dcca82c0bbd8c6a9f8ac8b1d07ffb37e) --- src/assets/i18n/fr.json5 | 366 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 366 insertions(+) diff --git a/src/assets/i18n/fr.json5 b/src/assets/i18n/fr.json5 index a73baacf2d..9c0b85cfe4 100644 --- a/src/assets/i18n/fr.json5 +++ b/src/assets/i18n/fr.json5 @@ -6241,6 +6241,372 @@ // "idle-modal.extend-session": "Extend session", "idle-modal.extend-session": "Prolonger la session", + //"researcher.profile.action.processing": "Processing...", + "researcher.profile.action.processing": "En traitement...", + + //"researcher.profile.associated": "Researcher profile associated", + "researcher.profile.associated": "Profil du chercheur associé", + + //"researcher.profile.change-visibility.fail": "An unexpected error occurs while changing the profile visibility", + "researcher.profile.change-visibility.fail": "Une erreur inattendue s'est produite pendant le changement apporté à la visibilité du profil.", + + //"researcher.profile.create.new": "Create new", + "researcher.profile.create.new": "Créer un nouveau", + + //"researcher.profile.create.success": "Researcher profile created successfully", + "researcher.profile.create.success": "Profil créé avec succès", + + //"researcher.profile.create.fail": "An error occurs during the researcher profile creation", + "researcher.profile.create.fail": "Une erreur s'est produite lors de la création du profil", + + //"researcher.profile.delete": "Delete", + "researcher.profile.delete": "Supprimer", + + //"researcher.profile.expose": "Expose", + "researcher.profile.expose": "Exposer", + + //"researcher.profile.hide": "Hide", + "researcher.profile.hide": "Cacher", + + //"researcher.profile.not.associated": "Researcher profile not yet associated", + "researcher.profile.not.associated": "Profil non associé", + + //"researcher.profile.view": "View", + "researcher.profile.view": "Voir", + + //"researcher.profile.private.visibility": "PRIVATE", + "researcher.profile.private.visibility": "PRIVÉ", + + //"researcher.profile.public.visibility": "PUBLIC", + "researcher.profile.public.visibility": "PUBLIC", + + //"researcher.profile.status": "Status:", + "researcher.profile.status": "Statut:", + + //"researcherprofile.claim.not-authorized": "You are not authorized to claim this item. For more details contact the administrator(s).", + "researcherprofile.claim.not-authorized": "Vous n'êtes pas autorisé à réclamer cet item. Pour plus de détails contacter la personne administratrice.", + + //"researcherprofile.error.claim.body": "An error occurred while claiming the profile, please try again later", + "researcherprofile.error.claim.body": "Une erreur s'est produite lors de la réclamation du profil, veuillez réessayer plus tard.", + + //"researcherprofile.error.claim.title": "Error", + "researcherprofile.error.claim.title": "Erreur", + + //"researcherprofile.success.claim.body": "Profile claimed with success", + "researcherprofile.success.claim.body": "Profil réclamé avec succès", + + //"researcherprofile.success.claim.title": "Success", + "researcherprofile.success.claim.title": "Succès", + + //"person.page.orcid.create": "Create an ORCID ID", + "person.page.orcid.create": "Créer un identifiant ORCID", + + //"person.page.orcid.granted-authorizations": "Granted authorizations", + "person.page.orcid.granted-authorizations": "Autorisations accordées", + + //"person.page.orcid.grant-authorizations": "Grant authorizations", + "person.page.orcid.grant-authorizations": "Accorder des autorisations", + + //"person.page.orcid.link": "Connect to ORCID ID", + "person.page.orcid.link": "Se connecter à ORCID", + + //"person.page.orcid.link.processing": "Linking profile to ORCID...", + "person.page.orcid.link.processing": "Liaison du profil avec ORCID en cours...", + + //"person.page.orcid.link.error.message": "Something went wrong while connecting the profile with ORCID. If the problem persists, contact the administrator.", + "person.page.orcid.link.error.message": "Quelque chose a échoué lors de la connection du profil avec ORCID. Si le problème persiste, contacter la personne administratice.", + + //"person.page.orcid.orcid-not-linked-message": "The ORCID iD of this profile ({{ orcid }}) has not yet been connected to an account on the ORCID registry or the connection is expired.", + "person.page.orcid.orcid-not-linked-message": "L'identifiant ORCID du profil ({{ orcid }}) n'a pas encore été connecté à une compte du registre ORCID ou la connection a expiré. ", + + //"person.page.orcid.unlink": "Disconnect from ORCID", + "person.page.orcid.unlink": "Déconnecter d'ORCID", + + //"person.page.orcid.unlink.processing": "Processing...", + "person.page.orcid.unlink.processing": "Traitement en cours...", + + //"person.page.orcid.missing-authorizations": "Missing authorizations", + "person.page.orcid.missing-authorizations": "Autorisations manquantes", + + //"person.page.orcid.missing-authorizations-message": "The following authorizations are missing:", + "person.page.orcid.missing-authorizations-message": "Les autorisations suivantes sont manquantes :", + + //"person.page.orcid.no-missing-authorizations-message": "Great! This box is empty, so you have granted all access rights to use all functions offers by your institution.", + "person.page.orcid.no-missing-authorizations-message": "Cette boite est vide, vous avez autorisé l'utilisation de toutes les fonctions proposées par votre organisation.", + + //"person.page.orcid.no-orcid-message": "No ORCID iD associated yet. By clicking on the button below it is possible to link this profile with an ORCID account.", + "person.page.orcid.no-orcid-message": "Aucun identifiant ORCID n'est encore associé. En cliquant sur le bouton ci-dessous, il est possible de lier ce profil à un compte ORCID.", + + //"person.page.orcid.profile-preferences": "Profile preferences", + "person.page.orcid.profile-preferences": "Préférences du profil", + + //"person.page.orcid.funding-preferences": "Funding preferences", + "person.page.orcid.funding-preferences": "Préférence pour la section Financement", + + //"person.page.orcid.publications-preferences": "Publication preferences", + "person.page.orcid.publications-preferences": "Préférence pour la section Publication", + + //"person.page.orcid.remove-orcid-message": "If you need to remove your ORCID, please contact the repository administrator", + "person.page.orcid.remove-orcid-message": "Si vous avez besoin de retirer votre ORCID, contacter la personne administratrice du dépôt.", + + //"person.page.orcid.save.preference.changes": "Update settings", + "person.page.orcid.save.preference.changes": "Mettre à jour les configurations", + + //"person.page.orcid.sync-profile.affiliation": "Affiliation", + "person.page.orcid.sync-profile.affiliation": "Affiliation", + + //"person.page.orcid.sync-profile.biographical": "Biographical data", + "person.page.orcid.sync-profile.biographical": "Données biographiques", + + //"person.page.orcid.sync-profile.education": "Education", + "person.page.orcid.sync-profile.education": "Éducation", + + //"person.page.orcid.sync-profile.identifiers": "Identifiers", + "person.page.orcid.sync-profile.identifiers": "Identifiants", + + //"person.page.orcid.sync-fundings.all": "All fundings", + "person.page.orcid.sync-fundings.all": "Tous les financements", + + //"person.page.orcid.sync-fundings.mine": "My fundings", + "person.page.orcid.sync-fundings.mine": "Mes financements", + + //"person.page.orcid.sync-fundings.my_selected": "Selected fundings", + "person.page.orcid.sync-fundings.my_selected": "Financements sélectionnés", + + //"person.page.orcid.sync-fundings.disabled": "Disabled", + "person.page.orcid.sync-fundings.disabled": "Désactivé", + + //"person.page.orcid.sync-publications.all": "All publications", + "person.page.orcid.sync-publications.all": "Toutes les publications", + + //"person.page.orcid.sync-publications.mine": "My publications", + "person.page.orcid.sync-publications.mine": "Mes publications", + + //"person.page.orcid.sync-publications.my_selected": "Selected publications", + "person.page.orcid.sync-publications.my_selected": "Publications sélectionnées", + + //"person.page.orcid.sync-publications.disabled": "Disabled", + "person.page.orcid.sync-publications.disabled": "Désactivé", + + //"person.page.orcid.sync-queue.discard": "Discard the change and do not synchronize with the ORCID registry", + "person.page.orcid.sync-queue.discard": "Ignorer les changements et ne pas synchroniser avec le registre ORCID.", + + //"person.page.orcid.sync-queue.discard.error": "The discarding of the ORCID queue record failed", + "person.page.orcid.sync-queue.discard.error": "L'annulation de la file d'attente ORCID a échoué.", + + //"person.page.orcid.sync-queue.discard.success": "The ORCID queue record have been discarded successfully", + "person.page.orcid.sync-queue.discard.success": "La file d'attente ORCID a été annulée avec succès.", + + //"person.page.orcid.sync-queue.empty-message": "The ORCID queue registry is empty", + "person.page.orcid.sync-queue.empty-message": "La file d'attente ORCID est vide.", + + //"person.page.orcid.sync-queue.table.header.type": "Type", + "person.page.orcid.sync-queue.table.header.type": "Type", + + //"person.page.orcid.sync-queue.table.header.description": "Description", + "person.page.orcid.sync-queue.table.header.description": "Description", + + //"person.page.orcid.sync-queue.table.header.action": "Action", + "person.page.orcid.sync-queue.table.header.action": "Action", + + //"person.page.orcid.sync-queue.description.affiliation": "Affiliations", + "person.page.orcid.sync-queue.description.affiliation": "Affiliations", + + //"person.page.orcid.sync-queue.description.country": "Country", + "person.page.orcid.sync-queue.description.country": "Pays", + + //"person.page.orcid.sync-queue.description.education": "Educations", + "person.page.orcid.sync-queue.description.education": "Éducation", + + //"person.page.orcid.sync-queue.description.external_ids": "External ids", + "person.page.orcid.sync-queue.description.external_ids": "Identifiants externes", + + //"person.page.orcid.sync-queue.description.other_names": "Other names", + "person.page.orcid.sync-queue.description.other_names": "Autres noms", + + //"person.page.orcid.sync-queue.description.qualification": "Qualifications", + "person.page.orcid.sync-queue.description.qualification": "Qualifications", + + //"person.page.orcid.sync-queue.description.researcher_urls": "Researcher urls", + "person.page.orcid.sync-queue.description.researcher_urls": "URLs du chercheur", + + //"person.page.orcid.sync-queue.description.keywords": "Keywords", + "person.page.orcid.sync-queue.description.keywords": "Mots clés", + + //"person.page.orcid.sync-queue.tooltip.insert": "Add a new entry in the ORCID registry", + "person.page.orcid.sync-queue.tooltip.insert": "Ajouter une nouvelle entrée dans le registre ORCID", + + //"person.page.orcid.sync-queue.tooltip.update": "Update this entry on the ORCID registry", + "person.page.orcid.sync-queue.tooltip.update": "Mettre à jour cette entrée dans le registre ORCID", + + //"person.page.orcid.sync-queue.tooltip.delete": "Remove this entry from the ORCID registry", + "person.page.orcid.sync-queue.tooltip.delete": "Supprimer cette entrée du registre ORCID", + + //"person.page.orcid.sync-queue.tooltip.publication": "Publication", + "person.page.orcid.sync-queue.tooltip.publication": "Publication", + + //"person.page.orcid.sync-queue.tooltip.project": "Project", + "person.page.orcid.sync-queue.tooltip.project": "Projet", + + //"person.page.orcid.sync-queue.tooltip.affiliation": "Affiliation", + "person.page.orcid.sync-queue.tooltip.affiliation": "Affiliation", + + //"person.page.orcid.sync-queue.tooltip.education": "Education", + "person.page.orcid.sync-queue.tooltip.education": "Éducation", + + //"person.page.orcid.sync-queue.tooltip.qualification": "Qualification", + "person.page.orcid.sync-queue.tooltip.qualification": "Qualification", + + //"person.page.orcid.sync-queue.tooltip.other_names": "Other name", + "person.page.orcid.sync-queue.tooltip.other_names": "Autre nom", + + //"person.page.orcid.sync-queue.tooltip.country": "Country", + "person.page.orcid.sync-queue.tooltip.country": "Pays", + + //"person.page.orcid.sync-queue.tooltip.keywords": "Keyword", + "person.page.orcid.sync-queue.tooltip.keywords": "Mot clé", + + //"person.page.orcid.sync-queue.tooltip.external_ids": "External identifier", + "person.page.orcid.sync-queue.tooltip.external_ids": "Identifiant externe", + + //"person.page.orcid.sync-queue.tooltip.researcher_urls": "Researcher url", + "person.page.orcid.sync-queue.tooltip.researcher_urls": "URL du chercheur", + + //"person.page.orcid.sync-queue.send": "Synchronize with ORCID registry", + "person.page.orcid.sync-queue.send": "Synchroniser avec le registre ORCID", + + //"person.page.orcid.sync-queue.send.unauthorized-error.title": "The submission to ORCID failed for missing authorizations.", + "person.page.orcid.sync-queue.send.unauthorized-error.title": "La transmission à ORCID a échoué en raison d'autorisations manquantes.", + + //"person.page.orcid.sync-queue.send.unauthorized-error.content": "Click here to grant again the required permissions. If the problem persists, contact the administrator", + "person.page.orcid.sync-queue.send.unauthorized-error.content": "Cliquer ici afin d'accorder à nouveau les permissions nécessaires. Si le problème persiste, contacter l'administrateur.", + + //"person.page.orcid.sync-queue.send.bad-request-error": "The submission to ORCID failed because the resource sent to ORCID registry is not valid", + "person.page.orcid.sync-queue.send.bad-request-error": "La transmission à ORCID a échoué en raison du fait que la ressource envoyée n'est pas valide.", + + //"person.page.orcid.sync-queue.send.error": "The submission to ORCID failed", + "person.page.orcid.sync-queue.send.error": "La transmission à ORCID a échoué.", + + //"person.page.orcid.sync-queue.send.conflict-error": "The submission to ORCID failed because the resource is already present on the ORCID registry", + "person.page.orcid.sync-queue.send.conflict-error": "La transmission à ORCID a échoué en raison du fait que la ressource est déjà présente dans le registre ORCID.", + + //"person.page.orcid.sync-queue.send.not-found-warning": "The resource does not exists anymore on the ORCID registry.", + "person.page.orcid.sync-queue.send.not-found-warning": "La ressource n'existe plus dans le registre ORCID.", + + //"person.page.orcid.sync-queue.send.success": "The submission to ORCID was completed successfully", + "person.page.orcid.sync-queue.send.success": "La transmission à ORCID a été effectuée avec succès.", + + //"person.page.orcid.sync-queue.send.validation-error": "The data that you want to synchronize with ORCID is not valid", + "person.page.orcid.sync-queue.send.validation-error": "Les données que vous souhaitez synchroniser avec ORCID ne sont pas valides.", + + //"person.page.orcid.sync-queue.send.validation-error.amount-currency.required": "The amount's currency is required", + "person.page.orcid.sync-queue.send.validation-error.amount-currency.required": "La devise du montant est requise.", + + //"person.page.orcid.sync-queue.send.validation-error.external-id.required": "The resource to be sent requires at least one identifier", + "person.page.orcid.sync-queue.send.validation-error.external-id.required": "La ressource à transmettre doit avoir au moins un identifiant.", + + //"person.page.orcid.sync-queue.send.validation-error.title.required": "The title is required", + "person.page.orcid.sync-queue.send.validation-error.title.required": "Le titre est obligatoire.", + + //"person.page.orcid.sync-queue.send.validation-error.type.required": "The dc.type is required", + "person.page.orcid.sync-queue.send.validation-error.type.required": "Le type de document est obligatoire.", + + //"person.page.orcid.sync-queue.send.validation-error.start-date.required": "The start date is required", + "person.page.orcid.sync-queue.send.validation-error.start-date.required": "La date de début est obbligatoire.", + + //"person.page.orcid.sync-queue.send.validation-error.funder.required": "The funder is required", + "person.page.orcid.sync-queue.send.validation-error.funder.required": "L'organisme subventionnaire est obligatoire.", + + //"person.page.orcid.sync-queue.send.validation-error.country.invalid": "Invalid 2 digits ISO 3166 country", + "person.page.orcid.sync-queue.send.validation-error.country.invalid": "Code de pays ISO 3166 à 2 chiffres invalide", + + //"person.page.orcid.sync-queue.send.validation-error.organization.required": "The organization is required", + "person.page.orcid.sync-queue.send.validation-error.organization.required": "L'organisation est obligatoire.", + + //"person.page.orcid.sync-queue.send.validation-error.organization.name-required": "The organization's name is required", + "person.page.orcid.sync-queue.send.validation-error.organization.name-required": "Le nom de l'organisation est obligatoire.", + + //"person.page.orcid.sync-queue.send.validation-error.publication.date-invalid": "The publication date must be one year after 1900", + "person.page.orcid.sync-queue.send.validation-error.publication.date-invalid": "La date de publication doit être d'au moins un an après 1900.", + + //"person.page.orcid.sync-queue.send.validation-error.organization.address-required": "The organization to be sent requires an address", + "person.page.orcid.sync-queue.send.validation-error.organization.address-required": "L'organisation a transmettre doit avoir une adresse.", + + //"person.page.orcid.sync-queue.send.validation-error.organization.city-required": "The address of the organization to be sent requires a city", + "person.page.orcid.sync-queue.send.validation-error.organization.city-required": "L'adresse de l'organisation à transmettre doit mentionner une ville.", + + //"person.page.orcid.sync-queue.send.validation-error.organization.country-required": "The address of the organization to be sent requires a valid 2 digits ISO 3166 country", + "person.page.orcid.sync-queue.send.validation-error.organization.country-required": "L'adresse de l'organisation à transmettre doit avoir une code de pays ISO 3166 à 2 chiffres valide.", + + //"person.page.orcid.sync-queue.send.validation-error.disambiguated-organization.required": "An identifier to disambiguate organizations is required. Supported ids are GRID, Ringgold, Legal Entity identifiers (LEIs) and Crossref Funder Registry identifiers", + "person.page.orcid.sync-queue.send.validation-error.disambiguated-organization.required": "Un identifiant pour désambiguer l'orgnisation est obligatoire. Les identifiants possibles sont GRID, Ringgold, Legal Entity identifiers (LEIs) et Crossref Funder Registry identifiers", + + //"person.page.orcid.sync-queue.send.validation-error.disambiguated-organization.value-required": "The organization's identifiers requires a value", + "person.page.orcid.sync-queue.send.validation-error.disambiguated-organization.value-required": "Une valeur pour l'identifiant de l'organisation est obligatoire.", + + //"person.page.orcid.sync-queue.send.validation-error.disambiguation-source.required": "The organization's identifiers requires a source", + "person.page.orcid.sync-queue.send.validation-error.disambiguation-source.required": "La source de l'identifiant de l'organisation est obligatoire.", + + //"person.page.orcid.sync-queue.send.validation-error.disambiguation-source.invalid": "The source of one of the organization identifiers is invalid. Supported sources are RINGGOLD, GRID, LEI and FUNDREF", + "person.page.orcid.sync-queue.send.validation-error.disambiguation-source.invalid": "La source d'un des identifiants d'organisation est invalide. Les sources possibles sont RINGGOLD, GRID, LEI and FUNDREF", + + //"person.page.orcid.synchronization-mode": "Synchronization mode", + "person.page.orcid.synchronization-mode": "Mode de synchronisation", + + //"person.page.orcid.synchronization-mode.batch": "Batch", + "person.page.orcid.synchronization-mode.batch": "En lot", + + //"person.page.orcid.synchronization-mode.label": "Synchronization mode", + "person.page.orcid.synchronization-mode.label": "Mode de synchronisation", + + //"person.page.orcid.synchronization-mode-message": "Please select how you would like synchronization to ORCID to occur. The options include \"Manual\" (you must send your data to ORCID manually), or \"Batch\" (the system will send your data to ORCID via a scheduled script).", + "person.page.orcid.synchronization-mode-message": "Sélectionner le mode de synchronisation vers ORCID. Les options sont \"Manuel\" (vous devrez sélectionner les données à transmettre vers ORCID manuellement), ou \"En lot\" (le système transmettra vos données vers ORCID automatiquement).", + + //"person.page.orcid.synchronization-mode-funding-message": "Select whether to send your linked Project entities to your ORCID record's list of funding information.", + "person.page.orcid.synchronization-mode-funding-message": "Sélectionnez si vous souhaitez transmettre vos projets de recherche vers votre profil ORCID.", + + //"person.page.orcid.synchronization-mode-publication-message": "Select whether to send your linked Publication entities to your ORCID record's list of works.", + "person.page.orcid.synchronization-mode-publication-message": "Sélectionnez si vous souhaitez transmettre vos publications vers votre profil ORCID.", + + //"person.page.orcid.synchronization-mode-profile-message": "Select whether to send your biographical data or personal identifiers to your ORCID record.", + "person.page.orcid.synchronization-mode-profile-message": "Sélectionnez si vous souhaitez transmettre vos données biographiques vers votre profil ORCID.", + + //"person.page.orcid.synchronization-settings-update.success": "The synchronization settings have been updated successfully", + "person.page.orcid.synchronization-settings-update.success": "Les paramètres de synchronisation ont été mis à jour avec succès.", + + //"person.page.orcid.synchronization-settings-update.error": "The update of the synchronization settings failed", + "person.page.orcid.synchronization-settings-update.error": "La mise à jour des paramètres de synchronisation a échoué.", + + //"person.page.orcid.synchronization-mode.manual": "Manual", + "person.page.orcid.synchronization-mode.manual": "Manuel", + + //"person.page.orcid.scope.authenticate": "Get your ORCID iD", + "person.page.orcid.scope.authenticate": "Obtenez votre identifiant ORCID", + + //"person.page.orcid.scope.read-limited": "Read your information with visibility set to Trusted Parties", + "person.page.orcid.scope.read-limited": "Consultez vos informations ayant le paramètre de visibilité réglé sur Parties de confiance.", + + //"person.page.orcid.scope.activities-update": "Add/update your research activities", + "person.page.orcid.scope.activities-update": "Ajouter ou mettre à jour vos activités de recherche", + + //"person.page.orcid.scope.person-update": "Add/update other information about you", + "person.page.orcid.scope.person-update": "Ajouter ou mettre à jour d'autre information sur vous", + + //"person.page.orcid.unlink.success": "The disconnection between the profile and the ORCID registry was successful", + "person.page.orcid.unlink.success": "La déconnexion entre votre profil et le registre ORCID a été effectuée avec succès.", + + //"person.page.orcid.unlink.error": "An error occurred while disconnecting between the profile and the ORCID registry. Try again", + "person.page.orcid.unlink.error": "Une erreur s'est produite lors de la déconnexion entre votre profil et le registre ORCID. Veuillez réessayer.", + + //"person.orcid.sync.setting": "ORCID Synchronization settings", + "person.orcid.sync.setting": "Paramètres de synchronisation ORCID", + + //"person.orcid.registry.queue": "ORCID Registry Queue", + "person.orcid.registry.queue": "Liste d'attente pour le registre ORCID", + + //"person.orcid.registry.auth": "ORCID Authorizations", + "person.orcid.registry.auth": "Autorisation ORCID", + // "system-wide-alert-banner.retrieval.error": "Something went wrong retrieving the system-wide alert banner", "system-wide-alert-banner.retrieval.error": "Une erreur s'est produite lors de la récupération de la bannière du message d'avertissement", From 33262795babe9ada5d4d97f83fbaaf36cd8c626f Mon Sep 17 00:00:00 2001 From: Pierre Lasou Date: Fri, 1 Nov 2024 11:35:05 -0400 Subject: [PATCH 181/720] Correct small alignment errors (cherry picked from commit fde2db85e72e44e9606938d216c1205c8dabd253) --- src/assets/i18n/fr.json5 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/assets/i18n/fr.json5 b/src/assets/i18n/fr.json5 index 9c0b85cfe4..8122486a4e 100644 --- a/src/assets/i18n/fr.json5 +++ b/src/assets/i18n/fr.json5 @@ -6358,8 +6358,8 @@ //"person.page.orcid.sync-profile.biographical": "Biographical data", "person.page.orcid.sync-profile.biographical": "Données biographiques", - //"person.page.orcid.sync-profile.education": "Education", - "person.page.orcid.sync-profile.education": "Éducation", + //"person.page.orcid.sync-profile.education": "Education", + "person.page.orcid.sync-profile.education": "Éducation", //"person.page.orcid.sync-profile.identifiers": "Identifiers", "person.page.orcid.sync-profile.identifiers": "Identifiants", @@ -6490,8 +6490,8 @@ //"person.page.orcid.sync-queue.send.conflict-error": "The submission to ORCID failed because the resource is already present on the ORCID registry", "person.page.orcid.sync-queue.send.conflict-error": "La transmission à ORCID a échoué en raison du fait que la ressource est déjà présente dans le registre ORCID.", - //"person.page.orcid.sync-queue.send.not-found-warning": "The resource does not exists anymore on the ORCID registry.", - "person.page.orcid.sync-queue.send.not-found-warning": "La ressource n'existe plus dans le registre ORCID.", + //"person.page.orcid.sync-queue.send.not-found-warning": "The resource does not exists anymore on the ORCID registry.", + "person.page.orcid.sync-queue.send.not-found-warning": "La ressource n'existe plus dans le registre ORCID.", //"person.page.orcid.sync-queue.send.success": "The submission to ORCID was completed successfully", "person.page.orcid.sync-queue.send.success": "La transmission à ORCID a été effectuée avec succès.", @@ -6547,7 +6547,7 @@ //"person.page.orcid.sync-queue.send.validation-error.disambiguation-source.required": "The organization's identifiers requires a source", "person.page.orcid.sync-queue.send.validation-error.disambiguation-source.required": "La source de l'identifiant de l'organisation est obligatoire.", - //"person.page.orcid.sync-queue.send.validation-error.disambiguation-source.invalid": "The source of one of the organization identifiers is invalid. Supported sources are RINGGOLD, GRID, LEI and FUNDREF", + //"person.page.orcid.sync-queue.send.validation-error.disambiguation-source.invalid": "The source of one of the organization identifiers is invalid. Supported sources are RINGGOLD, GRID, LEI and FUNDREF", "person.page.orcid.sync-queue.send.validation-error.disambiguation-source.invalid": "La source d'un des identifiants d'organisation est invalide. Les sources possibles sont RINGGOLD, GRID, LEI and FUNDREF", //"person.page.orcid.synchronization-mode": "Synchronization mode", @@ -6589,7 +6589,7 @@ //"person.page.orcid.scope.activities-update": "Add/update your research activities", "person.page.orcid.scope.activities-update": "Ajouter ou mettre à jour vos activités de recherche", - //"person.page.orcid.scope.person-update": "Add/update other information about you", + //"person.page.orcid.scope.person-update": "Add/update other information about you", "person.page.orcid.scope.person-update": "Ajouter ou mettre à jour d'autre information sur vous", //"person.page.orcid.unlink.success": "The disconnection between the profile and the ORCID registry was successful", From 9c3363d4652c97279836e5b655a369eab18e76dc Mon Sep 17 00:00:00 2001 From: Pierre Lasou Date: Fri, 1 Nov 2024 14:02:00 -0400 Subject: [PATCH 182/720] Correction of 2 lint errors due to spacing. (cherry picked from commit 23dd7903ba36bd4140c3e15af2f9d33bfd2fd9a5) --- src/assets/i18n/fr.json5 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/assets/i18n/fr.json5 b/src/assets/i18n/fr.json5 index 8122486a4e..0f5587626b 100644 --- a/src/assets/i18n/fr.json5 +++ b/src/assets/i18n/fr.json5 @@ -6314,7 +6314,7 @@ "person.page.orcid.link.processing": "Liaison du profil avec ORCID en cours...", //"person.page.orcid.link.error.message": "Something went wrong while connecting the profile with ORCID. If the problem persists, contact the administrator.", - "person.page.orcid.link.error.message": "Quelque chose a échoué lors de la connection du profil avec ORCID. Si le problème persiste, contacter la personne administratice.", + "person.page.orcid.link.error.message": "Quelque chose a échoué lors de la connection du profil avec ORCID. Si le problème persiste, contacter la personne administratice.", //"person.page.orcid.orcid-not-linked-message": "The ORCID iD of this profile ({{ orcid }}) has not yet been connected to an account on the ORCID registry or the connection is expired.", "person.page.orcid.orcid-not-linked-message": "L'identifiant ORCID du profil ({{ orcid }}) n'a pas encore été connecté à une compte du registre ORCID ou la connection a expiré. ", @@ -6567,7 +6567,7 @@ //"person.page.orcid.synchronization-mode-publication-message": "Select whether to send your linked Publication entities to your ORCID record's list of works.", "person.page.orcid.synchronization-mode-publication-message": "Sélectionnez si vous souhaitez transmettre vos publications vers votre profil ORCID.", - + //"person.page.orcid.synchronization-mode-profile-message": "Select whether to send your biographical data or personal identifiers to your ORCID record.", "person.page.orcid.synchronization-mode-profile-message": "Sélectionnez si vous souhaitez transmettre vos données biographiques vers votre profil ORCID.", From 8f708d0e2898b0bf0eef9ce0d50f07e6f4119d88 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Tue, 22 Oct 2024 11:37:47 +0200 Subject: [PATCH 183/720] 119602: Add AccessibilitySettingsService --- .../accessibility-settings.service.ts | 231 ++++++++++++++++++ src/app/core/auth/auth.service.ts | 23 +- 2 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 src/app/accessibility/accessibility-settings.service.ts diff --git a/src/app/accessibility/accessibility-settings.service.ts b/src/app/accessibility/accessibility-settings.service.ts new file mode 100644 index 0000000000..805d0d5a0b --- /dev/null +++ b/src/app/accessibility/accessibility-settings.service.ts @@ -0,0 +1,231 @@ +import { Injectable } from '@angular/core'; +import { Observable, of, switchMap } from 'rxjs'; +import { map, take } from 'rxjs/operators'; +import { CookieService } from '../core/services/cookie.service'; +import { hasValue, isNotEmpty, isNotEmptyOperator } from '../shared/empty.util'; +import { AuthService } from '../core/auth/auth.service'; +import { EPerson } from '../core/eperson/models/eperson.model'; +import { EPersonDataService } from '../core/eperson/eperson-data.service'; +import { getFirstCompletedRemoteData } from '../core/shared/operators'; +import cloneDeep from 'lodash/cloneDeep'; + +/** + * Name of the cookie used to store the settings locally + */ +export const ACCESSIBILITY_COOKIE = 'dsAccessibilityCookie'; + +/** + * Name of the metadata field to store settings on the ePerson + */ +export const ACCESSIBILITY_SETTINGS_METADATA_KEY = 'dspace.accessibility.settings'; + +/** + * The duration in days after which the accessibility settings cookie expires + */ +export const ACCESSIBILITY_SETTINGS_COOKIE_STORAGE_DURATION = 7; + +/** + * Enum containing all possible accessibility settings. + * When adding new settings, the {@link AccessibilitySettingsService#getInputType} method and the i18n keys for the + * accessibility settings page should be updated. + */ +export enum AccessibilitySetting { + NotificationTimeOut = 'notificationTimeOut', + LiveRegionTimeOut = 'liveRegionTimeOut', +} + +export type AccessibilitySettings = { [key in AccessibilitySetting]?: any }; + +/** + * Service handling the retrieval and configuration of accessibility settings. + * + * This service stores the configured settings in either a cookie or on the user's metadata depending on whether + * the user is authenticated. + */ +@Injectable({ + providedIn: 'root' +}) +export class AccessibilitySettingsService { + + constructor( + protected cookieService: CookieService, + protected authService: AuthService, + protected ePersonService: EPersonDataService, + ) { + } + + getAllAccessibilitySettingKeys(): AccessibilitySetting[] { + return Object.entries(AccessibilitySetting).map(([_, val]) => val); + } + + /** + * Get the stored value for the provided {@link AccessibilitySetting}. If the value does not exist or if it is empty, + * the provided defaultValue is emitted instead. + */ + get(setting: AccessibilitySetting, defaultValue: string = null): Observable { + return this.getAll().pipe( + map(settings => settings[setting]), + map(value => isNotEmpty(value) ? value : defaultValue), + ); + } + + /** + * Get the stored value for the provided {@link AccessibilitySetting} as a number. If the stored value + * could not be converted to a number, the value of the defaultValue parameter is emitted instead. + */ + getAsNumber(setting: AccessibilitySetting, defaultValue: number = null): Observable { + return this.get(setting).pipe( + map(value => hasValue(value) ? parseInt(value, 10) : NaN), + map(number => !isNaN(number) ? number : defaultValue), + ); + } + + /** + * Get all currently stored accessibility settings + */ + getAll(): Observable { + return this.getAllSettingsFromAuthenticatedUserMetadata().pipe( + map(value => isNotEmpty(value) ? value : this.getAllSettingsFromCookie()), + map(value => isNotEmpty(value) ? value : {}), + ); + } + + /** + * Get all settings from the accessibility settings cookie + */ + getAllSettingsFromCookie(): AccessibilitySettings { + return this.cookieService.get(ACCESSIBILITY_COOKIE); + } + + /** + * Attempts to retrieve all settings from the authenticated user's metadata. + * Returns an empty object when no user is authenticated. + */ + getAllSettingsFromAuthenticatedUserMetadata(): Observable { + return this.authService.getAuthenticatedUserFromStoreIfAuthenticated().pipe( + take(1), + map(user => hasValue(user) && hasValue(user.firstMetadataValue(ACCESSIBILITY_SETTINGS_METADATA_KEY)) ? + JSON.parse(user.firstMetadataValue(ACCESSIBILITY_SETTINGS_METADATA_KEY)) : + {} + ), + ); + } + + /** + * Set a single accessibility setting value, leaving all other settings unchanged. + * When setting all values, {@link AccessibilitySettingsService#setSettings} should be used. + * When updating multiple values, {@link AccessibilitySettingsService#updateSettings} should be used. + * + * Returns 'cookie' when the changes were stored in the cookie. + * Returns 'metadata' when the changes were stored in metadata. + */ + set(setting: AccessibilitySetting, value: string): Observable<'cookie' | 'metadata'> { + return this.updateSettings({ [setting]: value }); + } + + /** + * Set all accessibility settings simultaneously. + * This method removes existing settings if they are missing from the provided {@link AccessibilitySettings} object. + * Removes all settings if the provided object is empty. + * + * Returns 'cookie' when the changes were stored in the cookie. + * Returns 'metadata' when the changes were stored in metadata. + */ + setSettings(settings: AccessibilitySettings): Observable<'cookie' | 'metadata'> { + return this.setSettingsInAuthenticatedUserMetadata(settings).pipe( + take(1), + map((succeeded) => { + if (!succeeded) { + this.setSettingsInCookie(settings); + return 'cookie'; + } else { + return 'metadata'; + } + }) + ); + } + + /** + * Update multiple accessibility settings simultaneously. + * This method does not change the settings that are missing from the provided {@link AccessibilitySettings} object. + * + * Returns 'cookie' when the changes were stored in the cookie. + * Returns 'metadata' when the changes were stored in metadata. + */ + updateSettings(settings: AccessibilitySettings): Observable<'cookie' | 'metadata'> { + return this.getAll().pipe( + take(1), + map(currentSettings => Object.assign({}, currentSettings, settings)), + switchMap(newSettings => this.setSettings(newSettings)) + ); + } + + /** + * Attempts to set the provided settings on the currently authorized user's metadata. + * Emits false when no user is authenticated or when the metadata update failed. + * Emits true when the metadata update succeeded. + */ + setSettingsInAuthenticatedUserMetadata(settings: AccessibilitySettings): Observable { + return this.authService.getAuthenticatedUserFromStoreIfAuthenticated().pipe( + take(1), + switchMap(user => { + if (hasValue(user)) { + // EPerson has to be cloned, otherwise the EPerson's metadata can't be modified + const clonedUser = cloneDeep(user); + return this.setSettingsInMetadata(clonedUser, settings); + } else { + return of(false); + } + }) + ); + } + + /** + * Attempts to set the provided settings on the user's metadata. + * Emits false when the update failed, true when the update succeeded. + */ + setSettingsInMetadata( + user: EPerson, + settings: AccessibilitySettings, + ): Observable { + if (isNotEmpty(settings)) { + user.setMetadata(ACCESSIBILITY_SETTINGS_METADATA_KEY, null, JSON.stringify(settings)); + } else { + user.removeMetadata(ACCESSIBILITY_SETTINGS_METADATA_KEY); + } + + return this.ePersonService.createPatchFromCache(user).pipe( + take(1), + isNotEmptyOperator(), + switchMap(operations => this.ePersonService.patch(user, operations)), + getFirstCompletedRemoteData(), + map(rd => rd.hasSucceeded), + ); + } + + /** + * Sets the provided settings in a cookie + */ + setSettingsInCookie(settings: AccessibilitySettings) { + if (isNotEmpty(settings)) { + this.cookieService.set(ACCESSIBILITY_COOKIE, settings, { expires: ACCESSIBILITY_SETTINGS_COOKIE_STORAGE_DURATION }); + } else { + this.cookieService.remove(ACCESSIBILITY_COOKIE); + } + } + + /** + * Returns the input type that a form should use for the provided {@link AccessibilitySetting} + */ + getInputType(setting: AccessibilitySetting): string { + switch (setting) { + case AccessibilitySetting.NotificationTimeOut: + return 'number'; + case AccessibilitySetting.LiveRegionTimeOut: + return 'number'; + default: + return 'text'; + } + } + +} diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index 6604936cde..9b0ec27735 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -44,7 +44,11 @@ import { import { NativeWindowRef, NativeWindowService } from '../services/window.service'; import { RouteService } from '../services/route.service'; import { EPersonDataService } from '../eperson/eperson-data.service'; -import { getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData } from '../shared/operators'; +import { + getAllSucceededRemoteDataPayload, + getFirstCompletedRemoteData, + getFirstSucceededRemoteDataPayload +} from '../shared/operators'; import { AuthMethod } from './models/auth.method'; import { HardRedirectService } from '../services/hard-redirect.service'; import { RemoteData } from '../data/remote-data'; @@ -229,6 +233,23 @@ export class AuthService { ); } + /** + * Returns an observable which emits the currently authenticated user from the store, + * or null if the user is not authenticated. + */ + public getAuthenticatedUserFromStoreIfAuthenticated(): Observable { + return this.store.pipe( + select(getAuthenticatedUserId), + switchMap((id: string) => { + if (hasValue(id)) { + return this.epersonService.findById(id).pipe(getFirstSucceededRemoteDataPayload()); + } else { + return observableOf(null); + } + }), + ); + } + /** * Checks if token is present into browser storage and is valid. */ From b72ce73931ace11de9c8cb3f8aed4abbf73e1d08 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Tue, 22 Oct 2024 14:34:35 +0200 Subject: [PATCH 184/720] 119602: Add Accessibility Settings page --- src/app/footer/footer.component.html | 4 ++ .../accessibility-settings.component.html | 26 ++++++++++ .../accessibility-settings.component.ts | 47 +++++++++++++++++++ src/app/info/info-routing-paths.ts | 5 ++ src/app/info/info-routing.module.ts | 16 ++++++- src/app/info/info.module.ts | 4 +- src/assets/i18n/en.json5 | 20 ++++++++ 7 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 src/app/info/accessibility-settings/accessibility-settings.component.html create mode 100644 src/app/info/accessibility-settings/accessibility-settings.component.ts diff --git a/src/app/footer/footer.component.html b/src/app/footer/footer.component.html index 13d84e6e2e..d3534706e8 100644 --- a/src/app/footer/footer.component.html +++ b/src/app/footer/footer.component.html @@ -79,6 +79,10 @@
Footer Content
{{ 'footer.link.feedback' | translate}} +
  • + {{ 'footer.link.accessibility' | translate }} +
  • diff --git a/src/app/info/accessibility-settings/accessibility-settings.component.html b/src/app/info/accessibility-settings/accessibility-settings.component.html new file mode 100644 index 0000000000..6550c6a288 --- /dev/null +++ b/src/app/info/accessibility-settings/accessibility-settings.component.html @@ -0,0 +1,26 @@ +
    +

    {{ 'info.accessibility-settings.title' | translate }}

    + +
    +
    + + +
    + + + + {{ 'info.accessibility-settings.' + setting + '.hint' | translate }} + +
    +
    + + +
    + +
    diff --git a/src/app/info/accessibility-settings/accessibility-settings.component.ts b/src/app/info/accessibility-settings/accessibility-settings.component.ts new file mode 100644 index 0000000000..97e3ddb321 --- /dev/null +++ b/src/app/info/accessibility-settings/accessibility-settings.component.ts @@ -0,0 +1,47 @@ +import { Component, OnInit } from '@angular/core'; +import { AuthService } from '../../core/auth/auth.service'; +import { + AccessibilitySetting, + AccessibilitySettingsService, + AccessibilitySettings +} from '../../accessibility/accessibility-settings.service'; +import { take } from 'rxjs'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; + +@Component({ + selector: 'ds-accessibility-settings', + templateUrl: './accessibility-settings.component.html' +}) +export class AccessibilitySettingsComponent implements OnInit { + + protected accessibilitySettingsOptions: AccessibilitySetting[]; + + protected formValues: AccessibilitySettings = { }; + + constructor( + protected authService: AuthService, + protected settingsService: AccessibilitySettingsService, + protected notificationsService: NotificationsService, + protected translateService: TranslateService, + ) { + } + + ngOnInit() { + this.accessibilitySettingsOptions = this.settingsService.getAllAccessibilitySettingKeys(); + this.settingsService.getAll().pipe(take(1)).subscribe(currentSettings => { + this.formValues = currentSettings; + }); + } + + getInputType(setting: AccessibilitySetting): string { + return this.settingsService.getInputType(setting); + } + + saveSettings() { + this.settingsService.setSettings(this.formValues).pipe(take(1)).subscribe(location => { + this.notificationsService.success(null, this.translateService.instant('info.accessibility-settings.save-notification.' + location)); + }); + } + +} diff --git a/src/app/info/info-routing-paths.ts b/src/app/info/info-routing-paths.ts index a18de2c611..5210bd7062 100644 --- a/src/app/info/info-routing-paths.ts +++ b/src/app/info/info-routing-paths.ts @@ -3,6 +3,7 @@ import { getInfoModulePath } from '../app-routing-paths'; export const END_USER_AGREEMENT_PATH = 'end-user-agreement'; export const PRIVACY_PATH = 'privacy'; export const FEEDBACK_PATH = 'feedback'; +export const ACCESSIBILITY_SETTINGS_PATH = 'accessibility'; export function getEndUserAgreementPath() { return getSubPath(END_USER_AGREEMENT_PATH); @@ -16,6 +17,10 @@ export function getFeedbackPath() { return getSubPath(FEEDBACK_PATH); } +export function getAccessibilitySettingsPath() { + return getSubPath(ACCESSIBILITY_SETTINGS_PATH); +} + function getSubPath(path: string) { return `${getInfoModulePath()}/${path}`; } diff --git a/src/app/info/info-routing.module.ts b/src/app/info/info-routing.module.ts index 4c497461e7..45079ba498 100644 --- a/src/app/info/info-routing.module.ts +++ b/src/app/info/info-routing.module.ts @@ -1,12 +1,18 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; -import { PRIVACY_PATH, END_USER_AGREEMENT_PATH, FEEDBACK_PATH } from './info-routing-paths'; +import { + PRIVACY_PATH, + END_USER_AGREEMENT_PATH, + FEEDBACK_PATH, + ACCESSIBILITY_SETTINGS_PATH +} from './info-routing-paths'; import { ThemedEndUserAgreementComponent } from './end-user-agreement/themed-end-user-agreement.component'; import { ThemedPrivacyComponent } from './privacy/themed-privacy.component'; import { ThemedFeedbackComponent } from './feedback/themed-feedback.component'; import { FeedbackGuard } from '../core/feedback/feedback.guard'; import { environment } from '../../environments/environment'; +import { AccessibilitySettingsComponent } from './accessibility-settings/accessibility-settings.component'; const imports = [ @@ -17,7 +23,13 @@ const imports = [ resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { title: 'info.feedback.title', breadcrumbKey: 'info.feedback' }, canActivate: [FeedbackGuard] - } + }, + { + path: ACCESSIBILITY_SETTINGS_PATH, + component: AccessibilitySettingsComponent, + resolve: { breadcrumb: I18nBreadcrumbResolver }, + data: { title: 'info.accessibility-settings.title', breadcrumbKey: 'info.accessibility-settings' }, + }, ]) ]; diff --git a/src/app/info/info.module.ts b/src/app/info/info.module.ts index ccc4af0a7d..d01ded1af0 100644 --- a/src/app/info/info.module.ts +++ b/src/app/info/info.module.ts @@ -13,6 +13,7 @@ import { FeedbackFormComponent } from './feedback/feedback-form/feedback-form.co import { ThemedFeedbackFormComponent } from './feedback/feedback-form/themed-feedback-form.component'; import { ThemedFeedbackComponent } from './feedback/themed-feedback.component'; import { FeedbackGuard } from '../core/feedback/feedback.guard'; +import { AccessibilitySettingsComponent } from './accessibility-settings/accessibility-settings.component'; const DECLARATIONS = [ @@ -25,7 +26,8 @@ const DECLARATIONS = [ FeedbackComponent, FeedbackFormComponent, ThemedFeedbackFormComponent, - ThemedFeedbackComponent + ThemedFeedbackComponent, + AccessibilitySettingsComponent, ]; @NgModule({ diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 6c91bae4c1..1316c8d3cf 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1618,6 +1618,8 @@ "footer.copyright": "copyright © 2002-{{ year }}", + "footer.link.accessibility": "Accessibility settings", + "footer.link.dspace": "DSpace software", "footer.link.lyrasis": "LYRASIS", @@ -1840,6 +1842,24 @@ "home.top-level-communities.help": "Select a community to browse its collections.", + "info.accessibility-settings.breadcrumbs": "Accessibility settings", + + "info.accessibility-settings.liveRegionTimeOut.label": "Live region time-out", + + "info.accessibility-settings.liveRegionTimeOut.hint": "The duration in milliseconds after which a message in the live region disappears.", + + "info.accessibility-settings.notificationTimeOut.label": "Notification time-out", + + "info.accessibility-settings.notificationTimeOut.hint": "The duration in milliseconds after which a notification disappears. Set to 0 for notifications to remain indefinitely.", + + "info.accessibility-settings.save-notification.cookie": "Successfully saved settings locally.", + + "info.accessibility-settings.save-notification.metadata": "Successfully saved settings on the user profile.", + + "info.accessibility-settings.submit": "Save accessibility settings", + + "info.accessibility-settings.title": "Accessibility settings", + "info.end-user-agreement.accept": "I have read and I agree to the End User Agreement", "info.end-user-agreement.accept.error": "An error occurred accepting the End User Agreement", From d224a2c47db09d6e202f66f0192afc8ad2ab1ce5 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Tue, 22 Oct 2024 15:03:37 +0200 Subject: [PATCH 185/720] 119602: Integrate accessibility settings into live-region --- .../shared/live-region/live-region.service.ts | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/app/shared/live-region/live-region.service.ts b/src/app/shared/live-region/live-region.service.ts index 72940c1a0e..7a7b99fa6e 100644 --- a/src/app/shared/live-region/live-region.service.ts +++ b/src/app/shared/live-region/live-region.service.ts @@ -1,7 +1,10 @@ import { Injectable } from '@angular/core'; -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, map, Observable, switchMap, take, timer } from 'rxjs'; import { environment } from '../../../environments/environment'; import { UUIDService } from '../../core/shared/uuid.service'; +import { AccessibilitySettingsService, AccessibilitySetting } from '../../accessibility/accessibility-settings.service'; + +export const MIN_MESSAGE_DURATION = 200; /** * The LiveRegionService is responsible for handling the messages that are shown by the {@link LiveRegionComponent}. @@ -14,6 +17,7 @@ export class LiveRegionService { constructor( protected uuidService: UUIDService, + protected accessibilitySettingsService: AccessibilitySettingsService, ) { } @@ -64,7 +68,12 @@ export class LiveRegionService { addMessage(message: string): string { const uuid = this.uuidService.generate(); this.messages.push({ message, uuid }); - setTimeout(() => this.clearMessageByUUID(uuid), this.messageTimeOutDurationMs); + + this.getConfiguredMessageTimeOutMs().pipe( + take(1), + switchMap(timeOut => timer(timeOut)), + ).subscribe(() => this.clearMessageByUUID(uuid)); + this.emitCurrentMessages(); return uuid; } @@ -115,6 +124,17 @@ export class LiveRegionService { this.liveRegionIsVisible = isVisible; } + /** + * Gets the user-configured timeOut, or the stored timeOut if the user has not configured a timeOut duration. + * Emits {@link MIN_MESSAGE_DURATION} if the configured value is smaller. + */ + getConfiguredMessageTimeOutMs(): Observable { + return this.accessibilitySettingsService.getAsNumber( + AccessibilitySetting.LiveRegionTimeOut, + this.getMessageTimeOutMs(), + ).pipe(map(timeOut => Math.max(timeOut, MIN_MESSAGE_DURATION))); + } + /** * Gets the current message timeOut duration in milliseconds */ From 6a49df59af0206b1cf2bd8b6cba4a3391ba7eeac Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Fri, 25 Oct 2024 13:04:55 +0200 Subject: [PATCH 186/720] 119602: Integrate accessibility settings into notifications-board --- .../notifications-board.component.ts | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/src/app/shared/notifications/notifications-board/notifications-board.component.ts b/src/app/shared/notifications/notifications-board/notifications-board.component.ts index 97ae09c1a6..20bf7175f6 100644 --- a/src/app/shared/notifications/notifications-board/notifications-board.component.ts +++ b/src/app/shared/notifications/notifications-board/notifications-board.component.ts @@ -9,7 +9,7 @@ import { } from '@angular/core'; import { select, Store } from '@ngrx/store'; -import { BehaviorSubject, Subscription } from 'rxjs'; +import { BehaviorSubject, Subscription, take } from 'rxjs'; import difference from 'lodash/difference'; import { NotificationsService } from '../notifications.service'; @@ -18,6 +18,11 @@ import { notificationsStateSelector } from '../selectors'; import { INotification } from '../models/notification.model'; import { NotificationsState } from '../notifications.reducers'; import { INotificationBoardOptions } from '../../../../config/notifications-config.interfaces'; +import { + AccessibilitySettingsService, + AccessibilitySetting +} from '../../../accessibility/accessibility-settings.service'; +import cloneDeep from 'lodash/cloneDeep'; @Component({ selector: 'ds-notifications-board', @@ -49,9 +54,12 @@ export class NotificationsBoardComponent implements OnInit, OnDestroy { */ public isPaused$: BehaviorSubject = new BehaviorSubject(false); - constructor(private service: NotificationsService, - private store: Store, - private cdr: ChangeDetectorRef) { + constructor( + protected service: NotificationsService, + protected store: Store, + protected cdr: ChangeDetectorRef, + protected accessibilitySettingsService: AccessibilitySettingsService, + ) { } ngOnInit(): void { @@ -84,7 +92,22 @@ export class NotificationsBoardComponent implements OnInit, OnDestroy { if (this.notifications.length >= this.maxStack) { this.notifications.splice(this.notifications.length - 1, 1); } - this.notifications.splice(0, 0, item); + + // It would be a bit better to handle the retrieval of configured settings in the NotificationsService. + // Due to circular dependencies this is difficult to implement. + this.accessibilitySettingsService.getAsNumber(AccessibilitySetting.NotificationTimeOut, item.options.timeOut) + .pipe(take(1)).subscribe(timeOut => { + if (timeOut < 0) { + timeOut = 0; + } + + // Deep clone because the unaltered item is read-only + const modifiedNotification = cloneDeep(item); + modifiedNotification.options.timeOut = timeOut; + this.notifications.splice(0, 0, modifiedNotification); + this.cdr.detectChanges(); + }); + } else { // Remove the notification from the store // This notification was in the store, but not in this.notifications From cad086c94599675b195f2685312c52e8c83e16f8 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Fri, 25 Oct 2024 14:39:23 +0200 Subject: [PATCH 187/720] 119602: Add AccessibilitySettingsService stub & fix live-region test --- .../accessibility-settings.service.stub.ts | 34 ++++++++++++ .../live-region/live-region.service.spec.ts | 55 +++++-------------- .../notifications-board.component.spec.ts | 3 + 3 files changed, 51 insertions(+), 41 deletions(-) create mode 100644 src/app/accessibility/accessibility-settings.service.stub.ts diff --git a/src/app/accessibility/accessibility-settings.service.stub.ts b/src/app/accessibility/accessibility-settings.service.stub.ts new file mode 100644 index 0000000000..b619a337de --- /dev/null +++ b/src/app/accessibility/accessibility-settings.service.stub.ts @@ -0,0 +1,34 @@ +import { of } from 'rxjs'; +import { AccessibilitySettingsService } from './accessibility-settings.service'; + +export function getAccessibilitySettingsServiceStub(): AccessibilitySettingsService { + return new AccessibilitySettingsServiceStub() as unknown as AccessibilitySettingsService; +} + +export class AccessibilitySettingsServiceStub { + getAllAccessibilitySettingKeys = jasmine.createSpy('getAllAccessibilitySettingKeys').and.returnValue([]); + + get = jasmine.createSpy('get').and.returnValue(of(null)); + + getAsNumber = jasmine.createSpy('getAsNumber').and.returnValue(of(0)); + + getAll = jasmine.createSpy('getAll').and.returnValue(of({})); + + getAllSettingsFromCookie = jasmine.createSpy('getAllSettingsFromCookie').and.returnValue({}); + + getAllSettingsFromAuthenticatedUserMetadata = jasmine.createSpy('getAllSettingsFromAuthenticatedUserMetadata') + .and.returnValue(of({})); + + set = jasmine.createSpy('setSettings').and.returnValue(of('cookie')); + + updateSettings = jasmine.createSpy('updateSettings').and.returnValue(of('cookie')); + + setSettingsInAuthenticatedUserMetadata = jasmine.createSpy('setSettingsInAuthenticatedUserMetadata') + .and.returnValue(of(false)); + + setSettingsInMetadata = jasmine.createSpy('setSettingsInMetadata').and.returnValue(of(false)); + + setSettingsInCookie = jasmine.createSpy('setSettingsInCookie'); + + getInputType = jasmine.createSpy('getInputType').and.returnValue('text'); +} diff --git a/src/app/shared/live-region/live-region.service.spec.ts b/src/app/shared/live-region/live-region.service.spec.ts index 858ef88313..b14fa7abaf 100644 --- a/src/app/shared/live-region/live-region.service.spec.ts +++ b/src/app/shared/live-region/live-region.service.spec.ts @@ -1,13 +1,22 @@ import { LiveRegionService } from './live-region.service'; -import { fakeAsync, tick, flush } from '@angular/core/testing'; +import { fakeAsync, tick } from '@angular/core/testing'; import { UUIDService } from '../../core/shared/uuid.service'; +import { getAccessibilitySettingsServiceStub } from '../../accessibility/accessibility-settings.service.stub'; +import { AccessibilitySettingsService } from '../../accessibility/accessibility-settings.service'; +import { of } from 'rxjs'; describe('liveRegionService', () => { let service: LiveRegionService; + let accessibilitySettingsService: AccessibilitySettingsService; beforeEach(() => { + accessibilitySettingsService = getAccessibilitySettingsServiceStub(); + + accessibilitySettingsService.getAsNumber = jasmine.createSpy('getAsNumber').and.returnValue(of(100)); + service = new LiveRegionService( new UUIDService(), + accessibilitySettingsService, ); }); @@ -81,13 +90,16 @@ describe('liveRegionService', () => { expect(results[2]).toEqual(['Message One', 'Message Two']); service.clear(); - flush(); + tick(200); expect(results.length).toEqual(4); expect(results[3]).toEqual([]); })); it('should not pop messages added after clearing within timeOut period', fakeAsync(() => { + // test expects a clear rate of 30 seconds + accessibilitySettingsService.getAsNumber = jasmine.createSpy('getAsNumber').and.returnValue(of(30000)); + const results: string[][] = []; service.getMessages$().subscribe((messages) => { @@ -114,45 +126,6 @@ describe('liveRegionService', () => { expect(results.length).toEqual(5); expect(results[4]).toEqual([]); })); - - it('should respect configured timeOut', fakeAsync(() => { - const results: string[][] = []; - - service.getMessages$().subscribe((messages) => { - results.push(messages); - }); - - expect(results.length).toEqual(1); - expect(results[0]).toEqual([]); - - const timeOutMs = 500; - service.setMessageTimeOutMs(timeOutMs); - - service.addMessage('Message One'); - tick(timeOutMs - 1); - - expect(results.length).toEqual(2); - expect(results[1]).toEqual(['Message One']); - - tick(1); - - expect(results.length).toEqual(3); - expect(results[2]).toEqual([]); - - const timeOutMsTwo = 50000; - service.setMessageTimeOutMs(timeOutMsTwo); - - service.addMessage('Message Two'); - tick(timeOutMsTwo - 1); - - expect(results.length).toEqual(4); - expect(results[3]).toEqual(['Message Two']); - - tick(1); - - expect(results.length).toEqual(5); - expect(results[4]).toEqual([]); - })); }); describe('liveRegionVisibility', () => { diff --git a/src/app/shared/notifications/notifications-board/notifications-board.component.spec.ts b/src/app/shared/notifications/notifications-board/notifications-board.component.spec.ts index 08b9585a8c..22d0671d9c 100644 --- a/src/app/shared/notifications/notifications-board/notifications-board.component.spec.ts +++ b/src/app/shared/notifications/notifications-board/notifications-board.component.spec.ts @@ -15,6 +15,8 @@ import uniqueId from 'lodash/uniqueId'; import { INotificationBoardOptions } from '../../../../config/notifications-config.interfaces'; import { NotificationsServiceStub } from '../../testing/notifications-service.stub'; import { cold } from 'jasmine-marbles'; +import { AccessibilitySettingsService } from '../../../accessibility/accessibility-settings.service'; +import { getAccessibilitySettingsServiceStub } from '../../../accessibility/accessibility-settings.service.stub'; export const bools = { f: false, t: true }; @@ -36,6 +38,7 @@ describe('NotificationsBoardComponent', () => { declarations: [NotificationsBoardComponent, NotificationComponent], // declare the test component providers: [ { provide: NotificationsService, useClass: NotificationsServiceStub }, + { provide: AccessibilitySettingsService, useValue: getAccessibilitySettingsServiceStub() }, ChangeDetectorRef] }).compileComponents(); // compile template and css })); From 52eabec70d46649fd97faeee229deaac0da989f5 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Fri, 25 Oct 2024 15:27:37 +0200 Subject: [PATCH 188/720] 119602: Add AccessibilitySettingsService tests --- .../accessibility-settings.service.spec.ts | 379 ++++++++++++++++++ src/app/shared/testing/auth-service.stub.ts | 4 + 2 files changed, 383 insertions(+) create mode 100644 src/app/accessibility/accessibility-settings.service.spec.ts diff --git a/src/app/accessibility/accessibility-settings.service.spec.ts b/src/app/accessibility/accessibility-settings.service.spec.ts new file mode 100644 index 0000000000..d6f6184057 --- /dev/null +++ b/src/app/accessibility/accessibility-settings.service.spec.ts @@ -0,0 +1,379 @@ +import { + AccessibilitySettingsService, + AccessibilitySetting, + AccessibilitySettings, + ACCESSIBILITY_SETTINGS_METADATA_KEY, + ACCESSIBILITY_COOKIE +} from './accessibility-settings.service'; +import { CookieService } from '../core/services/cookie.service'; +import { AuthService } from '../core/auth/auth.service'; +import { EPersonDataService } from '../core/eperson/eperson-data.service'; +import { CookieServiceMock } from '../shared/mocks/cookie.service.mock'; +import { AuthServiceStub } from '../shared/testing/auth-service.stub'; +import { of } from 'rxjs'; +import { EPerson } from '../core/eperson/models/eperson.model'; +import { fakeAsync, flush } from '@angular/core/testing'; +import { createSuccessfulRemoteDataObject$, createFailedRemoteDataObject$ } from '../shared/remote-data.utils'; + + +describe('accessibilitySettingsService', () => { + let service: AccessibilitySettingsService; + let cookieService: CookieServiceMock; + let authService: AuthServiceStub; + let ePersonService: EPersonDataService; + + beforeEach(() => { + cookieService = new CookieServiceMock(); + authService = new AuthServiceStub(); + + ePersonService = jasmine.createSpyObj('ePersonService', { + createPatchFromCache: of([{ + op: 'add', + value: null, + }]), + patch: of({}), + }); + + service = new AccessibilitySettingsService( + cookieService as unknown as CookieService, + authService as unknown as AuthService, + ePersonService, + ); + }); + + describe('getALlAccessibilitySettingsKeys', () => { + it('should return an array containing all accessibility setting names', () => { + const settingNames: AccessibilitySetting[] = [ + AccessibilitySetting.NotificationTimeOut, + AccessibilitySetting.LiveRegionTimeOut, + ]; + + expect(service.getAllAccessibilitySettingKeys()).toEqual(settingNames); + }); + }); + + describe('get', () => { + it('should return the setting if it is set', () => { + const settings: AccessibilitySettings = { + notificationTimeOut: '1000', + }; + + service.getAll = jasmine.createSpy('getAll').and.returnValue(of(settings)); + + service.get(AccessibilitySetting.NotificationTimeOut, 'default').subscribe(value => + expect(value).toEqual('1000') + ); + }); + + it('should return the default value if the setting is not set', () => { + const settings: AccessibilitySettings = { + notificationTimeOut: '1000', + }; + + service.getAll = jasmine.createSpy('getAll').and.returnValue(of(settings)); + + service.get(AccessibilitySetting.LiveRegionTimeOut, 'default').subscribe(value => + expect(value).toEqual('default') + ); + }); + }); + + describe('getAsNumber', () => { + it('should return the setting as number if the value for the setting can be parsed to a number', () => { + service.get = jasmine.createSpy('get').and.returnValue(of('1000')); + + service.getAsNumber(AccessibilitySetting.NotificationTimeOut).subscribe(value => + expect(value).toEqual(1000) + ); + }); + + it('should return the default value if no value is set for the setting', () => { + service.get = jasmine.createSpy('get').and.returnValue(of(null)); + + service.getAsNumber(AccessibilitySetting.NotificationTimeOut, 123).subscribe(value => + expect(value).toEqual(123) + ); + }); + + it('should return the default value if the value for the setting can not be parsed to a number', () => { + service.get = jasmine.createSpy('get').and.returnValue(of('text')); + + service.getAsNumber(AccessibilitySetting.NotificationTimeOut, 123).subscribe(value => + expect(value).toEqual(123) + ); + }); + }); + + describe('getAll', () => { + it('should attempt to get the settings from metadata first', () => { + service.getAllSettingsFromAuthenticatedUserMetadata = + jasmine.createSpy('getAllSettingsFromAuthenticatedUserMetadata').and.returnValue(of({ })); + + service.getAll().subscribe(); + expect(service.getAllSettingsFromAuthenticatedUserMetadata).toHaveBeenCalled(); + }); + + it('should attempt to get the settings from the cookie if the settings from metadata are empty', () => { + service.getAllSettingsFromAuthenticatedUserMetadata = + jasmine.createSpy('getAllSettingsFromAuthenticatedUserMetadata').and.returnValue(of({ })); + + service.getAllSettingsFromCookie = jasmine.createSpy('getAllSettingsFromCookie').and.returnValue({ }); + + service.getAll().subscribe(); + expect(service.getAllSettingsFromCookie).toHaveBeenCalled(); + }); + + it('should not attempt to get the settings from the cookie if the settings from metadata are not empty', () => { + const settings: AccessibilitySettings = { + notificationTimeOut: '1000', + }; + + service.getAllSettingsFromAuthenticatedUserMetadata = + jasmine.createSpy('getAllSettingsFromAuthenticatedUserMetadata').and.returnValue(of(settings)); + + service.getAllSettingsFromCookie = jasmine.createSpy('getAllSettingsFromCookie').and.returnValue({ }); + + service.getAll().subscribe(); + expect(service.getAllSettingsFromCookie).not.toHaveBeenCalled(); + }); + + it('should return an empty object if both are empty', () => { + service.getAllSettingsFromAuthenticatedUserMetadata = + jasmine.createSpy('getAllSettingsFromAuthenticatedUserMetadata').and.returnValue(of({ })); + + service.getAllSettingsFromCookie = jasmine.createSpy('getAllSettingsFromCookie').and.returnValue({ }); + + service.getAll().subscribe(value => expect(value).toEqual({})); + }); + }); + + describe('getAllSettingsFromCookie', () => { + it('should retrieve the settings from the cookie', () => { + cookieService.get = jasmine.createSpy(); + + service.getAllSettingsFromCookie(); + expect(cookieService.get).toHaveBeenCalledWith(ACCESSIBILITY_COOKIE); + }); + }); + + describe('getAllSettingsFromAuthenticatedUserMetadata', () => { + it('should retrieve all settings from the user\'s metadata', () => { + const settings = { 'liveRegionTimeOut': '1000' }; + + const user = new EPerson(); + user.setMetadata(ACCESSIBILITY_SETTINGS_METADATA_KEY, null, JSON.stringify(settings)); + + authService.getAuthenticatedUserFromStoreIfAuthenticated = + jasmine.createSpy('getAuthenticatedUserFromStoreIfAuthenticated').and.returnValue(of(user)); + + service.getAllSettingsFromAuthenticatedUserMetadata().subscribe(value => + expect(value).toEqual(settings) + ); + }); + }); + + describe('set', () => { + it('should correctly update the chosen setting', () => { + service.updateSettings = jasmine.createSpy('updateSettings'); + + service.set(AccessibilitySetting.LiveRegionTimeOut, '500'); + expect(service.updateSettings).toHaveBeenCalledWith({ liveRegionTimeOut: '500' }); + }); + }); + + describe('setSettings', () => { + beforeEach(() => { + service.setSettingsInCookie = jasmine.createSpy('setSettingsInCookie'); + }); + + it('should attempt to set settings in metadata', () => { + service.setSettingsInAuthenticatedUserMetadata = + jasmine.createSpy('setSettingsInAuthenticatedUserMetadata').and.returnValue(of(false)); + + const settings: AccessibilitySettings = { + notificationTimeOut: '1000', + }; + + service.setSettings(settings).subscribe(); + expect(service.setSettingsInAuthenticatedUserMetadata).toHaveBeenCalledWith(settings); + }); + + it('should set settings in cookie if metadata failed', () => { + service.setSettingsInAuthenticatedUserMetadata = + jasmine.createSpy('setSettingsInAuthenticatedUserMetadata').and.returnValue(of(false)); + + const settings: AccessibilitySettings = { + notificationTimeOut: '1000', + }; + + service.setSettings(settings).subscribe(); + expect(service.setSettingsInCookie).toHaveBeenCalled(); + }); + + it('should not set settings in cookie if metadata succeeded', () => { + service.setSettingsInAuthenticatedUserMetadata = + jasmine.createSpy('setSettingsInAuthenticatedUserMetadata').and.returnValue(of(true)); + + const settings: AccessibilitySettings = { + notificationTimeOut: '1000', + }; + + service.setSettings(settings).subscribe(); + expect(service.setSettingsInCookie).not.toHaveBeenCalled(); + }); + + it('should return \'metadata\' if settings are stored in metadata', () => { + service.setSettingsInAuthenticatedUserMetadata = + jasmine.createSpy('setSettingsInAuthenticatedUserMetadata').and.returnValue(of(true)); + + const settings: AccessibilitySettings = { + notificationTimeOut: '1000', + }; + + service.setSettings(settings).subscribe(value => + expect(value).toEqual('metadata') + ); + }); + + it('should return \'cookie\' if settings are stored in cookie', () => { + service.setSettingsInAuthenticatedUserMetadata = + jasmine.createSpy('setSettingsInAuthenticatedUserMetadata').and.returnValue(of(false)); + + const settings: AccessibilitySettings = { + notificationTimeOut: '1000', + }; + + service.setSettings(settings).subscribe(value => + expect(value).toEqual('cookie') + ); + }); + }); + + describe('updateSettings', () => { + it('should call setSettings with the updated settings', () => { + const beforeSettings: AccessibilitySettings = { + notificationTimeOut: '1000', + }; + + service.getAll = jasmine.createSpy('getAll').and.returnValue(of(beforeSettings)); + service.setSettings = jasmine.createSpy('setSettings').and.returnValue(of('cookie')); + + const newSettings: AccessibilitySettings = { + liveRegionTimeOut: '2000', + }; + + const combinedSettings: AccessibilitySettings = { + notificationTimeOut: '1000', + liveRegionTimeOut: '2000', + }; + + service.updateSettings(newSettings).subscribe(); + expect(service.setSettings).toHaveBeenCalledWith(combinedSettings); + }); + }); + + describe('setSettingsInAuthenticatedUserMetadata', () => { + beforeEach(() => { + service.setSettingsInMetadata = jasmine.createSpy('setSettingsInMetadata').and.returnValue(of(null)); + }); + + it('should store settings in metadata when the user is authenticated', fakeAsync(() => { + const user = new EPerson(); + authService.getAuthenticatedUserFromStoreIfAuthenticated = jasmine.createSpy().and.returnValue(of(user)); + + service.setSettingsInAuthenticatedUserMetadata({}).subscribe(); + flush(); + + expect(service.setSettingsInMetadata).toHaveBeenCalled(); + })); + + it('should emit false when the user is not authenticated', fakeAsync(() => { + authService.getAuthenticatedUserFromStoreIfAuthenticated = jasmine.createSpy().and.returnValue(of(null)); + + service.setSettingsInAuthenticatedUserMetadata({}) + .subscribe(value => expect(value).toBeFalse()); + flush(); + + expect(service.setSettingsInMetadata).not.toHaveBeenCalled(); + })); + }); + + describe('setSettingsInMetadata', () => { + const ePerson = new EPerson(); + + beforeEach(() => { + ePerson.setMetadata = jasmine.createSpy('setMetadata'); + ePerson.removeMetadata = jasmine.createSpy('removeMetadata'); + }); + + it('should set the settings in metadata', () => { + service.setSettingsInMetadata(ePerson, { [AccessibilitySetting.LiveRegionTimeOut]: '500' }).subscribe(); + expect(ePerson.setMetadata).toHaveBeenCalled(); + }); + + it('should remove the metadata when the settings are emtpy', () => { + service.setSettingsInMetadata(ePerson, {}).subscribe(); + expect(ePerson.setMetadata).not.toHaveBeenCalled(); + expect(ePerson.removeMetadata).toHaveBeenCalled(); + }); + + it('should create a patch with the metadata changes', () => { + service.setSettingsInMetadata(ePerson, { [AccessibilitySetting.LiveRegionTimeOut]: '500' }).subscribe(); + expect(ePersonService.createPatchFromCache).toHaveBeenCalled(); + }); + + it('should send the patch request', () => { + service.setSettingsInMetadata(ePerson, { [AccessibilitySetting.LiveRegionTimeOut]: '500' }).subscribe(); + expect(ePersonService.patch).toHaveBeenCalled(); + }); + + it('should emit true when the update succeeded', fakeAsync(() => { + ePersonService.patch = jasmine.createSpy().and.returnValue(createSuccessfulRemoteDataObject$({})); + + service.setSettingsInMetadata(ePerson, { [AccessibilitySetting.LiveRegionTimeOut]: '500' }) + .subscribe(value => { + expect(value).toBeTrue(); + }); + + flush(); + })); + + it('should emit false when the update failed', fakeAsync(() => { + ePersonService.patch = jasmine.createSpy().and.returnValue(createFailedRemoteDataObject$()); + + service.setSettingsInMetadata(ePerson, { [AccessibilitySetting.LiveRegionTimeOut]: '500' }) + .subscribe(value => { + expect(value).toBeFalse(); + }); + + flush(); + })); + }); + + describe('setSettingsInCookie', () => { + beforeEach(() => { + cookieService.set = jasmine.createSpy('set'); + cookieService.remove = jasmine.createSpy('remove'); + }); + + it('should store the settings in a cookie', () => { + service.setSettingsInCookie({ [AccessibilitySetting.LiveRegionTimeOut]: '500' }); + expect(cookieService.set).toHaveBeenCalled(); + }); + + it('should remove the cookie when the settings are empty', () => { + service.setSettingsInCookie({}); + expect(cookieService.set).not.toHaveBeenCalled(); + expect(cookieService.remove).toHaveBeenCalled(); + }); + }); + + describe('getInputType', () => { + it('should correctly return the input type', () => { + expect(service.getInputType(AccessibilitySetting.NotificationTimeOut)).toEqual('number'); + expect(service.getInputType(AccessibilitySetting.LiveRegionTimeOut)).toEqual('number'); + expect(service.getInputType('unknownValue' as AccessibilitySetting)).toEqual('text'); + }); + }); + +}); diff --git a/src/app/shared/testing/auth-service.stub.ts b/src/app/shared/testing/auth-service.stub.ts index 7f3d040042..d150ac69f4 100644 --- a/src/app/shared/testing/auth-service.stub.ts +++ b/src/app/shared/testing/auth-service.stub.ts @@ -54,6 +54,10 @@ export class AuthServiceStub { return observableOf(EPersonMock); } + getAuthenticatedUserFromStoreIfAuthenticated(): Observable { + return observableOf(EPersonMock); + } + public buildAuthHeader(token?: AuthTokenInfo): string { return `Bearer ${token ? token.accessToken : ''}`; } From 82fd9539b7862186f2396a64db8988bc7f38f362 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Tue, 5 Nov 2024 15:03:51 +0100 Subject: [PATCH 189/720] 119602: Add AccessibilitySettingsComponent tests --- .../accessibility-settings.component.spec.ts | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 src/app/info/accessibility-settings/accessibility-settings.component.spec.ts diff --git a/src/app/info/accessibility-settings/accessibility-settings.component.spec.ts b/src/app/info/accessibility-settings/accessibility-settings.component.spec.ts new file mode 100644 index 0000000000..f6d3252a38 --- /dev/null +++ b/src/app/info/accessibility-settings/accessibility-settings.component.spec.ts @@ -0,0 +1,79 @@ +import { AccessibilitySettingsComponent } from './accessibility-settings.component'; +import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { AuthServiceStub } from '../../shared/testing/auth-service.stub'; +import { getAccessibilitySettingsServiceStub } from '../../accessibility/accessibility-settings.service.stub'; +import { AccessibilitySettingsService, AccessibilitySetting } from '../../accessibility/accessibility-settings.service'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; +import { AuthService } from '../../core/auth/auth.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { of } from 'rxjs'; + + +describe('AccessibilitySettingsComponent', () => { + let component: AccessibilitySettingsComponent; + let fixture: ComponentFixture; + + let authService: AuthServiceStub; + let settingsService: AccessibilitySettingsService; + let notificationsService: NotificationsServiceStub; + + beforeEach(waitForAsync(() => { + authService = new AuthServiceStub(); + settingsService = getAccessibilitySettingsServiceStub(); + notificationsService = new NotificationsServiceStub(); + + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [AccessibilitySettingsComponent], + providers: [ + { provide: AuthService, useValue: authService }, + { provide: AccessibilitySettingsService, useValue: settingsService }, + { provide: NotificationsService, useValue: notificationsService }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AccessibilitySettingsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('On Init', () => { + it('should retrieve all accessibility settings options', () => { + expect(settingsService.getAllAccessibilitySettingKeys).toHaveBeenCalled(); + }); + + it('should retrieve the current settings', () => { + expect(settingsService.getAll).toHaveBeenCalled(); + }); + }); + + describe('getInputType', () => { + it('should retrieve the input type for the setting from the service', () => { + component.getInputType(AccessibilitySetting.LiveRegionTimeOut); + expect(settingsService.getInputType).toHaveBeenCalledWith(AccessibilitySetting.LiveRegionTimeOut); + }); + }); + + describe('saveSettings', () => { + it('should save the settings in the service', () => { + settingsService.setSettings = jasmine.createSpy('setSettings').and.returnValue(of('cookie')); + component.saveSettings(); + expect(settingsService.setSettings).toHaveBeenCalled(); + }); + + it('should give the user a notification mentioning where the settings were saved', () => { + settingsService.setSettings = jasmine.createSpy('setSettings').and.returnValue(of('cookie')); + component.saveSettings(); + expect(notificationsService.success).toHaveBeenCalled(); + }); + }); +}); From 37455a8b6c8f768b5b8cc7a3f4877740b2ae44bd Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Tue, 5 Nov 2024 15:52:11 +0100 Subject: [PATCH 190/720] 119602: Make AccessibilitySettings cookie expiration configurable --- config/config.example.yml | 5 +++++ .../accessibility/accessibility-settings.config.ts | 11 +++++++++++ .../accessibility/accessibility-settings.service.ts | 8 ++------ src/config/app-config.interface.ts | 2 ++ src/config/default-app-config.ts | 6 ++++++ src/environments/environment.test.ts | 4 ++++ 6 files changed, 30 insertions(+), 6 deletions(-) create mode 100644 src/app/accessibility/accessibility-settings.config.ts diff --git a/config/config.example.yml b/config/config.example.yml index 58eb6ff33d..7b882958f2 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -394,3 +394,8 @@ liveRegion: messageTimeOutDurationMs: 30000 # The visibility of the live region. Setting this to true is only useful for debugging purposes. isVisible: false + +# Configuration for storing accessibility settings, used by the AccessibilitySettingsService +accessibility: + # The duration in days after which the accessibility settings cookie expires + cookieExpirationDuration: 7 diff --git a/src/app/accessibility/accessibility-settings.config.ts b/src/app/accessibility/accessibility-settings.config.ts new file mode 100644 index 0000000000..1852579c3d --- /dev/null +++ b/src/app/accessibility/accessibility-settings.config.ts @@ -0,0 +1,11 @@ +import { Config } from '../../config/config.interface'; + +/** + * Configuration interface used by the AccessibilitySettingsService + */ +export class AccessibilitySettingsConfig implements Config { + /** + * The duration in days after which the accessibility settings cookie expires + */ + cookieExpirationDuration: number; +} diff --git a/src/app/accessibility/accessibility-settings.service.ts b/src/app/accessibility/accessibility-settings.service.ts index 805d0d5a0b..4089fd03b1 100644 --- a/src/app/accessibility/accessibility-settings.service.ts +++ b/src/app/accessibility/accessibility-settings.service.ts @@ -8,6 +8,7 @@ import { EPerson } from '../core/eperson/models/eperson.model'; import { EPersonDataService } from '../core/eperson/eperson-data.service'; import { getFirstCompletedRemoteData } from '../core/shared/operators'; import cloneDeep from 'lodash/cloneDeep'; +import { environment } from '../../environments/environment'; /** * Name of the cookie used to store the settings locally @@ -19,11 +20,6 @@ export const ACCESSIBILITY_COOKIE = 'dsAccessibilityCookie'; */ export const ACCESSIBILITY_SETTINGS_METADATA_KEY = 'dspace.accessibility.settings'; -/** - * The duration in days after which the accessibility settings cookie expires - */ -export const ACCESSIBILITY_SETTINGS_COOKIE_STORAGE_DURATION = 7; - /** * Enum containing all possible accessibility settings. * When adding new settings, the {@link AccessibilitySettingsService#getInputType} method and the i18n keys for the @@ -208,7 +204,7 @@ export class AccessibilitySettingsService { */ setSettingsInCookie(settings: AccessibilitySettings) { if (isNotEmpty(settings)) { - this.cookieService.set(ACCESSIBILITY_COOKIE, settings, { expires: ACCESSIBILITY_SETTINGS_COOKIE_STORAGE_DURATION }); + this.cookieService.set(ACCESSIBILITY_COOKIE, settings, { expires: environment.accessibility.cookieExpirationDuration }); } else { this.cookieService.remove(ACCESSIBILITY_COOKIE); } diff --git a/src/config/app-config.interface.ts b/src/config/app-config.interface.ts index aa3033ecec..6cbcf782af 100644 --- a/src/config/app-config.interface.ts +++ b/src/config/app-config.interface.ts @@ -23,6 +23,7 @@ import { MarkdownConfig } from './markdown-config.interface'; import { FilterVocabularyConfig } from './filter-vocabulary-config'; import { DiscoverySortConfig } from './discovery-sort.config'; import { LiveRegionConfig } from '../app/shared/live-region/live-region.config'; +import { AccessibilitySettingsConfig } from '../app/accessibility/accessibility-settings.config'; interface AppConfig extends Config { ui: UIServerConfig; @@ -50,6 +51,7 @@ interface AppConfig extends Config { vocabularies: FilterVocabularyConfig[]; comcolSelectionSort: DiscoverySortConfig; liveRegion: LiveRegionConfig; + accessibility: AccessibilitySettingsConfig; } /** diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index 1c0f88cf47..c7aac9a2d7 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -23,6 +23,7 @@ import { MarkdownConfig } from './markdown-config.interface'; import { FilterVocabularyConfig } from './filter-vocabulary-config'; import { DiscoverySortConfig } from './discovery-sort.config'; import { LiveRegionConfig } from '../app/shared/live-region/live-region.config'; +import { AccessibilitySettingsConfig } from '../app/accessibility/accessibility-settings.config'; export class DefaultAppConfig implements AppConfig { production = false; @@ -439,4 +440,9 @@ export class DefaultAppConfig implements AppConfig { messageTimeOutDurationMs: 30000, isVisible: false, }; + + // Accessibility settings configuration, used by the AccessibilitySettingsService + accessibility: AccessibilitySettingsConfig = { + cookieExpirationDuration: 7, + }; } diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts index 498799a454..77094ada80 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -319,4 +319,8 @@ export const environment: BuildConfig = { messageTimeOutDurationMs: 30000, isVisible: false, }, + + accessibility: { + cookieExpirationDuration: 7, + }, }; From 04515591e2b1b5bd3d892556d195423913ee8779 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Tue, 5 Nov 2024 16:01:05 +0100 Subject: [PATCH 191/720] 119602: Add link to AccessibilitySettings on profile page --- src/app/profile-page/profile-page.component.html | 10 +++++++++- src/assets/i18n/en.json5 | 6 ++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/app/profile-page/profile-page.component.html b/src/app/profile-page/profile-page.component.html index 44783da84e..d8394ac5d4 100644 --- a/src/app/profile-page/profile-page.component.html +++ b/src/app/profile-page/profile-page.component.html @@ -28,10 +28,18 @@

    {{'profile.head' | translate}}

    >
    -
    +
    +
    +
    {{'profile.card.accessibility.header' | translate}}
    +
    +
    {{'profile.card.accessibility.content' | translate}}
    + {{'profile.card.accessibility.link' | translate}} +
    +
    +

    {{'profile.groups.head' | translate}}

    diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 1316c8d3cf..fc749377a9 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -3274,6 +3274,12 @@ "profile.breadcrumbs": "Update Profile", + "profile.card.accessibility.content": "Accessibility settings can be configured on the accessibility settings page.", + + "profile.card.accessibility.header": "Accessibility", + + "profile.card.accessibility.link": "Accessibility Settings Page", + "profile.card.identify": "Identify", "profile.card.security": "Security", From bb7f0cd3a5adb0729a6b979ad0a4562f1ca52761 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Thu, 7 Nov 2024 10:18:11 +0100 Subject: [PATCH 192/720] 119602: Improve profile page link accessibility --- src/app/profile-page/profile-page.component.html | 2 +- src/assets/i18n/en.json5 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/profile-page/profile-page.component.html b/src/app/profile-page/profile-page.component.html index d8394ac5d4..d2809a04b6 100644 --- a/src/app/profile-page/profile-page.component.html +++ b/src/app/profile-page/profile-page.component.html @@ -35,7 +35,7 @@

    {{'profile.head' | translate}}

    {{'profile.card.accessibility.header' | translate}}
    -
    {{'profile.card.accessibility.content' | translate}}
    + {{'profile.card.accessibility.content' | translate}} {{'profile.card.accessibility.link' | translate}}
    diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index fc749377a9..dc69f0fbe8 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -3278,7 +3278,7 @@ "profile.card.accessibility.header": "Accessibility", - "profile.card.accessibility.link": "Accessibility Settings Page", + "profile.card.accessibility.link": "Go to Accessibility Settings Page", "profile.card.identify": "Identify", From 87f5b502010581ad4d8f3152aa126dbc6c6bbe62 Mon Sep 17 00:00:00 2001 From: Sascha Szott Date: Thu, 7 Nov 2024 19:58:01 +0100 Subject: [PATCH 193/720] update comment to correctly describe component's purpose (cherry picked from commit 33dddc697fb86d449998900ab82e4f876631eaac) --- .../bitstream-authorizations.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/bitstream-page/bitstream-authorizations/bitstream-authorizations.component.ts b/src/app/bitstream-page/bitstream-authorizations/bitstream-authorizations.component.ts index adc0638780..90b4151a9d 100644 --- a/src/app/bitstream-page/bitstream-authorizations/bitstream-authorizations.component.ts +++ b/src/app/bitstream-page/bitstream-authorizations/bitstream-authorizations.component.ts @@ -12,7 +12,7 @@ import { DSpaceObject } from '../../core/shared/dspace-object.model'; templateUrl: './bitstream-authorizations.component.html', }) /** - * Component that handles the Collection Authorizations + * Component that handles the Bitstream Authorizations */ export class BitstreamAuthorizationsComponent implements OnInit { From 9f879d38501f68e068350b567dafbe9b0a858d92 Mon Sep 17 00:00:00 2001 From: Sascha Szott Date: Thu, 7 Nov 2024 20:02:39 +0100 Subject: [PATCH 194/720] fix invalid selector (cherry picked from commit 752951ce3b05436a13d689fc492fa0b95563862e) --- .../bitstream-authorizations.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/bitstream-page/bitstream-authorizations/bitstream-authorizations.component.ts b/src/app/bitstream-page/bitstream-authorizations/bitstream-authorizations.component.ts index 90b4151a9d..72683f5d74 100644 --- a/src/app/bitstream-page/bitstream-authorizations/bitstream-authorizations.component.ts +++ b/src/app/bitstream-page/bitstream-authorizations/bitstream-authorizations.component.ts @@ -8,7 +8,7 @@ import { RemoteData } from '../../core/data/remote-data'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; @Component({ - selector: 'ds-collection-authorizations', + selector: 'ds-bitstream-authorizations', templateUrl: './bitstream-authorizations.component.html', }) /** From 4353c57cd045f912f27bf8c3dce2a73497ff7d42 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Tue, 29 Oct 2024 13:58:50 -0500 Subject: [PATCH 195/720] Fix Klaro translations by forcing Klaro to use a 'zy' language key which DSpace will translate (cherry picked from commit 6076423907e22707a4c31c7c96d1b74ca6b0d81c) --- .../cookies/browser-klaro.service.spec.ts | 6 +++--- .../shared/cookies/browser-klaro.service.ts | 8 +++---- src/app/shared/cookies/klaro-configuration.ts | 21 +++++++++++++------ 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/app/shared/cookies/browser-klaro.service.spec.ts b/src/app/shared/cookies/browser-klaro.service.spec.ts index 7fd72b54b3..3da3a8b7a3 100644 --- a/src/app/shared/cookies/browser-klaro.service.spec.ts +++ b/src/app/shared/cookies/browser-klaro.service.spec.ts @@ -101,7 +101,7 @@ describe('BrowserKlaroService', () => { mockConfig = { translations: { - zz: { + zy: { purposes: {}, test: { testeritis: testKey @@ -159,8 +159,8 @@ describe('BrowserKlaroService', () => { it('addAppMessages', () => { service.addAppMessages(); - expect(mockConfig.translations.zz[appName]).toBeDefined(); - expect(mockConfig.translations.zz.purposes[purpose]).toBeDefined(); + expect(mockConfig.translations.zy[appName]).toBeDefined(); + expect(mockConfig.translations.zy.purposes[purpose]).toBeDefined(); }); it('translateConfiguration', () => { diff --git a/src/app/shared/cookies/browser-klaro.service.ts b/src/app/shared/cookies/browser-klaro.service.ts index 2b09c0bf15..adcb59e146 100644 --- a/src/app/shared/cookies/browser-klaro.service.ts +++ b/src/app/shared/cookies/browser-klaro.service.ts @@ -91,7 +91,7 @@ export class BrowserKlaroService extends KlaroService { initialize() { if (!environment.info.enablePrivacyStatement) { delete this.klaroConfig.privacyPolicy; - this.klaroConfig.translations.zz.consentNotice.description = 'cookies.consent.content-notice.description.no-privacy'; + this.klaroConfig.translations.zy.consentNotice.description = 'cookies.consent.content-notice.description.no-privacy'; } const hideGoogleAnalytics$ = this.configService.findByPropertyName(this.GOOGLE_ANALYTICS_KEY).pipe( @@ -238,12 +238,12 @@ export class BrowserKlaroService extends KlaroService { */ addAppMessages() { this.klaroConfig.services.forEach((app) => { - this.klaroConfig.translations.zz[app.name] = { + this.klaroConfig.translations.zy[app.name] = { title: this.getTitleTranslation(app.name), description: this.getDescriptionTranslation(app.name) }; app.purposes.forEach((purpose) => { - this.klaroConfig.translations.zz.purposes[purpose] = this.getPurposeTranslation(purpose); + this.klaroConfig.translations.zy.purposes[purpose] = this.getPurposeTranslation(purpose); }); }); } @@ -257,7 +257,7 @@ export class BrowserKlaroService extends KlaroService { */ this.translateService.setDefaultLang(environment.defaultLanguage); - this.translate(this.klaroConfig.translations.zz); + this.translate(this.klaroConfig.translations.zy); } /** diff --git a/src/app/shared/cookies/klaro-configuration.ts b/src/app/shared/cookies/klaro-configuration.ts index f527f7f096..6ec4855e28 100644 --- a/src/app/shared/cookies/klaro-configuration.ts +++ b/src/app/shared/cookies/klaro-configuration.ts @@ -17,7 +17,7 @@ export const GOOGLE_ANALYTICS_KLARO_KEY = 'google-analytics'; /** * Klaro configuration - * For more information see https://kiprotect.com/docs/klaro/annotated-config + * For more information see https://klaro.org/docs/integration/annotated-configuration */ export const klaroConfiguration: any = { storageName: ANONYMOUS_STORAGE_NAME_KLARO, @@ -47,21 +47,30 @@ export const klaroConfiguration: any = { htmlTexts: true, + /* + Force Klaro to use our custom "zy" lang configs defined below. + */ + lang: 'zy', + /* You can overwrite existing translations and add translations for your app descriptions and purposes. See `src/translations/` for a full list of translations that can be overwritten: - https://github.com/KIProtect/klaro/tree/master/src/translations + https://github.com/klaro-org/klaro-js/tree/master/src/translations */ translations: { /* - The `zz` key contains default translations that will be used as fallback values. - This can e.g. be useful for defining a fallback privacy policy URL. - FOR DSPACE: We use 'zz' to map to our own i18n translations for klaro, see + For DSpace we use this custom 'zy' key to map to our own i18n translations for klaro, see translateConfiguration() in browser-klaro.service.ts. All the below i18n keys are specified in your /src/assets/i18n/*.json5 translation pack. + This 'zy' key has no special meaning to Klaro & is not a valid language code. It just + allows DSpace to override Klaro's own translations in favor of DSpace's i18n keys. + NOTE: we do not use 'zz' as that has special meaning to Klaro and is ONLY used as a "fallback" + if no other translations can be found within Klaro. Currently, a bug in Klaro means that + 'zz' is never used as there's no way to exclude translations: + https://github.com/klaro-org/klaro-js/issues/515 */ - zz: { + zy: { acceptAll: 'cookies.consent.accept-all', acceptSelected: 'cookies.consent.accept-selected', close: 'cookies.consent.close', From 64628f7e0d92d8bbee3078719eabeea4aa4275a9 Mon Sep 17 00:00:00 2001 From: andreaNeki Date: Fri, 13 Sep 2024 16:34:16 -0300 Subject: [PATCH 196/720] DSpace#2668 - Adding and changing classes in global scss to make cookie settings more accessible --- src/styles/_custom_variables.scss | 1 + src/styles/_global-styles.scss | 34 +++++++++++++++++++++++++------ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/styles/_custom_variables.scss b/src/styles/_custom_variables.scss index 7171aea689..aa67acac1c 100644 --- a/src/styles/_custom_variables.scss +++ b/src/styles/_custom_variables.scss @@ -137,4 +137,5 @@ --green1: #1FB300; // This variable represents the success color for the Klaro cookie banner --button-text-color-cookie: #333; // This variable represents the text color for buttons in the Klaro cookie banner + --very-dark-cyan: #215E50; // This variable represents the background color of the save cookies button } diff --git a/src/styles/_global-styles.scss b/src/styles/_global-styles.scss index b3120c08cd..99cc075dbe 100644 --- a/src/styles/_global-styles.scss +++ b/src/styles/_global-styles.scss @@ -43,17 +43,39 @@ body { .cm-btn.cm-btn-success { color: var(--button-text-color-cookie); background-color: var(--green1); - } - .cm-btn.cm-btn-success.cm-btn-accept-all { - color: var(--button-text-color-cookie); - background-color: var(--green1); + font-weight: 600; } } } -.klaro .cookie-modal a, .klaro .context-notice a, .klaro .cookie-notice a -{ +.klaro .cookie-modal .cm-btn.cm-btn-success.cm-btn-accept-all { + color: var(--button-text-color-cookie); + background-color: var(--green1); + font-weight: 600; +} + +.klaro .cookie-modal a, +.klaro .context-notice a, +.klaro .cookie-notice a { color: var(--green1); + text-decoration: underline !important; +} + +.klaro .cookie-modal .cm-modal .cm-body span, +.klaro + .cookie-modal + .cm-modal + .cm-body + ul.cm-purposes + li.cm-purpose + span.cm-required, +p.purposes, +.klaro .cookie-modal .cm-modal .cm-footer .cm-powered-by a { + color: var(--bs-light) !important; +} + +.klaro .cookie-modal .cm-btn.cm-btn-info { + background-color: var(--very-dark-cyan) !important; } .media-viewer From bc50efec5e4387bfbae6f1405717d9d2c5bed0eb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Nov 2024 20:35:42 +0000 Subject: [PATCH 197/720] Bump core-js from 3.38.1 to 3.39.0 Bumps [core-js](https://github.com/zloirock/core-js/tree/HEAD/packages/core-js) from 3.38.1 to 3.39.0. - [Release notes](https://github.com/zloirock/core-js/releases) - [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md) - [Commits](https://github.com/zloirock/core-js/commits/v3.39.0/packages/core-js) --- updated-dependencies: - dependency-name: core-js dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index b3dbb92115..c4fad1e06a 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "colors": "^1.4.0", "compression": "^1.7.5", "cookie-parser": "1.4.7", - "core-js": "^3.38.1", + "core-js": "^3.39.0", "date-fns": "^2.30.0", "date-fns-tz": "^1.3.7", "deepmerge": "^4.3.1", diff --git a/yarn.lock b/yarn.lock index e6c67e0e80..eedca951f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4272,10 +4272,10 @@ core-js-compat@^3.25.1: dependencies: browserslist "^4.21.5" -core-js@^3.38.1: - version "3.38.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.38.1.tgz#aa375b79a286a670388a1a363363d53677c0383e" - integrity sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw== +core-js@^3.39.0: + version "3.39.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.39.0.tgz#57f7647f4d2d030c32a72ea23a0555b2eaa30f83" + integrity sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g== core-util-is@1.0.2: version "1.0.2" From 06783364c41f66dc237f606ad294ef67fc87c89f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Nov 2024 03:15:03 +0000 Subject: [PATCH 198/720] Bump express-static-gzip from 2.1.8 to 2.2.0 Bumps [express-static-gzip](https://github.com/tkoenig89/express-static-gzip) from 2.1.8 to 2.2.0. - [Release notes](https://github.com/tkoenig89/express-static-gzip/releases) - [Commits](https://github.com/tkoenig89/express-static-gzip/compare/v2.1.8...v2.2.0) --- updated-dependencies: - dependency-name: express-static-gzip dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index f8f725fafc..7381cdfa97 100644 --- a/package.json +++ b/package.json @@ -165,7 +165,7 @@ "eslint-plugin-jsonc": "^2.16.0", "eslint-plugin-lodash": "^7.4.0", "eslint-plugin-unused-imports": "^2.0.0", - "express-static-gzip": "^2.1.8", + "express-static-gzip": "^2.2.0", "jasmine-core": "^3.8.0", "jasmine-marbles": "0.9.2", "karma": "^6.4.4", diff --git a/yarn.lock b/yarn.lock index 0106427952..aa4fb3ed3c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5609,11 +5609,12 @@ express-rate-limit@^5.1.3: resolved "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.5.1.tgz" integrity sha512-MTjE2eIbHv5DyfuFz4zLYWxpqVhEhkTiwFGuB74Q9CSou2WHO52nlE5y3Zlg6SIsiYUIPj6ifFxnkPz6O3sIUg== -express-static-gzip@^2.1.8: - version "2.1.8" - resolved "https://registry.yarnpkg.com/express-static-gzip/-/express-static-gzip-2.1.8.tgz#f37f0fe9e8113e56cfac63a98c0197ee6bd6458f" - integrity sha512-g8tiJuI9Y9Ffy59ehVXvqb0hhP83JwZiLxzanobPaMbkB5qBWA8nuVgd+rcd5qzH3GkgogTALlc0BaADYwnMbQ== +express-static-gzip@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/express-static-gzip/-/express-static-gzip-2.2.0.tgz#7c3f7dd89da68e51c591edf02e6de6169c017f5f" + integrity sha512-4ZQ0pHX0CAauxmzry2/8XFLM6aZA4NBvg9QezSlsEO1zLnl7vMFa48/WIcjzdfOiEUS4S1npPPKP2NHHYAp6qg== dependencies: + parseurl "^1.3.3" serve-static "^1.16.2" express@^4.17.3, express@^4.18.2, express@^4.21.1: @@ -8988,7 +8989,7 @@ parse5@^7.0.0, parse5@^7.1.1, parse5@^7.1.2: dependencies: entities "^4.4.0" -parseurl@~1.3.2, parseurl@~1.3.3: +parseurl@^1.3.3, parseurl@~1.3.2, parseurl@~1.3.3: version "1.3.3" resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== From 6b32d04aecc57586318b919fa5fadaeb032772a8 Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Fri, 26 Jul 2024 15:32:57 +0200 Subject: [PATCH 199/720] Updated some messages following the lindat v5 and clarin-dspace v7 instance. (cherry picked from commit b10563ea5388e8c398ec8927dc562d6f841473db) --- src/assets/i18n/cs.json5 | 101 +++++++++++++++++++-------------------- 1 file changed, 48 insertions(+), 53 deletions(-) diff --git a/src/assets/i18n/cs.json5 b/src/assets/i18n/cs.json5 index 0b4168cca9..3824e9741a 100644 --- a/src/assets/i18n/cs.json5 +++ b/src/assets/i18n/cs.json5 @@ -1477,8 +1477,8 @@ // "bitstream.download.page": "Now downloading {{bitstream}}...", "bitstream.download.page": "Nyní se stahuje {{bitstream}}..." , - // "bitstream.download.page.back": "Back", - "bitstream.download.page.back": "Zpět" , + // "bitstream.download.page.back": "Back" , + "bitstream.download.page.back": "Zpět", // "bitstream.edit.authorizations.link": "Edit bitstream's Policies", "bitstream.edit.authorizations.link": "Upravit politiky souboru", @@ -1797,7 +1797,7 @@ // "claimed-declined-task-search-result-list-element.title": "Declined, sent back to Review Manager's workflow", "claimed-declined-task-search-result-list-element.title": "Zamítnuto, posláno zpět správci schvalovacího workflow", - // "collection.create.breadcrumbs": "Create collection", + // "collection.create.breadcrumbs": "Create collection", // TODO New key - Add a translation "collection.create.breadcrumbs": "Create collection", @@ -2210,8 +2210,7 @@ "collection.source.controls.harvest.last": "Naposledy harvestováno:", // "collection.source.controls.harvest.message": "Harvest info:", - "collection.source.controls.harvest.message": "Informace o harevstu:", - + "collection.source.controls.harvest.message": "Informace o harvestu:", // "collection.source.controls.harvest.no-information": "N/A", "collection.source.controls.harvest.no-information": "Není k dispozi", @@ -2703,7 +2702,7 @@ "dso-selector.create.community.head": "Nová komunita", // "dso-selector.create.community.or-divider": "or", - "dso-selector.create.community.or-divider": "or", + "dso-selector.create.community.or-divider": "nebo", // "dso-selector.create.community.sub-level": "Create a new community in", "dso-selector.create.community.sub-level": "Vytvořit novou komunitu v", @@ -2940,7 +2939,7 @@ "error.top-level-communities": "Chyba při načítání komunit nejvyšší úrovně", // "error.validation.license.notgranted": "You must grant this license to complete your submission. If you are unable to grant this license at this time you may save your work and return later or remove the submission.", - "error.validation.license.notgranted": "eslání. Pokud tuto licenci v tuto chvíli nemůžete udělit, můžete svou práci uložit a vrátit se k ní později nebo podání odstranit.", + "error.validation.license.notgranted": "Pro dokončení zaslání musíte udělit licenci. Pokud v tuto chvíli tuto licenci nemůžete udělit, můžete svou práci uložit a později se k svému příspěveku vrátit nebo jej smazat.", // "error.validation.pattern": "This input is restricted by the current pattern: {{ pattern }}.", "error.validation.pattern": "Toto zadání je omezeno aktuálním vzorcem: {{ pattern }}.", @@ -3252,7 +3251,7 @@ "grant-deny-request-copy.intro2": "Po výběru možnosti se zobrazí návrh e-mailové odpovědi, který můžete upravit.", // "grant-deny-request-copy.processed": "This request has already been processed. You can use the button below to get back to the home page.", - "grant-deny-request-copy.processed": " Tato žádost již byla zpracována. Pomocí níže uvedeného tlačítka se můžete vrátit na domovskou stránku.", + "grant-deny-request-copy.processed": "Tato žádost již byla zpracována. Pomocí níže uvedeného tlačítka se můžete vrátit na domovskou stránku.", // "grant-request-copy.email.subject": "Request copy of document", "grant-request-copy.email.subject": "Žádost o kopii dokumentu", @@ -3432,8 +3431,7 @@ "info.coar-notify-support.breadcrumbs": "COAR Notify Support", // "item.alerts.private": "This item is non-discoverable", - // TODO Source message changed - Revise the translation - "item.alerts.private": "Tento záznam je nezobrazitelný", + "item.alerts.private": "Tento záznam je nevyhledatelný", // "item.alerts.withdrawn": "This item has been withdrawn", "item.alerts.withdrawn": "Tento záznam byl stažen", @@ -4168,7 +4166,7 @@ "item.page.journal-title": "Název časopisu", // "item.page.publisher": "Publisher", - "item.page.publisher": "Vydavatel", + "item.page.publisher": "Nakladatel", // "item.page.titleprefix": "Item: ", "item.page.titleprefix": "Záznam: ", @@ -4268,7 +4266,7 @@ "item.page.abstract": "Abstrakt", // "item.page.author": "Authors", - "item.page.author": "Autor", + "item.page.author": "Autoři", // "item.page.citation": "Citation", "item.page.citation": "Citace", @@ -4310,10 +4308,10 @@ "item.page.journal.search.title": "Články v tomto časopise", // "item.page.link.full": "Full item page", - "item.page.link.full": "Úplný záznam", + "item.page.link.full": "Zobrazit celý záznam", // "item.page.link.simple": "Simple item page", - "item.page.link.simple": "Jednoduchý záznam", + "item.page.link.simple": "Zobrazit minimální záznam", // "item.page.orcid.title": "ORCID", "item.page.orcid.title": "ORCID", @@ -4346,7 +4344,7 @@ "item.page.subject": "Klíčová slova", // "item.page.uri": "URI", - "item.page.uri": "URI", + "item.page.uri": "Identifikátor", // "item.page.bitstreams.view-more": "Show more", "item.page.bitstreams.view-more": "Zobrazit více", @@ -4461,10 +4459,10 @@ "item.preview.oaire.awardNumber": "Identifikátor zdroje financování:", // "item.preview.dc.title.alternative": "Acronym:", - "item.preview.dc.title.alternative": "Akronym:", + "item.preview.dc.title.alternative": "Zkratka:", // "item.preview.dc.coverage.spatial": "Jurisdiction:", - "item.preview.dc.coverage.spatial": "Jurisdikce:", + "item.preview.dc.coverage.spatial": "Příslušnost:", // "item.preview.oaire.fundingStream": "Funding Stream:", "item.preview.oaire.fundingStream": "Tok finančních prostředků:", @@ -4841,7 +4839,7 @@ "journal.page.issn": "ISSN", // "journal.page.publisher": "Publisher", - "journal.page.publisher": "Vydavatel", + "journal.page.publisher": "Nakladatel", // "journal.page.titleprefix": "Journal: ", "journal.page.titleprefix": "Časopis: ", @@ -4936,7 +4934,7 @@ "iiif.page.doi": "Trvalý odkaz: ", // "iiif.page.issue": "Issue: ", - "iiif.page.issue": "Číslo: ", + "iiif.page.issue": "Problém:", // "iiif.page.description": "Description: ", "iiif.page.description": "Popis: ", @@ -5042,8 +5040,7 @@ "menu.header.nav.description": "Admin navigation bar", // "menu.header.admin": "Management", - // TODO Source message changed - Revise the translation - "menu.header.admin": "Management", + "menu.header.admin": "Admin", // "menu.header.image.logo": "Repository logo", "menu.header.image.logo": "Logo úložiště", @@ -5378,7 +5375,7 @@ "mydspace.new-submission-external-short": "Importovat metadata", // "mydspace.results.head": "Your submissions", - "mydspace.results.head": "Váš příspěvek", + "mydspace.results.head": "Vaše zázmany", // "mydspace.results.no-abstract": "No Abstract", "mydspace.results.no-abstract": "Žádný abstrakt", @@ -5390,7 +5387,7 @@ "mydspace.results.no-collections": "Žádné kolekce", // "mydspace.results.no-date": "No Date", - "mydspace.results.no-date": "Žádné datum", + "mydspace.results.no-date": "Žádny datum", // "mydspace.results.no-files": "No Files", "mydspace.results.no-files": "Žádné soubory", @@ -5412,9 +5409,9 @@ // TODO Source message changed - Revise the translation "mydspace.show.workflow": "Úlohy workflow", - // "mydspace.show.workspace": "Your submissions", + // "mydspace.show.workspace": "Your Submissions", // TODO Source message changed - Revise the translation - "mydspace.show.workspace": "Váš příspěvek", + "mydspace.show.workspace": "Vaše záznamy", // "mydspace.show.supervisedWorkspace": "Supervised items", "mydspace.show.supervisedWorkspace": "Zkontrolované záznamy", @@ -5488,7 +5485,7 @@ "nav.user-profile-menu-and-logout": "Menu uživatelského profilu a odhlášení", // "nav.logout": "Log Out", - "nav.logout": "Menu uživatelského profilu a odhlášení", + "nav.logout": "Odhlásit se", // "nav.main.description": "Main navigation bar", "nav.main.description": "Hlavní navigační panel", @@ -6289,7 +6286,7 @@ "publication.page.journal-title": "Název časopisu", // "publication.page.publisher": "Publisher", - "publication.page.publisher": "Vydavatel", + "publication.page.publisher": "Nakladatel", // "publication.page.titleprefix": "Publication: ", "publication.page.titleprefix": "Publikace: ", @@ -6913,7 +6910,7 @@ "search.filters.applied.f.subject": "Předmět", // "search.filters.applied.f.submitter": "Submitter", - "search.filters.applied.f.submitter": "Vkladatel", + "search.filters.applied.f.submitter": "Předkladatel", // "search.filters.applied.f.jobTitle": "Job Title", "search.filters.applied.f.jobTitle": "Název pracovní pozice", @@ -7019,10 +7016,10 @@ "search.filters.filter.creativeWorkKeywords.label": "Předmět hledání", // "search.filters.filter.creativeWorkPublisher.head": "Publisher", - "search.filters.filter.creativeWorkPublisher.head": "Vydavatel", + "search.filters.filter.creativeWorkPublisher.head": "Nakladatel", // "search.filters.filter.creativeWorkPublisher.placeholder": "Publisher", - "search.filters.filter.creativeWorkPublisher.placeholder": "Vydavatel", + "search.filters.filter.creativeWorkPublisher.placeholder": "Nakladatel", // "search.filters.filter.creativeWorkPublisher.label": "Search publisher", "search.filters.filter.creativeWorkPublisher.label": "Hledat vydavatele", @@ -7058,7 +7055,7 @@ "search.filters.filter.discoverable.head": "Nedohledatelné", // "search.filters.filter.withdrawn.head": "Withdrawn", - "search.filters.filter.withdrawn.head": "Zrušeno", + "search.filters.filter.withdrawn.head": "Vyřazeno", // "search.filters.filter.entityType.head": "Item Type", "search.filters.filter.entityType.head": "Typ záznamu", @@ -7142,7 +7139,7 @@ "search.filters.filter.objectpeople.placeholder": "Lidé", // "search.filters.filter.objectpeople.label": "Search people", - "search.filters.filter.objectpeople.label": "Hledat lidi", + "search.filters.filter.objectpeople.label": "Hledat osoby", // "search.filters.filter.organizationAddressCountry.head": "Country", "search.filters.filter.organizationAddressCountry.head": "Stát", @@ -7178,7 +7175,7 @@ "search.filters.filter.scope.placeholder": "Filtr rozsahu", // "search.filters.filter.scope.label": "Search scope filter", - "search.filters.filter.scope.label": "Hledat filtr rozsahu", + "search.filters.filter.scope.label": "Filtr rozsahu hledání", // "search.filters.filter.show-less": "Collapse", "search.filters.filter.show-less": "Sbalit", @@ -7196,13 +7193,13 @@ "search.filters.filter.subject.label": "Hledat předmět", // "search.filters.filter.submitter.head": "Submitter", - "search.filters.filter.submitter.head": "Vkladatel", + "search.filters.filter.submitter.head": "Předkladatel", // "search.filters.filter.submitter.placeholder": "Submitter", - "search.filters.filter.submitter.placeholder": "Vkladatel", + "search.filters.filter.submitter.placeholder": "Předkladatel", // "search.filters.filter.submitter.label": "Search submitter", - "search.filters.filter.submitter.label": "Hledat vkladatele", + "search.filters.filter.submitter.label": "Hledat předkladatele", // "search.filters.filter.show-tree": "Browse {{ name }} tree", "search.filters.filter.show-tree": "Procházet {{ name }} podle", @@ -7286,8 +7283,8 @@ // "search.filters.withdrawn.false": "No", "search.filters.withdrawn.false": "Ne", - // "search.filters.head": "Filters", - "search.filters.head": "Filtry", + // "search.filters.head": "Limit your search", + "search.filters.head": "Zúžit hledání", // "search.filters.reset": "Reset filters", "search.filters.reset": "Resetovat filtry", @@ -7526,7 +7523,7 @@ "submission.general.cannot_submit": "Nemáte oprávnění k vytvoření nového příspěvku.", // "submission.general.deposit": "Deposit", - "submission.general.deposit": "Vložit do repozitáře", + "submission.general.deposit": "Nahrát", // "submission.general.discard.confirm.cancel": "Cancel", "submission.general.discard.confirm.cancel": "Zrušit", @@ -7554,7 +7551,7 @@ "submission.general.info.pending-changes": "Neuložené změny", // "submission.general.save": "Save", - "submission.general.save": "Průběžně uložit záznam", + "submission.general.save": "Uložit", // "submission.general.save-later": "Save for later", "submission.general.save-later": "Uložit na později", @@ -7709,10 +7706,10 @@ "submission.import-external.preview.subtitle": "Níže uvedená metadata byla importována z externího zdroje. Budou předvyplněna při zahájení odesílání..", // "submission.import-external.preview.button.import": "Start submission", - "submission.import-external.preview.button.import": "Zahájit odesílání", + "submission.import-external.preview.button.import": "Začít nový příspěvek", // "submission.import-external.preview.error.import.title": "Submission error", - "submission.import-external.preview.error.import.title": "Chyba při odesílání", + "submission.import-external.preview.error.import.title": "Chyba při vytváření nového příspěvku", // "submission.import-external.preview.error.import.body": "An error occurs during the external source entry import process.", "submission.import-external.preview.error.import.body": "Během procesu importu externí zdrojového záznamu došlo k chybě.", @@ -7893,7 +7890,7 @@ "submission.sections.describe.relationship-lookup.search-tab.placeholder": "Vyhledávací dotaz", // "submission.sections.describe.relationship-lookup.search-tab.search": "Go", - "submission.sections.describe.relationship-lookup.search-tab.search": "Hledání", + "submission.sections.describe.relationship-lookup.search-tab.search": "Přejít na", // "submission.sections.describe.relationship-lookup.search-tab.search-form.placeholder": "Search...", "submission.sections.describe.relationship-lookup.search-tab.search-form.placeholder": "Hledání...", @@ -8080,7 +8077,7 @@ "submission.sections.describe.relationship-lookup.title.Funding Agency": "Financující agentura", // "submission.sections.describe.relationship-lookup.title.isFundingOfPublication": "Funding", - "submission.sections.describe.relationship-lookup.title.isFundingOfPublication": "Projekt, ke kterému publikace náleží", + "submission.sections.describe.relationship-lookup.title.isFundingOfPublication": "Financování", // "submission.sections.describe.relationship-lookup.title.isChildOrgUnitOf": "Parent Organizational Unit", "submission.sections.describe.relationship-lookup.title.isChildOrgUnitOf": "Nadřazená organizační jednotka", @@ -8311,7 +8308,7 @@ "submission.sections.submit.progressbar.describe.recycle": "Opětovně použít", // "submission.sections.submit.progressbar.describe.stepcustom": "Describe", - "submission.sections.submit.progressbar.describe.stepcustom": "Popsat", + "submission.sections.submit.progressbar.describe.stepcustom": "Popis", // "submission.sections.submit.progressbar.describe.stepone": "Describe", "submission.sections.submit.progressbar.describe.stepone": "Základní informace o dokumentu", @@ -8627,8 +8624,8 @@ "submission.submit.breadcrumbs": "Nově podaný záznam", // "submission.submit.title": "New submission", - // TODO Source message changed - Revise the translation - "submission.submit.title": "Nově podaný záznam", + "submission.submit.title": "Nový příspěvek", + // "submission.workflow.generic.delete": "Delete", "submission.workflow.generic.delete": "Smazat", @@ -8710,7 +8707,7 @@ "submission.workflow.tasks.generic.processing": "Zpracování...", // "submission.workflow.tasks.generic.submitter": "Submitter", - "submission.workflow.tasks.generic.submitter": "Zadavatel", + "submission.workflow.tasks.generic.submitter": "Předkladatel", // "submission.workflow.tasks.generic.success": "Operation successful", "submission.workflow.tasks.generic.success": "Operace proběhla úspěšně", @@ -9074,10 +9071,10 @@ "workflow-item.scorereviewaction.notification.error.content": "Nebylo možné zkontrolovat tento záznam", // "workflow-item.scorereviewaction.title": "Rate this item", - "workflow-item.scorereviewaction.title": "Zkontrolovat tento záznam", + "workflow-item.scorereviewaction.title": "Ohodnotit tento záznam", // "workflow-item.scorereviewaction.header": "Rate this item", - "workflow-item.scorereviewaction.header": "Zkontrolovat tento záznam", + "workflow-item.scorereviewaction.header": "Ohodnotit tento záznam", // "workflow-item.scorereviewaction.button.cancel": "Cancel", "workflow-item.scorereviewaction.button.cancel": "Zrušit", @@ -11034,6 +11031,4 @@ // "item.page.cc.license.disclaimer": "Except where otherwised noted, this item's license is described as", // TODO New key - Add a translation "item.page.cc.license.disclaimer": "Except where otherwised noted, this item's license is described as", - - -} \ No newline at end of file +} From e5ea435cbe58186e6dc42d56753147ade7ce3d5a Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Fri, 26 Jul 2024 15:49:37 +0200 Subject: [PATCH 200/720] Fixed linting error. (cherry picked from commit 7e864d27b45c58ef566d31c5d0e2a478a540ecdd) --- src/assets/i18n/cs.json5 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/assets/i18n/cs.json5 b/src/assets/i18n/cs.json5 index 3824e9741a..a957232a89 100644 --- a/src/assets/i18n/cs.json5 +++ b/src/assets/i18n/cs.json5 @@ -1797,7 +1797,7 @@ // "claimed-declined-task-search-result-list-element.title": "Declined, sent back to Review Manager's workflow", "claimed-declined-task-search-result-list-element.title": "Zamítnuto, posláno zpět správci schvalovacího workflow", - // "collection.create.breadcrumbs": "Create collection", + // "collection.create.breadcrumbs": "Create collection", // TODO New key - Add a translation "collection.create.breadcrumbs": "Create collection", From 865268e820fafcfe047a752d9effb9fcfd99c160 Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Wed, 9 Oct 2024 15:08:31 +0200 Subject: [PATCH 201/720] Updated cs messages following review requirements (cherry picked from commit 813d644b32a28685f8a4205bc383268b2502afdb) --- src/assets/i18n/cs.json5 | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/assets/i18n/cs.json5 b/src/assets/i18n/cs.json5 index a957232a89..5e52f751ec 100644 --- a/src/assets/i18n/cs.json5 +++ b/src/assets/i18n/cs.json5 @@ -1475,9 +1475,9 @@ "auth.messages.token-refresh-failed": "Nepodařilo se obnovit váš přístupový token. Prosím přihlašte se znovu.", // "bitstream.download.page": "Now downloading {{bitstream}}...", - "bitstream.download.page": "Nyní se stahuje {{bitstream}}..." , + "bitstream.download.page": "Nyní se stahuje {{bitstream}}...", - // "bitstream.download.page.back": "Back" , + // "bitstream.download.page.back": "Back", "bitstream.download.page.back": "Zpět", // "bitstream.edit.authorizations.link": "Edit bitstream's Policies", @@ -2939,7 +2939,7 @@ "error.top-level-communities": "Chyba při načítání komunit nejvyšší úrovně", // "error.validation.license.notgranted": "You must grant this license to complete your submission. If you are unable to grant this license at this time you may save your work and return later or remove the submission.", - "error.validation.license.notgranted": "Pro dokončení zaslání musíte udělit licenci. Pokud v tuto chvíli tuto licenci nemůžete udělit, můžete svou práci uložit a později se k svému příspěveku vrátit nebo jej smazat.", + "error.validation.license.notgranted": "Bez udělení licence nelze záznam dokončit. Pokud v tuto chvíli nemůžete licenci udělit, uložte svou práci a vraťte se k příspěveku později nebo jej smažte.", // "error.validation.pattern": "This input is restricted by the current pattern: {{ pattern }}.", "error.validation.pattern": "Toto zadání je omezeno aktuálním vzorcem: {{ pattern }}.", @@ -4308,7 +4308,7 @@ "item.page.journal.search.title": "Články v tomto časopise", // "item.page.link.full": "Full item page", - "item.page.link.full": "Zobrazit celý záznam", + "item.page.link.full": "Zobrazit úplný záznam", // "item.page.link.simple": "Simple item page", "item.page.link.simple": "Zobrazit minimální záznam", @@ -4344,7 +4344,7 @@ "item.page.subject": "Klíčová slova", // "item.page.uri": "URI", - "item.page.uri": "Identifikátor", + "item.page.uri": "Permanentní identifikátor", // "item.page.bitstreams.view-more": "Show more", "item.page.bitstreams.view-more": "Zobrazit více", @@ -4934,7 +4934,7 @@ "iiif.page.doi": "Trvalý odkaz: ", // "iiif.page.issue": "Issue: ", - "iiif.page.issue": "Problém:", + "iiif.page.issue": "Číslo:", // "iiif.page.description": "Description: ", "iiif.page.description": "Popis: ", @@ -5375,7 +5375,7 @@ "mydspace.new-submission-external-short": "Importovat metadata", // "mydspace.results.head": "Your submissions", - "mydspace.results.head": "Vaše zázmany", + "mydspace.results.head": "Vaše záznamy", // "mydspace.results.no-abstract": "No Abstract", "mydspace.results.no-abstract": "Žádný abstrakt", @@ -5387,7 +5387,7 @@ "mydspace.results.no-collections": "Žádné kolekce", // "mydspace.results.no-date": "No Date", - "mydspace.results.no-date": "Žádny datum", + "mydspace.results.no-date": "Žádné datum", // "mydspace.results.no-files": "No Files", "mydspace.results.no-files": "Žádné soubory", @@ -6910,7 +6910,7 @@ "search.filters.applied.f.subject": "Předmět", // "search.filters.applied.f.submitter": "Submitter", - "search.filters.applied.f.submitter": "Předkladatel", + "search.filters.applied.f.submitter": "Odesílatel", // "search.filters.applied.f.jobTitle": "Job Title", "search.filters.applied.f.jobTitle": "Název pracovní pozice", @@ -7193,13 +7193,13 @@ "search.filters.filter.subject.label": "Hledat předmět", // "search.filters.filter.submitter.head": "Submitter", - "search.filters.filter.submitter.head": "Předkladatel", + "search.filters.filter.submitter.head": "Odesílatel", // "search.filters.filter.submitter.placeholder": "Submitter", - "search.filters.filter.submitter.placeholder": "Předkladatel", + "search.filters.filter.submitter.placeholder": "Odesílatel", // "search.filters.filter.submitter.label": "Search submitter", - "search.filters.filter.submitter.label": "Hledat předkladatele", + "search.filters.filter.submitter.label": "Hledat odesílatele", // "search.filters.filter.show-tree": "Browse {{ name }} tree", "search.filters.filter.show-tree": "Procházet {{ name }} podle", @@ -8626,7 +8626,6 @@ // "submission.submit.title": "New submission", "submission.submit.title": "Nový příspěvek", - // "submission.workflow.generic.delete": "Delete", "submission.workflow.generic.delete": "Smazat", @@ -8692,7 +8691,7 @@ "submission.workflow.tasks.claimed.reject.submit": "Odmítnout", // "submission.workflow.tasks.claimed.reject_help": "If you have reviewed the item and found it is not suitable for inclusion in the collection, select \"Reject\". You will then be asked to enter a message indicating why the item is unsuitable, and whether the submitter should change something and resubmit.", - "submission.workflow.tasks.claimed.reject_help": "Pokud jste záznam zkontrolovali a zjistili, že není vhodný pro zařazení do kolekce, vyberte \"Odmítnout\". Poté budete vyzváni k zadání zprávy, ve které uvedete, proč je záznam nevhodný, a zda by měl vkladatel něco změnit a znovu předložit.", + "submission.workflow.tasks.claimed.reject_help": "Pokud jste záznam zkontrolovali a zjistili, že není vhodný pro zařazení do kolekce, vyberte \"Odmítnout\". Poté budete vyzváni k zadání zprávy, ve které uvedete, proč je záznam nevhodný, a zda by měl vkladatel něco změnit a znovu odeslat.", // "submission.workflow.tasks.claimed.return": "Return to pool", "submission.workflow.tasks.claimed.return": "Vrátit do fondu", @@ -8707,7 +8706,7 @@ "submission.workflow.tasks.generic.processing": "Zpracování...", // "submission.workflow.tasks.generic.submitter": "Submitter", - "submission.workflow.tasks.generic.submitter": "Předkladatel", + "submission.workflow.tasks.generic.submitter": "Odesílatel", // "submission.workflow.tasks.generic.success": "Operation successful", "submission.workflow.tasks.generic.success": "Operace proběhla úspěšně", From 33bc8ba1a3fe5934c10f648c2a756e65a6c59e27 Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Wed, 9 Oct 2024 16:09:51 +0200 Subject: [PATCH 202/720] Fixed messages following the PR from the UFAL - https://github.com/dataquest-dev/dspace-angular/pull/669/commits/f18d45ce23ea9c42778885f59e5f7d25e548b2e9 (cherry picked from commit 5e5c627b8b4dbafc5454d91e24797f3ef6ef76aa) --- src/assets/i18n/cs.json5 | 220 +++++++++++++++++++-------------------- 1 file changed, 110 insertions(+), 110 deletions(-) diff --git a/src/assets/i18n/cs.json5 b/src/assets/i18n/cs.json5 index 5e52f751ec..2cb95f10d3 100644 --- a/src/assets/i18n/cs.json5 +++ b/src/assets/i18n/cs.json5 @@ -13,7 +13,7 @@ "403.help": "Nemáte povolení k přístupu na tuto stránku. Pro návrat na domovskou stránku můžete použít tlačítko níže.", // "403.link.home-page": "Take me to the home page", - "403.link.home-page": "Přesměrujte mě na domovskou stránku", + "403.link.home-page": "Návrat na domovskou stránku", // "403.forbidden": "Forbidden", "403.forbidden": "Přístup zakázán", @@ -46,7 +46,7 @@ "error-page.description.500": "Služba je nedostupná", // "error-page.description.404": "Page not found", - "error-page.description.404": "Stránka nebyla nenalezena", + "error-page.description.404": "Stránka nebyla nalezena", // "error-page.orcid.generic-error": "An error occurred during login via ORCID. Make sure you have shared your ORCID account email address with DSpace. If the error persists, contact the administrator", "error-page.orcid.generic-error": "Při přihlašování přes ORCID došlo k chybě. Ujistěte se, že jste sdíleli e-mailovou adresu připojenou ke svému účtu ORCID s DSpace. Pokud chyba přetrvává, kontaktujte správce", @@ -67,13 +67,13 @@ "access-status.unknown.listelement.badge": "Status neznámý", // "admin.curation-tasks.breadcrumbs": "System curation tasks", - "admin.curation-tasks.breadcrumbs": "Kurátorská úloha systému", + "admin.curation-tasks.breadcrumbs": "Systémové úlohy správy", // "admin.curation-tasks.title": "System curation tasks", - "admin.curation-tasks.title": "Kurátorská úloha systému", + "admin.curation-tasks.title": "Systémové úlohy správy", // "admin.curation-tasks.header": "System curation tasks", - "admin.curation-tasks.header": "Kurátorská úloha systému", + "admin.curation-tasks.header": "Systémové úlohy správy", // "admin.registries.bitstream-formats.breadcrumbs": "Format registry", "admin.registries.bitstream-formats.breadcrumbs": "Registr formátů", @@ -172,7 +172,7 @@ "admin.registries.bitstream-formats.edit.supportLevel.label": "Úroveň podpory", // "admin.registries.bitstream-formats.head": "Bitstream Format Registry", - "admin.registries.bitstream-formats.head": "Registr formátu souboru", + "admin.registries.bitstream-formats.head": "Registr formátů souboru", // "admin.registries.bitstream-formats.no-items": "No bitstream formats to show.", "admin.registries.bitstream-formats.no-items": "Žádné formáty souboru k zobrazení.", @@ -373,7 +373,7 @@ "admin.access-control.bulk-access.breadcrumbs": "Hromadná správa přístupu", // "administrativeBulkAccess.search.results.head": "Search Results", - "administrativeBulkAccess.search.results.head": "Prohledávat výsledky", + "administrativeBulkAccess.search.results.head": "Výsledky vyhledávání", // "admin.access-control.bulk-access": "Bulk Access Management", "admin.access-control.bulk-access": "Hromadná správa přístupu", @@ -403,7 +403,7 @@ "admin.access-control.epeople.actions.reset": "Resetovat heslo", // "admin.access-control.epeople.actions.stop-impersonating": "Stop impersonating EPerson", - "admin.access-control.epeople.actions.stop-impersonating": "Přestat simulovat jiného uživatele", + "admin.access-control.epeople.actions.stop-impersonating": "Přestat vystupovat jako jiný uživatel", // "admin.access-control.epeople.breadcrumbs": "EPeople", "admin.access-control.epeople.breadcrumbs": "Uživatelé", @@ -612,7 +612,7 @@ "admin.access-control.groups.table.edit.buttons.remove": "Odstranit \"{{name}}\"", // "admin.access-control.groups.no-items": "No groups found with this in their name or this as UUID", - "admin.access-control.groups.no-items": "Nebyly nalezeny žádné skupiny, které by měly toto ve svém názvu nebo toto jako UUID,", + "admin.access-control.groups.no-items": "Nebyla nalezena žádná skupina s tímto v návzvu nebo UUID", // "admin.access-control.groups.notification.deleted.success": "Successfully deleted group \"{{name}}\"", "admin.access-control.groups.notification.deleted.success": "Úspěšně odstraněna skupina \"{{name}}\"", @@ -696,7 +696,7 @@ "admin.access-control.groups.form.members-list.button.see-all": "Procházet vše", // "admin.access-control.groups.form.members-list.headMembers": "Current Members", - "admin.access-control.groups.form.members-list.headMembers": "Aktuální členové", + "admin.access-control.groups.form.members-list.headMembers": "Současní členové", // "admin.access-control.groups.form.members-list.search.button": "Search", "admin.access-control.groups.form.members-list.search.button": "Hledat", @@ -738,13 +738,13 @@ "admin.access-control.groups.form.members-list.table.edit.buttons.add": "Přidat člena jménem \"{{name}}\"", // "admin.access-control.groups.form.members-list.notification.failure.noActiveGroup": "No current active group, submit a name first.", - "admin.access-control.groups.form.members-list.notification.failure.noActiveGroup": "Žádná aktuální aktivní skupina, nejprve zadejte jméno.", + "admin.access-control.groups.form.members-list.notification.failure.noActiveGroup": "Žádná aktuální aktivní skupina, nejprve zadejte název.", // "admin.access-control.groups.form.members-list.no-members-yet": "No members in group yet, search and add.", "admin.access-control.groups.form.members-list.no-members-yet": "Ve skupině zatím nejsou žádní členové, vyhledejte je a přidejte.", // "admin.access-control.groups.form.members-list.no-items": "No EPeople found in that search", - "admin.access-control.groups.form.members-list.no-items": "V tomto vyhledávání nebyly nalezeni žádní uživatelé", + "admin.access-control.groups.form.members-list.no-items": "V tomto vyhledávání nebyli nalezeni žádní uživatelé", // "admin.access-control.groups.form.subgroups-list.notification.failure": "Something went wrong: \"{{cause}}\"", "admin.access-control.groups.form.subgroups-list.notification.failure": "Neúspěch: Něco se pokazilo: \"{{cause}}\"", @@ -801,7 +801,7 @@ "admin.access-control.groups.form.subgroups-list.notification.failure.subgroupToAddIsActiveGroup": "Toto je aktuální skupina, nelze přidat.", // "admin.access-control.groups.form.subgroups-list.no-items": "No groups found with this in their name or this as UUID", - "admin.access-control.groups.form.subgroups-list.no-items": "Nebyly nalezeny žádné skupiny, které by měly toto ve svém názvu nebo toto UUID", + "admin.access-control.groups.form.subgroups-list.no-items": "Nebyla nalezena žádná skupina s tímto v návzvu nebo UUID", // "admin.access-control.groups.form.subgroups-list.no-subgroups-yet": "No subgroups in group yet.", "admin.access-control.groups.form.subgroups-list.no-subgroups-yet": "Ve skupině zatím nejsou žádné podskupiny.", @@ -831,7 +831,7 @@ "admin.notifications.source.breadcrumbs": "Quality Assurance", // "admin.access-control.groups.form.tooltip.editGroupPage": "On this page, you can modify the properties and members of a group. In the top section, you can edit the group name and description, unless this is an admin group for a collection or community, in which case the group name and description are auto-generated and cannot be edited. In the following sections, you can edit group membership. See [the wiki](https://wiki.lyrasis.org/display/DSDOC7x/Create+or+manage+a+user+group) for more details.", - "admin.access-control.groups.form.tooltip.editGroupPage": "Na této stránce můžete upravit vlastnosti a členy skupiny. V horní části můžete upravit název a popis skupiny, pokud se nejedá o skupinu admin pro kolekci nebo komunitu. V takovém případě jsou název a popis skupiny vygenerovány automaticky a nelze je upravovat. V následujících částech můžete upravovat členství ve skupině. Další podrobnosti naleznete v části [the wiki](https://wiki.lyrasis.org/display/DSDOC7x/Create+or+manage+a+user+group).", + "admin.access-control.groups.form.tooltip.editGroupPage": "Na této stránce můžete upravit vlastnosti a členy skupiny. V horní části můžete upravit název a popis skupiny, pokud se nejedá o skupinu admin pro kolekci nebo komunitu. V takovém případě jsou název a popis skupiny vygenerovány automaticky a nelze je upravovat. V následujících částech můžete upravovat členství ve skupině. Další podrobnosti naleznete v části [wiki](https://wiki.lyrasis.org/display/DSDOC7x/Create+or+manage+a+user+group).", // "admin.access-control.groups.form.tooltip.editGroup.addEpeople": "To add or remove an EPerson to/from this group, either click the 'Browse All' button or use the search bar below to search for users (use the dropdown to the left of the search bar to choose whether to search by metadata or by email). Then click the plus icon for each user you wish to add in the list below, or the trash can icon for each user you wish to remove. The list below may have several pages: use the page controls below the list to navigate to the next pages.", "admin.access-control.groups.form.tooltip.editGroup.addEpeople": "Pro přidání nebo odebrání uživatele z této skupiny, buď klikněte na „Prohledávat všechny“ nebo použijte vyhledávací okno pro vyhledání uživatele (použijte roletku na levé straně vyhledávacího okna a vyberte, zda chcete prohledávat pomocí metadat nebo e-mailu). Potom klikněte na tlačítko plus u každého uživatele, kterého chcete přidat, nebo na ikonu odpadkového koše u každého uživatele, kterého chcete odebrat. Seznam níže může obsahovat několik stránek: k přechodu na další stránku použijte ovládací prvky pod seznamem. Až budete hotovi, uložte změny pomocí tlačítka „Uložit“ v horní části.", @@ -1406,7 +1406,7 @@ "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.button.see-all": "Prohledávat vše", // "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.headMembers": "Current Members", - "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.headMembers": "Aktuální členové", + "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.headMembers": "Současní členové", // "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.search.button": "Search", "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.search.button": "Hledat", @@ -1448,7 +1448,7 @@ "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.table.edit.buttons.add": "Přidat uživatele jménem \"{{name}}\"", // "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.notification.failure.noActiveGroup": "No current active group, submit a name first.", - "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.notification.failure.noActiveGroup": "Žádná aktivní skupina, nejdříve vložte název.", + "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.notification.failure.noActiveGroup": "Žádná aktuální aktivní skupina, nejprve zadejte název.", // "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.no-members-yet": "No members in group yet, search and add.", "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.no-members-yet": "Žádní členové ve skupině, nejdříve vyhledejte a přidejte.", @@ -1792,7 +1792,7 @@ "claimed-approved-search-result-list-element.title": "Schváleno", // "claimed-declined-search-result-list-element.title": "Rejected, sent back to submitter", - "claimed-declined-search-result-list-element.title": "Zamítnuto, posláno zpět vkladateli", + "claimed-declined-search-result-list-element.title": "Zamítnuto, posláno zpět odesílateli", // "claimed-declined-task-search-result-list-element.title": "Declined, sent back to Review Manager's workflow", "claimed-declined-task-search-result-list-element.title": "Zamítnuto, posláno zpět správci schvalovacího workflow", @@ -1815,7 +1815,7 @@ "collection.create.sub-head": "Vytvořit kolekci pro komunitu {{ parent }}", // "collection.curate.header": "Curate Collection: {{collection}}", - "collection.curate.header": "Kurátorovat kolekci: {{collection}}", + "collection.curate.header": "Spravovat kolekci: {{collection}}", // "collection.delete.cancel": "Cancel", "collection.delete.cancel": "Zrušit", @@ -1923,7 +1923,7 @@ "collection.edit.logo.notifications.add.success": "Nahrání loga kolekce proběhlo úspěšně.", // "collection.edit.logo.notifications.delete.success.title": "Logo deleted", - "collection.edit.logo.notifications.delete.success.title": "Logo odstraněno", + "collection.edit.logo.notifications.delete.success.title": "Logo smazáno", // "collection.edit.logo.notifications.delete.success.content": "Successfully deleted the collection's logo", "collection.edit.logo.notifications.delete.success.content": "Úspěšně odstraněno logo kolekce", @@ -1948,10 +1948,10 @@ "collection.edit.tabs.access-control.title": "Úprava kolekce - Řízení přístupu", // "collection.edit.tabs.curate.head": "Curate", - "collection.edit.tabs.curate.head": "Kurátorovat", + "collection.edit.tabs.curate.head": "Spravovat", // "collection.edit.tabs.curate.title": "Collection Edit - Curate", - "collection.edit.tabs.curate.title": "Upravit kolekci - kurátorovat", + "collection.edit.tabs.curate.title": "Upravit kolekci - správa", // "collection.edit.tabs.authorizations.head": "Authorizations", "collection.edit.tabs.authorizations.head": "Oprávnění", @@ -2014,7 +2014,7 @@ "collection.edit.tabs.source.head": "Zdroj obsahu", // "collection.edit.tabs.source.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", - "collection.edit.tabs.source.notifications.discarded.content": "Vaše změny byly vyřazeny. Chcete-li své změny obnovit, klikněte na tlačítko Zpět", + "collection.edit.tabs.source.notifications.discarded.content": "Vaše změny byly zahozeny. Pro obnovení změn klikněte na tlačítko 'Vrátit změny'", // "collection.edit.tabs.source.notifications.discarded.title": "Changes discarded", // TODO Source message changed - Revise the translation @@ -2024,7 +2024,7 @@ "collection.edit.tabs.source.notifications.invalid.content": "Vaše změny nebyly uloženy. Před uložením se prosím ujistěte, že jsou všechna pole platná.", // "collection.edit.tabs.source.notifications.invalid.title": "Metadata invalid", - "collection.edit.tabs.source.notifications.invalid.title": "Metadata neplatná", + "collection.edit.tabs.source.notifications.invalid.title": "Neplatná metadata", // "collection.edit.tabs.source.notifications.saved.content": "Your changes to this collection's content source were saved.", "collection.edit.tabs.source.notifications.saved.content": "Vaše změny ve zdroji obsahu této kolekce byly uloženy.", @@ -2215,7 +2215,7 @@ "collection.source.controls.harvest.no-information": "Není k dispozi", // "collection.source.update.notifications.error.content": "The provided settings have been tested and didn't work.", - "collection.source.update.notifications.error.content": "Zadané nastavení bylo testováno a nefungovalo.", + "collection.source.update.notifications.error.content": "Zadané nastavení bylo otestováno a nefungovalo.", // "collection.source.update.notifications.error.title": "Server Error", "collection.source.update.notifications.error.title": "Chyba serveru", @@ -2262,7 +2262,7 @@ "community.create.sub-head": "Vytvořit dílčí komunitu pro komunitu {{ parent }}", // "community.curate.header": "Curate Community: {{community}}", - "community.curate.header": "Kurátorství komunity: {{ community }}", + "community.curate.header": "Spravovat komunitu: {{ community }}", // "community.delete.cancel": "Cancel", "community.delete.cancel": "Zrušit", @@ -2295,7 +2295,7 @@ "community.edit.breadcrumbs": "Upravit komunitu", // "community.edit.logo.delete.title": "Delete logo", - "community.edit.logo.delete.title": "Odstranit logo", + "community.edit.logo.delete.title": "Smazat logo", // "community.edit.logo.delete-undo.title": "Undo delete", "community.edit.logo.delete-undo.title": "Zrušit odstranění", @@ -2310,7 +2310,7 @@ "community.edit.logo.notifications.add.success": "Nahrání loga komunity proběhlo úspěšně.", // "community.edit.logo.notifications.delete.success.title": "Logo deleted", - "community.edit.logo.notifications.delete.success.title": "Logo odstraněno", + "community.edit.logo.notifications.delete.success.title": "Logo smazáno", // "community.edit.logo.notifications.delete.success.content": "Successfully deleted the community's logo", "community.edit.logo.notifications.delete.success.content": "Úspěšně odstraněno logo komunity", @@ -2335,10 +2335,10 @@ "community.edit.return": "Zpět", // "community.edit.tabs.curate.head": "Curate", - "community.edit.tabs.curate.head": "Kurátorovat", + "community.edit.tabs.curate.head": "Spravovat", // "community.edit.tabs.curate.title": "Community Edit - Curate", - "community.edit.tabs.curate.title": "Upravit komunitu - kurátorovat", + "community.edit.tabs.curate.title": "Upravit komunitu - správa", // "community.edit.tabs.access-control.head": "Access Control", "community.edit.tabs.access-control.head": "Řízení přístupu", @@ -2396,13 +2396,13 @@ "comcol-role.edit.collection-admin.name": "Správci", // "comcol-role.edit.community-admin.description": "Community administrators can create sub-communities or collections, and manage or assign management for those sub-communities or collections. In addition, they decide who can submit items to any sub-collections, edit item metadata (after submission), and add (map) existing items from other collections (subject to authorization).", - "comcol-role.edit.community-admin.description": "Správci komunit mohou vytvářet dílčí komunity nebo kolekce a spravovat nebo přidělovat správu těmto dílčím komunitám nebo kolekcím. Kromě toho rozhodují o tom, kdo může odesílat záznamy do všech dílčích kolekcí, upravovat metadata záznamů (po odeslání) a přidávat (mapovat) existující záznamy z jiných kolekcí (na základě oprávnění).", + "comcol-role.edit.community-admin.description": "Administrátoři komunit mohou vytvářet dílčí komunity nebo kolekce a spravovat nebo přidělovat správu těmto dílčím komunitám nebo kolekcím. Kromě toho rozhodují o tom, kdo může odesílat záznamy do všech dílčích kolekcí, upravovat metadata záznamů (po odeslání) a přidávat (mapovat) existující záznamy z jiných kolekcí (na základě oprávnění).", // "comcol-role.edit.collection-admin.description": "Collection administrators decide who can submit items to the collection, edit item metadata (after submission), and add (map) existing items from other collections to this collection (subject to authorization for that collection).", "comcol-role.edit.collection-admin.description": "Správci kolekcí rozhodují o tom, kdo může do kolekce vkládat záznamy, upravovat metadata záznamů (po vložení) a přidávat (mapovat) existující záznamy z jiných kolekcí do této kolekce (podléhá autorizaci pro danou kolekci).", // "comcol-role.edit.submitters.name": "Submitters", - "comcol-role.edit.submitters.name": "Vkladatelé", + "comcol-role.edit.submitters.name": "Odesílatelé", // "comcol-role.edit.submitters.description": "The E-People and Groups that have permission to submit new items to this collection.", "comcol-role.edit.submitters.description": "Uživatelé a skupiny, které mají oprávnění ke vkládání nových záznamů do této kolekce.", @@ -2460,7 +2460,7 @@ "community.form.errors.title.required": "Zadejte název komunity", // "community.form.rights": "Copyright text (HTML)", - "community.form.rights": "Text autorských práv (HTML)", + "community.form.rights": "Copyright (HTML)", // "community.form.tableofcontents": "News (HTML)", "community.form.tableofcontents": "Novinky (HTML)", @@ -2640,16 +2640,16 @@ "curation.form.submit": "Start", // "curation.form.submit.success.head": "The curation task has been started successfully", - "curation.form.submit.success.head": "Kurátorská úloha byla úspěšně spuštěna", + "curation.form.submit.success.head": "Úloha správy byla úspěšně spuštěna", // "curation.form.submit.success.content": "You will be redirected to the corresponding process page.", "curation.form.submit.success.content": "Budete přesměrováni na příslušnou stránku procesu.", // "curation.form.submit.error.head": "Running the curation task failed", - "curation.form.submit.error.head": "Spuštění kurátorské úlohy se nezdařilo.", + "curation.form.submit.error.head": "Spuštění úlohy správy se nezdařilo.", // "curation.form.submit.error.content": "An error occured when trying to start the curation task.", - "curation.form.submit.error.content": "Při pokusu o spuštění kurátorské úlohy došlo k chybě.", + "curation.form.submit.error.content": "Při pokusu o spuštění úlohy správy došlo k chybě.", // "curation.form.submit.error.invalid-handle": "Couldn't determine the handle for this object", "curation.form.submit.error.invalid-handle": "Nepodařilo se určit handle pro tento objekt", @@ -3044,7 +3044,7 @@ "forgot-email.form.success.head": "E-mail pro obnovení hesla odeslán", // "forgot-email.form.success.content": "An email has been sent to {{ email }} containing a special URL and further instructions.", - "forgot-email.form.success.content": "Na adresu {{ email }} byl odeslán e-mail obsahující speciální adresu URL a další pokyny.", + "forgot-email.form.success.content": "Na adresu {{ email }} byl odeslán e-mail obsahující speciální URL a další instrukce.", // "forgot-email.form.error.head": "Error when trying to reset password", // TODO Source message changed - Revise the translation @@ -3242,7 +3242,7 @@ "grant-deny-request-copy.header": "Žádost o kopii dokumentu", // "grant-deny-request-copy.home-page": "Take me to the home page", - "grant-deny-request-copy.home-page": "Přesměrujte mne na domovskou stránku", + "grant-deny-request-copy.home-page": "Návrat na domovskou stránku", // "grant-deny-request-copy.intro1": "If you are one of the authors of the document {{ name }}, then please use one of the options below to respond to the user's request.", "grant-deny-request-copy.intro1": "Pokud jste jedním z autorů dokumentu {{ name }}, použijte prosím jednu z níže uvedených možností, abyste odpověděli na žádost uživatele.", @@ -3278,13 +3278,13 @@ "health-page.info-tab": "Informace", // "health-page.status-tab": "Status", - "health-page.status-tab": "Status", + "health-page.status-tab": "Stav", // "health-page.error.msg": "The health check service is temporarily unavailable", "health-page.error.msg": "Služba kontrolující stav systému je momentálně nedostupná.", // "health-page.property.status": "Status code", - "health-page.property.status": "Kód statusu", + "health-page.property.status": "Stavový kód", // "health-page.section.db.title": "Database", "health-page.section.db.title": "Databáze", @@ -3455,7 +3455,7 @@ "item.badge.private": "Nedohledatelné", // "item.badge.withdrawn": "Withdrawn", - "item.badge.withdrawn": "Zrušeno", + "item.badge.withdrawn": "Vyřazeno", // "item.bitstreams.upload.bundle": "Bundle", "item.bitstreams.upload.bundle": "Svazek", @@ -3543,10 +3543,10 @@ "item.edit.bitstreams.headers.name": "Název", // "item.edit.bitstreams.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", - "item.edit.bitstreams.notifications.discarded.content": "Vaše změny byly zahozeny. Pro obnovení změn klikněte na tlačítko 'Undo'", + "item.edit.bitstreams.notifications.discarded.content": "Vaše změny byly zahozeny. Pro obnovení změn klikněte na tlačítko 'Vrátit změny'", // "item.edit.bitstreams.notifications.discarded.title": "Changes discarded", - "item.edit.bitstreams.notifications.discarded.title": "Změny zahozeny", + "item.edit.bitstreams.notifications.discarded.title": "Zahozené změny", // "item.edit.bitstreams.notifications.move.failed.title": "Error moving bitstreams", "item.edit.bitstreams.notifications.move.failed.title": "Chyba při přesunu souborů", @@ -3558,7 +3558,7 @@ "item.edit.bitstreams.notifications.move.saved.title": "Změny uloženy", // "item.edit.bitstreams.notifications.outdated.content": "The item you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts", - "item.edit.bitstreams.notifications.outdated.content": "Záznam, na které právě pracujete, byl změněn jiným uživatelem. Vaše aktuální změny jsou zahozeny, aby se zabránilo konfliktům", + "item.edit.bitstreams.notifications.outdated.content": "Záznam, na kterém právě pracujete, byl změněn jiným uživatelem. Vaše aktuální změny byly zahozeny, aby se zabránilo konfliktům", // "item.edit.bitstreams.notifications.outdated.title": "Changes outdated", "item.edit.bitstreams.notifications.outdated.title": "Změny jsou zastaralé", @@ -3651,13 +3651,13 @@ "item.edit.identifiers.doi.status.MINTED": "Vydáno (nezaregistrováno)", // "item.edit.tabs.status.buttons.register-doi.label": "Register a new or pending DOI", - "item.edit.tabs.status.buttons.register-doi.label": "Registrace nového nebo čekání na nové DOI", + "item.edit.tabs.status.buttons.register-doi.label": "Registrovat nové nebo čekající DOI", // "item.edit.tabs.status.buttons.register-doi.button": "Register DOI...", "item.edit.tabs.status.buttons.register-doi.button": "Zaregistrujte DOI...", // "item.edit.register-doi.header": "Register a new or pending DOI", - "item.edit.register-doi.header": "Zaregistrujte nové DOI nebo čekající k registraci", + "item.edit.register-doi.header": "Registrovat nové nebo čekající DOI", // "item.edit.register-doi.description": "Review any pending identifiers and item metadata below and click Confirm to proceed with DOI registration, or Cancel to back out", "item.edit.register-doi.description": "Níže zkontrolujte všechny identifikátory a metadata ve frontě. Zvolte \"Potvrdit\" pro pokračování v registraci DOI nebo \"Zrušit\" pro přerušení procesu.", @@ -3687,7 +3687,7 @@ "item.edit.item-mapper.cancel": "Zrušit", // "item.edit.item-mapper.description": "This is the item mapper tool that allows administrators to map this item to other collections. You can search for collections and map them, or browse the list of collections the item is currently mapped to.", - "item.edit.item-mapper.description": "Toto je nástroj pro mapování záznamů, který umožňuje správcům mapovat tuto záznam na jiné kolekce. Můžete vyhledávat kolekce a mapovat je nebo procházet seznam kolekcí, ke kterým je záznam aktuálně mapována.", + "item.edit.item-mapper.description": "Toto je nástroj pro mapování záznamů, který umožňuje správcům mapovat tento záznam do jiné kolekce. Můžete vyhledávat kolekce a mapovat je nebo procházet seznam kolekcí, ke kterým je záznam aktuálně mapována.", // "item.edit.item-mapper.head": "Item Mapper - Map Item to Collections", "item.edit.item-mapper.head": "Mapování do jiných kolekcí - mapovat záznam do kolekcí", @@ -3768,7 +3768,7 @@ "item.edit.metadata.edit.buttons.unedit": "Zastavit úpravy", // "item.edit.metadata.edit.buttons.virtual": "This is a virtual metadata value, i.e. a value inherited from a related entity. It can’t be modified directly. Add or remove the corresponding relationship in the \"Relationships\" tab", - "item.edit.metadata.edit.buttons.virtual": "Tato hodnota metadat je virtuální, tedy děděná z příbuzné entity. Nemůže být zděděna napřímo. Přidejte či odeberte příslušný vztah na záložce \"Vztahy\".", + "item.edit.metadata.edit.buttons.virtual": "Tato hodnota metadat je virtuální, tedy děděná z provázané entity. Nemůže být změněna napřímo. Přidejte či odeberte příslušný vztah na záložce \"Vztahy\".", // "item.edit.metadata.empty": "The item currently doesn't contain any metadata. Click Add to start adding a metadata value.", "item.edit.metadata.empty": "Záznam aktuálně neobsahuje žádná metadata. Kliknutím na tlačítko Přidat začněte přidávat hodnotu metadat.", @@ -3796,11 +3796,11 @@ "item.edit.metadata.metadatafield.invalid": "Prosím, vyberte platné metadatové pole", // "item.edit.metadata.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", - "item.edit.metadata.notifications.discarded.content": "Vaše změny byly vyřazeny. Pro obnovení změn klikněte na tlačítko 'Vrátit změny'", + "item.edit.metadata.notifications.discarded.content": "Vaše změny byly zahozeny. Pro obnovení změn klikněte na tlačítko 'Vrátit změny'", // "item.edit.metadata.notifications.discarded.title": "Changes discarded", // TODO Source message changed - Revise the translation - "item.edit.metadata.notifications.discarded.title": "Změny zahozeny", + "item.edit.metadata.notifications.discarded.title": "Zahozené změny", // "item.edit.metadata.notifications.error.title": "An error occurred", "item.edit.metadata.notifications.error.title": "Došlo k chybě", @@ -3809,17 +3809,17 @@ "item.edit.metadata.notifications.invalid.content": "Vaše změny nebyly uloženy. Před uložením se prosím ujistěte, že jsou všechna pole platná.", // "item.edit.metadata.notifications.invalid.title": "Metadata invalid", - "item.edit.metadata.notifications.invalid.title": "Metadata neplatná", + "item.edit.metadata.notifications.invalid.title": "Neplatná metadata", // "item.edit.metadata.notifications.outdated.content": "The item you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts", - "item.edit.metadata.notifications.outdated.content": "Záznam, na které právě pracujete, byl změněna jiným uživatelem. Vaše aktuální změny jsou zahozeny, aby se zabránilo konfliktům", + "item.edit.metadata.notifications.outdated.content": "Záznam, na kterém právě pracujete, byl změněn jiným uživatelem. Vaše aktuální změny byly zahozeny, aby se zabránilo konfliktům", // "item.edit.metadata.notifications.outdated.title": "Changes outdated", // TODO Source message changed - Revise the translation "item.edit.metadata.notifications.outdated.title": "Změny jsou zastaralé", // "item.edit.metadata.notifications.saved.content": "Your changes to this item's metadata were saved.", - "item.edit.metadata.notifications.saved.content": "Vaše změny metadat této záznamu byly uloženy.", + "item.edit.metadata.notifications.saved.content": "Vaše změny metadat tohoto záznamu byly uloženy.", // "item.edit.metadata.notifications.saved.title": "Metadata saved", "item.edit.metadata.notifications.saved.title": "Metadata uložena", @@ -3976,16 +3976,16 @@ "item.edit.relationships.no-relationships": "Žádné vztahy", // "item.edit.relationships.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", - "item.edit.relationships.notifications.discarded.content": "Vaše změny byly vyřazeny. Pro obnovení změn klikněte na tlačítko 'Vrátit změny'", + "item.edit.relationships.notifications.discarded.content": "Vaše změny byly zahozeny. Pro obnovení změn klikněte na tlačítko 'Vrátit změny'", // "item.edit.relationships.notifications.discarded.title": "Changes discarded", - "item.edit.relationships.notifications.discarded.title": "Změny vyřazeny", + "item.edit.relationships.notifications.discarded.title": "Zahozené změny", // "item.edit.relationships.notifications.failed.title": "Error editing relationships", "item.edit.relationships.notifications.failed.title": "Chyba při úpravě vztahů", // "item.edit.relationships.notifications.outdated.content": "The item you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts", - "item.edit.relationships.notifications.outdated.content": "Záznam, na které právě pracujete, byl změněna jiným uživatelem. Vaše aktuální změny jsou vyřazeny, aby se zabránilo konfliktům", + "item.edit.relationships.notifications.outdated.content": "Záznam, na kterém právě pracujete, byl změněn jiným uživatelem. Vaše aktuální změny byly zahozeny, aby se zabránilo konfliktům", // "item.edit.relationships.notifications.outdated.title": "Changes outdated", "item.edit.relationships.notifications.outdated.title": "Změny jsou zastaralé", @@ -4009,25 +4009,25 @@ "item.edit.return": "Zpět", // "item.edit.tabs.bitstreams.head": "Bitstreams", - "item.edit.tabs.bitstreams.head": "Soubor", + "item.edit.tabs.bitstreams.head": "Soubory", // "item.edit.tabs.bitstreams.title": "Item Edit - Bitstreams", "item.edit.tabs.bitstreams.title": "Úprava záznamu - soubor", // "item.edit.tabs.curate.head": "Curate", - "item.edit.tabs.curate.head": "Kurátorovat", + "item.edit.tabs.curate.head": "Spravovat", // "item.edit.tabs.curate.title": "Item Edit - Curate", - "item.edit.tabs.curate.title": "Úprava záznamu - Kurátorovat", + "item.edit.tabs.curate.title": "Úprava záznamu - spravovat", // "item.edit.curate.title": "Curate Item: {{item}}", - "item.edit.curate.title": "Kurátorovat záznam: {{item}}", + "item.edit.curate.title": "Spravovat záznam: {{item}}", // "item.edit.tabs.access-control.head": "Access Control", - "item.edit.tabs.access-control.head": "Kontrola přístupu", + "item.edit.tabs.access-control.head": "Řízení přístupu", // "item.edit.tabs.access-control.title": "Item Edit - Access Control", - "item.edit.tabs.access-control.title": "Úprava záznamu - Kontrola přístupu", + "item.edit.tabs.access-control.title": "Úprava záznamu - Řízení přístupu", // "item.edit.tabs.metadata.head": "Metadata", "item.edit.tabs.metadata.head": "Metadata", @@ -4275,7 +4275,7 @@ "item.page.collections": "Kolekce", // "item.page.collections.loading": "Loading...", - "item.page.collections.loading": "Načítání...", + "item.page.collections.loading": "Načítá se...", // "item.page.collections.load-more": "Load more", "item.page.collections.load-more": "Načíst další", @@ -4377,7 +4377,7 @@ "item.page.reinstate": "Request reinstatement", // "item.page.version.hasDraft": "A new version cannot be created because there is an inprogress submission in the version history", - "item.page.version.hasDraft": "Nová verze nemůže být vytvořena, protože v historii verzí právě probíhá zadání nové verze.", + "item.page.version.hasDraft": "Nová verze nemůže být vytvořena, protože v historii verzí už další rozpracovaná verze existuje.", // "item.page.claim.button": "Claim", "item.page.claim.button": "Prohlásit", @@ -4585,7 +4585,7 @@ "item.version.history.table.action.deleteVersion": "Smazat verzi", // "item.version.history.table.action.hasDraft": "A new version cannot be created because there is an inprogress submission in the version history", - "item.version.history.table.action.hasDraft": "Nová verze nemůže být vytvořena, protože v historii verzí probíhá odesílání.", + "item.version.history.table.action.hasDraft": "Nová verze nemůže být vytvořena, protože v historii verzí už další rozpracovaná verze existuje.", // "item.version.notice": "This is not the latest version of this item. The latest version can be found here.", "item.version.notice": "Toto není nejnovější verze tohoto záznamu. Nejnovější verzi naleznete zde.", @@ -4698,7 +4698,7 @@ "item.version.create.notification.failure": "Nová verze nebyla vytvořena", // "item.version.create.notification.inProgress": "A new version cannot be created because there is an inprogress submission in the version history", - "item.version.create.notification.inProgress": "Nová verze nemůže být vytvořena, protože v historii verzí probíhá odesílání.", + "item.version.create.notification.inProgress": "Nová verze nemůže být vytvořena, protože v historii verzí už další rozpracovaná verze existuje.", // "item.version.delete.modal.header": "Delete version", "item.version.delete.modal.header": "Smazat verzi", @@ -4788,7 +4788,7 @@ "itemtemplate.edit.metadata.metadatafield.invalid": "Prosím zvolte platné metadatové pole", // "itemtemplate.edit.metadata.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", - "itemtemplate.edit.metadata.notifications.discarded.content": "Vaše změny byly zahozeny. Chcete-li obnovit své změny, klikněte na tlačítko 'Zpět'.", + "itemtemplate.edit.metadata.notifications.discarded.content": "Vaše změny byly zahozeny. Pro obnovení změn klikněte na tlačítko 'Vrátit změny'", // "itemtemplate.edit.metadata.notifications.discarded.title": "Changes discarded", "itemtemplate.edit.metadata.notifications.discarded.title": "Změny byly zahozeny", @@ -4797,7 +4797,7 @@ "itemtemplate.edit.metadata.notifications.error.title": "Vyskytla se chyba", // "itemtemplate.edit.metadata.notifications.invalid.content": "Your changes were not saved. Please make sure all fields are valid before you save.", - "itemtemplate.edit.metadata.notifications.invalid.content": "Vaše změny nebyly uloženy. Před uložením se ujistěte, že všechna pole jsou platná.", + "itemtemplate.edit.metadata.notifications.invalid.content": "Vaše změny nebyly uloženy. Před uložením se prosím ujistěte, že jsou všechna pole platná.", // "itemtemplate.edit.metadata.notifications.invalid.title": "Metadata invalid", "itemtemplate.edit.metadata.notifications.invalid.title": "Neplatná metadata", @@ -4812,7 +4812,7 @@ "itemtemplate.edit.metadata.notifications.saved.content": "Vaše změny v šabloně metadat tohoto záznamu byly uloženy.", // "itemtemplate.edit.metadata.notifications.saved.title": "Metadata saved", - "itemtemplate.edit.metadata.notifications.saved.title": "Metadata uloženy", + "itemtemplate.edit.metadata.notifications.saved.title": "Metadata uložena", // "itemtemplate.edit.metadata.reinstate-button": "Undo", "itemtemplate.edit.metadata.reinstate-button": "Vrátit zpět", @@ -5043,7 +5043,7 @@ "menu.header.admin": "Admin", // "menu.header.image.logo": "Repository logo", - "menu.header.image.logo": "Logo úložiště", + "menu.header.image.logo": "Logo repozitáře", // "menu.header.admin.description": "Management menu", "menu.header.admin.description": "Management menu", @@ -5119,7 +5119,7 @@ "menu.section.control_panel": "Ovládací panel", // "menu.section.curation_task": "Curation Task", - "menu.section.curation_task": "Kurátorská úloha", + "menu.section.curation_task": "Úloha správy", // "menu.section.edit": "Edit", "menu.section.edit": "Upravit", @@ -5165,25 +5165,25 @@ "menu.section.icon.control_panel": "Sekce menu Ovládací panel", // "menu.section.icon.curation_tasks": "Curation Task menu section", - "menu.section.icon.curation_tasks": "Sekce menu Kurátorská úloha", + "menu.section.icon.curation_tasks": "Sekce menu úloha správy", // "menu.section.icon.edit": "Edit menu section", "menu.section.icon.edit": "Sekce menu Upravit", // "menu.section.icon.export": "Export menu section", - "menu.section.icon.export": "Exportovat sekci menu", + "menu.section.icon.export": "Sekce menu Exportovat", // "menu.section.icon.find": "Find menu section", - "menu.section.icon.find": "Najít sekci menu", + "menu.section.icon.find": "Sekce menu Najít", // "menu.section.icon.health": "Health check menu section", - "menu.section.icon.health": "Kontrola stavu sekce menu", + "menu.section.icon.health": "Sekce menu Kontrola stavu", // "menu.section.icon.import": "Import menu section", - "menu.section.icon.import": "Importovat sekci menu", + "menu.section.icon.import": "Sekce menu Importovat", // "menu.section.icon.new": "New menu section", - "menu.section.icon.new": "Nová sekce menu", + "menu.section.icon.new": "Sekce menu Nový", // "menu.section.icon.pin": "Pin sidebar", "menu.section.icon.pin": "Připnout postranní panel", @@ -5260,7 +5260,7 @@ "menu.section.health": "Stav systému", // "menu.section.registries": "Registries", - "menu.section.registries": "Rejstřík", + "menu.section.registries": "Registry", // "menu.section.registries_format": "Format", "menu.section.registries_format": "Formát", @@ -5285,7 +5285,7 @@ "menu.section.toggle.control_panel": "Přepnout sekci ovládacího panelu", // "menu.section.toggle.curation_task": "Toggle Curation Task section", - "menu.section.toggle.curation_task": "Přepnout sekci Kurátorská úloha", + "menu.section.toggle.curation_task": "Přepnout sekci úloha správy", // "menu.section.toggle.edit": "Toggle Edit section", "menu.section.toggle.edit": "Přepnout sekci Úpravy", @@ -5303,7 +5303,7 @@ "menu.section.toggle.new": "Přepnout sekci Nová", // "menu.section.toggle.registries": "Toggle Registries section", - "menu.section.toggle.registries": "Přepnout sekci Rejstříky", + "menu.section.toggle.registries": "Přepnout sekci Registry", // "menu.section.toggle.statistics_task": "Toggle Statistics Task section", "menu.section.toggle.statistics_task": "Přepnout sekci Statistická úloha", @@ -5327,7 +5327,7 @@ "mydspace.description": "", // "mydspace.messages.controller-help": "Select this option to send a message to item's submitter.", - "mydspace.messages.controller-help": "Zvolte tuto možnost, chcete-li odeslat zprávu vkladateli záznamu.", + "mydspace.messages.controller-help": "Zvolte tuto možnost, chcete-li odeslat zprávu odesílateli záznamu.", // "mydspace.messages.description-placeholder": "Insert your message here...", "mydspace.messages.description-placeholder": "Zde vložte svou zprávu...", @@ -5506,7 +5506,7 @@ "nav.statistics.header": "Statistika", // "nav.stop-impersonating": "Stop impersonating EPerson", - "nav.stop-impersonating": "Přestat simulovat jiného uživatele", + "nav.stop-impersonating": "Přestat vystupovat jako jiný uživatel", // "nav.subscriptions": "Subscriptions", "nav.subscriptions": "Nastavení notifikací", @@ -5810,7 +5810,7 @@ "orgunit.page.city": "Město", // "orgunit.page.country": "Country", - "orgunit.page.country": "Stát", + "orgunit.page.country": "Země", // "orgunit.page.dateestablished": "Date established", "orgunit.page.dateestablished": "Datum založení.", @@ -5892,7 +5892,7 @@ "person-relationships.search.results.head": "Výsledky vyhledávání osob", // "person.search.title": "Person Search", - "person.search.title": "Vyledatávání osob", + "person.search.title": "Vyhledávání osob", // "process.new.select-parameters": "Parameters", "process.new.select-parameters": "Parametry", @@ -6121,7 +6121,7 @@ "process.overview.delete.clear": "Zrušit výběr mazání", // "process.overview.delete.processing": "{{count}} process(es) are being deleted. Please wait for the deletion to fully complete. Note that this can take a while.", - "process.overview.delete.processing": "Smazává se {{count}} procesů. Počkejte prosím na úplné dokončení mazání. To může chvíli trvat.", + "process.overview.delete.processing": "Maže se {{count}} procesů. Počkejte prosím na úplné dokončení mazání. To může chvíli trvat.", // "process.overview.delete.body": "Are you sure you want to delete {{count}} process(es)?", "process.overview.delete.body": "Určitě chcete smazat {{count}} procesů", @@ -6512,7 +6512,7 @@ "register-page.registration.header": "Registrace nového uživatele", // "register-page.registration.info": "Register an account to subscribe to collections for email updates, and submit new items to DSpace.", - "register-page.registration.info": "Zaregistrujte si účet a přihlaste se k odběru sbírek pro e-mailové aktualizace a odesílejte nové záznamy do systému DSpace", + "register-page.registration.info": "Zaregistrujte si účet a přihlaste se k odběru kolekcí pro e-mailové aktualizace a odesílejte nové záznamy do systému DSpace", // "register-page.registration.email": "Email Address *", "register-page.registration.email": "E-mailová adresa *", @@ -6880,7 +6880,7 @@ "search.filters.applied.f.dateIssued.min": "Datum zahájení", // "search.filters.applied.f.dateSubmitted": "Date submitted", - "search.filters.applied.f.dateSubmitted": "Date vložení", + "search.filters.applied.f.dateSubmitted": "Datum vložení", // "search.filters.applied.f.discoverable": "Non-discoverable", // TODO Source message changed - Revise the translation @@ -6925,7 +6925,7 @@ "search.filters.applied.f.supervisedBy": "Zkontrolováno", // "search.filters.applied.f.withdrawn": "Withdrawn", - "search.filters.applied.f.withdrawn": "Zrušeno", + "search.filters.applied.f.withdrawn": "Vyřazeno", // "search.filters.applied.operator.equals": "", // TODO New key - Add a translation @@ -7142,10 +7142,10 @@ "search.filters.filter.objectpeople.label": "Hledat osoby", // "search.filters.filter.organizationAddressCountry.head": "Country", - "search.filters.filter.organizationAddressCountry.head": "Stát", + "search.filters.filter.organizationAddressCountry.head": "Země", // "search.filters.filter.organizationAddressCountry.placeholder": "Country", - "search.filters.filter.organizationAddressCountry.placeholder": "Stát", + "search.filters.filter.organizationAddressCountry.placeholder": "Země", // "search.filters.filter.organizationAddressCountry.label": "Search country", "search.filters.filter.organizationAddressCountry.label": "Hledat stát", @@ -7438,10 +7438,10 @@ "sorting.dc.date.issued.DESC": "Datum vydání sestupně", // "sorting.dc.date.accessioned.ASC": "Accessioned Date Ascending", - "sorting.dc.date.accessioned.ASC": "Datum přírůstku vzestupně", + "sorting.dc.date.accessioned.ASC": "Datum odeslání vzestupně", // "sorting.dc.date.accessioned.DESC": "Accessioned Date Descending", - "sorting.dc.date.accessioned.DESC": "Datum přírůstku sestupně", + "sorting.dc.date.accessioned.DESC": "Datum odeslání sestupně", // "sorting.lastModified.ASC": "Last modified Ascending", "sorting.lastModified.ASC": "Naposledy upraveno vzestupně", @@ -8284,7 +8284,7 @@ "submission.sections.identifiers.info": "Pro tento záznam budou vytvořeny následující identifikátory:", // "submission.sections.identifiers.no_handle": "No handles have been minted for this item.", - "submission.sections.identifiers.no_handle": "Tomuto záznamu nybyl přidělen žádný handle.", + "submission.sections.identifiers.no_handle": "Tomuto záznamu nebyl přidělen žádný handle.", // "submission.sections.identifiers.no_doi": "No DOIs have been minted for this item.", "submission.sections.identifiers.no_doi": "Tomuto záznamu nebylo přiděleno žádné DOI.", @@ -8446,10 +8446,10 @@ "submission.sections.upload.form.until-placeholder": "Až do", // "submission.sections.upload.header.policy.default.nolist": "Uploaded files in the {{collectionName}} collection will be accessible according to the following group(s):", - "submission.sections.upload.header.policy.default.nolist": "Nahrané soubory v kolekci {{collectionName}} budou přístupné podle následující skupiny (skupin):", + "submission.sections.upload.header.policy.default.nolist": "Nahrané soubory v kolekci {{collectionName}} budou přístupné podle následujících skupin:", // "submission.sections.upload.header.policy.default.withlist": "Please note that uploaded files in the {{collectionName}} collection will be accessible, in addition to what is explicitly decided for the single file, with the following group(s):", - "submission.sections.upload.header.policy.default.withlist": "Vezměte prosím na vědomí, že nahrané soubory v kolekci {{collectionName}} budou kromě toho, co je explicitně rozhodnuto pro jednotlivý soubor, přístupné podle následující skupiny (skupin):", + "submission.sections.upload.header.policy.default.withlist": "Vezměte prosím na vědomí, že nahrané soubory v kolekci {{collectionName}} budou kromě toho, co je explicitně rozhodnuto pro jednotlivý soubor, přístupné podle následujících skupin:", // "submission.sections.upload.info": "Here you will find all the files currently in the item. You can update the file metadata and access conditions or upload additional files by dragging & dropping them anywhere on the page.", "submission.sections.upload.info": "Zde najdete všechny soubory, které jsou aktuálně v záznamu. Můžete aktualizovat metadata souborů a podmínky přístupu nebo nahrát další soubory pouhým přetažením kdekoli na stránku.", @@ -8618,7 +8618,7 @@ "submission.sections.sherpa.record.information.uri": "URI", // "submission.sections.sherpa.error.message": "There was an error retrieving sherpa informations", - "submission.sections.sherpa.error.message": "Informace ze Sherpa Romeo se nepodařilo načíst.", + "submission.sections.sherpa.error.message": "Při načítání informací ze služby Sherpa došlo k chybě", // "submission.submit.breadcrumbs": "New submission", "submission.submit.breadcrumbs": "Nově podaný záznam", @@ -8676,7 +8676,7 @@ "submission.workflow.tasks.claimed.decline_help": "", // "submission.workflow.tasks.claimed.reject.reason.info": "Please enter your reason for rejecting the submission into the box below, indicating whether the submitter may fix a problem and resubmit.", - "submission.workflow.tasks.claimed.reject.reason.info": "Do níže uvedeného pole zadejte důvod odmítnutí podání a uveďte, zda může vkladatel problém odstranit a podání znovu odeslat.", + "submission.workflow.tasks.claimed.reject.reason.info": "Do níže uvedeného pole zadejte důvod odmítnutí podání a uveďte, zda může odesílatel problém odstranit a podání znovu odeslat.", // "submission.workflow.tasks.claimed.reject.reason.placeholder": "Describe the reason of reject", "submission.workflow.tasks.claimed.reject.reason.placeholder": "Popište důvod odmítnutí", @@ -8691,7 +8691,7 @@ "submission.workflow.tasks.claimed.reject.submit": "Odmítnout", // "submission.workflow.tasks.claimed.reject_help": "If you have reviewed the item and found it is not suitable for inclusion in the collection, select \"Reject\". You will then be asked to enter a message indicating why the item is unsuitable, and whether the submitter should change something and resubmit.", - "submission.workflow.tasks.claimed.reject_help": "Pokud jste záznam zkontrolovali a zjistili, že není vhodný pro zařazení do kolekce, vyberte \"Odmítnout\". Poté budete vyzváni k zadání zprávy, ve které uvedete, proč je záznam nevhodný, a zda by měl vkladatel něco změnit a znovu odeslat.", + "submission.workflow.tasks.claimed.reject_help": "Pokud jste záznam zkontrolovali a zjistili, že není vhodný pro zařazení do kolekce, vyberte \"Odmítnout\". Poté budete vyzváni k zadání zprávy, ve které uvedete, proč je záznam nevhodný, a zda by měl odesílatel něco změnit a znovu odeslat.", // "submission.workflow.tasks.claimed.return": "Return to pool", "submission.workflow.tasks.claimed.return": "Vrátit do fondu", @@ -8712,7 +8712,7 @@ "submission.workflow.tasks.generic.success": "Operace proběhla úspěšně", // "submission.workflow.tasks.pool.claim": "Claim", - "submission.workflow.tasks.pool.claim": "Vyžádat si", + "submission.workflow.tasks.pool.claim": "Převzít", // "submission.workflow.tasks.pool.claim_help": "Assign this task to yourself.", "submission.workflow.tasks.pool.claim_help": "Přiřaďte si tento úkol.", @@ -8909,7 +8909,7 @@ "uploader.or": ", nebo ", // "uploader.processing": "Processing uploaded file(s)... (it's now safe to close this page)", - "uploader.processing": "(nyní je bezpečné tuto stránku zavřít)", + "uploader.processing": "Zpracovávám nahrané soubory... (nyní je bezpečné tuto stránku zavřít)", // "uploader.queue-length": "Queue length", "uploader.queue-length": "Délka fronty", @@ -8933,7 +8933,7 @@ "workflowAdmin.search.results.head": "Spravovat workflow", // "workflow.search.results.head": "Workflow tasks", - "workflow.search.results.head": "Kroky workflow", + "workflow.search.results.head": "Úlohy workflow", // "supervision.search.results.head": "Workflow and Workspace tasks", "supervision.search.results.head": "Úlohy Workflow a osobního pracovního prostoru", @@ -8973,22 +8973,22 @@ "workflow-item.delete.button.confirm": "Smazat", // "workflow-item.send-back.notification.success.title": "Sent back to submitter", - "workflow-item.send-back.notification.success.title": "Odesláno zpět vkladateli", + "workflow-item.send-back.notification.success.title": "Vráceno zpět odesílateli", // "workflow-item.send-back.notification.success.content": "This workflow item was successfully sent back to the submitter", - "workflow-item.send-back.notification.success.content": "Tento záznam ve workflow byl úspěšně odeslán zpět vkladateli", + "workflow-item.send-back.notification.success.content": "Tento záznam ve workflow byl úspěšně vrácen zpět odesílateli", // "workflow-item.send-back.notification.error.title": "Something went wrong", "workflow-item.send-back.notification.error.title": "Něco se pokazilo", // "workflow-item.send-back.notification.error.content": "The workflow item could not be sent back to the submitter", - "workflow-item.send-back.notification.error.content": "Záznam ve workflow se nepodařilo odeslat zpět vkladateli", + "workflow-item.send-back.notification.error.content": "Záznam ve workflow se nepodařilo vrátit zpět odesílateli", // "workflow-item.send-back.title": "Send workflow item back to submitter", - "workflow-item.send-back.title": "Odeslat záznam ve workflow zpět vladateli", + "workflow-item.send-back.title": "Vrátit záznam ve workflow zpět odesílateli", // "workflow-item.send-back.header": "Send workflow item back to submitter", - "workflow-item.send-back.header": "Odeslat záznam ve workflow zpět vladateli", + "workflow-item.send-back.header": "Vrátit záznam ve workflow zpět odesílateli", // "workflow-item.send-back.button.cancel": "Cancel", "workflow-item.send-back.button.cancel": "Zrušit", @@ -9004,7 +9004,7 @@ "workspace-item.view.breadcrumbs": "Zobrazení pracovního prostoru", // "workspace-item.view.title": "Workspace View", - "workspace-item.view.title": "Zobrazení pracovní prostoru", + "workspace-item.view.title": "Zobrazení pracovního prostoru", // "workspace-item.delete.breadcrumbs": "Workspace Delete", "workspace-item.delete.breadcrumbs": "Vymazat obsah pracovního prostoru", @@ -9337,7 +9337,7 @@ "person.page.orcid.sync-queue.send.bad-request-error": "Odeslání do ORCID se nezdařilo, protože zdroj odeslaný do registru ORCID není platný", // "person.page.orcid.sync-queue.send.error": "The submission to ORCID failed", - "person.page.orcid.sync-queue.send.error": "Podání do ORCID se nezdařilo", + "person.page.orcid.sync-queue.send.error": "Odeslání do ORCID se nezdařilo", // "person.page.orcid.sync-queue.send.conflict-error": "The submission to ORCID failed because the resource is already present on the ORCID registry", "person.page.orcid.sync-queue.send.conflict-error": "Odeslání do ORCID se nezdařilo, protože zdroj se již nachází v registru ORCID", @@ -9490,7 +9490,7 @@ "system-wide-alert-form.retrieval.error": "Něco se pokazilo při načítání systémových upozornění", // "system-wide-alert.form.cancel": "Cancel", - "system-wide-alert.form.cancel": "Zrušir", + "system-wide-alert.form.cancel": "Zrušit", // "system-wide-alert.form.save": "Save", "system-wide-alert.form.save": "Uložit", @@ -9508,7 +9508,7 @@ "system-wide-alert.form.label.message": "Obsah upozornění", // "system-wide-alert.form.label.countdownTo.enable": "Enable a countdown timer", - "system-wide-alert.form.label.countdownTo.enable": "Povolit odpočítávací měřič", + "system-wide-alert.form.label.countdownTo.enable": "Povolit odpočet", // "system-wide-alert.form.label.countdownTo.hint": "Hint: Set a countdown timer. When enabled, a date can be set in the future and the system-wide alert banner will perform a countdown to the set date. When this timer ends, it will disappear from the alert. The server will NOT be automatically stopped.", "system-wide-alert.form.label.countdownTo.hint": "Nápověda: Nastavte odpočítávací měřič. Je-li to povoleno, lze datum nastavit i v budoucnosti a systémový upozornění bude uvádět odpočítávání do nastaveného data. Ve chvíli, kdy skončí odpočítávání, zmizí i odpočítávač z upozornění. Server NEBUDE automaticky zastaven.", @@ -9643,7 +9643,7 @@ // "vocabulary-treeview.search.form.add": "Add", // TODO New key - Add a translation - "vocabulary-treeview.search.form.add": "Add", + "vocabulary-treeview.search.form.add": "Přidat", // "admin.notifications.publicationclaim.breadcrumbs": "Publication Claim", // TODO New key - Add a translation From d586a8450d5d0207035d4703b7545d8caf1b5dfa Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Wed, 9 Oct 2024 16:14:04 +0200 Subject: [PATCH 203/720] Updated cs localization for subcommunities and subcollections (cherry picked from commit 9badd4a4b6261a2abf5ed838f16bb000b1a127f0) --- src/assets/i18n/cs.json5 | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/assets/i18n/cs.json5 b/src/assets/i18n/cs.json5 index 2cb95f10d3..09dd023e8d 100644 --- a/src/assets/i18n/cs.json5 +++ b/src/assets/i18n/cs.json5 @@ -2259,7 +2259,7 @@ "community.create.notifications.success": "Úspěšně vytvořena komunita", // "community.create.sub-head": "Create a Sub-Community for Community {{ parent }}", - "community.create.sub-head": "Vytvořit dílčí komunitu pro komunitu {{ parent }}", + "community.create.sub-head": "Vytvořit podkomunitu v komunitě {{ parent }}", // "community.curate.header": "Curate Community: {{community}}", "community.curate.header": "Spravovat komunitu: {{ community }}", @@ -2396,7 +2396,7 @@ "comcol-role.edit.collection-admin.name": "Správci", // "comcol-role.edit.community-admin.description": "Community administrators can create sub-communities or collections, and manage or assign management for those sub-communities or collections. In addition, they decide who can submit items to any sub-collections, edit item metadata (after submission), and add (map) existing items from other collections (subject to authorization).", - "comcol-role.edit.community-admin.description": "Administrátoři komunit mohou vytvářet dílčí komunity nebo kolekce a spravovat nebo přidělovat správu těmto dílčím komunitám nebo kolekcím. Kromě toho rozhodují o tom, kdo může odesílat záznamy do všech dílčích kolekcí, upravovat metadata záznamů (po odeslání) a přidávat (mapovat) existující záznamy z jiných kolekcí (na základě oprávnění).", + "comcol-role.edit.community-admin.description": "Administrátoři komunit mohou vytvářet podkomunity nebo kolekce a spravovat nebo přidělovat správu těmto podkomunitám nebo kolekcím. Kromě toho rozhodují o tom, kdo může odesílat záznamy do všech podkolekcí, upravovat metadata záznamů (po odeslání) a přidávat (mapovat) existující záznamy z jiných kolekcí (na základě oprávnění).", // "comcol-role.edit.collection-admin.description": "Collection administrators decide who can submit items to the collection, edit item metadata (after submission), and add (map) existing items from other collections to this collection (subject to authorization for that collection).", "comcol-role.edit.collection-admin.description": "Správci kolekcí rozhodují o tom, kdo může do kolekce vkládat záznamy, upravovat metadata záznamů (po vložení) a přidávat (mapovat) existující záznamy z jiných kolekcí do této kolekce (podléhá autorizaci pro danou kolekci).", @@ -2421,7 +2421,7 @@ // "comcol-role.edit.bitstream_read.description": "E-People and Groups that can read new bitstreams submitted to this collection. Changes to this role are not retroactive. Existing bitstreams in the system will still be viewable by those who had read access at the time of their addition.", // TODO Source message changed - Revise the translation - "comcol-role.edit.bitstream_read.description": "Správci komunit mohou vytvářet dílčí komunity nebo kolekce a spravovat nebo přidělovat správu těmto dílčím komunitám nebo kolekcím. Kromě toho rozhodují o tom, kdo může odesílat záznamy do libovolných dílčích kolekcí, upravovat metadata záznamů (po odeslání) a přidávat (mapovat) existující záznamy z jiných kolekcí (na základě oprávnění).", + "comcol-role.edit.bitstream_read.description": "Správci komunit mohou vytvářet podkomunity nebo kolekce a spravovat nebo přidělovat správu těmto podkomunitám nebo kolekcím. Kromě toho rozhodují o tom, kdo může odesílat záznamy do libovolných podkolekcí, upravovat metadata záznamů (po odeslání) a přidávat (mapovat) existující záznamy z jiných kolekcí (na základě oprávnění).", // "comcol-role.edit.bitstream_read.anonymous-group": "Default read for incoming bitstreams is currently set to Anonymous.", "comcol-role.edit.bitstream_read.anonymous-group": "Výchozí čtení pro příchozí soubory je v současné době nastaveno na hodnotu Anonymní.", @@ -2481,7 +2481,7 @@ "community.page.news": "Novinky", // "community.all-lists.head": "Subcommunities and Collections", - "community.all-lists.head": "Dílčí komunity a kolekce", + "community.all-lists.head": "Podkomunity a kolekce", // "community.search.results.head": "Search Results", // TODO New key - Add a translation @@ -2927,10 +2927,10 @@ "error.invalid-search-query": "Vyhledávací dotaz není platný. Další informace o této chybě naleznete v osvědčených postupech Solr query syntax.", // "error.sub-collections": "Error fetching sub-collections", - "error.sub-collections": "Chyba při načítání dílčích kolekcí", + "error.sub-collections": "Chyba při načítání podkolekcí", // "error.sub-communities": "Error fetching sub-communities", - "error.sub-communities": "Chyba při načítání dílčích komunit", + "error.sub-communities": "Chyba při načítání podkomunit", // "error.submission.sections.init-form-error": "An error occurred during section initialize, please check your input-form configuration. Details are below :

    ", "error.submission.sections.init-form-error": "Při inicializaci sekce došlo k chybě, zkontrolujte prosím konfiguraci vstupního formuláře. Podrobnosti jsou uvedeny níže :

    ", @@ -4985,10 +4985,10 @@ "loading.search-results": "Načítají se výsledky vyhledávání...", // "loading.sub-collections": "Loading sub-collections...", - "loading.sub-collections": "Načítají se dílčí kolekce...", + "loading.sub-collections": "Načítají se podkolekce...", // "loading.sub-communities": "Loading sub-communities...", - "loading.sub-communities": "Načítají se dílčí komunity...", + "loading.sub-communities": "Načítají se podkomunity...", // "loading.top-level-communities": "Loading top-level communities...", "loading.top-level-communities": "Načítají se komunity nejvyšší úrovně...", From 68b6cc9bd45e01a306990c05b464e94cacb1a4f2 Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Fri, 11 Oct 2024 13:37:24 +0200 Subject: [PATCH 204/720] Fixed small cs localization mistakes (cherry picked from commit 680d6c94166266d6195f13b92e3be916917ae8c0) --- src/assets/i18n/cs.json5 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/assets/i18n/cs.json5 b/src/assets/i18n/cs.json5 index 09dd023e8d..466e3475bf 100644 --- a/src/assets/i18n/cs.json5 +++ b/src/assets/i18n/cs.json5 @@ -2939,7 +2939,7 @@ "error.top-level-communities": "Chyba při načítání komunit nejvyšší úrovně", // "error.validation.license.notgranted": "You must grant this license to complete your submission. If you are unable to grant this license at this time you may save your work and return later or remove the submission.", - "error.validation.license.notgranted": "Bez udělení licence nelze záznam dokončit. Pokud v tuto chvíli nemůžete licenci udělit, uložte svou práci a vraťte se k příspěveku později nebo jej smažte.", + "error.validation.license.notgranted": "Bez udělení licence nelze záznam dokončit. Pokud v tuto chvíli nemůžete licenci udělit, uložte svou práci a vraťte se k příspěvku později nebo jej smažte.", // "error.validation.pattern": "This input is restricted by the current pattern: {{ pattern }}.", "error.validation.pattern": "Toto zadání je omezeno aktuálním vzorcem: {{ pattern }}.", @@ -4934,7 +4934,7 @@ "iiif.page.doi": "Trvalý odkaz: ", // "iiif.page.issue": "Issue: ", - "iiif.page.issue": "Číslo:", + "iiif.page.issue": "Číslo: ", // "iiif.page.description": "Description: ", "iiif.page.description": "Popis: ", From b11efbbf0cde5cde73f3873a41539bec7588cf9c Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Mon, 4 Nov 2024 14:37:04 +0100 Subject: [PATCH 205/720] Updated messages for the 'supervised' and 'claim' sentenses (cherry picked from commit 1aef6ce1d623ac8f0a52dcf8086847996a2d0180) --- src/assets/i18n/cs.json5 | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/assets/i18n/cs.json5 b/src/assets/i18n/cs.json5 index 466e3475bf..f06db668a3 100644 --- a/src/assets/i18n/cs.json5 +++ b/src/assets/i18n/cs.json5 @@ -4221,10 +4221,10 @@ "workflow-item.search.result.notification.deleted.failure": "Při mazání příkazu supervize \"{{name}}\" došlo k chybě", // "workflow-item.search.result.list.element.supervised-by": "Supervised by:", - "workflow-item.search.result.list.element.supervised-by": "Supervizován:", + "workflow-item.search.result.list.element.supervised-by": "Pod dohledem:", // "workflow-item.search.result.list.element.supervised.remove-tooltip": "Remove supervision group", - "workflow-item.search.result.list.element.supervised.remove-tooltip": "Odebrat skupinu supervize", + "workflow-item.search.result.list.element.supervised.remove-tooltip": "Odebrat skupinu dohledu", // "confidence.indicator.help-text.accepted": "This authority value has been confirmed as accurate by an interactive user", // TODO New key - Add a translation @@ -4380,10 +4380,10 @@ "item.page.version.hasDraft": "Nová verze nemůže být vytvořena, protože v historii verzí už další rozpracovaná verze existuje.", // "item.page.claim.button": "Claim", - "item.page.claim.button": "Prohlásit", + "item.page.claim.button": "Převzít", // "item.page.claim.tooltip": "Claim this item as profile", - "item.page.claim.tooltip": "Prohlásit tento záznam za profilovou", + "item.page.claim.tooltip": "Prohlásit tento záznam za profil", // "item.page.image.alt.ROR": "ROR logo", // TODO New key - Add a translation @@ -5414,7 +5414,7 @@ "mydspace.show.workspace": "Vaše záznamy", // "mydspace.show.supervisedWorkspace": "Supervised items", - "mydspace.show.supervisedWorkspace": "Zkontrolované záznamy", + "mydspace.show.supervisedWorkspace": "Záznamy pod dohledem", // "mydspace.status.mydspaceArchived": "Archived", "mydspace.status.mydspaceArchived": "Archivováno", @@ -6922,7 +6922,7 @@ "search.filters.applied.f.birthDate.min": "Datum narození od", // "search.filters.applied.f.supervisedBy": "Supervised by", - "search.filters.applied.f.supervisedBy": "Zkontrolováno", + "search.filters.applied.f.supervisedBy": "Pod dohledem", // "search.filters.applied.f.withdrawn": "Withdrawn", "search.filters.applied.f.withdrawn": "Vyřazeno", @@ -7221,8 +7221,7 @@ "search.filters.filter.supervisedBy.placeholder": "Supervised By", // "search.filters.filter.supervisedBy.label": "Search Supervised By", - // TODO New key - Add a translation - "search.filters.filter.supervisedBy.label": "Search Supervised By", + "search.filters.filter.supervisedBy.label": "Hledat pod dohledem", // "search.filters.entityType.JournalIssue": "Journal Issue", "search.filters.entityType.JournalIssue": "Číslo časopisu", @@ -8924,7 +8923,7 @@ "virtual-metadata.delete-relationship.modal-head": "Vyberte záznamy, pro které chcete uložit virtuální metadata jako skutečná metadata", // "supervisedWorkspace.search.results.head": "Supervised Items", - "supervisedWorkspace.search.results.head": "Zkontrolované záznamy", + "supervisedWorkspace.search.results.head": "Záznamy pod dohledem", // "workspace.search.results.head": "Your submissions", "workspace.search.results.head": "Moje záznamy", From bd638f03560015a5bdc31108227810ca1fdba540 Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Fri, 8 Nov 2024 15:24:06 +0100 Subject: [PATCH 206/720] Updated supervised by messages following NTK suggestions (cherry picked from commit d819cf43968665c20870ee16a1620e744dfbd821) --- src/assets/i18n/cs.json5 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/assets/i18n/cs.json5 b/src/assets/i18n/cs.json5 index f06db668a3..866660f513 100644 --- a/src/assets/i18n/cs.json5 +++ b/src/assets/i18n/cs.json5 @@ -4221,10 +4221,10 @@ "workflow-item.search.result.notification.deleted.failure": "Při mazání příkazu supervize \"{{name}}\" došlo k chybě", // "workflow-item.search.result.list.element.supervised-by": "Supervised by:", - "workflow-item.search.result.list.element.supervised-by": "Pod dohledem:", + "workflow-item.search.result.list.element.supervised-by": "Dohlížející autorita:", // "workflow-item.search.result.list.element.supervised.remove-tooltip": "Remove supervision group", - "workflow-item.search.result.list.element.supervised.remove-tooltip": "Odebrat skupinu dohledu", + "workflow-item.search.result.list.element.supervised.remove-tooltip": "Odebrat dohlížející autoritu", // "confidence.indicator.help-text.accepted": "This authority value has been confirmed as accurate by an interactive user", // TODO New key - Add a translation @@ -7221,7 +7221,7 @@ "search.filters.filter.supervisedBy.placeholder": "Supervised By", // "search.filters.filter.supervisedBy.label": "Search Supervised By", - "search.filters.filter.supervisedBy.label": "Hledat pod dohledem", + "search.filters.filter.supervisedBy.label": "Hledat dohlížející autoritu", // "search.filters.entityType.JournalIssue": "Journal Issue", "search.filters.entityType.JournalIssue": "Číslo časopisu", From 66dedc5b2e08de7c3691fb21a7238d1ac08be15c Mon Sep 17 00:00:00 2001 From: Alisa Ismailati Date: Fri, 19 Jul 2024 14:54:39 +0200 Subject: [PATCH 207/720] [CST-15591] Fixed headings by their rank --- .../create-collection-page.component.html | 2 +- .../create-community-page.component.html | 4 ++-- .../workspaceitem/workspaceitem-actions.component.html | 2 +- .../item-list-preview/item-list-preview.component.html | 2 +- .../scope-selector-modal.component.html | 6 +++--- .../search/search-results/search-results.component.html | 2 +- .../sections/upload/section-upload.component.html | 2 +- .../workspaceitems-delete-page.component.html | 4 ++-- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/app/collection-page/create-collection-page/create-collection-page.component.html b/src/app/collection-page/create-collection-page/create-collection-page.component.html index f3f9785692..4a09d25aed 100644 --- a/src/app/collection-page/create-collection-page/create-collection-page.component.html +++ b/src/app/collection-page/create-collection-page/create-collection-page.component.html @@ -1,7 +1,7 @@
    -

    {{'collection.create.sub-head' | translate:{ parent: dsoNameService.getName((parentRD$| async)?.payload) } }}

    +

    {{'collection.create.sub-head' | translate:{ parent: dsoNameService.getName((parentRD$| async)?.payload) } }}

    - -

    {{ 'community.create.sub-head' | translate:{ parent: dsoNameService.getName(parent) } }}

    +

    {{ 'community.create.head' | translate }}

    +

    {{ 'community.create.sub-head' | translate:{ parent: dsoNameService.getName(parent) } }}

    diff --git a/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.html b/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.html index 6e958c7a8b..26066df4b5 100644 --- a/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.html +++ b/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.html @@ -30,7 +30,7 @@ -

    +

    diff --git a/src/app/shared/search-form/scope-selector-modal/scope-selector-modal.component.html b/src/app/shared/search-form/scope-selector-modal/scope-selector-modal.component.html index 013274320b..74c71b1d12 100644 --- a/src/app/shared/search-form/scope-selector-modal/scope-selector-modal.component.html +++ b/src/app/shared/search-form/scope-selector-modal/scope-selector-modal.component.html @@ -6,14 +6,14 @@
    -
    {{'dso-selector.' + action + '.' + objectType.toString().toLowerCase() + '.input-header' | translate}}
    +

    {{'dso-selector.' + action + '.' + objectType.toString().toLowerCase() + '.input-header' | translate}}

    diff --git a/src/app/shared/search/search-results/search-results.component.html b/src/app/shared/search/search-results/search-results.component.html index 11c589254a..1f1e58ea10 100644 --- a/src/app/shared/search/search-results/search-results.component.html +++ b/src/app/shared/search/search-results/search-results.component.html @@ -1,5 +1,5 @@
    -

    {{ (configuration ? configuration + '.search.results.head' : 'search.results.head') | translate }}

    +

    {{ (configuration ? configuration + '.search.results.head' : 'search.results.head') | translate }}

    diff --git a/src/app/submission/sections/upload/section-upload.component.html b/src/app/submission/sections/upload/section-upload.component.html index b57b454288..3c4fbe49f6 100644 --- a/src/app/submission/sections/upload/section-upload.component.html +++ b/src/app/submission/sections/upload/section-upload.component.html @@ -5,7 +5,7 @@
    -

    {{'submission.sections.upload.no-file-uploaded' | translate}}

    +
    {{'submission.sections.upload.no-file-uploaded' | translate}}
    diff --git a/src/app/workspaceitems-edit-page/workspaceitems-delete-page/workspaceitems-delete-page.component.html b/src/app/workspaceitems-edit-page/workspaceitems-delete-page/workspaceitems-delete-page.component.html index a0f0a1711e..9d9bece197 100644 --- a/src/app/workspaceitems-edit-page/workspaceitems-delete-page/workspaceitems-delete-page.component.html +++ b/src/app/workspaceitems-edit-page/workspaceitems-delete-page/workspaceitems-delete-page.component.html @@ -1,5 +1,5 @@
    -

    {{ 'workspace-item.delete.header' | translate }}

    +

    {{ 'workspace-item.delete.header' | translate }}

    @@ -7,7 +7,7 @@

    {{ 'workspace-item.delete.header' | translate }}