Refactor selection state to handle additional elements

pull/5699/head
Oliver Günther 7 years ago
parent ed8baccc3b
commit ccca8641e8
No known key found for this signature in database
GPG Key ID: 88872239EB414F99
  1. 29
      frontend/app/components/context-menus/wp-context-menu/wp-context-menu.controller.ts
  2. 4
      frontend/app/components/context-menus/wp-context-menu/wp-context-menu.service.html
  3. 25
      frontend/app/components/context-menus/wp-context-menu/wp-context-menu.service.test.ts
  4. 4
      frontend/app/components/wp-fast-table/builders/modes/grouped/grouped-render-pass.ts
  5. 8
      frontend/app/components/wp-fast-table/builders/modes/hierarchy/hierarchy-render-pass.ts
  6. 6
      frontend/app/components/wp-fast-table/builders/modes/plain/plain-render-pass.ts
  7. 9
      frontend/app/components/wp-fast-table/handlers/row/click-handler.ts
  8. 4
      frontend/app/components/wp-fast-table/handlers/row/context-menu-handler.ts
  9. 11
      frontend/app/components/wp-fast-table/handlers/row/context-menu-keyboard-handler.ts
  10. 6
      frontend/app/components/wp-fast-table/handlers/row/double-click-handler.ts
  11. 2
      frontend/app/components/wp-fast-table/handlers/state/columns-transformer.ts
  12. 2
      frontend/app/components/wp-fast-table/handlers/state/selection-transformer.ts
  13. 4
      frontend/app/components/wp-fast-table/helpers/wp-table-hierarchy-helpers.ts
  14. 33
      frontend/app/components/wp-fast-table/state/wp-table-selection.service.ts
  15. 26
      frontend/app/components/wp-fast-table/wp-fast-table.ts
  16. 4
      frontend/app/components/wp-table/context-menu-helper/wp-context-menu-helper.service.ts

