Skip to content

Commit 7ad5a5b

Browse files
authored
Merge pull request #2633 from vNovski/edit-item-view-random-order-of-buttons-in-status-tab
Edit-item view: random order of buttons in status tab
2 parents e99fff8 + fbbbc18 commit 7ad5a5b

File tree

4 files changed

+130
-102
lines changed

4 files changed

+130
-102
lines changed

src/app/item-page/edit-item-page/item-operation/itemOperation.model.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,12 @@ export class ItemOperation {
2828
this.disabled = disabled;
2929
}
3030

31+
/**
32+
* Set whether this operation is authorized
33+
* @param authorized
34+
*/
35+
setAuthorized(authorized: boolean): void {
36+
this.authorized = authorized;
37+
}
38+
3139
}

src/app/item-page/edit-item-page/item-status/item-status.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
</div>
2828

2929
<div *ngFor="let operation of (operations$ | async)" class="w-100" [ngClass]="{'pt-3': operation}">
30-
<ds-item-operation *ngIf="operation" [operation]="operation"></ds-item-operation>
30+
<ds-item-operation [operation]="operation"></ds-item-operation>
3131
</div>
3232

3333
</div>

src/app/item-page/edit-item-page/item-status/item-status.component.spec.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { AuthorizationDataService } from '../../../core/data/feature-authorizati
1616
import { IdentifierDataService } from '../../../core/data/identifier-data.service';
1717
import { ConfigurationDataService } from '../../../core/data/configuration-data.service';
1818
import { ConfigurationProperty } from '../../../core/shared/configuration-property.model';
19+
import { OrcidAuthService } from '../../../core/orcid/orcid-auth.service';
1920

