diff --git a/src/app/root/root.component.spec.ts b/src/app/root/root.component.spec.ts index 504bc34e344..ab148b8ebd3 100644 --- a/src/app/root/root.component.spec.ts +++ b/src/app/root/root.component.spec.ts @@ -17,7 +17,7 @@ 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 { CSSVariableService } from '../shared/sass-helper/sass-helper.service'; +import { CSSVariableService } from '../shared/sass-helper/css-variable.service'; import { CSSVariableServiceStub } from '../shared/testing/css-variable-service.stub'; import { HostWindowService } from '../shared/host-window.service'; import { HostWindowServiceStub } from '../shared/testing/host-window-service.stub'; diff --git a/src/app/root/root.component.ts b/src/app/root/root.component.ts index c265cec0348..2bbeec6282b 100644 --- a/src/app/root/root.component.ts +++ b/src/app/root/root.component.ts @@ -10,7 +10,7 @@ import { MetadataService } from '../core/metadata/metadata.service'; import { HostWindowState } from '../shared/search/host-window.reducer'; import { NativeWindowRef, NativeWindowService } from '../core/services/window.service'; import { AuthService } from '../core/auth/auth.service'; -import { CSSVariableService } from '../shared/sass-helper/sass-helper.service'; +import { CSSVariableService } from '../shared/sass-helper/css-variable.service'; import { MenuService } from '../shared/menu/menu.service'; import { HostWindowService } from '../shared/host-window.service'; import { ThemeConfig } from '../../config/theme.model'; @@ -63,8 +63,8 @@ export class RootComponent implements OnInit { ngOnInit() { this.sidebarVisible = this.menuService.isMenuVisibleWithVisibleSections(MenuID.ADMIN); - this.collapsedSidebarWidth = this.cssService.getVariable('collapsedSidebarWidth'); - this.totalSidebarWidth = this.cssService.getVariable('totalSidebarWidth'); + this.collapsedSidebarWidth = this.cssService.getVariable('--ds-collapsed-sidebar-width'); + this.totalSidebarWidth = this.cssService.getVariable('--ds-total-sidebar-width'); const sidebarCollapsed = this.menuService.isMenuCollapsed(MenuID.ADMIN); this.slideSidebarOver = combineLatestObservable([sidebarCollapsed, this.windowService.isXsOrSm()]) diff --git a/src/app/search-page/search-page.module.ts b/src/app/search-page/search-page.module.ts index 758eca15c09..19fd9bd309d 100644 --- a/src/app/search-page/search-page.module.ts +++ b/src/app/search-page/search-page.module.ts @@ -7,7 +7,6 @@ import { ConfigurationSearchPageGuard } from './configuration-search-page.guard' import { SearchTrackerComponent } from './search-tracker.component'; import { StatisticsModule } from '../statistics/statistics.module'; import { SearchPageComponent } from './search-page.component'; -import { SidebarFilterService } from '../shared/sidebar/filter/sidebar-filter.service'; import { SearchFilterService } from '../core/shared/search/search-filter.service'; import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; import { JournalEntitiesModule } from '../entity-groups/journal-entities/journal-entities.module'; @@ -34,7 +33,6 @@ const components = [ declarations: components, providers: [ SidebarService, - SidebarFilterService, SearchFilterService, ConfigurationSearchPageGuard, SearchConfigurationService diff --git a/src/app/shared/animations/slide.ts b/src/app/shared/animations/slide.ts index b396333fb41..310ddbbfde9 100644 --- a/src/app/shared/animations/slide.ts +++ b/src/app/shared/animations/slide.ts @@ -12,7 +12,7 @@ export const slide = trigger('slide', [ export const slideMobileNav = trigger('slideMobileNav', [ - state('expanded', style({ height: '100vh' })), + state('expanded', style({ height: 'auto', 'min-height': '100vh' })), state('collapsed', style({ height: 0 })), diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.html b/src/app/shared/auth-nav-menu/auth-nav-menu.component.html index c2b414b6f39..94cbd4368a4 100644 --- a/src/app/shared/auth-nav-menu/auth-nav-menu.component.html +++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.html @@ -2,11 +2,11 @@ diff --git a/src/app/shared/auth-nav-menu/user-menu/user-menu.component.html b/src/app/shared/auth-nav-menu/user-menu/user-menu.component.html index 736d39d318a..e730b0d85c7 100644 --- a/src/app/shared/auth-nav-menu/user-menu/user-menu.component.html +++ b/src/app/shared/auth-nav-menu/user-menu/user-menu.component.html @@ -1,10 +1,13 @@
- {{(user$ | async)?.name}} ({{(user$ | async)?.email}}) - {{'nav.profile' | translate}} - {{'nav.mydspace' | translate}} + + {{(user$ | async)?.name}}
+ {{(user$ | async)?.email}} +
+ {{'nav.profile' | translate}} + {{'nav.mydspace' | translate}} - +
diff --git a/src/app/shared/auth-nav-menu/user-menu/user-menu.component.spec.ts b/src/app/shared/auth-nav-menu/user-menu/user-menu.component.spec.ts index 983fe68274b..5576b942b30 100644 --- a/src/app/shared/auth-nav-menu/user-menu/user-menu.component.spec.ts +++ b/src/app/shared/auth-nav-menu/user-menu/user-menu.component.spec.ts @@ -162,10 +162,24 @@ describe('UserMenuComponent', () => { }); it('should display user name and email', () => { - const user = 'User Test (test@test.com)'; + const username = 'User Test'; + const email = 'test@test.com'; const span = deUserMenu.query(By.css('.dropdown-item-text')); expect(span).toBeDefined(); - expect(span.nativeElement.innerHTML).toBe(user); + expect(span.nativeElement.innerHTML).toContain(username); + expect(span.nativeElement.innerHTML).toContain(email); + }); + + it('should create logout component', () => { + const components = fixture.debugElement.query(By.css('[data-test="log-out-component"]')); + expect(components).toBeTruthy(); + }); + + it('should not create logout component', () => { + component.inExpandableNavbar = true; + fixture.detectChanges(); + const components = fixture.debugElement.query(By.css('[data-test="log-out-component"]')); + expect(components).toBeFalsy(); }); }); diff --git a/src/app/shared/auth-nav-menu/user-menu/user-menu.component.ts b/src/app/shared/auth-nav-menu/user-menu/user-menu.component.ts index aa78be97498..22b076c31a5 100644 --- a/src/app/shared/auth-nav-menu/user-menu/user-menu.component.ts +++ b/src/app/shared/auth-nav-menu/user-menu/user-menu.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, Input, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { select, Store } from '@ngrx/store'; @@ -20,6 +20,11 @@ import { getProfileModuleRoute } from '../../../app-routing-paths'; }) export class UserMenuComponent implements OnInit { + /** + * The input flag to show user details in navbar expandable menu + */ + @Input() inExpandableNavbar = false; + /** * True if the authentication is loading. * @type {Observable} diff --git a/src/app/shared/collection-dropdown/themed-collection-dropdown.component.ts b/src/app/shared/collection-dropdown/themed-collection-dropdown.component.ts new file mode 100644 index 00000000000..27c883099d3 --- /dev/null +++ b/src/app/shared/collection-dropdown/themed-collection-dropdown.component.ts @@ -0,0 +1,33 @@ +import { CollectionDropdownComponent, CollectionListEntry } from './collection-dropdown.component'; +import { ThemedComponent } from '../theme-support/themed.component'; +import { Component, Input, Output, EventEmitter } from '@angular/core'; + +@Component({ + selector: 'ds-themed-collection-dropdown', + styleUrls: [], + templateUrl: '../../shared/theme-support/themed.component.html', +}) +export class ThemedCollectionDropdownComponent extends ThemedComponent { + + @Input() entityType: string; + + @Output() searchComplete = new EventEmitter(); + + @Output() theOnlySelectable = new EventEmitter(); + + @Output() selectionChange = new EventEmitter(); + + protected inAndOutputNames: (keyof CollectionDropdownComponent & keyof this)[] = ['entityType', 'searchComplete', 'theOnlySelectable', 'selectionChange']; + + protected getComponentName(): string { + return 'CollectionDropdownComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../themes/${themeName}/app/shared/collection-dropdown/collection-dropdown.component`); + } + + protected importUnthemedComponent(): Promise { + return import(`./collection-dropdown.component`); + } +} diff --git a/src/app/shared/comcol/comcol-forms/comcol-form/comcol-form.component.ts b/src/app/shared/comcol/comcol-forms/comcol-form/comcol-form.component.ts index 29be240753f..23dfca86165 100644 --- a/src/app/shared/comcol/comcol-forms/comcol-form/comcol-form.component.ts +++ b/src/app/shared/comcol/comcol-forms/comcol-form/comcol-form.component.ts @@ -17,8 +17,8 @@ import { MetadataMap, MetadataValue } from '../../../../core/shared/metadata.mod import { ResourceType } from '../../../../core/shared/resource-type'; import { hasValue, isNotEmpty } from '../../../empty.util'; import { NotificationsService } from '../../../notifications/notifications.service'; -import { UploaderOptions } from '../../../uploader/uploader-options.model'; -import { UploaderComponent } from '../../../uploader/uploader.component'; +import { UploaderOptions } from '../../../upload/uploader/uploader-options.model'; +import { UploaderComponent } from '../../../upload/uploader/uploader.component'; import { Operation } from 'fast-json-patch'; import { NoContent } from '../../../../core/shared/NoContent.model'; import { getFirstCompletedRemoteData } from '../../../../core/shared/operators'; diff --git a/src/app/shared/comcol/comcol-forms/delete-comcol-page/delete-comcol-page.component.spec.ts b/src/app/shared/comcol/comcol-forms/delete-comcol-page/delete-comcol-page.component.spec.ts index bc73e4134b5..1040e31c579 100644 --- a/src/app/shared/comcol/comcol-forms/delete-comcol-page/delete-comcol-page.component.spec.ts +++ b/src/app/shared/comcol/comcol-forms/delete-comcol-page/delete-comcol-page.component.spec.ts @@ -11,7 +11,6 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { DeleteComColPageComponent } from './delete-comcol-page.component'; import { NotificationsService } from '../../../notifications/notifications.service'; import { NotificationsServiceStub } from '../../../testing/notifications-service.stub'; -import { RequestService } from '../../../../core/data/request.service'; import { getTestScheduler } from 'jasmine-marbles'; import { ComColDataService } from '../../../../core/data/comcol-data.service'; import { createFailedRemoteDataObject$, createNoContentRemoteDataObject$ } from '../../../remote-data.utils'; diff --git a/src/app/shared/comcol/comcol.module.ts b/src/app/shared/comcol/comcol.module.ts index 094387929ae..efbcedf2c67 100644 --- a/src/app/shared/comcol/comcol.module.ts +++ b/src/app/shared/comcol/comcol.module.ts @@ -15,6 +15,7 @@ import { ThemedComcolPageBrowseByComponent } from './comcol-page-browse-by/theme import { ComcolRoleComponent } from './comcol-forms/edit-comcol-page/comcol-role/comcol-role.component'; import { SharedModule } from '../shared.module'; import { FormModule } from '../form/form.module'; +import { UploadModule } from '../upload/upload.module'; const COMPONENTS = [ ComcolPageContentComponent, @@ -28,9 +29,7 @@ const COMPONENTS = [ ComcolPageBrowseByComponent, ThemedComcolPageBrowseByComponent, ComcolRoleComponent, - ThemedComcolPageHandleComponent - ]; @NgModule({ @@ -40,10 +39,12 @@ const COMPONENTS = [ imports: [ CommonModule, FormModule, - SharedModule + SharedModule, + UploadModule, ], exports: [ - ...COMPONENTS + ...COMPONENTS, + UploadModule, ] }) export class ComcolModule { } diff --git a/src/app/shared/cookies/browser-klaro.service.spec.ts b/src/app/shared/cookies/browser-klaro.service.spec.ts index 9db9caf3641..7fd72b54b37 100644 --- a/src/app/shared/cookies/browser-klaro.service.spec.ts +++ b/src/app/shared/cookies/browser-klaro.service.spec.ts @@ -10,7 +10,8 @@ import { AuthService } from '../../core/auth/auth.service'; import { CookieService } from '../../core/services/cookie.service'; import { getTestScheduler } from 'jasmine-marbles'; import { MetadataValue } from '../../core/shared/metadata.models'; -import { clone, cloneDeep } from 'lodash'; +import clone from 'lodash/clone'; +import cloneDeep from 'lodash/cloneDeep'; import { ConfigurationDataService } from '../../core/data/configuration-data.service'; import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../remote-data.utils'; import { ConfigurationProperty } from '../../core/shared/configuration-property.model'; @@ -100,7 +101,7 @@ describe('BrowserKlaroService', () => { mockConfig = { translations: { - en: { + zz: { purposes: {}, test: { testeritis: testKey @@ -158,8 +159,8 @@ describe('BrowserKlaroService', () => { it('addAppMessages', () => { service.addAppMessages(); - expect(mockConfig.translations.en[appName]).toBeDefined(); - expect(mockConfig.translations.en.purposes[purpose]).toBeDefined(); + expect(mockConfig.translations.zz[appName]).toBeDefined(); + expect(mockConfig.translations.zz.purposes[purpose]).toBeDefined(); }); it('translateConfiguration', () => { diff --git a/src/app/shared/cookies/browser-klaro.service.ts b/src/app/shared/cookies/browser-klaro.service.ts index c6819012d96..2b09c0bf155 100644 --- a/src/app/shared/cookies/browser-klaro.service.ts +++ b/src/app/shared/cookies/browser-klaro.service.ts @@ -1,5 +1,4 @@ -import { Injectable } from '@angular/core'; -import * as Klaro from 'klaro'; +import { Inject, Injectable, InjectionToken } from '@angular/core'; import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; import { AuthService } from '../../core/auth/auth.service'; import { TranslateService } from '@ngx-translate/core'; @@ -10,7 +9,8 @@ import { KlaroService } from './klaro.service'; import { hasValue, isEmpty, isNotEmpty } from '../empty.util'; import { CookieService } from '../../core/services/cookie.service'; import { EPersonDataService } from '../../core/eperson/eperson-data.service'; -import { cloneDeep, debounce } from 'lodash'; +import cloneDeep from 'lodash/cloneDeep'; +import debounce from 'lodash/debounce'; import { ANONYMOUS_STORAGE_NAME_KLARO, klaroConfiguration } from './klaro-configuration'; import { Operation } from 'fast-json-patch'; import { getFirstCompletedRemoteData } from '../../core/shared/operators'; @@ -42,6 +42,17 @@ const cookiePurposeMessagePrefix = 'cookies.consent.purpose.'; */ const updateDebounce = 300; +/** + * By using this injection token instead of importing directly we can keep Klaro out of the main bundle + */ +const LAZY_KLARO = new InjectionToken>( + 'Lazily loaded Klaro', + { + providedIn: 'root', + factory: async () => (await import('klaro/dist/klaro-no-translations')), + } +); + /** * Browser implementation for the KlaroService, representing a service for handling Klaro consent preferences and UI */ @@ -64,7 +75,9 @@ export class BrowserKlaroService extends KlaroService { private authService: AuthService, private ePersonService: EPersonDataService, private configService: ConfigurationDataService, - private cookieService: CookieService) { + private cookieService: CookieService, + @Inject(LAZY_KLARO) private lazyKlaro: Promise, + ) { super(); } @@ -78,7 +91,7 @@ export class BrowserKlaroService extends KlaroService { initialize() { if (!environment.info.enablePrivacyStatement) { delete this.klaroConfig.privacyPolicy; - this.klaroConfig.translations.en.consentNotice.description = 'cookies.consent.content-notice.description.no-privacy'; + this.klaroConfig.translations.zz.consentNotice.description = 'cookies.consent.content-notice.description.no-privacy'; } const hideGoogleAnalytics$ = this.configService.findByPropertyName(this.GOOGLE_ANALYTICS_KEY).pipe( @@ -102,7 +115,6 @@ export class BrowserKlaroService extends KlaroService { if (hideRegistrationVerification) { servicesToHideArray.push(CAPTCHA_NAME); } - console.log(servicesToHideArray); return servicesToHideArray; }) ); @@ -134,8 +146,7 @@ export class BrowserKlaroService extends KlaroService { this.translateConfiguration(); this.klaroConfig.services = this.filterConfigServices(servicesToHide); - - Klaro.setup(this.klaroConfig); + this.lazyKlaro.then(({ setup }) => setup(this.klaroConfig)); }); } @@ -219,7 +230,7 @@ export class BrowserKlaroService extends KlaroService { * Show the cookie consent form */ showSettings() { - Klaro.show(this.klaroConfig); + this.lazyKlaro.then(({show}) => show(this.klaroConfig)); } /** @@ -227,12 +238,12 @@ export class BrowserKlaroService extends KlaroService { */ addAppMessages() { this.klaroConfig.services.forEach((app) => { - this.klaroConfig.translations.en[app.name] = { + this.klaroConfig.translations.zz[app.name] = { title: this.getTitleTranslation(app.name), description: this.getDescriptionTranslation(app.name) }; app.purposes.forEach((purpose) => { - this.klaroConfig.translations.en.purposes[purpose] = this.getPurposeTranslation(purpose); + this.klaroConfig.translations.zz.purposes[purpose] = this.getPurposeTranslation(purpose); }); }); } @@ -246,7 +257,7 @@ export class BrowserKlaroService extends KlaroService { */ this.translateService.setDefaultLang(environment.defaultLanguage); - this.translate(this.klaroConfig.translations.en); + this.translate(this.klaroConfig.translations.zz); } /** diff --git a/src/app/shared/cookies/klaro-configuration.ts b/src/app/shared/cookies/klaro-configuration.ts index 8a9855bd89a..a41b641dec1 100644 --- a/src/app/shared/cookies/klaro-configuration.ts +++ b/src/app/shared/cookies/klaro-configuration.ts @@ -54,10 +54,46 @@ export const klaroConfiguration: any = { https://github.com/KIProtect/klaro/tree/master/src/translations */ translations: { - en: { + /* + The `zz` key contains default translations that will be used as fallback values. + This can e.g. be useful for defining a fallback privacy policy URL. + FOR DSPACE: We use 'zz' to map to our own i18n translations for klaro, see + translateConfiguration() in browser-klaro.service.ts. All the below i18n keys are specified + in your /src/assets/i18n/*.json5 translation pack. + */ + zz: { acceptAll: 'cookies.consent.accept-all', acceptSelected: 'cookies.consent.accept-selected', - app: { + close: 'cookies.consent.close', + consentModal: { + title: 'cookies.consent.content-modal.title', + description: 'cookies.consent.content-modal.description' + }, + consentNotice: { + changeDescription: 'cookies.consent.update', + title: 'cookies.consent.content-notice.title', + description: 'cookies.consent.content-notice.description', + learnMore: 'cookies.consent.content-notice.learnMore', + }, + decline: 'cookies.consent.decline', + ok: 'cookies.consent.ok', + poweredBy: 'Powered by Klaro!', + privacyPolicy: { + name: 'cookies.consent.content-modal.privacy-policy.name', + text: 'cookies.consent.content-modal.privacy-policy.text' + }, + purposeItem: { + service: 'cookies.consent.content-modal.service', + services: 'cookies.consent.content-modal.services' + }, + purposes: { + }, + save: 'cookies.consent.save', + service: { + disableAll: { + description: 'cookies.consent.app.disable-all.description', + title: 'cookies.consent.app.disable-all.title' + }, optOut: { description: 'cookies.consent.app.opt-out.description', title: 'cookies.consent.app.opt-out.title' @@ -65,26 +101,10 @@ export const klaroConfiguration: any = { purpose: 'cookies.consent.app.purpose', purposes: 'cookies.consent.app.purposes', required: { - description: 'cookies.consent.app.required.description', - title: 'cookies.consent.app.required.title' + title: 'cookies.consent.app.required.title', + description: 'cookies.consent.app.required.description' } - }, - close: 'cookies.consent.close', - decline: 'cookies.consent.decline', - changeDescription: 'cookies.consent.update', - consentNotice: { - description: 'cookies.consent.content-notice.description', - learnMore: 'cookies.consent.content-notice.learnMore' - }, - consentModal: { - description: 'cookies.consent.content-modal.description', - privacyPolicy: { - name: 'cookies.consent.content-modal.privacy-policy.name', - text: 'cookies.consent.content-modal.privacy-policy.text' - }, - title: 'cookies.consent.content-modal.title' - }, - purposes: {} + } } }, services: [ diff --git a/src/app/shared/date.util.spec.ts b/src/app/shared/date.util.spec.ts new file mode 100644 index 00000000000..4576ea497c5 --- /dev/null +++ b/src/app/shared/date.util.spec.ts @@ -0,0 +1,107 @@ +import { dateToString, dateToNgbDateStruct, dateToISOFormat, isValidDate, yearFromString } from './date.util'; + +describe('Date Utils', () => { + + describe('dateToISOFormat', () => { + it('should convert Date to YYYY-MM-DDThh:mm:ssZ string', () => { + // NOTE: month is zero indexed which is why it increases by one + expect(dateToISOFormat(new Date(Date.UTC(2022, 5, 3)))).toEqual('2022-06-03T00:00:00Z'); + }); + it('should convert Date string to YYYY-MM-DDThh:mm:ssZ string', () => { + expect(dateToISOFormat('2022-06-03')).toEqual('2022-06-03T00:00:00Z'); + }); + it('should convert Month string to YYYY-MM-DDThh:mm:ssZ string', () => { + expect(dateToISOFormat('2022-06')).toEqual('2022-06-01T00:00:00Z'); + }); + it('should convert Year string to YYYY-MM-DDThh:mm:ssZ string', () => { + expect(dateToISOFormat('2022')).toEqual('2022-01-01T00:00:00Z'); + }); + it('should convert ISO Date string to YYYY-MM-DDThh:mm:ssZ string', () => { + // NOTE: Time is always zeroed out as proven by this test. + expect(dateToISOFormat('2022-06-03T03:24:04Z')).toEqual('2022-06-03T00:00:00Z'); + }); + it('should convert NgbDateStruct to YYYY-MM-DDThh:mm:ssZ string', () => { + // NOTE: month is zero indexed which is why it increases by one + const date = new Date(Date.UTC(2022, 5, 3)); + expect(dateToISOFormat(dateToNgbDateStruct(date))).toEqual('2022-06-03T00:00:00Z'); + }); + }); + + describe('dateToString', () => { + it('should convert Date to YYYY-MM-DD string', () => { + // NOTE: month is zero indexed which is why it increases by one + expect(dateToString(new Date(Date.UTC(2022, 5, 3)))).toEqual('2022-06-03'); + }); + it('should convert Date with time to YYYY-MM-DD string', () => { + // NOTE: month is zero indexed which is why it increases by one + expect(dateToString(new Date(Date.UTC(2022, 5, 3, 3, 24, 0)))).toEqual('2022-06-03'); + }); + it('should convert Month only to YYYY-MM-DD string', () => { + // NOTE: month is zero indexed which is why it increases by one + expect(dateToString(new Date(Date.UTC(2022, 5)))).toEqual('2022-06-01'); + }); + it('should convert ISO Date to YYYY-MM-DD string', () => { + expect(dateToString(new Date('2022-06-03T03:24:00Z'))).toEqual('2022-06-03'); + }); + it('should convert NgbDateStruct to YYYY-MM-DD string', () => { + // NOTE: month is zero indexed which is why it increases by one + const date = new Date(Date.UTC(2022, 5, 3)); + expect(dateToString(dateToNgbDateStruct(date))).toEqual('2022-06-03'); + }); + }); + + + describe('isValidDate', () => { + it('should return false for null', () => { + expect(isValidDate(null)).toBe(false); + }); + it('should return false for empty string', () => { + expect(isValidDate('')).toBe(false); + }); + it('should return false for text', () => { + expect(isValidDate('test')).toBe(false); + }); + it('should return true for YYYY', () => { + expect(isValidDate('2022')).toBe(true); + }); + it('should return true for YYYY-MM', () => { + expect(isValidDate('2022-12')).toBe(true); + }); + it('should return true for YYYY-MM-DD', () => { + expect(isValidDate('2022-06-03')).toBe(true); + }); + it('should return true for YYYY-MM-DDTHH:MM:SS', () => { + expect(isValidDate('2022-06-03T10:20:30')).toBe(true); + }); + it('should return true for YYYY-MM-DDTHH:MM:SSZ', () => { + expect(isValidDate('2022-06-03T10:20:30Z')).toBe(true); + }); + it('should return false for a month that does not exist', () => { + expect(isValidDate('2022-13')).toBe(false); + }); + it('should return false for a day that does not exist', () => { + expect(isValidDate('2022-02-60')).toBe(false); + }); + it('should return false for a time that does not exist', () => { + expect(isValidDate('2022-02-60T10:60:20')).toBe(false); + }); + }); + + describe('yearFromString', () => { + it('should return year from YYYY string', () => { + expect(yearFromString('2022')).toEqual(2022); + }); + it('should return year from YYYY-MM string', () => { + expect(yearFromString('1970-06')).toEqual(1970); + }); + it('should return year from YYYY-MM-DD string', () => { + expect(yearFromString('1914-10-23')).toEqual(1914); + }); + it('should return year from YYYY-MM-DDTHH:MM:SSZ string', () => { + expect(yearFromString('1914-10-23T10:20:30Z')).toEqual(1914); + }); + it('should return null if invalid date', () => { + expect(yearFromString('test')).toBeNull(); + }); + }); +}); diff --git a/src/app/shared/date.util.ts b/src/app/shared/date.util.ts index 5f7ccb2438c..5b74ed02d20 100644 --- a/src/app/shared/date.util.ts +++ b/src/app/shared/date.util.ts @@ -1,9 +1,8 @@ import { NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; - -import { isObject } from 'lodash'; -import * as moment from 'moment'; - -import { isNull, isUndefined } from './empty.util'; +import { formatInTimeZone } from 'date-fns-tz'; +import { isValid } from 'date-fns'; +import isObject from 'lodash/isObject'; +import { hasNoValue } from './empty.util'; /** * Returns true if the passed value is a NgbDateStruct. @@ -31,21 +30,7 @@ export function dateToISOFormat(date: Date | NgbDateStruct | string): string { const dateObj: Date = (date instanceof Date) ? date : ((typeof date === 'string') ? ngbDateStructToDate(stringToNgbDateStruct(date)) : ngbDateStructToDate(date)); - let year = dateObj.getUTCFullYear().toString(); - let month = (dateObj.getUTCMonth() + 1).toString(); - let day = dateObj.getUTCDate().toString(); - let hour = dateObj.getHours().toString(); - let min = dateObj.getMinutes().toString(); - let sec = dateObj.getSeconds().toString(); - - year = (year.length === 1) ? '0' + year : year; - month = (month.length === 1) ? '0' + month : month; - day = (day.length === 1) ? '0' + day : day; - hour = (hour.length === 1) ? '0' + hour : hour; - min = (min.length === 1) ? '0' + min : min; - sec = (sec.length === 1) ? '0' + sec : sec; - const dateStr = `${year}${month}${day}${hour}${min}${sec}`; - return moment.utc(dateStr, 'YYYYMMDDhhmmss').format(); + return formatInTimeZone(dateObj, 'UTC', "yyyy-MM-dd'T'HH:mm:ss'Z'"); } /** @@ -81,7 +66,7 @@ export function stringToNgbDateStruct(date: string): NgbDateStruct { * the NgbDateStruct object */ export function dateToNgbDateStruct(date?: Date): NgbDateStruct { - if (isNull(date) || isUndefined(date)) { + if (hasNoValue(date)) { date = new Date(); } @@ -102,16 +87,7 @@ export function dateToNgbDateStruct(date?: Date): NgbDateStruct { */ export function dateToString(date: Date | NgbDateStruct): string { const dateObj: Date = (date instanceof Date) ? date : ngbDateStructToDate(date); - - let year = dateObj.getUTCFullYear().toString(); - let month = (dateObj.getUTCMonth() + 1).toString(); - let day = dateObj.getUTCDate().toString(); - - year = (year.length === 1) ? '0' + year : year; - month = (month.length === 1) ? '0' + month : month; - day = (day.length === 1) ? '0' + day : day; - const dateStr = `${year}-${month}-${day}`; - return moment.utc(dateStr, 'YYYYMMDD').format('YYYY-MM-DD'); + return formatInTimeZone(dateObj, 'UTC', 'yyyy-MM-dd'); } /** @@ -119,5 +95,15 @@ export function dateToString(date: Date | NgbDateStruct): string { * @param date the string to be checked */ export function isValidDate(date: string) { - return moment(date).isValid(); + return (hasNoValue(date)) ? false : isValid(new Date(date)); } + +/** + * Parse given date string to a year number based on expected formats + * @param date the string to be parsed + * @param formats possible formats the string may align with. MUST be valid date-fns formats + */ +export function yearFromString(date: string) { + return isValidDate(date) ? new Date(date).getUTCFullYear() : null; +} + diff --git a/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts b/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts index 7312c7fe152..58c9ecf5d94 100644 --- a/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts +++ b/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts @@ -53,8 +53,9 @@ export class AuthorizedCollectionSelectorComponent extends DSOSelectorComponent * Perform a search for authorized collections with the current query and page * @param query Query to search objects for * @param page Page to retrieve + * @param useCache Whether or not to use the cache */ - search(query: string, page: number): Observable>>> { + search(query: string, page: number, useCache: boolean = true): Observable>>> { let searchListService$: Observable>> = null; const findOptions: FindListOptions = { currentPage: page, @@ -69,7 +70,7 @@ export class AuthorizedCollectionSelectorComponent extends DSOSelectorComponent findOptions); } else { searchListService$ = this.collectionDataService - .getAuthorizedCollection(query, findOptions, true, false, followLink('parentCommunity')); + .getAuthorizedCollection(query, findOptions, useCache, false, followLink('parentCommunity')); } return searchListService$.pipe( getFirstCompletedRemoteData(), diff --git a/src/app/shared/dso-selector/dso-selector/dso-selector.component.html b/src/app/shared/dso-selector/dso-selector/dso-selector.component.html index 8abb8ad5584..c4f5dbc4cd6 100644 --- a/src/app/shared/dso-selector/dso-selector/dso-selector.component.html +++ b/src/app/shared/dso-selector/dso-selector/dso-selector.component.html @@ -21,12 +21,12 @@ diff --git a/src/app/shared/log-in/methods/shibboleth/log-in-shibboleth.component.scss b/src/app/shared/log-in/methods/log-in-external-provider/log-in-external-provider.component.scss similarity index 100% rename from src/app/shared/log-in/methods/shibboleth/log-in-shibboleth.component.scss rename to src/app/shared/log-in/methods/log-in-external-provider/log-in-external-provider.component.scss diff --git a/src/app/shared/log-in/methods/orcid/log-in-orcid.component.spec.ts b/src/app/shared/log-in/methods/log-in-external-provider/log-in-external-provider.component.spec.ts similarity index 88% rename from src/app/shared/log-in/methods/orcid/log-in-orcid.component.spec.ts rename to src/app/shared/log-in/methods/log-in-external-provider/log-in-external-provider.component.spec.ts index 001f0a4959d..de4f62eb9eb 100644 --- a/src/app/shared/log-in/methods/orcid/log-in-orcid.component.spec.ts +++ b/src/app/shared/log-in/methods/log-in-external-provider/log-in-external-provider.component.spec.ts @@ -14,18 +14,17 @@ import { AuthServiceStub } from '../../../testing/auth-service.stub'; import { storeModuleConfig } from '../../../../app.reducer'; import { AuthMethod } from '../../../../core/auth/models/auth.method'; import { AuthMethodType } from '../../../../core/auth/models/auth.method-type'; -import { LogInOrcidComponent } from './log-in-orcid.component'; +import { LogInExternalProviderComponent } from './log-in-external-provider.component'; import { NativeWindowService } from '../../../../core/services/window.service'; import { RouterStub } from '../../../testing/router.stub'; import { ActivatedRouteStub } from '../../../testing/active-router.stub'; import { NativeWindowMockFactory } from '../../../mocks/mock-native-window-ref'; import { HardRedirectService } from '../../../../core/services/hard-redirect.service'; +describe('LogInExternalProviderComponent', () => { -describe('LogInOrcidComponent', () => { - - let component: LogInOrcidComponent; - let fixture: ComponentFixture; + let component: LogInExternalProviderComponent; + let fixture: ComponentFixture; let page: Page; let user: EPerson; let componentAsAny: any; @@ -66,7 +65,7 @@ describe('LogInOrcidComponent', () => { TranslateModule.forRoot() ], declarations: [ - LogInOrcidComponent + LogInExternalProviderComponent ], providers: [ { provide: AuthService, useClass: AuthServiceStub }, @@ -88,7 +87,7 @@ describe('LogInOrcidComponent', () => { beforeEach(() => { // create component and test fixture - fixture = TestBed.createComponent(LogInOrcidComponent); + fixture = TestBed.createComponent(LogInExternalProviderComponent); // get test component from the fixture component = fixture.componentInstance; @@ -109,7 +108,7 @@ describe('LogInOrcidComponent', () => { expect(componentAsAny.injectedAuthMethodModel.location).toBe(location); expect(componentAsAny._window.nativeWindow.location.href).toBe(currentUrl); - component.redirectToOrcid(); + component.redirectToExternalProvider(); expect(setHrefSpy).toHaveBeenCalledWith(currentUrl); @@ -124,7 +123,7 @@ describe('LogInOrcidComponent', () => { expect(componentAsAny.injectedAuthMethodModel.location).toBe(location); expect(componentAsAny._window.nativeWindow.location.href).toBe(currentUrl); - component.redirectToOrcid(); + component.redirectToExternalProvider(); expect(setHrefSpy).toHaveBeenCalledWith(currentUrl); @@ -143,7 +142,7 @@ class Page { public navigateSpy: jasmine.Spy; public passwordInput: HTMLInputElement; - constructor(private component: LogInOrcidComponent, private fixture: ComponentFixture) { + constructor(private component: LogInExternalProviderComponent, private fixture: ComponentFixture) { // use injector to get services const injector = fixture.debugElement.injector; const store = injector.get(Store); diff --git a/src/app/shared/log-in/methods/log-in-external-provider.component.ts b/src/app/shared/log-in/methods/log-in-external-provider/log-in-external-provider.component.ts similarity index 73% rename from src/app/shared/log-in/methods/log-in-external-provider.component.ts rename to src/app/shared/log-in/methods/log-in-external-provider/log-in-external-provider.component.ts index 037fc40e905..f1829684575 100644 --- a/src/app/shared/log-in/methods/log-in-external-provider.component.ts +++ b/src/app/shared/log-in/methods/log-in-external-provider/log-in-external-provider.component.ts @@ -4,22 +4,27 @@ import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; import { select, Store } from '@ngrx/store'; -import { AuthMethod } from '../../../core/auth/models/auth.method'; - -import { isAuthenticated, isAuthenticationLoading } from '../../../core/auth/selectors'; -import { NativeWindowRef, NativeWindowService } from '../../../core/services/window.service'; -import { isEmpty, isNotNull } from '../../empty.util'; -import { AuthService } from '../../../core/auth/auth.service'; -import { HardRedirectService } from '../../../core/services/hard-redirect.service'; -import { URLCombiner } from '../../../core/url-combiner/url-combiner'; -import { CoreState } from '../../../core/core-state.model'; +import { AuthMethod } from '../../../../core/auth/models/auth.method'; + +import { isAuthenticated, isAuthenticationLoading } from '../../../../core/auth/selectors'; +import { NativeWindowRef, NativeWindowService } from '../../../../core/services/window.service'; +import { isEmpty, isNotNull } from '../../../empty.util'; +import { AuthService } from '../../../../core/auth/auth.service'; +import { HardRedirectService } from '../../../../core/services/hard-redirect.service'; +import { URLCombiner } from '../../../../core/url-combiner/url-combiner'; +import { CoreState } from '../../../../core/core-state.model'; +import { renderAuthMethodFor } from '../log-in.methods-decorator'; +import { AuthMethodType } from '../../../../core/auth/models/auth.method-type'; @Component({ selector: 'ds-log-in-external-provider', - template: '' - + templateUrl: './log-in-external-provider.component.html', + styleUrls: ['./log-in-external-provider.component.scss'] }) -export abstract class LogInExternalProviderComponent implements OnInit { +@renderAuthMethodFor(AuthMethodType.Oidc) +@renderAuthMethodFor(AuthMethodType.Shibboleth) +@renderAuthMethodFor(AuthMethodType.Orcid) +export class LogInExternalProviderComponent implements OnInit { /** * The authentication method data. @@ -107,4 +112,7 @@ export abstract class LogInExternalProviderComponent implements OnInit { } + getButtonLabel() { + return `login.form.${this.authMethod.authMethodType}`; + } } diff --git a/src/app/shared/log-in/methods/oidc/log-in-oidc.component.html b/src/app/shared/log-in/methods/oidc/log-in-oidc.component.html deleted file mode 100644 index 7e78834305b..00000000000 --- a/src/app/shared/log-in/methods/oidc/log-in-oidc.component.html +++ /dev/null @@ -1,3 +0,0 @@ - \ No newline at end of file diff --git a/src/app/shared/log-in/methods/oidc/log-in-oidc.component.spec.ts b/src/app/shared/log-in/methods/oidc/log-in-oidc.component.spec.ts deleted file mode 100644 index 078a58dd5a7..00000000000 --- a/src/app/shared/log-in/methods/oidc/log-in-oidc.component.spec.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; - -import { provideMockStore } from '@ngrx/store/testing'; -import { Store, StoreModule } from '@ngrx/store'; -import { TranslateModule } from '@ngx-translate/core'; - -import { EPerson } from '../../../../core/eperson/models/eperson.model'; -import { EPersonMock } from '../../../testing/eperson.mock'; -import { authReducer } from '../../../../core/auth/auth.reducer'; -import { AuthService } from '../../../../core/auth/auth.service'; -import { AuthServiceStub } from '../../../testing/auth-service.stub'; -import { storeModuleConfig } from '../../../../app.reducer'; -import { AuthMethod } from '../../../../core/auth/models/auth.method'; -import { AuthMethodType } from '../../../../core/auth/models/auth.method-type'; -import { LogInOidcComponent } from './log-in-oidc.component'; -import { NativeWindowService } from '../../../../core/services/window.service'; -import { RouterStub } from '../../../testing/router.stub'; -import { ActivatedRouteStub } from '../../../testing/active-router.stub'; -import { NativeWindowMockFactory } from '../../../mocks/mock-native-window-ref'; -import { HardRedirectService } from '../../../../core/services/hard-redirect.service'; - - -describe('LogInOidcComponent', () => { - - let component: LogInOidcComponent; - let fixture: ComponentFixture; - let page: Page; - let user: EPerson; - let componentAsAny: any; - let setHrefSpy; - let oidcBaseUrl; - let location; - let initialState: any; - let hardRedirectService: HardRedirectService; - - beforeEach(() => { - user = EPersonMock; - oidcBaseUrl = 'dspace-rest.test/oidc?redirectUrl='; - location = oidcBaseUrl + 'http://dspace-angular.test/home'; - - hardRedirectService = jasmine.createSpyObj('hardRedirectService', { - getCurrentRoute: {}, - redirect: {} - }); - - initialState = { - core: { - auth: { - authenticated: false, - loaded: false, - blocking: false, - loading: false, - authMethods: [] - } - } - }; - }); - - beforeEach(waitForAsync(() => { - // refine the test module by declaring the test component - TestBed.configureTestingModule({ - imports: [ - StoreModule.forRoot({ auth: authReducer }, storeModuleConfig), - TranslateModule.forRoot() - ], - declarations: [ - LogInOidcComponent - ], - providers: [ - { provide: AuthService, useClass: AuthServiceStub }, - { provide: 'authMethodProvider', useValue: new AuthMethod(AuthMethodType.Oidc, location) }, - { provide: 'isStandalonePage', useValue: true }, - { provide: NativeWindowService, useFactory: NativeWindowMockFactory }, - { provide: Router, useValue: new RouterStub() }, - { provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, - { provide: HardRedirectService, useValue: hardRedirectService }, - provideMockStore({ initialState }), - ], - schemas: [ - CUSTOM_ELEMENTS_SCHEMA - ] - }) - .compileComponents(); - - })); - - beforeEach(() => { - // create component and test fixture - fixture = TestBed.createComponent(LogInOidcComponent); - - // get test component from the fixture - component = fixture.componentInstance; - componentAsAny = component; - - // create page - page = new Page(component, fixture); - setHrefSpy = spyOnProperty(componentAsAny._window.nativeWindow.location, 'href', 'set').and.callThrough(); - - }); - - it('should set the properly a new redirectUrl', () => { - const currentUrl = 'http://dspace-angular.test/collections/12345'; - componentAsAny._window.nativeWindow.location.href = currentUrl; - - fixture.detectChanges(); - - expect(componentAsAny.injectedAuthMethodModel.location).toBe(location); - expect(componentAsAny._window.nativeWindow.location.href).toBe(currentUrl); - - component.redirectToOidc(); - - expect(setHrefSpy).toHaveBeenCalledWith(currentUrl); - - }); - - it('should not set a new redirectUrl', () => { - const currentUrl = 'http://dspace-angular.test/home'; - componentAsAny._window.nativeWindow.location.href = currentUrl; - - fixture.detectChanges(); - - expect(componentAsAny.injectedAuthMethodModel.location).toBe(location); - expect(componentAsAny._window.nativeWindow.location.href).toBe(currentUrl); - - component.redirectToOidc(); - - expect(setHrefSpy).toHaveBeenCalledWith(currentUrl); - - }); - -}); - -/** - * I represent the DOM elements and attach spies. - * - * @class Page - */ -class Page { - - public emailInput: HTMLInputElement; - public navigateSpy: jasmine.Spy; - public passwordInput: HTMLInputElement; - - constructor(private component: LogInOidcComponent, private fixture: ComponentFixture) { - // use injector to get services - const injector = fixture.debugElement.injector; - const store = injector.get(Store); - - // add spies - this.navigateSpy = spyOn(store, 'dispatch'); - } - -} diff --git a/src/app/shared/log-in/methods/oidc/log-in-oidc.component.ts b/src/app/shared/log-in/methods/oidc/log-in-oidc.component.ts deleted file mode 100644 index 882996b2078..00000000000 --- a/src/app/shared/log-in/methods/oidc/log-in-oidc.component.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Component, } from '@angular/core'; - -import { renderAuthMethodFor } from '../log-in.methods-decorator'; -import { AuthMethodType } from '../../../../core/auth/models/auth.method-type'; -import { LogInExternalProviderComponent } from '../log-in-external-provider.component'; - -@Component({ - selector: 'ds-log-in-oidc', - templateUrl: './log-in-oidc.component.html', -}) -@renderAuthMethodFor(AuthMethodType.Oidc) -export class LogInOidcComponent extends LogInExternalProviderComponent { - - /** - * Redirect to orcid authentication url - */ - redirectToOidc() { - this.redirectToExternalProvider(); - } - -} diff --git a/src/app/shared/log-in/methods/orcid/log-in-orcid.component.html b/src/app/shared/log-in/methods/orcid/log-in-orcid.component.html deleted file mode 100644 index 6f5453fd60c..00000000000 --- a/src/app/shared/log-in/methods/orcid/log-in-orcid.component.html +++ /dev/null @@ -1,3 +0,0 @@ - \ No newline at end of file diff --git a/src/app/shared/log-in/methods/orcid/log-in-orcid.component.ts b/src/app/shared/log-in/methods/orcid/log-in-orcid.component.ts deleted file mode 100644 index e0b1da3db5f..00000000000 --- a/src/app/shared/log-in/methods/orcid/log-in-orcid.component.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Component, } from '@angular/core'; - -import { renderAuthMethodFor } from '../log-in.methods-decorator'; -import { AuthMethodType } from '../../../../core/auth/models/auth.method-type'; -import { LogInExternalProviderComponent } from '../log-in-external-provider.component'; - -@Component({ - selector: 'ds-log-in-orcid', - templateUrl: './log-in-orcid.component.html', -}) -@renderAuthMethodFor(AuthMethodType.Orcid) -export class LogInOrcidComponent extends LogInExternalProviderComponent { - - /** - * Redirect to orcid authentication url - */ - redirectToOrcid() { - this.redirectToExternalProvider(); - } - -} diff --git a/src/app/shared/log-in/methods/shibboleth/log-in-shibboleth.component.html b/src/app/shared/log-in/methods/shibboleth/log-in-shibboleth.component.html deleted file mode 100644 index 3a3b935cfa3..00000000000 --- a/src/app/shared/log-in/methods/shibboleth/log-in-shibboleth.component.html +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/src/app/shared/log-in/methods/shibboleth/log-in-shibboleth.component.spec.ts b/src/app/shared/log-in/methods/shibboleth/log-in-shibboleth.component.spec.ts deleted file mode 100644 index 075d33d98ef..00000000000 --- a/src/app/shared/log-in/methods/shibboleth/log-in-shibboleth.component.spec.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; - -import { provideMockStore } from '@ngrx/store/testing'; -import { Store, StoreModule } from '@ngrx/store'; -import { TranslateModule } from '@ngx-translate/core'; - -import { EPerson } from '../../../../core/eperson/models/eperson.model'; -import { EPersonMock } from '../../../testing/eperson.mock'; -import { authReducer } from '../../../../core/auth/auth.reducer'; -import { AuthService } from '../../../../core/auth/auth.service'; -import { AuthServiceStub } from '../../../testing/auth-service.stub'; -import { storeModuleConfig } from '../../../../app.reducer'; -import { AuthMethod } from '../../../../core/auth/models/auth.method'; -import { AuthMethodType } from '../../../../core/auth/models/auth.method-type'; -import { LogInShibbolethComponent } from './log-in-shibboleth.component'; -import { NativeWindowService } from '../../../../core/services/window.service'; -import { RouterStub } from '../../../testing/router.stub'; -import { ActivatedRouteStub } from '../../../testing/active-router.stub'; -import { NativeWindowMockFactory } from '../../../mocks/mock-native-window-ref'; -import { HardRedirectService } from '../../../../core/services/hard-redirect.service'; - - -describe('LogInShibbolethComponent', () => { - - let component: LogInShibbolethComponent; - let fixture: ComponentFixture; - let page: Page; - let user: EPerson; - let componentAsAny: any; - let setHrefSpy; - let shibbolethBaseUrl; - let location; - let initialState: any; - let hardRedirectService: HardRedirectService; - - beforeEach(() => { - user = EPersonMock; - shibbolethBaseUrl = 'dspace-rest.test/shibboleth?redirectUrl='; - location = shibbolethBaseUrl + 'http://dspace-angular.test/home'; - - hardRedirectService = jasmine.createSpyObj('hardRedirectService', { - getCurrentRoute: {}, - redirect: {} - }); - - initialState = { - core: { - auth: { - authenticated: false, - loaded: false, - blocking: false, - loading: false, - authMethods: [] - } - } - }; - }); - - beforeEach(waitForAsync(() => { - // refine the test module by declaring the test component - TestBed.configureTestingModule({ - imports: [ - StoreModule.forRoot({ auth: authReducer }, storeModuleConfig), - TranslateModule.forRoot() - ], - declarations: [ - LogInShibbolethComponent - ], - providers: [ - { provide: AuthService, useClass: AuthServiceStub }, - { provide: 'authMethodProvider', useValue: new AuthMethod(AuthMethodType.Shibboleth, location) }, - { provide: 'isStandalonePage', useValue: true }, - { provide: NativeWindowService, useFactory: NativeWindowMockFactory }, - { provide: Router, useValue: new RouterStub() }, - { provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, - { provide: HardRedirectService, useValue: hardRedirectService }, - provideMockStore({ initialState }), - ], - schemas: [ - CUSTOM_ELEMENTS_SCHEMA - ] - }) - .compileComponents(); - - })); - - beforeEach(() => { - // create component and test fixture - fixture = TestBed.createComponent(LogInShibbolethComponent); - - // get test component from the fixture - component = fixture.componentInstance; - componentAsAny = component; - - // create page - page = new Page(component, fixture); - setHrefSpy = spyOnProperty(componentAsAny._window.nativeWindow.location, 'href', 'set').and.callThrough(); - - }); - - it('should set the properly a new redirectUrl', () => { - const currentUrl = 'http://dspace-angular.test/collections/12345'; - componentAsAny._window.nativeWindow.location.href = currentUrl; - - fixture.detectChanges(); - - expect(componentAsAny.injectedAuthMethodModel.location).toBe(location); - expect(componentAsAny._window.nativeWindow.location.href).toBe(currentUrl); - - component.redirectToShibboleth(); - - expect(setHrefSpy).toHaveBeenCalledWith(currentUrl); - - }); - - it('should not set a new redirectUrl', () => { - const currentUrl = 'http://dspace-angular.test/home'; - componentAsAny._window.nativeWindow.location.href = currentUrl; - - fixture.detectChanges(); - - expect(componentAsAny.injectedAuthMethodModel.location).toBe(location); - expect(componentAsAny._window.nativeWindow.location.href).toBe(currentUrl); - - component.redirectToShibboleth(); - - expect(setHrefSpy).toHaveBeenCalledWith(currentUrl); - - }); - -}); - -/** - * I represent the DOM elements and attach spies. - * - * @class Page - */ -class Page { - - public emailInput: HTMLInputElement; - public navigateSpy: jasmine.Spy; - public passwordInput: HTMLInputElement; - - constructor(private component: LogInShibbolethComponent, private fixture: ComponentFixture) { - // use injector to get services - const injector = fixture.debugElement.injector; - const store = injector.get(Store); - - // add spies - this.navigateSpy = spyOn(store, 'dispatch'); - } - -} diff --git a/src/app/shared/log-in/methods/shibboleth/log-in-shibboleth.component.ts b/src/app/shared/log-in/methods/shibboleth/log-in-shibboleth.component.ts deleted file mode 100644 index dcfb3ccfc33..00000000000 --- a/src/app/shared/log-in/methods/shibboleth/log-in-shibboleth.component.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Component, } from '@angular/core'; - -import { renderAuthMethodFor } from '../log-in.methods-decorator'; -import { AuthMethodType } from '../../../../core/auth/models/auth.method-type'; -import { LogInExternalProviderComponent } from '../log-in-external-provider.component'; - -@Component({ - selector: 'ds-log-in-shibboleth', - templateUrl: './log-in-shibboleth.component.html', - styleUrls: ['./log-in-shibboleth.component.scss'], - -}) -@renderAuthMethodFor(AuthMethodType.Shibboleth) -export class LogInShibbolethComponent extends LogInExternalProviderComponent { - - /** - * Redirect to shibboleth authentication url - */ - redirectToShibboleth() { - this.redirectToExternalProvider(); - } - -} diff --git a/src/app/shared/menu/menu-item/link-menu-item.component.ts b/src/app/shared/menu/menu-item/link-menu-item.component.ts index c9a60f0c286..78d5b2fc6f0 100644 --- a/src/app/shared/menu/menu-item/link-menu-item.component.ts +++ b/src/app/shared/menu/menu-item/link-menu-item.component.ts @@ -1,4 +1,4 @@ -import { Component, Inject, Input, OnInit } from '@angular/core'; +import { Component, Inject, OnInit } from '@angular/core'; import { LinkMenuItemModel } from './models/link.model'; import { rendersMenuItemForType } from '../menu-item.decorator'; import { isNotEmpty } from '../../empty.util'; diff --git a/src/app/shared/menu/menu-item/text-menu-item.component.ts b/src/app/shared/menu/menu-item/text-menu-item.component.ts index af690d198c4..25549f53a89 100644 --- a/src/app/shared/menu/menu-item/text-menu-item.component.ts +++ b/src/app/shared/menu/menu-item/text-menu-item.component.ts @@ -1,4 +1,4 @@ -import { Component, Inject, Input } from '@angular/core'; +import { Component, Inject } from '@angular/core'; import { TextMenuItemModel } from './models/text.model'; import { rendersMenuItemForType } from '../menu-item.decorator'; import { MenuItemType } from '../menu-item-type.model'; diff --git a/src/app/shared/menu/menu.module.ts b/src/app/shared/menu/menu.module.ts index 1d186a9b7d0..c007af517dd 100644 --- a/src/app/shared/menu/menu.module.ts +++ b/src/app/shared/menu/menu.module.ts @@ -12,8 +12,11 @@ import { ExternalLinkMenuItemComponent } from './menu-item/external-link-menu-it const COMPONENTS = [ MenuSectionComponent, MenuComponent, - LinkMenuItemComponent, +]; + +const ENTRY_COMPONENTS = [ TextMenuItemComponent, + LinkMenuItemComponent, OnClickMenuItemComponent, ExternalLinkMenuItemComponent, ]; @@ -32,10 +35,12 @@ const PROVIDERS = [ ...MODULES ], declarations: [ - ...COMPONENTS + ...COMPONENTS, + ...ENTRY_COMPONENTS, ], providers: [ - ...PROVIDERS + ...PROVIDERS, + ...ENTRY_COMPONENTS, ], exports: [ ...COMPONENTS diff --git a/src/app/shared/menu/menu.reducer.spec.ts b/src/app/shared/menu/menu.reducer.spec.ts index 7ae05536af6..2865e887fca 100644 --- a/src/app/shared/menu/menu.reducer.spec.ts +++ b/src/app/shared/menu/menu.reducer.spec.ts @@ -1,3 +1,4 @@ +// eslint-disable-next-line import/no-namespace import * as deepFreeze from 'deep-freeze'; import { ActivateMenuSectionAction, diff --git a/src/app/item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.html b/src/app/shared/metadata-field-wrapper/metadata-field-wrapper.component.html similarity index 100% rename from src/app/item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.html rename to src/app/shared/metadata-field-wrapper/metadata-field-wrapper.component.html diff --git a/src/app/item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.scss b/src/app/shared/metadata-field-wrapper/metadata-field-wrapper.component.scss similarity index 100% rename from src/app/item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.scss rename to src/app/shared/metadata-field-wrapper/metadata-field-wrapper.component.scss diff --git a/src/app/item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.spec.ts b/src/app/shared/metadata-field-wrapper/metadata-field-wrapper.component.spec.ts similarity index 100% rename from src/app/item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.spec.ts rename to src/app/shared/metadata-field-wrapper/metadata-field-wrapper.component.spec.ts diff --git a/src/app/item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.ts b/src/app/shared/metadata-field-wrapper/metadata-field-wrapper.component.ts similarity index 100% rename from src/app/item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.ts rename to src/app/shared/metadata-field-wrapper/metadata-field-wrapper.component.ts diff --git a/src/app/shared/mocks/dso-name.service.mock.ts b/src/app/shared/mocks/dso-name.service.mock.ts index f4947cc860a..cf3cf5466b2 100644 --- a/src/app/shared/mocks/dso-name.service.mock.ts +++ b/src/app/shared/mocks/dso-name.service.mock.ts @@ -6,4 +6,23 @@ export class DSONameServiceMock { public getName(dso: DSpaceObject) { return UNDEFINED_NAME; } + + public getHitHighlights(object: any, dso: DSpaceObject) { + if (object.hitHighlights && object.hitHighlights['dc.title']) { + return object.hitHighlights['dc.title'][0].value; + } else if (object.hitHighlights && object.hitHighlights['organization.legalName']) { + return object.hitHighlights['organization.legalName'][0].value; + } else if (object.hitHighlights && (object.hitHighlights['person.familyName'] || object.hitHighlights['person.givenName'])) { + if (object.hitHighlights['person.familyName'] && object.hitHighlights['person.givenName']) { + return `${object.hitHighlights['person.familyName'][0].value}, ${object.hitHighlights['person.givenName'][0].value}`; + } + if (object.hitHighlights['person.familyName']) { + return `${object.hitHighlights['person.familyName'][0].value}`; + } + if (object.hitHighlights['person.givenName']) { + return `${object.hitHighlights['person.givenName'][0].value}`; + } + } + return UNDEFINED_NAME; + } } diff --git a/src/app/shared/mocks/dspace-rest/endpoint-mocking-rest.service.ts b/src/app/shared/mocks/dspace-rest/endpoint-mocking-rest.service.ts index 8d621ad4be8..0358451557a 100644 --- a/src/app/shared/mocks/dspace-rest/endpoint-mocking-rest.service.ts +++ b/src/app/shared/mocks/dspace-rest/endpoint-mocking-rest.service.ts @@ -7,7 +7,6 @@ import { RestRequestMethod } from '../../../core/data/rest-request-method'; import { RawRestResponse } from '../../../core/dspace-rest/raw-rest-response.model'; import { DspaceRestService, HttpOptions } from '../../../core/dspace-rest/dspace-rest.service'; import { MOCK_RESPONSE_MAP, ResponseMapMock } from './mocks/response-map.mock'; -import * as URL from 'url-parse'; import { environment } from '../../../../environments/environment'; /** diff --git a/src/app/shared/mydspace-actions/claimed-task/approve/claimed-task-actions-approve.component.spec.ts b/src/app/shared/mydspace-actions/claimed-task/approve/claimed-task-actions-approve.component.spec.ts index ce67da1349b..f43316c4e15 100644 --- a/src/app/shared/mydspace-actions/claimed-task/approve/claimed-task-actions-approve.component.spec.ts +++ b/src/app/shared/mydspace-actions/claimed-task/approve/claimed-task-actions-approve.component.spec.ts @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy, Injector, NO_ERRORS_SCHEMA } from '@angular/core'; -import { async, ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { of, of as observableOf } from 'rxjs'; diff --git a/src/app/shared/mydspace-actions/mydspace-actions.module.ts b/src/app/shared/mydspace-actions/mydspace-actions.module.ts new file mode 100644 index 00000000000..68e3a8fb58a --- /dev/null +++ b/src/app/shared/mydspace-actions/mydspace-actions.module.ts @@ -0,0 +1,59 @@ +/** + * 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 { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '../shared.module'; +import { ClaimedTaskActionsApproveComponent } from './claimed-task/approve/claimed-task-actions-approve.component'; +import { ClaimedTaskActionsRejectComponent } from './claimed-task/reject/claimed-task-actions-reject.component'; +import { ClaimedTaskActionsReturnToPoolComponent } from './claimed-task/return-to-pool/claimed-task-actions-return-to-pool.component'; +import { ClaimedTaskActionsEditMetadataComponent } from './claimed-task/edit-metadata/claimed-task-actions-edit-metadata.component'; +import { ClaimedTaskActionsComponent } from './claimed-task/claimed-task-actions.component'; +import { ClaimedTaskActionsLoaderComponent } from './claimed-task/switcher/claimed-task-actions-loader.component'; +import { ItemActionsComponent } from './item/item-actions.component'; +import { PoolTaskActionsComponent } from './pool-task/pool-task-actions.component'; +import { WorkflowitemActionsComponent } from './workflowitem/workflowitem-actions.component'; +import { WorkspaceitemActionsComponent } from './workspaceitem/workspaceitem-actions.component'; + +const ENTRY_COMPONENTS = [ + ClaimedTaskActionsApproveComponent, + ClaimedTaskActionsRejectComponent, + ClaimedTaskActionsReturnToPoolComponent, + ClaimedTaskActionsEditMetadataComponent, +]; + +const DECLARATIONS = [ + ...ENTRY_COMPONENTS, + ClaimedTaskActionsComponent, + ClaimedTaskActionsLoaderComponent, + ItemActionsComponent, + PoolTaskActionsComponent, + WorkflowitemActionsComponent, + WorkspaceitemActionsComponent, +]; + +/** + * This module contains Item actions used in MyDSpace + */ +@NgModule({ + imports: [ + CommonModule, + SharedModule, + ], + declarations: [ + ...DECLARATIONS, + ], + providers: [ + ...ENTRY_COMPONENTS, + ], + exports: [ + ...DECLARATIONS, + ], +}) +export class MyDSpaceActionsModule { + +} diff --git a/src/app/shared/notifications/notifications-board/notifications-board.component.spec.ts b/src/app/shared/notifications/notifications-board/notifications-board.component.spec.ts index 1d3faabdaa6..08b9585a8c7 100644 --- a/src/app/shared/notifications/notifications-board/notifications-board.component.spec.ts +++ b/src/app/shared/notifications/notifications-board/notifications-board.component.spec.ts @@ -11,7 +11,7 @@ import { AppState } from '../../../app.reducer'; import { NotificationComponent } from '../notification/notification.component'; import { Notification } from '../models/notification.model'; import { NotificationType } from '../models/notification-type'; -import { uniqueId } from 'lodash'; +import uniqueId from 'lodash/uniqueId'; import { INotificationBoardOptions } from '../../../../config/notifications-config.interfaces'; import { NotificationsServiceStub } from '../../testing/notifications-service.stub'; import { cold } from 'jasmine-marbles'; diff --git a/src/app/shared/notifications/notifications-board/notifications-board.component.ts b/src/app/shared/notifications/notifications-board/notifications-board.component.ts index f153d1009e8..97ae09c1a67 100644 --- a/src/app/shared/notifications/notifications-board/notifications-board.component.ts +++ b/src/app/shared/notifications/notifications-board/notifications-board.component.ts @@ -10,7 +10,7 @@ import { import { select, Store } from '@ngrx/store'; import { BehaviorSubject, Subscription } from 'rxjs'; -import { difference } from 'lodash'; +import difference from 'lodash/difference'; import { NotificationsService } from '../notifications.service'; import { AppState } from '../../../app.reducer'; diff --git a/src/app/shared/notifications/notifications.reducers.spec.ts b/src/app/shared/notifications/notifications.reducers.spec.ts index b8347971159..fde92e8891b 100644 --- a/src/app/shared/notifications/notifications.reducers.spec.ts +++ b/src/app/shared/notifications/notifications.reducers.spec.ts @@ -9,7 +9,7 @@ import { NotificationOptions } from './models/notification-options.model'; import { NotificationAnimationsType } from './models/notification-animations-type'; import { NotificationType } from './models/notification-type'; import { Notification } from './models/notification.model'; -import { uniqueId } from 'lodash'; +import uniqueId from 'lodash/uniqueId'; import { ChangeDetectorRef } from '@angular/core'; import { storeModuleConfig } from '../../app.reducer'; diff --git a/src/app/shared/notifications/notifications.service.ts b/src/app/shared/notifications/notifications.service.ts index 98272d4f433..d37d6a349b6 100644 --- a/src/app/shared/notifications/notifications.service.ts +++ b/src/app/shared/notifications/notifications.service.ts @@ -4,7 +4,7 @@ import { of as observableOf } from 'rxjs'; import { first } from 'rxjs/operators'; import { Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; -import { uniqueId } from 'lodash'; +import uniqueId from 'lodash/uniqueId'; import { INotification, Notification } from './models/notification.model'; import { NotificationType } from './models/notification-type'; diff --git a/src/app/shared/object-collection/shared/listable-object/listable-object.decorator.spec.ts b/src/app/shared/object-collection/shared/listable-object/listable-object.decorator.spec.ts index 7475aac967f..f7d00510f68 100644 --- a/src/app/shared/object-collection/shared/listable-object/listable-object.decorator.spec.ts +++ b/src/app/shared/object-collection/shared/listable-object/listable-object.decorator.spec.ts @@ -1,7 +1,6 @@ /* eslint-disable max-classes-per-file */ -import { Item } from '../../../../core/shared/item.model'; import { ViewMode } from '../../../../core/shared/view-mode.model'; -import { getListableObjectComponent, listableObjectComponent } from './listable-object.decorator'; +import { DEFAULT_VIEW_MODE, getListableObjectComponent, listableObjectComponent } from './listable-object.decorator'; import { Context } from '../../../../core/shared/context.model'; import { environment } from '../../../../../environments/environment'; @@ -13,6 +12,10 @@ describe('ListableObject decorator function', () => { const type3 = 'TestType3'; const typeAncestor = 'TestTypeAncestor'; const typeUnthemed = 'TestTypeUnthemed'; + const typeLowPriority = 'TypeLowPriority'; + const typeLowPriority2 = 'TypeLowPriority2'; + const typeMidPriority = 'TypeMidPriority'; + const typeHighPriority = 'TypeHighPriority'; class Test1List { } @@ -38,6 +41,21 @@ describe('ListableObject decorator function', () => { class TestUnthemedComponent { } + class TestDefaultLowPriorityComponent { + } + + class TestLowPriorityComponent { + } + + class TestDefaultMidPriorityComponent { + } + + class TestMidPriorityComponent { + } + + class TestHighPriorityComponent { + } + /* eslint-enable max-classes-per-file */ beforeEach(() => { @@ -54,6 +72,15 @@ describe('ListableObject decorator function', () => { listableObjectComponent(typeAncestor, ViewMode.ListElement, Context.Any, 'ancestor')(TestAncestorComponent); listableObjectComponent(typeUnthemed, ViewMode.ListElement, Context.Any)(TestUnthemedComponent); + // Register component with different priorities for expected parameters: + // ViewMode.DetailedListElement, Context.Search, 'custom' + listableObjectComponent(typeLowPriority, DEFAULT_VIEW_MODE, undefined, undefined)(TestDefaultLowPriorityComponent); + listableObjectComponent(typeLowPriority, DEFAULT_VIEW_MODE, Context.Search, 'custom')(TestLowPriorityComponent); + listableObjectComponent(typeLowPriority2, DEFAULT_VIEW_MODE, undefined, undefined)(TestDefaultLowPriorityComponent); + listableObjectComponent(typeMidPriority, ViewMode.DetailedListElement, undefined, undefined)(TestDefaultMidPriorityComponent); + listableObjectComponent(typeMidPriority, ViewMode.DetailedListElement, undefined, 'custom')(TestMidPriorityComponent); + listableObjectComponent(typeHighPriority, ViewMode.DetailedListElement, Context.Search, undefined)(TestHighPriorityComponent); + ogEnvironmentThemes = environment.themes; }); @@ -81,7 +108,7 @@ describe('ListableObject decorator function', () => { }); }); - describe('If there isn\'nt an exact match', () => { + describe('If there isn\'t an exact match', () => { describe('If there is a match for one of the entity types and the view mode', () => { it('should return the class with the matching entity type and view mode and default context', () => { const component = getListableObjectComponent([type3], ViewMode.ListElement, Context.Workspace); @@ -152,4 +179,45 @@ describe('ListableObject decorator function', () => { }); }); }); + + describe('priorities', () => { + beforeEach(() => { + environment.themes = [ + { + name: 'custom', + } + ]; + }); + + describe('If a component with default ViewMode contains specific context and/or theme', () => { + it('requesting a specific ViewMode should return the one with the requested context and/or theme', () => { + const component = getListableObjectComponent([typeLowPriority], ViewMode.DetailedListElement, Context.Search, 'custom'); + expect(component).toEqual(TestLowPriorityComponent); + }); + }); + + describe('If a component with default Context contains specific ViewMode and/or theme', () => { + it('requesting a specific Context should return the one with the requested view-mode and/or theme', () => { + const component = getListableObjectComponent([typeMidPriority], ViewMode.DetailedListElement, Context.Search, 'custom'); + expect(component).toEqual(TestMidPriorityComponent); + }); + }); + + describe('If multiple components exist, each containing a different default value for one of the requested parameters', () => { + it('the component with the latest default value in the list should be returned', () => { + let component = getListableObjectComponent([typeMidPriority, typeLowPriority], ViewMode.DetailedListElement, Context.Search, 'custom'); + expect(component).toEqual(TestMidPriorityComponent); + + component = getListableObjectComponent([typeLowPriority, typeMidPriority, typeHighPriority], ViewMode.DetailedListElement, Context.Search, 'custom'); + expect(component).toEqual(TestHighPriorityComponent); + }); + }); + + describe('If two components exist for two different types, both configured for the same view-mode, but one for a specific context and/or theme', () => { + it('requesting a component for that specific context and/or theme while providing both types should return the most relevant one', () => { + const component = getListableObjectComponent([typeLowPriority2, typeLowPriority], ViewMode.DetailedListElement, Context.Search, 'custom'); + expect(component).toEqual(TestLowPriorityComponent); + }); + }); + }); }); diff --git a/src/app/shared/object-collection/shared/listable-object/listable-object.decorator.ts b/src/app/shared/object-collection/shared/listable-object/listable-object.decorator.ts index b7f27d1553d..e5654e63e00 100644 --- a/src/app/shared/object-collection/shared/listable-object/listable-object.decorator.ts +++ b/src/app/shared/object-collection/shared/listable-object/listable-object.decorator.ts @@ -11,6 +11,53 @@ export const DEFAULT_VIEW_MODE = ViewMode.ListElement; export const DEFAULT_CONTEXT = Context.Any; export const DEFAULT_THEME = '*'; +/** + * A class used to compare two matches and their relevancy to determine which of the two gains priority over the other + * + * "level" represents the index of the first default value that was used to find the match with: + * ViewMode being index 0, Context index 1 and theme index 2. Examples: + * - If a default value was used for context, but not view-mode and theme, the "level" will be 1 + * - If a default value was used for view-mode and context, but not for theme, the "level" will be 0 + * - If no default value was used for any of the fields, the "level" will be 3 + * + * "relevancy" represents the amount of values that didn't require a default value to fall back on. Examples: + * - If a default value was used for theme, but not view-mode and context, the "relevancy" will be 2 + * - If a default value was used for view-mode and context, but not for theme, the "relevancy" will be 1 + * - If a default value was used for all fields, the "relevancy" will be 0 + * - If no default value was used for any of the fields, the "relevancy" will be 3 + * + * To determine which of two MatchRelevancies is the most relevant, we compare "level" and "relevancy" in that order. + * If any of the two is higher than the other, that match is most relevant. Examples: + * - { level: 1, relevancy: 1 } is more relevant than { level: 0, relevancy: 2 } + * - { level: 1, relevancy: 1 } is less relevant than { level: 1, relevancy: 2 } + * - { level: 1, relevancy: 1 } is more relevant than { level: 1, relevancy: 0 } + * - { level: 1, relevancy: 1 } is less relevant than { level: 2, relevancy: 0 } + * - { level: 1, relevancy: 1 } is more relevant than null + */ +class MatchRelevancy { + constructor(public match: any, + public level: number, + public relevancy: number) { + } + + isMoreRelevantThan(otherMatch: MatchRelevancy): boolean { + if (hasNoValue(otherMatch)) { + return true; + } + if (otherMatch.level > this.level) { + return false; + } + if (otherMatch.level === this.level && otherMatch.relevancy > this.relevancy) { + return false; + } + return true; + } + + isLessRelevantThan(otherMatch: MatchRelevancy): boolean { + return !this.isMoreRelevantThan(otherMatch); + } +} + /** * Factory to allow us to inject getThemeConfigFor so we can mock it in tests */ @@ -48,47 +95,70 @@ export function listableObjectComponent(objectType: string | GenericConstructor< /** * Getter to retrieve the matching listable object component + * + * Looping over the provided types, it'll attempt to find the best match depending on the {@link MatchRelevancy} returned by getMatch() + * The most relevant match between types is kept and eventually returned + * * @param types The types of which one should match the listable component * @param viewMode The view mode that should match the components * @param context The context that should match the components * @param theme The theme that should match the components */ export function getListableObjectComponent(types: (string | GenericConstructor)[], viewMode: ViewMode, context: Context = DEFAULT_CONTEXT, theme: string = DEFAULT_THEME) { - let bestMatch; - let bestMatchValue = 0; + let currentBestMatch: MatchRelevancy = null; for (const type of types) { const typeMap = map.get(type); if (hasValue(typeMap)) { - const typeModeMap = typeMap.get(viewMode); - if (hasValue(typeModeMap)) { - const contextMap = typeModeMap.get(context); - if (hasValue(contextMap)) { - const match = resolveTheme(contextMap, theme); - if (hasValue(match)) { - return match; - } - if (bestMatchValue < 3 && hasValue(contextMap.get(DEFAULT_THEME))) { - bestMatchValue = 3; - bestMatch = contextMap.get(DEFAULT_THEME); - } - } - if (bestMatchValue < 2 && - hasValue(typeModeMap.get(DEFAULT_CONTEXT)) && - hasValue(typeModeMap.get(DEFAULT_CONTEXT).get(DEFAULT_THEME))) { - bestMatchValue = 2; - bestMatch = typeModeMap.get(DEFAULT_CONTEXT).get(DEFAULT_THEME); - } + const match = getMatch(typeMap, [viewMode, context, theme], [DEFAULT_VIEW_MODE, DEFAULT_CONTEXT, DEFAULT_THEME]); + if (hasNoValue(currentBestMatch) || currentBestMatch.isLessRelevantThan(match)) { + currentBestMatch = match; + } + } + } + return hasValue(currentBestMatch) ? currentBestMatch.match : null; +} + +/** + * Find an object within a nested map, matching the provided keys as best as possible, falling back on defaults wherever + * needed. + * + * Starting off with a Map, it loops over the provided keys, going deeper into the map until it finds a value + * If at some point, no value is found, it'll attempt to use the default value for that index instead + * If the default value exists, the index is stored in the "level" + * If no default value exists, 1 is added to "relevancy" + * See {@link MatchRelevancy} what these represent + * + * @param typeMap a multi-dimensional map + * @param keys the keys of the multi-dimensional map to loop over. Each key represents a level within the map + * @param defaults the default values to use for each level, in case no value is found for the key at that index + * @returns matchAndLevel a {@link MatchRelevancy} object containing the match and its level of relevancy + */ +function getMatch(typeMap: Map, keys: any[], defaults: any[]): MatchRelevancy { + let currentMap = typeMap; + let level = -1; + let relevancy = 0; + for (let i = 0; i < keys.length; i++) { + // If we're currently checking the theme, resolve it first to take extended themes into account + let currentMatch = defaults[i] === DEFAULT_THEME ? resolveTheme(currentMap, keys[i]) : currentMap.get(keys[i]); + if (hasNoValue(currentMatch)) { + currentMatch = currentMap.get(defaults[i]); + if (level === -1) { + level = i; } - if (bestMatchValue < 1 && - hasValue(typeMap.get(DEFAULT_VIEW_MODE)) && - hasValue(typeMap.get(DEFAULT_VIEW_MODE).get(DEFAULT_CONTEXT)) && - hasValue(typeMap.get(DEFAULT_VIEW_MODE).get(DEFAULT_CONTEXT).get(DEFAULT_THEME))) { - bestMatchValue = 1; - bestMatch = typeMap.get(DEFAULT_VIEW_MODE).get(DEFAULT_CONTEXT).get(DEFAULT_THEME); + } else { + relevancy++; + } + if (hasValue(currentMatch)) { + if (currentMatch instanceof Map) { + currentMap = currentMatch as Map; + } else { + return new MatchRelevancy(currentMatch, level > -1 ? level : i + 1, relevancy); } + } else { + return null; } } - return bestMatch; + return null; } /** diff --git a/src/app/shared/object-collection/shared/mydspace-item-status/my-dspace-item-status.component.spec.ts b/src/app/shared/object-collection/shared/mydspace-item-status/my-dspace-item-status.component.spec.ts index 44e6a44b704..59fc29424d0 100644 --- a/src/app/shared/object-collection/shared/mydspace-item-status/my-dspace-item-status.component.spec.ts +++ b/src/app/shared/object-collection/shared/mydspace-item-status/my-dspace-item-status.component.spec.ts @@ -55,34 +55,34 @@ describe('MyDSpaceItemStatusComponent', () => { component.status = MyDspaceItemStatusType.VALIDATION; fixture.detectChanges(); expect(component.badgeContent).toBe(MyDspaceItemStatusType.VALIDATION); - expect(component.badgeClass).toBe('text-light badge badge-warning'); + expect(component.badgeClass).toBe('text-light badge badge-validation'); }); it('should init badge content and class', () => { component.status = MyDspaceItemStatusType.WAITING_CONTROLLER; fixture.detectChanges(); expect(component.badgeContent).toBe(MyDspaceItemStatusType.WAITING_CONTROLLER); - expect(component.badgeClass).toBe('text-light badge badge-info'); + expect(component.badgeClass).toBe('text-light badge badge-waiting-controller'); }); it('should init badge content and class', () => { component.status = MyDspaceItemStatusType.WORKSPACE; fixture.detectChanges(); expect(component.badgeContent).toBe(MyDspaceItemStatusType.WORKSPACE); - expect(component.badgeClass).toBe('text-light badge badge-primary'); + expect(component.badgeClass).toBe('text-light badge badge-workspace'); }); it('should init badge content and class', () => { component.status = MyDspaceItemStatusType.ARCHIVED; fixture.detectChanges(); expect(component.badgeContent).toBe(MyDspaceItemStatusType.ARCHIVED); - expect(component.badgeClass).toBe('text-light badge badge-success'); + expect(component.badgeClass).toBe('text-light badge badge-archived'); }); it('should init badge content and class', () => { component.status = MyDspaceItemStatusType.WORKFLOW; fixture.detectChanges(); expect(component.badgeContent).toBe(MyDspaceItemStatusType.WORKFLOW); - expect(component.badgeClass).toBe('text-light badge badge-info'); + expect(component.badgeClass).toBe('text-light badge badge-workflow'); }); }); diff --git a/src/app/shared/object-collection/shared/mydspace-item-status/my-dspace-item-status.component.ts b/src/app/shared/object-collection/shared/mydspace-item-status/my-dspace-item-status.component.ts index 917dd45accc..83b2656fbda 100644 --- a/src/app/shared/object-collection/shared/mydspace-item-status/my-dspace-item-status.component.ts +++ b/src/app/shared/object-collection/shared/mydspace-item-status/my-dspace-item-status.component.ts @@ -34,19 +34,19 @@ export class MyDSpaceItemStatusComponent implements OnInit { this.badgeClass = 'text-light badge '; switch (this.status) { case MyDspaceItemStatusType.VALIDATION: - this.badgeClass += 'badge-warning'; + this.badgeClass += 'badge-validation'; break; case MyDspaceItemStatusType.WAITING_CONTROLLER: - this.badgeClass += 'badge-info'; + this.badgeClass += 'badge-waiting-controller'; break; case MyDspaceItemStatusType.WORKSPACE: - this.badgeClass += 'badge-primary'; + this.badgeClass += 'badge-workspace'; break; case MyDspaceItemStatusType.ARCHIVED: - this.badgeClass += 'badge-success'; + this.badgeClass += 'badge-archived'; break; case MyDspaceItemStatusType.WORKFLOW: - this.badgeClass += 'badge-info'; + this.badgeClass += 'badge-workflow'; break; } } diff --git a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.spec.ts b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.spec.ts index 57b863a1b15..dc42b033d84 100644 --- a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.spec.ts +++ b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.spec.ts @@ -28,13 +28,19 @@ import { ItemSearchResultGridElementComponent } from './item-search-result-grid- const mockItemWithMetadata: ItemSearchResult = new ItemSearchResult(); mockItemWithMetadata.hitHighlights = {}; +const dcTitle = 'This is just another title'; mockItemWithMetadata.indexableObject = Object.assign(new Item(), { + hitHighlights: { + 'dc.title': [{ + value: dcTitle + }], + }, bundles: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), metadata: { 'dc.title': [ { language: 'en_US', - value: 'This is just another title' + value: dcTitle } ], 'dc.contributor.author': [ @@ -57,6 +63,114 @@ mockItemWithMetadata.indexableObject = Object.assign(new Item(), { ] } }); +const mockPerson: ItemSearchResult = Object.assign(new ItemSearchResult(), { + hitHighlights: { + 'person.familyName': [{ + value: 'Michel' + }], + }, + indexableObject: + Object.assign(new Item(), { + bundles: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), + entityType: 'Person', + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ], + 'dc.contributor.author': [ + { + language: 'en_US', + value: 'Smith, Donald' + } + ], + 'dc.publisher': [ + { + language: 'en_US', + value: 'a publisher' + } + ], + 'dc.date.issued': [ + { + language: 'en_US', + value: '2015-06-26' + } + ], + 'dc.description.abstract': [ + { + language: 'en_US', + value: 'This is the abstract' + } + ], + 'dspace.entity.type': [ + { + value: 'Person' + } + ], + 'person.familyName': [ + { + value: 'Michel' + } + ] + } + }) +}); +const mockOrgUnit: ItemSearchResult = Object.assign(new ItemSearchResult(), { + hitHighlights: { + 'organization.legalName': [{ + value: 'Science' + }], + }, + indexableObject: + Object.assign(new Item(), { + bundles: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), + entityType: 'OrgUnit', + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ], + 'dc.contributor.author': [ + { + language: 'en_US', + value: 'Smith, Donald' + } + ], + 'dc.publisher': [ + { + language: 'en_US', + value: 'a publisher' + } + ], + 'dc.date.issued': [ + { + language: 'en_US', + value: '2015-06-26' + } + ], + 'dc.description.abstract': [ + { + language: 'en_US', + value: 'This is the abstract' + } + ], + 'organization.legalName': [ + { + value: 'Science' + } + ], + 'dspace.entity.type': [ + { + value: 'OrgUnit' + } + ] + } + }) +}); const mockItemWithoutMetadata: ItemSearchResult = new ItemSearchResult(); mockItemWithoutMetadata.hitHighlights = {}; @@ -154,6 +268,41 @@ export function getEntityGridElementTestComponent(component, searchResultWithMet expect(itemAuthorField).toBeNull(); }); }); + + describe('When the item has title', () => { + beforeEach(() => { + comp.object = mockItemWithMetadata; + fixture.detectChanges(); + }); + it('should show highlighted title', () => { + const titleField = fixture.debugElement.query(By.css('.card-title')); + expect(titleField.nativeNode.innerHTML).toEqual(dcTitle); + }); + }); + + describe('When the item is Person and has title', () => { + beforeEach(() => { + comp.object = mockPerson; + fixture.detectChanges(); + }); + + it('should show highlighted title', () => { + const titleField = fixture.debugElement.query(By.css('.card-title')); + expect(titleField.nativeNode.innerHTML).toEqual('Michel'); + }); + }); + + describe('When the item is orgUnit and has title', () => { + beforeEach(() => { + comp.object = mockOrgUnit; + fixture.detectChanges(); + }); + + it('should show highlighted title', () => { + const titleField = fixture.debugElement.query(By.css('.card-title')); + expect(titleField.nativeNode.innerHTML).toEqual('Science'); + }); + }); }); }; } diff --git a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.ts b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.ts index b5f9c016e4f..303e4681a28 100644 --- a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.ts +++ b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.ts @@ -42,6 +42,6 @@ export class ItemSearchResultGridElementComponent extends SearchResultGridElemen ngOnInit(): void { super.ngOnInit(); this.itemPageRoute = getItemPageRoute(this.dso); - this.dsoTitle = this.dsoNameService.getName(this.dso); + this.dsoTitle = this.dsoNameService.getHitHighlights(this.object, this.dso); } } diff --git a/src/app/shared/object-list/listable-notification-object/listable-notification-object.component.html b/src/app/shared/object-list/listable-notification-object/listable-notification-object.component.html new file mode 100644 index 00000000000..d29199bae30 --- /dev/null +++ b/src/app/shared/object-list/listable-notification-object/listable-notification-object.component.html @@ -0,0 +1 @@ +
{{ object?.message | translate }}
diff --git a/src/app/shared/object-list/listable-notification-object/listable-notification-object.component.scss b/src/app/shared/object-list/listable-notification-object/listable-notification-object.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/app/shared/object-list/listable-notification-object/listable-notification-object.component.spec.ts b/src/app/shared/object-list/listable-notification-object/listable-notification-object.component.spec.ts new file mode 100644 index 00000000000..3cf05f7fec6 --- /dev/null +++ b/src/app/shared/object-list/listable-notification-object/listable-notification-object.component.spec.ts @@ -0,0 +1,43 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ListableNotificationObjectComponent } from './listable-notification-object.component'; +import { NotificationType } from '../../notifications/models/notification-type'; +import { ListableNotificationObject } from './listable-notification-object.model'; +import { By } from '@angular/platform-browser'; +import { TranslateModule } from '@ngx-translate/core'; + +describe('ListableNotificationObjectComponent', () => { + let component: ListableNotificationObjectComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + ], + declarations: [ + ListableNotificationObjectComponent, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ListableNotificationObjectComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + describe('ui', () => { + it('should display the given error message', () => { + component.object = new ListableNotificationObject(NotificationType.Error, 'test error message'); + fixture.detectChanges(); + + const listableNotificationObject: Element = fixture.debugElement.query(By.css('.alert')).nativeElement; + expect(listableNotificationObject.className).toContain(NotificationType.Error); + expect(listableNotificationObject.innerHTML).toBe('test error message'); + }); + }); + + afterEach(() => { + fixture.debugElement.nativeElement.remove(); + }); +}); diff --git a/src/app/shared/object-list/listable-notification-object/listable-notification-object.component.ts b/src/app/shared/object-list/listable-notification-object/listable-notification-object.component.ts new file mode 100644 index 00000000000..ca23ee76a2a --- /dev/null +++ b/src/app/shared/object-list/listable-notification-object/listable-notification-object.component.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; +import { + AbstractListableElementComponent +} from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; +import { ListableNotificationObject } from './listable-notification-object.model'; +import { listableObjectComponent } from '../../object-collection/shared/listable-object/listable-object.decorator'; +import { ViewMode } from '../../../core/shared/view-mode.model'; +import { LISTABLE_NOTIFICATION_OBJECT } from './listable-notification-object.resource-type'; + +/** + * The component for displaying a notifications inside an object list + */ +@listableObjectComponent(ListableNotificationObject, ViewMode.ListElement) +@listableObjectComponent(LISTABLE_NOTIFICATION_OBJECT.value, ViewMode.ListElement) +@Component({ + selector: 'ds-listable-notification-object', + templateUrl: './listable-notification-object.component.html', + styleUrls: ['./listable-notification-object.component.scss'], +}) +export class ListableNotificationObjectComponent extends AbstractListableElementComponent { +} diff --git a/src/app/shared/object-list/listable-notification-object/listable-notification-object.model.ts b/src/app/shared/object-list/listable-notification-object/listable-notification-object.model.ts new file mode 100644 index 00000000000..163d9096a2d --- /dev/null +++ b/src/app/shared/object-list/listable-notification-object/listable-notification-object.model.ts @@ -0,0 +1,36 @@ +import { ListableObject } from '../../object-collection/shared/listable-object.model'; +import { typedObject } from '../../../core/cache/builders/build-decorators'; +import { TypedObject } from '../../../core/cache/typed-object.model'; +import { LISTABLE_NOTIFICATION_OBJECT } from './listable-notification-object.resource-type'; +import { GenericConstructor } from '../../../core/shared/generic-constructor'; +import { NotificationType } from '../../notifications/models/notification-type'; +import { ResourceType } from '../../../core/shared/resource-type'; + +/** + * Object representing a notification message inside a list of objects + */ +@typedObject +export class ListableNotificationObject extends ListableObject implements TypedObject { + + static type: ResourceType = LISTABLE_NOTIFICATION_OBJECT; + type: ResourceType = LISTABLE_NOTIFICATION_OBJECT; + + protected renderTypes: string[]; + + constructor( + public notificationType: NotificationType = NotificationType.Error, + public message: string = 'listable-notification-object.default-message', + ...renderTypes: string[] + ) { + super(); + this.renderTypes = renderTypes; + } + + /** + * Method that returns as which type of object this object should be rendered. + */ + getRenderTypes(): (string | GenericConstructor)[] { + return [...this.renderTypes, this.constructor as GenericConstructor]; + } + +} diff --git a/src/app/shared/object-list/listable-notification-object/listable-notification-object.resource-type.ts b/src/app/shared/object-list/listable-notification-object/listable-notification-object.resource-type.ts new file mode 100644 index 00000000000..ed458126bb2 --- /dev/null +++ b/src/app/shared/object-list/listable-notification-object/listable-notification-object.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from '../../../core/shared/resource-type'; + +/** + * The resource type for {@link ListableNotificationObject} + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const LISTABLE_NOTIFICATION_OBJECT = new ResourceType('listable-notification-object'); diff --git a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts index 04f1e24d7b8..6b40678dedc 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts @@ -55,7 +55,7 @@ export class ItemListPreviewComponent implements OnInit { ngOnInit(): void { this.showThumbnails = this.appConfig.browseBy.showThumbnails; - this.dsoTitle = this.dsoNameService.getName(this.item); + this.dsoTitle = this.dsoNameService.getHitHighlights(this.object, this.item); } diff --git a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.spec.ts b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.spec.ts index d1e6c27ba43..7665b7d64e3 100644 --- a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.spec.ts +++ b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.spec.ts @@ -13,8 +13,13 @@ import { APP_CONFIG } from '../../../../../../../config/app-config.interface'; let publicationListElementComponent: ItemSearchResultListElementComponent; let fixture: ComponentFixture; - +const dcTitle = 'This is just another title'; const mockItemWithMetadata: ItemSearchResult = Object.assign(new ItemSearchResult(), { + hitHighlights: { + 'dc.title': [{ + value: dcTitle + }], + }, indexableObject: Object.assign(new Item(), { bundles: observableOf({}), @@ -22,7 +27,7 @@ const mockItemWithMetadata: ItemSearchResult = Object.assign(new ItemSearchResul 'dc.title': [ { language: 'en_US', - value: 'This is just another title' + value: dcTitle } ], 'dc.contributor.author': [ @@ -59,7 +64,114 @@ const mockItemWithoutMetadata: ItemSearchResult = Object.assign(new ItemSearchRe metadata: {} }) }); - +const mockPerson: ItemSearchResult = Object.assign(new ItemSearchResult(), { + hitHighlights: { + 'person.familyName': [{ + value: 'Michel' + }], + }, + indexableObject: + Object.assign(new Item(), { + bundles: observableOf({}), + entityType: 'Person', + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ], + 'dc.contributor.author': [ + { + language: 'en_US', + value: 'Smith, Donald' + } + ], + 'dc.publisher': [ + { + language: 'en_US', + value: 'a publisher' + } + ], + 'dc.date.issued': [ + { + language: 'en_US', + value: '2015-06-26' + } + ], + 'dc.description.abstract': [ + { + language: 'en_US', + value: 'This is the abstract' + } + ], + 'person.familyName': [ + { + value: 'Michel' + } + ], + 'dspace.entity.type': [ + { + value: 'Person' + } + ] + } + }) +}); +const mockOrgUnit: ItemSearchResult = Object.assign(new ItemSearchResult(), { + hitHighlights: { + 'organization.legalName': [{ + value: 'Science' + }], + }, + indexableObject: + Object.assign(new Item(), { + bundles: observableOf({}), + entityType: 'OrgUnit', + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ], + 'dc.contributor.author': [ + { + language: 'en_US', + value: 'Smith, Donald' + } + ], + 'dc.publisher': [ + { + language: 'en_US', + value: 'a publisher' + } + ], + 'dc.date.issued': [ + { + language: 'en_US', + value: '2015-06-26' + } + ], + 'dc.description.abstract': [ + { + language: 'en_US', + value: 'This is the abstract' + } + ], + 'organization.legalName': [ + { + value: 'Science' + } + ], + 'dspace.entity.type': [ + { + value: 'OrgUnit' + } + ] + } + }) +}); const environmentUseThumbs = { browseBy: { showThumbnails: true @@ -205,6 +317,42 @@ describe('ItemSearchResultListElementComponent', () => { }); }); + describe('When the item has title', () => { + beforeEach(() => { + publicationListElementComponent.object = mockItemWithMetadata; + fixture.detectChanges(); + }); + + it('should show highlighted title', () => { + const titleField = fixture.debugElement.query(By.css('.item-list-title')); + expect(titleField.nativeNode.innerHTML).toEqual(dcTitle); + }); + }); + + describe('When the item is Person and has title', () => { + beforeEach(() => { + publicationListElementComponent.object = mockPerson; + fixture.detectChanges(); + }); + + it('should show highlighted title', () => { + const titleField = fixture.debugElement.query(By.css('.item-list-title')); + expect(titleField.nativeNode.innerHTML).toEqual('Michel'); + }); + }); + + describe('When the item is orgUnit and has title', () => { + beforeEach(() => { + publicationListElementComponent.object = mockOrgUnit; + fixture.detectChanges(); + }); + + it('should show highlighted title', () => { + const titleField = fixture.debugElement.query(By.css('.item-list-title')); + expect(titleField.nativeNode.innerHTML).toEqual('Science'); + }); + }); + describe('When the item has no title', () => { beforeEach(() => { publicationListElementComponent.object = mockItemWithoutMetadata; diff --git a/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts b/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts index 72120a6b68c..e56b7e970a9 100644 --- a/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts +++ b/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts @@ -33,7 +33,7 @@ export class SearchResultListElementComponent, K exten ngOnInit(): void { if (hasValue(this.object)) { this.dso = this.object.indexableObject; - this.dsoTitle = this.dsoNameService.getName(this.dso); + this.dsoTitle = this.dsoNameService.getHitHighlights(this.object, this.dso); } } diff --git a/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.spec.ts b/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.spec.ts index 897ec43491a..226c1be33ef 100644 --- a/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.spec.ts +++ b/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.spec.ts @@ -11,7 +11,6 @@ import { createSuccessfulRemoteDataObject$ } from '../../remote-data.utils'; import { HALResource } from '../../../core/shared/hal-resource.model'; import { ChildHALResource } from '../../../core/shared/child-hal-resource.model'; import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; -import { DSONameServiceMock } from '../../mocks/dso-name.service.mock'; export function createSidebarSearchListElementTests( componentClass: any, diff --git a/src/app/shared/object.util.ts b/src/app/shared/object.util.ts index 1602fb9839a..4f8954259ef 100644 --- a/src/app/shared/object.util.ts +++ b/src/app/shared/object.util.ts @@ -1,5 +1,7 @@ import { isNotEmpty } from './empty.util'; -import { isEqual, isObject, transform } from 'lodash'; +import isEqual from 'lodash/isEqual'; +import isObject from 'lodash/isObject'; +import transform from 'lodash/transform'; /** * Returns passed object without specified property diff --git a/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.spec.ts b/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.spec.ts index 7db53425d5b..bac6b89583f 100644 --- a/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.spec.ts +++ b/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.spec.ts @@ -11,8 +11,6 @@ import { createSuccessfulRemoteDataObject } from '../remote-data.utils'; import { createPaginatedList } from '../testing/utils.test'; import { ObjectValuesPipe } from '../utils/object-values-pipe'; import { PaginationService } from '../../core/pagination/pagination.service'; -import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; -import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { PaginationServiceStub } from '../testing/pagination-service.stub'; import { FieldUpdates } from '../../core/data/object-updates/field-updates.model'; diff --git a/src/app/shared/pagination/pagination.component.spec.ts b/src/app/shared/pagination/pagination.component.spec.ts index 2d913da8a32..30ace4b2b9b 100644 --- a/src/app/shared/pagination/pagination.component.spec.ts +++ b/src/app/shared/pagination/pagination.component.spec.ts @@ -32,8 +32,7 @@ import { SortDirection, SortOptions } from '../../core/cache/models/sort-options import { createTestComponent } from '../testing/utils.test'; import { storeModuleConfig } from '../../app.reducer'; import { PaginationService } from '../../core/pagination/pagination.service'; -import { BehaviorSubject, of as observableOf } from 'rxjs'; -import { PaginationServiceStub } from '../testing/pagination-service.stub'; +import { BehaviorSubject } from 'rxjs'; import { FindListOptions } from '../../core/data/find-list-options.model'; function expectPages(fixture: ComponentFixture, pagesDef: string[]): void { diff --git a/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.spec.ts b/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.spec.ts index 91d9200c2da..cec67e721c5 100644 --- a/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.spec.ts +++ b/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.spec.ts @@ -4,7 +4,7 @@ import { ChangeDetectorRef, Component, Injector, NO_ERRORS_SCHEMA } from '@angul import { of as observableOf } from 'rxjs'; import { TranslateModule } from '@ngx-translate/core'; import { cold } from 'jasmine-marbles'; -import { uniqueId } from 'lodash'; +import uniqueId from 'lodash/uniqueId'; import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils'; import { createTestComponent } from '../../../testing/utils.test'; @@ -19,10 +19,8 @@ import { PaginationComponentOptions } from '../../../pagination/pagination-compo import { buildPaginatedList } from '../../../../core/data/paginated-list.model'; import { PageInfo } from '../../../../core/shared/page-info.model'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { SortDirection, SortOptions } from '../../../../core/cache/models/sort-options.model'; import { PaginationService } from '../../../../core/pagination/pagination.service'; import { PaginationServiceStub } from '../../../testing/pagination-service.stub'; -import { FindListOptions } from '../../../../core/data/find-list-options.model'; describe('EpersonGroupListComponent test suite', () => { let comp: EpersonGroupListComponent; diff --git a/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts b/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts index b120c3e016a..b8591848455 100644 --- a/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts +++ b/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts @@ -2,7 +2,7 @@ import { Component, EventEmitter, Injector, Input, OnDestroy, OnInit, Output } f import { BehaviorSubject, Observable, Subscription } from 'rxjs'; import { map } from 'rxjs/operators'; -import { uniqueId } from 'lodash'; +import uniqueId from 'lodash/uniqueId'; import { RemoteData } from '../../../../core/data/remote-data'; import { PaginatedList } from '../../../../core/data/paginated-list.model'; diff --git a/src/app/shared/rss-feed/rss.component.spec.ts b/src/app/shared/rss-feed/rss.component.spec.ts index fd7f2c53216..61b54a11251 100644 --- a/src/app/shared/rss-feed/rss.component.spec.ts +++ b/src/app/shared/rss-feed/rss.component.spec.ts @@ -1,5 +1,4 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { ConfigurationDataService } from '../../core/data/configuration-data.service'; import { RemoteData } from '../../core/data/remote-data'; import { GroupDataService } from '../../core/eperson/group-data.service'; @@ -23,7 +22,6 @@ import { RouterMock } from '../mocks/router.mock'; describe('RssComponent', () => { let comp: RSSComponent; - let options: SortOptions; let fixture: ComponentFixture; let uuid: string; let query: string; @@ -63,7 +61,6 @@ describe('RssComponent', () => { pageSize: 10, currentPage: 1 }), - sort: new SortOptions('dc.title', SortDirection.ASC), })); groupDataService = jasmine.createSpyObj('groupsDataService', { findListByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])), @@ -88,7 +85,6 @@ describe('RssComponent', () => { })); beforeEach(() => { - options = new SortOptions('dc.title', SortDirection.DESC); uuid = '2cfcf65e-0a51-4bcb-8592-b8db7b064790'; query = 'test'; fixture = TestBed.createComponent(RSSComponent); @@ -96,18 +92,18 @@ describe('RssComponent', () => { }); it('should formulate the correct url given params in url', () => { - const route = comp.formulateRoute(uuid, 'opensearch', options, query); - expect(route).toBe('/opensearch/search?format=atom&scope=2cfcf65e-0a51-4bcb-8592-b8db7b064790&sort=dc.title&sort_direction=DESC&query=test'); + const route = comp.formulateRoute(uuid, 'opensearch/search', query); + expect(route).toBe('/opensearch/search?format=atom&scope=2cfcf65e-0a51-4bcb-8592-b8db7b064790&query=test'); }); it('should skip uuid if its null', () => { - const route = comp.formulateRoute(null, 'opensearch', options, query); - expect(route).toBe('/opensearch/search?format=atom&sort=dc.title&sort_direction=DESC&query=test'); + const route = comp.formulateRoute(null, 'opensearch/search', query); + expect(route).toBe('/opensearch/search?format=atom&query=test'); }); it('should default to query * if none provided', () => { - const route = comp.formulateRoute(null, 'opensearch', options, null); - expect(route).toBe('/opensearch/search?format=atom&sort=dc.title&sort_direction=DESC&query=*'); + const route = comp.formulateRoute(null, 'opensearch/search', null); + expect(route).toBe('/opensearch/search?format=atom&query=*'); }); }); diff --git a/src/app/shared/rss-feed/rss.component.ts b/src/app/shared/rss-feed/rss.component.ts index 3fdb859bdce..8a33aeeb689 100644 --- a/src/app/shared/rss-feed/rss.component.ts +++ b/src/app/shared/rss-feed/rss.component.ts @@ -12,7 +12,6 @@ import { ConfigurationDataService } from '../../core/data/configuration-data.ser import { getFirstCompletedRemoteData } from '../../core/shared/operators'; import { environment } from '../../../../src/environments/environment'; import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; -import { SortOptions } from '../../core/cache/models/sort-options.model'; import { PaginationService } from '../../core/pagination/pagination.service'; import { Router } from '@angular/router'; import { map, switchMap } from 'rxjs/operators'; @@ -39,7 +38,6 @@ export class RSSComponent implements OnInit, OnDestroy { uuid: string; configuration$: Observable; - sortOption$: Observable; subs: Subscription[] = []; @@ -93,7 +91,7 @@ export class RSSComponent implements OnInit, OnDestroy { return null; } this.uuid = this.groupDataService.getUUIDFromString(this.router.url); - const route = environment.rest.baseUrl + this.formulateRoute(this.uuid, openSearchUri, searchOptions.sort, searchOptions.query); + const route = environment.rest.baseUrl + this.formulateRoute(this.uuid, openSearchUri, searchOptions.query); this.addLinks(route); this.linkHeadService.addTag({ href: environment.rest.baseUrl + '/' + openSearchUri + '/service', @@ -109,24 +107,20 @@ export class RSSComponent implements OnInit, OnDestroy { * Function created a route given the different params available to opensearch * @param uuid The uuid if a scope is present * @param opensearch openSearch uri - * @param sort The sort options for the opensearch request * @param query The query string that was provided in the search * @returns The combine URL to opensearch */ - formulateRoute(uuid: string, opensearch: string, sort: SortOptions, query: string): string { - let route = 'search?format=atom'; + formulateRoute(uuid: string, opensearch: string, query: string): string { + let route = '?format=atom'; if (uuid) { route += `&scope=${uuid}`; } - if (sort && sort.direction && sort.field && sort.field !== 'id') { - route += `&sort=${sort.field}&sort_direction=${sort.direction}`; - } if (query) { route += `&query=${query}`; } else { route += `&query=*`; } - route = '/' + opensearch + '/' + route; + route = '/' + opensearch + route; return route; } diff --git a/src/app/shared/sass-helper/sass-helper.actions.ts b/src/app/shared/sass-helper/css-variable.actions.ts similarity index 51% rename from src/app/shared/sass-helper/sass-helper.actions.ts rename to src/app/shared/sass-helper/css-variable.actions.ts index 144904646ea..2d58a2978b8 100644 --- a/src/app/shared/sass-helper/sass-helper.actions.ts +++ b/src/app/shared/sass-helper/css-variable.actions.ts @@ -1,5 +1,7 @@ +/* eslint-disable max-classes-per-file */ import { Action } from '@ngrx/store'; import { type } from '../ngrx/type'; +import { KeyValuePair } from '../key-value-pair.model'; /** * For each action type in an action group, make a simple @@ -11,6 +13,8 @@ import { type } from '../ngrx/type'; */ export const CSSVariableActionTypes = { ADD: type('dspace/css-variables/ADD'), + ADD_ALL: type('dspace/css-variables/ADD_ALL'), + CLEAR: type('dspace/css-variables/CLEAR'), }; export class AddCSSVariableAction implements Action { @@ -24,5 +28,17 @@ export class AddCSSVariableAction implements Action { this.payload = {name, value}; } } +export class AddAllCSSVariablesAction implements Action { + type = CSSVariableActionTypes.ADD_ALL; + payload: KeyValuePair[]; -export type CSSVariableAction = AddCSSVariableAction; + constructor(variables: KeyValuePair[]) { + this.payload = variables; + } +} + +export class ClearCSSVariablesAction implements Action { + type = CSSVariableActionTypes.CLEAR; +} + +export type CSSVariableAction = AddCSSVariableAction | AddAllCSSVariablesAction | ClearCSSVariablesAction; diff --git a/src/app/shared/sass-helper/css-variable.reducer.ts b/src/app/shared/sass-helper/css-variable.reducer.ts new file mode 100644 index 00000000000..449a936b4e2 --- /dev/null +++ b/src/app/shared/sass-helper/css-variable.reducer.ts @@ -0,0 +1,30 @@ +import { CSSVariableAction, CSSVariableActionTypes } from './css-variable.actions'; +import { KeyValuePair } from '../key-value-pair.model'; + +export interface CSSVariablesState { + [name: string]: string; +} + +const initialState: CSSVariablesState = Object.create({}); + +/** + * Reducer that handles the state of CSS variables in the store + * @param state The current state of the store + * @param action The action to apply onto the current state of the store + */ +export function cssVariablesReducer(state = initialState, action: CSSVariableAction): CSSVariablesState { + switch (action.type) { + case CSSVariableActionTypes.ADD: { + const variable = action.payload; + return Object.assign({}, state, { [variable.name]: variable.value }); + } case CSSVariableActionTypes.ADD_ALL: { + const variables = action.payload; + return Object.assign({}, state, ...variables.map(({ key, value }: KeyValuePair) => {return {[key]: value};})); + } case CSSVariableActionTypes.CLEAR: { + return initialState; + } + default: { + return state; + } + } +} diff --git a/src/app/shared/sass-helper/css-variable.service.spec.ts b/src/app/shared/sass-helper/css-variable.service.spec.ts new file mode 100644 index 00000000000..559384a5d7f --- /dev/null +++ b/src/app/shared/sass-helper/css-variable.service.spec.ts @@ -0,0 +1,78 @@ +import { TestBed } from '@angular/core/testing'; +import { CSSVariableService } from './css-variable.service'; +import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import { getTestScheduler } from 'jasmine-marbles'; +import { buildPaginatedList } from '../../core/data/paginated-list.model'; +import { PageInfo } from '../../core/shared/page-info.model'; +import { KeyValuePair } from '../key-value-pair.model'; + +describe('CSSVariableService', () => { + let store: MockStore; + + let service: CSSVariableService; + let initialState; + const varKey1 = '--test-1'; + const varValue1 = 'test-value-1'; + const varKey2 = '--test-2'; + const varValue2 = 'test-value-2'; + const varKey3 = '--test-3'; + const varValue3 = 'test-value-3'; + const queryInAll = 'test'; + const queryFor3 = '3'; + + function init() { + initialState = { + ['cssVariables']: { + [varKey1]: varValue1, + [varKey2]: varValue2, + [varKey3]: varValue3, + } + }; + } + + beforeEach(() => { + init(); + TestBed.configureTestingModule({ + providers: [ + CSSVariableService, + provideMockStore({ initialState }), + ], + }); + service = TestBed.inject(CSSVariableService as any); + store = TestBed.inject(MockStore as any); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + describe('searchVariable', () => { + it('should return the right keys and variables in a paginated list for query that returns all 3 results', () => { + const currentPage = 1; + const pageSize = 5; + const pageInfo = new PageInfo({ currentPage, elementsPerPage: pageSize, totalPages: 1, totalElements: 3 }); + const page: KeyValuePair[] = [{ key: varKey1, value: varValue1 }, { key: varKey2, value: varValue2 }, { key: varKey3, value: varValue3 }]; + const result = buildPaginatedList(pageInfo, page); + getTestScheduler().expectObservable(service.searchVariable(queryInAll, { currentPage, pageSize } as any)).toBe('a', { a: result }); + }); + + it('should return the right keys and variables in a paginated list for query that returns only the 3rd results', () => { + const currentPage = 1; + const pageSize = 5; + const pageInfo = new PageInfo({ currentPage, elementsPerPage: pageSize, totalPages: 1, totalElements: 1 }); + const page: KeyValuePair[] = [{ key: varKey3, value: varValue3 }]; + const result = buildPaginatedList(pageInfo, page); + getTestScheduler().expectObservable(service.searchVariable(queryFor3, { currentPage, pageSize } as any)).toBe('a', { a: result }); + }); + + it('should return the right keys and variables in a paginated list that\'s not longer than the page size', () => { + const currentPage = 1; + const pageSize = 2; + const pageInfo = new PageInfo({ currentPage, elementsPerPage: pageSize, totalPages: 2, totalElements: 3 }); + const page: KeyValuePair[] = [{ key: varKey1, value: varValue1 }, { key: varKey2, value: varValue2 }]; + const result = buildPaginatedList(pageInfo, page); + getTestScheduler().expectObservable(service.searchVariable(queryInAll, { currentPage, pageSize } as any)).toBe('a', { a: result }); + }); + }); + +}); diff --git a/src/app/shared/sass-helper/css-variable.service.ts b/src/app/shared/sass-helper/css-variable.service.ts new file mode 100644 index 00000000000..0190a05036f --- /dev/null +++ b/src/app/shared/sass-helper/css-variable.service.ts @@ -0,0 +1,161 @@ +import { Injectable } from '@angular/core'; +import { AppState, keySelector } from '../../app.reducer'; +import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; +import { AddAllCSSVariablesAction, AddCSSVariableAction, ClearCSSVariablesAction } from './css-variable.actions'; +import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; +import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model'; +import { Observable } from 'rxjs'; +import { hasValue, isNotEmpty } from '../empty.util'; +import { KeyValuePair } from '../key-value-pair.model'; +import { PageInfo } from '../../core/shared/page-info.model'; +import { CSSVariablesState } from './css-variable.reducer'; + +/** + * This service deals with adding and retrieving CSS variables to and from the store + */ +@Injectable({ + providedIn: 'root' +}) +export class CSSVariableService { + isSameDomain = (styleSheet) => { + // Internal style blocks won't have an href value + if (!styleSheet.href) { + return true; + } + + return styleSheet.href.indexOf(window.location.origin) === 0; + }; + + /* + Determine if the given rule is a CSSStyleRule + See: https://developer.mozilla.org/en-US/docs/Web/API/CSSRule#Type_constants + */ + isStyleRule = (rule) => rule.type === 1; + + constructor( + protected store: Store) { + } + + /** + * Adds a CSS variable to the store + * @param name The name/key of the CSS variable + * @param value The value of the CSS variable + */ + addCSSVariable(name: string, value: string) { + this.store.dispatch(new AddCSSVariableAction(name, value)); + } + + /** + * Adds multiples CSS variables to the store + * @param variables The key-value pairs with the CSS variables to be added + */ + addCSSVariables(variables: KeyValuePair[]) { + this.store.dispatch(new AddAllCSSVariablesAction(variables)); + } + + /** + * Clears all CSS variables ƒrom the store + */ + clearCSSVariables() { + this.store.dispatch(new ClearCSSVariablesAction()); + } + + /** + * Returns the value of a specific CSS key + * @param name The name/key of the CSS value + */ + getVariable(name: string): Observable { + return this.store.pipe(select(themeVariableByNameSelector(name))); + } + + /** + * Returns the CSSVariablesState of the store containing all variables + */ + getAllVariables(): Observable { + return this.store.pipe(select(themeVariablesSelector)); + } + + /** + * Method to find CSS variables by their partially supplying their key. Case sensitive. Returns a paginated list of KeyValuePairs with CSS variables that match the query. + * @param query The query to look for in the keys + * @param paginationOptions The pagination options for the requested page + */ + searchVariable(query: string, paginationOptions: PaginationComponentOptions): Observable>> { + return this.store.pipe(select(themePaginatedVariablesByQuery(query, paginationOptions))); + } + + /** + * Get all custom properties on a page + * @return array> + * ex; [{key: "--color-accent", value: "#b9f500"}, {key: "--color-text", value: "#252525"}, ...] + */ + getCSSVariablesFromStylesheets(document: Document): KeyValuePair[] { + if (isNotEmpty(document.styleSheets)) { + // styleSheets is array-like, so we convert it to an array. + // Filter out any stylesheets not on this domain + return [...document.styleSheets] + .filter(this.isSameDomain) + .reduce( + (finalArr, sheet) => + finalArr.concat( + // cssRules is array-like, so we convert it to an array + [...sheet.cssRules].filter(this.isStyleRule).reduce((propValArr, rule: any) => { + const props = [...rule.style] + .map((propName) => { + return { + key: propName.trim(), + value: rule.style.getPropertyValue(propName).trim() + } as KeyValuePair; + } + ) + // Discard any props that don't start with "--". Custom props are required to. + .filter(({ key }: KeyValuePair) => key.indexOf('--') === 0); + + return [...propValArr, ...props]; + }, []) + ), + [] + ); + } else { + return []; + } + } +} + +const themeVariablesSelector = (state: AppState) => state.cssVariables; + +const themeVariableByNameSelector = (name: string): MemoizedSelector => { + return keySelector(name, themeVariablesSelector); +}; + +// Split this up into two memoized selectors so the query search gets cached separately from the pagination, +// since the entire list has to be retrieved every time anyway +const themePaginatedVariablesByQuery = (query: string, pagination: PaginationComponentOptions): MemoizedSelector>> => { + return createSelector(themeVariablesByQuery(query), (pairs) => { + if (hasValue(pairs)) { + const { currentPage, pageSize } = pagination; + const startIndex = (currentPage - 1) * pageSize; + const endIndex = startIndex + pageSize; + const pairsPage = pairs.slice(startIndex, endIndex); + const totalPages = Math.ceil(pairs.length / pageSize); + const pageInfo = new PageInfo({ currentPage, elementsPerPage: pageSize, totalElements: pairs.length, totalPages }); + return buildPaginatedList(pageInfo, pairsPage); + } else { + return undefined; + } + }); +}; + +const themeVariablesByQuery = (query: string): MemoizedSelector[]> => { + return createSelector(themeVariablesSelector, (state) => { + if (hasValue(state)) { + return Object.keys(state) + .filter((key: string) => key.includes(query)) + .map((key: string) => { + return { key, value: state[key] }; + }); + } else { + return undefined; + } + }); +}; diff --git a/src/app/shared/sass-helper/sass-helper.reducer.ts b/src/app/shared/sass-helper/sass-helper.reducer.ts deleted file mode 100644 index 6f080619fa9..00000000000 --- a/src/app/shared/sass-helper/sass-helper.reducer.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { CSSVariableAction, CSSVariableActionTypes } from './sass-helper.actions'; - -export interface CSSVariablesState { - [name: string]: string; -} - -const initialState: CSSVariablesState = Object.create({}); - -export function cssVariablesReducer(state = initialState, action: CSSVariableAction): CSSVariablesState { - switch (action.type) { - case CSSVariableActionTypes.ADD: { - const variable = action.payload; - const t = Object.assign({}, state, { [variable.name]: variable.value }); - return t; - } - default: { - return state; - } - } -} diff --git a/src/app/shared/sass-helper/sass-helper.service.ts b/src/app/shared/sass-helper/sass-helper.service.ts deleted file mode 100644 index 7cc83dab2d8..00000000000 --- a/src/app/shared/sass-helper/sass-helper.service.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Injectable } from '@angular/core'; -import { AppState, keySelector } from '../../app.reducer'; -import { MemoizedSelector, select, Store } from '@ngrx/store'; -import { AddCSSVariableAction } from './sass-helper.actions'; - -@Injectable() -export class CSSVariableService { - constructor( - protected store: Store) { - } - - addCSSVariable(name: string, value: string) { - this.store.dispatch(new AddCSSVariableAction(name, value)); - } - - getVariable(name: string) { - return this.store.pipe(select(themeVariableByNameSelector(name))); - } - - getAllVariables() { - return this.store.pipe(select(themeVariablesSelector)); - } - -} - -const themeVariablesSelector = (state: AppState) => state.cssVariables; - -const themeVariableByNameSelector = (name: string): MemoizedSelector => { - return keySelector(name, themeVariablesSelector); -}; diff --git a/src/app/shared/search-form/scope-selector-modal/scope-selector-modal.component.html b/src/app/shared/search-form/scope-selector-modal/scope-selector-modal.component.html index bf5c15e9637..e7165a92136 100644 --- a/src/app/shared/search-form/scope-selector-modal/scope-selector-modal.component.html +++ b/src/app/shared/search-form/scope-selector-modal/scope-selector-modal.component.html @@ -9,7 +9,7 @@


- or + {{'dso-selector.' + action + '.' + objectType.toString().toLowerCase() + '.or-divider' | translate}}

diff --git a/src/app/shared/search/search-filters/search-filter/search-filter.reducer.spec.ts b/src/app/shared/search/search-filters/search-filter/search-filter.reducer.spec.ts index ae8108662d0..aa64589d2e0 100644 --- a/src/app/shared/search/search-filters/search-filter/search-filter.reducer.spec.ts +++ b/src/app/shared/search/search-filters/search-filter/search-filter.reducer.spec.ts @@ -1,3 +1,4 @@ +// eslint-disable-next-line import/no-namespace import * as deepFreeze from 'deep-freeze'; import { SearchFilterCollapseAction, diff --git a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html index 49ca6fe3fdd..eb49235641f 100644 --- a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html +++ b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html @@ -29,3 +29,10 @@ ngDefaultControl >