63945: Edit bitstream tab intermediate commit

This commit is contained in:
Kristof De Langhe
2019-07-25 17:46:23 +02:00
parent 4f3ec612a1
commit 4461e9025b
8 changed files with 308 additions and 5 deletions

View File

@@ -269,6 +269,24 @@
"content": "Your changes to this item's metadata were saved."
}
}
},
"bitstreams": {
"discard-button": "Discard",
"reinstate-button": "Undo",
"save-button": "Save",
"headers": {
"name": "Name",
"description": "Description",
"format": "Format",
"actions": "Actions",
"bundle": "Bundle"
},
"edit": {
"buttons": {
"remove": "Remove",
"undo": "Undo changes"
}
}
}
}
},

View File

@@ -15,6 +15,7 @@ import { ItemDeleteComponent } from './item-delete/item-delete.component';
import { ItemMetadataComponent } from './item-metadata/item-metadata.component';
import { EditInPlaceFieldComponent } from './item-metadata/edit-in-place-field/edit-in-place-field.component';
import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.component';
import { ItemEditBitstreamComponent } from './item-bitstreams/item-edit-bitstream/item-edit-bitstream.component';
/**
* Module that contains all components related to the Edit Item page administrator functionality
@@ -38,7 +39,8 @@ import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.compo
ItemStatusComponent,
ItemMetadataComponent,
ItemBitstreamsComponent,
EditInPlaceFieldComponent
EditInPlaceFieldComponent,
ItemEditBitstreamComponent
]
})
export class EditItemPageModule {

View File

@@ -1,3 +1,71 @@
<div>
<div class="item-bitstreams">
<div class="button-row top d-flex">
<button class="btn btn-danger ml-auto" *ngIf="!(isReinstatable() | async)"
[disabled]="!(hasChanges() | async)"
(click)="discard()"><i
class="fas fa-times"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.discard-button" | translate}}</span>
</button>
<button class="btn btn-warning ml-auto" *ngIf="isReinstatable() | async"
(click)="reinstate()"><i
class="fas fa-undo-alt"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.reinstate-button" | translate}}</span>
</button>
<button class="btn btn-primary" [disabled]="!(hasChanges() | async)"
(click)="submit()"><i
class="fas fa-save"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.save-button" | translate}}</span>
</button>
</div>
<table class="table table-responsive table-striped table-bordered">
<tbody>
<tr>
<th>{{'item.edit.bitstreams.headers.name' | translate}}</th>
<th>{{'item.edit.bitstreams.headers.description' | translate}}</th>
<th class="text-center">{{'item.edit.bitstreams.headers.format' | translate}}</th>
<th class="text-center">{{'item.edit.bitstreams.headers.actions' | translate}}</th>
</tr>
<ng-container *ngFor="let updatesItem of updatesMap | keyvalue">
<tr>
<th>{{'item.edit.bitstreams.headers.bundle' | translate}}: {{ updatesItem.key }}</th>
<td></td>
<td></td>
<td></td>
</tr>
<ng-container *ngVar="((updatesItem.value | async) | dsObjectValues) as updateValues">
<tr *ngFor="let updateValue of updateValues"
ds-item-edit-bitstream
[fieldUpdate]="updateValue"
[url]="url"
[ngClass]="{
'table-warning': updateValue.changeType === 0,
'table-danger': updateValue.changeType === 2,
'table-success': updateValue.changeType === 1
}">
</tr>
</ng-container>
</ng-container>
</tbody>
</table>
<div class="button-row bottom">
<div class="float-right">
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
[disabled]="!(hasChanges() | async)"
(click)="discard()"><i
class="fas fa-times"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.discard-button" | translate}}</span>
</button>
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
(click)="reinstate()"><i
class="fas fa-undo-alt"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.reinstate-button" | translate}}</span>
</button>
<button class="btn btn-primary" [disabled]="!(hasChanges() | async)"
(click)="submit()"><i
class="fas fa-save"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.save-button" | translate}}</span>
</button>
</div>
</div>
</div>

View File

@@ -1,4 +1,11 @@
import { Component } from '@angular/core';
import { Component, OnDestroy } from '@angular/core';
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
import { switchMap, take } from 'rxjs/operators';
import { Bitstream } from '../../../core/shared/bitstream.model';
import { getBundleNames, toBitstreamsArray, toBundleMap } from '../../../core/shared/item-bitstreams-utils';
import { Observable } from 'rxjs/internal/Observable';
import { FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer';
import { Subscription } from 'rxjs/internal/Subscription';
@Component({
selector: 'ds-item-bitstreams',
@@ -8,6 +15,56 @@ import { Component } from '@angular/core';
/**
* Component for displaying an item's bitstreams edit page
*/
export class ItemBitstreamsComponent {
/* TODO implement */
export class ItemBitstreamsComponent extends AbstractItemUpdateComponent implements OnDestroy {
bundleNames$: Observable<string[]>;
updatesMap: Map<string, Observable<FieldUpdates>>;
updatesMapSub: Subscription;
/**
* Set up and initialize all fields
*/
ngOnInit(): void {
super.ngOnInit();
this.bundleNames$ = this.item.bitstreams.pipe(getBundleNames());
}
initializeNotificationsPrefix(): void {
this.notificationsPrefix = 'item.edit.bitstreams.notifications.';
}
initializeOriginalFields(): void {
this.item.bitstreams.pipe(
toBitstreamsArray(),
take(1)
).subscribe((bitstreams: Bitstream[]) => {
this.objectUpdatesService.initialize(this.url, bitstreams, this.item.lastModified);
});
}
initializeUpdates(): void {
this.updates$ = this.item.bitstreams.pipe(
toBitstreamsArray(),
switchMap((bitstreams: Bitstream[]) => this.objectUpdatesService.getFieldUpdates(this.url, bitstreams))
);
this.updatesMapSub = this.item.bitstreams.pipe(
toBundleMap()
).subscribe((bundleMap: Map<string, Bitstream[]>) => {
const updatesMap = new Map();
bundleMap.forEach((bitstreams: Bitstream[], bundleName: string) => {
updatesMap.set(bundleName, this.objectUpdatesService.getFieldUpdatesExclusive(this.url, bitstreams));
});
this.updatesMap = updatesMap;
});
}
submit() {
// TODO: submit changes
}
ngOnDestroy(): void {
this.updatesMapSub.unsubscribe();
}
}

View File

@@ -0,0 +1,23 @@
<td>
{{ bitstream.name }}
</td>
<td class="w-100">
{{ bitstream.description }}
</td>
<td class="text-center">
{{ (format$ | async).shortDescription }}
</td>
<td class="text-center">
<div class="btn-group relationship-action-buttons">
<button [disabled]="!canRemove()" (click)="remove()"
class="btn btn-outline-danger btn-sm"
title="{{'item.edit.bitstreams.edit.buttons.remove' | translate}}">
<i class="fas fa-trash-alt fa-fw"></i>
</button>
<button [disabled]="!canUndo()" (click)="undo()"
class="btn btn-outline-warning btn-sm"
title="{{'item.edit.bitstreams.edit.buttons.undo' | translate}}">
<i class="fas fa-undo-alt fa-fw"></i>
</button>
</div>
</td>

View File

@@ -0,0 +1,64 @@
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { FieldUpdate } from '../../../../core/data/object-updates/object-updates.reducer';
import { Bitstream } from '../../../../core/shared/bitstream.model';
import { cloneDeep } from 'lodash';
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
import { Observable } from 'rxjs/internal/Observable';
import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model';
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
@Component({
// tslint:disable-next-line:component-selector
selector: '[ds-item-edit-bitstream]',
templateUrl: './item-edit-bitstream.component.html',
})
export class ItemEditBitstreamComponent implements OnChanges {
@Input() fieldUpdate: FieldUpdate;
@Input() url: string;
bitstream: Bitstream;
format$: Observable<BitstreamFormat>;
constructor(private objectUpdatesService: ObjectUpdatesService) {
}
ngOnChanges(changes: SimpleChanges): void {
this.bitstream = cloneDeep(this.fieldUpdate.field) as Bitstream;
this.format$ = this.bitstream.format.pipe(
getSucceededRemoteData(),
getRemoteDataPayload()
);
}
/**
* Sends a new remove update for this field to the object updates service
*/
remove(): void {
this.objectUpdatesService.saveRemoveFieldUpdate(this.url, this.bitstream);
}
/**
* Cancels the current update for this field in the object updates service
*/
undo(): void {
this.objectUpdatesService.removeSingleFieldUpdate(this.url, this.bitstream.uuid);
}
/**
* Check if a user should be allowed to remove this field
*/
canRemove(): boolean {
return this.fieldUpdate.changeType !== FieldChangeType.REMOVE;
}
/**
* Check if a user should be allowed to cancel the update to this field
*/
canUndo(): boolean {
return this.fieldUpdate.changeType >= 0;
}
}

View File

@@ -105,6 +105,27 @@ export class ObjectUpdatesService {
}))
}
/**
* Method that combines the state's updates (excluding updates that aren't part of the initialFields) with
* the initial values (when there's no update) to create a FieldUpdates object
* @param url The URL of the page for which the FieldUpdates should be requested
* @param initialFields The initial values of the fields
*/
getFieldUpdatesExclusive(url: string, initialFields: Identifiable[]): Observable<FieldUpdates> {
const objectUpdates = this.getObjectEntry(url);
return objectUpdates.pipe(map((objectEntry) => {
const fieldUpdates: FieldUpdates = {};
for (const object of initialFields) {
let fieldUpdate = objectEntry.fieldUpdates[object.uuid];
if (isEmpty(fieldUpdate)) {
fieldUpdate = { field: object, changeType: undefined };
}
fieldUpdates[object.uuid] = fieldUpdate;
}
return fieldUpdates;
}))
}
/**
* Method to check if a specific field is currently editable in the store
* @param url The URL of the page on which the field resides

View File

@@ -0,0 +1,50 @@
import { Observable } from 'rxjs/internal/Observable';
import { RemoteData } from '../data/remote-data';
import { PaginatedList } from '../data/paginated-list';
import { Bitstream } from './bitstream.model';
import { map } from 'rxjs/operators';
import { combineLatest as observableCombineLatest } from 'rxjs';
import { getSucceededRemoteData } from './operators';
export const toBitstreamsArray = () =>
(source: Observable<RemoteData<PaginatedList<Bitstream>>>): Observable<Bitstream[]> =>
source.pipe(
getSucceededRemoteData(),
map((bitstreamRD: RemoteData<PaginatedList<Bitstream>>) => bitstreamRD.payload.page)
);
export const getBundleNames = () =>
(source: Observable<RemoteData<PaginatedList<Bitstream>>>): Observable<string[]> =>
source.pipe(
toBitstreamsArray(),
map((bitstreams: Bitstream[]) => {
const result = [];
bitstreams.forEach((bitstream: Bitstream) => {
if (result.indexOf(bitstream.bundleName) < 0) {
result.push(bitstream.bundleName);
}
});
return result;
})
);
export const filterByBundleName = (bundleName: string) =>
(source: Observable<RemoteData<PaginatedList<Bitstream>>>): Observable<Bitstream[]> =>
source.pipe(
toBitstreamsArray(),
map((bitstreams: Bitstream[]) =>
bitstreams.filter((bitstream: Bitstream) => bitstream.bundleName === bundleName)
)
);
export const toBundleMap = () =>
(source: Observable<RemoteData<PaginatedList<Bitstream>>>): Observable<Map<string, Bitstream[]>> =>
observableCombineLatest(source.pipe(toBitstreamsArray()), source.pipe(getBundleNames())).pipe(
map(([bitstreams, bundleNames]) => {
const bundleMap = new Map();
bundleNames.forEach((bundleName: string) => {
bundleMap.set(bundleName, bitstreams.filter((bitstream: Bitstream) => bitstream.bundleName === bundleName));
});
return bundleMap;
})
);