diff --git a/src/app/core/data/feature-authorization/feature-id.ts b/src/app/core/data/feature-authorization/feature-id.ts index 9e73ca9a2b..d1692b5f0f 100644 --- a/src/app/core/data/feature-authorization/feature-id.ts +++ b/src/app/core/data/feature-authorization/feature-id.ts @@ -32,4 +32,5 @@ export enum FeatureID { CanSynchronizeWithORCID = 'canSynchronizeWithORCID', CanSubmit = 'canSubmit', CanEditItem = 'canEditItem', + CanRegisterDOI = 'canRegisterDOI', } diff --git a/src/app/core/data/identifier-data.service.ts b/src/app/core/data/identifier-data.service.ts new file mode 100644 index 0000000000..9847f0b2b5 --- /dev/null +++ b/src/app/core/data/identifier-data.service.ts @@ -0,0 +1,62 @@ +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'; +import {IDENTIFIERS} from '../../shared/object-list/identifier-data/identifier-data.resource-type'; +import {IdentifierData} from '../../shared/object-list/identifier-data/identifier-data.model'; +import {getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload} from '../shared/operators'; +import {map, startWith} from 'rxjs/operators'; +import {ConfigurationProperty} from '../shared/configuration-property.model'; +import {ConfigurationDataService} from './configuration-data.service'; + +@Injectable() +@dataService(IDENTIFIERS) +export class IdentifierDataService extends DataService { + + protected linkPath = 'identifiers'; + + 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, + private configurationService: ConfigurationDataService, + ) { + super(); + } + + /** + * Returns {@link RemoteData} of {@link IdentifierData} representing identifiers for this item + * @param item Item we are querying + */ + getIdentifierDataFor(item: Item): Observable> { + return this.findByHref(item._links.identifiers.href, false, true); + } + + /** + * Should we allow registration of new DOIs via the item status page? + */ + public getIdentifierRegistrationConfiguration(): Observable { + return this.configurationService.findByPropertyName('identifiers.item-status.register').pipe( + getFirstCompletedRemoteData(), + map((propertyRD: RemoteData) => propertyRD.hasSucceeded ? propertyRD.payload.values : []) + ); + } +} diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 09268a0282..5306dd468b 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -232,6 +232,36 @@ export abstract class BaseItemDataService extends IdentifiableDataService return this.rdbService.buildFromRequestUUID(requestId); } + /** + * Get the endpoint for an item's bundles + * @param itemId + */ + public getIdentifiersEndpoint(itemId: string): Observable { + return this.halService.getEndpoint(this.linkPath).pipe( + switchMap((url: string) => this.halService.getEndpoint('identifiers', `${url}/${itemId}`)) + ); + } + + /** + * Register a DOI for a given item + * @param itemId + */ + public registerDOI(itemId: string): Observable> { + const requestId = this.requestService.generateRequestId(); + const hrefObs = this.getIdentifiersEndpoint(itemId); + hrefObs.pipe( + take(1) + ).subscribe((href) => { + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'application/json'); + options.headers = headers; + const request = new PostRequest(requestId, href, JSON.stringify({}), options); + this.requestService.send(request); + }); + return this.rdbService.buildFromRequestUUID(requestId); + } + /** * Get the endpoint to move the item * @param itemId diff --git a/src/app/core/shared/item.model.ts b/src/app/core/shared/item.model.ts index 28f0d7fd36..a9acf8c10f 100644 --- a/src/app/core/shared/item.model.ts +++ b/src/app/core/shared/item.model.ts @@ -24,6 +24,8 @@ 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'; import { HandleObject } from './handle-object.model'; +import { IDENTIFIERS } from '../../shared/object-list/identifier-data/identifier-data.resource-type'; +import { IdentifierData } from '../../shared/object-list/identifier-data/identifier-data.model'; /** * Class representing a DSpace Item @@ -76,6 +78,7 @@ export class Item extends DSpaceObject implements ChildHALResource, HandleObject version: HALLink; thumbnail: HALLink; accessStatus: HALLink; + identifiers: HALLink; self: HALLink; }; @@ -121,6 +124,13 @@ export class Item extends DSpaceObject implements ChildHALResource, HandleObject @link(ACCESS_STATUS) accessStatus?: Observable>; + /** + * The identifier data for this Item + * Will be undefined unless the identifiers {@link HALLink} has been resolved. + */ + @link(IDENTIFIERS) + identifiers?: Observable>; + /** * Method that returns as which type of object this object should be rendered */ diff --git a/src/app/item-page/edit-item-page/edit-item-page.module.ts b/src/app/item-page/edit-item-page/edit-item-page.module.ts index 70f6c55c28..bbce184375 100644 --- a/src/app/item-page/edit-item-page/edit-item-page.module.ts +++ b/src/app/item-page/edit-item-page/edit-item-page.module.ts @@ -34,6 +34,9 @@ import { ItemAuthorizationsComponent } from './item-authorizations/item-authoriz import { ObjectValuesPipe } from '../../shared/utils/object-values-pipe'; import { ResourcePoliciesModule } from '../../shared/resource-policies/resource-policies.module'; import { ItemVersionsModule } from '../versions/item-versions.module'; +import { IdentifierDataService } from '../../core/data/identifier-data.service'; +import { IdentifierDataComponent } from '../../shared/object-list/identifier-data/identifier-data.component'; +import { ItemRegisterDoiComponent } from './item-register-doi/item-registerdoi.component'; import { DsoSharedModule } from '../../dso-shared/dso-shared.module'; @@ -76,10 +79,13 @@ import { DsoSharedModule } from '../../dso-shared/dso-shared.module'; ItemMoveComponent, ItemEditBitstreamDragHandleComponent, VirtualMetadataComponent, - ItemAuthorizationsComponent + ItemAuthorizationsComponent, + IdentifierDataComponent, + ItemRegisterDoiComponent ], providers: [ BundleDataService, + IdentifierDataService, ObjectValuesPipe ], }) diff --git a/src/app/item-page/edit-item-page/edit-item-page.routing-paths.ts b/src/app/item-page/edit-item-page/edit-item-page.routing-paths.ts index ce76a614dd..2826d06bb4 100644 --- a/src/app/item-page/edit-item-page/edit-item-page.routing-paths.ts +++ b/src/app/item-page/edit-item-page/edit-item-page.routing-paths.ts @@ -5,3 +5,4 @@ export const ITEM_EDIT_PUBLIC_PATH = 'public'; export const ITEM_EDIT_DELETE_PATH = 'delete'; export const ITEM_EDIT_MOVE_PATH = 'move'; export const ITEM_EDIT_AUTHORIZATIONS_PATH = 'authorizations'; +export const ITEM_EDIT_REGISTER_DOI_PATH = 'registerdoi'; diff --git a/src/app/item-page/edit-item-page/edit-item-page.routing.module.ts b/src/app/item-page/edit-item-page/edit-item-page.routing.module.ts index e6fa53f3f3..ee4c563646 100644 --- a/src/app/item-page/edit-item-page/edit-item-page.routing.module.ts +++ b/src/app/item-page/edit-item-page/edit-item-page.routing.module.ts @@ -10,6 +10,7 @@ import { ItemStatusComponent } from './item-status/item-status.component'; import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.component'; import { ItemCollectionMapperComponent } from './item-collection-mapper/item-collection-mapper.component'; import { ItemMoveComponent } from './item-move/item-move.component'; +import { ItemRegisterDoiComponent } from './item-register-doi/item-registerdoi.component' import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component'; import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component'; @@ -26,7 +27,8 @@ import { ITEM_EDIT_PRIVATE_PATH, ITEM_EDIT_PUBLIC_PATH, ITEM_EDIT_REINSTATE_PATH, - ITEM_EDIT_WITHDRAW_PATH + ITEM_EDIT_WITHDRAW_PATH, + ITEM_EDIT_REGISTER_DOI_PATH } from './edit-item-page.routing-paths'; import { ItemPageReinstateGuard } from './item-page-reinstate.guard'; import { ItemPageWithdrawGuard } from './item-page-withdraw.guard'; @@ -38,6 +40,7 @@ import { ItemPageRelationshipsGuard } from './item-page-relationships.guard'; import { ItemPageVersionHistoryGuard } from './item-page-version-history.guard'; import { ItemPageCollectionMapperGuard } from './item-page-collection-mapper.guard'; import { ThemedDsoEditMetadataComponent } from '../../dso-shared/dso-edit-metadata/themed-dso-edit-metadata.component'; +import { ItemPageRegisterDoiGuard } from './item-page-registerdoi.guard'; /** * Routing module that handles the routing for the Edit Item page administrator functionality @@ -142,6 +145,12 @@ import { ThemedDsoEditMetadataComponent } from '../../dso-shared/dso-edit-metada component: ItemMoveComponent, data: { title: 'item.edit.move.title' }, }, + { + path: ITEM_EDIT_REGISTER_DOI_PATH, + component: ItemRegisterDoiComponent, + canActivate: [ItemPageRegisterDoiGuard], + data: { title: 'item.edit.registerdoi.title' }, + }, { path: ITEM_EDIT_AUTHORIZATIONS_PATH, children: [ @@ -186,6 +195,7 @@ import { ThemedDsoEditMetadataComponent } from '../../dso-shared/dso-edit-metada ItemPageRelationshipsGuard, ItemPageVersionHistoryGuard, ItemPageCollectionMapperGuard, + ItemPageRegisterDoiGuard, ] }) export class EditItemPageRoutingModule { diff --git a/src/app/item-page/edit-item-page/item-page-registerdoi.guard.ts b/src/app/item-page/edit-item-page/item-page-registerdoi.guard.ts new file mode 100644 index 0000000000..6a20d4d348 --- /dev/null +++ b/src/app/item-page/edit-item-page/item-page-registerdoi.guard.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@angular/core'; +import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; +import { Item } from '../../core/shared/item.model'; +import { ItemPageResolver } from '../item-page.resolver'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; +import { Observable, of as observableOf } from 'rxjs'; +import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { AuthService } from '../../core/auth/auth.service'; + +@Injectable({ + providedIn: 'root' +}) +/** + * Guard for preventing unauthorized access to certain {@link Item} pages requiring DOI registration rights + */ +export class ItemPageRegisterDoiGuard extends DsoPageSingleFeatureGuard { + constructor(protected resolver: ItemPageResolver, + protected authorizationService: AuthorizationDataService, + protected router: Router, + protected authService: AuthService) { + super(resolver, authorizationService, router, authService); + } + + /** + * Check DOI registration authorization rights + */ + getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf(FeatureID.CanRegisterDOI); + } +} diff --git a/src/app/item-page/edit-item-page/item-page-status.guard.ts b/src/app/item-page/edit-item-page/item-page-status.guard.ts index 98f963a4be..ade565da69 100644 --- a/src/app/item-page/edit-item-page/item-page-status.guard.ts +++ b/src/app/item-page/edit-item-page/item-page-status.guard.ts @@ -27,6 +27,6 @@ export class ItemPageStatusGuard extends DsoPageSomeFeatureGuard { * Check authorization rights */ getFeatureIDs(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return observableOf([FeatureID.CanManageMappings, FeatureID.WithdrawItem, FeatureID.ReinstateItem, FeatureID.CanManagePolicies, FeatureID.CanMakePrivate, FeatureID.CanDelete, FeatureID.CanMove]); + return observableOf([FeatureID.CanManageMappings, FeatureID.WithdrawItem, FeatureID.ReinstateItem, FeatureID.CanManagePolicies, FeatureID.CanMakePrivate, FeatureID.CanDelete, FeatureID.CanMove, FeatureID.CanRegisterDOI]); } } diff --git a/src/app/item-page/edit-item-page/item-register-doi/item-registerdoi-component.html b/src/app/item-page/edit-item-page/item-register-doi/item-registerdoi-component.html new file mode 100644 index 0000000000..89f5718fc4 --- /dev/null +++ b/src/app/item-page/edit-item-page/item-register-doi/item-registerdoi-component.html @@ -0,0 +1,24 @@ +
+
+
+

