diff --git a/src/app/shared/animations/slide.ts b/src/app/shared/animations/slide.ts index b396333fb4..310ddbbfde 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 c2b414b6f3..94cbd4368a 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 736d39d318..e730b0d85c 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 983fe68274..5576b942b3 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 aa78be9749..22b076c31a 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/authority-confidence/authority-confidence-state.directive.ts b/src/app/shared/authority-confidence/authority-confidence-state.directive.ts index 8999a643e8..bf6f949575 100644 --- a/src/app/shared/authority-confidence/authority-confidence-state.directive.ts +++ b/src/app/shared/authority-confidence/authority-confidence-state.directive.ts @@ -11,7 +11,7 @@ import { SimpleChanges } from '@angular/core'; -import { findIndex } from 'lodash'; +import findIndex from 'lodash/findIndex'; import { VocabularyEntry } from '../../core/submission/vocabularies/models/vocabulary-entry.model'; import { FormFieldMetadataValueObject } from '../form/builder/models/form-field-metadata-value.model'; diff --git a/src/app/shared/chips/chips.component.ts b/src/app/shared/chips/chips.component.ts index 17a6b034ee..94dfa00791 100644 --- a/src/app/shared/chips/chips.component.ts +++ b/src/app/shared/chips/chips.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, } from '@angular/core'; import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; -import { isObject } from 'lodash'; +import isObject from 'lodash/isObject'; import { Chips } from './models/chips.model'; import { ChipsItem } from './models/chips-item.model'; diff --git a/src/app/shared/chips/models/chips-item.model.ts b/src/app/shared/chips/models/chips-item.model.ts index 5d0ff20e2f..9afeafd479 100644 --- a/src/app/shared/chips/models/chips-item.model.ts +++ b/src/app/shared/chips/models/chips-item.model.ts @@ -1,4 +1,5 @@ -import { isObject, uniqueId } from 'lodash'; +import isObject from 'lodash/isObject'; +import uniqueId from 'lodash/uniqueId'; import { hasValue, isNotEmpty } from '../../empty.util'; import { FormFieldMetadataValueObject } from '../../form/builder/models/form-field-metadata-value.model'; import { ConfidenceType } from '../../../core/shared/confidence-type'; diff --git a/src/app/shared/chips/models/chips.model.ts b/src/app/shared/chips/models/chips.model.ts index c15badb976..2979c3dc09 100644 --- a/src/app/shared/chips/models/chips.model.ts +++ b/src/app/shared/chips/models/chips.model.ts @@ -1,4 +1,6 @@ -import { findIndex, isEqual, isObject } from 'lodash'; +import findIndex from 'lodash/findIndex'; +import isEqual from 'lodash/isEqual'; +import isObject from 'lodash/isObject'; import { BehaviorSubject } from 'rxjs'; import { ChipsItem, ChipsItemIcon } from './chips-item.model'; import { hasValue, isNotEmpty } from '../../empty.util'; 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 bc73e4134b..1040e31c57 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/cookies/browser-klaro.service.spec.ts b/src/app/shared/cookies/browser-klaro.service.spec.ts index 9db9caf364..7fd72b54b3 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 c6819012d9..56e371242b 100644 --- a/src/app/shared/cookies/browser-klaro.service.ts +++ b/src/app/shared/cookies/browser-klaro.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import * as Klaro from 'klaro'; +import { setup, show } from 'klaro/dist/klaro-no-translations'; 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 +10,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'; @@ -78,7 +79,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( @@ -135,7 +136,7 @@ export class BrowserKlaroService extends KlaroService { this.klaroConfig.services = this.filterConfigServices(servicesToHide); - Klaro.setup(this.klaroConfig); + setup(this.klaroConfig); }); } @@ -219,7 +220,7 @@ export class BrowserKlaroService extends KlaroService { * Show the cookie consent form */ showSettings() { - Klaro.show(this.klaroConfig); + show(this.klaroConfig); } /** @@ -227,12 +228,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 +247,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 8a9855bd89..a41b641dec 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 0000000000..4576ea497c --- /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 5f7ccb2438..5b74ed02d2 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/file-dropzone-no-uploader/file-dropzone-no-uploader.component.ts b/src/app/shared/file-dropzone-no-uploader/file-dropzone-no-uploader.component.ts index 58960af19e..06636f4256 100644 --- a/src/app/shared/file-dropzone-no-uploader/file-dropzone-no-uploader.component.ts +++ b/src/app/shared/file-dropzone-no-uploader/file-dropzone-no-uploader.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, HostListener, Input, OnInit, Output } from '@angular/core'; -import { uniqueId } from 'lodash'; +import uniqueId from 'lodash/uniqueId'; import { FileUploader } from 'ng2-file-upload'; import { Observable, of as observableOf } from 'rxjs'; import { UploaderOptions } from '../uploader/uploader-options.model'; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.spec.ts index f8bc7ea886..7f0c7e2e35 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.spec.ts @@ -10,7 +10,7 @@ import { } from '@ng-dynamic-forms/core'; import { - mockInputWithTypeBindModel, MockRelationModel, mockDcTypeInputModel + mockInputWithTypeBindModel, MockRelationModel } from '../../../mocks/form-models.mock'; import {DsDynamicTypeBindRelationService} from './ds-dynamic-type-bind-relation.service'; import {FormFieldMetadataValueObject} from '../models/form-field-metadata-value.model'; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-concat.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-concat.model.ts index 1c568dbd32..1d6037a409 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-concat.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-concat.model.ts @@ -1,4 +1,10 @@ -import { DynamicFormControlLayout, DynamicFormGroupModel, DynamicFormGroupModelConfig, serializable } from '@ng-dynamic-forms/core'; +import { + DynamicFormControlLayout, + DynamicFormControlRelation, + DynamicFormGroupModel, + DynamicFormGroupModelConfig, + serializable +} from '@ng-dynamic-forms/core'; import { Subject } from 'rxjs'; @@ -16,6 +22,7 @@ export interface DynamicConcatModelConfig extends DynamicFormGroupModelConfig { separator: string; value?: any; hint?: string; + typeBindRelations?: DynamicFormControlRelation[]; relationship?: RelationshipOptions; repeatable: boolean; required: boolean; @@ -29,6 +36,8 @@ export class DynamicConcatModel extends DynamicFormGroupModel { @serializable() separator: string; @serializable() hasLanguages = false; + @serializable() typeBindRelations: DynamicFormControlRelation[]; + @serializable() typeBindHidden = false; @serializable() relationship?: RelationshipOptions; @serializable() repeatable?: boolean; @serializable() required?: boolean; @@ -55,6 +64,7 @@ export class DynamicConcatModel extends DynamicFormGroupModel { this.metadataValue = config.metadataValue; this.valueUpdates = new Subject(); this.valueUpdates.subscribe((value: string) => this.value = value); + this.typeBindRelations = config.typeBindRelations ? config.typeBindRelations : []; } get value() { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model.ts index d4ddd0c3c8..7cffdfe801 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model.ts @@ -2,6 +2,7 @@ import { Subject } from 'rxjs'; import { DynamicCheckboxGroupModel, DynamicFormControlLayout, + DynamicFormControlRelation, DynamicFormGroupModelConfig, serializable } from '@ng-dynamic-forms/core'; @@ -15,6 +16,7 @@ export interface DynamicListCheckboxGroupModelConfig extends DynamicFormGroupMod groupLength?: number; repeatable: boolean; value?: any; + typeBindRelations?: DynamicFormControlRelation[]; } export class DynamicListCheckboxGroupModel extends DynamicCheckboxGroupModel { @@ -23,6 +25,7 @@ export class DynamicListCheckboxGroupModel extends DynamicCheckboxGroupModel { @serializable() repeatable: boolean; @serializable() groupLength: number; @serializable() _value: VocabularyEntry[]; + @serializable() typeBindRelations: DynamicFormControlRelation[]; isListGroup = true; valueUpdates: Subject; @@ -37,6 +40,7 @@ export class DynamicListCheckboxGroupModel extends DynamicCheckboxGroupModel { this.valueUpdates = new Subject(); this.valueUpdates.subscribe((value: VocabularyEntry | VocabularyEntry[]) => this.value = value); this.valueUpdates.next(config.value); + this.typeBindRelations = config.typeBindRelations ? config.typeBindRelations : []; } get hasAuthority(): boolean { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts index bebef5860e..d44d24f8b8 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts @@ -7,7 +7,7 @@ import { DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dynamic-forms/core'; -import { findKey } from 'lodash'; +import findKey from 'lodash/findKey'; import { hasValue, isNotEmpty } from '../../../../../empty.util'; import { DynamicListCheckboxGroupModel } from './dynamic-list-checkbox-group.model'; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts index cd10b9a4a3..fe495419f0 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts @@ -11,7 +11,8 @@ import { DynamicFormValidationService, DynamicInputModel } from '@ng-dynamic-forms/core'; -import { isEqual, isObject } from 'lodash'; +import isEqual from 'lodash/isEqual'; +import isObject from 'lodash/isObject'; import { DynamicRelationGroupModel } from './dynamic-relation-group.model'; import { FormBuilderService } from '../../../form-builder.service'; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.ts index ef5e84e501..4978af970b 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.ts @@ -5,7 +5,7 @@ import { DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dyna import { Observable, of as observableOf } from 'rxjs'; import { catchError, debounceTime, distinctUntilChanged, map, merge, switchMap, tap } from 'rxjs/operators'; import { NgbTypeahead, NgbTypeaheadSelectItemEvent } from '@ng-bootstrap/ng-bootstrap'; -import { isEqual } from 'lodash'; +import isEqual from 'lodash/isEqual'; import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service'; import { DynamicTagModel } from './dynamic-tag.model'; diff --git a/src/app/shared/form/builder/form-builder.service.ts b/src/app/shared/form/builder/form-builder.service.ts index a281942bc7..b3a78d4669 100644 --- a/src/app/shared/form/builder/form-builder.service.ts +++ b/src/app/shared/form/builder/form-builder.service.ts @@ -18,7 +18,9 @@ import { DynamicPathable, parseReviver, } from '@ng-dynamic-forms/core'; -import { isObject, isString, mergeWith } from 'lodash'; +import isObject from 'lodash/isObject'; +import isString from 'lodash/isString'; +import mergeWith from 'lodash/mergeWith'; import { hasNoValue, diff --git a/src/app/shared/form/builder/models/form-field-previous-value-object.ts b/src/app/shared/form/builder/models/form-field-previous-value-object.ts index ca4a47c089..2aa40f97ae 100644 --- a/src/app/shared/form/builder/models/form-field-previous-value-object.ts +++ b/src/app/shared/form/builder/models/form-field-previous-value-object.ts @@ -1,4 +1,4 @@ -import { isEqual } from 'lodash'; +import isEqual from 'lodash/isEqual'; export class FormFieldPreviousValueObject { diff --git a/src/app/shared/form/builder/parsers/field-parser.ts b/src/app/shared/form/builder/parsers/field-parser.ts index bd6820d4b3..86a7d99e41 100644 --- a/src/app/shared/form/builder/parsers/field-parser.ts +++ b/src/app/shared/form/builder/parsers/field-parser.ts @@ -1,6 +1,6 @@ import {Inject, InjectionToken} from '@angular/core'; -import { uniqueId } from 'lodash'; +import uniqueId from 'lodash/uniqueId'; import {DynamicFormControlLayout, DynamicFormControlRelation, MATCH_VISIBLE, OR_OPERATOR} from '@ng-dynamic-forms/core'; import { hasValue, isNotEmpty, isNotNull, isNotUndefined } from '../../../empty.util'; diff --git a/src/app/shared/form/builder/parsers/row-parser.ts b/src/app/shared/form/builder/parsers/row-parser.ts index 764f52ffdf..2818e37b25 100644 --- a/src/app/shared/form/builder/parsers/row-parser.ts +++ b/src/app/shared/form/builder/parsers/row-parser.ts @@ -1,7 +1,7 @@ import { Injectable, Injector } from '@angular/core'; import { DYNAMIC_FORM_CONTROL_TYPE_ARRAY, DynamicFormGroupModelConfig } from '@ng-dynamic-forms/core'; -import { uniqueId } from 'lodash'; +import uniqueId from 'lodash/uniqueId'; import { isEmpty } from '../../../empty.util'; import { DynamicRowGroupModel } from '../ds-dynamic-form-ui/models/ds-dynamic-row-group-model'; diff --git a/src/app/shared/form/form.component.ts b/src/app/shared/form/form.component.ts index 7c16be7542..086b5e1fd8 100644 --- a/src/app/shared/form/form.component.ts +++ b/src/app/shared/form/form.component.ts @@ -11,7 +11,7 @@ import { DynamicFormLayout, } from '@ng-dynamic-forms/core'; import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { findIndex } from 'lodash'; +import findIndex from 'lodash/findIndex'; import { FormBuilderService } from './builder/form-builder.service'; import { hasValue, isNotEmpty, isNotNull, isNull } from '../empty.util'; diff --git a/src/app/shared/form/form.reducer.ts b/src/app/shared/form/form.reducer.ts index 22094b2e5d..c1e9b12efa 100644 --- a/src/app/shared/form/form.reducer.ts +++ b/src/app/shared/form/form.reducer.ts @@ -11,7 +11,8 @@ import { FormStatusChangeAction } from './form.actions'; import { hasValue } from '../empty.util'; -import { isEqual, uniqWith } from 'lodash'; +import isEqual from 'lodash/isEqual'; +import uniqWith from 'lodash/uniqWith'; export interface FormError { message: string; diff --git a/src/app/shared/form/form.service.ts b/src/app/shared/form/form.service.ts index c4d6003abd..2dbf78f565 100644 --- a/src/app/shared/form/form.service.ts +++ b/src/app/shared/form/form.service.ts @@ -9,7 +9,7 @@ import { formObjectFromIdSelector } from './selectors'; import { FormBuilderService } from './builder/form-builder.service'; import { DynamicFormControlEvent, DynamicFormControlModel, DynamicFormGroupModel } from '@ng-dynamic-forms/core'; import { isEmpty, isNotUndefined } from '../empty.util'; -import { uniqueId } from 'lodash'; +import uniqueId from 'lodash/uniqueId'; import { FormAddError, FormAddTouchedAction, diff --git a/src/app/shared/host-window.reducer.spec.ts b/src/app/shared/host-window.reducer.spec.ts index f0e7aa7245..f580c0e1da 100644 --- a/src/app/shared/host-window.reducer.spec.ts +++ b/src/app/shared/host-window.reducer.spec.ts @@ -1,3 +1,4 @@ +// eslint-disable-next-line import/no-namespace import * as deepFreeze from 'deep-freeze'; import { hostWindowReducer } from './search/host-window.reducer'; import { HostWindowResizeAction } from './host-window.actions'; diff --git a/src/app/shared/interfaces/modal-before-dismiss.interface.ts b/src/app/shared/interfaces/modal-before-dismiss.interface.ts index fca28e1cff..f884432fb8 100644 --- a/src/app/shared/interfaces/modal-before-dismiss.interface.ts +++ b/src/app/shared/interfaces/modal-before-dismiss.interface.ts @@ -1,4 +1,3 @@ -import { NgbModalConfig, NgbModal } from '@ng-bootstrap/ng-bootstrap'; /** * If a component implementing this interface is used to create a modal (i.e. it is passed to {@link NgbModal#open}), 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 c9a60f0c28..78d5b2fc6f 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 af690d198c..25549f53a8 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.reducer.spec.ts b/src/app/shared/menu/menu.reducer.spec.ts index 7ae05536af..2865e887fc 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/shared/mocks/dspace-rest/endpoint-mocking-rest.service.ts b/src/app/shared/mocks/dspace-rest/endpoint-mocking-rest.service.ts index 8d621ad4be..0358451557 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 ce67da1349..f43316c4e1 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/notifications/notifications-board/notifications-board.component.spec.ts b/src/app/shared/notifications/notifications-board/notifications-board.component.spec.ts index 1d3faabdaa..08b9585a8c 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 f153d1009e..97ae09c1a6 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 b834797115..fde92e8891 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 98272d4f43..d37d6a349b 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 7475aac967..f7d00510f6 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 b7f27d1553..e5654e63e0 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); - } - } - 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); + const match = getMatch(typeMap, [viewMode, context, theme], [DEFAULT_VIEW_MODE, DEFAULT_CONTEXT, DEFAULT_THEME]); + if (hasNoValue(currentBestMatch) || currentBestMatch.isLessRelevantThan(match)) { + currentBestMatch = match; } } } - return bestMatch; + 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; + } + } 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 null; } /** 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 897ec43491..226c1be33e 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 1602fb9839..4f8954259e 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 7db53425d5..bac6b89583 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 2d913da8a3..30ace4b2b9 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 91d9200c2d..cec67e721c 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 b120c3e016..b859184845 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 fd7f2c5321..61b54a1125 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 3fdb859bdc..8a33aeeb68 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/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 ae8108662d..aa64589d2e 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-range-filter/search-range-filter.component.spec.ts b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts index 44dda40d15..3a146f5059 100644 --- a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts +++ b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts @@ -31,7 +31,6 @@ describe('SearchRangeFilterComponent', () => { let fixture: ComponentFixture; const minSuffix = '.min'; const maxSuffix = '.max'; - const dateFormats = ['YYYY', 'YYYY-MM', 'YYYY-MM-DD']; const filterName1 = 'test name'; const value1 = '2000 - 2012'; const value2 = '1992 - 2000'; diff --git a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.ts b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.ts index fbd767284f..938f67412e 100644 --- a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.ts +++ b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.ts @@ -15,11 +15,11 @@ import { } from '../../../../../core/shared/search/search-filter.service'; import { SearchService } from '../../../../../core/shared/search/search.service'; import { Router } from '@angular/router'; -import * as moment from 'moment'; import { SEARCH_CONFIG_SERVICE } from '../../../../../my-dspace-page/my-dspace-page.component'; import { SearchConfigurationService } from '../../../../../core/shared/search/search-configuration.service'; import { RouteService } from '../../../../../core/services/route.service'; import { hasValue } from '../../../../empty.util'; +import { yearFromString } from 'src/app/shared/date.util'; /** * The suffix for a range filters' minimum in the frontend URL @@ -31,11 +31,6 @@ export const RANGE_FILTER_MIN_SUFFIX = '.min'; */ export const RANGE_FILTER_MAX_SUFFIX = '.max'; -/** - * The date formats that are possible to appear in a date filter - */ -const dateFormats = ['YYYY', 'YYYY-MM', 'YYYY-MM-DD']; - /** * This component renders a simple item page. * The route parameter 'id' is used to request the item it represents. @@ -99,8 +94,8 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple */ ngOnInit(): void { super.ngOnInit(); - this.min = moment(this.filterConfig.minValue, dateFormats).year() || this.min; - this.max = moment(this.filterConfig.maxValue, dateFormats).year() || this.max; + this.min = yearFromString(this.filterConfig.minValue) || this.min; + this.max = yearFromString(this.filterConfig.maxValue) || this.max; const iniMin = this.route.getQueryParameterValue(this.filterConfig.paramName + RANGE_FILTER_MIN_SUFFIX).pipe(startWith(undefined)); const iniMax = this.route.getQueryParameterValue(this.filterConfig.paramName + RANGE_FILTER_MAX_SUFFIX).pipe(startWith(undefined)); this.sub = observableCombineLatest(iniMin, iniMax).pipe( diff --git a/src/app/shared/search/search-labels/search-label/search-label.component.spec.ts b/src/app/shared/search/search-labels/search-label/search-label.component.spec.ts index 50bcbc6938..b2be2ae53f 100644 --- a/src/app/shared/search/search-labels/search-label/search-label.component.spec.ts +++ b/src/app/shared/search/search-labels/search-label/search-label.component.spec.ts @@ -12,11 +12,9 @@ import { SearchServiceStub } from '../../../testing/search-service.stub'; import { SearchConfigurationServiceStub } from '../../../testing/search-configuration-service.stub'; import { SearchService } from '../../../../core/shared/search/search.service'; import { PaginationComponentOptions } from '../../../pagination/pagination-component-options.model'; -import { SortDirection, SortOptions } from '../../../../core/cache/models/sort-options.model'; import { PaginationService } from '../../../../core/pagination/pagination.service'; import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service'; import { PaginationServiceStub } from '../../../testing/pagination-service.stub'; -import { FindListOptions } from '../../../../core/data/find-list-options.model'; describe('SearchLabelComponent', () => { let comp: SearchLabelComponent; diff --git a/src/app/shared/search/search-switch-configuration/search-switch-configuration.component.ts b/src/app/shared/search/search-switch-configuration/search-switch-configuration.component.ts index 1852277673..0e1b4f221b 100644 --- a/src/app/shared/search/search-switch-configuration/search-switch-configuration.component.ts +++ b/src/app/shared/search/search-switch-configuration/search-switch-configuration.component.ts @@ -10,7 +10,7 @@ import { MyDSpaceConfigurationValueType } from '../../../my-dspace-page/my-dspac import { SearchConfigurationOption } from './search-configuration-option.model'; import { SearchService } from '../../../core/shared/search/search.service'; import { currentPath } from '../../utils/route.utils'; -import { findIndex } from 'lodash'; +import findIndex from 'lodash/findIndex'; @Component({ selector: 'ds-search-switch-configuration', diff --git a/src/app/shared/search/search.component.ts b/src/app/shared/search/search.component.ts index 2abd5290cb..c094e37ef2 100644 --- a/src/app/shared/search/search.component.ts +++ b/src/app/shared/search/search.component.ts @@ -3,7 +3,7 @@ import { Router } from '@angular/router'; import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs'; import { debounceTime, distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators'; -import { uniqueId } from 'lodash'; +import uniqueId from 'lodash/uniqueId'; import { PaginatedList } from '../../core/data/paginated-list.model'; import { RemoteData } from '../../core/data/remote-data'; diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 45e9764151..f723f081d3 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -19,7 +19,6 @@ import { MissingTranslationHandler, TranslateModule } from '@ngx-translate/core' import { NgxPaginationModule } from 'ngx-pagination'; import { FileUploadModule } from 'ng2-file-upload'; import { InfiniteScrollModule } from 'ngx-infinite-scroll'; -import { MomentModule } from 'ngx-moment'; import { ConfirmationModalComponent } from './confirmation-modal/confirmation-modal.component'; import { ExportMetadataSelectorComponent @@ -342,7 +341,6 @@ const MODULES = [ ReactiveFormsModule, RouterModule, NouisliderModule, - MomentModule, DragDropModule, CdkTreeModule, GoogleRecaptchaModule, diff --git a/src/app/shared/sidebar/sidebar-effects.service.ts b/src/app/shared/sidebar/sidebar-effects.service.ts index ba53e2fec9..f6f99ca0fc 100644 --- a/src/app/shared/sidebar/sidebar-effects.service.ts +++ b/src/app/shared/sidebar/sidebar-effects.service.ts @@ -1,7 +1,7 @@ import { map, tap, filter } from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { createEffect, Actions, ofType } from '@ngrx/effects'; -import * as fromRouter from '@ngrx/router-store'; +import { ROUTER_NAVIGATION } from '@ngrx/router-store'; import { SidebarCollapseAction } from './sidebar.actions'; import { URLBaser } from '../../core/url-baser/url-baser'; @@ -14,7 +14,7 @@ export class SidebarEffects { private previousPath: string; routeChange$ = createEffect(() => this.actions$ .pipe( - ofType(fromRouter.ROUTER_NAVIGATION), + ofType(ROUTER_NAVIGATION), filter((action) => this.previousPath !== this.getBaseUrl(action)), tap((action) => { this.previousPath = this.getBaseUrl(action); diff --git a/src/app/shared/sidebar/sidebar.reducer.spec.ts b/src/app/shared/sidebar/sidebar.reducer.spec.ts index 796c40537c..76962f60c1 100644 --- a/src/app/shared/sidebar/sidebar.reducer.spec.ts +++ b/src/app/shared/sidebar/sidebar.reducer.spec.ts @@ -1,3 +1,4 @@ +// eslint-disable-next-line import/no-namespace import * as deepFreeze from 'deep-freeze'; import { sidebarReducer } from './sidebar.reducer'; diff --git a/src/app/shared/starts-with/date/starts-with-date.component.spec.ts b/src/app/shared/starts-with/date/starts-with-date.component.spec.ts index 3cd22a625f..2407f21fdf 100644 --- a/src/app/shared/starts-with/date/starts-with-date.component.spec.ts +++ b/src/app/shared/starts-with/date/starts-with-date.component.spec.ts @@ -11,11 +11,8 @@ import { StartsWithDateComponent } from './starts-with-date.component'; import { ActivatedRouteStub } from '../../testing/active-router.stub'; import { EnumKeysPipe } from '../../utils/enum-keys-pipe'; import { RouterStub } from '../../testing/router.stub'; -import { PaginationComponentOptions } from '../../pagination/pagination-component-options.model'; -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('StartsWithDateComponent', () => { let comp: StartsWithDateComponent; diff --git a/src/app/shared/starts-with/text/starts-with-text.component.spec.ts b/src/app/shared/starts-with/text/starts-with-text.component.spec.ts index c08ef5cfdc..b717c72d76 100644 --- a/src/app/shared/starts-with/text/starts-with-text.component.spec.ts +++ b/src/app/shared/starts-with/text/starts-with-text.component.spec.ts @@ -1,5 +1,5 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { ActivatedRoute, NavigationExtras, Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { CommonModule } from '@angular/common'; import { RouterTestingModule } from '@angular/router/testing'; import { TranslateModule } from '@ngx-translate/core'; @@ -8,12 +8,8 @@ import { EnumKeysPipe } from '../../utils/enum-keys-pipe'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { By } from '@angular/platform-browser'; import { StartsWithTextComponent } from './starts-with-text.component'; -import { PaginationComponentOptions } from '../../pagination/pagination-component-options.model'; -import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model'; -import { of as observableOf } from 'rxjs'; import { PaginationService } from '../../../core/pagination/pagination.service'; import { PaginationServiceStub } from '../../testing/pagination-service.stub'; -import { FindListOptions } from '../../../core/data/find-list-options.model'; describe('StartsWithTextComponent', () => { let comp: StartsWithTextComponent; diff --git a/src/app/shared/testing/group-mock.ts b/src/app/shared/testing/group-mock.ts index a6db4c922e..0d6f924c01 100644 --- a/src/app/shared/testing/group-mock.ts +++ b/src/app/shared/testing/group-mock.ts @@ -1,6 +1,5 @@ import { Group } from '../../core/eperson/models/group.model'; import { EPersonMock } from './eperson.mock'; -import { of } from 'rxjs'; import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils'; export const GroupMock2: Group = Object.assign(new Group(), { diff --git a/src/app/shared/truncatable/truncatable.reducer.spec.ts b/src/app/shared/truncatable/truncatable.reducer.spec.ts index 841ec5e367..9866f382f7 100644 --- a/src/app/shared/truncatable/truncatable.reducer.spec.ts +++ b/src/app/shared/truncatable/truncatable.reducer.spec.ts @@ -1,3 +1,4 @@ +// eslint-disable-next-line import/no-namespace import * as deepFreeze from 'deep-freeze'; import { truncatableReducer } from './truncatable.reducer'; diff --git a/src/app/shared/uploader/uploader.component.ts b/src/app/shared/uploader/uploader.component.ts index a0dd0e5bba..3cbf033b17 100644 --- a/src/app/shared/uploader/uploader.component.ts +++ b/src/app/shared/uploader/uploader.component.ts @@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Ho import { of as observableOf } from 'rxjs'; import { FileUploader } from 'ng2-file-upload'; -import { uniqueId } from 'lodash'; +import uniqueId from 'lodash/uniqueId'; import { ScrollToService } from '@nicky-lenaers/ngx-scroll-to'; import { UploaderOptions } from './uploader-options.model'; diff --git a/src/app/shared/utils/file-size-pipe.ts b/src/app/shared/utils/file-size-pipe.ts index 2d219cdaf4..934f3ee67a 100644 --- a/src/app/shared/utils/file-size-pipe.ts +++ b/src/app/shared/utils/file-size-pipe.ts @@ -1,4 +1,5 @@ import { Pipe, PipeTransform } from '@angular/core'; +// eslint-disable-next-line import/no-namespace import * as fileSize from 'filesize'; /* diff --git a/src/app/shared/utils/markdown.pipe.ts b/src/app/shared/utils/markdown.pipe.ts index f7e1032cac..e494a82613 100644 --- a/src/app/shared/utils/markdown.pipe.ts +++ b/src/app/shared/utils/markdown.pipe.ts @@ -1,9 +1,14 @@ -import { Inject, InjectionToken, Pipe, PipeTransform } from '@angular/core'; -import MarkdownIt from 'markdown-it'; -import * as sanitizeHtml from 'sanitize-html'; +import { Inject, InjectionToken, Pipe, PipeTransform, SecurityContext } from '@angular/core'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { environment } from '../../../environments/environment'; +const markdownItLoader = async () => (await import('markdown-it')).default; +type LazyMarkdownIt = ReturnType; +const MARKDOWN_IT = new InjectionToken( + 'Lazily loaded MarkdownIt', + { providedIn: 'root', factory: markdownItLoader } +); + const mathjaxLoader = async () => (await import('markdown-it-mathjax3')).default; type Mathjax = ReturnType; const MATHJAX = new InjectionToken( @@ -11,6 +16,13 @@ const MATHJAX = new InjectionToken( { providedIn: 'root', factory: mathjaxLoader } ); +const sanitizeHtmlLoader = async () => (await import('sanitize-html') as any).default; +type SanitizeHtml = ReturnType; +const SANITIZE_HTML = new InjectionToken( + 'Lazily loaded sanitize-html', + { providedIn: 'root', factory: sanitizeHtmlLoader } +); + /** * Pipe for rendering markdown and mathjax. * - markdown will only be rendered if {@link MarkdownConfig#enabled} is true @@ -31,7 +43,9 @@ export class MarkdownPipe implements PipeTransform { constructor( protected sanitizer: DomSanitizer, + @Inject(MARKDOWN_IT) private markdownIt: LazyMarkdownIt, @Inject(MATHJAX) private mathjax: Mathjax, + @Inject(SANITIZE_HTML) private sanitizeHtml: SanitizeHtml, ) { } @@ -39,15 +53,17 @@ export class MarkdownPipe implements PipeTransform { if (!environment.markdown.enabled) { return value; } + const MarkdownIt = await this.markdownIt; const md = new MarkdownIt({ html: true, linkify: true, }); + + let html: string; if (environment.markdown.mathjax) { md.use(await this.mathjax); - } - return this.sanitizer.bypassSecurityTrustHtml( - sanitizeHtml(md.render(value), { + const sanitizeHtml = await this.sanitizeHtml; + html = sanitizeHtml(md.render(value), { // sanitize-html doesn't let through SVG by default, so we extend its allowlists to cover MathJax SVG allowedTags: [ ...sanitizeHtml.defaults.allowedTags, @@ -77,7 +93,11 @@ export class MarkdownPipe implements PipeTransform { parser: { lowerCaseAttributeNames: false, }, - }) - ); + }); + } else { + html = this.sanitizer.sanitize(SecurityContext.HTML, md.render(value)); + } + + return this.sanitizer.bypassSecurityTrustHtml(html); } } diff --git a/src/app/shared/vocabulary-treeview/vocabulary-treeview.service.ts b/src/app/shared/vocabulary-treeview/vocabulary-treeview.service.ts index ad2525bb7f..f04cd1bc28 100644 --- a/src/app/shared/vocabulary-treeview/vocabulary-treeview.service.ts +++ b/src/app/shared/vocabulary-treeview/vocabulary-treeview.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable, of as observableOf } from 'rxjs'; import { map, merge, mergeMap, scan } from 'rxjs/operators'; -import { findIndex } from 'lodash'; +import findIndex from 'lodash/findIndex'; import { LOAD_MORE_NODE, diff --git a/src/app/statistics/statistics.service.spec.ts b/src/app/statistics/statistics.service.spec.ts index 76eadb2d31..b573daf476 100644 --- a/src/app/statistics/statistics.service.spec.ts +++ b/src/app/statistics/statistics.service.spec.ts @@ -2,7 +2,7 @@ import { StatisticsService } from './statistics.service'; import { RequestService } from '../core/data/request.service'; import { HALEndpointServiceStub } from '../shared/testing/hal-endpoint-service.stub'; import { getMockRequestService } from '../shared/mocks/request.service.mock'; -import { isEqual } from 'lodash'; +import isEqual from 'lodash/isEqual'; import { DSpaceObjectType } from '../core/shared/dspace-object-type.model'; import { SearchOptions } from '../shared/search/models/search-options.model'; import { RestRequest } from '../core/data/rest-request.model'; diff --git a/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.spec.ts b/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.spec.ts index 4f3c54b642..0f0ee579a1 100644 --- a/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.spec.ts +++ b/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.spec.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; +import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, fakeAsync, inject, TestBed, waitForAsync } from '@angular/core/testing'; import { TranslateModule } from '@ngx-translate/core'; import { createTestComponent } from '../../../shared/testing/utils.test'; diff --git a/src/app/submission/objects/submission-objects.effects.ts b/src/app/submission/objects/submission-objects.effects.ts index 4a7907cab1..98646009d5 100644 --- a/src/app/submission/objects/submission-objects.effects.ts +++ b/src/app/submission/objects/submission-objects.effects.ts @@ -2,7 +2,9 @@ import { Injectable } from '@angular/core'; import { Actions, createEffect, ofType } from '@ngrx/effects'; import { Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; -import { findKey, isEqual, union } from 'lodash'; +import findKey from 'lodash/findKey'; +import isEqual from 'lodash/isEqual'; +import union from 'lodash/union'; import { from as observableFrom, Observable, of as observableOf } from 'rxjs'; import { catchError, filter, map, mergeMap, switchMap, take, tap, withLatestFrom } from 'rxjs/operators'; diff --git a/src/app/submission/objects/submission-objects.reducer.ts b/src/app/submission/objects/submission-objects.reducer.ts index 564e5a701b..a05bf05f52 100644 --- a/src/app/submission/objects/submission-objects.reducer.ts +++ b/src/app/submission/objects/submission-objects.reducer.ts @@ -1,5 +1,8 @@ import { hasValue, isEmpty, isNotEmpty, isNotNull, isUndefined } from '../../shared/empty.util'; -import { differenceWith, findKey, isEqual, uniqWith } from 'lodash'; +import differenceWith from 'lodash/differenceWith'; +import findKey from 'lodash/findKey'; +import isEqual from 'lodash/isEqual'; +import uniqWith from 'lodash/uniqWith'; import { ChangeSubmissionCollectionAction, diff --git a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.html b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.html index 6b9d542abc..0796da5a64 100644 --- a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.html +++ b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.html @@ -81,7 +81,7 @@ - + {{ option.label }} @@ -136,7 +136,7 @@ - {{ 'submission.sections.ccLicense.confirmation' | translate }} + {{ 'submission.sections.ccLicense.confirmation' | translate }}