From 5f83139fcd611a7709c6784188ffd2ecf5261f84 Mon Sep 17 00:00:00 2001 From: Nicolas Boulay Date: Wed, 22 Jan 2025 08:18:25 -0500 Subject: [PATCH] Adding the new embargo badge on the file download component Fixed lint no-trailing-spaces --- config/config.example.yml | 2 + ...arch-result-grid-element.component.spec.ts | 2 +- .../data/access-status-data.service.spec.ts | 33 +++- .../core/data/access-status-data.service.ts | 11 +- src/app/core/provide-core.ts | 1 - src/app/core/shared/bitstream.model.ts | 10 + .../file-download-link.component.html | 1 + .../file-download-link.component.spec.ts | 11 ++ .../file-download-link.component.ts | 3 +- .../access-status-badge.component.spec.ts | 20 +- .../access-status-badge.component.ts | 2 +- .../access-status.model.ts | 6 + .../embargo-badge.component.html | 5 + .../embargo-badge.component.scss | 1 + .../embargo-badge.component.spec.ts | 177 ++++++++++++++++++ .../embargo-badge/embargo-badge.component.ts | 70 +++++++ .../themed-embargo-badge.component.ts | 36 ++++ src/app/thumbnail/thumbnail.component.spec.ts | 2 + src/assets/i18n/en.json5 | 2 + src/assets/i18n/fr.json5 | 3 + src/config/default-app-config.ts | 2 + src/config/item-config.interface.ts | 2 + src/environments/environment.test.ts | 2 + .../file-download-link.component.ts | 3 +- 24 files changed, 389 insertions(+), 18 deletions(-) create mode 100644 src/app/shared/object-collection/shared/badges/embargo-badge/embargo-badge.component.html create mode 100644 src/app/shared/object-collection/shared/badges/embargo-badge/embargo-badge.component.scss create mode 100644 src/app/shared/object-collection/shared/badges/embargo-badge/embargo-badge.component.spec.ts create mode 100644 src/app/shared/object-collection/shared/badges/embargo-badge/embargo-badge.component.ts create mode 100644 src/app/shared/object-collection/shared/badges/embargo-badge/themed-embargo-badge.component.ts diff --git a/config/config.example.yml b/config/config.example.yml index 5fa2e74cbb..ab1aecd2d0 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -350,6 +350,8 @@ item: # Rounded to the nearest size in the list of selectable sizes on the # settings menu. See pageSizeOptions in 'pagination-component-options.model.ts'. pageSize: 5 + # Show the bitstream access status label + showAccessStatuses: false # Community Page Config community: 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 3eb8d9caf7..9a8fb676f4 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 @@ -45,7 +45,7 @@ describe('ItemAdminSearchResultGridElementComponent', () => { }; const mockAccessStatusDataService = { - findAccessStatusFor(item: Item): Observable> { + findItemAccessStatusFor(item: Item): Observable> { return createSuccessfulRemoteDataObject$(new AccessStatusObject()); }, }; diff --git a/src/app/core/data/access-status-data.service.spec.ts b/src/app/core/data/access-status-data.service.spec.ts index ed587c26d2..dbc2bd9bf1 100644 --- a/src/app/core/data/access-status-data.service.spec.ts +++ b/src/app/core/data/access-status-data.service.spec.ts @@ -11,6 +11,7 @@ import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-servic import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; +import { Bitstream } from '../shared/bitstream.model'; import { Item } from '../shared/item.model'; import { AccessStatusDataService } from './access-status-data.service'; import { RemoteData } from './remote-data'; @@ -41,16 +42,44 @@ describe('AccessStatusDataService', () => { }, }); + const bitstreamId = '3d4c730u-5a4b-438b-9686-be1d5b4a1c5a'; + const mockBitstream: Bitstream = Object.assign(new Bitstream(), { + id: bitstreamId, + name: 'test-bitstream', + _links: { + accessStatus: { + href: `https://rest.api/core/bitstreams/${bitstreamId}/accessStatus`, + }, + self: { + href: `https://rest.api/core/bitstreams/${bitstreamId}`, + }, + }, + }); + describe('when the requests are successful', () => { beforeEach(() => { createService(); }); - describe('when calling findAccessStatusFor', () => { + describe('when calling findItemAccessStatusFor', () => { let contentSource$; beforeEach(() => { - contentSource$ = service.findAccessStatusFor(mockItem); + contentSource$ = service.findItemAccessStatusFor(mockItem); + }); + + it('should send a new GetRequest', fakeAsync(() => { + contentSource$.subscribe(); + tick(); + expect(requestService.send).toHaveBeenCalledWith(jasmine.any(GetRequest), true); + })); + }); + + describe('when calling findBitstreamAccessStatusFor', () => { + let contentSource$; + + beforeEach(() => { + contentSource$ = service.findBitstreamAccessStatusFor(mockBitstream); }); it('should send a new GetRequest', fakeAsync(() => { diff --git a/src/app/core/data/access-status-data.service.ts b/src/app/core/data/access-status-data.service.ts index 6d8acb1c8b..8ab206583d 100644 --- a/src/app/core/data/access-status-data.service.ts +++ b/src/app/core/data/access-status-data.service.ts @@ -4,6 +4,7 @@ import { AccessStatusObject } from 'src/app/shared/object-collection/shared/badg import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; +import { Bitstream } from '../shared/bitstream.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Item } from '../shared/item.model'; import { BaseDataService } from './base/base-data.service'; @@ -29,7 +30,15 @@ export class AccessStatusDataService extends BaseDataService * 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> { + findItemAccessStatusFor(item: Item): Observable> { return this.findByHref(item._links.accessStatus.href); } + + /** + * Returns {@link RemoteData} of {@link AccessStatusObject} that is the access status of the given bitstream + * @param bitstream Bitstream we want the access status of + */ + findBitstreamAccessStatusFor(bitstream: Bitstream): Observable> { + return this.findByHref(bitstream._links.accessStatus.href); + } } diff --git a/src/app/core/provide-core.ts b/src/app/core/provide-core.ts index 78629f9d95..0057c0823d 100644 --- a/src/app/core/provide-core.ts +++ b/src/app/core/provide-core.ts @@ -176,7 +176,6 @@ export const models = ResearcherProfile, OrcidQueue, OrcidHistory, - AccessStatusObject, IdentifierData, Subscription, ItemRequest, diff --git a/src/app/core/shared/bitstream.model.ts b/src/app/core/shared/bitstream.model.ts index 73e5e04b36..889f9fd658 100644 --- a/src/app/core/shared/bitstream.model.ts +++ b/src/app/core/shared/bitstream.model.ts @@ -4,6 +4,8 @@ import { inheritSerialization, } from 'cerialize'; import { Observable } from 'rxjs'; +import { AccessStatusObject } from 'src/app/shared/object-collection/shared/badges/access-status-badge/access-status.model'; +import { ACCESS_STATUS } from 'src/app/shared/object-collection/shared/badges/access-status-badge/access-status.resource-type'; import { link, @@ -52,6 +54,7 @@ export class Bitstream extends DSpaceObject implements ChildHALResource { format: HALLink; content: HALLink; thumbnail: HALLink; + accessStatus: HALLink; }; /** @@ -75,6 +78,13 @@ export class Bitstream extends DSpaceObject implements ChildHALResource { @link(BUNDLE) bundle?: Observable>; + /** + * The access status for this Bitstream + * Will be undefined unless the access status {@link HALLink} has been resolved. + */ + @link(ACCESS_STATUS) + accessStatus?: Observable>; + getParentLinkKey(): keyof this['_links'] { return 'format'; } diff --git a/src/app/shared/file-download-link/file-download-link.component.html b/src/app/shared/file-download-link/file-download-link.component.html index e305d2a82b..52ac8d3630 100644 --- a/src/app/shared/file-download-link/file-download-link.component.html +++ b/src/app/shared/file-download-link/file-download-link.component.html @@ -8,6 +8,7 @@ } + diff --git a/src/app/shared/file-download-link/file-download-link.component.spec.ts b/src/app/shared/file-download-link/file-download-link.component.spec.ts index d14ab8ac0d..2a4dfe2a36 100644 --- a/src/app/shared/file-download-link/file-download-link.component.spec.ts +++ b/src/app/shared/file-download-link/file-download-link.component.spec.ts @@ -8,11 +8,14 @@ import { ActivatedRoute, RouterLink, } from '@angular/router'; +import { Store } from '@ngrx/store'; import { TranslateModule } from '@ngx-translate/core'; import { cold, getTestScheduler, } from 'jasmine-marbles'; +import { of as observableOf } from 'rxjs'; +import { APP_DATA_SERVICES_MAP } from 'src/config/app-config.interface'; import { getBitstreamModuleRoute } from '../../app-routing-paths'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; @@ -34,6 +37,7 @@ describe('FileDownloadLinkComponent', () => { let bitstream: Bitstream; let item: Item; + let storeMock: any; function init() { authorizationService = jasmine.createSpyObj('authorizationService', { @@ -51,6 +55,11 @@ describe('FileDownloadLinkComponent', () => { self: { href: 'obj-selflink' }, }, }); + storeMock = jasmine.createSpyObj('store', { + dispatch: jasmine.createSpy('dispatch'), + select: jasmine.createSpy('select'), + pipe: observableOf(true), + }); } function initTestbed() { @@ -63,6 +72,8 @@ describe('FileDownloadLinkComponent', () => { RouterLinkDirectiveStub, { provide: AuthorizationDataService, useValue: authorizationService }, { provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, + { provide: Store, useValue: storeMock }, + { provide: APP_DATA_SERVICES_MAP, useValue: {} }, ], }) .overrideComponent(FileDownloadLinkComponent, { diff --git a/src/app/shared/file-download-link/file-download-link.component.ts b/src/app/shared/file-download-link/file-download-link.component.ts index a8475ee841..4ec2d600e3 100644 --- a/src/app/shared/file-download-link/file-download-link.component.ts +++ b/src/app/shared/file-download-link/file-download-link.component.ts @@ -30,13 +30,14 @@ import { hasValue, isNotEmpty, } from '../empty.util'; +import { ThemedEmbargoBadgeComponent } from '../object-collection/shared/badges/embargo-badge/themed-embargo-badge.component'; @Component({ selector: 'ds-base-file-download-link', templateUrl: './file-download-link.component.html', styleUrls: ['./file-download-link.component.scss'], standalone: true, - imports: [RouterLink, NgClass, NgTemplateOutlet, AsyncPipe, TranslateModule], + imports: [RouterLink, NgClass, NgTemplateOutlet, AsyncPipe, TranslateModule, ThemedEmbargoBadgeComponent], }) /** * Component displaying a download link diff --git a/src/app/shared/object-collection/shared/badges/access-status-badge/access-status-badge.component.spec.ts b/src/app/shared/object-collection/shared/badges/access-status-badge/access-status-badge.component.spec.ts index 8d3a9a6a37..57dc1a3385 100644 --- a/src/app/shared/object-collection/shared/badges/access-status-badge/access-status-badge.component.spec.ts +++ b/src/app/shared/object-collection/shared/badges/access-status-badge/access-status-badge.component.spec.ts @@ -51,7 +51,7 @@ describe('ItemAccessStatusBadgeComponent', () => { }); accessStatusDataService = jasmine.createSpyObj('accessStatusDataService', { - findAccessStatusFor: createSuccessfulRemoteDataObject$(unknownStatus), + findItemAccessStatusFor: createSuccessfulRemoteDataObject$(unknownStatus), }); item = Object.assign(new Item(), { @@ -97,7 +97,7 @@ describe('ItemAccessStatusBadgeComponent', () => { }); }); - describe('When the findAccessStatusFor method returns unknown', () => { + describe('When the findItemAccessStatusFor method returns unknown', () => { beforeEach(waitForAsync(() => { init(); initTestBed(); @@ -110,10 +110,10 @@ describe('ItemAccessStatusBadgeComponent', () => { }); }); - describe('When the findAccessStatusFor method returns metadata.only', () => { + describe('When the findItemAccessStatusFor method returns metadata.only', () => { beforeEach(waitForAsync(() => { init(); - (accessStatusDataService.findAccessStatusFor as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(metadataOnlyStatus)); + (accessStatusDataService.findItemAccessStatusFor as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(metadataOnlyStatus)); initTestBed(); })); beforeEach(() => { @@ -124,10 +124,10 @@ describe('ItemAccessStatusBadgeComponent', () => { }); }); - describe('When the findAccessStatusFor method returns open.access', () => { + describe('When the findItemAccessStatusFor method returns open.access', () => { beforeEach(waitForAsync(() => { init(); - (accessStatusDataService.findAccessStatusFor as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(openAccessStatus)); + (accessStatusDataService.findItemAccessStatusFor as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(openAccessStatus)); initTestBed(); })); beforeEach(() => { @@ -138,10 +138,10 @@ describe('ItemAccessStatusBadgeComponent', () => { }); }); - describe('When the findAccessStatusFor method returns embargo', () => { + describe('When the findItemAccessStatusFor method returns embargo', () => { beforeEach(waitForAsync(() => { init(); - (accessStatusDataService.findAccessStatusFor as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(embargoStatus)); + (accessStatusDataService.findItemAccessStatusFor as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(embargoStatus)); initTestBed(); })); beforeEach(() => { @@ -152,10 +152,10 @@ describe('ItemAccessStatusBadgeComponent', () => { }); }); - describe('When the findAccessStatusFor method returns restricted', () => { + describe('When the findItemAccessStatusFor method returns restricted', () => { beforeEach(waitForAsync(() => { init(); - (accessStatusDataService.findAccessStatusFor as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(restrictedStatus)); + (accessStatusDataService.findItemAccessStatusFor as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(restrictedStatus)); initTestBed(); })); beforeEach(() => { diff --git a/src/app/shared/object-collection/shared/badges/access-status-badge/access-status-badge.component.ts b/src/app/shared/object-collection/shared/badges/access-status-badge/access-status-badge.component.ts index 5fafb5db77..67c16b132a 100644 --- a/src/app/shared/object-collection/shared/badges/access-status-badge/access-status-badge.component.ts +++ b/src/app/shared/object-collection/shared/badges/access-status-badge/access-status-badge.component.ts @@ -71,7 +71,7 @@ export class AccessStatusBadgeComponent implements OnDestroy, OnInit { const item = this.object as Item; if (item.accessStatus == null) { // In case the access status has not been loaded, do it individually. - item.accessStatus = this.accessStatusDataService.findAccessStatusFor(item); + item.accessStatus = this.accessStatusDataService.findItemAccessStatusFor(item); } this.accessStatus$ = item.accessStatus.pipe( map((accessStatusRD) => { diff --git a/src/app/shared/object-collection/shared/badges/access-status-badge/access-status.model.ts b/src/app/shared/object-collection/shared/badges/access-status-badge/access-status.model.ts index f1b8001b21..62d59f45ff 100644 --- a/src/app/shared/object-collection/shared/badges/access-status-badge/access-status.model.ts +++ b/src/app/shared/object-collection/shared/badges/access-status-badge/access-status.model.ts @@ -27,6 +27,12 @@ export class AccessStatusObject implements CacheableObject { @autoserialize status: string; + /** + * The embargo date value + */ + @autoserialize + embargoDate: string; + /** * The {@link HALLink}s for this AccessStatusObject */ diff --git a/src/app/shared/object-collection/shared/badges/embargo-badge/embargo-badge.component.html b/src/app/shared/object-collection/shared/badges/embargo-badge/embargo-badge.component.html new file mode 100644 index 0000000000..2adcae1204 --- /dev/null +++ b/src/app/shared/object-collection/shared/badges/embargo-badge/embargo-badge.component.html @@ -0,0 +1,5 @@ + +
+ {{ 'embargo.listelement.badge' | translate: {date: embargoDate} }} +
+
diff --git a/src/app/shared/object-collection/shared/badges/embargo-badge/embargo-badge.component.scss b/src/app/shared/object-collection/shared/badges/embargo-badge/embargo-badge.component.scss new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/src/app/shared/object-collection/shared/badges/embargo-badge/embargo-badge.component.scss @@ -0,0 +1 @@ + diff --git a/src/app/shared/object-collection/shared/badges/embargo-badge/embargo-badge.component.spec.ts b/src/app/shared/object-collection/shared/badges/embargo-badge/embargo-badge.component.spec.ts new file mode 100644 index 0000000000..89fdcaeebb --- /dev/null +++ b/src/app/shared/object-collection/shared/badges/embargo-badge/embargo-badge.component.spec.ts @@ -0,0 +1,177 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { TranslateModule } from '@ngx-translate/core'; +import { AccessStatusDataService } from 'src/app/core/data/access-status-data.service'; +import { Bitstream } from 'src/app/core/shared/bitstream.model'; +import { createSuccessfulRemoteDataObject$ } from 'src/app/shared/remote-data.utils'; +import { environment } from 'src/environments/environment'; + +import { TruncatePipe } from '../../../../utils/truncate.pipe'; +import { AccessStatusObject } from '../access-status-badge/access-status.model'; +import { EmbargoBadgeComponent } from './embargo-badge.component'; + +describe('ItemEmbargoBadgeComponent', () => { + let component: EmbargoBadgeComponent; + let fixture: ComponentFixture; + + let unknownStatus: AccessStatusObject; + let metadataOnlyStatus: AccessStatusObject; + let openAccessStatus: AccessStatusObject; + let embargoStatus: AccessStatusObject; + let restrictedStatus: AccessStatusObject; + + let accessStatusDataService: AccessStatusDataService; + + let bitstream: Bitstream; + + function init() { + unknownStatus = Object.assign(new AccessStatusObject(), { + status: 'unknown', + embargoDate: null, + }); + + metadataOnlyStatus = Object.assign(new AccessStatusObject(), { + status: 'metadata.only', + embargoDate: null, + }); + + openAccessStatus = Object.assign(new AccessStatusObject(), { + status: 'open.access', + embargoDate: null, + }); + + embargoStatus = Object.assign(new AccessStatusObject(), { + status: 'embargo', + embargoDate: '2050-01-01', + }); + + restrictedStatus = Object.assign(new AccessStatusObject(), { + status: 'restricted', + embargoDate: null, + }); + + accessStatusDataService = jasmine.createSpyObj('accessStatusDataService', { + findBitstreamAccessStatusFor: createSuccessfulRemoteDataObject$(unknownStatus), + }); + + bitstream = Object.assign(new Bitstream(), { + uuid: 'bitstream-uuid', + }); + } + + function initTestBed() { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), EmbargoBadgeComponent, TruncatePipe], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + { provide: AccessStatusDataService, useValue: accessStatusDataService }, + ], + }).compileComponents(); + } + + function initFixtureAndComponent() { + environment.item.bitstream.showAccessStatuses = true; + fixture = TestBed.createComponent(EmbargoBadgeComponent); + component = fixture.componentInstance; + component.bitstream = bitstream; + fixture.detectChanges(); + environment.item.bitstream.showAccessStatuses = false; + } + + function lookForNoEmbargoBadge() { + const badge = fixture.debugElement.query(By.css('span.badge')); + expect(badge).toBeNull(); + } + + function lookForEmbargoBadge() { + const badge = fixture.debugElement.query(By.css('span.badge')); + expect(badge).toBeDefined(); + } + + describe('init', () => { + beforeEach(waitForAsync(() => { + init(); + initTestBed(); + })); + beforeEach(() => { + initFixtureAndComponent(); + }); + it('should init the component', () => { + expect(component).toBeTruthy(); + }); + }); + + describe('When the findBitstreamAccessStatusFor method returns unknown', () => { + beforeEach(waitForAsync(() => { + init(); + initTestBed(); + })); + beforeEach(() => { + initFixtureAndComponent(); + }); + it('should not show the embargo badge', () => { + lookForNoEmbargoBadge(); + }); + }); + + describe('When the findBitstreamAccessStatusFor method returns metadata.only', () => { + beforeEach(waitForAsync(() => { + init(); + (accessStatusDataService.findBitstreamAccessStatusFor as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(metadataOnlyStatus)); + initTestBed(); + })); + beforeEach(() => { + initFixtureAndComponent(); + }); + it('should not show the embargo badge', () => { + lookForNoEmbargoBadge(); + }); + }); + + describe('When the findBitstreamAccessStatusFor method returns open.access', () => { + beforeEach(waitForAsync(() => { + init(); + (accessStatusDataService.findBitstreamAccessStatusFor as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(openAccessStatus)); + initTestBed(); + })); + beforeEach(() => { + initFixtureAndComponent(); + }); + it('should not show the embargo badge', () => { + lookForNoEmbargoBadge(); + }); + }); + + describe('When the findBitstreamAccessStatusFor method returns embargo', () => { + beforeEach(waitForAsync(() => { + init(); + (accessStatusDataService.findBitstreamAccessStatusFor as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(embargoStatus)); + initTestBed(); + })); + beforeEach(() => { + initFixtureAndComponent(); + }); + it('should show the embargo badge', () => { + lookForEmbargoBadge(); + }); + }); + + describe('When the findBitstreamAccessStatusFor method returns restricted', () => { + beforeEach(waitForAsync(() => { + init(); + (accessStatusDataService.findBitstreamAccessStatusFor as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(restrictedStatus)); + initTestBed(); + })); + beforeEach(() => { + initFixtureAndComponent(); + }); + it('should not show the embargo badge', () => { + lookForNoEmbargoBadge(); + }); + }); +}); diff --git a/src/app/shared/object-collection/shared/badges/embargo-badge/embargo-badge.component.ts b/src/app/shared/object-collection/shared/badges/embargo-badge/embargo-badge.component.ts new file mode 100644 index 0000000000..166aa47e4d --- /dev/null +++ b/src/app/shared/object-collection/shared/badges/embargo-badge/embargo-badge.component.ts @@ -0,0 +1,70 @@ +import { + AsyncPipe, + NgIf, +} from '@angular/common'; +import { + Component, + Input, + OnInit, +} from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { + catchError, + map, + Observable, + of as observableOf, +} from 'rxjs'; +import { AccessStatusDataService } from 'src/app/core/data/access-status-data.service'; +import { Bitstream } from 'src/app/core/shared/bitstream.model'; +import { hasValue } from 'src/app/shared/empty.util'; +import { environment } from 'src/environments/environment'; + +import { AccessStatusObject } from '../access-status-badge/access-status.model'; + +@Component({ + selector: 'ds-base-embargo-badge', + templateUrl: './embargo-badge.component.html', + styleUrls: ['./embargo-badge.component.scss'], + standalone: true, + imports: [NgIf, AsyncPipe, TranslateModule], +}) +/** + * Component rendering the embargo date of a bitstream as a badge + */ +export class EmbargoBadgeComponent implements OnInit { + + @Input() bitstream: Bitstream; + + embargoDate$: Observable; + + /** + * Whether to show the badge or not + */ + showAccessStatus: boolean; + + /** + * Initialize instance variables + * + * @param {AccessStatusDataService} accessStatusDataService + */ + constructor(private accessStatusDataService: AccessStatusDataService) { } + + ngOnInit(): void { + this.showAccessStatus = environment.item.bitstream.showAccessStatuses; + if (!this.showAccessStatus || this.bitstream == null) { + // Do not show the badge if the feature is inactive or if the bitstream is null. + return; + } + this.embargoDate$ = this.accessStatusDataService.findBitstreamAccessStatusFor(this.bitstream).pipe( + map((accessStatusRD) => { + if (accessStatusRD.statusCode !== 401 && hasValue(accessStatusRD.payload)) { + return accessStatusRD.payload; + } else { + return null; + } + }), + map((accessStatus: AccessStatusObject) => hasValue(accessStatus) ? accessStatus.embargoDate : null), + catchError(() => observableOf(null)), + ); + } +} diff --git a/src/app/shared/object-collection/shared/badges/embargo-badge/themed-embargo-badge.component.ts b/src/app/shared/object-collection/shared/badges/embargo-badge/themed-embargo-badge.component.ts new file mode 100644 index 0000000000..79ecf55163 --- /dev/null +++ b/src/app/shared/object-collection/shared/badges/embargo-badge/themed-embargo-badge.component.ts @@ -0,0 +1,36 @@ +import { + Component, + Input, +} from '@angular/core'; +import { Bitstream } from 'src/app/core/shared/bitstream.model'; + +import { ThemedComponent } from '../../../../theme-support/themed.component'; +import { EmbargoBadgeComponent } from './embargo-badge.component'; + +/** + * Themed wrapper for EmbargoBadgeComponent + */ +@Component({ + selector: 'ds-embargo-badge', + styleUrls: [], + templateUrl: '../../../../theme-support/themed.component.html', + standalone: true, + imports: [EmbargoBadgeComponent], +}) +export class ThemedEmbargoBadgeComponent extends ThemedComponent { + @Input() bitstream: Bitstream; + + protected inAndOutputNames: (keyof EmbargoBadgeComponent & keyof this)[] = ['bitstream']; + + protected getComponentName(): string { + return 'EmbargoBadgeComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../../../../themes/${themeName}/app/shared/object-collection/shared/badges/embargo-badge/embargo-badge.component`); + } + + protected importUnthemedComponent(): Promise { + return import(`./embargo-badge.component`); + } +} diff --git a/src/app/thumbnail/thumbnail.component.spec.ts b/src/app/thumbnail/thumbnail.component.spec.ts index f81089e57c..ca99947820 100644 --- a/src/app/thumbnail/thumbnail.component.spec.ts +++ b/src/app/thumbnail/thumbnail.component.spec.ts @@ -288,6 +288,7 @@ describe('ThumbnailComponent', () => { format: { href: 'format.url' }, content: { href: CONTENT }, thumbnail: undefined, + accessStatus: { href: 'accessStatus.url' }, }; comp.thumbnail = thumbnail; }); @@ -324,6 +325,7 @@ describe('ThumbnailComponent', () => { format: { href: 'format.url' }, content: { href: CONTENT }, thumbnail: undefined, + accessStatus: { href: 'accessStatus.url' }, }; }); diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 893785c5a6..b9b554aa6a 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -6869,4 +6869,6 @@ "metadata-export-filtered-items.submit.error": "CSV export failed.", "metadata-export-filtered-items.columns.warning": "CSV export automatically includes all relevant fields, so selections in this list are not taken into account.", + + "embargo.listelement.badge": "embargo until {{ date }}", } diff --git a/src/assets/i18n/fr.json5 b/src/assets/i18n/fr.json5 index 9e8866c47d..53f6526dc1 100644 --- a/src/assets/i18n/fr.json5 +++ b/src/assets/i18n/fr.json5 @@ -8595,4 +8595,7 @@ //"metadata-export-filtered-items.columns.warning": "CSV export automatically includes all relevant fields, so selections in this list are not taken into account.", "metadata-export-filtered-items.columns.warning": "L'exportation CSV inclut automatiquement tous les champs pertinents, sans égard au contenu sélectionné de cette liste.", + + // "embargo.listelement.badge": "embargo until {{ date }}", + "embargo.listelement.badge": "embargo jusqu'à {{ date }}", } diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index 3c5e0ef0da..6fa24b8970 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -329,6 +329,8 @@ export class DefaultAppConfig implements AppConfig { // Rounded to the nearest size in the list of selectable sizes on the // settings menu. See pageSizeOptions in 'pagination-component-options.model.ts'. pageSize: 5, + // Show the bitstream access status label + showAccessStatuses: false, }, }; diff --git a/src/config/item-config.interface.ts b/src/config/item-config.interface.ts index 35cb5260ae..1b629bf872 100644 --- a/src/config/item-config.interface.ts +++ b/src/config/item-config.interface.ts @@ -12,5 +12,7 @@ export interface ItemConfig extends Config { // Rounded to the nearest size in the list of selectable sizes on the // settings menu. See pageSizeOptions in 'pagination-component-options.model.ts'. pageSize: number; + // Show the bitstream access status label + showAccessStatuses: boolean; } } diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts index 40c5e8bc64..4c7a9b13ab 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -269,6 +269,8 @@ export const environment: BuildConfig = { // Rounded to the nearest size in the list of selectable sizes on the // settings menu. See pageSizeOptions in 'pagination-component-options.model.ts'. pageSize: 5, + // Show the bitstream access status label + showAccessStatuses: false, }, }, community: { diff --git a/src/themes/custom/app/shared/file-download-link/file-download-link.component.ts b/src/themes/custom/app/shared/file-download-link/file-download-link.component.ts index fe11b5e6b7..167c0056e8 100644 --- a/src/themes/custom/app/shared/file-download-link/file-download-link.component.ts +++ b/src/themes/custom/app/shared/file-download-link/file-download-link.component.ts @@ -8,6 +8,7 @@ import { RouterLink } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { FileDownloadLinkComponent as BaseComponent } from '../../../../../app/shared/file-download-link/file-download-link.component'; +import { ThemedEmbargoBadgeComponent } from '../../../../../app/shared/object-collection/shared/badges/embargo-badge/themed-embargo-badge.component'; @Component({ selector: 'ds-themed-file-download-link', @@ -16,7 +17,7 @@ import { FileDownloadLinkComponent as BaseComponent } from '../../../../../app/s // styleUrls: ['./file-download-link.component.scss'], styleUrls: ['../../../../../app/shared/file-download-link/file-download-link.component.scss'], standalone: true, - imports: [RouterLink, NgClass, NgTemplateOutlet, AsyncPipe, TranslateModule], + imports: [RouterLink, NgClass, NgTemplateOutlet, AsyncPipe, TranslateModule, ThemedEmbargoBadgeComponent], }) export class FileDownloadLinkComponent extends BaseComponent { }