mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge pull request #2638 from DSpace/backport-2633-to-dspace-7_x
[Port dspace-7_x] Edit-item view: random order of buttons in status tab
This commit is contained in:
@@ -28,4 +28,12 @@ export class ItemOperation {
|
|||||||
this.disabled = disabled;
|
this.disabled = disabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set whether this operation is authorized
|
||||||
|
* @param authorized
|
||||||
|
*/
|
||||||
|
setAuthorized(authorized: boolean): void {
|
||||||
|
this.authorized = authorized;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -27,7 +27,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngFor="let operation of (operations$ | async)" class="w-100" [ngClass]="{'pt-3': operation}">
|
<div *ngFor="let operation of (operations$ | async)" class="w-100" [ngClass]="{'pt-3': operation}">
|
||||||
<ds-item-operation *ngIf="operation" [operation]="operation"></ds-item-operation>
|
<ds-item-operation [operation]="operation"></ds-item-operation>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@@ -16,6 +16,7 @@ import { AuthorizationDataService } from '../../../core/data/feature-authorizati
|
|||||||
import { IdentifierDataService } from '../../../core/data/identifier-data.service';
|
import { IdentifierDataService } from '../../../core/data/identifier-data.service';
|
||||||
import { ConfigurationDataService } from '../../../core/data/configuration-data.service';
|
import { ConfigurationDataService } from '../../../core/data/configuration-data.service';
|
||||||
import { ConfigurationProperty } from '../../../core/shared/configuration-property.model';
|
import { ConfigurationProperty } from '../../../core/shared/configuration-property.model';
|
||||||
|
import { OrcidAuthService } from '../../../core/orcid/orcid-auth.service';
|
||||||
|
|
||||||
let mockIdentifierDataService: IdentifierDataService;
|
let mockIdentifierDataService: IdentifierDataService;
|
||||||
let mockConfigurationDataService: ConfigurationDataService;
|
let mockConfigurationDataService: ConfigurationDataService;
|
||||||
@@ -57,12 +58,18 @@ describe('ItemStatusComponent', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let authorizationService: AuthorizationDataService;
|
let authorizationService: AuthorizationDataService;
|
||||||
|
let orcidAuthService: any;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||||
isAuthorized: observableOf(true)
|
isAuthorized: observableOf(true)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
orcidAuthService = jasmine.createSpyObj('OrcidAuthService', {
|
||||||
|
onlyAdminCanDisconnectProfileFromOrcid: observableOf ( true ),
|
||||||
|
isLinkedToOrcid: true
|
||||||
|
});
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
||||||
declarations: [ItemStatusComponent],
|
declarations: [ItemStatusComponent],
|
||||||
@@ -71,7 +78,8 @@ describe('ItemStatusComponent', () => {
|
|||||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||||
{ provide: IdentifierDataService, useValue: mockIdentifierDataService },
|
{ provide: IdentifierDataService, useValue: mockIdentifierDataService },
|
||||||
{ provide: ConfigurationDataService, useValue: mockConfigurationDataService }
|
{ provide: ConfigurationDataService, useValue: mockConfigurationDataService },
|
||||||
|
{ provide: OrcidAuthService, useValue: orcidAuthService },
|
||||||
], schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
], schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
@@ -3,21 +3,20 @@ import { fadeIn, fadeInOut } from '../../../shared/animations/fade';
|
|||||||
import { Item } from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { ItemOperation } from '../item-operation/itemOperation.model';
|
import { ItemOperation } from '../item-operation/itemOperation.model';
|
||||||
import { distinctUntilChanged, map, mergeMap, switchMap, toArray } from 'rxjs/operators';
|
import { concatMap, distinctUntilChanged, first, map, mergeMap, switchMap, toArray } from 'rxjs/operators';
|
||||||
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
import { BehaviorSubject, combineLatest, Observable, of, Subscription } from 'rxjs';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-paths';
|
import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-paths';
|
||||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||||
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||||
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
import {
|
import { getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData, } from '../../../core/shared/operators';
|
||||||
getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData, getFirstSucceededRemoteData, getRemoteDataPayload,
|
|
||||||
} from '../../../core/shared/operators';
|
|
||||||
import { IdentifierDataService } from '../../../core/data/identifier-data.service';
|
import { IdentifierDataService } from '../../../core/data/identifier-data.service';
|
||||||
import { Identifier } from '../../../shared/object-list/identifier-data/identifier.model';
|
import { Identifier } from '../../../shared/object-list/identifier-data/identifier.model';
|
||||||
import { ConfigurationProperty } from '../../../core/shared/configuration-property.model';
|
import { ConfigurationProperty } from '../../../core/shared/configuration-property.model';
|
||||||
import { ConfigurationDataService } from '../../../core/data/configuration-data.service';
|
import { ConfigurationDataService } from '../../../core/data/configuration-data.service';
|
||||||
import { IdentifierData } from '../../../shared/object-list/identifier-data/identifier-data.model';
|
import { IdentifierData } from '../../../shared/object-list/identifier-data/identifier-data.model';
|
||||||
|
import { OrcidAuthService } from '../../../core/orcid/orcid-auth.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-status',
|
selector: 'ds-item-status',
|
||||||
@@ -73,6 +72,7 @@ export class ItemStatusComponent implements OnInit {
|
|||||||
private authorizationService: AuthorizationDataService,
|
private authorizationService: AuthorizationDataService,
|
||||||
private identifierDataService: IdentifierDataService,
|
private identifierDataService: IdentifierDataService,
|
||||||
private configurationService: ConfigurationDataService,
|
private configurationService: ConfigurationDataService,
|
||||||
|
private orcidAuthService: OrcidAuthService
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,14 +82,16 @@ export class ItemStatusComponent implements OnInit {
|
|||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.itemRD$ = this.route.parent.data.pipe(map((data) => data.dso));
|
this.itemRD$ = this.route.parent.data.pipe(map((data) => data.dso));
|
||||||
this.itemRD$.pipe(
|
this.itemRD$.pipe(
|
||||||
|
first(),
|
||||||
map((data: RemoteData<Item>) => data.payload)
|
map((data: RemoteData<Item>) => data.payload)
|
||||||
).subscribe((item: Item) => {
|
).pipe(
|
||||||
this.statusData = Object.assign({
|
switchMap((item: Item) => {
|
||||||
id: item.id,
|
this.statusData = Object.assign({
|
||||||
handle: item.handle,
|
id: item.id,
|
||||||
lastModified: item.lastModified
|
handle: item.handle,
|
||||||
});
|
lastModified: item.lastModified
|
||||||
this.statusDataKeys = Object.keys(this.statusData);
|
});
|
||||||
|
this.statusDataKeys = Object.keys(this.statusData);
|
||||||
|
|
||||||
// Observable for item identifiers (retrieved from embedded link)
|
// Observable for item identifiers (retrieved from embedded link)
|
||||||
this.identifiers$ = this.identifierDataService.getIdentifierDataFor(item).pipe(
|
this.identifiers$ = this.identifierDataService.getIdentifierDataFor(item).pipe(
|
||||||
@@ -105,99 +107,108 @@ export class ItemStatusComponent implements OnInit {
|
|||||||
// Observable for configuration determining whether the Register DOI feature is enabled
|
// Observable for configuration determining whether the Register DOI feature is enabled
|
||||||
let registerConfigEnabled$: Observable<boolean> = this.configurationService.findByPropertyName('identifiers.item-status.register-doi').pipe(
|
let registerConfigEnabled$: Observable<boolean> = this.configurationService.findByPropertyName('identifiers.item-status.register-doi').pipe(
|
||||||
getFirstCompletedRemoteData(),
|
getFirstCompletedRemoteData(),
|
||||||
map((rd: RemoteData<ConfigurationProperty>) => {
|
map((enabledRD: RemoteData<ConfigurationProperty>) => enabledRD.hasSucceeded && enabledRD.payload.values.length > 0)
|
||||||
// If the config property is exposed via rest and has a value set, return it
|
|
||||||
if (rd.hasSucceeded && hasValue(rd.payload) && isNotEmpty(rd.payload.values)) {
|
|
||||||
return rd.payload.values[0] === 'true';
|
|
||||||
}
|
|
||||||
// Otherwise, return false
|
|
||||||
return false;
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/*
|
/**
|
||||||
Construct a base list of operations.
|
* Construct a base list of operations.
|
||||||
The key is used to build messages
|
* The key is used to build messages
|
||||||
i18n example: 'item.edit.tabs.status.buttons.<key>.label'
|
* i18n example: 'item.edit.tabs.status.buttons.<key>.label'
|
||||||
The value is supposed to be a href for the button
|
* The value is supposed to be a href for the button
|
||||||
*/
|
|
||||||
const operations: ItemOperation[] = [];
|
|
||||||
operations.push(new ItemOperation('authorizations', this.getCurrentUrl(item) + '/authorizations', FeatureID.CanManagePolicies, true));
|
|
||||||
operations.push(new ItemOperation('mappedCollections', this.getCurrentUrl(item) + '/mapper', FeatureID.CanManageMappings, true));
|
|
||||||
if (item.isWithdrawn) {
|
|
||||||
operations.push(new ItemOperation('reinstate', this.getCurrentUrl(item) + '/reinstate', FeatureID.ReinstateItem, true));
|
|
||||||
} else {
|
|
||||||
operations.push(new ItemOperation('withdraw', this.getCurrentUrl(item) + '/withdraw', FeatureID.WithdrawItem, true));
|
|
||||||
}
|
|
||||||
if (item.isDiscoverable) {
|
|
||||||
operations.push(new ItemOperation('private', this.getCurrentUrl(item) + '/private', FeatureID.CanMakePrivate, true));
|
|
||||||
} else {
|
|
||||||
operations.push(new ItemOperation('public', this.getCurrentUrl(item) + '/public', FeatureID.CanMakePrivate, true));
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
|
|
||||||
/*
|
|
||||||
When the identifier data stream changes, determine whether the register DOI button should be shown or not.
|
|
||||||
This is based on whether the DOI is in the right state (minted or pending, not already queued for registration
|
|
||||||
or registered) and whether the configuration property identifiers.item-status.register-doi is true
|
|
||||||
*/
|
*/
|
||||||
this.identifierDataService.getIdentifierDataFor(item).pipe(
|
const currentUrl = this.getCurrentUrl(item);
|
||||||
getFirstSucceededRemoteData(),
|
const inititalOperations: ItemOperation[] = [
|
||||||
getRemoteDataPayload(),
|
new ItemOperation('authorizations', `${currentUrl}/authorizations`, FeatureID.CanManagePolicies, true),
|
||||||
mergeMap((data: IdentifierData) => {
|
new ItemOperation('mappedCollections', `${currentUrl}/mapper`, FeatureID.CanManageMappings, true),
|
||||||
let identifiers = data.identifiers;
|
item.isWithdrawn
|
||||||
let no_doi = true;
|
? new ItemOperation('reinstate', `${currentUrl}/reinstate`, FeatureID.ReinstateItem, true)
|
||||||
let pending = false;
|
: new ItemOperation('withdraw', `${currentUrl}/withdraw`, FeatureID.WithdrawItem, true),
|
||||||
if (identifiers !== undefined && identifiers !== null) {
|
item.isDiscoverable
|
||||||
identifiers.forEach((identifier: Identifier) => {
|
? new ItemOperation('private', `${currentUrl}/private`, FeatureID.CanMakePrivate, true)
|
||||||
if (hasValue(identifier) && identifier.identifierType === 'doi') {
|
: new ItemOperation('public', `${currentUrl}/public`, FeatureID.CanMakePrivate, true),
|
||||||
// The item has some kind of DOI
|
new ItemOperation('move', `${currentUrl}/move`, FeatureID.CanMove, true),
|
||||||
no_doi = false;
|
new ItemOperation('delete', `${currentUrl}/delete`, FeatureID.CanDelete, true)
|
||||||
if (identifier.identifierStatus === 'PENDING' || identifier.identifierStatus === 'MINTED'
|
];
|
||||||
|| identifier.identifierStatus == null) {
|
|
||||||
// The item's DOI is pending, minted or null.
|
this.operations$.next(inititalOperations);
|
||||||
// It isn't registered, reserved, queued for registration or reservation or update, deleted
|
|
||||||
// or queued for deletion.
|
/**
|
||||||
pending = true;
|
* When the identifier data stream changes, determine whether the register DOI button should be shown or not.
|
||||||
}
|
* This is based on whether the DOI is in the right state (minted or pending, not already queued for registration
|
||||||
|
* or registered) and whether the configuration property identifiers.item-status.register-doi is true
|
||||||
|
*/
|
||||||
|
const ops$ = this.identifierDataService.getIdentifierDataFor(item).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
mergeMap((dataRD: RemoteData<IdentifierData>) => {
|
||||||
|
if (dataRD.hasSucceeded) {
|
||||||
|
let identifiers = dataRD.payload.identifiers;
|
||||||
|
let no_doi = true;
|
||||||
|
let pending = 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 (['PENDING', 'MINTED', null].includes(identifier.identifierStatus)) {
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
// If there is no DOI, or a pending/minted/null DOI, and the config is enabled, return true
|
||||||
}
|
return registerConfigEnabled$.pipe(
|
||||||
// If there is no DOI, or a pending/minted/null DOI, and the config is enabled, return true
|
map((enabled: boolean) => {
|
||||||
return registerConfigEnabled$.pipe(
|
return enabled && (pending || no_doi);
|
||||||
map((enabled: boolean) => {
|
}
|
||||||
return enabled && (pending || no_doi);
|
));
|
||||||
|
} else {
|
||||||
|
return of(false);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
// Switch map pushes the register DOI operation onto a copy of the base array then returns to the pipe
|
||||||
|
switchMap((showDoi: boolean) => {
|
||||||
|
const ops = [...inititalOperations];
|
||||||
|
if (showDoi) {
|
||||||
|
const op = new ItemOperation('register-doi', `${currentUrl}/register-doi`, FeatureID.CanRegisterDOI, true);
|
||||||
|
ops.splice(ops.length - 1, 0, op); // Add item before last
|
||||||
|
}
|
||||||
|
return inititalOperations;
|
||||||
|
}),
|
||||||
|
concatMap((op: ItemOperation) => {
|
||||||
|
if (hasValue(op.featureID)) {
|
||||||
|
return this.authorizationService.isAuthorized(op.featureID, item.self).pipe(
|
||||||
|
distinctUntilChanged(),
|
||||||
|
map((authorized) => {
|
||||||
|
op.setDisabled(!authorized);
|
||||||
|
op.setAuthorized(authorized);
|
||||||
|
return op;
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
));
|
|
||||||
}),
|
|
||||||
// Switch map pushes the register DOI operation onto a copy of the base array then returns to the pipe
|
|
||||||
switchMap((showDoi: boolean) => {
|
|
||||||
let ops = [...operations];
|
|
||||||
if (showDoi) {
|
|
||||||
ops.push(new ItemOperation('register-doi', this.getCurrentUrl(item) + '/register-doi', FeatureID.CanRegisterDOI, true));
|
|
||||||
}
|
|
||||||
return ops;
|
|
||||||
}),
|
|
||||||
// Merge map checks and transforms each operation in the array based on whether it is authorized or not (disabled)
|
|
||||||
mergeMap((op: ItemOperation) => {
|
|
||||||
if (hasValue(op.featureID)) {
|
|
||||||
return this.authorizationService.isAuthorized(op.featureID, item.self).pipe(
|
|
||||||
distinctUntilChanged(),
|
|
||||||
map((authorized) => new ItemOperation(op.operationKey, op.operationUrl, op.featureID, !authorized, authorized))
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return [op];
|
return [op];
|
||||||
}
|
}),
|
||||||
}),
|
toArray()
|
||||||
// Wait for all operations to be emitted and return as an array
|
);
|
||||||
toArray(),
|
|
||||||
).subscribe((data) => {
|
let orcidOps$ = of([]);
|
||||||
// Update the operations$ subject that draws the administrative buttons on the status page
|
if (this.orcidAuthService.isLinkedToOrcid(item)) {
|
||||||
this.operations$.next(data);
|
orcidOps$ = this.orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid().pipe(
|
||||||
});
|
map((canDisconnect) => {
|
||||||
});
|
if (canDisconnect) {
|
||||||
|
return [new ItemOperation('unlinkOrcid', `${currentUrl}/unlink-orcid`)];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return combineLatest([ops$, orcidOps$]);
|
||||||
|
}),
|
||||||
|
map(([ops, orcidOps]: [ItemOperation[], ItemOperation[]]) => [...ops, ...orcidOps])
|
||||||
|
).subscribe((ops) => this.operations$.next(ops));
|
||||||
|
|
||||||
this.itemPageRoute$ = this.itemRD$.pipe(
|
this.itemPageRoute$ = this.itemRD$.pipe(
|
||||||
getAllSucceededRemoteDataPayload(),
|
getAllSucceededRemoteDataPayload(),
|
||||||
@@ -206,6 +217,7 @@ export class ItemStatusComponent implements OnInit {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current url without query params
|
* Get the current url without query params
|
||||||
* @returns {string} url
|
* @returns {string} url
|
||||||
|
Reference in New Issue
Block a user