Merge branch 'accessibility-settings-7.6' into accessibility-settings-8_x

# Conflicts:
#	src/app/core/auth/auth.service.ts
#	src/app/info/info-routing-paths.ts
#	src/app/info/info-routing.module.ts
#	src/app/info/info.module.ts
#	src/app/shared/live-region/live-region.service.spec.ts
#	src/app/shared/live-region/live-region.service.ts
#	src/app/shared/notifications/notifications-board/notifications-board.component.spec.ts
#	src/app/shared/notifications/notifications-board/notifications-board.component.ts
#	src/config/app-config.interface.ts
#	src/config/default-app-config.ts
This commit is contained in:
Andreas Awouters
2024-11-06 11:20:45 +01:00
25 changed files with 1019 additions and 48 deletions

View File

@@ -516,3 +516,8 @@ liveRegion:
messageTimeOutDurationMs: 30000 messageTimeOutDurationMs: 30000
# The visibility of the live region. Setting this to true is only useful for debugging purposes. # The visibility of the live region. Setting this to true is only useful for debugging purposes.
isVisible: false 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

View File

@@ -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;
}

View File

@@ -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');
});
});
});

View File

@@ -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');
}

View File

@@ -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<string> {
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<number> {
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<AccessibilitySettings> {
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<AccessibilitySettings> {
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<boolean> {
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<boolean> {
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';
}
}
}

View File

@@ -60,6 +60,7 @@ import {
import { import {
getAllSucceededRemoteDataPayload, getAllSucceededRemoteDataPayload,
getFirstCompletedRemoteData, getFirstCompletedRemoteData,
getFirstSucceededRemoteDataPayload,
} from '../shared/operators'; } from '../shared/operators';
import { PageInfo } from '../shared/page-info.model'; import { PageInfo } from '../shared/page-info.model';
import { 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<EPerson> {
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. * Checks if token is present into browser storage and is valid.
*/ */

View File

@@ -80,6 +80,10 @@
<a class="btn text-white" <a class="btn text-white"
routerLink="info/feedback">{{ 'footer.link.feedback' | translate}}</a> routerLink="info/feedback">{{ 'footer.link.feedback' | translate}}</a>
</li> </li>
<li>
<a class="text-white"
routerLink="info/accessibility">{{ 'footer.link.accessibility' | translate }}</a>
</li>
</ul> </ul>
</div> </div>
<div *ngIf="coarLdnEnabled$ | async" class="notify-enabled text-white"> <div *ngIf="coarLdnEnabled$ | async" class="notify-enabled text-white">

View File

@@ -0,0 +1,26 @@
<div class="container">
<h2>{{ 'info.accessibility-settings.title' | translate }}</h2>
<form>
<div *ngFor="let setting of accessibilitySettingsOptions" class="form-group row">
<label [for]="setting + 'Input'" class="col-sm-2 col-form-label">
{{ 'info.accessibility-settings.' + setting + '.label' | translate }}
</label>
<div class="col-sm-10">
<input [type]="getInputType(setting)" [id]="setting + 'Input'" class="form-control"
[(ngModel)]="formValues[setting]" [ngModelOptions]="{ standalone: true }"
[attr.aria-describedby]="setting + 'Hint'">
<small [id]="setting + 'Hint'" class="form-text text-muted">
{{ 'info.accessibility-settings.' + setting + '.hint' | translate }}
</small>
</div>
</div>
<button type="submit" (click)="saveSettings()" class="btn btn-primary">
{{ 'info.accessibility-settings.submit' | translate }}
</button>
</form>
</div>

View File

@@ -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<AccessibilitySettingsComponent>;
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();
});
});
});

View File

@@ -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));
});
}
}

View File

