Merge pull request #1664 from 4Science/CST-5677

[CST-5677] Improve item authorization page
This commit is contained in:
Tim Donohue
2022-06-13 09:28:28 -05:00
committed by GitHub
8 changed files with 211 additions and 51 deletions

View File

@@ -1,7 +1,7 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
import { NgbTooltipModule, NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { SharedModule } from '../../shared/shared.module';
import { EditItemPageRoutingModule } from './edit-item-page.routing.module';
@@ -48,7 +48,8 @@ import { ResourcePoliciesModule } from '../../shared/resource-policies/resource-
EditItemPageRoutingModule,
SearchPageModule,
DragDropModule,
ResourcePoliciesModule
ResourcePoliciesModule,
NgbModule
],
declarations: [
EditItemPageComponent,

View File

@@ -1,13 +1,33 @@
<div class="container">
<ds-alert [type]="'alert-info'" [content]="'item.edit.authorizations.heading'"></ds-alert>
<ds-resource-policies [resourceType]="'item'" [resourceUUID]="(getItemUUID() | async)"></ds-resource-policies>
<ng-container *ngFor="let bundle of (getItemBundles() | async); trackById">
<ds-resource-policies [resourceType]="'bundle'"
[resourceUUID]="bundle.id"></ds-resource-policies>
<ng-container *ngFor="let bitstream of (bundleBitstreamsMap.get(bundle.id) | async)?.page; trackById">
<ds-resource-policies [resourceType]="'bitstream'"
[resourceUUID]="bitstream.id"></ds-resource-policies>
<ds-resource-policies [resourceType]="'item'" [resourceName]="(getItemName() | async)"
[resourceUUID]="(getItemUUID() | async)">
</ds-resource-policies>
<ng-container *ngFor="let bundle of (bundles$ | async); trackById">
<ds-resource-policies [resourceType]="'bundle'" [resourceUUID]="bundle.id" [resourceName]="bundle.name">
</ds-resource-policies>
<ng-container *ngIf="(bundleBitstreamsMap.get(bundle.id)?.bitstreams | async)?.length > 0">
<div class="card auth-bitstream-container">
<div class="card-header">
<button type="button" class="btn btn-outline-primary" (click)="collapseArea(bundle.id)"
[attr.aria-expanded]="false" [attr.aria-controls]="bundle.id">
{{ 'collection.edit.item.authorizations.show-bitstreams-button' | translate }} {{bundle.name}}
</button>
</div>
<div class="card-body" [id]="bundle.id" [ngbCollapse]="bundleBitstreamsMap.get(bundle.id).isCollapsed">
<ng-container
*ngFor="let bitstream of (bundleBitstreamsMap.get(bundle.id).bitstreams | async); trackById">
<ds-resource-policies [resourceType]="'bitstream'" [resourceUUID]="bitstream.id"
[resourceName]="bitstream.name"></ds-resource-policies>
</ng-container>
<div class="row justify-content-center" *ngIf="!bundleBitstreamsMap.get(bundle.id).allBitstreamsLoaded">
<button type="button" class="btn btn-link" (click)="onBitstreamsLoad(bundle)">{{ 'collection.edit.item.authorizations.load-more-button' | translate }}</button>
</div>
</div>
</div>
</ng-container>
</ng-container>
<div class="row justify-content-center" *ngIf="!allBundlesLoaded">
<button type="button" class="btn btn-link" (click)="onBundleLoad()">{{ 'collection.edit.item.authorizations.load-bundle-button' | translate }}</button>
</div>
</div>

View File

@@ -0,0 +1,4 @@
.auth-bitstream-container {
margin-top: -1em;
margin-bottom: 1.5em;
}

View File

@@ -1,11 +1,12 @@
import { Observable } from 'rxjs/internal/Observable';
import { waitForAsync, ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { Component, NO_ERRORS_SCHEMA } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { of as observableOf } from 'rxjs';
import { of as observableOf, of } from 'rxjs';
import { TranslateModule } from '@ngx-translate/core';
import { cold } from 'jasmine-marbles';
import { ItemAuthorizationsComponent } from './item-authorizations.component';
import { ItemAuthorizationsComponent, BitstreamMapValue } from './item-authorizations.component';
import { Bitstream } from '../../../core/shared/bitstream.model';
import { Bundle } from '../../../core/shared/bundle.model';
import { Item } from '../../../core/shared/item.model';
@@ -57,8 +58,6 @@ describe('ItemAuthorizationsComponent test suite', () => {
bitstreams: createSuccessfulRemoteDataObject$(createPaginatedList([bitstream3, bitstream4]))
});
const bundles = [bundle1, bundle2];
const bitstreamList1: PaginatedList<Bitstream> = buildPaginatedList(new PageInfo(), [bitstream1, bitstream2]);
const bitstreamList2: PaginatedList<Bitstream> = buildPaginatedList(new PageInfo(), [bitstream3, bitstream4]);
const item = Object.assign(new Item(), {
uuid: 'item',
@@ -142,13 +141,12 @@ describe('ItemAuthorizationsComponent test suite', () => {
expect(compAsAny.bundleBitstreamsMap.has('bundle1')).toBeTruthy();
expect(compAsAny.bundleBitstreamsMap.has('bundle2')).toBeTruthy();
let bitstreamList = compAsAny.bundleBitstreamsMap.get('bundle1');
expect(bitstreamList).toBeObservable(cold('(a|)', {
a: bitstreamList1
expect(bitstreamList.bitstreams).toBeObservable(cold('(a|)', {
a : [bitstream1, bitstream2]
}));
bitstreamList = compAsAny.bundleBitstreamsMap.get('bundle2');
expect(bitstreamList).toBeObservable(cold('(a|)', {
a: bitstreamList2
expect(bitstreamList.bitstreams).toBeObservable(cold('(a|)', {
a: [bitstream3, bitstream4]
}));
});

View File

@@ -1,3 +1,5 @@
import { isEqual } from 'lodash';
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@@ -6,7 +8,8 @@ import { catchError, filter, first, map, mergeMap, take } from 'rxjs/operators';
import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model';
import {
getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteDataWithNotEmptyPayload,
getFirstSucceededRemoteDataPayload,
getFirstSucceededRemoteDataWithNotEmptyPayload,
} from '../../../core/shared/operators';
import { Item } from '../../../core/shared/item.model';
import { followLink } from '../../../shared/utils/follow-link-config.model';
@@ -25,7 +28,8 @@ interface BundleBitstreamsMapEntry {
@Component({
selector: 'ds-item-authorizations',
templateUrl: './item-authorizations.component.html'
templateUrl: './item-authorizations.component.html',
styleUrls:['./item-authorizations.component.scss']
})
/**
* Component that handles the item Authorizations
@@ -36,13 +40,13 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy {
* A map that contains all bitstream of the item's bundles
* @type {Observable<Map<string, Observable<PaginatedList<Bitstream>>>>}
*/
public bundleBitstreamsMap: Map<string, Observable<PaginatedList<Bitstream>>> = new Map<string, Observable<PaginatedList<Bitstream>>>();
public bundleBitstreamsMap: Map<string, BitstreamMapValue> = new Map<string, BitstreamMapValue>();
/**
* The list of bundle for the item
* The list of all bundles for the item
* @type {Observable<PaginatedList<Bundle>>}
*/
private bundles$: BehaviorSubject<Bundle[]> = new BehaviorSubject<Bundle[]>([]);
bundles$: BehaviorSubject<Bundle[]> = new BehaviorSubject<Bundle[]>([]);
/**
* The target editing item
@@ -56,15 +60,48 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy {
*/
private subs: Subscription[] = [];
/**
* The size of the bundles to be loaded on demand
* @type {number}
*/
bundlesPerPage = 6;
/**
* The number of current page
* @type {number}
*/
bundlesPageSize = 1;
/**
* The flag to show or not the 'Load more' button
* based on the condition if all the bundles are loaded or not
* @type {boolean}
*/
allBundlesLoaded = false;
/**
* Initial size of loaded bitstreams
* The size of incrementation used in bitstream pagination
*/
bitstreamSize = 4;
/**
* The size of the loaded bitstremas at a certain moment
* @private
*/
private bitstreamPageSize = 4;
/**
* Initialize instance variables
*
* @param {LinkService} linkService
* @param {ActivatedRoute} route
* @param nameService
*/
constructor(
private linkService: LinkService,
private route: ActivatedRoute
private route: ActivatedRoute,
private nameService: DSONameService
) {
}
@@ -72,16 +109,53 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy {
* Initialize the component, setting up the bundle and bitstream within the item
*/
ngOnInit(): void {
this.getBundlesPerItem();
}
/**
* Return the item's UUID
*/
getItemUUID(): Observable<string> {
return this.item$.pipe(
map((item: Item) => item.id),
first((UUID: string) => isNotEmpty(UUID))
);
}
/**
* Return the item's name
*/
getItemName(): Observable<string> {
return this.item$.pipe(
map((item: Item) => this.nameService.getName(item))
);
}
/**
* Return all item's bundles
*
* @return an observable that emits all item's bundles
*/
getItemBundles(): Observable<Bundle[]> {
return this.bundles$.asObservable();
}
/**
* Get all bundles per item
* and all the bitstreams per bundle
* @param page number of current page
*/
getBundlesPerItem(page: number = 1) {
this.item$ = this.route.data.pipe(
map((data) => data.dso),
getFirstSucceededRemoteDataWithNotEmptyPayload(),
map((item: Item) => this.linkService.resolveLink(
item,
followLink('bundles', {}, followLink('bitstreams'))
followLink('bundles', {findListOptions: {currentPage : page, elementsPerPage: this.bundlesPerPage}}, followLink('bitstreams'))
))
) as Observable<Item>;
const bundles$: Observable<PaginatedList<Bundle>> = this.item$.pipe(
const bundles$: Observable<PaginatedList<Bundle>> = this.item$.pipe(
filter((item: Item) => isNotEmpty(item.bundles)),
mergeMap((item: Item) => item.bundles),
getFirstSucceededRemoteDataWithNotEmptyPayload(),
@@ -96,37 +170,36 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy {
take(1),
map((list: PaginatedList<Bundle>) => list.page)
).subscribe((bundles: Bundle[]) => {
this.bundles$.next(bundles);
if (isEqual(bundles.length,0) || bundles.length < this.bundlesPerPage) {
this.allBundlesLoaded = true;
}
if (isEqual(page, 1)) {
this.bundles$.next(bundles);
} else {
this.bundles$.next(this.bundles$.getValue().concat(bundles));
}
}),
bundles$.pipe(
take(1),
mergeMap((list: PaginatedList<Bundle>) => list.page),
map((bundle: Bundle) => ({ id: bundle.id, bitstreams: this.getBundleBitstreams(bundle) }))
).subscribe((entry: BundleBitstreamsMapEntry) => {
this.bundleBitstreamsMap.set(entry.id, entry.bitstreams);
let bitstreamMapValues: BitstreamMapValue = {
isCollapsed: true,
allBitstreamsLoaded: false,
bitstreams: null
};
bitstreamMapValues.bitstreams = entry.bitstreams.pipe(
map((b: PaginatedList<Bitstream>) => {
bitstreamMapValues.allBitstreamsLoaded = b?.page.length < this.bitstreamSize;
return [...b.page.slice(0, this.bitstreamSize)];
})
);
this.bundleBitstreamsMap.set(entry.id, bitstreamMapValues);
})
);
}
/**
* Return the item's UUID
*/
getItemUUID(): Observable<string> {
return this.item$.pipe(
map((item: Item) => item.id),
first((UUID: string) => isNotEmpty(UUID))
);
}
/**
* Return all item's bundles
*
* @return an observable that emits all item's bundles
*/
getItemBundles(): Observable<Bundle[]> {
return this.bundles$.asObservable();
}
/**
* Return all bundle's bitstreams
*
@@ -142,6 +215,46 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy {
);
}
/**
* Changes the collapsible state of the area that contains the bitstream list
* @param bundleId Id of bundle responsible for the requested bitstreams
*/
collapseArea(bundleId: string) {
this.bundleBitstreamsMap.get(bundleId).isCollapsed = !this.bundleBitstreamsMap.get(bundleId).isCollapsed;
}
/**
* Loads as much bundles as initial value of bundleSize to be displayed
*/
onBundleLoad(){
this.bundlesPageSize ++;
this.getBundlesPerItem(this.bundlesPageSize);
}
/**
* Calculates the bitstreams that are going to be loaded on demand,
* based on the number configured on this.bitstreamSize.
* @param bundle parent of bitstreams that are requested to be shown
* @returns Subscription
*/
onBitstreamsLoad(bundle: Bundle) {
return this.getBundleBitstreams(bundle).subscribe((res: PaginatedList<Bitstream>) => {
let nextBitstreams = res?.page.slice(this.bitstreamPageSize, this.bitstreamPageSize + this.bitstreamSize);
let bitstreamsToShow = this.bundleBitstreamsMap.get(bundle.id).bitstreams.pipe(
map((existingBits: Bitstream[])=> {
return [... existingBits, ...nextBitstreams];
})
);
this.bitstreamPageSize = this.bitstreamPageSize + this.bitstreamSize;
let bitstreamMapValues: BitstreamMapValue = {
bitstreams: bitstreamsToShow ,
isCollapsed: this.bundleBitstreamsMap.get(bundle.id).isCollapsed,
allBitstreamsLoaded: res?.page.length <= this.bitstreamPageSize
};
this.bundleBitstreamsMap.set(bundle.id, bitstreamMapValues);
});
}
/**
* Unsubscribe from all subscriptions
*/
@@ -151,3 +264,9 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy {
.forEach((subscription) => subscription.unsubscribe());
}
}
export interface BitstreamMapValue {
bitstreams: Observable<Bitstream[]>;
isCollapsed: boolean;
allBitstreamsLoaded: boolean;
}

View File

@@ -4,9 +4,15 @@
<tr>
<th colspan="10">
<div class="d-flex justify-content-between align-items-center m-0">
{{ 'resource-policies.table.headers.title.for.' + resourceType | translate }} {{resourceUUID}}
<span>
{{ 'resource-policies.table.headers.title.for.' + resourceType | translate }}
<span class="text-info"> {{resourceName}} </span>
<ng-container *ngIf="resourceType != 'item'">
({{resourceUUID}})
</ng-container>
</span>
<div class="space-children-mr">
<button class="btn btn-danger"
<button class="btn btn-danger p-1"
[disabled]="(!(canDelete() | async)) || (isProcessingDelete() | async)"
[title]="'resource-policies.delete.btn.title' | translate"
(click)="deleteSelectedResourcePolicies()">
@@ -18,7 +24,7 @@
{{'resource-policies.delete.btn' | translate}}
</span>
</button>
<button class="btn btn-success"
<button class="btn btn-success p-1"
[disabled]="(isProcessingDelete() | async)"
[title]="'resource-policies.add.for.' + resourceType | translate"
(click)="redirectToResourcePolicyCreatePage()">

View File

@@ -62,6 +62,12 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy {
*/
@Input() public resourceType: string;
/**
* The resource name
* @type {string}
*/
@Input() public resourceName: string;
/**
* A boolean representing if component is active
* @type {boolean}

View File

@@ -862,6 +862,12 @@
"collection.edit.tabs.authorizations.title": "Collection Edit - Authorizations",
"collection.edit.item.authorizations.load-bundle-button": "Load more bundles",
"collection.edit.item.authorizations.load-more-button": "Load more",
"collection.edit.item.authorizations.show-bitstreams-button": "Show bitstream policies for bundle",
"collection.edit.tabs.metadata.head": "Edit Metadata",
"collection.edit.tabs.metadata.title": "Collection Edit - Metadata",