diff --git a/src/app/root/root.component.spec.ts b/src/app/root/root.component.spec.ts index 504bc34e34..ab148b8ebd 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 472ba440c9..2bbeec6282 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'; @@ -61,10 +61,10 @@ export class RootComponent implements OnInit { } ngOnInit() { - this.sidebarVisible = this.menuService.isMenuVisible(MenuID.ADMIN); + 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-navbar/search-navbar.component.scss b/src/app/search-navbar/search-navbar.component.scss index c1ccd289b2..d5f3d8d615 100644 --- a/src/app/search-navbar/search-navbar.component.scss +++ b/src/app/search-navbar/search-navbar.component.scss @@ -1,9 +1,6 @@ input[type="text"] { margin-top: calc(-0.5 * var(--bs-font-size-base)); - - &:focus { - background-color: rgba(255, 255, 255, 0.5) !important; - } + background-color: #fff !important; &.collapsed { opacity: 0; @@ -14,6 +11,11 @@ a.submit-icon { cursor: pointer; position: sticky; top: 0; + + color: var(--ds-header-icon-color); + &:hover, &:focus { + color: var(--ds-header-icon-color-hover); + } } @media screen and (max-width: map-get($grid-breakpoints, md)) { @@ -22,8 +24,5 @@ a.submit-icon { width: 40vw !important; } - a.submit-icon { - color: var(--bs-link-color); - } } diff --git a/src/app/search-page/search-page.module.ts b/src/app/search-page/search-page.module.ts index 758eca15c0..19fd9bd309 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 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/auth-nav-menu.component.scss b/src/app/shared/auth-nav-menu/auth-nav-menu.component.scss index ac51af27bf..36161ff3da 100644 --- a/src/app/shared/auth-nav-menu/auth-nav-menu.component.scss +++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.scss @@ -13,9 +13,9 @@ } .dropdown-toggle { - color: var(--ds-header-icon-color) !important; + color: var(--ds-header-icon-color); - &:hover, &focus { + &:hover, &:focus { color: var(--ds-header-icon-color-hover); } } 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/browse-by/browse-by.component.html b/src/app/shared/browse-by/browse-by.component.html index 7d9153b9f8..d6a0005173 100644 --- a/src/app/shared/browse-by/browse-by.component.html +++ b/src/app/shared/browse-by/browse-by.component.html @@ -3,7 +3,7 @@
- +
- +
+ + + {{'search.filters.filter.show-tree' | translate: {name: ('search.filters.filter.' + filterConfig.name + '.head' | translate | lowercase )} }} + diff --git a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.spec.ts b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.spec.ts index 9302e66d98..e6c74d8047 100644 --- a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.spec.ts +++ b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.spec.ts @@ -1,155 +1,155 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; import { SearchHierarchyFilterComponent } from './search-hierarchy-filter.component'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { DebugElement, EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { VocabularyService } from '../../../../../core/submission/vocabularies/vocabulary.service'; +import { of as observableOf, BehaviorSubject } from 'rxjs'; +import { RemoteData } from '../../../../../core/data/remote-data'; +import { RequestEntryState } from '../../../../../core/data/request-entry-state.model'; +import { TranslateModule } from '@ngx-translate/core'; +import { RouterStub } from '../../../../testing/router.stub'; +import { buildPaginatedList } from '../../../../../core/data/paginated-list.model'; +import { PageInfo } from '../../../../../core/shared/page-info.model'; +import { CommonModule } from '@angular/common'; import { SearchService } from '../../../../../core/shared/search/search.service'; import { FILTER_CONFIG, IN_PLACE_SEARCH, - REFRESH_FILTER, - SearchFilterService + SearchFilterService, + REFRESH_FILTER } from '../../../../../core/shared/search/search-filter.service'; import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service'; -import { SearchFiltersComponent } from '../../search-filters.component'; import { Router } from '@angular/router'; -import { RouterStub } from '../../../../testing/router.stub'; -import { SearchServiceStub } from '../../../../testing/search-service.stub'; -import { BehaviorSubject, Observable, of as observableOf } from 'rxjs'; +import { NgbModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { SEARCH_CONFIG_SERVICE } from '../../../../../my-dspace-page/my-dspace-page.component'; import { SearchConfigurationServiceStub } from '../../../../testing/search-configuration-service.stub'; +import { VocabularyEntryDetail } from '../../../../../core/submission/vocabularies/models/vocabulary-entry-detail.model'; +import { FacetValue} from '../../../models/facet-value.model'; import { SearchFilterConfig } from '../../../models/search-filter-config.model'; -import { TranslateModule } from '@ngx-translate/core'; -import { - FilterInputSuggestionsComponent -} from '../../../../input-suggestions/filter-suggestions/filter-input-suggestions.component'; -import { FormsModule } from '@angular/forms'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; -import { createSuccessfulRemoteDataObject$ } from '../../../../remote-data.utils'; -import { FacetValue } from '../../../models/facet-value.model'; -import { FilterType } from '../../../models/filter-type.model'; -import { createPaginatedList } from '../../../../testing/utils.test'; -import { RemoteData } from '../../../../../core/data/remote-data'; -import { PaginatedList } from '../../../../../core/data/paginated-list.model'; describe('SearchHierarchyFilterComponent', () => { - let comp: SearchHierarchyFilterComponent; + let fixture: ComponentFixture; - let searchService: SearchService; - let router; + let showVocabularyTreeLink: DebugElement; - const value1 = 'testvalue1'; - const value2 = 'test2'; - const value3 = 'another value3'; - const values: FacetValue[] = [ - { - label: value1, - value: value1, - count: 52, - _links: { - self: { - href: '' - }, - search: { - href: '' - } - } - }, { - label: value2, - value: value2, - count: 20, - _links: { - self: { - href: '' - }, - search: { - href: '' - } - } - }, { - label: value3, - value: value3, - count: 5, - _links: { - self: { - href: '' - }, - search: { - href: '' - } - } - } - ]; - const mockValues = createSuccessfulRemoteDataObject$(createPaginatedList(values)); - - const searchFilterServiceStub = { - getSelectedValuesForFilter(_filterConfig: SearchFilterConfig): Observable { - return observableOf(values.map((value: FacetValue) => value.value)); - }, - getPage(_paramName: string): Observable { - return observableOf(0); - }, - resetPage(_filterName: string): void { - // empty - } + const testSearchLink = 'test-search'; + const testSearchFilter = 'test-search-filter'; + const VocabularyTreeViewComponent = { + select: new EventEmitter(), }; - const remoteDataBuildServiceStub = { - aggregate(_input: Observable>[]): Observable[]>> { - return createSuccessfulRemoteDataObject$([createPaginatedList(values)]); + const searchService = { + getSearchLink: () => testSearchLink, + getFacetValuesFor: () => observableOf([]), + }; + const searchFilterService = { + getPage: () => observableOf(0), + }; + const router = new RouterStub(); + const ngbModal = jasmine.createSpyObj('modal', { + open: { + componentInstance: VocabularyTreeViewComponent, } + }); + const vocabularyService = { + searchTopEntries: () => undefined, }; - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), NoopAnimationsModule, FormsModule], + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + CommonModule, + NgbModule, + TranslateModule.forRoot(), + ], declarations: [ SearchHierarchyFilterComponent, - SearchFiltersComponent, - FilterInputSuggestionsComponent ], providers: [ - { provide: SearchService, useValue: new SearchServiceStub() }, - { provide: SearchFilterService, useValue: searchFilterServiceStub }, - { provide: RemoteDataBuildService, useValue: remoteDataBuildServiceStub }, - { provide: Router, useValue: new RouterStub() }, + { provide: SearchService, useValue: searchService }, + { provide: SearchFilterService, useValue: searchFilterService }, + { provide: RemoteDataBuildService, useValue: {} }, + { provide: Router, useValue: router }, + { provide: NgbModal, useValue: ngbModal }, + { provide: VocabularyService, useValue: vocabularyService }, { provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() }, { provide: IN_PLACE_SEARCH, useValue: false }, - { provide: FILTER_CONFIG, useValue: new SearchFilterConfig() }, - { provide: REFRESH_FILTER, useValue: new BehaviorSubject(false) } + { provide: FILTER_CONFIG, useValue: Object.assign(new SearchFilterConfig(), { name: testSearchFilter }) }, + { provide: REFRESH_FILTER, useValue: new BehaviorSubject(false)} ], - schemas: [NO_ERRORS_SCHEMA] - }).overrideComponent(SearchHierarchyFilterComponent, { - set: { changeDetection: ChangeDetectionStrategy.Default } + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - }) - ; - const mockFilterConfig: SearchFilterConfig = Object.assign(new SearchFilterConfig(), { - name: 'filterName1', - filterType: FilterType.text, - hasFacets: false, - isOpenByDefault: false, - pageSize: 2 }); - beforeEach(async () => { + function init() { fixture = TestBed.createComponent(SearchHierarchyFilterComponent); - comp = fixture.componentInstance; // SearchHierarchyFilterComponent test instance - comp.filterConfig = mockFilterConfig; - searchService = (comp as any).searchService; - // @ts-ignore - spyOn(searchService, 'getFacetValuesFor').and.returnValue(mockValues); - router = (comp as any).router; fixture.detectChanges(); + showVocabularyTreeLink = fixture.debugElement.query(By.css('a#show-test-search-filter-tree')); + } + + describe('if the vocabulary doesn\'t exist', () => { + + beforeEach(() => { + spyOn(vocabularyService, 'searchTopEntries').and.returnValue(observableOf(new RemoteData( + undefined, 0, 0, RequestEntryState.Error, undefined, undefined, 404 + ))); + init(); + }); + + it('should not show the vocabulary tree link', () => { + expect(showVocabularyTreeLink).toBeNull(); + }); }); - it('should navigate to the correct filter with the query operator', () => { - expect((comp as any).searchService.getFacetValuesFor).toHaveBeenCalledWith(comp.filterConfig, 0, {}, null, true); + describe('if the vocabulary exists', () => { - const searchQuery = 'MARVEL'; - comp.onSubmit(searchQuery); + beforeEach(() => { + spyOn(vocabularyService, 'searchTopEntries').and.returnValue(observableOf(new RemoteData( + undefined, 0, 0, RequestEntryState.Success, undefined, buildPaginatedList(new PageInfo(), []), 200 + ))); + init(); + }); - expect(router.navigate).toHaveBeenCalledWith(['', 'search'], Object({ - queryParams: Object({ [mockFilterConfig.paramName]: [...values.map((value: FacetValue) => `${value.value},equals`), `${searchQuery},query`] }), - queryParamsHandling: 'merge' - })); + it('should show the vocabulary tree link', () => { + expect(showVocabularyTreeLink).toBeTruthy(); + }); + + describe('when clicking the vocabulary tree link', () => { + + const alreadySelectedValues = [ + 'already-selected-value-1', + 'already-selected-value-2', + ]; + const newSelectedValue = 'new-selected-value'; + + beforeEach(async () => { + showVocabularyTreeLink.nativeElement.click(); + fixture.componentInstance.selectedValues$ = observableOf( + alreadySelectedValues.map(value => Object.assign(new FacetValue(), { value })) + ); + VocabularyTreeViewComponent.select.emit(Object.assign(new VocabularyEntryDetail(), { + value: newSelectedValue, + })); + }); + + it('should open the vocabulary tree modal', () => { + expect(ngbModal.open).toHaveBeenCalled(); + }); + + describe('when selecting a value from the vocabulary tree', () => { + + it('should add a new search filter to the existing search filters', () => { + waitForAsync(() => expect(router.navigate).toHaveBeenCalledWith([testSearchLink], { + queryParams: { + [`f.${testSearchFilter}`]: [ + ...alreadySelectedValues, + newSelectedValue, + ].map((value => `${value},equals`)), + }, + queryParamsHandling: 'merge', + })); + }); + }); + }); }); }); diff --git a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.ts b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.ts index b3349a5dd9..8504237f09 100644 --- a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.ts +++ b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.ts @@ -1,7 +1,30 @@ -import { Component, OnInit } from '@angular/core'; -import { FilterType } from '../../../models/filter-type.model'; +import { Component, Inject, OnInit } from '@angular/core'; import { renderFacetFor } from '../search-filter-type-decorator'; +import { FilterType } from '../../../models/filter-type.model'; import { facetLoad, SearchFacetFilterComponent } from '../search-facet-filter/search-facet-filter.component'; +import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { VocabularyTreeviewComponent } from '../../../../form/vocabulary-treeview/vocabulary-treeview.component'; +import { + VocabularyEntryDetail +} from '../../../../../core/submission/vocabularies/models/vocabulary-entry-detail.model'; +import { SearchService } from '../../../../../core/shared/search/search.service'; +import { + FILTER_CONFIG, + IN_PLACE_SEARCH, + SearchFilterService, REFRESH_FILTER +} from '../../../../../core/shared/search/search-filter.service'; +import { Router } from '@angular/router'; +import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service'; +import { SEARCH_CONFIG_SERVICE } from '../../../../../my-dspace-page/my-dspace-page.component'; +import { SearchConfigurationService } from '../../../../../core/shared/search/search-configuration.service'; +import { SearchFilterConfig } from '../../../models/search-filter-config.model'; +import { FacetValue } from '../../../models/facet-value.model'; +import { getFacetValueForType } from '../../../search.utils'; +import { filter, map, take } from 'rxjs/operators'; +import { VocabularyService } from '../../../../../core/submission/vocabularies/vocabulary.service'; +import { Observable, BehaviorSubject } from 'rxjs'; +import { PageInfo } from '../../../../../core/shared/page-info.model'; +import { environment } from '../../../../../../environments/environment'; import { addOperatorToFilterValue } from '../../../search.utils'; @Component({ @@ -16,6 +39,23 @@ import { addOperatorToFilterValue } from '../../../search.utils'; */ @renderFacetFor(FilterType.hierarchy) export class SearchHierarchyFilterComponent extends SearchFacetFilterComponent implements OnInit { + + constructor(protected searchService: SearchService, + protected filterService: SearchFilterService, + protected rdbs: RemoteDataBuildService, + protected router: Router, + protected modalService: NgbModal, + protected vocabularyService: VocabularyService, + @Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService, + @Inject(IN_PLACE_SEARCH) public inPlaceSearch: boolean, + @Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig, + @Inject(REFRESH_FILTER) public refreshFilters: BehaviorSubject + ) { + super(searchService, filterService, rdbs, router, searchConfigService, inPlaceSearch, filterConfig, refreshFilters); + } + + vocabularyExists$: Observable; + /** * Submits a new active custom value to the filter from the input field * Overwritten method from parent component, adds the "query" operator to the received data before passing it on @@ -24,4 +64,59 @@ export class SearchHierarchyFilterComponent extends SearchFacetFilterComponent i onSubmit(data: any) { super.onSubmit(addOperatorToFilterValue(data, 'query')); } + + ngOnInit() { + super.ngOnInit(); + this.vocabularyExists$ = this.vocabularyService.searchTopEntries( + this.getVocabularyEntry(), new PageInfo(), true, false, + ).pipe( + filter(rd => rd.hasCompleted), + take(1), + map(rd => { + return rd.hasSucceeded; + }), + ); + } + + /** + * Open the vocabulary tree modal popup. + * When an entry is selected, add the filter query to the search options. + */ + showVocabularyTree() { + const modalRef: NgbModalRef = this.modalService.open(VocabularyTreeviewComponent, { + size: 'lg', + windowClass: 'treeview' + }); + modalRef.componentInstance.vocabularyOptions = { + name: this.getVocabularyEntry(), + closed: true + }; + modalRef.componentInstance.select.subscribe((detail: VocabularyEntryDetail) => { + this.selectedValues$ + .pipe(take(1)) + .subscribe((selectedValues) => { + this.router.navigate( + [this.searchService.getSearchLink()], + { + queryParams: { + [this.filterConfig.paramName]: [...selectedValues, {value: detail.value}] + .map((facetValue: FacetValue) => getFacetValueForType(facetValue, this.filterConfig)), + }, + queryParamsHandling: 'merge', + }, + ); + }); + }); + } + + /** + * Returns the matching vocabulary entry for the given search filter. + * These are configurable in the config file. + */ + getVocabularyEntry() { + const foundVocabularyConfig = environment.vocabularies.filter((v) => v.filter === this.filterConfig.name); + if (foundVocabularyConfig.length > 0 && foundVocabularyConfig[0].enabled === true) { + return foundVocabularyConfig[0].vocabulary; + } + } } 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/search/search.module.ts b/src/app/shared/search/search.module.ts index 426ed82aef..713b9925a6 100644 --- a/src/app/shared/search/search.module.ts +++ b/src/app/shared/search/search.module.ts @@ -31,6 +31,7 @@ import { SearchComponent } from './search.component'; import { ThemedSearchComponent } from './themed-search.component'; import { ThemedSearchResultsComponent } from './search-results/themed-search-results.component'; import { ThemedSearchSettingsComponent } from './search-settings/themed-search-settings.component'; +import { NouisliderModule } from 'ng2-nouislider'; const COMPONENTS = [ SearchComponent, @@ -91,7 +92,8 @@ export const MODELS = [ missingTranslationHandler: { provide: MissingTranslationHandler, useClass: MissingTranslationHelper }, useDefaultLang: true }), - SharedModule.withEntryComponents() + SharedModule.withEntryComponents(), + NouisliderModule, ], exports: [ ...COMPONENTS diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 45e9764151..bd46380452 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -2,24 +2,16 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule } from '@angular/router'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { CdkTreeModule } from '@angular/cdk/tree'; import { DragDropModule } from '@angular/cdk/drag-drop'; - -import { NouisliderModule } from 'ng2-nouislider'; import { - NgbDatepickerModule, NgbDropdownModule, NgbNavModule, NgbPaginationModule, - NgbTimepickerModule, NgbTooltipModule, NgbTypeaheadModule, } from '@ng-bootstrap/ng-bootstrap'; 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 @@ -30,7 +22,6 @@ import { import { ImportBatchSelectorComponent } from './dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component'; -import { FileDropzoneNoUploaderComponent } from './file-dropzone-no-uploader/file-dropzone-no-uploader.component'; import { ItemListElementComponent } from './object-list/item-list-element/item-types/item/item-list-element.component'; import { EnumKeysPipe } from './utils/enum-keys-pipe'; import { FileSizePipe } from './utils/file-size-pipe'; @@ -73,35 +64,13 @@ import { TruncatePipe } from './utils/truncate.pipe'; import { TruncatableComponent } from './truncatable/truncatable.component'; import { TruncatableService } from './truncatable/truncatable.service'; import { TruncatablePartComponent } from './truncatable/truncatable-part/truncatable-part.component'; -import { UploaderComponent } from './uploader/uploader.component'; -import { ChipsComponent } from './chips/chips.component'; -import { NumberPickerComponent } from './number-picker/number-picker.component'; import { MockAdminGuard } from './mocks/admin-guard.service.mock'; import { AlertComponent } from './alert/alert.component'; import { SearchResultDetailElementComponent } from './object-detail/my-dspace-result-detail-element/search-result-detail-element.component'; -import { ClaimedTaskActionsComponent } from './mydspace-actions/claimed-task/claimed-task-actions.component'; -import { PoolTaskActionsComponent } from './mydspace-actions/pool-task/pool-task-actions.component'; import { ObjectDetailComponent } from './object-detail/object-detail.component'; -import { - ItemDetailPreviewComponent -} from './object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component'; -import { - MyDSpaceItemStatusComponent -} from './object-collection/shared/mydspace-item-status/my-dspace-item-status.component'; -import { WorkspaceitemActionsComponent } from './mydspace-actions/workspaceitem/workspaceitem-actions.component'; -import { WorkflowitemActionsComponent } from './mydspace-actions/workflowitem/workflowitem-actions.component'; -import { ItemSubmitterComponent } from './object-collection/shared/mydspace-item-submitter/item-submitter.component'; -import { ItemActionsComponent } from './mydspace-actions/item/item-actions.component'; -import { - ClaimedTaskActionsApproveComponent -} from './mydspace-actions/claimed-task/approve/claimed-task-actions-approve.component'; -import { - ClaimedTaskActionsRejectComponent -} from './mydspace-actions/claimed-task/reject/claimed-task-actions-reject.component'; import { ObjNgFor } from './utils/object-ngfor.pipe'; -import { BrowseByComponent } from './browse-by/browse-by.component'; import { BrowseEntryListElementComponent } from './object-list/browse-entry-list-element/browse-entry-list-element.component'; @@ -111,11 +80,12 @@ import { EmphasizePipe } from './utils/emphasize.pipe'; import { InputSuggestionsComponent } from './input-suggestions/input-suggestions.component'; import { CapitalizePipe } from './utils/capitalize.pipe'; import { ObjectKeysPipe } from './utils/object-keys-pipe'; -import { AuthorityConfidenceStateDirective } from './authority-confidence/authority-confidence-state.directive'; import { LangSwitchComponent } from './lang-switch/lang-switch.component'; import { PlainTextMetadataListElementComponent } from './object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component'; +import { BrowseLinkMetadataListElementComponent } + from './object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component'; import { ItemMetadataListElementComponent } from './object-list/metadata-representation-list-element/item/item-metadata-list-element.component'; @@ -170,21 +140,8 @@ import { import { ThemedEditCollectionSelectorComponent } from './dso-selector/modal-wrappers/edit-collection-selector/themed-edit-collection-selector.component'; -import { - ItemListPreviewComponent -} from './object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component'; -import { - MetadataFieldWrapperComponent -} from '../item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component'; -import { MetadataValuesComponent } from '../item-page/field-components/metadata-values/metadata-values.component'; import { RoleDirective } from './roles/role.directive'; import { UserMenuComponent } from './auth-nav-menu/user-menu/user-menu.component'; -import { - ClaimedTaskActionsReturnToPoolComponent -} from './mydspace-actions/claimed-task/return-to-pool/claimed-task-actions-return-to-pool.component'; -import { - ItemDetailPreviewFieldComponent -} from './object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview-field/item-detail-preview-field.component'; import { CollectionSearchResultGridElementComponent } from './object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component'; @@ -223,42 +180,27 @@ import { } from './object-list/metadata-representation-list-element/item/item-metadata-representation-list-element.component'; import { PageWithSidebarComponent } from './sidebar/page-with-sidebar.component'; import { SidebarDropdownComponent } from './sidebar/sidebar-dropdown.component'; -import { SidebarFilterComponent } from './sidebar/filter/sidebar-filter.component'; -import { SidebarFilterSelectedOptionComponent } from './sidebar/filter/sidebar-filter-selected-option.component'; import { SelectableListItemControlComponent } from './object-collection/shared/selectable-list-item-control/selectable-list-item-control.component'; import { ImportableListItemControlComponent } from './object-collection/shared/importable-list-item-control/importable-list-item-control.component'; -import { ItemVersionsComponent } from './item/item-versions/item-versions.component'; -import { SortablejsModule } from 'ngx-sortablejs'; import { LogInContainerComponent } from './log-in/container/log-in-container.component'; -import { LogInShibbolethComponent } from './log-in/methods/shibboleth/log-in-shibboleth.component'; import { LogInPasswordComponent } from './log-in/methods/password/log-in-password.component'; import { LogInComponent } from './log-in/log-in.component'; -import { BundleListElementComponent } from './object-list/bundle-list-element/bundle-list-element.component'; import { MissingTranslationHelper } from './translate/missing-translation.helper'; -import { ItemVersionsNoticeComponent } from './item/item-versions/notice/item-versions-notice.component'; import { FileValidator } from './utils/require-file.validator'; import { FileValueAccessorDirective } from './utils/file-value-accessor.directive'; -import { FileSectionComponent } from '../item-page/simple/field-components/file-section/file-section.component'; import { ModifyItemOverviewComponent } from '../item-page/edit-item-page/modify-item-overview/modify-item-overview.component'; -import { - ClaimedTaskActionsLoaderComponent -} from './mydspace-actions/claimed-task/switcher/claimed-task-actions-loader.component'; import { ClaimedTaskActionsDirective } from './mydspace-actions/claimed-task/switcher/claimed-task-actions.directive'; -import { - ClaimedTaskActionsEditMetadataComponent -} from './mydspace-actions/claimed-task/edit-metadata/claimed-task-actions-edit-metadata.component'; import { ImpersonateNavbarComponent } from './impersonate-navbar/impersonate-navbar.component'; import { NgForTrackByIdDirective } from './ng-for-track-by-id.directive'; import { FileDownloadLinkComponent } from './file-download-link/file-download-link.component'; import { CollectionDropdownComponent } from './collection-dropdown/collection-dropdown.component'; import { EntityDropdownComponent } from './entity-dropdown/entity-dropdown.component'; -import { VocabularyTreeviewComponent } from './vocabulary-treeview/vocabulary-treeview.component'; import { CurationFormComponent } from '../curation-form/curation-form.component'; import { PublicationSidebarSearchListElementComponent @@ -276,76 +218,52 @@ import { AuthorizedCollectionSelectorComponent } from './dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component'; import { DsoPageEditButtonComponent } from './dso-page/dso-page-edit-button/dso-page-edit-button.component'; -import { DsoPageVersionButtonComponent } from './dso-page/dso-page-version-button/dso-page-version-button.component'; import { HoverClassDirective } from './hover-class.directive'; import { ValidationSuggestionsComponent } from './input-suggestions/validation-suggestions/validation-suggestions.component'; -import { ItemAlertsComponent } from './item/item-alerts/item-alerts.component'; import { ItemSearchResultGridElementComponent } from './object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component'; -import { BitstreamDownloadPageComponent } from './bitstream-download-page/bitstream-download-page.component'; -import { - GenericItemPageFieldComponent -} from '../item-page/simple/field-components/specific-field/generic/generic-item-page-field.component'; -import { - MetadataRepresentationListComponent -} from '../item-page/simple/metadata-representation-list/metadata-representation-list.component'; -import { RelatedItemsComponent } from '../item-page/simple/related-items/related-items-component'; -import { LinkMenuItemComponent } from './menu/menu-item/link-menu-item.component'; -import { OnClickMenuItemComponent } from './menu/menu-item/onclick-menu-item.component'; -import { TextMenuItemComponent } from './menu/menu-item/text-menu-item.component'; import { SearchNavbarComponent } from '../search-navbar/search-navbar.component'; import { ThemedSearchNavbarComponent } from '../search-navbar/themed-search-navbar.component'; -import { - ItemVersionsSummaryModalComponent -} from './item/item-versions/item-versions-summary-modal/item-versions-summary-modal.component'; -import { - ItemVersionsDeleteModalComponent -} from './item/item-versions/item-versions-delete-modal/item-versions-delete-modal.component'; import { ScopeSelectorModalComponent } from './search-form/scope-selector-modal/scope-selector-modal.component'; -import { - BitstreamRequestACopyPageComponent -} from './bitstream-request-a-copy-page/bitstream-request-a-copy-page.component'; import { DsSelectComponent } from './ds-select/ds-select.component'; -import { LogInOidcComponent } from './log-in/methods/oidc/log-in-oidc.component'; -import { ThemedItemListPreviewComponent } from './object-list/my-dspace-result-list-element/item-list-preview/themed-item-list-preview.component'; +import { ContextHelpDirective } from './context-help.directive'; +import { ContextHelpWrapperComponent } from './context-help-wrapper/context-help-wrapper.component'; import { RSSComponent } from './rss-feed/rss.component'; -import { ExternalLinkMenuItemComponent } from './menu/menu-item/external-link-menu-item.component'; -import { DsoPageOrcidButtonComponent } from './dso-page/dso-page-orcid-button/dso-page-orcid-button.component'; -import { LogInOrcidComponent } from './log-in/methods/orcid/log-in-orcid.component'; import { BrowserOnlyPipe } from './utils/browser-only.pipe'; import { ThemedLoadingComponent } from './loading/themed-loading.component'; -import { PersonPageClaimButtonComponent } from './dso-page/person-page-claim-button/person-page-claim-button.component'; import { SearchExportCsvComponent } from './search/search-export-csv/search-export-csv.component'; import { ItemPageTitleFieldComponent } from '../item-page/simple/field-components/specific-field/title/item-page-title-field.component'; import { MarkdownPipe } from './utils/markdown.pipe'; import { GoogleRecaptchaModule } from '../core/google-recaptcha/google-recaptcha.module'; +import { MenuModule } from './menu/menu.module'; +import { + ListableNotificationObjectComponent +} from './object-list/listable-notification-object/listable-notification-object.component'; +import { ThemedCollectionDropdownComponent } from './collection-dropdown/themed-collection-dropdown.component'; +import { MetadataFieldWrapperComponent } from './metadata-field-wrapper/metadata-field-wrapper.component'; +import { LogInExternalProviderComponent } from './log-in/methods/log-in-external-provider/log-in-external-provider.component'; + + const MODULES = [ CommonModule, - SortablejsModule, - FileUploadModule, FormsModule, InfiniteScrollModule, NgbNavModule, - NgbDatepickerModule, - NgbTimepickerModule, NgbTypeaheadModule, - NgxPaginationModule, NgbPaginationModule, NgbDropdownModule, NgbTooltipModule, ReactiveFormsModule, RouterModule, - NouisliderModule, - MomentModule, DragDropModule, - CdkTreeModule, GoogleRecaptchaModule, + MenuModule ]; const ROOT_MODULES = [ @@ -377,16 +295,13 @@ const COMPONENTS = [ AuthNavMenuComponent, ThemedAuthNavMenuComponent, UserMenuComponent, - ChipsComponent, DsSelectComponent, ErrorComponent, - FileSectionComponent, LangSwitchComponent, LoadingComponent, ThemedLoadingComponent, LogInComponent, LogOutComponent, - NumberPickerComponent, ObjectListComponent, ThemedObjectListComponent, ObjectDetailComponent, @@ -398,120 +313,43 @@ const COMPONENTS = [ SearchFormComponent, PageWithSidebarComponent, SidebarDropdownComponent, - SidebarFilterComponent, - SidebarFilterSelectedOptionComponent, ThumbnailComponent, - UploaderComponent, - FileDropzoneNoUploaderComponent, - ItemListPreviewComponent, - ThemedItemListPreviewComponent, - MyDSpaceItemStatusComponent, - ItemSubmitterComponent, - ItemDetailPreviewComponent, - ItemDetailPreviewFieldComponent, - ClaimedTaskActionsComponent, - ClaimedTaskActionsApproveComponent, - ClaimedTaskActionsRejectComponent, - ClaimedTaskActionsReturnToPoolComponent, - ClaimedTaskActionsEditMetadataComponent, - ClaimedTaskActionsLoaderComponent, - ItemActionsComponent, - PoolTaskActionsComponent, - WorkflowitemActionsComponent, - WorkspaceitemActionsComponent, ViewModeSwitchComponent, TruncatableComponent, TruncatablePartComponent, - BrowseByComponent, InputSuggestionsComponent, FilterInputSuggestionsComponent, ValidationSuggestionsComponent, DsoInputSuggestionsComponent, DSOSelectorComponent, - CreateCommunityParentSelectorComponent, - ThemedCreateCommunityParentSelectorComponent, - CreateCollectionParentSelectorComponent, - ThemedCreateCollectionParentSelectorComponent, - CreateItemParentSelectorComponent, - ThemedCreateItemParentSelectorComponent, - EditCommunitySelectorComponent, - ThemedEditCommunitySelectorComponent, - EditCollectionSelectorComponent, - ThemedEditCollectionSelectorComponent, - EditItemSelectorComponent, - ThemedEditItemSelectorComponent, - CommunitySearchResultListElementComponent, - CollectionSearchResultListElementComponent, - BrowseByComponent, - - CollectionSearchResultGridElementComponent, - CommunitySearchResultGridElementComponent, SearchExportCsvComponent, PageSizeSelectorComponent, ListableObjectComponentLoaderComponent, - CollectionListElementComponent, - CommunityListElementComponent, - CollectionGridElementComponent, - CommunityGridElementComponent, - BrowseByComponent, AbstractTrackableComponent, ComcolMetadataComponent, TypeBadgeComponent, AccessStatusBadgeComponent, - BrowseByComponent, - AbstractTrackableComponent, - ItemSelectComponent, CollectionSelectComponent, MetadataRepresentationLoaderComponent, SelectableListItemControlComponent, - ImportableListItemControlComponent, - - LogInShibbolethComponent, - LogInOidcComponent, - LogInOrcidComponent, - LogInPasswordComponent, LogInContainerComponent, - ItemVersionsComponent, - ItemSearchResultListElementComponent, - ItemVersionsNoticeComponent, ModifyItemOverviewComponent, ImpersonateNavbarComponent, - FileDownloadLinkComponent, - BitstreamDownloadPageComponent, - BitstreamRequestACopyPageComponent, - CollectionDropdownComponent, EntityDropdownComponent, ExportMetadataSelectorComponent, ImportBatchSelectorComponent, ExportBatchSelectorComponent, ConfirmationModalComponent, - VocabularyTreeviewComponent, AuthorizedCollectionSelectorComponent, - CurationFormComponent, - SearchResultListElementComponent, - SearchResultGridElementComponent, - ItemListElementComponent, - ItemGridElementComponent, - ItemSearchResultGridElementComponent, - BrowseEntryListElementComponent, - SearchResultDetailElementComponent, - PlainTextMetadataListElementComponent, - ItemMetadataListElementComponent, - MetadataRepresentationListElementComponent, - ItemMetadataRepresentationListElementComponent, - BundleListElementComponent, - StartsWithDateComponent, - StartsWithTextComponent, - SidebarSearchListElementComponent, - PublicationSidebarSearchListElementComponent, - CollectionSidebarSearchListElementComponent, - CommunitySidebarSearchListElementComponent, SearchNavbarComponent, - ScopeSelectorModalComponent, ItemPageTitleFieldComponent, ThemedSearchNavbarComponent, + ListableNotificationObjectComponent, + DsoPageEditButtonComponent, + MetadataFieldWrapperComponent, + ContextHelpWrapperComponent, ]; const ENTRY_COMPONENTS = [ @@ -547,51 +385,26 @@ const ENTRY_COMPONENTS = [ EditItemSelectorComponent, ThemedEditItemSelectorComponent, PlainTextMetadataListElementComponent, + BrowseLinkMetadataListElementComponent, ItemMetadataListElementComponent, MetadataRepresentationListElementComponent, ItemMetadataRepresentationListElementComponent, LogInPasswordComponent, - LogInShibbolethComponent, - LogInOidcComponent, - LogInOrcidComponent, - BundleListElementComponent, - ClaimedTaskActionsApproveComponent, - ClaimedTaskActionsRejectComponent, - ClaimedTaskActionsReturnToPoolComponent, - ClaimedTaskActionsEditMetadataComponent, + LogInExternalProviderComponent, CollectionDropdownComponent, + ThemedCollectionDropdownComponent, FileDownloadLinkComponent, - BitstreamDownloadPageComponent, - BitstreamRequestACopyPageComponent, CurationFormComponent, ExportMetadataSelectorComponent, ImportBatchSelectorComponent, ExportBatchSelectorComponent, ConfirmationModalComponent, - VocabularyTreeviewComponent, SidebarSearchListElementComponent, PublicationSidebarSearchListElementComponent, CollectionSidebarSearchListElementComponent, CommunitySidebarSearchListElementComponent, - LinkMenuItemComponent, - OnClickMenuItemComponent, - TextMenuItemComponent, ScopeSelectorModalComponent, - ExternalLinkMenuItemComponent -]; - -const SHARED_ITEM_PAGE_COMPONENTS = [ - MetadataFieldWrapperComponent, - MetadataValuesComponent, - DsoPageEditButtonComponent, - DsoPageVersionButtonComponent, - PersonPageClaimButtonComponent, - ItemAlertsComponent, - GenericItemPageFieldComponent, - MetadataRepresentationListComponent, - RelatedItemsComponent, - DsoPageOrcidButtonComponent - + ListableNotificationObjectComponent, ]; const PROVIDERS = [ @@ -605,7 +418,6 @@ const DIRECTIVES = [ DragClickDirective, DebounceDirective, ClickOutsideDirective, - AuthorityConfidenceStateDirective, InListValidator, AutoFocusDirective, RoleDirective, @@ -617,7 +429,8 @@ const DIRECTIVES = [ ClaimedTaskActionsDirective, NgForTrackByIdDirective, MetadataFieldValidator, - HoverClassDirective + HoverClassDirective, + ContextHelpDirective, ]; @NgModule({ @@ -628,10 +441,8 @@ const DIRECTIVES = [ declarations: [ ...PIPES, ...COMPONENTS, + ...ENTRY_COMPONENTS, ...DIRECTIVES, - ...SHARED_ITEM_PAGE_COMPONENTS, - ItemVersionsSummaryModalComponent, - ItemVersionsDeleteModalComponent, ], providers: [ ...PROVIDERS @@ -640,9 +451,9 @@ const DIRECTIVES = [ ...MODULES, ...PIPES, ...COMPONENTS, - ...SHARED_ITEM_PAGE_COMPONENTS, + ...ENTRY_COMPONENTS, ...DIRECTIVES, - TranslateModule + TranslateModule, ] }) diff --git a/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.html b/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.html deleted file mode 100644 index bbe0b93566..0000000000 --- a/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.html +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.scss b/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.scss deleted file mode 100644 index 30ab8912d3..0000000000 --- a/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.scss +++ /dev/null @@ -1,11 +0,0 @@ -a { - color: var(--bs-body-color); - - &:hover, &focus { - text-decoration: none; - } - - span.badge { - vertical-align: text-top; - } -} diff --git a/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.ts b/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.ts deleted file mode 100644 index 4f1d2415ae..0000000000 --- a/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; - -@Component({ - selector: 'ds-sidebar-filter-selected-option', - styleUrls: ['./sidebar-filter-selected-option.component.scss'], - templateUrl: './sidebar-filter-selected-option.component.html', -}) - -/** - * Represents a single selected option in a sidebar filter - */ -export class SidebarFilterSelectedOptionComponent { - @Input() label: string; - @Output() click: EventEmitter = new EventEmitter(); -} diff --git a/src/app/shared/sidebar/filter/sidebar-filter.actions.ts b/src/app/shared/sidebar/filter/sidebar-filter.actions.ts deleted file mode 100644 index 5dab677c5c..0000000000 --- a/src/app/shared/sidebar/filter/sidebar-filter.actions.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* eslint-disable max-classes-per-file */ -import { Action } from '@ngrx/store'; - -import { type } from '../../ngrx/type'; - -/** - * For each action type in an action group, make a simple - * enum object for all of this group's action types. - * - * The 'type' utility function coerces strings into string - * literal types and runs a simple check to guarantee all - * action types in the application are unique. - */ -export const SidebarFilterActionTypes = { - INITIALIZE: type('dspace/sidebar-filter/INITIALIZE'), - COLLAPSE: type('dspace/sidebar-filter/COLLAPSE'), - EXPAND: type('dspace/sidebar-filter/EXPAND'), - TOGGLE: type('dspace/sidebar-filter/TOGGLE'), -}; - -export class SidebarFilterAction implements Action { - /** - * Name of the filter the action is performed on, used to identify the filter - */ - filterName: string; - - /** - * Type of action that will be performed - */ - type; - - /** - * Initialize with the filter's name - * @param {string} name of the filter - */ - constructor(name: string) { - this.filterName = name; - } -} - -/** - * Used to initialize a filter - */ -export class FilterInitializeAction extends SidebarFilterAction { - type = SidebarFilterActionTypes.INITIALIZE; - initiallyExpanded; - - constructor(name: string, initiallyExpanded: boolean) { - super(name); - this.initiallyExpanded = initiallyExpanded; - } -} - -/** - * Used to collapse a filter - */ -export class FilterCollapseAction extends SidebarFilterAction { - type = SidebarFilterActionTypes.COLLAPSE; -} - -/** - * Used to expand a filter - */ -export class FilterExpandAction extends SidebarFilterAction { - type = SidebarFilterActionTypes.EXPAND; -} - -/** - * Used to collapse a filter when it's expanded and expand it when it's collapsed - */ -export class FilterToggleAction extends SidebarFilterAction { - type = SidebarFilterActionTypes.TOGGLE; -} diff --git a/src/app/shared/sidebar/filter/sidebar-filter.component.html b/src/app/shared/sidebar/filter/sidebar-filter.component.html deleted file mode 100644 index 79afaa7583..0000000000 --- a/src/app/shared/sidebar/filter/sidebar-filter.component.html +++ /dev/null @@ -1,26 +0,0 @@ -
-
-
- {{ label | translate }} -
- - -
- -
diff --git a/src/app/shared/sidebar/filter/sidebar-filter.component.scss b/src/app/shared/sidebar/filter/sidebar-filter.component.scss deleted file mode 100644 index bf7a089cb1..0000000000 --- a/src/app/shared/sidebar/filter/sidebar-filter.component.scss +++ /dev/null @@ -1,12 +0,0 @@ -:host .facet-filter { - border: 1px solid var(--bs-light); - cursor: pointer; - - .sidebar-filter-wrapper.closed { - overflow: hidden; - } - - .filter-toggle { - line-height: var(--bs-line-height-base); - } -} diff --git a/src/app/shared/sidebar/filter/sidebar-filter.component.ts b/src/app/shared/sidebar/filter/sidebar-filter.component.ts deleted file mode 100644 index 5a019d41df..0000000000 --- a/src/app/shared/sidebar/filter/sidebar-filter.component.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { Observable } from 'rxjs'; -import { SidebarFilterService } from './sidebar-filter.service'; -import { slide } from '../../animations/slide'; - -@Component({ - selector: 'ds-sidebar-filter', - styleUrls: ['./sidebar-filter.component.scss'], - templateUrl: './sidebar-filter.component.html', - animations: [slide], -}) -/** - * This components renders a sidebar filter including the label and the selected values. - * The filter input itself should still be provided in the content. - */ -export class SidebarFilterComponent implements OnInit { - - @Input() name: string; - @Input() type: string; - @Input() label: string; - @Input() expanded = true; - @Input() singleValue = false; - @Input() selectedValues: Observable; - @Output() removeValue: EventEmitter = new EventEmitter(); - - /** - * True when the filter is 100% collapsed in the UI - */ - closed = true; - - /** - * Emits true when the filter is currently collapsed in the store - */ - collapsed$: Observable; - - constructor( - protected filterService: SidebarFilterService, - ) { - } - - /** - * Changes the state for this filter to collapsed when it's expanded and to expanded it when it's collapsed - */ - toggle() { - this.filterService.toggle(this.name); - } - - /** - * Method to change this.collapsed to false when the slide animation ends and is sliding open - * @param event The animation event - */ - finishSlide(event: any): void { - if (event.fromState === 'collapsed') { - this.closed = false; - } - } - - /** - * Method to change this.collapsed to true when the slide animation starts and is sliding closed - * @param event The animation event - */ - startSlide(event: any): void { - if (event.toState === 'collapsed') { - this.closed = true; - } - } - - ngOnInit(): void { - this.closed = !this.expanded; - this.initializeFilter(); - this.collapsed$ = this.isCollapsed(); - } - - /** - * Sets the initial state of the filter - */ - initializeFilter() { - this.filterService.initializeFilter(this.name, this.expanded); - } - - /** - * Checks if the filter is currently collapsed - * @returns {Observable} Emits true when the current state of the filter is collapsed, false when it's expanded - */ - private isCollapsed(): Observable { - return this.filterService.isCollapsed(this.name); - } - -} diff --git a/src/app/shared/sidebar/filter/sidebar-filter.reducer.ts b/src/app/shared/sidebar/filter/sidebar-filter.reducer.ts deleted file mode 100644 index b7784dd12b..0000000000 --- a/src/app/shared/sidebar/filter/sidebar-filter.reducer.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { - FilterInitializeAction, - SidebarFilterAction, - SidebarFilterActionTypes -} from './sidebar-filter.actions'; - -/** - * Interface that represents the state for a single filters - */ -export interface SidebarFilterState { - filterCollapsed: boolean; -} - -/** - * Interface that represents the state for all available filters - */ -export interface SidebarFiltersState { - [name: string]: SidebarFilterState; -} - -const initialState: SidebarFiltersState = Object.create(null); - -/** - * Performs a filter action on the current state - * @param {SidebarFiltersState} state The state before the action is performed - * @param {SidebarFilterAction} action The action that should be performed - * @returns {SidebarFiltersState} The state after the action is performed - */ -export function sidebarFilterReducer(state = initialState, action: SidebarFilterAction): SidebarFiltersState { - - switch (action.type) { - - case SidebarFilterActionTypes.INITIALIZE: { - const initAction = (action as FilterInitializeAction); - return Object.assign({}, state, { - [action.filterName]: { - filterCollapsed: !initAction.initiallyExpanded, - } - }); - } - - case SidebarFilterActionTypes.COLLAPSE: { - return Object.assign({}, state, { - [action.filterName]: { - filterCollapsed: true, - } - }); - } - - case SidebarFilterActionTypes.EXPAND: { - return Object.assign({}, state, { - [action.filterName]: { - filterCollapsed: false, - } - }); - } - - case SidebarFilterActionTypes.TOGGLE: { - return Object.assign({}, state, { - [action.filterName]: { - filterCollapsed: !state[action.filterName].filterCollapsed, - } - }); - } - - default: { - return state; - } - } -} diff --git a/src/app/shared/sidebar/filter/sidebar-filter.service.spec.ts b/src/app/shared/sidebar/filter/sidebar-filter.service.spec.ts deleted file mode 100644 index 49192a2006..0000000000 --- a/src/app/shared/sidebar/filter/sidebar-filter.service.spec.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { TestBed, waitForAsync } from '@angular/core/testing'; -import { Store, StoreModule } from '@ngrx/store'; -import { provideMockStore } from '@ngrx/store/testing'; -import { cold } from 'jasmine-marbles'; - -import { sidebarFilterReducer } from './sidebar-filter.reducer'; -import { SidebarFilterService } from './sidebar-filter.service'; -import { - FilterCollapseAction, - FilterExpandAction, - FilterInitializeAction, - FilterToggleAction -} from './sidebar-filter.actions'; -import { storeModuleConfig } from '../../../app.reducer'; - -describe('SidebarFilterService', () => { - let service: SidebarFilterService; - let store: any; - let initialState; - - function init() { - - initialState = { - sidebarFilter: { - filter_1: { - filterCollapsed: true - }, - filter_2: { - filterCollapsed: false - }, - filter_3: { - filterCollapsed: true - } - } - }; - - } - - beforeEach(waitForAsync(() => { - init(); - TestBed.configureTestingModule({ - imports: [ - StoreModule.forRoot({ sidebarFilter: sidebarFilterReducer }, storeModuleConfig) - ], - providers: [ - provideMockStore({ initialState }), - { provide: SidebarFilterService, useValue: service } - ] - }).compileComponents(); - })); - - beforeEach(() => { - store = TestBed.inject(Store); - service = new SidebarFilterService(store); - spyOn(store, 'dispatch'); - }); - - describe('initializeFilter', () => { - it('should dispatch an FilterInitializeAction with the correct arguments', () => { - service.initializeFilter('fakeFilter', true); - expect(store.dispatch).toHaveBeenCalledWith(new FilterInitializeAction('fakeFilter', true)); - }); - }); - - describe('collapse', () => { - it('should dispatch an FilterInitializeAction with the correct arguments', () => { - service.collapse('fakeFilter'); - expect(store.dispatch).toHaveBeenCalledWith(new FilterCollapseAction('fakeFilter')); - }); - }); - - describe('expand', () => { - it('should dispatch an FilterInitializeAction with the correct arguments', () => { - service.expand('fakeFilter'); - expect(store.dispatch).toHaveBeenCalledWith(new FilterExpandAction('fakeFilter')); - }); - }); - - describe('toggle', () => { - it('should dispatch an FilterInitializeAction with the correct arguments', () => { - service.toggle('fakeFilter'); - expect(store.dispatch).toHaveBeenCalledWith(new FilterToggleAction('fakeFilter')); - }); - }); - - describe('isCollapsed', () => { - it('should return true', () => { - - const result = service.isCollapsed('filter_1'); - const expected = cold('b', { - b: true - }); - - expect(result).toBeObservable(expected); - }); - - it('should return false', () => { - - const result = service.isCollapsed('filter_2'); - const expected = cold('b', { - b: false - }); - - expect(result).toBeObservable(expected); - }); - }); -}); diff --git a/src/app/shared/sidebar/filter/sidebar-filter.service.ts b/src/app/shared/sidebar/filter/sidebar-filter.service.ts deleted file mode 100644 index b67de24f9e..0000000000 --- a/src/app/shared/sidebar/filter/sidebar-filter.service.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Injectable } from '@angular/core'; -import { - FilterCollapseAction, - FilterExpandAction, FilterInitializeAction, - FilterToggleAction -} from './sidebar-filter.actions'; -import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; -import { SidebarFiltersState, SidebarFilterState } from './sidebar-filter.reducer'; -import { Observable } from 'rxjs'; -import { distinctUntilChanged, map } from 'rxjs/operators'; -import { hasValue } from '../../empty.util'; - -/** - * Service that performs all actions that have to do with sidebar filters like collapsing or expanding them. - */ -@Injectable() -export class SidebarFilterService { - - constructor(private store: Store) { - } - - /** - * Dispatches an initialize action to the store for a given filter - * @param {string} filter The filter for which the action is dispatched - * @param {boolean} expanded If the filter should be open from the start - */ - public initializeFilter(filter: string, expanded: boolean): void { - this.store.dispatch(new FilterInitializeAction(filter, expanded)); - } - - /** - * Dispatches a collapse action to the store for a given filter - * @param {string} filterName The filter for which the action is dispatched - */ - public collapse(filterName: string): void { - this.store.dispatch(new FilterCollapseAction(filterName)); - } - - /** - * Dispatches an expand action to the store for a given filter - * @param {string} filterName The filter for which the action is dispatched - */ - public expand(filterName: string): void { - this.store.dispatch(new FilterExpandAction(filterName)); - } - - /** - * Dispatches a toggle action to the store for a given filter - * @param {string} filterName The filter for which the action is dispatched - */ - public toggle(filterName: string): void { - this.store.dispatch(new FilterToggleAction(filterName)); - } - - /** - * Checks if the state of a given filter is currently collapsed or not - * @param {string} filterName The filtername for which the collapsed state is checked - * @returns {Observable} Emits the current collapsed state of the given filter, if it's unavailable, return false - */ - isCollapsed(filterName: string): Observable { - return this.store.pipe( - select(filterByNameSelector(filterName)), - map((object: SidebarFilterState) => { - if (object) { - return object.filterCollapsed; - } else { - return false; - } - }), - distinctUntilChanged() - ); - } - -} - -const filterStateSelector = (state: SidebarFiltersState) => state.sidebarFilter; - -function filterByNameSelector(name: string): MemoizedSelector { - return keySelector(name); -} - -export function keySelector(key: string): MemoizedSelector { - return createSelector(filterStateSelector, (state: SidebarFilterState) => { - if (hasValue(state)) { - return state[key]; - } else { - return undefined; - } - }); -} 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/browse-definition-data-service.stub.ts b/src/app/shared/testing/browse-definition-data-service.stub.ts new file mode 100644 index 0000000000..ec1fc2f05e --- /dev/null +++ b/src/app/shared/testing/browse-definition-data-service.stub.ts @@ -0,0 +1,63 @@ +import { EMPTY, Observable, of as observableOf } from 'rxjs'; +import { RemoteData } from '../../core/data/remote-data'; +import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model'; +import { BrowseDefinition } from '../../core/shared/browse-definition.model'; +import { BrowseService } from '../../core/browse/browse.service'; +import { createSuccessfulRemoteDataObject } from '../remote-data.utils'; +import { PageInfo } from '../../core/shared/page-info.model'; + +// This data is in post-serialized form (metadata -> metadataKeys) +export const mockData: BrowseDefinition[] = [ + Object.assign(new BrowseDefinition, { + 'id' : 'dateissued', + 'metadataBrowse' : false, + 'dataType' : 'date', + 'sortOptions' : EMPTY, + 'order' : 'ASC', + 'type' : 'browse', + 'metadataKeys' : [ 'dc.date.issued' ], + '_links' : EMPTY + }), + Object.assign(new BrowseDefinition, { + 'id' : 'author', + 'metadataBrowse' : true, + 'dataType' : 'text', + 'sortOptions' : EMPTY, + 'order' : 'ASC', + 'type' : 'browse', + 'metadataKeys' : [ 'dc.contributor.*', 'dc.creator' ], + '_links' : EMPTY + }) +]; + +export const BrowseDefinitionDataServiceStub: any = { + + /** + * Get all BrowseDefinitions + */ + findAll(): Observable>> { + return observableOf(createSuccessfulRemoteDataObject(buildPaginatedList(new PageInfo(), mockData))); + }, + + /** + * Get all BrowseDefinitions with any link configuration + */ + findAllLinked(): Observable>> { + return observableOf(createSuccessfulRemoteDataObject(buildPaginatedList(new PageInfo(), mockData))); + }, + + /** + * Get the browse URL by providing a list of metadata keys + * + * @param metadataKeys a list of fields eg. ['dc.contributor.author', 'dc.creator'] + */ + findByFields(metadataKeys: string[]): Observable> { + let searchKeyArray: string[] = []; + metadataKeys.forEach((metadataKey) => { + searchKeyArray = searchKeyArray.concat(BrowseService.toSearchKeyArray(metadataKey)); + }); + // Return just the first, as a pretend match + return observableOf(createSuccessfulRemoteDataObject(mockData[0])); + } + +}; diff --git a/src/app/shared/testing/css-variable-service.stub.ts b/src/app/shared/testing/css-variable-service.stub.ts index 6159d89655..2f5c647945 100644 --- a/src/app/shared/testing/css-variable-service.stub.ts +++ b/src/app/shared/testing/css-variable-service.stub.ts @@ -1,10 +1,11 @@ import { Observable, of as observableOf } from 'rxjs'; +import { KeyValuePair } from '../key-value-pair.model'; const variables = { - smMin: '576px,', - mdMin: '768px,', - lgMin: '992px', - xlMin: '1200px', + '--bs-sm-min': '576px,', + '--bs-md-min': '768px,', + '--bs-lg-min': '992px', + '--bs-xl-min': '1200px', } as any; export class CSSVariableServiceStub { @@ -19,4 +20,16 @@ export class CSSVariableServiceStub { addCSSVariable(name: string, value: string): void { /**/ } + + addCSSVariables(variablesToAdd: KeyValuePair[]): void { + /**/ + } + + clearCSSVariables(): void { + /**/ + } + + getCSSVariablesFromStylesheets(document: Document): void { + /**/ + } } 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/testing/menu-service.stub.ts b/src/app/shared/testing/menu-service.stub.ts index 926232bad0..71ee777157 100644 --- a/src/app/shared/testing/menu-service.stub.ts +++ b/src/app/shared/testing/menu-service.stub.ts @@ -66,6 +66,10 @@ export class MenuServiceStub { return observableOf(true); } + isMenuVisibleWithVisibleSections(id: MenuID): Observable { + return observableOf(true); + } + isMenuCollapsed(id: MenuID): Observable { return observableOf(false); } diff --git a/src/app/shared/theme-support/themed.component.spec.ts b/src/app/shared/theme-support/themed.component.spec.ts index 404e970a7a..7776e60379 100644 --- a/src/app/shared/theme-support/themed.component.spec.ts +++ b/src/app/shared/theme-support/themed.component.spec.ts @@ -71,6 +71,12 @@ describe('ThemedComponent', () => { expect((component as any).compRef.instance.testInput).toEqual('changed'); }); })); + + it(`should set usedTheme to the name of the matched theme`, waitForAsync(() => { + fixture.whenStable().then(() => { + expect(component.usedTheme).toEqual('custom'); + }); + })); }); describe('when the current theme doesn\'t match a themed component', () => { @@ -92,6 +98,12 @@ describe('ThemedComponent', () => { expect((component as any).compRef.instance.testInput).toEqual('changed'); }); })); + + it(`should set usedTheme to the name of the base theme`, waitForAsync(() => { + fixture.whenStable().then(() => { + expect(component.usedTheme).toEqual('base'); + }); + })); }); describe('and it extends another theme', () => { @@ -117,6 +129,12 @@ describe('ThemedComponent', () => { expect((component as any).compRef.instance.testInput).toEqual('changed'); }); })); + + it(`should set usedTheme to the name of the base theme`, waitForAsync(() => { + fixture.whenStable().then(() => { + expect(component.usedTheme).toEqual('base'); + }); + })); }); describe('that does match it', () => { @@ -141,6 +159,12 @@ describe('ThemedComponent', () => { expect((component as any).compRef.instance.testInput).toEqual('changed'); }); })); + + it(`should set usedTheme to the name of the matched theme`, waitForAsync(() => { + fixture.whenStable().then(() => { + expect(component.usedTheme).toEqual('custom'); + }); + })); }); describe('that extends another theme that doesn\'t match it either', () => { @@ -167,6 +191,12 @@ describe('ThemedComponent', () => { expect((component as any).compRef.instance.testInput).toEqual('changed'); }); })); + + it(`should set usedTheme to the name of the base theme`, waitForAsync(() => { + fixture.whenStable().then(() => { + expect(component.usedTheme).toEqual('base'); + }); + })); }); describe('that extends another theme that does match it', () => { @@ -193,6 +223,12 @@ describe('ThemedComponent', () => { expect((component as any).compRef.instance.testInput).toEqual('changed'); }); })); + + it(`should set usedTheme to the name of the matched theme`, waitForAsync(() => { + fixture.whenStable().then(() => { + expect(component.usedTheme).toEqual('custom'); + }); + })); }); }); }); diff --git a/src/app/shared/theme-support/themed.component.ts b/src/app/shared/theme-support/themed.component.ts index 87f182a5ff..995122d284 100644 --- a/src/app/shared/theme-support/themed.component.ts +++ b/src/app/shared/theme-support/themed.component.ts @@ -8,13 +8,15 @@ import { OnDestroy, ComponentFactoryResolver, ChangeDetectorRef, - OnChanges + OnChanges, + HostBinding } from '@angular/core'; import { hasValue, isNotEmpty } from '../empty.util'; -import { from as fromPromise, Observable, of as observableOf, Subscription } from 'rxjs'; +import { from as fromPromise, Observable, of as observableOf, Subscription, BehaviorSubject } from 'rxjs'; import { ThemeService } from './theme.service'; -import { catchError, switchMap, map } from 'rxjs/operators'; +import { catchError, switchMap, map, tap } from 'rxjs/operators'; import { GenericConstructor } from '../../core/shared/generic-constructor'; +import { BASE_THEME_NAME } from './theme.constants'; @Component({ selector: 'ds-themed', @@ -25,11 +27,22 @@ export abstract class ThemedComponent implements OnInit, OnDestroy, OnChanges @ViewChild('vcr', { read: ViewContainerRef }) vcr: ViewContainerRef; protected compRef: ComponentRef; + /** + * A reference to the themed component. Will start as undefined and emit every time the themed + * component is rendered + */ + public compRef$: BehaviorSubject> = new BehaviorSubject(undefined); + protected lazyLoadSub: Subscription; protected themeSub: Subscription; protected inAndOutputNames: (keyof T & keyof this)[] = []; + /** + * A data attribute on the ThemedComponent to indicate which theme the rendered component came from. + */ + @HostBinding('attr.data-used-theme') usedTheme: string; + constructor( protected resolver: ComponentFactoryResolver, protected cdr: ChangeDetectorRef, @@ -80,6 +93,7 @@ export abstract class ThemedComponent implements OnInit, OnDestroy, OnChanges } else { // otherwise import and return the default component return fromPromise(this.importUnthemedComponent()).pipe( + tap(() => this.usedTheme = BASE_THEME_NAME), map((unthemedFile: any) => { return unthemedFile[this.getComponentName()]; }) @@ -90,6 +104,7 @@ export abstract class ThemedComponent implements OnInit, OnDestroy, OnChanges const factory = this.resolver.resolveComponentFactory(constructor); this.compRef = this.vcr.createComponent(factory); this.connectInputsAndOutputs(); + this.compRef$.next(this.compRef); this.cdr.markForCheck(); }); } @@ -123,6 +138,7 @@ export abstract class ThemedComponent implements OnInit, OnDestroy, OnChanges private resolveThemedComponent(themeName?: string, checkedThemeNames: string[] = []): Observable { if (isNotEmpty(themeName)) { return fromPromise(this.importThemedComponent(themeName)).pipe( + tap(() => this.usedTheme = themeName), catchError(() => { // Try the next ancestor theme instead const nextTheme = this.themeService.getThemeConfigFor(themeName)?.extends; 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/file-dropzone-no-uploader/file-dropzone-no-uploader.component.html b/src/app/shared/upload/file-dropzone-no-uploader/file-dropzone-no-uploader.component.html similarity index 100% rename from src/app/shared/file-dropzone-no-uploader/file-dropzone-no-uploader.component.html rename to src/app/shared/upload/file-dropzone-no-uploader/file-dropzone-no-uploader.component.html diff --git a/src/app/shared/file-dropzone-no-uploader/file-dropzone-no-uploader.component.ts b/src/app/shared/upload/file-dropzone-no-uploader/file-dropzone-no-uploader.component.ts similarity index 98% rename from src/app/shared/file-dropzone-no-uploader/file-dropzone-no-uploader.component.ts rename to src/app/shared/upload/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/upload/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/file-dropzone-no-uploader/file-dropzone-no-uploader.scss b/src/app/shared/upload/file-dropzone-no-uploader/file-dropzone-no-uploader.scss similarity index 100% rename from src/app/shared/file-dropzone-no-uploader/file-dropzone-no-uploader.scss rename to src/app/shared/upload/file-dropzone-no-uploader/file-dropzone-no-uploader.scss diff --git a/src/app/shared/upload/upload.module.ts b/src/app/shared/upload/upload.module.ts new file mode 100644 index 0000000000..9f2895d7ac --- /dev/null +++ b/src/app/shared/upload/upload.module.ts @@ -0,0 +1,38 @@ +/** + * 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 { FileUploadModule } from 'ng2-file-upload'; +import { UploaderComponent } from './uploader/uploader.component'; +import { FileDropzoneNoUploaderComponent } from './file-dropzone-no-uploader/file-dropzone-no-uploader.component'; + +const COMPONENTS = [ + UploaderComponent, + FileDropzoneNoUploaderComponent, +]; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + FileUploadModule, + ], + declarations: [ + ...COMPONENTS, + ], + providers: [ + ...COMPONENTS, + ], + exports: [ + ...COMPONENTS, + FileUploadModule, + ] +}) +export class UploadModule { +} diff --git a/src/app/shared/uploader/uploader-error.model.ts b/src/app/shared/upload/uploader/uploader-error.model.ts similarity index 100% rename from src/app/shared/uploader/uploader-error.model.ts rename to src/app/shared/upload/uploader/uploader-error.model.ts diff --git a/src/app/shared/uploader/uploader-options.model.ts b/src/app/shared/upload/uploader/uploader-options.model.ts similarity index 86% rename from src/app/shared/uploader/uploader-options.model.ts rename to src/app/shared/upload/uploader/uploader-options.model.ts index 959e5c3295..559fb0485b 100644 --- a/src/app/shared/uploader/uploader-options.model.ts +++ b/src/app/shared/upload/uploader/uploader-options.model.ts @@ -1,4 +1,4 @@ -import { RestRequestMethod } from '../../core/data/rest-request-method'; +import { RestRequestMethod } from '../../../core/data/rest-request-method'; export class UploaderOptions { /** diff --git a/src/app/shared/uploader/uploader-properties.model.ts b/src/app/shared/upload/uploader/uploader-properties.model.ts similarity index 83% rename from src/app/shared/uploader/uploader-properties.model.ts rename to src/app/shared/upload/uploader/uploader-properties.model.ts index bc0376b809..b84ae30bf8 100644 --- a/src/app/shared/uploader/uploader-properties.model.ts +++ b/src/app/shared/upload/uploader/uploader-properties.model.ts @@ -1,4 +1,4 @@ -import { MetadataMap } from '../../core/shared/metadata.models'; +import { MetadataMap } from '../../../core/shared/metadata.models'; /** * Properties to send to the REST API for uploading a bitstream diff --git a/src/app/shared/uploader/uploader.component.html b/src/app/shared/upload/uploader/uploader.component.html similarity index 100% rename from src/app/shared/uploader/uploader.component.html rename to src/app/shared/upload/uploader/uploader.component.html diff --git a/src/app/shared/uploader/uploader.component.scss b/src/app/shared/upload/uploader/uploader.component.scss similarity index 100% rename from src/app/shared/uploader/uploader.component.scss rename to src/app/shared/upload/uploader/uploader.component.scss diff --git a/src/app/shared/uploader/uploader.component.spec.ts b/src/app/shared/upload/uploader/uploader.component.spec.ts similarity index 86% rename from src/app/shared/uploader/uploader.component.spec.ts rename to src/app/shared/upload/uploader/uploader.component.spec.ts index 84fee2e147..8ea23c8acb 100644 --- a/src/app/shared/uploader/uploader.component.spec.ts +++ b/src/app/shared/upload/uploader/uploader.component.spec.ts @@ -4,16 +4,16 @@ import { ComponentFixture, inject, TestBed, waitForAsync, } from '@angular/core/ import { ScrollToService } from '@nicky-lenaers/ngx-scroll-to'; -import { UploaderService } from './uploader.service'; +import { DragService } from '../../../core/drag.service'; import { UploaderOptions } from './uploader-options.model'; import { UploaderComponent } from './uploader.component'; import { FileUploadModule } from 'ng2-file-upload'; import { TranslateModule } from '@ngx-translate/core'; -import { createTestComponent } from '../testing/utils.test'; +import { createTestComponent } from '../../testing/utils.test'; import { HttpXsrfTokenExtractor } from '@angular/common/http'; -import { CookieService } from '../../core/services/cookie.service'; -import { CookieServiceMock } from '../mocks/cookie.service.mock'; -import { HttpXsrfTokenExtractorMock } from '../mocks/http-xsrf-token-extractor.mock'; +import { CookieService } from '../../../core/services/cookie.service'; +import { CookieServiceMock } from '../../mocks/cookie.service.mock'; +import { HttpXsrfTokenExtractorMock } from '../../mocks/http-xsrf-token-extractor.mock'; describe('Chips component', () => { @@ -37,7 +37,7 @@ describe('Chips component', () => { ChangeDetectorRef, ScrollToService, UploaderComponent, - UploaderService, + DragService, { provide: HttpXsrfTokenExtractor, useValue: new HttpXsrfTokenExtractorMock('mock-token') }, { provide: CookieService, useValue: new CookieServiceMock() }, ], diff --git a/src/app/shared/uploader/uploader.component.ts b/src/app/shared/upload/uploader/uploader.component.ts similarity index 92% rename from src/app/shared/uploader/uploader.component.ts rename to src/app/shared/upload/uploader/uploader.component.ts index a0dd0e5bba..14b1ca9b94 100644 --- a/src/app/shared/uploader/uploader.component.ts +++ b/src/app/shared/upload/uploader/uploader.component.ts @@ -2,16 +2,16 @@ 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'; -import { hasValue, isNotEmpty, isUndefined } from '../empty.util'; -import { UploaderService } from './uploader.service'; +import { hasValue, isNotEmpty, isUndefined } from '../../empty.util'; import { UploaderProperties } from './uploader-properties.model'; import { HttpXsrfTokenExtractor } from '@angular/common/http'; -import { XSRF_COOKIE, XSRF_REQUEST_HEADER, XSRF_RESPONSE_HEADER } from '../../core/xsrf/xsrf.interceptor'; -import { CookieService } from '../../core/services/cookie.service'; +import { XSRF_COOKIE, XSRF_REQUEST_HEADER, XSRF_RESPONSE_HEADER } from '../../../core/xsrf/xsrf.interceptor'; +import { CookieService } from '../../../core/services/cookie.service'; +import { DragService } from '../../../core/drag.service'; @Component({ selector: 'ds-uploader', @@ -76,7 +76,7 @@ export class UploaderComponent { @HostListener('window:dragover', ['$event']) onDragOver(event: any) { - if (this.enableDragOverDocument && this.uploaderService.isAllowedDragOverPage()) { + if (this.enableDragOverDocument && this.dragService.isAllowedDragOverPage()) { // Show drop area on the page event.preventDefault(); if ((event.target as any).tagName !== 'HTML') { @@ -85,9 +85,13 @@ export class UploaderComponent { } } - constructor(private cdr: ChangeDetectorRef, private scrollToService: ScrollToService, - private uploaderService: UploaderService, private tokenExtractor: HttpXsrfTokenExtractor, - private cookieService: CookieService) { + constructor( + private cdr: ChangeDetectorRef, + private scrollToService: ScrollToService, + private dragService: DragService, + private tokenExtractor: HttpXsrfTokenExtractor, + private cookieService: CookieService + ) { } /** 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/statistics-page/statistics-table/statistics-table.component.html b/src/app/statistics-page/statistics-table/statistics-table.component.html index 3ecd256812..fb042b25c3 100644 --- a/src/app/statistics-page/statistics-table/statistics-table.component.html +++ b/src/app/statistics-page/statistics-table/statistics-table.component.html @@ -10,7 +10,7 @@ - + diff --git a/src/app/statistics/angulartics/dspace-provider.spec.ts b/src/app/statistics/angulartics/dspace-provider.spec.ts index 8491d8e80c..73c2419ce6 100644 --- a/src/app/statistics/angulartics/dspace-provider.spec.ts +++ b/src/app/statistics/angulartics/dspace-provider.spec.ts @@ -11,7 +11,7 @@ describe('Angulartics2DSpace', () => { beforeEach(() => { angulartics2 = { - eventTrack: observableOf({action: 'pageView', properties: {object: 'mock-object'}}), + eventTrack: observableOf({action: 'page_view', properties: {object: 'mock-object'}}), filterDeveloperMode: () => filter(() => true) } as any; statisticsService = jasmine.createSpyObj('statisticsService', {trackViewEvent: null}); diff --git a/src/app/statistics/angulartics/dspace-provider.ts b/src/app/statistics/angulartics/dspace-provider.ts index cd1aab94bd..6efa67f92a 100644 --- a/src/app/statistics/angulartics/dspace-provider.ts +++ b/src/app/statistics/angulartics/dspace-provider.ts @@ -24,7 +24,7 @@ export class Angulartics2DSpace { } private eventTrack(event) { - if (event.action === 'pageView') { + if (event.action === 'page_view') { this.statisticsService.trackViewEvent(event.properties.object); } else if (event.action === 'search') { this.statisticsService.trackSearchEvent( diff --git a/src/app/statistics/angulartics/dspace/view-tracker.component.ts b/src/app/statistics/angulartics/dspace/view-tracker.component.ts index 85588aeb97..d12b6c2f69 100644 --- a/src/app/statistics/angulartics/dspace/view-tracker.component.ts +++ b/src/app/statistics/angulartics/dspace/view-tracker.component.ts @@ -20,7 +20,7 @@ export class ViewTrackerComponent implements OnInit { ngOnInit(): void { this.angulartics2.eventTrack.next({ - action: 'pageView', + action: 'page_view', properties: {object: this.object}, }); } diff --git a/src/app/statistics/google-analytics.service.spec.ts b/src/app/statistics/google-analytics.service.spec.ts index 9e2b1c7edf..2465e4db0e 100644 --- a/src/app/statistics/google-analytics.service.spec.ts +++ b/src/app/statistics/google-analytics.service.spec.ts @@ -1,4 +1,7 @@ -import { Angulartics2GoogleAnalytics, Angulartics2GoogleTagManager } from 'angulartics2'; +import { + Angulartics2GoogleAnalytics, + Angulartics2GoogleGlobalSiteTag, +} from 'angulartics2'; import { of } from 'rxjs'; import { GoogleAnalyticsService } from './google-analytics.service'; @@ -16,7 +19,7 @@ describe('GoogleAnalyticsService', () => { const srcTestValue = 'mock-script-src'; let service: GoogleAnalyticsService; let googleAnalyticsSpy: Angulartics2GoogleAnalytics; - let googleTagManagerSpy: Angulartics2GoogleTagManager; + let googleTagManagerSpy: Angulartics2GoogleGlobalSiteTag; let configSpy: ConfigurationDataService; let klaroServiceSpy: jasmine.SpyObj; let scriptElementMock: any; @@ -37,7 +40,7 @@ describe('GoogleAnalyticsService', () => { googleAnalyticsSpy = jasmine.createSpyObj('Angulartics2GoogleAnalytics', [ 'startTracking', ]); - googleTagManagerSpy = jasmine.createSpyObj('Angulartics2GoogleTagManager', [ + googleTagManagerSpy = jasmine.createSpyObj('Angulartics2GoogleGlobalSiteTag', [ 'startTracking', ]); diff --git a/src/app/statistics/google-analytics.service.ts b/src/app/statistics/google-analytics.service.ts index 9c5883d183..9d32a61093 100644 --- a/src/app/statistics/google-analytics.service.ts +++ b/src/app/statistics/google-analytics.service.ts @@ -1,7 +1,10 @@ import { DOCUMENT } from '@angular/common'; import { Inject, Injectable } from '@angular/core'; -import { Angulartics2GoogleAnalytics, Angulartics2GoogleTagManager } from 'angulartics2'; +import { + Angulartics2GoogleAnalytics, + Angulartics2GoogleGlobalSiteTag, +} from 'angulartics2'; import { combineLatest } from 'rxjs'; import { ConfigurationDataService } from '../core/data/configuration-data.service'; @@ -19,7 +22,7 @@ export class GoogleAnalyticsService { constructor( private googleAnalytics: Angulartics2GoogleAnalytics, - private googleTagManager: Angulartics2GoogleTagManager, + private googleGlobalSiteTag: Angulartics2GoogleGlobalSiteTag, private klaroService: KlaroService, private configService: ConfigurationDataService, @Inject(DOCUMENT) private document: any, @@ -70,7 +73,7 @@ export class GoogleAnalyticsService { this.document.body.appendChild(libScript); // start tracking - this.googleTagManager.startTracking(); + this.googleGlobalSiteTag.startTracking(); } else { // add trackingId snippet to page const keyScript = this.document.createElement('script'); 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/form/collection/submission-form-collection.component.html b/src/app/submission/form/collection/submission-form-collection.component.html index d897cc31fd..15b6ff280e 100644 --- a/src/app/submission/form/collection/submission-form-collection.component.html +++ b/src/app/submission/form/collection/submission-form-collection.component.html @@ -35,9 +35,9 @@ class="dropdown-menu" id="collectionControlsDropdownMenu" aria-labelledby="collectionControlsMenuButton"> - - +