Reposition elements when no space is left

pull/7473/head
Oliver Günther 5 years ago
parent c5afa710cf
commit 208f927451
No known key found for this signature in database
GPG Key ID: A3A8BDAD7C0C552C
  1. 4
      frontend/src/app/components/wp-card-view/wp-card-view.component.ts
  2. 7
      frontend/src/app/components/wp-fast-table/builders/drag-and-drop/drag-drop-handle-builder.ts
  3. 23
      frontend/src/app/components/wp-fast-table/builders/drag-and-drop/drag-drop-handle-render-pass.ts
  4. 20
      frontend/src/app/components/wp-fast-table/handlers/state/drag-and-drop-transformer.ts
  5. 16
      frontend/src/app/components/wp-fast-table/state/wp-table-order.service.ts
  6. 6
      frontend/src/app/components/wp-table/wp-table.directive.ts
  7. 93
      frontend/src/app/modules/common/drag-and-drop/reorder-delta-builder.ts

@ -188,11 +188,11 @@ export class WorkPackageCardViewComponent implements OnInit {
return this.canDragOutOf(workPackage) && !card.dataset.isNew;
},
accepts: () => this.dragInto,
onMoved: (card:HTMLElement) => {
onMoved: async (card:HTMLElement) => {
const wpId:string = card.dataset.workPackageId!;
const toIndex = DragAndDropHelpers.findIndex(card);
const newOrder = this.reorderService.move(this.currentOrder, wpId, toIndex);
const newOrder = await this.reorderService.move(this.currentOrder, wpId, toIndex);
this.updateOrder(newOrder);
},
onRemoved: (card:HTMLElement) => {

@ -17,7 +17,7 @@ export class DragDropHandleBuilder {
/**
* Renders an angular CDK drag component into the column
*/
public build(workPackage:WorkPackageResource):HTMLElement {
public build(workPackage:WorkPackageResource, position?:number):HTMLElement {
// Append sort handle
let td = document.createElement('td');
@ -30,7 +30,12 @@ export class DragDropHandleBuilder {
// Wrap handle as span
let span = document.createElement('span');
span.classList.add('wp-table--drag-and-drop-handle', 'icon-toggle');
let text = document.createElement('span');
text.textContent = '' + position;
td.appendChild(span);
td.appendChild(text);
return td;
}

@ -3,10 +3,13 @@ import {WorkPackageTableColumnsService} from '../../state/wp-table-columns.servi
import {PrimaryRenderPass, RowRenderInfo} from '../primary-render-pass';
import {DragDropHandleBuilder} from "core-components/wp-fast-table/builders/drag-and-drop/drag-drop-handle-builder";
import {WorkPackageTable} from "core-components/wp-fast-table/wp-fast-table";
import {WorkPackageTableOrderService} from "core-components/wp-fast-table/state/wp-table-order.service";
import {QueryOrder} from "core-app/modules/hal/dm-services/query-order-dm.service";
export class DragDropHandleRenderPass {
public wpTableColumns = this.injector.get(WorkPackageTableColumnsService);
public wpTableOrder = this.injector.get(WorkPackageTableOrderService);
// Drag & Drop handle builder
protected dragDropHandleBuilder = new DragDropHandleBuilder(this.injector);
@ -17,17 +20,19 @@ export class DragDropHandleRenderPass {
}
public render() {
this.tablePass.renderedOrder.forEach((row:RowRenderInfo, position:number) => {
// We only care for rows that are natural work packages and are not relation sub-rows
if (!row.workPackage || row.renderType === 'relations') {
return;
}
this.wpTableOrder.withLoadedPositions().then((positions:QueryOrder) => {
this.tablePass.renderedOrder.forEach((row:RowRenderInfo, position:number) => {
// We only care for rows that are natural work packages and are not relation sub-rows
if (!row.workPackage || row.renderType === 'relations') {
return;
}
const handle = this.dragDropHandleBuilder.build(row.workPackage!);
const handle = this.dragDropHandleBuilder.build(row.workPackage!, positions[row.workPackage!.id!]);
if (handle) {
row.element.replaceChild(handle, row.element.firstElementChild!);
}
if (handle) {
row.element.replaceChild(handle, row.element.firstElementChild!);
}
});
});
}
}

@ -41,8 +41,8 @@ export class DragAndDropTransformer {
this.inlineCreateService.newInlineWorkPackageCreated
.pipe(takeUntil(this.querySpace.stopAllSubscriptions))
.subscribe((wpId) => {
const newOrder = this.wpTableOrder.add(this.currentOrder, wpId);
.subscribe(async (wpId) => {
const newOrder = await this.wpTableOrder.add(this.currentOrder, wpId);
this.updateRenderedOrder(newOrder);
});
@ -54,7 +54,7 @@ export class DragAndDropTransformer {
this.dragService.register({
dragContainer: this.table.tbody,
scrollContainers: [this.table.tbody],
scrollContainers: [this.table.container],
accepts: () => true,
moves: (el:any, source:any, handle:HTMLElement) => {
if (!handle.classList.contains('wp-table--drag-and-drop-handle')) {
@ -72,8 +72,8 @@ export class DragAndDropTransformer {
this.actionService
.handleDrop(workPackage, el)
.then(() => {
const newOrder = this.wpTableOrder.move(this.currentOrder, wpId, rowIndex);
.then(async () => {
const newOrder = await this.wpTableOrder.move(this.currentOrder, wpId, rowIndex);
this.updateRenderedOrder(newOrder);
this.actionService.onNewOrder(newOrder);
this.wpTableSortBy.switchToManualSorting();
@ -95,8 +95,8 @@ export class DragAndDropTransformer {
return this.actionService
.handleDrop(workPackage, el)
.then(() => {
const newOrder = this.wpTableOrder.add(this.currentOrder, wpId, rowIndex);
.then(async () => {
const newOrder = await this.wpTableOrder.add(this.currentOrder, wpId, rowIndex);
this.updateRenderedOrder(newOrder);
this.actionService.onNewOrder(newOrder);
@ -133,10 +133,8 @@ export class DragAndDropTransformer {
this.querySpace.rendered.putValue(mappedOrder);
/** If the timeline is visible, we will need to redraw it */
if (this.wpTableTimeline.isVisible) {
this.table.originalRows = this.currentRenderedOrder.map((e) => e.workPackageId!);
this.table.redrawTableAndTimeline();
}
this.table.originalRows = this.currentRenderedOrder.map((e) => e.workPackageId!);
this.table.redrawTableAndTimeline();
}
protected get actionService():TableDragActionService {

@ -41,6 +41,7 @@ import {QueryOrder, QueryOrderDmService} from "core-app/modules/hal/dm-services/
import {take} from "rxjs/operators";
import {InputState} from "reactivestates";
import {WorkPackageTableSortByService} from "core-components/wp-fast-table/state/wp-table-sort-by.service";
import {from} from "rxjs";
@Injectable()
export class WorkPackageTableOrderService extends WorkPackageQueryStateService<QueryOrder> {
@ -64,14 +65,14 @@ export class WorkPackageTableOrderService extends WorkPackageQueryStateService<Q
/**
* Move an item in the list
*/
public move(order:string[], wpId:string, toIndex:number):string[] {
public async move(order:string[], wpId:string, toIndex:number):Promise<string[]> {
// Find index of the work package
let fromIndex = order.findIndex((id) => id === wpId);
let fromIndex:number = order.findIndex((id) => id === wpId);
order.splice(fromIndex, 1);
order.splice(toIndex, 0, wpId);
this.assignPosition(order, wpId, toIndex, fromIndex);
await this.assignPosition(order, wpId, toIndex, fromIndex);
return order;
}
@ -88,14 +89,14 @@ export class WorkPackageTableOrderService extends WorkPackageQueryStateService<Q
/**
* Add an item to the list
*/
public add(order:string[], wpId:string, toIndex:number = -1):string[] {
public async add(order:string[], wpId:string, toIndex:number = -1):Promise<string[]> {
if (toIndex === -1) {
order.push(wpId);
} else {
order.splice(toIndex, 0, wpId);
}
this.assignPosition(order, wpId, toIndex);
await this.assignPosition(order, wpId, toIndex);
return order;
}
@ -118,7 +119,6 @@ export class WorkPackageTableOrderService extends WorkPackageQueryStateService<Q
const positions = await this.withLoadedPositions();
const delta = new ReorderDeltaBuilder(order, positions, wpId, toIndex, fromIndex).buildDelta();
debugLog("Updating positions " + JSON.stringify(delta));
this.update(delta);
}
@ -145,12 +145,12 @@ export class WorkPackageTableOrderService extends WorkPackageQueryStateService<Q
/**
* Initialize (or load if persisted) the order for the query space
*/
protected withLoadedPositions():Promise<QueryOrder> {
public withLoadedPositions():Promise<QueryOrder> {
if (this.currentQuery.persisted) {
const value = this.positions.value;
// Remove empty or stale values given we can reload them
if (value === {} || this.positions.isPromiseRequestOlderThan(10000)) {
if ((value === {} || this.positions.isPromiseRequestOlderThan(60000))) {
this.positions.clear("Clearing old positions value");
}

@ -32,7 +32,8 @@ import {
Component,
ElementRef,
Injector,
Input, NgZone,
Input,
NgZone,
OnDestroy,
OnInit,
ViewEncapsulation
@ -186,7 +187,8 @@ export class WorkPackagesTableController implements OnInit, OnDestroy {
public registerTimeline(controller:WorkPackageTimelineTableController, body:HTMLElement) {
const tbody = this.$element.find('.work-package--results-tbody');
this.workPackageTable = new WorkPackageTable(this.injector, this.$element[0], tbody[0], body, controller, this.configuration);
const scrollContainer = this.$element.find('.work-package-table--container')[0];
this.workPackageTable = new WorkPackageTable(this.injector, scrollContainer, tbody[0], body, controller, this.configuration);
this.tbody = tbody;
controller.workPackageTable = this.workPackageTable;
new TableHandlerRegistry(this.injector).attachTo(this.workPackageTable);

@ -1,4 +1,5 @@
import {QueryOrder} from "core-app/modules/hal/dm-services/query-order-dm.service";
import {debugLog, timeOutput} from "core-app/helpers/debug_output";
// min allowed position
export const MIN_ORDER = -2147483647;
@ -7,7 +8,7 @@ export const MAX_ORDER = 2147483647;
// default position to insert
export const DEFAULT_ORDER = 0;
// The distance to keep between each element
export const ORDER_DISTANCE = 1000;
export const ORDER_DISTANCE = 16384;
/**
* Computes the delta of positions for a given
@ -37,7 +38,10 @@ export class ReorderDeltaBuilder {
}
public buildDelta():QueryOrder {
this.buildInsertPosition();
timeOutput("Building delta", () => this.buildInsertPosition());
debugLog("Order DELTA was built as %O", this.delta);
return this.delta;
}
@ -50,7 +54,7 @@ export class ReorderDeltaBuilder {
}
// Special case, shifted movement by one
if (this.fromIndex && Math.abs(this.fromIndex - this.index) === 1 && this.positionSwap()) {
if (this.fromIndex !== null && Math.abs(this.fromIndex - this.index) === 1 && this.positionSwap()) {
return;
}
@ -68,7 +72,7 @@ export class ReorderDeltaBuilder {
if (successorPosition === undefined) {
// Successor does not have a position yet (is NULL), any position will work
// so let's use the optimal one.
this.delta[this.wpId] = predecessorPosition + ORDER_DISTANCE;
this.delta[this.wpId] = predecessorPosition + (ORDER_DISTANCE / 2);
return;
}
@ -76,9 +80,10 @@ export class ReorderDeltaBuilder {
// We will want to insert at the half way from predecessorPosition ... successorPosition
const distance = Math.floor((successorPosition - predecessorPosition) / 2);
// TODO: shifting when optimal becomes too small
// If there is no space to insert, we're going to optimize the available space
if (distance < 1) {
throw "Cannot insert at optimal position, no space left. Need to compress predecessors";
debugLog("Cannot insert at optimal position, no space left. Need to reorder");
return this.reorderedInsert();
}
const optimal = predecessorPosition + distance;
@ -105,9 +110,9 @@ export class ReorderDeltaBuilder {
* we can swap the positions.
*/
private positionSwap():boolean {
const myPosition = this.positionFor(this.index!)
const myPosition = this.positionFor(this.index!);
const neighbor = this.order[this.fromIndex!];
const neighborPosition = this.positionFor(this.fromIndex!)
const neighborPosition = this.positionFor(this.fromIndex!);
// If either the neighbor or wpid have no position yet,
// go through the regular update flow
@ -150,7 +155,7 @@ export class ReorderDeltaBuilder {
*/
private positionFor(index:number):number|undefined {
const wpId = this.order[index];
return this.positions[wpId];
return this.livePosition(wpId);
}
/**
@ -162,4 +167,74 @@ export class ReorderDeltaBuilder {
private livePosition(wpId:string):number|undefined {
return this.delta[wpId] || this.positions[wpId];
}
/**
* There was no space left at the desired insert position,
* we're going to evenly distribute all items again
*/
private reorderedInsert() {
const itemsToDistribute = this.order.length;
// Get the current distance between orders
// Both must be set by now due to +buildUpPredecessorPosition+ having run.
let min = this.minPosition!;
let max = this.maxPosition!;
// We can keep min and max orders if distance/(items to distribute) >= 1
let space = Math.floor((max - min) / (itemsToDistribute - 1));
// If no space is left, first try to add to the max item
// Or subtract from the min item
if (space < 1) {
if ((max + itemsToDistribute) <= MAX_ORDER) {
max += itemsToDistribute;
} else if ((min - itemsToDistribute) >= MIN_ORDER) {
min -= itemsToDistribute;
} else {
// This should not happen in a 4-byte integer with our frontend
throw "Elements cannot be moved further and no space is left. Too many elements";
}
// Rebuild space
space = Math.floor((max - min) / (itemsToDistribute - 1));
}
// Assign positions for all values in between min/max
for (let i = 1; i < itemsToDistribute; i++) {
const wpId = this.order[i];
// If we reached a point where the position is undefined
// and larger than our point of insertion, we can keep them this way
if (i > this.index && this.livePosition(wpId) === undefined) {
return;
}
this.delta[wpId] = min + (i * space);
}
}
/**
* Returns the minimal position assigned currently
*/
private get minPosition():number|undefined {
const wpId = this.order[0]!;
return this.livePosition(wpId);
}
/**
* Returns the maximum position assigned currently.
* Note that a list can be unpositioned at the beginning, so this may return undefined
*/
private get maxPosition():number|undefined {
for (let i = this.order.length - 1; i >= 0; i--) {
let position = this.livePosition(this.order[i]);
// Return the first set position.
if (position !== undefined) {
return position;
}
}
return;
}
}

Loading…
Cancel
Save