From 58c53edc2d335637f37b1098cde3c2ea422b5539 Mon Sep 17 00:00:00 2001 From: Nicolas Boulay Date: Wed, 22 Jan 2025 08:18:25 -0500 Subject: [PATCH 1/2] Adding the new embargo badge on the file download component --- 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 5fa2e74cbbc..ab1aecd2d05 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 3eb8d9caf7d..9a8fb676f4b 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 ed587c26d2f..dbc2bd9bf18 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.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.findAccessStatusFor(mockItem); + 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 6d8acb1c8b4..215ef6b4a60 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 78629f9d95a..0057c0823d7 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 73e5e04b364..889f9fd6587 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 aea5c4562cb..1f235c700a8 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 @@ -6,6 +6,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 d14ab8ac0d1..2a4dfe2a369 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 3080a94bd3e..cc27f35d0a2 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 @@ -31,13 +31,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, NgIf, NgTemplateOutlet, AsyncPipe, TranslateModule], + imports: [RouterLink, NgClass, NgIf, 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 8d3a9a6a37e..57dc1a33853 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 0b86b782f6f..db7b143380a 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 @@ -74,7 +74,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 f1b8001b210..62d59f45ff3 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 00000000000..2adcae12044 --- /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 00000000000..8b137891791 --- /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 00000000000..89fdcaeebba --- /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 00000000000..166aa47e4d1 --- /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 00000000000..79ecf55163f --- /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 f81089e57c9..ca99947820e 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 7da72f0fb3a..a7fbcfacf56 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -6787,4 +6787,6 @@ "live-region.ordering.dropped": "{{ itemName }}, dropped at position {{ index }} of {{ length }}.", "dynamic-form-array.sortable-list.label": "Sortable list", + + "embargo.listelement.badge": "embargo until {{ date }}", } diff --git a/src/assets/i18n/fr.json5 b/src/assets/i18n/fr.json5 index bee30294a96..1630c957591 100644 --- a/src/assets/i18n/fr.json5 +++ b/src/assets/i18n/fr.json5 @@ -8403,5 +8403,8 @@ //"browse.search-form.placeholder": "Search the repository", "browse.search-form.placeholder": "Chercher dans le dépôt", + + // "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 3c5e0ef0dac..6fa24b89703 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 35cb5260aea..1b629bf8726 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 d7e9067ea36..dc20a86bb7d 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 491d20e79c4..9c526ce31f0 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 @@ -9,6 +9,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', @@ -17,7 +18,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, NgIf, NgTemplateOutlet, AsyncPipe, TranslateModule], + imports: [RouterLink, NgClass, NgIf, NgTemplateOutlet, AsyncPipe, TranslateModule, ThemedEmbargoBadgeComponent], }) export class FileDownloadLinkComponent extends BaseComponent { } From 21de3ca39d810c87ad72f18eb27eedf12620f799 Mon Sep 17 00:00:00 2001 From: Nicolas Boulay Date: Wed, 22 Jan 2025 09:11:58 -0500 Subject: [PATCH 2/2] Fixed lint no-trailing-spaces --- src/app/core/data/access-status-data.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/core/data/access-status-data.service.ts b/src/app/core/data/access-status-data.service.ts index 215ef6b4a60..8ab206583da 100644 --- a/src/app/core/data/access-status-data.service.ts +++ b/src/app/core/data/access-status-data.service.ts @@ -33,7 +33,7 @@ export class AccessStatusDataService extends BaseDataService 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