mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-17 15:03:07 +00:00
118223: Implement bitstream reordering with keyboard
This commit is contained in:
@@ -33,7 +33,6 @@
|
||||
[item]="item"
|
||||
[columnSizes]="columnSizes"
|
||||
[isFirstTable]="isFirst"
|
||||
(dropObject)="dropBitstream(bundle, $event)"
|
||||
aria-describedby="reorder-description">
|
||||
</ds-item-edit-bitstream-bundle>
|
||||
</div>
|
||||
|
@@ -25,6 +25,10 @@ import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } f
|
||||
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||
import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model';
|
||||
import { BitstreamDataServiceStub } from '../../../shared/testing/bitstream-data-service.stub';
|
||||
import { ItemBitstreamsService } from './item-bitstreams.service';
|
||||
import { ResponsiveTableSizes } from '../../../shared/responsive-table-sizes/responsive-table-sizes';
|
||||
import { ResponsiveColumnSizes } from '../../../shared/responsive-table-sizes/responsive-column-sizes';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
|
||||
let comp: ItemBitstreamsComponent;
|
||||
let fixture: ComponentFixture<ItemBitstreamsComponent>;
|
||||
@@ -76,6 +80,7 @@ let objectCache: ObjectCacheService;
|
||||
let requestService: RequestService;
|
||||
let searchConfig: SearchConfigurationService;
|
||||
let bundleService: BundleDataService;
|
||||
let itemBitstreamsService: ItemBitstreamsService;
|
||||
|
||||
describe('ItemBitstreamsComponent', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
@@ -147,6 +152,19 @@ describe('ItemBitstreamsComponent', () => {
|
||||
patch: createSuccessfulRemoteDataObject$({}),
|
||||
});
|
||||
|
||||
itemBitstreamsService = jasmine.createSpyObj('itemBitstreamsService', {
|
||||
getColumnSizes: new ResponsiveTableSizes([
|
||||
new ResponsiveColumnSizes(2, 2, 3, 4, 4),
|
||||
new ResponsiveColumnSizes(2, 3, 3, 3, 3),
|
||||
new ResponsiveColumnSizes(2, 2, 2, 2, 2),
|
||||
new ResponsiveColumnSizes(6, 5, 4, 3, 3)
|
||||
]),
|
||||
getSelectedBitstream$: observableOf({}),
|
||||
getInitialBundlesPaginationOptions: new PaginationComponentOptions(),
|
||||
removeMarkedBitstreams: createSuccessfulRemoteDataObject$({}),
|
||||
displayNotifications: undefined,
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot()],
|
||||
declarations: [ItemBitstreamsComponent, ObjectValuesPipe, VarDirective],
|
||||
@@ -161,6 +179,7 @@ describe('ItemBitstreamsComponent', () => {
|
||||
{ provide: RequestService, useValue: requestService },
|
||||
{ provide: SearchConfigurationService, useValue: searchConfig },
|
||||
{ provide: BundleDataService, useValue: bundleService },
|
||||
{ provide: ItemBitstreamsService, useValue: itemBitstreamsService },
|
||||
ChangeDetectorRef
|
||||
], schemas: [
|
||||
NO_ERRORS_SCHEMA
|
||||
@@ -181,28 +200,8 @@ describe('ItemBitstreamsComponent', () => {
|
||||
comp.submit();
|
||||
});
|
||||
|
||||
it('should call removeMultiple on the bitstreamService for the marked field', () => {
|
||||
expect(bitstreamService.removeMultiple).toHaveBeenCalledWith([bitstream2]);
|
||||
});
|
||||
|
||||
it('should not call removeMultiple on the bitstreamService for the unmarked field', () => {
|
||||
expect(bitstreamService.removeMultiple).not.toHaveBeenCalledWith([bitstream1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when dropBitstream is called', () => {
|
||||
beforeEach((done) => {
|
||||
comp.dropBitstream(bundle, {
|
||||
fromIndex: 0,
|
||||
toIndex: 50,
|
||||
finish: () => {
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should send out a patch for the move operation', () => {
|
||||
expect(bundleService.patch).toHaveBeenCalled();
|
||||
it('should call removeMarkedBitstreams on the itemBitstreamsService', () => {
|
||||
expect(itemBitstreamsService.removeMarkedBitstreams).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { ChangeDetectorRef, Component, NgZone, OnDestroy } from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, NgZone, OnDestroy, HostListener } from '@angular/core';
|
||||
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
|
||||
import { map, switchMap, take } from 'rxjs/operators';
|
||||
import { Observable, Subscription, zip as observableZip } from 'rxjs';
|
||||
@@ -8,13 +8,11 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { BitstreamDataService } from '../../../core/data/bitstream-data.service';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
||||
import { RequestService } from '../../../core/data/request.service';
|
||||
import {
|
||||
getFirstSucceededRemoteData,
|
||||
getRemoteDataPayload,
|
||||
getFirstCompletedRemoteData
|
||||
} from '../../../core/shared/operators';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||
@@ -23,7 +21,6 @@ import { BundleDataService } from '../../../core/data/bundle-data.service';
|
||||
import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-search-options.model';
|
||||
import { ResponsiveTableSizes } from '../../../shared/responsive-table-sizes/responsive-table-sizes';
|
||||
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import { ItemBitstreamsService } from './item-bitstreams.service';
|
||||
import { AlertType } from '../../../shared/alert/aletr-type';
|
||||
|
||||
@@ -95,6 +92,56 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles keyboard events that should move the currently selected bitstream up
|
||||
*/
|
||||
@HostListener('document:keydown.arrowUp', ['$event'])
|
||||
moveUp(event: KeyboardEvent) {
|
||||
if (this.itemBitstreamsService.hasSelectedBitstream()) {
|
||||
event.preventDefault();
|
||||
this.itemBitstreamsService.moveSelectedBitstreamUp();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles keyboard events that should move the currently selected bitstream down
|
||||
*/
|
||||
@HostListener('document:keydown.arrowDown', ['$event'])
|
||||
moveDown(event: KeyboardEvent) {
|
||||
if (this.itemBitstreamsService.hasSelectedBitstream()) {
|
||||
event.preventDefault();
|
||||
this.itemBitstreamsService.moveSelectedBitstreamDown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles keyboard events that should cancel the currently selected bitstream.
|
||||
* A cancel means that the selected bitstream is returned to its original position and is no longer selected.
|
||||
* @param event
|
||||
*/
|
||||
@HostListener('document:keyup.escape', ['$event'])
|
||||
cancelSelection(event: KeyboardEvent) {
|
||||
if (this.itemBitstreamsService.hasSelectedBitstream()) {
|
||||
event.preventDefault();
|
||||
this.itemBitstreamsService.cancelSelection();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles keyboard events that should clear the currently selected bitstream.
|
||||
* A clear means that the selected bitstream remains in its current position but is no longer selected.
|
||||
*/
|
||||
@HostListener('document:keydown.enter', ['$event'])
|
||||
@HostListener('document:keydown.space', ['$event'])
|
||||
clearSelection(event: KeyboardEvent) {
|
||||
// Only when no specific element is in focus do we want to clear the currently selected bitstream
|
||||
// Otherwise we might clear the selection when a different action was intended, e.g. clicking a button or selecting
|
||||
// a different bitstream.
|
||||
if (event.target instanceof Element && event.target.tagName === 'BODY') {
|
||||
this.itemBitstreamsService.clearSelection();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the notification messages prefix
|
||||
*/
|
||||
@@ -120,36 +167,6 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A bitstream was dropped in a new location. Send out a Move Patch request to the REST API, display notifications,
|
||||
* refresh the bundle's cache (so the lists can properly reload) and call the event's callback function (which will
|
||||
* navigate the user to the correct page)
|
||||
* @param bundle The bundle to send patch requests to
|
||||
* @param event The event containing the index the bitstream came from and was dropped to
|
||||
*/
|
||||
dropBitstream(bundle: Bundle, event: any) {
|
||||
this.zone.runOutsideAngular(() => {
|
||||
if (hasValue(event) && hasValue(event.fromIndex) && hasValue(event.toIndex) && hasValue(event.finish)) {
|
||||
const moveOperation = {
|
||||
op: 'move',
|
||||
from: `/_links/bitstreams/${event.fromIndex}/href`,
|
||||
path: `/_links/bitstreams/${event.toIndex}/href`
|
||||
} as Operation;
|
||||
this.bundleService.patch(bundle, [moveOperation]).pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
).subscribe((response: RemoteData<Bundle>) => {
|
||||
this.zone.run(() => {
|
||||
this.itemBitstreamsService.displayNotifications('item.edit.bitstreams.notifications.move', [response]);
|
||||
// Remove all cached requests from this bundle and call the event's callback when the requests are cleared
|
||||
this.requestService.setStaleByHrefSubstring(bundle.self).pipe(
|
||||
take(1)
|
||||
).subscribe(() => event.finish());
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the object updates service to discard all current changes to this item
|
||||
* Shows a notification to remind the user that they can undo this
|
||||
|
@@ -16,6 +16,12 @@ import {
|
||||
createFailedRemoteDataObject,
|
||||
createSuccessfulRemoteDataObject
|
||||
} from '../../../shared/remote-data.utils';
|
||||
import { BundleDataService } from '../../../core/data/bundle-data.service';
|
||||
import { RequestService } from '../../../core/data/request.service';
|
||||
import { LiveRegionService } from '../../../shared/live-region/live-region.service';
|
||||
import { Bundle } from '../../../core/shared/bundle.model';
|
||||
import { of } from 'rxjs';
|
||||
import { getLiveRegionServiceStub } from '../../../shared/live-region/live-region.service.stub';
|
||||
|
||||
describe('ItemBitstreamsService', () => {
|
||||
let service: ItemBitstreamsService;
|
||||
@@ -23,21 +29,34 @@ describe('ItemBitstreamsService', () => {
|
||||
let translateService: TranslateService;
|
||||
let objectUpdatesService: ObjectUpdatesService;
|
||||
let bitstreamDataService: BitstreamDataService;
|
||||
let bundleDataService: BundleDataService;
|
||||
let dsoNameService: DSONameService;
|
||||
let requestService: RequestService;
|
||||
let liveRegionService: LiveRegionService;
|
||||
|
||||
beforeEach(() => {
|
||||
notificationsService = new NotificationsServiceStub() as any;
|
||||
translateService = getMockTranslateService();
|
||||
objectUpdatesService = new ObjectUpdatesServiceStub() as any;
|
||||
bitstreamDataService = new BitstreamDataServiceStub() as any;
|
||||
bundleDataService = jasmine.createSpyObj('bundleDataService', {
|
||||
patch: createSuccessfulRemoteDataObject$(new Bundle()),
|
||||
});
|
||||
dsoNameService = new DSONameServiceMock() as any;
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
setStaleByHrefSubstring: of(true),
|
||||
});
|
||||
liveRegionService = getLiveRegionServiceStub();
|
||||
|
||||
service = new ItemBitstreamsService(
|
||||
notificationsService,
|
||||
translateService,
|
||||
objectUpdatesService,
|
||||
bitstreamDataService,
|
||||
bundleDataService,
|
||||
dsoNameService,
|
||||
requestService,
|
||||
liveRegionService,
|
||||
);
|
||||
});
|
||||
|
||||
|
@@ -3,38 +3,277 @@ import { PaginationComponentOptions } from '../../../shared/pagination/paginatio
|
||||
import { ResponsiveTableSizes } from '../../../shared/responsive-table-sizes/responsive-table-sizes';
|
||||
import { ResponsiveColumnSizes } from '../../../shared/responsive-table-sizes/responsive-column-sizes';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { isNotEmpty, hasValue } from '../../../shared/empty.util';
|
||||
import { isNotEmpty, hasValue, hasNoValue } from '../../../shared/empty.util';
|
||||
import { Bundle } from '../../../core/shared/bundle.model';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Observable, zip as observableZip } from 'rxjs';
|
||||
import { Observable, zip as observableZip, BehaviorSubject } from 'rxjs';
|
||||
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||
import { take, switchMap, map } from 'rxjs/operators';
|
||||
import { take, switchMap, map, tap } from 'rxjs/operators';
|
||||
import { FieldUpdates } from '../../../core/data/object-updates/field-updates.model';
|
||||
import { FieldUpdate } from '../../../core/data/object-updates/field-update.model';
|
||||
import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model';
|
||||
import { Bitstream } from '../../../core/shared/bitstream.model';
|
||||
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
||||
import { BitstreamDataService } from '../../../core/data/bitstream-data.service';
|
||||
import { BitstreamTableEntry } from './item-edit-bitstream-bundle/item-edit-bitstream-bundle.component';
|
||||
import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
|
||||
import { getFirstSucceededRemoteDataPayload, getFirstCompletedRemoteData } from '../../../core/shared/operators';
|
||||
import { getBitstreamDownloadRoute } from '../../../app-routing-paths';
|
||||
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
||||
import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
|
||||
import { MoveOperation } from 'fast-json-patch';
|
||||
import { BundleDataService } from '../../../core/data/bundle-data.service';
|
||||
import { RequestService } from '../../../core/data/request.service';
|
||||
import { LiveRegionService } from '../../../shared/live-region/live-region.service';
|
||||
|
||||
/**
|
||||
* Interface storing all the information necessary to create a row in the bitstream edit table
|
||||
*/
|
||||
export interface BitstreamTableEntry {
|
||||
/**
|
||||
* The bitstream
|
||||
*/
|
||||
bitstream: Bitstream,
|
||||
/**
|
||||
* The uuid of the Bitstream
|
||||
*/
|
||||
id: string,
|
||||
/**
|
||||
* The name of the Bitstream
|
||||
*/
|
||||
name: string,
|
||||
/**
|
||||
* The name of the Bitstream with all whitespace removed
|
||||
*/
|
||||
nameStripped: string,
|
||||
/**
|
||||
* The description of the Bitstream
|
||||
*/
|
||||
description: string,
|
||||
/**
|
||||
* Observable emitting the Format of the Bitstream
|
||||
*/
|
||||
format: Observable<BitstreamFormat>,
|
||||
/**
|
||||
* The download url of the Bitstream
|
||||
*/
|
||||
downloadUrl: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface storing information necessary to highlight and reorder the selected bitstream entry
|
||||
*/
|
||||
export interface SelectedBitstreamTableEntry {
|
||||
/**
|
||||
* The selected entry
|
||||
*/
|
||||
bitstream: BitstreamTableEntry,
|
||||
/**
|
||||
* The bundle the bitstream belongs to
|
||||
*/
|
||||
bundle: Bundle,
|
||||
/**
|
||||
* The total number of bitstreams in the bundle
|
||||
*/
|
||||
bundleSize: number,
|
||||
/**
|
||||
* The original position of the bitstream within the bundle.
|
||||
*/
|
||||
originalPosition: number,
|
||||
/**
|
||||
* The current position of the bitstream within the bundle.
|
||||
*/
|
||||
currentPosition: number,
|
||||
}
|
||||
|
||||
/**
|
||||
* This service handles the selection and updating of the bitstreams and their order on the
|
||||
* 'Edit Item' -> 'Bitstreams' page.
|
||||
*/
|
||||
@Injectable(
|
||||
{ providedIn: 'root' },
|
||||
)
|
||||
export class ItemBitstreamsService {
|
||||
|
||||
/**
|
||||
* BehaviorSubject which emits every time the selected bitstream changes.
|
||||
*/
|
||||
protected selectedBitstream$: BehaviorSubject<SelectedBitstreamTableEntry> = new BehaviorSubject(null);
|
||||
|
||||
protected isPerformingMoveRequest = false;
|
||||
|
||||
constructor(
|
||||
protected notificationsService: NotificationsService,
|
||||
protected translateService: TranslateService,
|
||||
protected objectUpdatesService: ObjectUpdatesService,
|
||||
protected bitstreamService: BitstreamDataService,
|
||||
protected bundleService: BundleDataService,
|
||||
protected dsoNameService: DSONameService,
|
||||
protected requestService: RequestService,
|
||||
protected liveRegionService: LiveRegionService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the observable emitting the currently selected bitstream
|
||||
*/
|
||||
getSelectedBitstream$(): Observable<SelectedBitstreamTableEntry> {
|
||||
return this.selectedBitstream$;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of the currently selected bitstream
|
||||
*/
|
||||
getSelectedBitstream(): SelectedBitstreamTableEntry {
|
||||
const selected = this.selectedBitstream$.getValue();
|
||||
|
||||
if (hasNoValue(selected)) {
|
||||
return selected;
|
||||
}
|
||||
|
||||
return Object.assign({}, selected);
|
||||
}
|
||||
|
||||
hasSelectedBitstream(): boolean {
|
||||
return hasValue(this.getSelectedBitstream());
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the provided entry
|
||||
*/
|
||||
selectBitstreamEntry(entry: SelectedBitstreamTableEntry) {
|
||||
if (entry !== this.selectedBitstream$.getValue()) {
|
||||
this.announceSelect(entry.bitstream.name);
|
||||
this.updateSelectedBitstream(entry);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the {@link selectedBitstream$} observable emit the provided {@link SelectedBitstreamTableEntry}.
|
||||
* @protected
|
||||
*/
|
||||
protected updateSelectedBitstream(entry: SelectedBitstreamTableEntry) {
|
||||
this.selectedBitstream$.next(entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unselects the selected bitstream. Does nothing if no bitstream is selected.
|
||||
*/
|
||||
clearSelection() {
|
||||
const selected = this.getSelectedBitstream();
|
||||
|
||||
if (hasValue(selected)) {
|
||||
this.updateSelectedBitstream(null);
|
||||
this.announceClear(selected.bitstream.name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently selected bitstream to its original position and unselects the bitstream.
|
||||
* Does nothing if no bitstream is selected.
|
||||
*/
|
||||
cancelSelection() {
|
||||
const selected = this.getSelectedBitstream();
|
||||
|
||||
if (hasNoValue(selected) || this.isPerformingMoveRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedBitstream$.next(null);
|
||||
|
||||
const originalPosition = selected.originalPosition;
|
||||
const currentPosition = selected.currentPosition;
|
||||
|
||||
// If the selected bitstream has not moved, there is no need to return it to its original position
|
||||
if (currentPosition === originalPosition) {
|
||||
this.announceClear(selected.bitstream.name);
|
||||
} else {
|
||||
this.announceCancel(selected.bitstream.name, originalPosition);
|
||||
this.performBitstreamMoveRequest(selected.bundle, currentPosition, originalPosition);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the selected bitstream one position up in the bundle. Does nothing if no bitstream is selected or the
|
||||
* selected bitstream already is at the beginning of the bundle.
|
||||
*/
|
||||
moveSelectedBitstreamUp() {
|
||||
const selected = this.getSelectedBitstream();
|
||||
|
||||
if (hasNoValue(selected) || this.isPerformingMoveRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
const originalPosition = selected.currentPosition;
|
||||
if (originalPosition > 0) {
|
||||
const newPosition = originalPosition - 1;
|
||||
selected.currentPosition = newPosition;
|
||||
|
||||
const onRequestCompleted = () => {
|
||||
this.announceMove(selected.bitstream.name, newPosition);
|
||||
};
|
||||
|
||||
this.performBitstreamMoveRequest(selected.bundle, originalPosition, newPosition, onRequestCompleted);
|
||||
this.updateSelectedBitstream(selected);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the selected bitstream one position down in the bundle. Does nothing if no bitstream is selected or the
|
||||
* selected bitstream already is at the end of the bundle.
|
||||
*/
|
||||
moveSelectedBitstreamDown() {
|
||||
const selected = this.getSelectedBitstream();
|
||||
|
||||
if (hasNoValue(selected) || this.isPerformingMoveRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
const originalPosition = selected.currentPosition;
|
||||
if (originalPosition < selected.bundleSize - 1) {
|
||||
const newPosition = originalPosition + 1;
|
||||
selected.currentPosition = newPosition;
|
||||
|
||||
const onRequestCompleted = () => {
|
||||
this.announceMove(selected.bitstream.name, newPosition);
|
||||
};
|
||||
|
||||
this.performBitstreamMoveRequest(selected.bundle, originalPosition, newPosition, onRequestCompleted);
|
||||
this.updateSelectedBitstream(selected);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends out a Move Patch request to the REST API, display notifications,
|
||||
* refresh the bundle's cache (so the lists can properly reload)
|
||||
* @param bundle The bundle to send patch requests to
|
||||
* @param fromIndex The index to move from
|
||||
* @param toIndex The index to move to
|
||||
* @param finish Optional: Function to execute once the response has been received
|
||||
*/
|
||||
performBitstreamMoveRequest(bundle: Bundle, fromIndex: number, toIndex: number, finish?: () => void) {
|
||||
if (this.isPerformingMoveRequest) {
|
||||
console.warn('Attempted to perform move request while previous request has not completed yet');
|
||||
return;
|
||||
}
|
||||
|
||||
const moveOperation: MoveOperation = {
|
||||
op: 'move',
|
||||
from: `/_links/bitstreams/${fromIndex}/href`,
|
||||
path: `/_links/bitstreams/${toIndex}/href`,
|
||||
};
|
||||
|
||||
this.isPerformingMoveRequest = true;
|
||||
this.bundleService.patch(bundle, [moveOperation]).pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
tap((response: RemoteData<Bundle>) => this.displayNotifications('item.edit.bitstreams.notifications.move', [response])),
|
||||
switchMap(() => this.requestService.setStaleByHrefSubstring(bundle.self)),
|
||||
take(1),
|
||||
).subscribe(() => {
|
||||
this.isPerformingMoveRequest = false;
|
||||
finish?.();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the pagination options to use when fetching the bundles
|
||||
*/
|
||||
@@ -46,6 +285,10 @@ export class ItemBitstreamsService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the initial pagination options to use when fetching the bitstreams
|
||||
* @param bundleName The name of the bundle, will be as pagination id.
|
||||
*/
|
||||
getInitialBitstreamsPaginationOptions(bundleName: string): PaginationComponentOptions {
|
||||
return Object.assign(new PaginationComponentOptions(),{
|
||||
id: bundleName, // This might behave unexpectedly if the item contains two bundles with the same name
|
||||
@@ -118,6 +361,10 @@ export class ItemBitstreamsService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an array of {@link BitstreamTableEntry}s from an array of {@link Bitstream}s
|
||||
* @param bitstreams The bitstreams array to map to table entries
|
||||
*/
|
||||
mapBitstreamsToTableEntries(bitstreams: Bitstream[]): BitstreamTableEntry[] {
|
||||
return bitstreams.map((bitstream) => {
|
||||
const name = this.dsoNameService.getName(bitstream);
|
||||
@@ -143,7 +390,7 @@ export class ItemBitstreamsService {
|
||||
// To make it clear which headers are relevant for a specific field in the table, the 'headers' attribute is used to
|
||||
// refer to specific headers. The Bitstream's name is used as header ID for the row containing information regarding
|
||||
// that bitstream. As the 'headers' attribute contains a space-separated string of header IDs, the Bitstream's header
|
||||
// ID can not contain strings itself.
|
||||
// ID can not contain spaces itself.
|
||||
return this.stripWhiteSpace(name);
|
||||
}
|
||||
|
||||
@@ -155,4 +402,48 @@ export class ItemBitstreamsService {
|
||||
// '/\s+/g' matches all occurrences of any amount of whitespace characters
|
||||
return str.replace(/\s+/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a message to the live region mentioning that the bitstream with the provided name was selected.
|
||||
* @param bitstreamName The name of the bitstream that was selected.
|
||||
*/
|
||||
announceSelect(bitstreamName: string) {
|
||||
const message = this.translateService.instant('item.edit.bitstreams.edit.live.select',
|
||||
{ bitstream: bitstreamName });
|
||||
this.liveRegionService.addMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a message to the live region mentioning that the bitstream with the provided name was moved to the provided
|
||||
* position.
|
||||
* @param bitstreamName The name of the bitstream that moved.
|
||||
* @param toPosition The zero-indexed position that the bitstream moved to.
|
||||
*/
|
||||
announceMove(bitstreamName: string, toPosition: number) {
|
||||
const message = this.translateService.instant('item.edit.bitstreams.edit.live.move',
|
||||
{ bitstream: bitstreamName, toIndex: toPosition + 1 });
|
||||
this.liveRegionService.addMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a message to the live region mentioning that the bitstream with the provided name is no longer selected and
|
||||
* was returned to the provided position.
|
||||
* @param bitstreamName The name of the bitstream that is no longer selected
|
||||
* @param toPosition The zero-indexed position the bitstream returned to.
|
||||
*/
|
||||
announceCancel(bitstreamName: string, toPosition: number) {
|
||||
const message = this.translateService.instant('item.edit.bitstreams.edit.live.cancel',
|
||||
{ bitstream: bitstreamName, toIndex: toPosition + 1 });
|
||||
this.liveRegionService.addMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a message to the live region mentioning that the bitstream with the provided name is no longer selected.
|
||||
* @param bitstreamName The name of the bitstream that is no longer selected.
|
||||
*/
|
||||
announceClear(bitstreamName: string) {
|
||||
const message = this.translateService.instant('item.edit.bitstreams.edit.live.clear',
|
||||
{ bitstream: bitstreamName });
|
||||
this.liveRegionService.addMessage(message);
|
||||
}
|
||||
}
|
||||
|
@@ -5,7 +5,8 @@
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
[hidePaginationDetail]="true"
|
||||
[paginationOptions]="paginationOptions"
|
||||
[collectionSize]="bitstreamsList.totalElements">
|
||||
[collectionSize]="bitstreamsList.totalElements"
|
||||
[retainScrollPosition]="true">
|
||||
<ng-container *ngIf="(updates$ | async) as updates">
|
||||
|
||||
<table class="table" [class.mt-n1]="!isFirstTable"
|
||||
@@ -43,18 +44,26 @@
|
||||
title="{{'item.edit.bitstreams.bundle.edit.buttons.upload' | translate}}">
|
||||
<i class="fas fa-upload fa-fw"></i>
|
||||
</button>
|
||||
<div ngbDropdown #paginationControls="ngbDropdown" class="btn-group float-right btn-sm p-0" placement="bottom-right">
|
||||
<button class="btn btn-outline-secondary" id="paginationControls" ngbDropdownToggle [title]="'pagination.options.description' | translate" [attr.aria-label]="'pagination.options.description' | translate" aria-haspopup="true" aria-expanded="false">
|
||||
<div ngbDropdown #paginationControls="ngbDropdown" class="btn-group float-right btn-sm p-0"
|
||||
placement="bottom-right">
|
||||
<button class="btn btn-outline-secondary" id="paginationControls" ngbDropdownToggle
|
||||
[title]="'pagination.options.description' | translate"
|
||||
[attr.aria-label]="'pagination.options.description' | translate" aria-haspopup="true"
|
||||
aria-expanded="false">
|
||||
<i class="fas fa-cog" aria-hidden="true"></i>
|
||||
</button>
|
||||
|
||||
<ul id="paginationControlsDropdownMenu" aria-labelledby="paginationControls" role="menu" ngbDropdownMenu>
|
||||
<ul id="paginationControlsDropdownMenu" aria-labelledby="paginationControls" role="menu"
|
||||
ngbDropdownMenu>
|
||||
<li role="menuitem">
|
||||
<span class="dropdown-header" id="pagination-control_results-per-page" role="heading">{{ 'pagination.results-per-page' | translate}}</span>
|
||||
<span class="dropdown-header" id="pagination-control_results-per-page"
|
||||
role="heading">{{ 'pagination.results-per-page' | translate}}</span>
|
||||
<ul aria-labelledby="pagination-control_results-per-page" class="list-unstyled" role="listbox">
|
||||
<li *ngFor="let size of pageSizeOptions" role="option" [attr.aria-selected]="size === (pageSize$ | async)">
|
||||
<li *ngFor="let size of paginationOptions.pageSizeOptions" role="option"
|
||||
[attr.aria-selected]="size === (pageSize$ | async)">
|
||||
<button (click)="doPageSizeChange(size)" class="dropdown-item">
|
||||
<i [ngClass]="{'invisible': size !== (pageSize$ | async) }" class="fas fa-check" aria-hidden="true"></i> {{size}}
|
||||
<i [ngClass]="{'invisible': size !== (pageSize$ | async) }" class="fas fa-check"
|
||||
aria-hidden="true"></i> {{size}}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -67,13 +76,15 @@
|
||||
</tr>
|
||||
|
||||
<ng-container *ngFor="let entry of (tableEntries$ | async)">
|
||||
<tr *ngIf="updates[entry.id] as update" [ngClass]="getRowClass(update)" class="bitstream-row" cdkDrag
|
||||
(cdkDragStarted)="dragStart(entry.name)" (cdkDragEnded)="dragEnd(entry.name)">
|
||||
<tr *ngIf="updates[entry.id] as update" [ngClass]="getRowClass(update, entry)" class="bitstream-row" cdkDrag
|
||||
(cdkDragStarted)="dragStart()" (cdkDragEnded)="dragEnd()">
|
||||
|
||||
<th class="bitstream-name row-element {{ columnSizes.columns[0].buildClasses() }}"
|
||||
scope="row" id="{{ entry.nameStripped }}" headers="{{ bundleName }} name">
|
||||
<div class="drag-handle text-muted float-left p-1 mr-2" tabindex="0" cdkDragHandle>
|
||||
<i class="fas fa-grip-vertical fa-fw" [title]="'item.edit.bitstreams.edit.buttons.drag' | translate"></i>
|
||||
<div class="drag-handle text-muted float-left p-1 mr-2" tabindex="0" cdkDragHandle
|
||||
(keyup.enter)="select(entry)" (keyup.space)="select(entry)" (click)="select(entry)">
|
||||
<i class="fas fa-grip-vertical fa-fw"
|
||||
[title]="'item.edit.bitstreams.edit.buttons.drag' | translate"></i>
|
||||
</div>
|
||||
{{ entry.name }}
|
||||
</th>
|
||||
|
@@ -58,6 +58,7 @@ describe('ItemEditBitstreamBundleComponent', () => {
|
||||
currentPage: 1,
|
||||
pageSize: 9999
|
||||
}),
|
||||
getSelectedBitstream$: observableOf({}),
|
||||
});
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
|
@@ -1,4 +1,10 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output, ViewChild, ViewContainerRef } from '@angular/core';
|
||||
import {
|
||||
Component,
|
||||
Input,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
ViewContainerRef, OnDestroy,
|
||||
} from '@angular/core';
|
||||
import { Bundle } from '../../../../core/shared/bundle.model';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { ResponsiveColumnSizes } from '../../../../shared/responsive-table-sizes/responsive-column-sizes';
|
||||
@@ -8,7 +14,7 @@ import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
|
||||
import { RemoteData } from 'src/app/core/data/remote-data';
|
||||
import { PaginatedList } from 'src/app/core/data/paginated-list.model';
|
||||
import { Bitstream } from 'src/app/core/shared/bitstream.model';
|
||||
import { Observable, BehaviorSubject, switchMap } from 'rxjs';
|
||||
import { Observable, BehaviorSubject, switchMap, shareReplay, Subscription } from 'rxjs';
|
||||
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
||||
import { FieldUpdates } from '../../../../core/data/object-updates/field-updates.model';
|
||||
import { PaginatedSearchOptions } from '../../../../shared/search/models/paginated-search-options.model';
|
||||
@@ -17,55 +23,17 @@ import { followLink } from '../../../../shared/utils/follow-link-config.model';
|
||||
import {
|
||||
getAllSucceededRemoteData,
|
||||
paginatedListToArray,
|
||||
getFirstSucceededRemoteData
|
||||
} from '../../../../core/shared/operators';
|
||||
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
||||
import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model';
|
||||
import { map, take, filter } from 'rxjs/operators';
|
||||
import { map, take, filter, tap, pairwise } from 'rxjs/operators';
|
||||
import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model';
|
||||
import { FieldUpdate } from '../../../../core/data/object-updates/field-update.model';
|
||||
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
||||
import { PaginationComponent } from '../../../../shared/pagination/pagination.component';
|
||||
import { RequestService } from '../../../../core/data/request.service';
|
||||
import { ItemBitstreamsService } from '../item-bitstreams.service';
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import { hasValue } from '../../../../shared/empty.util';
|
||||
import { LiveRegionService } from '../../../../shared/live-region/live-region.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
/**
|
||||
* Interface storing all the information necessary to create a row in the bitstream edit table
|
||||
*/
|
||||
export interface BitstreamTableEntry {
|
||||
/**
|
||||
* The bitstream
|
||||
*/
|
||||
bitstream: Bitstream,
|
||||
/**
|
||||
* The uuid of the Bitstream
|
||||
*/
|
||||
id: string,
|
||||
/**
|
||||
* The name of the Bitstream
|
||||
*/
|
||||
name: string,
|
||||
/**
|
||||
* The name of the Bitstream with all whitespace removed
|
||||
*/
|
||||
nameStripped: string,
|
||||
/**
|
||||
* The description of the Bitstream
|
||||
*/
|
||||
description: string,
|
||||
/**
|
||||
* Observable emitting the Format of the Bitstream
|
||||
*/
|
||||
format: Observable<BitstreamFormat>,
|
||||
/**
|
||||
* The download url of the Bitstream
|
||||
*/
|
||||
downloadUrl: string,
|
||||
}
|
||||
import { ItemBitstreamsService, BitstreamTableEntry, SelectedBitstreamTableEntry } from '../item-bitstreams.service';
|
||||
import { CdkDragDrop } from '@angular/cdk/drag-drop';
|
||||
import { hasValue, hasNoValue } from '../../../../shared/empty.util';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-item-edit-bitstream-bundle',
|
||||
@@ -77,7 +45,7 @@ export interface BitstreamTableEntry {
|
||||
* Creates an embedded view of the contents. This is to ensure the table structure won't break.
|
||||
* (which means it'll be added to the parents html without a wrapping ds-item-edit-bitstream-bundle element)
|
||||
*/
|
||||
export class ItemEditBitstreamBundleComponent implements OnInit {
|
||||
export class ItemEditBitstreamBundleComponent implements OnInit, OnDestroy {
|
||||
protected readonly FieldChangeType = FieldChangeType;
|
||||
|
||||
/**
|
||||
@@ -115,13 +83,6 @@ export class ItemEditBitstreamBundleComponent implements OnInit {
|
||||
*/
|
||||
@Input() isFirstTable = false;
|
||||
|
||||
/**
|
||||
* Send an event when the user drops an object on the pagination
|
||||
* The event contains details about the index the object came from and is dropped to (across the entirety of the list,
|
||||
* not just within a single page)
|
||||
*/
|
||||
@Output() dropObject: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
/**
|
||||
* The bootstrap sizes used for the Bundle Name column
|
||||
* This column stretches over the first 3 columns and thus is a combination of their sizes processed in ngOnInit
|
||||
@@ -138,6 +99,11 @@ export class ItemEditBitstreamBundleComponent implements OnInit {
|
||||
*/
|
||||
bundleName: string;
|
||||
|
||||
/**
|
||||
* The number of bitstreams in the bundle
|
||||
*/
|
||||
bundleSize: number;
|
||||
|
||||
/**
|
||||
* The bitstreams to show in the table
|
||||
*/
|
||||
@@ -146,7 +112,7 @@ export class ItemEditBitstreamBundleComponent implements OnInit {
|
||||
/**
|
||||
* The data to show in the table
|
||||
*/
|
||||
tableEntries$: BehaviorSubject<BitstreamTableEntry[]> = new BehaviorSubject(null);
|
||||
tableEntries$: BehaviorSubject<BitstreamTableEntry[]> = new BehaviorSubject([]);
|
||||
|
||||
/**
|
||||
* The initial page options to use for fetching the bitstreams
|
||||
@@ -158,11 +124,6 @@ export class ItemEditBitstreamBundleComponent implements OnInit {
|
||||
*/
|
||||
currentPaginationOptions$: BehaviorSubject<PaginationComponentOptions>;
|
||||
|
||||
/**
|
||||
* The available page size options
|
||||
*/
|
||||
pageSizeOptions: number[];
|
||||
|
||||
/**
|
||||
* The currently selected page size
|
||||
*/
|
||||
@@ -178,6 +139,11 @@ export class ItemEditBitstreamBundleComponent implements OnInit {
|
||||
*/
|
||||
updates$: BehaviorSubject<FieldUpdates> = new BehaviorSubject(null);
|
||||
|
||||
/**
|
||||
* Array containing all subscriptions created by this component
|
||||
*/
|
||||
subscriptions: Subscription[] = [];
|
||||
|
||||
|
||||
constructor(
|
||||
protected viewContainerRef: ViewContainerRef,
|
||||
@@ -187,8 +153,6 @@ export class ItemEditBitstreamBundleComponent implements OnInit {
|
||||
protected paginationService: PaginationService,
|
||||
protected requestService: RequestService,
|
||||
protected itemBitstreamsService: ItemBitstreamsService,
|
||||
protected liveRegionService: LiveRegionService,
|
||||
protected translateService: TranslateService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -201,23 +165,27 @@ export class ItemEditBitstreamBundleComponent implements OnInit {
|
||||
|
||||
this.initializePagination();
|
||||
this.initializeBitstreams();
|
||||
this.initializeSelectionActions();
|
||||
}
|
||||
|
||||
// this.bitstreamsRD = this.
|
||||
ngOnDestroy() {
|
||||
this.subscriptions.forEach(sub => sub?.unsubscribe());
|
||||
}
|
||||
|
||||
protected initializePagination() {
|
||||
this.paginationOptions = this.itemBitstreamsService.getInitialBitstreamsPaginationOptions(this.bundleName);
|
||||
|
||||
this.pageSizeOptions = this.paginationOptions.pageSizeOptions;
|
||||
|
||||
this.currentPaginationOptions$ = new BehaviorSubject(this.paginationOptions);
|
||||
this.pageSize$ = new BehaviorSubject(this.paginationOptions.pageSize);
|
||||
|
||||
this.subscriptions.push(
|
||||
this.paginationService.getCurrentPagination(this.paginationOptions.id, this.paginationOptions)
|
||||
.subscribe((pagination) => {
|
||||
this.currentPaginationOptions$.next(pagination);
|
||||
this.pageSize$.next(pagination.pageSize);
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
protected initializeBitstreams() {
|
||||
@@ -233,26 +201,88 @@ export class ItemEditBitstreamBundleComponent implements OnInit {
|
||||
))
|
||||
);
|
||||
}),
|
||||
getAllSucceededRemoteData(),
|
||||
shareReplay(1),
|
||||
);
|
||||
|
||||
this.subscriptions.push(
|
||||
this.bitstreamsRD$.pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
take(1),
|
||||
tap(bitstreamsRD => this.bundleSize = bitstreamsRD.payload.totalElements),
|
||||
paginatedListToArray(),
|
||||
).subscribe((bitstreams) => {
|
||||
this.objectUpdatesService.initialize(this.bundleUrl, bitstreams, new Date());
|
||||
});
|
||||
}),
|
||||
|
||||
this.bitstreamsRD$.pipe(
|
||||
getAllSucceededRemoteData(),
|
||||
paginatedListToArray(),
|
||||
switchMap((bitstreams) => this.objectUpdatesService.getFieldUpdatesExclusive(this.bundleUrl, bitstreams))
|
||||
).subscribe((updates) => this.updates$.next(updates));
|
||||
).subscribe((updates) => this.updates$.next(updates)),
|
||||
|
||||
this.bitstreamsRD$.pipe(
|
||||
getAllSucceededRemoteData(),
|
||||
paginatedListToArray(),
|
||||
map((bitstreams) => this.itemBitstreamsService.mapBitstreamsToTableEntries(bitstreams)),
|
||||
).subscribe((tableEntries) => this.tableEntries$.next(tableEntries));
|
||||
).subscribe((tableEntries) => this.tableEntries$.next(tableEntries)),
|
||||
);
|
||||
}
|
||||
|
||||
protected initializeSelectionActions() {
|
||||
this.subscriptions.push(
|
||||
this.itemBitstreamsService.getSelectedBitstream$().pipe(pairwise()).subscribe(
|
||||
([previousSelection, currentSelection]) =>
|
||||
this.handleSelectedEntryChange(previousSelection, currentSelection))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a change in selected bitstream by changing the pagination if the change happened on a different page
|
||||
* @param previousSelectedEntry The previously selected entry
|
||||
* @param currentSelectedEntry The currently selected entry
|
||||
* @protected
|
||||
*/
|
||||
protected handleSelectedEntryChange(
|
||||
previousSelectedEntry: SelectedBitstreamTableEntry,
|
||||
currentSelectedEntry: SelectedBitstreamTableEntry
|
||||
) {
|
||||
if (hasValue(currentSelectedEntry) && currentSelectedEntry.bundle === this.bundle) {
|
||||
// If the currently selected bitstream belongs to this bundle, it has possibly moved to a different page.
|
||||
// In that case we want to change the pagination to the new page.
|
||||
this.redirectToCurrentPage(currentSelectedEntry);
|
||||
}
|
||||
|
||||
// If the selection is cancelled or cleared, it is possible the selected bitstream is currently on a different page
|
||||
// In that case we want to change the pagination to the place where the bitstream was returned to
|
||||
if (hasNoValue(currentSelectedEntry) && hasValue(previousSelectedEntry) && previousSelectedEntry.bundle === this.bundle) {
|
||||
this.redirectToOriginalPage(previousSelectedEntry);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect the user to the current page of the provided bitstream if it is on a different page.
|
||||
* @param bitstreamEntry The entry that the current position will be taken from to determine the page to move to
|
||||
* @protected
|
||||
*/
|
||||
protected redirectToCurrentPage(bitstreamEntry: SelectedBitstreamTableEntry) {
|
||||
const currentPage = this.getCurrentPage();
|
||||
const selectedEntryPage = this.bundleIndexToPage(bitstreamEntry.currentPosition);
|
||||
|
||||
if (currentPage !== selectedEntryPage) {
|
||||
this.changeToPage(selectedEntryPage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect the user to the original page of the provided bitstream if it is on a different page.
|
||||
* @param bitstreamEntry The entry that the original position will be taken from to determine the page to move to
|
||||
* @protected
|
||||
*/
|
||||
protected redirectToOriginalPage(bitstreamEntry: SelectedBitstreamTableEntry) {
|
||||
const currentPage = this.getCurrentPage();
|
||||
const originPage = this.bundleIndexToPage(bitstreamEntry.originalPosition);
|
||||
|
||||
if (currentPage !== originPage) {
|
||||
this.changeToPage(originPage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -283,7 +313,18 @@ export class ItemEditBitstreamBundleComponent implements OnInit {
|
||||
this.objectUpdatesService.removeSingleFieldUpdate(this.bundleUrl, bitstream.uuid);
|
||||
}
|
||||
|
||||
getRowClass(update: FieldUpdate): string {
|
||||
/**
|
||||
* Returns the css class for a table row depending on the state of the table entry.
|
||||
* @param update
|
||||
* @param bitstream
|
||||
*/
|
||||
getRowClass(update: FieldUpdate, bitstream: BitstreamTableEntry): string {
|
||||
const selected = this.itemBitstreamsService.getSelectedBitstream();
|
||||
|
||||
if (hasValue(selected) && bitstream.id === selected.bitstream.id) {
|
||||
return 'table-info';
|
||||
}
|
||||
|
||||
switch (update.changeType) {
|
||||
case FieldChangeType.UPDATE:
|
||||
return 'table-warning';
|
||||
@@ -296,11 +337,19 @@ export class ItemEditBitstreamBundleComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the page size to the provided page size.
|
||||
* @param pageSize
|
||||
*/
|
||||
public doPageSizeChange(pageSize: number) {
|
||||
this.paginationComponent.doPageSizeChange(pageSize);
|
||||
}
|
||||
|
||||
dragStart(bitstreamName: string) {
|
||||
/**
|
||||
* Handles start of dragging by opening the tooltip mentioning that it is possible to drag a bitstream to a different
|
||||
* page by dropping it on the page number, only if there are multiple pages.
|
||||
*/
|
||||
dragStart() {
|
||||
// Only open the drag tooltip when there are multiple pages
|
||||
this.paginationComponent.shouldShowBottomPager.pipe(
|
||||
take(1),
|
||||
@@ -308,66 +357,170 @@ export class ItemEditBitstreamBundleComponent implements OnInit {
|
||||
).subscribe(() => {
|
||||
this.dragTooltip.open();
|
||||
});
|
||||
|
||||
const message = this.translateService.instant('item.edit.bitstreams.edit.live.drag',
|
||||
{ bitstream: bitstreamName });
|
||||
this.liveRegionService.addMessage(message);
|
||||
}
|
||||
|
||||
dragEnd(bitstreamName: string) {
|
||||
/**
|
||||
* Handles end of dragging by closing the tooltip.
|
||||
*/
|
||||
dragEnd() {
|
||||
this.dragTooltip.close();
|
||||
|
||||
const message = this.translateService.instant('item.edit.bitstreams.edit.live.drop',
|
||||
{ bitstream: bitstreamName });
|
||||
this.liveRegionService.addMessage(message);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handles dropping by calculation the target position, and changing the page if the bitstream was dropped on a
|
||||
* different page.
|
||||
* @param event
|
||||
*/
|
||||
drop(event: CdkDragDrop<any>) {
|
||||
const dragIndex = event.previousIndex;
|
||||
let dropIndex = event.currentIndex;
|
||||
const dragPage = this.currentPaginationOptions$.value.currentPage - 1;
|
||||
let dropPage = this.currentPaginationOptions$.value.currentPage - 1;
|
||||
const dragPage = this.getCurrentPage();
|
||||
let dropPage = this.getCurrentPage();
|
||||
|
||||
// Check if the user is hovering over any of the pagination's pages at the time of dropping the object
|
||||
const droppedOnElement = document.elementFromPoint(event.dropPoint.x, event.dropPoint.y);
|
||||
if (hasValue(droppedOnElement) && hasValue(droppedOnElement.textContent) && droppedOnElement.classList.contains('page-link')) {
|
||||
// The user is hovering over a page, fetch the page's number from the element
|
||||
const droppedPage = Number(droppedOnElement.textContent);
|
||||
let droppedPage = Number(droppedOnElement.textContent);
|
||||
if (hasValue(droppedPage) && !Number.isNaN(droppedPage)) {
|
||||
dropPage = droppedPage - 1;
|
||||
droppedPage -= 1;
|
||||
|
||||
if (droppedPage !== dragPage) {
|
||||
dropPage = droppedPage;
|
||||
|
||||
if (dropPage > dragPage) {
|
||||
// When moving to later page, place bitstream at the top
|
||||
dropIndex = 0;
|
||||
} else {
|
||||
// When moving to earlier page, place bitstream at the bottom
|
||||
dropIndex = this.getCurrentPageSize() - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isNewPage = dragPage !== dropPage;
|
||||
// Move the object in the custom order array if the drop happened within the same page
|
||||
// This allows us to instantly display a change in the order, instead of waiting for the REST API's response first
|
||||
if (!isNewPage && dragIndex !== dropIndex) {
|
||||
const currentEntries = [...this.tableEntries$.value];
|
||||
moveItemInArray(currentEntries, dragIndex, dropIndex);
|
||||
this.tableEntries$.next(currentEntries);
|
||||
const fromIndex = this.pageIndexToBundleIndex(dragIndex, dragPage);
|
||||
const toIndex = this.pageIndexToBundleIndex(dropIndex, dropPage);
|
||||
|
||||
if (fromIndex === toIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pageSize = this.currentPaginationOptions$.value.pageSize;
|
||||
const redirectPage = dropPage + 1;
|
||||
const fromIndex = (dragPage * pageSize) + dragIndex;
|
||||
const toIndex = (dropPage * pageSize) + dropIndex;
|
||||
// Send out a drop event (and navigate to the new page) when the "from" and "to" indexes are different from each other
|
||||
if (fromIndex !== toIndex) {
|
||||
// if (isNewPage) {
|
||||
// this.loading$.next(true);
|
||||
// }
|
||||
this.dropObject.emit(Object.assign({
|
||||
fromIndex,
|
||||
toIndex,
|
||||
finish: () => {
|
||||
if (isNewPage) {
|
||||
this.paginationComponent.doPageChange(redirectPage);
|
||||
const selectedBitstream = this.tableEntries$.value[dragIndex];
|
||||
|
||||
const finish = () => {
|
||||
this.itemBitstreamsService.announceMove(selectedBitstream.name, toIndex);
|
||||
|
||||
if (dropPage !== this.getCurrentPage()) {
|
||||
this.changeToPage(dropPage);
|
||||
}
|
||||
};
|
||||
|
||||
this.itemBitstreamsService.performBitstreamMoveRequest(this.bundle, fromIndex, toIndex, finish);
|
||||
}
|
||||
}));
|
||||
|
||||
/**
|
||||
* Handles a select action for the provided bitstream entry.
|
||||
* If the selected bitstream is currently selected, the selection is cleared.
|
||||
* If no, or a different bitstream, is selected, the provided bitstream becomes the selected bitstream.
|
||||
* @param bitstream
|
||||
*/
|
||||
select(bitstream: BitstreamTableEntry) {
|
||||
const selectedBitstream = this.itemBitstreamsService.getSelectedBitstream();
|
||||
|
||||
if (hasValue(selectedBitstream) && selectedBitstream.bitstream === bitstream) {
|
||||
this.itemBitstreamsService.cancelSelection();
|
||||
} else {
|
||||
const selectionObject = this.createBitstreamSelectionObject(bitstream);
|
||||
|
||||
if (hasNoValue(selectionObject)) {
|
||||
console.warn('Failed to create selection object');
|
||||
return;
|
||||
}
|
||||
|
||||
this.itemBitstreamsService.selectBitstreamEntry(selectionObject);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link SelectedBitstreamTableEntry} from the provided {@link BitstreamTableEntry} so it can be given
|
||||
* to the {@link ItemBitstreamsService} to select the table entry.
|
||||
* @param bitstream The table entry to create the selection object from.
|
||||
* @protected
|
||||
*/
|
||||
protected createBitstreamSelectionObject(bitstream: BitstreamTableEntry): SelectedBitstreamTableEntry {
|
||||
const pageIndex = this.findBitstreamPageIndex(bitstream);
|
||||
|
||||
if (pageIndex === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const position = this.pageIndexToBundleIndex(pageIndex, this.getCurrentPage());
|
||||
|
||||
return {
|
||||
bitstream: bitstream,
|
||||
bundle: this.bundle,
|
||||
bundleSize: this.bundleSize,
|
||||
currentPosition: position,
|
||||
originalPosition: position,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the provided {@link BitstreamTableEntry} relative to the current page
|
||||
* If the current page size is 10, it will return a value from 0 to 9 (inclusive)
|
||||
* Returns -1 if the provided bitstream could not be found
|
||||
* @protected
|
||||
*/
|
||||
protected findBitstreamPageIndex(bitstream: BitstreamTableEntry): number {
|
||||
const entries = this.tableEntries$.value;
|
||||
return entries.findIndex(entry => entry === bitstream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current zero-indexed page
|
||||
* @protected
|
||||
*/
|
||||
protected getCurrentPage(): number {
|
||||
// The pagination component uses one-based numbering while zero-based numbering is more convenient for calculations
|
||||
return this.currentPaginationOptions$.value.currentPage - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current page size
|
||||
* @protected
|
||||
*/
|
||||
protected getCurrentPageSize(): number {
|
||||
return this.currentPaginationOptions$.value.pageSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an index relative to the page to an index relative to the bundle
|
||||
* @param index The index relative to the page
|
||||
* @param page The zero-indexed page number
|
||||
* @protected
|
||||
*/
|
||||
protected pageIndexToBundleIndex(index: number, page: number) {
|
||||
return page * this.getCurrentPageSize() + index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the zero-indexed page number from the index relative to the bundle
|
||||
* @param index The index relative to the bundle
|
||||
* @protected
|
||||
*/
|
||||
protected bundleIndexToPage(index: number) {
|
||||
return Math.floor(index / this.getCurrentPageSize());
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the pagination for this bundle to the provided zero-indexed page
|
||||
* @param page The zero-indexed page to change to
|
||||
* @protected
|
||||
*/
|
||||
protected changeToPage(page: number) {
|
||||
// Increments page by one because zero-indexing is way easier for calculations but the pagination component
|
||||
// uses one-indexing.
|
||||
this.paginationComponent.doPageChange(page + 1);
|
||||
}
|
||||
}
|
||||
|
30
src/app/shared/live-region/live-region.service.stub.ts
Normal file
30
src/app/shared/live-region/live-region.service.stub.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { of } from 'rxjs';
|
||||
import { LiveRegionService } from './live-region.service';
|
||||
|
||||
export function getLiveRegionServiceStub(): LiveRegionService {
|
||||
return new LiveRegionServiceStub() as unknown as LiveRegionService;
|
||||
}
|
||||
|
||||
export class LiveRegionServiceStub {
|
||||
getMessages = jasmine.createSpy('getMessages').and.returnValue(
|
||||
['Message One', 'Message Two']
|
||||
);
|
||||
|
||||
getMessages$ = jasmine.createSpy('getMessages$').and.returnValue(
|
||||
of(['Message One', 'Message Two'])
|
||||
);
|
||||
|
||||
addMessage = jasmine.createSpy('addMessage').and.returnValue('messageId');
|
||||
|
||||
clear = jasmine.createSpy('clear');
|
||||
|
||||
clearMessageByUUID = jasmine.createSpy('clearMessageByUUID');
|
||||
|
||||
getLiveRegionVisibility = jasmine.createSpy('getLiveRegionVisibility').and.returnValue(false);
|
||||
|
||||
setLiveRegionVisibility = jasmine.createSpy('setLiveRegionVisibility');
|
||||
|
||||
getMessageTimeOutMs = jasmine.createSpy('getMessageTimeOutMs').and.returnValue(30000);
|
||||
|
||||
setMessageTimeOutMs = jasmine.createSpy('setMessageTimeOutMs');
|
||||
}
|
@@ -1950,9 +1950,13 @@
|
||||
|
||||
"item.edit.bitstreams.edit.buttons.undo": "Undo changes",
|
||||
|
||||
"item.edit.bitstreams.edit.live.drag": "{{ bitstream }} grabbed",
|
||||
"item.edit.bitstreams.edit.live.cancel": "{{ bitstream }} was returned to position {{ toIndex }} and is no longer selected.",
|
||||
|
||||
"item.edit.bitstreams.edit.live.drop": "{{ bitstream }} dropped",
|
||||
"item.edit.bitstreams.edit.live.clear": "{{ bitstream }} is no longer selected.",
|
||||
|
||||
"item.edit.bitstreams.edit.live.select": "{{ bitstream }} is selected.",
|
||||
|
||||
"item.edit.bitstreams.edit.live.move": "{{ bitstream }} is now in position {{ toIndex }}.",
|
||||
|
||||
"item.edit.bitstreams.empty": "This item doesn't contain any bitstreams. Click the upload button to create one.",
|
||||
|
||||
|
Reference in New Issue
Block a user