@ -34,10 +34,12 @@ import {
WorkPackageResourceInterface
} from "../../api/api-v3/hal-resources/work-package-resource.service";
import {WorkPackageRelationsHierarchyService} from "../../wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.service";
import {States} from '../../states.service';
function wpContextMenuController($scope:any,
$rootScope:ng.IRootScopeService,
$state:ng.ui.IStateService,
states:States,
WorkPackageContextMenuHelper:any,
WorkPackageService:any,
wpRelationsHierarchyService:WorkPackageRelationsHierarchyService,
@ -49,8 +51,10 @@ function wpContextMenuController($scope:any,
$scope.I18n = I18n;
if (!wpTableSelection.isSelected($scope.row.object.id)) {
wpTableSelection.setSelection($scope.row);
const wpId = $scope.workPackageId;
const workPackage = states.workPackages.get(wpId).value!;
if (!wpTableSelection.isSelected(wpId)) {
wpTableSelection.setSelection(wpId, $scope.rowIndex);
}
$scope.permittedActions = WorkPackageContextMenuHelper.getPermittedActions(getSelectedWorkPackages(), PERMITTED_CONTEXT_MENU_ACTIONS);
@ -60,9 +64,6 @@ function wpContextMenuController($scope:any,
};
$scope.triggerContextMenuAction = function (action:any, link:any) {
let table:WorkPackageTable;
let wp:WorkPackageResourceInterface;
switch (action) {
case 'delete':
deleteSelectedWorkPackages();
@ -77,20 +78,15 @@ function wpContextMenuController($scope:any,
break;
case 'relation-precedes':
table = $scope.table;
wp = $scope.row.object;
table.timelineController.startAddRelationPredecessor(wp);
$scope.table.timelineController.startAddRelationPredecessor(workPackage);
break;
case 'relation-follows':
table = $scope.table;
wp = $scope.row.object;
table.timelineController.startAddRelationFollower(wp);
$scope.table.timelineController.startAddRelationFollower(workPackage);
break;
case 'relation-new-child':
wp = $scope.row.object;
wpRelationsHierarchyService.addNewChildWp(wp);
wpRelationsHierarchyService.addNewChildWp(workPackage);
break;
default:
@ -145,15 +141,14 @@ function wpContextMenuController($scope:any,
}
function getSelectedWorkPackages() {
let workPackagefromContext = $scope.row.object;
let selectedWorkPackages = wpTableSelection.getSelectedWorkPackages();
if (selectedWorkPackages.length === 0) {
return [workPackagefromContext];
return [workPackage];
}
if (selectedWorkPackages.indexOf(workPackagefromContext) === -1) {
selectedWorkPackages.push(workPackagefromContext);
if (selectedWorkPackages.indexOf(workPackage) === -1) {
selectedWorkPackages.push(workPackage);
}
return selectedWorkPackages;

@ -7,13 +7,13 @@
</a>
</li>
<li ng-if="!row.object.isNew" class="open detailsViewMenuItem">
<a role="menuitem" focus ui-sref="work-packages.list.details.overview({workPackageId: row.object.id})">
<a role="menuitem" focus ui-sref="work-packages.list.details.overview({workPackageId: workPackageId})">
<op-icon icon-classes="icon-action-menu icon-view-split"></op-icon>
<span ng-bind="I18n.t('js.button_open_details')"/>
</a>
</li>
<li ng-if="!row.object.isNew" class="openFullScreenView">
<a role="menuitem" ui-sref="work-packages.show({workPackageId: row.object.id})">
<a role="menuitem" ui-sref="work-packages.show({workPackageId: workPackageId})">
<op-icon icon-classes="icon-action-menu icon-view-fullscreen"></op-icon>
<span ng-bind="I18n.t('js.button_open_fullscreen')"/>
</a>

@ -26,6 +26,7 @@
// See doc/COPYRIGHT.rdoc for more details.
// ++
import {States} from '../../states.service';
describe('workPackageContextMenu', () => {
var container:any;
var contextMenu;
@ -34,6 +35,12 @@ describe('workPackageContextMenu', () => {
var setSelection:any;
var ngContextMenu;
var wpTableSelection;
var states:States;
var workPackage:any = {
id: 123,
update: '/work_packages/123/edit',
move: '/work_packages/move/new?ids%5B%5D=123',
};
beforeEach(angular.mock.module('ng-context-menu',
'openproject',
@ -60,11 +67,13 @@ describe('workPackageContextMenu', () => {
container = angular.element('<div></div>');
});
beforeEach(angular.mock.inject((_$rootScope_:any, _ngContextMenu_:any, _wpTableSelection_:any, $templateCache:any) => {
beforeEach(angular.mock.inject((_$rootScope_:any, _states_:States, _ngContextMenu_:any, _wpTableSelection_:any, $templateCache:any) => {
wpTableSelection = _wpTableSelection_;
$rootScope = _$rootScope_;
ngContextMenu = _ngContextMenu_;
states = _states_;
states.workPackages.get('123').putValue(workPackage);
sinon.stub(wpTableSelection, 'getSelectedWorkPackages').returns([]);
sinon.stub(wpTableSelection, 'isSelected').returns(false);
@ -81,17 +90,12 @@ describe('workPackageContextMenu', () => {
templateUrl: 'work_package_context_menu.html'
});
contextMenu.open({x: 0, y: 0});
contextMenu.open({x: 0, y: 0}, { workPackageId: '123', rowIndex: 1 });
}));
describe('when the context menu context contains one work package', () => {
var I18n:any;
var actions = ['edit', 'move'];
var workPackage:any = {
id: 123,
update: '/work_packages/123/edit',
move: '/work_packages/move/new?ids%5B%5D=123',
}
var directListElements:any;
@ -104,9 +108,6 @@ describe('workPackageContextMenu', () => {
}));
beforeEach(() => {
$rootScope.rows = [];
$rootScope.row = {object: workPackage};
$rootScope.$digest();
directListElements = container.find('.dropdown-menu > li:not(.folder)');
@ -125,15 +126,15 @@ describe('workPackageContextMenu', () => {
});
it('sets the checked property of the row within the context to true', () => {
expect(setSelection).to.have.been.calledWith($rootScope.row);
expect(setSelection).to.have.been.calledWith('123', 1);
});
describe('when delete is permitted on a work package', () => {
workPackage['delete'] = '/work_packages/bulk';
beforeEach(() => {
$rootScope.wpId = '123';
$rootScope.rows = [];
$rootScope.row = {object: workPackage};
$rootScope.$digest();
directListElements = container.find('.dropdown-menu > li:not(.folder)');

@ -22,8 +22,8 @@ export class GroupedRenderPass extends PlainRenderPass {
*/
protected doRender() {
let currentGroup:GroupObject | null = null;
this.workPackageTable.rows.forEach((wpId:string) => {
let row = this.workPackageTable.rowIndex[wpId];
this.workPackageTable.originalRows.forEach((wpId:string) => {
let row = this.workPackageTable.originalRowIndex[wpId];
let nextGroup = this.matchingGroup(row.object);
if (nextGroup && currentGroup !== nextGroup) {

@ -52,8 +52,8 @@ export class HierarchyRenderPass extends PrimaryRenderPass {
* Render the hierarchy table into the document fragment
*/
protected doRender() {
this.workPackageTable.rows.forEach((wpId:string) => {
const row:WorkPackageTableRow = this.workPackageTable.rowIndex[wpId];
this.workPackageTable.originalRows.forEach((wpId:string) => {
const row:WorkPackageTableRow = this.workPackageTable.originalRowIndex[wpId];
const workPackage:WorkPackageResourceInterface = row.object;
// If we need to defer this row, skip it for now
@ -94,7 +94,7 @@ export class HierarchyRenderPass extends PrimaryRenderPass {
// Will only defer is parent is
// 1. existent in the table results
// 1. yet to be rendered
if (this.workPackageTable.rowIndex[parent.id] === undefined || this.rendered[parent.id]) {
if (this.workPackageTable.originalRowIndex[parent.id] === undefined || this.rendered[parent.id]) {
return false;
}
@ -117,7 +117,7 @@ export class HierarchyRenderPass extends PrimaryRenderPass {
// run them through the callback
deferredChildren.forEach((child:WorkPackageResourceInterface) => {
// Callback on the child itself
const row:WorkPackageTableRow = this.workPackageTable.rowIndex[child.id];
const row:WorkPackageTableRow = this.workPackageTable.originalRowIndex[child.id];
this.insertUnderParent(row, child.parent);
// Descend into any children the child WP might have and callback

@ -13,9 +13,9 @@ export class PlainRenderPass extends PrimaryRenderPass {
* The actual render function of this renderer.
*/
protected doRender():void {
this.workPackageTable.rows.forEach((wpId:string) => {
let row = this.workPackageTable.rowIndex[wpId];
let [tr, _hidden] = this.rowBuilder.buildEmpty(row.object);
this.workPackageTable.originalRows.forEach((wpId:string) => {
let row = this.workPackageTable.originalRowIndex[wpId];
let [tr,] = this.rowBuilder.buildEmpty(row.object);
row.element = tr;
this.appendRow(row.object, tr);
this.tableBody.appendChild(tr);

@ -41,6 +41,7 @@ export class RowClickHandler implements TableEventHandler {
// Locate the row from event
let element = target.closest(this.SELECTOR);
let wpId = element.data('workPackageId');
let classIdentifier = element.data('classIdentifier');
if (!wpId) {
return true;
@ -54,22 +55,22 @@ export class RowClickHandler implements TableEventHandler {
// The current row is the last selected work package
// not matter what other rows are (de-)selected below.
// Thus save that row for the details view button.
let row = table.rowObject(wpId);
let [index, row] = table.findRenderedRow(classIdentifier);
this.states.focusedWorkPackage.putValue(wpId);
// Update single selection if no modifier present
if (!(evt.ctrlKey || evt.metaKey || evt.shiftKey)) {
this.wpTableSelection.setSelection(row);
this.wpTableSelection.setSelection(wpId, index);
}
// Multiple selection if shift present
if (evt.shiftKey) {
this.wpTableSelection.setMultiSelectionFrom(table.rows, row);
this.wpTableSelection.setMultiSelectionFrom(table.renderedRows, wpId, index);
}
// Single selection expansion if ctrl / cmd(mac)
if (evt.ctrlKey || evt.metaKey) {
this.wpTableSelection.toggleRow(row.workPackageId);
this.wpTableSelection.toggleRow(wpId);
}
return false;

@ -47,8 +47,8 @@ export class ContextMenuHandler implements TableEventHandler {
return false;
}
let row = table.rowObject(element.data('workPackageId'));
this.contextMenu.activate('WorkPackageContextMenu', evt, {row: row, table: table});
let [index,] = table.findRenderedRow(element.data('workPackageId'));
this.contextMenu.activate('WorkPackageContextMenu', evt, {workPackageId: wpId, rowIndex: index, table: table});
return false;
}
}

@ -9,7 +9,7 @@ export class ContextMenuKeyboardHandler implements TableEventHandler {
// Injections
public contextMenu:ContextMenuService;
constructor(table: WorkPackageTable) {
constructor(private table:WorkPackageTable) {
injectorBridge(this);
}
@ -25,7 +25,7 @@ export class ContextMenuKeyboardHandler implements TableEventHandler {
return jQuery(table.tbody);
}
public handleEvent(table: WorkPackageTable, evt:JQueryEventObject):boolean {
public handleEvent(table:WorkPackageTable, evt:JQueryEventObject):boolean {
let target = jQuery(evt.target);
if (!(evt.keyCode === keyCodes.F10 && evt.shiftKey && evt.altKey)) {
@ -36,13 +36,14 @@ export class ContextMenuKeyboardHandler implements TableEventHandler {
evt.stopPropagation();
// Locate the row from event
let element = target.closest(this.SELECTOR);
let row = table.rowObject(element.data('workPackageId'));
const element = target.closest(this.SELECTOR);
const wpId = element.data('workPackageId');
const [index,] = table.findRenderedRow(element.data('workPackageId'));
// Set position args to open at element
let position = { of: target };
this.contextMenu.activate('WorkPackageContextMenu', evt, { row: row }, position);
this.contextMenu.activate('WorkPackageContextMenu', evt, { workPackageId: wpId, rowIndex: index, table: this.table}, position);
return false;
}
}

@ -41,7 +41,7 @@ export class RowDoubleClickHandler implements TableEventHandler {
// Locate the row from event
let element = target.closest(this.SELECTOR);
let row = table.rowObject(element.data('workPackageId'));
let wpId = element.data('workPackageId');
// Ignore links
if (target.is('a') || target.parent().is('a')) {
@ -49,11 +49,11 @@ export class RowDoubleClickHandler implements TableEventHandler {
}
// Save the currently focused work package
this.states.focusedWorkPackage.putValue(row.workPackageId);
this.states.focusedWorkPackage.putValue(wpId);
this.$state.go(
'work-packages.show',
{ workPackageId: row.workPackageId }
{ workPackageId: wpId }
);
return false;

@ -16,7 +16,7 @@ export class ColumnsTransformer {
.filter(() => this.wpTableColumns.hasRelationColumns() === false)
.takeUntil(this.states.table.stopAllSubscriptions)
.subscribe(() => {
if (table.rows.length > 0) {
if (table.originalRows.length > 0) {
var t0 = performance.now();
// Redraw the table section, ignore timeline

@ -22,7 +22,7 @@ export class SelectionTransformer {
// Bind CTRL+A to select all work packages
Mousetrap.bind(['command+a', 'ctrl+a'], (e) => {
this.wpTableSelection.selectAll(table.rows);
this.wpTableSelection.selectAll(table.renderedRows);
e.preventDefault();
return false;

@ -29,8 +29,8 @@ export function hasChildrenInTable(workPackage:WorkPackageResourceInterface, tab
}
// Return if this work package is in the ancestor chain of any of the work packages
return !!_.find(table.rows, (wpId:string) => {
const row = table.rowIndex[wpId].object;
return !!_.find(table.originalRows, (wpId:string) => {
const row = table.originalRowIndex[wpId].object;
return row.ancestorIds.indexOf(workPackage.id.toString()) >= 0;
});

@ -3,6 +3,7 @@ import {opServicesModule} from '../../../angular-modules';
import {WPTableRowSelectionState, WorkPackageTableRow} from '../wp-table.interfaces';
import {WorkPackageResource} from '../../api/api-v3/hal-resources/work-package-resource.service';
import {InputState} from "reactivestates";
import {RenderedRow} from '../builders/primary-render-pass';
export class WorkPackageTableSelection {
@ -23,11 +24,13 @@ export class WorkPackageTableSelection {
/**
* Select all work packages
*/
public selectAll(rows: string[]) {
public selectAll(rows: RenderedRow[]) {
const state:WPTableRowSelectionState = this._emptyState;
rows.forEach((workPackageId:string) => {
state.selected[workPackageId] = true;
rows.forEach((row) => {
if (row.workPackageId) {
state.selected[row.workPackageId] = true;
}
});
this.selectionState.putValue(state);
@ -109,12 +112,12 @@ export class WorkPackageTableSelection {
/**
* Override current selection with the given work package id.
*/
public setSelection(row:WorkPackageTableRow) {
public setSelection(wpId:string, position:number) {
let state:WPTableRowSelectionState = {
selected: {},
activeRowIndex: row.position
activeRowIndex: position
};
state.selected[row.workPackageId] = true;
state.selected[wpId] = true;
this.selectionState.putValue(state);
}
@ -123,21 +126,21 @@ export class WorkPackageTableSelection {
* Select a number of rows from the current `activeRowIndex`
* to the selected target.
* (aka shift click expansion)
* @param rows Current visible rows
* @param selected Selection target
*/
public setMultiSelectionFrom(rows:string[], selected:WorkPackageTableRow) {
public setMultiSelectionFrom(rows:RenderedRow[], wpId:string, position:number) {
let state = this.currentState;
if (this.selectionCount === 0) {
state.selected[selected.workPackageId] = true;
state.activeRowIndex = selected.position;
state.selected[wpId] = true;
state.activeRowIndex = position;
} else if (state.activeRowIndex !== null) {
let start = Math.min(selected.position, state.activeRowIndex);
let end = Math.max(selected.position, state.activeRowIndex);
let start = Math.min(position, state.activeRowIndex);
let end = Math.max(position, state.activeRowIndex);
rows.forEach((workPackageId, i) => {
state.selected[workPackageId] = i >= start && i <= end;
rows.forEach((row, i) => {
if (row.workPackageId) {
state.selected[row.workPackageId] = i >= start && i <= end;
}
});
}

@ -15,7 +15,7 @@ import {GroupedRowsBuilder} from './builders/modes/grouped/grouped-rows-builder'
import {HierarchyRowsBuilder} from './builders/modes/hierarchy/hierarchy-rows-builder';
import {RowsBuilder} from './builders/modes/rows-builder';
import {WorkPackageTimelineTableController} from '../wp-table/timeline/container/wp-timeline-container.directive';
import {PrimaryRenderPass} from './builders/primary-render-pass';
import {PrimaryRenderPass, RenderedRow} from './builders/primary-render-pass';
import {debugLog} from '../../helpers/debug_output';
export class WorkPackageTable {
@ -23,8 +23,8 @@ export class WorkPackageTable {
public states:States;
public I18n:op.I18n;
public rows: string[] = [];
public rowIndex:{[id: string]: WorkPackageTableRow} = {};
public originalRows: string[] = [];
public originalRowIndex:{[id: string]: WorkPackageTableRow} = {};
// WP rows builder
// Ordered by priority
@ -45,8 +45,14 @@ export class WorkPackageTable {
TableHandlerRegistry.attachTo(this);
}
public rowObject(workPackageId:string):WorkPackageTableRow {
return this.rowIndex[workPackageId];
public get renderedRows() {
return this.states.table.rendered.getValueOr([]);
}
public findRenderedRow(classIdentifier:string):[number, RenderedRow] {
const index = _.findIndex(this.renderedRows, (row) => row.classIdentifier === classIdentifier);
return [index, this.renderedRows[index]];
}
public get rowBuilder():RowsBuilder {
@ -58,10 +64,10 @@ export class WorkPackageTable {
* @param rows
*/
private buildIndex(rows:WorkPackageResource[]) {
this.rowIndex = {};
this.rows = rows.map((wp:WorkPackageResource, i:number) => {
this.originalRowIndex = {};
this.originalRows = rows.map((wp:WorkPackageResource, i:number) => {
let wpId = wp.id;
this.rowIndex[wpId] = <WorkPackageTableRow> { object: wp, workPackageId: wpId, position: i };
this.originalRowIndex[wpId] = <WorkPackageTableRow> { object: wp, workPackageId: wpId, position: i };
return wpId;
});
}
@ -77,8 +83,8 @@ export class WorkPackageTable {
this.redrawTableAndTimeline();
// Preselect first work package as focused
if (this.rows.length && this.states.focusedWorkPackage.isPristine()) {
this.states.focusedWorkPackage.putValue(this.rows[0]);
if (this.originalRows.length && this.states.focusedWorkPackage.isPristine()) {
this.states.focusedWorkPackage.putValue(this.originalRows[0]);
}
}

@ -26,7 +26,6 @@
// See doc/COPYRIGHT.rdoc for more details.
//++
import {WorkPackageResourceInterface} from './../../api/api-v3/hal-resources/work-package-resource.service';
import {States} from "../../states.service";
import {WorkPackageTableTimelineService} from "../../wp-fast-table/state/wp-table-timeline.service";
angular
@ -38,8 +37,7 @@ function WorkPackageContextMenuHelper(
UrlParamsHelper:any,
wpTableTimeline:WorkPackageTableTimelineService,
PathHelper:any,
I18n: op.I18n,
states: States) {
I18n: op.I18n) {
const BULK_ACTIONS = [
{

Loading…
Cancel
Save