@@ -8,9 +8,11 @@ import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso
import { notifyInfoGuard } from '../core/coar-notify/notify-info/notify-info.guard'; import { notifyInfoGuard } from '../core/coar-notify/notify-info/notify-info.guard';
import { feedbackGuard } from '../core/feedback/feedback.guard'; import { feedbackGuard } from '../core/feedback/feedback.guard';
import { hasValue } from '../shared/empty.util'; 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 { ThemedEndUserAgreementComponent } from './end-user-agreement/themed-end-user-agreement.component';
import { ThemedFeedbackComponent } from './feedback/themed-feedback.component'; import { ThemedFeedbackComponent } from './feedback/themed-feedback.component';
import { import {
ACCESSIBILITY_SETTINGS_PATH,
COAR_NOTIFY_SUPPORT, COAR_NOTIFY_SUPPORT,
END_USER_AGREEMENT_PATH, END_USER_AGREEMENT_PATH,
FEEDBACK_PATH, FEEDBACK_PATH,
@@ -28,6 +30,12 @@ export const ROUTES: Routes = [
data: { title: 'info.feedback.title', breadcrumbKey: 'info.feedback' }, data: { title: 'info.feedback.title', breadcrumbKey: 'info.feedback' },
canActivate: [feedbackGuard], canActivate: [feedbackGuard],
}, },
{
path: ACCESSIBILITY_SETTINGS_PATH,
component: AccessibilitySettingsComponent,
resolve: { breadcrumb: i18nBreadcrumbResolver },
data: { title: 'info.accessibility-settings.title', breadcrumbKey: 'info.accessibility-settings' },
},
environment.info.enableEndUserAgreement ? { environment.info.enableEndUserAgreement ? {
path: END_USER_AGREEMENT_PATH, path: END_USER_AGREEMENT_PATH,
component: ThemedEndUserAgreementComponent, component: ThemedEndUserAgreementComponent,

View File

@@ -4,6 +4,7 @@ export const END_USER_AGREEMENT_PATH = 'end-user-agreement';
export const PRIVACY_PATH = 'privacy'; export const PRIVACY_PATH = 'privacy';
export const FEEDBACK_PATH = 'feedback'; export const FEEDBACK_PATH = 'feedback';
export const COAR_NOTIFY_SUPPORT = 'coar-notify-support'; export const COAR_NOTIFY_SUPPORT = 'coar-notify-support';
export const ACCESSIBILITY_SETTINGS_PATH = 'accessibility';
export function getEndUserAgreementPath() { export function getEndUserAgreementPath() {
return getSubPath(END_USER_AGREEMENT_PATH); return getSubPath(END_USER_AGREEMENT_PATH);
@@ -21,6 +22,10 @@ export function getCOARNotifySupportPath(): string {
return getSubPath(COAR_NOTIFY_SUPPORT); return getSubPath(COAR_NOTIFY_SUPPORT);
} }
export function getAccessibilitySettingsPath() {
return getSubPath(ACCESSIBILITY_SETTINGS_PATH);
}
function getSubPath(path: string) { function getSubPath(path: string) {
return `${getInfoModulePath()}/${path}`; return `${getInfoModulePath()}/${path}`;
} }

View File

@@ -29,10 +29,18 @@
></ds-profile-page-security-form> ></ds-profile-page-security-form>
</div> </div>
</div> </div>
<div class="col-12 text-right pr-0"> <div class="col-12 text-right pr-0 mb-4">
<button class="btn btn-primary" (click)="updateProfile()"><i class="fas fa-edit"></i> {{'profile.form.submit' | translate}}</button> <button class="btn btn-primary" (click)="updateProfile()"><i class="fas fa-edit"></i> {{'profile.form.submit' | translate}}</button>
</div> </div>
<div class="card mb-4">
<div class="card-header">{{'profile.card.accessibility.header' | translate}}</div>
<div class="card-body">
<div class="mb-1">{{'profile.card.accessibility.content' | translate}}</div>
<a [routerLink]="'/info/accessibility'">{{'profile.card.accessibility.link' | translate}}</a>
</div>
</div>
<ng-container *ngVar="(groupsRD$ | async)?.payload?.page as groups"> <ng-container *ngVar="(groupsRD$ | async)?.payload?.page as groups">
<div *ngIf="groups?.length > 0"> <div *ngIf="groups?.length > 0">
<h2 class="mt-4">{{'profile.groups.head' | translate}}</h2> <h2 class="mt-4">{{'profile.groups.head' | translate}}</h2>

View File

@@ -8,6 +8,7 @@ import {
OnInit, OnInit,
ViewChild, ViewChild,
} from '@angular/core'; } from '@angular/core';
import { RouterModule } from '@angular/router';
import { import {
TranslateModule, TranslateModule,
TranslateService, TranslateService,
@@ -65,6 +66,7 @@ import { ProfilePageSecurityFormComponent } from './profile-page-security-form/p
NgIf, NgIf,
NgForOf, NgForOf,
SuggestionsNotificationComponent, SuggestionsNotificationComponent,
RouterModule,
], ],
standalone: true, standalone: true,
}) })

