From 459da211bede61fb6f2170b96a80293885bdac6c Mon Sep 17 00:00:00 2001 From: nibou230 Date: Wed, 13 Apr 2022 13:03:37 -0400 Subject: [PATCH 1/9] Display the access status badges --- config/config.example.yml | 2 + src/app/core/core.module.ts | 4 +- src/app/core/data/item-data.service.ts | 22 +++ .../access-status-badge.component.html | 3 + .../access-status-badge.component.spec.ts | 160 ++++++++++++++++++ .../access-status-badge.component.ts | 45 +++++ .../access-status.model.ts | 23 +++ .../access-status.resource-type.ts | 9 + ...-search-result-list-element.component.html | 5 +- ...em-search-result-list-element.component.ts | 6 + src/app/shared/shared.module.ts | 2 + src/assets/i18n/en.json5 | 10 ++ src/assets/i18n/fr.json5 | 15 ++ src/config/default-app-config.ts | 5 +- src/config/ui-server-config.interface.ts | 2 + src/environments/environment.test.ts | 4 +- 16 files changed, 313 insertions(+), 4 deletions(-) create mode 100644 src/app/shared/object-list/access-status-badge/access-status-badge.component.html create mode 100644 src/app/shared/object-list/access-status-badge/access-status-badge.component.spec.ts create mode 100644 src/app/shared/object-list/access-status-badge/access-status-badge.component.ts create mode 100644 src/app/shared/object-list/access-status-badge/access-status.model.ts create mode 100644 src/app/shared/object-list/access-status-badge/access-status.resource-type.ts diff --git a/config/config.example.yml b/config/config.example.yml index 771c7b1653..724d7f8a64 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -13,6 +13,8 @@ ui: rateLimiter: windowMs: 60000 # 1 minute max: 500 # limit each IP to 500 requests per windowMs + # Show the file access status in items lists + showAccessStatuses: false # The REST API server settings # NOTE: these must be 'synced' with the 'dspace.server.url' setting in your backend's local.cfg. diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index e9e242dbc0..b90a3edd46 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -163,6 +163,7 @@ import { SequenceService } from './shared/sequence.service'; import { CoreState } from './core-state.model'; import { GroupDataService } from './eperson/group-data.service'; import { SubmissionAccessesModel } from './config/models/config-submission-accesses.model'; +import { AccessStatusObject } from '../shared/object-list/access-status-badge/access-status.model'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -346,7 +347,8 @@ export const models = UsageReport, Root, SearchConfig, - SubmissionAccessesModel + SubmissionAccessesModel, + AccessStatusObject ]; @NgModule({ diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index cb5d7a3d57..9a2dc5a8af 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -36,6 +36,7 @@ import { sendRequest } from '../shared/request.operators'; import { RestRequest } from './rest-request.model'; import { CoreState } from '../core-state.model'; import { FindListOptions } from './find-list-options.model'; +import { AccessStatusObject } from 'src/app/shared/object-list/access-status-badge/access-status.model'; @Injectable() @dataService(ITEM) @@ -291,6 +292,27 @@ export class ItemDataService extends DataService { ); } + /** + * Get the the access status + * @param itemId + */ + public getAccessStatus(itemId: string): Observable> { + const requestId = this.requestService.generateRequestId(); + const href$ = this.halService.getEndpoint('accessStatus').pipe( + map((href) => href.replace('{?uuid}', `?uuid=${itemId}`)) + ); + + href$.pipe( + find((href: string) => hasValue(href)), + map((href: string) => { + const request = new GetRequest(requestId, href); + this.requestService.send(request); + }) + ).subscribe(); + + return this.rdbService.buildFromRequestUUID(requestId); + } + /** * Invalidate the cache of the item * @param itemUUID diff --git a/src/app/shared/object-list/access-status-badge/access-status-badge.component.html b/src/app/shared/object-list/access-status-badge/access-status-badge.component.html new file mode 100644 index 0000000000..a7b3edc9b3 --- /dev/null +++ b/src/app/shared/object-list/access-status-badge/access-status-badge.component.html @@ -0,0 +1,3 @@ +
+ {{ status | translate }} +
diff --git a/src/app/shared/object-list/access-status-badge/access-status-badge.component.spec.ts b/src/app/shared/object-list/access-status-badge/access-status-badge.component.spec.ts new file mode 100644 index 0000000000..da0677a5de --- /dev/null +++ b/src/app/shared/object-list/access-status-badge/access-status-badge.component.spec.ts @@ -0,0 +1,160 @@ +import { Item } from '../../../core/shared/item.model'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { TruncatePipe } from '../../utils/truncate.pipe'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { AccessStatusBadgeComponent } from './access-status-badge.component'; +import { createSuccessfulRemoteDataObject$ } from '../../remote-data.utils'; +import { By } from '@angular/platform-browser'; +import { ItemDataService } from 'src/app/core/data/item-data.service'; +import { AccessStatusObject } from './access-status.model'; + +describe('ItemAccessStatusBadgeComponent', () => { + let component: AccessStatusBadgeComponent; + let fixture: ComponentFixture; + + let unknownStatus: AccessStatusObject; + let metadataOnlyStatus: AccessStatusObject; + let openAccessStatus: AccessStatusObject; + let embargoStatus: AccessStatusObject; + let restrictedStatus: AccessStatusObject; + + let itemDataService: ItemDataService; + + let item: Item; + + function init() { + unknownStatus = Object.assign(new AccessStatusObject(), { + status: 'unknown' + }); + + metadataOnlyStatus = Object.assign(new AccessStatusObject(), { + status: 'metadata.only' + }); + + openAccessStatus = Object.assign(new AccessStatusObject(), { + status: 'open.access' + }); + + embargoStatus = Object.assign(new AccessStatusObject(), { + status: 'embargo' + }); + + restrictedStatus = Object.assign(new AccessStatusObject(), { + status: 'restricted' + }); + + itemDataService = jasmine.createSpyObj('itemDataService', { + getAccessStatus: createSuccessfulRemoteDataObject$(unknownStatus) + }); + + item = Object.assign(new Item(), { + uuid: 'item-uuid' + }); + } + + function initTestBed() { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [AccessStatusBadgeComponent, TruncatePipe], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: ItemDataService, useValue: itemDataService} + ] + }).compileComponents(); + } + + function initFixtureAndComponent() { + fixture = TestBed.createComponent(AccessStatusBadgeComponent); + component = fixture.componentInstance; + component.uuid = item.uuid; + fixture.detectChanges(); + } + + function lookForAccessStatusBadge(status: string) { + const badge = fixture.debugElement.query(By.css('span.badge')); + expect(badge.nativeElement.textContent).toEqual(`access-status.${status.toLowerCase()}.listelement.badge`); + } + + describe('init', () => { + beforeEach(waitForAsync(() => { + init(); + initTestBed(); + })); + beforeEach(() => { + initFixtureAndComponent(); + }); + it('should init the component', () => { + expect(component).toBeTruthy(); + }); + }); + + describe('When the getAccessStatus method returns unknown', () => { + beforeEach(waitForAsync(() => { + init(); + initTestBed(); + })); + beforeEach(() => { + initFixtureAndComponent(); + }); + it('should show the unknown badge', () => { + lookForAccessStatusBadge('unknown'); + }); + }); + + describe('When the getAccessStatus method returns metadata.only', () => { + beforeEach(waitForAsync(() => { + init(); + (itemDataService.getAccessStatus as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(metadataOnlyStatus)); + initTestBed(); + })); + beforeEach(() => { + initFixtureAndComponent(); + }); + it('should show the metadata only badge', () => { + lookForAccessStatusBadge('metadata.only'); + }); + }); + + describe('When the getAccessStatus method returns open.access', () => { + beforeEach(waitForAsync(() => { + init(); + (itemDataService.getAccessStatus as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(openAccessStatus)); + initTestBed(); + })); + beforeEach(() => { + initFixtureAndComponent(); + }); + it('should show the open access badge', () => { + lookForAccessStatusBadge('open.access'); + }); + }); + + describe('When the getAccessStatus method returns embargo', () => { + beforeEach(waitForAsync(() => { + init(); + (itemDataService.getAccessStatus as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(embargoStatus)); + initTestBed(); + })); + beforeEach(() => { + initFixtureAndComponent(); + }); + it('should show the embargo badge', () => { + lookForAccessStatusBadge('embargo'); + }); + }); + + describe('When the getAccessStatus method returns restricted', () => { + beforeEach(waitForAsync(() => { + init(); + (itemDataService.getAccessStatus as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(restrictedStatus)); + initTestBed(); + })); + beforeEach(() => { + initFixtureAndComponent(); + }); + it('should show the restricted badge', () => { + lookForAccessStatusBadge('restricted'); + }); + }); +}); diff --git a/src/app/shared/object-list/access-status-badge/access-status-badge.component.ts b/src/app/shared/object-list/access-status-badge/access-status-badge.component.ts new file mode 100644 index 0000000000..b181ae4104 --- /dev/null +++ b/src/app/shared/object-list/access-status-badge/access-status-badge.component.ts @@ -0,0 +1,45 @@ +import { Component, Input } from '@angular/core'; +import { map } from 'rxjs/operators'; +import { Observable } from 'rxjs'; +import { getFirstSucceededRemoteDataPayload } from 'src/app/core/shared/operators'; +import { ItemDataService } from 'src/app/core/data/item-data.service'; +import { AccessStatusObject } from './access-status.model'; +import { hasValue } from '../../empty.util'; + +@Component({ + selector: 'ds-access-status-badge', + templateUrl: './access-status-badge.component.html' +}) +/** + * Component rendering the access status of an item as a badge + */ +export class AccessStatusBadgeComponent { + + private _uuid: string; + private _accessStatus$: Observable; + + /** + * Initialize instance variables + * + * @param {ItemDataService} itemDataService + */ + constructor(private itemDataService: ItemDataService) { } + + ngOnInit(): void { + this._accessStatus$ = this.itemDataService + .getAccessStatus(this._uuid) + .pipe( + getFirstSucceededRemoteDataPayload(), + map((accessStatus: AccessStatusObject) => hasValue(accessStatus.status) ? accessStatus.status : 'unknown'), + map((status: string) => `access-status.${status.toLowerCase()}.listelement.badge`) + ); + } + + @Input() set uuid(uuid: string) { + this._uuid = uuid; + } + + get accessStatus$(): Observable { + return this._accessStatus$; + } +} diff --git a/src/app/shared/object-list/access-status-badge/access-status.model.ts b/src/app/shared/object-list/access-status-badge/access-status.model.ts new file mode 100644 index 0000000000..6cdcafdbd8 --- /dev/null +++ b/src/app/shared/object-list/access-status-badge/access-status.model.ts @@ -0,0 +1,23 @@ +import { autoserialize } from 'cerialize'; +import { typedObject } from 'src/app/core/cache/builders/build-decorators'; +import { ResourceType } from 'src/app/core/shared/resource-type'; +import { excludeFromEquals } from 'src/app/core/utilities/equals.decorators'; +import { ACCESS_STATUS } from './access-status.resource-type'; + +@typedObject +export class AccessStatusObject { + static type = ACCESS_STATUS; + + /** + * The type for this AccessStatusObject + */ + @excludeFromEquals + @autoserialize + type: ResourceType; + + /** + * The access status value + */ + @autoserialize + status: string; +} diff --git a/src/app/shared/object-list/access-status-badge/access-status.resource-type.ts b/src/app/shared/object-list/access-status-badge/access-status.resource-type.ts new file mode 100644 index 0000000000..ead2afc0b1 --- /dev/null +++ b/src/app/shared/object-list/access-status-badge/access-status.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from 'src/app/core/shared/resource-type'; + +/** + * The resource type for Access Status + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const ACCESS_STATUS = new ResourceType('accessStatus'); diff --git a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html index 8898632eb5..8bea795cca 100644 --- a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html +++ b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html @@ -1,4 +1,7 @@ - +
+ + +
Date: Wed, 20 Apr 2022 09:12:59 -0400 Subject: [PATCH 2/9] Adapt the service to a LinkedRepository result --- src/app/core/data/item-data.service.spec.ts | 36 ++++++++++++++++--- src/app/core/data/item-data.service.ts | 30 +++++++++------- .../data/version-history-data.service.spec.ts | 2 +- .../access-status.model.ts | 14 ++++++-- .../testing/hal-endpoint-service.stub.ts | 6 +++- 5 files changed, 67 insertions(+), 21 deletions(-) diff --git a/src/app/core/data/item-data.service.spec.ts b/src/app/core/data/item-data.service.spec.ts index cc1e3b6e20..e85ddb2f38 100644 --- a/src/app/core/data/item-data.service.spec.ts +++ b/src/app/core/data/item-data.service.spec.ts @@ -10,12 +10,13 @@ import { ObjectCacheService } from '../cache/object-cache.service'; import { RestResponse } from '../cache/response.models'; import { ExternalSourceEntry } from '../shared/external-source-entry.model'; import { ItemDataService } from './item-data.service'; -import { DeleteRequest, PostRequest } from './request.models'; +import { DeleteRequest, GetRequest, PostRequest } from './request.models'; import { RequestService } from './request.service'; import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; import { CoreState } from '../core-state.model'; import { RequestEntry } from './request-entry.model'; import { FindListOptions } from './find-list-options.model'; +import { HALEndpointServiceStub } from 'src/app/shared/testing/hal-endpoint-service.stub'; describe('ItemDataService', () => { let scheduler: TestScheduler; @@ -36,13 +37,11 @@ describe('ItemDataService', () => { }) as RequestService; const rdbService = getMockRemoteDataBuildService(); - const itemEndpoint = 'https://rest.api/core/items'; + const itemEndpoint = 'https://rest.api/core'; const store = {} as Store; const objectCache = {} as ObjectCacheService; - const halEndpointService = jasmine.createSpyObj('halService', { - getEndpoint: observableOf(itemEndpoint) - }); + const halEndpointService: any = new HALEndpointServiceStub(itemEndpoint); const bundleService = jasmine.createSpyObj('bundleService', { findByHref: {} }); @@ -185,6 +184,33 @@ describe('ItemDataService', () => { }); }); + describe('getAccessStatusEndpoint', () => { + beforeEach(() => { + service = initTestService(); + }); + it('should retrieve the access status endpoint', () => { + const itemId = '3de6ea60-ec39-419b-ae6f-065930ac1429'; + const result = service.getAccessStatusEndpoint(itemId); + result.subscribe(((value) => { + expect(value).toEqual(`${itemEndpoint}/items/${itemId}/accessStatus`); + })); + }); + }); + + describe('getAccessStatus', () => { + beforeEach(() => { + service = initTestService(); + }); + it('should send a GET request', (done) => { + const itemId = '3de6ea60-ec39-419b-ae6f-065930ac1429'; + const result = service.getAccessStatus(itemId); + result.subscribe(() => { + expect(requestService.send).toHaveBeenCalledWith(jasmine.any(GetRequest)); + done(); + }); + }); + }); + describe('when cache is invalidated', () => { beforeEach(() => { service = initTestService(); diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 9a2dc5a8af..c7f0f541f8 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -292,25 +292,31 @@ export class ItemDataService extends DataService { ); } + /** + * Get the endpoint for an item's access status + * @param itemId + */ + public getAccessStatusEndpoint(itemId: string): Observable { + return this.halService.getEndpoint(this.linkPath).pipe( + switchMap((url: string) => this.halService.getEndpoint('accessStatus', `${url}/${itemId}`)) + ); + } + /** * Get the the access status * @param itemId */ public getAccessStatus(itemId: string): Observable> { - const requestId = this.requestService.generateRequestId(); - const href$ = this.halService.getEndpoint('accessStatus').pipe( - map((href) => href.replace('{?uuid}', `?uuid=${itemId}`)) - ); + const hrefObs = this.getAccessStatusEndpoint(itemId); - href$.pipe( - find((href: string) => hasValue(href)), - map((href: string) => { - const request = new GetRequest(requestId, href); - this.requestService.send(request); - }) - ).subscribe(); + hrefObs.pipe( + take(1) + ).subscribe((href) => { + const request = new GetRequest(this.requestService.generateRequestId(), href); + this.requestService.send(request); + }); - return this.rdbService.buildFromRequestUUID(requestId); + return this.rdbService.buildSingle(hrefObs); } /** diff --git a/src/app/core/data/version-history-data.service.spec.ts b/src/app/core/data/version-history-data.service.spec.ts index 207093b4d5..26ed370026 100644 --- a/src/app/core/data/version-history-data.service.spec.ts +++ b/src/app/core/data/version-history-data.service.spec.ts @@ -151,7 +151,7 @@ describe('VersionHistoryDataService', () => { describe('when getVersionsEndpoint is called', () => { it('should return the correct value', () => { service.getVersionsEndpoint(versionHistoryId).subscribe((res) => { - expect(res).toBe(url + '/versions'); + expect(res).toBe(url + '/versionhistories/version-history-id/versions'); }); }); }); diff --git a/src/app/shared/object-list/access-status-badge/access-status.model.ts b/src/app/shared/object-list/access-status-badge/access-status.model.ts index 6cdcafdbd8..31de1a3a38 100644 --- a/src/app/shared/object-list/access-status-badge/access-status.model.ts +++ b/src/app/shared/object-list/access-status-badge/access-status.model.ts @@ -1,11 +1,13 @@ -import { autoserialize } from 'cerialize'; +import { autoserialize, deserialize } from 'cerialize'; import { typedObject } from 'src/app/core/cache/builders/build-decorators'; +import { CacheableObject } from 'src/app/core/cache/object-cache.reducer'; +import { HALLink } from 'src/app/core/shared/hal-link.model'; import { ResourceType } from 'src/app/core/shared/resource-type'; import { excludeFromEquals } from 'src/app/core/utilities/equals.decorators'; import { ACCESS_STATUS } from './access-status.resource-type'; @typedObject -export class AccessStatusObject { +export class AccessStatusObject implements CacheableObject { static type = ACCESS_STATUS; /** @@ -20,4 +22,12 @@ export class AccessStatusObject { */ @autoserialize status: string; + + /** + * The {@link HALLink}s for this AccessStatusObject + */ + @deserialize + _links: { + self: HALLink; + }; } diff --git a/src/app/shared/testing/hal-endpoint-service.stub.ts b/src/app/shared/testing/hal-endpoint-service.stub.ts index 19f95d577c..753efcdb5d 100644 --- a/src/app/shared/testing/hal-endpoint-service.stub.ts +++ b/src/app/shared/testing/hal-endpoint-service.stub.ts @@ -1,9 +1,13 @@ import { of as observableOf } from 'rxjs'; +import { hasValue } from '../empty.util'; export class HALEndpointServiceStub { constructor(private url: string) {} - getEndpoint(path: string) { + getEndpoint(path: string, startHref?: string) { + if (hasValue(startHref)) { + return observableOf(startHref + '/' + path); + } return observableOf(this.url + '/' + path); } } From a2a241b906d457164f2954733f8481792e341269 Mon Sep 17 00:00:00 2001 From: nibou230 Date: Thu, 21 Apr 2022 12:32:11 -0400 Subject: [PATCH 3/9] Added the badge to some components for coherence --- ...m-search-result-grid-element.component.html | 1 + ...tem-search-result-grid-element.component.ts | 6 ++++++ .../access-status-badge.component.html | 4 ++-- .../access-status-badge.component.ts | 18 +++++++++++++----- .../item-list-preview.component.html | 5 ++++- .../item-list-preview.component.ts | 9 +++++++++ ...m-search-result-list-element.component.html | 2 +- 7 files changed, 36 insertions(+), 9 deletions(-) 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 7fdb505d43..c4bb444d44 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 @@ -18,6 +18,7 @@
+

diff --git a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.ts b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.ts index a628c1e7e8..77fc25ff29 100644 --- a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.ts +++ b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.ts @@ -6,6 +6,7 @@ import { SearchResultGridElementComponent } from '../../search-result-grid-eleme import { Item } from '../../../../../core/shared/item.model'; import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model'; import { getItemPageRoute } from '../../../../../item-page/item-page-routing-paths'; +import { environment } from 'src/environments/environment'; @listableObjectComponent('PublicationSearchResult', ViewMode.GridElement) @listableObjectComponent(ItemSearchResult, ViewMode.GridElement) @@ -23,9 +24,14 @@ export class ItemSearchResultGridElementComponent extends SearchResultGridElemen * Route to the item's page */ itemPageRoute: string; + /** + * Whether to show the access status badge or not + */ + showAccessStatus: boolean; ngOnInit(): void { super.ngOnInit(); this.itemPageRoute = getItemPageRoute(this.dso); + this.showAccessStatus = environment.ui.showAccessStatuses; } } diff --git a/src/app/shared/object-list/access-status-badge/access-status-badge.component.html b/src/app/shared/object-list/access-status-badge/access-status-badge.component.html index a7b3edc9b3..8fc3a82740 100644 --- a/src/app/shared/object-list/access-status-badge/access-status-badge.component.html +++ b/src/app/shared/object-list/access-status-badge/access-status-badge.component.html @@ -1,3 +1,3 @@ -
- {{ status | translate }} +
+ {{ accessStatus | translate }}
diff --git a/src/app/shared/object-list/access-status-badge/access-status-badge.component.ts b/src/app/shared/object-list/access-status-badge/access-status-badge.component.ts index b181ae4104..bf76a429f7 100644 --- a/src/app/shared/object-list/access-status-badge/access-status-badge.component.ts +++ b/src/app/shared/object-list/access-status-badge/access-status-badge.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from '@angular/core'; -import { map } from 'rxjs/operators'; -import { Observable } from 'rxjs'; -import { getFirstSucceededRemoteDataPayload } from 'src/app/core/shared/operators'; +import { catchError, map } from 'rxjs/operators'; +import { Observable, of as observableOf } from 'rxjs'; +import { getFirstCompletedRemoteData } from 'src/app/core/shared/operators'; import { ItemDataService } from 'src/app/core/data/item-data.service'; import { AccessStatusObject } from './access-status.model'; import { hasValue } from '../../empty.util'; @@ -29,9 +29,17 @@ export class AccessStatusBadgeComponent { this._accessStatus$ = this.itemDataService .getAccessStatus(this._uuid) .pipe( - getFirstSucceededRemoteDataPayload(), + getFirstCompletedRemoteData(), + map((accessStatusRD) => { + if (accessStatusRD.statusCode !== 401 && hasValue(accessStatusRD.payload)) { + return accessStatusRD.payload; + } else { + return []; + } + }), map((accessStatus: AccessStatusObject) => hasValue(accessStatus.status) ? accessStatus.status : 'unknown'), - map((status: string) => `access-status.${status.toLowerCase()}.listelement.badge`) + map((status: string) => `access-status.${status.toLowerCase()}.listelement.badge`), + catchError(() => observableOf('access-status.unknown.listelement.badge')) ); } diff --git a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html index c518d39bd9..4801e66a26 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html +++ b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html @@ -2,7 +2,10 @@ - +
+ + +

diff --git a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts index 840960d51f..6b2290c711 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts @@ -1,4 +1,5 @@ import { Component, Input } from '@angular/core'; +import { environment } from 'src/environments/environment'; import { Item } from '../../../../core/shared/item.model'; import { fadeInOut } from '../../../animations/fade'; @@ -36,4 +37,12 @@ export class ItemListPreviewComponent { */ @Input() showSubmitter = false; + /** + * Whether to show the access status badge or not + */ + showAccessStatus: boolean; + + ngOnInit(): void { + this.showAccessStatus = environment.ui.showAccessStatuses; + } } diff --git a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html index 8bea795cca..8e216bb82c 100644 --- a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html +++ b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html @@ -1,6 +1,6 @@
- +
From 9f50b4997cbe0b6a93c701ceff93596af0b9922b Mon Sep 17 00:00:00 2001 From: nibou230 Date: Thu, 21 Apr 2022 14:30:23 -0400 Subject: [PATCH 4/9] Changed the import for CacheableObject --- .../object-list/access-status-badge/access-status.model.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/object-list/access-status-badge/access-status.model.ts b/src/app/shared/object-list/access-status-badge/access-status.model.ts index 31de1a3a38..69b5e920d0 100644 --- a/src/app/shared/object-list/access-status-badge/access-status.model.ts +++ b/src/app/shared/object-list/access-status-badge/access-status.model.ts @@ -1,6 +1,6 @@ import { autoserialize, deserialize } from 'cerialize'; import { typedObject } from 'src/app/core/cache/builders/build-decorators'; -import { CacheableObject } from 'src/app/core/cache/object-cache.reducer'; +import { CacheableObject } from 'src/app/core/cache/cacheable-object.model'; import { HALLink } from 'src/app/core/shared/hal-link.model'; import { ResourceType } from 'src/app/core/shared/resource-type'; import { excludeFromEquals } from 'src/app/core/utilities/equals.decorators'; From 2b77e4a90d873312de08766a77fa650a4e9a5a12 Mon Sep 17 00:00:00 2001 From: nibou230 Date: Wed, 4 May 2022 11:17:25 -0400 Subject: [PATCH 5/9] Moved showAccessStatus from UI to item config Renamed item-page to item to make it generic --- config/config.example.yml | 6 +++--- .../item-search-result-grid-element.component.ts | 2 +- .../item-list-preview.component.ts | 2 +- .../item-search-result-list-element.component.ts | 2 +- src/config/app-config.interface.ts | 4 ++-- src/config/default-app-config.ts | 15 +++++++-------- src/config/item-config.interface.ts | 9 +++++++++ src/config/item-page-config.interface.ts | 7 ------- src/config/ui-server-config.interface.ts | 2 -- src/environments/environment.test.ts | 8 ++++---- 10 files changed, 28 insertions(+), 29 deletions(-) create mode 100644 src/config/item-config.interface.ts delete mode 100644 src/config/item-page-config.interface.ts diff --git a/config/config.example.yml b/config/config.example.yml index 724d7f8a64..37428bb7cd 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -13,8 +13,6 @@ ui: rateLimiter: windowMs: 60000 # 1 minute max: 500 # limit each IP to 500 requests per windowMs - # Show the file access status in items lists - showAccessStatuses: false # The REST API server settings # NOTE: these must be 'synced' with the 'dspace.server.url' setting in your backend's local.cfg. @@ -163,10 +161,12 @@ browseBy: # The absolute lowest year to display in the dropdown (only used when no lowest date can be found for all items) defaultLowerLimit: 1900 -# Item Page Config +# Item Config item: edit: undoTimeout: 10000 # 10 seconds + # Show the item access status label in items lists + showAccessStatuses: false # Collection Page Config collection: diff --git a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.ts b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.ts index 77fc25ff29..6873881d17 100644 --- a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.ts +++ b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.ts @@ -32,6 +32,6 @@ export class ItemSearchResultGridElementComponent extends SearchResultGridElemen ngOnInit(): void { super.ngOnInit(); this.itemPageRoute = getItemPageRoute(this.dso); - this.showAccessStatus = environment.ui.showAccessStatuses; + this.showAccessStatus = environment.item.showAccessStatuses; } } diff --git a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts index 6b2290c711..52d1b268d2 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts @@ -43,6 +43,6 @@ export class ItemListPreviewComponent { showAccessStatus: boolean; ngOnInit(): void { - this.showAccessStatus = environment.ui.showAccessStatuses; + this.showAccessStatus = environment.item.showAccessStatuses; } } diff --git a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.ts b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.ts index bbc81e8216..832654ac38 100644 --- a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.ts +++ b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.ts @@ -30,6 +30,6 @@ export class ItemSearchResultListElementComponent extends SearchResultListElemen ngOnInit(): void { super.ngOnInit(); this.itemPageRoute = getItemPageRoute(this.dso); - this.showAccessStatus = environment.ui.showAccessStatuses; + this.showAccessStatus = environment.item.showAccessStatuses; } } diff --git a/src/config/app-config.interface.ts b/src/config/app-config.interface.ts index 121e80cd74..cff908e149 100644 --- a/src/config/app-config.interface.ts +++ b/src/config/app-config.interface.ts @@ -7,7 +7,7 @@ import { INotificationBoardOptions } from './notifications-config.interfaces'; import { SubmissionConfig } from './submission-config.interface'; import { FormConfig } from './form-config.interfaces'; import { LangConfig } from './lang-config.interface'; -import { ItemPageConfig } from './item-page-config.interface'; +import { ItemConfig } from './item-config.interface'; import { CollectionPageConfig } from './collection-page-config.interface'; import { ThemeConfig } from './theme.model'; import { AuthConfig } from './auth-config.interfaces'; @@ -28,7 +28,7 @@ interface AppConfig extends Config { defaultLanguage: string; languages: LangConfig[]; browseBy: BrowseByConfig; - item: ItemPageConfig; + item: ItemConfig; collection: CollectionPageConfig; themes: ThemeConfig[]; mediaViewer: MediaViewerConfig; diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index 46f07f8f64..5bc1532e7a 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -6,7 +6,7 @@ import { BrowseByConfig } from './browse-by-config.interface'; import { CacheConfig } from './cache-config.interface'; import { CollectionPageConfig } from './collection-page-config.interface'; import { FormConfig } from './form-config.interfaces'; -import { ItemPageConfig } from './item-page-config.interface'; +import { ItemConfig } from './item-config.interface'; import { LangConfig } from './lang-config.interface'; import { MediaViewerConfig } from './media-viewer-config.interface'; import { INotificationBoardOptions } from './notifications-config.interfaces'; @@ -34,10 +34,7 @@ export class DefaultAppConfig implements AppConfig { rateLimiter: { windowMs: 1 * 60 * 1000, // 1 minute max: 500 // limit each IP to 500 requests per windowMs - }, - - // Show the file access status in items lists - showAccessStatuses: false + } }; // The REST API server settings @@ -199,11 +196,13 @@ export class DefaultAppConfig implements AppConfig { defaultLowerLimit: 1900 }; - // Item Page Config - item: ItemPageConfig = { + // Item Config + item: ItemConfig = { edit: { undoTimeout: 10000 // 10 seconds - } + }, + // Show the item access status label in items lists + showAccessStatuses: false }; // Collection Page Config diff --git a/src/config/item-config.interface.ts b/src/config/item-config.interface.ts new file mode 100644 index 0000000000..f842c37c05 --- /dev/null +++ b/src/config/item-config.interface.ts @@ -0,0 +1,9 @@ +import { Config } from './config.interface'; + +export interface ItemConfig extends Config { + edit: { + undoTimeout: number; + }; + // This is used to show the access status label of items in results lists + showAccessStatuses: boolean; +} diff --git a/src/config/item-page-config.interface.ts b/src/config/item-page-config.interface.ts deleted file mode 100644 index 2b05e28715..0000000000 --- a/src/config/item-page-config.interface.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Config } from './config.interface'; - -export interface ItemPageConfig extends Config { - edit: { - undoTimeout: number; - }; -} diff --git a/src/config/ui-server-config.interface.ts b/src/config/ui-server-config.interface.ts index 6ff10e214d..93f90c345c 100644 --- a/src/config/ui-server-config.interface.ts +++ b/src/config/ui-server-config.interface.ts @@ -10,7 +10,5 @@ export class UIServerConfig extends ServerConfig { windowMs: number; max: number; }; - // This section is used to show the access status of items in results lists - showAccessStatuses: boolean; } diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts index d84a65dc0e..da4800cc12 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -25,9 +25,7 @@ export const environment: BuildConfig = { rateLimiter: { windowMs: 1 * 60 * 1000, // 1 minute max: 500 // limit each IP to 500 requests per windowMs - }, - // Show the file access status in items lists - showAccessStatuses: false + } }, // The REST API server settings. @@ -198,7 +196,9 @@ export const environment: BuildConfig = { item: { edit: { undoTimeout: 10000 // 10 seconds - } + }, + // Show the item access status label in items lists + showAccessStatuses: false }, collection: { edit: { From 6c8d12394c2d621ac19857ba06020984ab75db1d Mon Sep 17 00:00:00 2001 From: nibou230 Date: Wed, 4 May 2022 11:45:36 -0400 Subject: [PATCH 6/9] Moved showAccessStatus to be self-contained --- .../item-search-result-grid-element.component.html | 2 +- .../item/item-search-result-grid-element.component.ts | 6 ------ .../access-status-badge.component.html | 8 +++++--- .../access-status-badge.component.ts | 7 +++++++ .../item-list-preview/item-list-preview.component.html | 2 +- .../item-list-preview/item-list-preview.component.ts | 10 ---------- .../item-search-result-list-element.component.html | 2 +- .../item/item-search-result-list-element.component.ts | 6 ------ 8 files changed, 15 insertions(+), 28 deletions(-) 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 c4bb444d44..7ccc722526 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 @@ -18,7 +18,7 @@
- +

diff --git a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.ts b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.ts index 6873881d17..a628c1e7e8 100644 --- a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.ts +++ b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.ts @@ -6,7 +6,6 @@ import { SearchResultGridElementComponent } from '../../search-result-grid-eleme import { Item } from '../../../../../core/shared/item.model'; import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model'; import { getItemPageRoute } from '../../../../../item-page/item-page-routing-paths'; -import { environment } from 'src/environments/environment'; @listableObjectComponent('PublicationSearchResult', ViewMode.GridElement) @listableObjectComponent(ItemSearchResult, ViewMode.GridElement) @@ -24,14 +23,9 @@ export class ItemSearchResultGridElementComponent extends SearchResultGridElemen * Route to the item's page */ itemPageRoute: string; - /** - * Whether to show the access status badge or not - */ - showAccessStatus: boolean; ngOnInit(): void { super.ngOnInit(); this.itemPageRoute = getItemPageRoute(this.dso); - this.showAccessStatus = environment.item.showAccessStatuses; } } diff --git a/src/app/shared/object-list/access-status-badge/access-status-badge.component.html b/src/app/shared/object-list/access-status-badge/access-status-badge.component.html index 8fc3a82740..3877663419 100644 --- a/src/app/shared/object-list/access-status-badge/access-status-badge.component.html +++ b/src/app/shared/object-list/access-status-badge/access-status-badge.component.html @@ -1,3 +1,5 @@ -
- {{ accessStatus | translate }} -
+ +
+ {{ accessStatus | translate }} +
+
diff --git a/src/app/shared/object-list/access-status-badge/access-status-badge.component.ts b/src/app/shared/object-list/access-status-badge/access-status-badge.component.ts index bf76a429f7..68fa9180d7 100644 --- a/src/app/shared/object-list/access-status-badge/access-status-badge.component.ts +++ b/src/app/shared/object-list/access-status-badge/access-status-badge.component.ts @@ -5,6 +5,7 @@ import { getFirstCompletedRemoteData } from 'src/app/core/shared/operators'; import { ItemDataService } from 'src/app/core/data/item-data.service'; import { AccessStatusObject } from './access-status.model'; import { hasValue } from '../../empty.util'; +import { environment } from 'src/environments/environment'; @Component({ selector: 'ds-access-status-badge', @@ -18,6 +19,11 @@ export class AccessStatusBadgeComponent { private _uuid: string; private _accessStatus$: Observable; + /** + * Whether to show the access status badge or not + */ + showAccessStatus: boolean; + /** * Initialize instance variables * @@ -26,6 +32,7 @@ export class AccessStatusBadgeComponent { constructor(private itemDataService: ItemDataService) { } ngOnInit(): void { + this.showAccessStatus = environment.item.showAccessStatuses; this._accessStatus$ = this.itemDataService .getAccessStatus(this._uuid) .pipe( diff --git a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html index 4801e66a26..137b59d77b 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html +++ b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html @@ -4,7 +4,7 @@
- +

diff --git a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts index 52d1b268d2..34b2d979c1 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts @@ -1,5 +1,4 @@ import { Component, Input } from '@angular/core'; -import { environment } from 'src/environments/environment'; import { Item } from '../../../../core/shared/item.model'; import { fadeInOut } from '../../../animations/fade'; @@ -36,13 +35,4 @@ export class ItemListPreviewComponent { * A boolean representing if to show submitter information */ @Input() showSubmitter = false; - - /** - * Whether to show the access status badge or not - */ - showAccessStatus: boolean; - - ngOnInit(): void { - this.showAccessStatus = environment.item.showAccessStatuses; - } } diff --git a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html index 8e216bb82c..b51c45290b 100644 --- a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html +++ b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html @@ -1,6 +1,6 @@
- +
diff --git a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.ts b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.ts index 832654ac38..b5eb64869d 100644 --- a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.ts +++ b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.ts @@ -5,7 +5,6 @@ import { ItemSearchResult } from '../../../../../object-collection/shared/item-s import { SearchResultListElementComponent } from '../../../search-result-list-element.component'; import { Item } from '../../../../../../core/shared/item.model'; import { getItemPageRoute } from '../../../../../../item-page/item-page-routing-paths'; -import { environment } from '../../../../../../../environments/environment'; @listableObjectComponent('PublicationSearchResult', ViewMode.ListElement) @listableObjectComponent(ItemSearchResult, ViewMode.ListElement) @@ -22,14 +21,9 @@ export class ItemSearchResultListElementComponent extends SearchResultListElemen * Route to the item's page */ itemPageRoute: string; - /** - * Whether to show the access status badge or not - */ - showAccessStatus: boolean; ngOnInit(): void { super.ngOnInit(); this.itemPageRoute = getItemPageRoute(this.dso); - this.showAccessStatus = environment.item.showAccessStatuses; } } From 8e03b28151842cebcb577412f40f98827dee960d Mon Sep 17 00:00:00 2001 From: nibou230 Date: Wed, 4 May 2022 11:49:33 -0400 Subject: [PATCH 7/9] Use component vars in AccessStatusBadgeComponent --- .../access-status-badge.component.ts | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/app/shared/object-list/access-status-badge/access-status-badge.component.ts b/src/app/shared/object-list/access-status-badge/access-status-badge.component.ts index 68fa9180d7..82f8fa3a03 100644 --- a/src/app/shared/object-list/access-status-badge/access-status-badge.component.ts +++ b/src/app/shared/object-list/access-status-badge/access-status-badge.component.ts @@ -16,8 +16,8 @@ import { environment } from 'src/environments/environment'; */ export class AccessStatusBadgeComponent { - private _uuid: string; - private _accessStatus$: Observable; + @Input() uuid: string; + accessStatus$: Observable; /** * Whether to show the access status badge or not @@ -33,8 +33,8 @@ export class AccessStatusBadgeComponent { ngOnInit(): void { this.showAccessStatus = environment.item.showAccessStatuses; - this._accessStatus$ = this.itemDataService - .getAccessStatus(this._uuid) + this.accessStatus$ = this.itemDataService + .getAccessStatus(this.uuid) .pipe( getFirstCompletedRemoteData(), map((accessStatusRD) => { @@ -49,12 +49,4 @@ export class AccessStatusBadgeComponent { catchError(() => observableOf('access-status.unknown.listelement.badge')) ); } - - @Input() set uuid(uuid: string) { - this._uuid = uuid; - } - - get accessStatus$(): Observable { - return this._accessStatus$; - } } From e77821eef08ef3061895ba0288be3d5d43444511 Mon Sep 17 00:00:00 2001 From: nibou230 Date: Thu, 5 May 2022 10:21:55 -0400 Subject: [PATCH 8/9] Use the item's HALLink to get the access status --- src/app/core/core.module.ts | 2 + .../core/data/access-status-data.service.ts | 45 +++++++++++++++++++ src/app/core/data/item-data.service.ts | 28 ------------ src/app/core/shared/item.model.ts | 10 +++++ ...-search-result-grid-element.component.html | 2 +- .../access-status-badge.component.ts | 45 ++++++++++--------- .../item-list-preview.component.html | 2 +- ...-search-result-list-element.component.html | 2 +- src/app/shared/search/search.component.ts | 4 +- 9 files changed, 88 insertions(+), 52 deletions(-) create mode 100644 src/app/core/data/access-status-data.service.ts diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index b90a3edd46..f1b0dc4f37 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -164,6 +164,7 @@ import { CoreState } from './core-state.model'; import { GroupDataService } from './eperson/group-data.service'; import { SubmissionAccessesModel } from './config/models/config-submission-accesses.model'; import { AccessStatusObject } from '../shared/object-list/access-status-badge/access-status.model'; +import { AccessStatusDataService } from './data/access-status-data.service'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -221,6 +222,7 @@ const PROVIDERS = [ MyDSpaceResponseParsingService, ServerResponseService, BrowseService, + AccessStatusDataService, SubmissionCcLicenseDataService, SubmissionCcLicenseUrlDataService, SubmissionFormsConfigService, diff --git a/src/app/core/data/access-status-data.service.ts b/src/app/core/data/access-status-data.service.ts new file mode 100644 index 0000000000..09843fac9b --- /dev/null +++ b/src/app/core/data/access-status-data.service.ts @@ -0,0 +1,45 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { dataService } from '../cache/builders/build-decorators'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { DataService } from './data.service'; +import { RequestService } from './request.service'; +import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; +import { CoreState } from '../core-state.model'; +import { AccessStatusObject } from 'src/app/shared/object-list/access-status-badge/access-status.model'; +import { ACCESS_STATUS } from 'src/app/shared/object-list/access-status-badge/access-status.resource-type'; +import { Observable } from 'rxjs'; +import { RemoteData } from './remote-data'; +import { Item } from '../shared/item.model'; + +@Injectable() +@dataService(ACCESS_STATUS) +export class AccessStatusDataService extends DataService { + + protected linkPath = 'accessStatus'; + + constructor( + protected comparator: DefaultChangeAnalyzer, + protected halService: HALEndpointService, + protected http: HttpClient, + protected notificationsService: NotificationsService, + protected objectCache: ObjectCacheService, + protected rdbService: RemoteDataBuildService, + protected requestService: RequestService, + protected store: Store, + ) { + super(); + } + + /** + * Returns {@link RemoteData} of {@link AccessStatusObject} that is the access status of the given item + * @param item Item we want the access status of + */ + findAccessStatusFor(item: Item): Observable> { + return this.findByHref(item._links.accessStatus.href); + } +} diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index c7f0f541f8..cb5d7a3d57 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -36,7 +36,6 @@ import { sendRequest } from '../shared/request.operators'; import { RestRequest } from './rest-request.model'; import { CoreState } from '../core-state.model'; import { FindListOptions } from './find-list-options.model'; -import { AccessStatusObject } from 'src/app/shared/object-list/access-status-badge/access-status.model'; @Injectable() @dataService(ITEM) @@ -292,33 +291,6 @@ export class ItemDataService extends DataService { ); } - /** - * Get the endpoint for an item's access status - * @param itemId - */ - public getAccessStatusEndpoint(itemId: string): Observable { - return this.halService.getEndpoint(this.linkPath).pipe( - switchMap((url: string) => this.halService.getEndpoint('accessStatus', `${url}/${itemId}`)) - ); - } - - /** - * Get the the access status - * @param itemId - */ - public getAccessStatus(itemId: string): Observable> { - const hrefObs = this.getAccessStatusEndpoint(itemId); - - hrefObs.pipe( - take(1) - ).subscribe((href) => { - const request = new GetRequest(this.requestService.generateRequestId(), href); - this.requestService.send(request); - }); - - return this.rdbService.buildSingle(hrefObs); - } - /** * Invalidate the cache of the item * @param itemUUID diff --git a/src/app/core/shared/item.model.ts b/src/app/core/shared/item.model.ts index d98c22225e..49ca7750b4 100644 --- a/src/app/core/shared/item.model.ts +++ b/src/app/core/shared/item.model.ts @@ -21,6 +21,8 @@ import { Version } from './version.model'; import { VERSION } from './version.resource-type'; import { BITSTREAM } from './bitstream.resource-type'; import { Bitstream } from './bitstream.model'; +import { ACCESS_STATUS } from 'src/app/shared/object-list/access-status-badge/access-status.resource-type'; +import { AccessStatusObject } from 'src/app/shared/object-list/access-status-badge/access-status.model'; /** * Class representing a DSpace Item @@ -72,6 +74,7 @@ export class Item extends DSpaceObject implements ChildHALResource { templateItemOf: HALLink; version: HALLink; thumbnail: HALLink; + accessStatus: HALLink; self: HALLink; }; @@ -110,6 +113,13 @@ export class Item extends DSpaceObject implements ChildHALResource { @link(BITSTREAM, false, 'thumbnail') thumbnail?: Observable>; + /** + * The access status for this Item + * Will be undefined unless the access status {@link HALLink} has been resolved. + */ + @link(ACCESS_STATUS) + accessStatus?: Observable>; + /** * Method that returns as which type of object this object should be rendered */ 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 7ccc722526..4344cf9a00 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 @@ -18,7 +18,7 @@
- +

diff --git a/src/app/shared/object-list/access-status-badge/access-status-badge.component.ts b/src/app/shared/object-list/access-status-badge/access-status-badge.component.ts index 82f8fa3a03..59ac30f393 100644 --- a/src/app/shared/object-list/access-status-badge/access-status-badge.component.ts +++ b/src/app/shared/object-list/access-status-badge/access-status-badge.component.ts @@ -1,11 +1,11 @@ import { Component, Input } from '@angular/core'; import { catchError, map } from 'rxjs/operators'; import { Observable, of as observableOf } from 'rxjs'; -import { getFirstCompletedRemoteData } from 'src/app/core/shared/operators'; -import { ItemDataService } from 'src/app/core/data/item-data.service'; import { AccessStatusObject } from './access-status.model'; import { hasValue } from '../../empty.util'; import { environment } from 'src/environments/environment'; +import { Item } from 'src/app/core/shared/item.model'; +import { AccessStatusDataService } from 'src/app/core/data/access-status-data.service'; @Component({ selector: 'ds-access-status-badge', @@ -16,7 +16,7 @@ import { environment } from 'src/environments/environment'; */ export class AccessStatusBadgeComponent { - @Input() uuid: string; + @Input() item: Item; accessStatus$: Observable; /** @@ -27,26 +27,31 @@ export class AccessStatusBadgeComponent { /** * Initialize instance variables * - * @param {ItemDataService} itemDataService + * @param {AccessStatusDataService} accessStatusDataService */ - constructor(private itemDataService: ItemDataService) { } + constructor(private accessStatusDataService: AccessStatusDataService) { } ngOnInit(): void { this.showAccessStatus = environment.item.showAccessStatuses; - this.accessStatus$ = this.itemDataService - .getAccessStatus(this.uuid) - .pipe( - getFirstCompletedRemoteData(), - map((accessStatusRD) => { - if (accessStatusRD.statusCode !== 401 && hasValue(accessStatusRD.payload)) { - return accessStatusRD.payload; - } else { - return []; - } - }), - map((accessStatus: AccessStatusObject) => hasValue(accessStatus.status) ? accessStatus.status : 'unknown'), - map((status: string) => `access-status.${status.toLowerCase()}.listelement.badge`), - catchError(() => observableOf('access-status.unknown.listelement.badge')) - ); + if (!this.showAccessStatus || this.item == null) { + // Do not show the badge if the feature is inactive or if the item is null. + return; + } + if (this.item.accessStatus == null) { + // In case the access status has not been loaded, do it individually. + this.item.accessStatus = this.accessStatusDataService.findAccessStatusFor(this.item); + } + this.accessStatus$ = this.item.accessStatus.pipe( + map((accessStatusRD) => { + if (accessStatusRD.statusCode !== 401 && hasValue(accessStatusRD.payload)) { + return accessStatusRD.payload; + } else { + return []; + } + }), + map((accessStatus: AccessStatusObject) => hasValue(accessStatus.status) ? accessStatus.status : 'unknown'), + map((status: string) => `access-status.${status.toLowerCase()}.listelement.badge`), + catchError(() => observableOf('access-status.unknown.listelement.badge')) + ); } } diff --git a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html index 137b59d77b..6d4c704b60 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html +++ b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html @@ -4,7 +4,7 @@
- +

diff --git a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html index b51c45290b..645cace8dc 100644 --- a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html +++ b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html @@ -1,6 +1,6 @@
- +
diff --git a/src/app/shared/search/search.component.ts b/src/app/shared/search/search.component.ts index e660b4bd3c..4d8a7c24df 100644 --- a/src/app/shared/search/search.component.ts +++ b/src/app/shared/search/search.component.ts @@ -31,6 +31,7 @@ import { ViewMode } from '../../core/shared/view-mode.model'; import { SelectionConfig } from './search-results/search-results.component'; import { ListableObject } from '../object-collection/shared/listable-object.model'; import { CollectionElementLinkType } from '../object-collection/collection-element-link.type'; +import { environment } from 'src/environments/environment'; @Component({ selector: 'ds-search', @@ -355,7 +356,8 @@ export class SearchComponent implements OnInit { undefined, this.useCachedVersionIfAvailable, true, - followLink('thumbnail', { isOptional: true }) + followLink('thumbnail', { isOptional: true }), + followLink('accessStatus', { isOptional: true, shouldEmbed: environment.item.showAccessStatuses }) ).pipe(getFirstCompletedRemoteData()) .subscribe((results: RemoteData>) => { if (results.hasSucceeded && results.payload?.page?.length > 0) { From 7f1d1ed7de2d6529b2de2c605eb4125d06a6bf11 Mon Sep 17 00:00:00 2001 From: nibou230 Date: Thu, 5 May 2022 11:46:00 -0400 Subject: [PATCH 9/9] Fixes for lint and tests --- ...arch-result-grid-element.component.spec.ts | 9 +++ .../data/access-status-data.service.spec.ts | 81 +++++++++++++++++++ src/app/core/data/item-data.service.spec.ts | 27 ------- .../access-status-badge.component.spec.ts | 33 ++++---- .../access-status-badge.component.ts | 2 +- 5 files changed, 109 insertions(+), 43 deletions(-) create mode 100644 src/app/core/data/access-status-data.service.spec.ts diff --git a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.spec.ts b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.spec.ts index dedada5f5f..334d69f19a 100644 --- a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.spec.ts +++ b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.spec.ts @@ -18,6 +18,8 @@ import { ItemAdminSearchResultGridElementComponent } from './item-admin-search-r import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils'; import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock'; import { ThemeService } from '../../../../../shared/theme-support/theme.service'; +import { AccessStatusDataService } from 'src/app/core/data/access-status-data.service'; +import { AccessStatusObject } from 'src/app/shared/object-list/access-status-badge/access-status.model'; describe('ItemAdminSearchResultGridElementComponent', () => { let component: ItemAdminSearchResultGridElementComponent; @@ -31,6 +33,12 @@ describe('ItemAdminSearchResultGridElementComponent', () => { } }; + const mockAccessStatusDataService = { + findAccessStatusFor(item: Item): Observable> { + return createSuccessfulRemoteDataObject$(new AccessStatusObject()); + } + }; + const mockThemeService = getMockThemeService(); function init() { @@ -55,6 +63,7 @@ describe('ItemAdminSearchResultGridElementComponent', () => { { provide: TruncatableService, useValue: mockTruncatableService }, { provide: BitstreamDataService, useValue: mockBitstreamDataService }, { provide: ThemeService, useValue: mockThemeService }, + { provide: AccessStatusDataService, useValue: mockAccessStatusDataService }, ], schemas: [NO_ERRORS_SCHEMA] }) diff --git a/src/app/core/data/access-status-data.service.spec.ts b/src/app/core/data/access-status-data.service.spec.ts new file mode 100644 index 0000000000..d81b9384f3 --- /dev/null +++ b/src/app/core/data/access-status-data.service.spec.ts @@ -0,0 +1,81 @@ +import { RequestService } from './request.service'; +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; +import { fakeAsync, tick } from '@angular/core/testing'; +import { GetRequest } from './request.models'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { Observable } from 'rxjs'; +import { RemoteData } from './remote-data'; +import { hasNoValue } from '../../shared/empty.util'; +import { AccessStatusDataService } from './access-status-data.service'; +import { Item } from '../shared/item.model'; + +const url = 'fake-url'; + +describe('AccessStatusDataService', () => { + let service: AccessStatusDataService; + let requestService: RequestService; + let notificationsService: any; + let rdbService: RemoteDataBuildService; + let objectCache: ObjectCacheService; + let halService: any; + + const itemId = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a'; + const mockItem: Item = Object.assign(new Item(), { + id: itemId, + name: 'test-item', + _links: { + accessStatus: { + href: `https://rest.api/items/${itemId}/accessStatus` + }, + self: { + href: `https://rest.api/items/${itemId}` + } + } + }); + + describe('when the requests are successful', () => { + beforeEach(() => { + createService(); + }); + + describe('when calling findAccessStatusFor', () => { + let contentSource$; + + beforeEach(() => { + contentSource$ = service.findAccessStatusFor(mockItem); + }); + + it('should send a new GetRequest', fakeAsync(() => { + contentSource$.subscribe(); + tick(); + expect(requestService.send).toHaveBeenCalledWith(jasmine.any(GetRequest), true); + })); + }); + }); + + /** + * Create an AccessStatusDataService used for testing + * @param reponse$ Supply a RemoteData to be returned by the REST API (optional) + */ + function createService(reponse$?: Observable>) { + requestService = getMockRequestService(); + let buildResponse$ = reponse$; + if (hasNoValue(reponse$)) { + buildResponse$ = createSuccessfulRemoteDataObject$({}); + } + rdbService = jasmine.createSpyObj('rdbService', { + buildFromRequestUUID: buildResponse$, + buildSingle: buildResponse$ + }); + objectCache = jasmine.createSpyObj('objectCache', { + remove: jasmine.createSpy('remove') + }); + halService = new HALEndpointServiceStub(url); + notificationsService = new NotificationsServiceStub(); + service = new AccessStatusDataService(null, halService, null, notificationsService, objectCache, rdbService, requestService, null); + } +}); diff --git a/src/app/core/data/item-data.service.spec.ts b/src/app/core/data/item-data.service.spec.ts index e85ddb2f38..a4ed9f882f 100644 --- a/src/app/core/data/item-data.service.spec.ts +++ b/src/app/core/data/item-data.service.spec.ts @@ -184,33 +184,6 @@ describe('ItemDataService', () => { }); }); - describe('getAccessStatusEndpoint', () => { - beforeEach(() => { - service = initTestService(); - }); - it('should retrieve the access status endpoint', () => { - const itemId = '3de6ea60-ec39-419b-ae6f-065930ac1429'; - const result = service.getAccessStatusEndpoint(itemId); - result.subscribe(((value) => { - expect(value).toEqual(`${itemEndpoint}/items/${itemId}/accessStatus`); - })); - }); - }); - - describe('getAccessStatus', () => { - beforeEach(() => { - service = initTestService(); - }); - it('should send a GET request', (done) => { - const itemId = '3de6ea60-ec39-419b-ae6f-065930ac1429'; - const result = service.getAccessStatus(itemId); - result.subscribe(() => { - expect(requestService.send).toHaveBeenCalledWith(jasmine.any(GetRequest)); - done(); - }); - }); - }); - describe('when cache is invalidated', () => { beforeEach(() => { service = initTestService(); diff --git a/src/app/shared/object-list/access-status-badge/access-status-badge.component.spec.ts b/src/app/shared/object-list/access-status-badge/access-status-badge.component.spec.ts index da0677a5de..9101df2f4c 100644 --- a/src/app/shared/object-list/access-status-badge/access-status-badge.component.spec.ts +++ b/src/app/shared/object-list/access-status-badge/access-status-badge.component.spec.ts @@ -6,8 +6,9 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { AccessStatusBadgeComponent } from './access-status-badge.component'; import { createSuccessfulRemoteDataObject$ } from '../../remote-data.utils'; import { By } from '@angular/platform-browser'; -import { ItemDataService } from 'src/app/core/data/item-data.service'; import { AccessStatusObject } from './access-status.model'; +import { AccessStatusDataService } from 'src/app/core/data/access-status-data.service'; +import { environment } from 'src/environments/environment'; describe('ItemAccessStatusBadgeComponent', () => { let component: AccessStatusBadgeComponent; @@ -19,7 +20,7 @@ describe('ItemAccessStatusBadgeComponent', () => { let embargoStatus: AccessStatusObject; let restrictedStatus: AccessStatusObject; - let itemDataService: ItemDataService; + let accessStatusDataService: AccessStatusDataService; let item: Item; @@ -44,8 +45,8 @@ describe('ItemAccessStatusBadgeComponent', () => { status: 'restricted' }); - itemDataService = jasmine.createSpyObj('itemDataService', { - getAccessStatus: createSuccessfulRemoteDataObject$(unknownStatus) + accessStatusDataService = jasmine.createSpyObj('accessStatusDataService', { + findAccessStatusFor: createSuccessfulRemoteDataObject$(unknownStatus) }); item = Object.assign(new Item(), { @@ -59,16 +60,18 @@ describe('ItemAccessStatusBadgeComponent', () => { declarations: [AccessStatusBadgeComponent, TruncatePipe], schemas: [NO_ERRORS_SCHEMA], providers: [ - {provide: ItemDataService, useValue: itemDataService} + {provide: AccessStatusDataService, useValue: accessStatusDataService} ] }).compileComponents(); } function initFixtureAndComponent() { + environment.item.showAccessStatuses = true; fixture = TestBed.createComponent(AccessStatusBadgeComponent); component = fixture.componentInstance; - component.uuid = item.uuid; + component.item = item; fixture.detectChanges(); + environment.item.showAccessStatuses = false; } function lookForAccessStatusBadge(status: string) { @@ -89,7 +92,7 @@ describe('ItemAccessStatusBadgeComponent', () => { }); }); - describe('When the getAccessStatus method returns unknown', () => { + describe('When the findAccessStatusFor method returns unknown', () => { beforeEach(waitForAsync(() => { init(); initTestBed(); @@ -102,10 +105,10 @@ describe('ItemAccessStatusBadgeComponent', () => { }); }); - describe('When the getAccessStatus method returns metadata.only', () => { + describe('When the findAccessStatusFor method returns metadata.only', () => { beforeEach(waitForAsync(() => { init(); - (itemDataService.getAccessStatus as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(metadataOnlyStatus)); + (accessStatusDataService.findAccessStatusFor as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(metadataOnlyStatus)); initTestBed(); })); beforeEach(() => { @@ -116,10 +119,10 @@ describe('ItemAccessStatusBadgeComponent', () => { }); }); - describe('When the getAccessStatus method returns open.access', () => { + describe('When the findAccessStatusFor method returns open.access', () => { beforeEach(waitForAsync(() => { init(); - (itemDataService.getAccessStatus as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(openAccessStatus)); + (accessStatusDataService.findAccessStatusFor as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(openAccessStatus)); initTestBed(); })); beforeEach(() => { @@ -130,10 +133,10 @@ describe('ItemAccessStatusBadgeComponent', () => { }); }); - describe('When the getAccessStatus method returns embargo', () => { + describe('When the findAccessStatusFor method returns embargo', () => { beforeEach(waitForAsync(() => { init(); - (itemDataService.getAccessStatus as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(embargoStatus)); + (accessStatusDataService.findAccessStatusFor as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(embargoStatus)); initTestBed(); })); beforeEach(() => { @@ -144,10 +147,10 @@ describe('ItemAccessStatusBadgeComponent', () => { }); }); - describe('When the getAccessStatus method returns restricted', () => { + describe('When the findAccessStatusFor method returns restricted', () => { beforeEach(waitForAsync(() => { init(); - (itemDataService.getAccessStatus as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(restrictedStatus)); + (accessStatusDataService.findAccessStatusFor as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(restrictedStatus)); initTestBed(); })); beforeEach(() => { diff --git a/src/app/shared/object-list/access-status-badge/access-status-badge.component.ts b/src/app/shared/object-list/access-status-badge/access-status-badge.component.ts index 59ac30f393..fbca3cb971 100644 --- a/src/app/shared/object-list/access-status-badge/access-status-badge.component.ts +++ b/src/app/shared/object-list/access-status-badge/access-status-badge.component.ts @@ -40,7 +40,7 @@ export class AccessStatusBadgeComponent { if (this.item.accessStatus == null) { // In case the access status has not been loaded, do it individually. this.item.accessStatus = this.accessStatusDataService.findAccessStatusFor(this.item); - } + } this.accessStatus$ = this.item.accessStatus.pipe( map((accessStatusRD) => { if (accessStatusRD.statusCode !== 401 && hasValue(accessStatusRD.payload)) {