From 9b8ada0326049ad2e17d6684a304b2a99202a931 Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Tue, 6 Apr 2021 10:06:15 +0200 Subject: [PATCH 001/351] [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 002/351] [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 003/351] [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 9c85328b17889fa4ce33de334015ff0a84929e54 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 7 Apr 2021 15:45:22 +0200 Subject: [PATCH 004/351] 77998: xdescribe fixes --- .../browse-by-decorator.ts | 7 ++ .../browse-by-switcher.component.spec.ts | 11 +-- .../browse-by-switcher.component.ts | 10 +- .../shared/item-relationships-utils.ts | 6 ++ .../core/cache/builders/build-decorators.ts | 14 +++ .../core/cache/builders/link.service.spec.ts | 91 ++++++++----------- src/app/core/cache/builders/link.service.ts | 18 ++-- .../core/data/relationship.service.spec.ts | 12 +-- src/app/core/data/relationship.service.ts | 10 +- .../cookies/browser-klaro.service.spec.ts | 5 +- ...ta-representation-loader.component.spec.ts | 31 +++++-- ...etadata-representation-loader.component.ts | 14 ++- .../metadata-representation.decorator.ts | 7 ++ 13 files changed, 136 insertions(+), 100 deletions(-) diff --git a/src/app/+browse-by/+browse-by-switcher/browse-by-decorator.ts b/src/app/+browse-by/+browse-by-switcher/browse-by-decorator.ts index 0143377922..efb4a4a9f4 100644 --- a/src/app/+browse-by/+browse-by-switcher/browse-by-decorator.ts +++ b/src/app/+browse-by/+browse-by-switcher/browse-by-decorator.ts @@ -1,4 +1,6 @@ import { hasNoValue } from '../../shared/empty.util'; +import { InjectionToken } from '@angular/core'; +import { GenericConstructor } from '../../core/shared/generic-constructor'; export enum BrowseByType { Title = 'title', @@ -8,6 +10,11 @@ export enum BrowseByType { export const DEFAULT_BROWSE_BY_TYPE = BrowseByType.Metadata; +export const BROWSE_BY_COMPONENT_FACTORY = new InjectionToken<(browseByType) => GenericConstructor>('getComponentByBrowseByType', { + providedIn: 'root', + factory: () => getComponentByBrowseByType +}); + const map = new Map(); /** diff --git a/src/app/+browse-by/+browse-by-switcher/browse-by-switcher.component.spec.ts b/src/app/+browse-by/+browse-by-switcher/browse-by-switcher.component.spec.ts index 545b766c88..f340237e26 100644 --- a/src/app/+browse-by/+browse-by-switcher/browse-by-switcher.component.spec.ts +++ b/src/app/+browse-by/+browse-by-switcher/browse-by-switcher.component.spec.ts @@ -2,12 +2,11 @@ import { BrowseBySwitcherComponent } from './browse-by-switcher.component'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import * as decorator from './browse-by-decorator'; import { BehaviorSubject } from 'rxjs'; import { environment } from '../../../environments/environment'; -import createSpy = jasmine.createSpy; +import { BROWSE_BY_COMPONENT_FACTORY } from './browse-by-decorator'; -xdescribe('BrowseBySwitcherComponent', () => { +describe('BrowseBySwitcherComponent', () => { let comp: BrowseBySwitcherComponent; let fixture: ComponentFixture; @@ -23,7 +22,8 @@ xdescribe('BrowseBySwitcherComponent', () => { TestBed.configureTestingModule({ declarations: [BrowseBySwitcherComponent], providers: [ - { provide: ActivatedRoute, useValue: activatedRouteStub } + { provide: ActivatedRoute, useValue: activatedRouteStub }, + { provide: BROWSE_BY_COMPONENT_FACTORY, useValue: jasmine.createSpy('getComponentByBrowseByType').and.returnValue(null) } ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); @@ -32,7 +32,6 @@ xdescribe('BrowseBySwitcherComponent', () => { beforeEach(waitForAsync(() => { fixture = TestBed.createComponent(BrowseBySwitcherComponent); comp = fixture.componentInstance; - spyOnProperty(decorator, 'getComponentByBrowseByType').and.returnValue(createSpy('getComponentByItemType')); })); types.forEach((type) => { @@ -43,7 +42,7 @@ xdescribe('BrowseBySwitcherComponent', () => { }); it(`should call getComponentByBrowseByType with type "${type.type}"`, () => { - expect(decorator.getComponentByBrowseByType).toHaveBeenCalledWith(type.type); + expect((comp as any).getComponentByBrowseByType).toHaveBeenCalledWith(type.type); }); }); }); diff --git a/src/app/+browse-by/+browse-by-switcher/browse-by-switcher.component.ts b/src/app/+browse-by/+browse-by-switcher/browse-by-switcher.component.ts index 6cfeebf796..043a4ce90a 100644 --- a/src/app/+browse-by/+browse-by-switcher/browse-by-switcher.component.ts +++ b/src/app/+browse-by/+browse-by-switcher/browse-by-switcher.component.ts @@ -1,10 +1,11 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, Inject, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; import { BrowseByTypeConfig } from '../../../config/browse-by-type-config.interface'; import { map } from 'rxjs/operators'; -import { getComponentByBrowseByType } from './browse-by-decorator'; +import { BROWSE_BY_COMPONENT_FACTORY } from './browse-by-decorator'; import { environment } from '../../../environments/environment'; +import { GenericConstructor } from '../../core/shared/generic-constructor'; @Component({ selector: 'ds-browse-by-switcher', @@ -20,7 +21,8 @@ export class BrowseBySwitcherComponent implements OnInit { */ browseByComponent: Observable; - public constructor(protected route: ActivatedRoute) { + public constructor(protected route: ActivatedRoute, + @Inject(BROWSE_BY_COMPONENT_FACTORY) private getComponentByBrowseByType: (browseByType) => GenericConstructor) { } /** @@ -32,7 +34,7 @@ export class BrowseBySwitcherComponent implements OnInit { const id = params.id; return environment.browseBy.types.find((config: BrowseByTypeConfig) => config.id === id); }), - map((config: BrowseByTypeConfig) => getComponentByBrowseByType(config.type)) + map((config: BrowseByTypeConfig) => this.getComponentByBrowseByType(config.type)) ); } diff --git a/src/app/+item-page/simple/item-types/shared/item-relationships-utils.ts b/src/app/+item-page/simple/item-types/shared/item-relationships-utils.ts index a8e2136131..b4c3da2cdc 100644 --- a/src/app/+item-page/simple/item-types/shared/item-relationships-utils.ts +++ b/src/app/+item-page/simple/item-types/shared/item-relationships-utils.ts @@ -9,6 +9,12 @@ import { getFirstSucceededRemoteData } from '../../../../core/shared/operators'; import { hasValue } from '../../../../shared/empty.util'; +import { InjectionToken } from '@angular/core'; + +export const PAGINATED_RELATIONS_TO_ITEMS_OPERATOR = new InjectionToken<(thisId: string) => (source: Observable>>) => Observable>>>('paginatedRelationsToItems', { + providedIn: 'root', + factory: () => paginatedRelationsToItems +}); /** * Operator for comparing arrays using a mapping function diff --git a/src/app/core/cache/builders/build-decorators.ts b/src/app/core/cache/builders/build-decorators.ts index 1ea09b6684..b561ababde 100644 --- a/src/app/core/cache/builders/build-decorators.ts +++ b/src/app/core/cache/builders/build-decorators.ts @@ -8,6 +8,20 @@ import { TypedObject, getResourceTypeValueFor } from '../object-cache.reducer'; +import { InjectionToken } from '@angular/core'; + +export const DATA_SERVICE_FACTORY = new InjectionToken<(resourceType: ResourceType) => GenericConstructor>('getDataServiceFor', { + providedIn: 'root', + factory: () => getDataServiceFor +}); +export const LINK_DEFINITION_FACTORY = new InjectionToken<(source: GenericConstructor, linkName: keyof T['_links']) => LinkDefinition>('getLinkDefinition', { + providedIn: 'root', + factory: () => getLinkDefinition +}); +export const LINK_DEFINITION_MAP_FACTORY = new InjectionToken<(source: GenericConstructor) => Map>>('getLinkDefinitions', { + providedIn: 'root', + factory: () => getLinkDefinitions +}); const resolvedLinkKey = Symbol('resolvedLink'); diff --git a/src/app/core/cache/builders/link.service.spec.ts b/src/app/core/cache/builders/link.service.spec.ts index 1c41cfee86..a6d9c59492 100644 --- a/src/app/core/cache/builders/link.service.spec.ts +++ b/src/app/core/cache/builders/link.service.spec.ts @@ -5,15 +5,9 @@ import { FindListOptions } from '../../data/request.models'; import { HALLink } from '../../shared/hal-link.model'; import { HALResource } from '../../shared/hal-resource.model'; import { ResourceType } from '../../shared/resource-type'; -import * as decorators from './build-decorators'; import { LinkService } from './link.service'; - -const spyOnFunction = (obj: T, func: keyof T) => { - const spy = jasmine.createSpy(func as string); - spyOnProperty(obj, func, 'get').and.returnValue(spy); - - return spy; -}; +import { DATA_SERVICE_FACTORY, LINK_DEFINITION_FACTORY, LINK_DEFINITION_MAP_FACTORY } from './build-decorators'; +import { isEmpty } from 'rxjs/operators'; const TEST_MODEL = new ResourceType('testmodel'); let result: any; @@ -51,7 +45,7 @@ let testDataService: TestDataService; let testModel: TestModel; -xdescribe('LinkService', () => { +describe('LinkService', () => { let service: LinkService; beforeEach(() => { @@ -76,6 +70,30 @@ xdescribe('LinkService', () => { providers: [LinkService, { provide: TestDataService, useValue: testDataService + }, { + provide: DATA_SERVICE_FACTORY, + useValue: jasmine.createSpy('getDataServiceFor').and.returnValue(TestDataService), + }, { + provide: LINK_DEFINITION_FACTORY, + useValue: jasmine.createSpy('getLinkDefinition').and.returnValue({ + resourceType: TEST_MODEL, + linkName: 'predecessor', + propertyName: 'predecessor' + }), + }, { + provide: LINK_DEFINITION_MAP_FACTORY, + useValue: jasmine.createSpy('getLinkDefinitions').and.returnValue([ + { + resourceType: TEST_MODEL, + linkName: 'predecessor', + propertyName: 'predecessor', + }, + { + resourceType: TEST_MODEL, + linkName: 'successor', + propertyName: 'successor', + } + ]), }] }); service = TestBed.inject(LinkService); @@ -84,12 +102,6 @@ xdescribe('LinkService', () => { describe('resolveLink', () => { describe(`when the linkdefinition concerns a single object`, () => { beforeEach(() => { - spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({ - resourceType: TEST_MODEL, - linkName: 'predecessor', - propertyName: 'predecessor' - }); - spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService); service.resolveLink(testModel, followLink('predecessor', {}, true, true, true, followLink('successor'))); }); it('should call dataservice.findByHref with the correct href and nested links', () => { @@ -98,13 +110,12 @@ xdescribe('LinkService', () => { }); describe(`when the linkdefinition concerns a list`, () => { beforeEach(() => { - spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({ + ((service as any).getLinkDefinition as jasmine.Spy).and.returnValue({ resourceType: TEST_MODEL, linkName: 'predecessor', propertyName: 'predecessor', isList: true }); - spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService); service.resolveLink(testModel, followLink('predecessor', { some: 'options ' } as any, true, true, true, followLink('successor'))); }); it('should call dataservice.findAllByHref with the correct href, findListOptions, and nested links', () => { @@ -113,21 +124,15 @@ xdescribe('LinkService', () => { }); describe('either way', () => { beforeEach(() => { - spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({ - resourceType: TEST_MODEL, - linkName: 'predecessor', - propertyName: 'predecessor' - }); - spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService); result = service.resolveLink(testModel, followLink('predecessor', {}, true, true, true, followLink('successor'))); }); it('should call getLinkDefinition with the correct model and link', () => { - expect(decorators.getLinkDefinition).toHaveBeenCalledWith(testModel.constructor as any, 'predecessor'); + expect((service as any).getLinkDefinition).toHaveBeenCalledWith(testModel.constructor as any, 'predecessor'); }); it('should call getDataServiceFor with the correct resource type', () => { - expect(decorators.getDataServiceFor).toHaveBeenCalledWith(TEST_MODEL); + expect((service as any).getDataServiceFor).toHaveBeenCalledWith(TEST_MODEL); }); it('should return the model with the resolved link', () => { @@ -140,7 +145,7 @@ xdescribe('LinkService', () => { describe(`when the specified link doesn't exist on the model's class`, () => { beforeEach(() => { - spyOnFunction(decorators, 'getLinkDefinition').and.returnValue(undefined); + ((service as any).getLinkDefinition as jasmine.Spy).and.returnValue(undefined); }); it('should throw an error', () => { expect(() => { @@ -151,12 +156,7 @@ xdescribe('LinkService', () => { describe(`when there is no dataservice for the resourcetype in the link`, () => { beforeEach(() => { - spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({ - resourceType: TEST_MODEL, - linkName: 'predecessor', - propertyName: 'predecessor' - }); - spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(undefined); + ((service as any).getDataServiceFor as jasmine.Spy).and.returnValue(undefined); }); it('should throw an error', () => { expect(() => { @@ -188,18 +188,6 @@ xdescribe('LinkService', () => { beforeEach(() => { testModel.predecessor = 'predecessor value' as any; testModel.successor = 'successor value' as any; - spyOnFunction(decorators, 'getLinkDefinitions').and.returnValue([ - { - resourceType: TEST_MODEL, - linkName: 'predecessor', - propertyName: 'predecessor', - }, - { - resourceType: TEST_MODEL, - linkName: 'successor', - propertyName: 'successor', - } - ]); }); it('should return a new version of the object without any resolved links', () => { @@ -231,16 +219,10 @@ xdescribe('LinkService', () => { } } }); - spyOnFunction(decorators, 'getDataServiceFor').and.returnValue(TestDataService); }); describe('resolving the available link', () => { beforeEach(() => { - spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({ - resourceType: TEST_MODEL, - linkName: 'predecessor', - propertyName: 'predecessor' - }); result = service.resolveLinks(testModel, followLink('predecessor')); }); @@ -251,7 +233,7 @@ xdescribe('LinkService', () => { describe('resolving the missing link', () => { beforeEach(() => { - spyOnFunction(decorators, 'getLinkDefinition').and.returnValue({ + ((service as any).getLinkDefinition as jasmine.Spy).and.returnValue({ resourceType: TEST_MODEL, linkName: 'successor', propertyName: 'successor' @@ -259,8 +241,11 @@ xdescribe('LinkService', () => { result = service.resolveLinks(testModel, followLink('successor')); }); - it('should return the model with no resolved link', () => { - expect(result.successor).toBeUndefined(); + it('should resolve to an empty observable', (done) => { + result.successor.pipe(isEmpty()).subscribe((v) => { + expect(v).toEqual(true); + done(); + }); }); }); }); diff --git a/src/app/core/cache/builders/link.service.ts b/src/app/core/cache/builders/link.service.ts index 56a1154b77..29f8633da5 100644 --- a/src/app/core/cache/builders/link.service.ts +++ b/src/app/core/cache/builders/link.service.ts @@ -1,17 +1,18 @@ -import { Injectable, Injector } from '@angular/core'; +import { Inject, Injectable, Injector } from '@angular/core'; import { hasNoValue, hasValue, isNotEmpty } from '../../../shared/empty.util'; import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; import { GenericConstructor } from '../../shared/generic-constructor'; import { HALResource } from '../../shared/hal-resource.model'; import { - getDataServiceFor, - getLinkDefinition, - getLinkDefinitions, + DATA_SERVICE_FACTORY, + LINK_DEFINITION_FACTORY, + LINK_DEFINITION_MAP_FACTORY, LinkDefinition } from './build-decorators'; import { RemoteData } from '../../data/remote-data'; import { Observable } from 'rxjs/internal/Observable'; import { EMPTY } from 'rxjs'; +import { ResourceType } from '../../shared/resource-type'; /** * A Service to handle the resolving and removing @@ -24,6 +25,9 @@ export class LinkService { constructor( protected parentInjector: Injector, + @Inject(DATA_SERVICE_FACTORY) private getDataServiceFor: (resourceType: ResourceType) => GenericConstructor, + @Inject(LINK_DEFINITION_FACTORY) private getLinkDefinition: (source: GenericConstructor, linkName: keyof T['_links']) => LinkDefinition, + @Inject(LINK_DEFINITION_MAP_FACTORY) private getLinkDefinitions: (source: GenericConstructor) => Map>, ) { } @@ -49,12 +53,12 @@ export class LinkService { * @param linkToFollow the {@link FollowLinkConfig} to resolve */ public resolveLinkWithoutAttaching(model, linkToFollow: FollowLinkConfig): Observable> { - const matchingLinkDef = getLinkDefinition(model.constructor, linkToFollow.name); + const matchingLinkDef = this.getLinkDefinition(model.constructor, linkToFollow.name); if (hasNoValue(matchingLinkDef)) { throw new Error(`followLink('${linkToFollow.name}') was used for a ${model.constructor.name}, but there is no property on ${model.constructor.name} models with an @link() for ${linkToFollow.name}`); } else { - const provider = getDataServiceFor(matchingLinkDef.resourceType); + const provider = this.getDataServiceFor(matchingLinkDef.resourceType); if (hasNoValue(provider)) { throw new Error(`The @link() for ${linkToFollow.name} on ${model.constructor.name} models uses the resource type ${matchingLinkDef.resourceType.value.toUpperCase()}, but there is no service with an @dataService(${matchingLinkDef.resourceType.value.toUpperCase()}) annotation in order to retrieve it`); @@ -104,7 +108,7 @@ export class LinkService { */ public removeResolvedLinks(model: T): T { const result = Object.assign(new (model.constructor as GenericConstructor)(), model); - const linkDefs = getLinkDefinitions(model.constructor as GenericConstructor); + const linkDefs = this.getLinkDefinitions(model.constructor as GenericConstructor); if (isNotEmpty(linkDefs)) { linkDefs.forEach((linkDef: LinkDefinition) => { result[linkDef.propertyName] = undefined; diff --git a/src/app/core/data/relationship.service.spec.ts b/src/app/core/data/relationship.service.spec.ts index 0a8ec48464..0f7dd319c3 100644 --- a/src/app/core/data/relationship.service.spec.ts +++ b/src/app/core/data/relationship.service.spec.ts @@ -1,5 +1,4 @@ import { of as observableOf } from 'rxjs'; -import * as ItemRelationshipsUtils from '../../+item-page/simple/item-types/shared/item-relationships-utils'; import { followLink } from '../../shared/utils/follow-link-config.model'; import { ObjectCacheService } from '../cache/object-cache.service'; import { RelationshipType } from '../shared/item-relationships/relationship-type.model'; @@ -15,9 +14,9 @@ import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-servic import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/remote-data-build.service.mock'; import { getMockRequestService } from '../../shared/mocks/request.service.mock'; -import { createPaginatedList, spyOnOperator } from '../../shared/testing/utils.test'; +import { createPaginatedList } from '../../shared/testing/utils.test'; -xdescribe('RelationshipService', () => { +describe('RelationshipService', () => { let service: RelationshipService; let requestService: RequestService; @@ -132,7 +131,8 @@ xdescribe('RelationshipService', () => { null, null, null, - null + null, + jasmine.createSpy('paginatedRelationsToItems').and.returnValue((v) => v), ); } @@ -195,8 +195,6 @@ xdescribe('RelationshipService', () => { const rd$ = createSuccessfulRemoteDataObject$(relationsList); spyOn(service, 'getItemRelationshipsByLabel').and.returnValue(rd$); - - spyOnOperator(ItemRelationshipsUtils, 'paginatedRelationsToItems').and.returnValue((v) => v); }); it('should call getItemRelationshipsByLabel with the correct params', (done) => { @@ -225,7 +223,7 @@ xdescribe('RelationshipService', () => { mockLabel, mockOptions ).subscribe((result) => { - expect(ItemRelationshipsUtils.paginatedRelationsToItems).toHaveBeenCalledWith(mockItem.uuid); + expect((service as any).paginatedRelationsToItems).toHaveBeenCalledWith(mockItem.uuid); done(); }); }); diff --git a/src/app/core/data/relationship.service.ts b/src/app/core/data/relationship.service.ts index 95512be678..da1ff790df 100644 --- a/src/app/core/data/relationship.service.ts +++ b/src/app/core/data/relationship.service.ts @@ -1,11 +1,10 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; -import { Injectable } from '@angular/core'; +import { Inject, Injectable } from '@angular/core'; import { MemoizedSelector, select, Store } from '@ngrx/store'; import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; import { distinctUntilChanged, filter, map, mergeMap, startWith, switchMap, take, tap } from 'rxjs/operators'; import { - compareArraysUsingIds, - paginatedRelationsToItems, + compareArraysUsingIds, PAGINATED_RELATIONS_TO_ITEMS_OPERATOR, relationsToItems } from '../../+item-page/simple/item-types/shared/item-relationships-utils'; import { AppState, keySelector } from '../../app.reducer'; @@ -87,7 +86,8 @@ export class RelationshipService extends DataService { protected notificationsService: NotificationsService, protected http: HttpClient, protected comparator: DefaultChangeAnalyzer, - protected appStore: Store) { + protected appStore: Store, + @Inject(PAGINATED_RELATIONS_TO_ITEMS_OPERATOR) private paginatedRelationsToItems: (thisId: string) => (source: Observable>>) => Observable>>) { super(); } @@ -254,7 +254,7 @@ export class RelationshipService extends DataService { * @param options */ getRelatedItemsByLabel(item: Item, label: string, options?: FindListOptions): Observable>> { - return this.getItemRelationshipsByLabel(item, label, options, true, true, followLink('leftItem'), followLink('rightItem'), followLink('relationshipType')).pipe(paginatedRelationsToItems(item.uuid)); + return this.getItemRelationshipsByLabel(item, label, options, true, true, followLink('leftItem'), followLink('rightItem'), followLink('relationshipType')).pipe(this.paginatedRelationsToItems(item.uuid)); } /** diff --git a/src/app/shared/cookies/browser-klaro.service.spec.ts b/src/app/shared/cookies/browser-klaro.service.spec.ts index 7cf8a190f4..2155fb1bad 100644 --- a/src/app/shared/cookies/browser-klaro.service.spec.ts +++ b/src/app/shared/cookies/browser-klaro.service.spec.ts @@ -1,5 +1,4 @@ import { TestBed } from '@angular/core/testing'; - import { BrowserKlaroService, COOKIE_MDFIELD } from './browser-klaro.service'; import { getMockTranslateService } from '../mocks/translate.service.mock'; import { of as observableOf } from 'rxjs'; @@ -13,7 +12,7 @@ import { getTestScheduler } from 'jasmine-marbles'; import { MetadataValue } from '../../core/shared/metadata.models'; import { cloneDeep } from 'lodash'; -xdescribe('BrowserKlaroService', () => { +describe('BrowserKlaroService', () => { let translateService; let ePersonService; let authService; @@ -81,7 +80,7 @@ xdescribe('BrowserKlaroService', () => { } } }, - apps: [{ + services: [{ name: appName, purposes: [purpose] }], diff --git a/src/app/shared/metadata-representation/metadata-representation-loader.component.spec.ts b/src/app/shared/metadata-representation/metadata-representation-loader.component.spec.ts index bd5031ddc5..7edf1a700e 100644 --- a/src/app/shared/metadata-representation/metadata-representation-loader.component.spec.ts +++ b/src/app/shared/metadata-representation/metadata-representation-loader.component.spec.ts @@ -1,15 +1,15 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { ChangeDetectionStrategy, ComponentFactoryResolver, NO_ERRORS_SCHEMA } from '@angular/core'; +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { Context } from '../../core/shared/context.model'; import { MetadataRepresentation, MetadataRepresentationType } from '../../core/shared/metadata-representation/metadata-representation.model'; import { MetadataRepresentationLoaderComponent } from './metadata-representation-loader.component'; -import { PlainTextMetadataListElementComponent } from '../object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component'; -import { spyOnExported } from '../testing/utils.test'; import { MetadataRepresentationDirective } from './metadata-representation.directive'; -import * as metadataRepresentationDecorator from './metadata-representation.decorator'; +import { METADATA_REPRESENTATION_COMPONENT_FACTORY } from './metadata-representation.decorator'; +import { ThemeService } from '../theme-support/theme.service'; +import { PlainTextMetadataListElementComponent } from '../object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component'; const testType = 'TestType'; const testContext = Context.Search; @@ -29,16 +29,30 @@ class TestType implements MetadataRepresentation { } } -xdescribe('MetadataRepresentationLoaderComponent', () => { +describe('MetadataRepresentationLoaderComponent', () => { let comp: MetadataRepresentationLoaderComponent; let fixture: ComponentFixture; + let themeService: ThemeService; + const themeName = 'test-theme'; beforeEach(waitForAsync(() => { + themeService = jasmine.createSpyObj('themeService', { + getThemeName: themeName, + }); TestBed.configureTestingModule({ imports: [], declarations: [MetadataRepresentationLoaderComponent, PlainTextMetadataListElementComponent, MetadataRepresentationDirective], schemas: [NO_ERRORS_SCHEMA], - providers: [ComponentFactoryResolver] + providers: [ + { + provide: METADATA_REPRESENTATION_COMPONENT_FACTORY, + useValue: jasmine.createSpy('getMetadataRepresentationComponent').and.returnValue(PlainTextMetadataListElementComponent) + }, + { + provide: ThemeService, + useValue: themeService, + } + ] }).overrideComponent(MetadataRepresentationLoaderComponent, { set: { changeDetection: ChangeDetectionStrategy.Default, @@ -53,15 +67,12 @@ xdescribe('MetadataRepresentationLoaderComponent', () => { comp.mdRepresentation = new TestType(); comp.context = testContext; - - spyOnExported(metadataRepresentationDecorator, 'getMetadataRepresentationComponent').and.returnValue(PlainTextMetadataListElementComponent); fixture.detectChanges(); - })); describe('When the component is rendered', () => { it('should call the getMetadataRepresentationComponent function with the right entity type, representation type and context', () => { - expect(metadataRepresentationDecorator.getMetadataRepresentationComponent).toHaveBeenCalledWith(testType, testRepresentationType, testContext); + expect((comp as any).getMetadataRepresentationComponent).toHaveBeenCalledWith(testType, testRepresentationType, testContext, themeName); }); }); }); diff --git a/src/app/shared/metadata-representation/metadata-representation-loader.component.ts b/src/app/shared/metadata-representation/metadata-representation-loader.component.ts index 976dfdbda8..7077949809 100644 --- a/src/app/shared/metadata-representation/metadata-representation-loader.component.ts +++ b/src/app/shared/metadata-representation/metadata-representation-loader.component.ts @@ -1,6 +1,9 @@ -import { Component, ComponentFactoryResolver, Input, OnInit, ViewChild } from '@angular/core'; -import { MetadataRepresentation } from '../../core/shared/metadata-representation/metadata-representation.model'; -import { getMetadataRepresentationComponent } from './metadata-representation.decorator'; +import { Component, ComponentFactoryResolver, Inject, Input, OnInit, ViewChild } from '@angular/core'; +import { + MetadataRepresentation, + MetadataRepresentationType +} from '../../core/shared/metadata-representation/metadata-representation.model'; +import { METADATA_REPRESENTATION_COMPONENT_FACTORY } from './metadata-representation.decorator'; import { Context } from '../../core/shared/context.model'; import { GenericConstructor } from '../../core/shared/generic-constructor'; import { MetadataRepresentationListElementComponent } from '../object-list/metadata-representation-list-element/metadata-representation-list-element.component'; @@ -45,7 +48,8 @@ export class MetadataRepresentationLoaderComponent implements OnInit { constructor( private componentFactoryResolver: ComponentFactoryResolver, - private themeService: ThemeService + private themeService: ThemeService, + @Inject(METADATA_REPRESENTATION_COMPONENT_FACTORY) private getMetadataRepresentationComponent: (entityType: string, mdRepresentationType: MetadataRepresentationType, context: Context, theme: string) => GenericConstructor, ) { } @@ -68,6 +72,6 @@ export class MetadataRepresentationLoaderComponent implements OnInit { * @returns {string} */ private getComponent(): GenericConstructor { - return getMetadataRepresentationComponent(this.mdRepresentation.itemType, this.mdRepresentation.representationType, this.context, this.themeService.getThemeName()); + return this.getMetadataRepresentationComponent(this.mdRepresentation.itemType, this.mdRepresentation.representationType, this.context, this.themeService.getThemeName()); } } diff --git a/src/app/shared/metadata-representation/metadata-representation.decorator.ts b/src/app/shared/metadata-representation/metadata-representation.decorator.ts index 30bb507b49..0b5bea33d9 100644 --- a/src/app/shared/metadata-representation/metadata-representation.decorator.ts +++ b/src/app/shared/metadata-representation/metadata-representation.decorator.ts @@ -1,6 +1,13 @@ import { MetadataRepresentationType } from '../../core/shared/metadata-representation/metadata-representation.model'; import { hasNoValue, hasValue } from '../empty.util'; import { Context } from '../../core/shared/context.model'; +import { InjectionToken } from '@angular/core'; +import { GenericConstructor } from '../../core/shared/generic-constructor'; + +export const METADATA_REPRESENTATION_COMPONENT_FACTORY = new InjectionToken<(entityType: string, mdRepresentationType: MetadataRepresentationType, context: Context, theme: string) => GenericConstructor>('getMetadataRepresentationComponent', { + providedIn: 'root', + factory: () => getMetadataRepresentationComponent +}); export const map = new Map(); From d54b7d9f7c8cbcffea2c9165e35861d4b7d2a1c1 Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Thu, 8 Apr 2021 15:43:07 +0200 Subject: [PATCH 005/351] [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 006/351] [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 a2e00bbd9f8217590242584e03a38507bc2d5a72 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 8 Apr 2021 17:54:27 +0200 Subject: [PATCH 007/351] 78243: edit item page fine-grained permission checks --- .../collection-page-administrator.guard.ts | 4 +- .../community-page-administrator.guard.ts | 4 +- .../edit-item-page.routing.module.ts | 26 ++++++--- .../item-operation.component.html | 18 +++--- .../item-operation/itemOperation.model.ts | 10 +++- .../item-page-bitstreams.guard.ts | 31 +++++++++++ .../item-page-collection-mapper.guard.ts | 31 +++++++++++ .../item-page-metadata.guard.ts} | 14 ++--- .../item-page-reinstate.guard.ts | 4 +- .../item-page-relationships.guard.ts | 31 +++++++++++ .../edit-item-page/item-page-status.guard.ts | 32 +++++++++++ .../item-page-version-history.guard.ts | 31 +++++++++++ .../item-page-withdraw.guard.ts | 4 +- .../item-status/item-status.component.ts | 55 ++++++++----------- .../item-page-administrator.guard.ts | 4 +- .../collection-administrator.guard.ts | 4 +- .../community-administrator.guard.ts | 4 +- ... => dso-page-single-feature.guard.spec.ts} | 6 +- .../dso-page-single-feature.guard.ts | 27 +++++++++ ...uard.ts => dso-page-some-feature.guard.ts} | 4 +- .../group-administrator.guard.ts | 4 +- ...ingle-feature-authorization.guard.spec.ts} | 6 +- .../single-feature-authorization.guard.ts | 27 +++++++++ .../site-administrator.guard.ts | 4 +- .../site-register.guard.ts | 4 +- ...ts => some-feature-authorization.guard.ts} | 22 ++++---- .../data/feature-authorization/feature-id.ts | 7 +++ src/app/core/shared/operators.ts | 19 ++++++- .../endpoint-mocking-rest.service.ts | 10 +++- ...e-item-can-manage-bitstreams-response.json | 51 +++++++++++++++++ .../dspace-rest/mocks/response-map.mock.ts | 2 + src/assets/i18n/en.json5 | 2 + 32 files changed, 402 insertions(+), 100 deletions(-) create mode 100644 src/app/+item-page/edit-item-page/item-page-bitstreams.guard.ts create mode 100644 src/app/+item-page/edit-item-page/item-page-collection-mapper.guard.ts rename src/app/+item-page/{item-page-edit-metadata.guard.ts => edit-item-page/item-page-metadata.guard.ts} (59%) create mode 100644 src/app/+item-page/edit-item-page/item-page-relationships.guard.ts create mode 100644 src/app/+item-page/edit-item-page/item-page-status.guard.ts create mode 100644 src/app/+item-page/edit-item-page/item-page-version-history.guard.ts rename src/app/core/data/feature-authorization/feature-authorization-guard/{dso-page-feature.guard.spec.ts => dso-page-single-feature.guard.spec.ts} (93%) create mode 100644 src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.ts rename src/app/core/data/feature-authorization/feature-authorization-guard/{dso-page-feature.guard.ts => dso-page-some-feature.guard.ts} (90%) rename src/app/core/data/feature-authorization/feature-authorization-guard/{feature-authorization.guard.spec.ts => single-feature-authorization.guard.spec.ts} (92%) create mode 100644 src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.ts rename src/app/core/data/feature-authorization/feature-authorization-guard/{feature-authorization.guard.ts => some-feature-authorization.guard.ts} (64%) create mode 100644 src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-manage-bitstreams-response.json diff --git a/src/app/+collection-page/collection-page-administrator.guard.ts b/src/app/+collection-page/collection-page-administrator.guard.ts index 748cca81cb..c7866515b2 100644 --- a/src/app/+collection-page/collection-page-administrator.guard.ts +++ b/src/app/+collection-page/collection-page-administrator.guard.ts @@ -4,7 +4,7 @@ import { Collection } from '../core/shared/collection.model'; import { CollectionPageResolver } from './collection-page.resolver'; import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; import { Observable, of as observableOf } from 'rxjs'; -import { DsoPageFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard'; +import { DsoPageSingleFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; import { FeatureID } from '../core/data/feature-authorization/feature-id'; import { AuthService } from '../core/auth/auth.service'; @@ -14,7 +14,7 @@ import { AuthService } from '../core/auth/auth.service'; /** * Guard for preventing unauthorized access to certain {@link Collection} pages requiring administrator rights */ -export class CollectionPageAdministratorGuard extends DsoPageFeatureGuard { +export class CollectionPageAdministratorGuard extends DsoPageSingleFeatureGuard { constructor(protected resolver: CollectionPageResolver, protected authorizationService: AuthorizationDataService, protected router: Router, diff --git a/src/app/+community-page/community-page-administrator.guard.ts b/src/app/+community-page/community-page-administrator.guard.ts index fad4a78f07..fd7ce5f7bf 100644 --- a/src/app/+community-page/community-page-administrator.guard.ts +++ b/src/app/+community-page/community-page-administrator.guard.ts @@ -4,7 +4,7 @@ import { Community } from '../core/shared/community.model'; import { CommunityPageResolver } from './community-page.resolver'; import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; import { Observable, of as observableOf } from 'rxjs'; -import { DsoPageFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard'; +import { DsoPageSingleFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; import { FeatureID } from '../core/data/feature-authorization/feature-id'; import { AuthService } from '../core/auth/auth.service'; @@ -14,7 +14,7 @@ import { AuthService } from '../core/auth/auth.service'; /** * Guard for preventing unauthorized access to certain {@link Community} pages requiring administrator rights */ -export class CommunityPageAdministratorGuard extends DsoPageFeatureGuard { +export class CommunityPageAdministratorGuard extends DsoPageSingleFeatureGuard { constructor(protected resolver: CommunityPageResolver, protected authorizationService: AuthorizationDataService, protected router: Router, diff --git a/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts index b7d650d8c3..2535e42216 100644 --- a/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts +++ b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts @@ -31,8 +31,13 @@ import { } from './edit-item-page.routing-paths'; import { ItemPageReinstateGuard } from './item-page-reinstate.guard'; import { ItemPageWithdrawGuard } from './item-page-withdraw.guard'; -import { ItemPageEditMetadataGuard } from '../item-page-edit-metadata.guard'; +import { ItemPageMetadataGuard } from './item-page-metadata.guard'; import { ItemPageAdministratorGuard } from '../item-page-administrator.guard'; +import { ItemPageStatusGuard } from './item-page-status.guard'; +import { ItemPageBitstreamsGuard } from './item-page-bitstreams.guard'; +import { ItemPageRelationshipsGuard } from './item-page-relationships.guard'; +import { ItemPageVersionHistoryGuard } from './item-page-version-history.guard'; +import { ItemPageCollectionMapperGuard } from './item-page-collection-mapper.guard'; /** * Routing module that handles the routing for the Edit Item page administrator functionality @@ -60,25 +65,25 @@ import { ItemPageAdministratorGuard } from '../item-page-administrator.guard'; path: 'status', component: ItemStatusComponent, data: { title: 'item.edit.tabs.status.title', showBreadcrumbs: true }, - canActivate: [ItemPageAdministratorGuard] + canActivate: [ItemPageStatusGuard] }, { path: 'bitstreams', component: ItemBitstreamsComponent, data: { title: 'item.edit.tabs.bitstreams.title', showBreadcrumbs: true }, - canActivate: [ItemPageAdministratorGuard] + canActivate: [ItemPageBitstreamsGuard] }, { path: 'metadata', component: ItemMetadataComponent, data: { title: 'item.edit.tabs.metadata.title', showBreadcrumbs: true }, - canActivate: [ItemPageEditMetadataGuard] + canActivate: [ItemPageMetadataGuard] }, { path: 'relationships', component: ItemRelationshipsComponent, data: { title: 'item.edit.tabs.relationships.title', showBreadcrumbs: true }, - canActivate: [ItemPageEditMetadataGuard] + canActivate: [ItemPageRelationshipsGuard] }, /* TODO - uncomment & fix when view page exists { @@ -96,13 +101,13 @@ import { ItemPageAdministratorGuard } from '../item-page-administrator.guard'; path: 'versionhistory', component: ItemVersionHistoryComponent, data: { title: 'item.edit.tabs.versionhistory.title', showBreadcrumbs: true }, - canActivate: [ItemPageAdministratorGuard] + canActivate: [ItemPageVersionHistoryGuard] }, { path: 'mapper', component: ItemCollectionMapperComponent, data: { title: 'item.edit.tabs.item-mapper.title', showBreadcrumbs: true }, - canActivate: [ItemPageAdministratorGuard] + canActivate: [ItemPageCollectionMapperGuard] } ] }, @@ -175,7 +180,12 @@ import { ItemPageAdministratorGuard } from '../item-page-administrator.guard'; ItemPageReinstateGuard, ItemPageWithdrawGuard, ItemPageAdministratorGuard, - ItemPageEditMetadataGuard, + ItemPageMetadataGuard, + ItemPageStatusGuard, + ItemPageBitstreamsGuard, + ItemPageRelationshipsGuard, + ItemPageVersionHistoryGuard, + ItemPageCollectionMapperGuard, ] }) export class EditItemPageRoutingModule { diff --git a/src/app/+item-page/edit-item-page/item-operation/item-operation.component.html b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.html index e3989f02f3..ecbc19aea8 100644 --- a/src/app/+item-page/edit-item-page/item-operation/item-operation.component.html +++ b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.html @@ -3,13 +3,15 @@ {{'item.edit.tabs.status.buttons.' + operation.operationKey + '.label' | translate}} - -
- - {{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}} +
+ + + + +
diff --git a/src/app/+item-page/edit-item-page/item-operation/itemOperation.model.ts b/src/app/+item-page/edit-item-page/item-operation/itemOperation.model.ts index 105889d42d..33302dcba6 100644 --- a/src/app/+item-page/edit-item-page/item-operation/itemOperation.model.ts +++ b/src/app/+item-page/edit-item-page/item-operation/itemOperation.model.ts @@ -1,3 +1,5 @@ +import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; + /** * Represents an item operation used on the edit item page with a key, an operation URL to which will be navigated * when performing the action and an option to disable the operation. @@ -7,11 +9,15 @@ export class ItemOperation { operationKey: string; operationUrl: string; disabled: boolean; + authorized: boolean; + featureID: FeatureID; - constructor(operationKey: string, operationUrl: string) { + constructor(operationKey: string, operationUrl: string, featureID?: FeatureID, disabled = false, authorized = true) { this.operationKey = operationKey; this.operationUrl = operationUrl; - this.setDisabled(false); + this.featureID = featureID; + this.authorized = authorized; + this.setDisabled(disabled); } /** diff --git a/src/app/+item-page/edit-item-page/item-page-bitstreams.guard.ts b/src/app/+item-page/edit-item-page/item-page-bitstreams.guard.ts new file mode 100644 index 0000000000..bf4a6dc681 --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-page-bitstreams.guard.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@angular/core'; +import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; +import { Item } from '../../core/shared/item.model'; +import { ItemPageResolver } from '../item-page.resolver'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; +import { Observable, of as observableOf } from 'rxjs'; +import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { AuthService } from '../../core/auth/auth.service'; + +@Injectable({ + providedIn: 'root' +}) +/** + * Guard for preventing unauthorized access to certain {@link Item} pages requiring manage bitstreams rights + */ +export class ItemPageBitstreamsGuard extends DsoPageSingleFeatureGuard { + constructor(protected resolver: ItemPageResolver, + protected authorizationService: AuthorizationDataService, + protected router: Router, + protected authService: AuthService) { + super(resolver, authorizationService, router, authService); + } + + /** + * Check manage bitstreams authorization rights + */ + getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf(FeatureID.CanManageBitstreams); + } +} diff --git a/src/app/+item-page/edit-item-page/item-page-collection-mapper.guard.ts b/src/app/+item-page/edit-item-page/item-page-collection-mapper.guard.ts new file mode 100644 index 0000000000..2380377aea --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-page-collection-mapper.guard.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { ItemPageResolver } from '../item-page.resolver'; +import { Item } from '../../core/shared/item.model'; +import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; +import { Observable, of as observableOf } from 'rxjs'; +import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { AuthService } from '../../core/auth/auth.service'; + +@Injectable({ + providedIn: 'root' +}) +/** + * Guard for preventing unauthorized access to certain {@link Item} pages requiring manage mappings rights + */ +export class ItemPageCollectionMapperGuard extends DsoPageSingleFeatureGuard { + constructor(protected resolver: ItemPageResolver, + protected authorizationService: AuthorizationDataService, + protected router: Router, + protected authService: AuthService) { + super(resolver, authorizationService, router, authService); + } + + /** + * Check manage mappings authorization rights + */ + getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf(FeatureID.CanManageMappings); + } +} diff --git a/src/app/+item-page/item-page-edit-metadata.guard.ts b/src/app/+item-page/edit-item-page/item-page-metadata.guard.ts similarity index 59% rename from src/app/+item-page/item-page-edit-metadata.guard.ts rename to src/app/+item-page/edit-item-page/item-page-metadata.guard.ts index a9b870b1cd..a6846bec4e 100644 --- a/src/app/+item-page/item-page-edit-metadata.guard.ts +++ b/src/app/+item-page/edit-item-page/item-page-metadata.guard.ts @@ -1,12 +1,12 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; -import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; -import { ItemPageResolver } from './item-page.resolver'; -import { Item } from '../core/shared/item.model'; -import { DsoPageFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { ItemPageResolver } from '../item-page.resolver'; +import { Item } from '../../core/shared/item.model'; +import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; import { Observable, of as observableOf } from 'rxjs'; -import { FeatureID } from '../core/data/feature-authorization/feature-id'; -import { AuthService } from '../core/auth/auth.service'; +import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { AuthService } from '../../core/auth/auth.service'; @Injectable({ providedIn: 'root' @@ -14,7 +14,7 @@ import { AuthService } from '../core/auth/auth.service'; /** * Guard for preventing unauthorized access to certain {@link Item} pages requiring edit metadata rights */ -export class ItemPageEditMetadataGuard extends DsoPageFeatureGuard { +export class ItemPageMetadataGuard extends DsoPageSingleFeatureGuard { constructor(protected resolver: ItemPageResolver, protected authorizationService: AuthorizationDataService, protected router: Router, diff --git a/src/app/+item-page/edit-item-page/item-page-reinstate.guard.ts b/src/app/+item-page/edit-item-page/item-page-reinstate.guard.ts index 0288e30b0a..88c9c20b12 100644 --- a/src/app/+item-page/edit-item-page/item-page-reinstate.guard.ts +++ b/src/app/+item-page/edit-item-page/item-page-reinstate.guard.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { DsoPageFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard'; +import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; import { Item } from '../../core/shared/item.model'; import { ItemPageResolver } from '../item-page.resolver'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; @@ -14,7 +14,7 @@ import { AuthService } from '../../core/auth/auth.service'; /** * Guard for preventing unauthorized access to certain {@link Item} pages requiring reinstate rights */ -export class ItemPageReinstateGuard extends DsoPageFeatureGuard { +export class ItemPageReinstateGuard extends DsoPageSingleFeatureGuard { constructor(protected resolver: ItemPageResolver, protected authorizationService: AuthorizationDataService, protected router: Router, diff --git a/src/app/+item-page/edit-item-page/item-page-relationships.guard.ts b/src/app/+item-page/edit-item-page/item-page-relationships.guard.ts new file mode 100644 index 0000000000..77da92ae02 --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-page-relationships.guard.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { ItemPageResolver } from '../item-page.resolver'; +import { Item } from '../../core/shared/item.model'; +import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; +import { Observable, of as observableOf } from 'rxjs'; +import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { AuthService } from '../../core/auth/auth.service'; + +@Injectable({ + providedIn: 'root' +}) +/** + * Guard for preventing unauthorized access to certain {@link Item} pages requiring manage relationships rights + */ +export class ItemPageRelationshipsGuard extends DsoPageSingleFeatureGuard { + constructor(protected resolver: ItemPageResolver, + protected authorizationService: AuthorizationDataService, + protected router: Router, + protected authService: AuthService) { + super(resolver, authorizationService, router, authService); + } + + /** + * Check manage relationships authorization rights + */ + getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf(FeatureID.CanManageRelationships); + } +} diff --git a/src/app/+item-page/edit-item-page/item-page-status.guard.ts b/src/app/+item-page/edit-item-page/item-page-status.guard.ts new file mode 100644 index 0000000000..98f963a4be --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-page-status.guard.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; +import { Item } from '../../core/shared/item.model'; +import { ItemPageResolver } from '../item-page.resolver'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; +import { Observable, of as observableOf } from 'rxjs'; +import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { AuthService } from '../../core/auth/auth.service'; +import { DsoPageSomeFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard'; + +@Injectable({ + providedIn: 'root' +}) +/** + * Guard for preventing unauthorized access to certain {@link Item} pages requiring any of the rights required for + * the status page + */ +export class ItemPageStatusGuard extends DsoPageSomeFeatureGuard { + constructor(protected resolver: ItemPageResolver, + protected authorizationService: AuthorizationDataService, + protected router: Router, + protected authService: AuthService) { + super(resolver, authorizationService, router, authService); + } + + /** + * Check authorization rights + */ + getFeatureIDs(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf([FeatureID.CanManageMappings, FeatureID.WithdrawItem, FeatureID.ReinstateItem, FeatureID.CanManagePolicies, FeatureID.CanMakePrivate, FeatureID.CanDelete, FeatureID.CanMove]); + } +} diff --git a/src/app/+item-page/edit-item-page/item-page-version-history.guard.ts b/src/app/+item-page/edit-item-page/item-page-version-history.guard.ts new file mode 100644 index 0000000000..dccdd9e641 --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-page-version-history.guard.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { ItemPageResolver } from '../item-page.resolver'; +import { Item } from '../../core/shared/item.model'; +import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; +import { Observable, of as observableOf } from 'rxjs'; +import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { AuthService } from '../../core/auth/auth.service'; + +@Injectable({ + providedIn: 'root' +}) +/** + * Guard for preventing unauthorized access to certain {@link Item} pages requiring manage versions rights + */ +export class ItemPageVersionHistoryGuard extends DsoPageSingleFeatureGuard { + constructor(protected resolver: ItemPageResolver, + protected authorizationService: AuthorizationDataService, + protected router: Router, + protected authService: AuthService) { + super(resolver, authorizationService, router, authService); + } + + /** + * Check manage versions authorization rights + */ + getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf(FeatureID.CanManageVersions); + } +} diff --git a/src/app/+item-page/edit-item-page/item-page-withdraw.guard.ts b/src/app/+item-page/edit-item-page/item-page-withdraw.guard.ts index 243f751974..de9b7d0147 100644 --- a/src/app/+item-page/edit-item-page/item-page-withdraw.guard.ts +++ b/src/app/+item-page/edit-item-page/item-page-withdraw.guard.ts @@ -1,4 +1,4 @@ -import { DsoPageFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard'; +import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; import { Item } from '../../core/shared/item.model'; import { Injectable } from '@angular/core'; import { ItemPageResolver } from '../item-page.resolver'; @@ -14,7 +14,7 @@ import { AuthService } from '../../core/auth/auth.service'; /** * Guard for preventing unauthorized access to certain {@link Item} pages requiring withdraw rights */ -export class ItemPageWithdrawGuard extends DsoPageFeatureGuard { +export class ItemPageWithdrawGuard extends DsoPageSingleFeatureGuard { constructor(protected resolver: ItemPageResolver, protected authorizationService: AuthorizationDataService, protected router: Router, diff --git a/src/app/+item-page/edit-item-page/item-status/item-status.component.ts b/src/app/+item-page/edit-item-page/item-status/item-status.component.ts index 2745fc8df7..51aa24ea6c 100644 --- a/src/app/+item-page/edit-item-page/item-status/item-status.component.ts +++ b/src/app/+item-page/edit-item-page/item-status/item-status.component.ts @@ -4,7 +4,7 @@ import { Item } from '../../../core/shared/item.model'; import { ActivatedRoute } from '@angular/router'; import { ItemOperation } from '../item-operation/itemOperation.model'; import { distinctUntilChanged, first, map } from 'rxjs/operators'; -import { BehaviorSubject, Observable } from 'rxjs'; +import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, of } from 'rxjs'; import { RemoteData } from '../../../core/data/remote-data'; import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-paths'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; @@ -78,42 +78,33 @@ export class ItemStatusComponent implements OnInit { The value is supposed to be a href for the button */ const operations = []; - operations.push(new ItemOperation('authorizations', this.getCurrentUrl(item) + '/authorizations')); - operations.push(new ItemOperation('mappedCollections', this.getCurrentUrl(item) + '/mapper')); - operations.push(undefined); - // Store the index of the "withdraw" or "reinstate" operation, because it's added asynchronously - const indexOfWithdrawReinstate = operations.length - 1; - if (item.isDiscoverable) { - operations.push(new ItemOperation('private', this.getCurrentUrl(item) + '/private')); + operations.push(new ItemOperation('authorizations', this.getCurrentUrl(item) + '/authorizations', FeatureID.CanManagePolicies, true)); + operations.push(new ItemOperation('mappedCollections', this.getCurrentUrl(item) + '/mapper', FeatureID.CanManageMappings, true)); + if (item.isWithdrawn) { + operations.push(new ItemOperation('reinstate', this.getCurrentUrl(item) + '/reinstate', FeatureID.ReinstateItem, true)); } else { - operations.push(new ItemOperation('public', this.getCurrentUrl(item) + '/public')); + operations.push(new ItemOperation('reinstate', this.getCurrentUrl(item) + '/reinstate', FeatureID.WithdrawItem, true)); } - operations.push(new ItemOperation('delete', this.getCurrentUrl(item) + '/delete')); - operations.push(new ItemOperation('move', this.getCurrentUrl(item) + '/move')); + if (item.isDiscoverable) { + operations.push(new ItemOperation('private', this.getCurrentUrl(item) + '/private', FeatureID.CanMakePrivate, true)); + } else { + operations.push(new ItemOperation('public', this.getCurrentUrl(item) + '/public', FeatureID.CanMakePrivate, true)); + } + operations.push(new ItemOperation('delete', this.getCurrentUrl(item) + '/delete', FeatureID.CanDelete, true)); + operations.push(new ItemOperation('move', this.getCurrentUrl(item) + '/move', FeatureID.CanMove, true)); this.operations$.next(operations); - if (item.isWithdrawn) { - this.authorizationService.isAuthorized(FeatureID.ReinstateItem, item.self).pipe(distinctUntilChanged()).subscribe((authorized) => { - const newOperations = [...this.operations$.value]; - if (authorized) { - newOperations[indexOfWithdrawReinstate] = new ItemOperation('reinstate', this.getCurrentUrl(item) + '/reinstate'); - } else { - newOperations[indexOfWithdrawReinstate] = undefined; - } - this.operations$.next(newOperations); - }); - } else { - this.authorizationService.isAuthorized(FeatureID.WithdrawItem, item.self).pipe(distinctUntilChanged()).subscribe((authorized) => { - const newOperations = [...this.operations$.value]; - if (authorized) { - newOperations[indexOfWithdrawReinstate] = new ItemOperation('withdraw', this.getCurrentUrl(item) + '/withdraw'); - } else { - newOperations[indexOfWithdrawReinstate] = undefined; - } - this.operations$.next(newOperations); - }); - } + observableCombineLatest(operations.map((operation) => { + if (hasValue(operation.featureID)) { + return this.authorizationService.isAuthorized(operation.featureID, item.self).pipe( + distinctUntilChanged(), + map((authorized) => new ItemOperation(operation.operationKey, operation.operationUrl, operation.featureID, !authorized, authorized)) + ); + } else { + return of(operation); + } + })).subscribe((ops) => this.operations$.next(ops)); }); this.itemPageRoute$ = this.itemRD$.pipe( getAllSucceededRemoteDataPayload(), diff --git a/src/app/+item-page/item-page-administrator.guard.ts b/src/app/+item-page/item-page-administrator.guard.ts index c90502472e..5d3464aa75 100644 --- a/src/app/+item-page/item-page-administrator.guard.ts +++ b/src/app/+item-page/item-page-administrator.guard.ts @@ -3,7 +3,7 @@ import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/ro import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; import { ItemPageResolver } from './item-page.resolver'; import { Item } from '../core/shared/item.model'; -import { DsoPageFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard'; +import { DsoPageSingleFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; import { Observable, of as observableOf } from 'rxjs'; import { FeatureID } from '../core/data/feature-authorization/feature-id'; import { AuthService } from '../core/auth/auth.service'; @@ -14,7 +14,7 @@ import { AuthService } from '../core/auth/auth.service'; /** * Guard for preventing unauthorized access to certain {@link Item} pages requiring administrator rights */ -export class ItemPageAdministratorGuard extends DsoPageFeatureGuard { +export class ItemPageAdministratorGuard extends DsoPageSingleFeatureGuard { constructor(protected resolver: ItemPageResolver, protected authorizationService: AuthorizationDataService, protected router: Router, diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/collection-administrator.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/collection-administrator.guard.ts index bc39397ed9..b41a322cb6 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/collection-administrator.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/collection-administrator.guard.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { FeatureAuthorizationGuard } from './feature-authorization.guard'; +import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; import { AuthorizationDataService } from '../authorization-data.service'; import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; import { AuthService } from '../../../auth/auth.service'; @@ -13,7 +13,7 @@ import { FeatureID } from '../feature-id'; @Injectable({ providedIn: 'root' }) -export class CollectionAdministratorGuard extends FeatureAuthorizationGuard { +export class CollectionAdministratorGuard extends SingleFeatureAuthorizationGuard { constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { super(authorizationService, router, authService); } diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/community-administrator.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/community-administrator.guard.ts index afb1fea63d..2ab77a00cc 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/community-administrator.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/community-administrator.guard.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { FeatureAuthorizationGuard } from './feature-authorization.guard'; +import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; import { AuthorizationDataService } from '../authorization-data.service'; import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; import { AuthService } from '../../../auth/auth.service'; @@ -13,7 +13,7 @@ import { FeatureID } from '../feature-id'; @Injectable({ providedIn: 'root' }) -export class CommunityAdministratorGuard extends FeatureAuthorizationGuard { +export class CommunityAdministratorGuard extends SingleFeatureAuthorizationGuard { constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { super(authorizationService, router, authService); } diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard.spec.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.spec.ts similarity index 93% rename from src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard.spec.ts rename to src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.spec.ts index f98e3f1837..06677d1f22 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard.spec.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.spec.ts @@ -4,14 +4,14 @@ import { RemoteData } from '../../remote-data'; import { Observable, of as observableOf } from 'rxjs'; import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; import { DSpaceObject } from '../../../shared/dspace-object.model'; -import { DsoPageFeatureGuard } from './dso-page-feature.guard'; +import { DsoPageSingleFeatureGuard } from './dso-page-single-feature.guard'; import { FeatureID } from '../feature-id'; import { AuthService } from '../../../auth/auth.service'; /** * Test implementation of abstract class DsoPageAdministratorGuard */ -class DsoPageFeatureGuardImpl extends DsoPageFeatureGuard { +class DsoPageFeatureGuardImpl extends DsoPageSingleFeatureGuard { constructor(protected resolver: Resolve>, protected authorizationService: AuthorizationDataService, protected router: Router, @@ -26,7 +26,7 @@ class DsoPageFeatureGuardImpl extends DsoPageFeatureGuard { } describe('DsoPageAdministratorGuard', () => { - let guard: DsoPageFeatureGuard; + let guard: DsoPageSingleFeatureGuard; let authorizationService: AuthorizationDataService; let router: Router; let authService: AuthService; diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.ts new file mode 100644 index 0000000000..3fc90f9069 --- /dev/null +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.ts @@ -0,0 +1,27 @@ +import { DSpaceObject } from '../../../shared/dspace-object.model'; +import { DsoPageSomeFeatureGuard } from './dso-page-some-feature.guard'; +import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; +import { FeatureID } from '../feature-id'; +import { map } from 'rxjs/operators'; +import { Observable } from 'rxjs'; + +/** + * Abstract Guard for preventing unauthorized access to {@link DSpaceObject} pages that require rights for a specific feature + * This guard utilizes a resolver to retrieve the relevant object to check authorizations for + */ +export abstract class DsoPageSingleFeatureGuard extends DsoPageSomeFeatureGuard { + /** + * The features to check authorization for + */ + getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return this.getFeatureID(route, state).pipe( + map((featureID) => [featureID]), + ); + } + + /** + * The type of feature to check authorization for + * Override this method to define a feature + */ + abstract getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable; +} diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.ts similarity index 90% rename from src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard.ts rename to src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.ts index c50dd7f95d..7b7cb4c196 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.ts @@ -5,15 +5,15 @@ import { Observable } from 'rxjs'; import { getAllSucceededRemoteDataPayload } from '../../../shared/operators'; import { map } from 'rxjs/operators'; import { DSpaceObject } from '../../../shared/dspace-object.model'; -import { FeatureAuthorizationGuard } from './feature-authorization.guard'; import { AuthService } from '../../../auth/auth.service'; import { hasNoValue, hasValue } from '../../../../shared/empty.util'; +import { SomeFeatureAuthorizationGuard } from './some-feature-authorization.guard'; /** * Abstract Guard for preventing unauthorized access to {@link DSpaceObject} pages that require rights for a specific feature * This guard utilizes a resolver to retrieve the relevant object to check authorizations for */ -export abstract class DsoPageFeatureGuard extends FeatureAuthorizationGuard { +export abstract class DsoPageSomeFeatureGuard extends SomeFeatureAuthorizationGuard { constructor(protected resolver: Resolve>, protected authorizationService: AuthorizationDataService, protected router: Router, diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/group-administrator.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/group-administrator.guard.ts index 3fee767fdc..5afd572326 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/group-administrator.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/group-administrator.guard.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { FeatureAuthorizationGuard } from './feature-authorization.guard'; +import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; import { AuthorizationDataService } from '../authorization-data.service'; import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; import { AuthService } from '../../../auth/auth.service'; @@ -13,7 +13,7 @@ import { FeatureID } from '../feature-id'; @Injectable({ providedIn: 'root' }) -export class GroupAdministratorGuard extends FeatureAuthorizationGuard { +export class GroupAdministratorGuard extends SingleFeatureAuthorizationGuard { constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { super(authorizationService, router, authService); } diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/feature-authorization.guard.spec.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.spec.ts similarity index 92% rename from src/app/core/data/feature-authorization/feature-authorization-guard/feature-authorization.guard.spec.ts rename to src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.spec.ts index 2c6f4b0717..1fa5498f12 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/feature-authorization.guard.spec.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.spec.ts @@ -1,4 +1,4 @@ -import { FeatureAuthorizationGuard } from './feature-authorization.guard'; +import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; import { Observable, of as observableOf } from 'rxjs'; @@ -9,7 +9,7 @@ import { AuthService } from '../../../auth/auth.service'; * Test implementation of abstract class FeatureAuthorizationGuard * Provide the return values of the overwritten getters as constructor arguments */ -class FeatureAuthorizationGuardImpl extends FeatureAuthorizationGuard { +class FeatureAuthorizationGuardImpl extends SingleFeatureAuthorizationGuard { constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService, @@ -33,7 +33,7 @@ class FeatureAuthorizationGuardImpl extends FeatureAuthorizationGuard { } describe('FeatureAuthorizationGuard', () => { - let guard: FeatureAuthorizationGuard; + let guard: SingleFeatureAuthorizationGuard; let authorizationService: AuthorizationDataService; let router: Router; let authService: AuthService; diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.ts new file mode 100644 index 0000000000..cb71d2f418 --- /dev/null +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.ts @@ -0,0 +1,27 @@ +import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; +import { FeatureID } from '../feature-id'; +import { Observable } from 'rxjs'; +import { map} from 'rxjs/operators'; +import { SomeFeatureAuthorizationGuard } from './some-feature-authorization.guard'; + +/** + * Abstract Guard for preventing unauthorized activating and loading of routes when a user + * doesn't have authorized rights on a specific feature and/or object. + * Override the desired getters in the parent class for checking specific authorization on a feature and/or object. + */ +export abstract class SingleFeatureAuthorizationGuard extends SomeFeatureAuthorizationGuard { + /** + * The features to check authorization for + */ + getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return this.getFeatureID(route, state).pipe( + map((featureID) => [featureID]), + ); + } + + /** + * The type of feature to check authorization for + * Override this method to define a feature + */ + abstract getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable; +} diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/site-administrator.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/site-administrator.guard.ts index bb678ebf33..cc6f50c161 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/site-administrator.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/site-administrator.guard.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { FeatureAuthorizationGuard } from './feature-authorization.guard'; +import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; import { FeatureID } from '../feature-id'; import { AuthorizationDataService } from '../authorization-data.service'; import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; @@ -13,7 +13,7 @@ import { AuthService } from '../../../auth/auth.service'; @Injectable({ providedIn: 'root' }) -export class SiteAdministratorGuard extends FeatureAuthorizationGuard { +export class SiteAdministratorGuard extends SingleFeatureAuthorizationGuard { constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { super(authorizationService, router, authService); } diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/site-register.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/site-register.guard.ts index 709d9ff266..bdbb8250e2 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/site-register.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/site-register.guard.ts @@ -1,4 +1,4 @@ -import { FeatureAuthorizationGuard } from './feature-authorization.guard'; +import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; import { Injectable } from '@angular/core'; import { AuthorizationDataService } from '../authorization-data.service'; import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; @@ -13,7 +13,7 @@ import { AuthService } from '../../../auth/auth.service'; @Injectable({ providedIn: 'root' }) -export class SiteRegisterGuard extends FeatureAuthorizationGuard { +export class SiteRegisterGuard extends SingleFeatureAuthorizationGuard { constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { super(authorizationService, router, authService); } diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/feature-authorization.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.ts similarity index 64% rename from src/app/core/data/feature-authorization/feature-authorization-guard/feature-authorization.guard.ts rename to src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.ts index 86b75b637e..3a6cf745c9 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/feature-authorization.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.ts @@ -2,16 +2,16 @@ import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTr import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; -import { returnForbiddenUrlTreeOrLoginOnFalse } from '../../../shared/operators'; +import { returnForbiddenUrlTreeOrLoginOnAllFalse } from '../../../shared/operators'; import { switchMap } from 'rxjs/operators'; import { AuthService } from '../../../auth/auth.service'; /** * Abstract Guard for preventing unauthorized activating and loading of routes when a user - * doesn't have authorized rights on a specific feature and/or object. - * Override the desired getters in the parent class for checking specific authorization on a feature and/or object. + * doesn't have authorized rights on any of the specified features and/or object. + * Override the desired getters in the parent class for checking specific authorization on a list of features and/or object. */ -export abstract class FeatureAuthorizationGuard implements CanActivate { +export abstract class SomeFeatureAuthorizationGuard implements CanActivate { constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { @@ -22,17 +22,19 @@ export abstract class FeatureAuthorizationGuard implements CanActivate { * Redirect the user to the unauthorized page when he/she's not authorized for the given feature */ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return observableCombineLatest(this.getFeatureID(route, state), this.getObjectUrl(route, state), this.getEPersonUuid(route, state)).pipe( - switchMap(([featureID, objectUrl, ePersonUuid]) => this.authorizationService.isAuthorized(featureID, objectUrl, ePersonUuid)), - returnForbiddenUrlTreeOrLoginOnFalse(this.router, this.authService, state.url) + return observableCombineLatest(this.getFeatureIDs(route, state), this.getObjectUrl(route, state), this.getEPersonUuid(route, state)).pipe( + switchMap(([featureIDs, objectUrl, ePersonUuid]) => + observableCombineLatest(...featureIDs.map((featureID) => this.authorizationService.isAuthorized(featureID, objectUrl, ePersonUuid))) + ), + returnForbiddenUrlTreeOrLoginOnAllFalse(this.router, this.authService, state.url) ); } /** - * The type of feature to check authorization for - * Override this method to define a feature + * The features to check authorization for + * Override this method to define a list of features */ - abstract getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable; + abstract getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable; /** * The URL of the object to check if the user has authorized rights for diff --git a/src/app/core/data/feature-authorization/feature-id.ts b/src/app/core/data/feature-authorization/feature-id.ts index e3473a895e..df77dd8949 100644 --- a/src/app/core/data/feature-authorization/feature-id.ts +++ b/src/app/core/data/feature-authorization/feature-id.ts @@ -12,4 +12,11 @@ export enum FeatureID { CanManageGroups = 'canManageGroups', IsCollectionAdmin = 'isCollectionAdmin', IsCommunityAdmin = 'isCommunityAdmin', + CanManageVersions = 'canManageVersions', + CanManageBitstreams = 'canManageBitstreams', + CanManageRelationships = 'canManageRelationships', + CanManageMappings = 'canManageMappings', + CanManagePolicies = 'canManagePolicies', + CanMakePrivate = 'canMakePrivate', + CanMove = 'canMove', } diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index dd610b6ca7..2d0ab70e2c 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -201,10 +201,23 @@ export const redirectOn4xx = (router: Router, authService: AuthService) => */ export const returnForbiddenUrlTreeOrLoginOnFalse = (router: Router, authService: AuthService, redirectUrl: string) => (source: Observable): Observable => + source.pipe( + map((authorized) => [authorized]), + returnForbiddenUrlTreeOrLoginOnAllFalse(router, authService, redirectUrl), + ); + +/** + * Operator that returns a UrlTree to a forbidden page or the login page when the booleans received are all false + * @param router The router used to navigate to a forbidden page + * @param authService The AuthService used to determine whether or not the user is logged in + * @param redirectUrl The URL to redirect back to after logging in + */ +export const returnForbiddenUrlTreeOrLoginOnAllFalse = (router: Router, authService: AuthService, redirectUrl: string) => + (source: Observable): Observable => observableCombineLatest(source, authService.isAuthenticated()).pipe( - map(([authorized, authenticated]: [boolean, boolean]) => { - if (authorized) { - return authorized; + map(([authorizedList, authenticated]: [boolean[], boolean]) => { + if (authorizedList.indexOf(true) > -1) { + return true; } else { if (authenticated) { return router.parseUrl(getForbiddenRoute()); diff --git a/src/app/shared/mocks/dspace-rest/endpoint-mocking-rest.service.ts b/src/app/shared/mocks/dspace-rest/endpoint-mocking-rest.service.ts index 278a392e18..8d621ad4be 100644 --- a/src/app/shared/mocks/dspace-rest/endpoint-mocking-rest.service.ts +++ b/src/app/shared/mocks/dspace-rest/endpoint-mocking-rest.service.ts @@ -97,8 +97,14 @@ export class EndpointMockingRestService extends DspaceRestService { * the mock response if there is one, undefined otherwise */ private getMockData(urlStr: string): any { - const url = new URL(urlStr); - const key = url.pathname.slice(environment.rest.nameSpace.length); + let key; + if (this.mockResponseMap.has(urlStr)) { + key = urlStr; + } else { + // didn't find an exact match for the url, try to match only the endpoint without namespace and parameters + const url = new URL(urlStr); + key = url.pathname.slice(environment.rest.nameSpace.length); + } if (this.mockResponseMap.has(key)) { // parse and stringify to clone the object to ensure that any changes made // to it afterwards don't affect future calls diff --git a/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-manage-bitstreams-response.json b/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-manage-bitstreams-response.json new file mode 100644 index 0000000000..1642691672 --- /dev/null +++ b/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-manage-bitstreams-response.json @@ -0,0 +1,51 @@ +{ + "_embedded": { + "authorizations": [ + { + "id": "cd824a61-95be-4e16-bccd-51fea26707d0_canManageBitstreams_core.item_96715576-3748-4761-ad45-001646632963", + "type": "authorization", + "_links": { + "eperson": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageBitstreams_core.item_96715576-3748-4761-ad45-001646632963/eperson" + }, + "feature": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageBitstreams_core.item_96715576-3748-4761-ad45-001646632963/feature" + }, + "object": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageBitstreams_core.item_96715576-3748-4761-ad45-001646632963/object" + }, + "self": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageBitstreams_core.item_96715576-3748-4761-ad45-001646632963" + } + }, + "_embedded": { + "feature": { + "id": "canManageBitstreams", + "description": "It can be used to verify if the bitstreams of the specified objects can be managed", + "type": "feature", + "resourcetypes": [ + "core.item", + "core.bundle" + ], + "_links": { + "self": { + "href": "https://api7.dspace.org/server/api/authz/features/canManageBitstreams" + } + } + } + } + } + ] + }, + "_links": { + "self": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/96715576-3748-4761-ad45-001646632963&feature=canManageBitstreams" + } + }, + "page": { + "size": 20, + "totalElements": 1, + "totalPages": 1, + "number": 0 + } +} diff --git a/src/app/shared/mocks/dspace-rest/mocks/response-map.mock.ts b/src/app/shared/mocks/dspace-rest/mocks/response-map.mock.ts index 92f04a7bc5..4cc5c39b7d 100644 --- a/src/app/shared/mocks/dspace-rest/mocks/response-map.mock.ts +++ b/src/app/shared/mocks/dspace-rest/mocks/response-map.mock.ts @@ -2,6 +2,7 @@ import { InjectionToken } from '@angular/core'; // import mockSubmissionResponse from './mock-submission-response.json'; // import mockPublicationResponse from './mock-publication-response.json'; // import mockUntypedItemResponse from './mock-untyped-item-response.json'; +import mockFeatureItemCanManageBitstreamsResponse from './mock-feature-item-can-manage-bitstreams-response.json'; export class ResponseMapMock extends Map {} @@ -16,4 +17,5 @@ export const mockResponseMap: ResponseMapMock = new Map([ // [ '/config/submissionforms/traditionalpageone', mockSubmissionResponse ] // [ '/api/pid/find', mockPublicationResponse ], // [ '/api/pid/find', mockUntypedItemResponse ], + [ 'https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/96715576-3748-4761-ad45-001646632963&feature=canManageBitstreams&embed=feature', mockFeatureItemCanManageBitstreamsResponse ], ]); diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 2924a3a47e..abecfce291 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1761,6 +1761,8 @@ "item.edit.tabs.status.buttons.reinstate.label": "Reinstate item into the repository", + "item.edit.tabs.status.buttons.unauthorized": "You don't have permission to perform this action", + "item.edit.tabs.status.buttons.withdraw.button": "Withdraw...", "item.edit.tabs.status.buttons.withdraw.label": "Withdraw item from the repository", From 3504feedf3178589dcb7d4c123c243b6a8adff69 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 9 Apr 2021 13:34:04 +0200 Subject: [PATCH 008/351] 78243: edit item page fine-grained permission checks - mocks + tests --- .../item-operation.component.spec.ts | 14 +-- .../dso-page-single-feature.guard.spec.ts | 8 +- .../dso-page-some-feature.guard.spec.ts | 87 ++++++++++++++ .../dso-page-some-feature.guard.ts | 2 +- ...single-feature-authorization.guard.spec.ts | 8 +- .../some-feature-authorization.guard.spec.ts | 110 ++++++++++++++++++ ...feature-item-can-delete-none-response.json | 13 +++ .../mock-feature-item-can-move-response.json | 50 ++++++++ .../dspace-rest/mocks/response-map.mock.ts | 4 + 9 files changed, 280 insertions(+), 16 deletions(-) create mode 100644 src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.spec.ts create mode 100644 src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.spec.ts create mode 100644 src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-delete-none-response.json create mode 100644 src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-move-response.json diff --git a/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts index 00e7f8452a..7570119b3a 100644 --- a/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts @@ -28,19 +28,19 @@ describe('ItemOperationComponent', () => { }); it('should render operation row', () => { - const span = fixture.debugElement.query(By.css('span')).nativeElement; + const span = fixture.debugElement.query(By.css('.action-label span')).nativeElement; expect(span.textContent).toContain('item.edit.tabs.status.buttons.key1.label'); - const link = fixture.debugElement.query(By.css('a')).nativeElement; - expect(link.href).toContain('url1'); - expect(link.textContent).toContain('item.edit.tabs.status.buttons.key1.button'); + const button = fixture.debugElement.query(By.css('button')).nativeElement; + expect(button.textContent).toContain('item.edit.tabs.status.buttons.key1.button'); }); it('should render disabled operation row', () => { itemOperation.setDisabled(true); fixture.detectChanges(); - const span = fixture.debugElement.query(By.css('span')).nativeElement; + const span = fixture.debugElement.query(By.css('.action-label span')).nativeElement; expect(span.textContent).toContain('item.edit.tabs.status.buttons.key1.label'); - const span2 = fixture.debugElement.query(By.css('span.btn-danger')).nativeElement; - expect(span2.textContent).toContain('item.edit.tabs.status.buttons.key1.button'); + const button = fixture.debugElement.query(By.css('button')).nativeElement; + expect(button.disabled).toBeTrue(); + expect(button.textContent).toContain('item.edit.tabs.status.buttons.key1.button'); }); }); diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.spec.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.spec.ts index 06677d1f22..6c1f330c69 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.spec.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.spec.ts @@ -9,9 +9,9 @@ import { FeatureID } from '../feature-id'; import { AuthService } from '../../../auth/auth.service'; /** - * Test implementation of abstract class DsoPageAdministratorGuard + * Test implementation of abstract class DsoPageSingleFeatureGuard */ -class DsoPageFeatureGuardImpl extends DsoPageSingleFeatureGuard { +class DsoPageSingleFeatureGuardImpl extends DsoPageSingleFeatureGuard { constructor(protected resolver: Resolve>, protected authorizationService: AuthorizationDataService, protected router: Router, @@ -25,7 +25,7 @@ class DsoPageFeatureGuardImpl extends DsoPageSingleFeatureGuard { } } -describe('DsoPageAdministratorGuard', () => { +describe('DsoPageSingleFeatureGuard', () => { let guard: DsoPageSingleFeatureGuard; let authorizationService: AuthorizationDataService; let router: Router; @@ -62,7 +62,7 @@ describe('DsoPageAdministratorGuard', () => { }, parent: parentRoute }; - guard = new DsoPageFeatureGuardImpl(resolver, authorizationService, router, authService, undefined); + guard = new DsoPageSingleFeatureGuardImpl(resolver, authorizationService, router, authService, undefined); } beforeEach(() => { diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.spec.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.spec.ts new file mode 100644 index 0000000000..071b1b0731 --- /dev/null +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.spec.ts @@ -0,0 +1,87 @@ +import { AuthorizationDataService } from '../authorization-data.service'; +import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router'; +import { RemoteData } from '../../remote-data'; +import { Observable, of as observableOf } from 'rxjs'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; +import { DSpaceObject } from '../../../shared/dspace-object.model'; +import { FeatureID } from '../feature-id'; +import { AuthService } from '../../../auth/auth.service'; +import { DsoPageSomeFeatureGuard } from './dso-page-some-feature.guard'; + +/** + * Test implementation of abstract class DsoPageSomeFeatureGuard + */ +class DsoPageSomeFeatureGuardImpl extends DsoPageSomeFeatureGuard { + constructor(protected resolver: Resolve>, + protected authorizationService: AuthorizationDataService, + protected router: Router, + protected authService: AuthService, + protected featureIDs: FeatureID[]) { + super(resolver, authorizationService, router, authService); + } + + getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf(this.featureIDs); + } +} + +describe('DsoPageSomeFeatureGuard', () => { + let guard: DsoPageSomeFeatureGuard; + let authorizationService: AuthorizationDataService; + let router: Router; + let authService: AuthService; + let resolver: Resolve>; + let object: DSpaceObject; + let route; + let parentRoute; + + function init() { + object = { + self: 'test-selflink' + } as DSpaceObject; + + authorizationService = jasmine.createSpyObj('authorizationService', { + isAuthorized: observableOf(true) + }); + router = jasmine.createSpyObj('router', { + parseUrl: {} + }); + resolver = jasmine.createSpyObj('resolver', { + resolve: createSuccessfulRemoteDataObject$(object) + }); + authService = jasmine.createSpyObj('authService', { + isAuthenticated: observableOf(true) + }); + parentRoute = { + params: { + id: '3e1a5327-dabb-41ff-af93-e6cab9d032f0' + } + }; + route = { + params: { + }, + parent: parentRoute + }; + guard = new DsoPageSomeFeatureGuardImpl(resolver, authorizationService, router, authService, []); + } + + beforeEach(() => { + init(); + }); + + describe('getObjectUrl', () => { + it('should return the resolved object\'s selflink', (done) => { + guard.getObjectUrl(route, undefined).subscribe((selflink) => { + expect(selflink).toEqual(object.self); + done(); + }); + }); + }); + + describe('getRouteWithDSOId', () => { + it('should return the route that has the UUID of the DSO', () => { + const foundRoute = (guard as any).getRouteWithDSOId(route); + expect(foundRoute).toBe(parentRoute); + }); + }); +}); diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.ts index 7b7cb4c196..8683709345 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.ts @@ -10,7 +10,7 @@ import { hasNoValue, hasValue } from '../../../../shared/empty.util'; import { SomeFeatureAuthorizationGuard } from './some-feature-authorization.guard'; /** - * Abstract Guard for preventing unauthorized access to {@link DSpaceObject} pages that require rights for a specific feature + * Abstract Guard for preventing unauthorized access to {@link DSpaceObject} pages that require rights for any specific feature in a list * This guard utilizes a resolver to retrieve the relevant object to check authorizations for */ export abstract class DsoPageSomeFeatureGuard extends SomeFeatureAuthorizationGuard { diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.spec.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.spec.ts index 1fa5498f12..635aa3530b 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.spec.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.spec.ts @@ -6,10 +6,10 @@ import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/ro import { AuthService } from '../../../auth/auth.service'; /** - * Test implementation of abstract class FeatureAuthorizationGuard + * Test implementation of abstract class SingleFeatureAuthorizationGuard * Provide the return values of the overwritten getters as constructor arguments */ -class FeatureAuthorizationGuardImpl extends SingleFeatureAuthorizationGuard { +class SingleFeatureAuthorizationGuardImpl extends SingleFeatureAuthorizationGuard { constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService, @@ -32,7 +32,7 @@ class FeatureAuthorizationGuardImpl extends SingleFeatureAuthorizationGuard { } } -describe('FeatureAuthorizationGuard', () => { +describe('SingleFeatureAuthorizationGuard', () => { let guard: SingleFeatureAuthorizationGuard; let authorizationService: AuthorizationDataService; let router: Router; @@ -56,7 +56,7 @@ describe('FeatureAuthorizationGuard', () => { authService = jasmine.createSpyObj('authService', { isAuthenticated: observableOf(true) }); - guard = new FeatureAuthorizationGuardImpl(authorizationService, router, authService, featureId, objectUrl, ePersonUuid); + guard = new SingleFeatureAuthorizationGuardImpl(authorizationService, router, authService, featureId, objectUrl, ePersonUuid); } beforeEach(() => { diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.spec.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.spec.ts new file mode 100644 index 0000000000..90153d2d14 --- /dev/null +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.spec.ts @@ -0,0 +1,110 @@ +import { AuthorizationDataService } from '../authorization-data.service'; +import { FeatureID } from '../feature-id'; +import { Observable, of as observableOf } from 'rxjs'; +import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; +import { AuthService } from '../../../auth/auth.service'; +import { SomeFeatureAuthorizationGuard } from './some-feature-authorization.guard'; + +/** + * Test implementation of abstract class SomeFeatureAuthorizationGuard + * Provide the return values of the overwritten getters as constructor arguments + */ +class SomeFeatureAuthorizationGuardImpl extends SomeFeatureAuthorizationGuard { + constructor(protected authorizationService: AuthorizationDataService, + protected router: Router, + protected authService: AuthService, + protected featureIds: FeatureID[], + protected objectUrl: string, + protected ePersonUuid: string) { + super(authorizationService, router, authService); + } + + getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf(this.featureIds); + } + + getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf(this.objectUrl); + } + + getEPersonUuid(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf(this.ePersonUuid); + } +} + +describe('SomeFeatureAuthorizationGuard', () => { + let guard: SomeFeatureAuthorizationGuard; + let authorizationService: AuthorizationDataService; + let router: Router; + let authService: AuthService; + + let featureIds: FeatureID[]; + let authorizedFeatureIds: FeatureID[]; + let objectUrl: string; + let ePersonUuid: string; + + function init() { + featureIds = [FeatureID.LoginOnBehalfOf, FeatureID.CanDelete]; + authorizedFeatureIds = []; + objectUrl = 'fake-object-url'; + ePersonUuid = 'fake-eperson-uuid'; + + authorizationService = Object.assign({ + isAuthorized(featureId?: FeatureID): Observable { + return observableOf(authorizedFeatureIds.indexOf(featureId) > -1); + } + }); + router = jasmine.createSpyObj('router', { + parseUrl: {} + }); + authService = jasmine.createSpyObj('authService', { + isAuthenticated: observableOf(true) + }); + guard = new SomeFeatureAuthorizationGuardImpl(authorizationService, router, authService, featureIds, objectUrl, ePersonUuid); + } + + beforeEach(() => { + init(); + }); + + describe('canActivate', () => { + describe('when the user isn\'t authorized', () => { + beforeEach(() => { + authorizedFeatureIds = []; + }); + + it('should not return true', (done) => { + guard.canActivate(undefined, { url: 'current-url' } as any).subscribe((result) => { + expect(result).not.toEqual(true); + done(); + }); + }); + }); + + describe('when the user is authorized for at least one of the guard\'s features', () => { + beforeEach(() => { + authorizedFeatureIds = [featureIds[0]]; + }); + + it('should return true', (done) => { + guard.canActivate(undefined, { url: 'current-url' } as any).subscribe((result) => { + expect(result).toEqual(true); + done(); + }); + }); + }); + + describe('when the user is authorized for all of the guard\'s features', () => { + beforeEach(() => { + authorizedFeatureIds = featureIds; + }); + + it('should return true', (done) => { + guard.canActivate(undefined, { url: 'current-url' } as any).subscribe((result) => { + expect(result).toEqual(true); + done(); + }); + }); + }); + }); +}); diff --git a/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-delete-none-response.json b/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-delete-none-response.json new file mode 100644 index 0000000000..51968bd5da --- /dev/null +++ b/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-delete-none-response.json @@ -0,0 +1,13 @@ +{ + "_links": { + "self": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/96715576-3748-4761-ad45-001646632963&feature=canDelete" + } + }, + "page": { + "size": 20, + "totalElements": 0, + "totalPages": 1, + "number": 0 + } +} diff --git a/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-move-response.json b/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-move-response.json new file mode 100644 index 0000000000..692751d671 --- /dev/null +++ b/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-move-response.json @@ -0,0 +1,50 @@ +{ + "_embedded": { + "authorizations": [ + { + "id": "cd824a61-95be-4e16-bccd-51fea26707d0_canMove_core.item_96715576-3748-4761-ad45-001646632963", + "type": "authorization", + "_links": { + "eperson": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canMove_core.item_96715576-3748-4761-ad45-001646632963/eperson" + }, + "feature": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canMove_core.item_96715576-3748-4761-ad45-001646632963/feature" + }, + "object": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canMove_core.item_96715576-3748-4761-ad45-001646632963/object" + }, + "self": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canMove_core.item_96715576-3748-4761-ad45-001646632963" + } + }, + "_embedded": { + "feature": { + "id": "canMove", + "description": "It can be used to verify if the user is allowed to move items", + "type": "feature", + "resourcetypes": [ + "core.item" + ], + "_links": { + "self": { + "href": "https://api7.dspace.org/server/api/authz/features/canMove" + } + } + } + } + } + ] + }, + "_links": { + "self": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/96715576-3748-4761-ad45-001646632963&feature=canMove" + } + }, + "page": { + "size": 20, + "totalElements": 1, + "totalPages": 1, + "number": 0 + } +} diff --git a/src/app/shared/mocks/dspace-rest/mocks/response-map.mock.ts b/src/app/shared/mocks/dspace-rest/mocks/response-map.mock.ts index 4cc5c39b7d..f276c24bbf 100644 --- a/src/app/shared/mocks/dspace-rest/mocks/response-map.mock.ts +++ b/src/app/shared/mocks/dspace-rest/mocks/response-map.mock.ts @@ -3,6 +3,8 @@ import { InjectionToken } from '@angular/core'; // import mockPublicationResponse from './mock-publication-response.json'; // import mockUntypedItemResponse from './mock-untyped-item-response.json'; import mockFeatureItemCanManageBitstreamsResponse from './mock-feature-item-can-manage-bitstreams-response.json'; +import mockFeatureItemCanMoveResponse from './mock-feature-item-can-move-response.json'; +import mockFeatureItemCanDeleteNoneResponse from './mock-feature-item-can-delete-none-response.json'; export class ResponseMapMock extends Map {} @@ -18,4 +20,6 @@ export const mockResponseMap: ResponseMapMock = new Map([ // [ '/api/pid/find', mockPublicationResponse ], // [ '/api/pid/find', mockUntypedItemResponse ], [ 'https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/96715576-3748-4761-ad45-001646632963&feature=canManageBitstreams&embed=feature', mockFeatureItemCanManageBitstreamsResponse ], + [ 'https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/96715576-3748-4761-ad45-001646632963&feature=canMove&embed=feature', mockFeatureItemCanMoveResponse ], + [ 'https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/96715576-3748-4761-ad45-001646632963&feature=canDelete&embed=feature', mockFeatureItemCanDeleteNoneResponse ], ]); From 5fca681222f349cb50d0108f336905816a449d72 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 7 Apr 2021 17:07:26 +0200 Subject: [PATCH 009/351] 78001: RelationshipEffects test fixes --- src/app/core/shared/operators.ts | 8 +- .../relationship.effects.spec.ts | 136 ++++++++---------- .../relationship.effects.ts | 9 +- 3 files changed, 72 insertions(+), 81 deletions(-) diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index dd610b6ca7..5cef7e5e09 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -1,6 +1,6 @@ import { Router, UrlTree } from '@angular/router'; import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; -import { filter, find, map, mergeMap, switchMap, take, takeWhile, tap } from 'rxjs/operators'; +import { debounceTime, filter, find, map, mergeMap, switchMap, take, takeWhile, tap } from 'rxjs/operators'; import { hasNoValue, hasValue, hasValueOperator, isNotEmpty } from '../../shared/empty.util'; import { SearchResult } from '../../shared/search/search-result.model'; import { PaginatedList } from '../data/paginated-list.model'; @@ -15,6 +15,12 @@ import { DSpaceObject } from './dspace-object.model'; import { getForbiddenRoute, getPageNotFoundRoute } from '../../app-routing-paths'; import { getEndUserAgreementPath } from '../../info/info-routing-paths'; import { AuthService } from '../auth/auth.service'; +import { InjectionToken } from '@angular/core'; + +export const DEBOUNCE_TIME_OPERATOR = new InjectionToken<(dueTime: number) => (source: Observable) => Observable>('debounceTime', { + providedIn: 'root', + factory: () => debounceTime +}); /** * This file contains custom RxJS operators that can be used in multiple places diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.spec.ts index 5e2ffd30fb..e988eba9eb 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.spec.ts @@ -1,10 +1,7 @@ import { TestBed, waitForAsync } from '@angular/core/testing'; - import { BehaviorSubject, Observable, of as observableOf } from 'rxjs'; -import { TestScheduler } from 'rxjs/testing'; import { provideMockActions } from '@ngrx/effects/testing'; import { Store } from '@ngrx/store'; - import { RelationshipEffects } from './relationship.effects'; import { AddRelationshipAction, RelationshipActionTypes, RemoveRelationshipAction } from './relationship.actions'; import { Item } from '../../../../../core/shared/item.model'; @@ -23,6 +20,9 @@ import { RequestService } from '../../../../../core/data/request.service'; import { NotificationsService } from '../../../../notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; import { SelectableListService } from '../../../../object-list/selectable-list/selectable-list.service'; +import { cold, hot } from 'jasmine-marbles'; +import { DEBOUNCE_TIME_OPERATOR } from '../../../../../core/shared/operators'; +import { last } from 'rxjs/operators'; describe('RelationshipEffects', () => { let relationEffects: RelationshipEffects; @@ -51,7 +51,6 @@ describe('RelationshipEffects', () => { let notificationsService; let translateService; let selectableListService; - let testScheduler: TestScheduler; function init() { testUUID1 = '20e24c2f-a00a-467c-bdee-c929e79bf08d'; @@ -131,6 +130,7 @@ describe('RelationshipEffects', () => { { provide: NotificationsService, useValue: notificationsService }, { provide: TranslateService, useValue: translateService }, { provide: SelectableListService, useValue: selectableListService }, + { provide: DEBOUNCE_TIME_OPERATOR, useValue: jasmine.createSpy('debounceTime').and.returnValue((v) => v.pipe(last())) }, ], }); })); @@ -140,9 +140,6 @@ describe('RelationshipEffects', () => { identifier = (relationEffects as any).createIdentifier(leftItem, rightItem, relationshipType.leftwardType); spyOn((relationEffects as any), 'addRelationship').and.stub(); spyOn((relationEffects as any), 'removeRelationship').and.stub(); - testScheduler = new TestScheduler((actual, expected) => { - expect(actual).toEqual(expected); - }); }); describe('mapLastActions$', () => { @@ -151,15 +148,13 @@ describe('RelationshipEffects', () => { let action; it('should set the current value debounceMap and the value of the initialActionMap to ADD_RELATIONSHIP', () => { - testScheduler.run(({ hot, expectObservable, flush }) => { - action = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); - actions = hot('--a-|', { a: action }); - expectObservable(relationEffects.mapLastActions$).toBe('--b-|', { b: undefined }); - flush(); - // TODO check following expectations with the implementation - // expect((relationEffects as any).initialActionMap[identifier]).toBe(action.type); - // expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type); - }); + action = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); + actions = hot('--a-|', { a: action }); + const expected = cold('--b-|', { b: undefined }); + expect(relationEffects.mapLastActions$).toBeObservable(expected); + + expect((relationEffects as any).initialActionMap[identifier]).toBe(action.type); + expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type); }); }); @@ -172,14 +167,14 @@ describe('RelationshipEffects', () => { }); it('should set the current value debounceMap to ADD_RELATIONSHIP but not change the value of the initialActionMap', () => { - testScheduler.run(({ hot, expectObservable, flush }) => { - action = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); - actions = hot('--a-|', { a: action }); - expectObservable(relationEffects.mapLastActions$).toBe('--b-|', { b: undefined }); - flush(); - expect((relationEffects as any).initialActionMap[identifier]).toBe(testActionType); - expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type); - }); + action = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); + actions = hot('--a-|', { a: action }); + + const expected = cold('--b-|', { b: undefined }); + expect(relationEffects.mapLastActions$).toBeObservable(expected); + + expect((relationEffects as any).initialActionMap[identifier]).toBe(testActionType); + expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type); }); }); @@ -188,30 +183,26 @@ describe('RelationshipEffects', () => { describe('When the last value in the debounceMap is also an ADD_RELATIONSHIP action', () => { beforeEach(() => { (relationEffects as any).initialActionMap[identifier] = RelationshipActionTypes.ADD_RELATIONSHIP; + ((relationEffects as any).debounceTime as jasmine.Spy).and.returnValue((v) => v); }); it('should call addRelationship on the effect', () => { - testScheduler.run(({ hot, expectObservable, flush }) => { - action = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); - actions = hot('--a-|', { a: action }); - expectObservable(relationEffects.mapLastActions$).toBe('--b-|', { b: undefined }); - flush(); - expect((relationEffects as any).addRelationship).toHaveBeenCalledWith(leftItem, rightItem, relationshipType.leftwardType, '1234', undefined); - }); + action = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); + actions = hot('--a-|', { a: action }); + const expected = cold('--b-|', { b: undefined }); + expect(relationEffects.mapLastActions$).toBeObservable(expected); + expect((relationEffects as any).addRelationship).toHaveBeenCalledWith(leftItem, rightItem, relationshipType.leftwardType, '1234', undefined); }); }); describe('When the last value in the debounceMap is instead a REMOVE_RELATIONSHIP action', () => { - it('should not call removeRelationship or addRelationship on the effect', () => { - testScheduler.run(({ hot, expectObservable, flush }) => { - const actiona = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); - const actionb = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); - actions = hot('--ab-|', { a: actiona, b: actionb }); - expectObservable(relationEffects.mapLastActions$).toBe('--bb-|', { b: undefined }); - flush(); - expect((relationEffects as any).addRelationship).not.toHaveBeenCalled(); - expect((relationEffects as any).removeRelationship).not.toHaveBeenCalled(); - }); + const actiona = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); + const actionb = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); + actions = hot('--ab-|', { a: actiona, b: actionb }); + const expected = cold('--bb-|', { b: undefined }); + expect(relationEffects.mapLastActions$).toBeObservable(expected); + expect((relationEffects as any).addRelationship).not.toHaveBeenCalled(); + expect((relationEffects as any).removeRelationship).not.toHaveBeenCalled(); }); }); }); @@ -222,15 +213,13 @@ describe('RelationshipEffects', () => { let action; it('should set the current value debounceMap and the value of the initialActionMap to REMOVE_RELATIONSHIP', () => { - testScheduler.run(({ hot, expectObservable, flush }) => { - action = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); - actions = hot('--a-|', { a: action }); - expectObservable(relationEffects.mapLastActions$).toBe('--b-|', { b: undefined }); - flush(); - // TODO check following expectations with the implementation - // expect((relationEffects as any).initialActionMap[identifier]).toBe(action.type); - // expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type); - }); + action = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); + actions = hot('--a-|', { a: action }); + const expected = cold('--b-|', { b: undefined }); + expect(relationEffects.mapLastActions$).toBeObservable(expected); + + expect((relationEffects as any).initialActionMap[identifier]).toBe(action.type); + expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type); }); }); @@ -238,20 +227,19 @@ describe('RelationshipEffects', () => { let action; const testActionType = 'TEST_TYPE'; beforeEach(() => { + ((relationEffects as any).debounceTime as jasmine.Spy).and.returnValue((v) => v); (relationEffects as any).initialActionMap[identifier] = testActionType; (relationEffects as any).debounceMap[identifier] = new BehaviorSubject(testActionType); }); it('should set the current value debounceMap to REMOVE_RELATIONSHIP but not change the value of the initialActionMap', () => { - testScheduler.run(({ hot, expectObservable, flush }) => { - action = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); - actions = hot('--a-|', { a: action }); - expectObservable(relationEffects.mapLastActions$).toBe('--b-|', { b: undefined }); - flush(); - // TODO check following expectations with the implementation - // expect((relationEffects as any).initialActionMap[identifier]).toBe(testActionType); - // expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type); - }); + action = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); + actions = hot('--a-|', { a: action }); + const expected = cold('--b-|', { b: undefined }); + expect(relationEffects.mapLastActions$).toBeObservable(expected); + + expect((relationEffects as any).initialActionMap[identifier]).toBe(testActionType); + expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type); }); }); @@ -259,32 +247,28 @@ describe('RelationshipEffects', () => { let action; describe('When the last value in the debounceMap is also an REMOVE_RELATIONSHIP action', () => { beforeEach(() => { + ((relationEffects as any).debounceTime as jasmine.Spy).and.returnValue((v) => v); (relationEffects as any).initialActionMap[identifier] = RelationshipActionTypes.REMOVE_RELATIONSHIP; }); it('should call removeRelationship on the effect', () => { - testScheduler.run(({ hot, expectObservable, flush }) => { - action = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); - actions = hot('a', { a: action }); - expectObservable(relationEffects.mapLastActions$).toBe('b', { b: undefined }); - flush(); - expect((relationEffects as any).removeRelationship).toHaveBeenCalledWith(leftItem, rightItem, relationshipType.leftwardType, '1234',); - }); + action = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); + actions = hot('--a-|', { a: action }); + const expected = cold('--b-|', { b: undefined }); + expect(relationEffects.mapLastActions$).toBeObservable(expected); + expect((relationEffects as any).removeRelationship).toHaveBeenCalledWith(leftItem, rightItem, relationshipType.leftwardType, '1234',); }); }); describe('When the last value in the debounceMap is instead a ADD_RELATIONSHIP action', () => { - it('should not call addRelationship or removeRelationship on the effect', () => { - testScheduler.run(({ hot, expectObservable, flush }) => { - const actionb = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); - const actiona = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); - actions = hot('--ab-|', { a: actiona, b: actionb }); - expectObservable(relationEffects.mapLastActions$).toBe('--bb-|', { b: undefined }); - flush(); - expect((relationEffects as any).addRelationship).not.toHaveBeenCalled(); - expect((relationEffects as any).removeRelationship).not.toHaveBeenCalled(); - }); + const actionb = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); + const actiona = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType, '1234'); + actions = hot('--ab-|', { a: actiona, b: actionb }); + const expected = cold('--bb-|', { b: undefined }); + expect(relationEffects.mapLastActions$).toBeObservable(expected); + expect((relationEffects as any).addRelationship).not.toHaveBeenCalled(); + expect((relationEffects as any).removeRelationship).not.toHaveBeenCalled(); }); }); }); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts index 5a20ed8b51..a810cb0ad9 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts @@ -1,11 +1,11 @@ -import { Injectable } from '@angular/core'; +import { Inject, Injectable } from '@angular/core'; import { Actions, Effect, ofType } from '@ngrx/effects'; -import { debounceTime, filter, map, mergeMap, switchMap, take } from 'rxjs/operators'; +import { filter, map, mergeMap, switchMap, take } from 'rxjs/operators'; import { BehaviorSubject, Observable } from 'rxjs'; import { RelationshipService } from '../../../../../core/data/relationship.service'; import { getRemoteDataPayload, - getFirstSucceededRemoteData + getFirstSucceededRemoteData, DEBOUNCE_TIME_OPERATOR } from '../../../../../core/shared/operators'; import { AddRelationshipAction, @@ -71,7 +71,7 @@ export class RelationshipEffects { this.initialActionMap[identifier] = action.type; this.debounceMap[identifier] = new BehaviorSubject(action.type); this.debounceMap[identifier].pipe( - debounceTime(DEBOUNCE_TIME), + this.debounceTime(DEBOUNCE_TIME), take(1) ).subscribe( (type) => { @@ -159,6 +159,7 @@ export class RelationshipEffects { private notificationsService: NotificationsService, private translateService: TranslateService, private selectableListService: SelectableListService, + @Inject(DEBOUNCE_TIME_OPERATOR) private debounceTime: (dueTime: number) => (source: Observable) => Observable, ) { } From fd437eb7ee0481750520589c0ff94e74245638c4 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 16 Apr 2021 11:36:33 +0200 Subject: [PATCH 010/351] 78243: edit-item-page mocks, item-status operation refactoring --- .../item-status/item-status.component.ts | 27 +++++----- src/app/core/shared/operators.ts | 2 +- ...feature-item-can-delete-none-response.json | 13 ----- ...re-item-can-manage-mappings-response.json} | 18 +++---- ...tem-can-manage-relationships-response.json | 50 +++++++++++++++++++ ...ure-item-can-manage-versions-response.json | 50 +++++++++++++++++++ .../dspace-rest/mocks/response-map.mock.ts | 10 ++-- src/assets/i18n/en.json5 | 2 +- 8 files changed, 132 insertions(+), 40 deletions(-) delete mode 100644 src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-delete-none-response.json rename src/app/shared/mocks/dspace-rest/mocks/{mock-feature-item-can-move-response.json => mock-feature-item-can-manage-mappings-response.json} (56%) create mode 100644 src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-manage-relationships-response.json create mode 100644 src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-manage-versions-response.json diff --git a/src/app/+item-page/edit-item-page/item-status/item-status.component.ts b/src/app/+item-page/edit-item-page/item-status/item-status.component.ts index 51aa24ea6c..f95d2d1517 100644 --- a/src/app/+item-page/edit-item-page/item-status/item-status.component.ts +++ b/src/app/+item-page/edit-item-page/item-status/item-status.component.ts @@ -3,8 +3,8 @@ import { fadeIn, fadeInOut } from '../../../shared/animations/fade'; import { Item } from '../../../core/shared/item.model'; import { ActivatedRoute } from '@angular/router'; import { ItemOperation } from '../item-operation/itemOperation.model'; -import { distinctUntilChanged, first, map } from 'rxjs/operators'; -import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, of } from 'rxjs'; +import { distinctUntilChanged, first, map, mergeMap, toArray } from 'rxjs/operators'; +import { BehaviorSubject, Observable, from as observableFrom } from 'rxjs'; import { RemoteData } from '../../../core/data/remote-data'; import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-paths'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; @@ -95,16 +95,19 @@ export class ItemStatusComponent implements OnInit { this.operations$.next(operations); - observableCombineLatest(operations.map((operation) => { - if (hasValue(operation.featureID)) { - return this.authorizationService.isAuthorized(operation.featureID, item.self).pipe( - distinctUntilChanged(), - map((authorized) => new ItemOperation(operation.operationKey, operation.operationUrl, operation.featureID, !authorized, authorized)) - ); - } else { - return of(operation); - } - })).subscribe((ops) => this.operations$.next(ops)); + observableFrom(operations).pipe( + mergeMap((operation) => { + if (hasValue(operation.featureID)) { + return this.authorizationService.isAuthorized(operation.featureID, item.self).pipe( + distinctUntilChanged(), + map((authorized) => new ItemOperation(operation.operationKey, operation.operationUrl, operation.featureID, !authorized, authorized)) + ); + } else { + return [operation]; + } + }), + toArray() + ).subscribe((ops) => this.operations$.next(ops)); }); this.itemPageRoute$ = this.itemRD$.pipe( getAllSucceededRemoteDataPayload(), diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index 2d0ab70e2c..fc3f2651e1 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -216,7 +216,7 @@ export const returnForbiddenUrlTreeOrLoginOnAllFalse = (router: Router, authServ (source: Observable): Observable => observableCombineLatest(source, authService.isAuthenticated()).pipe( map(([authorizedList, authenticated]: [boolean[], boolean]) => { - if (authorizedList.indexOf(true) > -1) { + if (authorizedList.some((b: boolean) => b === true)) { return true; } else { if (authenticated) { diff --git a/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-delete-none-response.json b/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-delete-none-response.json deleted file mode 100644 index 51968bd5da..0000000000 --- a/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-delete-none-response.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "_links": { - "self": { - "href": "https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/96715576-3748-4761-ad45-001646632963&feature=canDelete" - } - }, - "page": { - "size": 20, - "totalElements": 0, - "totalPages": 1, - "number": 0 - } -} diff --git a/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-move-response.json b/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-manage-mappings-response.json similarity index 56% rename from src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-move-response.json rename to src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-manage-mappings-response.json index 692751d671..c186ef8cc4 100644 --- a/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-move-response.json +++ b/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-manage-mappings-response.json @@ -2,33 +2,33 @@ "_embedded": { "authorizations": [ { - "id": "cd824a61-95be-4e16-bccd-51fea26707d0_canMove_core.item_96715576-3748-4761-ad45-001646632963", + "id": "cd824a61-95be-4e16-bccd-51fea26707d0_canManageMappings_core.item_e98b0f27-5c19-49a0-960d-eb6ad5287067", "type": "authorization", "_links": { "eperson": { - "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canMove_core.item_96715576-3748-4761-ad45-001646632963/eperson" + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageMappings_core.item_e98b0f27-5c19-49a0-960d-eb6ad5287067/eperson" }, "feature": { - "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canMove_core.item_96715576-3748-4761-ad45-001646632963/feature" + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageMappings_core.item_e98b0f27-5c19-49a0-960d-eb6ad5287067/feature" }, "object": { - "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canMove_core.item_96715576-3748-4761-ad45-001646632963/object" + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageMappings_core.item_e98b0f27-5c19-49a0-960d-eb6ad5287067/object" }, "self": { - "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canMove_core.item_96715576-3748-4761-ad45-001646632963" + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageMappings_core.item_e98b0f27-5c19-49a0-960d-eb6ad5287067" } }, "_embedded": { "feature": { - "id": "canMove", - "description": "It can be used to verify if the user is allowed to move items", + "id": "canManageMappings", + "description": "It can be used to verify if the mappings of the specified objects can be managed", "type": "feature", "resourcetypes": [ "core.item" ], "_links": { "self": { - "href": "https://api7.dspace.org/server/api/authz/features/canMove" + "href": "https://api7.dspace.org/server/api/authz/features/canManageMappings" } } } @@ -38,7 +38,7 @@ }, "_links": { "self": { - "href": "https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/96715576-3748-4761-ad45-001646632963&feature=canMove" + "href": "https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/e98b0f27-5c19-49a0-960d-eb6ad5287067&feature=canManageMappings" } }, "page": { diff --git a/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-manage-relationships-response.json b/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-manage-relationships-response.json new file mode 100644 index 0000000000..b6de452dd2 --- /dev/null +++ b/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-manage-relationships-response.json @@ -0,0 +1,50 @@ +{ + "_embedded": { + "authorizations": [ + { + "id": "cd824a61-95be-4e16-bccd-51fea26707d0_canManageRelationships_core.item_047556d1-3d01-4c53-bc68-0cee7ad7ed4e", + "type": "authorization", + "_links": { + "eperson": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageRelationships_core.item_047556d1-3d01-4c53-bc68-0cee7ad7ed4e/eperson" + }, + "feature": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageRelationships_core.item_047556d1-3d01-4c53-bc68-0cee7ad7ed4e/feature" + }, + "object": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageRelationships_core.item_047556d1-3d01-4c53-bc68-0cee7ad7ed4e/object" + }, + "self": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageRelationships_core.item_047556d1-3d01-4c53-bc68-0cee7ad7ed4e" + } + }, + "_embedded": { + "feature": { + "id": "canManageRelationships", + "description": "It can be used to verify if the relationships of the specified objects can be managed", + "type": "feature", + "resourcetypes": [ + "core.item" + ], + "_links": { + "self": { + "href": "https://api7.dspace.org/server/api/authz/features/canManageRelationships" + } + } + } + } + } + ] + }, + "_links": { + "self": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/047556d1-3d01-4c53-bc68-0cee7ad7ed4e&feature=canManageRelationships" + } + }, + "page": { + "size": 20, + "totalElements": 1, + "totalPages": 1, + "number": 0 + } +} diff --git a/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-manage-versions-response.json b/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-manage-versions-response.json new file mode 100644 index 0000000000..55eb69a7bf --- /dev/null +++ b/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-manage-versions-response.json @@ -0,0 +1,50 @@ +{ + "_embedded": { + "authorizations": [ + { + "id": "cd824a61-95be-4e16-bccd-51fea26707d0_canManageVersions_core.item_e98b0f27-5c19-49a0-960d-eb6ad5287067", + "type": "authorization", + "_links": { + "eperson": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageVersions_core.item_e98b0f27-5c19-49a0-960d-eb6ad5287067/eperson" + }, + "feature": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageVersions_core.item_e98b0f27-5c19-49a0-960d-eb6ad5287067/feature" + }, + "object": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageVersions_core.item_e98b0f27-5c19-49a0-960d-eb6ad5287067/object" + }, + "self": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageVersions_core.item_e98b0f27-5c19-49a0-960d-eb6ad5287067" + } + }, + "_embedded": { + "feature": { + "id": "canManageVersions", + "description": "It can be used to verify if the versions of the specified objects can be managed", + "type": "feature", + "resourcetypes": [ + "core.item" + ], + "_links": { + "self": { + "href": "https://api7.dspace.org/server/api/authz/features/canManageVersions" + } + } + } + } + } + ] + }, + "_links": { + "self": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/e98b0f27-5c19-49a0-960d-eb6ad5287067&feature=canManageVersions" + } + }, + "page": { + "size": 20, + "totalElements": 1, + "totalPages": 1, + "number": 0 + } +} diff --git a/src/app/shared/mocks/dspace-rest/mocks/response-map.mock.ts b/src/app/shared/mocks/dspace-rest/mocks/response-map.mock.ts index f276c24bbf..a746416e77 100644 --- a/src/app/shared/mocks/dspace-rest/mocks/response-map.mock.ts +++ b/src/app/shared/mocks/dspace-rest/mocks/response-map.mock.ts @@ -3,8 +3,9 @@ import { InjectionToken } from '@angular/core'; // import mockPublicationResponse from './mock-publication-response.json'; // import mockUntypedItemResponse from './mock-untyped-item-response.json'; import mockFeatureItemCanManageBitstreamsResponse from './mock-feature-item-can-manage-bitstreams-response.json'; -import mockFeatureItemCanMoveResponse from './mock-feature-item-can-move-response.json'; -import mockFeatureItemCanDeleteNoneResponse from './mock-feature-item-can-delete-none-response.json'; +import mockFeatureItemCanManageRelationshipsResponse from './mock-feature-item-can-manage-relationships-response.json'; +import mockFeatureItemCanManageVersionsResponse from './mock-feature-item-can-manage-versions-response.json'; +import mockFeatureItemCanManageMappingsResponse from './mock-feature-item-can-manage-mappings-response.json'; export class ResponseMapMock extends Map {} @@ -20,6 +21,7 @@ export const mockResponseMap: ResponseMapMock = new Map([ // [ '/api/pid/find', mockPublicationResponse ], // [ '/api/pid/find', mockUntypedItemResponse ], [ 'https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/96715576-3748-4761-ad45-001646632963&feature=canManageBitstreams&embed=feature', mockFeatureItemCanManageBitstreamsResponse ], - [ 'https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/96715576-3748-4761-ad45-001646632963&feature=canMove&embed=feature', mockFeatureItemCanMoveResponse ], - [ 'https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/96715576-3748-4761-ad45-001646632963&feature=canDelete&embed=feature', mockFeatureItemCanDeleteNoneResponse ], + [ 'https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/047556d1-3d01-4c53-bc68-0cee7ad7ed4e&feature=canManageRelationships&embed=feature', mockFeatureItemCanManageRelationshipsResponse ], + [ 'https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/e98b0f27-5c19-49a0-960d-eb6ad5287067&feature=canManageVersions&embed=feature', mockFeatureItemCanManageVersionsResponse ], + [ 'https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/e98b0f27-5c19-49a0-960d-eb6ad5287067&feature=canManageMappings&embed=feature', mockFeatureItemCanManageMappingsResponse ], ]); diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index abecfce291..818606372b 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1761,7 +1761,7 @@ "item.edit.tabs.status.buttons.reinstate.label": "Reinstate item into the repository", - "item.edit.tabs.status.buttons.unauthorized": "You don't have permission to perform this action", + "item.edit.tabs.status.buttons.unauthorized": "You're not authorized to perform this action", "item.edit.tabs.status.buttons.withdraw.button": "Withdraw...", From 25f02b99d323fe630d0b482fdf3a02597b9e20b0 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 16 Apr 2021 12:31:22 +0200 Subject: [PATCH 011/351] 78243: message change --- src/assets/i18n/en.json5 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 818606372b..600c884938 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1516,7 +1516,7 @@ "item.edit.breadcrumbs": "Edit Item", - "item.edit.tabs.disabled.tooltip": "You don't have permission to access this tab", + "item.edit.tabs.disabled.tooltip": "You're not authorized to access this tab", "item.edit.tabs.mapper.head": "Collection Mapper", From 2561d54b2dc68f8e407a9aec41cc23ff3bee458b Mon Sep 17 00:00:00 2001 From: Michael Spalti Date: Fri, 16 Apr 2021 08:46:20 -0700 Subject: [PATCH 012/351] Added comments --- .../mirador-viewer/mirador-viewer.component.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/app/entity-groups/iiif-entities/mirador-viewer/mirador-viewer.component.ts b/src/app/entity-groups/iiif-entities/mirador-viewer/mirador-viewer.component.ts index 8e41c93572..765804c00a 100644 --- a/src/app/entity-groups/iiif-entities/mirador-viewer/mirador-viewer.component.ts +++ b/src/app/entity-groups/iiif-entities/mirador-viewer/mirador-viewer.component.ts @@ -62,7 +62,7 @@ export class MiradorViewerComponent implements OnInit { if (this.notMobile) { viewerPath += '¬Mobile=true'; } - // TODO: review whether the item.id should be sanitized. The query term should be (check mirador viewer). + // TODO: review whether the item.id should be sanitized. The query term probably should be. return this.sanitizer.bypassSecurityTrustResourceUrl(viewerPath); } @@ -71,6 +71,7 @@ export class MiradorViewerComponent implements OnInit { * Initializes the iframe url observable. */ if (isPlatformBrowser(this.platformId)) { + // This will not be responsive to resizing. if (window.innerWidth > 768) { this.notMobile = true; } @@ -81,8 +82,9 @@ export class MiradorViewerComponent implements OnInit { map((bitstreamsRD: RemoteData>) => { if (hasValue(bitstreamsRD.payload)) { if (bitstreamsRD.payload.totalElements > 2) { - /* IIIF bundle contains multiple images. The IIIF bundle also contains - * a single json file so multi is true only when count is 3 or more . */ + /* IIIF bundle contains multiple images and optionally a + * a single json file, thus multi is true only when the count is 3 or more . + * multi=true enables the side navigation panel in Mirador. */ this.multi = true; } } From add2aac9349573bcbcbb3db3035ef3f7595a99e5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Apr 2021 15:41:33 +0000 Subject: [PATCH 013/351] Bump ssri from 6.0.1 to 6.0.2 Bumps [ssri](https://github.com/npm/ssri) from 6.0.1 to 6.0.2. - [Release notes](https://github.com/npm/ssri/releases) - [Changelog](https://github.com/npm/ssri/blob/v6.0.2/CHANGELOG.md) - [Commits](https://github.com/npm/ssri/compare/v6.0.1...v6.0.2) Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 29e0ee4648..7c8b54f47c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10792,9 +10792,9 @@ sshpk@^1.7.0: tweetnacl "~0.14.0" ssri@^6.0.0, ssri@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8" - integrity sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA== + version "6.0.2" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.2.tgz#157939134f20464e7301ddba3e90ffa8f7728ac5" + integrity sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q== dependencies: figgy-pudding "^3.5.1" From ed7454ffc9230b535f0a63d0b1f2972793935571 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Tue, 20 Apr 2021 09:37:28 +0200 Subject: [PATCH 014/351] Fix visibility issue with collapsed navbar menu --- .../header-nav-wrapper/header-navbar-wrapper.component.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app/header-nav-wrapper/header-navbar-wrapper.component.scss b/src/app/header-nav-wrapper/header-navbar-wrapper.component.scss index b297979fd0..a2ebd0d41a 100644 --- a/src/app/header-nav-wrapper/header-navbar-wrapper.component.scss +++ b/src/app/header-nav-wrapper/header-navbar-wrapper.component.scss @@ -5,3 +5,7 @@ position: sticky; } } + +:host { + z-index: var(--ds-nav-z-index); +} From a205aa02b3316d97c1bc9650987375f827cc79b2 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 15 Apr 2021 13:01:31 +0200 Subject: [PATCH 015/351] [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 016/351] [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 1aa659e6b7e9899bbccc6b4858a688d638163b19 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Fri, 23 Apr 2021 13:28:41 +0200 Subject: [PATCH 017/351] 78849: Fix cache/re-request issue & other improvements --- .../core/data/eperson-registration.service.ts | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/src/app/core/data/eperson-registration.service.ts b/src/app/core/data/eperson-registration.service.ts index fd55c031d8..b8614e62b5 100644 --- a/src/app/core/data/eperson-registration.service.ts +++ b/src/app/core/data/eperson-registration.service.ts @@ -3,7 +3,7 @@ import { RequestService } from './request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { GetRequest, PostRequest } from './request.models'; import { Observable } from 'rxjs'; -import { filter, find, map, take } from 'rxjs/operators'; +import { filter, find, map, skipWhile, take } from 'rxjs/operators'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { Registration } from '../shared/registration.model'; import { getFirstCompletedRemoteData, getFirstSucceededRemoteData } from '../shared/operators'; @@ -60,9 +60,9 @@ export class EpersonRegistrationService { const requestId = this.requestService.generateRequestId(); - const hrefObs = this.getRegistrationEndpoint(); + const href$ = this.getRegistrationEndpoint(); - hrefObs.pipe( + href$.pipe( find((href: string) => hasValue(href)), map((href: string) => { const request = new PostRequest(requestId, href, registration); @@ -82,27 +82,28 @@ export class EpersonRegistrationService { searchByToken(token: string): Observable { const requestId = this.requestService.generateRequestId(); - const hrefObs = this.getTokenSearchEndpoint(token); - - hrefObs.pipe( + const href$ = this.getTokenSearchEndpoint(token).pipe( find((href: string) => hasValue(href)), - map((href: string) => { - const request = new GetRequest(requestId, href); - Object.assign(request, { - getResponseParser(): GenericConstructor { - return RegistrationResponseParsingService; - } - }); - this.requestService.send(request, true); - }) - ).subscribe(); + ) - return this.rdbService.buildFromRequestUUID(requestId).pipe( + href$.subscribe((href: string) => { + const request = new GetRequest(requestId, href); + Object.assign(request, { + getResponseParser(): GenericConstructor { + return RegistrationResponseParsingService; + } + }); + this.requestService.send(request, true); + }); + + return this.rdbService.buildSingle(href$).pipe( + skipWhile((rd: RemoteData) => rd.isStale), getFirstSucceededRemoteData(), map((restResponse: RemoteData) => { - return Object.assign(new Registration(), {email: restResponse.payload.email, token: token, user: restResponse.payload.user}); + return Object.assign(new Registration(), { + email: restResponse.payload.email, token: token, user: restResponse.payload.user + }); }), - take(1), ); } From 7d0ea04b3e9d5b3bd75aa45ce70266aa2a4db5d1 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Fri, 23 Apr 2021 15:39:15 +0200 Subject: [PATCH 018/351] 78849: Add unit tests for EPerson registration caching --- .../data/eperson-registration.service.spec.ts | 61 +++++++++++++++++-- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/src/app/core/data/eperson-registration.service.spec.ts b/src/app/core/data/eperson-registration.service.spec.ts index fe3a1958eb..cffd3266e5 100644 --- a/src/app/core/data/eperson-registration.service.spec.ts +++ b/src/app/core/data/eperson-registration.service.spec.ts @@ -1,15 +1,18 @@ import { RequestService } from './request.service'; import { EpersonRegistrationService } from './eperson-registration.service'; import { RestResponse } from '../cache/response.models'; -import { RequestEntry } from './request.reducer'; +import { RequestEntry, RequestEntryState } from './request.reducer'; import { cold } from 'jasmine-marbles'; import { PostRequest } from './request.models'; import { Registration } from '../shared/registration.model'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; -import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; +import { createPendingRemoteDataObject, createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; import { of as observableOf } from 'rxjs/internal/observable/of'; +import { TestScheduler } from 'rxjs/testing'; + +fdescribe('EpersonRegistrationService', () => { + let testScheduler; -describe('EpersonRegistrationService', () => { let service: EpersonRegistrationService; let requestService: RequestService; @@ -29,6 +32,12 @@ describe('EpersonRegistrationService', () => { rd = createSuccessfulRemoteDataObject(registrationWithUser); halService = new HALEndpointServiceStub('rest-url'); + testScheduler = new TestScheduler((actual, expected) => { + // asserting the two objects are equal + // e.g. using chai. + expect(actual).toEqual(expected); + }); + requestService = jasmine.createSpyObj('requestService', { generateRequestId: 'request-id', send: {}, @@ -36,7 +45,8 @@ describe('EpersonRegistrationService', () => { { a: Object.assign(new RequestEntry(), { response: new RestResponse(true, 200, 'Success') }) }) }); rdbService = jasmine.createSpyObj('rdbService', { - buildFromRequestUUID: observableOf(rd) + buildSingle: observableOf(rd), + buildFromRequestUUID: observableOf(rd), }); service = new EpersonRegistrationService( requestService, @@ -88,6 +98,49 @@ describe('EpersonRegistrationService', () => { })); }); + + it('should return the original registration if it was already cached', () => { + testScheduler.run(({ cold, expectObservable }) => { + rdbService.buildSingle.and.returnValue(cold('a-b-c', { + a: createSuccessfulRemoteDataObject(registrationWithUser), + b: createPendingRemoteDataObject(), + c: createSuccessfulRemoteDataObject(new Registration()) + })); + + expectObservable( + service.searchByToken('test-token') + ).toBe('(a|)', { + a: Object.assign(new Registration(), { + email: registrationWithUser.email, + token: 'test-token', + user: registrationWithUser.user + }) + }); + }); + }); + + it('should re-request the registration if it was already cached but stale', () => { + const rdCachedStale = createSuccessfulRemoteDataObject(new Registration()); + rdCachedStale.state = RequestEntryState.SuccessStale; + + testScheduler.run(({ cold, expectObservable }) => { + rdbService.buildSingle.and.returnValue(cold('a-b-c', { + a: rdCachedStale, + b: createPendingRemoteDataObject(), + c: createSuccessfulRemoteDataObject(registrationWithUser), + })); + + expectObservable( + service.searchByToken('test-token') + ).toBe('----(c|)', { + c: Object.assign(new Registration(), { + email: registrationWithUser.email, + token: 'test-token', + user: registrationWithUser.user + }) + }); + }); + }); }); }); From 7c0d9acbf1bca7a340cf040d80b00a80322f1c1e Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Fri, 23 Apr 2021 16:46:44 +0200 Subject: [PATCH 019/351] [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 020/351] 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 021/351] 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 022/351] 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 023/351] 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 024/351] 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 025/351] 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 026/351] 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 027/351] 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 0fe199a97c05a05f5f26847f778ffeab8acdc99f Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Mon, 26 Apr 2021 16:49:07 +0200 Subject: [PATCH 028/351] 78849: Fix unit test --- .../data/eperson-registration.service.spec.ts | 55 +++++-------------- 1 file changed, 15 insertions(+), 40 deletions(-) diff --git a/src/app/core/data/eperson-registration.service.spec.ts b/src/app/core/data/eperson-registration.service.spec.ts index cffd3266e5..cebe7ffa80 100644 --- a/src/app/core/data/eperson-registration.service.spec.ts +++ b/src/app/core/data/eperson-registration.service.spec.ts @@ -1,16 +1,16 @@ import { RequestService } from './request.service'; import { EpersonRegistrationService } from './eperson-registration.service'; import { RestResponse } from '../cache/response.models'; -import { RequestEntry, RequestEntryState } from './request.reducer'; +import { RequestEntry } from './request.reducer'; import { cold } from 'jasmine-marbles'; import { PostRequest } from './request.models'; import { Registration } from '../shared/registration.model'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; -import { createPendingRemoteDataObject, createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; +import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; import { of as observableOf } from 'rxjs/internal/observable/of'; import { TestScheduler } from 'rxjs/testing'; -fdescribe('EpersonRegistrationService', () => { +describe('EpersonRegistrationService', () => { let testScheduler; let service: EpersonRegistrationService; @@ -96,51 +96,26 @@ fdescribe('EpersonRegistrationService', () => { user: registrationWithUser.user }) })); - }); - it('should return the original registration if it was already cached', () => { + it('should use cached responses and /registrations/search/findByToken?', () => { testScheduler.run(({ cold, expectObservable }) => { - rdbService.buildSingle.and.returnValue(cold('a-b-c', { - a: createSuccessfulRemoteDataObject(registrationWithUser), - b: createPendingRemoteDataObject(), - c: createSuccessfulRemoteDataObject(new Registration()) - })); + rdbService.buildSingle.and.returnValue(cold('a', { a: rd })); - expectObservable( - service.searchByToken('test-token') - ).toBe('(a|)', { - a: Object.assign(new Registration(), { - email: registrationWithUser.email, - token: 'test-token', - user: registrationWithUser.user - }) + service.searchByToken('test-token'); + + expect(requestService.send).toHaveBeenCalledWith( + jasmine.objectContaining({ + uuid: 'request-id', method: 'GET', + href: 'rest-url/registrations/search/findByToken?token=test-token', + }), true + ); + expectObservable(rdbService.buildSingle.calls.argsFor(0)[0]).toBe('(a|)', { + a: 'rest-url/registrations/search/findByToken?token=test-token' }); }); }); - it('should re-request the registration if it was already cached but stale', () => { - const rdCachedStale = createSuccessfulRemoteDataObject(new Registration()); - rdCachedStale.state = RequestEntryState.SuccessStale; - - testScheduler.run(({ cold, expectObservable }) => { - rdbService.buildSingle.and.returnValue(cold('a-b-c', { - a: rdCachedStale, - b: createPendingRemoteDataObject(), - c: createSuccessfulRemoteDataObject(registrationWithUser), - })); - - expectObservable( - service.searchByToken('test-token') - ).toBe('----(c|)', { - c: Object.assign(new Registration(), { - email: registrationWithUser.email, - token: 'test-token', - user: registrationWithUser.user - }) - }); - }); - }); }); }); From bdc2dd5f9ca98ddb0e89371e02201cdb5ef1d94a Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Tue, 27 Apr 2021 09:53:38 +0200 Subject: [PATCH 029/351] [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 030/351] [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 d2b44318fa05cd31ee4b84e9333a7cb2e6eb91d2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 28 Apr 2021 21:57:44 +0000 Subject: [PATCH 031/351] Bump elliptic from 6.5.3 to 6.5.4 Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.3 to 6.5.4. - [Release notes](https://github.com/indutny/elliptic/releases) - [Commits](https://github.com/indutny/elliptic/compare/v6.5.3...v6.5.4) Signed-off-by: dependabot[bot] --- yarn.lock | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/yarn.lock b/yarn.lock index 29e0ee4648..6428d3b153 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2440,10 +2440,10 @@ bluebird@^3.5.1, bluebird@^3.5.3, bluebird@^3.5.5: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0: - version "4.11.9" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828" - integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw== +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9: + version "4.12.0" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" + integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== bn.js@^5.0.0, bn.js@^5.1.1: version "5.1.3" @@ -2533,7 +2533,7 @@ braces@^3.0.1, braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" -brorand@^1.0.1: +brorand@^1.0.1, brorand@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= @@ -4370,17 +4370,17 @@ electron-to-chromium@^1.3.621: integrity sha512-AJT0Fm1W0uZlMVVkkJrcCVvczDuF8tPm3bwzQf5WO8AaASB2hwTRP7B8pU5rqjireH+ib6am8+hH5/QkXzzYKw== elliptic@^6.5.3: - version "6.5.3" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" - integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw== + version "6.5.4" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" + integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== dependencies: - bn.js "^4.4.0" - brorand "^1.0.1" + bn.js "^4.11.9" + brorand "^1.1.0" hash.js "^1.0.0" - hmac-drbg "^1.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.0" + hmac-drbg "^1.0.1" + inherits "^2.0.4" + minimalistic-assert "^1.0.1" + minimalistic-crypto-utils "^1.0.1" emoji-regex@^7.0.1: version "7.0.3" @@ -5557,7 +5557,7 @@ hex-color-regex@^1.1.0: resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== -hmac-drbg@^1.0.0: +hmac-drbg@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE= @@ -7393,7 +7393,7 @@ minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== -minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: +minimalistic-crypto-utils@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= From 781a88bc4c2a4478a99e31cb468486611e2e0289 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Thu, 29 Apr 2021 13:29:18 +0200 Subject: [PATCH 032/351] 78849: Fix double notification on submit --- .../forgot-password-form/forgot-password-form.component.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/forgot-password/forgot-password-form/forgot-password-form.component.ts b/src/app/forgot-password/forgot-password-form/forgot-password-form.component.ts index 98188a07e8..707c70f19c 100644 --- a/src/app/forgot-password/forgot-password-form/forgot-password-form.component.ts +++ b/src/app/forgot-password/forgot-password-form/forgot-password-form.component.ts @@ -11,6 +11,7 @@ import { Store } from '@ngrx/store'; import { CoreState } from '../../core/core.reducers'; import { RemoteData } from '../../core/data/remote-data'; import { EPerson } from '../../core/eperson/models/eperson.model'; +import { getFirstCompletedRemoteData } from '../../core/shared/operators'; @Component({ selector: 'ds-forgot-password-form', @@ -70,7 +71,9 @@ export class ForgotPasswordFormComponent { */ submit() { if (!this.isInValid) { - this.ePersonDataService.patchPasswordWithToken(this.user, this.token, this.password).subscribe((response: RemoteData) => { + this.ePersonDataService.patchPasswordWithToken(this.user, this.token, this.password).pipe( + getFirstCompletedRemoteData() + ).subscribe((response: RemoteData) => { if (response.hasSucceeded) { this.notificationsService.success( this.translateService.instant(this.NOTIFICATIONS_PREFIX + '.success.title'), From 032231f10e563cf7a898ebdf731385c08fd48c49 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Thu, 29 Apr 2021 13:57:31 +0200 Subject: [PATCH 033/351] 78849: Fix lint issues --- src/app/core/data/eperson-registration.service.spec.ts | 5 +++-- src/app/core/data/eperson-registration.service.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/app/core/data/eperson-registration.service.spec.ts b/src/app/core/data/eperson-registration.service.spec.ts index cebe7ffa80..2860880803 100644 --- a/src/app/core/data/eperson-registration.service.spec.ts +++ b/src/app/core/data/eperson-registration.service.spec.ts @@ -99,8 +99,8 @@ describe('EpersonRegistrationService', () => { }); it('should use cached responses and /registrations/search/findByToken?', () => { - testScheduler.run(({ cold, expectObservable }) => { - rdbService.buildSingle.and.returnValue(cold('a', { a: rd })); + testScheduler.run(({ tscold, expectObservable }) => { + rdbService.buildSingle.and.returnValue(tscold('a', { a: rd })); service.searchByToken('test-token'); @@ -119,3 +119,4 @@ describe('EpersonRegistrationService', () => { }); }); +/**/ diff --git a/src/app/core/data/eperson-registration.service.ts b/src/app/core/data/eperson-registration.service.ts index b8614e62b5..8ea43b3c3f 100644 --- a/src/app/core/data/eperson-registration.service.ts +++ b/src/app/core/data/eperson-registration.service.ts @@ -84,7 +84,7 @@ export class EpersonRegistrationService { const href$ = this.getTokenSearchEndpoint(token).pipe( find((href: string) => hasValue(href)), - ) + ); href$.subscribe((href: string) => { const request = new GetRequest(requestId, href); From a24cfe4cc74d5988bf18886d93699baeb4e40d36 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 29 Apr 2021 14:15:03 +0200 Subject: [PATCH 034/351] 78243: Feedback 2021-04-29 --- src/app/+item-page/edit-item-page/item-page-bitstreams.guard.ts | 2 +- .../edit-item-page/item-status/item-status.component.ts | 2 +- src/app/core/data/feature-authorization/feature-id.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/+item-page/edit-item-page/item-page-bitstreams.guard.ts b/src/app/+item-page/edit-item-page/item-page-bitstreams.guard.ts index bf4a6dc681..764c6ac7c8 100644 --- a/src/app/+item-page/edit-item-page/item-page-bitstreams.guard.ts +++ b/src/app/+item-page/edit-item-page/item-page-bitstreams.guard.ts @@ -26,6 +26,6 @@ export class ItemPageBitstreamsGuard extends DsoPageSingleFeatureGuard { * Check manage bitstreams authorization rights */ getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return observableOf(FeatureID.CanManageBitstreams); + return observableOf(FeatureID.CanManageBitstreamBundles); } } diff --git a/src/app/+item-page/edit-item-page/item-status/item-status.component.ts b/src/app/+item-page/edit-item-page/item-status/item-status.component.ts index f95d2d1517..f01f5c1f7a 100644 --- a/src/app/+item-page/edit-item-page/item-status/item-status.component.ts +++ b/src/app/+item-page/edit-item-page/item-status/item-status.component.ts @@ -83,7 +83,7 @@ export class ItemStatusComponent implements OnInit { if (item.isWithdrawn) { operations.push(new ItemOperation('reinstate', this.getCurrentUrl(item) + '/reinstate', FeatureID.ReinstateItem, true)); } else { - operations.push(new ItemOperation('reinstate', this.getCurrentUrl(item) + '/reinstate', FeatureID.WithdrawItem, true)); + operations.push(new ItemOperation('withdraw', this.getCurrentUrl(item) + '/withdraw', FeatureID.WithdrawItem, true)); } if (item.isDiscoverable) { operations.push(new ItemOperation('private', this.getCurrentUrl(item) + '/private', FeatureID.CanMakePrivate, true)); diff --git a/src/app/core/data/feature-authorization/feature-id.ts b/src/app/core/data/feature-authorization/feature-id.ts index 9a3d13183b..6d070fcd4c 100644 --- a/src/app/core/data/feature-authorization/feature-id.ts +++ b/src/app/core/data/feature-authorization/feature-id.ts @@ -14,7 +14,7 @@ export enum FeatureID { IsCommunityAdmin = 'isCommunityAdmin', CanDownload = 'canDownload', CanManageVersions = 'canManageVersions', - CanManageBitstreams = 'canManageBitstreams', + CanManageBitstreamBundles = 'canManageBitstreamBundles', CanManageRelationships = 'canManageRelationships', CanManageMappings = 'canManageMappings', CanManagePolicies = 'canManagePolicies', From b2b077868e46b73636c7cbdcc99e816b475a0ed8 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Thu, 29 Apr 2021 14:26:33 +0200 Subject: [PATCH 035/351] 78994: Disable no-shadowed-variable --- src/app/core/data/eperson-registration.service.spec.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/core/data/eperson-registration.service.spec.ts b/src/app/core/data/eperson-registration.service.spec.ts index 2860880803..768d83c024 100644 --- a/src/app/core/data/eperson-registration.service.spec.ts +++ b/src/app/core/data/eperson-registration.service.spec.ts @@ -98,9 +98,10 @@ describe('EpersonRegistrationService', () => { })); }); + // tslint:disable:no-shadowed-variable it('should use cached responses and /registrations/search/findByToken?', () => { - testScheduler.run(({ tscold, expectObservable }) => { - rdbService.buildSingle.and.returnValue(tscold('a', { a: rd })); + testScheduler.run(({ cold, expectObservable }) => { + rdbService.buildSingle.and.returnValue(cold('a', { a: rd })); service.searchByToken('test-token'); From 60009144a117a526716e29226ad9df1331066727 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Thu, 29 Apr 2021 14:28:37 +0200 Subject: [PATCH 036/351] 78994: Remove unused import --- src/app/core/data/eperson-registration.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/core/data/eperson-registration.service.ts b/src/app/core/data/eperson-registration.service.ts index 8ea43b3c3f..adf01b0ce9 100644 --- a/src/app/core/data/eperson-registration.service.ts +++ b/src/app/core/data/eperson-registration.service.ts @@ -3,7 +3,7 @@ import { RequestService } from './request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { GetRequest, PostRequest } from './request.models'; import { Observable } from 'rxjs'; -import { filter, find, map, skipWhile, take } from 'rxjs/operators'; +import { filter, find, map, skipWhile } from 'rxjs/operators'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { Registration } from '../shared/registration.model'; import { getFirstCompletedRemoteData, getFirstSucceededRemoteData } from '../shared/operators'; From b5342e0fab6220c6dc5ff722c86a91911a12f10f Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Mon, 3 May 2021 18:41:56 +0200 Subject: [PATCH 037/351] 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 038/351] 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 039/351] 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 040/351] 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 041/351] 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 93450e1dcfb2588dea8c863165467a516681061b Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Tue, 4 May 2021 11:08:55 +0200 Subject: [PATCH 042/351] 79219: Remove -e option from MetadataImportPageComponent --- .../metadata-import-page.component.spec.ts | 17 +-------- .../metadata-import-page.component.ts | 36 +++++-------------- 2 files changed, 9 insertions(+), 44 deletions(-) diff --git a/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.spec.ts b/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.spec.ts index db6bb7db84..d663481b8c 100644 --- a/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.spec.ts +++ b/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.spec.ts @@ -6,10 +6,7 @@ import { By } from '@angular/platform-browser'; import { Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { TranslateModule } from '@ngx-translate/core'; -import { of as observableOf } from 'rxjs'; -import { AuthService } from '../../core/auth/auth.service'; import { METADATA_IMPORT_SCRIPT_NAME, ScriptDataService } from '../../core/data/processes/script-data.service'; -import { EPerson } from '../../core/eperson/models/eperson.model'; import { ProcessParameter } from '../../process-page/processes/process-parameter.model'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; @@ -22,12 +19,9 @@ describe('MetadataImportPageComponent', () => { let comp: MetadataImportPageComponent; let fixture: ComponentFixture; - let user; - let notificationService: NotificationsServiceStub; let scriptService: any; let router; - let authService; let locationStub; function init() { @@ -37,13 +31,6 @@ describe('MetadataImportPageComponent', () => { invoke: createSuccessfulRemoteDataObject$({ processId: '45' }) } ); - user = Object.assign(new EPerson(), { - id: 'userId', - email: 'user@test.com' - }); - authService = jasmine.createSpyObj('authService', { - getAuthenticatedUserFromStore: observableOf(user) - }); router = jasmine.createSpyObj('router', { navigateByUrl: jasmine.createSpy('navigateByUrl') }); @@ -65,7 +52,6 @@ describe('MetadataImportPageComponent', () => { { provide: NotificationsService, useValue: notificationService }, { provide: ScriptDataService, useValue: scriptService }, { provide: Router, useValue: router }, - { provide: AuthService, useValue: authService }, { provide: Location, useValue: locationStub }, ], schemas: [NO_ERRORS_SCHEMA] @@ -107,9 +93,8 @@ describe('MetadataImportPageComponent', () => { proceed.click(); fixture.detectChanges(); })); - it('metadata-import script is invoked with its -e currentUserEmail, -f fileName and the mockFile', () => { + it('metadata-import script is invoked with -f fileName and the mockFile', () => { const parameterValues: ProcessParameter[] = [ - Object.assign(new ProcessParameter(), { name: '-e', value: user.email }), Object.assign(new ProcessParameter(), { name: '-f', value: 'filename.txt' }), ]; expect(scriptService.invoke).toHaveBeenCalledWith(METADATA_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]); diff --git a/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.ts b/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.ts index bcef54377b..3bdcca3084 100644 --- a/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.ts +++ b/src/app/+admin/admin-import-metadata-page/metadata-import-page.component.ts @@ -23,20 +23,14 @@ import { getProcessDetailRoute } from '../../process-page/process-page-routing.p /** * Component that represents a metadata import page for administrators */ -export class MetadataImportPageComponent implements OnInit { +export class MetadataImportPageComponent { /** * The current value of the file */ fileObject: File; - /** - * The authenticated user's email - */ - private currentUserEmail$: Observable; - - public constructor(protected authService: AuthService, - private location: Location, + public constructor(private location: Location, protected translate: TranslateService, protected notificationsService: NotificationsService, private scriptDataService: ScriptDataService, @@ -51,15 +45,6 @@ export class MetadataImportPageComponent implements OnInit { this.fileObject = file; } - /** - * Method provided by Angular. Invoked after the constructor. - */ - ngOnInit() { - this.currentUserEmail$ = this.authService.getAuthenticatedUserFromStore().pipe( - map((user: EPerson) => user.email) - ); - } - /** * When return button is pressed go to previous location */ @@ -68,22 +53,17 @@ export class MetadataImportPageComponent implements OnInit { } /** - * Starts import-metadata script with -e currentUserEmail -f fileName (and the selected file) + * Starts import-metadata script with -f fileName (and the selected file) */ public importMetadata() { if (this.fileObject == null) { this.notificationsService.error(this.translate.get('admin.metadata-import.page.error.addFile')); } else { - this.currentUserEmail$.pipe( - switchMap((email: string) => { - if (isNotEmpty(email)) { - const parameterValues: ProcessParameter[] = [ - Object.assign(new ProcessParameter(), { name: '-e', value: email }), - Object.assign(new ProcessParameter(), { name: '-f', value: this.fileObject.name }), - ]; - return this.scriptDataService.invoke(METADATA_IMPORT_SCRIPT_NAME, parameterValues, [this.fileObject]); - } - }), + const parameterValues: ProcessParameter[] = [ + Object.assign(new ProcessParameter(), { name: '-f', value: this.fileObject.name }), + ]; + + this.scriptDataService.invoke(METADATA_IMPORT_SCRIPT_NAME, parameterValues, [this.fileObject]).pipe( getFirstCompletedRemoteData(), ).subscribe((rd: RemoteData) => { if (rd.hasSucceeded) { From f49a7746fb0f2531cc936bbddd2fc80784863fa0 Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Tue, 4 May 2021 11:19:01 +0200 Subject: [PATCH 043/351] 79219: Remove -f from ExportMetadataSelectorComponent and pass uuid to -i --- .../export-metadata-selector.component.spec.ts | 10 ++++------ .../export-metadata-selector.component.ts | 8 +++----- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/app/shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component.spec.ts index 2f0c4e2651..f25a9afc81 100644 --- a/src/app/shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component.spec.ts @@ -157,10 +157,9 @@ describe('ExportMetadataSelectorComponent', () => { done(); }); }); - it('metadata-export script is invoked with its -i handle and -f uuid.csv', () => { + it('metadata-export script is invoked with its -i handle', () => { const parameterValues: ProcessParameter[] = [ - Object.assign(new ProcessParameter(), { name: '-i', value: mockCollection.handle }), - Object.assign(new ProcessParameter(), { name: '-f', value: mockCollection.uuid + '.csv' }), + Object.assign(new ProcessParameter(), { name: '-i', value: mockCollection.uuid }), ]; expect(scriptService.invoke).toHaveBeenCalledWith(METADATA_EXPORT_SCRIPT_NAME, parameterValues, []); }); @@ -182,10 +181,9 @@ describe('ExportMetadataSelectorComponent', () => { done(); }); }); - it('metadata-export script is invoked with its -i handle and -f uuid.csv', () => { + it('metadata-export script is invoked with its -i handle', () => { const parameterValues: ProcessParameter[] = [ - Object.assign(new ProcessParameter(), { name: '-i', value: mockCommunity.handle }), - Object.assign(new ProcessParameter(), { name: '-f', value: mockCommunity.uuid + '.csv' }), + Object.assign(new ProcessParameter(), { name: '-i', value: mockCommunity.uuid }), ]; expect(scriptService.invoke).toHaveBeenCalledWith(METADATA_EXPORT_SCRIPT_NAME, parameterValues, []); }); diff --git a/src/app/shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component.ts index 24aa1704c6..1a43e82aef 100644 --- a/src/app/shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component.ts @@ -56,7 +56,7 @@ export class ExportMetadataSelectorComponent extends DSOSelectorModalWrapperComp modalRef.componentInstance.confirmIcon = 'fas fa-file-export'; const resp$ = modalRef.componentInstance.response.pipe(switchMap((confirm: boolean) => { if (confirm) { - const startScriptSucceeded$ = this.startScriptNotifyAndRedirect(dso, dso.handle); + const startScriptSucceeded$ = this.startScriptNotifyAndRedirect(dso); return startScriptSucceeded$.pipe( switchMap((r: boolean) => { return observableOf(r); @@ -78,12 +78,10 @@ export class ExportMetadataSelectorComponent extends DSOSelectorModalWrapperComp * Start export-metadata script of dso & navigate to process if successful * Otherwise show error message * @param dso Dso to export - * @param handle Dso handle to export */ - private startScriptNotifyAndRedirect(dso: DSpaceObject, handle: string): Observable { + private startScriptNotifyAndRedirect(dso: DSpaceObject): Observable { const parameterValues: ProcessParameter[] = [ - Object.assign(new ProcessParameter(), { name: '-i', value: handle }), - Object.assign(new ProcessParameter(), { name: '-f', value: dso.uuid + '.csv' }), + Object.assign(new ProcessParameter(), { name: '-i', value: dso.uuid }), ]; return this.scriptDataService.invoke(METADATA_EXPORT_SCRIPT_NAME, parameterValues, []) .pipe( From 15ad31bd8474d84a51defe6339bca8cb356be1ef Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Tue, 4 May 2021 11:04:04 +0200 Subject: [PATCH 044/351] 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 045/351] 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 f0fb8c1005bdde5bef6d1a4f674331fecc216ce1 Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Tue, 4 May 2021 13:43:51 +0200 Subject: [PATCH 046/351] 79219: Improve tests and wording --- .../export-metadata-selector.component.spec.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/app/shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component.spec.ts index f25a9afc81..074d0316af 100644 --- a/src/app/shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component.spec.ts @@ -51,12 +51,14 @@ describe('ExportMetadataSelectorComponent', () => { const mockItem = Object.assign(new Item(), { id: 'fake-id', + uuid: 'fake-id', handle: 'fake/handle', lastModified: '2018' }); const mockCollection: Collection = Object.assign(new Collection(), { id: 'test-collection-1-1', + uuid: 'test-collection-1-1', name: 'test-collection-1', metadata: { 'dc.identifier.uri': [ @@ -70,6 +72,7 @@ describe('ExportMetadataSelectorComponent', () => { const mockCommunity = Object.assign(new Community(), { id: 'test-uuid', + uuid: 'test-uuid', metadata: { 'dc.identifier.uri': [ { @@ -157,7 +160,7 @@ describe('ExportMetadataSelectorComponent', () => { done(); }); }); - it('metadata-export script is invoked with its -i handle', () => { + it('should invoke the metadata-export script with option -i uuid', () => { const parameterValues: ProcessParameter[] = [ Object.assign(new ProcessParameter(), { name: '-i', value: mockCollection.uuid }), ]; @@ -181,7 +184,7 @@ describe('ExportMetadataSelectorComponent', () => { done(); }); }); - it('metadata-export script is invoked with its -i handle', () => { + it('should invoke the metadata-export script with option -i uuid', () => { const parameterValues: ProcessParameter[] = [ Object.assign(new ProcessParameter(), { name: '-i', value: mockCommunity.uuid }), ]; From 86bf7ff4f233bf139757c8d6e1446bc6a75b6914 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 6 May 2021 18:03:46 +0200 Subject: [PATCH 047/351] 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 a77ca2f126ad353695e1d10b9075bdb1732dfbbe Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Tue, 11 May 2021 11:39:46 +0200 Subject: [PATCH 048/351] 79325: Fix pagination in the external metadata lookup --- ...elation-external-source-tab.component.html | 2 +- ...tion-external-source-tab.component.spec.ts | 5 +++- ...-relation-external-source-tab.component.ts | 26 ++++++++++++++++--- ...okup-relation-search-tab.component.spec.ts | 5 +++- ...ic-lookup-relation-search-tab.component.ts | 8 +++--- ...okup-relation-selection-tab.component.html | 2 +- ...p-relation-selection-tab.component.spec.ts | 5 ++++ ...lookup-relation-selection-tab.component.ts | 24 +++++++++++++++-- 8 files changed, 64 insertions(+), 13 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.html index d55cdbffed..27697b76a0 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.html @@ -14,7 +14,7 @@ { let component: DsDynamicLookupRelationExternalSourceTabComponent; @@ -103,7 +105,8 @@ describe('DsDynamicLookupRelationExternalSourceTabComponent', () => { } }, { provide: ExternalSourceService, useValue: externalSourceService }, - { provide: SelectableListService, useValue: selectableListService } + { provide: SelectableListService, useValue: selectableListService }, + { provide: PaginationService, useValue: new PaginationServiceStub() } ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts index b697dc9280..f0a86fef7c 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts @@ -3,7 +3,6 @@ import { SEARCH_CONFIG_SERVICE } from '../../../../../../+my-dspace-page/my-dspa import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service'; import { Router } from '@angular/router'; import { ExternalSourceService } from '../../../../../../core/data/external-source.service'; -import { Observable, Subscription } from 'rxjs'; import { RemoteData } from '../../../../../../core/data/remote-data'; import { PaginatedList } from '../../../../../../core/data/paginated-list.model'; import { ExternalSourceEntry } from '../../../../../../core/shared/external-source-entry.model'; @@ -21,6 +20,8 @@ import { hasValue } from '../../../../../empty.util'; import { SelectableListService } from '../../../../../object-list/selectable-list/selectable-list.service'; import { Item } from '../../../../../../core/shared/item.model'; import { Collection } from '../../../../../../core/shared/collection.model'; +import { PaginationService } from '../../../../../../core/pagination/pagination.service'; +import { Observable, Subscription } from 'rxjs'; @Component({ selector: 'ds-dynamic-lookup-relation-external-source-tab', @@ -81,10 +82,15 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit * The initial pagination options */ initialPagination = Object.assign(new PaginationComponentOptions(), { - id: 'submission-external-source-relation-list', + id: 'spc', pageSize: 5 }); + /** + * The current pagination options + */ + currentPagination$: Observable; + /** * The external source we're selecting entries for */ @@ -114,17 +120,21 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit public searchConfigService: SearchConfigurationService, private externalSourceService: ExternalSourceService, private modalService: NgbModal, - private selectableListService: SelectableListService) { + private selectableListService: SelectableListService, + private paginationService: PaginationService + ) { } /** * Get the entries for the selected external source */ ngOnInit(): void { + this.resetRoute(); this.entriesRD$ = this.searchConfigService.paginatedSearchOptions.pipe( switchMap((searchOptions: PaginatedSearchOptions) => this.externalSourceService.getExternalSourceEntries(this.externalSource.id, searchOptions).pipe(startWith(undefined))) ); + this.currentPagination$ = this.paginationService.getCurrentPagination(this.searchConfigService.paginationID, this.initialPagination); this.importConfig = { buttonLabel: 'submission.sections.describe.relationship-lookup.external-source.import-button-title.' + this.label }; @@ -159,4 +169,14 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit this.importObjectSub.unsubscribe(); } } + + /** + * Method to reset the route when the tab is opened to make sure no strange pagination issues appears + */ + resetRoute() { + this.paginationService.updateRoute(this.searchConfigService.paginationID, { + page: 1, + pageSize: 5 + }); + } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.spec.ts index 700026ba10..83a8d05217 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.spec.ts @@ -17,6 +17,8 @@ import { ItemSearchResult } from '../../../../../object-collection/shared/item-s import { Item } from '../../../../../../core/shared/item.model'; import { ActivatedRoute } from '@angular/router'; import { LookupRelationService } from '../../../../../../core/data/lookup-relation.service'; +import { PaginationService } from '../../../../../../core/pagination/pagination.service'; +import { PaginationServiceStub } from '../../../../../testing/pagination-service.stub'; describe('DsDynamicLookupRelationSearchTabComponent', () => { let component: DsDynamicLookupRelationSearchTabComponent; @@ -88,7 +90,8 @@ describe('DsDynamicLookupRelationSearchTabComponent', () => { } }, { provide: ActivatedRoute, useValue: { snapshot: { queryParams: {} } } }, - { provide: LookupRelationService, useValue: lookupRelationService } + { provide: LookupRelationService, useValue: lookupRelationService }, + { provide: PaginationService, useValue: new PaginationServiceStub() } ], schemas: [NO_ERRORS_SCHEMA] }) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts index f4addf646c..e778b524b0 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts @@ -19,6 +19,7 @@ import { RouteService } from '../../../../../../core/services/route.service'; import { CollectionElementLinkType } from '../../../../../object-collection/collection-element-link.type'; import { Context } from '../../../../../../core/shared/context.model'; import { LookupRelationService } from '../../../../../../core/data/lookup-relation.service'; +import { PaginationService } from '../../../../../../core/pagination/pagination.service'; @Component({ selector: 'ds-dynamic-lookup-relation-search-tab', @@ -117,7 +118,8 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest private selectableListService: SelectableListService, public searchConfigService: SearchConfigurationService, private routeService: RouteService, - public lookupRelationService: LookupRelationService + public lookupRelationService: LookupRelationService, + private paginationService: PaginationService ) { } @@ -137,9 +139,7 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest * Method to reset the route when the window is opened to make sure no strange pagination issues appears */ resetRoute() { - this.router.navigate([], { - queryParams: Object.assign({ query: this.query }, this.route.snapshot.queryParams, this.initialPagination), - }); + this.paginationService.updateRoute(this.searchConfigService.paginationID, this.initialPagination); } /** diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.html index cd55553f5b..8d0053a1df 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.html @@ -12,7 +12,7 @@ { let component: DsDynamicLookupRelationSelectionTabComponent; @@ -54,6 +56,9 @@ describe('DsDynamicLookupRelationSelectionTabComponent', () => { }, { provide: Router, useValue: router + }, + { + provide: PaginationService, useValue: new PaginationServiceStub() } ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts index 8fac2ce7d1..2c8bc22bff 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts @@ -15,6 +15,7 @@ import { PaginatedSearchOptions } from '../../../../../search/paginated-search-o import { PageInfo } from '../../../../../../core/shared/page-info.model'; import { Context } from '../../../../../../core/shared/context.model'; import { createSuccessfulRemoteDataObject } from '../../../../../remote-data.utils'; +import { PaginationService } from '../../../../../../core/pagination/pagination.service'; @Component({ selector: 'ds-dynamic-lookup-relation-selection-tab', @@ -76,18 +77,26 @@ export class DsDynamicLookupRelationSelectionTabComponent { * The initial pagination to use */ initialPagination = Object.assign(new PaginationComponentOptions(), { - id: 'submission-relation-list', + id: 'spc', pageSize: 5 }); + /** + * The current pagination options + */ + currentPagination$: Observable; + constructor(private router: Router, - private searchConfigService: SearchConfigurationService) { + private searchConfigService: SearchConfigurationService, + private paginationService: PaginationService + ) { } /** * Set up the selection and pagination on load */ ngOnInit() { + this.resetRoute(); this.selectionRD$ = this.searchConfigService.paginatedSearchOptions .pipe( map((options: PaginatedSearchOptions) => options.pagination), @@ -110,5 +119,16 @@ export class DsDynamicLookupRelationSelectionTabComponent { ); }) ); + this.currentPagination$ = this.paginationService.getCurrentPagination(this.searchConfigService.paginationID, this.initialPagination); + } + + /** + * Method to reset the route when the tab is opened to make sure no strange pagination issues appears + */ + resetRoute() { + this.paginationService.updateRoute(this.searchConfigService.paginationID, { + page: 1, + pageSize: 5 + }); } } From 4a8becf662140bd8d19029f15185bc8ce2c3613a Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Wed, 12 May 2021 14:47:16 +0200 Subject: [PATCH 049/351] Fix ORCID tab loading --- .../dynamic-lookup-relation-external-source-tab.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.html index 27697b76a0..61be99cbf2 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.html @@ -21,7 +21,7 @@ [importConfig]="importConfig" (importObject)="import($event)"> - From 6631980ee65fd236727b8a73ab474bf4cade59ae Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Wed, 12 May 2021 16:31:48 +0200 Subject: [PATCH 050/351] 79327: Fix item-level statistics pages --- .../abstract-item-update.component.ts | 2 +- src/app/+item-page/item-page.resolver.ts | 40 +++--------- src/app/+item-page/item.resolver.ts | 64 +++++++++++++++++++ .../breadcrumbs/item-breadcrumb.resolver.ts | 2 +- .../statistics-page-routing.module.ts | 5 +- 5 files changed, 78 insertions(+), 35 deletions(-) create mode 100644 src/app/+item-page/item.resolver.ts diff --git a/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts b/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts index 17ec0ff133..01d6cc7439 100644 --- a/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts +++ b/src/app/+item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts @@ -15,9 +15,9 @@ import { RemoteData } from '../../../core/data/remote-data'; import { AbstractTrackableComponent } from '../../../shared/trackable/abstract-trackable.component'; import { environment } from '../../../../environments/environment'; import { getItemPageRoute } from '../../item-page-routing-paths'; -import { ITEM_PAGE_LINKS_TO_FOLLOW } from '../../item-page.resolver'; import { getAllSucceededRemoteData } from '../../../core/shared/operators'; import { hasValue } from '../../../shared/empty.util'; +import { ITEM_PAGE_LINKS_TO_FOLLOW } from '../../item.resolver'; @Component({ selector: 'ds-abstract-item-update', diff --git a/src/app/+item-page/item-page.resolver.ts b/src/app/+item-page/item-page.resolver.ts index a131cda7f8..a12f961e57 100644 --- a/src/app/+item-page/item-page.resolver.ts +++ b/src/app/+item-page/item-page.resolver.ts @@ -12,31 +12,20 @@ import { ResolvedAction } from '../core/resolving/resolver.actions'; import { map } from 'rxjs/operators'; import { hasValue } from '../shared/empty.util'; import { getItemPageRoute } from './item-page-routing-paths'; +import { ItemResolver } from './item.resolver'; /** - * The self links defined in this list are expected to be requested somewhere in the near future - * Requesting them as embeds will limit the number of requests - */ -export const ITEM_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig[] = [ - followLink('owningCollection', undefined, true, true, true, - followLink('parentCommunity', undefined, true, true, true, - followLink('parentCommunity')) - ), - followLink('bundles', new FindListOptions(), true, true, true, followLink('bitstreams')), - followLink('relationships'), - followLink('version', undefined, true, true, true, followLink('versionhistory')), -]; - -/** - * This class represents a resolver that requests a specific item before the route is activated + * This class represents a resolver that requests a specific item before the route is activated and will redirect to the + * entity page */ @Injectable() -export class ItemPageResolver implements Resolve> { +export class ItemPageResolver extends ItemResolver { constructor( - private itemService: ItemDataService, - private store: Store, - private router: Router + protected itemService: ItemDataService, + protected store: Store, + protected router: Router ) { + super(itemService, store, router); } /** @@ -47,12 +36,7 @@ export class ItemPageResolver implements Resolve> { * or an error if something went wrong */ resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - const itemRD$ = this.itemService.findById(route.params.id, - true, - false, - ...ITEM_PAGE_LINKS_TO_FOLLOW - ).pipe( - getFirstCompletedRemoteData(), + return super.resolve(route, state).pipe( map((rd: RemoteData) => { if (rd.hasSucceeded && hasValue(rd.payload)) { const itemRoute = getItemPageRoute(rd.payload); @@ -66,11 +50,5 @@ export class ItemPageResolver implements Resolve> { return rd; }) ); - - itemRD$.subscribe((itemRD: RemoteData) => { - this.store.dispatch(new ResolvedAction(state.url, itemRD.payload)); - }); - - return itemRD$; } } diff --git a/src/app/+item-page/item.resolver.ts b/src/app/+item-page/item.resolver.ts new file mode 100644 index 0000000000..7d020309dc --- /dev/null +++ b/src/app/+item-page/item.resolver.ts @@ -0,0 +1,64 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router'; +import { Observable } from 'rxjs'; +import { RemoteData } from '../core/data/remote-data'; +import { ItemDataService } from '../core/data/item-data.service'; +import { Item } from '../core/shared/item.model'; +import { followLink, FollowLinkConfig } from '../shared/utils/follow-link-config.model'; +import { FindListOptions } from '../core/data/request.models'; +import { getFirstCompletedRemoteData } from '../core/shared/operators'; +import { Store } from '@ngrx/store'; +import { ResolvedAction } from '../core/resolving/resolver.actions'; +import { map } from 'rxjs/operators'; +import { hasValue } from '../shared/empty.util'; +import { getItemPageRoute } from './item-page-routing-paths'; + +/** + * The self links defined in this list are expected to be requested somewhere in the near future + * Requesting them as embeds will limit the number of requests + */ +export const ITEM_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig[] = [ + followLink('owningCollection', undefined, true, true, true, + followLink('parentCommunity', undefined, true, true, true, + followLink('parentCommunity')) + ), + followLink('bundles', new FindListOptions(), true, true, true, followLink('bitstreams')), + followLink('relationships'), + followLink('version', undefined, true, true, true, followLink('versionhistory')), +]; + +/** + * This class represents a resolver that requests a specific item before the route is activated + */ +@Injectable() +export class ItemResolver implements Resolve> { + constructor( + protected itemService: ItemDataService, + protected store: Store, + protected router: Router + ) { + } + + /** + * Method for resolving an item based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @returns Observable<> Emits the found item based on the parameters in the current route, + * or an error if something went wrong + */ + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { + const itemRD$ = this.itemService.findById(route.params.id, + true, + false, + ...ITEM_PAGE_LINKS_TO_FOLLOW + ).pipe( + getFirstCompletedRemoteData(), + ); + + itemRD$.subscribe((itemRD: RemoteData) => { + this.store.dispatch(new ResolvedAction(state.url, itemRD.payload)); + }); + + return itemRD$; + } +} diff --git a/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts index 2b9bbd6b3d..529349eb89 100644 --- a/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts @@ -4,7 +4,7 @@ import { ItemDataService } from '../data/item-data.service'; import { Item } from '../shared/item.model'; import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { ITEM_PAGE_LINKS_TO_FOLLOW } from '../../+item-page/item-page.resolver'; +import { ITEM_PAGE_LINKS_TO_FOLLOW } from '../../+item-page/item.resolver'; /** * The class that resolves the BreadcrumbConfig object for an Item diff --git a/src/app/statistics-page/statistics-page-routing.module.ts b/src/app/statistics-page/statistics-page-routing.module.ts index 6a133fc9ac..0c63e35b34 100644 --- a/src/app/statistics-page/statistics-page-routing.module.ts +++ b/src/app/statistics-page/statistics-page-routing.module.ts @@ -10,6 +10,7 @@ import { ThemedCollectionStatisticsPageComponent } from './collection-statistics import { ThemedCommunityStatisticsPageComponent } from './community-statistics-page/themed-community-statistics-page.component'; import { ThemedItemStatisticsPageComponent } from './item-statistics-page/themed-item-statistics-page.component'; import { ThemedSiteStatisticsPageComponent } from './site-statistics-page/themed-site-statistics-page.component'; +import { ItemResolver } from '../+item-page/item.resolver'; @NgModule({ imports: [ @@ -34,7 +35,7 @@ import { ThemedSiteStatisticsPageComponent } from './site-statistics-page/themed { path: `items/:id`, resolve: { - scope: ItemPageResolver, + scope: ItemResolver, breadcrumb: I18nBreadcrumbResolver }, data: { @@ -75,7 +76,7 @@ import { ThemedSiteStatisticsPageComponent } from './site-statistics-page/themed I18nBreadcrumbsService, CollectionPageResolver, CommunityPageResolver, - ItemPageResolver + ItemResolver ] }) export class StatisticsPageRoutingModule { From 43b8f45eee67898706cc7f7695f3018021c0c0d9 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 12 May 2021 16:50:18 +0200 Subject: [PATCH 051/351] 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'; From d3466c3e8215dc0012b1e3f063aafde9ecf67de9 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 12 May 2021 17:59:11 +0200 Subject: [PATCH 052/351] Fix issue where uploaded files disappear --- src/app/submission/form/submission-form.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/submission/form/submission-form.component.ts b/src/app/submission/form/submission-form.component.ts index 8df0ab1658..6d4ddb4ca0 100644 --- a/src/app/submission/form/submission-form.component.ts +++ b/src/app/submission/form/submission-form.component.ts @@ -122,7 +122,7 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy { * Initialize all instance variables and retrieve form configuration */ ngOnChanges(changes: SimpleChanges) { - if (this.collectionId && this.submissionId) { + if ((changes.collectionId && this.collectionId) && (changes.submissionId && this.submissionId)) { this.isActive = true; // retrieve submission's section list From 3bd8e355f634d1506f4f7e092c97afebb2f61765 Mon Sep 17 00:00:00 2001 From: Michael Spalti Date: Tue, 18 May 2021 15:42:46 -0700 Subject: [PATCH 053/351] Added description field to iiif item view. --- .../item-pages/iiif-searchable/iiif-searchable.component.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app/entity-groups/iiif-entities/item-pages/iiif-searchable/iiif-searchable.component.html b/src/app/entity-groups/iiif-entities/item-pages/iiif-searchable/iiif-searchable.component.html index 89c1db296d..743fa105bb 100644 --- a/src/app/entity-groups/iiif-entities/item-pages/iiif-searchable/iiif-searchable.component.html +++ b/src/app/entity-groups/iiif-entities/item-pages/iiif-searchable/iiif-searchable.component.html @@ -11,6 +11,10 @@
{{'iiifsearchable.page.titleprefix' | translate}}

+ + From 7e129f282f64458da7958ab529457dd290a5d808 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Wed, 19 May 2021 13:05:15 +0200 Subject: [PATCH 054/351] remove extra author field from item pages --- .../author/item-page-author-field.component.ts | 6 +++++- .../publication/publication.component.html | 13 ++++++------- .../publication/publication.component.spec.ts | 9 +++++++-- .../untyped-item/untyped-item.component.html | 13 ++++++------- .../untyped-item/untyped-item.component.spec.ts | 9 +++++++-- 5 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/app/+item-page/simple/field-components/specific-field/author/item-page-author-field.component.ts b/src/app/+item-page/simple/field-components/specific-field/author/item-page-author-field.component.ts index 51941d2cc8..2da0dc154b 100644 --- a/src/app/+item-page/simple/field-components/specific-field/author/item-page-author-field.component.ts +++ b/src/app/+item-page/simple/field-components/specific-field/author/item-page-author-field.component.ts @@ -8,7 +8,11 @@ import { ItemPageFieldComponent } from '../item-page-field.component'; templateUrl: '../item-page-field.component.html' }) /** - * This component is used for displaying the author (dc.contributor.author, dc.creator and dc.contributor) metadata of an item + * This component is used for displaying the author (dc.contributor.author, dc.creator and + * dc.contributor) metadata of an item. + * + * Note that it purely deals with metadata. It won't turn related Person authors into links to their + * item page. For that use a {@link MetadataRepresentationListComponent} instead. */ export class ItemPageAuthorFieldComponent extends ItemPageFieldComponent { diff --git a/src/app/+item-page/simple/item-types/publication/publication.component.html b/src/app/+item-page/simple/item-types/publication/publication.component.html index 73219cbb8f..ba73ed43be 100644 --- a/src/app/+item-page/simple/item-types/publication/publication.component.html +++ b/src/app/+item-page/simple/item-types/publication/publication.component.html @@ -18,7 +18,12 @@ - + + @@ -37,12 +42,6 @@
- - { expect(fields.length).toBeGreaterThanOrEqual(1); }); - it('should contain a component to display the author', () => { + it('should not contain a metadata only author field', () => { const fields = fixture.debugElement.queryAll(By.css('ds-item-page-author-field')); - expect(fields.length).toBeGreaterThanOrEqual(1); + expect(fields.length).toBe(0); + }); + + it('should contain a mixed metadata and relationship field for authors', () => { + const fields = fixture.debugElement.queryAll(By.css('.ds-item-page-mixed-author-field')); + expect(fields.length).toBe(1); }); it('should contain a component to display the abstract', () => { diff --git a/src/app/+item-page/simple/item-types/untyped-item/untyped-item.component.html b/src/app/+item-page/simple/item-types/untyped-item/untyped-item.component.html index 8d46a2c5a9..969062aea6 100644 --- a/src/app/+item-page/simple/item-types/untyped-item/untyped-item.component.html +++ b/src/app/+item-page/simple/item-types/untyped-item/untyped-item.component.html @@ -18,7 +18,12 @@ - + + @@ -37,12 +42,6 @@
- - { expect(fields.length).toBeGreaterThanOrEqual(1); }); - it('should contain a component to display the author', () => { + it('should not contain a metadata only author field', () => { const fields = fixture.debugElement.queryAll(By.css('ds-item-page-author-field')); - expect(fields.length).toBeGreaterThanOrEqual(1); + expect(fields.length).toBe(0); + }); + + it('should contain a mixed metadata and relationship field for authors', () => { + const fields = fixture.debugElement.queryAll(By.css('.ds-item-page-mixed-author-field')); + expect(fields.length).toBe(1); }); it('should contain a component to display the abstract', () => { From d06b76af3f850ec81b599798b2076b7b37989d14 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 19 May 2021 15:04:12 +0200 Subject: [PATCH 055/351] Fix issue with patching value with a date --- .../json-patch/builder/json-patch-operations-builder.ts | 4 +++- src/app/shared/date.util.ts | 9 +++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/app/core/json-patch/builder/json-patch-operations-builder.ts b/src/app/core/json-patch/builder/json-patch-operations-builder.ts index ced3750834..d3896c4a6c 100644 --- a/src/app/core/json-patch/builder/json-patch-operations-builder.ts +++ b/src/app/core/json-patch/builder/json-patch-operations-builder.ts @@ -9,7 +9,7 @@ import { import { JsonPatchOperationPathObject } from './json-patch-operation-path-combiner'; import { Injectable } from '@angular/core'; import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util'; -import { dateToISOFormat } from '../../../shared/date.util'; +import { dateToISOFormat, dateToString, isNgbDateStruct } from '../../../shared/date.util'; import { VocabularyEntry } from '../../submission/vocabularies/models/vocabulary-entry.model'; import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model'; import { FormFieldLanguageValueObject } from '../../../shared/form/builder/models/form-field-language-value.model'; @@ -136,6 +136,8 @@ export class JsonPatchOperationsBuilder { operationValue = new FormFieldMetadataValueObject(value.value, value.language); } else if (value.hasOwnProperty('authority')) { operationValue = new FormFieldMetadataValueObject(value.value, value.language, value.authority); + } else if (isNgbDateStruct(value)) { + operationValue = new FormFieldMetadataValueObject(dateToString(value)); } else if (value.hasOwnProperty('value')) { operationValue = new FormFieldMetadataValueObject(value.value); } else { diff --git a/src/app/shared/date.util.ts b/src/app/shared/date.util.ts index 063820784c..44afdd10a4 100644 --- a/src/app/shared/date.util.ts +++ b/src/app/shared/date.util.ts @@ -3,7 +3,7 @@ import { NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; import { isObject } from 'lodash'; import * as moment from 'moment'; -import { isNull } from './empty.util'; +import { isNull, isUndefined } from './empty.util'; /** * Returns true if the passed value is a NgbDateStruct. @@ -27,8 +27,9 @@ export function isNgbDateStruct(value: object): boolean { * @return string * the formatted date */ -export function dateToISOFormat(date: Date | NgbDateStruct): string { - const dateObj: Date = (date instanceof Date) ? date : ngbDateStructToDate(date); +export function dateToISOFormat(date: Date | NgbDateStruct | string): string { + const dateObj: Date = (date instanceof Date) ? date : + ((typeof date === 'string') ? ngbDateStructToDate(stringToNgbDateStruct(date)) : ngbDateStructToDate(date)); let year = dateObj.getFullYear().toString(); let month = (dateObj.getMonth() + 1).toString(); @@ -80,7 +81,7 @@ export function stringToNgbDateStruct(date: string): NgbDateStruct { * the NgbDateStruct object */ export function dateToNgbDateStruct(date?: Date): NgbDateStruct { - if (isNull(date)) { + if (isNull(date) || isUndefined(date)) { date = new Date(); } From e0edcd64d2dcedd695ae2634f845a58b1f54d7f7 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 19 May 2021 15:35:37 +0200 Subject: [PATCH 056/351] Fix wrong visualization of bitstream access condition form within submission form --- src/app/shared/mocks/submission.mock.ts | 154 +++++++++--------- .../section-upload-file-edit.component.scss | 6 + .../section-upload-file-edit.component.ts | 8 +- .../edit/section-upload-file-edit.model.ts | 29 ++-- .../file/section-upload-file.component.ts | 1 + 5 files changed, 110 insertions(+), 88 deletions(-) create mode 100644 src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.scss diff --git a/src/app/shared/mocks/submission.mock.ts b/src/app/shared/mocks/submission.mock.ts index 1ee097af71..eaebb38df8 100644 --- a/src/app/shared/mocks/submission.mock.ts +++ b/src/app/shared/mocks/submission.mock.ts @@ -1519,83 +1519,87 @@ export const mockFileFormData = { }, accessConditions: [ { - name: [ - { - value: 'openaccess', - language: null, - authority: null, - display: 'openaccess', - confidence: -1, - place: 0, - otherInformation: null - } - ], - } - , + accessConditionGroup: { + name: [ + { + value: 'openaccess', + language: null, + authority: null, + display: 'openaccess', + confidence: -1, + place: 0, + otherInformation: null + } + ], + }, + }, { - name: [ - { - value: 'lease', - language: null, - authority: null, - display: 'lease', - confidence: -1, - place: 0, - otherInformation: null - } - ], - endDate: [ - { - value: { - year: 2019, - month: 1, - day: 16 - }, - language: null, - authority: null, - display: { - year: 2019, - month: 1, - day: 16 - }, - confidence: -1, - place: 0, - otherInformation: null - } - ], - } - , + accessConditionGroup:{ + name: [ + { + value: 'lease', + language: null, + authority: null, + display: 'lease', + confidence: -1, + place: 0, + otherInformation: null + } + ], + endDate: [ + { + value: { + year: 2019, + month: 1, + day: 16 + }, + language: null, + authority: null, + display: { + year: 2019, + month: 1, + day: 16 + }, + confidence: -1, + place: 0, + otherInformation: null + } + ], + } + }, { - name: [ - { - value: 'embargo', - language: null, - authority: null, - display: 'lease', - confidence: -1, - place: 0, - otherInformation: null - } - ], - startDate: [ - { - value: { - year: 2019, - month: 1, - day: 16 - }, - language: null, - authority: null, - display: { - year: 2019, - month: 1, - day: 16 - }, - confidence: -1, - place: 0, - otherInformation: null - } - ], + accessConditionGroup: { + name: [ + { + value: 'embargo', + language: null, + authority: null, + display: 'lease', + confidence: -1, + place: 0, + otherInformation: null + } + ], + startDate: [ + { + value: { + year: 2019, + month: 1, + day: 16 + }, + language: null, + authority: null, + display: { + year: 2019, + month: 1, + day: 16 + }, + confidence: -1, + place: 0, + otherInformation: null + } + ], + } } ] }; diff --git a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.scss b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.scss new file mode 100644 index 0000000000..b443db711b --- /dev/null +++ b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.scss @@ -0,0 +1,6 @@ + +::ng-deep .access-condition-group { + position: relative; + top: -2.3rem; + margin-bottom: -2.3rem; +} diff --git a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.ts b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.ts index 512453d84e..cfece7a5fe 100644 --- a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.ts +++ b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.ts @@ -18,6 +18,8 @@ import { import { WorkspaceitemSectionUploadFileObject } from '../../../../../core/submission/models/workspaceitem-section-upload-file.model'; import { FormBuilderService } from '../../../../../shared/form/builder/form-builder.service'; import { + BITSTREAM_ACCESS_CONDITION_GROUP_CONFIG, + BITSTREAM_ACCESS_CONDITION_GROUP_LAYOUT, BITSTREAM_ACCESS_CONDITIONS_FORM_ARRAY_CONFIG, BITSTREAM_ACCESS_CONDITIONS_FORM_ARRAY_LAYOUT, BITSTREAM_FORM_ACCESS_CONDITION_END_DATE_CONFIG, @@ -43,6 +45,7 @@ import { FormComponent } from '../../../../../shared/form/form.component'; */ @Component({ selector: 'ds-submission-section-upload-file-edit', + styleUrls: ['./section-upload-file-edit.component.scss'], templateUrl: './section-upload-file-edit.component.html', }) export class SubmissionSectionUploadFileEditComponent implements OnChanges { @@ -209,8 +212,9 @@ export class SubmissionSectionUploadFileEditComponent implements OnChanges { const startDate = new DynamicDatePickerModel(startDateConfig, BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_LAYOUT); const endDate = new DynamicDatePickerModel(endDateConfig, BITSTREAM_FORM_ACCESS_CONDITION_END_DATE_LAYOUT); - - return [type, startDate, endDate]; + const accessConditionGroupConfig = Object.assign({}, BITSTREAM_ACCESS_CONDITION_GROUP_CONFIG); + accessConditionGroupConfig.group = [type, startDate, endDate]; + return [new DynamicFormGroupModel(accessConditionGroupConfig, BITSTREAM_ACCESS_CONDITION_GROUP_LAYOUT)]; }; // Number of access conditions blocks in form diff --git a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.model.ts b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.model.ts index 096954659e..300a4b461f 100644 --- a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.model.ts +++ b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.model.ts @@ -15,12 +15,24 @@ export const BITSTREAM_METADATA_FORM_GROUP_CONFIG: DynamicFormGroupModelConfig = export const BITSTREAM_METADATA_FORM_GROUP_LAYOUT: DynamicFormControlLayout = { element: { container: 'form-group', - label: 'col-form-label' + label: 'col-form-label' }, grid: { label: 'col-sm-3' } }; +export const BITSTREAM_ACCESS_CONDITION_GROUP_CONFIG: DynamicFormGroupModelConfig = { + id: 'accessConditionGroup', + group: [] +}; + +export const BITSTREAM_ACCESS_CONDITION_GROUP_LAYOUT: DynamicFormControlLayout = { + element: { + host: 'form-group flex-fill access-condition-group', + container: 'pl-1 pr-1', + control: 'form-row ' + } +}; export const BITSTREAM_ACCESS_CONDITIONS_FORM_ARRAY_CONFIG: DynamicFormArrayModelConfig = { id: 'accessConditions', @@ -28,7 +40,7 @@ export const BITSTREAM_ACCESS_CONDITIONS_FORM_ARRAY_CONFIG: DynamicFormArrayMode }; export const BITSTREAM_ACCESS_CONDITIONS_FORM_ARRAY_LAYOUT: DynamicFormControlLayout = { grid: { - group: 'form-row' + group: 'form-row pt-4', } }; @@ -39,11 +51,8 @@ export const BITSTREAM_FORM_ACCESS_CONDITION_TYPE_CONFIG: DynamicSelectModelConf }; export const BITSTREAM_FORM_ACCESS_CONDITION_TYPE_LAYOUT: DynamicFormControlLayout = { element: { - container: 'p-0', - label: 'col-form-label' - }, - grid: { - host: 'col-md-10' + host: 'col-12', + label: 'col-form-label name-label' } }; @@ -70,11 +79,10 @@ export const BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_CONFIG: DynamicDatePicke }; export const BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_LAYOUT: DynamicFormControlLayout = { element: { - container: 'p-0', label: 'col-form-label' }, grid: { - host: 'col-md-4' + host: 'col-6' } }; @@ -101,10 +109,9 @@ export const BITSTREAM_FORM_ACCESS_CONDITION_END_DATE_CONFIG: DynamicDatePickerM }; export const BITSTREAM_FORM_ACCESS_CONDITION_END_DATE_LAYOUT: DynamicFormControlLayout = { element: { - container: 'p-0', label: 'col-form-label' }, grid: { - host: 'col-md-4' + host: 'col-6' } }; diff --git a/src/app/submission/sections/upload/file/section-upload-file.component.ts b/src/app/submission/sections/upload/file/section-upload-file.component.ts index 5a97140a70..d4c901b290 100644 --- a/src/app/submission/sections/upload/file/section-upload-file.component.ts +++ b/src/app/submission/sections/upload/file/section-upload-file.component.ts @@ -255,6 +255,7 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit { }); const accessConditionsToSave = []; formData.accessConditions + .map((accessConditions) => accessConditions.accessConditionGroup) .filter((accessCondition) => isNotEmpty(accessCondition)) .forEach((accessCondition) => { let accessConditionOpt; From e18c66d6888e4c5eeb481e64879902d1dc843b58 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 14 May 2021 19:14:56 +0200 Subject: [PATCH 057/351] Fix issue with patch operations related to repeatable fields --- .../form/section-form-operations.service.ts | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/app/submission/sections/form/section-form-operations.service.ts b/src/app/submission/sections/form/section-form-operations.service.ts index a1bb99e3cd..8aef798cbc 100644 --- a/src/app/submission/sections/form/section-form-operations.service.ts +++ b/src/app/submission/sections/form/section-form-operations.service.ts @@ -6,7 +6,8 @@ import { DYNAMIC_FORM_CONTROL_TYPE_GROUP, DynamicFormArrayGroupModel, DynamicFormControlEvent, - DynamicFormControlModel, isDynamicFormControlEvent + DynamicFormControlModel, + isDynamicFormControlEvent } from '@ng-dynamic-forms/core'; import { hasValue, isNotEmpty, isNotNull, isNotUndefined, isNull, isUndefined } from '../../../shared/empty.util'; @@ -299,7 +300,7 @@ export class SectionFormOperationsService { if (event.context && event.context instanceof DynamicFormArrayGroupModel) { // Model is a DynamicRowArrayModel - this.handleArrayGroupPatch(pathCombiner, event, (event as any).context.context); + this.handleArrayGroupPatch(pathCombiner, event, (event as any).context.context, previousValue); return; } @@ -368,7 +369,7 @@ export class SectionFormOperationsService { if (event.context && event.context instanceof DynamicFormArrayGroupModel) { // Model is a DynamicRowArrayModel - this.handleArrayGroupPatch(pathCombiner, event, (event as any).context.context); + this.handleArrayGroupPatch(pathCombiner, event, (event as any).context.context, previousValue); return; } @@ -498,23 +499,37 @@ export class SectionFormOperationsService { event: DynamicFormControlEvent, previousValue: FormFieldPreviousValueObject) { - return this.handleArrayGroupPatch(pathCombiner, event.$event, (event as any).$event.arrayModel); + return this.handleArrayGroupPatch(pathCombiner, event.$event, (event as any).$event.arrayModel, previousValue); } /** * Specific patch handler for a DynamicRowArrayModel. * Configure a Patch ADD with the current array value. * @param pathCombiner + * the [[JsonPatchOperationPathCombiner]] object for the specified operation * @param event + * the [[DynamicFormControlEvent]] for the specified operation * @param model + * the [[DynamicRowArrayModel]] model + * @param previousValue + * the [[FormFieldPreviousValueObject]] for the specified operation */ private handleArrayGroupPatch(pathCombiner: JsonPatchOperationPathCombiner, event, - model: DynamicRowArrayModel) { + model: DynamicRowArrayModel, + previousValue: FormFieldPreviousValueObject) { + const arrayValue = this.formBuilder.getValueFromModel([model]); - const segmentedPath2 = this.getFieldPathSegmentedFromChangeEvent(event); - this.operationsBuilder.add( - pathCombiner.getPath(segmentedPath2), - arrayValue[segmentedPath2], false); + const segmentedPath = this.getFieldPathSegmentedFromChangeEvent(event); + if (isNotEmpty(arrayValue)) { + this.operationsBuilder.add( + pathCombiner.getPath(segmentedPath), + arrayValue[segmentedPath], + false + ); + } else if (previousValue.isPathEqual(this.formBuilder.getPath(event.model))) { + this.operationsBuilder.remove(pathCombiner.getPath(segmentedPath)); + } + } } From d6dbbd1f1fa853e06d387668491462f939b7fad8 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Tue, 18 May 2021 10:41:15 +0200 Subject: [PATCH 058/351] Add tests for handleArrayGroupPatch method --- .../section-form-operations.service.spec.ts | 92 ++++++++++++++++++- 1 file changed, 91 insertions(+), 1 deletion(-) diff --git a/src/app/submission/sections/form/section-form-operations.service.spec.ts b/src/app/submission/sections/form/section-form-operations.service.spec.ts index c76a15abcb..9c9b6d971b 100644 --- a/src/app/submission/sections/form/section-form-operations.service.spec.ts +++ b/src/app/submission/sections/form/section-form-operations.service.spec.ts @@ -4,7 +4,8 @@ import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { DYNAMIC_FORM_CONTROL_TYPE_ARRAY, DYNAMIC_FORM_CONTROL_TYPE_GROUP, - DynamicFormControlEvent + DynamicFormControlEvent, + DynamicInputModel } from '@ng-dynamic-forms/core'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; @@ -28,6 +29,7 @@ import { } from '../../../shared/mocks/form-models.mock'; import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model'; import { VocabularyEntry } from '../../../core/submission/vocabularies/models/vocabulary-entry.model'; +import { DynamicRowArrayModel } from '../../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model'; describe('SectionFormOperationsService test suite', () => { let formBuilderService: any; @@ -83,6 +85,11 @@ describe('SectionFormOperationsService test suite', () => { formBuilderService = TestBed.inject(FormBuilderService); }); + afterEach(() => { + jsonPatchOpBuilder.add.calls.reset(); + jsonPatchOpBuilder.remove.calls.reset(); + }); + describe('dispatchOperationsFromEvent', () => { it('should call dispatchOperationsFromRemoveEvent on remove event', () => { const previousValue = new FormFieldPreviousValueObject(['path', 'test'], 'value'); @@ -760,4 +767,87 @@ describe('SectionFormOperationsService test suite', () => { }); }); + describe('handleArrayGroupPatch', () => { + let arrayModel; + let previousValue; + beforeEach(() => { + arrayModel = new DynamicRowArrayModel( + { + id: 'testFormRowArray', + initialCount: 5, + notRepeatable: false, + relationshipConfig: undefined, + submissionId: '1234', + isDraggable: true, + showButtons: false, + groupFactory: () => { + return [ + new DynamicInputModel({ id: 'testFormRowArrayGroupInput' }) + ]; + }, + required: false, + metadataKey: 'dc.contributor.author', + metadataFields: ['dc.contributor.author'], + hasSelectableMetadata: true + } + ); + spyOn(serviceAsAny, 'getFieldPathSegmentedFromChangeEvent').and.returnValue('path'); + previousValue = new FormFieldPreviousValueObject(['path'], null); + }); + + it('should not dispatch a json-path operation when a array value is empty', () => { + formBuilderService.getValueFromModel.and.returnValue({}); + spyOn(previousValue, 'isPathEqual').and.returnValue(false); + + serviceAsAny.handleArrayGroupPatch( + pathCombiner, + dynamicFormControlChangeEvent, + arrayModel, + previousValue + ); + + expect(jsonPatchOpBuilder.add).not.toHaveBeenCalled(); + expect(jsonPatchOpBuilder.remove).not.toHaveBeenCalled(); + }); + + it('should dispatch a json-path add operation when a array value is not empty', () => { + const pathValue = [ + new FormFieldMetadataValueObject('test'), + new FormFieldMetadataValueObject('test two') + ]; + formBuilderService.getValueFromModel.and.returnValue({ + path:pathValue + }); + spyOn(previousValue, 'isPathEqual').and.returnValue(false); + + serviceAsAny.handleArrayGroupPatch( + pathCombiner, + dynamicFormControlChangeEvent, + arrayModel, + previousValue + ); + + expect(jsonPatchOpBuilder.add).toHaveBeenCalledWith( + pathCombiner.getPath('path'), + pathValue, + false + ); + expect(jsonPatchOpBuilder.remove).not.toHaveBeenCalled(); + }); + + it('should dispatch a json-path remove operation when a array value is empty and has previous value', () => { + formBuilderService.getValueFromModel.and.returnValue({}); + spyOn(previousValue, 'isPathEqual').and.returnValue(true); + + serviceAsAny.handleArrayGroupPatch( + pathCombiner, + dynamicFormControlChangeEvent, + arrayModel, + previousValue + ); + + expect(jsonPatchOpBuilder.add).not.toHaveBeenCalled(); + expect(jsonPatchOpBuilder.remove).toHaveBeenCalledWith(pathCombiner.getPath('path')); + }); + }); }); From 98dde58f9d0f330945f6faf4665c703709475ad9 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Tue, 18 May 2021 11:52:03 +0200 Subject: [PATCH 059/351] [D4CRIS-1080] Fix issue where a replace patch operation was dispatched instead of an add one when field's previous value is empty --- .../section-form-operations.service.spec.ts | 29 ++++++++++++++++++- .../form/section-form-operations.service.ts | 4 +-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/app/submission/sections/form/section-form-operations.service.spec.ts b/src/app/submission/sections/form/section-form-operations.service.spec.ts index 9c9b6d971b..ec179b6151 100644 --- a/src/app/submission/sections/form/section-form-operations.service.spec.ts +++ b/src/app/submission/sections/form/section-form-operations.service.spec.ts @@ -574,7 +574,7 @@ describe('SectionFormOperationsService test suite', () => { }); it('should dispatch a json-path remove operation when has a stored value', () => { - const previousValue = new FormFieldPreviousValueObject(['path', 'test'], 'value'); + let previousValue = new FormFieldPreviousValueObject(['path', 'test'], 'value'); const event = Object.assign({}, dynamicFormControlChangeEvent, { model: { parent: mockRowGroupModel @@ -597,6 +597,7 @@ describe('SectionFormOperationsService test suite', () => { spyIndex.and.returnValue(1); spyPath.and.returnValue('path/1'); + previousValue = new FormFieldPreviousValueObject(['path', 'test'], 'value'); serviceAsAny.dispatchOperationsFromChangeEvent(pathCombiner, event, previousValue, true); expect(jsonPatchOpBuilder.remove).toHaveBeenCalledWith(pathCombiner.getPath('path/1')); @@ -627,6 +628,32 @@ describe('SectionFormOperationsService test suite', () => { new FormFieldMetadataValueObject('test')); }); + it('should dispatch a json-path add operation when has a stored value but previous value is empty', () => { + const previousValue = new FormFieldPreviousValueObject(['path', 'test'], null); + const event = Object.assign({}, dynamicFormControlChangeEvent, { + model: { + parent: mockRowGroupModel + } + }); + spyOn(service, 'getFieldPathFromEvent').and.returnValue('path/0'); + spyOn(service, 'getFieldPathSegmentedFromChangeEvent').and.returnValue('path'); + spyOn(service, 'getFieldValueFromChangeEvent').and.returnValue(new FormFieldMetadataValueObject('test')); + spyOn(service, 'getArrayIndexFromEvent').and.returnValue(0); + spyOn(serviceAsAny, 'getValueMap'); + spyOn(serviceAsAny, 'dispatchOperationsFromMap'); + formBuilderService.isQualdropGroup.and.returnValue(false); + formBuilderService.isRelationGroup.and.returnValue(false); + formBuilderService.hasArrayGroupValue.and.returnValue(false); + spyOn(previousValue, 'isPathEqual').and.returnValue(false); + + serviceAsAny.dispatchOperationsFromChangeEvent(pathCombiner, event, previousValue, true); + + expect(jsonPatchOpBuilder.add).toHaveBeenCalledWith( + pathCombiner.getPath('path'), + new FormFieldMetadataValueObject('test'), + true); + }); + it('should dispatch a json-path add operation when has a value and field index is zero or undefined', () => { const previousValue = new FormFieldPreviousValueObject(['path', 'test'], 'value'); const event = Object.assign({}, dynamicFormControlChangeEvent, { diff --git a/src/app/submission/sections/form/section-form-operations.service.ts b/src/app/submission/sections/form/section-form-operations.service.ts index 8aef798cbc..7174d5da67 100644 --- a/src/app/submission/sections/form/section-form-operations.service.ts +++ b/src/app/submission/sections/form/section-form-operations.service.ts @@ -389,7 +389,7 @@ export class SectionFormOperationsService { this.operationsBuilder.add( pathCombiner.getPath(segmentedPath), value, true); - } else if (previousValue.isPathEqual(this.formBuilder.getPath(event.model)) || hasStoredValue) { + } else if (previousValue.isPathEqual(this.formBuilder.getPath(event.model)) || (hasStoredValue && isNotEmpty(previousValue.value)) ) { // Here model has a previous value changed or stored in the server if (hasValue(event.$event) && hasValue(event.$event.previousIndex)) { if (event.$event.previousIndex < 0) { @@ -422,7 +422,7 @@ export class SectionFormOperationsService { previousValue.delete(); } else if (value.hasValue()) { // Here model has no previous value but a new one - if (isUndefined(this.getArrayIndexFromEvent(event)) || this.getArrayIndexFromEvent(event) === 0) { + if (isUndefined(this.getArrayIndexFromEvent(event)) || this.getArrayIndexFromEvent(event) === 0) { // Model is single field or is part of an array model but is the first item, // so dispatch an add operation that initialize the values of a specific metadata this.operationsBuilder.add( From 3c0cb33bc707a641131c57deb8a2b4f17f32d777 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 19 May 2021 18:16:31 +0200 Subject: [PATCH 060/351] fix failed build --- .../sections/form/section-form-operations.service.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/submission/sections/form/section-form-operations.service.spec.ts b/src/app/submission/sections/form/section-form-operations.service.spec.ts index ec179b6151..d5798b82c8 100644 --- a/src/app/submission/sections/form/section-form-operations.service.spec.ts +++ b/src/app/submission/sections/form/section-form-operations.service.spec.ts @@ -806,7 +806,6 @@ describe('SectionFormOperationsService test suite', () => { relationshipConfig: undefined, submissionId: '1234', isDraggable: true, - showButtons: false, groupFactory: () => { return [ new DynamicInputModel({ id: 'testFormRowArrayGroupInput' }) From 926dd466271351e8ec00b19dea601a646c4a2e50 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Thu, 20 May 2021 09:45:48 +0200 Subject: [PATCH 061/351] 79327: Fix LGTM issues and add e2e tests --- .../item-statistics.e2e-spec.ts | 31 +++++++++++++++++++ e2e/src/item-statistics/item-statistics.po.ts | 23 ++++++++++++++ package.json | 3 +- src/app/+item-page/item-page.resolver.ts | 4 --- src/app/+item-page/item.resolver.ts | 3 -- .../statistics-page-routing.module.ts | 1 - yarn.lock | 8 ++--- 7 files changed, 60 insertions(+), 13 deletions(-) create mode 100644 e2e/src/item-statistics/item-statistics.e2e-spec.ts create mode 100644 e2e/src/item-statistics/item-statistics.po.ts diff --git a/e2e/src/item-statistics/item-statistics.e2e-spec.ts b/e2e/src/item-statistics/item-statistics.e2e-spec.ts new file mode 100644 index 0000000000..fe629804a3 --- /dev/null +++ b/e2e/src/item-statistics/item-statistics.e2e-spec.ts @@ -0,0 +1,31 @@ +import { ProtractorPage } from './item-statistics.po'; +import { browser } from 'protractor'; + +describe('protractor Item statics', () => { + let page: ProtractorPage; + + beforeEach(() => { + page = new ProtractorPage(); + }); + + it('should contain element ds-item-page when navigating when navigating to an item page', () => { + page.navigateToItemPage(); + expect(page.elementTagExists('ds-item-page')).toEqual(true); + expect(page.elementTagExists('ds-item-statistics-page')).toEqual(false); + }); + + it('should redirect to the entity page when navigating to an item page', () => { + page.navigateToItemPage(); + expect(browser.getCurrentUrl()).toEqual('http://localhost:4000' + page.ENTITYPAGE); + }); + + it('should contain element ds-item-statistics-page when navigating when navigating to an item statistics page', () => { + page.navigateToItemStatisticsPage(); + expect(page.elementTagExists('ds-item-statistics-page')).toEqual(true); + expect(page.elementTagExists('ds-item-page')).toEqual(false); + }); + it('should contain the item statistics page url when navigating to an item statistics page', () => { + page.navigateToItemStatisticsPage(); + expect(browser.getCurrentUrl()).toEqual('http://localhost:4000' + page.ITEMSTATISTICSPAGE); + }); +}); diff --git a/e2e/src/item-statistics/item-statistics.po.ts b/e2e/src/item-statistics/item-statistics.po.ts new file mode 100644 index 0000000000..618f65dc98 --- /dev/null +++ b/e2e/src/item-statistics/item-statistics.po.ts @@ -0,0 +1,23 @@ +import { browser, element, by } from 'protractor'; + +export class ProtractorPage { + ITEMPAGE = '/items/e98b0f27-5c19-49a0-960d-eb6ad5287067'; + ENTITYPAGE = '/entities/publication/e98b0f27-5c19-49a0-960d-eb6ad5287067'; + ITEMSTATISTICSPAGE = '/statistics/items/e98b0f27-5c19-49a0-960d-eb6ad5287067'; + + navigateToItemPage() { + return browser.get(this.ITEMPAGE); + } + navigateToItemStatisticsPage() { + return browser.get(this.ITEMSTATISTICSPAGE); + } + + elementTagExists(tag: string) { + return element(by.tagName(tag)).isPresent(); + } + + verifyCurrentUrl(url) { + browser.getCurrentUrl().then((currentUrl) => currentUrl === url ); + } + +} diff --git a/package.json b/package.json index 80af52e264..652ab409ac 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,8 @@ }, "private": true, "resolutions": { - "minimist": "^1.2.5" + "minimist": "^1.2.5", + "webdriver-manager": "^12.1.8" }, "dependencies": { "@angular/animations": "~10.2.3", diff --git a/src/app/+item-page/item-page.resolver.ts b/src/app/+item-page/item-page.resolver.ts index a12f961e57..7edffc5357 100644 --- a/src/app/+item-page/item-page.resolver.ts +++ b/src/app/+item-page/item-page.resolver.ts @@ -4,11 +4,7 @@ import { Observable } from 'rxjs'; import { RemoteData } from '../core/data/remote-data'; import { ItemDataService } from '../core/data/item-data.service'; import { Item } from '../core/shared/item.model'; -import { followLink, FollowLinkConfig } from '../shared/utils/follow-link-config.model'; -import { FindListOptions } from '../core/data/request.models'; -import { getFirstCompletedRemoteData } from '../core/shared/operators'; import { Store } from '@ngrx/store'; -import { ResolvedAction } from '../core/resolving/resolver.actions'; import { map } from 'rxjs/operators'; import { hasValue } from '../shared/empty.util'; import { getItemPageRoute } from './item-page-routing-paths'; diff --git a/src/app/+item-page/item.resolver.ts b/src/app/+item-page/item.resolver.ts index 7d020309dc..99b96511fe 100644 --- a/src/app/+item-page/item.resolver.ts +++ b/src/app/+item-page/item.resolver.ts @@ -9,9 +9,6 @@ import { FindListOptions } from '../core/data/request.models'; import { getFirstCompletedRemoteData } from '../core/shared/operators'; import { Store } from '@ngrx/store'; import { ResolvedAction } from '../core/resolving/resolver.actions'; -import { map } from 'rxjs/operators'; -import { hasValue } from '../shared/empty.util'; -import { getItemPageRoute } from './item-page-routing-paths'; /** * The self links defined in this list are expected to be requested somewhere in the near future diff --git a/src/app/statistics-page/statistics-page-routing.module.ts b/src/app/statistics-page/statistics-page-routing.module.ts index 0c63e35b34..5b96bcca6e 100644 --- a/src/app/statistics-page/statistics-page-routing.module.ts +++ b/src/app/statistics-page/statistics-page-routing.module.ts @@ -3,7 +3,6 @@ import { RouterModule } from '@angular/router'; import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service'; import { StatisticsPageModule } from './statistics-page.module'; -import { ItemPageResolver } from '../+item-page/item-page.resolver'; import { CollectionPageResolver } from '../+collection-page/collection-page.resolver'; import { CommunityPageResolver } from '../+community-page/community-page.resolver'; import { ThemedCollectionStatisticsPageComponent } from './collection-statistics-page/themed-collection-statistics-page.component'; diff --git a/yarn.lock b/yarn.lock index 9b6e2d31d6..d6388ddb53 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11933,10 +11933,10 @@ webdriver-js-extender@2.1.0: "@types/selenium-webdriver" "^3.0.0" selenium-webdriver "^3.0.1" -webdriver-manager@^12.1.7: - version "12.1.7" - resolved "https://registry.yarnpkg.com/webdriver-manager/-/webdriver-manager-12.1.7.tgz#ed4eaee8f906b33c146e869b55e850553a1b1162" - integrity sha512-XINj6b8CYuUYC93SG3xPkxlyUc3IJbD6Vvo75CVGuG9uzsefDzWQrhz0Lq8vbPxtb4d63CZdYophF8k8Or/YiA== +webdriver-manager@^12.1.7, webdriver-manager@^12.1.8: + version "12.1.8" + resolved "https://registry.yarnpkg.com/webdriver-manager/-/webdriver-manager-12.1.8.tgz#5e70e73eaaf53a0767d5745270addafbc5905fd4" + integrity sha512-qJR36SXG2VwKugPcdwhaqcLQOD7r8P2Xiv9sfNbfZrKBnX243iAkOueX1yAmeNgIKhJ3YAT/F2gq6IiEZzahsg== dependencies: adm-zip "^0.4.9" chalk "^1.1.1" From 579f98d0276d575155537e4a461dbad30b84db8c Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Thu, 20 May 2021 10:45:14 +0200 Subject: [PATCH 062/351] 79327: Follow up fixes to test --- e2e/src/item-statistics/item-statistics.e2e-spec.ts | 9 +++++++-- e2e/src/item-statistics/item-statistics.po.ts | 5 ----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/e2e/src/item-statistics/item-statistics.e2e-spec.ts b/e2e/src/item-statistics/item-statistics.e2e-spec.ts index fe629804a3..58cf569d2a 100644 --- a/e2e/src/item-statistics/item-statistics.e2e-spec.ts +++ b/e2e/src/item-statistics/item-statistics.e2e-spec.ts @@ -1,5 +1,6 @@ import { ProtractorPage } from './item-statistics.po'; import { browser } from 'protractor'; +import { UIURLCombiner } from '../../../src/app/core/url-combiner/ui-url-combiner'; describe('protractor Item statics', () => { let page: ProtractorPage; @@ -16,7 +17,9 @@ describe('protractor Item statics', () => { it('should redirect to the entity page when navigating to an item page', () => { page.navigateToItemPage(); - expect(browser.getCurrentUrl()).toEqual('http://localhost:4000' + page.ENTITYPAGE); + expect(browser.getCurrentUrl()).toEqual(new UIURLCombiner(page.ENTITYPAGE).toString()); + expect(browser.getCurrentUrl()).not.toEqual(new UIURLCombiner(page.ITEMSTATISTICSPAGE).toString()); + expect(browser.getCurrentUrl()).not.toEqual(new UIURLCombiner(page.ITEMPAGE).toString()); }); it('should contain element ds-item-statistics-page when navigating when navigating to an item statistics page', () => { @@ -26,6 +29,8 @@ describe('protractor Item statics', () => { }); it('should contain the item statistics page url when navigating to an item statistics page', () => { page.navigateToItemStatisticsPage(); - expect(browser.getCurrentUrl()).toEqual('http://localhost:4000' + page.ITEMSTATISTICSPAGE); + expect(browser.getCurrentUrl()).toEqual(new UIURLCombiner(page.ITEMSTATISTICSPAGE).toString()); + expect(browser.getCurrentUrl()).not.toEqual(new UIURLCombiner(page.ENTITYPAGE).toString()); + expect(browser.getCurrentUrl()).not.toEqual(new UIURLCombiner(page.ITEMPAGE).toString()); }); }); diff --git a/e2e/src/item-statistics/item-statistics.po.ts b/e2e/src/item-statistics/item-statistics.po.ts index 618f65dc98..ec227b9636 100644 --- a/e2e/src/item-statistics/item-statistics.po.ts +++ b/e2e/src/item-statistics/item-statistics.po.ts @@ -15,9 +15,4 @@ export class ProtractorPage { elementTagExists(tag: string) { return element(by.tagName(tag)).isPresent(); } - - verifyCurrentUrl(url) { - browser.getCurrentUrl().then((currentUrl) => currentUrl === url ); - } - } From 55affdebced2046e45988e727254bfd662e94842 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Tue, 25 May 2021 09:28:02 +0200 Subject: [PATCH 063/351] 79597: Add alt text to ds-thumbnail --- src/app/thumbnail/thumbnail.component.html | 2 +- src/app/thumbnail/thumbnail.component.scss | 28 ++++++++++++++++++++++ src/app/thumbnail/thumbnail.component.ts | 6 ++++- src/assets/i18n/en.json5 | 4 ++++ 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/app/thumbnail/thumbnail.component.html b/src/app/thumbnail/thumbnail.component.html index dbf8f6732c..ec11ba6c0f 100644 --- a/src/app/thumbnail/thumbnail.component.html +++ b/src/app/thumbnail/thumbnail.component.html @@ -1,4 +1,4 @@
- +
diff --git a/src/app/thumbnail/thumbnail.component.scss b/src/app/thumbnail/thumbnail.component.scss index e2718bac06..9feac243db 100644 --- a/src/app/thumbnail/thumbnail.component.scss +++ b/src/app/thumbnail/thumbnail.component.scss @@ -1,3 +1,31 @@ img { max-width: 100%; } + +.outer { // .outer/.inner generated ~ https://ratiobuddy.com/ + position: relative; + &:before { + display: block; + content: ""; + width: 100%; + padding-top: (210 / 297) * 100%; // A4 ratio + } + > .inner { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + + > .thumbnail-placeholder { + background: var(--bs-gray-100); + border: var(--bs-gray-200) 1px; + color: var(--bs-gray-600); + font-weight: bold; + display: flex; + justify-content: center; + align-items: center; + text-align: center; + } + } +} diff --git a/src/app/thumbnail/thumbnail.component.ts b/src/app/thumbnail/thumbnail.component.ts index 7e981d5fe6..8d5e780f27 100644 --- a/src/app/thumbnail/thumbnail.component.ts +++ b/src/app/thumbnail/thumbnail.component.ts @@ -18,7 +18,6 @@ export const THUMBNAIL_PLACEHOLDER = 'data:image/svg+xml;charset=UTF-8,%3Csvg%20 templateUrl: './thumbnail.component.html' }) export class ThumbnailComponent implements OnInit { - /** * The thumbnail Bitstream */ @@ -34,6 +33,11 @@ export class ThumbnailComponent implements OnInit { */ src: string; + /** + * i18n key of thumbnail alt text + */ + @Input() alt? = 'thumbnail.default.alt'; + /** * Initialize the thumbnail. * Use a default image if no actual image is available. diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 4c3317a0c0..ac37eba016 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -3539,6 +3539,10 @@ "submission.workflow.tasks.pool.show-detail": "Show detail", + "thumbnail.default.alt": "Thumbnail Image", + + "thumbnail.default.placeholder": "No Thumbnail Available", + "title": "DSpace", From d80da3bbfe2cd6185e9f90538425897336475651 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Tue, 25 May 2021 09:28:46 +0200 Subject: [PATCH 064/351] 79597: Add HTML placeholder for missing thumbnails --- src/app/thumbnail/thumbnail.component.html | 9 ++++++++- src/app/thumbnail/thumbnail.component.scss | 2 +- src/app/thumbnail/thumbnail.component.ts | 23 ++++++++++++---------- src/assets/i18n/en.json5 | 14 +++++++++++++ 4 files changed, 36 insertions(+), 12 deletions(-) diff --git a/src/app/thumbnail/thumbnail.component.html b/src/app/thumbnail/thumbnail.component.html index ec11ba6c0f..4789917f1c 100644 --- a/src/app/thumbnail/thumbnail.component.html +++ b/src/app/thumbnail/thumbnail.component.html @@ -1,4 +1,11 @@
- + +
+
+
+ {{ placeholder | translate }} +
+
+
diff --git a/src/app/thumbnail/thumbnail.component.scss b/src/app/thumbnail/thumbnail.component.scss index 9feac243db..4c8c7d90ad 100644 --- a/src/app/thumbnail/thumbnail.component.scss +++ b/src/app/thumbnail/thumbnail.component.scss @@ -19,7 +19,7 @@ img { > .thumbnail-placeholder { background: var(--bs-gray-100); - border: var(--bs-gray-200) 1px; + border: 1px solid var(--bs-gray-200); color: var(--bs-gray-600); font-weight: bold; display: flex; diff --git a/src/app/thumbnail/thumbnail.component.ts b/src/app/thumbnail/thumbnail.component.ts index 8d5e780f27..21a5da3567 100644 --- a/src/app/thumbnail/thumbnail.component.ts +++ b/src/app/thumbnail/thumbnail.component.ts @@ -2,11 +2,6 @@ import { Component, Input, OnInit } from '@angular/core'; import { Bitstream } from '../core/shared/bitstream.model'; import { hasValue } from '../shared/empty.util'; -/** - * A fallback placeholder image as a base64 string - */ -export const THUMBNAIL_PLACEHOLDER = 'data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2293%22%20height%3D%22120%22%20viewBox%3D%220%200%2093%20120%22%20preserveAspectRatio%3D%22none%22%3E%3C!--%0ASource%20URL%3A%20holder.js%2F93x120%3Ftext%3DNo%20Thumbnail%0ACreated%20with%20Holder.js%202.8.2.%0ALearn%20more%20at%20http%3A%2F%2Fholderjs.com%0A(c)%202012-2015%20Ivan%20Malopinsky%20-%20http%3A%2F%2Fimsky.co%0A--%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%3C!%5BCDATA%5B%23holder_1543e460b05%20text%20%7B%20fill%3A%23AAAAAA%3Bfont-weight%3Abold%3Bfont-family%3AArial%2C%20Helvetica%2C%20Open%20Sans%2C%20sans-serif%2C%20monospace%3Bfont-size%3A10pt%20%7D%20%5D%5D%3E%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_1543e460b05%22%3E%3Crect%20width%3D%2293%22%20height%3D%22120%22%20fill%3D%22%23FFFFFF%22%2F%3E%3Cg%3E%3Ctext%20x%3D%2235.6171875%22%20y%3D%2257%22%3ENo%3C%2Ftext%3E%3Ctext%20x%3D%2210.8125%22%20y%3D%2272%22%3EThumbnail%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E'; - /** * This component renders a given Bitstream as a thumbnail. * One input parameter of type Bitstream is expected. @@ -24,9 +19,10 @@ export class ThumbnailComponent implements OnInit { @Input() thumbnail: Bitstream; /** - * The default image, used if the thumbnail isn't set or can't be downloaded + * The default image, used if the thumbnail isn't set or can't be downloaded. + * If defaultImage is null, a HTML placeholder is used instead. */ - @Input() defaultImage? = THUMBNAIL_PLACEHOLDER; + @Input() defaultImage? = null; /** * The src attribute used in the template to render the image. @@ -38,12 +34,19 @@ export class ThumbnailComponent implements OnInit { */ @Input() alt? = 'thumbnail.default.alt'; + /** + * i18n key of HTML placeholder text + */ + @Input() placeholder? = 'thumbnail.default.placeholder'; + /** * Initialize the thumbnail. * Use a default image if no actual image is available. */ ngOnInit(): void { - if (hasValue(this.thumbnail) && hasValue(this.thumbnail._links) && hasValue(this.thumbnail._links.content) && this.thumbnail._links.content.href) { + if (hasValue(this.thumbnail) && hasValue(this.thumbnail._links) + && hasValue(this.thumbnail._links.content) + && this.thumbnail._links.content.href) { this.src = this.thumbnail._links.content.href; } else { this.src = this.defaultImage; @@ -53,13 +56,13 @@ export class ThumbnailComponent implements OnInit { /** * Handle image download errors. * If the image can't be found, use the defaultImage instead. - * If that also can't be found, use the base64 placeholder. + * If that also can't be found, use null to fall back to the HTML placeholder. */ errorHandler() { if (this.src !== this.defaultImage) { this.src = this.defaultImage; } else { - this.src = THUMBNAIL_PLACEHOLDER; + this.src = null; } } } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index ac37eba016..2235eda34d 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -3539,10 +3539,24 @@ "submission.workflow.tasks.pool.show-detail": "Show detail", + "thumbnail.default.alt": "Thumbnail Image", "thumbnail.default.placeholder": "No Thumbnail Available", + "thumbnail.project.alt": "Project Logo", + + "thumbnail.project.placeholder": "Project Placeholder Image", + + "thumbnail.orgunit.alt": "OrgUnit Logo", + + "thumbnail.orgunit.placeholder": "OrgUnit Placeholder Image", + + "thumbnail.person.alt": "Profile Picture", + + "thumbnail.person.placeholder": "No Profile Picture Available", + + "title": "DSpace", From 7a69a23f0c50e28801d04045e905e80f5929ebd1 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Tue, 25 May 2021 09:46:54 +0200 Subject: [PATCH 065/351] 79597: Improve placeholder contrast --- src/app/thumbnail/thumbnail.component.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/thumbnail/thumbnail.component.scss b/src/app/thumbnail/thumbnail.component.scss index 4c8c7d90ad..290b395212 100644 --- a/src/app/thumbnail/thumbnail.component.scss +++ b/src/app/thumbnail/thumbnail.component.scss @@ -19,8 +19,8 @@ img { > .thumbnail-placeholder { background: var(--bs-gray-100); - border: 1px solid var(--bs-gray-200); - color: var(--bs-gray-600); + border: 1px solid var(--bs-gray-300); + color: var(--bs-gray-800); font-weight: bold; display: flex; justify-content: center; From 4b6e02f773b27897fe0ce178a516d2a12a969bed Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Tue, 25 May 2021 09:55:19 +0200 Subject: [PATCH 066/351] 79597: Specify i18n text for person, project & orgunit --- .../item-pages/org-unit/org-unit.component.html | 7 ++++++- .../item-pages/person/person.component.html | 6 +++++- .../item-pages/project/project.component.html | 7 ++++++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html b/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html index 822d4858ce..14d56d4104 100644 --- a/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html +++ b/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html @@ -9,7 +9,12 @@
- + +
- + +
- + + From c9ff89a143d178f24728c353ede4cb8e21e12d41 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Tue, 25 May 2021 10:03:18 +0200 Subject: [PATCH 067/351] 79597: Thumbnail placeholder style ~ CSS variables --- src/app/thumbnail/thumbnail.component.scss | 6 +++--- src/styles/_custom_variables.scss | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/app/thumbnail/thumbnail.component.scss b/src/app/thumbnail/thumbnail.component.scss index 290b395212..e2374c96db 100644 --- a/src/app/thumbnail/thumbnail.component.scss +++ b/src/app/thumbnail/thumbnail.component.scss @@ -18,9 +18,9 @@ img { left: 0; > .thumbnail-placeholder { - background: var(--bs-gray-100); - border: 1px solid var(--bs-gray-300); - color: var(--bs-gray-800); + background: var(--ds-thumbnail-placeholder-background); + border: var(--ds-thumbnail-placeholder-border); + color: var(--ds-thumbnail-placeholder-color); font-weight: bold; display: flex; justify-content: center; diff --git a/src/styles/_custom_variables.scss b/src/styles/_custom_variables.scss index 298be09f67..d0e1564281 100644 --- a/src/styles/_custom_variables.scss +++ b/src/styles/_custom_variables.scss @@ -46,6 +46,9 @@ --ds-edit-item-language-field-width: 43px; --ds-thumbnail-max-width: 175px; + --ds-thumbnail-placeholder-background: #{$gray-100}; + --ds-thumbnail-placeholder-border: 1px solid #{$gray-300}; + --ds-thumbnail-placeholder-color: #{lighten($gray-800, 7%)}; --ds-dso-selector-list-max-height: 475px; --ds-dso-selector-current-background-color: #eeeeee; From 4567f8cc2cfb880e414ead44c7f76326b546517b Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Tue, 25 May 2021 10:20:46 +0200 Subject: [PATCH 068/351] 79597: Limit thumbnail width & set to portrait --- src/app/thumbnail/thumbnail.component.scss | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/thumbnail/thumbnail.component.scss b/src/app/thumbnail/thumbnail.component.scss index e2374c96db..a96147468c 100644 --- a/src/app/thumbnail/thumbnail.component.scss +++ b/src/app/thumbnail/thumbnail.component.scss @@ -1,3 +1,7 @@ +.thumbnail { + max-width: var(--ds-thumbnail-max-width); +} + img { max-width: 100%; } @@ -8,7 +12,7 @@ img { display: block; content: ""; width: 100%; - padding-top: (210 / 297) * 100%; // A4 ratio + padding-top: (297 / 210) * 100%; // A4 ratio } > .inner { position: absolute; From 363d1d74dff0d82291cef6893f6989ea20f7cda8 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Tue, 25 May 2021 11:03:51 +0200 Subject: [PATCH 069/351] 79597: Update unit tests --- .../shared/mocks/translate.service.mock.ts | 1 + src/app/thumbnail/thumbnail.component.html | 6 +-- src/app/thumbnail/thumbnail.component.spec.ts | 42 ++++++++++++++++--- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/app/shared/mocks/translate.service.mock.ts b/src/app/shared/mocks/translate.service.mock.ts index 0bc172b408..38b088e50f 100644 --- a/src/app/shared/mocks/translate.service.mock.ts +++ b/src/app/shared/mocks/translate.service.mock.ts @@ -3,6 +3,7 @@ import { TranslateService } from '@ngx-translate/core'; export function getMockTranslateService(): TranslateService { return jasmine.createSpyObj('translateService', { get: jasmine.createSpy('get'), + use: jasmine.createSpy('use'), instant: jasmine.createSpy('instant'), setDefaultLang: jasmine.createSpy('setDefaultLang') }); diff --git a/src/app/thumbnail/thumbnail.component.html b/src/app/thumbnail/thumbnail.component.html index 4789917f1c..cef88e0192 100644 --- a/src/app/thumbnail/thumbnail.component.html +++ b/src/app/thumbnail/thumbnail.component.html @@ -1,10 +1,8 @@
- +
-
- {{ placeholder | translate }} -
+
{{ placeholder | translate }}
diff --git a/src/app/thumbnail/thumbnail.component.spec.ts b/src/app/thumbnail/thumbnail.component.spec.ts index 21678c9162..687282a373 100644 --- a/src/app/thumbnail/thumbnail.component.spec.ts +++ b/src/app/thumbnail/thumbnail.component.spec.ts @@ -1,10 +1,18 @@ -import { DebugElement } from '@angular/core'; +import { DebugElement, Pipe, PipeTransform } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { Bitstream } from '../core/shared/bitstream.model'; import { SafeUrlPipe } from '../shared/utils/safe-url-pipe'; -import { THUMBNAIL_PLACEHOLDER, ThumbnailComponent } from './thumbnail.component'; +import { ThumbnailComponent } from './thumbnail.component'; +import { TranslateService } from '@ngx-translate/core'; + +@Pipe({ name: 'translate' }) +class MockTranslatePipe implements PipeTransform { + transform(key: string): string { + return 'TRANSLATED ' + key; + } +} describe('ThumbnailComponent', () => { let comp: ThumbnailComponent; @@ -14,7 +22,7 @@ describe('ThumbnailComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ThumbnailComponent, SafeUrlPipe] + declarations: [ThumbnailComponent, SafeUrlPipe, MockTranslatePipe], }).compileComponents(); })); @@ -39,6 +47,19 @@ describe('ThumbnailComponent', () => { const image: HTMLElement = de.query(By.css('img')).nativeElement; expect(image.getAttribute('src')).toBe(comp.thumbnail._links.content.href); }); + it('should include the alt text', () => { + const thumbnail = new Bitstream(); + thumbnail._links = { + self: { href: 'self.url' }, + bundle: { href: 'bundle.url' }, + format: { href: 'format.url' }, + content: { href: 'content.url' }, + }; + comp.thumbnail = thumbnail; + fixture.detectChanges(); + const image: HTMLElement = de.query(By.css('img')).nativeElement; + expect(image.getAttribute('alt')).toBe('TRANSLATED ' + comp.alt); + }); }); describe(`when the thumbnail doesn't exist`, () => { describe('and there is a default image', () => { @@ -48,13 +69,24 @@ describe('ThumbnailComponent', () => { comp.errorHandler(); expect(comp.src).toBe(comp.defaultImage); }); + it('should include the alt text', () => { + comp.src = 'http://bit.stream'; + comp.defaultImage = 'http://default.img'; + comp.errorHandler(); + fixture.detectChanges(); + const image: HTMLElement = de.query(By.css('img')).nativeElement; + expect(image.getAttribute('alt')).toBe('TRANSLATED ' + comp.alt); + }); }); describe('and there is no default image', () => { it('should display the placeholder', () => { comp.src = 'http://default.img'; - comp.defaultImage = 'http://default.img'; comp.errorHandler(); - expect(comp.src).toBe(THUMBNAIL_PLACEHOLDER); + expect(comp.src).toBe(null); + + fixture.detectChanges(); + const placeholder = fixture.debugElement.query(By.css('div.thumbnail-placeholder')).nativeElement; + expect(placeholder.innerHTML).toBe('TRANSLATED ' + comp.placeholder); }); }); }); From ca7d45ff0c0fa2b221bb27587a7ce67e5905cb7d Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Tue, 25 May 2021 11:18:08 +0200 Subject: [PATCH 070/351] 79597: Fix tslint issue --- src/app/thumbnail/thumbnail.component.spec.ts | 2 +- src/app/thumbnail/thumbnail.component.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/thumbnail/thumbnail.component.spec.ts b/src/app/thumbnail/thumbnail.component.spec.ts index 687282a373..82eadc04de 100644 --- a/src/app/thumbnail/thumbnail.component.spec.ts +++ b/src/app/thumbnail/thumbnail.component.spec.ts @@ -5,8 +5,8 @@ import { Bitstream } from '../core/shared/bitstream.model'; import { SafeUrlPipe } from '../shared/utils/safe-url-pipe'; import { ThumbnailComponent } from './thumbnail.component'; -import { TranslateService } from '@ngx-translate/core'; +// tslint:disable-next-line:pipe-prefix @Pipe({ name: 'translate' }) class MockTranslatePipe implements PipeTransform { transform(key: string): string { diff --git a/src/app/thumbnail/thumbnail.component.ts b/src/app/thumbnail/thumbnail.component.ts index 21a5da3567..11dee37037 100644 --- a/src/app/thumbnail/thumbnail.component.ts +++ b/src/app/thumbnail/thumbnail.component.ts @@ -10,7 +10,7 @@ import { hasValue } from '../shared/empty.util'; @Component({ selector: 'ds-thumbnail', styleUrls: ['./thumbnail.component.scss'], - templateUrl: './thumbnail.component.html' + templateUrl: './thumbnail.component.html', }) export class ThumbnailComponent implements OnInit { /** From 21686c86df2b3a316fe087dc5c6cc531468698c2 Mon Sep 17 00:00:00 2001 From: Samuel Date: Tue, 25 May 2021 11:51:19 +0200 Subject: [PATCH 071/351] add support for multiple metadata fields to the MetadataRepresentationListComponent --- .../publication/publication.component.html | 2 +- .../untyped-item/untyped-item.component.html | 2 +- ...etadata-representation-list.component.spec.ts | 16 ++++++++++++---- .../metadata-representation-list.component.ts | 4 ++-- src/app/core/shared/dspace-object.model.ts | 4 ++-- .../item-pages/project/project.component.html | 2 +- 6 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/app/+item-page/simple/item-types/publication/publication.component.html b/src/app/+item-page/simple/item-types/publication/publication.component.html index ba73ed43be..27f5ff43a1 100644 --- a/src/app/+item-page/simple/item-types/publication/publication.component.html +++ b/src/app/+item-page/simple/item-types/publication/publication.component.html @@ -21,7 +21,7 @@ { comp = fixture.componentInstance; comp.parentItem = parentItem; comp.itemType = itemType; - comp.metadataField = metadataField; + comp.metadataFields = metadataFields; fixture.detectChanges(); })); - it('should load 2 ds-metadata-representation-loader components', () => { + it('should load 3 ds-metadata-representation-loader components', () => { const fields = fixture.debugElement.queryAll(By.css('ds-metadata-representation-loader')); - expect(fields.length).toBe(2); + expect(fields.length).toBe(3); }); it('should contain one page of items', () => { diff --git a/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.ts b/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.ts index 3db7abfe84..e5301dabc0 100644 --- a/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.ts +++ b/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.ts @@ -42,7 +42,7 @@ export class MetadataRepresentationListComponent extends AbstractIncrementalList /** * The metadata field to use for fetching metadata from the item */ - @Input() metadataField: string; + @Input() metadataFields: string[]; /** * An i18n label to use as a title for the list @@ -70,7 +70,7 @@ export class MetadataRepresentationListComponent extends AbstractIncrementalList * @param page The page to fetch */ getPage(page: number): Observable { - const metadata = this.parentItem.findMetadataSortedByPlace(this.metadataField); + const metadata = this.parentItem.findMetadataSortedByPlace(this.metadataFields); this.total = metadata.length; return this.resolveMetadataRepresentations(metadata, page); } diff --git a/src/app/core/shared/dspace-object.model.ts b/src/app/core/shared/dspace-object.model.ts index 9d1fba4f86..5ea2bced3d 100644 --- a/src/app/core/shared/dspace-object.model.ts +++ b/src/app/core/shared/dspace-object.model.ts @@ -163,8 +163,8 @@ export class DSpaceObject extends ListableObject implements CacheableObject { * Find metadata on a specific field and order all of them using their "place" property. * @param key */ - findMetadataSortedByPlace(key: string): MetadataValue[] { - return this.allMetadata([key]).sort((a: MetadataValue, b: MetadataValue) => { + findMetadataSortedByPlace(keyOrKeys: string | string[]): MetadataValue[] { + return this.allMetadata(keyOrKeys).sort((a: MetadataValue, b: MetadataValue) => { if (hasNoValue(a.place) && hasNoValue(b.place)) { return 0; } diff --git a/src/app/entity-groups/research-entities/item-pages/project/project.component.html b/src/app/entity-groups/research-entities/item-pages/project/project.component.html index 9967b940ac..c18fed78af 100644 --- a/src/app/entity-groups/research-entities/item-pages/project/project.component.html +++ b/src/app/entity-groups/research-entities/item-pages/project/project.component.html @@ -18,7 +18,7 @@ Date: Wed, 26 May 2021 22:39:06 +0200 Subject: [PATCH 072/351] Fix the rel name for the submissioncclicenseUrls-search link --- .../core/submission/submission-cc-license-url-data.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/core/submission/submission-cc-license-url-data.service.ts b/src/app/core/submission/submission-cc-license-url-data.service.ts index ba0baca2eb..0ca3853d0e 100644 --- a/src/app/core/submission/submission-cc-license-url-data.service.ts +++ b/src/app/core/submission/submission-cc-license-url-data.service.ts @@ -22,7 +22,7 @@ import { isNotEmpty } from '../../shared/empty.util'; @dataService(SUBMISSION_CC_LICENSE_URL) export class SubmissionCcLicenseUrlDataService extends DataService { - protected linkPath = 'submissioncclicenseUrl-search'; + protected linkPath = 'submissioncclicenseUrls-search'; constructor( protected comparator: DefaultChangeAnalyzer, From 060d0dd556ad9bbee790018fbb48f8815198093e Mon Sep 17 00:00:00 2001 From: Alessandro Martelli Date: Thu, 27 May 2021 09:43:29 +0200 Subject: [PATCH 073/351] [CST-4223] Creating a new submission should redirect to workspaceitem edit page --- .../submit/submission-submit.component.html | 10 ------- .../submission-submit.component.spec.ts | 27 ++++++++----------- .../submit/submission-submit.component.ts | 8 +----- 3 files changed, 12 insertions(+), 33 deletions(-) diff --git a/src/app/submission/submit/submission-submit.component.html b/src/app/submission/submit/submission-submit.component.html index a7680f07c2..e69de29bb2 100644 --- a/src/app/submission/submit/submission-submit.component.html +++ b/src/app/submission/submit/submission-submit.component.html @@ -1,10 +0,0 @@ -
-
- -
-
diff --git a/src/app/submission/submit/submission-submit.component.spec.ts b/src/app/submission/submit/submission-submit.component.spec.ts index 16e26e3b33..1b76082d1b 100644 --- a/src/app/submission/submit/submission-submit.component.spec.ts +++ b/src/app/submission/submit/submission-submit.component.spec.ts @@ -26,7 +26,6 @@ describe('SubmissionSubmitComponent Component', () => { let itemDataService: ItemDataService; let router: RouterStub; - const submissionId = '826'; const submissionObject: any = mockSubmissionObject; beforeEach(waitForAsync(() => { @@ -67,27 +66,23 @@ describe('SubmissionSubmitComponent Component', () => { router = null; }); - it('should init properly when a valid SubmissionObject has been retrieved',() => { - - submissionServiceStub.createSubmission.and.returnValue(observableOf(submissionObject)); - - fixture.detectChanges(); - - expect(comp.submissionId.toString()).toEqual(submissionId); - expect(comp.collectionId).toBe(submissionObject.collection.id); - expect(comp.selfUrl).toBe(submissionObject._links.self.href); - expect(comp.sections).toBe(submissionObject.sections); - expect(comp.submissionDefinition).toBe(submissionObject.submissionDefinition); - - }); - it('should redirect to mydspace when an empty SubmissionObject has been retrieved',() => { submissionServiceStub.createSubmission.and.returnValue(observableOf({})); fixture.detectChanges(); - expect(router.navigate).toHaveBeenCalled(); + expect(router.navigate).toHaveBeenCalledWith(['/mydspace']); + + }); + + it('should redirect to workspaceitem edit when a not empty SubmissionObject has been retrieved',() => { + + submissionServiceStub.createSubmission.and.returnValue(observableOf({ id: '1234'})); + + fixture.detectChanges(); + + expect(router.navigate).toHaveBeenCalledWith(['/workspaceitems', '1234', 'edit'], { replaceUrl: true}); }); diff --git a/src/app/submission/submit/submission-submit.component.ts b/src/app/submission/submit/submission-submit.component.ts index 0c2172368a..af1bf38539 100644 --- a/src/app/submission/submit/submission-submit.component.ts +++ b/src/app/submission/submit/submission-submit.component.ts @@ -122,13 +122,7 @@ export class SubmissionSubmitComponent implements OnDestroy, OnInit { this.notificationsService.info(null, this.translate.get('submission.general.cannot_submit')); this.router.navigate(['/mydspace']); } else { - this.collectionId = (submissionObject.collection as Collection).id; - this.sections = submissionObject.sections; - this.selfUrl = submissionObject._links.self.href; - this.submissionDefinition = (submissionObject.submissionDefinition as SubmissionDefinitionsModel); - this.submissionId = submissionObject.id; - this.itemLink$.next(submissionObject._links.item.href); - this.item = submissionObject.item as Item; + this.router.navigate(['/workspaceitems', submissionObject.id, 'edit'], { replaceUrl: true}); } } }), From 41c07e74ca5c3ad3a6a69492c40873c8b7fb7089 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Thu, 27 May 2021 08:59:32 +0200 Subject: [PATCH 074/351] 79597: Add input to toggle thumbnail max-width --- src/app/thumbnail/thumbnail.component.html | 9 +++++---- src/app/thumbnail/thumbnail.component.scss | 2 +- src/app/thumbnail/thumbnail.component.ts | 7 ++++++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/app/thumbnail/thumbnail.component.html b/src/app/thumbnail/thumbnail.component.html index cef88e0192..645bce9b80 100644 --- a/src/app/thumbnail/thumbnail.component.html +++ b/src/app/thumbnail/thumbnail.component.html @@ -1,8 +1,9 @@ -
- -
+
+ +
-
{{ placeholder | translate }}
+
{{ placeholder | translate }}
diff --git a/src/app/thumbnail/thumbnail.component.scss b/src/app/thumbnail/thumbnail.component.scss index a96147468c..b15238afac 100644 --- a/src/app/thumbnail/thumbnail.component.scss +++ b/src/app/thumbnail/thumbnail.component.scss @@ -1,4 +1,4 @@ -.thumbnail { +.limit-width { max-width: var(--ds-thumbnail-max-width); } diff --git a/src/app/thumbnail/thumbnail.component.ts b/src/app/thumbnail/thumbnail.component.ts index 11dee37037..30911644f7 100644 --- a/src/app/thumbnail/thumbnail.component.ts +++ b/src/app/thumbnail/thumbnail.component.ts @@ -5,7 +5,7 @@ import { hasValue } from '../shared/empty.util'; /** * This component renders a given Bitstream as a thumbnail. * One input parameter of type Bitstream is expected. - * If no Bitstream is provided, a holderjs image will be rendered instead. + * If no Bitstream is provided, a HTML placeholder will be rendered instead. */ @Component({ selector: 'ds-thumbnail', @@ -39,6 +39,11 @@ export class ThumbnailComponent implements OnInit { */ @Input() placeholder? = 'thumbnail.default.placeholder'; + /** + * Limit thumbnail width to --ds-thumbnail-max-width + */ + @Input() limitWidth? = true; + /** * Initialize the thumbnail. * Use a default image if no actual image is available. From 4f38821bb3ff269d890ebf381beb5f0ff701152a Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Thu, 27 May 2021 09:10:31 +0200 Subject: [PATCH 075/351] 79597: Replace ds-grid-thumbnail with ds-thumbnail --- ...ournal-issue-search-result-grid-element.component.html | 8 ++++---- ...urnal-volume-search-result-grid-element.component.html | 8 ++++---- .../journal-search-result-grid-element.component.html | 8 ++++---- .../org-unit-search-result-grid-element.component.html | 8 ++++---- .../person-search-result-grid-element.component.html | 8 ++++---- .../project-search-result-grid-element.component.html | 8 ++++---- .../collection-grid-element.component.html | 8 ++++---- .../community-grid-element.component.html | 8 ++++---- src/app/shared/object-grid/object-grid.component.scss | 2 +- .../collection-search-result-grid-element.component.html | 8 ++++---- .../community-search-result-grid-element.component.html | 8 ++++---- .../item/item-search-result-grid-element.component.html | 8 ++++---- 12 files changed, 45 insertions(+), 45 deletions(-) diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.html index df6c9e60c0..feb282d3a7 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.html +++ b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.html @@ -8,14 +8,14 @@ rel="noopener noreferrer" [routerLink]="[itemPageRoute]" class="card-img-top full-width">
- - + +
- - + +
diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.html index cdc19b7f14..aa2352b284 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.html +++ b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.html @@ -8,14 +8,14 @@ rel="noopener noreferrer" [routerLink]="[itemPageRoute]" class="card-img-top full-width">
- - + +
- - + +
diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.html index bacd657663..8fdad59827 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.html +++ b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.html @@ -8,14 +8,14 @@ rel="noopener noreferrer" [routerLink]="[itemPageRoute]" class="card-img-top full-width">
- - + +
- - + +
diff --git a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/org-unit/org-unit-search-result-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/org-unit/org-unit-search-result-grid-element.component.html index 2c3087d701..b8be58a603 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/org-unit/org-unit-search-result-grid-element.component.html +++ b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/org-unit/org-unit-search-result-grid-element.component.html @@ -8,14 +8,14 @@ rel="noopener noreferrer" [routerLink]="[itemPageRoute]" class="card-img-top full-width">
- - + +
- - + +
diff --git a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.html index 005fa9cc83..8281eb0e04 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.html +++ b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.html @@ -8,14 +8,14 @@ rel="noopener noreferrer" [routerLink]="[itemPageRoute]" class="card-img-top full-width">
- - + +
- - + +
diff --git a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.html index e84e8c49d0..88498a4d67 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.html +++ b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.html @@ -8,14 +8,14 @@ rel="noopener noreferrer" [routerLink]="[itemPageRoute]" class="card-img-top full-width">
- - + +
- - + +
diff --git a/src/app/shared/object-grid/collection-grid-element/collection-grid-element.component.html b/src/app/shared/object-grid/collection-grid-element/collection-grid-element.component.html index 9b9d174704..d47e897edc 100644 --- a/src/app/shared/object-grid/collection-grid-element/collection-grid-element.component.html +++ b/src/app/shared/object-grid/collection-grid-element/collection-grid-element.component.html @@ -1,11 +1,11 @@
- - + + - - + +

{{object.name}}

diff --git a/src/app/shared/object-grid/community-grid-element/community-grid-element.component.html b/src/app/shared/object-grid/community-grid-element/community-grid-element.component.html index f676ba303b..63097c4f57 100644 --- a/src/app/shared/object-grid/community-grid-element/community-grid-element.component.html +++ b/src/app/shared/object-grid/community-grid-element/community-grid-element.component.html @@ -1,11 +1,11 @@
- - + + - - + +

{{object.name}}

diff --git a/src/app/shared/object-grid/object-grid.component.scss b/src/app/shared/object-grid/object-grid.component.scss index 46675615f0..68a7f2f991 100644 --- a/src/app/shared/object-grid/object-grid.component.scss +++ b/src/app/shared/object-grid/object-grid.component.scss @@ -1,7 +1,7 @@ :host ::ng-deep { --ds-wrapper-grid-spacing: calc(var(--bs-spacer) / 2); - div.thumbnail > img { + div.thumbnail > .thumbnail-content { height: var(--ds-card-thumbnail-height); width: 100%; display: block; diff --git a/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.html b/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.html index f8c75fc0d4..739fa6c7a8 100644 --- a/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.html +++ b/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.html @@ -1,11 +1,11 @@
- - + + - - + +
diff --git a/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.html b/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.html index 8025213b3b..d8c253c8a9 100644 --- a/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.html +++ b/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.html @@ -1,11 +1,11 @@
- - + + - - + +
diff --git a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.html b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.html index 85aeb63a6b..bc16853721 100644 --- a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.html +++ b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.html @@ -6,14 +6,14 @@
- - + +
- - + +
From 6cbd9dc920c7937e4f5217ee40bd4b2242ecf328 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Thu, 27 May 2021 09:21:00 +0200 Subject: [PATCH 076/351] 79597: Remove GridThumbnailComponent --- .../grid-thumbnail.component.html | 3 - .../grid-thumbnail.component.scss | 0 .../grid-thumbnail.component.spec.ts | 50 ------------- .../grid-thumbnail.component.ts | 72 ------------------- src/app/shared/shared.module.ts | 5 +- 5 files changed, 1 insertion(+), 129 deletions(-) delete mode 100644 src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.html delete mode 100644 src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.scss delete mode 100644 src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.spec.ts delete mode 100644 src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.ts diff --git a/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.html b/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.html deleted file mode 100644 index 1df4026f83..0000000000 --- a/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.html +++ /dev/null @@ -1,3 +0,0 @@ -
- -
diff --git a/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.scss b/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.spec.ts b/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.spec.ts deleted file mode 100644 index 825a4d5c60..0000000000 --- a/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { DebugElement } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { Bitstream } from '../../../core/shared/bitstream.model'; -import { SafeUrlPipe } from '../../utils/safe-url-pipe'; - -import { GridThumbnailComponent } from './grid-thumbnail.component'; - -describe('GridThumbnailComponent', () => { - let comp: GridThumbnailComponent; - let fixture: ComponentFixture; - let de: DebugElement; - let el: HTMLElement; - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - declarations: [GridThumbnailComponent, SafeUrlPipe] - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(GridThumbnailComponent); - comp = fixture.componentInstance; // BannerComponent test instance - de = fixture.debugElement.query(By.css('div.thumbnail')); - el = de.nativeElement; - }); - - it('should display image', () => { - const thumbnail = new Bitstream(); - thumbnail._links = { - self: { href: 'self.url' }, - bundle: { href: 'bundle.url' }, - format: { href: 'format.url' }, - content: { href: 'content.url' }, - }; - comp.thumbnail = thumbnail; - fixture.detectChanges(); - const image: HTMLElement = de.query(By.css('img')).nativeElement; - expect(image.getAttribute('src')).toBe(comp.thumbnail._links.content.href); - }); - - it('should display placeholder', () => { - const thumbnail = new Bitstream(); - comp.thumbnail = thumbnail; - fixture.detectChanges(); - const image: HTMLElement = de.query(By.css('img')).nativeElement; - expect(image.getAttribute('src')).toBe(comp.defaultImage); - }); - -}); diff --git a/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.ts b/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.ts deleted file mode 100644 index 92d93686dc..0000000000 --- a/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { - Component, - Input, - OnChanges, - OnInit, - SimpleChanges, -} from '@angular/core'; -import { Bitstream } from '../../../core/shared/bitstream.model'; -import { hasValue } from '../../empty.util'; - -/** - * This component renders a given Bitstream as a thumbnail. - * One input parameter of type Bitstream is expected. - * If no Bitstream is provided, a holderjs image will be rendered instead. - */ - -@Component({ - selector: 'ds-grid-thumbnail', - styleUrls: ['./grid-thumbnail.component.scss'], - templateUrl: './grid-thumbnail.component.html', -}) -export class GridThumbnailComponent implements OnInit, OnChanges { - @Input() thumbnail: Bitstream; - - data: any = {}; - - /** - * The default 'holder.js' image - */ - @Input() defaultImage? = - 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9InllcyI/PjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMjYwIiBoZWlnaHQ9IjE4MCIgdmlld0JveD0iMCAwIDI2MCAxODAiIHByZXNlcnZlQXNwZWN0UmF0aW89Im5vbmUiPjwhLS0KU291cmNlIFVSTDogaG9sZGVyLmpzLzEwMCV4MTgwL3RleHQ6Tm8gVGh1bWJuYWlsCkNyZWF0ZWQgd2l0aCBIb2xkZXIuanMgMi42LjAuCkxlYXJuIG1vcmUgYXQgaHR0cDovL2hvbGRlcmpzLmNvbQooYykgMjAxMi0yMDE1IEl2YW4gTWFsb3BpbnNreSAtIGh0dHA6Ly9pbXNreS5jbwotLT48ZGVmcz48c3R5bGUgdHlwZT0idGV4dC9jc3MiPjwhW0NEQVRBWyNob2xkZXJfMTVmNzJmMmFlMGIgdGV4dCB7IGZpbGw6I0FBQUFBQTtmb250LXdlaWdodDpib2xkO2ZvbnQtZmFtaWx5OkFyaWFsLCBIZWx2ZXRpY2EsIE9wZW4gU2Fucywgc2Fucy1zZXJpZiwgbW9ub3NwYWNlO2ZvbnQtc2l6ZToxM3B0IH0gXV0+PC9zdHlsZT48L2RlZnM+PGcgaWQ9ImhvbGRlcl8xNWY3MmYyYWUwYiI+PHJlY3Qgd2lkdGg9IjI2MCIgaGVpZ2h0PSIxODAiIGZpbGw9IiNFRUVFRUUiLz48Zz48dGV4dCB4PSI3Mi4yNDIxODc1IiB5PSI5NiI+Tm8gVGh1bWJuYWlsPC90ZXh0PjwvZz48L2c+PC9zdmc+'; - - src: string; - - errorHandler(event) { - event.currentTarget.src = this.defaultImage; - } - - /** - * Initialize the src - */ - ngOnInit(): void { - this.src = this.defaultImage; - - this.checkThumbnail(this.thumbnail); - } - - /** - * If the old input is undefined and the new one is a bitsream then set src - */ - ngOnChanges(changes: SimpleChanges): void { - if ( - !hasValue(changes.thumbnail.previousValue) && - hasValue(changes.thumbnail.currentValue) - ) { - this.checkThumbnail(changes.thumbnail.currentValue); - } - } - - /** - * check if the Bitstream has any content than set the src - */ - checkThumbnail(thumbnail: Bitstream) { - if ( - hasValue(thumbnail) && - hasValue(thumbnail._links) && - thumbnail._links.content.href - ) { - this.src = thumbnail._links.content.href; - } - } -} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 4de0f2901e..c5a91bd02c 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -46,7 +46,6 @@ import { ThumbnailComponent } from '../thumbnail/thumbnail.component'; import { SearchFormComponent } from './search-form/search-form.component'; import { SearchResultGridElementComponent } from './object-grid/search-result-grid-element/search-result-grid-element.component'; import { ViewModeSwitchComponent } from './view-mode-switch/view-mode-switch.component'; -import { GridThumbnailComponent } from './object-grid/grid-thumbnail/grid-thumbnail.component'; import { VarDirective } from './utils/var.directive'; import { AuthNavMenuComponent } from './auth-nav-menu/auth-nav-menu.component'; import { LogOutComponent } from './log-out/log-out.component'; @@ -54,8 +53,7 @@ import { FormComponent } from './form/form.component'; import { DsDynamicOneboxComponent } from './form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component'; import { DsDynamicScrollableDropdownComponent } from './form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component'; import { - DsDynamicFormControlContainerComponent, - dsDynamicFormControlMapFn + DsDynamicFormControlContainerComponent, dsDynamicFormControlMapFn, } from './form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component'; import { DsDynamicFormComponent } from './form/builder/ds-dynamic-form-ui/ds-dynamic-form.component'; import { DragClickDirective } from './utils/drag-click.directive'; @@ -340,7 +338,6 @@ const COMPONENTS = [ SidebarFilterComponent, SidebarFilterSelectedOptionComponent, ThumbnailComponent, - GridThumbnailComponent, UploaderComponent, FileDropzoneNoUploaderComponent, ItemListPreviewComponent, From 9b95fc5de9b07adc038332f0c67848bf13448129 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Thu, 27 May 2021 11:18:40 +0200 Subject: [PATCH 077/351] 79597: Add space between rows in FullFileSectionComponent --- .../file-section/full-file-section.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/+item-page/full/field-components/file-section/full-file-section.component.html b/src/app/+item-page/full/field-components/file-section/full-file-section.component.html index bc1c63cc32..c5393055df 100644 --- a/src/app/+item-page/full/field-components/file-section/full-file-section.component.html +++ b/src/app/+item-page/full/field-components/file-section/full-file-section.component.html @@ -11,7 +11,7 @@ [retainScrollPosition]="true"> -
+
From 772ac123295403d7a1d04c78cd98ec438f6836c8 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Thu, 27 May 2021 11:20:39 +0200 Subject: [PATCH 078/351] create separate startup css to load before the theme is chosen --- angular.json | 1 + src/app/root/root.component.html | 2 +- src/index.html | 1 - src/styles/_global-styles.scss | 4 ---- src/styles/startup.scss | 8 ++++++++ 5 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 src/styles/startup.scss diff --git a/angular.json b/angular.json index 00799dc33c..d158577e06 100644 --- a/angular.json +++ b/angular.json @@ -47,6 +47,7 @@ "src/robots.txt" ], "styles": [ + "src/styles/startup.scss", { "input": "src/styles/base-theme.scss", "inject": false, diff --git a/src/app/root/root.component.html b/src/app/root/root.component.html index aef07d79f4..4768fb737c 100644 --- a/src/app/root/root.component.html +++ b/src/app/root/root.component.html @@ -24,7 +24,7 @@
-
+
diff --git a/src/index.html b/src/index.html index cb5d35e4f5..491a2d319c 100644 --- a/src/index.html +++ b/src/index.html @@ -7,7 +7,6 @@ DSpace - diff --git a/src/styles/_global-styles.scss b/src/styles/_global-styles.scss index d151bb3ba7..2098fc4f74 100644 --- a/src/styles/_global-styles.scss +++ b/src/styles/_global-styles.scss @@ -40,10 +40,6 @@ ds-admin-sidebar { z-index: var(--ds-sidebar-z-index); } -.ds-full-screen-loader { - height: 100vh; -} - .sticky-top { z-index: 0; } diff --git a/src/styles/startup.scss b/src/styles/startup.scss new file mode 100644 index 0000000000..c6866f6b45 --- /dev/null +++ b/src/styles/startup.scss @@ -0,0 +1,8 @@ +.ds-full-screen-loader { + height: 100vh; + text-align: center; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; +} From 899b30213e748422dcd2836535c5baba14809219 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Thu, 27 May 2021 13:26:40 +0200 Subject: [PATCH 079/351] show full screen loader when switching themes --- src/app/app.component.html | 4 ++- src/app/app.component.ts | 39 +++++++++++++++++++-------- src/app/root/root.component.html | 8 +++--- src/app/root/root.component.spec.ts | 2 ++ src/app/root/root.component.ts | 8 +++--- src/app/root/themed-root.component.ts | 6 ++--- 6 files changed, 44 insertions(+), 23 deletions(-) diff --git a/src/app/app.component.html b/src/app/app.component.html index a40e5ea163..fb9983a6ef 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1 +1,3 @@ - + diff --git a/src/app/app.component.ts b/src/app/app.component.ts index a596ba56cd..c9996f275a 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,4 +1,4 @@ -import { delay, map, distinctUntilChanged, filter, take } from 'rxjs/operators'; +import { delay, distinctUntilChanged, filter, take } from 'rxjs/operators'; import { AfterViewInit, ChangeDetectionStrategy, @@ -7,6 +7,7 @@ import { Inject, OnInit, Optional, + PLATFORM_ID, } from '@angular/core'; import { NavigationCancel, NavigationEnd, NavigationStart, Router } from '@angular/router'; @@ -32,7 +33,7 @@ import { LocaleService } from './core/locale/locale.service'; import { hasValue, isNotEmpty } from './shared/empty.util'; import { KlaroService } from './shared/cookies/klaro.service'; import { GoogleAnalyticsService } from './statistics/google-analytics.service'; -import { DOCUMENT } from '@angular/common'; +import { DOCUMENT, isPlatformBrowser } from '@angular/common'; import { ThemeService } from './shared/theme-support/theme.service'; import { BASE_THEME_NAME } from './shared/theme-support/theme.constants'; import { DEFAULT_THEME_CONFIG } from './shared/theme-support/theme.effects'; @@ -45,7 +46,6 @@ import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class AppComponent implements OnInit, AfterViewInit { - isLoading$: BehaviorSubject = new BehaviorSubject(true); sidebarVisible: Observable; slideSidebarOver: Observable; collapsedSidebarWidth: Observable; @@ -57,11 +57,23 @@ export class AppComponent implements OnInit, AfterViewInit { /** * Whether or not the authentication is currently blocking the UI */ - isNotAuthBlocking$: Observable; + isAuthBlocking$: Observable; + + /** + * Whether or not the app is in the process of rerouting + */ + isRouteLoading$: BehaviorSubject = new BehaviorSubject(true); + + /** + * Whether or not the theme is in the process of being swapped + */ + isThemeLoading$: BehaviorSubject = new BehaviorSubject(false); + constructor( @Inject(NativeWindowService) private _window: NativeWindowRef, - @Inject(DOCUMENT) private document: any, + @Inject(DOCUMENT) private document: any, + @Inject(PLATFORM_ID) private platformId: any, private themeService: ThemeService, private translate: TranslateService, private store: Store, @@ -83,6 +95,10 @@ export class AppComponent implements OnInit, AfterViewInit { this.models = models; this.themeService.getThemeName$().subscribe((themeName: string) => { + if (isPlatformBrowser(this.platformId)) { + // the theme css will never download server side, so this should only happen on the browser + this.isThemeLoading$.next(true); + } if (hasValue(themeName)) { this.setThemeCss(themeName); } else if (hasValue(DEFAULT_THEME_CONFIG)) { @@ -118,13 +134,12 @@ export class AppComponent implements OnInit, AfterViewInit { } ngOnInit() { - this.isNotAuthBlocking$ = this.store.pipe(select(isAuthenticationBlocking)).pipe( - map((isBlocking: boolean) => isBlocking === false), + this.isAuthBlocking$ = this.store.pipe(select(isAuthenticationBlocking)).pipe( distinctUntilChanged() ); - this.isNotAuthBlocking$ + this.isAuthBlocking$ .pipe( - filter((notBlocking: boolean) => notBlocking), + filter((isBlocking: boolean) => isBlocking === false), take(1) ).subscribe(() => this.initializeKlaro()); @@ -156,12 +171,12 @@ export class AppComponent implements OnInit, AfterViewInit { delay(0) ).subscribe((event) => { if (event instanceof NavigationStart) { - this.isLoading$.next(true); + this.isRouteLoading$.next(true); } else if ( event instanceof NavigationEnd || event instanceof NavigationCancel ) { - this.isLoading$.next(false); + this.isRouteLoading$.next(false); } }); } @@ -209,6 +224,8 @@ export class AppComponent implements OnInit, AfterViewInit { } }); } + // the fact that this callback is used, proves we're on the browser. + this.isThemeLoading$.next(false); }; head.appendChild(link); } diff --git a/src/app/root/root.component.html b/src/app/root/root.component.html index 4768fb737c..8b84fabf14 100644 --- a/src/app/root/root.component.html +++ b/src/app/root/root.component.html @@ -1,4 +1,4 @@ -
+
-
+
-
+
@@ -23,7 +23,7 @@
- +
diff --git a/src/app/root/root.component.spec.ts b/src/app/root/root.component.spec.ts index c945a35764..81b22592d6 100644 --- a/src/app/root/root.component.spec.ts +++ b/src/app/root/root.component.spec.ts @@ -27,6 +27,7 @@ import { provideMockStore } from '@ngrx/store/testing'; import { RouteService } from '../core/services/route.service'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { MenuServiceStub } from '../shared/testing/menu-service.stub'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; describe('RootComponent', () => { let component: RootComponent; @@ -36,6 +37,7 @@ describe('RootComponent', () => { await TestBed.configureTestingModule({ imports: [ CommonModule, + NoopAnimationsModule, StoreModule.forRoot(authReducer, storeModuleConfig), TranslateModule.forRoot({ loader: { diff --git a/src/app/root/root.component.ts b/src/app/root/root.component.ts index f9c475a8fa..576a0152be 100644 --- a/src/app/root/root.component.ts +++ b/src/app/root/root.component.ts @@ -38,14 +38,14 @@ export class RootComponent implements OnInit { models; /** - * Whether or not the authentication is currently blocking the UI + * Whether or not to show a full screen loader */ - @Input() isNotAuthBlocking: boolean; + @Input() shouldShowFullscreenLoader: boolean; /** - * Whether or not the the application is loading; + * Whether or not to show a loader across the router outlet */ - @Input() isLoading: boolean; + @Input() shouldShowRouteLoader: boolean; constructor( @Inject(NativeWindowService) private _window: NativeWindowRef, diff --git a/src/app/root/themed-root.component.ts b/src/app/root/themed-root.component.ts index 43aacc416f..5dfa1edf31 100644 --- a/src/app/root/themed-root.component.ts +++ b/src/app/root/themed-root.component.ts @@ -11,14 +11,14 @@ export class ThemedRootComponent extends ThemedComponent { /** * Whether or not the authentication is currently blocking the UI */ - @Input() isNotAuthBlocking: boolean; + @Input() shouldShowFullscreenLoader: boolean; /** * Whether or not the the application is loading; */ - @Input() isLoading: boolean; + @Input() shouldShowRouteLoader: boolean; - protected inAndOutputNames: (keyof RootComponent & keyof this)[] = ['isLoading', 'isNotAuthBlocking']; + protected inAndOutputNames: (keyof RootComponent & keyof this)[] = ['shouldShowRouteLoader', 'shouldShowFullscreenLoader']; protected getComponentName(): string { return 'RootComponent'; From c717fc5ec815d0b29b244781b1efcf2e5b0ac0b7 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Thu, 27 May 2021 15:51:54 +0200 Subject: [PATCH 080/351] 79597: Fix thumbnails ~ item page direct request --- .../metadata-field-wrapper.component.html | 2 +- .../publication/publication.component.html | 2 +- .../item-types/shared/item.component.ts | 21 ++++++---- .../untyped-item/untyped-item.component.html | 2 +- .../journal-issue.component.html | 2 +- .../journal-volume.component.html | 2 +- .../item-pages/journal/journal.component.html | 2 +- ...-search-result-grid-element.component.html | 2 +- .../org-unit/org-unit.component.html | 2 +- .../item-pages/person/person.component.html | 2 +- .../item-pages/project/project.component.html | 2 +- src/app/thumbnail/thumbnail.component.html | 18 ++++---- src/app/thumbnail/thumbnail.component.ts | 41 ++++++++++++++----- 13 files changed, 66 insertions(+), 34 deletions(-) diff --git a/src/app/+item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.html b/src/app/+item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.html index c791cec600..a41aa0d67a 100644 --- a/src/app/+item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.html +++ b/src/app/+item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.html @@ -1,4 +1,4 @@ -
+
{{ label }}
diff --git a/src/app/+item-page/simple/item-types/publication/publication.component.html b/src/app/+item-page/simple/item-types/publication/publication.component.html index 73219cbb8f..0758f7cda4 100644 --- a/src/app/+item-page/simple/item-types/publication/publication.component.html +++ b/src/app/+item-page/simple/item-types/publication/publication.component.html @@ -10,7 +10,7 @@
- + diff --git a/src/app/+item-page/simple/item-types/shared/item.component.ts b/src/app/+item-page/simple/item-types/shared/item.component.ts index 120eda930f..8763d8c899 100644 --- a/src/app/+item-page/simple/item-types/shared/item.component.ts +++ b/src/app/+item-page/simple/item-types/shared/item.component.ts @@ -4,8 +4,10 @@ import { environment } from '../../../../../environments/environment'; import { BitstreamDataService } from '../../../../core/data/bitstream-data.service'; import { Bitstream } from '../../../../core/shared/bitstream.model'; import { Item } from '../../../../core/shared/item.model'; -import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators'; +import { getFirstSucceededRemoteDataPayload, takeUntilCompletedRemoteData } from '../../../../core/shared/operators'; import { getItemPageRoute } from '../../../item-page-routing-paths'; +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; +import { RemoteData } from '../../../../core/data/remote-data'; @Component({ selector: 'ds-item', @@ -17,6 +19,11 @@ import { getItemPageRoute } from '../../../item-page-routing-paths'; export class ItemComponent implements OnInit { @Input() object: Item; + /** + * The Item's thumbnail + */ + thumbnail$: BehaviorSubject>; + /** * Route to the item page */ @@ -28,12 +35,12 @@ export class ItemComponent implements OnInit { ngOnInit(): void { this.itemPageRoute = getItemPageRoute(this.object); - } - // TODO refactor to return RemoteData, and thumbnail template to deal with loading - getThumbnail(): Observable { - return this.bitstreamDataService.getThumbnailFor(this.object).pipe( - getFirstSucceededRemoteDataPayload() - ); + this.thumbnail$ = new BehaviorSubject>(undefined); + this.bitstreamDataService.getThumbnailFor(this.object).pipe( + takeUntilCompletedRemoteData(), + ).subscribe((rd: RemoteData) => { + this.thumbnail$.next(rd); + }); } } diff --git a/src/app/+item-page/simple/item-types/untyped-item/untyped-item.component.html b/src/app/+item-page/simple/item-types/untyped-item/untyped-item.component.html index 8d46a2c5a9..3e20da3094 100644 --- a/src/app/+item-page/simple/item-types/untyped-item/untyped-item.component.html +++ b/src/app/+item-page/simple/item-types/untyped-item/untyped-item.component.html @@ -10,7 +10,7 @@
- + diff --git a/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html b/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html index 5749c5797d..af87daa243 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html +++ b/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html @@ -9,7 +9,7 @@
- +
- +
- +
- +
diff --git a/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html b/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html index 14d56d4104..67b79163ed 100644 --- a/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html +++ b/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html @@ -9,7 +9,7 @@
-
- diff --git a/src/app/entity-groups/research-entities/item-pages/project/project.component.html b/src/app/entity-groups/research-entities/item-pages/project/project.component.html index 0590c8bfe4..facecb4996 100644 --- a/src/app/entity-groups/research-entities/item-pages/project/project.component.html +++ b/src/app/entity-groups/research-entities/item-pages/project/project.component.html @@ -10,7 +10,7 @@
diff --git a/src/app/thumbnail/thumbnail.component.html b/src/app/thumbnail/thumbnail.component.html index 645bce9b80..593c1c10b8 100644 --- a/src/app/thumbnail/thumbnail.component.html +++ b/src/app/thumbnail/thumbnail.component.html @@ -1,10 +1,14 @@
- -
-
-
{{ placeholder | translate }}
+ + text-content + + + +
+
+
{{ placeholder | translate }}
+
-
+
- diff --git a/src/app/thumbnail/thumbnail.component.ts b/src/app/thumbnail/thumbnail.component.ts index 30911644f7..ec1e7eb368 100644 --- a/src/app/thumbnail/thumbnail.component.ts +++ b/src/app/thumbnail/thumbnail.component.ts @@ -1,6 +1,8 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input, OnChanges } from '@angular/core'; import { Bitstream } from '../core/shared/bitstream.model'; import { hasValue } from '../shared/empty.util'; +import { RemoteData } from '../core/data/remote-data'; +import { BITSTREAM } from '../core/shared/bitstream.resource-type'; /** * This component renders a given Bitstream as a thumbnail. @@ -12,11 +14,11 @@ import { hasValue } from '../shared/empty.util'; styleUrls: ['./thumbnail.component.scss'], templateUrl: './thumbnail.component.html', }) -export class ThumbnailComponent implements OnInit { +export class ThumbnailComponent implements OnChanges { /** * The thumbnail Bitstream */ - @Input() thumbnail: Bitstream; + @Input() thumbnail: Bitstream | RemoteData; /** * The default image, used if the thumbnail isn't set or can't be downloaded. @@ -27,7 +29,7 @@ export class ThumbnailComponent implements OnInit { /** * The src attribute used in the template to render the image. */ - src: string; + src: string = null; /** * i18n key of thumbnail alt text @@ -44,18 +46,37 @@ export class ThumbnailComponent implements OnInit { */ @Input() limitWidth? = true; + isLoading: boolean; + /** - * Initialize the thumbnail. + * Resolve the thumbnail. * Use a default image if no actual image is available. */ - ngOnInit(): void { - if (hasValue(this.thumbnail) && hasValue(this.thumbnail._links) - && hasValue(this.thumbnail._links.content) - && this.thumbnail._links.content.href) { - this.src = this.thumbnail._links.content.href; + ngOnChanges(): void { + if (this.thumbnail === undefined || this.thumbnail === null) { + return; + } + if (this.thumbnail instanceof Bitstream) { + this.resolveThumbnail(this.thumbnail as Bitstream) + } else { + const thumbnailRD = this.thumbnail as RemoteData; + if (thumbnailRD.isLoading) { + this.isLoading = true; + } else { + this.resolveThumbnail(thumbnailRD.payload as Bitstream); + } + } + } + + private resolveThumbnail(thumbnail: Bitstream): void { + if (hasValue(thumbnail) && hasValue(thumbnail._links) + && hasValue(thumbnail._links.content) + && thumbnail._links.content.href) { + this.src = thumbnail._links.content.href; } else { this.src = this.defaultImage; } + this.isLoading = false; } /** From bcfb890e1aa1dae27e87fd57afc1cd6232532f0e Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Thu, 27 May 2021 16:34:52 +0200 Subject: [PATCH 081/351] 79597: Update unit tests --- src/app/thumbnail/thumbnail.component.html | 2 +- src/app/thumbnail/thumbnail.component.spec.ts | 125 +++++++++++++----- 2 files changed, 96 insertions(+), 31 deletions(-) diff --git a/src/app/thumbnail/thumbnail.component.html b/src/app/thumbnail/thumbnail.component.html index 593c1c10b8..a7f4c51510 100644 --- a/src/app/thumbnail/thumbnail.component.html +++ b/src/app/thumbnail/thumbnail.component.html @@ -1,4 +1,4 @@ -
+
text-content diff --git a/src/app/thumbnail/thumbnail.component.spec.ts b/src/app/thumbnail/thumbnail.component.spec.ts index 82eadc04de..bc9d159750 100644 --- a/src/app/thumbnail/thumbnail.component.spec.ts +++ b/src/app/thumbnail/thumbnail.component.spec.ts @@ -5,6 +5,10 @@ import { Bitstream } from '../core/shared/bitstream.model'; import { SafeUrlPipe } from '../shared/utils/safe-url-pipe'; import { ThumbnailComponent } from './thumbnail.component'; +import { RemoteData } from '../core/data/remote-data'; +import { + createFailedRemoteDataObject, createPendingRemoteDataObject, createSuccessfulRemoteDataObject, +} from '../shared/remote-data.utils'; // tslint:disable-next-line:pipe-prefix @Pipe({ name: 'translate' }) @@ -28,40 +32,12 @@ describe('ThumbnailComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(ThumbnailComponent); - comp = fixture.componentInstance; // BannerComponent test instance + comp = fixture.componentInstance; // ThumbnailComponent test instance de = fixture.debugElement.query(By.css('div.thumbnail')); el = de.nativeElement; }); - describe('when the thumbnail exists', () => { - it('should display an image', () => { - const thumbnail = new Bitstream(); - thumbnail._links = { - self: { href: 'self.url' }, - bundle: { href: 'bundle.url' }, - format: { href: 'format.url' }, - content: { href: 'content.url' }, - }; - comp.thumbnail = thumbnail; - fixture.detectChanges(); - const image: HTMLElement = de.query(By.css('img')).nativeElement; - expect(image.getAttribute('src')).toBe(comp.thumbnail._links.content.href); - }); - it('should include the alt text', () => { - const thumbnail = new Bitstream(); - thumbnail._links = { - self: { href: 'self.url' }, - bundle: { href: 'bundle.url' }, - format: { href: 'format.url' }, - content: { href: 'content.url' }, - }; - comp.thumbnail = thumbnail; - fixture.detectChanges(); - const image: HTMLElement = de.query(By.css('img')).nativeElement; - expect(image.getAttribute('alt')).toBe('TRANSLATED ' + comp.alt); - }); - }); - describe(`when the thumbnail doesn't exist`, () => { + const withoutThumbnail = () => { describe('and there is a default image', () => { it('should display the default image', () => { comp.src = 'http://bit.stream'; @@ -73,6 +49,7 @@ describe('ThumbnailComponent', () => { comp.src = 'http://bit.stream'; comp.defaultImage = 'http://default.img'; comp.errorHandler(); + comp.ngOnChanges(); fixture.detectChanges(); const image: HTMLElement = de.query(By.css('img')).nativeElement; expect(image.getAttribute('alt')).toBe('TRANSLATED ' + comp.alt); @@ -84,10 +61,98 @@ describe('ThumbnailComponent', () => { comp.errorHandler(); expect(comp.src).toBe(null); + comp.ngOnChanges(); fixture.detectChanges(); const placeholder = fixture.debugElement.query(By.css('div.thumbnail-placeholder')).nativeElement; expect(placeholder.innerHTML).toBe('TRANSLATED ' + comp.placeholder); }); }); + }; + + describe('with thumbnail as Bitstream', () => { + let thumbnail: Bitstream; + beforeEach(() => { + thumbnail = new Bitstream(); + thumbnail._links = { + self: { href: 'self.url' }, + bundle: { href: 'bundle.url' }, + format: { href: 'format.url' }, + content: { href: 'content.url' }, + }; + }); + + it('should display an image', () => { + comp.thumbnail = thumbnail; + comp.ngOnChanges(); + fixture.detectChanges(); + const image: HTMLElement = de.query(By.css('img')).nativeElement; + expect(image.getAttribute('src')).toBe(comp.thumbnail._links.content.href); + }); + + it('should include the alt text', () => { + comp.thumbnail = thumbnail; + comp.ngOnChanges(); + fixture.detectChanges(); + const image: HTMLElement = de.query(By.css('img')).nativeElement; + expect(image.getAttribute('alt')).toBe('TRANSLATED ' + comp.alt); + }); + + describe('when there is no thumbnail', () => { + withoutThumbnail(); + }); + }); + + describe('with thumbnail as RemoteData', () => { + let thumbnail: RemoteData; + + describe('while loading', () => { + beforeEach(() => { + thumbnail = createPendingRemoteDataObject(); + }); + + it('should show a loading animation', () => { + comp.thumbnail = thumbnail; + comp.ngOnChanges(); + fixture.detectChanges(); + expect(de.query(By.css('ds-loading'))).toBeTruthy(); + }); + }); + + describe('when there is a thumbnail', () => { + beforeEach(() => { + const bitstream = new Bitstream(); + bitstream._links = { + self: { href: 'self.url' }, + bundle: { href: 'bundle.url' }, + format: { href: 'format.url' }, + content: { href: 'content.url' }, + }; + thumbnail = createSuccessfulRemoteDataObject(bitstream); + }); + + it('should display an image', () => { + comp.thumbnail = thumbnail; + comp.ngOnChanges(); + fixture.detectChanges(); + const image: HTMLElement = de.query(By.css('img')).nativeElement; + expect(image.getAttribute('src')).toBe(comp.thumbnail.payload._links.content.href); + }); + + it('should display the alt text', () => { + comp.thumbnail = thumbnail; + comp.ngOnChanges(); + fixture.detectChanges(); + const image: HTMLElement = de.query(By.css('img')).nativeElement; + expect(image.getAttribute('alt')).toBe('TRANSLATED ' + comp.alt); + }); + }); + + describe('when there is no thumbnail', () => { + beforeEach(() => { + thumbnail = createFailedRemoteDataObject(); + }); + + withoutThumbnail(); + }); }); }); From 120ecc6988cec452ab5c897155b712c368ed88df Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Thu, 27 May 2021 17:09:57 +0200 Subject: [PATCH 082/351] 79597: Fix ds-metadata-field-wrapper for thumbnails --- .../metadata-field-wrapper.component.html | 2 +- .../metadata-field-wrapper.component.ts | 7 +------ .../item-types/publication/publication.component.html | 2 +- .../item-types/untyped-item/untyped-item.component.html | 2 +- .../item-pages/journal-issue/journal-issue.component.html | 2 +- .../journal-volume/journal-volume.component.html | 2 +- .../item-pages/journal/journal.component.html | 2 +- .../item-pages/org-unit/org-unit.component.html | 2 +- .../item-pages/person/person.component.html | 2 +- .../item-pages/project/project.component.html | 2 +- .../item-detail-preview/item-detail-preview.component.html | 2 +- src/app/thumbnail/thumbnail.component.html | 2 +- src/app/thumbnail/thumbnail.component.ts | 3 +-- 13 files changed, 13 insertions(+), 19 deletions(-) diff --git a/src/app/+item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.html b/src/app/+item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.html index a41aa0d67a..d69f87883b 100644 --- a/src/app/+item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.html +++ b/src/app/+item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.html @@ -1,4 +1,4 @@ -
+
{{ label }}
diff --git a/src/app/+item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.ts b/src/app/+item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.ts index 8af108cceb..8c4e200423 100644 --- a/src/app/+item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.ts +++ b/src/app/+item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.ts @@ -17,10 +17,5 @@ export class MetadataFieldWrapperComponent { */ @Input() label: string; - /** - * Make hasNoValue() available in the template - */ - hasNoValue(o: any): boolean { - return hasNoValue(o); - } + @Input() hideIfNoTextContent = true; } diff --git a/src/app/+item-page/simple/item-types/publication/publication.component.html b/src/app/+item-page/simple/item-types/publication/publication.component.html index 0758f7cda4..57b460c814 100644 --- a/src/app/+item-page/simple/item-types/publication/publication.component.html +++ b/src/app/+item-page/simple/item-types/publication/publication.component.html @@ -9,7 +9,7 @@
- + diff --git a/src/app/+item-page/simple/item-types/untyped-item/untyped-item.component.html b/src/app/+item-page/simple/item-types/untyped-item/untyped-item.component.html index 3e20da3094..7ae9a1a909 100644 --- a/src/app/+item-page/simple/item-types/untyped-item/untyped-item.component.html +++ b/src/app/+item-page/simple/item-types/untyped-item/untyped-item.component.html @@ -9,7 +9,7 @@
- + diff --git a/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html b/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html index af87daa243..8e357140d8 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html +++ b/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html @@ -8,7 +8,7 @@
- +
- +
- +
- +
- +
- +
- + diff --git a/src/app/thumbnail/thumbnail.component.html b/src/app/thumbnail/thumbnail.component.html index a7f4c51510..bf70928392 100644 --- a/src/app/thumbnail/thumbnail.component.html +++ b/src/app/thumbnail/thumbnail.component.html @@ -1,4 +1,4 @@ -
+
text-content diff --git a/src/app/thumbnail/thumbnail.component.ts b/src/app/thumbnail/thumbnail.component.ts index ec1e7eb368..3e122cde78 100644 --- a/src/app/thumbnail/thumbnail.component.ts +++ b/src/app/thumbnail/thumbnail.component.ts @@ -2,7 +2,6 @@ import { Component, Input, OnChanges } from '@angular/core'; import { Bitstream } from '../core/shared/bitstream.model'; import { hasValue } from '../shared/empty.util'; import { RemoteData } from '../core/data/remote-data'; -import { BITSTREAM } from '../core/shared/bitstream.resource-type'; /** * This component renders a given Bitstream as a thumbnail. @@ -57,7 +56,7 @@ export class ThumbnailComponent implements OnChanges { return; } if (this.thumbnail instanceof Bitstream) { - this.resolveThumbnail(this.thumbnail as Bitstream) + this.resolveThumbnail(this.thumbnail as Bitstream); } else { const thumbnailRD = this.thumbnail as RemoteData; if (thumbnailRD.isLoading) { From 4b238e18423f97583e3454e3a0947395e6fcdc18 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Thu, 27 May 2021 18:03:43 +0200 Subject: [PATCH 083/351] 79597: Update ds-metadata-field-wrapper unit tests --- .../metadata-field-wrapper.component.spec.ts | 109 +++++++++++------- 1 file changed, 65 insertions(+), 44 deletions(-) diff --git a/src/app/+item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.spec.ts b/src/app/+item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.spec.ts index 9c62b80cad..e17e5397b7 100644 --- a/src/app/+item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.spec.ts +++ b/src/app/+item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.spec.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, Input } from '@angular/core'; import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; import { MetadataFieldWrapperComponent } from './metadata-field-wrapper.component'; @@ -6,35 +6,34 @@ import { MetadataFieldWrapperComponent } from './metadata-field-wrapper.componen /* tslint:disable:max-classes-per-file */ @Component({ selector: 'ds-component-without-content', - template: '\n' + + template: '\n' + '' }) -class NoContentComponent {} +class NoContentComponent { + public hideIfNoTextContent = true; +} @Component({ selector: 'ds-component-with-empty-spans', - template: '\n' + + template: '\n' + ' \n' + ' \n' + '' }) -class SpanContentComponent {} +class SpanContentComponent { + @Input() hideIfNoTextContent = true; +} @Component({ selector: 'ds-component-with-text', - template: '\n' + + template: '\n' + ' The quick brown fox jumps over the lazy dog\n' + '' }) -class TextContentComponent {} +class TextContentComponent { + @Input() hideIfNoTextContent = true; +} -@Component({ - selector: 'ds-component-with-image', - template: '\n' + - ' an alt text\n' + - '' -}) -class ImgContentComponent {} /* tslint:enable:max-classes-per-file */ describe('MetadataFieldWrapperComponent', () => { @@ -43,7 +42,7 @@ describe('MetadataFieldWrapperComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [MetadataFieldWrapperComponent, NoContentComponent, SpanContentComponent, TextContentComponent, ImgContentComponent] + declarations: [MetadataFieldWrapperComponent, NoContentComponent, SpanContentComponent, TextContentComponent] }).compileComponents(); })); @@ -58,38 +57,60 @@ describe('MetadataFieldWrapperComponent', () => { expect(component).toBeDefined(); }); - it('should not show the component when there is no content', () => { - const parentFixture = TestBed.createComponent(NoContentComponent); - parentFixture.detectChanges(); - const parentNative = parentFixture.nativeElement; - const nativeWrapper = parentNative.querySelector(wrapperSelector); - expect(nativeWrapper.classList.contains('d-none')).toBe(true); + describe('with hideIfNoTextContent=true', () => { + it('should not show the component when there is no content', () => { + const parentFixture = TestBed.createComponent(NoContentComponent); + parentFixture.detectChanges(); + const parentNative = parentFixture.nativeElement; + const nativeWrapper = parentNative.querySelector(wrapperSelector); + expect(nativeWrapper.classList.contains('d-none')).toBe(true); + }); + + it('should not show the component when there is no text content', () => { + const parentFixture = TestBed.createComponent(SpanContentComponent); + parentFixture.detectChanges(); + const parentNative = parentFixture.nativeElement; + const nativeWrapper = parentNative.querySelector(wrapperSelector); + expect(nativeWrapper.classList.contains('d-none')).toBe(true); + }); + + it('should show the component when there is text content', () => { + const parentFixture = TestBed.createComponent(TextContentComponent); + parentFixture.detectChanges(); + const parentNative = parentFixture.nativeElement; + const nativeWrapper = parentNative.querySelector(wrapperSelector); + parentFixture.detectChanges(); + expect(nativeWrapper.classList.contains('d-none')).toBe(false); + }); }); - it('should not show the component when there is DOM content but not text or an image', () => { - const parentFixture = TestBed.createComponent(SpanContentComponent); - parentFixture.detectChanges(); - const parentNative = parentFixture.nativeElement; - const nativeWrapper = parentNative.querySelector(wrapperSelector); - expect(nativeWrapper.classList.contains('d-none')).toBe(true); - }); + describe('with hideIfNoTextContent=false', () => { + it('should show the component when there is no content', () => { + const parentFixture = TestBed.createComponent(NoContentComponent); + parentFixture.componentInstance.hideIfNoTextContent = false; + parentFixture.detectChanges(); + const parentNative = parentFixture.nativeElement; + const nativeWrapper = parentNative.querySelector(wrapperSelector); + expect(nativeWrapper.classList.contains('d-none')).toBe(false); + }); - it('should show the component when there is text content', () => { - const parentFixture = TestBed.createComponent(TextContentComponent); - parentFixture.detectChanges(); - const parentNative = parentFixture.nativeElement; - const nativeWrapper = parentNative.querySelector(wrapperSelector); - parentFixture.detectChanges(); - expect(nativeWrapper.classList.contains('d-none')).toBe(false); - }); + it('should show the component when there is no text content', () => { + const parentFixture = TestBed.createComponent(SpanContentComponent); + parentFixture.componentInstance.hideIfNoTextContent = false; + parentFixture.detectChanges(); + const parentNative = parentFixture.nativeElement; + const nativeWrapper = parentNative.querySelector(wrapperSelector); + expect(nativeWrapper.classList.contains('d-none')).toBe(false); + }); - it('should show the component when there is img content', () => { - const parentFixture = TestBed.createComponent(ImgContentComponent); - parentFixture.detectChanges(); - const parentNative = parentFixture.nativeElement; - const nativeWrapper = parentNative.querySelector(wrapperSelector); - parentFixture.detectChanges(); - expect(nativeWrapper.classList.contains('d-none')).toBe(false); + it('should show the component when there is text content', () => { + const parentFixture = TestBed.createComponent(TextContentComponent); + parentFixture.componentInstance.hideIfNoTextContent = false; + parentFixture.detectChanges(); + const parentNative = parentFixture.nativeElement; + const nativeWrapper = parentNative.querySelector(wrapperSelector); + parentFixture.detectChanges(); + expect(nativeWrapper.classList.contains('d-none')).toBe(false); + }); }); - }); From bcfecc53a150911d6780f4787cc6d3c88134c87b Mon Sep 17 00:00:00 2001 From: Samuel Date: Fri, 28 May 2021 11:41:55 +0200 Subject: [PATCH 084/351] add support for multiple metadata fields to the MetadataRepresentationListComponent - fix bug when unauthorized for related item --- ...data-representation-list.component.spec.ts | 54 +++++++++++++++---- .../metadata-representation-list.component.ts | 6 ++- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.spec.ts b/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.spec.ts index e1c934246c..6023aa7b6d 100644 --- a/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.spec.ts +++ b/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.spec.ts @@ -5,7 +5,7 @@ import { MetadataRepresentationListComponent } from './metadata-representation-l import { RelationshipService } from '../../../core/data/relationship.service'; import { Item } from '../../../core/shared/item.model'; import { Relationship } from '../../../core/shared/item-relationships/relationship.model'; -import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { createSuccessfulRemoteDataObject$, createFailedRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { TranslateModule } from '@ngx-translate/core'; import { VarDirective } from '../../../shared/utils/var.directive'; import { of as observableOf } from 'rxjs'; @@ -35,6 +35,12 @@ const parentItem: Item = Object.assign(new Item(), { authority: 'virtual::related-creator', place: 3, }, + { + language: null, + value: 'Related Creator with authority - unauthorized', + authority: 'virtual::related-creator-unauthorized', + place: 4, + }, ], 'dc.title': [ { @@ -55,21 +61,49 @@ const relatedAuthor: Item = Object.assign(new Item(), { ] } }); -const relation: Relationship = Object.assign(new Relationship(), { +const relatedCreator: Item = Object.assign(new Item(), { + id: 'related-creator', + metadata: { + 'dc.title': [ + { + language: null, + value: 'Related Creator' + } + ], + 'dspace.entity.type': 'Person', + } +}); +const authorRelation: Relationship = Object.assign(new Relationship(), { leftItem: createSuccessfulRemoteDataObject$(parentItem), rightItem: createSuccessfulRemoteDataObject$(relatedAuthor) }); -let relationshipService: RelationshipService; +const creatorRelation: Relationship = Object.assign(new Relationship(), { + leftItem: createSuccessfulRemoteDataObject$(parentItem), + rightItem: createSuccessfulRemoteDataObject$(relatedCreator), +}); +const creatorRelationUnauthorized: Relationship = Object.assign(new Relationship(), { + leftItem: createSuccessfulRemoteDataObject$(parentItem), + rightItem: createFailedRemoteDataObject$('Unauthorized', 401), +}); +let relationshipService; describe('MetadataRepresentationListComponent', () => { let comp: MetadataRepresentationListComponent; let fixture: ComponentFixture; - relationshipService = jasmine.createSpyObj('relationshipService', - { - findById: createSuccessfulRemoteDataObject$(relation) - } - ); + relationshipService = { + findById: (id: string) => { + if (id === 'related-author') { + return createSuccessfulRemoteDataObject$(authorRelation); + } + if (id === 'related-creator') { + return createSuccessfulRemoteDataObject$(creatorRelation); + } + if (id === 'related-creator-unauthorized') { + return createSuccessfulRemoteDataObject$(creatorRelationUnauthorized); + } + }, + }; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -93,9 +127,9 @@ describe('MetadataRepresentationListComponent', () => { fixture.detectChanges(); })); - it('should load 3 ds-metadata-representation-loader components', () => { + it('should load 4 ds-metadata-representation-loader components', () => { const fields = fixture.debugElement.queryAll(By.css('ds-metadata-representation-loader')); - expect(fields.length).toBe(3); + expect(fields.length).toBe(4); }); it('should contain one page of items', () => { diff --git a/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.ts b/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.ts index e5301dabc0..620c63ed62 100644 --- a/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.ts +++ b/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.ts @@ -91,9 +91,11 @@ export class MetadataRepresentationListComponent extends AbstractIncrementalList getFirstSucceededRemoteData(), switchMap((relRD: RemoteData) => observableCombineLatest(relRD.payload.leftItem, relRD.payload.rightItem).pipe( - filter(([leftItem, rightItem]) => leftItem.hasSucceeded && rightItem.hasSucceeded), + filter(([leftItem, rightItem]) => leftItem.hasCompleted && rightItem.hasCompleted), map(([leftItem, rightItem]) => { - if (leftItem.payload.id === this.parentItem.id) { + if (!leftItem.hasSucceeded || !rightItem.hasSucceeded) { + return observableOf(Object.assign(new MetadatumRepresentation(this.itemType), metadatum)); + } else if (rightItem.hasSucceeded && leftItem.payload.id === this.parentItem.id) { return rightItem.payload; } else if (rightItem.payload.id === this.parentItem.id) { return leftItem.payload; From 4ad089ef5407fadd98dc56906d0c092e7202b85e Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Fri, 28 May 2021 15:40:20 +0200 Subject: [PATCH 085/351] 79698: Escape browse by author data requests --- src/app/core/browse/browse.service.spec.ts | 5 +++-- src/app/core/browse/browse.service.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/app/core/browse/browse.service.spec.ts b/src/app/core/browse/browse.service.spec.ts index 89875b3069..a28add2e30 100644 --- a/src/app/core/browse/browse.service.spec.ts +++ b/src/app/core/browse/browse.service.spec.ts @@ -127,7 +127,8 @@ describe('BrowseService', () => { }); describe('getBrowseEntriesFor and findList', () => { - const mockAuthorName = 'Donald Smith'; + // should contain special characters such that url encoding can be tested as well + const mockAuthorName = 'Donald Smith & Sons'; beforeEach(() => { requestService = getMockRequestService(getRequestEntry$(true)); @@ -152,7 +153,7 @@ describe('BrowseService', () => { describe('when findList is called with a valid browse definition id', () => { it('should call hrefOnlyDataService.findAllByHref with the expected href', () => { - const expected = browseDefinitions[1]._links.items.href + '?filterValue=' + mockAuthorName; + const expected = browseDefinitions[1]._links.items.href + '?filterValue=' + encodeURIComponent(mockAuthorName); scheduler.schedule(() => service.getBrowseItemsFor(mockAuthorName, new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe()); scheduler.flush(); diff --git a/src/app/core/browse/browse.service.ts b/src/app/core/browse/browse.service.ts index 7e55d381a6..ffc6f313b9 100644 --- a/src/app/core/browse/browse.service.ts +++ b/src/app/core/browse/browse.service.ts @@ -130,7 +130,7 @@ export class BrowseService { args.push(`startsWith=${options.startsWith}`); } if (isNotEmpty(filterValue)) { - args.push(`filterValue=${filterValue}`); + args.push(`filterValue=${encodeURIComponent(filterValue)}`); } if (isNotEmpty(args)) { href = new URLCombiner(href, `?${args.join('&')}`).toString(); From 95b98d3f7960776645eff3da8df00a0349a32a94 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Mon, 31 May 2021 10:20:28 +0200 Subject: [PATCH 086/351] 79597: Remove unused imports --- .../metadata-field-wrapper/metadata-field-wrapper.component.ts | 1 - src/app/+item-page/simple/item-types/shared/item.component.ts | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/app/+item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.ts b/src/app/+item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.ts index 8c4e200423..5c6b99248f 100644 --- a/src/app/+item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.ts +++ b/src/app/+item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.ts @@ -1,5 +1,4 @@ import { Component, Input } from '@angular/core'; -import { hasNoValue } from '../../../shared/empty.util'; /** * This component renders any content inside this wrapper. diff --git a/src/app/+item-page/simple/item-types/shared/item.component.ts b/src/app/+item-page/simple/item-types/shared/item.component.ts index 8763d8c899..130f67edc7 100644 --- a/src/app/+item-page/simple/item-types/shared/item.component.ts +++ b/src/app/+item-page/simple/item-types/shared/item.component.ts @@ -1,10 +1,9 @@ import { Component, Input, OnInit } from '@angular/core'; -import { Observable } from 'rxjs'; import { environment } from '../../../../../environments/environment'; import { BitstreamDataService } from '../../../../core/data/bitstream-data.service'; import { Bitstream } from '../../../../core/shared/bitstream.model'; import { Item } from '../../../../core/shared/item.model'; -import { getFirstSucceededRemoteDataPayload, takeUntilCompletedRemoteData } from '../../../../core/shared/operators'; +import { takeUntilCompletedRemoteData } from '../../../../core/shared/operators'; import { getItemPageRoute } from '../../../item-page-routing-paths'; import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; import { RemoteData } from '../../../../core/data/remote-data'; From 2dfed863edcb84d7b58066a97daf48e0457d4252 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 28 May 2021 18:49:47 +0200 Subject: [PATCH 087/351] [DSC-75] Fix issue while deleting multiple qualdrop value --- src/app/shared/form/form.component.ts | 9 ++++++++- .../sections/form/section-form-operations.service.ts | 9 +++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/app/shared/form/form.component.ts b/src/app/shared/form/form.component.ts index 42469ddba2..8d75d7f13a 100644 --- a/src/app/shared/form/form.component.ts +++ b/src/app/shared/form/form.component.ts @@ -309,9 +309,16 @@ export class FormComponent implements OnDestroy, OnInit { removeItem($event, arrayContext: DynamicFormArrayModel, index: number): void { const formArrayControl = this.formGroup.get(this.formBuilderService.getPath(arrayContext)) as FormArray; const event = this.getEvent($event, arrayContext, index, 'remove'); + if (this.formBuilderService.isQualdropGroup(event.model as DynamicFormControlModel)) { + // In case of qualdrop value remove event must be dispatched before removing the control from array + this.removeArrayItem.emit(event); + } this.formBuilderService.removeFormArrayGroup(index, formArrayControl, arrayContext); this.formService.changeForm(this.formId, this.formModel); - this.removeArrayItem.emit(event); + if (!this.formBuilderService.isQualdropGroup(event.model as DynamicFormControlModel)) { + // dispatch remove event for any field type except for qualdrop value + this.removeArrayItem.emit(event); + } } insertItem($event, arrayContext: DynamicFormArrayModel, index: number): void { diff --git a/src/app/submission/sections/form/section-form-operations.service.ts b/src/app/submission/sections/form/section-form-operations.service.ts index 7174d5da67..adba46bf3a 100644 --- a/src/app/submission/sections/form/section-form-operations.service.ts +++ b/src/app/submission/sections/form/section-form-operations.service.ts @@ -298,17 +298,14 @@ export class SectionFormOperationsService { event: DynamicFormControlEvent, previousValue: FormFieldPreviousValueObject): void { - if (event.context && event.context instanceof DynamicFormArrayGroupModel) { - // Model is a DynamicRowArrayModel - this.handleArrayGroupPatch(pathCombiner, event, (event as any).context.context, previousValue); - return; - } - const path = this.getFieldPathFromEvent(event); const value = this.getFieldValueFromChangeEvent(event); console.log(value); if (this.formBuilder.isQualdropGroup(event.model as DynamicFormControlModel)) { this.dispatchOperationsFromMap(this.getQualdropValueMap(event), pathCombiner, event, previousValue); + } else if (event.context && event.context instanceof DynamicFormArrayGroupModel) { + // Model is a DynamicRowArrayModel + this.handleArrayGroupPatch(pathCombiner, event, (event as any).context.context, previousValue); } else if ((isNotEmpty(value) && typeof value === 'string') || (isNotEmpty(value) && value instanceof FormFieldMetadataValueObject && value.hasValue())) { this.operationsBuilder.remove(pathCombiner.getPath(path)); } From 3e53b7c7b17f83547f727833f4408a479330f6e0 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Tue, 1 Jun 2021 14:09:17 +0200 Subject: [PATCH 088/351] [CST-4248] Add possibility to add additional content to form.component --- src/app/shared/form/form.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/form/form.component.html b/src/app/shared/form/form.component.html index 39ccda360f..de24880b3b 100644 --- a/src/app/shared/form/form.component.html +++ b/src/app/shared/form/form.component.html @@ -48,7 +48,7 @@ - +
From c150fb881eae270591e3b4329b595d4aa6a7d2d2 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Tue, 1 Jun 2021 14:12:12 +0200 Subject: [PATCH 089/351] [CST-4248] bitstream authorizations page --- .../bitstream-authorizations.component.html | 10 +++ ...bitstream-authorizations.component.spec.ts | 84 +++++++++++++++++++ .../bitstream-authorizations.component.ts | 40 +++++++++ .../bitstream-page-routing.module.ts | 36 ++++++++ .../+bitstream-page/bitstream-page.module.ts | 2 + src/assets/i18n/en.json5 | 8 +- 6 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 src/app/+bitstream-page/bitstream-authorizations/bitstream-authorizations.component.html create mode 100644 src/app/+bitstream-page/bitstream-authorizations/bitstream-authorizations.component.spec.ts create mode 100644 src/app/+bitstream-page/bitstream-authorizations/bitstream-authorizations.component.ts diff --git a/src/app/+bitstream-page/bitstream-authorizations/bitstream-authorizations.component.html b/src/app/+bitstream-page/bitstream-authorizations/bitstream-authorizations.component.html new file mode 100644 index 0000000000..804bb4f891 --- /dev/null +++ b/src/app/+bitstream-page/bitstream-authorizations/bitstream-authorizations.component.html @@ -0,0 +1,10 @@ + diff --git a/src/app/+bitstream-page/bitstream-authorizations/bitstream-authorizations.component.spec.ts b/src/app/+bitstream-page/bitstream-authorizations/bitstream-authorizations.component.spec.ts new file mode 100644 index 0000000000..c41351f380 --- /dev/null +++ b/src/app/+bitstream-page/bitstream-authorizations/bitstream-authorizations.component.spec.ts @@ -0,0 +1,84 @@ +import { CommonModule } from '@angular/common'; +import { ChangeDetectorRef, NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; + +import { cold } from 'jasmine-marbles'; +import { of as observableOf } from 'rxjs'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; + +import { DSpaceObject } from '../../core/shared/dspace-object.model'; +import { BitstreamAuthorizationsComponent } from './bitstream-authorizations.component'; +import { Bitstream } from '../../core/shared/bitstream.model'; +import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; +import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; + +describe('BitstreamAuthorizationsComponent', () => { + let comp: BitstreamAuthorizationsComponent; + let fixture: ComponentFixture>; + + const bitstream = Object.assign(new Bitstream(), { + sizeBytes: 10000, + metadata: { + 'dc.title': [ + { + value: 'file name', + language: null + } + ] + }, + _links: { + content: { href: 'file-selflink' } + } + }); + + const bitstreamRD = createSuccessfulRemoteDataObject(bitstream); + + const routeStub = { + data: observableOf({ + bitstream: bitstreamRD + }) + }; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + CommonModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }) + ], + declarations: [BitstreamAuthorizationsComponent], + providers: [ + { provide: ActivatedRoute, useValue: routeStub }, + ChangeDetectorRef, + BitstreamAuthorizationsComponent, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(BitstreamAuthorizationsComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + }); + + afterEach(() => { + comp = null; + fixture.destroy(); + }); + + it('should create', () => { + expect(comp).toBeTruthy(); + }); + + it('should init dso remote data properly', (done) => { + const expected = cold('(a|)', { a: bitstreamRD }); + expect(comp.dsoRD$).toBeObservable(expected); + done(); + }); +}); diff --git a/src/app/+bitstream-page/bitstream-authorizations/bitstream-authorizations.component.ts b/src/app/+bitstream-page/bitstream-authorizations/bitstream-authorizations.component.ts new file mode 100644 index 0000000000..adc0638780 --- /dev/null +++ b/src/app/+bitstream-page/bitstream-authorizations/bitstream-authorizations.component.ts @@ -0,0 +1,40 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +import { Observable } from 'rxjs'; +import { first, map } from 'rxjs/operators'; + +import { RemoteData } from '../../core/data/remote-data'; +import { DSpaceObject } from '../../core/shared/dspace-object.model'; + +@Component({ + selector: 'ds-collection-authorizations', + templateUrl: './bitstream-authorizations.component.html', +}) +/** + * Component that handles the Collection Authorizations + */ +export class BitstreamAuthorizationsComponent implements OnInit { + + /** + * The initial DSO object + */ + public dsoRD$: Observable>; + + /** + * Initialize instance variables + * + * @param {ActivatedRoute} route + */ + constructor( + private route: ActivatedRoute + ) { + } + + /** + * Initialize the component, setting up the collection + */ + ngOnInit(): void { + this.dsoRD$ = this.route.data.pipe(first(), map((data) => data.bitstream)); + } +} diff --git a/src/app/+bitstream-page/bitstream-page-routing.module.ts b/src/app/+bitstream-page/bitstream-page-routing.module.ts index bbbd65f279..284f29f7b4 100644 --- a/src/app/+bitstream-page/bitstream-page-routing.module.ts +++ b/src/app/+bitstream-page/bitstream-page-routing.module.ts @@ -4,8 +4,14 @@ import { EditBitstreamPageComponent } from './edit-bitstream-page/edit-bitstream import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; import { BitstreamPageResolver } from './bitstream-page.resolver'; import { BitstreamDownloadPageComponent } from '../shared/bitstream-download-page/bitstream-download-page.component'; +import { ResourcePolicyTargetResolver } from '../shared/resource-policies/resolvers/resource-policy-target.resolver'; +import { ResourcePolicyCreateComponent } from '../shared/resource-policies/create/resource-policy-create.component'; +import { ResourcePolicyResolver } from '../shared/resource-policies/resolvers/resource-policy.resolver'; +import { ResourcePolicyEditComponent } from '../shared/resource-policies/edit/resource-policy-edit.component'; +import { BitstreamAuthorizationsComponent } from './bitstream-authorizations/bitstream-authorizations.component'; const EDIT_BITSTREAM_PATH = ':id/edit'; +const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations'; /** * Routing module to help navigate Bitstream pages @@ -27,6 +33,36 @@ const EDIT_BITSTREAM_PATH = ':id/edit'; bitstream: BitstreamPageResolver }, canActivate: [AuthenticatedGuard] + }, + { + path: EDIT_BITSTREAM_AUTHORIZATIONS_PATH, + + children: [ + { + path: 'create', + resolve: { + resourcePolicyTarget: ResourcePolicyTargetResolver + }, + component: ResourcePolicyCreateComponent, + data: { title: 'resource-policies.create.page.title', showBreadcrumbs: true } + }, + { + path: 'edit', + resolve: { + resourcePolicy: ResourcePolicyResolver + }, + component: ResourcePolicyEditComponent, + data: { title: 'resource-policies.edit.page.title', showBreadcrumbs: true } + }, + { + path: '', + resolve: { + bitstream: BitstreamPageResolver + }, + component: BitstreamAuthorizationsComponent, + data: { title: 'bitstream.edit.authorizations.title', showBreadcrumbs: true } + } + ] } ]) ], diff --git a/src/app/+bitstream-page/bitstream-page.module.ts b/src/app/+bitstream-page/bitstream-page.module.ts index 24b4cd512f..80e5ad14e3 100644 --- a/src/app/+bitstream-page/bitstream-page.module.ts +++ b/src/app/+bitstream-page/bitstream-page.module.ts @@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common'; import { SharedModule } from '../shared/shared.module'; import { EditBitstreamPageComponent } from './edit-bitstream-page/edit-bitstream-page.component'; import { BitstreamPageRoutingModule } from './bitstream-page-routing.module'; +import { BitstreamAuthorizationsComponent } from './bitstream-authorizations/bitstream-authorizations.component'; /** * This module handles all components that are necessary for Bitstream related pages @@ -14,6 +15,7 @@ import { BitstreamPageRoutingModule } from './bitstream-page-routing.module'; BitstreamPageRoutingModule ], declarations: [ + BitstreamAuthorizationsComponent, EditBitstreamPageComponent ] }) diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 4c3317a0c0..44725337be 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -530,6 +530,12 @@ + "bitstream.edit.authorizations.link": "Edit bitstream's Policies", + + "bitstream.edit.authorizations.title": "Edit bitstream's Policies", + + "bitstream.edit.return": "Back", + "bitstream.edit.bitstream": "Bitstream: ", "bitstream.edit.form.description.hint": "Optionally, provide a brief description of the file, for example \"Main article\" or \"Experiment data readings\".", @@ -1817,8 +1823,6 @@ "item.page.description": "Description", - "item.page.edit": "Edit this item", - "item.page.journal-issn": "Journal ISSN", "item.page.journal-title": "Journal Title", From eaaad88443694d30d4ff4eed5cbba929c1c389e8 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Tue, 1 Jun 2021 14:13:19 +0200 Subject: [PATCH 090/351] [CST-4248] Remove embargo form field and add link to bitstream authorization page --- .../edit-bitstream-page.component.html | 6 +++- .../edit-bitstream-page.component.spec.ts | 21 ++++---------- .../edit-bitstream-page.component.ts | 28 +++---------------- 3 files changed, 15 insertions(+), 40 deletions(-) diff --git a/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.html b/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.html index fd13e249a0..cbb587cca4 100644 --- a/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.html +++ b/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.html @@ -19,7 +19,11 @@ [submitLabel]="'form.save'" (submitForm)="onSubmit()" (cancel)="onCancel()" - (dfChange)="onChange($event)"> + (dfChange)="onChange($event)"> + +
diff --git a/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts b/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts index 2e7eb4e1d1..9c2cb3a093 100644 --- a/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts +++ b/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts @@ -18,12 +18,8 @@ import { hasValue } from '../../shared/empty.util'; import { FormControl, FormGroup } from '@angular/forms'; import { FileSizePipe } from '../../shared/utils/file-size-pipe'; import { VarDirective } from '../../shared/utils/var.directive'; -import { - createSuccessfulRemoteDataObject, - createSuccessfulRemoteDataObject$ -} from '../../shared/remote-data.utils'; -import { RouterStub } from '../../shared/testing/router.stub'; -import { getEntityEditRoute, getItemEditRoute } from '../../+item-page/item-page-routing-paths'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { getEntityEditRoute } from '../../+item-page/item-page-routing-paths'; import { createPaginatedList } from '../../shared/testing/utils.test'; import { Item } from '../../core/shared/item.model'; @@ -39,7 +35,6 @@ let bitstream: Bitstream; let selectedFormat: BitstreamFormat; let allFormats: BitstreamFormat[]; let router: Router; -let routerStub; describe('EditBitstreamPageComponent', () => { let comp: EditBitstreamPageComponent; @@ -129,10 +124,6 @@ describe('EditBitstreamPageComponent', () => { findAll: createSuccessfulRemoteDataObject$(createPaginatedList(allFormats)) }); - const itemPageUrl = `fake-url/some-uuid`; - routerStub = Object.assign(new RouterStub(), { - url: `${itemPageUrl}` - }); TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), RouterTestingModule], declarations: [EditBitstreamPageComponent, FileSizePipe, VarDirective], @@ -142,7 +133,6 @@ describe('EditBitstreamPageComponent', () => { { provide: ActivatedRoute, useValue: { data: observableOf({ bitstream: createSuccessfulRemoteDataObject(bitstream) }), snapshot: { queryParams: {} } } }, { provide: BitstreamDataService, useValue: bitstreamService }, { provide: BitstreamFormatDataService, useValue: bitstreamFormatService }, - { provide: Router, useValue: routerStub }, ChangeDetectorRef ], schemas: [NO_ERRORS_SCHEMA] @@ -154,7 +144,8 @@ describe('EditBitstreamPageComponent', () => { fixture = TestBed.createComponent(EditBitstreamPageComponent); comp = fixture.componentInstance; fixture.detectChanges(); - router = (comp as any).router; + router = TestBed.inject(Router); + spyOn(router, 'navigate'); }); describe('on startup', () => { @@ -241,14 +232,14 @@ describe('EditBitstreamPageComponent', () => { it('should redirect to the item edit page on the bitstreams tab with the itemId from the component', () => { comp.itemId = 'some-uuid1'; comp.navigateToItemEditBitstreams(); - expect(routerStub.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid1'), 'bitstreams']); + expect(router.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid1'), 'bitstreams']); }); }); describe('when navigateToItemEditBitstreams is called, and the component does not have an itemId', () => { it('should redirect to the item edit page on the bitstreams tab with the itemId from the bundle links ', () => { comp.itemId = undefined; comp.navigateToItemEditBitstreams(); - expect(routerStub.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid'), 'bitstreams']); + expect(router.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid'), 'bitstreams']); }); }); }); diff --git a/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts b/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts index 8a4d584647..4ad0aac7ef 100644 --- a/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts +++ b/src/app/+bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts @@ -19,10 +19,10 @@ import { cloneDeep } from 'lodash'; import { BitstreamDataService } from '../../core/data/bitstream-data.service'; import { getAllSucceededRemoteDataPayload, - getFirstSucceededRemoteDataPayload, - getRemoteDataPayload, + getFirstCompletedRemoteData, getFirstSucceededRemoteData, - getFirstCompletedRemoteData + getFirstSucceededRemoteDataPayload, + getRemoteDataPayload } from '../../core/shared/operators'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { BitstreamFormatDataService } from '../../core/data/bitstream-format-data.service'; @@ -131,15 +131,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { rows: 10 }); - /** - * The Dynamic Input Model for the file's embargo (disabled on this page) - */ - embargoModel = new DynamicInputModel({ - id: 'embargo', - name: 'embargo', - disabled: true - }); - /** * The Dynamic Input Model for the selected format */ @@ -159,7 +150,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { /** * All input models in a simple array for easier iterations */ - inputModels = [this.fileNameModel, this.primaryBitstreamModel, this.descriptionModel, this.embargoModel, this.selectedFormatModel, this.newFormatModel]; + inputModels = [this.fileNameModel, this.primaryBitstreamModel, this.descriptionModel, this.selectedFormatModel, this.newFormatModel]; /** * The dynamic form fields used for editing the information of a bitstream @@ -179,12 +170,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { this.descriptionModel ] }), - new DynamicFormGroupModel({ - id: 'embargoContainer', - group: [ - this.embargoModel - ] - }), new DynamicFormGroupModel({ id: 'formatContainer', group: [ @@ -243,11 +228,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy { host: 'row' } }, - embargoContainer: { - grid: { - host: 'row' - } - }, formatContainer: { grid: { host: 'row' From bb2892edd896b6fa6ea9bb8261dd77e6d799de5d Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Tue, 8 Jun 2021 23:48:24 +0200 Subject: [PATCH 091/351] BUGFIX: Encode special characters when sending workflow action --- src/app/core/data/request.service.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index 4a85df0d34..14499b8214 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -265,11 +265,13 @@ export class RequestService { if (isNotEmpty(body) && typeof body === 'object') { Object.keys(body) .forEach((param) => { - const paramValue = `${param}=${body[param]}`; + const encodedParam = encodeURIComponent(param); + const encodedBody = encodeURIComponent(body[param]); + const paramValue = `${encodedParam}=${encodedBody}`; queryParams = isEmpty(queryParams) ? queryParams.concat(paramValue) : queryParams.concat('&', paramValue); }); } - return encodeURI(queryParams); + return queryParams; } /** From a27a7a4083d5fd35cd9e4466e3c7c99062cbcf48 Mon Sep 17 00:00:00 2001 From: Bruno Roemers Date: Thu, 10 Jun 2021 15:00:03 +0200 Subject: [PATCH 092/351] Test requestService.uriEncodeBody --- src/app/core/data/request.service.spec.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/app/core/data/request.service.spec.ts b/src/app/core/data/request.service.spec.ts index e6dde7a032..7a07f6fe10 100644 --- a/src/app/core/data/request.service.spec.ts +++ b/src/app/core/data/request.service.spec.ts @@ -579,4 +579,19 @@ describe('RequestService', () => { }); }); }); + + describe('uriEncodeBody', () => { + it('should properly encode the body', () => { + const body = { + 'property1': 'multiple\nlines\nto\nsend', + 'property2': 'sp&ci@l characters', + 'sp&ci@l-chars in prop': 'test123', + }; + const queryParams = service.uriEncodeBody(body); + expect(queryParams).toEqual( + 'property1=multiple%0Alines%0Ato%0Asend&property2=sp%26ci%40l%20characters&sp%26ci%40l-chars%20in%20prop=test123' + ); + }); + }); + }); From b1049584730e584b1255d34fb225e03935a7c9d0 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Thu, 10 Jun 2021 09:39:10 -0500 Subject: [PATCH 093/351] Add option to pin to a specific version of Chrome/ChromeDriver. Pin to v90 until v91 bugs are fixed --- .github/workflows/build.yml | 38 ++++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d2e8b9fe5e..f04db98e1e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,6 +16,9 @@ jobs: DSPACE_REST_PORT: 8080 DSPACE_REST_NAMESPACE: '/server' DSPACE_REST_SSL: false + # When Chrome version is specified, we pin to a specific version of Chrome & ChromeDriver + # Comment this out to use the latest release of both. + CHROME_VERSION: "90.0.4430.212-1" strategy: # Create a matrix of Node versions to test against (in parallel) matrix: @@ -34,10 +37,20 @@ jobs: with: node-version: ${{ matrix.node-version }} - - name: Install latest Chrome (for e2e tests) + # If CHROME_VERSION env variable specified above, then pin to that version. + # Otherwise, just install latest version of Chrome. + - name: Install Chrome (for e2e tests) run: | - sudo apt-get update - sudo apt-get --only-upgrade install google-chrome-stable -y + if [[ -z "${CHROME_VERSION}" ]] + then + echo "Installing latest stable version" + sudo apt-get update + sudo apt-get --only-upgrade install google-chrome-stable -y + else + echo "Installing version ${CHROME_VERSION}" + wget -q "https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${CHROME_VERSION}_amd64.deb" + sudo dpkg -i "google-chrome-stable_${CHROME_VERSION}_amd64.deb" + fi google-chrome --version # https://github.com/actions/cache/blob/main/examples.md#node---yarn @@ -53,8 +66,23 @@ jobs: key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} restore-keys: ${{ runner.os }}-yarn- - - name: Install the latest chromedriver compatible with the installed chrome version - run: yarn global add chromedriver --detect_chromedriver_version + # If CHROME_VERSION env variable specified above, determine the corresponding latest ChromeDriver version + # and install it manually (we must install manually as it seems to be the only way to downgrade). + # Otherwise use "detect" flag to install based on installed Chrome version. + - name: Install ChromeDriver compatible with installed Chrome + run: | + if [[ -z "${CHROME_VERSION}" ]] + then + echo "Installing version based on Chrome" + yarn global add chromedriver --detect_chromedriver_version + else + latest_version_string="LATEST_RELEASE_$(echo $CHROME_VERSION | cut -d'.' -f1)" + version=$(curl -s "https://chromedriver.storage.googleapis.com/${latest_version_string}") + echo "Installing ${latest_version_string} (${version})" + wget -qP /tmp/ "https://chromedriver.storage.googleapis.com/${version}/chromedriver_linux64.zip" + sudo unzip -o /tmp/chromedriver_linux64.zip -d /usr/bin + fi + chromedriver -v - name: Install Yarn dependencies run: yarn install --frozen-lockfile From 326bffae7f09d11904241bc7a94136f865cc618f Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Fri, 11 Jun 2021 09:38:04 +0200 Subject: [PATCH 094/351] switch chromedriver to npm --- .github/workflows/build.yml | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f04db98e1e..02cc590028 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -66,23 +66,9 @@ jobs: key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} restore-keys: ${{ runner.os }}-yarn- - # If CHROME_VERSION env variable specified above, determine the corresponding latest ChromeDriver version - # and install it manually (we must install manually as it seems to be the only way to downgrade). - # Otherwise use "detect" flag to install based on installed Chrome version. - - name: Install ChromeDriver compatible with installed Chrome - run: | - if [[ -z "${CHROME_VERSION}" ]] - then - echo "Installing version based on Chrome" - yarn global add chromedriver --detect_chromedriver_version - else - latest_version_string="LATEST_RELEASE_$(echo $CHROME_VERSION | cut -d'.' -f1)" - version=$(curl -s "https://chromedriver.storage.googleapis.com/${latest_version_string}") - echo "Installing ${latest_version_string} (${version})" - wget -qP /tmp/ "https://chromedriver.storage.googleapis.com/${version}/chromedriver_linux64.zip" - sudo unzip -o /tmp/chromedriver_linux64.zip -d /usr/bin - fi - chromedriver -v + - name: Install the latest chromedriver compatible with the installed chrome version + # needs to be npm, the --detect_chromedriver_version flag doesn't work with yarn global + run: npm install -g chromedriver --detect_chromedriver_version - name: Install Yarn dependencies run: yarn install --frozen-lockfile From e7282bdbd7ace08d212ef47868f6ffa0d10af2d7 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 11 Jun 2021 08:46:17 -0500 Subject: [PATCH 095/351] Minor cleanup, print chromedriver version after installation --- .github/workflows/build.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 02cc590028..c856e8f5fe 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -66,9 +66,11 @@ jobs: key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} restore-keys: ${{ runner.os }}-yarn- - - name: Install the latest chromedriver compatible with the installed chrome version + - name: Install latest ChromeDriver compatible with installed Chrome # needs to be npm, the --detect_chromedriver_version flag doesn't work with yarn global - run: npm install -g chromedriver --detect_chromedriver_version + run: | + npm install -g chromedriver --detect_chromedriver_version + chromedriver -v - name: Install Yarn dependencies run: yarn install --frozen-lockfile From c756c68f286e3a244a24fd3e33b3af829da54b24 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Tue, 1 Jun 2021 12:46:23 +0200 Subject: [PATCH 096/351] move header changes to dspace theme --- .../+home-page/home-page-routing.module.ts | 1 + src/app/app.module.ts | 2 + .../header-navbar-wrapper.component.html | 1 + .../header-navbar-wrapper.component.scss | 4 -- ...hemed-header-navbar-wrapper.component.scss | 3 + .../themed-header-navbar-wrapper.component.ts | 25 +++++++++ src/app/header/header.component.html | 41 +++++++------- src/app/header/header.component.scss | 32 ++++++----- .../expandable-navbar-section.component.html | 2 +- .../navbar-section.component.html | 2 +- src/app/navbar/navbar.component.html | 31 ++++------ src/app/navbar/navbar.component.scss | 17 +----- src/app/navbar/navbar.component.ts | 3 +- src/app/root/root.component.html | 2 +- src/styles/_custom_variables.scss | 2 +- .../header-navbar-wrapper.component.html | 0 .../header-navbar-wrapper.component.scss | 0 .../header-navbar-wrapper.component.ts | 15 +++++ src/themes/custom/theme.module.ts | 2 + .../header-navbar-wrapper.component.html | 3 + .../header-navbar-wrapper.component.scss | 0 .../header-navbar-wrapper.component.ts | 13 +++++ .../dspace/app/header/header.component.html | 24 ++++++++ .../dspace/app/header/header.component.scss | 19 +++++++ .../dspace/app/header/header.component.ts | 13 +++++ .../dspace/app/navbar/navbar.component.html | 24 ++++++++ .../dspace/app/navbar/navbar.component.scss | 56 ++++++++++++++++++- .../dspace/app/navbar/navbar.component.ts | 2 +- src/themes/dspace/styles/_global-styles.scss | 12 +++- .../styles/_theme_css_variable_overrides.scss | 1 + src/themes/dspace/theme.module.ts | 4 ++ 31 files changed, 274 insertions(+), 82 deletions(-) create mode 100644 src/app/header-nav-wrapper/themed-header-navbar-wrapper.component.scss create mode 100644 src/app/header-nav-wrapper/themed-header-navbar-wrapper.component.ts create mode 100644 src/themes/custom/app/header-nav-wrapper/header-navbar-wrapper.component.html create mode 100644 src/themes/custom/app/header-nav-wrapper/header-navbar-wrapper.component.scss create mode 100644 src/themes/custom/app/header-nav-wrapper/header-navbar-wrapper.component.ts create mode 100644 src/themes/dspace/app/header-nav-wrapper/header-navbar-wrapper.component.html create mode 100644 src/themes/dspace/app/header-nav-wrapper/header-navbar-wrapper.component.scss create mode 100644 src/themes/dspace/app/header-nav-wrapper/header-navbar-wrapper.component.ts create mode 100644 src/themes/dspace/app/header/header.component.html create mode 100644 src/themes/dspace/app/header/header.component.scss create mode 100644 src/themes/dspace/app/header/header.component.ts create mode 100644 src/themes/dspace/app/navbar/navbar.component.html diff --git a/src/app/+home-page/home-page-routing.module.ts b/src/app/+home-page/home-page-routing.module.ts index ec6a547359..2a41c079da 100644 --- a/src/app/+home-page/home-page-routing.module.ts +++ b/src/app/+home-page/home-page-routing.module.ts @@ -20,6 +20,7 @@ import { ThemedHomePageComponent } from './themed-home-page.component'; id: 'statistics_site', active: true, visible: true, + index: 2, model: { type: MenuItemType.LINK, text: 'menu.section.statistics', diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 03cd819625..3d45ffbfc2 100755 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -46,6 +46,7 @@ import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component import { ThemedHeaderComponent } from './header/themed-header.component'; import { ThemedFooterComponent } from './footer/themed-footer.component'; import { ThemedBreadcrumbsComponent } from './breadcrumbs/themed-breadcrumbs.component'; +import { ThemedHeaderNavbarWrapperComponent } from './header-nav-wrapper/themed-header-navbar-wrapper.component'; export function getBase() { return environment.ui.nameSpace; @@ -129,6 +130,7 @@ const DECLARATIONS = [ HeaderComponent, ThemedHeaderComponent, HeaderNavbarWrapperComponent, + ThemedHeaderNavbarWrapperComponent, AdminSidebarComponent, AdminSidebarSectionComponent, ExpandableAdminSidebarSectionComponent, diff --git a/src/app/header-nav-wrapper/header-navbar-wrapper.component.html b/src/app/header-nav-wrapper/header-navbar-wrapper.component.html index c1843318b8..f99070b738 100644 --- a/src/app/header-nav-wrapper/header-navbar-wrapper.component.html +++ b/src/app/header-nav-wrapper/header-navbar-wrapper.component.html @@ -1,3 +1,4 @@
+
diff --git a/src/app/header-nav-wrapper/header-navbar-wrapper.component.scss b/src/app/header-nav-wrapper/header-navbar-wrapper.component.scss index a2ebd0d41a..b297979fd0 100644 --- a/src/app/header-nav-wrapper/header-navbar-wrapper.component.scss +++ b/src/app/header-nav-wrapper/header-navbar-wrapper.component.scss @@ -5,7 +5,3 @@ position: sticky; } } - -:host { - z-index: var(--ds-nav-z-index); -} diff --git a/src/app/header-nav-wrapper/themed-header-navbar-wrapper.component.scss b/src/app/header-nav-wrapper/themed-header-navbar-wrapper.component.scss new file mode 100644 index 0000000000..db392096aa --- /dev/null +++ b/src/app/header-nav-wrapper/themed-header-navbar-wrapper.component.scss @@ -0,0 +1,3 @@ +:host { + z-index: var(--ds-nav-z-index); +} diff --git a/src/app/header-nav-wrapper/themed-header-navbar-wrapper.component.ts b/src/app/header-nav-wrapper/themed-header-navbar-wrapper.component.ts new file mode 100644 index 0000000000..7f9c181fe2 --- /dev/null +++ b/src/app/header-nav-wrapper/themed-header-navbar-wrapper.component.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; +import { ThemedComponent } from '../shared/theme-support/themed.component'; +import { HeaderNavbarWrapperComponent } from './header-navbar-wrapper.component'; + +/** + * Themed wrapper for BreadcrumbsComponent + */ +@Component({ + selector: 'ds-themed-header-navbar-wrapper', + styleUrls: ['./themed-header-navbar-wrapper.component.scss'], + templateUrl: '../shared/theme-support/themed.component.html', +}) +export class ThemedHeaderNavbarWrapperComponent extends ThemedComponent { + protected getComponentName(): string { + return 'HeaderNavbarWrapperComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../themes/${themeName}/app/header-nav-wrapper/header-navbar-wrapper.component`); + } + + protected importUnthemedComponent(): Promise { + return import(`./header-navbar-wrapper.component`); + } +} diff --git a/src/app/header/header.component.html b/src/app/header/header.component.html index dc0d066a4e..a1fa051610 100644 --- a/src/app/header/header.component.html +++ b/src/app/header/header.component.html @@ -1,24 +1,23 @@ -
- - + +
+
diff --git a/src/app/header/header.component.scss b/src/app/header/header.component.scss index c85daf8516..922b2d02e1 100644 --- a/src/app/header/header.component.scss +++ b/src/app/header/header.component.scss @@ -1,19 +1,23 @@ -@media screen and (min-width: map-get($grid-breakpoints, md)) { - nav.navbar { - display: none; - } - .header { - background-color: var(--ds-header-bg); +.navbar-brand img { + max-height: var(--ds-header-logo-height); + max-width: 100%; + @media screen and (max-width: map-get($grid-breakpoints, sm)) { + max-height: var(--ds-header-logo-height-xs); } } -.navbar-brand img { - @media screen and (max-width: map-get($grid-breakpoints, md)) { - height: var(--ds-header-logo-height-xs); - } -} .navbar-toggler .navbar-toggler-icon { - background-image: none !important; - line-height: 1.5; - color: var(--bs-link-color); + background-image: none !important; + line-height: 1.5; } + +.navbar ::ng-deep { + a { + color: var(--ds-header-icon-color); + + &:hover, &focus { + color: var(--ds-header-icon-color-hover); + } + } +} + diff --git a/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.html b/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.html index a1c02bfa31..a7cf7c1856 100644 --- a/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.html +++ b/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.html @@ -1,4 +1,4 @@ - diff --git a/src/app/navbar/navbar.component.html b/src/app/navbar/navbar.component.html index 50e526b78b..2356077e43 100644 --- a/src/app/navbar/navbar.component.html +++ b/src/app/navbar/navbar.component.html @@ -1,24 +1,17 @@ diff --git a/src/app/navbar/navbar.component.scss b/src/app/navbar/navbar.component.scss index be6e8ac55e..d131bf95bf 100644 --- a/src/app/navbar/navbar.component.scss +++ b/src/app/navbar/navbar.component.scss @@ -1,14 +1,12 @@ nav.navbar { - border-top: 1px var(--ds-header-navbar-border-top-color) solid; - border-bottom: 1px var(--ds-header-navbar-border-bottom-color) solid; + border-bottom: 1px var(--bs-gray-400) solid; align-items: baseline; - color: var(--ds-header-icon-color); } /** Mobile menu styling **/ @media screen and (max-width: map-get($grid-breakpoints, md)) { .navbar { - width: 100%; + width: 100vw; background-color: var(--bs-white); position: absolute; overflow: hidden; @@ -31,20 +29,9 @@ nav.navbar { @media screen and (max-width: map-get($grid-breakpoints, md)) { > .container { padding: 0 var(--bs-spacer); - a.navbar-brand { - display: none; - } - .navbar-collapsed { - display: none; - } } padding: 0; } - height: 80px; -} - -a.navbar-brand img { - max-height: var(--ds-header-logo-height); } .navbar-nav { diff --git a/src/app/navbar/navbar.component.ts b/src/app/navbar/navbar.component.ts index ae5fb262ae..e741cea285 100644 --- a/src/app/navbar/navbar.component.ts +++ b/src/app/navbar/navbar.component.ts @@ -46,6 +46,7 @@ export class NavbarComponent extends MenuComponent { id: `browse_global_communities_and_collections`, active: false, visible: true, + index: 0, model: { type: MenuItemType.LINK, text: `menu.section.browse_global_communities_and_collections`, @@ -57,11 +58,11 @@ export class NavbarComponent extends MenuComponent { id: 'browse_global', active: false, visible: true, + index: 1, model: { type: MenuItemType.TEXT, text: 'menu.section.browse_global' } as TextMenuItemModel, - index: 0 }, ]; // Read the different Browse-By types from config and add them to the browse menu diff --git a/src/app/root/root.component.html b/src/app/root/root.component.html index aef07d79f4..67d04f999f 100644 --- a/src/app/root/root.component.html +++ b/src/app/root/root.component.html @@ -4,7 +4,7 @@ value: (!(sidebarVisible | async) ? 'hidden' : (slideSidebarOver | async) ? 'shown' : 'expanded'), params: {collapsedSidebarWidth: (collapsedSidebarWidth | async), totalSidebarWidth: (totalSidebarWidth | async)} }"> - + diff --git a/src/styles/_custom_variables.scss b/src/styles/_custom_variables.scss index 298be09f67..dc76d772d1 100644 --- a/src/styles/_custom_variables.scss +++ b/src/styles/_custom_variables.scss @@ -20,7 +20,7 @@ --ds-sidebar-z-index: 20; --ds-header-bg: #{$white}; - --ds-header-logo-height: 40px; + --ds-header-logo-height: 50px; --ds-header-logo-height-xs: 50px; --ds-header-icon-color: #{$cyan}; --ds-header-icon-color-hover: #{darken($white, 15%)}; diff --git a/src/themes/custom/app/header-nav-wrapper/header-navbar-wrapper.component.html b/src/themes/custom/app/header-nav-wrapper/header-navbar-wrapper.component.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/themes/custom/app/header-nav-wrapper/header-navbar-wrapper.component.scss b/src/themes/custom/app/header-nav-wrapper/header-navbar-wrapper.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/themes/custom/app/header-nav-wrapper/header-navbar-wrapper.component.ts b/src/themes/custom/app/header-nav-wrapper/header-navbar-wrapper.component.ts new file mode 100644 index 0000000000..875b5f69b8 --- /dev/null +++ b/src/themes/custom/app/header-nav-wrapper/header-navbar-wrapper.component.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { HeaderNavbarWrapperComponent as BaseComponent } from '../../../../app/header-nav-wrapper/header-navbar-wrapper.component'; + +/** + * This component represents a wrapper for the horizontal navbar and the header + */ +@Component({ + selector: 'ds-header-navbar-wrapper', + // styleUrls: ['header-navbar-wrapper.component.scss'], + styleUrls: ['../../../../app/header-nav-wrapper/header-navbar-wrapper.component.scss'], + // templateUrl: 'header-navbar-wrapper.component.html', + templateUrl: '../../../../app/header-nav-wrapper/header-navbar-wrapper.component.html', +}) +export class HeaderNavbarWrapperComponent extends BaseComponent { +} diff --git a/src/themes/custom/theme.module.ts b/src/themes/custom/theme.module.ts index 23fcf4c325..49b54cd850 100644 --- a/src/themes/custom/theme.module.ts +++ b/src/themes/custom/theme.module.ts @@ -78,6 +78,7 @@ import { NavbarComponent } from './app/navbar/navbar.component'; import { HeaderComponent } from './app/header/header.component'; import { FooterComponent } from './app/footer/footer.component'; import { BreadcrumbsComponent } from './app/breadcrumbs/breadcrumbs.component'; +import { HeaderNavbarWrapperComponent } from './app/header-nav-wrapper/header-navbar-wrapper.component'; const DECLARATIONS = [ HomePageComponent, @@ -117,6 +118,7 @@ const DECLARATIONS = [ FooterComponent, HeaderComponent, NavbarComponent, + HeaderNavbarWrapperComponent, BreadcrumbsComponent ]; diff --git a/src/themes/dspace/app/header-nav-wrapper/header-navbar-wrapper.component.html b/src/themes/dspace/app/header-nav-wrapper/header-navbar-wrapper.component.html new file mode 100644 index 0000000000..091d152258 --- /dev/null +++ b/src/themes/dspace/app/header-nav-wrapper/header-navbar-wrapper.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/themes/dspace/app/header-nav-wrapper/header-navbar-wrapper.component.scss b/src/themes/dspace/app/header-nav-wrapper/header-navbar-wrapper.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/themes/dspace/app/header-nav-wrapper/header-navbar-wrapper.component.ts b/src/themes/dspace/app/header-nav-wrapper/header-navbar-wrapper.component.ts new file mode 100644 index 0000000000..36e23e174a --- /dev/null +++ b/src/themes/dspace/app/header-nav-wrapper/header-navbar-wrapper.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { HeaderNavbarWrapperComponent as BaseComponent } from '../../../../app/header-nav-wrapper/header-navbar-wrapper.component'; + +/** + * This component represents a wrapper for the horizontal navbar and the header + */ +@Component({ + selector: 'ds-header-navbar-wrapper', + styleUrls: ['header-navbar-wrapper.component.scss'], + templateUrl: 'header-navbar-wrapper.component.html', +}) +export class HeaderNavbarWrapperComponent extends BaseComponent { +} diff --git a/src/themes/dspace/app/header/header.component.html b/src/themes/dspace/app/header/header.component.html new file mode 100644 index 0000000000..dc0d066a4e --- /dev/null +++ b/src/themes/dspace/app/header/header.component.html @@ -0,0 +1,24 @@ +
+ + + +
diff --git a/src/themes/dspace/app/header/header.component.scss b/src/themes/dspace/app/header/header.component.scss new file mode 100644 index 0000000000..ab418865f1 --- /dev/null +++ b/src/themes/dspace/app/header/header.component.scss @@ -0,0 +1,19 @@ +@media screen and (min-width: map-get($grid-breakpoints, md)) { + nav.navbar { + display: none; + } + .header { + background-color: var(--ds-header-bg); + } +} + +.navbar-brand img { + @media screen and (max-width: map-get($grid-breakpoints, md)) { + height: var(--ds-header-logo-height-xs); + } +} +.navbar-toggler .navbar-toggler-icon { + background-image: none !important; + line-height: 1.5; + color: var(--bs-link-color); +} diff --git a/src/themes/dspace/app/header/header.component.ts b/src/themes/dspace/app/header/header.component.ts new file mode 100644 index 0000000000..6da89b47d5 --- /dev/null +++ b/src/themes/dspace/app/header/header.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { HeaderComponent as BaseComponent } from '../../../../app/header/header.component'; + +/** + * Represents the header with the logo and simple navigation + */ +@Component({ + selector: 'ds-header', + styleUrls: ['header.component.scss'], + templateUrl: 'header.component.html', +}) +export class HeaderComponent extends BaseComponent { +} diff --git a/src/themes/dspace/app/navbar/navbar.component.html b/src/themes/dspace/app/navbar/navbar.component.html new file mode 100644 index 0000000000..50e526b78b --- /dev/null +++ b/src/themes/dspace/app/navbar/navbar.component.html @@ -0,0 +1,24 @@ + + diff --git a/src/themes/dspace/app/navbar/navbar.component.scss b/src/themes/dspace/app/navbar/navbar.component.scss index 463a4269ee..210847c1d9 100644 --- a/src/themes/dspace/app/navbar/navbar.component.scss +++ b/src/themes/dspace/app/navbar/navbar.component.scss @@ -1,5 +1,57 @@ -@import 'src/app/navbar/navbar.component.scss'; - nav.navbar { + border-top: 1px var(--ds-header-navbar-border-top-color) solid; border-bottom: 5px var(--bs-green) solid; + align-items: baseline; + color: var(--ds-header-icon-color); +} + +/** Mobile menu styling **/ +@media screen and (max-width: map-get($grid-breakpoints, md)) { + .navbar { + width: 100%; + background-color: var(--bs-white); + position: absolute; + overflow: hidden; + height: 0; + &.open { + height: 100vh; //doesn't matter because wrapper is sticky + } + } +} + +@media screen and (min-width: map-get($grid-breakpoints, md)) { + .reset-padding-md { + margin-left: calc(var(--bs-spacer) / -2); + margin-right: calc(var(--bs-spacer) / -2); + } +} + +/* TODO remove when https://github.com/twbs/bootstrap/issues/24726 is fixed */ +.navbar-expand-md.navbar-container { + @media screen and (max-width: map-get($grid-breakpoints, md)) { + > .container { + padding: 0 var(--bs-spacer); + a.navbar-brand { + display: none; + } + .navbar-collapsed { + display: none; + } + } + padding: 0; + } + height: 80px; +} + +a.navbar-brand img { + max-height: var(--ds-header-logo-height); +} + +.navbar-nav { + ::ng-deep a.nav-link { + color: var(--ds-navbar-link-color); + } + ::ng-deep a.nav-link:hover { + color: var(--ds-navbar-link-color-hover); + } } diff --git a/src/themes/dspace/app/navbar/navbar.component.ts b/src/themes/dspace/app/navbar/navbar.component.ts index e375011683..321351a933 100644 --- a/src/themes/dspace/app/navbar/navbar.component.ts +++ b/src/themes/dspace/app/navbar/navbar.component.ts @@ -8,7 +8,7 @@ import { slideMobileNav } from '../../../../app/shared/animations/slide'; @Component({ selector: 'ds-navbar', styleUrls: ['./navbar.component.scss'], - templateUrl: '../../../../app/navbar/navbar.component.html', + templateUrl: './navbar.component.html', animations: [slideMobileNav] }) export class NavbarComponent extends BaseComponent { diff --git a/src/themes/dspace/styles/_global-styles.scss b/src/themes/dspace/styles/_global-styles.scss index 1fb60b64a2..72fac11156 100644 --- a/src/themes/dspace/styles/_global-styles.scss +++ b/src/themes/dspace/styles/_global-styles.scss @@ -3,7 +3,7 @@ // imports the base global style @import '../../../styles/_global-styles.scss'; -.facet-filter,.setting-option { +.facet-filter, .setting-option { background-color: var(--bs-light); border-radius: var(--bs-border-radius); @@ -21,3 +21,13 @@ font-size: 1.1rem } } + +header { + ds-navbar-section > li, + ds-expandable-navbar-section > li { + display: flex; + flex-direction: column; + justify-content: center; + height: 100%; + } +} diff --git a/src/themes/dspace/styles/_theme_css_variable_overrides.scss b/src/themes/dspace/styles/_theme_css_variable_overrides.scss index 2a61babdb7..e4b4b61f45 100644 --- a/src/themes/dspace/styles/_theme_css_variable_overrides.scss +++ b/src/themes/dspace/styles/_theme_css_variable_overrides.scss @@ -1,6 +1,7 @@ // Override or add CSS variables for your theme here :root { + --ds-header-logo-height: 40px; --ds-banner-text-background: rgba(0, 0, 0, 0.45); --ds-banner-background-gradient-width: 300px; --ds-home-news-link-color: #{$green}; diff --git a/src/themes/dspace/theme.module.ts b/src/themes/dspace/theme.module.ts index ed840c2e25..40138aa490 100644 --- a/src/themes/dspace/theme.module.ts +++ b/src/themes/dspace/theme.module.ts @@ -41,9 +41,13 @@ import { CollectionPageModule } from '../../app/+collection-page/collection-page import { SubmissionModule } from '../../app/submission/submission.module'; import { MyDSpacePageModule } from '../../app/+my-dspace-page/my-dspace-page.module'; import { NavbarComponent } from './app/navbar/navbar.component'; +import { HeaderComponent } from './app/header/header.component'; +import { HeaderNavbarWrapperComponent } from './app/header-nav-wrapper/header-navbar-wrapper.component'; const DECLARATIONS = [ HomeNewsComponent, + HeaderComponent, + HeaderNavbarWrapperComponent, NavbarComponent ]; From 74a17da5b846b7bea48e325d57a6690d31fbe891 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Tue, 1 Jun 2021 13:25:01 +0200 Subject: [PATCH 097/351] fix issue where home page background image would wrap around on certain screen widths --- .../dspace/app/+home-page/home-news/home-news.component.scss | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/themes/dspace/app/+home-page/home-news/home-news.component.scss b/src/themes/dspace/app/+home-page/home-news/home-news.component.scss index b5a070e51e..5e89f6b62f 100644 --- a/src/themes/dspace/app/+home-page/home-news/home-news.component.scss +++ b/src/themes/dspace/app/+home-page/home-news/home-news.component.scss @@ -6,12 +6,8 @@ color: white; background-color: var(--bs-info); position: relative; - background-position-y: -200px; background-image: url('/assets/dspace/images/banner.jpg'); background-size: cover; - @media screen and (max-width: map-get($grid-breakpoints, lg)) { - background-position-y: 0; - } .container { position: relative; From 2ddda1c766eb2062e6f32d476c89cde5e57226ff Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Tue, 1 Jun 2021 16:03:45 +0200 Subject: [PATCH 098/351] enable e2e stacktrace --- e2e/protractor.conf.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/e2e/protractor.conf.js b/e2e/protractor.conf.js index 51180c8044..93bf7f3301 100644 --- a/e2e/protractor.conf.js +++ b/e2e/protractor.conf.js @@ -69,7 +69,6 @@ exports.config = { plugins: [{ path: '../node_modules/protractor-istanbul-plugin' }], - framework: 'jasmine', jasmineNodeOpts: { showColors: true, @@ -85,7 +84,7 @@ exports.config = { onPrepare: function () { jasmine.getEnv().addReporter(new SpecReporter({ spec: { - displayStacktrace: true + displayStacktrace: 'pretty' } })); } From f2a29a642576dd8b116280db4f2c0b5b66ee953d Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Fri, 4 Jun 2021 10:35:42 +0200 Subject: [PATCH 099/351] improve structure double footer --- src/app/footer/footer.component.html | 109 ++++++++++++++------------- src/app/footer/footer.component.scss | 19 +++-- src/styles/_custom_variables.scss | 10 +-- 3 files changed, 75 insertions(+), 63 deletions(-) diff --git a/src/app/footer/footer.component.html b/src/app/footer/footer.component.html index bc407c2a97..4990290037 100644 --- a/src/app/footer/footer.component.html +++ b/src/app/footer/footer.component.html @@ -1,72 +1,79 @@ -
- -
- -
+
+
Date: Tue, 1 Jun 2021 09:45:17 +0200 Subject: [PATCH 106/351] 79730: Add labels around date range inputs --- .../search-range-filter.component.html | 31 +++++++++++++------ src/assets/i18n/en.json5 | 8 +++-- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.html b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.html index 8c4fe2b174..e4e8152e97 100644 --- a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.html +++ b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.html @@ -3,18 +3,31 @@
- +
- +
- +
diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index e9f22fd52c..90aac07a54 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -2914,9 +2914,13 @@ "search.filters.filter.dateIssued.head": "Date", - "search.filters.filter.dateIssued.max.placeholder": "Minimum Date", + "search.filters.filter.dateIssued.max.placeholder": "Maximum Date", - "search.filters.filter.dateIssued.min.placeholder": "Maximum Date", + "search.filters.filter.dateIssued.max.label": "End", + + "search.filters.filter.dateIssued.min.placeholder": "Minimum Date", + + "search.filters.filter.dateIssued.min.label": "Start", "search.filters.filter.dateSubmitted.head": "Date submitted", From abe26ce9f828daf5a63e18cb79b2270dfe430cbe Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Tue, 1 Jun 2021 12:42:40 +0200 Subject: [PATCH 107/351] 79730: Keyboard navigation for expandable filter facets --- .../search-filter.component.html | 12 +++--- .../search-filter.component.scss | 38 ++++++++++++++++--- .../search-filter/search-filter.component.ts | 24 ++++++++++++ 3 files changed, 63 insertions(+), 11 deletions(-) diff --git a/src/app/shared/search/search-filters/search-filter/search-filter.component.html b/src/app/shared/search/search-filters/search-filter/search-filter.component.html index eb2105f4e7..b71111de6a 100644 --- a/src/app/shared/search/search-filters/search-filter/search-filter.component.html +++ b/src/app/shared/search/search-filters/search-filter/search-filter.component.html @@ -1,17 +1,19 @@ -
-
+
+
+
+ class="search-filter-wrapper" [ngClass]="{ 'closed' : closed, 'notab': notab }"> diff --git a/src/app/shared/search/search-filters/search-filter/search-filter.component.scss b/src/app/shared/search/search-filters/search-filter/search-filter.component.scss index 518e7c9d5f..7e2631b55f 100644 --- a/src/app/shared/search/search-filters/search-filter/search-filter.component.scss +++ b/src/app/shared/search/search-filters/search-filter/search-filter.component.scss @@ -1,10 +1,36 @@ :host .facet-filter { - border: 1px solid var(--bs-light); - cursor: pointer; - .search-filter-wrapper.closed { - overflow: hidden; + border: 1px solid var(--bs-light); + cursor: pointer; + line-height: 0; + + .search-filter-wrapper { + line-height: var(--bs-line-height-base); + &.closed { + overflow: hidden; } - .filter-toggle { - line-height: var(--bs-line-height-base); + &.notab { + visibility: hidden; } + } + + .filter-toggle { + line-height: var(--bs-line-height-base); + text-align: right; + position: relative; + top: -0.125rem; // Fix weird outline shape in Chrome + } + + > button { + appearance: none; + border: 0; + padding: 0; + background: transparent; + width: 100%; + outline: none !important; + } + + &.focus { + outline: none; + box-shadow: var(--bs-input-btn-focus-box-shadow); + } } 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 31ace10a7d..23cd92a601 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 @@ -37,6 +37,16 @@ export class SearchFilterComponent implements OnInit { */ closed: boolean; + /** + * True when the filter controls should be hidden & removed from the tablist + */ + notab: boolean; + + /** + * True when the filter toggle button is focused + */ + focusBox: boolean = false; + /** * Emits true when the filter is currently collapsed in the store */ @@ -112,6 +122,9 @@ export class SearchFilterComponent implements OnInit { if (event.fromState === 'collapsed') { this.closed = false; } + if (event.toState === 'collapsed') { + this.notab = true; + } } /** @@ -122,6 +135,17 @@ export class SearchFilterComponent implements OnInit { if (event.toState === 'collapsed') { this.closed = true; } + if (event.fromState === 'collapsed') { + this.notab = false; + } + } + + get regionId(): string { + return `search-filter-region-${this.constructor['ɵcmp'].id}`; + } + + get toggleId(): string { + return `search-filter-toggle-${this.constructor['ɵcmp'].id}`; } /** From cb3f5ad259a5088b2bf70659a00559d67ec8ff58 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Tue, 1 Jun 2021 13:04:49 +0200 Subject: [PATCH 108/351] 79730: Add null href to more/collapse toggle links --- .../search-authority-filter.component.html | 10 ++++++---- .../search-boolean-filter.component.html | 10 ++++++---- .../search-hierarchy-filter.component.html | 10 ++++++---- .../search-text-filter.component.html | 10 ++++++---- 4 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/app/shared/search/search-filters/search-filter/search-authority-filter/search-authority-filter.component.html b/src/app/shared/search/search-filters/search-filter/search-authority-filter/search-authority-filter.component.html index 4a7f769f21..44aed494e3 100644 --- a/src/app/shared/search/search-filters/search-filter/search-authority-filter/search-authority-filter.component.html +++ b/src/app/shared/search/search-filters/search-filter/search-authority-filter/search-authority-filter.component.html @@ -8,11 +8,13 @@
{{"search.filters.filter.show-more" - | translate}} + (click)="showMore()" href="javascript:void(0);"> + {{"search.filters.filter.show-more" | translate}} + {{"search.filters.filter.show-less" - | translate}} + (click)="showFirstPageOnly()" href="javascript:void(0);"> + {{"search.filters.filter.show-less" | translate}} +
{{"search.filters.filter.show-more" - | translate}} + (click)="showMore()" href="javascript:void(0);"> + {{"search.filters.filter.show-more" | translate}} + {{"search.filters.filter.show-less" - | translate}} + (click)="showFirstPageOnly()" href="javascript:void(0);"> + {{"search.filters.filter.show-less" | translate}} +
diff --git a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html index 2154ae2e24..49ca6fe3fd 100644 --- a/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html +++ b/src/app/shared/search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html @@ -8,11 +8,13 @@
{{"search.filters.filter.show-more" - | translate}} + (click)="showMore()" href="javascript:void(0);"> + {{"search.filters.filter.show-more" | translate}} + {{"search.filters.filter.show-less" - | translate}} + (click)="showFirstPageOnly()" href="javascript:void(0);"> + {{"search.filters.filter.show-less" | translate}} +
{{"search.filters.filter.show-more" - | translate}} + (click)="showMore()" href="javascript:void(0);"> + {{"search.filters.filter.show-more" | translate}} + {{"search.filters.filter.show-less" - | translate}} + (click)="showFirstPageOnly()" href="javascript:void(0);"> + {{"search.filters.filter.show-less" | translate}} +
Date: Tue, 1 Jun 2021 14:01:48 +0200 Subject: [PATCH 109/351] 79730: Improve slider handles keyboard control --- .../search-range-filter/search-range-filter.component.html | 5 +++-- .../search-range-filter/search-range-filter.component.scss | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.html b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.html index e4e8152e97..3a6a6565c0 100644 --- a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.html +++ b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.html @@ -32,8 +32,9 @@ - + [dsDebounce]="500" (onDebounce)="onSubmit()" + [(ngModel)]="range" ngDefaultControl> +
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 2c98280e7f..f26806abfb 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 @@ -21,6 +21,7 @@ } &:focus { outline: none; + box-shadow: var(--bs-input-btn-focus-box-shadow); } } From c60fa2c441257ebe46f3d3ab5a85334b3b33dc9e Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Tue, 1 Jun 2021 15:20:33 +0200 Subject: [PATCH 110/351] 79730: Don't submit date slider changes until keyup --- .../search-range-filter.component.html | 3 ++- .../search-range-filter.component.ts | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.html b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.html index 3a6a6565c0..0ebd5f74a2 100644 --- a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.html +++ b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.html @@ -32,7 +32,8 @@ 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 62b1cb98a6..b23a2d8224 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 @@ -68,6 +68,12 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple */ sub: Subscription; + /** + * Whether the sider is being controlled by the keyboard. + * Supresses any changes until the key is released. + */ + keyboardControl: boolean; + constructor(protected searchService: SearchService, protected filterService: SearchFilterService, protected router: Router, @@ -104,6 +110,10 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple * Submits new custom range values to the range filter from the widget */ onSubmit() { + if (this.keyboardControl) { + return; // don't submit if a key is being held down + } + const newMin = this.range[0] !== this.min ? [this.range[0]] : null; const newMax = this.range[1] !== this.max ? [this.range[1]] : null; this.router.navigate(this.getSearchLinkParts(), { @@ -117,6 +127,14 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple this.filter = ''; } + startKeyboardControl(): void { + this.keyboardControl = true; + } + + stopKeyboardControl(): void { + this.keyboardControl = false; + } + /** * TODO when upgrading nouislider, verify that this check is still needed. * Prevents AoT bug From 08878941aba73fd5e849b0e4f7c6dede293d7701 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Tue, 1 Jun 2021 15:31:30 +0200 Subject: [PATCH 111/351] 79730: Fix tslint issues --- .../shared/input-suggestions/input-suggestions.component.ts | 4 ++-- .../search-filters/search-filter/search-filter.component.ts | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/app/shared/input-suggestions/input-suggestions.component.ts b/src/app/shared/input-suggestions/input-suggestions.component.ts index 7e05dbcc8c..7b5c9f34f2 100644 --- a/src/app/shared/input-suggestions/input-suggestions.component.ts +++ b/src/app/shared/input-suggestions/input-suggestions.component.ts @@ -111,10 +111,10 @@ export class InputSuggestionsComponent implements ControlValueAccessor, OnChange @Input() disabled = false; propagateChange = (_: any) => { /* Empty implementation */ - }; + } propagateTouch = (_: any) => { /* Empty implementation */ - }; + } /** * When any of the inputs change, check if we should still show the suggestions 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 23cd92a601..57c4f991db 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 @@ -45,7 +45,7 @@ export class SearchFilterComponent implements OnInit { /** * True when the filter toggle button is focused */ - focusBox: boolean = false; + focusBox = false; /** * Emits true when the filter is currently collapsed in the store @@ -141,10 +141,12 @@ export class SearchFilterComponent implements OnInit { } get regionId(): string { + // tslint:disable-next-line:no-string-literal return `search-filter-region-${this.constructor['ɵcmp'].id}`; } get toggleId(): string { + // tslint:disable-next-line:no-string-literal return `search-filter-toggle-${this.constructor['ɵcmp'].id}`; } From d37d043531ab41ab4ed3b99835472788ed12a636 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Thu, 3 Jun 2021 15:10:20 +0200 Subject: [PATCH 112/351] 79730: Show input labels when available --- .../filter-input-suggestions.component.html | 17 ++++++++----- .../search-range-filter.component.html | 24 ++++++++++--------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/app/shared/input-suggestions/filter-suggestions/filter-input-suggestions.component.html b/src/app/shared/input-suggestions/filter-suggestions/filter-input-suggestions.component.html index f1b0ba9023..d239c8db8d 100644 --- a/src/app/shared/input-suggestions/filter-suggestions/filter-input-suggestions.component.html +++ b/src/app/shared/input-suggestions/filter-suggestions/filter-input-suggestions.component.html @@ -3,20 +3,25 @@ (keydown.arrowdown)="shiftFocusDown($event)" (keydown.arrowup)="shiftFocusUp($event)" (keydown.esc)="close()" (dsClickOutside)="close();"> - +
+ +
+ [ngModelOptions]="{standalone: true}" autocomplete="off" + />