2021
let mockIdentifierDataService: IdentifierDataService;
2122
let mockConfigurationDataService: ConfigurationDataService;
@@ -57,12 +58,18 @@ describe('ItemStatusComponent', () => {
5758
};
5859

5960
let authorizationService: AuthorizationDataService;
61+
let orcidAuthService: any;
6062

6163
beforeEach(waitForAsync(() => {
6264
authorizationService = jasmine.createSpyObj('authorizationService', {
6365
isAuthorized: observableOf(true)
6466
});
6567

68+
orcidAuthService = jasmine.createSpyObj('OrcidAuthService', {
69+
onlyAdminCanDisconnectProfileFromOrcid: observableOf ( true ),
70+
isLinkedToOrcid: true
71+
});
72+
6673
TestBed.configureTestingModule({
6774
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
6875
declarations: [ItemStatusComponent],
@@ -71,7 +78,8 @@ describe('ItemStatusComponent', () => {
7178
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
7279
{ provide: AuthorizationDataService, useValue: authorizationService },
7380
{ provide: IdentifierDataService, useValue: mockIdentifierDataService },
74-
{ provide: ConfigurationDataService, useValue: mockConfigurationDataService }
81+
{ provide: ConfigurationDataService, useValue: mockConfigurationDataService },
82+
{ provide: OrcidAuthService, useValue: orcidAuthService },
7583
], schemas: [CUSTOM_ELEMENTS_SCHEMA]
7684
}).compileComponents();
7785
}));

src/app/item-page/edit-item-page/item-status/item-status.component.ts

Lines changed: 112 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,20 @@ import { fadeIn, fadeInOut } from '../../../shared/animations/fade';
33
import { Item } from '../../../core/shared/item.model';
44
import { ActivatedRoute } from '@angular/router';
55
import { ItemOperation } from '../item-operation/itemOperation.model';
6-
import { distinctUntilChanged, map, mergeMap, switchMap, toArray } from 'rxjs/operators';
7-
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
6+
import { concatMap, distinctUntilChanged, first, map, mergeMap, switchMap, toArray } from 'rxjs/operators';
7+
import { BehaviorSubject, combineLatest, Observable, of, Subscription } from 'rxjs';
88
import { RemoteData } from '../../../core/data/remote-data';
99
import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-paths';
1010
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
1111
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
12-
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
13-
import {
14-
getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData, getFirstSucceededRemoteData, getRemoteDataPayload,
15-
} from '../../../core/shared/operators';
12+
import { hasValue } from '../../../shared/empty.util';
13+
import { getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData, } from '../../../core/shared/operators';
1614
import { IdentifierDataService } from '../../../core/data/identifier-data.service';
1715
import { Identifier } from '../../../shared/object-list/identifier-data/identifier.model';
1816
import { ConfigurationProperty } from '../../../core/shared/configuration-property.model';
1917
import { ConfigurationDataService } from '../../../core/data/configuration-data.service';
2018
import { IdentifierData } from '../../../shared/object-list/identifier-data/identifier-data.model';
19+
import { OrcidAuthService } from '../../../core/orcid/orcid-auth.service';
2120

2221
@Component({
2322
selector: 'ds-item-status',
@@ -73,6 +72,7 @@ export class ItemStatusComponent implements OnInit {
7372
private authorizationService: AuthorizationDataService,
7473
private identifierDataService: IdentifierDataService,
7574
private configurationService: ConfigurationDataService,
75+
private orcidAuthService: OrcidAuthService
7676
) {
7777
}
7878

@@ -82,14 +82,16 @@ export class ItemStatusComponent implements OnInit {
8282
ngOnInit(): void {
8383
this.itemRD$ = this.route.parent.data.pipe(map((data) => data.dso));
8484
this.itemRD$.pipe(
85+
first(),
8586
map((data: RemoteData<Item>) => data.payload)
86-
).subscribe((item: Item) => {
87-
this.statusData = Object.assign({
88-
id: item.id,
89-
handle: item.handle,
90-
lastModified: item.lastModified
91-
});
92-
this.statusDataKeys = Object.keys(this.statusData);
87+
).pipe(
88+
switchMap((item: Item) => {
89+
this.statusData = Object.assign({
90+
id: item.id,
91+
handle: item.handle,
92+
lastModified: item.lastModified
93+
});
94+
this.statusDataKeys = Object.keys(this.statusData);
9395

9496
// Observable for item identifiers (retrieved from embedded link)
9597
this.identifiers$ = this.identifierDataService.getIdentifierDataFor(item).pipe(
@@ -105,99 +107,108 @@ export class ItemStatusComponent implements OnInit {
105107
// Observable for configuration determining whether the Register DOI feature is enabled
106108
let registerConfigEnabled$: Observable<boolean> = this.configurationService.findByPropertyName('identifiers.item-status.register-doi').pipe(
107109
getFirstCompletedRemoteData(),
108-
map((rd: RemoteData<ConfigurationProperty>) => {
109-
// If the config property is exposed via rest and has a value set, return it
110-
if (rd.hasSucceeded && hasValue(rd.payload) && isNotEmpty(rd.payload.values)) {
111-
return rd.payload.values[0] === 'true';
112-
}
113-
// Otherwise, return false
114-
return false;
115-
})
110+
map((enabledRD: RemoteData<ConfigurationProperty>) => enabledRD.hasSucceeded && enabledRD.payload.values.length > 0)
116111
);
117112

118-
/*
119-
Construct a base list of operations.
120-
The key is used to build messages
121-
i18n example: 'item.edit.tabs.status.buttons.<key>.label'
122-
The value is supposed to be a href for the button
123-
*/
124-
const operations: ItemOperation[] = [];
125-
operations.push(new ItemOperation('authorizations', this.getCurrentUrl(item) + '/authorizations', FeatureID.CanManagePolicies, true));
126-
operations.push(new ItemOperation('mappedCollections', this.getCurrentUrl(item) + '/mapper', FeatureID.CanManageMappings, true));
127-
if (item.isWithdrawn) {
128-
operations.push(new ItemOperation('reinstate', this.getCurrentUrl(item) + '/reinstate', FeatureID.ReinstateItem, true));
129-
} else {
130-
operations.push(new ItemOperation('withdraw', this.getCurrentUrl(item) + '/withdraw', FeatureID.WithdrawItem, true));
131-
}
132-
if (item.isDiscoverable) {
133-
operations.push(new ItemOperation('private', this.getCurrentUrl(item) + '/private', FeatureID.CanMakePrivate, true));
134-
} else {
135-
operations.push(new ItemOperation('public', this.getCurrentUrl(item) + '/public', FeatureID.CanMakePrivate, true));
136-
}
137-
operations.push(new ItemOperation('delete', this.getCurrentUrl(item) + '/delete', FeatureID.CanDelete, true));
138-
operations.push(new ItemOperation('move', this.getCurrentUrl(item) + '/move', FeatureID.CanMove, true));
139-
this.operations$.next(operations);
140-
141-
/*
142-
When the identifier data stream changes, determine whether the register DOI button should be shown or not.
143-
This is based on whether the DOI is in the right state (minted or pending, not already queued for registration
144-
or registered) and whether the configuration property identifiers.item-status.register-doi is true
113+
/**
114+
* Construct a base list of operations.
115+
* The key is used to build messages
116+
* i18n example: 'item.edit.tabs.status.buttons.<key>.label'
117+
* The value is supposed to be a href for the button
145118
*/
146-
this.identifierDataService.getIdentifierDataFor(item).pipe(
147-
getFirstSucceededRemoteData(),
148-
getRemoteDataPayload(),
149-
mergeMap((data: IdentifierData) => {
150-
let identifiers = data.identifiers;
151-
let no_doi = true;
152-
let pending = false;
153-
if (identifiers !== undefined && identifiers !== null) {
154-
identifiers.forEach((identifier: Identifier) => {
155-
if (hasValue(identifier) && identifier.identifierType === 'doi') {
156-
// The item has some kind of DOI
157-
no_doi = false;
158-
if (identifier.identifierStatus === 'PENDING' || identifier.identifierStatus === 'MINTED'
159-
|| identifier.identifierStatus == null) {
160-
// The item's DOI is pending, minted or null.
161-
// It isn't registered, reserved, queued for registration or reservation or update, deleted
162-
// or queued for deletion.
163-
pending = true;
164-
}
119+
const currentUrl = this.getCurrentUrl(item);
120+
const inititalOperations: ItemOperation[] = [
121+
new ItemOperation('authorizations', `${currentUrl}/authorizations`, FeatureID.CanManagePolicies, true),
122+
new ItemOperation('mappedCollections', `${currentUrl}/mapper`, FeatureID.CanManageMappings, true),
123+
item.isWithdrawn
124+
? new ItemOperation('reinstate', `${currentUrl}/reinstate`, FeatureID.ReinstateItem, true)
125+
: new ItemOperation('withdraw', `${currentUrl}/withdraw`, FeatureID.WithdrawItem, true),
126+
item.isDiscoverable
127+
? new ItemOperation('private', `${currentUrl}/private`, FeatureID.CanMakePrivate, true)
128+
: new ItemOperation('public', `${currentUrl}/public`, FeatureID.CanMakePrivate, true),
129+
new ItemOperation('move', `${currentUrl}/move`, FeatureID.CanMove, true),
130+
new ItemOperation('delete', `${currentUrl}/delete`, FeatureID.CanDelete, true)
131+
];
132+
133+
this.operations$.next(inititalOperations);
134+
135+
/**
136+
* When the identifier data stream changes, determine whether the register DOI button should be shown or not.
137+
* This is based on whether the DOI is in the right state (minted or pending, not already queued for registration
138+
* or registered) and whether the configuration property identifiers.item-status.register-doi is true
139+
*/
140+
const ops$ = this.identifierDataService.getIdentifierDataFor(item).pipe(
141+
getFirstCompletedRemoteData(),
142+
mergeMap((dataRD: RemoteData<IdentifierData>) => {
143+
if (dataRD.hasSucceeded) {
144+
let identifiers = dataRD.payload.identifiers;
145+
let no_doi = true;
146+
let pending = false;
147+
if (identifiers !== undefined && identifiers !== null) {
148+
identifiers.forEach((identifier: Identifier) => {
149+
if (hasValue(identifier) && identifier.identifierType === 'doi') {
150+
// The item has some kind of DOI
151+
no_doi = false;
152+
if (['PENDING', 'MINTED', null].includes(identifier.identifierStatus)) {
153+
// The item's DOI is pending, minted or null.
154+
// It isn't registered, reserved, queued for registration or reservation or update, deleted
155+
// or queued for deletion.
156+
pending = true;
157+
}
158+
}
159+
});
165160
}
166-
});
167-
}
168-
// If there is no DOI, or a pending/minted/null DOI, and the config is enabled, return true
169-
return registerConfigEnabled$.pipe(
170-
map((enabled: boolean) => {
171-
return enabled && (pending || no_doi);
161+
// If there is no DOI, or a pending/minted/null DOI, and the config is enabled, return true
162+
return registerConfigEnabled$.pipe(
163+
map((enabled: boolean) => {
164+
return enabled && (pending || no_doi);
165+
}
166+
));
167+
} else {
168+
return of(false);
169+
}
170+
}),
171+
// Switch map pushes the register DOI operation onto a copy of the base array then returns to the pipe
172+
switchMap((showDoi: boolean) => {
173+
const ops = [...inititalOperations];
174+
if (showDoi) {
175+
const op = new ItemOperation('register-doi', `${currentUrl}/register-doi`, FeatureID.CanRegisterDOI, true);
176+
ops.splice(ops.length - 1, 0, op); // Add item before last
177+
}
178+
return inititalOperations;
179+
}),
180+
concatMap((op: ItemOperation) => {
181+
if (hasValue(op.featureID)) {
182+
return this.authorizationService.isAuthorized(op.featureID, item.self).pipe(
183+
distinctUntilChanged(),
184+
map((authorized) => {
185+
op.setDisabled(!authorized);
186+
op.setAuthorized(authorized);
187+
return op;
188+
})
189+
);
172190
}
173-
));
174-
}),
175-
// Switch map pushes the register DOI operation onto a copy of the base array then returns to the pipe
176-
switchMap((showDoi: boolean) => {
177-
let ops = [...operations];
178-
if (showDoi) {
179-
ops.push(new ItemOperation('register-doi', this.getCurrentUrl(item) + '/register-doi', FeatureID.CanRegisterDOI, true));
180-
}
181-
return ops;
182-
}),
183-
// Merge map checks and transforms each operation in the array based on whether it is authorized or not (disabled)
184-
mergeMap((op: ItemOperation) => {
185-
if (hasValue(op.featureID)) {
186-
return this.authorizationService.isAuthorized(op.featureID, item.self).pipe(
187-
distinctUntilChanged(),
188-
map((authorized) => new ItemOperation(op.operationKey, op.operationUrl, op.featureID, !authorized, authorized))
189-
);
190-
} else {
191191
return [op];
192-
}
193-
}),
194-
// Wait for all operations to be emitted and return as an array
195-
toArray(),
196-
).subscribe((data) => {
197-
// Update the operations$ subject that draws the administrative buttons on the status page
198-
this.operations$.next(data);
199-
});
200-
});
192+
}),
193+
toArray()
194+
);
195+
196+
let orcidOps$ = of([]);
197+
if (this.orcidAuthService.isLinkedToOrcid(item)) {
198+
orcidOps$ = this.orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid().pipe(
199+
map((canDisconnect) => {
200+
if (canDisconnect) {
201+
return [new ItemOperation('unlinkOrcid', `${currentUrl}/unlink-orcid`)];
202+
}
203+
return [];
204+
})
205+
);
206+
}
207+
208+
return combineLatest([ops$, orcidOps$]);
209+
}),
210+
map(([ops, orcidOps]: [ItemOperation[], ItemOperation[]]) => [...ops, ...orcidOps])
211+
).subscribe((ops) => this.operations$.next(ops));
201212

202213
this.itemPageRoute$ = this.itemRD$.pipe(
203214
getAllSucceededRemoteDataPayload(),
@@ -206,6 +217,7 @@ export class ItemStatusComponent implements OnInit {
206217

207218
}
208219

220+
209221
/**
210222
* Get the current url without query params
211223
* @returns {string} url

0 commit comments

Comments
 (0)