{{headerMessage | translate: {id: item.handle} }}

+

{{descriptionMessage | translate}}

+
+
+

{{doiToUpdateMessage | translate}}: {{identifier.value}} + ({{"item.edit.identifiers.doi.status."+identifier.identifierStatus|translate}}) +

+
+
+ +
+ + +
+
+
+ +
diff --git a/src/app/item-page/edit-item-page/item-register-doi/item-registerdoi.component.ts b/src/app/item-page/edit-item-page/item-register-doi/item-registerdoi.component.ts new file mode 100644 index 0000000000..060fef3b32 --- /dev/null +++ b/src/app/item-page/edit-item-page/item-register-doi/item-registerdoi.component.ts @@ -0,0 +1,87 @@ +import { Component } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { TranslateService } from '@ngx-translate/core'; + +import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component'; +import { RemoteData } from '../../../core/data/remote-data'; +import { Item } from '../../../core/shared/item.model'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { ItemDataService } from '../../../core/data/item-data.service'; +import { getFirstCompletedRemoteData, getFirstSucceededRemoteData } from '../../../core/shared/operators'; +import { first, map } from 'rxjs/operators'; +import { hasValue } from '../../../shared/empty.util'; +import { Observable } from 'rxjs'; +import {getItemEditRoute, getItemPageRoute} from '../../item-page-routing-paths'; +import { IdentifierDataService } from '../../../core/data/identifier-data.service'; +import {Identifier} from '../../../shared/object-list/identifier-data/identifier.model'; + +@Component({ + selector: 'ds-item-registerdoi', + templateUrl: './item-registerdoi-component.html' +}) +/** + * Component responsible for rendering the Item Registe DOI page + */ +export class ItemRegisterDoiComponent extends AbstractSimpleItemActionComponent { + + protected messageKey = 'registerdoi'; + doiToUpdateMessage = 'item.edit.' + this.messageKey + '.to-update'; + identifiers$: Observable; + processing: boolean = false; + + constructor(protected route: ActivatedRoute, + protected router: Router, + protected notificationsService: NotificationsService, + protected itemDataService: ItemDataService, + protected translateService: TranslateService, + protected identifierDataService: IdentifierDataService) { + super(route, router, notificationsService, itemDataService, translateService); + } + + ngOnInit(): void { + this.itemRD$ = this.route.data.pipe( + map((data) => data.dso), + getFirstSucceededRemoteData() + )as Observable>; + + this.itemRD$.pipe(first()).subscribe((rd) => { + this.item = rd.payload; + this.itemPageRoute = getItemPageRoute(this.item); + this.identifiers$ = this.identifierDataService.getIdentifierDataFor(this.item).pipe( + map((identifierRD) => { + if (identifierRD.statusCode !== 401 && hasValue(identifierRD.payload)) { + return identifierRD.payload.identifiers; + } else { + return null; + } + }), + ); + } + ); + + this.confirmMessage = 'item.edit.' + this.messageKey + '.confirm'; + this.cancelMessage = 'item.edit.' + this.messageKey + '.cancel'; + this.headerMessage = 'item.edit.' + this.messageKey + '.header'; + this.descriptionMessage = 'item.edit.' + this.messageKey + '.description'; + + + } + + /** + * Perform the register DOI action to the item + */ + performAction() { + this.registerDoi(); + } + + registerDoi() { + this.processing = true; + this.itemDataService.registerDOI(this.item.id).pipe(getFirstCompletedRemoteData()).subscribe( + (response: RemoteData) => { + this.processing = false; + this.router.navigateByUrl(getItemEditRoute(this.item)); + } + ); + } + +} diff --git a/src/app/item-page/edit-item-page/item-status/item-status.component.html b/src/app/item-page/edit-item-page/item-status/item-status.component.html index d4f61c99c1..8d4faaa2ac 100644 --- a/src/app/item-page/edit-item-page/item-status/item-status.component.html +++ b/src/app/item-page/edit-item-page/item-status/item-status.component.html @@ -8,6 +8,17 @@ {{statusData[statusKey]}} + +
+
+
+ {{identifier.identifierType.toLocaleUpperCase()}} +
+
{{identifier.value}} + ({{"item.edit.identifiers.doi.status."+identifier.identifierStatus|translate}})
+
+
+
{{'item.edit.tabs.status.labels.itemPage' | translate}}:
@@ -18,4 +29,5 @@
+ diff --git a/src/app/item-page/edit-item-page/item-status/item-status.component.ts b/src/app/item-page/edit-item-page/item-status/item-status.component.ts index f01f5c1f7a..a719e621dc 100644 --- a/src/app/item-page/edit-item-page/item-status/item-status.component.ts +++ b/src/app/item-page/edit-item-page/item-status/item-status.component.ts @@ -3,14 +3,23 @@ import { fadeIn, fadeInOut } from '../../../shared/animations/fade'; import { Item } from '../../../core/shared/item.model'; import { ActivatedRoute } from '@angular/router'; import { ItemOperation } from '../item-operation/itemOperation.model'; -import { distinctUntilChanged, first, map, mergeMap, toArray } from 'rxjs/operators'; -import { BehaviorSubject, Observable, from as observableFrom } from 'rxjs'; +import {distinctUntilChanged, first, map, mergeMap, startWith, switchMap, toArray} from 'rxjs/operators'; +import {BehaviorSubject, Observable, from as observableFrom, Subscription, combineLatest, of} from 'rxjs'; import { RemoteData } from '../../../core/data/remote-data'; import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-paths'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; import { hasValue } from '../../../shared/empty.util'; -import { getAllSucceededRemoteDataPayload } from '../../../core/shared/operators'; +import { + getAllSucceededRemoteDataPayload, + getFirstCompletedRemoteData, getFirstSucceededRemoteData, + getFirstSucceededRemoteDataPayload +} from '../../../core/shared/operators'; +import {IdentifierDataService} from '../../../core/data/identifier-data.service'; +import {IdentifierData} from '../../../shared/object-list/identifier-data/identifier-data.model'; +import {Identifier} from '../../../shared/object-list/identifier-data/identifier.model'; +import {ConfigurationProperty} from '../../../core/shared/configuration-property.model'; +import {ConfigurationDataService} from '../../../core/data/configuration-data.service'; @Component({ selector: 'ds-item-status', @@ -51,15 +60,32 @@ export class ItemStatusComponent implements OnInit { */ actionsKeys; + /** + * Identifiers (handles, DOIs) + */ + identifiers$: Observable; + + /** + * Configuration and state variables regarding DOIs + */ + + public subs: Subscription[] = []; + /** * Route to the item's page */ itemPageRoute$: Observable; constructor(private route: ActivatedRoute, - private authorizationService: AuthorizationDataService) { + private authorizationService: AuthorizationDataService, + private identifierDataService: IdentifierDataService, + private configurationService: ConfigurationDataService, + ) { } + /** + * Initialise component + */ ngOnInit(): void { this.itemRD$ = this.route.parent.data.pipe(map((data) => data.dso)); this.itemRD$.pipe( @@ -72,6 +98,35 @@ export class ItemStatusComponent implements OnInit { lastModified: item.lastModified }); this.statusDataKeys = Object.keys(this.statusData); + + // Observable for item identifiers (retrieved from embedded link) + this.identifiers$ = this.identifierDataService.getIdentifierDataFor(item).pipe( + map((identifierRD) => { + if (identifierRD.statusCode !== 401 && hasValue(identifierRD.payload)) { + return identifierRD.payload.identifiers; + } else { + return null; + } + }), + ); + + // Observable for configuration determining whether the Register DOI feature is enabled + let registerConfigEnabled$: Observable = this.configurationService.findByPropertyName('identifiers.item-status.register').pipe( + map((enabled: RemoteData) => { + let show: boolean = false; + if (enabled.hasSucceeded) { + if (enabled.payload !== undefined && enabled.payload !== null) { + if (enabled.payload.values !== undefined) { + enabled.payload.values.forEach((value) => { + show = true; + }); + } + } + } + return show; + }) + ); + /* The key is used to build messages i18n example: 'item.edit.tabs.status.buttons..label' @@ -92,27 +147,66 @@ export class ItemStatusComponent implements OnInit { } operations.push(new ItemOperation('delete', this.getCurrentUrl(item) + '/delete', FeatureID.CanDelete, true)); operations.push(new ItemOperation('move', this.getCurrentUrl(item) + '/move', FeatureID.CanMove, true)); - this.operations$.next(operations); - observableFrom(operations).pipe( - mergeMap((operation) => { - if (hasValue(operation.featureID)) { - return this.authorizationService.isAuthorized(operation.featureID, item.self).pipe( - distinctUntilChanged(), - map((authorized) => new ItemOperation(operation.operationKey, operation.operationUrl, operation.featureID, !authorized, authorized)) - ); - } else { - return [operation]; + // Observable that reads identifiers and their status and, and config properties, and decides + // if we're allowed to show a Register DOI feature + let showRegister$: Observable = combineLatest([this.identifiers$, registerConfigEnabled$]).pipe( + distinctUntilChanged(), + map(([identifiers, enabled]) => { + let no_doi: boolean = true; + let pending: boolean = false; + if (identifiers !== undefined && identifiers !== null) { + identifiers.forEach((identifier: Identifier) => { + if (hasValue(identifier) && identifier.identifierType == 'doi') { + // The item has some kind of DOI + no_doi = false; + if (identifier.identifierStatus == '10' || identifier.identifierStatus == '11' + || identifier.identifierStatus == null) { + // The item's DOI is pending, minted or null. + // It isn't registered, reserved, queued for registration or reservation or update, deleted + // or queued for deletion. + pending = true; + } + } + }); } - }), - toArray() - ).subscribe((ops) => this.operations$.next(ops)); + // If there is no DOI, or a pending/minted/null DOI, and the config is enabled, return true + return ((pending || no_doi) && enabled); + }) + ) + + // Subscribe to changes from the showRegister check and rebuild operations list accordingly + this.subs.push(showRegister$.subscribe((show) => { + // Copy the static array first so we don't keep appending to it + let tmp_operations = [...operations]; + if (show) { + // Push the new Register DOI item operation + tmp_operations.push(new ItemOperation('registerDOI', this.getCurrentUrl(item) + '/registerdoi', FeatureID.CanRegisterDOI)); + } + // Check authorisations and merge into new operations list + observableFrom(tmp_operations).pipe( + mergeMap((operation) => { + if (hasValue(operation.featureID)) { + return this.authorizationService.isAuthorized(operation.featureID, item.self).pipe( + distinctUntilChanged(), + map((authorized) => new ItemOperation(operation.operationKey, operation.operationUrl, operation.featureID, !authorized, authorized)) + ); + } else { + return [operation]; + } + }), + toArray() + ).subscribe((ops) => this.operations$.next(ops)); + })); + }); + this.itemPageRoute$ = this.itemRD$.pipe( getAllSucceededRemoteDataPayload(), map((item) => getItemPageRoute(item)) ); + } /** @@ -127,4 +221,10 @@ export class ItemStatusComponent implements OnInit { return hasValue(operation) ? operation.operationKey : undefined; } + ngOnDestroy(): void { + this.subs + .filter((subscription) => hasValue(subscription)) + .forEach((subscription) => subscription.unsubscribe()); + } + } diff --git a/src/app/shared/object-list/identifier-data/identifier-data.component.html b/src/app/shared/object-list/identifier-data/identifier-data.component.html new file mode 100644 index 0000000000..91470628c4 --- /dev/null +++ b/src/app/shared/object-list/identifier-data/identifier-data.component.html @@ -0,0 +1,5 @@ + +
+ {{ identifiers[0].value | translate }} +
+
diff --git a/src/app/shared/object-list/identifier-data/identifier-data.component.ts b/src/app/shared/object-list/identifier-data/identifier-data.component.ts new file mode 100644 index 0000000000..85989f34c6 --- /dev/null +++ b/src/app/shared/object-list/identifier-data/identifier-data.component.ts @@ -0,0 +1,56 @@ +import { Component, Input } from '@angular/core'; +import { catchError, map } from 'rxjs/operators'; +import { Observable, of as observableOf } from 'rxjs'; +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'; +import {IdentifierData} from './identifier-data.model'; +import {IdentifierDataService} from '../../../core/data/identifier-data.service'; + +@Component({ + selector: 'ds-identifier-data', + templateUrl: './identifier-data.component.html' +}) +/** + * Component rendering the access status of an item as a badge + */ +export class IdentifierDataComponent { + + @Input() item: Item; + identifiers$: Observable; + + /** + * Whether to show the access status badge or not + */ + showAccessStatus: boolean; + + /** + * Initialize instance variables + * + * @param {IdentifierDataService} identifierDataService + */ + constructor(private identifierDataService: IdentifierDataService) { } + + ngOnInit(): void { + if (this.item == null) { + // Do not show the badge if the feature is inactive or if the item is null. + return; + } + if (this.item.identifiers == null) { + // In case the access status has not been loaded, do it individually. + this.item.identifiers = this.identifierDataService.getIdentifierDataFor(this.item); + } + this.identifiers$ = this.item.identifiers.pipe( + map((identifierRD) => { + if (identifierRD.statusCode !== 401 && hasValue(identifierRD.payload)) { + return identifierRD.payload; + } else { + return null; + } + }), + // EMpty array if none + //map((identifiers: IdentifierData) => hasValue(identifiers.identifiers) ? identifiers.identifiers : []) + ); + } +} diff --git a/src/app/shared/object-list/identifier-data/identifier-data.model.ts b/src/app/shared/object-list/identifier-data/identifier-data.model.ts new file mode 100644 index 0000000000..e707f396e4 --- /dev/null +++ b/src/app/shared/object-list/identifier-data/identifier-data.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 { IDENTIFIERS } from './identifier-data.resource-type'; +import {Identifier} from './identifier.model'; + +@typedObject +export class IdentifierData implements CacheableObject { + static type = IDENTIFIERS; + /** + * The type for this IdentifierData + */ + @excludeFromEquals + @autoserialize + type: ResourceType; + + /** + * The + */ + @autoserialize + identifiers: Identifier[]; + + /** + * The {@link HALLink}s for this IdentifierData + */ + @deserialize + _links: { + self: HALLink; + }; +} diff --git a/src/app/shared/object-list/identifier-data/identifier-data.resource-type.ts b/src/app/shared/object-list/identifier-data/identifier-data.resource-type.ts new file mode 100644 index 0000000000..823a43eff9 --- /dev/null +++ b/src/app/shared/object-list/identifier-data/identifier-data.resource-type.ts @@ -0,0 +1,9 @@ +import { ResourceType } from 'src/app/core/shared/resource-type'; + +/** + * The resource type for Identifiers + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +export const IDENTIFIERS = new ResourceType('identifiers'); diff --git a/src/app/shared/object-list/identifier-data/identifier.model.ts b/src/app/shared/object-list/identifier-data/identifier.model.ts new file mode 100644 index 0000000000..87b162afe1 --- /dev/null +++ b/src/app/shared/object-list/identifier-data/identifier.model.ts @@ -0,0 +1,12 @@ +import {autoserialize} from 'cerialize'; + +export class Identifier { + @autoserialize + value: string; + @autoserialize + identifierType: string; + @autoserialize + identifierStatus: string; + @autoserialize + type: string; +} diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 2790c664ec..47e9f57b6d 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1918,6 +1918,46 @@ "item.edit.tabs.item-mapper.title": "Item Edit - Collection Mapper", + "item.edit.identifiers.doi.status.1": "Queued for registration", + + "item.edit.identifiers.doi.status.2": "Queued for reservation", + + "item.edit.identifiers.doi.status.3": "Registered", + + "item.edit.identifiers.doi.status.4": "Reserved", + + "item.edit.identifiers.doi.status.5": "Reserved", + + "item.edit.identifiers.doi.status.6": "Registered", + + "item.edit.identifiers.doi.status.7": "Queued for registration", + + "item.edit.identifiers.doi.status.8": "Queued for deletion", + + "item.edit.identifiers.doi.status.9": "Deleted", + + "item.edit.identifiers.doi.status.10": "Pending approval", + + "item.edit.identifiers.doi.status.11": "Minted, not registered", + + "item.edit.tabs.status.buttons.registerDOI.label": "Register a new or pending identifier", + + "item.edit.tabs.status.buttons.registerDOI.button": "Register DOI...", + + "item.edit.registerdoi.header": "Register a new or pending DOI", + + "item.edit.registerdoi.description": "Review any pending identifiers and item metadata below and click Confirm to proceed with DOI registration, or Cancel to back out", + + "item.edit.registerdoi.confirm": "Confirm", + + "item.edit.registerdoi.cancel": "Cancel", + + "item.edit.registerdoi.success": "DOI registered successfully. Refresh Item Status page to see new DOI details.", + + "item.edit.registerdoi.error": "Error registering DOI", + + "item.edit.registerdoi.to-update": "The following DOI has already been minted and will be queued for registration online", + "item.edit.item-mapper.buttons.add": "Map item to selected collections", "item.edit.item-mapper.buttons.remove": "Remove item's mapping for selected collections",