diff --git a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts index a9c0a6648d..e172f9717b 100644 --- a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts +++ b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts @@ -179,17 +179,18 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { link: '/processes/new' } as LinkMenuItemModel, }, - { - id: 'new_item_version', - parentID: 'new', - active: false, - visible: true, - model: { - type: MenuItemType.LINK, - text: 'menu.section.new_item_version', - link: '' - } as LinkMenuItemModel, - }, + // TODO: enable this menu item once the feature has been implemented + // { + // id: 'new_item_version', + // parentID: 'new', + // active: false, + // visible: true, + // model: { + // type: MenuItemType.LINK, + // text: 'menu.section.new_item_version', + // link: '' + // } as LinkMenuItemModel, + // }, /* Edit */ { @@ -243,47 +244,35 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { } as OnClickMenuItemModel, }, - /* Curation tasks */ - { - id: 'curation_tasks', - active: false, - visible: isCollectionAdmin, - model: { - type: MenuItemType.LINK, - text: 'menu.section.curation_task', - link: '' - } as LinkMenuItemModel, - icon: 'filter', - index: 7 - }, - /* Statistics */ - { - id: 'statistics_task', - active: false, - visible: true, - model: { - type: MenuItemType.LINK, - text: 'menu.section.statistics_task', - link: '' - } as LinkMenuItemModel, - icon: 'chart-bar', - index: 8 - }, + // TODO: enable this menu item once the feature has been implemented + // { + // id: 'statistics_task', + // active: false, + // visible: true, + // model: { + // type: MenuItemType.LINK, + // text: 'menu.section.statistics_task', + // link: '' + // } as LinkMenuItemModel, + // icon: 'chart-bar', + // index: 8 + // }, /* Control Panel */ - { - id: 'control_panel', - active: false, - visible: isSiteAdmin, - model: { - type: MenuItemType.LINK, - text: 'menu.section.control_panel', - link: '' - } as LinkMenuItemModel, - icon: 'cogs', - index: 9 - }, + // TODO: enable this menu item once the feature has been implemented + // { + // id: 'control_panel', + // active: false, + // visible: isSiteAdmin, + // model: { + // type: MenuItemType.LINK, + // text: 'menu.section.control_panel', + // link: '' + // } as LinkMenuItemModel, + // icon: 'cogs', + // index: 9 + // }, /* Processes */ { @@ -324,42 +313,45 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { index: 3, shouldPersistOnRouteChange: true }, - { - id: 'export_community', - parentID: 'export', - active: false, - visible: true, - model: { - type: MenuItemType.LINK, - text: 'menu.section.export_community', - link: '' - } as LinkMenuItemModel, - shouldPersistOnRouteChange: true - }, - { - id: 'export_collection', - parentID: 'export', - active: false, - visible: true, - model: { - type: MenuItemType.LINK, - text: 'menu.section.export_collection', - link: '' - } as LinkMenuItemModel, - shouldPersistOnRouteChange: true - }, - { - id: 'export_item', - parentID: 'export', - active: false, - visible: true, - model: { - type: MenuItemType.LINK, - text: 'menu.section.export_item', - link: '' - } as LinkMenuItemModel, - shouldPersistOnRouteChange: true - }, + // TODO: enable this menu item once the feature has been implemented + // { + // id: 'export_community', + // parentID: 'export', + // active: false, + // visible: true, + // model: { + // type: MenuItemType.LINK, + // text: 'menu.section.export_community', + // link: '' + // } as LinkMenuItemModel, + // shouldPersistOnRouteChange: true + // }, + // TODO: enable this menu item once the feature has been implemented + // { + // id: 'export_collection', + // parentID: 'export', + // active: false, + // visible: true, + // model: { + // type: MenuItemType.LINK, + // text: 'menu.section.export_collection', + // link: '' + // } as LinkMenuItemModel, + // shouldPersistOnRouteChange: true + // }, + // TODO: enable this menu item once the feature has been implemented + // { + // id: 'export_item', + // parentID: 'export', + // active: false, + // visible: true, + // model: { + // type: MenuItemType.LINK, + // text: 'menu.section.export_item', + // link: '' + // } as LinkMenuItemModel, + // shouldPersistOnRouteChange: true + // }, ]; menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, menuSection)); @@ -406,17 +398,18 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { icon: 'file-import', index: 2 }, - { - id: 'import_batch', - parentID: 'import', - active: false, - visible: true, - model: { - type: MenuItemType.LINK, - text: 'menu.section.import_batch', - link: '' - } as LinkMenuItemModel, - } + // TODO: enable this menu item once the feature has been implemented + // { + // id: 'import_batch', + // parentID: 'import', + // active: false, + // visible: true, + // model: { + // type: MenuItemType.LINK, + // text: 'menu.section.import_batch', + // link: '' + // } as LinkMenuItemModel, + // } ]; menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, { shouldPersistOnRouteChange: true @@ -563,17 +556,18 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { link: '/access-control/groups' } as LinkMenuItemModel, }, - { - id: 'access_control_authorizations', - parentID: 'access_control', - active: false, - visible: authorized, - model: { - type: MenuItemType.LINK, - text: 'menu.section.access_control_authorizations', - link: '' - } as LinkMenuItemModel, - }, + // TODO: enable this menu item once the feature has been implemented + // { + // id: 'access_control_authorizations', + // parentID: 'access_control', + // active: false, + // visible: authorized, + // model: { + // type: MenuItemType.LINK, + // text: 'menu.section.access_control_authorizations', + // link: '' + // } as LinkMenuItemModel, + // }, { id: 'access_control', active: false, diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts index 2082143e05..3f9637cdc9 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts @@ -18,7 +18,7 @@ import { RelationshipType } from '../../../../core/shared/item-relationships/rel import { getAllSucceededRemoteData, getRemoteDataPayload, - getFirstSucceededRemoteData, + getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload, } from '../../../../core/shared/operators'; import { ItemType } from '../../../../core/shared/item-relationships/item-type.model'; import { DsDynamicLookupRelationModalComponent } from '../../../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component'; @@ -29,6 +29,7 @@ import { SearchResult } from '../../../../shared/search/search-result.model'; import { followLink } from '../../../../shared/utils/follow-link-config.model'; import { PaginatedList } from '../../../../core/data/paginated-list.model'; import { RemoteData } from '../../../../core/data/remote-data'; +import { Collection } from '../../../../core/shared/collection.model'; @Component({ selector: 'ds-edit-relationship-list', @@ -146,6 +147,11 @@ export class EditRelationshipListComponent implements OnInit { modalComp.repeatable = true; modalComp.listId = this.listId; modalComp.item = this.item; + this.item.owningCollection.pipe( + getFirstSucceededRemoteDataPayload() + ).subscribe((collection: Collection) => { + modalComp.collection = collection; + }); modalComp.select = (...selectableObjects: SearchResult[]) => { selectableObjects.forEach((searchResult) => { const relatedItem: Item = searchResult.indexableObject; diff --git a/src/app/+my-dspace-page/my-dspace-configuration.service.spec.ts b/src/app/+my-dspace-page/my-dspace-configuration.service.spec.ts index 87a2f8a9dd..fa278da967 100644 --- a/src/app/+my-dspace-page/my-dspace-configuration.service.spec.ts +++ b/src/app/+my-dspace-page/my-dspace-configuration.service.spec.ts @@ -27,7 +27,10 @@ describe('MyDSpaceConfigurationService', () => { scope: '' }); - const backendFilters = [new SearchFilter('f.namedresourcetype', ['another value']), new SearchFilter('f.dateSubmitted', ['[2013 TO 2018]'])]; + const backendFilters = [ + new SearchFilter('f.namedresourcetype', ['another value']), + new SearchFilter('f.dateSubmitted', ['[2013 TO 2018]'], 'equals') + ]; const spy = jasmine.createSpyObj('RouteService', { getQueryParameterValue: observableOf(value1), diff --git a/src/app/+my-dspace-page/my-dspace-page.component.html b/src/app/+my-dspace-page/my-dspace-page.component.html index c911e2c319..32e3a0d710 100644 --- a/src/app/+my-dspace-page/my-dspace-page.component.html +++ b/src/app/+my-dspace-page/my-dspace-page.component.html @@ -6,6 +6,8 @@ [configurationList]="(configurationList$ | async)" [resultCount]="(resultsRD$ | async)?.payload.totalElements" [viewModeList]="viewModeList" + [searchOptions]="(searchOptions$ | async)" + [sortOptions]="(sortOptions$ | async)" [refreshFilters]="refreshFilters.asObservable()" [inPlaceSearch]="inPlaceSearch">
@@ -28,6 +30,8 @@ [resultCount]="(resultsRD$ | async)?.payload.totalElements" (toggleSidebar)="closeSidebar()" [ngClass]="{'active': !(isSidebarCollapsed() | async)}" + [searchOptions]="(searchOptions$ | async)" + [sortOptions]="(sortOptions$ | async)" [refreshFilters]="refreshFilters.asObservable()" [inPlaceSearch]="inPlaceSearch"> diff --git a/src/app/+my-dspace-page/my-dspace-page.component.spec.ts b/src/app/+my-dspace-page/my-dspace-page.component.spec.ts index 59581d0da8..b4b75b42a0 100644 --- a/src/app/+my-dspace-page/my-dspace-page.component.spec.ts +++ b/src/app/+my-dspace-page/my-dspace-page.component.spec.ts @@ -45,6 +45,7 @@ describe('MyDSpacePageComponent', () => { pagination.id = 'mydspace-results-pagination'; pagination.currentPage = 1; pagination.pageSize = 10; + const sortOption = { name: 'score', sortOrder: 'DESC', metadata: null }; const sort: SortOptions = new SortOptions('score', SortDirection.DESC); const mockResults = createSuccessfulRemoteDataObject$(['test', 'data']); const searchServiceStub = jasmine.createSpyObj('SearchService', { @@ -52,7 +53,8 @@ describe('MyDSpacePageComponent', () => { getEndpoint: observableOf('discover/search/objects'), getSearchLink: '/mydspace', getScopes: observableOf(['test-scope']), - setServiceOptions: {} + setServiceOptions: {}, + getSearchConfigurationFor: createSuccessfulRemoteDataObject$({ sortOptions: [sortOption]}) }); const configurationParam = 'default'; const queryParam = 'test query'; @@ -188,4 +190,24 @@ describe('MyDSpacePageComponent', () => { }); }); + + describe('when stable', () => { + + beforeEach(() => { + fixture.detectChanges(); + }); + + it('should have initialized the sortOptions$ observable', (done) => { + + comp.sortOptions$.subscribe((sortOptions) => { + + expect(sortOptions.length).toEqual(2); + expect(sortOptions[0]).toEqual(new SortOptions('score', SortDirection.ASC)); + expect(sortOptions[1]).toEqual(new SortOptions('score', SortDirection.DESC)); + done(); + }); + + }); + + }); }); diff --git a/src/app/+my-dspace-page/my-dspace-page.component.ts b/src/app/+my-dspace-page/my-dspace-page.component.ts index 5ee2a47d9f..3ded17191e 100644 --- a/src/app/+my-dspace-page/my-dspace-page.component.ts +++ b/src/app/+my-dspace-page/my-dspace-page.component.ts @@ -8,7 +8,7 @@ import { } from '@angular/core'; import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs'; -import { map, switchMap, tap, } from 'rxjs/operators'; +import { map, switchMap, tap } from 'rxjs/operators'; import { PaginatedList } from '../core/data/paginated-list.model'; import { RemoteData } from '../core/data/remote-data'; @@ -29,6 +29,8 @@ import { ViewMode } from '../core/shared/view-mode.model'; import { MyDSpaceRequest } from '../core/data/request.models'; import { SearchResult } from '../shared/search/search-result.model'; import { Context } from '../core/shared/context.model'; +import { SortOptions } from '../core/cache/models/sort-options.model'; +import { RouteService } from '../core/services/route.service'; export const MYDSPACE_ROUTE = '/mydspace'; export const SEARCH_CONFIG_SERVICE: InjectionToken = new InjectionToken('searchConfigurationService'); @@ -71,6 +73,11 @@ export class MyDSpacePageComponent implements OnInit { */ searchOptions$: Observable; + /** + * The current available sort options + */ + sortOptions$: Observable; + /** * The current relevant scopes */ @@ -109,7 +116,8 @@ export class MyDSpacePageComponent implements OnInit { constructor(private service: SearchService, private sidebarService: SidebarService, private windowService: HostWindowService, - @Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: MyDSpaceConfigurationService) { + @Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: MyDSpaceConfigurationService, + private routeService: RouteService) { this.isXsOrSm$ = this.windowService.isXsOrSm(); this.service.setServiceOptions(MyDSpaceResponseParsingService, MyDSpaceRequest); } @@ -151,6 +159,12 @@ export class MyDSpacePageComponent implements OnInit { }) ); + const configuration$ = this.searchConfigService.getCurrentConfiguration('workspace'); + const searchConfig$ = this.searchConfigService.getConfigurationSearchConfigObservable(configuration$, this.service); + + this.sortOptions$ = this.searchConfigService.getConfigurationSortOptionsObservable(searchConfig$); + this.searchConfigService.initializeSortOptionsFromConfiguration(searchConfig$); + } /** diff --git a/src/app/+search-page/search.component.html b/src/app/+search-page/search.component.html index 7cb16caebe..d8aa25e4a3 100644 --- a/src/app/+search-page/search.component.html +++ b/src/app/+search-page/search.component.html @@ -31,11 +31,14 @@ + [searchOptions]="(searchOptions$ | async)" + [sortOptions]="(sortOptions$ | async)" + (toggleSidebar)="closeSidebar()"> diff --git a/src/app/+search-page/search.component.spec.ts b/src/app/+search-page/search.component.spec.ts index 989aed403d..bcbf3f0f77 100644 --- a/src/app/+search-page/search.component.spec.ts +++ b/src/app/+search-page/search.component.spec.ts @@ -40,12 +40,14 @@ const pagination: PaginationComponentOptions = new PaginationComponentOptions(); pagination.id = 'search-results-pagination'; pagination.currentPage = 1; pagination.pageSize = 10; +const sortOption = { name: 'score', sortOrder: 'DESC', metadata: null }; const sort: SortOptions = new SortOptions('score', SortDirection.DESC); const mockResults = createSuccessfulRemoteDataObject$(['test', 'data']); const searchServiceStub = jasmine.createSpyObj('SearchService', { search: mockResults, getSearchLink: '/search', - getScopes: observableOf(['test-scope']) + getScopes: observableOf(['test-scope']), + getSearchConfigurationFor: createSuccessfulRemoteDataObject$({ sortOptions: [sortOption]}) }); const configurationParam = 'default'; const queryParam = 'test query'; @@ -181,4 +183,24 @@ describe('SearchComponent', () => { }); }); + + describe('when stable', () => { + + beforeEach(() => { + fixture.detectChanges(); + }); + + it('should have initialized the sortOptions$ observable', (done) => { + + comp.sortOptions$.subscribe((sortOptions) => { + + expect(sortOptions.length).toEqual(2); + expect(sortOptions[0]).toEqual(new SortOptions('score', SortDirection.ASC)); + expect(sortOptions[1]).toEqual(new SortOptions('score', SortDirection.DESC)); + done(); + }); + + }); + + }); }); diff --git a/src/app/+search-page/search.component.ts b/src/app/+search-page/search.component.ts index 84077ebdc8..b817c82a57 100644 --- a/src/app/+search-page/search.component.ts +++ b/src/app/+search-page/search.component.ts @@ -1,13 +1,13 @@ import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core'; import { BehaviorSubject, Observable, Subscription } from 'rxjs'; -import { startWith, switchMap, } from 'rxjs/operators'; +import { startWith, switchMap } from 'rxjs/operators'; import { PaginatedList } from '../core/data/paginated-list.model'; import { RemoteData } from '../core/data/remote-data'; import { DSpaceObject } from '../core/shared/dspace-object.model'; import { pushInOut } from '../shared/animations/push'; import { HostWindowService } from '../shared/host-window.service'; import { SidebarService } from '../shared/sidebar/sidebar.service'; -import { hasValue, isNotEmpty } from '../shared/empty.util'; +import { hasValue, isEmpty } from '../shared/empty.util'; import { getFirstSucceededRemoteData } from '../core/shared/operators'; import { RouteService } from '../core/services/route.service'; import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component'; @@ -16,8 +16,9 @@ import { SearchResult } from '../shared/search/search-result.model'; import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; import { SearchService } from '../core/shared/search/search.service'; import { currentPath } from '../shared/utils/route.utils'; -import { Router } from '@angular/router'; +import { Router} from '@angular/router'; import { Context } from '../core/shared/context.model'; +import { SortOptions } from '../core/cache/models/sort-options.model'; @Component({ selector: 'ds-search', @@ -47,6 +48,11 @@ export class SearchComponent implements OnInit { */ searchOptions$: Observable; + /** + * The current available sort options + */ + sortOptions$: Observable; + /** * The current relevant scopes */ @@ -129,9 +135,15 @@ export class SearchComponent implements OnInit { this.scopeListRD$ = this.searchConfigService.getCurrentScope('').pipe( switchMap((scopeId) => this.service.getScopes(scopeId)) ); - if (!isNotEmpty(this.configuration$)) { - this.configuration$ = this.routeService.getRouteParameterValue('configuration'); + if (isEmpty(this.configuration$)) { + this.configuration$ = this.searchConfigService.getCurrentConfiguration('default'); } + + const searchConfig$ = this.searchConfigService.getConfigurationSearchConfigObservable(this.configuration$, this.service); + + this.sortOptions$ = this.searchConfigService.getConfigurationSortOptionsObservable(searchConfig$); + this.searchConfigService.initializeSortOptionsFromConfiguration(searchConfig$); + } /** diff --git a/src/app/access-control/group-registry/group-form/group-form.component.spec.ts b/src/app/access-control/group-registry/group-form/group-form.component.spec.ts index d213a071d7..5f0f570044 100644 --- a/src/app/access-control/group-registry/group-form/group-form.component.spec.ts +++ b/src/app/access-control/group-registry/group-form/group-form.component.spec.ts @@ -210,4 +210,11 @@ describe('GroupFormComponent', () => { }); }); + describe('ngOnDestroy', () => { + it('does NOT call router.navigate', () => { + component.ngOnDestroy(); + expect(router.navigate).toHaveBeenCalledTimes(0); + }); + }); + }); diff --git a/src/app/access-control/group-registry/group-form/group-form.component.ts b/src/app/access-control/group-registry/group-form/group-form.component.ts index 9b74d26fe8..c2c694f445 100644 --- a/src/app/access-control/group-registry/group-form/group-form.component.ts +++ b/src/app/access-control/group-registry/group-form/group-form.component.ts @@ -405,7 +405,7 @@ export class GroupFormComponent implements OnInit, OnDestroy { */ @HostListener('window:beforeunload') ngOnDestroy(): void { - this.onCancel(); + this.groupDataService.cancelEditGroup(); this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); } diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 619a7dbadc..615c2b3977 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -161,6 +161,7 @@ import { ShortLivedToken } from './auth/models/short-lived-token.model'; import { UsageReport } from './statistics/models/usage-report.model'; import { RootDataService } from './data/root-data.service'; import { Root } from './data/root.model'; +import { SearchConfig } from './shared/search/search-filters/search-config.model'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -340,6 +341,7 @@ export const models = Registration, UsageReport, Root, + SearchConfig ]; @NgModule({ diff --git a/src/app/core/shared/search/search-configuration.service.spec.ts b/src/app/core/shared/search/search-configuration.service.spec.ts index 061182c2fc..805ecd0486 100644 --- a/src/app/core/shared/search/search-configuration.service.spec.ts +++ b/src/app/core/shared/search/search-configuration.service.spec.ts @@ -23,7 +23,10 @@ describe('SearchConfigurationService', () => { scope: '' }); - const backendFilters = [new SearchFilter('f.author', ['another value']), new SearchFilter('f.date', ['[2013 TO 2018]'])]; + const backendFilters = [ + new SearchFilter('f.author', ['another value']), + new SearchFilter('f.date', ['[2013 TO 2018]'], 'equals') + ]; const routeService = jasmine.createSpyObj('RouteService', { getQueryParameterValue: observableOf(value1), diff --git a/src/app/core/shared/search/search-configuration.service.ts b/src/app/core/shared/search/search-configuration.service.ts index 798a0de287..74af230810 100644 --- a/src/app/core/shared/search/search-configuration.service.ts +++ b/src/app/core/shared/search/search-configuration.service.ts @@ -1,8 +1,15 @@ import { Injectable, OnDestroy } from '@angular/core'; import { ActivatedRoute, Params } from '@angular/router'; -import { BehaviorSubject, combineLatest as observableCombineLatest, merge as observableMerge, Observable, Subscription } from 'rxjs'; -import { filter, map, startWith } from 'rxjs/operators'; +import { + BehaviorSubject, + combineLatest, + combineLatest as observableCombineLatest, + merge as observableMerge, + Observable, + Subscription +} from 'rxjs'; +import { distinctUntilChanged, filter, map, startWith, switchMap, take } from 'rxjs/operators'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; import { SearchOptions } from '../../../shared/search/search-options.model'; import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model'; @@ -11,9 +18,15 @@ import { RemoteData } from '../../data/remote-data'; import { DSpaceObjectType } from '../dspace-object-type.model'; import { SortDirection, SortOptions } from '../../cache/models/sort-options.model'; import { RouteService } from '../../services/route.service'; -import { getFirstSucceededRemoteData } from '../operators'; +import { + getAllSucceededRemoteDataPayload, + getFirstSucceededRemoteData +} from '../operators'; import { hasNoValue, hasValue, isNotEmpty, isNotEmptyOperator } from '../../../shared/empty.util'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { SearchConfig } from './search-filters/search-config.model'; +import { SearchService } from './search.service'; +import { of } from 'rxjs/internal/observable/of'; import { PaginationService } from '../../pagination/pagination.service'; /** @@ -168,7 +181,7 @@ export class SearchConfigurationService implements OnDestroy { if (hasNoValue(filters.find((f) => f.key === realKey))) { const min = filterParams[realKey + '.min'] ? filterParams[realKey + '.min'][0] : '*'; const max = filterParams[realKey + '.max'] ? filterParams[realKey + '.max'][0] : '*'; - filters.push(new SearchFilter(realKey, ['[' + min + ' TO ' + max + ']'])); + filters.push(new SearchFilter(realKey, ['[' + min + ' TO ' + max + ']'], 'equals')); } } else { filters.push(new SearchFilter(key, filterParams[key])); @@ -194,6 +207,60 @@ export class SearchConfigurationService implements OnDestroy { return this.routeService.getQueryParamsWithPrefix('f.'); } + /** + * Creates an observable of SearchConfig every time the configuration$ stream emits. + * @param configuration$ + * @param service + */ + getConfigurationSearchConfigObservable(configuration$: Observable, service: SearchService): Observable { + return configuration$.pipe( + distinctUntilChanged(), + switchMap((configuration) => service.getSearchConfigurationFor(null, configuration)), + getAllSucceededRemoteDataPayload()); + } + + /** + * Every time searchConfig change (after a configuration change) it update the navigation with the default sort option + * and emit the new paginateSearchOptions value. + * @param configuration$ + * @param service + */ + initializeSortOptionsFromConfiguration(searchConfig$: Observable) { + const subscription = searchConfig$.pipe(switchMap((searchConfig) => combineLatest([ + of(searchConfig), + this.paginatedSearchOptions.pipe(take(1)) + ]))).subscribe(([searchConfig, searchOptions]) => { + const field = searchConfig.sortOptions[0].name; + const direction = searchConfig.sortOptions[0].sortOrder.toLowerCase() === SortDirection.ASC.toLowerCase() ? SortDirection.ASC : SortDirection.DESC; + const updateValue = Object.assign(new PaginatedSearchOptions({}), searchOptions, { + sort: new SortOptions(field, direction) + }); + this.paginationService.updateRoute(this.paginationID, + { + sortDirection: updateValue.sort.direction, + sortField: updateValue.sort.field, + }); + this.paginatedSearchOptions.next(updateValue); + }); + this.subs.push(subscription); + } + + /** + * Creates an observable of available SortOptions[] every time the searchConfig$ stream emits. + * @param searchConfig$ + * @param service + */ + getConfigurationSortOptionsObservable(searchConfig$: Observable): Observable { + return searchConfig$.pipe(map((searchConfig) => { + const sortOptions = []; + searchConfig.sortOptions.forEach(sortOption => { + sortOptions.push(new SortOptions(sortOption.name, SortDirection.ASC)); + sortOptions.push(new SortOptions(sortOption.name, SortDirection.DESC)); + }); + return sortOptions; + })); + } + /** * Sets up a subscription to all necessary parameters to make sure the searchOptions emits a new value every time they update * @param {SearchOptions} defaults Default values for when no parameters are available diff --git a/src/app/core/shared/search/search-filters/search-config.model.ts b/src/app/core/shared/search/search-filters/search-config.model.ts new file mode 100644 index 0000000000..725761fe7b --- /dev/null +++ b/src/app/core/shared/search/search-filters/search-config.model.ts @@ -0,0 +1,76 @@ +import { autoserialize, deserialize } from 'cerialize'; + +import { SEARCH_CONFIG } from './search-config.resource-type'; +import { typedObject } from '../../../cache/builders/build-decorators'; +import { CacheableObject } from '../../../cache/object-cache.reducer'; +import { HALLink } from '../../hal-link.model'; +import { ResourceType } from '../../resource-type'; + +/** + * The configuration for a search + */ +@typedObject +export class SearchConfig implements CacheableObject { + static type = SEARCH_CONFIG; + + /** + * The id of this search configuration. + */ + @autoserialize + id: string; + + /** + * The configured filters. + */ + @autoserialize + filters: FilterConfig[]; + + /** + * The configured sort options. + */ + @autoserialize + sortOptions: SortOption[]; + + /** + * The object type. + */ + @autoserialize + type: ResourceType; + + /** + * The {@link HALLink}s for this Item + */ + @deserialize + _links: { + facets: HALLink; + objects: HALLink; + self: HALLink; + }; +} + +/** + * Interface to model filter's configuration. + */ +export interface FilterConfig { + filter: string; + hasFacets: boolean; + operators: OperatorConfig[]; + openByDefault: boolean; + pageSize: number; + type: string; +} + +/** + * Interface to model sort option's configuration. + */ +export interface SortOption { + name: string; + sortOrder: string; +} + +/** + * Interface to model operator's configuration. + */ +export interface OperatorConfig { + operator: string; +} diff --git a/src/app/core/shared/search/search-filters/search-config.resource-type.ts b/src/app/core/shared/search/search-filters/search-config.resource-type.ts new file mode 100644 index 0000000000..967a654006 --- /dev/null +++ b/src/app/core/shared/search/search-filters/search-config.resource-type.ts @@ -0,0 +1,9 @@ +import {ResourceType} from '../../resource-type'; + +/** + * The resource type for SearchConfig + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const SEARCH_CONFIG = new ResourceType('discover'); diff --git a/src/app/core/shared/search/search.service.spec.ts b/src/app/core/shared/search/search.service.spec.ts index 60cb0a87b9..00f10230c3 100644 --- a/src/app/core/shared/search/search.service.spec.ts +++ b/src/app/core/shared/search/search.service.spec.ts @@ -240,5 +240,55 @@ describe('SearchService', () => { expect((searchService as any).requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({ href: requestUrl }), true); }); }); + + describe('when getSearchConfigurationFor is called without a scope', () => { + const endPoint = 'http://endpoint.com/test/config'; + beforeEach(() => { + spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint)); + spyOn((searchService as any).rdb, 'buildFromHref').and.callThrough(); + /* tslint:disable:no-empty */ + searchService.getSearchConfigurationFor(null).subscribe((t) => { + }); // subscribe to make sure all methods are called + /* tslint:enable:no-empty */ + }); + + it('should call getEndpoint on the halService', () => { + expect((searchService as any).halService.getEndpoint).toHaveBeenCalled(); + }); + + it('should send out the request on the request service', () => { + expect((searchService as any).requestService.send).toHaveBeenCalled(); + }); + + it('should call send containing a request with the correct request url', () => { + expect((searchService as any).requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({ href: endPoint }), true); + }); + }); + + describe('when getSearchConfigurationFor is called with a scope', () => { + const endPoint = 'http://endpoint.com/test/config'; + const scope = 'test'; + const requestUrl = endPoint + '?scope=' + scope; + beforeEach(() => { + spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint)); + /* tslint:disable:no-empty */ + searchService.getSearchConfigurationFor(scope).subscribe((t) => { + }); // subscribe to make sure all methods are called + /* tslint:enable:no-empty */ + }); + + it('should call getEndpoint on the halService', () => { + expect((searchService as any).halService.getEndpoint).toHaveBeenCalled(); + }); + + it('should send out the request on the request service', () => { + expect((searchService as any).requestService.send).toHaveBeenCalled(); + }); + + it('should call send containing a request with the correct request url', () => { + expect((searchService as any).requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({ href: requestUrl }), true); + }); + }); + }); }); diff --git a/src/app/core/shared/search/search.service.ts b/src/app/core/shared/search/search.service.ts index 054bde4c08..9cb284db32 100644 --- a/src/app/core/shared/search/search.service.ts +++ b/src/app/core/shared/search/search.service.ts @@ -37,6 +37,7 @@ import { ListableObject } from '../../../shared/object-collection/shared/listabl import { getSearchResultFor } from '../../../shared/search/search-result-element-decorator'; import { FacetConfigResponse } from '../../../shared/search/facet-config-response.model'; import { FacetValues } from '../../../shared/search/facet-values.model'; +import { SearchConfig } from './search-filters/search-config.model'; import { PaginationService } from '../../pagination/pagination.service'; import { SearchConfigurationService } from './search-configuration.service'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; @@ -46,6 +47,12 @@ import { PaginationComponentOptions } from '../../../shared/pagination/paginatio */ @Injectable() export class SearchService implements OnDestroy { + + /** + * Endpoint link path for retrieving search configurations + */ + private configurationLinkPath = 'discover/search'; + /** * Endpoint link path for retrieving general search results */ @@ -229,6 +236,24 @@ export class SearchService implements OnDestroy { ); } + private getConfigUrl(url: string, scope?: string, configurationName?: string) { + const args: string[] = []; + + if (isNotEmpty(scope)) { + args.push(`scope=${scope}`); + } + + if (isNotEmpty(configurationName)) { + args.push(`configuration=${configurationName}`); + } + + if (isNotEmpty(args)) { + url = new URLCombiner(url, `?${args.join('&')}`).toString(); + } + + return url; + } + /** * Request the filter configuration for a given scope or the whole repository * @param {string} scope UUID of the object for which config the filter config is requested, when no scope is provided the configuration for the whole repository is loaded @@ -237,33 +262,17 @@ export class SearchService implements OnDestroy { */ getConfig(scope?: string, configurationName?: string): Observable> { const href$ = this.halService.getEndpoint(this.facetLinkPathPrefix).pipe( - map((url: string) => { - const args: string[] = []; - - if (isNotEmpty(scope)) { - args.push(`scope=${scope}`); - } - - if (isNotEmpty(configurationName)) { - args.push(`configuration=${configurationName}`); - } - - if (isNotEmpty(args)) { - url = new URLCombiner(url, `?${args.join('&')}`).toString(); - } - - return url; - }), + map((url: string) => this.getConfigUrl(url, scope, configurationName)), ); href$.pipe(take(1)).subscribe((url: string) => { - let request = new this.request(this.requestService.generateRequestId(), url); - request = Object.assign(request, { - getResponseParser(): GenericConstructor { - return FacetConfigResponseParsingService; - } - }); - this.requestService.send(request, true); + let request = new this.request(this.requestService.generateRequestId(), url); + request = Object.assign(request, { + getResponseParser(): GenericConstructor { + return FacetConfigResponseParsingService; + } + }); + this.requestService.send(request, true); }); return this.rdb.buildFromHref(href$).pipe( @@ -398,6 +407,25 @@ export class SearchService implements OnDestroy { }); } + /** + * Request the search configuration for a given scope or the whole repository + * @param {string} scope UUID of the object for which config the filter config is requested, when no scope is provided the configuration for the whole repository is loaded + * @param {string} configurationName the name of the configuration + * @returns {Observable>} The found configuration + */ + getSearchConfigurationFor(scope?: string, configurationName?: string ): Observable> { + const href$ = this.halService.getEndpoint(this.configurationLinkPath).pipe( + map((url: string) => this.getConfigUrl(url, scope, configurationName)), + ); + + href$.pipe(take(1)).subscribe((url: string) => { + const request = new this.request(this.requestService.generateRequestId(), url); + this.requestService.send(request, true); + }); + + return this.rdb.buildFromHref(href$); + } + /** * @returns {string} The base path to the search page */ diff --git a/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.spec.ts b/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.spec.ts index 23b0367b22..55634dbf7f 100644 --- a/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.spec.ts +++ b/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.spec.ts @@ -10,6 +10,7 @@ import { createSuccessfulRemoteDataObject$ } from '../../../remote-data.utils'; import { createPaginatedList } from '../../../testing/utils.test'; import { Collection } from '../../../../core/shared/collection.model'; import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model'; +import { NotificationsService } from '../../../notifications/notifications.service'; describe('AuthorizedCollectionSelectorComponent', () => { let component: AuthorizedCollectionSelectorComponent; @@ -18,6 +19,8 @@ describe('AuthorizedCollectionSelectorComponent', () => { let collectionService; let collection; + let notificationsService: NotificationsService; + beforeEach(waitForAsync(() => { collection = Object.assign(new Collection(), { id: 'authorized-collection' @@ -25,12 +28,14 @@ describe('AuthorizedCollectionSelectorComponent', () => { collectionService = jasmine.createSpyObj('collectionService', { getAuthorizedCollection: createSuccessfulRemoteDataObject$(createPaginatedList([collection])) }); + notificationsService = jasmine.createSpyObj('notificationsService', ['error']); TestBed.configureTestingModule({ declarations: [AuthorizedCollectionSelectorComponent, VarDirective], imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])], providers: [ { provide: SearchService, useValue: {} }, - { provide: CollectionDataService, useValue: collectionService } + { provide: CollectionDataService, useValue: collectionService }, + { provide: NotificationsService, useValue: notificationsService }, ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); @@ -45,10 +50,10 @@ describe('AuthorizedCollectionSelectorComponent', () => { describe('search', () => { it('should call getAuthorizedCollection and return the authorized collection in a SearchResult', (done) => { - component.search('', 1).subscribe((result) => { + component.search('', 1).subscribe((resultRD) => { expect(collectionService.getAuthorizedCollection).toHaveBeenCalled(); - expect(result.page.length).toEqual(1); - expect(result.page[0].indexableObject).toEqual(collection); + expect(resultRD.payload.page.length).toEqual(1); + expect(resultRD.payload.page[0].indexableObject).toEqual(collection); done(); }); }); diff --git a/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts b/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts index c15d0e2821..bca1727542 100644 --- a/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts +++ b/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts @@ -3,13 +3,17 @@ import { DSOSelectorComponent } from '../dso-selector.component'; import { SearchService } from '../../../../core/shared/search/search.service'; import { CollectionDataService } from '../../../../core/data/collection-data.service'; import { Observable } from 'rxjs/internal/Observable'; -import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators'; +import { getFirstCompletedRemoteData } from '../../../../core/shared/operators'; import { map } from 'rxjs/operators'; import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model'; import { SearchResult } from '../../../search/search-result.model'; import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; import { buildPaginatedList, PaginatedList } from '../../../../core/data/paginated-list.model'; import { followLink } from '../../../utils/follow-link-config.model'; +import { RemoteData } from '../../../../core/data/remote-data'; +import { hasValue } from '../../../empty.util'; +import { NotificationsService } from '../../../notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'ds-authorized-collection-selector', @@ -21,8 +25,10 @@ import { followLink } from '../../../utils/follow-link-config.model'; */ export class AuthorizedCollectionSelectorComponent extends DSOSelectorComponent { constructor(protected searchService: SearchService, - protected collectionDataService: CollectionDataService) { - super(searchService); + protected collectionDataService: CollectionDataService, + protected notifcationsService: NotificationsService, + protected translate: TranslateService) { + super(searchService, notifcationsService, translate); } /** @@ -37,13 +43,15 @@ export class AuthorizedCollectionSelectorComponent extends DSOSelectorComponent * @param query Query to search objects for * @param page Page to retrieve */ - search(query: string, page: number): Observable>> { + search(query: string, page: number): Observable>>> { return this.collectionDataService.getAuthorizedCollection(query, Object.assign({ currentPage: page, elementsPerPage: this.defaultPagination.pageSize }),true, false, followLink('parentCommunity')).pipe( - getFirstSucceededRemoteDataPayload(), - map((list) => buildPaginatedList(list.pageInfo, list.page.map((col) => Object.assign(new CollectionSearchResult(), { indexableObject: col })))) + getFirstCompletedRemoteData(), + map((rd) => Object.assign(new RemoteData(null, null, null, null), rd, { + payload: hasValue(rd.payload) ? buildPaginatedList(rd.payload.pageInfo, rd.payload.page.map((col) => Object.assign(new CollectionSearchResult(), { indexableObject: col }))) : null, + })) ); } } diff --git a/src/app/shared/dso-selector/dso-selector/dso-selector.component.html b/src/app/shared/dso-selector/dso-selector/dso-selector.component.html index 0e914a3783..122f37b031 100644 --- a/src/app/shared/dso-selector/dso-selector/dso-selector.component.html +++ b/src/app/shared/dso-selector/dso-selector/dso-selector.component.html @@ -10,24 +10,26 @@
- - - + + diff --git a/src/app/shared/dso-selector/dso-selector/dso-selector.component.spec.ts b/src/app/shared/dso-selector/dso-selector/dso-selector.component.spec.ts index 11418cfcf2..9e68c0564b 100644 --- a/src/app/shared/dso-selector/dso-selector/dso-selector.component.spec.ts +++ b/src/app/shared/dso-selector/dso-selector/dso-selector.component.spec.ts @@ -6,10 +6,11 @@ import { SearchService } from '../../../core/shared/search/search.service'; import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model'; import { ItemSearchResult } from '../../object-collection/shared/item-search-result.model'; import { Item } from '../../../core/shared/item.model'; -import { createSuccessfulRemoteDataObject$ } from '../../remote-data.utils'; +import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../remote-data.utils'; import { PaginatedSearchOptions } from '../../search/paginated-search-options.model'; import { hasValue } from '../../empty.util'; import { createPaginatedList } from '../../testing/utils.test'; +import { NotificationsService } from '../../notifications/notifications.service'; describe('DSOSelectorComponent', () => { let component: DSOSelectorComponent; @@ -59,12 +60,17 @@ describe('DSOSelectorComponent', () => { }); } + let notificationsService: NotificationsService; + beforeEach(waitForAsync(() => { + notificationsService = jasmine.createSpyObj('notificationsService', ['error']); + TestBed.configureTestingModule({ imports: [TranslateModule.forRoot()], declarations: [DSOSelectorComponent], providers: [ { provide: SearchService, useValue: searchService }, + { provide: NotificationsService, useValue: notificationsService }, ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); @@ -104,4 +110,15 @@ describe('DSOSelectorComponent', () => { }); }); }); + + describe('when search returns an error', () => { + beforeEach(() => { + spyOn(searchService, 'search').and.returnValue(createFailedRemoteDataObject$()); + component.ngOnInit(); + }); + + it('should display an error notification', () => { + expect(notificationsService.error).toHaveBeenCalled(); + }); + }); }); diff --git a/src/app/shared/dso-selector/dso-selector/dso-selector.component.ts b/src/app/shared/dso-selector/dso-selector/dso-selector.component.ts index 23931bf42b..c62e0df763 100644 --- a/src/app/shared/dso-selector/dso-selector/dso-selector.component.ts +++ b/src/app/shared/dso-selector/dso-selector/dso-selector.component.ts @@ -27,10 +27,13 @@ import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model' import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { ViewMode } from '../../../core/shared/view-mode.model'; import { Context } from '../../../core/shared/context.model'; -import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; -import { hasValue, isEmpty, isNotEmpty } from '../../empty.util'; +import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; +import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../empty.util'; import { PaginatedList, buildPaginatedList } from '../../../core/data/paginated-list.model'; import { SearchResult } from '../../search/search-result.model'; +import { RemoteData } from '../../../core/data/remote-data'; +import { NotificationsService } from '../../notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'ds-dso-selector', @@ -78,7 +81,7 @@ export class DSOSelectorComponent implements OnInit, OnDestroy { /** * List with search results of DSpace objects for the current query */ - listEntries: SearchResult[] = []; + listEntries: SearchResult[] = null; /** * The current page to load @@ -93,9 +96,9 @@ export class DSOSelectorComponent implements OnInit, OnDestroy { hasNextPage = false; /** - * Whether or not the list should be reset next time it receives a page to load + * Whether or not new results are currently loading */ - resetList = false; + loading = false; /** * List of element references to all elements @@ -123,7 +126,9 @@ export class DSOSelectorComponent implements OnInit, OnDestroy { */ public subs: Subscription[] = []; - constructor(protected searchService: SearchService) { + constructor(protected searchService: SearchService, + protected notifcationsService: NotificationsService, + protected translate: TranslateService) { } /** @@ -136,7 +141,7 @@ export class DSOSelectorComponent implements OnInit, OnDestroy { // Create an observable searching for the current DSO (return empty list if there's no current DSO) let currentDSOResult$; if (isNotEmpty(this.currentDSOId)) { - currentDSOResult$ = this.search(this.getCurrentDSOQuery(), 1); + currentDSOResult$ = this.search(this.getCurrentDSOQuery(), 1).pipe(getFirstSucceededRemoteDataPayload()); } else { currentDSOResult$ = observableOf(buildPaginatedList(undefined, [])); } @@ -152,31 +157,41 @@ export class DSOSelectorComponent implements OnInit, OnDestroy { this.currentPage$ ).pipe( switchMap(([currentDSOResult, query, page]: [PaginatedList>, string, number]) => { + this.loading = true; if (page === 1) { // The first page is loading, this means we should reset the list instead of adding to it - this.resetList = true; + this.listEntries = null; } return this.search(query, page).pipe( - map((list) => { - // If it's the first page and no query is entered, add the current DSO to the start of the list - // If no query is entered, filter out the current DSO from the results, as it'll be displayed at the start of the list already - list.page = [ - ...((isEmpty(query) && page === 1) ? currentDSOResult.page : []), - ...list.page.filter((result) => isNotEmpty(query) || result.indexableObject.id !== this.currentDSOId) - ]; - return list; + map((rd) => { + if (rd.hasSucceeded) { + // If it's the first page and no query is entered, add the current DSO to the start of the list + // If no query is entered, filter out the current DSO from the results, as it'll be displayed at the start of the list already + rd.payload.page = [ + ...((isEmpty(query) && page === 1) ? currentDSOResult.page : []), + ...rd.payload.page.filter((result) => isNotEmpty(query) || result.indexableObject.id !== this.currentDSOId) + ]; + } else if (rd.hasFailed) { + this.notifcationsService.error(this.translate.instant('dso-selector.error.title', { type: this.typesString }), rd.errorMessage); + } + return rd; }) ); }) - ).subscribe((list) => { - if (this.resetList) { - this.listEntries = list.page; - this.resetList = false; + ).subscribe((rd) => { + this.loading = false; + if (rd.hasSucceeded) { + if (hasNoValue(this.listEntries)) { + this.listEntries = rd.payload.page; + } else { + this.listEntries.push(...rd.payload.page); + } + // Check if there are more pages available after the current one + this.hasNextPage = rd.payload.totalElements > this.listEntries.length; } else { - this.listEntries.push(...list.page); + this.listEntries = null; + this.hasNextPage = false; } - // Check if there are more pages available after the current one - this.hasNextPage = list.totalElements > this.listEntries.length; })); } @@ -192,7 +207,7 @@ export class DSOSelectorComponent implements OnInit, OnDestroy { * @param query Query to search objects for * @param page Page to retrieve */ - search(query: string, page: number): Observable>> { + search(query: string, page: number): Observable>>> { return this.searchService.search( new PaginatedSearchOptions({ query: query, @@ -202,7 +217,7 @@ export class DSOSelectorComponent implements OnInit, OnDestroy { }) }) ).pipe( - getFirstSucceededRemoteDataPayload() + getFirstCompletedRemoteData() ); } @@ -210,7 +225,7 @@ export class DSOSelectorComponent implements OnInit, OnDestroy { * When the user reaches the bottom of the page (or almost) and there's a next page available, increase the current page */ onScrollDown() { - if (this.hasNextPage) { + if (this.hasNextPage && !this.loading) { this.currentPage$.next(this.currentPage$.value + 1); } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.spec.ts index 47dd857d53..19d4760183 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.spec.ts @@ -21,8 +21,6 @@ import { createPaginatedList } from '../../../../testing/utils.test'; import { ExternalSourceService } from '../../../../../core/data/external-source.service'; import { LookupRelationService } from '../../../../../core/data/lookup-relation.service'; import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service'; -import { SubmissionService } from '../../../../../submission/submission.service'; -import { SubmissionObjectDataService } from '../../../../../core/submission/submission-object-data.service'; import { WorkspaceItem } from '../../../../../core/submission/models/workspaceitem.model'; import { Collection } from '../../../../../core/shared/collection.model'; @@ -46,8 +44,6 @@ describe('DsDynamicLookupRelationModalComponent', () => { let lookupRelationService; let rdbService; let submissionId; - let submissionService; - let submissionObjectDataService; const externalSources = [ Object.assign(new ExternalSource(), { @@ -99,12 +95,6 @@ describe('DsDynamicLookupRelationModalComponent', () => { rdbService = jasmine.createSpyObj('rdbService', { aggregate: createSuccessfulRemoteDataObject$(externalSources) }); - submissionService = jasmine.createSpyObj('SubmissionService', { - dispatchSave: jasmine.createSpy('dispatchSave') - }); - submissionObjectDataService = jasmine.createSpyObj('SubmissionObjectDataService', { - findById: createSuccessfulRemoteDataObject$(testWSI) - }); submissionId = '1234'; } @@ -129,8 +119,6 @@ describe('DsDynamicLookupRelationModalComponent', () => { }, { provide: RelationshipTypeService, useValue: {} }, { provide: RemoteDataBuildService, useValue: rdbService }, - { provide: SubmissionService, useValue: submissionService }, - { provide: SubmissionObjectDataService, useValue: submissionObjectDataService }, { provide: Store, useValue: { // tslint:disable-next-line:no-empty diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts index 4ed972b2fa..6d3b14d8e8 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts @@ -12,11 +12,8 @@ import { RelationshipOptions } from '../../models/relationship-options.model'; import { SearchResult } from '../../../../search/search-result.model'; import { Item } from '../../../../../core/shared/item.model'; import { - getAllSucceededRemoteData, - getAllSucceededRemoteDataPayload, - getRemoteDataPayload -} from '../../../../../core/shared/operators'; -import { AddRelationshipAction, RemoveRelationshipAction, UpdateRelationshipNameVariantAction } from './relationship.actions'; + AddRelationshipAction, RemoveRelationshipAction, UpdateRelationshipNameVariantAction, +} from './relationship.actions'; import { RelationshipService } from '../../../../../core/data/relationship.service'; import { RelationshipTypeService } from '../../../../../core/data/relationship-type.service'; import { Store } from '@ngrx/store'; @@ -27,12 +24,7 @@ import { ExternalSource } from '../../../../../core/shared/external-source.model import { ExternalSourceService } from '../../../../../core/data/external-source.service'; import { Router } from '@angular/router'; import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service'; -import { followLink } from '../../../../utils/follow-link-config.model'; -import { SubmissionObject } from '../../../../../core/submission/models/submission-object.model'; -import { Collection } from '../../../../../core/shared/collection.model'; -import { SubmissionService } from '../../../../../submission/submission.service'; -import { SubmissionObjectDataService } from '../../../../../core/submission/submission-object-data.service'; -import { RemoteData } from '../../../../../core/data/remote-data'; +import { getAllSucceededRemoteDataPayload } from '../../../../../core/shared/operators'; @Component({ selector: 'ds-dynamic-lookup-relation-modal', @@ -122,10 +114,6 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy */ totalExternal$: Observable; - /** - * List of subscriptions to unsubscribe from - */ - private subs: Subscription[] = []; constructor( public modal: NgbActiveModal, @@ -136,17 +124,14 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy private lookupRelationService: LookupRelationService, private searchConfigService: SearchConfigurationService, private rdbService: RemoteDataBuildService, - private submissionService: SubmissionService, - private submissionObjectService: SubmissionObjectDataService, private zone: NgZone, private store: Store, - private router: Router + private router: Router, ) { } ngOnInit(): void { - this.setItem(); this.selection$ = this.selectableListService .getSelectableList(this.listId) .pipe(map((listState: SelectableListState) => hasValue(listState) && hasValue(listState.selection) ? listState.selection : [])); @@ -206,24 +191,6 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy }); } - /** - * Initialize this.item$ based on this.model.submissionId - */ - private setItem() { - const submissionObject$ = this.submissionObjectService - .findById(this.submissionId, true, true, followLink('item'), followLink('collection')).pipe( - getAllSucceededRemoteData(), - getRemoteDataPayload() - ); - - const item$ = submissionObject$.pipe(switchMap((submissionObject: SubmissionObject) => (submissionObject.item as Observable>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()))); - const collection$ = submissionObject$.pipe(switchMap((submissionObject: SubmissionObject) => (submissionObject.collection as Observable>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()))); - - this.subs.push(item$.subscribe((item) => this.item = item)); - this.subs.push(collection$.subscribe((collection) => this.collection = collection)); - - } - /** * Add a subscription updating relationships with name variants * @param sri The search result to track name variants for @@ -279,8 +246,5 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy ngOnDestroy() { this.router.navigate([], {}); Object.values(this.subMap).forEach((subscription) => subscription.unsubscribe()); - this.subs - .filter((sub) => hasValue(sub)) - .forEach((sub) => sub.unsubscribe()); } } diff --git a/src/app/shared/search/paginated-search-options.model.spec.ts b/src/app/shared/search/paginated-search-options.model.spec.ts index b75d2b8fab..9881cc1149 100644 --- a/src/app/shared/search/paginated-search-options.model.spec.ts +++ b/src/app/shared/search/paginated-search-options.model.spec.ts @@ -8,7 +8,12 @@ describe('PaginatedSearchOptions', () => { let options: PaginatedSearchOptions; const sortOptions = new SortOptions('test.field', SortDirection.DESC); const pageOptions = Object.assign(new PaginationComponentOptions(), { pageSize: 40, page: 1 }); - const filters = [new SearchFilter('f.test', ['value']), new SearchFilter('f.example', ['another value', 'second value'])]; + const filters = [ + new SearchFilter('f.test', ['value']), + new SearchFilter('f.example', ['another value', 'second value']), // should be split into two arguments, spaces should be URI-encoded + new SearchFilter('f.range', ['[2002 TO 2021]'], 'equals'), // value should be URI-encoded, ',equals' should not + ]; + const fixedFilter = 'f.fixed=1234,5678,equals'; // '=' and ',equals' should not be URI-encoded const query = 'search query'; const scope = '0fde1ecb-82cc-425a-b600-ac3576d76b47'; const baseUrl = 'www.rest.com'; @@ -19,7 +24,8 @@ describe('PaginatedSearchOptions', () => { filters: filters, query: query, scope: scope, - dsoTypes: [DSpaceObjectType.ITEM] + dsoTypes: [DSpaceObjectType.ITEM], + fixedFilter: fixedFilter, }); }); @@ -31,12 +37,14 @@ describe('PaginatedSearchOptions', () => { 'sort=test.field,DESC&' + 'page=0&' + 'size=40&' + - 'query=search query&' + + 'f.fixed=1234%2C5678,equals&' + + 'query=search%20query&' + 'scope=0fde1ecb-82cc-425a-b600-ac3576d76b47&' + 'dsoType=ITEM&' + 'f.test=value&' + - 'f.example=another value&' + - 'f.example=second value' + 'f.example=another%20value&' + + 'f.example=second%20value&' + + 'f.range=%5B2002%20TO%202021%5D,equals' ); }); diff --git a/src/app/shared/search/search-filters/search-filter/search-filter.component.ts b/src/app/shared/search/search-filters/search-filter/search-filter.component.ts index 76a8a0d323..31ace10a7d 100644 --- a/src/app/shared/search/search-filters/search-filter/search-filter.component.ts +++ b/src/app/shared/search/search-filters/search-filter/search-filter.component.ts @@ -35,7 +35,7 @@ export class SearchFilterComponent implements OnInit { /** * True when the filter is 100% collapsed in the UI */ - closed = true; + closed: boolean; /** * Emits true when the filter is currently collapsed in the store diff --git a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.scss b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.scss index 2b5029fce2..2c98280e7f 100644 --- a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.scss +++ b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.scss @@ -8,18 +8,16 @@ ::ng-deep { - --ds-slider-handle-width: 18px; - html:not([dir=rtl]) .noUi-horizontal .noUi-handle { right: calc(var(--ds-slider-handle-width) / -2); } .noUi-horizontal .noUi-handle { width: var(--ds-slider-handle-width); &:before { - left: calc(calc(calc(var(--ds-slider-handle-width) - 2) / 2) - 2); + left: calc(((var(--ds-slider-handle-width) - 2px) / 2) - 2px); } &:after { - left: calc(calc(calc(var(--ds-slider-handle-width) - 2) / 2) + 2); + left: calc(((var(--ds-slider-handle-width) - 2px) / 2) + 2px); } &:focus { outline: none; 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 c3139c0217..62b1cb98a6 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 @@ -56,7 +56,7 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple /** * Fallback maximum for the range */ - max = 2018; + max = new Date().getFullYear(); /** * The current range of the filter diff --git a/src/app/shared/search/search-options.model.spec.ts b/src/app/shared/search/search-options.model.spec.ts index 62fe732218..8bed046736 100644 --- a/src/app/shared/search/search-options.model.spec.ts +++ b/src/app/shared/search/search-options.model.spec.ts @@ -4,13 +4,25 @@ import { SearchFilter } from './search-filter.model'; import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model'; describe('SearchOptions', () => { - let options: PaginatedSearchOptions; - const filters = [new SearchFilter('f.test', ['value']), new SearchFilter('f.example', ['another value', 'second value'])]; + let options: SearchOptions; + + const filters = [ + new SearchFilter('f.test', ['value']), + new SearchFilter('f.example', ['another value', 'second value']), // should be split into two arguments, spaces should be URI-encoded + new SearchFilter('f.range', ['[2002 TO 2021]'], 'equals'), // value should be URI-encoded, ',equals' should not + ]; + const fixedFilter = 'f.fixed=1234,5678,equals'; // '=' and ',equals' should not be URI-encoded const query = 'search query'; const scope = '0fde1ecb-82cc-425a-b600-ac3576d76b47'; const baseUrl = 'www.rest.com'; beforeEach(() => { - options = new SearchOptions({ filters: filters, query: query, scope: scope, dsoTypes: [DSpaceObjectType.ITEM] }); + options = new SearchOptions({ + filters: filters, + query: query, + scope: scope, + dsoTypes: [DSpaceObjectType.ITEM], + fixedFilter: fixedFilter, + }); }); describe('when toRestUrl is called', () => { @@ -18,12 +30,14 @@ describe('SearchOptions', () => { it('should generate a string with all parameters that are present', () => { const outcome = options.toRestUrl(baseUrl); expect(outcome).toEqual('www.rest.com?' + - 'query=search query&' + + 'f.fixed=1234%2C5678,equals&' + + 'query=search%20query&' + 'scope=0fde1ecb-82cc-425a-b600-ac3576d76b47&' + 'dsoType=ITEM&' + 'f.test=value&' + - 'f.example=another value&' + - 'f.example=second value' + 'f.example=another%20value&' + + 'f.example=second%20value&' + + 'f.range=%5B2002%20TO%202021%5D,equals' ); }); diff --git a/src/app/shared/search/search-options.model.ts b/src/app/shared/search/search-options.model.ts index fb4e8d4caf..591e4fcb04 100644 --- a/src/app/shared/search/search-options.model.ts +++ b/src/app/shared/search/search-options.model.ts @@ -1,4 +1,4 @@ -import { isNotEmpty } from '../empty.util'; +import { hasValue, isNotEmpty } from '../empty.util'; import { URLCombiner } from '../../core/url-combiner/url-combiner'; import { SearchFilter } from './search-filter.model'; import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model'; @@ -13,10 +13,15 @@ export class SearchOptions { scope?: string; query?: string; dsoTypes?: DSpaceObjectType[]; - filters?: any; - fixedFilter?: any; + filters?: SearchFilter[]; + fixedFilter?: string; - constructor(options: {configuration?: string, scope?: string, query?: string, dsoTypes?: DSpaceObjectType[], filters?: SearchFilter[], fixedFilter?: any}) { + constructor( + options: { + configuration?: string, scope?: string, query?: string, dsoTypes?: DSpaceObjectType[], filters?: SearchFilter[], + fixedFilter?: string + } + ) { this.configuration = options.configuration; this.scope = options.scope; this.query = options.query; @@ -33,27 +38,27 @@ export class SearchOptions { */ toRestUrl(url: string, args: string[] = []): string { if (isNotEmpty(this.configuration)) { - args.push(`configuration=${this.configuration}`); + args.push(`configuration=${encodeURIComponent(this.configuration)}`); } if (isNotEmpty(this.fixedFilter)) { - args.push(this.fixedFilter); + args.push(this.encodedFixedFilter); } if (isNotEmpty(this.query)) { - args.push(`query=${this.query}`); + args.push(`query=${encodeURIComponent(this.query)}`); } if (isNotEmpty(this.scope)) { - args.push(`scope=${this.scope}`); + args.push(`scope=${encodeURIComponent(this.scope)}`); } if (isNotEmpty(this.dsoTypes)) { this.dsoTypes.forEach((dsoType: string) => { - args.push(`dsoType=${dsoType}`); + args.push(`dsoType=${encodeURIComponent(dsoType)}`); }); } if (isNotEmpty(this.filters)) { this.filters.forEach((filter: SearchFilter) => { filter.values.forEach((value) => { const filterValue = value.includes(',') ? `${value}` : value + (filter.operator ? ',' + filter.operator : ''); - args.push(`${filter.key}=${filterValue}`); + args.push(`${filter.key}=${this.encodeFilterQueryValue(filterValue)}`); }); }); } @@ -62,4 +67,28 @@ export class SearchOptions { } return url; } + + get encodedFixedFilter(): string { + // expected format: 'arg=value' + // -> split the query agument into (arg=)(value) and only encode 'value' + const match = this.fixedFilter.match(/^([^=]+=)(.+)$/); + + if (hasValue(match)) { + return match[1] + this.encodeFilterQueryValue(match[2]); + } else { + return this.encodeFilterQueryValue(this.fixedFilter); + } + } + + encodeFilterQueryValue(filterQueryValue: string): string { + // expected format: 'value' or 'value,operator' + // -> split into (value)(,operator) and only encode 'value' + const match = filterQueryValue.match(/^(.*)(,\w+)$/); + + if (hasValue(match)) { + return encodeURIComponent(match[1]) + match[2]; + } else { + return encodeURIComponent(filterQueryValue); + } + } } diff --git a/src/app/shared/search/search-settings/search-settings.component.html b/src/app/shared/search/search-settings/search-settings.component.html index f6806e6843..a31678743d 100644 --- a/src/app/shared/search/search-settings/search-settings.component.html +++ b/src/app/shared/search/search-settings/search-settings.component.html @@ -1,4 +1,4 @@ - +

{{ 'search.sidebar.settings.title' | translate}}

-
-
\ No newline at end of file +
diff --git a/src/app/shared/search/search-settings/search-settings.component.spec.ts b/src/app/shared/search/search-settings/search-settings.component.spec.ts index dff8935ef5..6c483a5bc4 100644 --- a/src/app/shared/search/search-settings/search-settings.component.spec.ts +++ b/src/app/shared/search/search-settings/search-settings.component.spec.ts @@ -12,7 +12,6 @@ import { EnumKeysPipe } from '../../utils/enum-keys-pipe'; import { By } from '@angular/platform-browser'; import { SearchFilterService } from '../../../core/shared/search/search-filter.service'; import { VarDirective } from '../../utils/var.directive'; -import { take } from 'rxjs/operators'; import { SEARCH_CONFIG_SERVICE } from '../../../+my-dspace-page/my-dspace-page.component'; import { SidebarService } from '../../sidebar/sidebar.service'; import { SidebarServiceStub } from '../../testing/sidebar-service.stub'; @@ -91,7 +90,7 @@ describe('SearchSettingsComponent', () => { provide: SEARCH_CONFIG_SERVICE, useValue: { paginatedSearchOptions: observableOf(paginatedSearchOptions), - getCurrentScope: observableOf('test-id') + getCurrentScope: observableOf('test-id'), } }, ], @@ -103,6 +102,14 @@ describe('SearchSettingsComponent', () => { fixture = TestBed.createComponent(SearchSettingsComponent); comp = fixture.componentInstance; + comp.sortOptions = [ + new SortOptions('score', SortDirection.DESC), + new SortOptions('dc.title', SortDirection.ASC), + new SortOptions('dc.title', SortDirection.DESC) + ]; + + comp.searchOptions = paginatedSearchOptions; + // SearchPageComponent test instance fixture.detectChanges(); searchServiceObject = (comp as any).service; @@ -111,34 +118,24 @@ describe('SearchSettingsComponent', () => { }); - it('it should show the order settings with the respective selectable options', (done) => { - (comp as any).searchOptions$.pipe(take(1)).subscribe((options) => { - fixture.detectChanges(); - const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings')); - expect(orderSetting).toBeDefined(); - const childElements = orderSetting.queryAll(By.css('option')); - expect(childElements.length).toEqual(comp.searchOptionPossibilities.length); - done(); - }); + it('it should show the order settings with the respective selectable options', () => { + fixture.detectChanges(); + const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings')); + expect(orderSetting).toBeDefined(); + const childElements = orderSetting.queryAll(By.css('option')); + expect(childElements.length).toEqual(comp.sortOptions.length); }); - it('it should show the size settings', (done) => { - (comp as any).searchOptions$.pipe(take(1)).subscribe((options) => { - fixture.detectChanges(); - const pageSizeSetting = fixture.debugElement.query(By.css('page-size-settings')); - expect(pageSizeSetting).toBeDefined(); - done(); - } - ); + it('it should show the size settings', () => { + fixture.detectChanges(); + const pageSizeSetting = fixture.debugElement.query(By.css('page-size-settings')); + expect(pageSizeSetting).toBeDefined(); }); - it('should have the proper order value selected by default', (done) => { - (comp as any).searchOptions$.pipe(take(1)).subscribe((options) => { - fixture.detectChanges(); - const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings')); - const childElementToBeSelected = orderSetting.query(By.css('option[value="0"][selected="selected"]')); - expect(childElementToBeSelected).toBeDefined(); - done(); - }); + it('should have the proper order value selected by default', () => { + fixture.detectChanges(); + const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings')); + const childElementToBeSelected = orderSetting.query(By.css('option[value="score,DESC"][selected="selected"]')); + expect(childElementToBeSelected).toBeDefined(); }); }); diff --git a/src/app/shared/search/search-settings/search-settings.component.ts b/src/app/shared/search/search-settings/search-settings.component.ts index 2241fc6c6a..b9d86571f6 100644 --- a/src/app/shared/search/search-settings/search-settings.component.ts +++ b/src/app/shared/search/search-settings/search-settings.component.ts @@ -1,9 +1,8 @@ -import { Component, Inject, OnInit } from '@angular/core'; +import { Component, Inject, Input } from '@angular/core'; import { SearchService } from '../../../core/shared/search/search.service'; import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model'; import { ActivatedRoute, Router } from '@angular/router'; import { PaginatedSearchOptions } from '../paginated-search-options.model'; -import { Observable } from 'rxjs'; import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service'; import { SEARCH_CONFIG_SERVICE } from '../../../+my-dspace-page/my-dspace-page.component'; import { PaginationService } from '../../../core/pagination/pagination.service'; @@ -17,16 +16,17 @@ import { PaginationService } from '../../../core/pagination/pagination.service'; /** * This component represents the part of the search sidebar that contains the general search settings. */ -export class SearchSettingsComponent implements OnInit { +export class SearchSettingsComponent { + /** * The configuration for the current paginated search results */ - searchOptions$: Observable; + @Input() searchOptions: PaginatedSearchOptions; /** * All sort options that are shown in the settings */ - searchOptionPossibilities = [new SortOptions('score', SortDirection.DESC), new SortOptions('dc.title', SortDirection.ASC), new SortOptions('dc.title', SortDirection.DESC)]; + @Input() sortOptions: SortOptions[]; constructor(private service: SearchService, private route: ActivatedRoute, @@ -35,13 +35,6 @@ export class SearchSettingsComponent implements OnInit { @Inject(SEARCH_CONFIG_SERVICE) public searchConfigurationService: SearchConfigurationService) { } - /** - * Initialize paginated search options - */ - ngOnInit(): void { - this.searchOptions$ = this.searchConfigurationService.paginatedSearchOptions; - } - /** * Method to change the current sort field and direction * @param {Event} event Change event containing the sort direction and sort field diff --git a/src/app/shared/search/search-sidebar/search-sidebar.component.html b/src/app/shared/search/search-sidebar/search-sidebar.component.html index 74abeadfd8..624d094d22 100644 --- a/src/app/shared/search/search-sidebar/search-sidebar.component.html +++ b/src/app/shared/search/search-sidebar/search-sidebar.component.html @@ -12,7 +12,7 @@
diff --git a/src/app/shared/search/search-sidebar/search-sidebar.component.ts b/src/app/shared/search/search-sidebar/search-sidebar.component.ts index 2060e0f345..7f134cacd3 100644 --- a/src/app/shared/search/search-sidebar/search-sidebar.component.ts +++ b/src/app/shared/search/search-sidebar/search-sidebar.component.ts @@ -2,6 +2,8 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; import { SearchConfigurationOption } from '../search-switch-configuration/search-configuration-option.model'; import { Observable } from 'rxjs'; +import { PaginatedSearchOptions } from '../paginated-search-options.model'; +import { SortOptions } from '../../../core/cache/models/sort-options.model'; /** * This component renders a simple item page. @@ -45,6 +47,16 @@ export class SearchSidebarComponent { */ @Input() inPlaceSearch; + /** + * The configuration for the current paginated search results + */ + @Input() searchOptions: PaginatedSearchOptions; + + /** + * All sort options that are shown in the settings + */ + @Input() sortOptions: SortOptions[]; + /** * Emits when the search filters values may be stale, and so they must be refreshed. */ diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index bdec3bfb23..4c3317a0c0 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1168,6 +1168,8 @@ "dso-selector.edit.item.head": "Edit item", + "dso-selector.error.title": "An error occurred searching for a {{ type }}", + "dso-selector.export-metadata.dspaceobject.head": "Export metadata from", "dso-selector.no-results": "No {{ type }} found", @@ -3010,6 +3012,8 @@ "search.results.empty": "Your search returned no results.", + "default.search.results.head": "Search Results", + "search.sidebar.close": "Back to results", @@ -3043,8 +3047,21 @@ "sorting.dc.title.DESC": "Title Descending", - "sorting.score.DESC": "Relevance", + "sorting.score.ASC": "Least Relevant", + "sorting.score.DESC": "Most Relevant", + + "sorting.dc.date.issued.ASC": "Date Issued Ascending", + + "sorting.dc.date.issued.DESC": "Date Issued Descending", + + "sorting.dc.date.accessioned.ASC": "Accessioned Date Ascending", + + "sorting.dc.date.accessioned.DESC": "Accessioned Date Descending", + + "sorting.lastModified.ASC": "Last modified Ascending", + + "sorting.lastModified.DESC": "Last modified Descending", "statistics.title": "Statistics", diff --git a/src/styles/_custom_variables.scss b/src/styles/_custom_variables.scss index 56cf4abca9..298be09f67 100644 --- a/src/styles/_custom_variables.scss +++ b/src/styles/_custom_variables.scss @@ -78,4 +78,5 @@ --ds-breadcrumb-link-active-color: #{darken($cyan, 30%)}; --ds-slider-color: #{$green}; + --ds-slider-handle-width: 18px; }