Skip to content

Commit 181ea6d

Browse files
committed
118223: Implement bitstream reordering with keyboard
1 parent 1f909dc commit 181ea6d

10 files changed

Lines changed: 727 additions & 203 deletions

src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
[item]="item"
3434
[columnSizes]="columnSizes"
3535
[isFirstTable]="isFirst"
36-
(dropObject)="dropBitstream(bundle, $event)"
3736
aria-describedby="reorder-description">
3837
</ds-item-edit-bitstream-bundle>
3938
</div>

src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } f
2525
import { createPaginatedList } from '../../../shared/testing/utils.test';
2626
import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model';
2727
import { BitstreamDataServiceStub } from '../../../shared/testing/bitstream-data-service.stub';
28+
import { ItemBitstreamsService } from './item-bitstreams.service';
29+
import { ResponsiveTableSizes } from '../../../shared/responsive-table-sizes/responsive-table-sizes';
30+
import { ResponsiveColumnSizes } from '../../../shared/responsive-table-sizes/responsive-column-sizes';
31+
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
2832

2933
let comp: ItemBitstreamsComponent;
3034
let fixture: ComponentFixture<ItemBitstreamsComponent>;
@@ -76,6 +80,7 @@ let objectCache: ObjectCacheService;
7680
let requestService: RequestService;
7781
let searchConfig: SearchConfigurationService;
7882
let bundleService: BundleDataService;
83+
let itemBitstreamsService: ItemBitstreamsService;
7984