View File

@@ -8,6 +8,8 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core'; 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 { ThemedAdminSidebarComponent } from '../admin/admin-sidebar/themed-admin-sidebar.component';
import { ThemedBreadcrumbsComponent } from '../breadcrumbs/themed-breadcrumbs.component'; import { ThemedBreadcrumbsComponent } from '../breadcrumbs/themed-breadcrumbs.component';
import { ThemedFooterComponent } from '../footer/themed-footer.component'; import { ThemedFooterComponent } from '../footer/themed-footer.component';
@@ -41,6 +43,7 @@ describe('RootComponent', () => {
{ provide: MenuService, useValue: new MenuServiceStub() }, { provide: MenuService, useValue: new MenuServiceStub() },
{ provide: CSSVariableService, useClass: CSSVariableServiceStub }, { provide: CSSVariableService, useClass: CSSVariableServiceStub },
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, { provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
{ provide: AccessibilitySettingsService, useValue: new AccessibilitySettingsServiceStub() },
], ],
schemas: [CUSTOM_ELEMENTS_SCHEMA], schemas: [CUSTOM_ELEMENTS_SCHEMA],
}) })

View File

@@ -1,18 +1,26 @@
import { import {
fakeAsync, fakeAsync,
flush,
tick, tick,
} from '@angular/core/testing'; } 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 { UUIDService } from '../../core/shared/uuid.service';
import { LiveRegionService } from './live-region.service'; import { LiveRegionService } from './live-region.service';
describe('liveRegionService', () => { describe('liveRegionService', () => {
let service: LiveRegionService; let service: LiveRegionService;
let accessibilitySettingsService: AccessibilitySettingsService;
beforeEach(() => { beforeEach(() => {
accessibilitySettingsService = getAccessibilitySettingsServiceStub();
accessibilitySettingsService.getAsNumber = jasmine.createSpy('getAsNumber').and.returnValue(of(100));
service = new LiveRegionService( service = new LiveRegionService(
new UUIDService(), new UUIDService(),
accessibilitySettingsService,
); );
}); });
@@ -86,13 +94,16 @@ describe('liveRegionService', () => {
expect(results[2]).toEqual(['Message One', 'Message Two']); expect(results[2]).toEqual(['Message One', 'Message Two']);
service.clear(); service.clear();
flush(); tick(200);
expect(results.length).toEqual(4); expect(results.length).toEqual(4);
expect(results[3]).toEqual([]); expect(results[3]).toEqual([]);
})); }));
it('should not pop messages added after clearing within timeOut period', fakeAsync(() => { 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[][] = []; const results: string[][] = [];
service.getMessages$().subscribe((messages) => { service.getMessages$().subscribe((messages) => {
@@ -119,45 +130,6 @@ describe('liveRegionService', () => {
expect(results.length).toEqual(5); expect(results.length).toEqual(5);
expect(results[4]).toEqual([]); 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', () => { describe('liveRegionVisibility', () => {

View File

@@ -1,9 +1,22 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs'; import {
BehaviorSubject,
map,
Observable,
switchMap,
take,
timer,
} from 'rxjs';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import {
AccessibilitySetting,
AccessibilitySettingsService,
} from '../../accessibility/accessibility-settings.service';
import { UUIDService } from '../../core/shared/uuid.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}. * 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. * Use this service to add or remove messages to the Live Region.
@@ -15,6 +28,7 @@ export class LiveRegionService {
constructor( constructor(
protected uuidService: UUIDService, protected uuidService: UUIDService,
protected accessibilitySettingsService: AccessibilitySettingsService,
) { ) {
} }
@@ -65,7 +79,12 @@ export class LiveRegionService {
addMessage(message: string): string { addMessage(message: string): string {
const uuid = this.uuidService.generate(); const uuid = this.uuidService.generate();
this.messages.push({ message, uuid }); 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(); this.emitCurrentMessages();
return uuid; return uuid;
} }
@@ -116,6 +135,17 @@ export class LiveRegionService {
this.liveRegionIsVisible = isVisible; 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<number> {
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 * Gets the current message timeOut duration in milliseconds
*/ */

View File

@@ -18,6 +18,8 @@ import { cold } from 'jasmine-marbles';
import uniqueId from 'lodash/uniqueId'; import uniqueId from 'lodash/uniqueId';
import { INotificationBoardOptions } from '../../../../config/notifications-config.interfaces'; 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 { AppState } from '../../../app.reducer';
import { NotificationsServiceStub } from '../../testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../testing/notifications-service.stub';
import { Notification } from '../models/notification.model'; import { Notification } from '../models/notification.model';
@@ -48,6 +50,7 @@ describe('NotificationsBoardComponent', () => {
], ],
providers: [ providers: [
{ provide: NotificationsService, useClass: NotificationsServiceStub }, { provide: NotificationsService, useClass: NotificationsServiceStub },
{ provide: AccessibilitySettingsService, useValue: getAccessibilitySettingsServiceStub() },
ChangeDetectorRef, ChangeDetectorRef,
], ],
}).compileComponents(); // compile template and css }).compileComponents(); // compile template and css

View File

@@ -15,13 +15,19 @@ import {
select, select,
Store, Store,
} from '@ngrx/store'; } from '@ngrx/store';
import cloneDeep from 'lodash/cloneDeep';
import difference from 'lodash/difference'; import difference from 'lodash/difference';
import { import {
BehaviorSubject, BehaviorSubject,
Subscription, Subscription,
take,
} from 'rxjs'; } from 'rxjs';
import { INotificationBoardOptions } from '../../../../config/notifications-config.interfaces'; import { INotificationBoardOptions } from '../../../../config/notifications-config.interfaces';
import {
AccessibilitySetting,
AccessibilitySettingsService,
} from '../../../accessibility/accessibility-settings.service';
import { AppState } from '../../../app.reducer'; import { AppState } from '../../../app.reducer';
import { INotification } from '../models/notification.model'; import { INotification } from '../models/notification.model';
import { NotificationComponent } from '../notification/notification.component'; import { NotificationComponent } from '../notification/notification.component';
@@ -61,9 +67,12 @@ export class NotificationsBoardComponent implements OnInit, OnDestroy {
*/ */
public isPaused$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false); public isPaused$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
constructor(private service: NotificationsService, constructor(
private store: Store<AppState>, protected service: NotificationsService,
private cdr: ChangeDetectorRef) { protected store: Store<AppState>,
protected cdr: ChangeDetectorRef,
protected accessibilitySettingsService: AccessibilitySettingsService,
) {
} }
ngOnInit(): void { ngOnInit(): void {
@@ -96,7 +105,22 @@ export class NotificationsBoardComponent implements OnInit, OnDestroy {
if (this.notifications.length >= this.maxStack) { if (this.notifications.length >= this.maxStack) {
this.notifications.splice(this.notifications.length - 1, 1); 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 { } else {
// Remove the notification from the store // Remove the notification from the store
// This notification was in the store, but not in this.notifications // This notification was in the store, but not in this.notifications

View File

@@ -59,6 +59,10 @@ export class AuthServiceStub {
return observableOf(EPersonMock); return observableOf(EPersonMock);
} }
getAuthenticatedUserFromStoreIfAuthenticated(): Observable<EPerson> {
return observableOf(EPersonMock);
}
public buildAuthHeader(token?: AuthTokenInfo): string { public buildAuthHeader(token?: AuthTokenInfo): string {
return `Bearer ${token ? token.accessToken : ''}`; return `Bearer ${token ? token.accessToken : ''}`;
} }

View File

@@ -1900,6 +1900,8 @@
"footer.copyright": "copyright © 2002-{{ year }}", "footer.copyright": "copyright © 2002-{{ year }}",
"footer.link.accessibility": "Accessibility settings",
"footer.link.dspace": "DSpace software", "footer.link.dspace": "DSpace software",
"footer.link.lyrasis": "LYRASIS", "footer.link.lyrasis": "LYRASIS",
@@ -2128,6 +2130,24 @@
"home.top-level-communities.help": "Select a community to browse its collections.", "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": "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", "info.end-user-agreement.accept.error": "An error occurred accepting the End User Agreement",
@@ -3870,6 +3890,12 @@
"profile.breadcrumbs": "Update Profile", "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.identify": "Identify",
"profile.card.security": "Security", "profile.card.security": "Security",

View File

@@ -4,6 +4,7 @@ import {
Type, Type,
} from '@angular/core'; } 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 { 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 { HALDataService } from '../app/core/data/base/hal-data-service.interface';
import { LiveRegionConfig } from '../app/shared/live-region/live-region.config'; import { LiveRegionConfig } from '../app/shared/live-region/live-region.config';
@@ -66,6 +67,7 @@ interface AppConfig extends Config {
search: SearchConfig; search: SearchConfig;
notifyMetrics: AdminNotifyMetricsRow[]; notifyMetrics: AdminNotifyMetricsRow[];
liveRegion: LiveRegionConfig; liveRegion: LiveRegionConfig;
accessibility: AccessibilitySettingsConfig;
} }
/** /**

View File

@@ -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 { AdminNotifyMetricsRow } from '../app/admin/admin-notify-dashboard/admin-notify-metrics/admin-notify-metrics.model';
import { RestRequestMethod } from '../app/core/data/rest-request-method'; import { RestRequestMethod } from '../app/core/data/rest-request-method';
import { LiveRegionConfig } from '../app/shared/live-region/live-region.config'; import { LiveRegionConfig } from '../app/shared/live-region/live-region.config';
@@ -598,4 +599,9 @@ export class DefaultAppConfig implements AppConfig {
messageTimeOutDurationMs: 30000, messageTimeOutDurationMs: 30000,
isVisible: false, isVisible: false,
}; };
// Accessibility settings configuration, used by the AccessibilitySettingsService
accessibility: AccessibilitySettingsConfig = {
cookieExpirationDuration: 7,
};
} }

View File

@@ -427,4 +427,8 @@ export const environment: BuildConfig = {
messageTimeOutDurationMs: 30000, messageTimeOutDurationMs: 30000,
isVisible: false, isVisible: false,
}, },
accessibility: {
cookieExpirationDuration: 7,
},
}; };

View File

@@ -4,6 +4,7 @@ import {
NgIf, NgIf,
} from '@angular/common'; } from '@angular/common';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { SuggestionsNotificationComponent } from '../../../../app/notifications/suggestions-notification/suggestions-notification.component'; import { SuggestionsNotificationComponent } from '../../../../app/notifications/suggestions-notification/suggestions-notification.component';
@@ -30,6 +31,7 @@ import { VarDirective } from '../../../../app/shared/utils/var.directive';
NgIf, NgIf,
NgForOf, NgForOf,
SuggestionsNotificationComponent, SuggestionsNotificationComponent,
RouterModule,
], ],
}) })
/** /**