From 9b8ada0326049ad2e17d6684a304b2a99202a931 Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Tue, 6 Apr 2021 10:06:15 +0200 Subject: [PATCH 01/27] [CST-4009] Discovery result sort options not reflecting what is configured --- .../my-dspace-page.component.html | 4 + .../my-dspace-page.component.spec.ts | 24 +++++- .../my-dspace-page.component.ts | 34 ++++++++- src/app/core/core.module.ts | 2 + .../search-filters/search-config.model.ts | 75 ++++++++++++++++++ .../search-config.resource-type.ts | 9 +++ .../core/shared/search/search.service.spec.ts | 50 ++++++++++++ src/app/core/shared/search/search.service.ts | 76 +++++++++++++------ .../search-settings.component.html | 6 +- .../search-settings.component.spec.ts | 51 ++++++------- .../search-settings.component.ts | 19 ++--- .../search-sidebar.component.html | 2 +- .../search-sidebar.component.ts | 12 +++ 13 files changed, 292 insertions(+), 72 deletions(-) create mode 100644 src/app/core/shared/search/search-filters/search-config.model.ts create mode 100644 src/app/core/shared/search/search-filters/search-config.resource-type.ts 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..909239b61c 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', 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..ba5c0e5cc7 100644 --- a/src/app/+my-dspace-page/my-dspace-page.component.ts +++ b/src/app/+my-dspace-page/my-dspace-page.component.ts @@ -7,8 +7,8 @@ import { OnInit } from '@angular/core'; -import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs'; -import { map, switchMap, tap, } from 'rxjs/operators'; +import { BehaviorSubject, combineLatest, Observable, Subject, Subscription } from 'rxjs'; +import { map, switchMap, take, tap } from 'rxjs/operators'; import { PaginatedList } from '../core/data/paginated-list.model'; import { RemoteData } from '../core/data/remote-data'; @@ -19,7 +19,7 @@ import { PaginatedSearchOptions } from '../shared/search/paginated-search-option import { SearchService } from '../core/shared/search/search.service'; import { SidebarService } from '../shared/sidebar/sidebar.service'; import { hasValue } from '../shared/empty.util'; -import { getFirstSucceededRemoteData } from '../core/shared/operators'; +import { getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload } from '../core/shared/operators'; import { MyDSpaceResponseParsingService } from '../core/data/mydspace-response-parsing.service'; import { SearchConfigurationOption } from '../shared/search/search-switch-configuration/search-configuration-option.model'; import { RoleType } from '../core/roles/role-types'; @@ -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 { SortDirection, SortOptions } from '../core/cache/models/sort-options.model'; +import { SearchConfig } from '../core/shared/search/search-filters/search-config.model'; 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 */ @@ -151,6 +158,27 @@ export class MyDSpacePageComponent implements OnInit { }) ); + this.sortOptions$ = this.context$.pipe( + switchMap((context) => this.service.getSearchConfigurationFor(null, context)), + getFirstSucceededRemoteDataPayload(), + map((searchConfig: 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; + })); + + combineLatest([ + this.sortOptions$, + this.searchConfigService.paginatedSearchOptions + ]).pipe(take(1)) + .subscribe(([sortOptions, searchOptions]) => { + const updateValue = Object.assign(new PaginatedSearchOptions({}), searchOptions, { sort: sortOptions[0]}); + this.searchConfigService.paginatedSearchOptions.next(updateValue); + }); + } /** diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index f73bfd0bdf..62265fdedf 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -162,6 +162,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 @@ -342,6 +343,7 @@ export const models = Registration, UsageReport, Root, + SearchConfig ]; @NgModule({ 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..dd7a799f37 --- /dev/null +++ b/src/app/core/shared/search/search-filters/search-config.model.ts @@ -0,0 +1,75 @@ +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; +} + +/** + * 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 06208094bd..d09444ad6c 100644 --- a/src/app/core/shared/search/search.service.spec.ts +++ b/src/app/core/shared/search/search.service.spec.ts @@ -230,5 +230,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 b380a70d44..7747717830 100644 --- a/src/app/core/shared/search/search.service.ts +++ b/src/app/core/shared/search/search.service.ts @@ -37,12 +37,19 @@ 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'; /** * Service that performs all general actions that have to do with the search page */ @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 */ @@ -224,6 +231,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 @@ -232,33 +257,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( @@ -397,6 +406,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/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 cd4a872815..221e3a0dea 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'; @@ -81,7 +80,7 @@ describe('SearchSettingsComponent', () => { provide: SEARCH_CONFIG_SERVICE, useValue: { paginatedSearchOptions: observableOf(paginatedSearchOptions), - getCurrentScope: observableOf('test-id') + getCurrentScope: observableOf('test-id'), } }, ], @@ -93,6 +92,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; @@ -101,34 +108,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 45d7c7b432..d85f234aa3 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 { SortOptions } from '../../../core/cache/models/sort-options.model'; import { ActivatedRoute, NavigationExtras, 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'; @@ -16,16 +15,17 @@ import { SEARCH_CONFIG_SERVICE } from '../../../+my-dspace-page/my-dspace-page.c /** * 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, @@ -33,13 +33,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. */ From 4d85c0270f31332fad98e6effb68bf50e68b0ea7 Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Tue, 6 Apr 2021 10:30:26 +0200 Subject: [PATCH 02/27] [CST-4009] sorting options translations --- src/assets/i18n/en.json5 | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 2924a3a47e..20a21f93e8 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -3037,8 +3037,13 @@ "sorting.dc.title.DESC": "Title Descending", - "sorting.score.DESC": "Relevance", + "sorting.score.ASC": "Relevance Ascending", + "sorting.score.DESC": "Relevance Descending", + + "sorting.dc.date.issued.ASC": "Date Issued Ascending", + + "sorting.dc.date.issued.DESC": "Date Issued Descending", "statistics.title": "Statistics", From b4686deb63e1df21fb0bf3889a4b32ac52e038d4 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Tue, 6 Apr 2021 17:49:06 +0200 Subject: [PATCH 03/27] [CST-4009] Retrieve sort options also for search page --- src/app/+search-page/search.component.html | 7 +++- src/app/+search-page/search.component.spec.ts | 24 ++++++++++- src/app/+search-page/search.component.ts | 40 ++++++++++++++++--- 3 files changed, 63 insertions(+), 8 deletions(-) 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..06061c1d40 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', 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..43535278a1 100644 --- a/src/app/+search-page/search.component.ts +++ b/src/app/+search-page/search.component.ts @@ -1,14 +1,14 @@ import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core'; -import { BehaviorSubject, Observable, Subscription } from 'rxjs'; -import { startWith, switchMap, } from 'rxjs/operators'; +import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs'; +import { map, startWith, switchMap, take, } 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 { getFirstSucceededRemoteData } from '../core/shared/operators'; +import { hasValue, isEmpty } from '../shared/empty.util'; +import { getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload } from '../core/shared/operators'; import { RouteService } from '../core/services/route.service'; import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component'; import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model'; @@ -18,6 +18,8 @@ import { SearchService } from '../core/shared/search/search.service'; import { currentPath } from '../shared/utils/route.utils'; import { Router } from '@angular/router'; import { Context } from '../core/shared/context.model'; +import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model'; +import { SearchConfig } from '../core/shared/search/search-filters/search-config.model'; @Component({ selector: 'ds-search', @@ -47,6 +49,11 @@ export class SearchComponent implements OnInit { */ searchOptions$: Observable; + /** + * The current available sort options + */ + sortOptions$: Observable; + /** * The current relevant scopes */ @@ -129,9 +136,32 @@ export class SearchComponent implements OnInit { this.scopeListRD$ = this.searchConfigService.getCurrentScope('').pipe( switchMap((scopeId) => this.service.getScopes(scopeId)) ); - if (!isNotEmpty(this.configuration$)) { + if (isEmpty(this.configuration$)) { this.configuration$ = this.routeService.getRouteParameterValue('configuration'); } + + this.sortOptions$ = this.configuration$.pipe( + switchMap((configuration) => this.service.getSearchConfigurationFor(null, configuration)), + getFirstSucceededRemoteDataPayload(), + map((searchConfig: SearchConfig) => { + const sortOptions = []; + searchConfig.sortOptions.forEach(sortOption => { + sortOptions.push(new SortOptions(sortOption.name, SortDirection.ASC)); + sortOptions.push(new SortOptions(sortOption.name, SortDirection.DESC)); + }); + console.log(searchConfig, sortOptions); + return sortOptions; + })); + + combineLatest([ + this.sortOptions$, + this.searchConfigService.paginatedSearchOptions + ]).pipe(take(1)) + .subscribe(([sortOptions, searchOptions]) => { + const updateValue = Object.assign(new PaginatedSearchOptions({}), searchOptions, { sort: sortOptions[0]}); + console.log(updateValue); + this.searchConfigService.paginatedSearchOptions.next(updateValue); + }); } /** From d54b7d9f7c8cbcffea2c9165e35861d4b7d2a1c1 Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Thu, 8 Apr 2021 15:43:07 +0200 Subject: [PATCH 04/27] [CST-4009] fix sort options on pagination change --- .../my-dspace-page.component.ts | 29 +++------ src/app/+search-page/search.component.ts | 29 ++------- .../search/search-configuration.service.ts | 63 +++++++++++++++++-- src/assets/i18n/en.json5 | 4 ++ 4 files changed, 77 insertions(+), 48 deletions(-) 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 ba5c0e5cc7..de51d9afd3 100644 --- a/src/app/+my-dspace-page/my-dspace-page.component.ts +++ b/src/app/+my-dspace-page/my-dspace-page.component.ts @@ -31,6 +31,8 @@ import { SearchResult } from '../shared/search/search-result.model'; import { Context } from '../core/shared/context.model'; import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model'; import { SearchConfig } from '../core/shared/search/search-filters/search-config.model'; +import {RouteService} from '../core/services/route.service'; +import {Router} from '@angular/router'; export const MYDSPACE_ROUTE = '/mydspace'; export const SEARCH_CONFIG_SERVICE: InjectionToken = new InjectionToken('searchConfigurationService'); @@ -116,7 +118,9 @@ 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, + private router: Router) { this.isXsOrSm$ = this.windowService.isXsOrSm(); this.service.setServiceOptions(MyDSpaceResponseParsingService, MyDSpaceRequest); } @@ -158,26 +162,11 @@ export class MyDSpacePageComponent implements OnInit { }) ); - this.sortOptions$ = this.context$.pipe( - switchMap((context) => this.service.getSearchConfigurationFor(null, context)), - getFirstSucceededRemoteDataPayload(), - map((searchConfig: 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; - })); + const configuration$ = this.routeService.getRouteParameterValue('configuration'); - combineLatest([ - this.sortOptions$, - this.searchConfigService.paginatedSearchOptions - ]).pipe(take(1)) - .subscribe(([sortOptions, searchOptions]) => { - const updateValue = Object.assign(new PaginatedSearchOptions({}), searchOptions, { sort: sortOptions[0]}); - this.searchConfigService.paginatedSearchOptions.next(updateValue); - }); + this.sortOptions$ = this.searchConfigService.getConfigurationSortOptionsObservable(configuration$, this.service); + + this.searchConfigService.initializeSortOptionsFromConfiguration(this.sortOptions$, this.router); } diff --git a/src/app/+search-page/search.component.ts b/src/app/+search-page/search.component.ts index 43535278a1..c5813a2fd1 100644 --- a/src/app/+search-page/search.component.ts +++ b/src/app/+search-page/search.component.ts @@ -16,10 +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 { SortDirection, SortOptions } from '../core/cache/models/sort-options.model'; -import { SearchConfig } from '../core/shared/search/search-filters/search-config.model'; +import { SortOptions } from '../core/cache/models/sort-options.model'; @Component({ selector: 'ds-search', @@ -140,28 +139,10 @@ export class SearchComponent implements OnInit { this.configuration$ = this.routeService.getRouteParameterValue('configuration'); } - this.sortOptions$ = this.configuration$.pipe( - switchMap((configuration) => this.service.getSearchConfigurationFor(null, configuration)), - getFirstSucceededRemoteDataPayload(), - map((searchConfig: SearchConfig) => { - const sortOptions = []; - searchConfig.sortOptions.forEach(sortOption => { - sortOptions.push(new SortOptions(sortOption.name, SortDirection.ASC)); - sortOptions.push(new SortOptions(sortOption.name, SortDirection.DESC)); - }); - console.log(searchConfig, sortOptions); - return sortOptions; - })); + this.sortOptions$ = this.searchConfigService.getConfigurationSortOptionsObservable(this.configuration$, this.service); + + this.searchConfigService.initializeSortOptionsFromConfiguration(this.sortOptions$, this.router); - combineLatest([ - this.sortOptions$, - this.searchConfigService.paginatedSearchOptions - ]).pipe(take(1)) - .subscribe(([sortOptions, searchOptions]) => { - const updateValue = Object.assign(new PaginatedSearchOptions({}), searchOptions, { sort: sortOptions[0]}); - console.log(updateValue); - this.searchConfigService.paginatedSearchOptions.next(updateValue); - }); } /** diff --git a/src/app/core/shared/search/search-configuration.service.ts b/src/app/core/shared/search/search-configuration.service.ts index edd3982319..c114ee6616 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 { ActivatedRoute, NavigationExtras, Params, Router } 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,12 @@ 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 { getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload } 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'; /** * Service that performs all actions that have to do with the current search configuration @@ -209,6 +219,51 @@ export class SearchConfigurationService implements OnDestroy { return this.routeService.getQueryParamsWithPrefix('f.'); } + /** + * Creates an observable of SortOptions[] every time the configuration$ stream emits. + * @param configuration$ + * @param service + */ + getConfigurationSortOptionsObservable(configuration$: Observable, service: SearchService): Observable { + return configuration$.pipe( + distinctUntilChanged(), + switchMap((configuration) => service.getSearchConfigurationFor(null, configuration)), + getFirstSucceededRemoteDataPayload(), + map((searchConfig: 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; + })); + } + + /** + * Every time sortOptions 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(sortOptions$: Observable, router: Router) { + const subscription = sortOptions$.pipe(switchMap((sortOptions) => combineLatest([ + of(sortOptions), + this.paginatedSearchOptions.pipe(take(1)) + ]))).subscribe(([sortOptions, searchOptions]) => { + const updateValue = Object.assign(new PaginatedSearchOptions({}), searchOptions, { sort: sortOptions[0]}); + const navigationExtras: NavigationExtras = { + queryParams: { + sortDirection: updateValue.sort.direction, + sortField: updateValue.sort.field, + }, + queryParamsHandling: 'merge' + }; + router.navigate([], navigationExtras); + this.paginatedSearchOptions.next(updateValue); + }); + this.subs.push(subscription); + } + /** * 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/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 20a21f93e8..bcf2a92666 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -3045,6 +3045,10 @@ "sorting.dc.date.issued.DESC": "Date Issued Descending", + "sorting.dc.date.accessioned.ASC": "Accessioned Date Ascending", + + "sorting.dc.date.accessioned.DESC": "Accessioned Date Descending", + "statistics.title": "Statistics", From 23fe338c5d68fc15e3e2f3abfb6af0ec825f1a69 Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Thu, 8 Apr 2021 16:18:31 +0200 Subject: [PATCH 05/27] [CST-4009] fix sort options on pagination change --- .../+my-dspace-page/my-dspace-page.component.ts | 17 +++++++---------- src/app/+search-page/search.component.ts | 2 +- .../search/search-configuration.service.ts | 13 +++++-------- 3 files changed, 13 insertions(+), 19 deletions(-) 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 de51d9afd3..b95a296ed1 100644 --- a/src/app/+my-dspace-page/my-dspace-page.component.ts +++ b/src/app/+my-dspace-page/my-dspace-page.component.ts @@ -7,8 +7,8 @@ import { OnInit } from '@angular/core'; -import { BehaviorSubject, combineLatest, Observable, Subject, Subscription } from 'rxjs'; -import { map, switchMap, take, tap } from 'rxjs/operators'; +import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs'; +import { map, switchMap, tap } from 'rxjs/operators'; import { PaginatedList } from '../core/data/paginated-list.model'; import { RemoteData } from '../core/data/remote-data'; @@ -19,7 +19,7 @@ import { PaginatedSearchOptions } from '../shared/search/paginated-search-option import { SearchService } from '../core/shared/search/search.service'; import { SidebarService } from '../shared/sidebar/sidebar.service'; import { hasValue } from '../shared/empty.util'; -import { getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload } from '../core/shared/operators'; +import { getFirstSucceededRemoteData } from '../core/shared/operators'; import { MyDSpaceResponseParsingService } from '../core/data/mydspace-response-parsing.service'; import { SearchConfigurationOption } from '../shared/search/search-switch-configuration/search-configuration-option.model'; import { RoleType } from '../core/roles/role-types'; @@ -29,10 +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 { SortDirection, SortOptions } from '../core/cache/models/sort-options.model'; -import { SearchConfig } from '../core/shared/search/search-filters/search-config.model'; -import {RouteService} from '../core/services/route.service'; -import {Router} from '@angular/router'; +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'); @@ -119,8 +117,7 @@ export class MyDSpacePageComponent implements OnInit { private sidebarService: SidebarService, private windowService: HostWindowService, @Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: MyDSpaceConfigurationService, - private routeService: RouteService, - private router: Router) { + private routeService: RouteService) { this.isXsOrSm$ = this.windowService.isXsOrSm(); this.service.setServiceOptions(MyDSpaceResponseParsingService, MyDSpaceRequest); } @@ -166,7 +163,7 @@ export class MyDSpacePageComponent implements OnInit { this.sortOptions$ = this.searchConfigService.getConfigurationSortOptionsObservable(configuration$, this.service); - this.searchConfigService.initializeSortOptionsFromConfiguration(this.sortOptions$, this.router); + this.searchConfigService.initializeSortOptionsFromConfiguration(this.sortOptions$); } diff --git a/src/app/+search-page/search.component.ts b/src/app/+search-page/search.component.ts index c5813a2fd1..bffa958ee5 100644 --- a/src/app/+search-page/search.component.ts +++ b/src/app/+search-page/search.component.ts @@ -141,7 +141,7 @@ export class SearchComponent implements OnInit { this.sortOptions$ = this.searchConfigService.getConfigurationSortOptionsObservable(this.configuration$, this.service); - this.searchConfigService.initializeSortOptionsFromConfiguration(this.sortOptions$, this.router); + this.searchConfigService.initializeSortOptionsFromConfiguration(this.sortOptions$); } diff --git a/src/app/core/shared/search/search-configuration.service.ts b/src/app/core/shared/search/search-configuration.service.ts index 339eefdc6b..e10ab668cc 100644 --- a/src/app/core/shared/search/search-configuration.service.ts +++ b/src/app/core/shared/search/search-configuration.service.ts @@ -1,5 +1,5 @@ import { Injectable, OnDestroy } from '@angular/core'; -import { ActivatedRoute, NavigationExtras, Params, Router } from '@angular/router'; +import { ActivatedRoute, Params } from '@angular/router'; import { BehaviorSubject, @@ -230,20 +230,17 @@ export class SearchConfigurationService implements OnDestroy { * @param configuration$ * @param service */ - initializeSortOptionsFromConfiguration(sortOptions$: Observable, router: Router) { + initializeSortOptionsFromConfiguration(sortOptions$: Observable) { const subscription = sortOptions$.pipe(switchMap((sortOptions) => combineLatest([ of(sortOptions), this.paginatedSearchOptions.pipe(take(1)) ]))).subscribe(([sortOptions, searchOptions]) => { const updateValue = Object.assign(new PaginatedSearchOptions({}), searchOptions, { sort: sortOptions[0]}); - const navigationExtras: NavigationExtras = { - queryParams: { + this.paginationService.updateRoute(this.paginationID, + { sortDirection: updateValue.sort.direction, sortField: updateValue.sort.field, - }, - queryParamsHandling: 'merge' - }; - router.navigate([], navigationExtras); + }); this.paginatedSearchOptions.next(updateValue); }); this.subs.push(subscription); From a205aa02b3316d97c1bc9650987375f827cc79b2 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 15 Apr 2021 13:01:31 +0200 Subject: [PATCH 06/27] [CST-4009] Retrieve configuration by search config service --- src/app/+my-dspace-page/my-dspace-page.component.ts | 2 +- src/app/+search-page/search.component.ts | 2 +- src/assets/i18n/en.json5 | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) 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 b95a296ed1..a5dcfe96be 100644 --- a/src/app/+my-dspace-page/my-dspace-page.component.ts +++ b/src/app/+my-dspace-page/my-dspace-page.component.ts @@ -159,7 +159,7 @@ export class MyDSpacePageComponent implements OnInit { }) ); - const configuration$ = this.routeService.getRouteParameterValue('configuration'); + const configuration$ = this.searchConfigService.getCurrentConfiguration('workspace'); this.sortOptions$ = this.searchConfigService.getConfigurationSortOptionsObservable(configuration$, this.service); diff --git a/src/app/+search-page/search.component.ts b/src/app/+search-page/search.component.ts index bffa958ee5..ce08d2c365 100644 --- a/src/app/+search-page/search.component.ts +++ b/src/app/+search-page/search.component.ts @@ -136,7 +136,7 @@ export class SearchComponent implements OnInit { switchMap((scopeId) => this.service.getScopes(scopeId)) ); if (isEmpty(this.configuration$)) { - this.configuration$ = this.routeService.getRouteParameterValue('configuration'); + this.configuration$ = this.searchConfigService.getCurrentConfiguration('default'); } this.sortOptions$ = this.searchConfigService.getConfigurationSortOptionsObservable(this.configuration$, this.service); diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 4ea4d1de42..a63d57d0d8 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -3053,6 +3053,10 @@ "sorting.dc.date.accessioned.DESC": "Accessioned Date Descending", + "sorting.lastModified.ASC": "Last modified Ascending", + + "sorting.lastModified.DESC": "Last modified Descending", + "statistics.title": "Statistics", From b6ab3d2067031dfed44e4015112bd7e46d8758c8 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 21 Apr 2021 16:06:15 +0200 Subject: [PATCH 07/27] [CST-4087] fix issue with mydspace result default order --- .../shared/search/search-configuration.service.ts | 11 +++++++---- .../search/search-filters/search-config.model.ts | 1 + 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/app/core/shared/search/search-configuration.service.ts b/src/app/core/shared/search/search-configuration.service.ts index e10ab668cc..5de30fc4a7 100644 --- a/src/app/core/shared/search/search-configuration.service.ts +++ b/src/app/core/shared/search/search-configuration.service.ts @@ -21,7 +21,7 @@ import { RouteService } from '../../services/route.service'; import { getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload } 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 { SearchConfig, SortOption } from './search-filters/search-config.model'; import { SearchService } from './search.service'; import { of } from 'rxjs/internal/observable/of'; import { PaginationService } from '../../pagination/pagination.service'; @@ -216,9 +216,12 @@ export class SearchConfigurationService implements OnDestroy { getFirstSucceededRemoteDataPayload(), map((searchConfig: SearchConfig) => { const sortOptions = []; - searchConfig.sortOptions.forEach(sortOption => { - sortOptions.push(new SortOptions(sortOption.name, SortDirection.ASC)); - sortOptions.push(new SortOptions(sortOption.name, SortDirection.DESC)); + searchConfig.sortOptions.forEach((sortOption: SortOption) => { + console.log(sortOption); + const firstOrder = (sortOption.sortOrder.toLowerCase() === SortDirection.ASC.toLowerCase()) ? SortDirection.ASC : SortDirection.DESC; + const secondOrder = (sortOption.sortOrder.toLowerCase() !== SortDirection.ASC.toLowerCase()) ? SortDirection.ASC : SortDirection.DESC; + sortOptions.push(new SortOptions(sortOption.name, firstOrder)); + sortOptions.push(new SortOptions(sortOption.name, secondOrder)); }); return sortOptions; })); 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 index dd7a799f37..725761fe7b 100644 --- a/src/app/core/shared/search/search-filters/search-config.model.ts +++ b/src/app/core/shared/search/search-filters/search-config.model.ts @@ -65,6 +65,7 @@ export interface FilterConfig { */ export interface SortOption { name: string; + sortOrder: string; } /** From 7c0d9acbf1bca7a340cf040d80b00a80322f1c1e Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Fri, 23 Apr 2021 16:46:44 +0200 Subject: [PATCH 08/27] [CST-4009] default sort option configured with default sort order --- .../my-dspace-page.component.spec.ts | 2 +- .../my-dspace-page.component.ts | 6 +-- src/app/+search-page/search.component.spec.ts | 2 +- src/app/+search-page/search.component.ts | 11 ++-- .../search/search-configuration.service.ts | 51 +++++++++++-------- 5 files changed, 41 insertions(+), 31 deletions(-) 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 909239b61c..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,7 +45,7 @@ describe('MyDSpacePageComponent', () => { pagination.id = 'mydspace-results-pagination'; pagination.currentPage = 1; pagination.pageSize = 10; - const sortOption = { name: 'score', metadata: null }; + 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', { 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 a5dcfe96be..3ded17191e 100644 --- a/src/app/+my-dspace-page/my-dspace-page.component.ts +++ b/src/app/+my-dspace-page/my-dspace-page.component.ts @@ -160,10 +160,10 @@ export class MyDSpacePageComponent implements OnInit { ); const configuration$ = this.searchConfigService.getCurrentConfiguration('workspace'); + const searchConfig$ = this.searchConfigService.getConfigurationSearchConfigObservable(configuration$, this.service); - this.sortOptions$ = this.searchConfigService.getConfigurationSortOptionsObservable(configuration$, this.service); - - this.searchConfigService.initializeSortOptionsFromConfiguration(this.sortOptions$); + this.sortOptions$ = this.searchConfigService.getConfigurationSortOptionsObservable(searchConfig$); + this.searchConfigService.initializeSortOptionsFromConfiguration(searchConfig$); } diff --git a/src/app/+search-page/search.component.spec.ts b/src/app/+search-page/search.component.spec.ts index 06061c1d40..bcbf3f0f77 100644 --- a/src/app/+search-page/search.component.spec.ts +++ b/src/app/+search-page/search.component.spec.ts @@ -40,7 +40,7 @@ const pagination: PaginationComponentOptions = new PaginationComponentOptions(); pagination.id = 'search-results-pagination'; pagination.currentPage = 1; pagination.pageSize = 10; -const sortOption = { name: 'score', metadata: null }; +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', { diff --git a/src/app/+search-page/search.component.ts b/src/app/+search-page/search.component.ts index ce08d2c365..b817c82a57 100644 --- a/src/app/+search-page/search.component.ts +++ b/src/app/+search-page/search.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core'; -import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs'; -import { map, startWith, switchMap, take, } from 'rxjs/operators'; +import { BehaviorSubject, Observable, Subscription } from 'rxjs'; +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'; @@ -8,7 +8,7 @@ import { pushInOut } from '../shared/animations/push'; import { HostWindowService } from '../shared/host-window.service'; import { SidebarService } from '../shared/sidebar/sidebar.service'; import { hasValue, isEmpty } from '../shared/empty.util'; -import { getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload } from '../core/shared/operators'; +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'; import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model'; @@ -139,9 +139,10 @@ export class SearchComponent implements OnInit { this.configuration$ = this.searchConfigService.getCurrentConfiguration('default'); } - this.sortOptions$ = this.searchConfigService.getConfigurationSortOptionsObservable(this.configuration$, this.service); + const searchConfig$ = this.searchConfigService.getConfigurationSearchConfigObservable(this.configuration$, this.service); - this.searchConfigService.initializeSortOptionsFromConfiguration(this.sortOptions$); + this.sortOptions$ = this.searchConfigService.getConfigurationSortOptionsObservable(searchConfig$); + this.searchConfigService.initializeSortOptionsFromConfiguration(searchConfig$); } diff --git a/src/app/core/shared/search/search-configuration.service.ts b/src/app/core/shared/search/search-configuration.service.ts index 5de30fc4a7..c209d79e40 100644 --- a/src/app/core/shared/search/search-configuration.service.ts +++ b/src/app/core/shared/search/search-configuration.service.ts @@ -21,7 +21,7 @@ import { RouteService } from '../../services/route.service'; import { getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload } from '../operators'; import { hasNoValue, hasValue, isNotEmpty, isNotEmptyOperator } from '../../../shared/empty.util'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; -import { SearchConfig, SortOption } from './search-filters/search-config.model'; +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'; @@ -205,40 +205,33 @@ export class SearchConfigurationService implements OnDestroy { } /** - * Creates an observable of SortOptions[] every time the configuration$ stream emits. + * Creates an observable of SearchConfig every time the configuration$ stream emits. * @param configuration$ * @param service */ - getConfigurationSortOptionsObservable(configuration$: Observable, service: SearchService): Observable { + getConfigurationSearchConfigObservable(configuration$: Observable, service: SearchService): Observable { return configuration$.pipe( distinctUntilChanged(), switchMap((configuration) => service.getSearchConfigurationFor(null, configuration)), - getFirstSucceededRemoteDataPayload(), - map((searchConfig: SearchConfig) => { - const sortOptions = []; - searchConfig.sortOptions.forEach((sortOption: SortOption) => { - console.log(sortOption); - const firstOrder = (sortOption.sortOrder.toLowerCase() === SortDirection.ASC.toLowerCase()) ? SortDirection.ASC : SortDirection.DESC; - const secondOrder = (sortOption.sortOrder.toLowerCase() !== SortDirection.ASC.toLowerCase()) ? SortDirection.ASC : SortDirection.DESC; - sortOptions.push(new SortOptions(sortOption.name, firstOrder)); - sortOptions.push(new SortOptions(sortOption.name, secondOrder)); - }); - return sortOptions; - })); + getFirstSucceededRemoteDataPayload()); } /** - * Every time sortOptions change (after a configuration change) it update the navigation with the default sort option + * 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(sortOptions$: Observable) { - const subscription = sortOptions$.pipe(switchMap((sortOptions) => combineLatest([ - of(sortOptions), + initializeSortOptionsFromConfiguration(searchConfig$: Observable) { + const subscription = searchConfig$.pipe(switchMap((searchConfig) => combineLatest([ + of(searchConfig), this.paginatedSearchOptions.pipe(take(1)) - ]))).subscribe(([sortOptions, searchOptions]) => { - const updateValue = Object.assign(new PaginatedSearchOptions({}), searchOptions, { sort: sortOptions[0]}); + ]))).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, @@ -249,6 +242,22 @@ export class SearchConfigurationService implements OnDestroy { 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 From 8433f49ed92c9c5ad8faf101c1ea649b348a6b82 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Mon, 26 Apr 2021 09:09:51 +0200 Subject: [PATCH 09/27] 78991: Initialize slider handle width --- src/styles/_custom_variables.scss | 1 + 1 file changed, 1 insertion(+) 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; } From eb9a7a15d6d008bf63b944211a447c7e4be50b99 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Mon, 26 Apr 2021 09:55:28 +0200 Subject: [PATCH 10/27] 78991: Specify filter operator for (date) ranges --- .../+my-dspace-page/my-dspace-configuration.service.spec.ts | 5 ++++- .../core/shared/search/search-configuration.service.spec.ts | 5 ++++- src/app/core/shared/search/search-configuration.service.ts | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) 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/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..7983bec64d 100644 --- a/src/app/core/shared/search/search-configuration.service.ts +++ b/src/app/core/shared/search/search-configuration.service.ts @@ -168,7 +168,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])); From 4fa6a3e97651a4e57dad2fb93c2f53db97e323ad Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Mon, 26 Apr 2021 10:53:37 +0200 Subject: [PATCH 11/27] 78991: Set fallback max date to the current year --- .../search-range-filter/search-range-filter.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 6c2a3431c1a8af0bbb5541d4edf1bd7d12d2ea26 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Mon, 26 Apr 2021 11:41:43 +0200 Subject: [PATCH 12/27] 78991: Fix range handle lines & remove duplicate CSS variable --- .../search-range-filter/search-range-filter.component.scss | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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; From 4634d2a4a81afe311841199a4e5a08554c7d7c56 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Mon, 26 Apr 2021 11:46:16 +0200 Subject: [PATCH 13/27] 78991: Don't initialize SearchFilterComponent with closed=true --- .../search-filters/search-filter/search-filter.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 5f45e93d12d3efa42322dfc268424a660c122e17 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Mon, 26 Apr 2021 13:24:26 +0200 Subject: [PATCH 14/27] 78994: Remove setItem method --- ...dynamic-lookup-relation-modal.component.ts | 44 ++----------------- 1 file changed, 4 insertions(+), 40 deletions(-) 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()); } } From d62d9b0f485212ca6a31f0ee5b9d7afa63d024f0 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Mon, 26 Apr 2021 15:13:27 +0200 Subject: [PATCH 15/27] 78994: Update unit tests --- .../dynamic-lookup-relation-modal.component.spec.ts | 12 ------------ 1 file changed, 12 deletions(-) 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 From aca1c86455292b1ecddb02d1688dae8d02bf5c95 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Mon, 26 Apr 2021 15:02:50 +0200 Subject: [PATCH 16/27] 78994: Provide Item's owning collection to relation modal --- .../edit-relationship-list.component.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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; From bdc2dd5f9ca98ddb0e89371e02201cdb5ef1d94a Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Tue, 27 Apr 2021 09:53:38 +0200 Subject: [PATCH 17/27] [CST-4009] fixed search configuration stream --- src/app/core/shared/search/search-configuration.service.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/app/core/shared/search/search-configuration.service.ts b/src/app/core/shared/search/search-configuration.service.ts index c209d79e40..6a1373c87e 100644 --- a/src/app/core/shared/search/search-configuration.service.ts +++ b/src/app/core/shared/search/search-configuration.service.ts @@ -18,7 +18,10 @@ 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, getFirstSucceededRemoteDataPayload } 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'; @@ -213,7 +216,7 @@ export class SearchConfigurationService implements OnDestroy { return configuration$.pipe( distinctUntilChanged(), switchMap((configuration) => service.getSearchConfigurationFor(null, configuration)), - getFirstSucceededRemoteDataPayload()); + getAllSucceededRemoteDataPayload()); } /** From ad7824460b713bc3fd85d725cac8069838f1ae4a Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Wed, 28 Apr 2021 15:28:02 +0200 Subject: [PATCH 18/27] [CST-4009] update en.json5 --- src/assets/i18n/en.json5 | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index a63d57d0d8..23c889847f 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -3008,6 +3008,8 @@ "search.results.empty": "Your search returned no results.", + "default.search.results.head": "Search Results", + "search.sidebar.close": "Back to results", @@ -3041,9 +3043,9 @@ "sorting.dc.title.DESC": "Title Descending", - "sorting.score.ASC": "Relevance Ascending", + "sorting.score.ASC": "Least Relevant", - "sorting.score.DESC": "Relevance Descending", + "sorting.score.DESC": "Most Relevant", "sorting.dc.date.issued.ASC": "Date Issued Ascending", From b5342e0fab6220c6dc5ff722c86a91911a12f10f Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Mon, 3 May 2021 18:41:56 +0200 Subject: [PATCH 19/27] 79218: Remove duplicate menu item --- .../admin-sidebar/admin-sidebar.component.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts index a9c0a6648d..dbe8ac1042 100644 --- a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts +++ b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts @@ -243,20 +243,6 @@ 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', From fe4fe9e8d34fff27a9def8ad483f6170f09176d8 Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Mon, 3 May 2021 19:07:02 +0200 Subject: [PATCH 20/27] 79218: Comment out all disabled menu options --- .../admin-sidebar/admin-sidebar.component.ts | 194 +++++++++--------- 1 file changed, 101 insertions(+), 93 deletions(-) diff --git a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts index dbe8ac1042..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 */ { @@ -244,32 +245,34 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { }, /* 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 */ { @@ -310,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)); @@ -392,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 @@ -549,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, From 340f9518cda741c2590bcd64a27cef1b1d95b33d Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Tue, 4 May 2021 10:31:55 +0200 Subject: [PATCH 21/27] 78991: Fix SearchOptions typing --- src/app/shared/search/search-options.model.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/app/shared/search/search-options.model.ts b/src/app/shared/search/search-options.model.ts index fb4e8d4caf..053b9225f7 100644 --- a/src/app/shared/search/search-options.model.ts +++ b/src/app/shared/search/search-options.model.ts @@ -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; From e682997195eb2ef759f964a3d6d464431ffa9331 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Tue, 4 May 2021 10:34:05 +0200 Subject: [PATCH 22/27] 78991: URI-encode SearchOptions query values --- .../paginated-search-options.model.spec.ts | 13 +++++++---- .../search/search-options.model.spec.ts | 16 +++++++++---- src/app/shared/search/search-options.model.ts | 23 ++++++++++++++----- 3 files changed, 37 insertions(+), 15 deletions(-) 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..2b8fe7aeb5 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,11 @@ 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']), + new SearchFilter('f.range', ['[2002 TO 2021]'], 'equals'), + ]; const query = 'search query'; const scope = '0fde1ecb-82cc-425a-b600-ac3576d76b47'; const baseUrl = 'www.rest.com'; @@ -31,12 +35,13 @@ describe('PaginatedSearchOptions', () => { 'sort=test.field,DESC&' + 'page=0&' + 'size=40&' + - 'query=search query&' + + '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.spec.ts b/src/app/shared/search/search-options.model.spec.ts index 62fe732218..d20ebac6bf 100644 --- a/src/app/shared/search/search-options.model.spec.ts +++ b/src/app/shared/search/search-options.model.spec.ts @@ -4,8 +4,13 @@ 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']), + new SearchFilter('f.range', ['[2002 TO 2021]'], 'equals'), + ]; const query = 'search query'; const scope = '0fde1ecb-82cc-425a-b600-ac3576d76b47'; const baseUrl = 'www.rest.com'; @@ -18,12 +23,13 @@ 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&' + + '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 053b9225f7..fad4002caf 100644 --- a/src/app/shared/search/search-options.model.ts +++ b/src/app/shared/search/search-options.model.ts @@ -38,27 +38,38 @@ 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); + let fixedFilter: string; + const match = this.fixedFilter.match(/^([^=]+=)(.+)$/); + + if (match) { + fixedFilter = match[1] + encodeURIComponent(match[2]); + } else { + fixedFilter = encodeURIComponent(this.fixedFilter); + } + + args.push(fixedFilter); } 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}`); + + // we don't want commas to get URI-encoded + args.push(`${filter.key}=${encodeURIComponent(filterValue).replace(/%2C/g, ',')}`); }); }); } From b9a8bfb2bd3fcd51065fa7c821ab546ae604df5b Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Tue, 4 May 2021 10:44:06 +0200 Subject: [PATCH 23/27] 78991: Also test SearchOptions.fixedFilter --- .../search/paginated-search-options.model.spec.ts | 5 ++++- src/app/shared/search/search-options.model.spec.ts | 10 +++++++++- src/app/shared/search/search-options.model.ts | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) 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 2b8fe7aeb5..0523e2b139 100644 --- a/src/app/shared/search/paginated-search-options.model.spec.ts +++ b/src/app/shared/search/paginated-search-options.model.spec.ts @@ -13,6 +13,7 @@ describe('PaginatedSearchOptions', () => { new SearchFilter('f.example', ['another value', 'second value']), new SearchFilter('f.range', ['[2002 TO 2021]'], 'equals'), ]; + const fixedFilter = 'f.fixed=1234 5678,equals'; const query = 'search query'; const scope = '0fde1ecb-82cc-425a-b600-ac3576d76b47'; const baseUrl = 'www.rest.com'; @@ -23,7 +24,8 @@ describe('PaginatedSearchOptions', () => { filters: filters, query: query, scope: scope, - dsoTypes: [DSpaceObjectType.ITEM] + dsoTypes: [DSpaceObjectType.ITEM], + fixedFilter: fixedFilter, }); }); @@ -35,6 +37,7 @@ describe('PaginatedSearchOptions', () => { 'sort=test.field,DESC&' + 'page=0&' + 'size=40&' + + 'f.fixed=1234%205678,equals&' + 'query=search%20query&' + 'scope=0fde1ecb-82cc-425a-b600-ac3576d76b47&' + 'dsoType=ITEM&' + diff --git a/src/app/shared/search/search-options.model.spec.ts b/src/app/shared/search/search-options.model.spec.ts index d20ebac6bf..7fb73daa40 100644 --- a/src/app/shared/search/search-options.model.spec.ts +++ b/src/app/shared/search/search-options.model.spec.ts @@ -11,11 +11,18 @@ describe('SearchOptions', () => { new SearchFilter('f.example', ['another value', 'second value']), new SearchFilter('f.range', ['[2002 TO 2021]'], 'equals'), ]; + const fixedFilter = 'f.fixed=1234 5678,equals'; 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', () => { @@ -23,6 +30,7 @@ describe('SearchOptions', () => { it('should generate a string with all parameters that are present', () => { const outcome = options.toRestUrl(baseUrl); expect(outcome).toEqual('www.rest.com?' + + 'f.fixed=1234%205678,equals&' + 'query=search%20query&' + 'scope=0fde1ecb-82cc-425a-b600-ac3576d76b47&' + 'dsoType=ITEM&' + diff --git a/src/app/shared/search/search-options.model.ts b/src/app/shared/search/search-options.model.ts index fad4002caf..4f56e48534 100644 --- a/src/app/shared/search/search-options.model.ts +++ b/src/app/shared/search/search-options.model.ts @@ -45,7 +45,7 @@ export class SearchOptions { const match = this.fixedFilter.match(/^([^=]+=)(.+)$/); if (match) { - fixedFilter = match[1] + encodeURIComponent(match[2]); + fixedFilter = match[1] + encodeURIComponent(match[2]).replace(/%2C/g, ','); } else { fixedFilter = encodeURIComponent(this.fixedFilter); } From 15ad31bd8474d84a51defe6339bca8cb356be1ef Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Tue, 4 May 2021 11:04:04 +0200 Subject: [PATCH 24/27] 78991: Clean up SearchOptions.toRestUrl * Extract "selective encoding" ops into separate methods * Add comments to clarify regex * Use hasValue() instead of just checking on 'match' * Leave only the last comma of filter values unencoded --- .../paginated-search-options.model.spec.ts | 8 ++-- .../search/search-options.model.spec.ts | 8 ++-- src/app/shared/search/search-options.model.ts | 41 ++++++++++++------- 3 files changed, 35 insertions(+), 22 deletions(-) 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 0523e2b139..9881cc1149 100644 --- a/src/app/shared/search/paginated-search-options.model.spec.ts +++ b/src/app/shared/search/paginated-search-options.model.spec.ts @@ -10,10 +10,10 @@ describe('PaginatedSearchOptions', () => { 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']), - new SearchFilter('f.range', ['[2002 TO 2021]'], 'equals'), + 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'; + 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'; @@ -37,7 +37,7 @@ describe('PaginatedSearchOptions', () => { 'sort=test.field,DESC&' + 'page=0&' + 'size=40&' + - 'f.fixed=1234%205678,equals&' + + 'f.fixed=1234%2C5678,equals&' + 'query=search%20query&' + 'scope=0fde1ecb-82cc-425a-b600-ac3576d76b47&' + 'dsoType=ITEM&' + diff --git a/src/app/shared/search/search-options.model.spec.ts b/src/app/shared/search/search-options.model.spec.ts index 7fb73daa40..8bed046736 100644 --- a/src/app/shared/search/search-options.model.spec.ts +++ b/src/app/shared/search/search-options.model.spec.ts @@ -8,10 +8,10 @@ describe('SearchOptions', () => { const filters = [ new SearchFilter('f.test', ['value']), - new SearchFilter('f.example', ['another value', 'second value']), - new SearchFilter('f.range', ['[2002 TO 2021]'], 'equals'), + 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'; + 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'; @@ -30,7 +30,7 @@ describe('SearchOptions', () => { it('should generate a string with all parameters that are present', () => { const outcome = options.toRestUrl(baseUrl); expect(outcome).toEqual('www.rest.com?' + - 'f.fixed=1234%205678,equals&' + + 'f.fixed=1234%2C5678,equals&' + 'query=search%20query&' + 'scope=0fde1ecb-82cc-425a-b600-ac3576d76b47&' + 'dsoType=ITEM&' + diff --git a/src/app/shared/search/search-options.model.ts b/src/app/shared/search/search-options.model.ts index 4f56e48534..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'; @@ -41,16 +41,7 @@ export class SearchOptions { args.push(`configuration=${encodeURIComponent(this.configuration)}`); } if (isNotEmpty(this.fixedFilter)) { - let fixedFilter: string; - const match = this.fixedFilter.match(/^([^=]+=)(.+)$/); - - if (match) { - fixedFilter = match[1] + encodeURIComponent(match[2]).replace(/%2C/g, ','); - } else { - fixedFilter = encodeURIComponent(this.fixedFilter); - } - - args.push(fixedFilter); + args.push(this.encodedFixedFilter); } if (isNotEmpty(this.query)) { args.push(`query=${encodeURIComponent(this.query)}`); @@ -67,9 +58,7 @@ export class SearchOptions { this.filters.forEach((filter: SearchFilter) => { filter.values.forEach((value) => { const filterValue = value.includes(',') ? `${value}` : value + (filter.operator ? ',' + filter.operator : ''); - - // we don't want commas to get URI-encoded - args.push(`${filter.key}=${encodeURIComponent(filterValue).replace(/%2C/g, ',')}`); + args.push(`${filter.key}=${this.encodeFilterQueryValue(filterValue)}`); }); }); } @@ -78,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); + } + } } From 042afd9bb8ad8603b3bc258b12709222cec20d08 Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Tue, 4 May 2021 11:46:04 +0200 Subject: [PATCH 25/27] 79220: Fix edit group navigation bug --- .../group-registry/group-form/group-form.component.spec.ts | 7 +++++++ .../group-registry/group-form/group-form.component.ts | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) 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()); } From 86bf7ff4f233bf139757c8d6e1446bc6a75b6914 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 6 May 2021 18:03:46 +0200 Subject: [PATCH 26/27] 79252: Fix dso-selector issues --- ...ized-collection-selector.component.spec.ts | 13 ++-- ...uthorized-collection-selector.component.ts | 20 ++++-- .../dso-selector/dso-selector.component.html | 30 +++++---- .../dso-selector.component.spec.ts | 19 +++++- .../dso-selector/dso-selector.component.ts | 67 ++++++++++++------- src/assets/i18n/en.json5 | 2 + 6 files changed, 100 insertions(+), 51 deletions(-) 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..2a1d875370 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, getFirstSucceededRemoteDataPayload } 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/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 008b69a23e..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", From 43b8f45eee67898706cc7f7695f3018021c0c0d9 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 12 May 2021 16:50:18 +0200 Subject: [PATCH 27/27] 79252: Remove unused import --- .../authorized-collection-selector.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2a1d875370..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,7 +3,7 @@ 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 { getFirstCompletedRemoteData, 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';