From 8f708d0e2898b0bf0eef9ce0d50f07e6f4119d88 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Tue, 22 Oct 2024 11:37:47 +0200 Subject: [PATCH 01/41] 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 00000000000..805d0d5a0b1 --- /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 6604936cde1..9b0ec277352 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 02/41] 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 13d84e6e2e1..d3534706e88 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 00000000000..6550c6a2880 --- /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 00000000000..97e3ddb321f --- /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 a18de2c6111..5210bd7062a 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 4c497461e71..45079ba4980 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 ccc4af0a7dd..d01ded1af09 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 6c91bae4c16..1316c8d3cf6 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 03/41] 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 72940c1a0eb..7a7b99fa6e7 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 04/41] 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 97ae09c1a67..20bf7175f6f 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 05/41] 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 00000000000..b619a337de9 --- /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 858ef883134..b14fa7abaf2 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 08b9585a8c7..22d0671d9c7 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 06/41] 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 00000000000..d6f61840575 --- /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 7f3d040042f..d150ac69f4d 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 07/41] 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 00000000000..f6d3252a384 --- /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 08/41] 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 58eb6ff33d2..7b882958f24 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 00000000000..1852579c3d3 --- /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 805d0d5a0b1..4089fd03b17 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 aa3033ecec3..6cbcf782af9 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 1c0f88cf477..c7aac9a2d78 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 498799a454b..77094ada805 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 09/41] 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 44783da84e8..d8394ac5d4c 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 1316c8d3cf6..fc749377a98 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 10/41] 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 d8394ac5d4c..d2809a04b64 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 fc749377a98..dc69f0fbe86 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 fe90d39943f62d025b0e3ab64d57bcc017ed46b2 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Mon, 18 Nov 2024 11:36:10 +0100 Subject: [PATCH 11/41] 119602: Add placeholder in form --- .../accessibility/accessibility-settings.service.ts | 11 +++++++++++ .../accessibility-settings.component.html | 1 + .../accessibility-settings.component.ts | 4 ++++ 3 files changed, 16 insertions(+) diff --git a/src/app/accessibility/accessibility-settings.service.ts b/src/app/accessibility/accessibility-settings.service.ts index 4089fd03b17..77b6c91bea8 100644 --- a/src/app/accessibility/accessibility-settings.service.ts +++ b/src/app/accessibility/accessibility-settings.service.ts @@ -224,4 +224,15 @@ export class AccessibilitySettingsService { } } + getPlaceholder(setting: AccessibilitySetting): string { + switch (setting) { + case AccessibilitySetting.NotificationTimeOut: + return environment.notifications.timeOut.toString(); + case AccessibilitySetting.LiveRegionTimeOut: + return environment.liveRegion.messageTimeOutDurationMs.toString(); + default: + return ''; + } + } + } diff --git a/src/app/info/accessibility-settings/accessibility-settings.component.html b/src/app/info/accessibility-settings/accessibility-settings.component.html index 6550c6a2880..8a76917462c 100644 --- a/src/app/info/accessibility-settings/accessibility-settings.component.html +++ b/src/app/info/accessibility-settings/accessibility-settings.component.html @@ -9,6 +9,7 @@

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

    diff --git a/src/app/info/accessibility-settings/accessibility-settings.component.ts b/src/app/info/accessibility-settings/accessibility-settings.component.ts index 97e3ddb321f..ed1d481be61 100644 --- a/src/app/info/accessibility-settings/accessibility-settings.component.ts +++ b/src/app/info/accessibility-settings/accessibility-settings.component.ts @@ -38,6 +38,10 @@ export class AccessibilitySettingsComponent implements OnInit { return this.settingsService.getInputType(setting); } + getPlaceholder(setting: AccessibilitySetting): string { + return this.settingsService.getPlaceholder(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)); From ecb00a95a07424bafa1976e871086585c3374b7a Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Mon, 18 Nov 2024 13:50:46 +0100 Subject: [PATCH 12/41] 119602: Implement converting of accessibility settings values --- .../accessibility-settings.service.ts | 105 +++++++++++++++++- .../accessibility-settings.component.ts | 22 +++- 2 files changed, 122 insertions(+), 5 deletions(-) diff --git a/src/app/accessibility/accessibility-settings.service.ts b/src/app/accessibility/accessibility-settings.service.ts index 77b6c91bea8..bdf1d296797 100644 --- a/src/app/accessibility/accessibility-settings.service.ts +++ b/src/app/accessibility/accessibility-settings.service.ts @@ -30,7 +30,7 @@ export enum AccessibilitySetting { LiveRegionTimeOut = 'liveRegionTimeOut', } -export type AccessibilitySettings = { [key in AccessibilitySetting]?: any }; +export type AccessibilitySettings = { [key in AccessibilitySetting]?: string }; /** * Service handling the retrieval and configuration of accessibility settings. @@ -235,4 +235,107 @@ export class AccessibilitySettingsService { } } + /** + * Returns the converter method for the provided setting. + * The converter methods are used to convert the value entered by the user in the form to the format that is used + * to store the setting value. + */ + getFormValueToStoredValueConverterMethod(setting: AccessibilitySetting): (value: string) => string { + switch (setting) { + case AccessibilitySetting.NotificationTimeOut: + return secondsToMilliseconds; + case AccessibilitySetting.LiveRegionTimeOut: + return secondsToMilliseconds; + default: + return null; + } + } + + /** + * Convert the user-configured value to the format used to store the setting value. + * Returns the provided value without conversion if no converter is configured for the provided setting. + */ + convertFormValueToStoredValue(setting: AccessibilitySetting, value: string): string { + const converterMethod = this.getFormValueToStoredValueConverterMethod(setting); + + if (hasValue(converterMethod)) { + return converterMethod(value); + } else { + return value; + } + } + + /** + * Convert all values in the provided accessibility settings object to values ready to be stored. + */ + convertAllFormValuesToStoredValues(settings: AccessibilitySettings): AccessibilitySettings { + const convertedSettings = {}; + + this.getAllAccessibilitySettingKeys().filter(setting => setting in settings).forEach(setting => + convertedSettings[setting] = this.convertFormValueToStoredValue(setting, settings[setting]) + ); + + return convertedSettings; + } + + /** + * Returns the converter method for the provided setting. + * The converter methods are used to convert the value as it is stored to the format visible by the user in the form. + */ + getStoredValueToFormValueConverterMethod(setting: AccessibilitySetting): (value: string) => string { + switch (setting) { + case AccessibilitySetting.NotificationTimeOut: + return millisecondsToSeconds; + case AccessibilitySetting.LiveRegionTimeOut: + return millisecondsToSeconds; + default: + return null; + } + } + + /** + * Convert the stored value to the format used in the form. + * Returns the provided value without conversion if no converter is configured for the provided setting. + */ + convertStoredValueToFormValue(setting: AccessibilitySetting, value: string): string { + const converterMethod = this.getStoredValueToFormValueConverterMethod(setting); + + if (hasValue(converterMethod)) { + return converterMethod(value); + } else { + return value; + } + } + + /** + * Convert all values in the provided accessibility settings object to values ready to show in the form. + */ + convertAllStoredValuesToFormValues(settings: AccessibilitySettings): AccessibilitySettings { + const convertedSettings = {}; + + this.getAllAccessibilitySettingKeys().filter(setting => setting in settings).forEach(setting => + convertedSettings[setting] = this.convertStoredValueToFormValue(setting, settings[setting]) + ); + + return convertedSettings; + } + +} + +function secondsToMilliseconds(secondsStr: string): string { + const seconds = parseFloat(secondsStr); + if (isNaN(seconds)) { + return null; + } else { + return (seconds * 1000).toString(); + } +} + +function millisecondsToSeconds(millisecondsStr: string): string { + const milliseconds = parseFloat(millisecondsStr); + if (isNaN(milliseconds)) { + return null; + } else { + return (milliseconds / 1000).toString(); + } } diff --git a/src/app/info/accessibility-settings/accessibility-settings.component.ts b/src/app/info/accessibility-settings/accessibility-settings.component.ts index ed1d481be61..6d325e8f0cf 100644 --- a/src/app/info/accessibility-settings/accessibility-settings.component.ts +++ b/src/app/info/accessibility-settings/accessibility-settings.component.ts @@ -29,9 +29,7 @@ export class AccessibilitySettingsComponent implements OnInit { ngOnInit() { this.accessibilitySettingsOptions = this.settingsService.getAllAccessibilitySettingKeys(); - this.settingsService.getAll().pipe(take(1)).subscribe(currentSettings => { - this.formValues = currentSettings; - }); + this.updateFormValues(); } getInputType(setting: AccessibilitySetting): string { @@ -42,10 +40,26 @@ export class AccessibilitySettingsComponent implements OnInit { return this.settingsService.getPlaceholder(setting); } + /** + * Saves the user-configured settings + */ saveSettings() { - this.settingsService.setSettings(this.formValues).pipe(take(1)).subscribe(location => { + const formValues = this.formValues; + const convertedValues = this.settingsService.convertAllFormValuesToStoredValues(formValues); + this.settingsService.setSettings(convertedValues).pipe(take(1)).subscribe(location => { this.notificationsService.success(null, this.translateService.instant('info.accessibility-settings.save-notification.' + location)); }); + + this.updateFormValues(); + } + + /** + * Updates the form values with the currently stored accessibility settings + */ + updateFormValues() { + this.settingsService.getAll().pipe(take(1)).subscribe(storedSettings => { + this.formValues = this.settingsService.convertAllStoredValuesToFormValues(storedSettings); + }); } } From fdfa6e2c06042a22b6055760818406ea6b83ff9f Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Mon, 18 Nov 2024 14:26:48 +0100 Subject: [PATCH 13/41] 119602: Update values after converters implementation --- src/app/accessibility/accessibility-settings.service.ts | 4 ++-- src/assets/i18n/en.json5 | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/accessibility/accessibility-settings.service.ts b/src/app/accessibility/accessibility-settings.service.ts index bdf1d296797..8434614434f 100644 --- a/src/app/accessibility/accessibility-settings.service.ts +++ b/src/app/accessibility/accessibility-settings.service.ts @@ -227,9 +227,9 @@ export class AccessibilitySettingsService { getPlaceholder(setting: AccessibilitySetting): string { switch (setting) { case AccessibilitySetting.NotificationTimeOut: - return environment.notifications.timeOut.toString(); + return millisecondsToSeconds(environment.notifications.timeOut.toString()); case AccessibilitySetting.LiveRegionTimeOut: - return environment.liveRegion.messageTimeOutDurationMs.toString(); + return millisecondsToSeconds(environment.liveRegion.messageTimeOutDurationMs.toString()); default: return ''; } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index dc69f0fbe86..6cf15ed8448 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1846,11 +1846,11 @@ "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.liveRegionTimeOut.hint": "The duration in seconds 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.notificationTimeOut.hint": "The duration in seconds after which a notification disappears. Set to 0 for notifications to remain indefinitely.", "info.accessibility-settings.save-notification.cookie": "Successfully saved settings locally.", From dc8a699e94be3069b1eed53a3f9927d2aa151bec Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Fri, 22 Nov 2024 11:55:05 +0100 Subject: [PATCH 14/41] 119602: Rework convertion & form format --- .../accessibility-settings.service.ts | 108 +++--------------- .../accessibility-settings.component.html | 35 ++++-- .../accessibility-settings.component.ts | 16 +-- 3 files changed, 49 insertions(+), 110 deletions(-) diff --git a/src/app/accessibility/accessibility-settings.service.ts b/src/app/accessibility/accessibility-settings.service.ts index 8434614434f..48e2c8f5de9 100644 --- a/src/app/accessibility/accessibility-settings.service.ts +++ b/src/app/accessibility/accessibility-settings.service.ts @@ -32,6 +32,11 @@ export enum AccessibilitySetting { export type AccessibilitySettings = { [key in AccessibilitySetting]?: string }; +export interface AccessibilitySettingsFormValues { + notificationTimeOut: string, + liveRegionTimeOut: string, +} + /** * Service handling the retrieval and configuration of accessibility settings. * @@ -210,20 +215,6 @@ export class AccessibilitySettingsService { } } - /** - * 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'; - } - } - getPlaceholder(setting: AccessibilitySetting): string { switch (setting) { case AccessibilitySetting.NotificationTimeOut: @@ -236,88 +227,23 @@ export class AccessibilitySettingsService { } /** - * Returns the converter method for the provided setting. - * The converter methods are used to convert the value entered by the user in the form to the format that is used - * to store the setting value. + * Convert values in the provided accessibility settings object to values ready to be stored. */ - getFormValueToStoredValueConverterMethod(setting: AccessibilitySetting): (value: string) => string { - switch (setting) { - case AccessibilitySetting.NotificationTimeOut: - return secondsToMilliseconds; - case AccessibilitySetting.LiveRegionTimeOut: - return secondsToMilliseconds; - default: - return null; - } - } - - /** - * Convert the user-configured value to the format used to store the setting value. - * Returns the provided value without conversion if no converter is configured for the provided setting. - */ - convertFormValueToStoredValue(setting: AccessibilitySetting, value: string): string { - const converterMethod = this.getFormValueToStoredValueConverterMethod(setting); - - if (hasValue(converterMethod)) { - return converterMethod(value); - } else { - return value; - } + convertFormValuesToStoredValues(settings: AccessibilitySettingsFormValues): AccessibilitySettings { + return { + 'notificationTimeOut': secondsToMilliseconds(settings.notificationTimeOut), + 'liveRegionTimeOut': secondsToMilliseconds(settings.liveRegionTimeOut), + }; } /** - * Convert all values in the provided accessibility settings object to values ready to be stored. + * Convert values in the provided accessibility settings object to values ready to show in the form. */ - convertAllFormValuesToStoredValues(settings: AccessibilitySettings): AccessibilitySettings { - const convertedSettings = {}; - - this.getAllAccessibilitySettingKeys().filter(setting => setting in settings).forEach(setting => - convertedSettings[setting] = this.convertFormValueToStoredValue(setting, settings[setting]) - ); - - return convertedSettings; - } - - /** - * Returns the converter method for the provided setting. - * The converter methods are used to convert the value as it is stored to the format visible by the user in the form. - */ - getStoredValueToFormValueConverterMethod(setting: AccessibilitySetting): (value: string) => string { - switch (setting) { - case AccessibilitySetting.NotificationTimeOut: - return millisecondsToSeconds; - case AccessibilitySetting.LiveRegionTimeOut: - return millisecondsToSeconds; - default: - return null; - } - } - - /** - * Convert the stored value to the format used in the form. - * Returns the provided value without conversion if no converter is configured for the provided setting. - */ - convertStoredValueToFormValue(setting: AccessibilitySetting, value: string): string { - const converterMethod = this.getStoredValueToFormValueConverterMethod(setting); - - if (hasValue(converterMethod)) { - return converterMethod(value); - } else { - return value; - } - } - - /** - * Convert all values in the provided accessibility settings object to values ready to show in the form. - */ - convertAllStoredValuesToFormValues(settings: AccessibilitySettings): AccessibilitySettings { - const convertedSettings = {}; - - this.getAllAccessibilitySettingKeys().filter(setting => setting in settings).forEach(setting => - convertedSettings[setting] = this.convertStoredValueToFormValue(setting, settings[setting]) - ); - - return convertedSettings; + convertStoredValuesToFormValues(settings: AccessibilitySettings): AccessibilitySettingsFormValues { + return { + notificationTimeOut: millisecondsToSeconds(settings.notificationTimeOut), + liveRegionTimeOut: millisecondsToSeconds(settings.liveRegionTimeOut), + }; } } diff --git a/src/app/info/accessibility-settings/accessibility-settings.component.html b/src/app/info/accessibility-settings/accessibility-settings.component.html index 8a76917462c..947bff495a1 100644 --- a/src/app/info/accessibility-settings/accessibility-settings.component.html +++ b/src/app/info/accessibility-settings/accessibility-settings.component.html @@ -2,19 +2,36 @@

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

    -
    -
    diff --git a/src/app/info/accessibility-settings/accessibility-settings.component.ts b/src/app/info/accessibility-settings/accessibility-settings.component.ts index ea7c853a972..0de18152eef 100644 --- a/src/app/info/accessibility-settings/accessibility-settings.component.ts +++ b/src/app/info/accessibility-settings/accessibility-settings.component.ts @@ -57,4 +57,14 @@ export class AccessibilitySettingsComponent implements OnInit { }); } + /** + * Resets accessibility settings + */ + resetSettings() { + this.settingsService.clearSettings().pipe(take(1)).subscribe(() => { + this.notificationsService.success(null, this.translateService.instant('info.accessibility-settings.reset-notification')); + this.updateFormValues(); + }); + } + } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index d9533da9c7f..69127375cf4 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1860,6 +1860,10 @@ "info.accessibility-settings.save-notification.metadata": "Successfully saved settings on the user profile.", + "info.accessibility-settings.reset-notification": "Successfully reset settings.", + + "info.accessibility-settings.reset": "Reset accessibility settings", + "info.accessibility-settings.submit": "Save accessibility settings", "info.accessibility-settings.title": "Accessibility settings", From 287d0283313beae07b7e8d0fcd47b71c349730c1 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Fri, 22 Nov 2024 16:03:52 +0100 Subject: [PATCH 17/41] 119602: Make input boxes smaller --- .../accessibility-settings.component.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/info/accessibility-settings/accessibility-settings.component.html b/src/app/info/accessibility-settings/accessibility-settings.component.html index 43371d33366..076cfdf9648 100644 --- a/src/app/info/accessibility-settings/accessibility-settings.component.html +++ b/src/app/info/accessibility-settings/accessibility-settings.component.html @@ -7,7 +7,7 @@

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

    {{ 'info.accessibility-settings.disableNotificationTimeOut.label' | translate }} -
    +
    {{ 'info.accessibility-settings.title' | translate }} {{ 'info.accessibility-settings.notificationTimeOut.label' | translate }} -
    +
    {{ 'info.accessibility-settings.title' | translate }} {{ 'info.accessibility-settings.liveRegionTimeOut.label' | translate }} -
    +
    Date: Fri, 22 Nov 2024 16:09:12 +0100 Subject: [PATCH 18/41] 119602: Add unit after input fields --- .../accessibility-settings.component.html | 8 ++++++++ src/assets/i18n/en.json5 | 8 ++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/app/info/accessibility-settings/accessibility-settings.component.html b/src/app/info/accessibility-settings/accessibility-settings.component.html index 076cfdf9648..d3c03f556b6 100644 --- a/src/app/info/accessibility-settings/accessibility-settings.component.html +++ b/src/app/info/accessibility-settings/accessibility-settings.component.html @@ -35,6 +35,10 @@

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

    {{ 'info.accessibility-settings.notificationTimeOut.hint' | translate }}
    + +
    + {{ 'info.accessibility-settings.notificationTimeOut.unit' | translate }} +
    @@ -52,6 +56,10 @@

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

    {{ 'info.accessibility-settings.liveRegionTimeOut.hint' | translate }}
    + +
    + {{ 'info.accessibility-settings.liveRegionTimeOut.unit' | translate }} +
    diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 69127375cf4..54286d60744 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1850,11 +1850,15 @@ "info.accessibility-settings.liveRegionTimeOut.label": "Live region time-out", - "info.accessibility-settings.liveRegionTimeOut.hint": "The duration in seconds after which a message in the live region disappears.", + "info.accessibility-settings.liveRegionTimeOut.hint": "The duration after which a message in the live region disappears.", + + "info.accessibility-settings.liveRegionTimeOut.unit": "Seconds", "info.accessibility-settings.notificationTimeOut.label": "Notification time-out", - "info.accessibility-settings.notificationTimeOut.hint": "The duration in seconds after which a notification disappears.", + "info.accessibility-settings.notificationTimeOut.hint": "The duration after which a notification disappears.", + + "info.accessibility-settings.notificationTimeOut.unit": "Seconds", "info.accessibility-settings.save-notification.cookie": "Successfully saved settings locally.", From 5a28e66b2f6b6c8335821eb5526f7f347810115d Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Mon, 25 Nov 2024 09:44:27 +0100 Subject: [PATCH 19/41] 119602: Move hints to contextHelp --- .../accessibility-settings.component.html | 40 ++++++++++++------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/src/app/info/accessibility-settings/accessibility-settings.component.html b/src/app/info/accessibility-settings/accessibility-settings.component.html index d3c03f556b6..d247b12f949 100644 --- a/src/app/info/accessibility-settings/accessibility-settings.component.html +++ b/src/app/info/accessibility-settings/accessibility-settings.component.html @@ -7,15 +7,19 @@

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

    {{ 'info.accessibility-settings.disableNotificationTimeOut.label' | translate }} -
    +
    +
    - - {{ 'info.accessibility-settings.disableNotificationTimeOut.hint' | translate }} - +
    @@ -30,15 +34,19 @@

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

    [readOnly]="formValues.disableNotificationTimeOut" [(ngModel)]="formValues.notificationTimeOut" [ngModelOptions]="{ standalone: true }" [attr.aria-describedby]="'notificationTimeOutHint'"> - - - {{ 'info.accessibility-settings.notificationTimeOut.hint' | translate }} -
    -
    +
    {{ 'info.accessibility-settings.notificationTimeOut.unit' | translate }}
    + +
    +
    @@ -51,15 +59,19 @@

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

    [placeholder]="getPlaceholder(AccessibilitySetting.LiveRegionTimeOut)" [(ngModel)]="formValues.liveRegionTimeOut" [ngModelOptions]="{ standalone: true }" [attr.aria-describedby]="'liveRegionTimeOutHint'"> - - - {{ 'info.accessibility-settings.liveRegionTimeOut.hint' | translate }} -
    -
    +
    {{ 'info.accessibility-settings.liveRegionTimeOut.unit' | translate }}
    + +
    +
    From ced163a25f40dabd446c9141f10aa6e69b1460a8 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Mon, 25 Nov 2024 09:49:09 +0100 Subject: [PATCH 20/41] 119602: Remove obsolete tests --- .../accessibility-settings.service.spec.ts | 8 -------- .../accessibility-settings.component.spec.ts | 13 +------------ 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/src/app/accessibility/accessibility-settings.service.spec.ts b/src/app/accessibility/accessibility-settings.service.spec.ts index d6f61840575..5344c31d452 100644 --- a/src/app/accessibility/accessibility-settings.service.spec.ts +++ b/src/app/accessibility/accessibility-settings.service.spec.ts @@ -368,12 +368,4 @@ describe('accessibilitySettingsService', () => { }); }); - 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/info/accessibility-settings/accessibility-settings.component.spec.ts b/src/app/info/accessibility-settings/accessibility-settings.component.spec.ts index f6d3252a384..3ba0e1ab967 100644 --- a/src/app/info/accessibility-settings/accessibility-settings.component.spec.ts +++ b/src/app/info/accessibility-settings/accessibility-settings.component.spec.ts @@ -4,7 +4,7 @@ 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 { AccessibilitySettingsService } 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'; @@ -47,22 +47,11 @@ describe('AccessibilitySettingsComponent', () => { }); 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')); From ec016e80fb931f8f9ea25a64e0cbeab8d8c4dc16 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Mon, 25 Nov 2024 10:00:04 +0100 Subject: [PATCH 21/41] 119602: Update AccessibilitySettingsService stub --- .../accessibility/accessibility-settings.service.stub.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/app/accessibility/accessibility-settings.service.stub.ts b/src/app/accessibility/accessibility-settings.service.stub.ts index b619a337de9..4ac965059e6 100644 --- a/src/app/accessibility/accessibility-settings.service.stub.ts +++ b/src/app/accessibility/accessibility-settings.service.stub.ts @@ -31,4 +31,10 @@ export class AccessibilitySettingsServiceStub { setSettingsInCookie = jasmine.createSpy('setSettingsInCookie'); getInputType = jasmine.createSpy('getInputType').and.returnValue('text'); + + convertFormValuesToStoredValues = jasmine.createSpy('convertFormValuesToStoredValues').and.returnValue({}); + + convertStoredValuesToFormValues = jasmine.createSpy('convertStoredValuesToFormValues').and.returnValue({}); + + getPlaceholder = jasmine.createSpy('getPlaceholder').and.returnValue('placeholder'); } From deb4a63c88dce3b9dd906911cfc2cd83a6ffc8e5 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Mon, 25 Nov 2024 10:12:39 +0100 Subject: [PATCH 22/41] 119602: Add additional accessibilitySettingsComponent tests --- .../accessibility-settings.component.spec.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/app/info/accessibility-settings/accessibility-settings.component.spec.ts b/src/app/info/accessibility-settings/accessibility-settings.component.spec.ts index 3ba0e1ab967..bcd4ff94c86 100644 --- a/src/app/info/accessibility-settings/accessibility-settings.component.spec.ts +++ b/src/app/info/accessibility-settings/accessibility-settings.component.spec.ts @@ -50,6 +50,10 @@ describe('AccessibilitySettingsComponent', () => { it('should retrieve the current settings', () => { expect(settingsService.getAll).toHaveBeenCalled(); }); + + it('should convert retrieved settings to form format', () => { + expect(settingsService.convertStoredValuesToFormValues).toHaveBeenCalled(); + }); }); describe('saveSettings', () => { @@ -59,6 +63,12 @@ describe('AccessibilitySettingsComponent', () => { expect(settingsService.setSettings).toHaveBeenCalled(); }); + it('should convert form settings to stored format', () => { + settingsService.setSettings = jasmine.createSpy('setSettings').and.returnValue(of('cookie')); + component.saveSettings(); + expect(settingsService.convertFormValuesToStoredValues).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(); From c38352ed22072921469b7c8b7f00ef6bc18a0019 Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Mon, 25 Nov 2024 10:29:16 +0100 Subject: [PATCH 23/41] 119602: Update doc comments --- .../accessibility-settings.service.ts | 14 ++++++++++++-- .../accessibility-settings.component.ts | 3 +++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/app/accessibility/accessibility-settings.service.ts b/src/app/accessibility/accessibility-settings.service.ts index e86742d76de..f2151675391 100644 --- a/src/app/accessibility/accessibility-settings.service.ts +++ b/src/app/accessibility/accessibility-settings.service.ts @@ -23,16 +23,22 @@ export const ACCESSIBILITY_SETTINGS_METADATA_KEY = 'dspace.accessibility.setting /** * 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. + * When adding new settings, make sure to add the new setting to the accessibility-settings component. + * The converter methods to convert from stored format to form format (and vice-versa) need to be updated as well. */ export enum AccessibilitySetting { NotificationTimeOut = 'notificationTimeOut', LiveRegionTimeOut = 'liveRegionTimeOut', } +/** + * Type representing an object that contains accessibility settings values. + */ export type AccessibilitySettings = { [key in AccessibilitySetting]?: string }; +/** + * The accessibility settings object format used by the accessibility-settings component form. + */ export interface AccessibilitySettingsFormValues { disableNotificationTimeOut: boolean, notificationTimeOut: string, @@ -226,6 +232,10 @@ export class AccessibilitySettingsService { return this.setSettingsInAuthenticatedUserMetadata({}); } + /** + * Retrieve the placeholder to be used for the provided AccessibilitySetting. + * Returns an empty string when no placeholder is specified for the provided setting. + */ getPlaceholder(setting: AccessibilitySetting): string { switch (setting) { case AccessibilitySetting.NotificationTimeOut: diff --git a/src/app/info/accessibility-settings/accessibility-settings.component.ts b/src/app/info/accessibility-settings/accessibility-settings.component.ts index 0de18152eef..cd417393d44 100644 --- a/src/app/info/accessibility-settings/accessibility-settings.component.ts +++ b/src/app/info/accessibility-settings/accessibility-settings.component.ts @@ -9,6 +9,9 @@ import { take } from 'rxjs'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; +/** + * Component providing the form where users can update accessibility settings. + */ @Component({ selector: 'ds-accessibility-settings', templateUrl: './accessibility-settings.component.html' From cdec4880d2e62c32b7fc40173b936124762163ed Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Fri, 29 Nov 2024 10:44:44 +0100 Subject: [PATCH 24/41] 119602: Compare notifications by id to correctly update store --- .../notifications-board/notifications-board.component.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 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 20bf7175f6f..8439a1af16d 100644 --- a/src/app/shared/notifications/notifications-board/notifications-board.component.ts +++ b/src/app/shared/notifications/notifications-board/notifications-board.component.ts @@ -10,7 +10,6 @@ import { import { select, Store } from '@ngrx/store'; import { BehaviorSubject, Subscription, take } from 'rxjs'; -import difference from 'lodash/difference'; import { NotificationsService } from '../notifications.service'; import { AppState } from '../../../app.reducer'; @@ -23,6 +22,7 @@ import { AccessibilitySetting } from '../../../accessibility/accessibility-settings.service'; import cloneDeep from 'lodash/cloneDeep'; +import differenceWith from 'lodash/differenceWith'; @Component({ selector: 'ds-notifications-board', @@ -69,13 +69,13 @@ export class NotificationsBoardComponent implements OnInit, OnDestroy { this.notifications = []; } else if (state.length > this.notifications.length) { // Add - const newElem = difference(state, this.notifications); + const newElem = differenceWith(state, this.notifications, this.byId); newElem.forEach((notification) => { this.add(notification); }); } else { // Remove - const delElem = difference(this.notifications, state); + const delElem = differenceWith(this.notifications, state, this.byId); delElem.forEach((notification) => { this.notifications = this.notifications.filter((item: INotification) => item.id !== notification.id); @@ -85,6 +85,9 @@ export class NotificationsBoardComponent implements OnInit, OnDestroy { }); } + private byId = (notificationA: INotification, notificationB: INotification) => + notificationA.id === notificationB.id; + // Add the new notification to the notification array add(item: INotification): void { const toBlock: boolean = this.block(item); From 010b2f9693da1e6905951118f35aefc2a7f6a3fa Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Wed, 11 Dec 2024 09:35:01 +0100 Subject: [PATCH 25/41] 119602: Improve types & docs --- .../accessibility-settings.service.spec.ts | 36 ++++++----------- .../accessibility-settings.service.ts | 40 +++++++++++-------- .../accessibility-settings.component.html | 4 +- .../accessibility-settings.component.ts | 3 -- .../shared/live-region/live-region.service.ts | 4 +- .../notifications-board.component.ts | 5 +-- 6 files changed, 41 insertions(+), 51 deletions(-) diff --git a/src/app/accessibility/accessibility-settings.service.spec.ts b/src/app/accessibility/accessibility-settings.service.spec.ts index 5344c31d452..3d58c02d109 100644 --- a/src/app/accessibility/accessibility-settings.service.spec.ts +++ b/src/app/accessibility/accessibility-settings.service.spec.ts @@ -1,6 +1,5 @@ import { AccessibilitySettingsService, - AccessibilitySetting, AccessibilitySettings, ACCESSIBILITY_SETTINGS_METADATA_KEY, ACCESSIBILITY_COOKIE @@ -41,17 +40,6 @@ describe('accessibilitySettingsService', () => { ); }); - 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 = { @@ -60,7 +48,7 @@ describe('accessibilitySettingsService', () => { service.getAll = jasmine.createSpy('getAll').and.returnValue(of(settings)); - service.get(AccessibilitySetting.NotificationTimeOut, 'default').subscribe(value => + service.get('notificationTimeOut', 'default').subscribe(value => expect(value).toEqual('1000') ); }); @@ -72,7 +60,7 @@ describe('accessibilitySettingsService', () => { service.getAll = jasmine.createSpy('getAll').and.returnValue(of(settings)); - service.get(AccessibilitySetting.LiveRegionTimeOut, 'default').subscribe(value => + service.get('liveRegionTimeOut', 'default').subscribe(value => expect(value).toEqual('default') ); }); @@ -82,7 +70,7 @@ describe('accessibilitySettingsService', () => { 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 => + service.getAsNumber('notificationTimeOut').subscribe(value => expect(value).toEqual(1000) ); }); @@ -90,7 +78,7 @@ describe('accessibilitySettingsService', () => { 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 => + service.getAsNumber('notificationTimeOut', 123).subscribe(value => expect(value).toEqual(123) ); }); @@ -98,7 +86,7 @@ describe('accessibilitySettingsService', () => { 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 => + service.getAsNumber('notificationTimeOut', 123).subscribe(value => expect(value).toEqual(123) ); }); @@ -176,7 +164,7 @@ describe('accessibilitySettingsService', () => { it('should correctly update the chosen setting', () => { service.updateSettings = jasmine.createSpy('updateSettings'); - service.set(AccessibilitySetting.LiveRegionTimeOut, '500'); + service.set('liveRegionTimeOut', '500'); expect(service.updateSettings).toHaveBeenCalledWith({ liveRegionTimeOut: '500' }); }); }); @@ -307,7 +295,7 @@ describe('accessibilitySettingsService', () => { }); it('should set the settings in metadata', () => { - service.setSettingsInMetadata(ePerson, { [AccessibilitySetting.LiveRegionTimeOut]: '500' }).subscribe(); + service.setSettingsInMetadata(ePerson, { ['liveRegionTimeOut']: '500' }).subscribe(); expect(ePerson.setMetadata).toHaveBeenCalled(); }); @@ -318,19 +306,19 @@ describe('accessibilitySettingsService', () => { }); it('should create a patch with the metadata changes', () => { - service.setSettingsInMetadata(ePerson, { [AccessibilitySetting.LiveRegionTimeOut]: '500' }).subscribe(); + service.setSettingsInMetadata(ePerson, { ['liveRegionTimeOut']: '500' }).subscribe(); expect(ePersonService.createPatchFromCache).toHaveBeenCalled(); }); it('should send the patch request', () => { - service.setSettingsInMetadata(ePerson, { [AccessibilitySetting.LiveRegionTimeOut]: '500' }).subscribe(); + service.setSettingsInMetadata(ePerson, { ['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' }) + service.setSettingsInMetadata(ePerson, { ['liveRegionTimeOut']: '500' }) .subscribe(value => { expect(value).toBeTrue(); }); @@ -341,7 +329,7 @@ describe('accessibilitySettingsService', () => { it('should emit false when the update failed', fakeAsync(() => { ePersonService.patch = jasmine.createSpy().and.returnValue(createFailedRemoteDataObject$()); - service.setSettingsInMetadata(ePerson, { [AccessibilitySetting.LiveRegionTimeOut]: '500' }) + service.setSettingsInMetadata(ePerson, { ['liveRegionTimeOut']: '500' }) .subscribe(value => { expect(value).toBeFalse(); }); @@ -357,7 +345,7 @@ describe('accessibilitySettingsService', () => { }); it('should store the settings in a cookie', () => { - service.setSettingsInCookie({ [AccessibilitySetting.LiveRegionTimeOut]: '500' }); + service.setSettingsInCookie({ ['liveRegionTimeOut']: '500' }); expect(cookieService.set).toHaveBeenCalled(); }); diff --git a/src/app/accessibility/accessibility-settings.service.ts b/src/app/accessibility/accessibility-settings.service.ts index f2151675391..e9e41954c7c 100644 --- a/src/app/accessibility/accessibility-settings.service.ts +++ b/src/app/accessibility/accessibility-settings.service.ts @@ -22,19 +22,21 @@ export const ACCESSIBILITY_COOKIE = 'dsAccessibilityCookie'; export const ACCESSIBILITY_SETTINGS_METADATA_KEY = 'dspace.accessibility.settings'; /** - * Enum containing all possible accessibility settings. - * When adding new settings, make sure to add the new setting to the accessibility-settings component. + * Type containing all possible accessibility settings. + * When adding new settings, make sure to add the new setting to the accessibility-settings component form. * The converter methods to convert from stored format to form format (and vice-versa) need to be updated as well. */ -export enum AccessibilitySetting { - NotificationTimeOut = 'notificationTimeOut', - LiveRegionTimeOut = 'liveRegionTimeOut', -} +export type AccessibilitySetting = 'notificationTimeOut' | 'liveRegionTimeOut'; /** - * Type representing an object that contains accessibility settings values. + * Type representing an object that contains accessibility settings values for all accessibility settings. */ -export type AccessibilitySettings = { [key in AccessibilitySetting]?: string }; +export type FullAccessibilitySettings = { [key in AccessibilitySetting]: string }; + +/** + * Type representing an object that contains accessibility settings values for some accessibility settings. + */ +export type AccessibilitySettings = Partial; /** * The accessibility settings object format used by the accessibility-settings component form. @@ -63,10 +65,6 @@ export class AccessibilitySettingsService { ) { } - 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. @@ -238,9 +236,9 @@ export class AccessibilitySettingsService { */ getPlaceholder(setting: AccessibilitySetting): string { switch (setting) { - case AccessibilitySetting.NotificationTimeOut: + case 'notificationTimeOut': return millisecondsToSeconds(environment.notifications.timeOut.toString()); - case AccessibilitySetting.LiveRegionTimeOut: + case 'liveRegionTimeOut': return millisecondsToSeconds(environment.liveRegion.messageTimeOutDurationMs.toString()); default: return ''; @@ -250,11 +248,11 @@ export class AccessibilitySettingsService { /** * Convert values in the provided accessibility settings object to values ready to be stored. */ - convertFormValuesToStoredValues(settings: AccessibilitySettingsFormValues): AccessibilitySettings { + convertFormValuesToStoredValues(settings: AccessibilitySettingsFormValues): FullAccessibilitySettings { return { - 'notificationTimeOut': settings.disableNotificationTimeOut ? '0' + notificationTimeOut: settings.disableNotificationTimeOut ? '0' : secondsToMilliseconds(settings.notificationTimeOut), - 'liveRegionTimeOut': secondsToMilliseconds(settings.liveRegionTimeOut), + liveRegionTimeOut: secondsToMilliseconds(settings.liveRegionTimeOut), }; } @@ -271,6 +269,10 @@ export class AccessibilitySettingsService { } +/** + * Converts a string representing seconds to a string representing milliseconds + * Returns null if the input could not be parsed to a float + */ function secondsToMilliseconds(secondsStr: string): string { const seconds = parseFloat(secondsStr); if (isNaN(seconds)) { @@ -280,6 +282,10 @@ function secondsToMilliseconds(secondsStr: string): string { } } +/** + * Converts a string representing milliseconds to a string representing seconds + * Returns null if the input could not be parsed to a float + */ function millisecondsToSeconds(millisecondsStr: string): string { const milliseconds = parseFloat(millisecondsStr); if (isNaN(milliseconds)) { diff --git a/src/app/info/accessibility-settings/accessibility-settings.component.html b/src/app/info/accessibility-settings/accessibility-settings.component.html index d247b12f949..5a5493f0bea 100644 --- a/src/app/info/accessibility-settings/accessibility-settings.component.html +++ b/src/app/info/accessibility-settings/accessibility-settings.component.html @@ -30,7 +30,7 @@

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

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

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

    diff --git a/src/app/info/accessibility-settings/accessibility-settings.component.ts b/src/app/info/accessibility-settings/accessibility-settings.component.ts index cd417393d44..d537c363c3f 100644 --- a/src/app/info/accessibility-settings/accessibility-settings.component.ts +++ b/src/app/info/accessibility-settings/accessibility-settings.component.ts @@ -18,9 +18,6 @@ import { TranslateService } from '@ngx-translate/core'; }) export class AccessibilitySettingsComponent implements OnInit { - // Re-export for use in template - protected readonly AccessibilitySetting = AccessibilitySetting; - protected formValues: AccessibilitySettingsFormValues; constructor( diff --git a/src/app/shared/live-region/live-region.service.ts b/src/app/shared/live-region/live-region.service.ts index 7a7b99fa6e7..37f58a2795d 100644 --- a/src/app/shared/live-region/live-region.service.ts +++ b/src/app/shared/live-region/live-region.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; 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'; +import { AccessibilitySettingsService } from '../../accessibility/accessibility-settings.service'; export const MIN_MESSAGE_DURATION = 200; @@ -130,7 +130,7 @@ export class LiveRegionService { */ getConfiguredMessageTimeOutMs(): Observable { return this.accessibilitySettingsService.getAsNumber( - AccessibilitySetting.LiveRegionTimeOut, + 'liveRegionTimeOut', this.getMessageTimeOutMs(), ).pipe(map(timeOut => Math.max(timeOut, MIN_MESSAGE_DURATION))); } 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 8439a1af16d..96319416268 100644 --- a/src/app/shared/notifications/notifications-board/notifications-board.component.ts +++ b/src/app/shared/notifications/notifications-board/notifications-board.component.ts @@ -18,8 +18,7 @@ import { INotification } from '../models/notification.model'; import { NotificationsState } from '../notifications.reducers'; import { INotificationBoardOptions } from '../../../../config/notifications-config.interfaces'; import { - AccessibilitySettingsService, - AccessibilitySetting + AccessibilitySettingsService } from '../../../accessibility/accessibility-settings.service'; import cloneDeep from 'lodash/cloneDeep'; import differenceWith from 'lodash/differenceWith'; @@ -98,7 +97,7 @@ export class NotificationsBoardComponent implements OnInit, OnDestroy { // 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) + this.accessibilitySettingsService.getAsNumber('notificationTimeOut', item.options.timeOut) .pipe(take(1)).subscribe(timeOut => { if (timeOut < 0) { timeOut = 0; From c71c6667e03cae24b718803363e71b91e83b0c8c Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Wed, 11 Dec 2024 10:10:16 +0100 Subject: [PATCH 26/41] 119602: Improve notification hiding toggle useability --- src/app/accessibility/accessibility-settings.service.ts | 8 ++++---- .../accessibility-settings.component.html | 4 ++-- src/assets/i18n/en.json5 | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/app/accessibility/accessibility-settings.service.ts b/src/app/accessibility/accessibility-settings.service.ts index e9e41954c7c..813bccd4706 100644 --- a/src/app/accessibility/accessibility-settings.service.ts +++ b/src/app/accessibility/accessibility-settings.service.ts @@ -42,7 +42,7 @@ export type AccessibilitySettings = Partial; * The accessibility settings object format used by the accessibility-settings component form. */ export interface AccessibilitySettingsFormValues { - disableNotificationTimeOut: boolean, + notificationTimeOutEnabled: boolean, notificationTimeOut: string, liveRegionTimeOut: string, } @@ -250,8 +250,8 @@ export class AccessibilitySettingsService { */ convertFormValuesToStoredValues(settings: AccessibilitySettingsFormValues): FullAccessibilitySettings { return { - notificationTimeOut: settings.disableNotificationTimeOut ? '0' - : secondsToMilliseconds(settings.notificationTimeOut), + notificationTimeOut: settings.notificationTimeOutEnabled ? + secondsToMilliseconds(settings.notificationTimeOut) : '0', liveRegionTimeOut: secondsToMilliseconds(settings.liveRegionTimeOut), }; } @@ -261,7 +261,7 @@ export class AccessibilitySettingsService { */ convertStoredValuesToFormValues(settings: AccessibilitySettings): AccessibilitySettingsFormValues { return { - disableNotificationTimeOut: parseFloat(settings.notificationTimeOut) === 0, + notificationTimeOutEnabled: parseFloat(settings.notificationTimeOut) !== 0, notificationTimeOut: millisecondsToSeconds(settings.notificationTimeOut), liveRegionTimeOut: millisecondsToSeconds(settings.liveRegionTimeOut), }; diff --git a/src/app/info/accessibility-settings/accessibility-settings.component.html b/src/app/info/accessibility-settings/accessibility-settings.component.html index 5a5493f0bea..cacfae61fce 100644 --- a/src/app/info/accessibility-settings/accessibility-settings.component.html +++ b/src/app/info/accessibility-settings/accessibility-settings.component.html @@ -9,7 +9,7 @@

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

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

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

    diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 54286d60744..94126bbc168 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1844,7 +1844,7 @@ "info.accessibility-settings.breadcrumbs": "Accessibility settings", - "info.accessibility-settings.disableNotificationTimeOut.label": "Disable automatic notification hiding", + "info.accessibility-settings.disableNotificationTimeOut.label": "Hide notifications automatically", "info.accessibility-settings.disableNotificationTimeOut.hint": "When this toggle is activated, notifications will remain until manually closed.", From b55a3186763e863033e567043530782c1bea71da Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Wed, 22 Jan 2025 15:47:13 +0100 Subject: [PATCH 27/41] 119602: Reset notificationTimeOut when left on 0 --- .../accessibility-settings.service.spec.ts | 25 ++++++++++++++++++- .../accessibility-settings.service.ts | 11 +++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/app/accessibility/accessibility-settings.service.spec.ts b/src/app/accessibility/accessibility-settings.service.spec.ts index 3d58c02d109..4c720634944 100644 --- a/src/app/accessibility/accessibility-settings.service.spec.ts +++ b/src/app/accessibility/accessibility-settings.service.spec.ts @@ -2,7 +2,7 @@ import { AccessibilitySettingsService, AccessibilitySettings, ACCESSIBILITY_SETTINGS_METADATA_KEY, - ACCESSIBILITY_COOKIE + ACCESSIBILITY_COOKIE, AccessibilitySettingsFormValues, FullAccessibilitySettings } from './accessibility-settings.service'; import { CookieService } from '../core/services/cookie.service'; import { AuthService } from '../core/auth/auth.service'; @@ -356,4 +356,27 @@ describe('accessibilitySettingsService', () => { }); }); + describe('convertFormValuesToStoredValues', () => { + it('should reset the notificationTimeOut when timeOut is enabled but set to "0"', () => { + const formValues: AccessibilitySettingsFormValues = { + notificationTimeOutEnabled: true, + notificationTimeOut: '0', + liveRegionTimeOut: null, + }; + + const storedValues: FullAccessibilitySettings = service.convertFormValuesToStoredValues(formValues); + expect('notificationTimeOut' in storedValues).toBeFalse(); + }); + }); + + it('should keep the notificationTimeOut when timeOut is enabled and differs from "0"', () => { + const formValues: AccessibilitySettingsFormValues = { + notificationTimeOutEnabled: true, + notificationTimeOut: '3', + liveRegionTimeOut: null, + }; + + const storedValues: FullAccessibilitySettings = service.convertFormValuesToStoredValues(formValues); + expect('notificationTimeOut' in storedValues).toBeTrue(); + }); }); diff --git a/src/app/accessibility/accessibility-settings.service.ts b/src/app/accessibility/accessibility-settings.service.ts index 813bccd4706..fc8876619d3 100644 --- a/src/app/accessibility/accessibility-settings.service.ts +++ b/src/app/accessibility/accessibility-settings.service.ts @@ -249,11 +249,20 @@ export class AccessibilitySettingsService { * Convert values in the provided accessibility settings object to values ready to be stored. */ convertFormValuesToStoredValues(settings: AccessibilitySettingsFormValues): FullAccessibilitySettings { - return { + const storedValues = { notificationTimeOut: settings.notificationTimeOutEnabled ? secondsToMilliseconds(settings.notificationTimeOut) : '0', liveRegionTimeOut: secondsToMilliseconds(settings.liveRegionTimeOut), }; + + // When the user enables the timeout but does not change the timeout duration from 0, + // it is removed from the values to be stored so the default value is used. + // Keeping it at 0 would mean the notifications are not automatically removed. + if (settings.notificationTimeOutEnabled && settings.notificationTimeOut === '0') { + delete storedValues.notificationTimeOut; + } + + return storedValues; } /** From 59cb19ae80ecad76dec655df950f9ac8fffc3e7e Mon Sep 17 00:00:00 2001 From: Andreas Awouters Date: Fri, 7 Feb 2025 14:54:24 +0100 Subject: [PATCH 28/41] 119602: Restructure labels --- .../accessibility-settings.component.html | 14 ++++---------- src/assets/i18n/en.json5 | 14 +++++--------- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/src/app/info/accessibility-settings/accessibility-settings.component.html b/src/app/info/accessibility-settings/accessibility-settings.component.html index cacfae61fce..b0d14f99b78 100644 --- a/src/app/info/accessibility-settings/accessibility-settings.component.html +++ b/src/app/info/accessibility-settings/accessibility-settings.component.html @@ -3,11 +3,11 @@

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

    -