mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
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:
@@ -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
|
||||
|
11
src/app/accessibility/accessibility-settings.config.ts
Normal file
11
src/app/accessibility/accessibility-settings.config.ts
Normal 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;
|
||||
}
|
386
src/app/accessibility/accessibility-settings.service.spec.ts
Normal file
386
src/app/accessibility/accessibility-settings.service.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
35
src/app/accessibility/accessibility-settings.service.stub.ts
Normal file
35
src/app/accessibility/accessibility-settings.service.stub.ts
Normal 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');
|
||||
}
|
239
src/app/accessibility/accessibility-settings.service.ts
Normal file
239
src/app/accessibility/accessibility-settings.service.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -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<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.
|
||||
*/
|
||||
|
@@ -80,6 +80,10 @@
|
||||
<a class="btn text-white"
|
||||
routerLink="info/feedback">{{ 'footer.link.feedback' | translate}}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="text-white"
|
||||
routerLink="info/accessibility">{{ 'footer.link.accessibility' | translate }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div *ngIf="coarLdnEnabled$ | async" class="notify-enabled text-white">
|
||||
|
@@ -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>
|
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
@@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@@ -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,
|
||||
|
@@ -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}`;
|
||||
}
|
||||
|
@@ -29,10 +29,18 @@
|
||||
></ds-profile-page-security-form>
|
||||
</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>
|
||||
</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">
|
||||
<div *ngIf="groups?.length > 0">
|
||||
<h2 class="mt-4">{{'profile.groups.head' | translate}}</h2>
|
||||
|
@@ -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,
|
||||
})
|
||||
|
@@ -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],
|
||||
})
|
||||
|
@@ -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', () => {
|
||||
|
@@ -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<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
|
||||
*/
|
||||
|
@@ -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
|
||||
|
@@ -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<boolean> = new BehaviorSubject<boolean>(false);
|
||||
|
||||
constructor(private service: NotificationsService,
|
||||
private store: Store<AppState>,
|
||||
private cdr: ChangeDetectorRef) {
|
||||
constructor(
|
||||
protected service: NotificationsService,
|
||||
protected store: Store<AppState>,
|
||||
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
|
||||
|
@@ -59,6 +59,10 @@ export class AuthServiceStub {
|
||||
return observableOf(EPersonMock);
|
||||
}
|
||||
|
||||
getAuthenticatedUserFromStoreIfAuthenticated(): Observable<EPerson> {
|
||||
return observableOf(EPersonMock);
|
||||
}
|
||||
|
||||
public buildAuthHeader(token?: AuthTokenInfo): string {
|
||||
return `Bearer ${token ? token.accessToken : ''}`;
|
||||
}
|
||||
|
@@ -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",
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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,
|
||||
};
|
||||
}
|
||||
|
@@ -427,4 +427,8 @@ export const environment: BuildConfig = {
|
||||
messageTimeOutDurationMs: 30000,
|
||||
isVisible: false,
|
||||
},
|
||||
|
||||
accessibility: {
|
||||
cookieExpirationDuration: 7,
|
||||
},
|
||||
};
|
||||
|
@@ -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,
|
||||
],
|
||||
})
|
||||
/**
|
||||
|
Reference in New Issue
Block a user