Intermediate commit

This commit is contained in:
Giuseppe Digilio
2020-04-06 20:36:56 +02:00
parent b210a36e53
commit e078071dfb
14 changed files with 383 additions and 151 deletions

View File

@@ -1812,6 +1812,12 @@
"resource-policies.create.page.title": "Create new resource policy",
"resource-policies.delete.btn": "Delete selected resource policies",
"resource-policies.delete.failure.content": "An error occurred while deleting selected resource policies.",
"resource-policies.delete.success.content": "Operation successful",
"resource-policies.edit.page.heading": "Edit resource policy ",
"resource-policies.edit.page.failure.content": "An error occurred while editing the resource policy.",

View File

@@ -1,7 +1,7 @@
<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)?.page; trackById">
<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">

View File

@@ -1,12 +1,15 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable, of as observableOf, Subscription } from 'rxjs';
import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs';
import { catchError, filter, first, flatMap, map, take } from 'rxjs/operators';
import { ResourcePolicyService } from '../../../core/resource-policy/resource-policy.service';
import { PaginatedList } from '../../../core/data/paginated-list';
import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
import {
getFirstSucceededRemoteDataPayload,
getFirstSucceededRemoteDataWithNotEmptyPayload
} from '../../../core/shared/operators';
import { Item } from '../../../core/shared/item.model';
import { followLink } from '../../../shared/utils/follow-link-config.model';
import { LinkService } from '../../../core/cache/builders/link.service';
@@ -15,6 +18,9 @@ import { hasValue, isNotEmpty } from '../../../shared/empty.util';
import { Bitstream } from '../../../core/shared/bitstream.model';
import { FindListOptions } from '../../../core/data/request.models';
/**
* Interface for a bundle's bitstream map entry
*/
interface BundleBitstreamsMapEntry {
id: string;
bitstreams: Observable<PaginatedList<Bitstream>>
@@ -35,7 +41,7 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy {
* The list of bundle for the item
* @type {Observable<PaginatedList<Bundle>>}
*/
private bundles$: Observable<PaginatedList<Bundle>>;
private bundles$: BehaviorSubject<Bundle[]> = new BehaviorSubject<Bundle[]>([]);
/**
* The target editing item
@@ -69,22 +75,28 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy {
ngOnInit(): void {
this.item$ = this.route.data.pipe(
map((data) => data.item),
getFirstSucceededRemoteDataPayload(),
getFirstSucceededRemoteDataWithNotEmptyPayload(),
map((item: Item) => this.linkService.resolveLink(
item,
followLink('bundles', new FindListOptions(), true, followLink('bitstreams'))
))
) as Observable<Item>;
this.bundles$ = this.item$.pipe(
const bundles$: Observable<PaginatedList<Bundle>> = this.item$.pipe(
filter((item: Item) => isNotEmpty(item.bundles)),
flatMap((item: Item) => item.bundles),
getFirstSucceededRemoteDataPayload(),
getFirstSucceededRemoteDataWithNotEmptyPayload(),
catchError(() => observableOf(new PaginatedList(null, [])))
);
this.subs.push(
this.bundles$.pipe(
bundles$.pipe(
take(1),
map((list: PaginatedList<Bundle>) => list.page)
).subscribe((bundles: Bundle[]) => {
this.bundles$.next(bundles);
}),
bundles$.pipe(
take(1),
flatMap((list: PaginatedList<Bundle>) => list.page),
map((bundle: Bundle) => ({ id: bundle.id, bitstreams: this.getBundleBitstreams(bundle) }))
@@ -109,8 +121,8 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy {
*
* @return an observable that emits all item's bundles
*/
getItemBundles(): Observable<PaginatedList<Bundle>> {
return this.bundles$
getItemBundles(): Observable<Bundle[]> {
return this.bundles$.asObservable();
}
/**

View File

@@ -7,6 +7,7 @@ import { Item } from '../core/shared/item.model';
import { hasValue } from '../shared/empty.util';
import { find } from 'rxjs/operators';
import { followLink } from '../shared/utils/follow-link-config.model';
import { FindListOptions } from '../core/data/request.models';
/**
* This class represents a resolver that requests a specific item before the route is activated
@@ -26,7 +27,7 @@ export class ItemPageResolver implements Resolve<RemoteData<Item>> {
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Item>> {
return this.itemService.findById(route.params.id,
followLink('owningCollection'),
followLink('bundles'),
followLink('bundles', new FindListOptions(), true, followLink('bitstreams')),
followLink('relationships'),
followLink('version', undefined, true, followLink('versionhistory')),
).pipe(

View File

@@ -1,6 +1,7 @@
<div class="container">
<h4 class="mb-3">{{'resource-policies.edit.page.heading' | translate}} {{targetResourceName}}</h4>
<h4 class="mb-3">{{'resource-policies.create.page.heading' | translate}} {{targetResourceName}}</h4>
<ds-resource-policy-form (reset)="redirectToAuthorizationsPage()"
<ds-resource-policy-form [isProcessing]="isProcessing()"
(reset)="redirectToAuthorizationsPage()"
(submit)="createResourcePolicy($event)"></ds-resource-policy-form>
</div>

View File

@@ -1,7 +1,9 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { BehaviorSubject, Observable } from 'rxjs';
import { first, map, take } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { ResourcePolicyService } from '../../../core/resource-policy/resource-policy.service';
@@ -18,19 +20,29 @@ import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
})
export class ResourcePolicyCreateComponent implements OnInit {
/**
* The name of the resource target of the policy
*/
public targetResourceName: string;
/**
* A boolean representing if a submission creation operation is pending
* @type {BehaviorSubject<boolean>}
*/
private processing$ = new BehaviorSubject<boolean>(false);
/**
* The uuid of the resource target of the policy
*/
private targetResourceUUID: string;
public targetResourceName: string;
constructor(
private dsoNameService: DSONameService,
private notificationsService: NotificationsService,
private resourcePolicyService: ResourcePolicyService,
private route: ActivatedRoute,
private router: Router) {
private router: Router,
private translate: TranslateService) {
}
ngOnInit(): void {
@@ -43,11 +55,16 @@ export class ResourcePolicyCreateComponent implements OnInit {
});
}
redirectToAuthorizationsPage() {
isProcessing(): Observable<boolean> {
return this.processing$.asObservable();
}
redirectToAuthorizationsPage(): void {
this.router.navigate([`../../${ITEM_EDIT_AUTHORIZATIONS_PATH}`], { relativeTo: this.route });
}
createResourcePolicy(event: ResourcePolicyEvent) {
createResourcePolicy(event: ResourcePolicyEvent): void {
this.processing$.next(true);
let response$;
if (event.target.type === 'eperson') {
response$ = this.resourcePolicyService.create(event.object, this.targetResourceUUID, event.target.uuid);
@@ -57,11 +74,12 @@ export class ResourcePolicyCreateComponent implements OnInit {
response$.pipe(
first((response: RemoteData<ResourcePolicy>) => !response.isResponsePending)
).subscribe((responseRD: RemoteData<ResourcePolicy>) => {
this.processing$.next(false);
if (responseRD.hasSucceeded) {
this.notificationsService.success(null, 'resource-policies.create.page.success.content');
this.notificationsService.success(null, this.translate.get('resource-policies.create.page.success.content'));
this.redirectToAuthorizationsPage();
} else {
this.notificationsService.error(null, 'resource-policies.create.page.failure.content');
this.notificationsService.success(null, this.translate.get('resource-policies.create.page.failure.content'));
}
})
}

View File

@@ -2,6 +2,7 @@
<h4 class="mb-3">{{'resource-policies.edit.page.heading' | translate}} {{resourcePolicy.id}}</h4>
<ds-resource-policy-form [resourcePolicy]="resourcePolicy"
[isProcessing]="isProcessing()"
(reset)="redirectToAuthorizationsPage()"
(submit)="updateResourcePolicy($event)"></ds-resource-policy-form>
</div>

View File

@@ -1,7 +1,9 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { BehaviorSubject, Observable } from 'rxjs';
import { first, map, take } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core';
import { ResourcePolicyService } from '../../../core/resource-policy/resource-policy.service';
import { NotificationsService } from '../../notifications/notifications.service';
@@ -9,6 +11,7 @@ import { RemoteData } from '../../../core/data/remote-data';
import { ResourcePolicy } from '../../../core/resource-policy/models/resource-policy.model';
import { ResourcePolicyEvent } from '../form/resource-policy-form';
import { ITEM_EDIT_AUTHORIZATIONS_PATH } from '../../../+item-page/edit-item-page/edit-item-page.routing.module';
import { RESOURCE_POLICY } from '../../../core/resource-policy/models/resource-policy.resource-type';
@Component({
selector: 'ds-resource-policy-edit',
@@ -21,11 +24,18 @@ export class ResourcePolicyEditComponent implements OnInit {
*/
public resourcePolicy: ResourcePolicy;
/**
* A boolean representing if a submission editing operation is pending
* @type {BehaviorSubject<boolean>}
*/
private processing$ = new BehaviorSubject<boolean>(false);
constructor(
private notificationsService: NotificationsService,
private resourcePolicyService: ResourcePolicyService,
private route: ActivatedRoute,
private router: Router) {
private router: Router,
private translate: TranslateService) {
}
ngOnInit(): void {
@@ -34,26 +44,33 @@ export class ResourcePolicyEditComponent implements OnInit {
take(1)
).subscribe((data: any) => {
this.resourcePolicy = (data.resourcePolicy as RemoteData<ResourcePolicy>).payload;
console.log(data)
});
}
isProcessing(): Observable<boolean> {
return this.processing$.asObservable();
}
redirectToAuthorizationsPage() {
this.router.navigate([`../../${ITEM_EDIT_AUTHORIZATIONS_PATH}`], { relativeTo: this.route });
}
updateResourcePolicy(event: ResourcePolicyEvent) {
this.processing$.next(true);
const updatedObject = Object.assign({}, event.object, {
id: this.resourcePolicy.id,
type: RESOURCE_POLICY.value,
_links: this.resourcePolicy._links
});
this.resourcePolicyService.update(updatedObject).pipe(
first((response: RemoteData<ResourcePolicy>) => !response.isResponsePending)
).subscribe((responseRD: RemoteData<ResourcePolicy>) => {
this.processing$.next(false);
if (responseRD.hasSucceeded) {
this.notificationsService.success(null, 'resource-policies.edit.page.success.content');
this.notificationsService.success(null, this.translate.get('resource-policies.edit.page.success.content'));
this.redirectToAuthorizationsPage();
} else {
this.notificationsService.error(null, 'resource-policies.edit.page.failure.content');
this.notificationsService.error(null, this.translate.get('resource-policies.edit.page.failure.content'));
}
})
}

View File

@@ -151,6 +151,7 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy {
* Unsubscribe from all subscriptions
*/
ngOnDestroy(): void {
this.list$ = null;
this.subs
.filter((subscription) => hasValue(subscription))
.forEach((subscription) => subscription.unsubscribe())

View File

@@ -27,11 +27,19 @@
<div class="col text-right">
<button type="reset"
class="btn btn-default"
[disabled]="(isProcessing | async)"
(click)="onReset()">{{'form.cancel' | translate}}</button>
<button type="button"
class="btn btn-primary"
[disabled]="!(isFormValid() | async)"
(click)="onSubmit()">{{'form.submit' | translate}}</button>
[disabled]="!(isFormValid() | async) || (isProcessing | async)"
(click)="onSubmit()">
<span *ngIf="(isProcessing | async)">
<i class='fas fa-circle-notch fa-spin'></i> {{'submission.workflow.tasks.generic.processing' | translate}}
</span>
<span *ngIf="!(isProcessing | async)">
{{'form.submit' | translate}}
</span>
</button>
</div>
</div>
</div>

View File

@@ -1,5 +1,5 @@
import {
DynamicDateControlModelConfig,
DynamicDatePickerModelConfig,
DynamicFormControlLayout,
DynamicFormGroupModelConfig,
DynamicFormOptionConfig,
@@ -118,9 +118,12 @@ export const RESOURCE_POLICY_FORM_DATE_GROUP_LAYOUT: DynamicFormControlLayout =
}
};
export const RESOURCE_POLICY_FORM_START_DATE_CONFIG: DynamicDateControlModelConfig = {
export const RESOURCE_POLICY_FORM_START_DATE_CONFIG: DynamicDatePickerModelConfig = {
id: 'start',
label: 'resource-policies.form.date.start.label',
placeholder: 'resource-policies.form.date.start.label',
inline: false,
toggleIcon: 'far fa-calendar-alt'
};
export const RESOURCE_POLICY_FORM_START_DATE_LAYOUT: DynamicFormControlLayout = {
@@ -133,9 +136,12 @@ export const RESOURCE_POLICY_FORM_START_DATE_LAYOUT: DynamicFormControlLayout =
}
};
export const RESOURCE_POLICY_FORM_END_DATE_CONFIG: DynamicDateControlModelConfig = {
export const RESOURCE_POLICY_FORM_END_DATE_CONFIG: DynamicDatePickerModelConfig = {
id: 'end',
label: 'resource-policies.form.date.end.label'
label: 'resource-policies.form.date.end.label',
placeholder: 'resource-policies.form.date.end.label',
inline: false,
toggleIcon: 'far fa-calendar-alt'
};
export const RESOURCE_POLICY_FORM_END_DATE_LAYOUT: DynamicFormControlLayout = {
element: {

View File

@@ -1,8 +1,13 @@
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { DynamicFormControlModel, DynamicFormGroupModel, DynamicSelectModel } from '@ng-dynamic-forms/core';
import { Observable, of as observableOf, race as observableRace } from 'rxjs';
import { filter, map, take } from 'rxjs/operators';
import {
DynamicDatePickerModel,
DynamicFormControlModel,
DynamicFormGroupModel,
DynamicSelectModel
} from '@ng-dynamic-forms/core';
import { ResourcePolicy } from '../../../core/resource-policy/models/resource-policy.model';
import { DsDynamicInputModel } from '../../form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model';
@@ -19,7 +24,6 @@ import {
RESOURCE_POLICY_FORM_START_DATE_LAYOUT
} from './resource-policy-form.model';
import { DsDynamicTextAreaModel } from '../../form/builder/ds-dynamic-form-ui/models/ds-dynamic-textarea.model';
import { DynamicDsDatePickerModel } from '../../form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.model';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
import { hasValue, isEmpty, isNotEmpty } from '../../empty.util';
@@ -27,6 +31,11 @@ import { FormService } from '../../form/form.service';
import { RESOURCE_POLICY } from '../../../core/resource-policy/models/resource-policy.resource-type';
import { RemoteData } from '../../../core/data/remote-data';
import { Subscription } from 'rxjs/internal/Subscription';
import { dateToISOFormat, stringToNgbDateStruct } from '../../date.util';
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
import { GroupDataService } from '../../../core/eperson/group-data.service';
import { getSucceededRemoteData } from '../../../core/shared/operators';
import { RequestService } from '../../../core/data/request.service';
export interface ResourcePolicyEvent {
object: ResourcePolicy,
@@ -51,6 +60,12 @@ export class ResourcePolicyFormComponent implements OnInit, OnDestroy {
*/
@Input() resourcePolicy: ResourcePolicy;
/**
* A boolean representing if form submit operation is processing
* @type {boolean}
*/
@Input() isProcessing: Observable<boolean> = observableOf(false);
/**
* An event fired when form is canceled.
* Event's payload is empty.
@@ -87,6 +102,12 @@ export class ResourcePolicyFormComponent implements OnInit, OnDestroy {
*/
public resourcePolicyGrantType: string;
/**
* A boolean representing if component is active
* @type {boolean}
*/
private isActive: boolean;
/**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
@@ -97,11 +118,17 @@ export class ResourcePolicyFormComponent implements OnInit, OnDestroy {
* Initialize instance variables
*
* @param {DSONameService} dsoNameService
* @param {EPersonDataService} ePersonService
* @param {FormService} formService
* @param {GroupDataService} groupService
* @param {RequestService} requestService
*/
constructor(
private dsoNameService: DSONameService,
private ePersonService: EPersonDataService,
private formService: FormService,
private groupService: GroupDataService,
private requestService: RequestService,
) {
}
@@ -109,13 +136,24 @@ export class ResourcePolicyFormComponent implements OnInit, OnDestroy {
* Initialize the component, setting up the form model
*/
ngOnInit(): void {
this.isActive = true;
this.formId = this.formService.getUniqueId('resource-policy-form');
this.formModel = this.buildResourcePolicyForm();
if (!this.canSetGrant()) {
this.subs.push(observableCombineLatest([this.resourcePolicy.eperson, this.resourcePolicy.group])
.subscribe(([epersonRD, groupRD]: [RemoteData<DSpaceObject>, RemoteData<DSpaceObject>]) => {
this.resourcePolicyGrant = epersonRD.payload || groupRD.payload;
this.requestService.removeByHrefSubstring(this.resourcePolicy._links.eperson.href);
this.requestService.removeByHrefSubstring(this.resourcePolicy._links.group.href);
const epersonRD$ = this.ePersonService.findByHref(this.resourcePolicy._links.eperson.href).pipe(
getSucceededRemoteData()
);
const groupRD$ = this.groupService.findByHref(this.resourcePolicy._links.group.href).pipe(
getSucceededRemoteData()
);
this.subs.push(
observableRace(epersonRD$, groupRD$).pipe(
filter(() => this.isActive),
).subscribe((dsoRD: RemoteData<DSpaceObject>) => {
this.resourcePolicyGrant = dsoRD.payload;
})
)
}
@@ -146,15 +184,15 @@ export class ResourcePolicyFormComponent implements OnInit, OnDestroy {
new DynamicSelectModel(RESOURCE_POLICY_FORM_ACTION_TYPE_CONFIG)
);
const startDateModel = new DynamicDsDatePickerModel(
const startDateModel = new DynamicDatePickerModel(
RESOURCE_POLICY_FORM_START_DATE_CONFIG,
RESOURCE_POLICY_FORM_START_DATE_LAYOUT
);
const endDateModel = new DynamicDsDatePickerModel(
const endDateModel = new DynamicDatePickerModel(
RESOURCE_POLICY_FORM_END_DATE_CONFIG,
RESOURCE_POLICY_FORM_END_DATE_LAYOUT
);
const dateGroupConfig = Object.assign({}, RESOURCE_POLICY_FORM_DATE_GROUP_CONFIG);
const dateGroupConfig = Object.assign({}, RESOURCE_POLICY_FORM_DATE_GROUP_CONFIG, { group: [] });
dateGroupConfig.group.push(startDateModel, endDateModel);
formModel.push(new DynamicFormGroupModel(dateGroupConfig, RESOURCE_POLICY_FORM_DATE_GROUP_LAYOUT));
@@ -172,10 +210,10 @@ export class ResourcePolicyFormComponent implements OnInit, OnDestroy {
formModel.forEach((model: any) => {
if (model.id === 'date') {
if (hasValue(this.resourcePolicy.startDate)) {
model.get(0).valueUpdates.next(this.resourcePolicy.startDate);
model.get(0).valueUpdates.next(stringToNgbDateStruct(this.resourcePolicy.startDate));
}
if (hasValue(this.resourcePolicy.endDate)) {
model.get(1).valueUpdates.next(this.resourcePolicy.startDate);
model.get(1).valueUpdates.next(stringToNgbDateStruct(this.resourcePolicy.endDate));
}
} else {
if (this.resourcePolicy.hasOwnProperty(model.id) && this.resourcePolicy[model.id]) {
@@ -203,7 +241,6 @@ export class ResourcePolicyFormComponent implements OnInit, OnDestroy {
* @return the object name
*/
getResourcePolicyTargetName(): string {
console.log(this.resourcePolicy);
return isNotEmpty(this.resourcePolicyGrant) ? this.dsoNameService.getName(this.resourcePolicyGrant) : '';
}
@@ -228,11 +265,10 @@ export class ResourcePolicyFormComponent implements OnInit, OnDestroy {
* Emit a new submit Event whether the form is valid
*/
onSubmit(): void {
this.formService.getFormData(this.formId)
this.formService.getFormData(this.formId).pipe(take(1))
.subscribe((data) => {
const eventPayload: ResourcePolicyEvent = Object.create({});
eventPayload.object = this.createResourcePolicyByFormData(data);
console.log('resourcePolicyTarget', this.resourcePolicyGrant.type.value);
eventPayload.target = {
type: this.resourcePolicyGrantType,
uuid: this.resourcePolicyGrant.id
@@ -252,8 +288,8 @@ export class ResourcePolicyFormComponent implements OnInit, OnDestroy {
resourcePolicy.description = (data.description) ? data.description[0].value : null;
resourcePolicy.policyType = (data.policyType) ? data.policyType[0].value : null;
resourcePolicy.action = (data.action) ? data.action[0].value : null;
resourcePolicy.startDate = (data.date && data.date.start) ? data.date.start[0].value : null;
resourcePolicy.endDate = (data.date && data.date.end) ? data.date.end[0].value : null;
resourcePolicy.startDate = (data.date && data.date.start) ? dateToISOFormat(data.date.start[0].value) : null;
resourcePolicy.endDate = (data.date && data.date.end) ? dateToISOFormat(data.date.end[0].value) : null;
resourcePolicy.type = RESOURCE_POLICY;
return resourcePolicy;
@@ -263,6 +299,8 @@ export class ResourcePolicyFormComponent implements OnInit, OnDestroy {
* Unsubscribe from all subscriptions
*/
ngOnDestroy(): void {
this.isActive = false;
this.formModel = null;
this.subs
.filter((subscription) => hasValue(subscription))
.forEach((subscription) => subscription.unsubscribe())

View File

@@ -1,17 +1,40 @@
<div *ngIf="(getResourcePolicies() | async)?.hasSucceeded">
<div *ngIf="(getResourcePolicies() | async)?.length > 0">
<table class="table table-striped table-bordered table-hover">
<thead>
<tr>
<th colspan="7">
<p class="d-flex justify-content-between align-items-center m-0">
<th colspan="9">
<div class="d-flex justify-content-between align-items-center m-0">
{{ 'resource-policies.table.headers.title.for.' + resourceType | translate }} {{resourceUUID}}
<button class="btn btn-outline-primary float-right" (click)="createResourcePolicy()">
{{'resource-policies.add.for.' + resourceType | translate}}
</button>
</p>
<div>
<button class="btn btn-outline-primary float-right ml-1"
[disabled]="(!(canDelete() | async)) || (isProcessingDelete() | async)"
(click)="deleteSelectedResourcePolicies()">
<span *ngIf="(isProcessingDelete() | async)">
<i class='fas fa-circle-notch fa-spin'></i> {{'submission.workflow.tasks.generic.processing' | translate}}
</span>
<span *ngIf="!(isProcessingDelete() | async)">
{{'resource-policies.delete.btn' | translate}}
</span>
</button>
<button class="btn btn-outline-primary float-right"
[disabled]="(isProcessingDelete() | async)"
(click)="redirectToResourcePolicyCreatePage()">
{{'resource-policies.add.for.' + resourceType | translate}}
</button>
</div>
</div>
</th>
</tr>
<tr class="text-center">
<th>
<div class="custom-control custom-checkbox">
<input type="checkbox"
class="custom-control-input"
[id]="'selectAll_' + resourceUUID"
(change)="selectAllCheckbox($event)">
<label class="custom-control-label" [for]="'selectAll_' + resourceUUID"></label>
</div>
</th>
<th>{{'resource-policies.table.headers.id' | translate}}</th>
<th>{{'resource-policies.table.headers.name' | translate}}</th>
<th>{{'resource-policies.table.headers.policyType' | translate}}</th>
@@ -23,29 +46,39 @@
</tr>
</thead>
<tbody>
<tr *ngFor="let policy of (getResourcePolicies() | async)?.payload?.page; trackById">
<tr *ngFor="let entry of (getResourcePolicies() | async); trackById">
<td class="text-center">
<div class="custom-control custom-checkbox">
<input type="checkbox"
class="custom-control-input"
[id]="entry.id"
[ngModel]="entry.checked"
(ngModelChange)="selectCheckbox(entry, $event)">
<label class="custom-control-label" [for]="entry.id"></label>
</div>
</td>
<th scope="row">
{{policy.id}}
<button class="btn btn-link pt-0 pb-0" (click)="editResourcePolicy(policy)">
{{entry.id}}
<button class="btn btn-link pt-0 pb-0" (click)="redirectToResourcePolicyEditPage(entry.policy)">
{{'resource-policies.table.headers.edit' | translate}}
</button>
</th>
<td>{{policy.name}}</td>
<td>{{policy.policyType}}</td>
<td>{{policy.action}}</td>
<td *ngIf="(hasEPerson(policy) | async)">
{{getEPersonName(policy) | async}}
<td>{{entry.policy.name}}</td>
<td>{{entry.policy.policyType}}</td>
<td>{{entry.policy.action}}</td>
<td *ngIf="(hasEPerson(entry.policy) | async)">
{{getEPersonName(entry.policy) | async}}
</td>
<td *ngIf="!(hasEPerson(policy) | async)"></td>
<td *ngIf="(hasGroup(policy) | async)">
{{getGroupName(policy) | async}}
<button class="btn btn-link pt-0 pb-0" (click)="redirectToGroupEditPage(policy)">
<td *ngIf="!(hasEPerson(entry.policy) | async)"></td>
<td *ngIf="(hasGroup(entry.policy) | async)">
{{getGroupName(entry.policy) | async}}
<button class="btn btn-link pt-0 pb-0" (click)="redirectToGroupEditPage(entry.policy)">
{{'resource-policies.table.headers.edit' | translate}}
</button>
</td>
<td *ngIf="!(hasGroup(policy) | async)"></td>
<td>{{policy.startDate}}</td>
<td>{{policy.endDate}}</td>
<td *ngIf="!(hasGroup(entry.policy) | async)"></td>
<td>{{formatDate(entry.policy.startDate)}}</td>
<td>{{formatDate(entry.policy.endDate)}}</td>
</tr>
</tbody>
</table>

View File

@@ -1,26 +1,32 @@
import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { filter, map, startWith, take } from 'rxjs/operators';
import { BehaviorSubject, from as observableFrom, Observable, Subscription } from 'rxjs';
import { concatMap, distinctUntilChanged, filter, map, reduce, scan, startWith, take } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core';
import { ResourcePolicyService } from '../../core/resource-policy/resource-policy.service';
import { PaginatedList } from '../../core/data/paginated-list';
import {
getFirstSucceededRemoteDataPayload,
getFirstSucceededRemoteDataWithNotEmptyPayload,
getSucceededRemoteData
} from '../../core/shared/operators';
import { RemoteData } from '../../core/data/remote-data';
import { ResourcePolicy } from '../../core/resource-policy/models/resource-policy.model';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { Group } from '../../core/eperson/models/group.model';
import { GroupDataService } from '../../core/eperson/group-data.service';
import { hasValue, isNotEmpty } from '../empty.util';
import { hasValue, isEmpty, isNotEmpty } from '../empty.util';
import { EPerson } from '../../core/eperson/models/eperson.model';
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
import { followLink } from '../utils/follow-link-config.model';
import { RequestService } from '../../core/data/request.service';
import { NotificationsService } from '../notifications/notifications.service';
import { dateToString, stringToNgbDateStruct } from '../date.util';
interface ResourcePolicyCheckboxEntry {
id: string;
policy: ResourcePolicy;
checked: boolean
}
@Component({
selector: 'ds-resource-policies',
@@ -51,11 +57,17 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy {
private isActive: boolean;
/**
* The list of policies for given resource
* @type {Observable<RemoteData<PaginatedList<ResourcePolicy>>>}
* A boolean representing if a submission delete operation is pending
* @type {BehaviorSubject<boolean>}
*/
private resourcePolicies$: BehaviorSubject<RemoteData<PaginatedList<ResourcePolicy>>> =
new BehaviorSubject<RemoteData<PaginatedList<ResourcePolicy>>>({} as any);
private processingDelete$ = new BehaviorSubject<boolean>(false);
/**
* The list of policies for given resource
* @type {BehaviorSubject<ResourcePolicyCheckboxEntry[]>}
*/
private resourcePoliciesEntries$: BehaviorSubject<ResourcePolicyCheckboxEntry[]> =
new BehaviorSubject<ResourcePolicyCheckboxEntry[]>([]);
/**
* Array to track all subscriptions and unsubscribe them onDestroy
@@ -70,20 +82,24 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy {
* @param {DSONameService} dsoNameService
* @param {EPersonDataService} ePersonService
* @param {GroupDataService} groupService
* @param {NotificationsService} notificationsService
* @param {RequestService} requestService
* @param {ResourcePolicyService} resourcePolicyService
* @param {ActivatedRoute} route
* @param {Router} router
* @param {TranslateService} translate
*/
constructor(
private cdr: ChangeDetectorRef,
private dsoNameService: DSONameService,
private ePersonService: EPersonDataService,
private groupService: GroupDataService,
private notificationsService: NotificationsService,
private requestService: RequestService,
private resourcePolicyService: ResourcePolicyService,
private route: ActivatedRoute,
private router: Router
private router: Router,
private translate: TranslateService
) {
}
@@ -92,22 +108,144 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy {
*/
ngOnInit(): void {
this.isActive = true;
this.initResourcePolicyLIst();
}
canDelete(): Observable<boolean> {
return observableFrom(this.resourcePoliciesEntries$.value).pipe(
filter((entry: ResourcePolicyCheckboxEntry) => entry.checked),
reduce((acc: any, value: any) => [...acc, ...value], []),
map((entries: ResourcePolicyCheckboxEntry[]) => isNotEmpty(entries)),
distinctUntilChanged()
)
}
deleteSelectedResourcePolicies(): void {
this.processingDelete$.next(true);
const policiesToDelete: ResourcePolicyCheckboxEntry[] = this.resourcePoliciesEntries$.value
.filter((entry: ResourcePolicyCheckboxEntry) => entry.checked);
observableFrom(policiesToDelete).pipe(
concatMap((entry: ResourcePolicyCheckboxEntry) => this.resourcePolicyService.delete(entry.policy.id)),
scan((acc: any, value: any) => [...acc, ...value], []),
filter((results: boolean[]) => results.length === policiesToDelete.length),
take(1),
).subscribe((results: boolean[]) => {
const failureResults = results.filter((result: boolean) => !result);
if (isEmpty(failureResults)) {
this.notificationsService.success(null, this.translate.get('resource-policies.delete.success.content'));
} else {
this.notificationsService.error(null, this.translate.get('resource-policies.delete.failure.content'));
}
this.initResourcePolicyLIst();
this.processingDelete$.next(false);
})
}
/**
* Returns a date in simplified format (YYYY-MM-DD).
*
* @param date
* @return a string with formatted date
*/
formatDate(date: string): string {
return isNotEmpty(date) ? dateToString(stringToNgbDateStruct(date)) : '';
}
/**
* Return the ePerson's name which the given policy is linked to
*
* @param policy The resource policy
*/
getEPersonName(policy: ResourcePolicy): Observable<string> {
// TODO to be reviewed when https://github.com/DSpace/dspace-angular/issues/644 will be resolved
return this.ePersonService.findByHref(policy._links.eperson.href).pipe(
filter(() => this.isActive),
getFirstSucceededRemoteDataWithNotEmptyPayload(),
map((eperson: EPerson) => this.dsoNameService.getName(eperson)),
startWith('')
)
}
/**
* Return the group's name which the given policy is linked to
*
* @param policy The resource policy
*/
getGroupName(policy: ResourcePolicy): Observable<string> {
// TODO to be reviewed when https://github.com/DSpace/dspace-angular/issues/644 will be resolved
return this.groupService.findByHref(policy._links.group.href).pipe(
filter(() => this.isActive),
getFirstSucceededRemoteDataWithNotEmptyPayload(),
map((group: Group) => this.dsoNameService.getName(group)),
startWith('')
)
}
/**
* Return all resource's policies
*
* @return an observable that emits all resource's policies
*/
getResourcePolicies(): Observable<ResourcePolicyCheckboxEntry[]> {
return this.resourcePoliciesEntries$.asObservable();
}
/**
* Check whether the given policy is linked to a ePerson
*
* @param policy The resource policy
* @return an observable that emits true when the policy is linked to a ePerson, false otherwise
*/
hasEPerson(policy): Observable<boolean> {
// TODO to be reviewed when https://github.com/DSpace/dspace-angular/issues/644 will be resolved
return this.ePersonService.findByHref(policy._links.eperson.href).pipe(
filter(() => this.isActive),
getFirstSucceededRemoteDataPayload(),
map((eperson: EPerson) => isNotEmpty(eperson))
)
}
/**
* Check whether the given policy is linked to a group
*
* @param policy The resource policy
* @return an observable that emits true when the policy is linked to a group, false otherwise
*/
hasGroup(policy): Observable<boolean> {
// TODO to be reviewed when https://github.com/DSpace/dspace-angular/issues/644 will be resolved
return this.groupService.findByHref(policy._links.group.href).pipe(
filter(() => this.isActive),
getFirstSucceededRemoteDataPayload(),
map((group: Group) => isNotEmpty(group))
)
}
initResourcePolicyLIst() {
// TODO to be reviewed when https://github.com/DSpace/dspace-angular/issues/644 will be resolved
this.requestService.removeByHrefSubstring(this.resourceUUID);
this.resourcePolicyService.searchByResource(this.resourceUUID, null,
followLink('eperson'), followLink('group')).pipe(
this.resourcePolicyService.searchByResource(this.resourceUUID).pipe(
filter(() => this.isActive),
getSucceededRemoteData(),
take(1)
).subscribe((result) => {
this.resourcePolicies$.next(result);
const entries = result.payload.page.map((policy: ResourcePolicy) => ({
id: policy.id,
policy: policy,
checked: false
}));
this.resourcePoliciesEntries$.next(entries);
this.cdr.detectChanges();
});
}
isProcessingDelete(): Observable<boolean> {
return this.processingDelete$.asObservable();
}
/**
* Redirect to resource policy creation page
*/
createResourcePolicy(): void {
redirectToResourcePolicyCreatePage(): void {
this.router.navigate([`../create`], {
relativeTo: this.route,
queryParams: {
@@ -122,7 +260,7 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy {
*
* @param policy The resource policy
*/
editResourcePolicy(policy: ResourcePolicy): void {
redirectToResourcePolicyEditPage(policy: ResourcePolicy): void {
this.router.navigate([`../edit`], {
relativeTo: this.route,
queryParams: {
@@ -131,79 +269,15 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy {
})
}
/**
* Return the ePerson's name which the given policy is linked to
*
* @param policy The resource policy
*/
getEPersonName(policy: ResourcePolicy): Observable<string> {
return policy.eperson.pipe(
filter(() => this.isActive),
getFirstSucceededRemoteDataWithNotEmptyPayload(),
map((eperson: EPerson) => this.dsoNameService.getName(eperson)),
startWith('')
)
}
/**
* Return the group's name which the given policy is linked to
*
* @param policy The resource policy
*/
getGroupName(policy: ResourcePolicy): Observable<string> {
return policy.group.pipe(
filter(() => this.isActive),
getFirstSucceededRemoteDataWithNotEmptyPayload(),
map((group: Group) => this.dsoNameService.getName(group)),
startWith('')
)
}
/**
* Return all resource's policies
*
* @return an observable that emits all resource's policies
*/
getResourcePolicies(): Observable<RemoteData<PaginatedList<ResourcePolicy>>> {
return this.resourcePolicies$.asObservable();
}
/**
* Check whether the given policy is linked to a ePerson
*
* @param policy The resource policy
* @return an observable that emits true when the policy is linked to a ePerson, false otherwise
*/
hasEPerson(policy): Observable<boolean> {
return policy.eperson.pipe(
filter(() => this.isActive),
getFirstSucceededRemoteDataPayload(),
map((eperson: EPerson) => isNotEmpty(eperson))
)
}
/**
* Check whether the given policy is linked to a group
*
* @param policy The resource policy
* @return an observable that emits true when the policy is linked to a group, false otherwise
*/
hasGroup(policy): Observable<boolean> {
return policy.group.pipe(
filter(() => this.isActive),
getFirstSucceededRemoteDataPayload(),
map((group: Group) => isNotEmpty(group))
)
}
/**
* Redirect to group edit page
*
* @param policy The resource policy
*/
redirectToGroupEditPage(policy: ResourcePolicy): void {
this.requestService.removeByHrefSubstring(policy._links.group.href);
this.subs.push(
policy.group.pipe(
this.groupService.findByHref(policy._links.group.href).pipe(
filter(() => this.isActive),
getFirstSucceededRemoteDataPayload(),
map((group: Group) => group.id)
@@ -211,6 +285,21 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy {
)
}
/**
* Select/unselect all checkbox in the list
*/
selectAllCheckbox(event: any): void {
const checked = event.target.checked;
this.resourcePoliciesEntries$.value.forEach((entry: ResourcePolicyCheckboxEntry) => entry.checked = checked);
}
/**
* Select/unselect checkbox
*/
selectCheckbox(policyEntry: ResourcePolicyCheckboxEntry, checked: boolean) {
policyEntry.checked = checked;
}
/**
* Unsubscribe from all subscriptions
*/
@@ -220,4 +309,5 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy {
.filter((subscription) => hasValue(subscription))
.forEach((subscription) => subscription.unsubscribe())
}
}