mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 18:14:17 +00:00
Merge pull request #3615 from atmire/accessibility-settings-7_x
[Port dspace-7_x] Accessibility settings page
This commit is contained in:
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -29,6 +29,8 @@ jobs:
|
|||||||
DSPACE_CACHE_SERVERSIDE_ANONYMOUSCACHE_MAX: 0
|
DSPACE_CACHE_SERVERSIDE_ANONYMOUSCACHE_MAX: 0
|
||||||
# Tell Cypress to run e2e tests using the same UI URL
|
# Tell Cypress to run e2e tests using the same UI URL
|
||||||
CYPRESS_BASE_URL: http://127.0.0.1:4000
|
CYPRESS_BASE_URL: http://127.0.0.1:4000
|
||||||
|
# Disable the cookie consent banner in e2e tests to avoid errors because of elements hidden by it
|
||||||
|
DSPACE_INFO_ENABLECOOKIECONSENTPOPUP: false
|
||||||
# When Chrome version is specified, we pin to a specific version of Chrome
|
# When Chrome version is specified, we pin to a specific version of Chrome
|
||||||
# Comment this out to use the latest release
|
# Comment this out to use the latest release
|
||||||
#CHROME_VERSION: "90.0.4430.212-1"
|
#CHROME_VERSION: "90.0.4430.212-1"
|
||||||
|
@@ -458,3 +458,8 @@ search:
|
|||||||
# Since we don't know how many filters will be loaded before we receive a response from the server we use this parameter for the skeletons count.
|
# Since we don't know how many filters will be loaded before we receive a response from the server we use this parameter for the skeletons count.
|
||||||
# e.g. If we set 5 then 5 loading skeletons will be visualized before the actual filters are retrieved.
|
# e.g. If we set 5 then 5 loading skeletons will be visualized before the actual filters are retrieved.
|
||||||
defaultFiltersCount: 5
|
defaultFiltersCount: 5
|
||||||
|
|
||||||
|
# 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;
|
||||||
|
}
|
410
src/app/accessibility/accessibility-settings.service.spec.ts
Normal file
410
src/app/accessibility/accessibility-settings.service.spec.ts
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
import {
|
||||||
|
AccessibilitySettingsService,
|
||||||
|
AccessibilitySettings,
|
||||||
|
ACCESSIBILITY_SETTINGS_METADATA_KEY,
|
||||||
|
ACCESSIBILITY_COOKIE, AccessibilitySettingsFormValues, FullAccessibilitySettings
|
||||||
|
} from './accessibility-settings.service';
|
||||||
|
import { CookieService } from '../core/services/cookie.service';
|
||||||
|
import { AuthService } from '../core/auth/auth.service';
|
||||||
|
import { EPersonDataService } from '../core/eperson/eperson-data.service';
|
||||||
|
import { CookieServiceMock } from '../shared/mocks/cookie.service.mock';
|
||||||
|
import { AuthServiceStub } from '../shared/testing/auth-service.stub';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { EPerson } from '../core/eperson/models/eperson.model';
|
||||||
|
import { fakeAsync, flush } from '@angular/core/testing';
|
||||||
|
import { createSuccessfulRemoteDataObject$, createFailedRemoteDataObject$ } from '../shared/remote-data.utils';
|
||||||
|
import { KlaroServiceStub } from '../shared/cookies/klaro.service.stub';
|
||||||
|
import { AppConfig } from '../../config/app-config.interface';
|
||||||
|
|
||||||
|
|
||||||
|
describe('accessibilitySettingsService', () => {
|
||||||
|
let service: AccessibilitySettingsService;
|
||||||
|
let cookieService: CookieServiceMock;
|
||||||
|
let authService: AuthServiceStub;
|
||||||
|
let ePersonService: EPersonDataService;
|
||||||
|
let klaroService: KlaroServiceStub;
|
||||||
|
let appConfig: AppConfig;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cookieService = new CookieServiceMock();
|
||||||
|
authService = new AuthServiceStub();
|
||||||
|
klaroService = new KlaroServiceStub();
|
||||||
|
appConfig = { accessibility: { cookieExpirationDuration: 10 }} as AppConfig;
|
||||||
|
|
||||||
|
klaroService.getSavedPreferences.and.returnValue(of({ accessibility: true }));
|
||||||
|
|
||||||
|
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,
|
||||||
|
klaroService,
|
||||||
|
appConfig,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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('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('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('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('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('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('liveRegionTimeOut', '500');
|
||||||
|
expect(service.updateSettings).toHaveBeenCalledWith({ liveRegionTimeOut: '500' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setSettings', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.setSettingsInCookie = jasmine.createSpy('setSettingsInCookie').and.returnValue(of('cookie'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should attempt to set settings in metadata', () => {
|
||||||
|
service.setSettingsInAuthenticatedUserMetadata =
|
||||||
|
jasmine.createSpy('setSettingsInAuthenticatedUserMetadata').and.returnValue(of('failed'));
|
||||||
|
|
||||||
|
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('metadata'));
|
||||||
|
|
||||||
|
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('metadata'));
|
||||||
|
|
||||||
|
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 "failed" when the user is not authenticated', fakeAsync(() => {
|
||||||
|
authService.getAuthenticatedUserFromStoreIfAuthenticated = jasmine.createSpy().and.returnValue(of(null));
|
||||||
|
|
||||||
|
service.setSettingsInAuthenticatedUserMetadata({})
|
||||||
|
.subscribe(value => expect(value).toEqual('failed'));
|
||||||
|
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, { ['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, { ['liveRegionTimeOut']: '500' }).subscribe();
|
||||||
|
expect(ePersonService.createPatchFromCache).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send the patch request', () => {
|
||||||
|
service.setSettingsInMetadata(ePerson, { ['liveRegionTimeOut']: '500' }).subscribe();
|
||||||
|
expect(ePersonService.patch).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit "metadata" when the update succeeded', fakeAsync(() => {
|
||||||
|
ePersonService.patch = jasmine.createSpy().and.returnValue(createSuccessfulRemoteDataObject$({}));
|
||||||
|
|
||||||
|
service.setSettingsInMetadata(ePerson, { ['liveRegionTimeOut']: '500' })
|
||||||
|
.subscribe(value => {
|
||||||
|
expect(value).toEqual('metadata');
|
||||||
|
});
|
||||||
|
|
||||||
|
flush();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should emit "failed" when the update failed', fakeAsync(() => {
|
||||||
|
ePersonService.patch = jasmine.createSpy().and.returnValue(createFailedRemoteDataObject$());
|
||||||
|
|
||||||
|
service.setSettingsInMetadata(ePerson, { ['liveRegionTimeOut']: '500' })
|
||||||
|
.subscribe(value => {
|
||||||
|
expect(value).toEqual('failed');
|
||||||
|
});
|
||||||
|
|
||||||
|
flush();
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setSettingsInCookie', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cookieService.set = jasmine.createSpy('set');
|
||||||
|
cookieService.remove = jasmine.createSpy('remove');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail to store settings in the cookie when the user has not accepted the cookie', fakeAsync(() => {
|
||||||
|
klaroService.getSavedPreferences.and.returnValue(of({ accessibility: false }));
|
||||||
|
|
||||||
|
service.setSettingsInCookie({ ['liveRegionTimeOut']: '500' }).subscribe(value => {
|
||||||
|
expect(value).toEqual('failed');
|
||||||
|
});
|
||||||
|
flush();
|
||||||
|
expect(cookieService.set).not.toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should store the settings in a cookie', fakeAsync(() => {
|
||||||
|
service.setSettingsInCookie({ ['liveRegionTimeOut']: '500' }).subscribe(value => {
|
||||||
|
expect(value).toEqual('cookie');
|
||||||
|
});
|
||||||
|
flush();
|
||||||
|
expect(cookieService.set).toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should remove the cookie when the settings are empty', fakeAsync(() => {
|
||||||
|
service.setSettingsInCookie({}).subscribe(value => {
|
||||||
|
expect(value).toEqual('cookie');
|
||||||
|
});
|
||||||
|
|
||||||
|
flush();
|
||||||
|
|
||||||
|
expect(cookieService.set).not.toHaveBeenCalled();
|
||||||
|
expect(cookieService.remove).toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('convertFormValuesToStoredValues', () => {
|
||||||
|
it('should reset the notificationTimeOut when timeOut is enabled but set to "0"', () => {
|
||||||
|
const formValues: AccessibilitySettingsFormValues = {
|
||||||
|
notificationTimeOutEnabled: true,
|
||||||
|
notificationTimeOut: '0',
|
||||||
|
liveRegionTimeOut: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const storedValues: FullAccessibilitySettings = service.convertFormValuesToStoredValues(formValues);
|
||||||
|
expect('notificationTimeOut' in storedValues).toBeFalse();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep the notificationTimeOut when timeOut is enabled and differs from "0"', () => {
|
||||||
|
const formValues: AccessibilitySettingsFormValues = {
|
||||||
|
notificationTimeOutEnabled: true,
|
||||||
|
notificationTimeOut: '3',
|
||||||
|
liveRegionTimeOut: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const storedValues: FullAccessibilitySettings = service.convertFormValuesToStoredValues(formValues);
|
||||||
|
expect('notificationTimeOut' in storedValues).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
44
src/app/accessibility/accessibility-settings.service.stub.ts
Normal file
44
src/app/accessibility/accessibility-settings.service.stub.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
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');
|
||||||
|
|
||||||
|
convertFormValuesToStoredValues = jasmine.createSpy('convertFormValuesToStoredValues').and.returnValue({});
|
||||||
|
|
||||||
|
convertStoredValuesToFormValues = jasmine.createSpy('convertStoredValuesToFormValues').and.returnValue({});
|
||||||
|
|
||||||
|
getDefaultValue = jasmine.createSpy('getPlaceholder').and.returnValue('placeholder');
|
||||||
|
|
||||||
|
isValid = jasmine.createSpy('isValid').and.returnValue(true);
|
||||||
|
|
||||||
|
formValuesValid = jasmine.createSpy('allValid').and.returnValue(true);
|
||||||
|
}
|
361
src/app/accessibility/accessibility-settings.service.ts
Normal file
361
src/app/accessibility/accessibility-settings.service.ts
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
import { Inject, Injectable, Optional } from '@angular/core';
|
||||||
|
import { Observable, of, switchMap, combineLatest } from 'rxjs';
|
||||||
|
import { map, take } from 'rxjs/operators';
|
||||||
|
import { CookieService } from '../core/services/cookie.service';
|
||||||
|
import { hasValue, isNotEmpty, hasNoValue } from '../shared/empty.util';
|
||||||
|
import { AuthService } from '../core/auth/auth.service';
|
||||||
|
import { EPerson } from '../core/eperson/models/eperson.model';
|
||||||
|
import { EPersonDataService } from '../core/eperson/eperson-data.service';
|
||||||
|
import { getFirstCompletedRemoteData } from '../core/shared/operators';
|
||||||
|
import cloneDeep from 'lodash/cloneDeep';
|
||||||
|
import { environment } from '../../environments/environment';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
|
||||||
|
import { KlaroService } from '../shared/cookies/klaro.service';
|
||||||
|
import { AppConfig, APP_CONFIG } from '../../config/app-config.interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array containing all possible accessibility settings.
|
||||||
|
* When adding new settings, make sure to add the new setting to the accessibility-settings component form.
|
||||||
|
* The converter methods to convert from stored format to form format (and vice-versa) need to be updated as well.
|
||||||
|
*/
|
||||||
|
export const accessibilitySettingKeys = ['notificationTimeOut', 'liveRegionTimeOut'] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type representing the possible accessibility settings
|
||||||
|
*/
|
||||||
|
export type AccessibilitySetting = typeof accessibilitySettingKeys[number];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type representing an object that contains accessibility settings values for all accessibility settings.
|
||||||
|
*/
|
||||||
|
export type FullAccessibilitySettings = { [key in AccessibilitySetting]: string };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type representing an object that contains accessibility settings values for some accessibility settings.
|
||||||
|
*/
|
||||||
|
export type AccessibilitySettings = Partial<FullAccessibilitySettings>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The accessibility settings object format used by the accessibility-settings component form.
|
||||||
|
*/
|
||||||
|
export interface AccessibilitySettingsFormValues {
|
||||||
|
notificationTimeOutEnabled: boolean,
|
||||||
|
notificationTimeOut: string,
|
||||||
|
liveRegionTimeOut: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
@Optional() protected klaroService: KlaroService,
|
||||||
|
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
* Returns 'failed' when both options failed.
|
||||||
|
*/
|
||||||
|
set(setting: AccessibilitySetting, value: string): Observable<'metadata' | 'cookie' | 'failed'> {
|
||||||
|
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.
|
||||||
|
* Returns 'failed' when both options failed.
|
||||||
|
*/
|
||||||
|
setSettings(settings: AccessibilitySettings): Observable<'metadata' | 'cookie' | 'failed'> {
|
||||||
|
return this.setSettingsInAuthenticatedUserMetadata(settings).pipe(
|
||||||
|
take(1),
|
||||||
|
map(saveLocation => saveLocation === 'metadata'),
|
||||||
|
switchMap((savedInMetadata) =>
|
||||||
|
savedInMetadata ? ofMetadata() : this.setSettingsInCookie(settings)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
* Returns 'failed' when both options failed.
|
||||||
|
*/
|
||||||
|
updateSettings(settings: AccessibilitySettings): Observable<'metadata' | 'cookie' | 'failed'> {
|
||||||
|
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 'metadata' when the metadata update succeeded, and 'failed' otherwise.
|
||||||
|
*/
|
||||||
|
setSettingsInAuthenticatedUserMetadata(settings: AccessibilitySettings): Observable<'metadata' | 'failed'> {
|
||||||
|
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 ofFailed();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<'metadata' | 'failed'> {
|
||||||
|
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),
|
||||||
|
switchMap(operations =>
|
||||||
|
isNotEmpty(operations) ? this.ePersonService.patch(user, operations) : createSuccessfulRemoteDataObject$({})),
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
switchMap(rd => rd.hasSucceeded ? ofMetadata() : ofFailed()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to set the provided settings in a cookie.
|
||||||
|
* Emits 'failed' when setting in a cookie failed due to the cookie not being accepted, 'cookie' when it succeeded.
|
||||||
|
*/
|
||||||
|
setSettingsInCookie(settings: AccessibilitySettings): Observable<'cookie' | 'failed'> {
|
||||||
|
if (hasNoValue(this.klaroService)) {
|
||||||
|
return of('failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.klaroService.getSavedPreferences().pipe(
|
||||||
|
map(preferences => preferences.accessibility),
|
||||||
|
map((accessibilityCookieAccepted: boolean) => {
|
||||||
|
if (accessibilityCookieAccepted) {
|
||||||
|
if (isNotEmpty(settings)) {
|
||||||
|
this.cookieService.set(ACCESSIBILITY_COOKIE, settings, { expires: this.appConfig.accessibility.cookieExpirationDuration });
|
||||||
|
} else {
|
||||||
|
this.cookieService.remove(ACCESSIBILITY_COOKIE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'cookie';
|
||||||
|
} else {
|
||||||
|
return 'failed';
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all settings in the cookie and attempts to clear settings in metadata.
|
||||||
|
* Emits an array mentioning which settings succeeded or failed.
|
||||||
|
*/
|
||||||
|
clearSettings(): Observable<['cookie' | 'failed', 'metadata' | 'failed']> {
|
||||||
|
return combineLatest([
|
||||||
|
this.setSettingsInCookie({}),
|
||||||
|
this.setSettingsInAuthenticatedUserMetadata({}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the default value to be used for the provided AccessibilitySetting.
|
||||||
|
* Returns an empty string when no default value is specified for the provided setting.
|
||||||
|
*/
|
||||||
|
getDefaultValue(setting: AccessibilitySetting): string {
|
||||||
|
switch (setting) {
|
||||||
|
case 'notificationTimeOut':
|
||||||
|
return millisecondsToSeconds(environment.notifications.timeOut.toString());
|
||||||
|
case 'liveRegionTimeOut':
|
||||||
|
return millisecondsToSeconds(environment.liveRegion.messageTimeOutDurationMs.toString());
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert values in the provided accessibility settings object to values ready to be stored.
|
||||||
|
*/
|
||||||
|
convertFormValuesToStoredValues(settings: AccessibilitySettingsFormValues): FullAccessibilitySettings {
|
||||||
|
const storedValues = {
|
||||||
|
notificationTimeOut: settings.notificationTimeOutEnabled ?
|
||||||
|
secondsToMilliseconds(settings.notificationTimeOut) : '0',
|
||||||
|
liveRegionTimeOut: secondsToMilliseconds(settings.liveRegionTimeOut),
|
||||||
|
};
|
||||||
|
|
||||||
|
// When the user enables the timeout but does not change the timeout duration from 0,
|
||||||
|
// it is removed from the values to be stored so the default value is used.
|
||||||
|
// Keeping it at 0 would mean the notifications are not automatically removed.
|
||||||
|
if (settings.notificationTimeOutEnabled && settings.notificationTimeOut === '0') {
|
||||||
|
delete storedValues.notificationTimeOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
return storedValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert values in the provided accessibility settings object to values ready to show in the form.
|
||||||
|
*/
|
||||||
|
convertStoredValuesToFormValues(settings: AccessibilitySettings): AccessibilitySettingsFormValues {
|
||||||
|
return {
|
||||||
|
notificationTimeOutEnabled: parseFloat(settings.notificationTimeOut) !== 0,
|
||||||
|
notificationTimeOut: millisecondsToSeconds(settings.notificationTimeOut),
|
||||||
|
liveRegionTimeOut: millisecondsToSeconds(settings.liveRegionTimeOut),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the provided AccessibilitySetting is valid in regard to the provided formValues.
|
||||||
|
*/
|
||||||
|
isValid(setting: AccessibilitySetting, formValues: AccessibilitySettingsFormValues): boolean {
|
||||||
|
switch (setting) {
|
||||||
|
case 'notificationTimeOut':
|
||||||
|
return formValues.notificationTimeOutEnabled ?
|
||||||
|
hasNoValue(formValues.notificationTimeOut) || parseFloat(formValues.notificationTimeOut) > 0 :
|
||||||
|
true;
|
||||||
|
case 'liveRegionTimeOut':
|
||||||
|
return hasNoValue(formValues.liveRegionTimeOut) || parseFloat(formValues.liveRegionTimeOut) > 0;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unhandled accessibility setting during validity check: ${setting}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if all settings in the provided AccessibilitySettingsFormValues object are valid
|
||||||
|
*/
|
||||||
|
formValuesValid(formValues: AccessibilitySettingsFormValues) {
|
||||||
|
return accessibilitySettingKeys.every(setting => this.isValid(setting, formValues));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a string representing seconds to a string representing milliseconds
|
||||||
|
* Returns null if the input could not be parsed to a float
|
||||||
|
*/
|
||||||
|
function secondsToMilliseconds(secondsStr: string): string {
|
||||||
|
const seconds = parseFloat(secondsStr);
|
||||||
|
if (isNaN(seconds)) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return (seconds * 1000).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a string representing milliseconds to a string representing seconds
|
||||||
|
* Returns null if the input could not be parsed to a float
|
||||||
|
*/
|
||||||
|
function millisecondsToSeconds(millisecondsStr: string): string {
|
||||||
|
const milliseconds = parseFloat(millisecondsStr);
|
||||||
|
if (isNaN(milliseconds)) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return (milliseconds / 1000).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ofMetadata(): Observable<'metadata'> {
|
||||||
|
return of('metadata');
|
||||||
|
}
|
||||||
|
|
||||||
|
function ofFailed(): Observable<'failed'> {
|
||||||
|
return of('failed');
|
||||||
|
}
|
@@ -43,7 +43,11 @@ import {
|
|||||||
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
||||||
import { RouteService } from '../services/route.service';
|
import { RouteService } from '../services/route.service';
|
||||||
import { EPersonDataService } from '../eperson/eperson-data.service';
|
import { EPersonDataService } from '../eperson/eperson-data.service';
|
||||||
import { getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData } from '../shared/operators';
|
import {
|
||||||
|
getAllSucceededRemoteDataPayload,
|
||||||
|
getFirstCompletedRemoteData,
|
||||||
|
getFirstSucceededRemoteDataPayload
|
||||||
|
} from '../shared/operators';
|
||||||
import { AuthMethod } from './models/auth.method';
|
import { AuthMethod } from './models/auth.method';
|
||||||
import { HardRedirectService } from '../services/hard-redirect.service';
|
import { HardRedirectService } from '../services/hard-redirect.service';
|
||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
@@ -227,6 +231,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.
|
||||||
*/
|
*/
|
||||||
|
@@ -63,11 +63,15 @@
|
|||||||
href="https://www.lyrasis.org/" role="link" tabindex="0">{{ 'footer.link.lyrasis' | translate}}</a>
|
href="https://www.lyrasis.org/" role="link" tabindex="0">{{ 'footer.link.lyrasis' | translate}}</a>
|
||||||
</p>
|
</p>
|
||||||
<ul class="footer-info list-unstyled d-flex justify-content-center mb-0">
|
<ul class="footer-info list-unstyled d-flex justify-content-center mb-0">
|
||||||
<li>
|
<li *ngIf="showCookieSettings">
|
||||||
<button class="btn btn-link text-white" type="button" (click)="showCookieSettings()" role="button" tabindex="0">
|
<button class="btn btn-link text-white" type="button" (click)="openCookieSettings()" role="button" tabindex="0">
|
||||||
{{ 'footer.link.cookies' | translate}}
|
{{ 'footer.link.cookies' | translate}}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="btn text-white"
|
||||||
|
routerLink="info/accessibility" role="link" tabindex="0">{{ 'footer.link.accessibility' | translate }}</a>
|
||||||
|
</li>
|
||||||
<li *ngIf="showPrivacyPolicy">
|
<li *ngIf="showPrivacyPolicy">
|
||||||
<a class="btn text-white"
|
<a class="btn text-white"
|
||||||
routerLink="info/privacy" role="link" tabindex="0">{{ 'footer.link.privacy-policy' | translate}}</a>
|
routerLink="info/privacy" role="link" tabindex="0">{{ 'footer.link.privacy-policy' | translate}}</a>
|
||||||
|
@@ -17,6 +17,8 @@ import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock';
|
|||||||
import { storeModuleConfig } from '../app.reducer';
|
import { storeModuleConfig } from '../app.reducer';
|
||||||
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
||||||
import { AuthorizationDataServiceStub } from '../shared/testing/authorization-service.stub';
|
import { AuthorizationDataServiceStub } from '../shared/testing/authorization-service.stub';
|
||||||
|
import { APP_CONFIG } from '../../config/app-config.interface';
|
||||||
|
import { environment } from '../../environments/environment.test';
|
||||||
|
|
||||||
let comp: FooterComponent;
|
let comp: FooterComponent;
|
||||||
let fixture: ComponentFixture<FooterComponent>;
|
let fixture: ComponentFixture<FooterComponent>;
|
||||||
@@ -38,6 +40,7 @@ describe('Footer component', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
FooterComponent,
|
FooterComponent,
|
||||||
{ provide: AuthorizationDataService, useClass: AuthorizationDataServiceStub },
|
{ provide: AuthorizationDataService, useClass: AuthorizationDataServiceStub },
|
||||||
|
{ provide: APP_CONFIG, useValue: environment },
|
||||||
],
|
],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
});
|
});
|
||||||
|
@@ -1,23 +1,25 @@
|
|||||||
import { Component, Optional } from '@angular/core';
|
import { Component, Optional, Inject, OnInit } from '@angular/core';
|
||||||
import { hasValue } from '../shared/empty.util';
|
import { hasValue } from '../shared/empty.util';
|
||||||
import { KlaroService } from '../shared/cookies/klaro.service';
|
import { KlaroService } from '../shared/cookies/klaro.service';
|
||||||
import { environment } from '../../environments/environment';
|
import { environment } from '../../environments/environment';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
||||||
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
||||||
|
import { AppConfig, APP_CONFIG } from '../../config/app-config.interface';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-footer',
|
selector: 'ds-footer',
|
||||||
styleUrls: ['footer.component.scss'],
|
styleUrls: ['footer.component.scss'],
|
||||||
templateUrl: 'footer.component.html'
|
templateUrl: 'footer.component.html'
|
||||||
})
|
})
|
||||||
export class FooterComponent {
|
export class FooterComponent implements OnInit {
|
||||||
dateObj: number = Date.now();
|
dateObj: number = Date.now();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A boolean representing if to show or not the top footer container
|
* A boolean representing if to show or not the top footer container
|
||||||
*/
|
*/
|
||||||
showTopFooter = false;
|
showTopFooter = false;
|
||||||
|
showCookieSettings = false;
|
||||||
showPrivacyPolicy = environment.info.enablePrivacyStatement;
|
showPrivacyPolicy = environment.info.enablePrivacyStatement;
|
||||||
showEndUserAgreement = environment.info.enableEndUserAgreement;
|
showEndUserAgreement = environment.info.enableEndUserAgreement;
|
||||||
showSendFeedback$: Observable<boolean>;
|
showSendFeedback$: Observable<boolean>;
|
||||||
@@ -25,11 +27,16 @@ export class FooterComponent {
|
|||||||
constructor(
|
constructor(
|
||||||
@Optional() private cookies: KlaroService,
|
@Optional() private cookies: KlaroService,
|
||||||
private authorizationService: AuthorizationDataService,
|
private authorizationService: AuthorizationDataService,
|
||||||
|
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
||||||
) {
|
) {
|
||||||
this.showSendFeedback$ = this.authorizationService.isAuthorized(FeatureID.CanSendFeedback);
|
this.showSendFeedback$ = this.authorizationService.isAuthorized(FeatureID.CanSendFeedback);
|
||||||
}
|
}
|
||||||
|
|
||||||
showCookieSettings() {
|
ngOnInit() {
|
||||||
|
this.showCookieSettings = this.appConfig.info.enableCookieConsentPopup;
|
||||||
|
}
|
||||||
|
|
||||||
|
openCookieSettings() {
|
||||||
if (hasValue(this.cookies)) {
|
if (hasValue(this.cookies)) {
|
||||||
this.cookies.showSettings();
|
this.cookies.showSettings();
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,94 @@
|
|||||||
|
<div class="container">
|
||||||
|
<h2>{{ 'info.accessibility-settings.title' | translate }}</h2>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label [for]="'disableNotificationTimeOutInput'" class="col-sm-4 col-form-label">
|
||||||
|
{{ 'info.accessibility-settings.disableNotificationTimeOut.label' | translate }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<ui-switch [id]="'disableNotificationTimeOutInput'"
|
||||||
|
[(ngModel)]="formValues.notificationTimeOutEnabled"
|
||||||
|
[ngModelOptions]="{ standalone: true }"
|
||||||
|
></ui-switch>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *dsContextHelp="{
|
||||||
|
content: 'info.accessibility-settings.disableNotificationTimeOut.hint',
|
||||||
|
id: 'disableNotificationTimeOutHelp',
|
||||||
|
iconPlacement: 'right',
|
||||||
|
tooltipPlacement: 'left'
|
||||||
|
}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<label [for]="'notificationTimeOutInput'" class="col-sm-4 col-form-label">
|
||||||
|
{{ 'info.accessibility-settings.notificationTimeOut.label' | translate }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<input [type]="'number'" [id]="'notificationTimeOutInput'" class="form-control"
|
||||||
|
[ngClass]="{'is-invalid': !settingsService.isValid('notificationTimeOut', formValues)}"
|
||||||
|
[min]="1"
|
||||||
|
[readOnly]="!formValues.notificationTimeOutEnabled"
|
||||||
|
[(ngModel)]="formValues.notificationTimeOut" [ngModelOptions]="{ standalone: true }"
|
||||||
|
[attr.aria-describedby]="'notificationTimeOutHint'">
|
||||||
|
<div class="invalid-feedback" [ngClass]="{ 'd-block': !settingsService.isValid('notificationTimeOut', formValues) }">
|
||||||
|
{{ 'info.accessibility-settings.notificationTimeOut.invalid' | translate }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="col-sm-1" *dsContextHelp="{
|
||||||
|
content: 'info.accessibility-settings.notificationTimeOut.hint',
|
||||||
|
id: 'notificationTimeOutHelp',
|
||||||
|
iconPlacement: 'right',
|
||||||
|
tooltipPlacement: 'left'
|
||||||
|
}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<label [for]="'liveRegionTimeOutInput'" class="col-sm-4 col-form-label">
|
||||||
|
{{ 'info.accessibility-settings.liveRegionTimeOut.label' | translate }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<input [type]="'number'" [id]="'liveRegionTimeOutInput'" class="form-control"
|
||||||
|
[ngClass]="{'is-invalid': !settingsService.isValid('liveRegionTimeOut', formValues)}"
|
||||||
|
[min]="1"
|
||||||
|
[(ngModel)]="formValues.liveRegionTimeOut" [ngModelOptions]="{ standalone: true }"
|
||||||
|
[attr.aria-describedby]="'liveRegionTimeOutHint'">
|
||||||
|
<div class="invalid-feedback" [ngClass]="{ 'd-block': !settingsService.isValid('liveRegionTimeOut', formValues) }">
|
||||||
|
{{ 'info.accessibility-settings.liveRegionTimeOut.invalid' | translate }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="col-sm-1" *dsContextHelp="{
|
||||||
|
content: 'info.accessibility-settings.liveRegionTimeOut.hint',
|
||||||
|
id: 'liveRegionTimeOutHelp',
|
||||||
|
iconPlacement: 'right',
|
||||||
|
tooltipPlacement: 'left'
|
||||||
|
}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div role="group">
|
||||||
|
<button type="submit" (click)="saveSettings()" class="btn btn-primary mr-2">
|
||||||
|
{{ 'info.accessibility-settings.submit' | translate }}
|
||||||
|
</button>
|
||||||
|
<button (click)="resetSettings()" class="btn btn-warning">
|
||||||
|
{{ 'info.accessibility-settings.reset' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div *ngIf="(isAuthenticated | async) === false && (cookieIsAccepted | async) === false" class="mt-2">
|
||||||
|
<ds-alert [type]="AlertType.Warning">{{ 'info.accessibility-settings.cookie-warning' | translate }}</ds-alert>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
@@ -0,0 +1,89 @@
|
|||||||
|
import { AccessibilitySettingsComponent } from './accessibility-settings.component';
|
||||||
|
import { ComponentFixture, waitForAsync, TestBed } from '@angular/core/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { AuthServiceStub } from '../../shared/testing/auth-service.stub';
|
||||||
|
import { getAccessibilitySettingsServiceStub } from '../../accessibility/accessibility-settings.service.stub';
|
||||||
|
import { AccessibilitySettingsService } from '../../accessibility/accessibility-settings.service';
|
||||||
|
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { KlaroServiceStub } from '../../shared/cookies/klaro.service.stub';
|
||||||
|
import { KlaroService } from '../../shared/cookies/klaro.service';
|
||||||
|
|
||||||
|
|
||||||
|
describe('AccessibilitySettingsComponent', () => {
|
||||||
|
let component: AccessibilitySettingsComponent;
|
||||||
|
let fixture: ComponentFixture<AccessibilitySettingsComponent>;
|
||||||
|
|
||||||
|
let authService: AuthServiceStub;
|
||||||
|
let settingsService: AccessibilitySettingsService;
|
||||||
|
let notificationsService: NotificationsServiceStub;
|
||||||
|
let klaroService: KlaroServiceStub;
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
authService = new AuthServiceStub();
|
||||||
|
settingsService = getAccessibilitySettingsServiceStub();
|
||||||
|
notificationsService = new NotificationsServiceStub();
|
||||||
|
klaroService = new KlaroServiceStub();
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot()],
|
||||||
|
declarations: [AccessibilitySettingsComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: AuthService, useValue: authService },
|
||||||
|
{ provide: AccessibilitySettingsService, useValue: settingsService },
|
||||||
|
{ provide: NotificationsService, useValue: notificationsService },
|
||||||
|
{ provide: KlaroService, useValue: klaroService },
|
||||||
|
],
|
||||||
|
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 the current settings', () => {
|
||||||
|
expect(settingsService.getAll).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert retrieved settings to form format', () => {
|
||||||
|
expect(settingsService.convertStoredValuesToFormValues).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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 convert form settings to stored format', () => {
|
||||||
|
settingsService.setSettings = jasmine.createSpy('setSettings').and.returnValue(of('cookie'));
|
||||||
|
component.saveSettings();
|
||||||
|
expect(settingsService.convertFormValuesToStoredValues).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should give the user a notification mentioning where the settings were saved', () => {
|
||||||
|
settingsService.setSettings = jasmine.createSpy('setSettings').and.returnValue(of('cookie'));
|
||||||
|
component.saveSettings();
|
||||||
|
expect(notificationsService.success).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should give the user a notification mentioning why saving failed, if it failed', () => {
|
||||||
|
settingsService.setSettings = jasmine.createSpy('setSettings').and.returnValue(of('failed'));
|
||||||
|
component.saveSettings();
|
||||||
|
expect(notificationsService.error).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,118 @@
|
|||||||
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
import {
|
||||||
|
AccessibilitySetting,
|
||||||
|
AccessibilitySettingsService,
|
||||||
|
AccessibilitySettingsFormValues,
|
||||||
|
} from '../../accessibility/accessibility-settings.service';
|
||||||
|
import { BehaviorSubject, distinctUntilChanged, map, Subscription, take } from 'rxjs';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { isEmpty } from 'src/app/shared/empty.util';
|
||||||
|
import { AlertType } from '../../shared/alert/alert-type';
|
||||||
|
import { KlaroService } from '../../shared/cookies/klaro.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component providing the form where users can update accessibility settings.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-accessibility-settings',
|
||||||
|
templateUrl: './accessibility-settings.component.html'
|
||||||
|
})
|
||||||
|
export class AccessibilitySettingsComponent implements OnInit, OnDestroy {
|
||||||
|
// Redeclared for use in template
|
||||||
|
protected readonly AlertType = AlertType;
|
||||||
|
|
||||||
|
protected formValues: AccessibilitySettingsFormValues;
|
||||||
|
|
||||||
|
isAuthenticated: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||||
|
cookieIsAccepted: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||||
|
|
||||||
|
private subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected authService: AuthService,
|
||||||
|
protected settingsService: AccessibilitySettingsService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected translateService: TranslateService,
|
||||||
|
protected klaroService: KlaroService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.updateFormValues();
|
||||||
|
|
||||||
|
this.subscriptions.push(
|
||||||
|
this.authService.isAuthenticated().pipe(distinctUntilChanged())
|
||||||
|
.subscribe(val => this.isAuthenticated.next(val)),
|
||||||
|
this.klaroService.getSavedPreferences().pipe(
|
||||||
|
map(preferences => preferences?.accessibility === true),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
).subscribe(val => this.cookieIsAccepted.next(val)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the user-configured settings
|
||||||
|
*/
|
||||||
|
saveSettings() {
|
||||||
|
const formValues = this.formValues;
|
||||||
|
|
||||||
|
if (this.settingsService.formValuesValid(formValues)) {
|
||||||
|
const convertedValues = this.settingsService.convertFormValuesToStoredValues(formValues);
|
||||||
|
this.settingsService.setSettings(convertedValues).pipe(take(1)).subscribe(location => {
|
||||||
|
if (location !== 'failed') {
|
||||||
|
this.notificationsService.success(null, this.translateService.instant('info.accessibility-settings.save-notification.' + location));
|
||||||
|
this.updateFormValues();
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(null, this.translateService.instant('info.accessibility-settings.failed-notification'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(
|
||||||
|
null,
|
||||||
|
this.translateService.instant('info.accessibility-settings.invalid-form-notification'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the form values with the currently stored accessibility settings and sets the default values for settings
|
||||||
|
* that have no stored value.
|
||||||
|
*/
|
||||||
|
updateFormValues() {
|
||||||
|
this.settingsService.getAll().pipe(take(1)).subscribe(storedSettings => {
|
||||||
|
const formValues = this.settingsService.convertStoredValuesToFormValues(storedSettings);
|
||||||
|
|
||||||
|
const settingsRequiringDefaultValue: AccessibilitySetting[] = ['notificationTimeOut', 'liveRegionTimeOut'];
|
||||||
|
|
||||||
|
for (const setting of settingsRequiringDefaultValue) {
|
||||||
|
if (isEmpty(formValues[setting])) {
|
||||||
|
const defaultValue = this.settingsService.getDefaultValue(setting);
|
||||||
|
formValues[setting] = defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.formValues = formValues;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets accessibility settings
|
||||||
|
*/
|
||||||
|
resetSettings() {
|
||||||
|
this.settingsService.clearSettings().pipe(take(1)).subscribe(([cookieReset, metadataReset]) => {
|
||||||
|
if (cookieReset === 'failed' && metadataReset === 'failed') {
|
||||||
|
this.notificationsService.warning(null, this.translateService.instant('info.accessibility-settings.reset-failed'));
|
||||||
|
} else {
|
||||||
|
this.notificationsService.success(null, this.translateService.instant('info.accessibility-settings.reset-notification'));
|
||||||
|
this.updateFormValues();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -3,6 +3,7 @@ import { getInfoModulePath } from '../app-routing-paths';
|
|||||||
export const END_USER_AGREEMENT_PATH = 'end-user-agreement';
|
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 ACCESSIBILITY_SETTINGS_PATH = 'accessibility';
|
||||||
|
|
||||||
export function getEndUserAgreementPath() {
|
export function getEndUserAgreementPath() {
|
||||||
return getSubPath(END_USER_AGREEMENT_PATH);
|
return getSubPath(END_USER_AGREEMENT_PATH);
|
||||||
@@ -16,6 +17,10 @@ export function getFeedbackPath() {
|
|||||||
return getSubPath(FEEDBACK_PATH);
|
return getSubPath(FEEDBACK_PATH);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getAccessibilitySettingsPath() {
|
||||||
|
return getSubPath(ACCESSIBILITY_SETTINGS_PATH);
|
||||||
|
}
|
||||||
|
|
||||||
function getSubPath(path: string) {
|
function getSubPath(path: string) {
|
||||||
return `${getInfoModulePath()}/${path}`;
|
return `${getInfoModulePath()}/${path}`;
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,18 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
import { PRIVACY_PATH, END_USER_AGREEMENT_PATH, FEEDBACK_PATH } from './info-routing-paths';
|
import {
|
||||||
|
PRIVACY_PATH,
|
||||||
|
END_USER_AGREEMENT_PATH,
|
||||||
|
FEEDBACK_PATH,
|
||||||
|
ACCESSIBILITY_SETTINGS_PATH
|
||||||
|
} from './info-routing-paths';
|
||||||
import { ThemedEndUserAgreementComponent } from './end-user-agreement/themed-end-user-agreement.component';
|
import { ThemedEndUserAgreementComponent } from './end-user-agreement/themed-end-user-agreement.component';
|
||||||
import { ThemedPrivacyComponent } from './privacy/themed-privacy.component';
|
import { ThemedPrivacyComponent } from './privacy/themed-privacy.component';
|
||||||
import { ThemedFeedbackComponent } from './feedback/themed-feedback.component';
|
import { ThemedFeedbackComponent } from './feedback/themed-feedback.component';
|
||||||
import { FeedbackGuard } from '../core/feedback/feedback.guard';
|
import { FeedbackGuard } from '../core/feedback/feedback.guard';
|
||||||
import { environment } from '../../environments/environment';
|
import { environment } from '../../environments/environment';
|
||||||
|
import { AccessibilitySettingsComponent } from './accessibility-settings/accessibility-settings.component';
|
||||||
|
|
||||||
|
|
||||||
const imports = [
|
const imports = [
|
||||||
@@ -17,7 +23,13 @@ const imports = [
|
|||||||
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||||
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' },
|
||||||
|
},
|
||||||
])
|
])
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@@ -13,6 +13,8 @@ import { FeedbackFormComponent } from './feedback/feedback-form/feedback-form.co
|
|||||||
import { ThemedFeedbackFormComponent } from './feedback/feedback-form/themed-feedback-form.component';
|
import { ThemedFeedbackFormComponent } from './feedback/feedback-form/themed-feedback-form.component';
|
||||||
import { ThemedFeedbackComponent } from './feedback/themed-feedback.component';
|
import { ThemedFeedbackComponent } from './feedback/themed-feedback.component';
|
||||||
import { FeedbackGuard } from '../core/feedback/feedback.guard';
|
import { FeedbackGuard } from '../core/feedback/feedback.guard';
|
||||||
|
import { AccessibilitySettingsComponent } from './accessibility-settings/accessibility-settings.component';
|
||||||
|
import { UiSwitchModule } from 'ngx-ui-switch';
|
||||||
|
|
||||||
|
|
||||||
const DECLARATIONS = [
|
const DECLARATIONS = [
|
||||||
@@ -25,7 +27,8 @@ const DECLARATIONS = [
|
|||||||
FeedbackComponent,
|
FeedbackComponent,
|
||||||
FeedbackFormComponent,
|
FeedbackFormComponent,
|
||||||
ThemedFeedbackFormComponent,
|
ThemedFeedbackFormComponent,
|
||||||
ThemedFeedbackComponent
|
ThemedFeedbackComponent,
|
||||||
|
AccessibilitySettingsComponent,
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@@ -33,6 +36,7 @@ const DECLARATIONS = [
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
InfoRoutingModule,
|
InfoRoutingModule,
|
||||||
|
UiSwitchModule,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
...DECLARATIONS
|
...DECLARATIONS
|
||||||
|
@@ -28,10 +28,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">
|
||||||
|
<ds-alert class="mb-4" [type]="'alert-info'">{{'profile.card.accessibility.content' | translate}}</ds-alert>
|
||||||
|
<a [routerLink]="'/info/accessibility'">{{'profile.card.accessibility.link' | translate}}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ng-container *ngIf="(groupsRD$ | async) as groupsRD;">
|
<ng-container *ngIf="(groupsRD$ | async) as groupsRD;">
|
||||||
<ng-container *ngTemplateOutlet="groupsRD?.isLoading ? loader : content"></ng-container>
|
<ng-container *ngTemplateOutlet="groupsRD?.isLoading ? loader : content"></ng-container>
|
||||||
<ng-template #content>
|
<ng-template #content>
|
||||||
|
@@ -145,7 +145,12 @@ export class BrowserKlaroService extends KlaroService {
|
|||||||
*/
|
*/
|
||||||
this.translateConfiguration();
|
this.translateConfiguration();
|
||||||
|
|
||||||
|
if (!environment.info?.enableCookieConsentPopup) {
|
||||||
|
this.klaroConfig.services = [];
|
||||||
|
} else {
|
||||||
this.klaroConfig.services = this.filterConfigServices(servicesToHide);
|
this.klaroConfig.services = this.filterConfigServices(servicesToHide);
|
||||||
|
}
|
||||||
|
|
||||||
this.lazyKlaro.then(({ setup }) => setup(this.klaroConfig));
|
this.lazyKlaro.then(({ setup }) => setup(this.klaroConfig));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,7 @@ import { TOKENITEM } from '../../core/auth/models/auth-token-info.model';
|
|||||||
import { IMPERSONATING_COOKIE, REDIRECT_COOKIE } from '../../core/auth/auth.service';
|
import { IMPERSONATING_COOKIE, REDIRECT_COOKIE } from '../../core/auth/auth.service';
|
||||||
import { LANG_COOKIE } from '../../core/locale/locale.service';
|
import { LANG_COOKIE } from '../../core/locale/locale.service';
|
||||||
import { CAPTCHA_COOKIE, CAPTCHA_NAME } from '../../core/google-recaptcha/google-recaptcha.service';
|
import { CAPTCHA_COOKIE, CAPTCHA_NAME } from '../../core/google-recaptcha/google-recaptcha.service';
|
||||||
|
import { ACCESSIBILITY_COOKIE } from '../../accessibility/accessibility-settings.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cookie for has_agreed_end_user
|
* Cookie for has_agreed_end_user
|
||||||
@@ -197,6 +198,13 @@ export const klaroConfiguration: any = {
|
|||||||
onAccept: `window.refreshCaptchaScript?.call()`,
|
onAccept: `window.refreshCaptchaScript?.call()`,
|
||||||
onDecline: `window.refreshCaptchaScript?.call()`,
|
onDecline: `window.refreshCaptchaScript?.call()`,
|
||||||
onlyOnce: true,
|
onlyOnce: true,
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
name: 'accessibility',
|
||||||
|
purposes: ['functional'],
|
||||||
|
required: false,
|
||||||
|
cookies: [ACCESSIBILITY_COOKIE],
|
||||||
|
onlyOnce: false,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
9
src/app/shared/cookies/klaro.service.stub.ts
Normal file
9
src/app/shared/cookies/klaro.service.stub.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
|
export class KlaroServiceStub {
|
||||||
|
initialize = jasmine.createSpy('initialize');
|
||||||
|
|
||||||
|
showSettings = jasmine.createSpy('showSettings');
|
||||||
|
|
||||||
|
getSavedPreferences = jasmine.createSpy('getSavedPreferences').and.returnValue(of({}));
|
||||||
|
}
|
@@ -32,6 +32,8 @@ import { DynamicRowArrayModel } from '../ds-dynamic-row-array-model';
|
|||||||
import { DsDynamicFormArrayComponent } from './dynamic-form-array.component';
|
import { DsDynamicFormArrayComponent } from './dynamic-form-array.component';
|
||||||
import { UUIDService } from '../../../../../../core/shared/uuid.service';
|
import { UUIDService } from '../../../../../../core/shared/uuid.service';
|
||||||
import { TranslateLoaderMock } from '../../../../../mocks/translate-loader.mock';
|
import { TranslateLoaderMock } from '../../../../../mocks/translate-loader.mock';
|
||||||
|
import { LiveRegionService } from '../../../../../live-region/live-region.service';
|
||||||
|
import { getLiveRegionServiceStub } from '../../../../../live-region/live-region.service.stub';
|
||||||
|
|
||||||
describe('DsDynamicFormArrayComponent', () => {
|
describe('DsDynamicFormArrayComponent', () => {
|
||||||
const translateServiceStub = {
|
const translateServiceStub = {
|
||||||
@@ -73,6 +75,7 @@ describe('DsDynamicFormArrayComponent', () => {
|
|||||||
{ provide: SubmissionService, useValue: {} },
|
{ provide: SubmissionService, useValue: {} },
|
||||||
{ provide: APP_CONFIG, useValue: environment },
|
{ provide: APP_CONFIG, useValue: environment },
|
||||||
{ provide: UUIDService, useValue: uuidServiceStub },
|
{ provide: UUIDService, useValue: uuidServiceStub },
|
||||||
|
{ provide: LiveRegionService, useValue: getLiveRegionServiceStub() },
|
||||||
],
|
],
|
||||||
}).overrideComponent(DsDynamicFormArrayComponent, {
|
}).overrideComponent(DsDynamicFormArrayComponent, {
|
||||||
remove: {
|
remove: {
|
||||||
|
@@ -1,13 +1,22 @@
|
|||||||
import { LiveRegionService } from './live-region.service';
|
import { LiveRegionService } from './live-region.service';
|
||||||
import { fakeAsync, tick, flush } from '@angular/core/testing';
|
import { fakeAsync, tick } from '@angular/core/testing';
|
||||||
import { UUIDService } from '../../core/shared/uuid.service';
|
import { UUIDService } from '../../core/shared/uuid.service';
|
||||||
|
import { getAccessibilitySettingsServiceStub } from '../../accessibility/accessibility-settings.service.stub';
|
||||||
|
import { AccessibilitySettingsService } from '../../accessibility/accessibility-settings.service';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
describe('liveRegionService', () => {
|
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,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -81,13 +90,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) => {
|
||||||
@@ -114,45 +126,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', () => {
|
||||||
|
@@ -1,7 +1,10 @@
|
|||||||
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 { UUIDService } from '../../core/shared/uuid.service';
|
import { UUIDService } from '../../core/shared/uuid.service';
|
||||||
|
import { AccessibilitySettingsService } from '../../accessibility/accessibility-settings.service';
|
||||||
|
|
||||||
|
export const MIN_MESSAGE_DURATION = 200;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The LiveRegionService is responsible for handling the messages that are shown by the {@link LiveRegionComponent}.
|
* The LiveRegionService is responsible for handling the messages that are shown by the {@link LiveRegionComponent}.
|
||||||
@@ -14,6 +17,7 @@ export class LiveRegionService {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected uuidService: UUIDService,
|
protected uuidService: UUIDService,
|
||||||
|
protected accessibilitySettingsService: AccessibilitySettingsService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +68,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;
|
||||||
}
|
}
|
||||||
@@ -115,6 +124,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(
|
||||||
|
'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
|
||||||
*/
|
*/
|
||||||
|
@@ -18,6 +18,8 @@ import { cold } from 'jasmine-marbles';
|
|||||||
import { LiveRegionService } from '../../live-region/live-region.service';
|
import { LiveRegionService } from '../../live-region/live-region.service';
|
||||||
import { LiveRegionServiceStub } from '../../live-region/live-region.service.stub';
|
import { LiveRegionServiceStub } from '../../live-region/live-region.service.stub';
|
||||||
import { NotificationOptions } from '../models/notification-options.model';
|
import { NotificationOptions } from '../models/notification-options.model';
|
||||||
|
import { AccessibilitySettingsService } from '../../../accessibility/accessibility-settings.service';
|
||||||
|
import { getAccessibilitySettingsServiceStub } from '../../../accessibility/accessibility-settings.service.stub';
|
||||||
|
|
||||||
export const bools = { f: false, t: true };
|
export const bools = { f: false, t: true };
|
||||||
|
|
||||||
@@ -43,6 +45,7 @@ describe('NotificationsBoardComponent', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{ provide: NotificationsService, useClass: NotificationsServiceStub },
|
{ provide: NotificationsService, useClass: NotificationsServiceStub },
|
||||||
{ provide: LiveRegionService, useValue: liveRegionService },
|
{ provide: LiveRegionService, useValue: liveRegionService },
|
||||||
|
{ provide: AccessibilitySettingsService, useValue: getAccessibilitySettingsServiceStub() },
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
]
|
]
|
||||||
}).compileComponents(); // compile template and css
|
}).compileComponents(); // compile template and css
|
||||||
|
@@ -9,8 +9,7 @@ import {
|
|||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
|
||||||
import { select, Store } from '@ngrx/store';
|
import { select, Store } from '@ngrx/store';
|
||||||
import { BehaviorSubject, Subscription, of as observableOf } from 'rxjs';
|
import { BehaviorSubject, Subscription, take, of as observableOf } from 'rxjs';
|
||||||
import difference from 'lodash/difference';
|
|
||||||
|
|
||||||
import { NotificationsService } from '../notifications.service';
|
import { NotificationsService } from '../notifications.service';
|
||||||
import { AppState } from '../../../app.reducer';
|
import { AppState } from '../../../app.reducer';
|
||||||
@@ -20,7 +19,11 @@ import { NotificationsState } from '../notifications.reducers';
|
|||||||
import { INotificationBoardOptions } from '../../../../config/notifications-config.interfaces';
|
import { INotificationBoardOptions } from '../../../../config/notifications-config.interfaces';
|
||||||
import { LiveRegionService } from '../../live-region/live-region.service';
|
import { LiveRegionService } from '../../live-region/live-region.service';
|
||||||
import { hasNoValue, isNotEmptyOperator } from '../../empty.util';
|
import { hasNoValue, isNotEmptyOperator } from '../../empty.util';
|
||||||
import { take } from 'rxjs/operators';
|
import {
|
||||||
|
AccessibilitySettingsService
|
||||||
|
} from '../../../accessibility/accessibility-settings.service';
|
||||||
|
import cloneDeep from 'lodash/cloneDeep';
|
||||||
|
import differenceWith from 'lodash/differenceWith';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-notifications-board',
|
selector: 'ds-notifications-board',
|
||||||
@@ -53,10 +56,11 @@ export class NotificationsBoardComponent implements OnInit, OnDestroy {
|
|||||||
public isPaused$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
public isPaused$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private service: NotificationsService,
|
protected service: NotificationsService,
|
||||||
private store: Store<AppState>,
|
protected store: Store<AppState>,
|
||||||
private cdr: ChangeDetectorRef,
|
protected cdr: ChangeDetectorRef,
|
||||||
protected liveRegionService: LiveRegionService,
|
protected liveRegionService: LiveRegionService,
|
||||||
|
protected accessibilitySettingsService: AccessibilitySettingsService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,13 +71,13 @@ export class NotificationsBoardComponent implements OnInit, OnDestroy {
|
|||||||
this.notifications = [];
|
this.notifications = [];
|
||||||
} else if (state.length > this.notifications.length) {
|
} else if (state.length > this.notifications.length) {
|
||||||
// Add
|
// Add
|
||||||
const newElem = difference(state, this.notifications);
|
const newElem = differenceWith(state, this.notifications, this.byId);
|
||||||
newElem.forEach((notification) => {
|
newElem.forEach((notification) => {
|
||||||
this.add(notification);
|
this.add(notification);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Remove
|
// Remove
|
||||||
const delElem = difference(this.notifications, state);
|
const delElem = differenceWith(this.notifications, state, this.byId);
|
||||||
delElem.forEach((notification) => {
|
delElem.forEach((notification) => {
|
||||||
this.notifications = this.notifications.filter((item: INotification) => item.id !== notification.id);
|
this.notifications = this.notifications.filter((item: INotification) => item.id !== notification.id);
|
||||||
|
|
||||||
@@ -83,6 +87,9 @@ export class NotificationsBoardComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private byId = (notificationA: INotification, notificationB: INotification) =>
|
||||||
|
notificationA.id === notificationB.id;
|
||||||
|
|
||||||
// Add the new notification to the notification array
|
// Add the new notification to the notification array
|
||||||
add(item: INotification): void {
|
add(item: INotification): void {
|
||||||
const toBlock: boolean = this.block(item);
|
const toBlock: boolean = this.block(item);
|
||||||
@@ -90,8 +97,23 @@ 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);
|
|
||||||
this.addContentToLiveRegion(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('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.addContentToLiveRegion(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
|
||||||
|
@@ -20,6 +20,8 @@ import { PaginationService } from '../../../../../../core/pagination/pagination.
|
|||||||
import { PaginationServiceStub } from '../../../../../testing/pagination-service.stub';
|
import { PaginationServiceStub } from '../../../../../testing/pagination-service.stub';
|
||||||
import { ShortNumberPipe } from '../../../../../utils/short-number.pipe';
|
import { ShortNumberPipe } from '../../../../../utils/short-number.pipe';
|
||||||
import { UUIDService } from '../../../../../../core/shared/uuid.service';
|
import { UUIDService } from '../../../../../../core/shared/uuid.service';
|
||||||
|
import { LiveRegionService } from '../../../../../live-region/live-region.service';
|
||||||
|
import { getLiveRegionServiceStub } from '../../../../../live-region/live-region.service.stub';
|
||||||
|
|
||||||
describe('SearchFacetOptionComponent', () => {
|
describe('SearchFacetOptionComponent', () => {
|
||||||
let comp: SearchFacetOptionComponent;
|
let comp: SearchFacetOptionComponent;
|
||||||
@@ -116,6 +118,7 @@ describe('SearchFacetOptionComponent', () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ provide: UUIDService, useClass: UUIDService },
|
{ provide: UUIDService, useClass: UUIDService },
|
||||||
|
{ provide: LiveRegionService, useValue: getLiveRegionServiceStub() },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).overrideComponent(SearchFacetOptionComponent, {
|
}).overrideComponent(SearchFacetOptionComponent, {
|
||||||
|
@@ -55,6 +55,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 : ''}`;
|
||||||
}
|
}
|
||||||
|
@@ -1395,6 +1395,10 @@
|
|||||||
|
|
||||||
"cookies.consent.content-modal.service": "service",
|
"cookies.consent.content-modal.service": "service",
|
||||||
|
|
||||||
|
"cookies.consent.app.title.accessibility": "Accessibility Settings",
|
||||||
|
|
||||||
|
"cookies.consent.app.description.accessibility": "Required for saving your accessibility settings locally",
|
||||||
|
|
||||||
"cookies.consent.app.title.authentication": "Authentication",
|
"cookies.consent.app.title.authentication": "Authentication",
|
||||||
|
|
||||||
"cookies.consent.app.description.authentication": "Required for signing you in",
|
"cookies.consent.app.description.authentication": "Required for signing you in",
|
||||||
@@ -1673,6 +1677,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",
|
||||||
@@ -1905,6 +1911,44 @@
|
|||||||
|
|
||||||
"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.cookie-warning": "Saving the accessibility settings is currently not possible. Either log in to save the settings in user data, or accept the 'Accessibility Settings' cookie using the 'Cookie Settings' menu at the bottom of the page. Once the cookie has been accepted, you can reload the page to remove this message.",
|
||||||
|
|
||||||
|
"info.accessibility-settings.disableNotificationTimeOut.label": "Automatically close notifications after time out",
|
||||||
|
|
||||||
|
"info.accessibility-settings.disableNotificationTimeOut.hint": "When this toggle is activated, notifications will close automatically after the time out passes. When deactivated, notifications will remain open untill manually closed.",
|
||||||
|
|
||||||
|
"info.accessibility-settings.failed-notification": "Failed to save accessibility settings",
|
||||||
|
|
||||||
|
"info.accessibility-settings.invalid-form-notification": "Did not save. The form contains invalid values.",
|
||||||
|
|
||||||
|
"info.accessibility-settings.liveRegionTimeOut.label": "ARIA Live region time out (in seconds)",
|
||||||
|
|
||||||
|
"info.accessibility-settings.liveRegionTimeOut.hint": "The duration after which a message in the ARIA live region disappears. ARIA live regions are not visible on the page, but provide announcements of notifications (or other actions) to screen readers.",
|
||||||
|
|
||||||
|
"info.accessibility-settings.liveRegionTimeOut.invalid": "Live region time out must be greater than 0",
|
||||||
|
|
||||||
|
"info.accessibility-settings.notificationTimeOut.label": "Notification time out (in seconds)",
|
||||||
|
|
||||||
|
"info.accessibility-settings.notificationTimeOut.hint": "The duration after which a notification disappears.",
|
||||||
|
|
||||||
|
"info.accessibility-settings.notificationTimeOut.invalid": "Notification time out must be greater than 0",
|
||||||
|
|
||||||
|
"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.reset-failed": "Failed to reset. Either log in or accept the 'Accessibility Settings' cookie.",
|
||||||
|
|
||||||
|
"info.accessibility-settings.reset-notification": "Successfully reset settings.",
|
||||||
|
|
||||||
|
"info.accessibility-settings.reset": "Reset accessibility settings",
|
||||||
|
|
||||||
|
"info.accessibility-settings.submit": "Save accessibility settings",
|
||||||
|
|
||||||
|
"info.accessibility-settings.title": "Accessibility settings",
|
||||||
|
|
||||||
"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",
|
||||||
@@ -3381,6 +3425,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": "Go to Accessibility Settings Page",
|
||||||
|
|
||||||
"profile.card.identify": "Identify",
|
"profile.card.identify": "Identify",
|
||||||
|
|
||||||
"profile.card.security": "Security",
|
"profile.card.security": "Security",
|
||||||
|
@@ -24,6 +24,7 @@ import { FilterVocabularyConfig } from './filter-vocabulary-config';
|
|||||||
import { DiscoverySortConfig } from './discovery-sort.config';
|
import { DiscoverySortConfig } from './discovery-sort.config';
|
||||||
import { LiveRegionConfig } from '../app/shared/live-region/live-region.config';
|
import { LiveRegionConfig } from '../app/shared/live-region/live-region.config';
|
||||||
import { SearchConfig } from './search-page-config.interface';
|
import { SearchConfig } from './search-page-config.interface';
|
||||||
|
import { AccessibilitySettingsConfig } from '../app/accessibility/accessibility-settings.config';
|
||||||
|
|
||||||
interface AppConfig extends Config {
|
interface AppConfig extends Config {
|
||||||
ui: UIServerConfig;
|
ui: UIServerConfig;
|
||||||
@@ -52,6 +53,7 @@ interface AppConfig extends Config {
|
|||||||
comcolSelectionSort: DiscoverySortConfig;
|
comcolSelectionSort: DiscoverySortConfig;
|
||||||
liveRegion: LiveRegionConfig;
|
liveRegion: LiveRegionConfig;
|
||||||
search: SearchConfig
|
search: SearchConfig
|
||||||
|
accessibility: AccessibilitySettingsConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -24,6 +24,7 @@ import { FilterVocabularyConfig } from './filter-vocabulary-config';
|
|||||||
import { DiscoverySortConfig } from './discovery-sort.config';
|
import { DiscoverySortConfig } from './discovery-sort.config';
|
||||||
import { LiveRegionConfig } from '../app/shared/live-region/live-region.config';
|
import { LiveRegionConfig } from '../app/shared/live-region/live-region.config';
|
||||||
import { SearchConfig } from './search-page-config.interface';
|
import { SearchConfig } from './search-page-config.interface';
|
||||||
|
import { AccessibilitySettingsConfig } from '../app/accessibility/accessibility-settings.config';
|
||||||
|
|
||||||
export class DefaultAppConfig implements AppConfig {
|
export class DefaultAppConfig implements AppConfig {
|
||||||
production = false;
|
production = false;
|
||||||
@@ -411,7 +412,8 @@ export class DefaultAppConfig implements AppConfig {
|
|||||||
// - All mentions of the privacy policy being removed from the UI (e.g. in the footer)
|
// - All mentions of the privacy policy being removed from the UI (e.g. in the footer)
|
||||||
info: InfoConfig = {
|
info: InfoConfig = {
|
||||||
enableEndUserAgreement: true,
|
enableEndUserAgreement: true,
|
||||||
enablePrivacyStatement: true
|
enablePrivacyStatement: true,
|
||||||
|
enableCookieConsentPopup: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Whether to enable Markdown (https://commonmark.org/) and MathJax (https://www.mathjax.org/)
|
// Whether to enable Markdown (https://commonmark.org/) and MathJax (https://www.mathjax.org/)
|
||||||
@@ -447,4 +449,9 @@ export class DefaultAppConfig implements AppConfig {
|
|||||||
search: SearchConfig = {
|
search: SearchConfig = {
|
||||||
filterPlaceholdersCount: 5
|
filterPlaceholdersCount: 5
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Accessibility settings configuration, used by the AccessibilitySettingsService
|
||||||
|
accessibility: AccessibilitySettingsConfig = {
|
||||||
|
cookieExpirationDuration: 7,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@@ -3,4 +3,5 @@ import { Config } from './config.interface';
|
|||||||
export interface InfoConfig extends Config {
|
export interface InfoConfig extends Config {
|
||||||
enableEndUserAgreement: boolean;
|
enableEndUserAgreement: boolean;
|
||||||
enablePrivacyStatement: boolean;
|
enablePrivacyStatement: boolean;
|
||||||
|
enableCookieConsentPopup: boolean;
|
||||||
}
|
}
|
||||||
|
@@ -321,6 +321,7 @@ export const environment: BuildConfig = {
|
|||||||
info: {
|
info: {
|
||||||
enableEndUserAgreement: true,
|
enableEndUserAgreement: true,
|
||||||
enablePrivacyStatement: true,
|
enablePrivacyStatement: true,
|
||||||
|
enableCookieConsentPopup: true,
|
||||||
},
|
},
|
||||||
markdown: {
|
markdown: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@@ -346,5 +347,9 @@ export const environment: BuildConfig = {
|
|||||||
|
|
||||||
search: {
|
search: {
|
||||||
filterPlaceholdersCount: 5
|
filterPlaceholdersCount: 5
|
||||||
}
|
},
|
||||||
|
|
||||||
|
accessibility: {
|
||||||
|
cookieExpirationDuration: 7,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user