8085
describe('ItemBitstreamsComponent', () => {
8186
beforeEach(waitForAsync(() => {
@@ -147,6 +152,19 @@ describe('ItemBitstreamsComponent', () => {
147152
patch: createSuccessfulRemoteDataObject$({}),
148153
});
149154

155+
itemBitstreamsService = jasmine.createSpyObj('itemBitstreamsService', {
156+
getColumnSizes: new ResponsiveTableSizes([
157+
new ResponsiveColumnSizes(2, 2, 3, 4, 4),
158+
new ResponsiveColumnSizes(2, 3, 3, 3, 3),
159+
new ResponsiveColumnSizes(2, 2, 2, 2, 2),
160+
new ResponsiveColumnSizes(6, 5, 4, 3, 3)
161+
]),
162+
getSelectedBitstream$: observableOf({}),
163+
getInitialBundlesPaginationOptions: new PaginationComponentOptions(),
164+
removeMarkedBitstreams: createSuccessfulRemoteDataObject$({}),
165+
displayNotifications: undefined,
166+
});
167+
150168
TestBed.configureTestingModule({
151169
imports: [TranslateModule.forRoot()],
152170
declarations: [ItemBitstreamsComponent, ObjectValuesPipe, VarDirective],
@@ -161,6 +179,7 @@ describe('ItemBitstreamsComponent', () => {
161179
{ provide: RequestService, useValue: requestService },
162180
{ provide: SearchConfigurationService, useValue: searchConfig },
163181
{ provide: BundleDataService, useValue: bundleService },
182+
{ provide: ItemBitstreamsService, useValue: itemBitstreamsService },
164183
ChangeDetectorRef
165184
], schemas: [
166185
NO_ERRORS_SCHEMA
@@ -181,28 +200,8 @@ describe('ItemBitstreamsComponent', () => {
181200
comp.submit();
182201
});
183202

184-
it('should call removeMultiple on the bitstreamService for the marked field', () => {
185-
expect(bitstreamService.removeMultiple).toHaveBeenCalledWith([bitstream2]);
186-
});
187-
188-
it('should not call removeMultiple on the bitstreamService for the unmarked field', () => {
189-
expect(bitstreamService.removeMultiple).not.toHaveBeenCalledWith([bitstream1]);
190-
});
191-
});
192-
193-
describe('when dropBitstream is called', () => {
194-
beforeEach((done) => {
195-
comp.dropBitstream(bundle, {
196-
fromIndex: 0,
197-
toIndex: 50,
198-
finish: () => {
199-
done();
200-
}
201-
});
202-
});
203-
204-
it('should send out a patch for the move operation', () => {
205-
expect(bundleService.patch).toHaveBeenCalled();
203+
it('should call removeMarkedBitstreams on the itemBitstreamsService', () => {
204+
expect(itemBitstreamsService.removeMarkedBitstreams).toHaveBeenCalled();
206205
});
207206
});
208207

src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts

Lines changed: 52 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ChangeDetectorRef, Component, NgZone, OnDestroy } from '@angular/core';
1+
import { ChangeDetectorRef, Component, NgZone, OnDestroy, HostListener } from '@angular/core';
22
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
33
import { map, switchMap, take } from 'rxjs/operators';
44
import { Observable, Subscription, zip as observableZip } from 'rxjs';
@@ -8,13 +8,11 @@ import { ActivatedRoute, Router } from '@angular/router';
88
import { NotificationsService } from '../../../shared/notifications/notifications.service';
99
import { TranslateService } from '@ngx-translate/core';
1010
import { BitstreamDataService } from '../../../core/data/bitstream-data.service';
11-
import { hasValue } from '../../../shared/empty.util';
1211
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
1312
import { RequestService } from '../../../core/data/request.service';
1413
import {
1514
getFirstSucceededRemoteData,
1615
getRemoteDataPayload,
17-
getFirstCompletedRemoteData
1816
} from '../../../core/shared/operators';
1917
import { RemoteData } from '../../../core/data/remote-data';
2018
import { PaginatedList } from '../../../core/data/paginated-list.model';
@@ -23,7 +21,6 @@ import { BundleDataService } from '../../../core/data/bundle-data.service';
2321
import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-search-options.model';
2422
import { ResponsiveTableSizes } from '../../../shared/responsive-table-sizes/responsive-table-sizes';
2523
import { NoContent } from '../../../core/shared/NoContent.model';
26-
import { Operation } from 'fast-json-patch';
2724
import { ItemBitstreamsService } from './item-bitstreams.service';
2825
import { AlertType } from '../../../shared/alert/aletr-type';
2926

@@ -88,13 +85,63 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
8885
postItemInit(): void {
8986
const bundlesOptions = this.itemBitstreamsService.getInitialBundlesPaginationOptions();
9087

91-
this. bundles$ = this.itemService.getBundles(this.item.id, new PaginatedSearchOptions({pagination: bundlesOptions})).pipe(
88+
this.bundles$ = this.itemService.getBundles(this.item.id, new PaginatedSearchOptions({pagination: bundlesOptions})).pipe(
9289
getFirstSucceededRemoteData(),
9390
getRemoteDataPayload(),
9491
map((bundlePage: PaginatedList<Bundle>) => bundlePage.page)
9592
);
9693
}
9794

95+
/**
96+
* Handles keyboard events that should move the currently selected bitstream up
97+
*/
98+
@HostListener('document:keydown.arrowUp', ['$event'])
99+
moveUp(event: KeyboardEvent) {
100+
if (this.itemBitstreamsService.hasSelectedBitstream()) {
101+
event.preventDefault();
102+
this.itemBitstreamsService.moveSelectedBitstreamUp();
103+
}
104+
}
105+
106+
/**
107+
* Handles keyboard events that should move the currently selected bitstream down
108+
*/
109+
@HostListener('document:keydown.arrowDown', ['$event'])
110+
moveDown(event: KeyboardEvent) {
111+
if (this.itemBitstreamsService.hasSelectedBitstream()) {
112+
event.preventDefault();
113+
this.itemBitstreamsService.moveSelectedBitstreamDown();
114+
}
115+
}
116+
117+
/**
118+
* Handles keyboard events that should cancel the currently selected bitstream.
119+
* A cancel means that the selected bitstream is returned to its original position and is no longer selected.
120+
* @param event
121+
*/
122+
@HostListener('document:keyup.escape', ['$event'])
123+
cancelSelection(event: KeyboardEvent) {
124+
if (this.itemBitstreamsService.hasSelectedBitstream()) {
125+
event.preventDefault();
126+
this.itemBitstreamsService.cancelSelection();
127+
}
128+
}
129+
130+
/**
131+
* Handles keyboard events that should clear the currently selected bitstream.
132+
* A clear means that the selected bitstream remains in its current position but is no longer selected.
133+
*/
134+
@HostListener('document:keydown.enter', ['$event'])
135+
@HostListener('document:keydown.space', ['$event'])
136+
clearSelection(event: KeyboardEvent) {
137+
// Only when no specific element is in focus do we want to clear the currently selected bitstream
138+
// Otherwise we might clear the selection when a different action was intended, e.g. clicking a button or selecting
139+
// a different bitstream.
140+
if (event.target instanceof Element && event.target.tagName === 'BODY') {
141+
this.itemBitstreamsService.clearSelection();
142+
}
143+
}
144+
98145
/**
99146
* Initialize the notification messages prefix
100147
*/
@@ -120,36 +167,6 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
120167
});
121168
}
122169

123-
/**
124-
* A bitstream was dropped in a new location. Send out a Move Patch request to the REST API, display notifications,
125-
* refresh the bundle's cache (so the lists can properly reload) and call the event's callback function (which will
126-
* navigate the user to the correct page)
127-
* @param bundle The bundle to send patch requests to
128-
* @param event The event containing the index the bitstream came from and was dropped to
129-
*/
130-
dropBitstream(bundle: Bundle, event: any) {
131-
this.zone.runOutsideAngular(() => {
132-
if (hasValue(event) && hasValue(event.fromIndex) && hasValue(event.toIndex) && hasValue(event.finish)) {
133-
const moveOperation = {
134-
op: 'move',
135-
from: `/_links/bitstreams/${event.fromIndex}/href`,
136-
path: `/_links/bitstreams/${event.toIndex}/href`
137-
} as Operation;
138-
this.bundleService.patch(bundle, [moveOperation]).pipe(
139-
getFirstCompletedRemoteData(),
140-
).subscribe((response: RemoteData<Bundle>) => {
141-
this.zone.run(() => {
142-
this.itemBitstreamsService.displayNotifications('item.edit.bitstreams.notifications.move', [response]);
143-
// Remove all cached requests from this bundle and call the event's callback when the requests are cleared
144-
this.requestService.setStaleByHrefSubstring(bundle.self).pipe(
145-
take(1)
146-
).subscribe(() => event.finish());
147-
});
148-
});
149-
}
150-
});
151-
}
152-
153170
/**
154171
* Request the object updates service to discard all current changes to this item
155172
* Shows a notification to remind the user that they can undo this

src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,47 @@ import {
1616
createFailedRemoteDataObject,
1717
createSuccessfulRemoteDataObject
1818
} from '../../../shared/remote-data.utils';
19+
import { BundleDataService } from '../../../core/data/bundle-data.service';
20+
import { RequestService } from '../../../core/data/request.service';
21+
import { LiveRegionService } from '../../../shared/live-region/live-region.service';
22+
import { Bundle } from '../../../core/shared/bundle.model';
23+
import { of } from 'rxjs';
24+
import { getLiveRegionServiceStub } from '../../../shared/live-region/live-region.service.stub';
1925

2026
describe('ItemBitstreamsService', () => {
2127
let service: ItemBitstreamsService;
2228
let notificationsService: NotificationsService;
2329
let translateService: TranslateService;
2430
let objectUpdatesService: ObjectUpdatesService;
2531
let bitstreamDataService: BitstreamDataService;
32+
let bundleDataService: BundleDataService;
2633
let dsoNameService: DSONameService;
34+
let requestService: RequestService;
35+
let liveRegionService: LiveRegionService;
2736

2837
beforeEach(() => {
2938
notificationsService = new NotificationsServiceStub() as any;
3039
translateService = getMockTranslateService();
3140
objectUpdatesService = new ObjectUpdatesServiceStub() as any;
3241
bitstreamDataService = new BitstreamDataServiceStub() as any;
42+
bundleDataService = jasmine.createSpyObj('bundleDataService', {
43+
patch: createSuccessfulRemoteDataObject$(new Bundle()),
44+
});
3345
dsoNameService = new DSONameServiceMock() as any;
46+
requestService = jasmine.createSpyObj('requestService', {
47+
setStaleByHrefSubstring: of(true),
48+
});
49+
liveRegionService = getLiveRegionServiceStub();
3450

3551
service = new ItemBitstreamsService(
3652
notificationsService,
3753
translateService,
3854
objectUpdatesService,
3955
bitstreamDataService,
56+
bundleDataService,
4057
dsoNameService,
58+
requestService,
59+
liveRegionService,
4160
);
4261
});
4362

0 commit comments

Comments
 (0)