diff --git a/config/config.example.yml b/config/config.example.yml index 77134d0075..898b47784f 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -164,10 +164,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/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/core.module.ts b/src/app/core/core.module.ts index e9e242dbc0..f1b0dc4f37 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -163,6 +163,8 @@ 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'; +import { AccessStatusDataService } from './data/access-status-data.service'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -220,6 +222,7 @@ const PROVIDERS = [ MyDSpaceResponseParsingService, ServerResponseService, BrowseService, + AccessStatusDataService, SubmissionCcLicenseDataService, SubmissionCcLicenseUrlDataService, SubmissionFormsConfigService, @@ -346,7 +349,8 @@ export const models = UsageReport, Root, SearchConfig, - SubmissionAccessesModel + SubmissionAccessesModel, + AccessStatusObject ]; @NgModule({ 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/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.spec.ts b/src/app/core/data/item-data.service.spec.ts index cc1e3b6e20..a4ed9f882f 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: {} }); 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/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 7fdb505d43..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,6 +18,7 @@
+

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..3877663419 --- /dev/null +++ b/src/app/shared/object-list/access-status-badge/access-status-badge.component.html @@ -0,0 +1,5 @@ + +
+ {{ accessStatus | 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..9101df2f4c --- /dev/null +++ b/src/app/shared/object-list/access-status-badge/access-status-badge.component.spec.ts @@ -0,0 +1,163 @@ +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 { 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; + let fixture: ComponentFixture; + + let unknownStatus: AccessStatusObject; + let metadataOnlyStatus: AccessStatusObject; + let openAccessStatus: AccessStatusObject; + let embargoStatus: AccessStatusObject; + let restrictedStatus: AccessStatusObject; + + let accessStatusDataService: AccessStatusDataService; + + 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' + }); + + accessStatusDataService = jasmine.createSpyObj('accessStatusDataService', { + findAccessStatusFor: 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: AccessStatusDataService, useValue: accessStatusDataService} + ] + }).compileComponents(); + } + + function initFixtureAndComponent() { + environment.item.showAccessStatuses = true; + fixture = TestBed.createComponent(AccessStatusBadgeComponent); + component = fixture.componentInstance; + component.item = item; + fixture.detectChanges(); + environment.item.showAccessStatuses = false; + } + + 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 findAccessStatusFor method returns unknown', () => { + beforeEach(waitForAsync(() => { + init(); + initTestBed(); + })); + beforeEach(() => { + initFixtureAndComponent(); + }); + it('should show the unknown badge', () => { + lookForAccessStatusBadge('unknown'); + }); + }); + + describe('When the findAccessStatusFor method returns metadata.only', () => { + beforeEach(waitForAsync(() => { + init(); + (accessStatusDataService.findAccessStatusFor as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(metadataOnlyStatus)); + initTestBed(); + })); + beforeEach(() => { + initFixtureAndComponent(); + }); + it('should show the metadata only badge', () => { + lookForAccessStatusBadge('metadata.only'); + }); + }); + + describe('When the findAccessStatusFor method returns open.access', () => { + beforeEach(waitForAsync(() => { + init(); + (accessStatusDataService.findAccessStatusFor as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(openAccessStatus)); + initTestBed(); + })); + beforeEach(() => { + initFixtureAndComponent(); + }); + it('should show the open access badge', () => { + lookForAccessStatusBadge('open.access'); + }); + }); + + describe('When the findAccessStatusFor method returns embargo', () => { + beforeEach(waitForAsync(() => { + init(); + (accessStatusDataService.findAccessStatusFor as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$(embargoStatus)); + initTestBed(); + })); + beforeEach(() => { + initFixtureAndComponent(); + }); + it('should show the embargo badge', () => { + lookForAccessStatusBadge('embargo'); + }); + }); + + describe('When the findAccessStatusFor method returns restricted', () => { + beforeEach(waitForAsync(() => { + init(); + (accessStatusDataService.findAccessStatusFor 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..fbca3cb971 --- /dev/null +++ b/src/app/shared/object-list/access-status-badge/access-status-badge.component.ts @@ -0,0 +1,57 @@ +import { Component, Input } from '@angular/core'; +import { catchError, map } from 'rxjs/operators'; +import { Observable, of as observableOf } from 'rxjs'; +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', + templateUrl: './access-status-badge.component.html' +}) +/** + * Component rendering the access status of an item as a badge + */ +export class AccessStatusBadgeComponent { + + @Input() item: Item; + accessStatus$: Observable; + + /** + * Whether to show the access status badge or not + */ + showAccessStatus: boolean; + + /** + * Initialize instance variables + * + * @param {AccessStatusDataService} accessStatusDataService + */ + constructor(private accessStatusDataService: AccessStatusDataService) { } + + ngOnInit(): void { + this.showAccessStatus = environment.item.showAccessStatuses; + 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/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..69b5e920d0 --- /dev/null +++ b/src/app/shared/object-list/access-status-badge/access-status.model.ts @@ -0,0 +1,33 @@ +import { autoserialize, deserialize } from 'cerialize'; +import { typedObject } from 'src/app/core/cache/builders/build-decorators'; +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'; +import { ACCESS_STATUS } from './access-status.resource-type'; + +@typedObject +export class AccessStatusObject implements CacheableObject { + static type = ACCESS_STATUS; + + /** + * The type for this AccessStatusObject + */ + @excludeFromEquals + @autoserialize + type: ResourceType; + + /** + * The access status value + */ + @autoserialize + status: string; + + /** + * The {@link HALLink}s for this AccessStatusObject + */ + @deserialize + _links: { + self: HALLink; + }; +} 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/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..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 @@ -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..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 @@ -35,5 +35,4 @@ export class ItemListPreviewComponent { * A boolean representing if to show submitter information */ @Input() showSubmitter = false; - } 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..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,4 +1,7 @@ - +
+ + +
('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) { diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 37f4fafb1e..12b6a482dc 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -111,6 +111,7 @@ import { FilterInputSuggestionsComponent } from './input-suggestions/filter-sugg import { DsoInputSuggestionsComponent } from './input-suggestions/dso-input-suggestions/dso-input-suggestions.component'; import { ItemGridElementComponent } from './object-grid/item-grid-element/item-types/item/item-grid-element.component'; import { TypeBadgeComponent } from './object-list/type-badge/type-badge.component'; +import { AccessStatusBadgeComponent } from './object-list/access-status-badge/access-status-badge.component'; import { MetadataRepresentationLoaderComponent } from './metadata-representation/metadata-representation-loader.component'; import { MetadataRepresentationDirective } from './metadata-representation/metadata-representation.directive'; import { ListableObjectComponentLoaderComponent } from './object-collection/shared/listable-object/listable-object-component-loader.component'; @@ -293,6 +294,7 @@ const COMPONENTS = [ AbstractTrackableComponent, ComcolMetadataComponent, TypeBadgeComponent, + AccessStatusBadgeComponent, BrowseByComponent, AbstractTrackableComponent, 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); } } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 76f4f538ea..d30661f3d7 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -27,6 +27,16 @@ "404.page-not-found": "page not found", + "access-status.embargo.listelement.badge": "Embargo", + + "access-status.metadata.only.listelement.badge": "Metadata only", + + "access-status.open.access.listelement.badge": "Open Access", + + "access-status.restricted.listelement.badge": "Restricted", + + "access-status.unknown.listelement.badge": "Unknown", + "admin.curation-tasks.breadcrumbs": "System curation tasks", "admin.curation-tasks.title": "System curation tasks", diff --git a/src/assets/i18n/fr.json5 b/src/assets/i18n/fr.json5 index 0f14b78341..3b9d7498be 100644 --- a/src/assets/i18n/fr.json5 +++ b/src/assets/i18n/fr.json5 @@ -36,6 +36,21 @@ // "404.page-not-found": "page not found", "404.page-not-found": "Page introuvable", + // "access-status.embargo.listelement.badge": "Embargo", + "access-status.embargo.listelement.badge": "Restriction temporaire", + + // "access-status.metadata.only.listelement.badge": "Metadata only", + "access-status.metadata.only.listelement.badge": "Métadonnées seulement", + + // "access-status.open.access.listelement.badge": "Open Access", + "access-status.open.access.listelement.badge": "Accès libre", + + // "access-status.restricted.listelement.badge": "Restricted", + "access-status.restricted.listelement.badge": "Restreint", + + // "access-status.unknown.listelement.badge": "Unknown", + "access-status.unknown.listelement.badge": "Inconnu", + // "admin.curation-tasks.breadcrumbs": "System curation tasks", "admin.curation-tasks.breadcrumbs": "Tâches de conservation système", diff --git a/src/config/app-config.interface.ts b/src/config/app-config.interface.ts index a41ec05b82..8bfa1f66f1 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'; @@ -29,7 +29,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 a7360bd1d1..4c68a38081 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'; @@ -198,11 +198,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/environments/environment.test.ts b/src/environments/environment.test.ts index bc1622f5f2..b2eb0081da 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -196,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: {