diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index c55865f3b8..1077ff9e15 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -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.", diff --git a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.html b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.html index 0cf61579f1..71aa7b44de 100644 --- a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.html +++ b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.html @@ -1,7 +1,7 @@
- + diff --git a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts index e241166a47..3322d4cc36 100644 --- a/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts +++ b/src/app/+item-page/edit-item-page/item-authorizations/item-authorizations.component.ts @@ -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> @@ -35,7 +41,7 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy { * The list of bundle for the item * @type {Observable>} */ - private bundles$: Observable>; + private bundles$: BehaviorSubject = new BehaviorSubject([]); /** * 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; - this.bundles$ = this.item$.pipe( + const bundles$: Observable> = 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) => list.page) + ).subscribe((bundles: Bundle[]) => { + this.bundles$.next(bundles); + }), + bundles$.pipe( take(1), flatMap((list: PaginatedList) => 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> { - return this.bundles$ + getItemBundles(): Observable { + return this.bundles$.asObservable(); } /** diff --git a/src/app/+item-page/item-page.resolver.ts b/src/app/+item-page/item-page.resolver.ts index 501bb34d2c..63a560778b 100644 --- a/src/app/+item-page/item-page.resolver.ts +++ b/src/app/+item-page/item-page.resolver.ts @@ -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> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { 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( diff --git a/src/app/shared/resource-policies/create/resource-policy-create.component.html b/src/app/shared/resource-policies/create/resource-policy-create.component.html index c4eb42bb18..85d0d13e96 100644 --- a/src/app/shared/resource-policies/create/resource-policy-create.component.html +++ b/src/app/shared/resource-policies/create/resource-policy-create.component.html @@ -1,6 +1,7 @@
-

{{'resource-policies.edit.page.heading' | translate}} {{targetResourceName}}

+

{{'resource-policies.create.page.heading' | translate}} {{targetResourceName}}

-
diff --git a/src/app/shared/resource-policies/create/resource-policy-create.component.ts b/src/app/shared/resource-policies/create/resource-policy-create.component.ts index 375966509b..4785e39222 100644 --- a/src/app/shared/resource-policies/create/resource-policy-create.component.ts +++ b/src/app/shared/resource-policies/create/resource-policy-create.component.ts @@ -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} + */ + private processing$ = new BehaviorSubject(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 { + 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) => !response.isResponsePending) ).subscribe((responseRD: RemoteData) => { + 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')); } }) } diff --git a/src/app/shared/resource-policies/edit/resource-policy-edit.component.html b/src/app/shared/resource-policies/edit/resource-policy-edit.component.html index ede5519c74..0f285c4948 100644 --- a/src/app/shared/resource-policies/edit/resource-policy-edit.component.html +++ b/src/app/shared/resource-policies/edit/resource-policy-edit.component.html @@ -2,6 +2,7 @@

{{'resource-policies.edit.page.heading' | translate}} {{resourcePolicy.id}}

diff --git a/src/app/shared/resource-policies/edit/resource-policy-edit.component.ts b/src/app/shared/resource-policies/edit/resource-policy-edit.component.ts index 844ac5b4e4..20f2a5a34e 100644 --- a/src/app/shared/resource-policies/edit/resource-policy-edit.component.ts +++ b/src/app/shared/resource-policies/edit/resource-policy-edit.component.ts @@ -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} + */ + private processing$ = new BehaviorSubject(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).payload; - console.log(data) }); } + isProcessing(): Observable { + 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) => !response.isResponsePending) ).subscribe((responseRD: RemoteData) => { + 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')); } }) } diff --git a/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts b/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts index 2b4572cba5..b9e1259501 100644 --- a/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts +++ b/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts @@ -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()) diff --git a/src/app/shared/resource-policies/form/resource-policy-form.html b/src/app/shared/resource-policies/form/resource-policy-form.html index 999e7cf66c..6585755145 100644 --- a/src/app/shared/resource-policies/form/resource-policy-form.html +++ b/src/app/shared/resource-policies/form/resource-policy-form.html @@ -27,11 +27,19 @@
+ [disabled]="!(isFormValid() | async) || (isProcessing | async)" + (click)="onSubmit()"> + + {{'submission.workflow.tasks.generic.processing' | translate}} + + + {{'form.submit' | translate}} + +
diff --git a/src/app/shared/resource-policies/form/resource-policy-form.model.ts b/src/app/shared/resource-policies/form/resource-policy-form.model.ts index 3192946c9b..37f9b866a9 100644 --- a/src/app/shared/resource-policies/form/resource-policy-form.model.ts +++ b/src/app/shared/resource-policies/form/resource-policy-form.model.ts @@ -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: { diff --git a/src/app/shared/resource-policies/form/resource-policy-form.ts b/src/app/shared/resource-policies/form/resource-policy-form.ts index e5b29cdaf1..b8316112e9 100644 --- a/src/app/shared/resource-policies/form/resource-policy-form.ts +++ b/src/app/shared/resource-policies/form/resource-policy-form.ts @@ -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 = 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, RemoteData]) => { - 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) => { + 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()) diff --git a/src/app/shared/resource-policies/resource-policies.component.html b/src/app/shared/resource-policies/resource-policies.component.html index 19303b67ed..8209c836ff 100644 --- a/src/app/shared/resource-policies/resource-policies.component.html +++ b/src/app/shared/resource-policies/resource-policies.component.html @@ -1,17 +1,40 @@ -
+
- + @@ -23,29 +46,39 @@ - + + - - - - + + + - - + - - - + + +
-

+

+
{{ 'resource-policies.table.headers.title.for.' + resourceType | translate }} {{resourceUUID}} - -

+
+ + +
+
+
+ + +
+
{{'resource-policies.table.headers.id' | translate}} {{'resource-policies.table.headers.name' | translate}} {{'resource-policies.table.headers.policyType' | translate}}
+
+ + +
+
- {{policy.id}} - {{policy.name}}{{policy.policyType}}{{policy.action}} - {{getEPersonName(policy) | async}} + {{entry.policy.name}}{{entry.policy.policyType}}{{entry.policy.action}} + {{getEPersonName(entry.policy) | async}} - {{getGroupName(policy) | async}} - + {{getGroupName(entry.policy) | async}} + {{policy.startDate}}{{policy.endDate}}{{formatDate(entry.policy.startDate)}}{{formatDate(entry.policy.endDate)}}
diff --git a/src/app/shared/resource-policies/resource-policies.component.ts b/src/app/shared/resource-policies/resource-policies.component.ts index 257657afdd..8ad11315a4 100644 --- a/src/app/shared/resource-policies/resource-policies.component.ts +++ b/src/app/shared/resource-policies/resource-policies.component.ts @@ -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>>} + * A boolean representing if a submission delete operation is pending + * @type {BehaviorSubject} */ - private resourcePolicies$: BehaviorSubject>> = - new BehaviorSubject>>({} as any); + private processingDelete$ = new BehaviorSubject(false); + + /** + * The list of policies for given resource + * @type {BehaviorSubject} + */ + private resourcePoliciesEntries$: BehaviorSubject = + new BehaviorSubject([]); /** * 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 { + 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 { + // 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 { + // 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 { + 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 { + // 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 { + // 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 { + 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 { - 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 { - 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>> { - 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 { - 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 { - 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()) } + }