diff --git a/config/config.example.yml b/config/config.example.yml index 6c3c900f6ab..ebe1b2ff9b6 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -516,3 +516,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.spec.ts b/src/app/accessibility/accessibility-settings.service.spec.ts new file mode 100644 index 00000000000..26cf488450e --- /dev/null +++ b/src/app/accessibility/accessibility-settings.service.spec.ts @@ -0,0 +1,386 @@ +import { + fakeAsync, + flush, +} from '@angular/core/testing'; +import { of } from 'rxjs'; + +import { AuthService } from '../core/auth/auth.service'; +import { EPersonDataService } from '../core/eperson/eperson-data.service'; +import { EPerson } from '../core/eperson/models/eperson.model'; +import { CookieService } from '../core/services/cookie.service'; +import { CookieServiceMock } from '../shared/mocks/cookie.service.mock'; +import { + createFailedRemoteDataObject$, + createSuccessfulRemoteDataObject$, +} from '../shared/remote-data.utils'; +import { AuthServiceStub } from '../shared/testing/auth-service.stub'; +import { + ACCESSIBILITY_COOKIE, + ACCESSIBILITY_SETTINGS_METADATA_KEY, + AccessibilitySetting, + AccessibilitySettings, + AccessibilitySettingsService, +} from './accessibility-settings.service'; + + +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/accessibility/accessibility-settings.service.stub.ts b/src/app/accessibility/accessibility-settings.service.stub.ts new file mode 100644 index 00000000000..b3fc40d1b3b --- /dev/null +++ b/src/app/accessibility/accessibility-settings.service.stub.ts @@ -0,0 +1,35 @@ +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/accessibility/accessibility-settings.service.ts b/src/app/accessibility/accessibility-settings.service.ts new file mode 100644 index 00000000000..6ff19be9bb0 --- /dev/null +++ b/src/app/accessibility/accessibility-settings.service.ts @@ -0,0 +1,239 @@ +import { Injectable } from '@angular/core'; +import cloneDeep from 'lodash/cloneDeep'; +import { + Observable, + of, + switchMap, +} from 'rxjs'; +import { + map, + take, +} from 'rxjs/operators'; + +import { environment } from '../../environments/environment'; +import { AuthService } from '../core/auth/auth.service'; +import { EPersonDataService } from '../core/eperson/eperson-data.service'; +import { EPerson } from '../core/eperson/models/eperson.model'; +import { CookieService } from '../core/services/cookie.service'; +import { getFirstCompletedRemoteData } from '../core/shared/operators'; +import { + hasValue, + isNotEmpty, + isNotEmptyOperator, +} from '../shared/empty.util'; + +/** + * 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'; + +/** + * 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: environment.accessibility.cookieExpirationDuration }); + } 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 cd773b68cfa..1e32fc308c3 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -60,6 +60,7 @@ import { import { getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData, + getFirstSucceededRemoteDataPayload, } from '../shared/operators'; import { PageInfo } from '../shared/page-info.model'; import { @@ -261,6 +262,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. */ diff --git a/src/app/footer/footer.component.html b/src/app/footer/footer.component.html index 4841674852b..f715945e3c2 100644 --- a/src/app/footer/footer.component.html +++ b/src/app/footer/footer.component.html @@ -80,6 +80,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.spec.ts b/src/app/info/accessibility-settings/accessibility-settings.component.spec.ts new file mode 100644 index 00000000000..10147960d6c --- /dev/null +++ b/src/app/info/accessibility-settings/accessibility-settings.component.spec.ts @@ -0,0 +1,86 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { of } from 'rxjs'; + +import { + AccessibilitySetting, + AccessibilitySettingsService, +} from '../../accessibility/accessibility-settings.service'; +import { getAccessibilitySettingsServiceStub } from '../../accessibility/accessibility-settings.service.stub'; +import { AuthService } from '../../core/auth/auth.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { AuthServiceStub } from '../../shared/testing/auth-service.stub'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; +import { AccessibilitySettingsComponent } from './accessibility-settings.component'; + + +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()], + 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(); + }); + }); +}); 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..dca94c67c1d --- /dev/null +++ b/src/app/info/accessibility-settings/accessibility-settings.component.ts @@ -0,0 +1,62 @@ +import { CommonModule } from '@angular/common'; +import { + Component, + OnInit, +} from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; +import { take } from 'rxjs'; + +import { + AccessibilitySetting, + AccessibilitySettings, + AccessibilitySettingsService, +} from '../../accessibility/accessibility-settings.service'; +import { AuthService } from '../../core/auth/auth.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; + +@Component({ + selector: 'ds-accessibility-settings', + templateUrl: './accessibility-settings.component.html', + imports: [ + CommonModule, + TranslateModule, + FormsModule, + ], + standalone: true, +}) +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-routes.ts b/src/app/info/info-routes.ts index 8d70ac740fc..4b7decb49c5 100644 --- a/src/app/info/info-routes.ts +++ b/src/app/info/info-routes.ts @@ -8,9 +8,11 @@ import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso import { notifyInfoGuard } from '../core/coar-notify/notify-info/notify-info.guard'; import { feedbackGuard } from '../core/feedback/feedback.guard'; import { hasValue } from '../shared/empty.util'; +import { AccessibilitySettingsComponent } from './accessibility-settings/accessibility-settings.component'; import { ThemedEndUserAgreementComponent } from './end-user-agreement/themed-end-user-agreement.component'; import { ThemedFeedbackComponent } from './feedback/themed-feedback.component'; import { + ACCESSIBILITY_SETTINGS_PATH, COAR_NOTIFY_SUPPORT, END_USER_AGREEMENT_PATH, FEEDBACK_PATH, @@ -28,6 +30,12 @@ export const ROUTES: Routes = [ 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' }, + }, environment.info.enableEndUserAgreement ? { path: END_USER_AGREEMENT_PATH, component: ThemedEndUserAgreementComponent, diff --git a/src/app/info/info-routing-paths.ts b/src/app/info/info-routing-paths.ts index cd42dd9c1d4..6aa4bfbc721 100644 --- a/src/app/info/info-routing-paths.ts +++ b/src/app/info/info-routing-paths.ts @@ -4,6 +4,7 @@ export const END_USER_AGREEMENT_PATH = 'end-user-agreement'; export const PRIVACY_PATH = 'privacy'; export const FEEDBACK_PATH = 'feedback'; export const COAR_NOTIFY_SUPPORT = 'coar-notify-support'; +export const ACCESSIBILITY_SETTINGS_PATH = 'accessibility'; export function getEndUserAgreementPath() { return getSubPath(END_USER_AGREEMENT_PATH); @@ -21,6 +22,10 @@ export function getCOARNotifySupportPath(): string { return getSubPath(COAR_NOTIFY_SUPPORT); } +export function getAccessibilitySettingsPath() { + return getSubPath(ACCESSIBILITY_SETTINGS_PATH); +} + function getSubPath(path: string) { return `${getInfoModulePath()}/${path}`; } diff --git a/src/app/profile-page/profile-page.component.html b/src/app/profile-page/profile-page.component.html index b5703c47a31..6b0dd8e6d60 100644 --- a/src/app/profile-page/profile-page.component.html +++ b/src/app/profile-page/profile-page.component.html @@ -29,10 +29,18 @@

    {{'profile.title' | translate}}

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

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

    diff --git a/src/app/profile-page/profile-page.component.ts b/src/app/profile-page/profile-page.component.ts index afa89360b25..673b9856985 100644 --- a/src/app/profile-page/profile-page.component.ts +++ b/src/app/profile-page/profile-page.component.ts @@ -8,6 +8,7 @@ import { OnInit, ViewChild, } from '@angular/core'; +import { RouterModule } from '@angular/router'; import { TranslateModule, TranslateService, @@ -65,6 +66,7 @@ import { ProfilePageSecurityFormComponent } from './profile-page-security-form/p NgIf, NgForOf, SuggestionsNotificationComponent, + RouterModule, ], standalone: true, }) diff --git a/src/app/root/root.component.spec.ts b/src/app/root/root.component.spec.ts index 1abc3ffd176..77b39d29088 100644 --- a/src/app/root/root.component.spec.ts +++ b/src/app/root/root.component.spec.ts @@ -8,6 +8,8 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { Router } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; +import { AccessibilitySettingsService } from '../accessibility/accessibility-settings.service'; +import { AccessibilitySettingsServiceStub } from '../accessibility/accessibility-settings.service.stub'; import { ThemedAdminSidebarComponent } from '../admin/admin-sidebar/themed-admin-sidebar.component'; import { ThemedBreadcrumbsComponent } from '../breadcrumbs/themed-breadcrumbs.component'; import { ThemedFooterComponent } from '../footer/themed-footer.component'; @@ -41,6 +43,7 @@ describe('RootComponent', () => { { provide: MenuService, useValue: new MenuServiceStub() }, { provide: CSSVariableService, useClass: CSSVariableServiceStub }, { provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, + { provide: AccessibilitySettingsService, useValue: new AccessibilitySettingsServiceStub() }, ], schemas: [CUSTOM_ELEMENTS_SCHEMA], }) 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 375c0d0d699..60ab2215087 100644 --- a/src/app/shared/live-region/live-region.service.spec.ts +++ b/src/app/shared/live-region/live-region.service.spec.ts @@ -1,18 +1,26 @@ import { fakeAsync, - flush, tick, } from '@angular/core/testing'; +import { of } from 'rxjs'; +import { AccessibilitySettingsService } from 'src/app/accessibility/accessibility-settings.service'; +import { getAccessibilitySettingsServiceStub } from 'src/app/accessibility/accessibility-settings.service.stub'; import { UUIDService } from '../../core/shared/uuid.service'; import { LiveRegionService } from './live-region.service'; 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, ); }); @@ -86,13 +94,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) => { @@ -119,45 +130,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/live-region/live-region.service.ts b/src/app/shared/live-region/live-region.service.ts index 79470240919..c1b889be040 100644 --- a/src/app/shared/live-region/live-region.service.ts +++ b/src/app/shared/live-region/live-region.service.ts @@ -1,9 +1,22 @@ import { Injectable } from '@angular/core'; -import { BehaviorSubject } from 'rxjs'; +import { + BehaviorSubject, + map, + Observable, + switchMap, + take, + timer, +} from 'rxjs'; import { environment } from '../../../environments/environment'; +import { + AccessibilitySetting, + AccessibilitySettingsService, +} from '../../accessibility/accessibility-settings.service'; import { UUIDService } from '../../core/shared/uuid.service'; +export const MIN_MESSAGE_DURATION = 200; + /** * The LiveRegionService is responsible for handling the messages that are shown by the {@link LiveRegionComponent}. * Use this service to add or remove messages to the Live Region. @@ -15,6 +28,7 @@ export class LiveRegionService { constructor( protected uuidService: UUIDService, + protected accessibilitySettingsService: AccessibilitySettingsService, ) { } @@ -65,7 +79,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; } @@ -116,6 +135,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 */ 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 8a4ea204ac0..5fc7ae3ccae 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 @@ -18,6 +18,8 @@ import { cold } from 'jasmine-marbles'; import uniqueId from 'lodash/uniqueId'; import { INotificationBoardOptions } from '../../../../config/notifications-config.interfaces'; +import { AccessibilitySettingsService } from '../../../accessibility/accessibility-settings.service'; +import { getAccessibilitySettingsServiceStub } from '../../../accessibility/accessibility-settings.service.stub'; import { AppState } from '../../../app.reducer'; import { NotificationsServiceStub } from '../../testing/notifications-service.stub'; import { Notification } from '../models/notification.model'; @@ -48,6 +50,7 @@ describe('NotificationsBoardComponent', () => { ], providers: [ { provide: NotificationsService, useClass: NotificationsServiceStub }, + { provide: AccessibilitySettingsService, useValue: getAccessibilitySettingsServiceStub() }, ChangeDetectorRef, ], }).compileComponents(); // compile template and css 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 51d60368ab6..fdb6d2240ea 100644 --- a/src/app/shared/notifications/notifications-board/notifications-board.component.ts +++ b/src/app/shared/notifications/notifications-board/notifications-board.component.ts @@ -15,13 +15,19 @@ import { select, Store, } from '@ngrx/store'; +import cloneDeep from 'lodash/cloneDeep'; import difference from 'lodash/difference'; import { BehaviorSubject, Subscription, + take, } from 'rxjs'; import { INotificationBoardOptions } from '../../../../config/notifications-config.interfaces'; +import { + AccessibilitySetting, + AccessibilitySettingsService, +} from '../../../accessibility/accessibility-settings.service'; import { AppState } from '../../../app.reducer'; import { INotification } from '../models/notification.model'; import { NotificationComponent } from '../notification/notification.component'; @@ -61,9 +67,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 { @@ -96,7 +105,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 diff --git a/src/app/shared/testing/auth-service.stub.ts b/src/app/shared/testing/auth-service.stub.ts index 5a40e05d1db..c844d247545 100644 --- a/src/app/shared/testing/auth-service.stub.ts +++ b/src/app/shared/testing/auth-service.stub.ts @@ -59,6 +59,10 @@ export class AuthServiceStub { return observableOf(EPersonMock); } + getAuthenticatedUserFromStoreIfAuthenticated(): Observable { + return observableOf(EPersonMock); + } + public buildAuthHeader(token?: AuthTokenInfo): string { return `Bearer ${token ? token.accessToken : ''}`; } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 0ba1f5476b0..54a48032079 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1900,6 +1900,8 @@ "footer.copyright": "copyright © 2002-{{ year }}", + "footer.link.accessibility": "Accessibility settings", + "footer.link.dspace": "DSpace software", "footer.link.lyrasis": "LYRASIS", @@ -2128,6 +2130,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", @@ -3870,6 +3890,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", diff --git a/src/config/app-config.interface.ts b/src/config/app-config.interface.ts index 7f5f0199582..27763c94184 100644 --- a/src/config/app-config.interface.ts +++ b/src/config/app-config.interface.ts @@ -4,6 +4,7 @@ import { Type, } from '@angular/core'; +import { AccessibilitySettingsConfig } from '../app/accessibility/accessibility-settings.config'; import { AdminNotifyMetricsRow } from '../app/admin/admin-notify-dashboard/admin-notify-metrics/admin-notify-metrics.model'; import { HALDataService } from '../app/core/data/base/hal-data-service.interface'; import { LiveRegionConfig } from '../app/shared/live-region/live-region.config'; @@ -66,6 +67,7 @@ interface AppConfig extends Config { search: SearchConfig; notifyMetrics: AdminNotifyMetricsRow[]; liveRegion: LiveRegionConfig; + accessibility: AccessibilitySettingsConfig; } /** diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index 3455ef5f92a..a93bd3a80bd 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -1,3 +1,4 @@ +import { AccessibilitySettingsConfig } from '../app/accessibility/accessibility-settings.config'; import { AdminNotifyMetricsRow } from '../app/admin/admin-notify-dashboard/admin-notify-metrics/admin-notify-metrics.model'; import { RestRequestMethod } from '../app/core/data/rest-request-method'; import { LiveRegionConfig } from '../app/shared/live-region/live-region.config'; @@ -598,4 +599,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 c7146dfb078..9ddc40bb8c3 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -427,4 +427,8 @@ export const environment: BuildConfig = { messageTimeOutDurationMs: 30000, isVisible: false, }, + + accessibility: { + cookieExpirationDuration: 7, + }, }; diff --git a/src/themes/custom/app/profile-page/profile-page.component.ts b/src/themes/custom/app/profile-page/profile-page.component.ts index 74a88fdba63..687cf9bc9d5 100644 --- a/src/themes/custom/app/profile-page/profile-page.component.ts +++ b/src/themes/custom/app/profile-page/profile-page.component.ts @@ -4,6 +4,7 @@ import { NgIf, } from '@angular/common'; import { Component } from '@angular/core'; +import { RouterModule } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { SuggestionsNotificationComponent } from '../../../../app/notifications/suggestions-notification/suggestions-notification.component'; @@ -30,6 +31,7 @@ import { VarDirective } from '../../../../app/shared/utils/var.directive'; NgIf, NgForOf, SuggestionsNotificationComponent, + RouterModule, ], }) /**