mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge branch 'main' into CST-6565
# Conflicts: # src/app/shared/cookies/browser-klaro.service.ts
This commit is contained in:
@@ -179,7 +179,7 @@ If needing to update default configurations values for production, update local
|
|||||||
|
|
||||||
- Update `environment.production.ts` file in `src/environment/` for a `production` environment;
|
- Update `environment.production.ts` file in `src/environment/` for a `production` environment;
|
||||||
|
|
||||||
The environment object is provided for use as import in code and is extended with he runtime configuration on bootstrap of the application.
|
The environment object is provided for use as import in code and is extended with the runtime configuration on bootstrap of the application.
|
||||||
|
|
||||||
> Take caution moving runtime configs into the buildtime configuration. They will be overwritten by what is defined in the runtime config on bootstrap.
|
> Take caution moving runtime configs into the buildtime configuration. They will be overwritten by what is defined in the runtime config on bootstrap.
|
||||||
|
|
||||||
|
@@ -150,6 +150,9 @@ languages:
|
|||||||
- code: fi
|
- code: fi
|
||||||
label: Suomi
|
label: Suomi
|
||||||
active: true
|
active: true
|
||||||
|
- code: sv
|
||||||
|
label: Svenska
|
||||||
|
active: true
|
||||||
- code: tr
|
- code: tr
|
||||||
label: Türkçe
|
label: Türkçe
|
||||||
active: true
|
active: true
|
||||||
@@ -248,3 +251,10 @@ bundle:
|
|||||||
mediaViewer:
|
mediaViewer:
|
||||||
image: false
|
image: false
|
||||||
video: false
|
video: false
|
||||||
|
|
||||||
|
# Whether the end user agreement is required before users use the repository.
|
||||||
|
# If enabled, the user will be required to accept the agreement before they can use the repository.
|
||||||
|
# And whether the privacy statement should exist or not.
|
||||||
|
info:
|
||||||
|
enableEndUserAgreement: true
|
||||||
|
enablePrivacyStatement: true
|
||||||
|
@@ -107,7 +107,7 @@
|
|||||||
"mirador": "^3.3.0",
|
"mirador": "^3.3.0",
|
||||||
"mirador-dl-plugin": "^0.13.0",
|
"mirador-dl-plugin": "^0.13.0",
|
||||||
"mirador-share-plugin": "^0.11.0",
|
"mirador-share-plugin": "^0.11.0",
|
||||||
"moment": "^2.29.2",
|
"moment": "^2.29.4",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"ng-mocks": "^13.1.1",
|
"ng-mocks": "^13.1.1",
|
||||||
"ng2-file-upload": "1.4.0",
|
"ng2-file-upload": "1.4.0",
|
||||||
|
@@ -32,7 +32,6 @@ import { storeModuleConfig } from './app.reducer';
|
|||||||
import { LocaleService } from './core/locale/locale.service';
|
import { LocaleService } from './core/locale/locale.service';
|
||||||
import { authReducer } from './core/auth/auth.reducer';
|
import { authReducer } from './core/auth/auth.reducer';
|
||||||
import { provideMockStore } from '@ngrx/store/testing';
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
import { GoogleAnalyticsService } from './statistics/google-analytics.service';
|
|
||||||
import { ThemeService } from './shared/theme-support/theme.service';
|
import { ThemeService } from './shared/theme-support/theme.service';
|
||||||
import { getMockThemeService } from './shared/mocks/theme-service.mock';
|
import { getMockThemeService } from './shared/mocks/theme-service.mock';
|
||||||
import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service';
|
import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service';
|
||||||
@@ -46,16 +45,16 @@ const initialState = {
|
|||||||
core: { auth: { loading: false } }
|
core: { auth: { loading: false } }
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('App component', () => {
|
export function getMockLocaleService(): LocaleService {
|
||||||
|
|
||||||
let breadcrumbsServiceSpy;
|
|
||||||
|
|
||||||
function getMockLocaleService(): LocaleService {
|
|
||||||
return jasmine.createSpyObj('LocaleService', {
|
return jasmine.createSpyObj('LocaleService', {
|
||||||
setCurrentLanguageCode: jasmine.createSpy('setCurrentLanguageCode')
|
setCurrentLanguageCode: jasmine.createSpy('setCurrentLanguageCode')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
describe('App component', () => {
|
||||||
|
|
||||||
|
let breadcrumbsServiceSpy;
|
||||||
|
|
||||||
const getDefaultTestBedConf = () => {
|
const getDefaultTestBedConf = () => {
|
||||||
breadcrumbsServiceSpy = jasmine.createSpyObj(['listenForRouteChanges']);
|
breadcrumbsServiceSpy = jasmine.createSpyObj(['listenForRouteChanges']);
|
||||||
|
|
||||||
@@ -130,66 +129,4 @@ describe('App component', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('the constructor', () => {
|
|
||||||
it('should call breadcrumbsService.listenForRouteChanges', () => {
|
|
||||||
expect(breadcrumbsServiceSpy.listenForRouteChanges).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when GoogleAnalyticsService is provided', () => {
|
|
||||||
let googleAnalyticsSpy;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
// NOTE: Cannot override providers once components have been compiled, so TestBed needs to be reset
|
|
||||||
TestBed.resetTestingModule();
|
|
||||||
TestBed.configureTestingModule(getDefaultTestBedConf());
|
|
||||||
googleAnalyticsSpy = jasmine.createSpyObj('googleAnalyticsService', [
|
|
||||||
'addTrackingIdToPage',
|
|
||||||
]);
|
|
||||||
TestBed.overrideProvider(GoogleAnalyticsService, {useValue: googleAnalyticsSpy});
|
|
||||||
fixture = TestBed.createComponent(AppComponent);
|
|
||||||
comp = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create component', () => {
|
|
||||||
expect(comp).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('the constructor', () => {
|
|
||||||
it('should call googleAnalyticsService.addTrackingIdToPage()', () => {
|
|
||||||
expect(googleAnalyticsSpy.addTrackingIdToPage).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when ThemeService returns a custom theme', () => {
|
|
||||||
let document;
|
|
||||||
let headSpy;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
// NOTE: Cannot override providers once components have been compiled, so TestBed needs to be reset
|
|
||||||
TestBed.resetTestingModule();
|
|
||||||
TestBed.configureTestingModule(getDefaultTestBedConf());
|
|
||||||
TestBed.overrideProvider(ThemeService, {useValue: getMockThemeService('custom')});
|
|
||||||
document = TestBed.inject(DOCUMENT);
|
|
||||||
headSpy = jasmine.createSpyObj('head', ['appendChild', 'getElementsByClassName']);
|
|
||||||
headSpy.getElementsByClassName.and.returnValue([]);
|
|
||||||
spyOn(document, 'getElementsByTagName').and.returnValue([headSpy]);
|
|
||||||
fixture = TestBed.createComponent(AppComponent);
|
|
||||||
comp = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should append a link element with the correct attributes to the head element', () => {
|
|
||||||
const link = document.createElement('link');
|
|
||||||
link.setAttribute('rel', 'stylesheet');
|
|
||||||
link.setAttribute('type', 'text/css');
|
|
||||||
link.setAttribute('class', 'theme-css');
|
|
||||||
link.setAttribute('href', 'custom-theme.css');
|
|
||||||
|
|
||||||
expect(headSpy.appendChild).toHaveBeenCalledWith(link);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { distinctUntilChanged, filter, switchMap, take, withLatestFrom } from 'rxjs/operators';
|
import { distinctUntilChanged, take, withLatestFrom } from 'rxjs/operators';
|
||||||
import { DOCUMENT, isPlatformBrowser } from '@angular/common';
|
import { DOCUMENT, isPlatformBrowser } from '@angular/common';
|
||||||
import {
|
import {
|
||||||
AfterViewInit,
|
AfterViewInit,
|
||||||
@@ -7,48 +7,30 @@ import {
|
|||||||
HostListener,
|
HostListener,
|
||||||
Inject,
|
Inject,
|
||||||
OnInit,
|
OnInit,
|
||||||
Optional,
|
|
||||||
PLATFORM_ID,
|
PLATFORM_ID,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {
|
import {
|
||||||
ActivatedRouteSnapshot,
|
|
||||||
ActivationEnd,
|
|
||||||
NavigationCancel,
|
NavigationCancel,
|
||||||
NavigationEnd,
|
NavigationEnd,
|
||||||
NavigationStart, ResolveEnd,
|
NavigationStart,
|
||||||
Router,
|
Router,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
|
|
||||||
import { isEqual } from 'lodash';
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
import { BehaviorSubject, Observable, of } from 'rxjs';
|
|
||||||
import { select, Store } from '@ngrx/store';
|
import { select, Store } from '@ngrx/store';
|
||||||
import { NgbModal, NgbModalConfig } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal, NgbModalConfig } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { Angulartics2GoogleAnalytics } from 'angulartics2';
|
|
||||||
|
|
||||||
import { MetadataService } from './core/metadata/metadata.service';
|
|
||||||
import { HostWindowResizeAction } from './shared/host-window.actions';
|
import { HostWindowResizeAction } from './shared/host-window.actions';
|
||||||
import { HostWindowState } from './shared/search/host-window.reducer';
|
import { HostWindowState } from './shared/search/host-window.reducer';
|
||||||
import { NativeWindowRef, NativeWindowService } from './core/services/window.service';
|
import { NativeWindowRef, NativeWindowService } from './core/services/window.service';
|
||||||
import { isAuthenticationBlocking } from './core/auth/selectors';
|
import { isAuthenticationBlocking } from './core/auth/selectors';
|
||||||
import { AuthService } from './core/auth/auth.service';
|
import { AuthService } from './core/auth/auth.service';
|
||||||
import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
|
import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
|
||||||
import { MenuService } from './shared/menu/menu.service';
|
|
||||||
import { HostWindowService } from './shared/host-window.service';
|
|
||||||
import { HeadTagConfig, ThemeConfig } from '../config/theme.model';
|
|
||||||
import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider';
|
|
||||||
import { environment } from '../environments/environment';
|
import { environment } from '../environments/environment';
|
||||||
import { models } from './core/core.module';
|
import { models } from './core/core.module';
|
||||||
import { LocaleService } from './core/locale/locale.service';
|
|
||||||
import { hasNoValue, hasValue, isNotEmpty } from './shared/empty.util';
|
|
||||||
import { KlaroService } from './shared/cookies/klaro.service';
|
|
||||||
import { GoogleAnalyticsService } from './statistics/google-analytics.service';
|
|
||||||
import { ThemeService } from './shared/theme-support/theme.service';
|
import { ThemeService } from './shared/theme-support/theme.service';
|
||||||
import { BASE_THEME_NAME } from './shared/theme-support/theme.constants';
|
|
||||||
import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service';
|
|
||||||
import { IdleModalComponent } from './shared/idle-modal/idle-modal.component';
|
import { IdleModalComponent } from './shared/idle-modal/idle-modal.component';
|
||||||
import { getDefaultThemeConfig } from '../config/config.util';
|
import { distinctNext } from './core/shared/distinct-next';
|
||||||
import { AppConfig, APP_CONFIG } from 'src/config/app-config.interface';
|
|
||||||
import { ModalBeforeDismiss } from './shared/interfaces/modal-before-dismiss.interface';
|
import { ModalBeforeDismiss } from './shared/interfaces/modal-before-dismiss.interface';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -58,11 +40,6 @@ import { ModalBeforeDismiss } from './shared/interfaces/modal-before-dismiss.int
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class AppComponent implements OnInit, AfterViewInit {
|
export class AppComponent implements OnInit, AfterViewInit {
|
||||||
sidebarVisible: Observable<boolean>;
|
|
||||||
slideSidebarOver: Observable<boolean>;
|
|
||||||
collapsedSidebarWidth: Observable<string>;
|
|
||||||
totalSidebarWidth: Observable<string>;
|
|
||||||
theme: Observable<ThemeConfig> = of({} as any);
|
|
||||||
notificationOptions;
|
notificationOptions;
|
||||||
models;
|
models;
|
||||||
|
|
||||||
@@ -79,9 +56,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
/**
|
/**
|
||||||
* Whether or not the theme is in the process of being swapped
|
* Whether or not the theme is in the process of being swapped
|
||||||
*/
|
*/
|
||||||
isThemeLoading$: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
isThemeLoading$: Observable<boolean>;
|
||||||
|
|
||||||
isThemeCSSLoading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the idle modal is is currently open
|
* Whether or not the idle modal is is currently open
|
||||||
@@ -92,78 +67,26 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
@Inject(NativeWindowService) private _window: NativeWindowRef,
|
@Inject(NativeWindowService) private _window: NativeWindowRef,
|
||||||
@Inject(DOCUMENT) private document: any,
|
@Inject(DOCUMENT) private document: any,
|
||||||
@Inject(PLATFORM_ID) private platformId: any,
|
@Inject(PLATFORM_ID) private platformId: any,
|
||||||
@Inject(APP_CONFIG) private appConfig: AppConfig,
|
|
||||||
private themeService: ThemeService,
|
private themeService: ThemeService,
|
||||||
private translate: TranslateService,
|
private translate: TranslateService,
|
||||||
private store: Store<HostWindowState>,
|
private store: Store<HostWindowState>,
|
||||||
private metadata: MetadataService,
|
|
||||||
private angulartics2GoogleAnalytics: Angulartics2GoogleAnalytics,
|
|
||||||
private angulartics2DSpace: Angulartics2DSpace,
|
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private cssService: CSSVariableService,
|
private cssService: CSSVariableService,
|
||||||
private menuService: MenuService,
|
|
||||||
private windowService: HostWindowService,
|
|
||||||
private localeService: LocaleService,
|
|
||||||
private breadcrumbsService: BreadcrumbsService,
|
|
||||||
private modalService: NgbModal,
|
private modalService: NgbModal,
|
||||||
private modalConfig: NgbModalConfig,
|
private modalConfig: NgbModalConfig,
|
||||||
@Optional() private cookiesService: KlaroService,
|
|
||||||
@Optional() private googleAnalyticsService: GoogleAnalyticsService,
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
if (!isEqual(environment, this.appConfig)) {
|
|
||||||
throw new Error('environment does not match app config!');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.notificationOptions = environment.notifications;
|
this.notificationOptions = environment.notifications;
|
||||||
|
|
||||||
/* Use models object so all decorators are actually called */
|
/* Use models object so all decorators are actually called */
|
||||||
this.models = models;
|
this.models = models;
|
||||||
|
|
||||||
this.themeService.getThemeName$().subscribe((themeName: string) => {
|
|
||||||
if (isPlatformBrowser(this.platformId)) {
|
if (isPlatformBrowser(this.platformId)) {
|
||||||
// the theme css will never download server side, so this should only happen on the browser
|
|
||||||
this.distinctNext(this.isThemeCSSLoading$, true);
|
|
||||||
}
|
|
||||||
if (hasValue(themeName)) {
|
|
||||||
this.loadGlobalThemeConfig(themeName);
|
|
||||||
} else {
|
|
||||||
const defaultThemeConfig = getDefaultThemeConfig();
|
|
||||||
if (hasValue(defaultThemeConfig)) {
|
|
||||||
this.loadGlobalThemeConfig(defaultThemeConfig.name);
|
|
||||||
} else {
|
|
||||||
this.loadGlobalThemeConfig(BASE_THEME_NAME);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isPlatformBrowser(this.platformId)) {
|
|
||||||
this.authService.trackTokenExpiration();
|
|
||||||
this.trackIdleModal();
|
this.trackIdleModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load all the languages that are defined as active from the config file
|
this.isThemeLoading$ = this.themeService.isThemeLoading$;
|
||||||
translate.addLangs(environment.languages.filter((LangConfig) => LangConfig.active === true).map((a) => a.code));
|
|
||||||
|
|
||||||
// Load the default language from the config file
|
|
||||||
// translate.setDefaultLang(environment.defaultLanguage);
|
|
||||||
|
|
||||||
// set the current language code
|
|
||||||
this.localeService.setCurrentLanguageCode();
|
|
||||||
|
|
||||||
// analytics
|
|
||||||
if (hasValue(googleAnalyticsService)) {
|
|
||||||
googleAnalyticsService.addTrackingIdToPage();
|
|
||||||
}
|
|
||||||
angulartics2DSpace.startTracking();
|
|
||||||
|
|
||||||
metadata.listenForRouteChange();
|
|
||||||
breadcrumbsService.listenForRouteChanges();
|
|
||||||
|
|
||||||
if (environment.debug) {
|
|
||||||
console.info(environment);
|
|
||||||
}
|
|
||||||
this.storeCSSVariables();
|
this.storeCSSVariables();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,18 +101,11 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.isAuthBlocking$ = this.store.pipe(select(isAuthenticationBlocking)).pipe(
|
this.isAuthBlocking$ = this.store.pipe(
|
||||||
|
select(isAuthenticationBlocking),
|
||||||
distinctUntilChanged()
|
distinctUntilChanged()
|
||||||
);
|
);
|
||||||
this.isAuthBlocking$
|
|
||||||
.pipe(
|
|
||||||
filter((isBlocking: boolean) => isBlocking === false),
|
|
||||||
take(1)
|
|
||||||
).subscribe(() => this.initializeKlaro());
|
|
||||||
|
|
||||||
const env: string = environment.production ? 'Production' : 'Development';
|
|
||||||
const color: string = environment.production ? 'red' : 'green';
|
|
||||||
console.info(`Environment: %c${env}`, `color: ${color}; font-weight: bold;`);
|
|
||||||
this.dispatchWindowSize(this._window.nativeWindow.innerWidth, this._window.nativeWindow.innerHeight);
|
this.dispatchWindowSize(this._window.nativeWindow.innerWidth, this._window.nativeWindow.innerHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,54 +125,15 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
let updatingTheme = false;
|
|
||||||
let snapshot: ActivatedRouteSnapshot;
|
|
||||||
|
|
||||||
this.router.events.subscribe((event) => {
|
this.router.events.subscribe((event) => {
|
||||||
if (event instanceof NavigationStart) {
|
if (event instanceof NavigationStart) {
|
||||||
updatingTheme = false;
|
distinctNext(this.isRouteLoading$, true);
|
||||||
this.distinctNext(this.isRouteLoading$, true);
|
} else if (
|
||||||
} else if (event instanceof ResolveEnd) {
|
event instanceof NavigationEnd ||
|
||||||
// this is the earliest point where we have all the information we need
|
event instanceof NavigationCancel
|
||||||
// to update the theme, but this event is not emitted on first load
|
) {
|
||||||
this.updateTheme(event.urlAfterRedirects, event.state.root);
|
distinctNext(this.isRouteLoading$, false);
|
||||||
updatingTheme = true;
|
|
||||||
} else if (!updatingTheme && event instanceof ActivationEnd) {
|
|
||||||
// if there was no ResolveEnd, keep track of the snapshot...
|
|
||||||
snapshot = event.snapshot;
|
|
||||||
} else if (event instanceof NavigationEnd) {
|
|
||||||
if (!updatingTheme) {
|
|
||||||
// ...and use it to update the theme on NavigationEnd instead
|
|
||||||
this.updateTheme(event.urlAfterRedirects, snapshot);
|
|
||||||
updatingTheme = true;
|
|
||||||
}
|
}
|
||||||
this.distinctNext(this.isRouteLoading$, false);
|
|
||||||
} else if (event instanceof NavigationCancel) {
|
|
||||||
if (!updatingTheme) {
|
|
||||||
this.distinctNext(this.isThemeLoading$, false);
|
|
||||||
}
|
|
||||||
this.distinctNext(this.isRouteLoading$, false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the theme according to the current route, if applicable.
|
|
||||||
* @param urlAfterRedirects the current URL after redirects
|
|
||||||
* @param snapshot the current route snapshot
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private updateTheme(urlAfterRedirects: string, snapshot: ActivatedRouteSnapshot): void {
|
|
||||||
this.themeService.updateThemeOnRouteChange$(urlAfterRedirects, snapshot).pipe(
|
|
||||||
switchMap((changed) => {
|
|
||||||
if (changed) {
|
|
||||||
return this.isThemeCSSLoading$;
|
|
||||||
} else {
|
|
||||||
return [false];
|
|
||||||
}
|
|
||||||
})
|
|
||||||
).subscribe((changed) => {
|
|
||||||
this.distinctNext(this.isThemeLoading$, changed);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,125 +148,6 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private initializeKlaro() {
|
|
||||||
if (hasValue(this.cookiesService)) {
|
|
||||||
this.cookiesService.initialize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private loadGlobalThemeConfig(themeName: string): void {
|
|
||||||
this.setThemeCss(themeName);
|
|
||||||
this.setHeadTags(themeName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the theme css file in <head>
|
|
||||||
*
|
|
||||||
* @param themeName The name of the new theme
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private setThemeCss(themeName: string): void {
|
|
||||||
const head = this.document.getElementsByTagName('head')[0];
|
|
||||||
if (hasNoValue(head)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Array.from to ensure we end up with an array, not an HTMLCollection, which would be
|
|
||||||
// automatically updated if we add nodes later
|
|
||||||
const currentThemeLinks = Array.from(head.getElementsByClassName('theme-css'));
|
|
||||||
const link = this.document.createElement('link');
|
|
||||||
link.setAttribute('rel', 'stylesheet');
|
|
||||||
link.setAttribute('type', 'text/css');
|
|
||||||
link.setAttribute('class', 'theme-css');
|
|
||||||
link.setAttribute('href', `${encodeURIComponent(themeName)}-theme.css`);
|
|
||||||
// wait for the new css to download before removing the old one to prevent a
|
|
||||||
// flash of unstyled content
|
|
||||||
link.onload = () => {
|
|
||||||
if (isNotEmpty(currentThemeLinks)) {
|
|
||||||
currentThemeLinks.forEach((currentThemeLink: any) => {
|
|
||||||
if (hasValue(currentThemeLink)) {
|
|
||||||
currentThemeLink.remove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// the fact that this callback is used, proves we're on the browser.
|
|
||||||
this.distinctNext(this.isThemeCSSLoading$, false);
|
|
||||||
};
|
|
||||||
head.appendChild(link);
|
|
||||||
}
|
|
||||||
|
|
||||||
private setHeadTags(themeName: string): void {
|
|
||||||
const head = this.document.getElementsByTagName('head')[0];
|
|
||||||
if (hasNoValue(head)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// clear head tags
|
|
||||||
const currentHeadTags = Array.from(head.getElementsByClassName('theme-head-tag'));
|
|
||||||
if (hasValue(currentHeadTags)) {
|
|
||||||
currentHeadTags.forEach((currentHeadTag: any) => currentHeadTag.remove());
|
|
||||||
}
|
|
||||||
|
|
||||||
// create new head tags (not yet added to DOM)
|
|
||||||
const headTagFragment = this.document.createDocumentFragment();
|
|
||||||
this.createHeadTags(themeName)
|
|
||||||
.forEach(newHeadTag => headTagFragment.appendChild(newHeadTag));
|
|
||||||
|
|
||||||
// add new head tags to DOM
|
|
||||||
head.appendChild(headTagFragment);
|
|
||||||
}
|
|
||||||
|
|
||||||
private createHeadTags(themeName: string): HTMLElement[] {
|
|
||||||
const themeConfig = this.themeService.getThemeConfigFor(themeName);
|
|
||||||
const headTagConfigs = themeConfig?.headTags;
|
|
||||||
|
|
||||||
if (hasNoValue(headTagConfigs)) {
|
|
||||||
const parentThemeName = themeConfig?.extends;
|
|
||||||
if (hasValue(parentThemeName)) {
|
|
||||||
// inherit the head tags of the parent theme
|
|
||||||
return this.createHeadTags(parentThemeName);
|
|
||||||
}
|
|
||||||
const defaultThemeConfig = getDefaultThemeConfig();
|
|
||||||
const defaultThemeName = defaultThemeConfig.name;
|
|
||||||
if (
|
|
||||||
hasNoValue(defaultThemeName) ||
|
|
||||||
themeName === defaultThemeName ||
|
|
||||||
themeName === BASE_THEME_NAME
|
|
||||||
) {
|
|
||||||
// last resort, use fallback favicon.ico
|
|
||||||
return [
|
|
||||||
this.createHeadTag({
|
|
||||||
'tagName': 'link',
|
|
||||||
'attributes': {
|
|
||||||
'rel': 'icon',
|
|
||||||
'href': 'assets/images/favicon.ico',
|
|
||||||
'sizes': 'any',
|
|
||||||
}
|
|
||||||
})
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// inherit the head tags of the default theme
|
|
||||||
return this.createHeadTags(defaultThemeConfig.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
return headTagConfigs.map(this.createHeadTag.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
private createHeadTag(headTagConfig: HeadTagConfig): HTMLElement {
|
|
||||||
const tag = this.document.createElement(headTagConfig.tagName);
|
|
||||||
|
|
||||||
if (hasValue(headTagConfig.attributes)) {
|
|
||||||
Object.entries(headTagConfig.attributes)
|
|
||||||
.forEach(([key, value]) => tag.setAttribute(key, value));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 'class' attribute should always be 'theme-head-tag' for removal
|
|
||||||
tag.setAttribute('class', 'theme-head-tag');
|
|
||||||
|
|
||||||
return tag;
|
|
||||||
}
|
|
||||||
|
|
||||||
private trackIdleModal() {
|
private trackIdleModal() {
|
||||||
const isIdle$ = this.authService.isUserIdle();
|
const isIdle$ = this.authService.isUserIdle();
|
||||||
const isAuthenticated$ = this.authService.isAuthenticated();
|
const isAuthenticated$ = this.authService.isAuthenticated();
|
||||||
@@ -409,16 +167,4 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Use nextValue to update a given BehaviorSubject, only if it differs from its current value
|
|
||||||
*
|
|
||||||
* @param bs a BehaviorSubject
|
|
||||||
* @param nextValue the next value for that BehaviorSubject
|
|
||||||
* @protected
|
|
||||||
*/
|
|
||||||
protected distinctNext<T>(bs: BehaviorSubject<T>, nextValue: T): void {
|
|
||||||
if (bs.getValue() !== nextValue) {
|
|
||||||
bs.next(nextValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -1,18 +1,14 @@
|
|||||||
import { APP_BASE_HREF, CommonModule, DOCUMENT } from '@angular/common';
|
import { APP_BASE_HREF, CommonModule, DOCUMENT } from '@angular/common';
|
||||||
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
|
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
|
||||||
import { APP_INITIALIZER, NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { AbstractControl } from '@angular/forms';
|
import { AbstractControl } from '@angular/forms';
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { EffectsModule } from '@ngrx/effects';
|
import { EffectsModule } from '@ngrx/effects';
|
||||||
import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router-store';
|
import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router-store';
|
||||||
import { MetaReducer, Store, StoreModule, USER_PROVIDED_META_REDUCERS } from '@ngrx/store';
|
import { MetaReducer, StoreModule, USER_PROVIDED_META_REDUCERS } from '@ngrx/store';
|
||||||
import {
|
import { DYNAMIC_ERROR_MESSAGES_MATCHER, DYNAMIC_MATCHER_PROVIDERS, DynamicErrorMessagesMatcher } from '@ng-dynamic-forms/core';
|
||||||
DYNAMIC_ERROR_MESSAGES_MATCHER,
|
|
||||||
DYNAMIC_MATCHER_PROVIDERS,
|
|
||||||
DynamicErrorMessagesMatcher
|
|
||||||
} from '@ng-dynamic-forms/core';
|
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
|
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
|
||||||
import { AppRoutingModule } from './app-routing.module';
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
@@ -20,7 +16,6 @@ import { AppComponent } from './app.component';
|
|||||||
import { appEffects } from './app.effects';
|
import { appEffects } from './app.effects';
|
||||||
import { appMetaReducers, debugMetaReducers } from './app.metareducers';
|
import { appMetaReducers, debugMetaReducers } from './app.metareducers';
|
||||||
import { appReducers, AppState, storeModuleConfig } from './app.reducer';
|
import { appReducers, AppState, storeModuleConfig } from './app.reducer';
|
||||||
import { CheckAuthenticationTokenAction } from './core/auth/auth.actions';
|
|
||||||
import { CoreModule } from './core/core.module';
|
import { CoreModule } from './core/core.module';
|
||||||
import { ClientCookieService } from './core/services/client-cookie.service';
|
import { ClientCookieService } from './core/services/client-cookie.service';
|
||||||
import { NavbarModule } from './navbar/navbar.module';
|
import { NavbarModule } from './navbar/navbar.module';
|
||||||
@@ -32,7 +27,6 @@ import { LocaleInterceptor } from './core/locale/locale.interceptor';
|
|||||||
import { XsrfInterceptor } from './core/xsrf/xsrf.interceptor';
|
import { XsrfInterceptor } from './core/xsrf/xsrf.interceptor';
|
||||||
import { LogInterceptor } from './core/log/log.interceptor';
|
import { LogInterceptor } from './core/log/log.interceptor';
|
||||||
import { EagerThemesModule } from '../themes/eager-themes.module';
|
import { EagerThemesModule } from '../themes/eager-themes.module';
|
||||||
|
|
||||||
import { APP_CONFIG, AppConfig } from '../config/app-config.interface';
|
import { APP_CONFIG, AppConfig } from '../config/app-config.interface';
|
||||||
import { NgxMaskModule } from 'ngx-mask';
|
import { NgxMaskModule } from 'ngx-mask';
|
||||||
import { StoreDevModules } from '../config/store/devtools';
|
import { StoreDevModules } from '../config/store/devtools';
|
||||||
@@ -80,10 +74,6 @@ const IMPORTS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const PROVIDERS = [
|
const PROVIDERS = [
|
||||||
{
|
|
||||||
provide: APP_CONFIG,
|
|
||||||
useFactory: getConfig
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
provide: APP_BASE_HREF,
|
provide: APP_BASE_HREF,
|
||||||
useFactory: getBaseHref,
|
useFactory: getBaseHref,
|
||||||
@@ -99,15 +89,6 @@ const PROVIDERS = [
|
|||||||
useClass: DSpaceRouterStateSerializer
|
useClass: DSpaceRouterStateSerializer
|
||||||
},
|
},
|
||||||
ClientCookieService,
|
ClientCookieService,
|
||||||
// Check the authentication token when the app initializes
|
|
||||||
{
|
|
||||||
provide: APP_INITIALIZER,
|
|
||||||
useFactory: (store: Store<AppState>,) => {
|
|
||||||
return () => store.dispatch(new CheckAuthenticationTokenAction());
|
|
||||||
},
|
|
||||||
deps: [Store],
|
|
||||||
multi: true
|
|
||||||
},
|
|
||||||
// register AuthInterceptor as HttpInterceptor
|
// register AuthInterceptor as HttpInterceptor
|
||||||
{
|
{
|
||||||
provide: HTTP_INTERCEPTORS,
|
provide: HTTP_INTERCEPTORS,
|
||||||
|
@@ -18,6 +18,8 @@ import { BrowseByDataType, rendersBrowseBy } from '../browse-by-switcher/browse-
|
|||||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
|
export const BBM_PAGINATION_ID = 'bbm';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-browse-by-metadata-page',
|
selector: 'ds-browse-by-metadata-page',
|
||||||
styleUrls: ['./browse-by-metadata-page.component.scss'],
|
styleUrls: ['./browse-by-metadata-page.component.scss'],
|
||||||
@@ -50,7 +52,7 @@ export class BrowseByMetadataPageComponent implements OnInit {
|
|||||||
* The pagination config used to display the values
|
* The pagination config used to display the values
|
||||||
*/
|
*/
|
||||||
paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||||
id: 'bbm',
|
id: BBM_PAGINATION_ID,
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 20
|
pageSize: 20
|
||||||
});
|
});
|
||||||
|
@@ -4,7 +4,7 @@ import { HttpHeaders } from '@angular/common/http';
|
|||||||
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
|
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
|
||||||
|
|
||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { map, startWith, switchMap, take } from 'rxjs/operators';
|
import { filter, map, startWith, switchMap, take } from 'rxjs/operators';
|
||||||
import { select, Store } from '@ngrx/store';
|
import { select, Store } from '@ngrx/store';
|
||||||
import { CookieAttributes } from 'js-cookie';
|
import { CookieAttributes } from 'js-cookie';
|
||||||
|
|
||||||
@@ -93,6 +93,8 @@ export class AuthService {
|
|||||||
private translateService: TranslateService
|
private translateService: TranslateService
|
||||||
) {
|
) {
|
||||||
this.store.pipe(
|
this.store.pipe(
|
||||||
|
// when this service is constructed the store is not fully initialized yet
|
||||||
|
filter((state: any) => state?.core?.auth !== undefined),
|
||||||
select(isAuthenticated),
|
select(isAuthenticated),
|
||||||
startWith(false)
|
startWith(false)
|
||||||
).subscribe((authenticated: boolean) => this._authenticated = authenticated);
|
).subscribe((authenticated: boolean) => this._authenticated = authenticated);
|
||||||
@@ -346,7 +348,7 @@ export class AuthService {
|
|||||||
let token: AuthTokenInfo;
|
let token: AuthTokenInfo;
|
||||||
let currentlyRefreshingToken = false;
|
let currentlyRefreshingToken = false;
|
||||||
this.store.pipe(select(getAuthenticationToken)).subscribe((authTokenInfo: AuthTokenInfo) => {
|
this.store.pipe(select(getAuthenticationToken)).subscribe((authTokenInfo: AuthTokenInfo) => {
|
||||||
// If new token is undefined an it wasn't previously => Refresh failed
|
// If new token is undefined and it wasn't previously => Refresh failed
|
||||||
if (currentlyRefreshingToken && token !== undefined && authTokenInfo === undefined) {
|
if (currentlyRefreshingToken && token !== undefined && authTokenInfo === undefined) {
|
||||||
// Token refresh failed => Error notification => 10 second wait => Page reloads & user logged out
|
// Token refresh failed => Error notification => 10 second wait => Page reloads & user logged out
|
||||||
this.notificationService.error(this.translateService.get('auth.messages.token-refresh-failed'));
|
this.notificationService.error(this.translateService.get('auth.messages.token-refresh-failed'));
|
||||||
|
@@ -19,7 +19,7 @@ export abstract class SomeFeatureAuthorizationGuard implements CanActivate {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* True when user has authorization rights for the feature and object provided
|
* True when user has authorization rights for the feature and object provided
|
||||||
* Redirect the user to the unauthorized page when he/she's not authorized for the given feature
|
* Redirect the user to the unauthorized page when they are not authorized for the given feature
|
||||||
*/
|
*/
|
||||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
|
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
|
||||||
return observableCombineLatest(this.getFeatureIDs(route, state), this.getObjectUrl(route, state), this.getEPersonUuid(route, state)).pipe(
|
return observableCombineLatest(this.getFeatureIDs(route, state), this.getObjectUrl(route, state), this.getEPersonUuid(route, state)).pipe(
|
||||||
|
@@ -38,7 +38,7 @@ export class ProcessDataService extends DataService<Process> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the endpoint for a process his files
|
* Get the endpoint for the files of the process
|
||||||
* @param processId The ID of the process
|
* @param processId The ID of the process
|
||||||
*/
|
*/
|
||||||
getFilesEndpoint(processId: string): Observable<string> {
|
getFilesEndpoint(processId: string): Observable<string> {
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
|
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { returnEndUserAgreementUrlTreeOnFalse } from '../shared/authorized.operators';
|
import { returnEndUserAgreementUrlTreeOnFalse } from '../shared/authorized.operators';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An abstract guard for redirecting users to the user agreement page if a certain condition is met
|
* An abstract guard for redirecting users to the user agreement page if a certain condition is met
|
||||||
@@ -18,6 +19,9 @@ export abstract class AbstractEndUserAgreementGuard implements CanActivate {
|
|||||||
* when they're finished accepting the agreement
|
* when they're finished accepting the agreement
|
||||||
*/
|
*/
|
||||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
|
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
|
||||||
|
if (!environment.info.enableEndUserAgreement) {
|
||||||
|
return observableOf(true);
|
||||||
|
}
|
||||||
return this.hasAccepted().pipe(
|
return this.hasAccepted().pipe(
|
||||||
returnEndUserAgreementUrlTreeOnFalse(this.router, state.url)
|
returnEndUserAgreementUrlTreeOnFalse(this.router, state.url)
|
||||||
);
|
);
|
||||||
|
@@ -2,6 +2,7 @@ import { EndUserAgreementCurrentUserGuard } from './end-user-agreement-current-u
|
|||||||
import { EndUserAgreementService } from './end-user-agreement.service';
|
import { EndUserAgreementService } from './end-user-agreement.service';
|
||||||
import { Router, UrlTree } from '@angular/router';
|
import { Router, UrlTree } from '@angular/router';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { environment } from '../../../environments/environment.test';
|
||||||
|
|
||||||
describe('EndUserAgreementGuard', () => {
|
describe('EndUserAgreementGuard', () => {
|
||||||
let guard: EndUserAgreementCurrentUserGuard;
|
let guard: EndUserAgreementCurrentUserGuard;
|
||||||
@@ -44,5 +45,24 @@ describe('EndUserAgreementGuard', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when the end user agreement is disabled', () => {
|
||||||
|
it('should return true', (done) => {
|
||||||
|
environment.info.enableEndUserAgreement = false;
|
||||||
|
guard.canActivate(undefined, Object.assign({ url: 'redirect' })).subscribe((result) => {
|
||||||
|
console.log(result);
|
||||||
|
expect(result).toEqual(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not resolve to the end user agreement page', (done) => {
|
||||||
|
environment.info.enableEndUserAgreement = false;
|
||||||
|
guard.canActivate(undefined, Object.assign({ url: 'redirect' })).subscribe((result) => {
|
||||||
|
expect(router.navigateByUrl).not.toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { AbstractEndUserAgreementGuard } from './abstract-end-user-agreement.guard';
|
import { AbstractEndUserAgreementGuard } from './abstract-end-user-agreement.guard';
|
||||||
import { EndUserAgreementService } from './end-user-agreement.service';
|
import { EndUserAgreementService } from './end-user-agreement.service';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A guard redirecting logged in users to the end agreement page when they haven't accepted the latest user agreement
|
* A guard redirecting logged in users to the end agreement page when they haven't accepted the latest user agreement
|
||||||
@@ -19,6 +20,10 @@ export class EndUserAgreementCurrentUserGuard extends AbstractEndUserAgreementGu
|
|||||||
* True when the currently logged in user has accepted the agreements or when the user is not currently authenticated
|
* True when the currently logged in user has accepted the agreements or when the user is not currently authenticated
|
||||||
*/
|
*/
|
||||||
hasAccepted(): Observable<boolean> {
|
hasAccepted(): Observable<boolean> {
|
||||||
|
if (!environment.info.enableEndUserAgreement) {
|
||||||
|
return observableOf(true);
|
||||||
|
}
|
||||||
|
|
||||||
return this.endUserAgreementService.hasCurrentUserAcceptedAgreement(true);
|
return this.endUserAgreementService.hasCurrentUserAcceptedAgreement(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -55,7 +55,7 @@ export class EndUserAgreementService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the current user's accepted agreement status
|
* Set the current user's accepted agreement status
|
||||||
* When a user is authenticated, set his/her metadata to the provided value
|
* When a user is authenticated, set their metadata to the provided value
|
||||||
* When no user is authenticated, set the cookie to the provided value
|
* When no user is authenticated, set the cookie to the provided value
|
||||||
* @param accepted
|
* @param accepted
|
||||||
*/
|
*/
|
||||||
|
22
src/app/core/shared/distinct-next.ts
Normal file
22
src/app/core/shared/distinct-next.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* The contents of this file are subject to the license and copyright
|
||||||
|
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||||
|
* tree and available online at
|
||||||
|
*
|
||||||
|
* http://www.dspace.org/license/
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use nextValue to update a given BehaviorSubject, only if it differs from its current value
|
||||||
|
*
|
||||||
|
* @param bs a BehaviorSubject
|
||||||
|
* @param nextValue the next value for that BehaviorSubject
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
export function distinctNext<T>(bs: BehaviorSubject<T>, nextValue: T): void {
|
||||||
|
if (bs.getValue() !== nextValue) {
|
||||||
|
bs.next(nextValue);
|
||||||
|
}
|
||||||
|
}
|
@@ -39,7 +39,7 @@ export class MetadataValue implements MetadataValueInterface {
|
|||||||
value: string;
|
value: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The place of this MetadataValue within his list of metadata
|
* The place of this MetadataValue within its list of metadata
|
||||||
* This is used to render metadata in a specific custom order
|
* This is used to render metadata in a specific custom order
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
@autoserialize
|
||||||
@@ -105,7 +105,7 @@ export class MetadatumViewModel {
|
|||||||
value: string;
|
value: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The place of this MetadataValue within his list of metadata
|
* The place of this MetadataValue within its list of metadata
|
||||||
* This is used to render metadata in a specific custom order
|
* This is used to render metadata in a specific custom order
|
||||||
*/
|
*/
|
||||||
place: number;
|
place: number;
|
||||||
|
@@ -67,11 +67,11 @@
|
|||||||
<a class="text-white" href="javascript:void(0);"
|
<a class="text-white" href="javascript:void(0);"
|
||||||
(click)="showCookieSettings()">{{ 'footer.link.cookies' | translate}}</a>
|
(click)="showCookieSettings()">{{ 'footer.link.cookies' | translate}}</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li *ngIf="showPrivacyPolicy">
|
||||||
<a class="text-white"
|
<a class="text-white"
|
||||||
routerLink="info/privacy">{{ 'footer.link.privacy-policy' | translate}}</a>
|
routerLink="info/privacy">{{ 'footer.link.privacy-policy' | translate}}</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li *ngIf="showEndUserAgreement">
|
||||||
<a class="text-white"
|
<a class="text-white"
|
||||||
routerLink="info/end-user-agreement">{{ 'footer.link.end-user-agreement' | translate}}</a>
|
routerLink="info/end-user-agreement">{{ 'footer.link.end-user-agreement' | translate}}</a>
|
||||||
</li>
|
</li>
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { Component, Optional } from '@angular/core';
|
import { Component, Optional } 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';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-footer',
|
selector: 'ds-footer',
|
||||||
@@ -14,6 +15,8 @@ export class FooterComponent {
|
|||||||
* 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;
|
||||||
|
showPrivacyPolicy = environment.info.enablePrivacyStatement;
|
||||||
|
showEndUserAgreement = environment.info.enableEndUserAgreement;
|
||||||
|
|
||||||
constructor(@Optional() private cookies: KlaroService) {
|
constructor(@Optional() private cookies: KlaroService) {
|
||||||
}
|
}
|
||||||
|
@@ -16,7 +16,7 @@ export class HealthStatusComponent {
|
|||||||
@Input() status: HealthStatus;
|
@Input() status: HealthStatus;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* He
|
* Health Status
|
||||||
*/
|
*/
|
||||||
HealthStatus = HealthStatus;
|
HealthStatus = HealthStatus;
|
||||||
|
|
||||||
|
@@ -76,7 +76,7 @@ export class EndUserAgreementComponent implements OnInit {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Cancel the agreement
|
* Cancel the agreement
|
||||||
* If the user is logged in, this will log him/her out
|
* If the user is logged in, this will log them out
|
||||||
* If the user is not logged in, they will be redirected to the homepage
|
* If the user is not logged in, they will be redirected to the homepage
|
||||||
*/
|
*/
|
||||||
cancel() {
|
cancel() {
|
||||||
|
@@ -6,26 +6,10 @@ import { ThemedEndUserAgreementComponent } from './end-user-agreement/themed-end
|
|||||||
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';
|
||||||
|
|
||||||
|
|
||||||
@NgModule({
|
const imports = [
|
||||||
imports: [
|
|
||||||
RouterModule.forChild([
|
|
||||||
{
|
|
||||||
path: END_USER_AGREEMENT_PATH,
|
|
||||||
component: ThemedEndUserAgreementComponent,
|
|
||||||
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
|
||||||
data: { title: 'info.end-user-agreement.title', breadcrumbKey: 'info.end-user-agreement' }
|
|
||||||
}
|
|
||||||
]),
|
|
||||||
RouterModule.forChild([
|
|
||||||
{
|
|
||||||
path: PRIVACY_PATH,
|
|
||||||
component: ThemedPrivacyComponent,
|
|
||||||
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
|
||||||
data: { title: 'info.privacy.title', breadcrumbKey: 'info.privacy' }
|
|
||||||
}
|
|
||||||
]),
|
|
||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
{
|
{
|
||||||
path: FEEDBACK_PATH,
|
path: FEEDBACK_PATH,
|
||||||
@@ -35,6 +19,34 @@ import { FeedbackGuard } from '../core/feedback/feedback.guard';
|
|||||||
canActivate: [FeedbackGuard]
|
canActivate: [FeedbackGuard]
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
];
|
||||||
|
|
||||||
|
if (environment.info.enableEndUserAgreement) {
|
||||||
|
imports.push(
|
||||||
|
RouterModule.forChild([
|
||||||
|
{
|
||||||
|
path: END_USER_AGREEMENT_PATH,
|
||||||
|
component: ThemedEndUserAgreementComponent,
|
||||||
|
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||||
|
data: { title: 'info.end-user-agreement.title', breadcrumbKey: 'info.end-user-agreement' }
|
||||||
|
}
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
if (environment.info.enablePrivacyStatement) {
|
||||||
|
imports.push(
|
||||||
|
RouterModule.forChild([
|
||||||
|
{
|
||||||
|
path: PRIVACY_PATH,
|
||||||
|
component: ThemedPrivacyComponent,
|
||||||
|
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||||
|
data: { title: 'info.privacy.title', breadcrumbKey: 'info.privacy' }
|
||||||
|
}
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
...imports
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
|
187
src/app/init.service.spec.ts
Normal file
187
src/app/init.service.spec.ts
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
import { InitService } from './init.service';
|
||||||
|
import { APP_CONFIG } from 'src/config/app-config.interface';
|
||||||
|
import { APP_INITIALIZER, Injectable } from '@angular/core';
|
||||||
|
import { inject, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { MetadataService } from './core/metadata/metadata.service';
|
||||||
|
import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { StoreModule } from '@ngrx/store';
|
||||||
|
import { authReducer } from './core/auth/auth.reducer';
|
||||||
|
import { storeModuleConfig } from './app.reducer';
|
||||||
|
import { AngularticsProviderMock } from './shared/mocks/angulartics-provider.service.mock';
|
||||||
|
import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider';
|
||||||
|
import { AuthService } from './core/auth/auth.service';
|
||||||
|
import { AuthServiceMock } from './shared/mocks/auth.service.mock';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { RouterMock } from './shared/mocks/router.mock';
|
||||||
|
import { MockActivatedRoute } from './shared/mocks/active-router.mock';
|
||||||
|
import { MenuService } from './shared/menu/menu.service';
|
||||||
|
import { LocaleService } from './core/locale/locale.service';
|
||||||
|
import { environment } from '../environments/environment';
|
||||||
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
import { RouteService } from './core/services/route.service';
|
||||||
|
import { getMockLocaleService } from './app.component.spec';
|
||||||
|
import { MenuServiceStub } from './shared/testing/menu-service.stub';
|
||||||
|
import { CorrelationIdService } from './correlation-id/correlation-id.service';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { TranslateLoaderMock } from './shared/mocks/translate-loader.mock';
|
||||||
|
import { ThemeService } from './shared/theme-support/theme.service';
|
||||||
|
import { getMockThemeService } from './shared/mocks/theme-service.mock';
|
||||||
|
import objectContaining = jasmine.objectContaining;
|
||||||
|
import createSpyObj = jasmine.createSpyObj;
|
||||||
|
import SpyObj = jasmine.SpyObj;
|
||||||
|
|
||||||
|
let spy: SpyObj<any>;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ConcreteInitServiceMock extends InitService {
|
||||||
|
protected static resolveAppConfig() {
|
||||||
|
spy.resolveAppConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected init(): () => Promise<boolean> {
|
||||||
|
spy.init();
|
||||||
|
return async () => true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
core: {
|
||||||
|
auth: {
|
||||||
|
loading: false,
|
||||||
|
blocking: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
describe('InitService', () => {
|
||||||
|
describe('providers', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spy = createSpyObj('ConcreteInitServiceMock', {
|
||||||
|
resolveAppConfig: null,
|
||||||
|
init: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error when called on abstract InitService', () => {
|
||||||
|
expect(() => InitService.providers()).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly set up provider dependencies', () => {
|
||||||
|
const providers = ConcreteInitServiceMock.providers();
|
||||||
|
|
||||||
|
expect(providers).toContain(objectContaining({
|
||||||
|
provide: InitService,
|
||||||
|
useClass: ConcreteInitServiceMock
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(providers).toContain(objectContaining({
|
||||||
|
provide: APP_CONFIG,
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(providers).toContain(objectContaining({
|
||||||
|
provide: APP_INITIALIZER,
|
||||||
|
deps: [ InitService ],
|
||||||
|
multi: true,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call resolveAppConfig() in APP_CONFIG factory', () => {
|
||||||
|
const factory = (
|
||||||
|
ConcreteInitServiceMock.providers()
|
||||||
|
.find((p: any) => p.provide === APP_CONFIG) as any
|
||||||
|
).useFactory;
|
||||||
|
|
||||||
|
// this factory is called _before_ InitService is instantiated
|
||||||
|
factory();
|
||||||
|
expect(spy.resolveAppConfig).toHaveBeenCalled();
|
||||||
|
expect(spy.init).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should defer to init() in APP_INITIALIZER factory', () => {
|
||||||
|
const factory = (
|
||||||
|
ConcreteInitServiceMock.providers()
|
||||||
|
.find((p: any) => p.provide === APP_INITIALIZER) as any
|
||||||
|
).useFactory;
|
||||||
|
|
||||||
|
// we don't care about the dependencies here
|
||||||
|
// @ts-ignore
|
||||||
|
const instance = new ConcreteInitServiceMock(null, null, null);
|
||||||
|
|
||||||
|
// provider ensures that the right concrete instance is passed to the factory
|
||||||
|
factory(instance);
|
||||||
|
expect(spy.resolveAppConfig).not.toHaveBeenCalled();
|
||||||
|
expect(spy.init).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('common initialization steps', () => {
|
||||||
|
let correlationIdServiceSpy;
|
||||||
|
let dspaceTransferStateSpy;
|
||||||
|
let transferStateSpy;
|
||||||
|
let metadataServiceSpy;
|
||||||
|
let breadcrumbsServiceSpy;
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
correlationIdServiceSpy = jasmine.createSpyObj('correlationIdServiceSpy', [
|
||||||
|
'initCorrelationId',
|
||||||
|
]);
|
||||||
|
dspaceTransferStateSpy = jasmine.createSpyObj('dspaceTransferStateSpy', [
|
||||||
|
'transfer',
|
||||||
|
]);
|
||||||
|
transferStateSpy = jasmine.createSpyObj('dspaceTransferStateSpy', [
|
||||||
|
'get', 'hasKey'
|
||||||
|
]);
|
||||||
|
breadcrumbsServiceSpy = jasmine.createSpyObj('breadcrumbsServiceSpy', [
|
||||||
|
'listenForRouteChanges',
|
||||||
|
]);
|
||||||
|
metadataServiceSpy = jasmine.createSpyObj('metadataService', [
|
||||||
|
'listenForRouteChange',
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
TestBed.resetTestingModule();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
StoreModule.forRoot(authReducer, storeModuleConfig),
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: InitService, useClass: ConcreteInitServiceMock },
|
||||||
|
{ provide: CorrelationIdService, useValue: correlationIdServiceSpy },
|
||||||
|
{ provide: APP_CONFIG, useValue: environment },
|
||||||
|
{ provide: LocaleService, useValue: getMockLocaleService() },
|
||||||
|
{ provide: Angulartics2DSpace, useValue: new AngularticsProviderMock() },
|
||||||
|
{ provide: MetadataService, useValue: metadataServiceSpy },
|
||||||
|
{ provide: BreadcrumbsService, useValue: breadcrumbsServiceSpy },
|
||||||
|
{ provide: AuthService, useValue: new AuthServiceMock() },
|
||||||
|
{ provide: Router, useValue: new RouterMock() },
|
||||||
|
{ provide: ActivatedRoute, useValue: new MockActivatedRoute() },
|
||||||
|
{ provide: MenuService, useValue: new MenuServiceStub() },
|
||||||
|
{ provide: ThemeService, useValue: getMockThemeService() },
|
||||||
|
provideMockStore({ initialState }),
|
||||||
|
AppComponent,
|
||||||
|
RouteService,
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('initRouteListeners', () => {
|
||||||
|
it('should call listenForRouteChanges', inject([InitService], (service) => {
|
||||||
|
// @ts-ignore
|
||||||
|
service.initRouteListeners();
|
||||||
|
expect(metadataServiceSpy.listenForRouteChange).toHaveBeenCalledTimes(1);
|
||||||
|
expect(breadcrumbsServiceSpy.listenForRouteChanges).toHaveBeenCalledTimes(1);
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
189
src/app/init.service.ts
Normal file
189
src/app/init.service.ts
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
/**
|
||||||
|
* The contents of this file are subject to the license and copyright
|
||||||
|
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||||
|
* tree and available online at
|
||||||
|
*
|
||||||
|
* http://www.dspace.org/license/
|
||||||
|
*/
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { CheckAuthenticationTokenAction } from './core/auth/auth.actions';
|
||||||
|
import { CorrelationIdService } from './correlation-id/correlation-id.service';
|
||||||
|
import { APP_INITIALIZER, Inject, Provider, Type } from '@angular/core';
|
||||||
|
import { makeStateKey, TransferState } from '@angular/platform-browser';
|
||||||
|
import { APP_CONFIG, AppConfig } from '../config/app-config.interface';
|
||||||
|
import { environment } from '../environments/environment';
|
||||||
|
import { AppState } from './app.reducer';
|
||||||
|
import { isEqual } from 'lodash';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { LocaleService } from './core/locale/locale.service';
|
||||||
|
import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider';
|
||||||
|
import { MetadataService } from './core/metadata/metadata.service';
|
||||||
|
import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service';
|
||||||
|
import { ThemeService } from './shared/theme-support/theme.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs the initialization of the app.
|
||||||
|
*
|
||||||
|
* Should be extended to implement server- & browser-specific functionality.
|
||||||
|
* Initialization steps shared between the server and brower implementations
|
||||||
|
* can be included in this class.
|
||||||
|
*
|
||||||
|
* Note that the service cannot (indirectly) depend on injection tokens that are only available _after_ APP_INITIALIZER.
|
||||||
|
* For example, NgbModal depends on ApplicationRef and can therefore not be used during initialization.
|
||||||
|
*/
|
||||||
|
export abstract class InitService {
|
||||||
|
/**
|
||||||
|
* The state transfer key to use for the NgRx store state
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected static NGRX_STATE = makeStateKey('NGRX_STATE');
|
||||||
|
|
||||||
|
protected constructor(
|
||||||
|
protected store: Store<AppState>,
|
||||||
|
protected correlationIdService: CorrelationIdService,
|
||||||
|
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
||||||
|
protected translate: TranslateService,
|
||||||
|
protected localeService: LocaleService,
|
||||||
|
protected angulartics2DSpace: Angulartics2DSpace,
|
||||||
|
protected metadata: MetadataService,
|
||||||
|
protected breadcrumbsService: BreadcrumbsService,
|
||||||
|
protected themeService: ThemeService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The initialization providers to use in `*AppModule`
|
||||||
|
* - this concrete {@link InitService}
|
||||||
|
* - {@link APP_CONFIG} with optional pre-initialization hook
|
||||||
|
* - {@link APP_INITIALIZER}
|
||||||
|
* <br>
|
||||||
|
* Should only be called on concrete subclasses of InitService for the initialization hooks to work
|
||||||
|
*/
|
||||||
|
public static providers(): Provider[] {
|
||||||
|
if (!InitService.isPrototypeOf(this)) {
|
||||||
|
throw new Error(
|
||||||
|
'Initalization providers should only be generated from concrete subclasses of InitService'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
provide: InitService,
|
||||||
|
useClass: this as unknown as Type<InitService>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: APP_CONFIG,
|
||||||
|
useFactory: (transferState: TransferState) => {
|
||||||
|
this.resolveAppConfig(transferState);
|
||||||
|
return environment;
|
||||||
|
},
|
||||||
|
deps: [ TransferState ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: APP_INITIALIZER,
|
||||||
|
useFactory: (initService: InitService) => initService.init(),
|
||||||
|
deps: [ InitService ],
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional pre-initialization method to ensure that {@link APP_CONFIG} is fully resolved before {@link init} is called.
|
||||||
|
*
|
||||||
|
* For example, Router depends on APP_BASE_HREF, which in turn depends on APP_CONFIG.
|
||||||
|
* In production mode, APP_CONFIG is resolved from the TransferState when the app is initialized.
|
||||||
|
* If we want to use Router within APP_INITIALIZER, we have to make sure APP_BASE_HREF is resolved beforehand.
|
||||||
|
* In this case that means that we must transfer the configuration from the SSR state during pre-initialization.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected static resolveAppConfig(
|
||||||
|
transferState: TransferState
|
||||||
|
): void {
|
||||||
|
// overriden in subclasses if applicable
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main initialization method.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected abstract init(): () => Promise<boolean>;
|
||||||
|
|
||||||
|
// Common initialization steps
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch a {@link CheckAuthenticationTokenAction} to start off the chain of
|
||||||
|
* actions used to determine whether a user is already logged in.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected checkAuthenticationToken(): void {
|
||||||
|
this.store.dispatch(new CheckAuthenticationTokenAction());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the correlation ID (from cookie, NgRx store or random)
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected initCorrelationId(): void {
|
||||||
|
this.correlationIdService.initCorrelationId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make sure the {@link environment} matches {@link APP_CONFIG} and print
|
||||||
|
* some information about it to the console
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected checkEnvironment(): void {
|
||||||
|
if (!isEqual(environment, this.appConfig)) {
|
||||||
|
throw new Error('environment does not match app config!');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (environment.debug) {
|
||||||
|
console.info(environment);
|
||||||
|
}
|
||||||
|
|
||||||
|
const env: string = environment.production ? 'Production' : 'Development';
|
||||||
|
const color: string = environment.production ? 'red' : 'green';
|
||||||
|
console.info(`Environment: %c${env}`, `color: ${color}; font-weight: bold;`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize internationalization services
|
||||||
|
* - Specify the active languages
|
||||||
|
* - Set the current locale
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected initI18n(): void {
|
||||||
|
// Load all the languages that are defined as active from the config file
|
||||||
|
this.translate.addLangs(
|
||||||
|
environment.languages
|
||||||
|
.filter((LangConfig) => LangConfig.active === true)
|
||||||
|
.map((a) => a.code)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load the default language from the config file
|
||||||
|
// translate.setDefaultLang(environment.defaultLanguage);
|
||||||
|
|
||||||
|
this.localeService.setCurrentLanguageCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize Angulartics
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected initAngulartics(): void {
|
||||||
|
this.angulartics2DSpace.startTracking();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start route-listening subscriptions
|
||||||
|
* - {@link MetadataService.listenForRouteChange}
|
||||||
|
* - {@link BreadcrumbsService.listenForRouteChanges}
|
||||||
|
* - {@link ThemeService.listenForRouteChanges}
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected initRouteListeners(): void {
|
||||||
|
this.metadata.listenForRouteChange();
|
||||||
|
this.breadcrumbsService.listenForRouteChanges();
|
||||||
|
this.themeService.listenForRouteChanges();
|
||||||
|
}
|
||||||
|
}
|
@@ -16,24 +16,24 @@ import { filter, find, map, take } from 'rxjs/operators';
|
|||||||
import { hasValue } from './shared/empty.util';
|
import { hasValue } from './shared/empty.util';
|
||||||
import { FeatureID } from './core/data/feature-authorization/feature-id';
|
import { FeatureID } from './core/data/feature-authorization/feature-id';
|
||||||
import {
|
import {
|
||||||
CreateCommunityParentSelectorComponent
|
ThemedCreateCommunityParentSelectorComponent
|
||||||
} from './shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component';
|
} from './shared/dso-selector/modal-wrappers/create-community-parent-selector/themed-create-community-parent-selector.component';
|
||||||
import { OnClickMenuItemModel } from './shared/menu/menu-item/models/onclick.model';
|
import { OnClickMenuItemModel } from './shared/menu/menu-item/models/onclick.model';
|
||||||
import {
|
import {
|
||||||
CreateCollectionParentSelectorComponent
|
ThemedCreateCollectionParentSelectorComponent
|
||||||
} from './shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component';
|
} from './shared/dso-selector/modal-wrappers/create-collection-parent-selector/themed-create-collection-parent-selector.component';
|
||||||
import {
|
import {
|
||||||
CreateItemParentSelectorComponent
|
ThemedCreateItemParentSelectorComponent
|
||||||
} from './shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
|
} from './shared/dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component';
|
||||||
import {
|
import {
|
||||||
EditCommunitySelectorComponent
|
ThemedEditCommunitySelectorComponent
|
||||||
} from './shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component';
|
} from './shared/dso-selector/modal-wrappers/edit-community-selector/themed-edit-community-selector.component';
|
||||||
import {
|
import {
|
||||||
EditCollectionSelectorComponent
|
ThemedEditCollectionSelectorComponent
|
||||||
} from './shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component';
|
} from './shared/dso-selector/modal-wrappers/edit-collection-selector/themed-edit-collection-selector.component';
|
||||||
import {
|
import {
|
||||||
EditItemSelectorComponent
|
ThemedEditItemSelectorComponent
|
||||||
} from './shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component';
|
} from './shared/dso-selector/modal-wrappers/edit-item-selector/themed-edit-item-selector.component';
|
||||||
import {
|
import {
|
||||||
ExportMetadataSelectorComponent
|
ExportMetadataSelectorComponent
|
||||||
} from './shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component';
|
} from './shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component';
|
||||||
@@ -188,7 +188,7 @@ export class MenuResolver implements Resolve<boolean> {
|
|||||||
type: MenuItemType.ONCLICK,
|
type: MenuItemType.ONCLICK,
|
||||||
text: 'menu.section.new_community',
|
text: 'menu.section.new_community',
|
||||||
function: () => {
|
function: () => {
|
||||||
this.modalService.open(CreateCommunityParentSelectorComponent);
|
this.modalService.open(ThemedCreateCommunityParentSelectorComponent);
|
||||||
}
|
}
|
||||||
} as OnClickMenuItemModel,
|
} as OnClickMenuItemModel,
|
||||||
},
|
},
|
||||||
@@ -201,7 +201,7 @@ export class MenuResolver implements Resolve<boolean> {
|
|||||||
type: MenuItemType.ONCLICK,
|
type: MenuItemType.ONCLICK,
|
||||||
text: 'menu.section.new_collection',
|
text: 'menu.section.new_collection',
|
||||||
function: () => {
|
function: () => {
|
||||||
this.modalService.open(CreateCollectionParentSelectorComponent);
|
this.modalService.open(ThemedCreateCollectionParentSelectorComponent);
|
||||||
}
|
}
|
||||||
} as OnClickMenuItemModel,
|
} as OnClickMenuItemModel,
|
||||||
},
|
},
|
||||||
@@ -214,7 +214,7 @@ export class MenuResolver implements Resolve<boolean> {
|
|||||||
type: MenuItemType.ONCLICK,
|
type: MenuItemType.ONCLICK,
|
||||||
text: 'menu.section.new_item',
|
text: 'menu.section.new_item',
|
||||||
function: () => {
|
function: () => {
|
||||||
this.modalService.open(CreateItemParentSelectorComponent);
|
this.modalService.open(ThemedCreateItemParentSelectorComponent);
|
||||||
}
|
}
|
||||||
} as OnClickMenuItemModel,
|
} as OnClickMenuItemModel,
|
||||||
},
|
},
|
||||||
@@ -263,7 +263,7 @@ export class MenuResolver implements Resolve<boolean> {
|
|||||||
type: MenuItemType.ONCLICK,
|
type: MenuItemType.ONCLICK,
|
||||||
text: 'menu.section.edit_community',
|
text: 'menu.section.edit_community',
|
||||||
function: () => {
|
function: () => {
|
||||||
this.modalService.open(EditCommunitySelectorComponent);
|
this.modalService.open(ThemedEditCommunitySelectorComponent);
|
||||||
}
|
}
|
||||||
} as OnClickMenuItemModel,
|
} as OnClickMenuItemModel,
|
||||||
},
|
},
|
||||||
@@ -276,7 +276,7 @@ export class MenuResolver implements Resolve<boolean> {
|
|||||||
type: MenuItemType.ONCLICK,
|
type: MenuItemType.ONCLICK,
|
||||||
text: 'menu.section.edit_collection',
|
text: 'menu.section.edit_collection',
|
||||||
function: () => {
|
function: () => {
|
||||||
this.modalService.open(EditCollectionSelectorComponent);
|
this.modalService.open(ThemedEditCollectionSelectorComponent);
|
||||||
}
|
}
|
||||||
} as OnClickMenuItemModel,
|
} as OnClickMenuItemModel,
|
||||||
},
|
},
|
||||||
@@ -289,7 +289,7 @@ export class MenuResolver implements Resolve<boolean> {
|
|||||||
type: MenuItemType.ONCLICK,
|
type: MenuItemType.ONCLICK,
|
||||||
text: 'menu.section.edit_item',
|
text: 'menu.section.edit_item',
|
||||||
function: () => {
|
function: () => {
|
||||||
this.modalService.open(EditItemSelectorComponent);
|
this.modalService.open(ThemedEditItemSelectorComponent);
|
||||||
}
|
}
|
||||||
} as OnClickMenuItemModel,
|
} as OnClickMenuItemModel,
|
||||||
},
|
},
|
||||||
|
@@ -1,20 +1,22 @@
|
|||||||
<div class="container" *ngVar="(processRD$ | async)?.payload as process">
|
<div class="container" *ngVar="(processRD$ | async)?.payload as process">
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<h2 class="flex-grow-1">{{'process.detail.title' | translate:{id: process?.processId, name: process?.scriptName} }}</h2>
|
<h2 class="flex-grow-1">{{'process.detail.title' | translate:{
|
||||||
<div>
|
id: process?.processId,
|
||||||
<button class="btn btn-lg btn-success " routerLink="/processes/new" [queryParams]="{id: process?.processId}"><i class="fas fa-plus pr-2"></i>{{'process.detail.create' | translate}}</button>
|
name: process?.scriptName
|
||||||
</div>
|
} }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<ds-process-detail-field id="process-name" [title]="'process.detail.script'">
|
<ds-process-detail-field id="process-name" [title]="'process.detail.script'">
|
||||||
<div>{{ process?.scriptName }}</div>
|
<div>{{ process?.scriptName }}</div>
|
||||||
</ds-process-detail-field>
|
</ds-process-detail-field>
|
||||||
|
|
||||||
<ds-process-detail-field *ngIf="process?.parameters && process?.parameters?.length > 0" id="process-arguments" [title]="'process.detail.arguments'">
|
<ds-process-detail-field *ngIf="process?.parameters && process?.parameters?.length > 0" id="process-arguments"
|
||||||
|
[title]="'process.detail.arguments'">
|
||||||
<div *ngFor="let argument of process?.parameters">{{ argument?.name }} {{ argument?.value }}</div>
|
<div *ngFor="let argument of process?.parameters">{{ argument?.name }} {{ argument?.value }}</div>
|
||||||
</ds-process-detail-field>
|
</ds-process-detail-field>
|
||||||
|
|
||||||
<div *ngVar="(filesRD$ | async)?.payload?.page as files">
|
<div *ngVar="(filesRD$ | async)?.payload?.page as files">
|
||||||
<ds-process-detail-field *ngIf="files && files?.length > 0" id="process-files" [title]="'process.detail.output-files'">
|
<ds-process-detail-field *ngIf="files && files?.length > 0" id="process-files"
|
||||||
|
[title]="'process.detail.output-files'">
|
||||||
<ds-file-download-link *ngFor="let file of files; let last=last;" [bitstream]="file">
|
<ds-file-download-link *ngFor="let file of files; let last=last;" [bitstream]="file">
|
||||||
<span>{{getFileName(file)}}</span>
|
<span>{{getFileName(file)}}</span>
|
||||||
<span>({{(file?.sizeBytes) | dsFileSize }})</span>
|
<span>({{(file?.sizeBytes) | dsFileSize }})</span>
|
||||||
@@ -22,23 +24,28 @@
|
|||||||
</ds-process-detail-field>
|
</ds-process-detail-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ds-process-detail-field *ngIf="process && process.startTime" id="process-start-time" [title]="'process.detail.start-time' | translate">
|
<ds-process-detail-field *ngIf="process && process.startTime" id="process-start-time"
|
||||||
|
[title]="'process.detail.start-time' | translate">
|
||||||
<div>{{ process.startTime | date:dateFormat:'UTC' }}</div>
|
<div>{{ process.startTime | date:dateFormat:'UTC' }}</div>
|
||||||
</ds-process-detail-field>
|
</ds-process-detail-field>
|
||||||
|
|
||||||
<ds-process-detail-field *ngIf="process && process.endTime" id="process-end-time" [title]="'process.detail.end-time' | translate">
|
<ds-process-detail-field *ngIf="process && process.endTime" id="process-end-time"
|
||||||
|
[title]="'process.detail.end-time' | translate">
|
||||||
<div>{{ process.endTime | date:dateFormat:'UTC' }}</div>
|
<div>{{ process.endTime | date:dateFormat:'UTC' }}</div>
|
||||||
</ds-process-detail-field>
|
</ds-process-detail-field>
|
||||||
|
|
||||||
<ds-process-detail-field *ngIf="process && process.processStatus" id="process-status" [title]="'process.detail.status' | translate">
|
<ds-process-detail-field *ngIf="process && process.processStatus" id="process-status"
|
||||||
|
[title]="'process.detail.status' | translate">
|
||||||
<div>{{ process.processStatus }}</div>
|
<div>{{ process.processStatus }}</div>
|
||||||
</ds-process-detail-field>
|
</ds-process-detail-field>
|
||||||
|
|
||||||
<ds-process-detail-field *ngIf="isProcessFinished(process)" id="process-output" [title]="'process.detail.output'">
|
<ds-process-detail-field *ngIf="isProcessFinished(process)" id="process-output" [title]="'process.detail.output'">
|
||||||
<button *ngIf="!showOutputLogs && process?._links?.output?.href != undefined" id="showOutputButton" class="btn btn-primary" (click)="showProcessOutputLogs()">
|
<button *ngIf="!showOutputLogs && process?._links?.output?.href != undefined" id="showOutputButton"
|
||||||
|
class="btn btn-primary" (click)="showProcessOutputLogs()">
|
||||||
{{ 'process.detail.logs.button' | translate }}
|
{{ 'process.detail.logs.button' | translate }}
|
||||||
</button>
|
</button>
|
||||||
<ds-themed-loading *ngIf="retrievingOutputLogs$ | async" class="ds-themed-loading" message="{{ 'process.detail.logs.loading' | translate }}"></ds-themed-loading>
|
<ds-themed-loading *ngIf="retrievingOutputLogs$ | async" class="ds-themed-loading"
|
||||||
|
message="{{ 'process.detail.logs.loading' | translate }}"></ds-themed-loading>
|
||||||
<pre class="font-weight-bold text-secondary bg-light p-3"
|
<pre class="font-weight-bold text-secondary bg-light p-3"
|
||||||
*ngIf="showOutputLogs && (outputLogs$ | async)?.length > 0">{{ (outputLogs$ | async) }}</pre>
|
*ngIf="showOutputLogs && (outputLogs$ | async)?.length > 0">{{ (outputLogs$ | async) }}</pre>
|
||||||
<p id="no-output-logs-message" *ngIf="(!(retrievingOutputLogs$ | async) && showOutputLogs)
|
<p id="no-output-logs-message" *ngIf="(!(retrievingOutputLogs$ | async) && showOutputLogs)
|
||||||
@@ -47,7 +54,46 @@
|
|||||||
</p>
|
</p>
|
||||||
</ds-process-detail-field>
|
</ds-process-detail-field>
|
||||||
|
|
||||||
|
<ds-process-detail-field id="process-actions" [title]="'process.detail.actions'">
|
||||||
|
<button class="btn btn-success mr-2" routerLink="/processes/new" [queryParams]="{id: process?.processId}"><i
|
||||||
|
class="fas fa-plus pr-2"></i>{{'process.detail.create' | translate}}</button>
|
||||||
|
<button *ngIf="isProcessFinished(process)" id="delete" class="btn btn-danger"
|
||||||
|
(click)="openDeleteModal(deleteModal)">
|
||||||
|
<i class="fas fa-trash pr-2"></i>{{ 'process.detail.delete.button' | translate }}
|
||||||
|
</button>
|
||||||
|
</ds-process-detail-field>
|
||||||
|
|
||||||
<div style="text-align: right;">
|
<div style="text-align: right;">
|
||||||
<a class="btn btn-outline-secondary mt-3" [routerLink]="'/processes'">{{'process.detail.back' | translate}}</a>
|
<a class="btn btn-outline-secondary mt-3" [routerLink]="'/processes'">{{'process.detail.back' | translate}}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ng-template #deleteModal >
|
||||||
|
|
||||||
|
<div *ngVar="(processRD$ | async)?.payload as process">
|
||||||
|
|
||||||
|
<div class="modal-header">
|
||||||
|
<div>
|
||||||
|
<h4>{{'process.detail.delete.header' | translate }}</h4>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="close"
|
||||||
|
(click)="closeModal()" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<div>{{'process.detail.delete.body' | translate }}</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<button class="btn btn-primary mr-2" (click)="closeModal()">{{'process.detail.delete.cancel' | translate}}</button>
|
||||||
|
<button id="delete-confirm" class="btn btn-danger"
|
||||||
|
(click)="deleteProcess(process)">{{ 'process.detail.delete.confirm' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
@@ -19,15 +19,23 @@ import { RouterTestingModule } from '@angular/router/testing';
|
|||||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import { ProcessDetailFieldComponent } from './process-detail-field/process-detail-field.component';
|
import { ProcessDetailFieldComponent } from './process-detail-field/process-detail-field.component';
|
||||||
import { Process } from '../processes/process.model';
|
import { Process } from '../processes/process.model';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { FileSizePipe } from '../../shared/utils/file-size-pipe';
|
import { FileSizePipe } from '../../shared/utils/file-size-pipe';
|
||||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||||
import { ProcessDataService } from '../../core/data/processes/process-data.service';
|
import { ProcessDataService } from '../../core/data/processes/process-data.service';
|
||||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
import {
|
||||||
|
createFailedRemoteDataObject$,
|
||||||
|
createSuccessfulRemoteDataObject,
|
||||||
|
createSuccessfulRemoteDataObject$
|
||||||
|
} from '../../shared/remote-data.utils';
|
||||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||||
|
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { getProcessListRoute } from '../process-page-routing.paths';
|
||||||
|
|
||||||
describe('ProcessDetailComponent', () => {
|
describe('ProcessDetailComponent', () => {
|
||||||
let component: ProcessDetailComponent;
|
let component: ProcessDetailComponent;
|
||||||
@@ -44,6 +52,11 @@ describe('ProcessDetailComponent', () => {
|
|||||||
|
|
||||||
let processOutput;
|
let processOutput;
|
||||||
|
|
||||||
|
let modalService;
|
||||||
|
let notificationsService;
|
||||||
|
|
||||||
|
let router;
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
processOutput = 'Process Started';
|
processOutput = 'Process Started';
|
||||||
process = Object.assign(new Process(), {
|
process = Object.assign(new Process(), {
|
||||||
@@ -93,7 +106,8 @@ describe('ProcessDetailComponent', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
processService = jasmine.createSpyObj('processService', {
|
processService = jasmine.createSpyObj('processService', {
|
||||||
getFiles: createSuccessfulRemoteDataObject$(createPaginatedList(files))
|
getFiles: createSuccessfulRemoteDataObject$(createPaginatedList(files)),
|
||||||
|
delete: createSuccessfulRemoteDataObject$(null)
|
||||||
});
|
});
|
||||||
bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', {
|
bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', {
|
||||||
findByHref: createSuccessfulRemoteDataObject$(logBitstream)
|
findByHref: createSuccessfulRemoteDataObject$(logBitstream)
|
||||||
@@ -104,13 +118,23 @@ describe('ProcessDetailComponent', () => {
|
|||||||
httpClient = jasmine.createSpyObj('httpClient', {
|
httpClient = jasmine.createSpyObj('httpClient', {
|
||||||
get: observableOf(processOutput)
|
get: observableOf(processOutput)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modalService = jasmine.createSpyObj('modalService', {
|
||||||
|
open: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
notificationsService = new NotificationsServiceStub();
|
||||||
|
|
||||||
|
router = jasmine.createSpyObj('router', {
|
||||||
|
navigateByUrl:{}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
init();
|
init();
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ProcessDetailComponent, ProcessDetailFieldComponent, VarDirective, FileSizePipe],
|
declarations: [ProcessDetailComponent, ProcessDetailFieldComponent, VarDirective, FileSizePipe],
|
||||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
imports: [TranslateModule.forRoot()],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
provide: ActivatedRoute,
|
provide: ActivatedRoute,
|
||||||
@@ -121,6 +145,9 @@ describe('ProcessDetailComponent', () => {
|
|||||||
{ provide: DSONameService, useValue: nameService },
|
{ provide: DSONameService, useValue: nameService },
|
||||||
{ provide: AuthService, useValue: new AuthServiceMock() },
|
{ provide: AuthService, useValue: new AuthServiceMock() },
|
||||||
{ provide: HttpClient, useValue: httpClient },
|
{ provide: HttpClient, useValue: httpClient },
|
||||||
|
{ provide: NgbModal, useValue: modalService },
|
||||||
|
{ provide: NotificationsService, useValue: notificationsService },
|
||||||
|
{ provide: Router, useValue: router },
|
||||||
],
|
],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
@@ -207,4 +234,34 @@ describe('ProcessDetailComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('openDeleteModal', () => {
|
||||||
|
it('should open the modal', () => {
|
||||||
|
component.openDeleteModal({});
|
||||||
|
expect(modalService.open).toHaveBeenCalledWith({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteProcess', () => {
|
||||||
|
it('should delete the process and navigate back to the overview page on success', () => {
|
||||||
|
spyOn(component, 'closeModal');
|
||||||
|
component.deleteProcess(process);
|
||||||
|
|
||||||
|
expect(processService.delete).toHaveBeenCalledWith(process.processId);
|
||||||
|
expect(notificationsService.success).toHaveBeenCalled();
|
||||||
|
expect(component.closeModal).toHaveBeenCalled();
|
||||||
|
expect(router.navigateByUrl).toHaveBeenCalledWith(getProcessListRoute());
|
||||||
|
});
|
||||||
|
it('should delete the process and not navigate on error', () => {
|
||||||
|
(processService.delete as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$());
|
||||||
|
spyOn(component, 'closeModal');
|
||||||
|
|
||||||
|
component.deleteProcess(process);
|
||||||
|
|
||||||
|
expect(processService.delete).toHaveBeenCalledWith(process.processId);
|
||||||
|
expect(notificationsService.error).toHaveBeenCalled();
|
||||||
|
expect(component.closeModal).not.toHaveBeenCalled();
|
||||||
|
expect(router.navigateByUrl).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -12,8 +12,9 @@ import { RemoteData } from '../../core/data/remote-data';
|
|||||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
import {
|
import {
|
||||||
getFirstSucceededRemoteDataPayload,
|
getFirstCompletedRemoteData,
|
||||||
getFirstSucceededRemoteData
|
getFirstSucceededRemoteData,
|
||||||
|
getFirstSucceededRemoteDataPayload
|
||||||
} from '../../core/shared/operators';
|
} from '../../core/shared/operators';
|
||||||
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
||||||
import { AlertType } from '../../shared/alert/aletr-type';
|
import { AlertType } from '../../shared/alert/aletr-type';
|
||||||
@@ -21,6 +22,10 @@ import { hasValue } from '../../shared/empty.util';
|
|||||||
import { ProcessStatus } from '../processes/process-status.model';
|
import { ProcessStatus } from '../processes/process-status.model';
|
||||||
import { Process } from '../processes/process.model';
|
import { Process } from '../processes/process.model';
|
||||||
import { redirectOn4xx } from '../../core/shared/authorized.operators';
|
import { redirectOn4xx } from '../../core/shared/authorized.operators';
|
||||||
|
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { getProcessListRoute } from '../process-page-routing.paths';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-process-detail',
|
selector: 'ds-process-detail',
|
||||||
@@ -71,6 +76,11 @@ export class ProcessDetailComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
dateFormat = 'yyyy-MM-dd HH:mm:ss ZZZZ';
|
dateFormat = 'yyyy-MM-dd HH:mm:ss ZZZZ';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to NgbModal
|
||||||
|
*/
|
||||||
|
protected modalRef: NgbModalRef;
|
||||||
|
|
||||||
constructor(protected route: ActivatedRoute,
|
constructor(protected route: ActivatedRoute,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
protected processService: ProcessDataService,
|
protected processService: ProcessDataService,
|
||||||
@@ -78,7 +88,11 @@ export class ProcessDetailComponent implements OnInit {
|
|||||||
protected nameService: DSONameService,
|
protected nameService: DSONameService,
|
||||||
private zone: NgZone,
|
private zone: NgZone,
|
||||||
protected authService: AuthService,
|
protected authService: AuthService,
|
||||||
protected http: HttpClient) {
|
protected http: HttpClient,
|
||||||
|
protected modalService: NgbModal,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected translateService: TranslateService
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -172,4 +186,36 @@ export class ProcessDetailComponent implements OnInit {
|
|||||||
|| process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()));
|
|| process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the current process
|
||||||
|
* @param process
|
||||||
|
*/
|
||||||
|
deleteProcess(process: Process) {
|
||||||
|
this.processService.delete(process.processId).pipe(
|
||||||
|
getFirstCompletedRemoteData()
|
||||||
|
).subscribe((rd) => {
|
||||||
|
if (rd.hasSucceeded) {
|
||||||
|
this.notificationsService.success(this.translateService.get('process.detail.delete.success'));
|
||||||
|
this.closeModal();
|
||||||
|
this.router.navigateByUrl(getProcessListRoute());
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(this.translateService.get('process.detail.delete.error'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open a given modal.
|
||||||
|
* @param content - the modal content.
|
||||||
|
*/
|
||||||
|
openDeleteModal(content) {
|
||||||
|
this.modalRef = this.modalService.open(content);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Close the modal.
|
||||||
|
*/
|
||||||
|
closeModal() {
|
||||||
|
this.modalRef.close();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,149 @@
|
|||||||
|
import { ProcessBulkDeleteService } from './process-bulk-delete.service';
|
||||||
|
import { waitForAsync } from '@angular/core/testing';
|
||||||
|
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
||||||
|
import { getMockTranslateService } from '../../shared/mocks/translate.service.mock';
|
||||||
|
|
||||||
|
describe('ProcessBulkDeleteService', () => {
|
||||||
|
|
||||||
|
let service: ProcessBulkDeleteService;
|
||||||
|
let processDataService;
|
||||||
|
let notificationsService;
|
||||||
|
let mockTranslateService;
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
processDataService = jasmine.createSpyObj('processDataService', {
|
||||||
|
delete: createSuccessfulRemoteDataObject$(null)
|
||||||
|
});
|
||||||
|
notificationsService = new NotificationsServiceStub();
|
||||||
|
mockTranslateService = getMockTranslateService();
|
||||||
|
service = new ProcessBulkDeleteService(processDataService, notificationsService, mockTranslateService);
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('toggleDelete', () => {
|
||||||
|
it('should add a new value to the processesToDelete list when not yet present', () => {
|
||||||
|
service.toggleDelete('test-id-1');
|
||||||
|
service.toggleDelete('test-id-2');
|
||||||
|
|
||||||
|
expect(service.processesToDelete).toEqual(['test-id-1', 'test-id-2']);
|
||||||
|
});
|
||||||
|
it('should remove a value from the processesToDelete list when already present', () => {
|
||||||
|
service.toggleDelete('test-id-1');
|
||||||
|
service.toggleDelete('test-id-2');
|
||||||
|
|
||||||
|
expect(service.processesToDelete).toEqual(['test-id-1', 'test-id-2']);
|
||||||
|
|
||||||
|
service.toggleDelete('test-id-1');
|
||||||
|
expect(service.processesToDelete).toEqual(['test-id-2']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isToBeDeleted', () => {
|
||||||
|
it('should return true when the provided process id is present in the list', () => {
|
||||||
|
service.toggleDelete('test-id-1');
|
||||||
|
service.toggleDelete('test-id-2');
|
||||||
|
|
||||||
|
expect(service.isToBeDeleted('test-id-1')).toBeTrue();
|
||||||
|
});
|
||||||
|
it('should return false when the provided process id is not present in the list', () => {
|
||||||
|
service.toggleDelete('test-id-1');
|
||||||
|
service.toggleDelete('test-id-2');
|
||||||
|
|
||||||
|
expect(service.isToBeDeleted('test-id-3')).toBeFalse();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clearAllProcesses', () => {
|
||||||
|
it('should clear the list of to be deleted processes', () => {
|
||||||
|
service.toggleDelete('test-id-1');
|
||||||
|
service.toggleDelete('test-id-2');
|
||||||
|
|
||||||
|
expect(service.processesToDelete).toEqual(['test-id-1', 'test-id-2']);
|
||||||
|
|
||||||
|
service.clearAllProcesses();
|
||||||
|
expect(service.processesToDelete).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('getAmountOfSelectedProcesses', () => {
|
||||||
|
it('should return the amount of the currently selected processes for deletion', () => {
|
||||||
|
service.toggleDelete('test-id-1');
|
||||||
|
service.toggleDelete('test-id-2');
|
||||||
|
|
||||||
|
expect(service.getAmountOfSelectedProcesses()).toEqual(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('isProcessing$', () => {
|
||||||
|
it('should return a behavior subject containing whether a delete is currently processing or not', () => {
|
||||||
|
const result = service.isProcessing$();
|
||||||
|
expect(result.getValue()).toBeFalse();
|
||||||
|
|
||||||
|
result.next(true);
|
||||||
|
expect(result.getValue()).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('hasSelected', () => {
|
||||||
|
it('should return if the list of selected processes has values', () => {
|
||||||
|
expect(service.hasSelected()).toBeFalse();
|
||||||
|
|
||||||
|
service.toggleDelete('test-id-1');
|
||||||
|
service.toggleDelete('test-id-2');
|
||||||
|
|
||||||
|
expect(service.hasSelected()).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('deleteSelectedProcesses', () => {
|
||||||
|
it('should delete all selected processes, show an error for each failed one and a notification at the end with the amount of succeeded deletions', () => {
|
||||||
|
(processDataService.delete as jasmine.Spy).and.callFake((processId: string) => {
|
||||||
|
if (processId.includes('error')) {
|
||||||
|
return createFailedRemoteDataObject$();
|
||||||
|
} else {
|
||||||
|
return createSuccessfulRemoteDataObject$(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
service.toggleDelete('test-id-1');
|
||||||
|
service.toggleDelete('test-id-2');
|
||||||
|
service.toggleDelete('error-id-3');
|
||||||
|
service.toggleDelete('test-id-4');
|
||||||
|
service.toggleDelete('error-id-5');
|
||||||
|
service.toggleDelete('error-id-6');
|
||||||
|
service.toggleDelete('test-id-7');
|
||||||
|
|
||||||
|
|
||||||
|
service.deleteSelectedProcesses();
|
||||||
|
|
||||||
|
expect(processDataService.delete).toHaveBeenCalledWith('test-id-1');
|
||||||
|
|
||||||
|
|
||||||
|
expect(processDataService.delete).toHaveBeenCalledWith('test-id-2');
|
||||||
|
|
||||||
|
|
||||||
|
expect(processDataService.delete).toHaveBeenCalledWith('error-id-3');
|
||||||
|
expect(notificationsService.error).toHaveBeenCalled();
|
||||||
|
expect(mockTranslateService.get).toHaveBeenCalledWith('process.bulk.delete.error.body', {processId: 'error-id-3'});
|
||||||
|
|
||||||
|
|
||||||
|
expect(processDataService.delete).toHaveBeenCalledWith('test-id-4');
|
||||||
|
|
||||||
|
|
||||||
|
expect(processDataService.delete).toHaveBeenCalledWith('error-id-5');
|
||||||
|
expect(notificationsService.error).toHaveBeenCalled();
|
||||||
|
expect(mockTranslateService.get).toHaveBeenCalledWith('process.bulk.delete.error.body', {processId: 'error-id-5'});
|
||||||
|
|
||||||
|
|
||||||
|
expect(processDataService.delete).toHaveBeenCalledWith('error-id-6');
|
||||||
|
expect(notificationsService.error).toHaveBeenCalled();
|
||||||
|
expect(mockTranslateService.get).toHaveBeenCalledWith('process.bulk.delete.error.body', {processId: 'error-id-6'});
|
||||||
|
|
||||||
|
|
||||||
|
expect(processDataService.delete).toHaveBeenCalledWith('test-id-7');
|
||||||
|
|
||||||
|
expect(notificationsService.success).toHaveBeenCalled();
|
||||||
|
expect(mockTranslateService.get).toHaveBeenCalledWith('process.bulk.delete.success', {count: 4});
|
||||||
|
|
||||||
|
expect(service.processesToDelete).toEqual(['error-id-3', 'error-id-5', 'error-id-6']);
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
118
src/app/process-page/overview/process-bulk-delete.service.ts
Normal file
118
src/app/process-page/overview/process-bulk-delete.service.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import { Process } from '../processes/process.model';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ProcessDataService } from '../../core/data/processes/process-data.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { BehaviorSubject, count, from } from 'rxjs';
|
||||||
|
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
|
||||||
|
import { concatMap, filter, tap } from 'rxjs/operators';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Service to facilitate removing processes in bulk.
|
||||||
|
*/
|
||||||
|
export class ProcessBulkDeleteService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array to track the processes to be deleted
|
||||||
|
*/
|
||||||
|
processesToDelete: string[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Behavior subject to track whether the delete is processing
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected isProcessingBehaviorSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected processDataService: ProcessDataService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected translateService: TranslateService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add or remove a process id to/from the list
|
||||||
|
* If the id is already present it will be removed, otherwise it will be added.
|
||||||
|
*
|
||||||
|
* @param processId - The process id to add or remove
|
||||||
|
*/
|
||||||
|
toggleDelete(processId: string) {
|
||||||
|
if (this.isToBeDeleted(processId)) {
|
||||||
|
this.processesToDelete.splice(this.processesToDelete.indexOf(processId), 1);
|
||||||
|
} else {
|
||||||
|
this.processesToDelete.push(processId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the provided process id is present in the to be deleted list
|
||||||
|
* @param processId
|
||||||
|
*/
|
||||||
|
isToBeDeleted(processId: string) {
|
||||||
|
return this.processesToDelete.includes(processId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the list of processes to be deleted
|
||||||
|
*/
|
||||||
|
clearAllProcesses() {
|
||||||
|
this.processesToDelete.splice(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the amount of processes selected for deletion
|
||||||
|
*/
|
||||||
|
getAmountOfSelectedProcesses() {
|
||||||
|
return this.processesToDelete.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a behavior subject to indicate whether the bulk delete is processing
|
||||||
|
*/
|
||||||
|
isProcessing$() {
|
||||||
|
return this.isProcessingBehaviorSubject;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether there currently are values selected for deletion
|
||||||
|
*/
|
||||||
|
hasSelected(): boolean {
|
||||||
|
return isNotEmpty(this.processesToDelete);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all selected processes one by one
|
||||||
|
* When the deletion for a process fails, an error notification will be shown with the process id,
|
||||||
|
* but it will continue deleting the other processes.
|
||||||
|
* At the end it will show a notification stating the amount of successful deletes
|
||||||
|
* The successfully deleted processes will be removed from the list of selected values, the failed ones will be retained.
|
||||||
|
*/
|
||||||
|
deleteSelectedProcesses() {
|
||||||
|
this.isProcessingBehaviorSubject.next(true);
|
||||||
|
|
||||||
|
from([...this.processesToDelete]).pipe(
|
||||||
|
concatMap((processId) => {
|
||||||
|
return this.processDataService.delete(processId).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
tap((rd: RemoteData<Process>) => {
|
||||||
|
if (rd.hasFailed) {
|
||||||
|
this.notificationsService.error(this.translateService.get('process.bulk.delete.error.head'), this.translateService.get('process.bulk.delete.error.body', {processId: processId}));
|
||||||
|
} else {
|
||||||
|
this.toggleDelete(processId);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
filter((rd: RemoteData<Process>) => rd.hasSucceeded),
|
||||||
|
count(),
|
||||||
|
).subscribe((value) => {
|
||||||
|
this.notificationsService.success(this.translateService.get('process.bulk.delete.success', {count: value}));
|
||||||
|
this.isProcessingBehaviorSubject.next(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -1,7 +1,19 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<h2 class="flex-grow-1">{{'process.overview.title' | translate}}</h2>
|
<h2 class="flex-grow-1">{{'process.overview.title' | translate}}</h2>
|
||||||
<button class="btn btn-lg btn-success " routerLink="/processes/new"><i class="fas fa-plus pr-2"></i>{{'process.overview.new' | translate}}</button>
|
</div>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<button *ngIf="processBulkDeleteService.hasSelected()" class="btn btn-primary mr-2"
|
||||||
|
(click)="processBulkDeleteService.clearAllProcesses()"><i
|
||||||
|
class="fas fa-undo pr-2"></i>{{'process.overview.delete.clear' | translate }}
|
||||||
|
</button>
|
||||||
|
<button *ngIf="processBulkDeleteService.hasSelected()" class="btn btn-danger mr-2"
|
||||||
|
(click)="openDeleteModal(deleteModal)"><i
|
||||||
|
class="fas fa-trash pr-2"></i>{{'process.overview.delete' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-success" routerLink="/processes/new"><i
|
||||||
|
class="fas fa-plus pr-2"></i>{{'process.overview.new' | translate}}</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<ds-pagination *ngIf="(processesRD$ | async)?.payload?.totalElements > 0"
|
<ds-pagination *ngIf="(processesRD$ | async)?.payload?.totalElements > 0"
|
||||||
[paginationOptions]="pageConfig"
|
[paginationOptions]="pageConfig"
|
||||||
@@ -19,19 +31,61 @@
|
|||||||
<th scope="col">{{'process.overview.table.start' | translate}}</th>
|
<th scope="col">{{'process.overview.table.start' | translate}}</th>
|
||||||
<th scope="col">{{'process.overview.table.finish' | translate}}</th>
|
<th scope="col">{{'process.overview.table.finish' | translate}}</th>
|
||||||
<th scope="col">{{'process.overview.table.status' | translate}}</th>
|
<th scope="col">{{'process.overview.table.status' | translate}}</th>
|
||||||
|
<th scope="col">{{'process.overview.table.actions' | translate}}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let process of (processesRD$ | async)?.payload?.page">
|
<tr *ngFor="let process of (processesRD$ | async)?.payload?.page"
|
||||||
|
[class.table-danger]="processBulkDeleteService.isToBeDeleted(process.processId)">
|
||||||
<td><a [routerLink]="['/processes/', process.processId]">{{process.processId}}</a></td>
|
<td><a [routerLink]="['/processes/', process.processId]">{{process.processId}}</a></td>
|
||||||
<td><a [routerLink]="['/processes/', process.processId]">{{process.scriptName}}</a></td>
|
<td><a [routerLink]="['/processes/', process.processId]">{{process.scriptName}}</a></td>
|
||||||
<td *ngVar="(getEpersonName(process.userId) | async) as ePersonName">{{ePersonName}}</td>
|
<td *ngVar="(getEpersonName(process.userId) | async) as ePersonName">{{ePersonName}}</td>
|
||||||
<td>{{process.startTime | date:dateFormat:'UTC'}}</td>
|
<td>{{process.startTime | date:dateFormat:'UTC'}}</td>
|
||||||
<td>{{process.endTime | date:dateFormat:'UTC'}}</td>
|
<td>{{process.endTime | date:dateFormat:'UTC'}}</td>
|
||||||
<td>{{process.processStatus}}</td>
|
<td>{{process.processStatus}}</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-outline-danger"
|
||||||
|
(click)="processBulkDeleteService.toggleDelete(process.processId)"><i
|
||||||
|
class="fas fa-trash"></i></button>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</ds-pagination>
|
</ds-pagination>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ng-template #deleteModal>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<div class="modal-header">
|
||||||
|
<div>
|
||||||
|
<h4>{{'process.overview.delete.header' | translate }}</h4>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="close"
|
||||||
|
(click)="closeModal()" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<div *ngIf="!(processBulkDeleteService.isProcessing$() |async)">{{'process.overview.delete.body' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}</div>
|
||||||
|
<div *ngIf="processBulkDeleteService.isProcessing$() |async" class="alert alert-info">
|
||||||
|
<span class="spinner-border spinner-border-sm spinner-button" role="status" aria-hidden="true"></span>
|
||||||
|
<span> {{ 'process.overview.delete.processing' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<button class="btn btn-primary mr-2" [disabled]="processBulkDeleteService.isProcessing$() |async"
|
||||||
|
(click)="closeModal()">{{'process.detail.delete.cancel' | translate}}</button>
|
||||||
|
<button id="delete-confirm" class="btn btn-danger"
|
||||||
|
[disabled]="processBulkDeleteService.isProcessing$() |async"
|
||||||
|
(click)="deleteSelected()">{{ 'process.overview.delete' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { ProcessOverviewComponent } from './process-overview.component';
|
import { ProcessOverviewComponent } from './process-overview.component';
|
||||||
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
import { VarDirective } from '../../shared/utils/var.directive';
|
import { VarDirective } from '../../shared/utils/var.directive';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
@@ -13,11 +13,11 @@ import { ProcessStatus } from '../processes/process-status.model';
|
|||||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
|
||||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
|
||||||
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
||||||
import { FindListOptions } from '../../core/data/find-list-options.model';
|
|
||||||
import { DatePipe } from '@angular/common';
|
import { DatePipe } from '@angular/common';
|
||||||
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
import { ProcessBulkDeleteService } from './process-bulk-delete.service';
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
describe('ProcessOverviewComponent', () => {
|
describe('ProcessOverviewComponent', () => {
|
||||||
let component: ProcessOverviewComponent;
|
let component: ProcessOverviewComponent;
|
||||||
@@ -30,6 +30,9 @@ describe('ProcessOverviewComponent', () => {
|
|||||||
let processes: Process[];
|
let processes: Process[];
|
||||||
let ePerson: EPerson;
|
let ePerson: EPerson;
|
||||||
|
|
||||||
|
let processBulkDeleteService;
|
||||||
|
let modalService;
|
||||||
|
|
||||||
const pipe = new DatePipe('en-US');
|
const pipe = new DatePipe('en-US');
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
@@ -80,6 +83,29 @@ describe('ProcessOverviewComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
paginationService = new PaginationServiceStub();
|
paginationService = new PaginationServiceStub();
|
||||||
|
|
||||||
|
processBulkDeleteService = jasmine.createSpyObj('processBulkDeleteService', {
|
||||||
|
clearAllProcesses: {},
|
||||||
|
deleteSelectedProcesses: {},
|
||||||
|
isProcessing$: new BehaviorSubject(false),
|
||||||
|
hasSelected: true,
|
||||||
|
isToBeDeleted: true,
|
||||||
|
toggleDelete: {},
|
||||||
|
getAmountOfSelectedProcesses: 5
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
(processBulkDeleteService.isToBeDeleted as jasmine.Spy).and.callFake((id) => {
|
||||||
|
if (id === 2) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
modalService = jasmine.createSpyObj('modalService', {
|
||||||
|
open: {}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
@@ -90,7 +116,9 @@ describe('ProcessOverviewComponent', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{ provide: ProcessDataService, useValue: processService },
|
{ provide: ProcessDataService, useValue: processService },
|
||||||
{ provide: EPersonDataService, useValue: ePersonService },
|
{ provide: EPersonDataService, useValue: ePersonService },
|
||||||
{ provide: PaginationService, useValue: paginationService }
|
{ provide: PaginationService, useValue: paginationService },
|
||||||
|
{ provide: ProcessBulkDeleteService, useValue: processBulkDeleteService },
|
||||||
|
{ provide: NgbModal, useValue: modalService },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
@@ -154,5 +182,71 @@ describe('ProcessOverviewComponent', () => {
|
|||||||
expect(el.textContent).toContain(processes[index].processStatus);
|
expect(el.textContent).toContain(processes[index].processStatus);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
it('should display a delete button in the seventh column', () => {
|
||||||
|
rowElements.forEach((rowElement, index) => {
|
||||||
|
const el = rowElement.query(By.css('td:nth-child(7)'));
|
||||||
|
expect(el.nativeElement.innerHTML).toContain('fas fa-trash');
|
||||||
|
|
||||||
|
el.query(By.css('button')).triggerEventHandler('click', null);
|
||||||
|
expect(processBulkDeleteService.toggleDelete).toHaveBeenCalledWith(processes[index].processId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should indicate a row that has been selected for deletion', () => {
|
||||||
|
const deleteRow = fixture.debugElement.query(By.css('.table-danger'));
|
||||||
|
expect(deleteRow.nativeElement.innerHTML).toContain('/processes/' + processes[1].processId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('overview buttons', () => {
|
||||||
|
it('should show a button to clear selected processes when there are selected processes', () => {
|
||||||
|
const clearButton = fixture.debugElement.query(By.css('.btn-primary'));
|
||||||
|
expect(clearButton.nativeElement.innerHTML).toContain('process.overview.delete.clear');
|
||||||
|
|
||||||
|
clearButton.triggerEventHandler('click', null);
|
||||||
|
expect(processBulkDeleteService.clearAllProcesses).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
it('should not show a button to clear selected processes when there are no selected processes', () => {
|
||||||
|
(processBulkDeleteService.hasSelected as jasmine.Spy).and.returnValue(false);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const clearButton = fixture.debugElement.query(By.css('.btn-primary'));
|
||||||
|
expect(clearButton).toBeNull();
|
||||||
|
});
|
||||||
|
it('should show a button to open the delete modal when there are selected processes', () => {
|
||||||
|
spyOn(component, 'openDeleteModal');
|
||||||
|
|
||||||
|
const deleteButton = fixture.debugElement.query(By.css('.btn-danger'));
|
||||||
|
expect(deleteButton.nativeElement.innerHTML).toContain('process.overview.delete');
|
||||||
|
|
||||||
|
deleteButton.triggerEventHandler('click', null);
|
||||||
|
expect(component.openDeleteModal).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
it('should not show a button to clear selected processes when there are no selected processes', () => {
|
||||||
|
(processBulkDeleteService.hasSelected as jasmine.Spy).and.returnValue(false);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const deleteButton = fixture.debugElement.query(By.css('.btn-danger'));
|
||||||
|
expect(deleteButton).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('openDeleteModal', () => {
|
||||||
|
it('should open the modal', () => {
|
||||||
|
component.openDeleteModal({});
|
||||||
|
expect(modalService.open).toHaveBeenCalledWith({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteSelected', () => {
|
||||||
|
it('should call the deleteSelectedProcesses method on the processBulkDeleteService and close the modal when processing is done', () => {
|
||||||
|
spyOn(component, 'closeModal');
|
||||||
|
spyOn(component, 'setProcesses');
|
||||||
|
|
||||||
|
component.deleteSelected();
|
||||||
|
|
||||||
|
expect(processBulkDeleteService.deleteSelectedProcesses).toHaveBeenCalled();
|
||||||
|
expect(component.closeModal).toHaveBeenCalled();
|
||||||
|
expect(component.setProcesses).toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, Subscription } from 'rxjs';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { PaginatedList } from '../../core/data/paginated-list.model';
|
import { PaginatedList } from '../../core/data/paginated-list.model';
|
||||||
import { Process } from '../processes/process.model';
|
import { Process } from '../processes/process.model';
|
||||||
@@ -11,6 +11,9 @@ import { map, switchMap } from 'rxjs/operators';
|
|||||||
import { ProcessDataService } from '../../core/data/processes/process-data.service';
|
import { ProcessDataService } from '../../core/data/processes/process-data.service';
|
||||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||||
import { FindListOptions } from '../../core/data/find-list-options.model';
|
import { FindListOptions } from '../../core/data/find-list-options.model';
|
||||||
|
import { ProcessBulkDeleteService } from './process-bulk-delete.service';
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-process-overview',
|
selector: 'ds-process-overview',
|
||||||
@@ -19,7 +22,7 @@ import { FindListOptions } from '../../core/data/find-list-options.model';
|
|||||||
/**
|
/**
|
||||||
* Component displaying a list of all processes in a paginated table
|
* Component displaying a list of all processes in a paginated table
|
||||||
*/
|
*/
|
||||||
export class ProcessOverviewComponent implements OnInit {
|
export class ProcessOverviewComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of all processes
|
* List of all processes
|
||||||
@@ -46,13 +49,22 @@ export class ProcessOverviewComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
dateFormat = 'yyyy-MM-dd HH:mm:ss';
|
dateFormat = 'yyyy-MM-dd HH:mm:ss';
|
||||||
|
|
||||||
|
processesToDelete: string[] = [];
|
||||||
|
private modalRef: any;
|
||||||
|
|
||||||
|
isProcessingSub: Subscription;
|
||||||
|
|
||||||
constructor(protected processService: ProcessDataService,
|
constructor(protected processService: ProcessDataService,
|
||||||
protected paginationService: PaginationService,
|
protected paginationService: PaginationService,
|
||||||
protected ePersonService: EPersonDataService) {
|
protected ePersonService: EPersonDataService,
|
||||||
|
protected modalService: NgbModal,
|
||||||
|
public processBulkDeleteService: ProcessBulkDeleteService,
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.setProcesses();
|
this.setProcesses();
|
||||||
|
this.processBulkDeleteService.clearAllProcesses();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,7 +72,7 @@ export class ProcessOverviewComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
setProcesses() {
|
setProcesses() {
|
||||||
this.processesRD$ = this.paginationService.getFindListOptions(this.pageConfig.id, this.config).pipe(
|
this.processesRD$ = this.paginationService.getFindListOptions(this.pageConfig.id, this.config).pipe(
|
||||||
switchMap((config) => this.processService.findAll(config))
|
switchMap((config) => this.processService.findAll(config, true, false))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,8 +86,46 @@ export class ProcessOverviewComponent implements OnInit {
|
|||||||
map((eperson: EPerson) => eperson.name)
|
map((eperson: EPerson) => eperson.name)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.paginationService.clearPagination(this.pageConfig.id);
|
this.paginationService.clearPagination(this.pageConfig.id);
|
||||||
|
if (hasValue(this.isProcessingSub)) {
|
||||||
|
this.isProcessingSub.unsubscribe();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open a given modal.
|
||||||
|
* @param content - the modal content.
|
||||||
|
*/
|
||||||
|
openDeleteModal(content) {
|
||||||
|
this.modalRef = this.modalService.open(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the modal.
|
||||||
|
*/
|
||||||
|
closeModal() {
|
||||||
|
this.modalRef.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the previously selected processes using the processBulkDeleteService
|
||||||
|
* After the deletion has started, subscribe to the isProcessing$ and when it is set
|
||||||
|
* to false after the processing is done, close the modal and reinitialise the processes
|
||||||
|
*/
|
||||||
|
deleteSelected() {
|
||||||
|
this.processBulkDeleteService.deleteSelectedProcesses();
|
||||||
|
|
||||||
|
if (hasValue(this.isProcessingSub)) {
|
||||||
|
this.isProcessingSub.unsubscribe();
|
||||||
|
}
|
||||||
|
this.isProcessingSub = this.processBulkDeleteService.isProcessing$()
|
||||||
|
.subscribe((isProcessing) => {
|
||||||
|
if (!isProcessing) {
|
||||||
|
this.closeModal();
|
||||||
|
this.setProcesses();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -24,7 +24,7 @@ import { ConfirmationModalComponent } from '../../shared/confirmation-modal/conf
|
|||||||
templateUrl: './profile-page-researcher-form.component.html',
|
templateUrl: './profile-page-researcher-form.component.html',
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
* Component for a user to create/delete or change his researcher profile.
|
* Component for a user to create/delete or change their researcher profile.
|
||||||
*/
|
*/
|
||||||
export class ProfilePageResearcherFormComponent implements OnInit {
|
export class ProfilePageResearcherFormComponent implements OnInit {
|
||||||
|
|
||||||
|
@@ -240,10 +240,12 @@ describe('BrowseByComponent', () => {
|
|||||||
});
|
});
|
||||||
describe('back', () => {
|
describe('back', () => {
|
||||||
it('should navigate back to the main browse page', () => {
|
it('should navigate back to the main browse page', () => {
|
||||||
|
const id = 'test-pagination';
|
||||||
comp.back();
|
comp.back();
|
||||||
expect(paginationService.updateRoute).toHaveBeenCalledWith('test-pagination', {page: 1}, {
|
expect(paginationService.updateRoute).toHaveBeenCalledWith(id, {page: 1}, {
|
||||||
value: null,
|
value: null,
|
||||||
startsWith: null
|
startsWith: null,
|
||||||
|
[id + '.return']: null
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
import { Component, EventEmitter, Injector, Input, OnInit, Output } from '@angular/core';
|
import { Component, EventEmitter, Injector, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { PaginatedList } from '../../core/data/paginated-list.model';
|
import { PaginatedList } from '../../core/data/paginated-list.model';
|
||||||
import { PaginationComponentOptions } from '../pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../pagination/pagination-component-options.model';
|
||||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
import { fadeIn, fadeInOut } from '../animations/fade';
|
import { fadeIn, fadeInOut } from '../animations/fade';
|
||||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs';
|
||||||
import { ListableObject } from '../object-collection/shared/listable-object.model';
|
import { ListableObject } from '../object-collection/shared/listable-object.model';
|
||||||
import { getStartsWithComponent, StartsWithType } from '../starts-with/starts-with-decorator';
|
import { getStartsWithComponent, StartsWithType } from '../starts-with/starts-with-decorator';
|
||||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||||
@@ -25,7 +25,7 @@ import { hasValue } from '../empty.util';
|
|||||||
/**
|
/**
|
||||||
* Component to display a browse-by page for any ListableObject
|
* Component to display a browse-by page for any ListableObject
|
||||||
*/
|
*/
|
||||||
export class BrowseByComponent implements OnInit {
|
export class BrowseByComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ViewMode that should be passed to {@link ListableObjectComponentLoaderComponent}.
|
* ViewMode that should be passed to {@link ListableObjectComponentLoaderComponent}.
|
||||||
@@ -112,6 +112,16 @@ export class BrowseByComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
shouldDisplayResetButton$: Observable<boolean>;
|
shouldDisplayResetButton$: Observable<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page number of the previous page
|
||||||
|
*/
|
||||||
|
previousPage$ = new BehaviorSubject<string>('1');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscription that has to be unsubscribed from on destroy
|
||||||
|
*/
|
||||||
|
sub: Subscription;
|
||||||
|
|
||||||
public constructor(private injector: Injector,
|
public constructor(private injector: Injector,
|
||||||
protected paginationService: PaginationService,
|
protected paginationService: PaginationService,
|
||||||
private routeService: RouteService,
|
private routeService: RouteService,
|
||||||
@@ -171,9 +181,20 @@ export class BrowseByComponent implements OnInit {
|
|||||||
this.shouldDisplayResetButton$ = observableCombineLatest([startsWith$, value$]).pipe(
|
this.shouldDisplayResetButton$ = observableCombineLatest([startsWith$, value$]).pipe(
|
||||||
map(([startsWith, value]) => hasValue(startsWith) || hasValue(value))
|
map(([startsWith, value]) => hasValue(startsWith) || hasValue(value))
|
||||||
);
|
);
|
||||||
|
this.sub = this.routeService.getQueryParameterValue(this.paginationConfig.id + '.return').subscribe(this.previousPage$);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate back to the previous browse by page
|
||||||
|
*/
|
||||||
back() {
|
back() {
|
||||||
this.paginationService.updateRoute(this.paginationConfig.id, {page: 1}, {value: null, startsWith: null});
|
const page = +this.previousPage$.value > 1 ? +this.previousPage$.value : 1;
|
||||||
|
this.paginationService.updateRoute(this.paginationConfig.id, {page: page}, {[this.paginationConfig.id + '.return']: null, value: null, startsWith: null});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (this.sub) {
|
||||||
|
this.sub.unsubscribe();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -66,6 +66,11 @@ export class BrowserKlaroService extends KlaroService {
|
|||||||
* - Add and translate klaro configuration messages
|
* - Add and translate klaro configuration messages
|
||||||
*/
|
*/
|
||||||
initialize() {
|
initialize() {
|
||||||
|
if (!environment.info.enablePrivacyStatement) {
|
||||||
|
delete this.klaroConfig.privacyPolicy;
|
||||||
|
this.klaroConfig.translations.en.consentNotice.description = 'cookies.consent.content-notice.description.no-privacy';
|
||||||
|
}
|
||||||
|
|
||||||
this.configService.findByPropertyName('google.analytics.key').pipe(
|
this.configService.findByPropertyName('google.analytics.key').pipe(
|
||||||
getFirstCompletedRemoteData(),
|
getFirstCompletedRemoteData(),
|
||||||
).subscribe((remoteData) => {
|
).subscribe((remoteData) => {
|
||||||
@@ -74,6 +79,7 @@ export class BrowserKlaroService extends KlaroService {
|
|||||||
this.removeGoogleAnalytics();
|
this.removeGoogleAnalytics();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.translateService.setDefaultLang(environment.defaultLanguage);
|
this.translateService.setDefaultLang(environment.defaultLanguage);
|
||||||
|
|
||||||
const user$: Observable<EPerson> = this.getUser$();
|
const user$: Observable<EPerson> = this.getUser$();
|
||||||
@@ -101,7 +107,6 @@ export class BrowserKlaroService extends KlaroService {
|
|||||||
this.translateConfiguration();
|
this.translateConfiguration();
|
||||||
Klaro.setup(this.klaroConfig);
|
Klaro.setup(this.klaroConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -24,7 +24,7 @@ export const klaroConfiguration: any = {
|
|||||||
/*
|
/*
|
||||||
Setting 'hideLearnMore' to 'true' will hide the "learn more / customize" link in
|
Setting 'hideLearnMore' to 'true' will hide the "learn more / customize" link in
|
||||||
the consent notice. We strongly advise against using this under most
|
the consent notice. We strongly advise against using this under most
|
||||||
circumstances, as it keeps the user from customizing his/her consent choices.
|
circumstances, as it keeps the user from customizing their consent choices.
|
||||||
*/
|
*/
|
||||||
hideLearnMore: false,
|
hideLearnMore: false,
|
||||||
|
|
||||||
|
@@ -0,0 +1,28 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {CreateCollectionParentSelectorComponent} from './create-collection-parent-selector.component';
|
||||||
|
import {ThemedComponent} from 'src/app/shared/theme-support/themed.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Themed wrapper for CreateCollectionParentSelectorComponent
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-themed-create-collection-parent-selector',
|
||||||
|
styleUrls: [],
|
||||||
|
templateUrl: '../../../theme-support/themed.component.html'
|
||||||
|
})
|
||||||
|
export class ThemedCreateCollectionParentSelectorComponent
|
||||||
|
extends ThemedComponent<CreateCollectionParentSelectorComponent> {
|
||||||
|
|
||||||
|
protected getComponentName(): string {
|
||||||
|
return 'CreateCollectionParentSelectorComponent';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importThemedComponent(themeName: string): Promise<any> {
|
||||||
|
return import(`../../../../../themes/${themeName}/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component`);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importUnthemedComponent(): Promise<any> {
|
||||||
|
return import('./create-collection-parent-selector.component');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,27 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {CreateCommunityParentSelectorComponent} from './create-community-parent-selector.component';
|
||||||
|
import {ThemedComponent} from 'src/app/shared/theme-support/themed.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Themed wrapper for CreateCommunityParentSelectorComponent
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-themed-create-community-parent-selector',
|
||||||
|
styleUrls: [],
|
||||||
|
templateUrl: '../../../theme-support/themed.component.html'
|
||||||
|
})
|
||||||
|
export class ThemedCreateCommunityParentSelectorComponent
|
||||||
|
extends ThemedComponent<CreateCommunityParentSelectorComponent> {
|
||||||
|
protected getComponentName(): string {
|
||||||
|
return 'CreateCommunityParentSelectorComponent';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importThemedComponent(themeName: string): Promise<any> {
|
||||||
|
return import(`../../../../../themes/${themeName}/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component`);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importUnthemedComponent(): Promise<any> {
|
||||||
|
return import('./create-community-parent-selector.component');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,31 @@
|
|||||||
|
import {Component, Input} from '@angular/core';
|
||||||
|
import {CreateItemParentSelectorComponent} from './create-item-parent-selector.component';
|
||||||
|
import {ThemedComponent} from 'src/app/shared/theme-support/themed.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Themed wrapper for CreateItemParentSelectorComponent
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-themed-create-item-parent-selector',
|
||||||
|
styleUrls: [],
|
||||||
|
templateUrl: '../../../theme-support/themed.component.html'
|
||||||
|
})
|
||||||
|
export class ThemedCreateItemParentSelectorComponent
|
||||||
|
extends ThemedComponent<CreateItemParentSelectorComponent> {
|
||||||
|
@Input() entityType: string;
|
||||||
|
|
||||||
|
protected inAndOutputNames: (keyof CreateItemParentSelectorComponent & keyof this)[] = ['entityType'];
|
||||||
|
|
||||||
|
protected getComponentName(): string {
|
||||||
|
return 'CreateItemParentSelectorComponent';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importThemedComponent(themeName: string): Promise<any> {
|
||||||
|
return import(`../../../../../themes/${themeName}/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component`);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importUnthemedComponent(): Promise<any> {
|
||||||
|
return import('./create-item-parent-selector.component');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,27 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {EditCollectionSelectorComponent} from './edit-collection-selector.component';
|
||||||
|
import {ThemedComponent} from 'src/app/shared/theme-support/themed.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Themed wrapper for EditCollectionSelectorComponent
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-themed-edit-collection-selector',
|
||||||
|
styleUrls: [],
|
||||||
|
templateUrl: '../../../theme-support/themed.component.html'
|
||||||
|
})
|
||||||
|
export class ThemedEditCollectionSelectorComponent
|
||||||
|
extends ThemedComponent<EditCollectionSelectorComponent> {
|
||||||
|
protected getComponentName(): string {
|
||||||
|
return 'EditCollectionSelectorComponent';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importThemedComponent(themeName: string): Promise<any> {
|
||||||
|
return import(`../../../../../themes/${themeName}/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component`);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importUnthemedComponent(): Promise<any> {
|
||||||
|
return import('./edit-collection-selector.component');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,27 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {EditCommunitySelectorComponent} from './edit-community-selector.component';
|
||||||
|
import {ThemedComponent} from 'src/app/shared/theme-support/themed.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Themed wrapper for EditCommunitySelectorComponent
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-themed-edit-community-selector',
|
||||||
|
styleUrls: [],
|
||||||
|
templateUrl: '../../../theme-support/themed.component.html'
|
||||||
|
})
|
||||||
|
export class ThemedEditCommunitySelectorComponent
|
||||||
|
extends ThemedComponent<EditCommunitySelectorComponent> {
|
||||||
|
protected getComponentName(): string {
|
||||||
|
return 'EditCommunitySelectorComponent';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importThemedComponent(themeName: string): Promise<any> {
|
||||||
|
return import(`../../../../../themes/${themeName}/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component`);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importUnthemedComponent(): Promise<any> {
|
||||||
|
return import('./edit-community-selector.component');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,27 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {EditItemSelectorComponent} from './edit-item-selector.component';
|
||||||
|
import {ThemedComponent} from 'src/app/shared/theme-support/themed.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Themed wrapper for EditItemSelectorComponent
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-themed-edit-item-selector',
|
||||||
|
styleUrls: [],
|
||||||
|
templateUrl: '../../../theme-support/themed.component.html'
|
||||||
|
})
|
||||||
|
export class ThemedEditItemSelectorComponent
|
||||||
|
extends ThemedComponent<EditItemSelectorComponent> {
|
||||||
|
protected getComponentName(): string {
|
||||||
|
return 'EditItemSelectorComponent';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importThemedComponent(themeName: string): Promise<any> {
|
||||||
|
return import(`../../../../../themes/${themeName}/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component`);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importUnthemedComponent(): Promise<any> {
|
||||||
|
return import('./edit-item-selector.component');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -20,6 +20,7 @@ import {
|
|||||||
createSuccessfulRemoteDataObject$
|
createSuccessfulRemoteDataObject$
|
||||||
} from '../../../remote-data.utils';
|
} from '../../../remote-data.utils';
|
||||||
import { ExportMetadataSelectorComponent } from './export-metadata-selector.component';
|
import { ExportMetadataSelectorComponent } from './export-metadata-selector.component';
|
||||||
|
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
|
||||||
// No way to add entryComponents yet to testbed; alternative implemented; source: https://stackoverflow.com/questions/41689468/how-to-shallow-test-a-component-with-an-entrycomponents
|
// No way to add entryComponents yet to testbed; alternative implemented; source: https://stackoverflow.com/questions/41689468/how-to-shallow-test-a-component-with-an-entrycomponents
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@@ -47,6 +48,7 @@ describe('ExportMetadataSelectorComponent', () => {
|
|||||||
let router;
|
let router;
|
||||||
let notificationService: NotificationsServiceStub;
|
let notificationService: NotificationsServiceStub;
|
||||||
let scriptService;
|
let scriptService;
|
||||||
|
let authorizationDataService;
|
||||||
|
|
||||||
const mockItem = Object.assign(new Item(), {
|
const mockItem = Object.assign(new Item(), {
|
||||||
id: 'fake-id',
|
id: 'fake-id',
|
||||||
@@ -95,6 +97,9 @@ describe('ExportMetadataSelectorComponent', () => {
|
|||||||
invoke: createSuccessfulRemoteDataObject$({ processId: '45' })
|
invoke: createSuccessfulRemoteDataObject$({ processId: '45' })
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
authorizationDataService = jasmine.createSpyObj('authorizationDataService', {
|
||||||
|
isAuthorized: observableOf(true)
|
||||||
|
});
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), ModelTestModule],
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), ModelTestModule],
|
||||||
declarations: [ExportMetadataSelectorComponent],
|
declarations: [ExportMetadataSelectorComponent],
|
||||||
@@ -102,6 +107,7 @@ describe('ExportMetadataSelectorComponent', () => {
|
|||||||
{ provide: NgbActiveModal, useValue: modalStub },
|
{ provide: NgbActiveModal, useValue: modalStub },
|
||||||
{ provide: NotificationsService, useValue: notificationService },
|
{ provide: NotificationsService, useValue: notificationService },
|
||||||
{ provide: ScriptDataService, useValue: scriptService },
|
{ provide: ScriptDataService, useValue: scriptService },
|
||||||
|
{ provide: AuthorizationDataService, useValue: authorizationDataService },
|
||||||
{
|
{
|
||||||
provide: ActivatedRoute,
|
provide: ActivatedRoute,
|
||||||
useValue: {
|
useValue: {
|
||||||
@@ -150,7 +156,7 @@ describe('ExportMetadataSelectorComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('if collection is selected', () => {
|
describe('if collection is selected and is admin', () => {
|
||||||
let scriptRequestSucceeded;
|
let scriptRequestSucceeded;
|
||||||
beforeEach((done) => {
|
beforeEach((done) => {
|
||||||
spyOn((component as any).modalService, 'open').and.returnValue(modalRef);
|
spyOn((component as any).modalService, 'open').and.returnValue(modalRef);
|
||||||
@@ -159,7 +165,32 @@ describe('ExportMetadataSelectorComponent', () => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('should invoke the metadata-export script with option -i uuid', () => {
|
it('should invoke the metadata-export script with option -i uuid and -a option', () => {
|
||||||
|
const parameterValues: ProcessParameter[] = [
|
||||||
|
Object.assign(new ProcessParameter(), { name: '-i', value: mockCollection.uuid }),
|
||||||
|
Object.assign(new ProcessParameter(), { name: '-a' }),
|
||||||
|
];
|
||||||
|
expect(scriptService.invoke).toHaveBeenCalledWith(METADATA_EXPORT_SCRIPT_NAME, parameterValues, []);
|
||||||
|
});
|
||||||
|
it('success notification is shown', () => {
|
||||||
|
expect(scriptRequestSucceeded).toBeTrue();
|
||||||
|
expect(notificationService.success).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
it('redirected to process page', () => {
|
||||||
|
expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/45');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('if collection is selected and is not admin', () => {
|
||||||
|
let scriptRequestSucceeded;
|
||||||
|
beforeEach((done) => {
|
||||||
|
(authorizationDataService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(false));
|
||||||
|
spyOn((component as any).modalService, 'open').and.returnValue(modalRef);
|
||||||
|
component.navigate(mockCollection).subscribe((succeeded: boolean) => {
|
||||||
|
scriptRequestSucceeded = succeeded;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should invoke the metadata-export script with option -i uuid without the -a option', () => {
|
||||||
const parameterValues: ProcessParameter[] = [
|
const parameterValues: ProcessParameter[] = [
|
||||||
Object.assign(new ProcessParameter(), { name: '-i', value: mockCollection.uuid }),
|
Object.assign(new ProcessParameter(), { name: '-i', value: mockCollection.uuid }),
|
||||||
];
|
];
|
||||||
@@ -174,7 +205,7 @@ describe('ExportMetadataSelectorComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('if community is selected', () => {
|
describe('if community is selected and is an admin', () => {
|
||||||
let scriptRequestSucceeded;
|
let scriptRequestSucceeded;
|
||||||
beforeEach((done) => {
|
beforeEach((done) => {
|
||||||
spyOn((component as any).modalService, 'open').and.returnValue(modalRef);
|
spyOn((component as any).modalService, 'open').and.returnValue(modalRef);
|
||||||
@@ -183,7 +214,32 @@ describe('ExportMetadataSelectorComponent', () => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('should invoke the metadata-export script with option -i uuid', () => {
|
it('should invoke the metadata-export script with option -i uuid and -a option if the user is an admin', () => {
|
||||||
|
const parameterValues: ProcessParameter[] = [
|
||||||
|
Object.assign(new ProcessParameter(), { name: '-i', value: mockCommunity.uuid }),
|
||||||
|
Object.assign(new ProcessParameter(), { name: '-a' }),
|
||||||
|
];
|
||||||
|
expect(scriptService.invoke).toHaveBeenCalledWith(METADATA_EXPORT_SCRIPT_NAME, parameterValues, []);
|
||||||
|
});
|
||||||
|
it('success notification is shown', () => {
|
||||||
|
expect(scriptRequestSucceeded).toBeTrue();
|
||||||
|
expect(notificationService.success).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
it('redirected to process page', () => {
|
||||||
|
expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/45');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('if community is selected and is not an admin', () => {
|
||||||
|
let scriptRequestSucceeded;
|
||||||
|
beforeEach((done) => {
|
||||||
|
(authorizationDataService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(false));
|
||||||
|
spyOn((component as any).modalService, 'open').and.returnValue(modalRef);
|
||||||
|
component.navigate(mockCommunity).subscribe((succeeded: boolean) => {
|
||||||
|
scriptRequestSucceeded = succeeded;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should invoke the metadata-export script with option -i uuid without the -a option', () => {
|
||||||
const parameterValues: ProcessParameter[] = [
|
const parameterValues: ProcessParameter[] = [
|
||||||
Object.assign(new ProcessParameter(), { name: '-i', value: mockCommunity.uuid }),
|
Object.assign(new ProcessParameter(), { name: '-i', value: mockCommunity.uuid }),
|
||||||
];
|
];
|
||||||
|
@@ -19,6 +19,8 @@ import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';
|
|||||||
import { Process } from '../../../../process-page/processes/process.model';
|
import { Process } from '../../../../process-page/processes/process.model';
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
import { getProcessDetailRoute } from '../../../../process-page/process-page-routing.paths';
|
import { getProcessDetailRoute } from '../../../../process-page/process-page-routing.paths';
|
||||||
|
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { FeatureID } from '../../../../core/data/feature-authorization/feature-id';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component to wrap a list of existing dso's inside a modal
|
* Component to wrap a list of existing dso's inside a modal
|
||||||
@@ -36,6 +38,7 @@ export class ExportMetadataSelectorComponent extends DSOSelectorModalWrapperComp
|
|||||||
constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router,
|
constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router,
|
||||||
protected notificationsService: NotificationsService, protected translationService: TranslateService,
|
protected notificationsService: NotificationsService, protected translationService: TranslateService,
|
||||||
protected scriptDataService: ScriptDataService,
|
protected scriptDataService: ScriptDataService,
|
||||||
|
protected authorizationDataService: AuthorizationDataService,
|
||||||
private modalService: NgbModal) {
|
private modalService: NgbModal) {
|
||||||
super(activeModal, route);
|
super(activeModal, route);
|
||||||
}
|
}
|
||||||
@@ -82,8 +85,13 @@ export class ExportMetadataSelectorComponent extends DSOSelectorModalWrapperComp
|
|||||||
const parameterValues: ProcessParameter[] = [
|
const parameterValues: ProcessParameter[] = [
|
||||||
Object.assign(new ProcessParameter(), { name: '-i', value: dso.uuid }),
|
Object.assign(new ProcessParameter(), { name: '-i', value: dso.uuid }),
|
||||||
];
|
];
|
||||||
return this.scriptDataService.invoke(METADATA_EXPORT_SCRIPT_NAME, parameterValues, [])
|
return this.authorizationDataService.isAuthorized(FeatureID.AdministratorOf).pipe(
|
||||||
.pipe(
|
switchMap((isAdmin) => {
|
||||||
|
if (isAdmin) {
|
||||||
|
parameterValues.push(Object.assign(new ProcessParameter(), {name: '-a'}));
|
||||||
|
}
|
||||||
|
return this.scriptDataService.invoke(METADATA_EXPORT_SCRIPT_NAME, parameterValues, []);
|
||||||
|
}),
|
||||||
getFirstCompletedRemoteData(),
|
getFirstCompletedRemoteData(),
|
||||||
map((rd: RemoteData<Process>) => {
|
map((rd: RemoteData<Process>) => {
|
||||||
if (rd.hasSucceeded) {
|
if (rd.hasSucceeded) {
|
||||||
|
@@ -8,6 +8,7 @@ export function getMockThemeService(themeName = 'base', themes?: ThemeConfig[]):
|
|||||||
getThemeName: themeName,
|
getThemeName: themeName,
|
||||||
getThemeName$: observableOf(themeName),
|
getThemeName$: observableOf(themeName),
|
||||||
getThemeConfigFor: undefined,
|
getThemeConfigFor: undefined,
|
||||||
|
listenForRouteChanges: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isNotEmpty(themes)) {
|
if (isNotEmpty(themes)) {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<div class="d-flex flex-row">
|
<div class="d-flex flex-row">
|
||||||
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="[]" [queryParams]="getQueryParams()" [queryParamsHandling]="'merge'" class="lead">
|
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="[]" [queryParams]="queryParams$ | async" [queryParamsHandling]="'merge'" class="lead">
|
||||||
{{object.value}}
|
{{object.value}}
|
||||||
</a>
|
</a>
|
||||||
<span *ngIf="linkType == linkTypes.None" class="lead">
|
<span *ngIf="linkType == linkTypes.None" class="lead">
|
||||||
|
@@ -4,7 +4,9 @@ import { By } from '@angular/platform-browser';
|
|||||||
import { TruncatePipe } from '../../utils/truncate.pipe';
|
import { TruncatePipe } from '../../utils/truncate.pipe';
|
||||||
import { BrowseEntryListElementComponent } from './browse-entry-list-element.component';
|
import { BrowseEntryListElementComponent } from './browse-entry-list-element.component';
|
||||||
import { BrowseEntry } from '../../../core/shared/browse-entry.model';
|
import { BrowseEntry } from '../../../core/shared/browse-entry.model';
|
||||||
|
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||||
|
import { RouteService } from '../../../core/services/route.service';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
let browseEntryListElementComponent: BrowseEntryListElementComponent;
|
let browseEntryListElementComponent: BrowseEntryListElementComponent;
|
||||||
let fixture: ComponentFixture<BrowseEntryListElementComponent>;
|
let fixture: ComponentFixture<BrowseEntryListElementComponent>;
|
||||||
|
|
||||||
@@ -13,12 +15,28 @@ const mockValue: BrowseEntry = Object.assign(new BrowseEntry(), {
|
|||||||
value: 'De Langhe Kristof'
|
value: 'De Langhe Kristof'
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('MetadataListElementComponent', () => {
|
let paginationService;
|
||||||
|
let routeService;
|
||||||
|
const pageParam = 'bbm.page';
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
paginationService = jasmine.createSpyObj('paginationService', {
|
||||||
|
getPageParam: pageParam
|
||||||
|
});
|
||||||
|
|
||||||
|
routeService = jasmine.createSpyObj('routeService', {
|
||||||
|
getQueryParameterValue: observableOf('1')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
describe('BrowseEntryListElementComponent', () => {
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
init();
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [BrowseEntryListElementComponent, TruncatePipe],
|
declarations: [BrowseEntryListElementComponent, TruncatePipe],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: 'objectElementProvider', useValue: { mockValue } }
|
{ provide: 'objectElementProvider', useValue: { mockValue } },
|
||||||
|
{provide: PaginationService, useValue: paginationService},
|
||||||
|
{provide: RouteService, useValue: routeService},
|
||||||
],
|
],
|
||||||
|
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
@@ -1,9 +1,15 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
|
||||||
import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
|
import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
|
||||||
import { BrowseEntry } from '../../../core/shared/browse-entry.model';
|
import { BrowseEntry } from '../../../core/shared/browse-entry.model';
|
||||||
import { ViewMode } from '../../../core/shared/view-mode.model';
|
import { ViewMode } from '../../../core/shared/view-mode.model';
|
||||||
import { listableObjectComponent } from '../../object-collection/shared/listable-object/listable-object.decorator';
|
import { listableObjectComponent } from '../../object-collection/shared/listable-object/listable-object.decorator';
|
||||||
|
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||||
|
import { Params } from '@angular/router';
|
||||||
|
import { BBM_PAGINATION_ID } from '../../../browse-by/browse-by-metadata-page/browse-by-metadata-page.component';
|
||||||
|
import { RouteService } from 'src/app/core/services/route.service';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-browse-entry-list-element',
|
selector: 'ds-browse-entry-list-element',
|
||||||
@@ -15,16 +21,35 @@ import { listableObjectComponent } from '../../object-collection/shared/listable
|
|||||||
* This component is automatically used to create a list view for BrowseEntry objects when used in ObjectCollectionComponent
|
* This component is automatically used to create a list view for BrowseEntry objects when used in ObjectCollectionComponent
|
||||||
*/
|
*/
|
||||||
@listableObjectComponent(BrowseEntry, ViewMode.ListElement)
|
@listableObjectComponent(BrowseEntry, ViewMode.ListElement)
|
||||||
export class BrowseEntryListElementComponent extends AbstractListableElementComponent<BrowseEntry> {
|
export class BrowseEntryListElementComponent extends AbstractListableElementComponent<BrowseEntry> implements OnInit {
|
||||||
|
/**
|
||||||
|
* Emits the query parameters for the link of this browse entry list element
|
||||||
|
*/
|
||||||
|
queryParams$: Observable<Params>;
|
||||||
|
|
||||||
|
constructor(private paginationService: PaginationService, private routeService: RouteService) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.queryParams$ = this.getQueryParams();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the query params to access the item page of this browse entry.
|
* Get the query params to access the item page of this browse entry.
|
||||||
*/
|
*/
|
||||||
public getQueryParams(): {[param: string]: any} {
|
private getQueryParams(): Observable<Params> {
|
||||||
|
const pageParamName = this.paginationService.getPageParam(BBM_PAGINATION_ID);
|
||||||
|
return this.routeService.getQueryParameterValue(pageParamName).pipe(
|
||||||
|
map((currentPage) => {
|
||||||
return {
|
return {
|
||||||
value: this.object.value,
|
value: this.object.value,
|
||||||
authority: !!this.object.authority ? this.object.authority : undefined,
|
authority: !!this.object.authority ? this.object.authority : undefined,
|
||||||
startsWith: undefined,
|
startsWith: undefined,
|
||||||
|
[pageParamName]: null,
|
||||||
|
[BBM_PAGINATION_ID + '.return']: currentPage
|
||||||
};
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -12,7 +12,7 @@ import { metadataRepresentationComponent } from '../../../metadata-representatio
|
|||||||
/**
|
/**
|
||||||
* A component for displaying MetadataRepresentation objects in the form of items
|
* A component for displaying MetadataRepresentation objects in the form of items
|
||||||
* It will send the MetadataRepresentation object along with ElementViewMode.SetElement to the ItemTypeSwitcherComponent,
|
* It will send the MetadataRepresentation object along with ElementViewMode.SetElement to the ItemTypeSwitcherComponent,
|
||||||
* which will in his turn decide how to render the item as metadata.
|
* which will in its turn decide how to render the item as metadata.
|
||||||
*/
|
*/
|
||||||
export class ItemMetadataListElementComponent extends MetadataRepresentationListElementComponent {
|
export class ItemMetadataListElementComponent extends MetadataRepresentationListElementComponent {
|
||||||
/**
|
/**
|
||||||
|
@@ -124,12 +124,21 @@ import { DSOSelectorComponent } from './dso-selector/dso-selector/dso-selector.c
|
|||||||
import {
|
import {
|
||||||
CreateCommunityParentSelectorComponent
|
CreateCommunityParentSelectorComponent
|
||||||
} from './dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component';
|
} from './dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component';
|
||||||
|
import {
|
||||||
|
ThemedCreateCommunityParentSelectorComponent
|
||||||
|
} from './dso-selector/modal-wrappers/create-community-parent-selector/themed-create-community-parent-selector.component';
|
||||||
import {
|
import {
|
||||||
CreateItemParentSelectorComponent
|
CreateItemParentSelectorComponent
|
||||||
} from './dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
|
} from './dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
|
||||||
|
import {
|
||||||
|
ThemedCreateItemParentSelectorComponent
|
||||||
|
} from './dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component';
|
||||||
import {
|
import {
|
||||||
CreateCollectionParentSelectorComponent
|
CreateCollectionParentSelectorComponent
|
||||||
} from './dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component';
|
} from './dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component';
|
||||||
|
import {
|
||||||
|
ThemedCreateCollectionParentSelectorComponent
|
||||||
|
} from './dso-selector/modal-wrappers/create-collection-parent-selector/themed-create-collection-parent-selector.component';
|
||||||
import {
|
import {
|
||||||
CommunitySearchResultListElementComponent
|
CommunitySearchResultListElementComponent
|
||||||
} from './object-list/search-result-list-element/community-search-result/community-search-result-list-element.component';
|
} from './object-list/search-result-list-element/community-search-result/community-search-result-list-element.component';
|
||||||
@@ -139,12 +148,21 @@ import {
|
|||||||
import {
|
import {
|
||||||
EditItemSelectorComponent
|
EditItemSelectorComponent
|
||||||
} from './dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component';
|
} from './dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component';
|
||||||
|
import {
|
||||||
|
ThemedEditItemSelectorComponent
|
||||||
|
} from './dso-selector/modal-wrappers/edit-item-selector/themed-edit-item-selector.component';
|
||||||
import {
|
import {
|
||||||
EditCommunitySelectorComponent
|
EditCommunitySelectorComponent
|
||||||
} from './dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component';
|
} from './dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component';
|
||||||
|
import {
|
||||||
|
ThemedEditCommunitySelectorComponent
|
||||||
|
} from './dso-selector/modal-wrappers/edit-community-selector/themed-edit-community-selector.component';
|
||||||
import {
|
import {
|
||||||
EditCollectionSelectorComponent
|
EditCollectionSelectorComponent
|
||||||
} from './dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component';
|
} from './dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component';
|
||||||
|
import {
|
||||||
|
ThemedEditCollectionSelectorComponent
|
||||||
|
} from './dso-selector/modal-wrappers/edit-collection-selector/themed-edit-collection-selector.component';
|
||||||
import {
|
import {
|
||||||
ItemListPreviewComponent
|
ItemListPreviewComponent
|
||||||
} from './object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component';
|
} from './object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component';
|
||||||
@@ -395,11 +413,17 @@ const COMPONENTS = [
|
|||||||
DsoInputSuggestionsComponent,
|
DsoInputSuggestionsComponent,
|
||||||
DSOSelectorComponent,
|
DSOSelectorComponent,
|
||||||
CreateCommunityParentSelectorComponent,
|
CreateCommunityParentSelectorComponent,
|
||||||
|
ThemedCreateCommunityParentSelectorComponent,
|
||||||
CreateCollectionParentSelectorComponent,
|
CreateCollectionParentSelectorComponent,
|
||||||
|
ThemedCreateCollectionParentSelectorComponent,
|
||||||
CreateItemParentSelectorComponent,
|
CreateItemParentSelectorComponent,
|
||||||
|
ThemedCreateItemParentSelectorComponent,
|
||||||
EditCommunitySelectorComponent,
|
EditCommunitySelectorComponent,
|
||||||
|
ThemedEditCommunitySelectorComponent,
|
||||||
EditCollectionSelectorComponent,
|
EditCollectionSelectorComponent,
|
||||||
|
ThemedEditCollectionSelectorComponent,
|
||||||
EditItemSelectorComponent,
|
EditItemSelectorComponent,
|
||||||
|
ThemedEditItemSelectorComponent,
|
||||||
CommunitySearchResultListElementComponent,
|
CommunitySearchResultListElementComponent,
|
||||||
CollectionSearchResultListElementComponent,
|
CollectionSearchResultListElementComponent,
|
||||||
BrowseByComponent,
|
BrowseByComponent,
|
||||||
@@ -491,11 +515,17 @@ const ENTRY_COMPONENTS = [
|
|||||||
StartsWithDateComponent,
|
StartsWithDateComponent,
|
||||||
StartsWithTextComponent,
|
StartsWithTextComponent,
|
||||||
CreateCommunityParentSelectorComponent,
|
CreateCommunityParentSelectorComponent,
|
||||||
|
ThemedCreateCommunityParentSelectorComponent,
|
||||||
CreateCollectionParentSelectorComponent,
|
CreateCollectionParentSelectorComponent,
|
||||||
|
ThemedCreateCollectionParentSelectorComponent,
|
||||||
CreateItemParentSelectorComponent,
|
CreateItemParentSelectorComponent,
|
||||||
|
ThemedCreateItemParentSelectorComponent,
|
||||||
EditCommunitySelectorComponent,
|
EditCommunitySelectorComponent,
|
||||||
|
ThemedEditCommunitySelectorComponent,
|
||||||
EditCollectionSelectorComponent,
|
EditCollectionSelectorComponent,
|
||||||
|
ThemedEditCollectionSelectorComponent,
|
||||||
EditItemSelectorComponent,
|
EditItemSelectorComponent,
|
||||||
|
ThemedEditItemSelectorComponent,
|
||||||
PlainTextMetadataListElementComponent,
|
PlainTextMetadataListElementComponent,
|
||||||
ItemMetadataListElementComponent,
|
ItemMetadataListElementComponent,
|
||||||
MetadataRepresentationListElementComponent,
|
MetadataRepresentationListElementComponent,
|
||||||
|
@@ -2,7 +2,7 @@ import { of as observableOf } from 'rxjs';
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
import { provideMockActions } from '@ngrx/effects/testing';
|
import { provideMockActions } from '@ngrx/effects/testing';
|
||||||
import { LinkService } from '../../core/cache/builders/link.service';
|
import { LinkService } from '../../core/cache/builders/link.service';
|
||||||
import { cold, hot } from 'jasmine-marbles';
|
import { hot } from 'jasmine-marbles';
|
||||||
import { SetThemeAction } from './theme.actions';
|
import { SetThemeAction } from './theme.actions';
|
||||||
import { Theme } from '../../../config/theme.model';
|
import { Theme } from '../../../config/theme.model';
|
||||||
import { provideMockStore } from '@ngrx/store/testing';
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
@@ -21,7 +21,9 @@ import {
|
|||||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||||
import { ThemeService } from './theme.service';
|
import { ThemeService } from './theme.service';
|
||||||
import { ROUTER_NAVIGATED } from '@ngrx/router-store';
|
import { ROUTER_NAVIGATED } from '@ngrx/router-store';
|
||||||
import { ActivatedRouteSnapshot } from '@angular/router';
|
import { ActivatedRouteSnapshot, Router } from '@angular/router';
|
||||||
|
import { CommonModule, DOCUMENT } from '@angular/common';
|
||||||
|
import { RouterMock } from '../mocks/router.mock';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LinkService able to mock recursively resolving DSO parent links
|
* LinkService able to mock recursively resolving DSO parent links
|
||||||
@@ -84,12 +86,16 @@ describe('ThemeService', () => {
|
|||||||
findById: () => createSuccessfulRemoteDataObject$(mockCommunity)
|
findById: () => createSuccessfulRemoteDataObject$(mockCommunity)
|
||||||
};
|
};
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
ThemeService,
|
ThemeService,
|
||||||
{ provide: LinkService, useValue: linkService },
|
{ provide: LinkService, useValue: linkService },
|
||||||
provideMockStore({ initialState }),
|
provideMockStore({ initialState }),
|
||||||
provideMockActions(() => mockActions),
|
provideMockActions(() => mockActions),
|
||||||
{ provide: DSpaceObjectDataService, useValue: mockDsoService }
|
{ provide: DSpaceObjectDataService, useValue: mockDsoService },
|
||||||
|
{ provide: Router, useValue: new RouterMock() },
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -367,4 +373,49 @@ describe('ThemeService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('listenForThemeChanges', () => {
|
||||||
|
let document;
|
||||||
|
let headSpy;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const mockDsoService = {
|
||||||
|
findById: () => createSuccessfulRemoteDataObject$(mockCommunity)
|
||||||
|
};
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
ThemeService,
|
||||||
|
{ provide: LinkService, useValue: linkService },
|
||||||
|
provideMockStore({ initialState }),
|
||||||
|
{ provide: DSpaceObjectDataService, useValue: mockDsoService },
|
||||||
|
{ provide: Router, useValue: new RouterMock() },
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
document = TestBed.inject(DOCUMENT);
|
||||||
|
headSpy = jasmine.createSpyObj('head', ['appendChild', 'getElementsByClassName']);
|
||||||
|
headSpy.getElementsByClassName.and.returnValue([]);
|
||||||
|
spyOn(document, 'getElementsByTagName').and.returnValue([headSpy]);
|
||||||
|
|
||||||
|
themeService = TestBed.inject(ThemeService);
|
||||||
|
spyOn(themeService, 'getThemeName').and.returnValue('custom');
|
||||||
|
spyOn(themeService, 'getThemeName$').and.returnValue(observableOf('custom'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should append a link element with the correct attributes to the head element', () => {
|
||||||
|
themeService.listenForThemeChanges(true);
|
||||||
|
|
||||||
|
const link = document.createElement('link');
|
||||||
|
link.setAttribute('rel', 'stylesheet');
|
||||||
|
link.setAttribute('type', 'text/css');
|
||||||
|
link.setAttribute('class', 'theme-css');
|
||||||
|
link.setAttribute('href', '/custom-theme.css');
|
||||||
|
|
||||||
|
expect(headSpy.appendChild).toHaveBeenCalledWith(link);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
import { Injectable, Inject } from '@angular/core';
|
import { Inject, Injectable } from '@angular/core';
|
||||||
import { Store, createFeatureSelector, createSelector, select } from '@ngrx/store';
|
import { createFeatureSelector, createSelector, select, Store } from '@ngrx/store';
|
||||||
import { EMPTY, Observable, of as observableOf } from 'rxjs';
|
import { BehaviorSubject, EMPTY, Observable, of as observableOf } from 'rxjs';
|
||||||
import { ThemeState } from './theme.reducer';
|
import { ThemeState } from './theme.reducer';
|
||||||
import { SetThemeAction, ThemeActionTypes } from './theme.actions';
|
import { SetThemeAction, ThemeActionTypes } from './theme.actions';
|
||||||
import { expand, filter, map, switchMap, take, toArray } from 'rxjs/operators';
|
import { expand, filter, map, switchMap, take, toArray } from 'rxjs/operators';
|
||||||
import { hasValue, isNotEmpty } from '../empty.util';
|
import { hasNoValue, hasValue, isNotEmpty } from '../empty.util';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
import {
|
import {
|
||||||
@@ -12,14 +12,18 @@ import {
|
|||||||
getFirstSucceededRemoteData,
|
getFirstSucceededRemoteData,
|
||||||
getRemoteDataPayload
|
getRemoteDataPayload
|
||||||
} from '../../core/shared/operators';
|
} from '../../core/shared/operators';
|
||||||
import { Theme, ThemeConfig, themeFactory } from '../../../config/theme.model';
|
import { HeadTagConfig, Theme, ThemeConfig, themeFactory } from '../../../config/theme.model';
|
||||||
import { NO_OP_ACTION_TYPE, NoOpAction } from '../ngrx/no-op.action';
|
import { NO_OP_ACTION_TYPE, NoOpAction } from '../ngrx/no-op.action';
|
||||||
import { followLink } from '../utils/follow-link-config.model';
|
import { followLink } from '../utils/follow-link-config.model';
|
||||||
import { LinkService } from '../../core/cache/builders/link.service';
|
import { LinkService } from '../../core/cache/builders/link.service';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||||
import { ActivatedRouteSnapshot } from '@angular/router';
|
import { ActivatedRouteSnapshot, ResolveEnd, Router } from '@angular/router';
|
||||||
import { GET_THEME_CONFIG_FOR_FACTORY } from '../object-collection/shared/listable-object/listable-object.decorator';
|
import { GET_THEME_CONFIG_FOR_FACTORY } from '../object-collection/shared/listable-object/listable-object.decorator';
|
||||||
|
import { distinctNext } from 'src/app/core/shared/distinct-next';
|
||||||
|
import { DOCUMENT } from '@angular/common';
|
||||||
|
import { getDefaultThemeConfig } from '../../../config/config.util';
|
||||||
|
import { BASE_THEME_NAME } from './theme.constants';
|
||||||
|
|
||||||
export const themeStateSelector = createFeatureSelector<ThemeState>('theme');
|
export const themeStateSelector = createFeatureSelector<ThemeState>('theme');
|
||||||
|
|
||||||
@@ -42,11 +46,16 @@ export class ThemeService {
|
|||||||
*/
|
*/
|
||||||
hasDynamicTheme: boolean;
|
hasDynamicTheme: boolean;
|
||||||
|
|
||||||
|
private _isThemeLoading$ = new BehaviorSubject<boolean>(false);
|
||||||
|
private _isThemeCSSLoading$ = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private store: Store<ThemeState>,
|
private store: Store<ThemeState>,
|
||||||
private linkService: LinkService,
|
private linkService: LinkService,
|
||||||
private dSpaceObjectDataService: DSpaceObjectDataService,
|
private dSpaceObjectDataService: DSpaceObjectDataService,
|
||||||
@Inject(GET_THEME_CONFIG_FOR_FACTORY) private gtcf: (str) => ThemeConfig
|
@Inject(GET_THEME_CONFIG_FOR_FACTORY) private gtcf: (str) => ThemeConfig,
|
||||||
|
private router: Router,
|
||||||
|
@Inject(DOCUMENT) private document: any,
|
||||||
) {
|
) {
|
||||||
// Create objects from the theme configs in the environment file
|
// Create objects from the theme configs in the environment file
|
||||||
this.themes = environment.themes.map((themeConfig: ThemeConfig) => themeFactory(themeConfig));
|
this.themes = environment.themes.map((themeConfig: ThemeConfig) => themeFactory(themeConfig));
|
||||||
@@ -57,10 +66,17 @@ export class ThemeService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the current theme
|
||||||
|
* @param newName
|
||||||
|
*/
|
||||||
setTheme(newName: string) {
|
setTheme(newName: string) {
|
||||||
this.store.dispatch(new SetThemeAction(newName));
|
this.store.dispatch(new SetThemeAction(newName));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the current theme (synchronous)
|
||||||
|
*/
|
||||||
getThemeName(): string {
|
getThemeName(): string {
|
||||||
let currentTheme: string;
|
let currentTheme: string;
|
||||||
this.store.pipe(
|
this.store.pipe(
|
||||||
@@ -72,12 +88,205 @@ export class ThemeService {
|
|||||||
return currentTheme;
|
return currentTheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the current theme (asynchronous, tracks changes)
|
||||||
|
*/
|
||||||
getThemeName$(): Observable<string> {
|
getThemeName$(): Observable<string> {
|
||||||
return this.store.pipe(
|
return this.store.pipe(
|
||||||
select(currentThemeSelector)
|
select(currentThemeSelector)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the theme is currently loading
|
||||||
|
*/
|
||||||
|
get isThemeLoading$(): Observable<boolean> {
|
||||||
|
return this._isThemeLoading$;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every time the theme is changed
|
||||||
|
* - if the theme name is valid, load it (CSS + <head> tags)
|
||||||
|
* - otherwise fall back to {@link getDefaultThemeConfig} or {@link BASE_THEME_NAME}
|
||||||
|
* Should be called when initializing the app.
|
||||||
|
* @param isBrowser
|
||||||
|
*/
|
||||||
|
listenForThemeChanges(isBrowser: boolean): void {
|
||||||
|
this.getThemeName$().subscribe((themeName: string) => {
|
||||||
|
if (isBrowser) {
|
||||||
|
// the theme css will never download server side, so this should only happen on the browser
|
||||||
|
distinctNext(this._isThemeCSSLoading$, true);
|
||||||
|
}
|
||||||
|
if (hasValue(themeName)) {
|
||||||
|
this.loadGlobalThemeConfig(themeName);
|
||||||
|
} else {
|
||||||
|
const defaultThemeConfig = getDefaultThemeConfig();
|
||||||
|
if (hasValue(defaultThemeConfig)) {
|
||||||
|
this.loadGlobalThemeConfig(defaultThemeConfig.name);
|
||||||
|
} else {
|
||||||
|
this.loadGlobalThemeConfig(BASE_THEME_NAME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For every resolved route, check if it matches a dynamic theme. If it does, load that theme.
|
||||||
|
* Should be called when initializing the app.
|
||||||
|
*/
|
||||||
|
listenForRouteChanges(): void {
|
||||||
|
this.router.events.pipe(
|
||||||
|
filter(event => event instanceof ResolveEnd),
|
||||||
|
switchMap((event: ResolveEnd) => this.updateThemeOnRouteChange$(event.urlAfterRedirects, event.state.root)),
|
||||||
|
switchMap((changed) => {
|
||||||
|
if (changed) {
|
||||||
|
return this._isThemeCSSLoading$;
|
||||||
|
} else {
|
||||||
|
return [false];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).subscribe((changed) => {
|
||||||
|
distinctNext(this._isThemeLoading$, changed);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a theme's configuration
|
||||||
|
* - CSS
|
||||||
|
* - <head> tags
|
||||||
|
* @param themeName
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private loadGlobalThemeConfig(themeName: string): void {
|
||||||
|
this.setThemeCss(themeName);
|
||||||
|
this.setHeadTags(themeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the theme css file in <head>
|
||||||
|
*
|
||||||
|
* @param themeName The name of the new theme
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private setThemeCss(themeName: string): void {
|
||||||
|
const head = this.document.getElementsByTagName('head')[0];
|
||||||
|
if (hasNoValue(head)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Array.from to ensure we end up with an array, not an HTMLCollection, which would be
|
||||||
|
// automatically updated if we add nodes later
|
||||||
|
const currentThemeLinks = Array.from(head.getElementsByClassName('theme-css'));
|
||||||
|
const link = this.document.createElement('link');
|
||||||
|
link.setAttribute('rel', 'stylesheet');
|
||||||
|
link.setAttribute('type', 'text/css');
|
||||||
|
link.setAttribute('class', 'theme-css');
|
||||||
|
link.setAttribute('href', `/${encodeURIComponent(themeName)}-theme.css`);
|
||||||
|
// wait for the new css to download before removing the old one to prevent a
|
||||||
|
// flash of unstyled content
|
||||||
|
link.onload = () => {
|
||||||
|
if (isNotEmpty(currentThemeLinks)) {
|
||||||
|
currentThemeLinks.forEach((currentThemeLink: any) => {
|
||||||
|
if (hasValue(currentThemeLink)) {
|
||||||
|
currentThemeLink.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// the fact that this callback is used, proves we're on the browser.
|
||||||
|
distinctNext(this._isThemeCSSLoading$, false);
|
||||||
|
};
|
||||||
|
head.appendChild(link);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the page to add a theme's <head> tags
|
||||||
|
* @param themeName the theme in question
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private setHeadTags(themeName: string): void {
|
||||||
|
const head = this.document.getElementsByTagName('head')[0];
|
||||||
|
if (hasNoValue(head)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear head tags
|
||||||
|
const currentHeadTags = Array.from(head.getElementsByClassName('theme-head-tag'));
|
||||||
|
if (hasValue(currentHeadTags)) {
|
||||||
|
currentHeadTags.forEach((currentHeadTag: any) => currentHeadTag.remove());
|
||||||
|
}
|
||||||
|
|
||||||
|
// create new head tags (not yet added to DOM)
|
||||||
|
const headTagFragment = this.document.createDocumentFragment();
|
||||||
|
this.createHeadTags(themeName)
|
||||||
|
.forEach(newHeadTag => headTagFragment.appendChild(newHeadTag));
|
||||||
|
|
||||||
|
// add new head tags to DOM
|
||||||
|
head.appendChild(headTagFragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create HTML elements for a theme's <head> tags
|
||||||
|
* (including those defined in the parent theme, if applicable)
|
||||||
|
* @param themeName the theme in question
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private createHeadTags(themeName: string): HTMLElement[] {
|
||||||
|
const themeConfig = this.getThemeConfigFor(themeName);
|
||||||
|
const headTagConfigs = themeConfig?.headTags;
|
||||||
|
|
||||||
|
if (hasNoValue(headTagConfigs)) {
|
||||||
|
const parentThemeName = themeConfig?.extends;
|
||||||
|
if (hasValue(parentThemeName)) {
|
||||||
|
// inherit the head tags of the parent theme
|
||||||
|
return this.createHeadTags(parentThemeName);
|
||||||
|
}
|
||||||
|
const defaultThemeConfig = getDefaultThemeConfig();
|
||||||
|
const defaultThemeName = defaultThemeConfig.name;
|
||||||
|
if (
|
||||||
|
hasNoValue(defaultThemeName) ||
|
||||||
|
themeName === defaultThemeName ||
|
||||||
|
themeName === BASE_THEME_NAME
|
||||||
|
) {
|
||||||
|
// last resort, use fallback favicon.ico
|
||||||
|
return [
|
||||||
|
this.createHeadTag({
|
||||||
|
'tagName': 'link',
|
||||||
|
'attributes': {
|
||||||
|
'rel': 'icon',
|
||||||
|
'href': 'assets/images/favicon.ico',
|
||||||
|
'sizes': 'any',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// inherit the head tags of the default theme
|
||||||
|
return this.createHeadTags(defaultThemeConfig.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return headTagConfigs.map(this.createHeadTag.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a single <head> tag element
|
||||||
|
* @param headTagConfig the configuration for this <head> tag
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private createHeadTag(headTagConfig: HeadTagConfig): HTMLElement {
|
||||||
|
const tag = this.document.createElement(headTagConfig.tagName);
|
||||||
|
|
||||||
|
if (hasValue(headTagConfig.attributes)) {
|
||||||
|
Object.entries(headTagConfig.attributes)
|
||||||
|
.forEach(([key, value]) => tag.setAttribute(key, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 'class' attribute should always be 'theme-head-tag' for removal
|
||||||
|
tag.setAttribute('class', 'theme-head-tag');
|
||||||
|
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine whether or not the theme needs to change depending on the current route's URL and snapshot data
|
* Determine whether or not the theme needs to change depending on the current route's URL and snapshot data
|
||||||
* If the snapshot contains a dso, this will be used to match a theme
|
* If the snapshot contains a dso, this will be used to match a theme
|
||||||
|
@@ -24,9 +24,12 @@
|
|||||||
</ds-viewable-collection>
|
</ds-viewable-collection>
|
||||||
<ds-themed-loading *ngIf="(isLoading$ | async)"
|
<ds-themed-loading *ngIf="(isLoading$ | async)"
|
||||||
message="{{'loading.search-results' | translate}}"></ds-themed-loading>
|
message="{{'loading.search-results' | translate}}"></ds-themed-loading>
|
||||||
<div *ngIf="!(isLoading$ | async) && entriesRD?.payload?.page?.length === 0" id="empty-external-entry-list">
|
<div *ngIf="!(isLoading$ | async) && entriesRD?.payload?.page?.length === 0" data-test="empty-external-entry-list">
|
||||||
<ds-alert [type]="'alert-info'">{{ 'search.results.empty' | translate }}</ds-alert>
|
<ds-alert [type]="'alert-info'">{{ 'search.results.empty' | translate }}</ds-alert>
|
||||||
</div>
|
</div>
|
||||||
|
<div *ngIf="!(isLoading$ | async) && entriesRD.statusCode === 500" data-test="empty-external-error-500">
|
||||||
|
<ds-alert [type]="'alert-info'">{{ 'search.results.response.500' | translate }}</ds-alert>
|
||||||
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="reload$.value.sourceId === ''" class="col-md-12">
|
<div *ngIf="reload$.value.sourceId === ''" class="col-md-12">
|
||||||
|
@@ -19,9 +19,15 @@ import { VarDirective } from '../../shared/utils/var.directive';
|
|||||||
import { routeServiceStub } from '../../shared/testing/route-service.stub';
|
import { routeServiceStub } from '../../shared/testing/route-service.stub';
|
||||||
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
|
||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
import {
|
||||||
|
createFailedRemoteDataObject$,
|
||||||
|
createSuccessfulRemoteDataObject,
|
||||||
|
createSuccessfulRemoteDataObject$
|
||||||
|
} from '../../shared/remote-data.utils';
|
||||||
import { ExternalSourceEntry } from '../../core/shared/external-source-entry.model';
|
import { ExternalSourceEntry } from '../../core/shared/external-source-entry.model';
|
||||||
import { SubmissionImportExternalPreviewComponent } from './import-external-preview/submission-import-external-preview.component';
|
import { SubmissionImportExternalPreviewComponent } from './import-external-preview/submission-import-external-preview.component';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
|
||||||
describe('SubmissionImportExternalComponent test suite', () => {
|
describe('SubmissionImportExternalComponent test suite', () => {
|
||||||
let comp: SubmissionImportExternalComponent;
|
let comp: SubmissionImportExternalComponent;
|
||||||
@@ -44,7 +50,8 @@ describe('SubmissionImportExternalComponent test suite', () => {
|
|||||||
beforeEach(waitForAsync (() => {
|
beforeEach(waitForAsync (() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
TranslateModule.forRoot()
|
TranslateModule.forRoot(),
|
||||||
|
BrowserAnimationsModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
SubmissionImportExternalComponent,
|
SubmissionImportExternalComponent,
|
||||||
@@ -177,6 +184,326 @@ describe('SubmissionImportExternalComponent test suite', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('handle backend response for search query', () => {
|
||||||
|
const paginatedData: any = {
|
||||||
|
'timeCompleted': 1657009282990,
|
||||||
|
'msToLive': 900000,
|
||||||
|
'lastUpdated': 1657009282990,
|
||||||
|
'state': 'Success',
|
||||||
|
'errorMessage': null,
|
||||||
|
'payload': {
|
||||||
|
'type': {
|
||||||
|
'value': 'paginated-list'
|
||||||
|
},
|
||||||
|
'pageInfo': {
|
||||||
|
'elementsPerPage': 10,
|
||||||
|
'totalElements': 11971608,
|
||||||
|
'totalPages': 1197161,
|
||||||
|
'currentPage': 1
|
||||||
|
},
|
||||||
|
'_links': {
|
||||||
|
'first': {
|
||||||
|
'href': 'https://example.com/server/api/integration/externalsources/scopus/entries?query=test&page=0&size=10&sort=id,asc'
|
||||||
|
},
|
||||||
|
'self': {
|
||||||
|
'href': 'https://example.com/server/api/integration/externalsources/scopus/entries?sort=id,ASC&page=0&size=10&query=test'
|
||||||
|
},
|
||||||
|
'next': {
|
||||||
|
'href': 'https://example.com/server/api/integration/externalsources/scopus/entries?query=test&page=1&size=10&sort=id,asc'
|
||||||
|
},
|
||||||
|
'last': {
|
||||||
|
'href': 'https://example.com/server/api/integration/externalsources/scopus/entries?query=test&page=1197160&size=10&sort=id,asc'
|
||||||
|
},
|
||||||
|
'page': [
|
||||||
|
{
|
||||||
|
'href': 'https://example.com/server/api/integration/externalsources/scopus/entryValues/2-s2.0-85130258665'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'page': [
|
||||||
|
{
|
||||||
|
'id': '2-s2.0-85130258665',
|
||||||
|
'type': 'externalSourceEntry',
|
||||||
|
'display': 'Biological activities of endophytic fungi isolated from Annona muricata Linnaeus: a systematic review',
|
||||||
|
'value': 'Biological activities of endophytic fungi isolated from Annona muricata Linnaeus: a systematic review',
|
||||||
|
'externalSource': 'scopus',
|
||||||
|
'metadata': {
|
||||||
|
'dc.contributor.author': [
|
||||||
|
{
|
||||||
|
'uuid': 'cbceba09-4c12-4968-ab02-2f77a985b422',
|
||||||
|
'language': null,
|
||||||
|
'value': 'Silva I.M.M.',
|
||||||
|
'place': -1,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'dc.date.issued': [
|
||||||
|
{
|
||||||
|
'uuid': 'e8d3c306-ce21-43e2-8a80-5f257cc3b7ea',
|
||||||
|
'language': null,
|
||||||
|
'value': '2024-01-01',
|
||||||
|
'place': -1,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'dc.description.abstract': [
|
||||||
|
{
|
||||||
|
'uuid': 'c9ee4076-c602-4c1d-ab1a-60bbdd0dd511',
|
||||||
|
'language': null,
|
||||||
|
'value': 'This systematic review integrates the data available in the literature regarding the biological activities of the extracts of endophytic fungi isolated from Annona muricata and their secondary metabolites. The search was performed using four electronic databases, and studies’ quality was evaluated using an adapted assessment tool. The initial database search yielded 436 results; ten studies were selected for inclusion. The leaf was the most studied part of the plant (in nine studies); Periconia sp. was the most tested fungus (n = 4); the most evaluated biological activity was anticancer (n = 6), followed by antiviral (n = 3). Antibacterial, antifungal, and antioxidant activities were also tested. Terpenoids or terpenoid hybrid compounds were the most abundant chemical metabolites. Phenolic compounds, esters, alkaloids, saturated and unsaturated fatty acids, aromatic compounds, and peptides were also reported. The selected studies highlighted the biotechnological potentiality of the endophytic fungi extracts from A. muricata. Consequently, it can be considered a promising source of biological compounds with antioxidant effects and active against different microorganisms and cancer cells. Further research is needed involving different plant tissues, other microorganisms, such as SARS-CoV-2, and different cancer cells.',
|
||||||
|
'place': -1,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'dc.identifier.doi': [
|
||||||
|
{
|
||||||
|
'uuid': '95ec26be-c1b4-4c4a-b12d-12421a4f181d',
|
||||||
|
'language': null,
|
||||||
|
'value': '10.1590/1519-6984.259525',
|
||||||
|
'place': -1,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'dc.identifier.pmid': [
|
||||||
|
{
|
||||||
|
'uuid': 'd6913cd6-1007-4013-b486-3f07192bc739',
|
||||||
|
'language': null,
|
||||||
|
'value': '35588520',
|
||||||
|
'place': -1,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'dc.identifier.scopus': [
|
||||||
|
{
|
||||||
|
'uuid': '6386a1f6-84ba-431d-a583-e16d19af8db0',
|
||||||
|
'language': null,
|
||||||
|
'value': '2-s2.0-85130258665',
|
||||||
|
'place': -1,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'dc.relation.grantno': [
|
||||||
|
{
|
||||||
|
'uuid': 'bcafd7b0-827d-4abb-8608-95dc40a8e58a',
|
||||||
|
'language': null,
|
||||||
|
'value': 'undefined',
|
||||||
|
'place': -1,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'dc.relation.ispartof': [
|
||||||
|
{
|
||||||
|
'uuid': '680819c8-c143-405f-9d09-f84d2d5cd338',
|
||||||
|
'language': null,
|
||||||
|
'value': 'Brazilian Journal of Biology',
|
||||||
|
'place': -1,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'dc.relation.ispartofseries': [
|
||||||
|
{
|
||||||
|
'uuid': '06634104-127b-44f6-9dcc-efae24b74bd1',
|
||||||
|
'language': null,
|
||||||
|
'value': 'Brazilian Journal of Biology',
|
||||||
|
'place': -1,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'dc.relation.issn': [
|
||||||
|
{
|
||||||
|
'uuid': '5f6cce46-2538-49e9-8ed0-a3988dcac6c5',
|
||||||
|
'language': null,
|
||||||
|
'value': '15196984',
|
||||||
|
'place': -1,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'dc.subject': [
|
||||||
|
{
|
||||||
|
'uuid': '0b6fbc77-de54-4f4a-b317-3d74a429f22a',
|
||||||
|
'language': null,
|
||||||
|
'value': 'biological products | biotechnology | mycology | soursop',
|
||||||
|
'place': -1,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'dc.title': [
|
||||||
|
{
|
||||||
|
'uuid': '4c0fa3d3-1a8c-4302-a772-4a4d0408df35',
|
||||||
|
'language': null,
|
||||||
|
'value': 'Biological activities of endophytic fungi isolated from Annona muricata Linnaeus: a systematic review',
|
||||||
|
'place': -1,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'dc.type': [
|
||||||
|
{
|
||||||
|
'uuid': '5b6e0337-6f79-4574-a720-536816d1dc6e',
|
||||||
|
'language': null,
|
||||||
|
'value': 'Journal',
|
||||||
|
'place': -1,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'oaire.citation.volume': [
|
||||||
|
{
|
||||||
|
'uuid': 'b88b0246-61a9-4aca-917f-68afc8ead7d8',
|
||||||
|
'language': null,
|
||||||
|
'value': '84',
|
||||||
|
'place': -1,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'oairecerif.affiliation.orgunit': [
|
||||||
|
{
|
||||||
|
'uuid': '487c0fbc-3622-4cc7-a5fa-4edf780c6a21',
|
||||||
|
'language': null,
|
||||||
|
'value': 'Universidade Federal do Reconcavo da Bahia',
|
||||||
|
'place': -1,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'oairecerif.citation.number': [
|
||||||
|
{
|
||||||
|
'uuid': '90808bdd-f456-4ba3-91aa-b82fb3c453f6',
|
||||||
|
'language': null,
|
||||||
|
'value': 'e259525',
|
||||||
|
'place': -1,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'person.identifier.orcid': [
|
||||||
|
{
|
||||||
|
'uuid': 'e533d0d2-cf26-4c3e-b5ae-cabf497dfb6b',
|
||||||
|
'language': null,
|
||||||
|
'value': '#PLACEHOLDER_PARENT_METADATA_VALUE#',
|
||||||
|
'place': -1,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'person.identifier.scopus-author-id': [
|
||||||
|
{
|
||||||
|
'uuid': '4faf0be5-0226-4d4f-92a0-938397c4ec02',
|
||||||
|
'language': null,
|
||||||
|
'value': '42561627000',
|
||||||
|
'place': -1,
|
||||||
|
'authority': null,
|
||||||
|
'confidence': -1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'_links': {
|
||||||
|
'self': {
|
||||||
|
'href': 'https://example.com/server/api/integration/externalsources/scopus/entryValues/2-s2.0-85130258665'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'statusCode': 200
|
||||||
|
};
|
||||||
|
const errorObj = {
|
||||||
|
errorMessage: 'Http failure response for ' +
|
||||||
|
'https://example.com/server/api/integration/externalsources/pubmed/entries?sort=id,ASC&page=0&size=10&query=test: 500 OK',
|
||||||
|
statusCode: 500,
|
||||||
|
timeCompleted: 1656950434666,
|
||||||
|
errors: [{
|
||||||
|
'message': 'Internal Server Error', 'paths': ['/server/api/integration/externalsources/pubmed/entries']
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(SubmissionImportExternalComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
compAsAny = comp;
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fixture.destroy();
|
||||||
|
comp = null;
|
||||||
|
compAsAny = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('REST endpoint returns a 200 response with valid content', () => {
|
||||||
|
mockExternalSourceService.getExternalSourceEntries.and.returnValue(createSuccessfulRemoteDataObject$(paginatedData.payload));
|
||||||
|
const expectedEntries = createSuccessfulRemoteDataObject(paginatedData.payload);
|
||||||
|
spyOn(routeServiceStub, 'getQueryParameterValue').and.callFake((param) => {
|
||||||
|
if (param === 'entity') {
|
||||||
|
return observableOf('Publication');
|
||||||
|
} else if (param === 'sourceId') {
|
||||||
|
return observableOf('scopus');
|
||||||
|
} else if (param === 'query') {
|
||||||
|
return observableOf('test');
|
||||||
|
}
|
||||||
|
return observableOf({});
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(comp.isLoading$.value).toBe(false);
|
||||||
|
expect(comp.entriesRD$.value).toEqual(expectedEntries);
|
||||||
|
const viewableCollection = fixture.debugElement.query(By.css('ds-viewable-collection'));
|
||||||
|
expect(viewableCollection).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('REST endpoint returns a 200 response with no results', () => {
|
||||||
|
mockExternalSourceService.getExternalSourceEntries.and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList([])));
|
||||||
|
const expectedEntries = createSuccessfulRemoteDataObject(createPaginatedList([]));
|
||||||
|
spyOn(routeServiceStub, 'getQueryParameterValue').and.callFake((param) => {
|
||||||
|
if (param === 'entity') {
|
||||||
|
return observableOf('Publication');
|
||||||
|
}
|
||||||
|
return observableOf({});
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(comp.isLoading$.value).toBe(false);
|
||||||
|
expect(comp.entriesRD$.value).toEqual(expectedEntries);
|
||||||
|
const noDataAlert = fixture.debugElement.query(By.css('[data-test="empty-external-entry-list"]'));
|
||||||
|
expect(noDataAlert).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('REST endpoint returns a 500 error', () => {
|
||||||
|
mockExternalSourceService.getExternalSourceEntries.and.returnValue(createFailedRemoteDataObject$(
|
||||||
|
errorObj.errorMessage,
|
||||||
|
errorObj.statusCode,
|
||||||
|
errorObj.timeCompleted
|
||||||
|
));
|
||||||
|
spyOn(routeServiceStub, 'getQueryParameterValue').and.callFake((param) => {
|
||||||
|
if (param === 'entity') {
|
||||||
|
return observableOf('Publication');
|
||||||
|
} else if (param === 'sourceId') {
|
||||||
|
return observableOf('pubmed');
|
||||||
|
} else if (param === 'query') {
|
||||||
|
return observableOf('test');
|
||||||
|
}
|
||||||
|
return observableOf({});
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(comp.isLoading$.value).toBe(false);
|
||||||
|
expect(comp.entriesRD$.value.statusCode).toEqual(500);
|
||||||
|
const noDataAlert = fixture.debugElement.query(By.css('[data-test="empty-external-error-500"]'));
|
||||||
|
expect(noDataAlert).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// declare a test component
|
// declare a test component
|
||||||
|
@@ -1225,6 +1225,8 @@
|
|||||||
|
|
||||||
"cookies.consent.content-notice.description": "We collect and process your personal information for the following purposes: <strong>Authentication, Preferences, Acknowledgement and Statistics</strong>. <br/> To learn more, please read our {privacyPolicy}.",
|
"cookies.consent.content-notice.description": "We collect and process your personal information for the following purposes: <strong>Authentication, Preferences, Acknowledgement and Statistics</strong>. <br/> To learn more, please read our {privacyPolicy}.",
|
||||||
|
|
||||||
|
"cookies.consent.content-notice.description.no-privacy": "We collect and process your personal information for the following purposes: <strong>Authentication, Preferences, Acknowledgement and Statistics</strong>.",
|
||||||
|
|
||||||
"cookies.consent.content-notice.learnMore": "Customize",
|
"cookies.consent.content-notice.learnMore": "Customize",
|
||||||
|
|
||||||
"cookies.consent.content-modal.description": "Here you can see and customize the information that we collect about you.",
|
"cookies.consent.content-modal.description": "Here you can see and customize the information that we collect about you.",
|
||||||
@@ -2981,6 +2983,22 @@
|
|||||||
|
|
||||||
"process.detail.create" : "Create similar process",
|
"process.detail.create" : "Create similar process",
|
||||||
|
|
||||||
|
"process.detail.actions": "Actions",
|
||||||
|
|
||||||
|
"process.detail.delete.button": "Delete process",
|
||||||
|
|
||||||
|
"process.detail.delete.header": "Delete process",
|
||||||
|
|
||||||
|
"process.detail.delete.body": "Are you sure you want to delete the current process?",
|
||||||
|
|
||||||
|
"process.detail.delete.cancel": "Cancel",
|
||||||
|
|
||||||
|
"process.detail.delete.confirm": "Delete process",
|
||||||
|
|
||||||
|
"process.detail.delete.success": "The process was successfully deleted.",
|
||||||
|
|
||||||
|
"process.detail.delete.error": "Something went wrong when deleting the process",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"process.overview.table.finish" : "Finish time (UTC)",
|
"process.overview.table.finish" : "Finish time (UTC)",
|
||||||
@@ -3001,6 +3019,25 @@
|
|||||||
|
|
||||||
"process.overview.new": "New",
|
"process.overview.new": "New",
|
||||||
|
|
||||||
|
"process.overview.table.actions": "Actions",
|
||||||
|
|
||||||
|
"process.overview.delete": "Delete {{count}} processes",
|
||||||
|
|
||||||
|
"process.overview.delete.clear": "Clear delete selection",
|
||||||
|
|
||||||
|
"process.overview.delete.processing": "{{count}} process(es) are being deleted. Please wait for the deletion to fully complete. Note that this can take a while.",
|
||||||
|
|
||||||
|
"process.overview.delete.body": "Are you sure you want to delete {{count}} process(es)?",
|
||||||
|
|
||||||
|
"process.overview.delete.header": "Delete processes",
|
||||||
|
|
||||||
|
"process.bulk.delete.error.head": "Error on deleteing process",
|
||||||
|
|
||||||
|
"process.bulk.delete.error.body": "The process with ID {{processId}} could not be deleted. The remaining processes will continue being deleted. ",
|
||||||
|
|
||||||
|
"process.bulk.delete.success": "{{count}} process(es) have been succesfully deleted",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"profile.breadcrumbs": "Update Profile",
|
"profile.breadcrumbs": "Update Profile",
|
||||||
|
|
||||||
@@ -3581,6 +3618,7 @@
|
|||||||
|
|
||||||
"search.results.view-result": "View",
|
"search.results.view-result": "View",
|
||||||
|
|
||||||
|
"search.results.response.500": "An error occurred during query execution, please try again later",
|
||||||
|
|
||||||
"default.search.results.head": "Search Results",
|
"default.search.results.head": "Search Results",
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
8078
src/assets/i18n/sv.json5
Normal file
8078
src/assets/i18n/sv.json5
Normal file
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,7 @@ import { MediaViewerConfig } from './media-viewer-config.interface';
|
|||||||
import { BrowseByConfig } from './browse-by-config.interface';
|
import { BrowseByConfig } from './browse-by-config.interface';
|
||||||
import { BundleConfig } from './bundle-config.interface';
|
import { BundleConfig } from './bundle-config.interface';
|
||||||
import { ActuatorsConfig } from './actuators.config';
|
import { ActuatorsConfig } from './actuators.config';
|
||||||
|
import { InfoConfig } from './info-config.interface';
|
||||||
|
|
||||||
interface AppConfig extends Config {
|
interface AppConfig extends Config {
|
||||||
ui: UIServerConfig;
|
ui: UIServerConfig;
|
||||||
@@ -36,8 +37,13 @@ interface AppConfig extends Config {
|
|||||||
mediaViewer: MediaViewerConfig;
|
mediaViewer: MediaViewerConfig;
|
||||||
bundle: BundleConfig;
|
bundle: BundleConfig;
|
||||||
actuators: ActuatorsConfig
|
actuators: ActuatorsConfig
|
||||||
|
info: InfoConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection token for the app configuration.
|
||||||
|
* Provided in {@link InitService.providers}.
|
||||||
|
*/
|
||||||
const APP_CONFIG = new InjectionToken<AppConfig>('APP_CONFIG');
|
const APP_CONFIG = new InjectionToken<AppConfig>('APP_CONFIG');
|
||||||
|
|
||||||
const APP_CONFIG_STATE = makeStateKey('APP_CONFIG_STATE');
|
const APP_CONFIG_STATE = makeStateKey('APP_CONFIG_STATE');
|
||||||
|
@@ -16,6 +16,7 @@ import { ThemeConfig } from './theme.model';
|
|||||||
import { UIServerConfig } from './ui-server-config.interface';
|
import { UIServerConfig } from './ui-server-config.interface';
|
||||||
import { BundleConfig } from './bundle-config.interface';
|
import { BundleConfig } from './bundle-config.interface';
|
||||||
import { ActuatorsConfig } from './actuators.config';
|
import { ActuatorsConfig } from './actuators.config';
|
||||||
|
import { InfoConfig } from './info-config.interface';
|
||||||
|
|
||||||
export class DefaultAppConfig implements AppConfig {
|
export class DefaultAppConfig implements AppConfig {
|
||||||
production = false;
|
production = false;
|
||||||
@@ -192,6 +193,7 @@ export class DefaultAppConfig implements AppConfig {
|
|||||||
{ code: 'pt-PT', label: 'Português', active: true },
|
{ code: 'pt-PT', label: 'Português', active: true },
|
||||||
{ code: 'pt-BR', label: 'Português do Brasil', active: true },
|
{ code: 'pt-BR', label: 'Português do Brasil', active: true },
|
||||||
{ code: 'fi', label: 'Suomi', active: true },
|
{ code: 'fi', label: 'Suomi', active: true },
|
||||||
|
{ code: 'sv', label: 'Svenska', active: true },
|
||||||
{ code: 'tr', label: 'Türkçe', active: true },
|
{ code: 'tr', label: 'Türkçe', active: true },
|
||||||
{ code: 'bn', label: 'বাংলা', active: true }
|
{ code: 'bn', label: 'বাংলা', active: true }
|
||||||
];
|
];
|
||||||
@@ -324,4 +326,16 @@ export class DefaultAppConfig implements AppConfig {
|
|||||||
image: false,
|
image: false,
|
||||||
video: false
|
video: false
|
||||||
};
|
};
|
||||||
|
// Whether the end-user-agreement and privacy policy feature should be enabled or not.
|
||||||
|
// Disabling the end user agreement feature will result in:
|
||||||
|
// - Users no longer being forced to accept the end-user-agreement before they can access the repository
|
||||||
|
// - A 404 page if you manually try to navigate to the end-user-agreement page at info/end-user-agreement
|
||||||
|
// - All end-user-agreement related links and pages will be removed from the UI (e.g. in the footer)
|
||||||
|
// Disabling the privacy policy feature will result in:
|
||||||
|
// - A 404 page if you manually try to navigate to the privacy policy page at info/privacy
|
||||||
|
// - All mentions of the privacy policy being removed from the UI (e.g. in the footer)
|
||||||
|
info: InfoConfig = {
|
||||||
|
enableEndUserAgreement: true,
|
||||||
|
enablePrivacyStatement: true
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
6
src/config/info-config.interface.ts
Normal file
6
src/config/info-config.interface.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { Config } from './config.interface';
|
||||||
|
|
||||||
|
export interface InfoConfig extends Config {
|
||||||
|
enableEndUserAgreement: boolean;
|
||||||
|
enablePrivacyStatement: boolean;
|
||||||
|
}
|
@@ -243,5 +243,9 @@ export const environment: BuildConfig = {
|
|||||||
mediaViewer: {
|
mediaViewer: {
|
||||||
image: true,
|
image: true,
|
||||||
video: true
|
video: true
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
enableEndUserAgreement: true,
|
||||||
|
enablePrivacyStatement: true,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { HttpClient, HttpClientModule } from '@angular/common/http';
|
import { HttpClient, HttpClientModule } from '@angular/common/http';
|
||||||
import { APP_INITIALIZER, NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { BrowserModule, makeStateKey, TransferState } from '@angular/platform-browser';
|
import { BrowserModule, BrowserTransferStateModule, makeStateKey, TransferState } from '@angular/platform-browser';
|
||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
||||||
|
|
||||||
@@ -12,8 +12,6 @@ import { IdlePreloadModule } from 'angular-idle-preload';
|
|||||||
import { AppComponent } from '../../app/app.component';
|
import { AppComponent } from '../../app/app.component';
|
||||||
|
|
||||||
import { AppModule } from '../../app/app.module';
|
import { AppModule } from '../../app/app.module';
|
||||||
import { DSpaceBrowserTransferStateModule } from '../transfer-state/dspace-browser-transfer-state.module';
|
|
||||||
import { DSpaceTransferState } from '../transfer-state/dspace-transfer-state.service';
|
|
||||||
import { ClientCookieService } from '../../app/core/services/client-cookie.service';
|
import { ClientCookieService } from '../../app/core/services/client-cookie.service';
|
||||||
import { CookieService } from '../../app/core/services/cookie.service';
|
import { CookieService } from '../../app/core/services/cookie.service';
|
||||||
import { AuthService } from '../../app/core/auth/auth.service';
|
import { AuthService } from '../../app/core/auth/auth.service';
|
||||||
@@ -23,21 +21,12 @@ import { StatisticsModule } from '../../app/statistics/statistics.module';
|
|||||||
import { BrowserKlaroService } from '../../app/shared/cookies/browser-klaro.service';
|
import { BrowserKlaroService } from '../../app/shared/cookies/browser-klaro.service';
|
||||||
import { KlaroService } from '../../app/shared/cookies/klaro.service';
|
import { KlaroService } from '../../app/shared/cookies/klaro.service';
|
||||||
import { HardRedirectService } from '../../app/core/services/hard-redirect.service';
|
import { HardRedirectService } from '../../app/core/services/hard-redirect.service';
|
||||||
import {
|
import { BrowserHardRedirectService, locationProvider, LocationToken } from '../../app/core/services/browser-hard-redirect.service';
|
||||||
BrowserHardRedirectService,
|
|
||||||
locationProvider,
|
|
||||||
LocationToken
|
|
||||||
} from '../../app/core/services/browser-hard-redirect.service';
|
|
||||||
import { LocaleService } from '../../app/core/locale/locale.service';
|
import { LocaleService } from '../../app/core/locale/locale.service';
|
||||||
import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.service';
|
import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.service';
|
||||||
import { AuthRequestService } from '../../app/core/auth/auth-request.service';
|
import { AuthRequestService } from '../../app/core/auth/auth-request.service';
|
||||||
import { BrowserAuthRequestService } from '../../app/core/auth/browser-auth-request.service';
|
import { BrowserAuthRequestService } from '../../app/core/auth/browser-auth-request.service';
|
||||||
import { AppConfig, APP_CONFIG_STATE } from '../../config/app-config.interface';
|
import { BrowserInitService } from './browser-init.service';
|
||||||
import { DefaultAppConfig } from '../../config/default-app-config';
|
|
||||||
import { extendEnvironmentWithAppConfig } from '../../config/config.util';
|
|
||||||
import { CorrelationIdService } from '../../app/correlation-id/correlation-id.service';
|
|
||||||
|
|
||||||
import { environment } from '../../environments/environment';
|
|
||||||
|
|
||||||
export const REQ_KEY = makeStateKey<string>('req');
|
export const REQ_KEY = makeStateKey<string>('req');
|
||||||
|
|
||||||
@@ -61,7 +50,7 @@ export function getRequest(transferState: TransferState): any {
|
|||||||
StatisticsModule.forRoot(),
|
StatisticsModule.forRoot(),
|
||||||
Angulartics2RouterlessModule.forRoot(),
|
Angulartics2RouterlessModule.forRoot(),
|
||||||
BrowserAnimationsModule,
|
BrowserAnimationsModule,
|
||||||
DSpaceBrowserTransferStateModule,
|
BrowserTransferStateModule,
|
||||||
TranslateModule.forRoot({
|
TranslateModule.forRoot({
|
||||||
loader: {
|
loader: {
|
||||||
provide: TranslateLoader,
|
provide: TranslateLoader,
|
||||||
@@ -72,27 +61,7 @@ export function getRequest(transferState: TransferState): any {
|
|||||||
AppModule
|
AppModule
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
...BrowserInitService.providers(),
|
||||||
provide: APP_INITIALIZER,
|
|
||||||
useFactory: (
|
|
||||||
transferState: TransferState,
|
|
||||||
dspaceTransferState: DSpaceTransferState,
|
|
||||||
correlationIdService: CorrelationIdService
|
|
||||||
) => {
|
|
||||||
if (transferState.hasKey<AppConfig>(APP_CONFIG_STATE)) {
|
|
||||||
const appConfig = transferState.get<AppConfig>(APP_CONFIG_STATE, new DefaultAppConfig());
|
|
||||||
// extend environment with app config for browser
|
|
||||||
extendEnvironmentWithAppConfig(environment, appConfig);
|
|
||||||
}
|
|
||||||
return () =>
|
|
||||||
dspaceTransferState.transfer().then((b: boolean) => {
|
|
||||||
correlationIdService.initCorrelationId();
|
|
||||||
return b;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
deps: [TransferState, DSpaceTransferState, CorrelationIdService],
|
|
||||||
multi: true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
provide: REQUEST,
|
provide: REQUEST,
|
||||||
useFactory: getRequest,
|
useFactory: getRequest,
|
||||||
|
155
src/modules/app/browser-init.service.spec.ts
Normal file
155
src/modules/app/browser-init.service.spec.ts
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import { InitService } from '../../app/init.service';
|
||||||
|
import { APP_CONFIG } from 'src/config/app-config.interface';
|
||||||
|
import { inject, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.service';
|
||||||
|
import { MetadataService } from '../../app/core/metadata/metadata.service';
|
||||||
|
import { BreadcrumbsService } from '../../app/breadcrumbs/breadcrumbs.service';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Store, StoreModule } from '@ngrx/store';
|
||||||
|
import { authReducer } from '../../app/core/auth/auth.reducer';
|
||||||
|
import { storeModuleConfig } from '../../app/app.reducer';
|
||||||
|
import { AngularticsProviderMock } from '../../app/shared/mocks/angulartics-provider.service.mock';
|
||||||
|
import { Angulartics2DSpace } from '../../app/statistics/angulartics/dspace-provider';
|
||||||
|
import { AuthService } from '../../app/core/auth/auth.service';
|
||||||
|
import { AuthServiceMock } from '../../app/shared/mocks/auth.service.mock';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { RouterMock } from '../../app/shared/mocks/router.mock';
|
||||||
|
import { MockActivatedRoute } from '../../app/shared/mocks/active-router.mock';
|
||||||
|
import { MenuService } from '../../app/shared/menu/menu.service';
|
||||||
|
import { LocaleService } from '../../app/core/locale/locale.service';
|
||||||
|
import { environment } from '../../environments/environment';
|
||||||
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
|
import { AppComponent } from '../../app/app.component';
|
||||||
|
import { RouteService } from '../../app/core/services/route.service';
|
||||||
|
import { getMockLocaleService } from '../../app/app.component.spec';
|
||||||
|
import { MenuServiceStub } from '../../app/shared/testing/menu-service.stub';
|
||||||
|
import { CorrelationIdService } from '../../app/correlation-id/correlation-id.service';
|
||||||
|
import { KlaroService } from '../../app/shared/cookies/klaro.service';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { TranslateLoaderMock } from '../../app/shared/mocks/translate-loader.mock';
|
||||||
|
import { getTestScheduler } from 'jasmine-marbles';
|
||||||
|
import { ThemeService } from '../../app/shared/theme-support/theme.service';
|
||||||
|
import { getMockThemeService } from '../../app/shared/mocks/theme-service.mock';
|
||||||
|
import { BrowserInitService } from './browser-init.service';
|
||||||
|
import { TransferState } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
core: {
|
||||||
|
auth: {
|
||||||
|
loading: false,
|
||||||
|
blocking: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('BrowserInitService', () => {
|
||||||
|
describe('browser-specific initialization steps', () => {
|
||||||
|
let correlationIdServiceSpy;
|
||||||
|
let dspaceTransferStateSpy;
|
||||||
|
let transferStateSpy;
|
||||||
|
let metadataServiceSpy;
|
||||||
|
let breadcrumbsServiceSpy;
|
||||||
|
let klaroServiceSpy;
|
||||||
|
let googleAnalyticsSpy;
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
correlationIdServiceSpy = jasmine.createSpyObj('correlationIdServiceSpy', [
|
||||||
|
'initCorrelationId',
|
||||||
|
]);
|
||||||
|
dspaceTransferStateSpy = jasmine.createSpyObj('dspaceTransferStateSpy', [
|
||||||
|
'transfer',
|
||||||
|
]);
|
||||||
|
transferStateSpy = jasmine.createSpyObj('dspaceTransferStateSpy', [
|
||||||
|
'get', 'hasKey'
|
||||||
|
]);
|
||||||
|
breadcrumbsServiceSpy = jasmine.createSpyObj('breadcrumbsServiceSpy', [
|
||||||
|
'listenForRouteChanges',
|
||||||
|
]);
|
||||||
|
metadataServiceSpy = jasmine.createSpyObj('metadataService', [
|
||||||
|
'listenForRouteChange',
|
||||||
|
]);
|
||||||
|
klaroServiceSpy = jasmine.createSpyObj('klaroServiceSpy', [
|
||||||
|
'initialize',
|
||||||
|
]);
|
||||||
|
googleAnalyticsSpy = jasmine.createSpyObj('googleAnalyticsService', [
|
||||||
|
'addTrackingIdToPage',
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
TestBed.resetTestingModule();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
StoreModule.forRoot(authReducer, storeModuleConfig),
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: InitService, useClass: BrowserInitService },
|
||||||
|
{ provide: CorrelationIdService, useValue: correlationIdServiceSpy },
|
||||||
|
{ provide: APP_CONFIG, useValue: environment },
|
||||||
|
{ provide: LocaleService, useValue: getMockLocaleService() },
|
||||||
|
{ provide: Angulartics2DSpace, useValue: new AngularticsProviderMock() },
|
||||||
|
{ provide: MetadataService, useValue: metadataServiceSpy },
|
||||||
|
{ provide: BreadcrumbsService, useValue: breadcrumbsServiceSpy },
|
||||||
|
{ provide: AuthService, useValue: new AuthServiceMock() },
|
||||||
|
{ provide: Router, useValue: new RouterMock() },
|
||||||
|
{ provide: ActivatedRoute, useValue: new MockActivatedRoute() },
|
||||||
|
{ provide: MenuService, useValue: new MenuServiceStub() },
|
||||||
|
{ provide: KlaroService, useValue: klaroServiceSpy },
|
||||||
|
{ provide: GoogleAnalyticsService, useValue: googleAnalyticsSpy },
|
||||||
|
{ provide: ThemeService, useValue: getMockThemeService() },
|
||||||
|
provideMockStore({ initialState }),
|
||||||
|
AppComponent,
|
||||||
|
RouteService,
|
||||||
|
{ provide: TransferState, useValue: undefined },
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('initGoogleÀnalytics', () => {
|
||||||
|
it('should call googleAnalyticsService.addTrackingIdToPage()', inject([InitService], (service) => {
|
||||||
|
// @ts-ignore
|
||||||
|
service.initGoogleAnalytics();
|
||||||
|
expect(googleAnalyticsSpy.addTrackingIdToPage).toHaveBeenCalledTimes(1);
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('initKlaro', () => {
|
||||||
|
const BLOCKING = {
|
||||||
|
t: { core: { auth: { blocking: true } } },
|
||||||
|
f: { core: { auth: { blocking: false } } },
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should not initialize Klaro while auth is blocking', () => {
|
||||||
|
getTestScheduler().run(({ cold, flush}) => {
|
||||||
|
TestBed.overrideProvider(Store, { useValue: cold('t--t--t--', BLOCKING) });
|
||||||
|
const service = TestBed.inject(InitService);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
service.initKlaro();
|
||||||
|
flush();
|
||||||
|
expect(klaroServiceSpy.initialize).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should only initialize Klaro the first time auth is unblocked', () => {
|
||||||
|
getTestScheduler().run(({ cold, flush}) => {
|
||||||
|
TestBed.overrideProvider(Store, { useValue: cold('t--t--f--t--f--', BLOCKING) });
|
||||||
|
const service = TestBed.inject(InitService);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
service.initKlaro();
|
||||||
|
flush();
|
||||||
|
expect(klaroServiceSpy.initialize).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
136
src/modules/app/browser-init.service.ts
Normal file
136
src/modules/app/browser-init.service.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
/**
|
||||||
|
* The contents of this file are subject to the license and copyright
|
||||||
|
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||||
|
* tree and available online at
|
||||||
|
*
|
||||||
|
* http://www.dspace.org/license/
|
||||||
|
*/
|
||||||
|
import { InitService } from '../../app/init.service';
|
||||||
|
import { select, Store } from '@ngrx/store';
|
||||||
|
import { AppState } from '../../app/app.reducer';
|
||||||
|
import { TransferState } from '@angular/platform-browser';
|
||||||
|
import { APP_CONFIG, APP_CONFIG_STATE, AppConfig } from '../../config/app-config.interface';
|
||||||
|
import { DefaultAppConfig } from '../../config/default-app-config';
|
||||||
|
import { extendEnvironmentWithAppConfig } from '../../config/config.util';
|
||||||
|
import { environment } from '../../environments/environment';
|
||||||
|
import { CorrelationIdService } from '../../app/correlation-id/correlation-id.service';
|
||||||
|
import { Inject, Injectable } from '@angular/core';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { LocaleService } from '../../app/core/locale/locale.service';
|
||||||
|
import { Angulartics2DSpace } from '../../app/statistics/angulartics/dspace-provider';
|
||||||
|
import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.service';
|
||||||
|
import { MetadataService } from '../../app/core/metadata/metadata.service';
|
||||||
|
import { BreadcrumbsService } from '../../app/breadcrumbs/breadcrumbs.service';
|
||||||
|
import { KlaroService } from '../../app/shared/cookies/klaro.service';
|
||||||
|
import { AuthService } from '../../app/core/auth/auth.service';
|
||||||
|
import { ThemeService } from '../../app/shared/theme-support/theme.service';
|
||||||
|
import { StoreAction, StoreActionTypes } from '../../app/store.actions';
|
||||||
|
import { coreSelector } from '../../app/core/core.selectors';
|
||||||
|
import { distinctUntilChanged, filter, find, map, take } from 'rxjs/operators';
|
||||||
|
import { isNotEmpty } from '../../app/shared/empty.util';
|
||||||
|
import { isAuthenticationBlocking } from '../../app/core/auth/selectors';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs client-side initialization.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class BrowserInitService extends InitService {
|
||||||
|
constructor(
|
||||||
|
protected store: Store<AppState>,
|
||||||
|
protected correlationIdService: CorrelationIdService,
|
||||||
|
protected transferState: TransferState,
|
||||||
|
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
||||||
|
protected translate: TranslateService,
|
||||||
|
protected localeService: LocaleService,
|
||||||
|
protected angulartics2DSpace: Angulartics2DSpace,
|
||||||
|
protected googleAnalyticsService: GoogleAnalyticsService,
|
||||||
|
protected metadata: MetadataService,
|
||||||
|
protected breadcrumbsService: BreadcrumbsService,
|
||||||
|
protected klaroService: KlaroService,
|
||||||
|
protected authService: AuthService,
|
||||||
|
protected themeService: ThemeService,
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
store,
|
||||||
|
correlationIdService,
|
||||||
|
appConfig,
|
||||||
|
translate,
|
||||||
|
localeService,
|
||||||
|
angulartics2DSpace,
|
||||||
|
metadata,
|
||||||
|
breadcrumbsService,
|
||||||
|
themeService,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static resolveAppConfig(
|
||||||
|
transferState: TransferState,
|
||||||
|
) {
|
||||||
|
if (transferState.hasKey<AppConfig>(APP_CONFIG_STATE)) {
|
||||||
|
const appConfig = transferState.get<AppConfig>(APP_CONFIG_STATE, new DefaultAppConfig());
|
||||||
|
// extend environment with app config for browser
|
||||||
|
extendEnvironmentWithAppConfig(environment, appConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected init(): () => Promise<boolean> {
|
||||||
|
return async () => {
|
||||||
|
await this.loadAppState();
|
||||||
|
this.checkAuthenticationToken();
|
||||||
|
this.initCorrelationId();
|
||||||
|
|
||||||
|
this.checkEnvironment();
|
||||||
|
|
||||||
|
this.initI18n();
|
||||||
|
this.initAngulartics();
|
||||||
|
this.initGoogleAnalytics();
|
||||||
|
this.initRouteListeners();
|
||||||
|
this.themeService.listenForThemeChanges(true);
|
||||||
|
this.trackAuthTokenExpiration();
|
||||||
|
|
||||||
|
this.initKlaro();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Browser-only initialization steps
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve server-side application state from the {@link NGRX_STATE} key and rehydrate the store.
|
||||||
|
* Resolves once the store is no longer empty.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async loadAppState(): Promise<boolean> {
|
||||||
|
const state = this.transferState.get<any>(InitService.NGRX_STATE, null);
|
||||||
|
this.transferState.remove(InitService.NGRX_STATE);
|
||||||
|
this.store.dispatch(new StoreAction(StoreActionTypes.REHYDRATE, state));
|
||||||
|
return this.store.select(coreSelector).pipe(
|
||||||
|
find((core: any) => isNotEmpty(core)),
|
||||||
|
map(() => true)
|
||||||
|
).toPromise();
|
||||||
|
}
|
||||||
|
|
||||||
|
private trackAuthTokenExpiration(): void {
|
||||||
|
this.authService.trackTokenExpiration();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize Klaro
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected initKlaro() {
|
||||||
|
this.store.pipe(
|
||||||
|
select(isAuthenticationBlocking),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
filter((isBlocking: boolean) => isBlocking === false),
|
||||||
|
take(1)
|
||||||
|
).subscribe(() => {
|
||||||
|
this.klaroService.initialize();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected initGoogleAnalytics() {
|
||||||
|
this.googleAnalyticsService.addTrackingIdToPage();
|
||||||
|
}
|
||||||
|
}
|
@@ -1,8 +1,8 @@
|
|||||||
import { HTTP_INTERCEPTORS } from '@angular/common/http';
|
import { HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||||
import { APP_INITIALIZER, NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { BrowserModule, TransferState } from '@angular/platform-browser';
|
import { BrowserModule, TransferState } from '@angular/platform-browser';
|
||||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import { ServerModule } from '@angular/platform-server';
|
import { ServerModule, ServerTransferStateModule } from '@angular/platform-server';
|
||||||
|
|
||||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
@@ -12,8 +12,6 @@ import { Angulartics2GoogleAnalytics } from 'angulartics2';
|
|||||||
import { AppComponent } from '../../app/app.component';
|
import { AppComponent } from '../../app/app.component';
|
||||||
|
|
||||||
import { AppModule } from '../../app/app.module';
|
import { AppModule } from '../../app/app.module';
|
||||||
import { DSpaceServerTransferStateModule } from '../transfer-state/dspace-server-transfer-state.module';
|
|
||||||
import { DSpaceTransferState } from '../transfer-state/dspace-transfer-state.service';
|
|
||||||
import { TranslateServerLoader } from '../../ngx-translate-loaders/translate-server.loader';
|
import { TranslateServerLoader } from '../../ngx-translate-loaders/translate-server.loader';
|
||||||
import { CookieService } from '../../app/core/services/cookie.service';
|
import { CookieService } from '../../app/core/services/cookie.service';
|
||||||
import { ServerCookieService } from '../../app/core/services/server-cookie.service';
|
import { ServerCookieService } from '../../app/core/services/server-cookie.service';
|
||||||
@@ -31,10 +29,7 @@ import { ServerHardRedirectService } from '../../app/core/services/server-hard-r
|
|||||||
import { Angulartics2Mock } from '../../app/shared/mocks/angulartics2.service.mock';
|
import { Angulartics2Mock } from '../../app/shared/mocks/angulartics2.service.mock';
|
||||||
import { AuthRequestService } from '../../app/core/auth/auth-request.service';
|
import { AuthRequestService } from '../../app/core/auth/auth-request.service';
|
||||||
import { ServerAuthRequestService } from '../../app/core/auth/server-auth-request.service';
|
import { ServerAuthRequestService } from '../../app/core/auth/server-auth-request.service';
|
||||||
import { CorrelationIdService } from '../../app/correlation-id/correlation-id.service';
|
import { ServerInitService } from './server-init.service';
|
||||||
import { AppConfig, APP_CONFIG_STATE } from '../../config/app-config.interface';
|
|
||||||
|
|
||||||
import { environment } from '../../environments/environment';
|
|
||||||
|
|
||||||
export function createTranslateLoader(transferState: TransferState) {
|
export function createTranslateLoader(transferState: TransferState) {
|
||||||
return new TranslateServerLoader(transferState, 'dist/server/assets/i18n/', '.json5');
|
return new TranslateServerLoader(transferState, 'dist/server/assets/i18n/', '.json5');
|
||||||
@@ -47,7 +42,7 @@ export function createTranslateLoader(transferState: TransferState) {
|
|||||||
appId: 'dspace-angular'
|
appId: 'dspace-angular'
|
||||||
}),
|
}),
|
||||||
NoopAnimationsModule,
|
NoopAnimationsModule,
|
||||||
DSpaceServerTransferStateModule,
|
ServerTransferStateModule,
|
||||||
TranslateModule.forRoot({
|
TranslateModule.forRoot({
|
||||||
loader: {
|
loader: {
|
||||||
provide: TranslateLoader,
|
provide: TranslateLoader,
|
||||||
@@ -59,22 +54,7 @@ export function createTranslateLoader(transferState: TransferState) {
|
|||||||
ServerModule,
|
ServerModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
// Initialize app config and extend environment
|
...ServerInitService.providers(),
|
||||||
{
|
|
||||||
provide: APP_INITIALIZER,
|
|
||||||
useFactory: (
|
|
||||||
transferState: TransferState,
|
|
||||||
dspaceTransferState: DSpaceTransferState,
|
|
||||||
correlationIdService: CorrelationIdService,
|
|
||||||
) => {
|
|
||||||
transferState.set<AppConfig>(APP_CONFIG_STATE, environment as AppConfig);
|
|
||||||
dspaceTransferState.transfer();
|
|
||||||
correlationIdService.initCorrelationId();
|
|
||||||
return () => true;
|
|
||||||
},
|
|
||||||
deps: [TransferState, DSpaceTransferState, CorrelationIdService],
|
|
||||||
multi: true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
provide: Angulartics2,
|
provide: Angulartics2,
|
||||||
useClass: Angulartics2Mock
|
useClass: Angulartics2Mock
|
||||||
|
93
src/modules/app/server-init.service.ts
Normal file
93
src/modules/app/server-init.service.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* The contents of this file are subject to the license and copyright
|
||||||
|
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||||
|
* tree and available online at
|
||||||
|
*
|
||||||
|
* http://www.dspace.org/license/
|
||||||
|
*/
|
||||||
|
import { InitService } from '../../app/init.service';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { AppState } from '../../app/app.reducer';
|
||||||
|
import { TransferState } from '@angular/platform-browser';
|
||||||
|
import { CorrelationIdService } from '../../app/correlation-id/correlation-id.service';
|
||||||
|
import { APP_CONFIG, APP_CONFIG_STATE, AppConfig } from '../../config/app-config.interface';
|
||||||
|
import { environment } from '../../environments/environment';
|
||||||
|
import { Inject, Injectable } from '@angular/core';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { LocaleService } from '../../app/core/locale/locale.service';
|
||||||
|
import { Angulartics2DSpace } from '../../app/statistics/angulartics/dspace-provider';
|
||||||
|
import { MetadataService } from '../../app/core/metadata/metadata.service';
|
||||||
|
import { BreadcrumbsService } from '../../app/breadcrumbs/breadcrumbs.service';
|
||||||
|
import { CSSVariableService } from '../../app/shared/sass-helper/sass-helper.service';
|
||||||
|
import { ThemeService } from '../../app/shared/theme-support/theme.service';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs server-side initialization.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ServerInitService extends InitService {
|
||||||
|
constructor(
|
||||||
|
protected store: Store<AppState>,
|
||||||
|
protected correlationIdService: CorrelationIdService,
|
||||||
|
protected transferState: TransferState,
|
||||||
|
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
||||||
|
protected translate: TranslateService,
|
||||||
|
protected localeService: LocaleService,
|
||||||
|
protected angulartics2DSpace: Angulartics2DSpace,
|
||||||
|
protected metadata: MetadataService,
|
||||||
|
protected breadcrumbsService: BreadcrumbsService,
|
||||||
|
protected cssService: CSSVariableService,
|
||||||
|
protected themeService: ThemeService,
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
store,
|
||||||
|
correlationIdService,
|
||||||
|
appConfig,
|
||||||
|
translate,
|
||||||
|
localeService,
|
||||||
|
angulartics2DSpace,
|
||||||
|
metadata,
|
||||||
|
breadcrumbsService,
|
||||||
|
themeService,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected init(): () => Promise<boolean> {
|
||||||
|
return async () => {
|
||||||
|
this.checkAuthenticationToken();
|
||||||
|
this.saveAppConfigForCSR();
|
||||||
|
this.saveAppState();
|
||||||
|
this.initCorrelationId();
|
||||||
|
|
||||||
|
this.checkEnvironment();
|
||||||
|
this.initI18n();
|
||||||
|
this.initAngulartics();
|
||||||
|
this.initRouteListeners();
|
||||||
|
this.themeService.listenForThemeChanges(false);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server-only initialization steps
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the {@link NGRX_STATE} key when state is serialized to be transfered
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private saveAppState() {
|
||||||
|
this.transferState.onSerialize(InitService.NGRX_STATE, () => {
|
||||||
|
let state;
|
||||||
|
this.store.pipe(take(1)).subscribe((saveState: any) => {
|
||||||
|
state = saveState;
|
||||||
|
});
|
||||||
|
|
||||||
|
return state;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private saveAppConfigForCSR(): void {
|
||||||
|
this.transferState.set<AppConfig>(APP_CONFIG_STATE, environment as AppConfig);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,16 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { BrowserTransferStateModule } from '@angular/platform-browser';
|
|
||||||
import { DSpaceBrowserTransferState } from './dspace-browser-transfer-state.service';
|
|
||||||
import { DSpaceTransferState } from './dspace-transfer-state.service';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [
|
|
||||||
BrowserTransferStateModule
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
{ provide: DSpaceTransferState, useClass: DSpaceBrowserTransferState }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
export class DSpaceBrowserTransferStateModule {
|
|
||||||
|
|
||||||
}
|
|
@@ -1,19 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { coreSelector } from 'src/app/core/core.selectors';
|
|
||||||
import { StoreAction, StoreActionTypes } from '../../app/store.actions';
|
|
||||||
import { DSpaceTransferState } from './dspace-transfer-state.service';
|
|
||||||
import { find, map } from 'rxjs/operators';
|
|
||||||
import { isNotEmpty } from '../../app/shared/empty.util';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class DSpaceBrowserTransferState extends DSpaceTransferState {
|
|
||||||
transfer(): Promise<boolean> {
|
|
||||||
const state = this.transferState.get<any>(DSpaceTransferState.NGRX_STATE, null);
|
|
||||||
this.transferState.remove(DSpaceTransferState.NGRX_STATE);
|
|
||||||
this.store.dispatch(new StoreAction(StoreActionTypes.REHYDRATE, state));
|
|
||||||
return this.store.select(coreSelector).pipe(
|
|
||||||
find((core: any) => isNotEmpty(core)),
|
|
||||||
map(() => true)
|
|
||||||
).toPromise();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,16 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { ServerTransferStateModule } from '@angular/platform-server';
|
|
||||||
import { DSpaceServerTransferState } from './dspace-server-transfer-state.service';
|
|
||||||
import { DSpaceTransferState } from './dspace-transfer-state.service';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [
|
|
||||||
ServerTransferStateModule
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
{ provide: DSpaceTransferState, useClass: DSpaceServerTransferState }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
export class DSpaceServerTransferStateModule {
|
|
||||||
|
|
||||||
}
|
|
@@ -1,20 +0,0 @@
|
|||||||
|
|
||||||
import {take} from 'rxjs/operators';
|
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { DSpaceTransferState } from './dspace-transfer-state.service';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class DSpaceServerTransferState extends DSpaceTransferState {
|
|
||||||
transfer(): Promise<boolean> {
|
|
||||||
this.transferState.onSerialize(DSpaceTransferState.NGRX_STATE, () => {
|
|
||||||
let state;
|
|
||||||
this.store.pipe(take(1)).subscribe((saveState: any) => {
|
|
||||||
state = saveState;
|
|
||||||
});
|
|
||||||
|
|
||||||
return state;
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Promise<boolean>(() => true);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,18 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { makeStateKey, TransferState } from '@angular/platform-browser';
|
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
import { AppState } from '../../app/app.reducer';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export abstract class DSpaceTransferState {
|
|
||||||
|
|
||||||
protected static NGRX_STATE = makeStateKey('NGRX_STATE');
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
protected transferState: TransferState,
|
|
||||||
protected store: Store<AppState>
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract transfer(): Promise<boolean>;
|
|
||||||
}
|
|
@@ -12,6 +12,9 @@ $fa-font-path: "^assets/fonts" !default;
|
|||||||
/* Images */
|
/* Images */
|
||||||
$image-path: "../assets/images" !default;
|
$image-path: "../assets/images" !default;
|
||||||
|
|
||||||
|
// enable-responsive-font-sizes allows text to scale more naturally across device and viewport sizes
|
||||||
|
$enable-responsive-font-sizes: true;
|
||||||
|
|
||||||
/** Bootstrap Variables **/
|
/** Bootstrap Variables **/
|
||||||
/* Colors */
|
/* Colors */
|
||||||
$gray-700: #495057 !default; // Bootstrap $gray-700
|
$gray-700: #495057 !default; // Bootstrap $gray-700
|
||||||
|
5
src/styles/_vendor.scss
Normal file
5
src/styles/_vendor.scss
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// node_modules imports meant for all the themes
|
||||||
|
|
||||||
|
@import '~node_modules/bootstrap/scss/bootstrap.scss';
|
||||||
|
@import '~node_modules/nouislider/distribute/nouislider.min';
|
||||||
|
@import '~node_modules/ngx-ui-switch/ui-switch.component.scss';
|
@@ -1,8 +1,6 @@
|
|||||||
@import './helpers/font_awesome_imports.scss';
|
@import './helpers/font_awesome_imports.scss';
|
||||||
@import '../../node_modules/bootstrap/scss/bootstrap.scss';
|
@import './_vendor.scss';
|
||||||
@import '../../node_modules/nouislider/distribute/nouislider.min';
|
|
||||||
@import './_custom_variables.scss';
|
@import './_custom_variables.scss';
|
||||||
@import './bootstrap_variables_mapping.scss';
|
@import './bootstrap_variables_mapping.scss';
|
||||||
@import './_truncatable-part.component.scss';
|
@import './_truncatable-part.component.scss';
|
||||||
@import './_global-styles.scss';
|
@import './_global-styles.scss';
|
||||||
@import '../../node_modules/ngx-ui-switch/ui-switch.component.scss';
|
|
||||||
|
@@ -0,0 +1,11 @@
|
|||||||
|
<div>
|
||||||
|
<div class="modal-header">{{'dso-selector.'+ action + '.' + objectType.toString().toLowerCase() + '.head' | translate}}
|
||||||
|
<button type="button" class="close" (click)="close()" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<h5 *ngIf="header" class="px-2">{{header | translate}}</h5>
|
||||||
|
<ds-dso-selector [currentDSOId]="dsoRD?.payload.uuid" [types]="selectorTypes" (onSelect)="selectObject($event)"></ds-dso-selector>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,13 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import {
|
||||||
|
CreateCollectionParentSelectorComponent as BaseComponent
|
||||||
|
} from '../../../../../../../app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-create-collection-parent-selector',
|
||||||
|
// styleUrls: ['./create-collection-parent-selector.component.scss'],
|
||||||
|
// templateUrl: './create-collection-parent-selector.component.html',
|
||||||
|
templateUrl: '../../../../../../../app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html',
|
||||||
|
})
|
||||||
|
export class CreateCollectionParentSelectorComponent extends BaseComponent {
|
||||||
|
}
|
@@ -0,0 +1,19 @@
|
|||||||
|
<div>
|
||||||
|
<div class="modal-header">{{'dso-selector.'+ action + '.' + objectType.toString().toLowerCase() + '.head' | translate}}
|
||||||
|
<button type="button" class="close" (click)="close()" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<button class="btn btn-outline-primary btn-lg btn-block" (click)="selectObject(undefined)">{{'dso-selector.create.community.top-level' | translate}}</button>
|
||||||
|
<h3 class="position-relative py-1 my-3 font-weight-normal">
|
||||||
|
<hr>
|
||||||
|
<div id="create-community-or-separator" class="text-center position-absolute w-100">
|
||||||
|
<span class="px-4 bg-white">or</span>
|
||||||
|
</div>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<h5 class="px-2">{{'dso-selector.create.community.sub-level' | translate}}</h5>
|
||||||
|
<ds-dso-selector [currentDSOId]="dsoRD?.payload.uuid" [types]="selectorTypes" (onSelect)="selectObject($event)"></ds-dso-selector>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,3 @@
|
|||||||
|
#create-community-or-separator {
|
||||||
|
top: 0;
|
||||||
|
}
|
@@ -0,0 +1,14 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import {
|
||||||
|
CreateCommunityParentSelectorComponent as BaseComponent
|
||||||
|
} from '../../../../../../../app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-create-community-parent-selector',
|
||||||
|
// styleUrls: ['./create-community-parent-selector.component.scss'],
|
||||||
|
styleUrls: ['../../../../../../../app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.scss'],
|
||||||
|
// templateUrl: './create-community-parent-selector.component.html',
|
||||||
|
templateUrl: '../../../../../../../app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.html',
|
||||||
|
})
|
||||||
|
export class CreateCommunityParentSelectorComponent extends BaseComponent {
|
||||||
|
}
|
@@ -0,0 +1,15 @@
|
|||||||
|
<div>
|
||||||
|
<div class="modal-header">{{'dso-selector.'+ action + '.' + objectType.toString().toLowerCase() + '.head' | translate}}
|
||||||
|
<button type="button" class="close" (click)="close()" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div [innerHTML]="'dso-selector.create.item.intro' | translate"></div>
|
||||||
|
<h5 *ngIf="header" class="px-2">{{header | translate}}</h5>
|
||||||
|
<ds-authorized-collection-selector [currentDSOId]="dsoRD?.payload.uuid"
|
||||||
|
[entityType]="entityType"
|
||||||
|
[types]="selectorTypes"
|
||||||
|
(onSelect)="selectObject($event)"></ds-authorized-collection-selector>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,13 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {
|
||||||
|
CreateItemParentSelectorComponent as BaseComponent
|
||||||
|
} from '../../../../../../../app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-create-item-parent-selector',
|
||||||
|
// styleUrls: ['./create-item-parent-selector.component.scss'],
|
||||||
|
// templateUrl: './create-item-parent-selector.component.html',
|
||||||
|
templateUrl: '../../../../../../../app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.html',
|
||||||
|
})
|
||||||
|
export class CreateItemParentSelectorComponent extends BaseComponent {
|
||||||
|
}
|
@@ -0,0 +1,11 @@
|
|||||||
|
<div>
|
||||||
|
<div class="modal-header">{{'dso-selector.'+ action + '.' + objectType.toString().toLowerCase() + '.head' | translate}}
|
||||||
|
<button type="button" class="close" (click)="close()" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<h5 *ngIf="header" class="px-2">{{header | translate}}</h5>
|
||||||
|
<ds-dso-selector [currentDSOId]="dsoRD?.payload.uuid" [types]="selectorTypes" (onSelect)="selectObject($event)"></ds-dso-selector>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,13 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import {
|
||||||
|
EditCollectionSelectorComponent as BaseComponent
|
||||||
|
} from '../../../../../../../app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-edit-collection-selector',
|
||||||
|
// styleUrls: ['./edit-collection-selector.component.scss'],
|
||||||
|
// templateUrl: './edit-collection-selector.component.html',
|
||||||
|
templateUrl: '../../../../../../../app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html',
|
||||||
|
})
|
||||||
|
export class EditCollectionSelectorComponent extends BaseComponent {
|
||||||
|
}
|
@@ -0,0 +1,11 @@
|
|||||||
|
<div>
|
||||||
|
<div class="modal-header">{{'dso-selector.'+ action + '.' + objectType.toString().toLowerCase() + '.head' | translate}}
|
||||||
|
<button type="button" class="close" (click)="close()" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<h5 *ngIf="header" class="px-2">{{header | translate}}</h5>
|
||||||
|
<ds-dso-selector [currentDSOId]="dsoRD?.payload.uuid" [types]="selectorTypes" (onSelect)="selectObject($event)"></ds-dso-selector>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,13 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import {
|
||||||
|
EditCommunitySelectorComponent as BaseComponent
|
||||||
|
} from '../../../../../../../app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-edit-item-selector',
|
||||||
|
// styleUrls: ['./edit-community-selector.component.scss'],
|
||||||
|
// templateUrl: './edit-community-selector.component.html',
|
||||||
|
templateUrl: '../../../../../../../app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html',
|
||||||
|
})
|
||||||
|
export class EditCommunitySelectorComponent extends BaseComponent {
|
||||||
|
}
|
@@ -0,0 +1,11 @@
|
|||||||
|
<div>
|
||||||
|
<div class="modal-header">{{'dso-selector.'+ action + '.' + objectType.toString().toLowerCase() + '.head' | translate}}
|
||||||
|
<button type="button" class="close" (click)="close()" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<h5 *ngIf="header" class="px-2">{{header | translate}}</h5>
|
||||||
|
<ds-dso-selector [currentDSOId]="dsoRD?.payload.uuid" [types]="selectorTypes" (onSelect)="selectObject($event)"></ds-dso-selector>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,13 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import {
|
||||||
|
EditItemSelectorComponent as BaseComponent
|
||||||
|
} from 'src/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-edit-item-selector',
|
||||||
|
// styleUrls: ['./edit-item-selector.component.scss'],
|
||||||
|
// templateUrl: './edit-item-selector.component.html',
|
||||||
|
templateUrl: '../../../../../../../app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html',
|
||||||
|
})
|
||||||
|
export class EditItemSelectorComponent extends BaseComponent {
|
||||||
|
}
|
@@ -21,6 +21,24 @@ import {
|
|||||||
} from './app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component';
|
} from './app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component';
|
||||||
import { UntypedItemComponent } from './app/item-page/simple/item-types/untyped-item/untyped-item.component';
|
import { UntypedItemComponent } from './app/item-page/simple/item-types/untyped-item/untyped-item.component';
|
||||||
import { ItemSharedModule } from '../../app/item-page/item-shared.module';
|
import { ItemSharedModule } from '../../app/item-page/item-shared.module';
|
||||||
|
import {
|
||||||
|
CreateCollectionParentSelectorComponent
|
||||||
|
} from './app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component';
|
||||||
|
import {
|
||||||
|
CreateCommunityParentSelectorComponent
|
||||||
|
} from './app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component';
|
||||||
|
import {
|
||||||
|
CreateItemParentSelectorComponent
|
||||||
|
} from './app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
|
||||||
|
import {
|
||||||
|
EditCollectionSelectorComponent
|
||||||
|
} from './app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component';
|
||||||
|
import {
|
||||||
|
EditCommunitySelectorComponent
|
||||||
|
} from './app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component';
|
||||||
|
import {
|
||||||
|
EditItemSelectorComponent
|
||||||
|
} from './app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add components that use a custom decorator to ENTRY_COMPONENTS as well as DECLARATIONS.
|
* Add components that use a custom decorator to ENTRY_COMPONENTS as well as DECLARATIONS.
|
||||||
@@ -41,6 +59,12 @@ const DECLARATIONS = [
|
|||||||
HeaderNavbarWrapperComponent,
|
HeaderNavbarWrapperComponent,
|
||||||
NavbarComponent,
|
NavbarComponent,
|
||||||
FooterComponent,
|
FooterComponent,
|
||||||
|
CreateCollectionParentSelectorComponent,
|
||||||
|
CreateCommunityParentSelectorComponent,
|
||||||
|
CreateItemParentSelectorComponent,
|
||||||
|
EditCollectionSelectorComponent,
|
||||||
|
EditCommunitySelectorComponent,
|
||||||
|
EditItemSelectorComponent,
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@@ -4,11 +4,9 @@
|
|||||||
@import '../../../styles/_variables.scss';
|
@import '../../../styles/_variables.scss';
|
||||||
@import '../../../styles/_mixins.scss';
|
@import '../../../styles/_mixins.scss';
|
||||||
@import '../../../styles/helpers/font_awesome_imports.scss';
|
@import '../../../styles/helpers/font_awesome_imports.scss';
|
||||||
@import '../../../../node_modules/bootstrap/scss/bootstrap.scss';
|
@import '../../../styles/_vendor.scss';
|
||||||
@import '../../../../node_modules/nouislider/distribute/nouislider.min';
|
|
||||||
@import '../../../styles/_custom_variables.scss';
|
@import '../../../styles/_custom_variables.scss';
|
||||||
@import './_theme_css_variable_overrides.scss';
|
@import './_theme_css_variable_overrides.scss';
|
||||||
@import '../../../styles/bootstrap_variables_mapping.scss';
|
@import '../../../styles/bootstrap_variables_mapping.scss';
|
||||||
@import '../../../styles/_truncatable-part.component.scss';
|
@import '../../../styles/_truncatable-part.component.scss';
|
||||||
@import './_global-styles.scss';
|
@import './_global-styles.scss';
|
||||||
@import '../../../../node_modules/ngx-ui-switch/ui-switch.component.scss';
|
|
||||||
|
@@ -4,11 +4,9 @@
|
|||||||
@import '../../../styles/_variables.scss';
|
@import '../../../styles/_variables.scss';
|
||||||
@import '../../../styles/_mixins.scss';
|
@import '../../../styles/_mixins.scss';
|
||||||
@import '../../../styles/helpers/font_awesome_imports.scss';
|
@import '../../../styles/helpers/font_awesome_imports.scss';
|
||||||
@import '../../../../node_modules/bootstrap/scss/bootstrap.scss';
|
@import '../../../styles/_vendor.scss';
|
||||||
@import '../../../../node_modules/nouislider/distribute/nouislider.min';
|
|
||||||
@import '../../../styles/_custom_variables.scss';
|
@import '../../../styles/_custom_variables.scss';
|
||||||
@import './_theme_css_variable_overrides.scss';
|
@import './_theme_css_variable_overrides.scss';
|
||||||
@import '../../../styles/bootstrap_variables_mapping.scss';
|
@import '../../../styles/bootstrap_variables_mapping.scss';
|
||||||
@import '../../../styles/_truncatable-part.component.scss';
|
@import '../../../styles/_truncatable-part.component.scss';
|
||||||
@import './_global-styles.scss';
|
@import './_global-styles.scss';
|
||||||
@import '../../../../node_modules/ngx-ui-switch/ui-switch.component.scss';
|
|
||||||
|
@@ -8741,10 +8741,10 @@ mkdirp@^1.0.3, mkdirp@^1.0.4:
|
|||||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
||||||
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
|
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
|
||||||
|
|
||||||
moment@^2.29.2:
|
moment@^2.29.4:
|
||||||
version "2.29.2"
|
version "2.29.4"
|
||||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.2.tgz#00910c60b20843bcba52d37d58c628b47b1f20e4"
|
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
|
||||||
integrity sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==
|
integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
|
||||||
|
|
||||||
morgan@^1.10.0:
|
morgan@^1.10.0:
|
||||||
version "1.10.0"
|
version "1.10.0"
|
||||||
|
Reference in New Issue
Block a user