From 49838f3b0cb3e2110a63c3b796b360940f76e3c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 13 Dec 2017 08:26:13 +0100 Subject: [PATCH] [26575] Control initial focussing through wpTableFocusService (#6064) https://community.openproject.com/wp/26575 [ci skip] --- .../wp-destroy-modal.controller.ts | 6 +- .../components/routing/first-route-service.ts | 48 +++++++++++ .../components/routing/ui-router.config.ts | 7 ++ .../wp-details/wp-details.controller.ts | 19 +++-- .../routing/wp-show/wp-show.controller.ts | 5 ++ .../wp-view-base/wp-view-base.controller.ts | 5 +- frontend/app/components/states.service.ts | 3 +- .../wp-details-view-button.directive.ts | 4 +- .../wp-view-button.directive.ts | 4 +- .../wp-edit-field-group.directive.ts | 4 +- .../handlers/row/click-handler.ts | 12 +-- .../handlers/row/double-click-handler.ts | 11 +-- .../handlers/row/wp-state-links-handler.ts | 6 +- .../handlers/state/hierarchy-transformer.ts | 20 +---- .../handlers/state/selection-transformer.ts | 27 +++---- .../helpers/wp-table-row-helpers.ts | 20 +++++ .../state/wp-table-focus.service.ts | 80 +++++++++++++++++++ .../state/wp-table-selection.service.ts | 50 ++---------- .../wp-fast-table/wp-table.interfaces.ts | 4 +- .../wp-inline-create.directive.ts | 4 +- .../select_work_package_row_spec.rb | 4 +- 21 files changed, 230 insertions(+), 113 deletions(-) create mode 100644 frontend/app/components/routing/first-route-service.ts create mode 100644 frontend/app/components/wp-fast-table/state/wp-table-focus.service.ts diff --git a/frontend/app/components/modals/wp-destroy-modal/wp-destroy-modal.controller.ts b/frontend/app/components/modals/wp-destroy-modal/wp-destroy-modal.controller.ts index 47bb474aa3..94a397fd54 100644 --- a/frontend/app/components/modals/wp-destroy-modal/wp-destroy-modal.controller.ts +++ b/frontend/app/components/modals/wp-destroy-modal/wp-destroy-modal.controller.ts @@ -33,6 +33,7 @@ import {WorkPackageNotificationService} from '../../wp-edit/wp-notification.serv import {QueryResource} from '../../api/api-v3/hal-resources/query-resource.service'; import {QueryDmService} from '../../api/api-v3/hal-resource-dms/query-dm.service'; import {WorkPackageResourceInterface} from '../../api/api-v3/hal-resources/work-package-resource.service'; +import {WorkPackageTableFocusService} from 'core-components/wp-fast-table/state/wp-table-focus.service'; export class WorkPackageDestroyModalController { public text:any; @@ -43,6 +44,7 @@ export class WorkPackageDestroyModalController { private $state:ng.ui.IStateService, private states:States, private WorkPackageService:any, + private wpTableFocus:WorkPackageTableFocusService, private I18n:op.I18n, private wpDestroyModal:any) { @@ -84,8 +86,8 @@ export class WorkPackageDestroyModalController { this.wpDestroyModal.deactivate(); this.WorkPackageService.performBulkDelete(this.workPackages.map(el => el.id), true) .then(() => { - this.close() - this.states.focusedWorkPackage.clear(); + this.close(); + this.wpTableFocus.clear(); this.$state.go('work-packages.list'); }); } diff --git a/frontend/app/components/routing/first-route-service.ts b/frontend/app/components/routing/first-route-service.ts new file mode 100644 index 0000000000..46a176acb5 --- /dev/null +++ b/frontend/app/components/routing/first-route-service.ts @@ -0,0 +1,48 @@ + +// -- copyright +// OpenProject is a project management system. +// Copyright (C) 2012-2015 the OpenProject Foundation (OPF) +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License version 3. +// +// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +// Copyright (C) 2006-2013 Jean-Philippe Lang +// Copyright (C) 2010-2013 the ChiliProject Team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// +// See doc/COPYRIGHT.rdoc for more details. +// ++ + +import {opServicesModule} from '../../angular-modules'; + +export class FirstRouteService { + public name:string; + public params:any; + + constructor() {} + + public setIfFirst(state:ng.ui.IState, params:any) { + if (this.name) { + return; + } + + this.name = state.name!; + this.params = params; + } +} + +opServicesModule.service('firstRoute', FirstRouteService); diff --git a/frontend/app/components/routing/ui-router.config.ts b/frontend/app/components/routing/ui-router.config.ts index 546b612c64..762c6189ae 100644 --- a/frontend/app/components/routing/ui-router.config.ts +++ b/frontend/app/components/routing/ui-router.config.ts @@ -27,6 +27,7 @@ // ++ import {openprojectModule} from '../../angular-modules'; +import {FirstRouteService} from 'app/components/routing/first-route-service'; const panels = { get overview() { @@ -198,6 +199,7 @@ openprojectModule .run(($location:ng.ILocationService, $rootElement:ng.IRootElementService, + firstRoute:FirstRouteService, $timeout:ng.ITimeoutService, $rootScope:ng.IRootScopeService, $state:ng.ui.IStateService, @@ -236,7 +238,12 @@ openprojectModule evt.preventDefault(); }); + $rootScope.$on('$stateChangeStart', (event, toState, toParams) => { + // We need to distinguish between actions that should run on the initial page load + // (ie. openining a new tab in the details view should focus on the element in the table) + // so we need to know which route we visited initially + firstRoute.setIfFirst(toState, toParams); $rootScope.$emit('notifications.clearAll'); diff --git a/frontend/app/components/routing/wp-details/wp-details.controller.ts b/frontend/app/components/routing/wp-details/wp-details.controller.ts index 74804212b6..49f3034000 100644 --- a/frontend/app/components/routing/wp-details/wp-details.controller.ts +++ b/frontend/app/components/routing/wp-details/wp-details.controller.ts @@ -33,24 +33,29 @@ import {WorkPackageTableSelection} from "../../wp-fast-table/state/wp-table-sele import {KeepTabService} from "../../wp-panels/keep-tab/keep-tab.service"; import {WorkPackageViewController} from "../wp-view-base/wp-view-base.controller"; import {WorkPackageEditingService} from '../../wp-edit-form/work-package-editing-service'; +import {FirstRouteService} from "core-components/routing/first-route-service"; +import {WorkPackageTableFocusService} from "core-components/wp-fast-table/state/wp-table-focus.service"; export class WorkPackageDetailsController extends WorkPackageViewController { constructor(public $scope:ng.IScope, public states:States, + public firstRoute:FirstRouteService, public keepTab:KeepTabService, public wpTableSelection:WorkPackageTableSelection, + public wpTableFocus:WorkPackageTableFocusService, public $state:ng.ui.IStateService) { super($scope, $state.params['workPackageId']); this.observeWorkPackage(); let wpId = $state.params['workPackageId']; - let focusState = this.states.focusedWorkPackage; - let focusedWP = focusState.value; + let focusedWP = this.wpTableFocus.focusedWorkPackage; if (!focusedWP) { - focusState.putValue(wpId); - this.wpTableSelection.setRowState(wpId, true); + // Focus on the work package if we're the first route + const isFirstRoute = firstRoute.name === 'work-packages.list.details.overview'; + const isSameID = firstRoute.params && wpId === firstRoute.params.workPackageI; + this.wpTableFocus.updateFocus(wpId, (isFirstRoute && isSameID)); } if (this.wpTableSelection.isEmpty) { @@ -59,10 +64,8 @@ export class WorkPackageDetailsController extends WorkPackageViewController { scopedObservable( $scope, - this.states.focusedWorkPackage.values$()) - .map(wpId => wpId.toString()) - .distinctUntilChanged() - .subscribe((newId) => { + this.wpTableFocus.whenChanged() + ).subscribe(newId => { if (wpId !== newId && $state.includes('work-packages.list.details')) { $state.go( ($state.current.name as string), diff --git a/frontend/app/components/routing/wp-show/wp-show.controller.ts b/frontend/app/components/routing/wp-show/wp-show.controller.ts index 8411b1232f..28017c3220 100644 --- a/frontend/app/components/routing/wp-show/wp-show.controller.ts +++ b/frontend/app/components/routing/wp-show/wp-show.controller.ts @@ -33,6 +33,7 @@ import {WorkPackageResourceInterface} from "../../api/api-v3/hal-resources/work- import {WorkPackageViewController} from "../wp-view-base/wp-view-base.controller"; import {WorkPackagesListChecksumService} from "../../wp-list/wp-list-checksum.service"; import {WorkPackageMoreMenuService} from '../../work-packages/work-package-more-menu.service' +import {WorkPackageTableFocusService} from "core-components/wp-fast-table/state/wp-table-focus.service"; export class WorkPackageShowController extends WorkPackageViewController { @@ -52,6 +53,7 @@ export class WorkPackageShowController extends WorkPackageViewController { constructor(public $scope:ng.IScope, public $state:ng.ui.IStateService, + public wpTableFocus:WorkPackageTableFocusService, protected wpMoreMenuService:WorkPackageMoreMenuService) { super($scope, $state.params['workPackageId']); this.observeWorkPackage(); @@ -60,6 +62,9 @@ export class WorkPackageShowController extends WorkPackageViewController { protected init() { super.init(); + // Set Focused WP + this.wpTableFocus.updateFocus(this.workPackage.id); + // initialization this.wpMoreMenu = new (this.wpMoreMenuService as any)(this.workPackage); diff --git a/frontend/app/components/routing/wp-view-base/wp-view-base.controller.ts b/frontend/app/components/routing/wp-view-base/wp-view-base.controller.ts index 199dafd6ea..fd8c55b78d 100644 --- a/frontend/app/components/routing/wp-view-base/wp-view-base.controller.ts +++ b/frontend/app/components/routing/wp-view-base/wp-view-base.controller.ts @@ -35,6 +35,7 @@ import {KeepTabService} from '../../wp-panels/keep-tab/keep-tab.service'; import {WorkPackageTableRefreshService} from '../../wp-table/wp-table-refresh-request.service'; import {$injectFields} from '../../angular/angular-injector-bridge.functions'; import {WorkPackageEditingService} from '../../wp-edit-form/work-package-editing-service'; +import {WorkPackageTableFocusService} from 'core-components/wp-fast-table/state/wp-table-focus.service'; export class WorkPackageViewController { @@ -49,6 +50,7 @@ export class WorkPackageViewController { protected I18n:op.I18n; protected wpTableRefresh:WorkPackageTableRefreshService; protected wpEditing:WorkPackageEditingService; + protected wpTableFocus:WorkPackageTableFocusService; // Helper promise to detect when the controller has been initialized // (when a WP has loaded). @@ -67,7 +69,7 @@ export class WorkPackageViewController { constructor(public $scope:ng.IScope, protected workPackageId:string) { $injectFields(this, '$q', '$state', 'keepTab', 'wpCacheService', 'WorkPackageService', - 'states', 'wpEditing', 'PathHelper', 'I18n', 'wpTableRefresh'); + 'states', 'wpEditing', 'PathHelper', 'I18n', 'wpTableRefresh', 'wpTableFocus'); this.initialized = this.$q.defer(); this.initializeTexts(); @@ -111,7 +113,6 @@ export class WorkPackageViewController { // Preselect this work package for future list operations this.showStaticPagePath = this.PathHelper.workPackagePath(this.workPackage); - this.states.focusedWorkPackage.putValue(this.workPackage.id); // Listen to tab changes to update the tab label scopedObservable(this.$scope, this.keepTab.observable).subscribe((tabs:any) => { diff --git a/frontend/app/components/states.service.ts b/frontend/app/components/states.service.ts index aebeeceb3e..0549fc0ee8 100644 --- a/frontend/app/components/states.service.ts +++ b/frontend/app/components/states.service.ts @@ -37,6 +37,7 @@ import {QuerySortByResource} from './api/api-v3/hal-resources/query-sort-by-reso import {QueryGroupByResource} from './api/api-v3/hal-resources/query-group-by-resource.service'; import {WPTableRowSelectionState} from './wp-fast-table/wp-table.interfaces'; import {WorkPackageTableRelationColumns} from './wp-fast-table/wp-table-relation-columns'; +import {WPFocusState} from 'core-components/wp-fast-table/state/wp-table-focus.service'; export class States extends StatesGroup { @@ -62,7 +63,7 @@ export class States extends StatesGroup { updates = new UserUpdaterStates(this); // Current focused work package (e.g, row preselected for details button) - focusedWorkPackage = input(); + focusedWorkPackage = input(); } diff --git a/frontend/app/components/wp-buttons/wp-details-view-button/wp-details-view-button.directive.ts b/frontend/app/components/wp-buttons/wp-details-view-button/wp-details-view-button.directive.ts index 8312ce624f..f5a52e69e6 100644 --- a/frontend/app/components/wp-buttons/wp-details-view-button/wp-details-view-button.directive.ts +++ b/frontend/app/components/wp-buttons/wp-details-view-button/wp-details-view-button.directive.ts @@ -30,6 +30,7 @@ import {wpButtonsModule} from '../../../angular-modules'; import {WorkPackageButtonController, wpButtonDirective} from '../wp-buttons.module'; import {KeepTabService} from '../../wp-panels/keep-tab/keep-tab.service'; import {States} from '../../states.service'; +import {WorkPackageTableFocusService} from 'core-components/wp-fast-table/state/wp-table-focus.service'; export class WorkPackageDetailsViewButtonController extends WorkPackageButtonController { @@ -49,6 +50,7 @@ export class WorkPackageDetailsViewButtonController extends WorkPackageButtonCon public states:States, public I18n:op.I18n, public loadingIndicator:any, + public wpTableFocus:WorkPackageTableFocusService, public keepTab:KeepTabService) { 'ngInject'; super(I18n); @@ -92,7 +94,7 @@ export class WorkPackageDetailsViewButtonController extends WorkPackageButtonCon public openDetailsView() { var params = { - workPackageId: this.states.focusedWorkPackage.value, + workPackageId: this.wpTableFocus.focusedWorkPackage, projectPath: this.projectIdentifier, }; diff --git a/frontend/app/components/wp-buttons/wp-view-button/wp-view-button.directive.ts b/frontend/app/components/wp-buttons/wp-view-button/wp-view-button.directive.ts index 60dc80c6f4..fd95052a06 100644 --- a/frontend/app/components/wp-buttons/wp-view-button/wp-view-button.directive.ts +++ b/frontend/app/components/wp-buttons/wp-view-button/wp-view-button.directive.ts @@ -30,6 +30,7 @@ import {wpButtonsModule} from '../../../angular-modules'; import {WorkPackageNavigationButtonController, wpButtonDirective} from '../wp-buttons.module'; import {KeepTabService} from '../../wp-panels/keep-tab/keep-tab.service'; import {States} from '../../states.service'; +import {WorkPackageTableFocusService} from 'core-components/wp-fast-table/state/wp-table-focus.service'; export class WorkPackageViewButtonController extends WorkPackageNavigationButtonController { public workPackageId:number; @@ -43,6 +44,7 @@ export class WorkPackageViewButtonController extends WorkPackageNavigationButton public $state:ng.ui.IStateService, public states:States, public I18n:op.I18n, + public wpTableFocus:WorkPackageTableFocusService, public keepTab:KeepTabService) { 'ngInject'; @@ -59,7 +61,7 @@ export class WorkPackageViewButtonController extends WorkPackageNavigationButton public openWorkPackageShowView() { let args = ['work-packages.new', this.$state.params]; - let id = this.$state.params['workPackageId'] || this.workPackageId || this.states.focusedWorkPackage.value; + let id = this.$state.params['workPackageId'] || this.workPackageId || this.wpTableFocus.focusedWorkPackage; if (!this.$state.is('work-packages.list.new')) { let params = { diff --git a/frontend/app/components/wp-edit/wp-edit-field/wp-edit-field-group.directive.ts b/frontend/app/components/wp-edit/wp-edit-field/wp-edit-field-group.directive.ts index fa9f64b0ae..1c507b1cca 100644 --- a/frontend/app/components/wp-edit/wp-edit-field/wp-edit-field-group.directive.ts +++ b/frontend/app/components/wp-edit/wp-edit-field/wp-edit-field-group.directive.ts @@ -38,6 +38,7 @@ import {WorkPackageResourceInterface} from '../../api/api-v3/hal-resources/work- import {WorkPackageTableSelection} from '../../wp-fast-table/state/wp-table-selection.service'; import {WorkPackageNotificationService} from '../wp-notification.service'; import {WorkPackageCreateService} from './../../wp-create/wp-create.service'; +import {WorkPackageTableFocusService} from 'core-components/wp-fast-table/state/wp-table-focus.service'; export class WorkPackageEditFieldGroupController { public workPackage:WorkPackageResourceInterface; @@ -55,6 +56,7 @@ export class WorkPackageEditFieldGroupController { protected wpEditing:WorkPackageEditingService, protected wpNotificationsService:WorkPackageNotificationService, protected wpTableSelection:WorkPackageTableSelection, + protected wpTableFocus:WorkPackageTableFocusService, protected $rootScope:ng.IRootScopeService, protected $window:ng.IWindowService, protected ConfigurationService:any, @@ -172,7 +174,7 @@ export class WorkPackageEditFieldGroupController { if (this.successState) { this.$state.go(this.successState, {workPackageId: savedWorkPackage.id}) .then(() => { - this.wpTableSelection.focusOn(savedWorkPackage.id); + this.wpTableFocus.updateFocus(savedWorkPackage.id); this.wpNotificationsService.showSave(savedWorkPackage, isInitial); }); } diff --git a/frontend/app/components/wp-fast-table/handlers/row/click-handler.ts b/frontend/app/components/wp-fast-table/handlers/row/click-handler.ts index f59a9a62ae..9421a7772c 100644 --- a/frontend/app/components/wp-fast-table/handlers/row/click-handler.ts +++ b/frontend/app/components/wp-fast-table/handlers/row/click-handler.ts @@ -7,6 +7,7 @@ import {WorkPackageTableSelection} from '../../state/wp-table-selection.service' import {tableRowClassName} from '../../builders/rows/single-row-builder'; import {tdClassName} from '../../builders/cell-builder'; import {KeepTabService} from "../../../wp-panels/keep-tab/keep-tab.service"; +import {WorkPackageTableFocusService} from 'core-components/wp-fast-table/state/wp-table-focus.service'; export class RowClickHandler implements TableEventHandler { // Injections @@ -14,12 +15,13 @@ export class RowClickHandler implements TableEventHandler { public states:States; public keepTab:KeepTabService; public wpTableSelection:WorkPackageTableSelection; + public wpTableFocus:WorkPackageTableFocusService; private clicks = 0; private timer:number; constructor(table:WorkPackageTable) { - $injectFields(this, 'keepTab', '$state', 'states', 'wpTableSelection'); + $injectFields(this, 'keepTab', '$state', 'states', 'wpTableSelection', 'wpTableFocus'); } public get EVENT() { @@ -63,11 +65,7 @@ export class RowClickHandler implements TableEventHandler { return true; } - // 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 [index, row] = table.findRenderedRow(classIdentifier); - this.states.focusedWorkPackage.putValue(wpId); // Update single selection if no modifier present if (!(evt.ctrlKey || evt.metaKey || evt.shiftKey)) { @@ -84,6 +82,10 @@ export class RowClickHandler implements TableEventHandler { this.wpTableSelection.toggleRow(wpId); } + // 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. + this.wpTableFocus.updateFocus(wpId); return false; } } diff --git a/frontend/app/components/wp-fast-table/handlers/row/double-click-handler.ts b/frontend/app/components/wp-fast-table/handlers/row/double-click-handler.ts index 68228ea636..1acde4f8fa 100644 --- a/frontend/app/components/wp-fast-table/handlers/row/double-click-handler.ts +++ b/frontend/app/components/wp-fast-table/handlers/row/double-click-handler.ts @@ -1,20 +1,22 @@ import {debugLog} from '../../../../helpers/debug_output'; -import {injectorBridge} from '../../../angular/angular-injector-bridge.functions'; +import {$injectFields} from '../../../angular/angular-injector-bridge.functions'; import {WorkPackageTable} from '../../wp-fast-table'; import {States} from '../../../states.service'; import {TableEventHandler} from '../table-handler-registry'; import {WorkPackageTableSelection} from '../../state/wp-table-selection.service'; import {tableRowClassName} from '../../builders/rows/single-row-builder'; import {tdClassName} from '../../builders/cell-builder'; +import {WorkPackageTableFocusService} from 'core-components/wp-fast-table/state/wp-table-focus.service'; export class RowDoubleClickHandler implements TableEventHandler { // Injections public $state:ng.ui.IStateService; public states:States; public wpTableSelection:WorkPackageTableSelection; + public wpTableFocus:WorkPackageTableFocusService; - constructor(table: WorkPackageTable) { - injectorBridge(this); + constructor(table:WorkPackageTable) { + $injectFields(this, '$state', 'states', 'wpTableSelection', 'wpTableFocus'); } public get EVENT() { @@ -49,7 +51,7 @@ export class RowDoubleClickHandler implements TableEventHandler { } // Save the currently focused work package - this.states.focusedWorkPackage.putValue(wpId); + this.wpTableFocus.updateFocus(wpId); this.$state.go( 'work-packages.show', @@ -60,4 +62,3 @@ export class RowDoubleClickHandler implements TableEventHandler { } } -RowDoubleClickHandler.$inject = ['$state', 'states', 'wpTableSelection']; diff --git a/frontend/app/components/wp-fast-table/handlers/row/wp-state-links-handler.ts b/frontend/app/components/wp-fast-table/handlers/row/wp-state-links-handler.ts index 87a993fd7b..79d761ca6a 100644 --- a/frontend/app/components/wp-fast-table/handlers/row/wp-state-links-handler.ts +++ b/frontend/app/components/wp-fast-table/handlers/row/wp-state-links-handler.ts @@ -7,6 +7,7 @@ import {uiStateLinkClass} from '../../builders/ui-state-link-builder'; import {tableRowClassName} from "../../builders/rows/single-row-builder"; import {States} from "../../../states.service"; import {WorkPackageTableSelection} from "../../state/wp-table-selection.service"; +import {WorkPackageTableFocusService} from 'core-components/wp-fast-table/state/wp-table-focus.service'; export class WorkPackageStateLinksHandler implements TableEventHandler { // Injections @@ -14,9 +15,10 @@ export class WorkPackageStateLinksHandler implements TableEventHandler { public keepTab:KeepTabService; public states:States; public wpTableSelection:WorkPackageTableSelection; + public wpTableFocus:WorkPackageTableFocusService; constructor(table: WorkPackageTable) { - $injectFields(this, '$state', 'keepTab', 'states', 'wpTableSelection'); + $injectFields(this, '$state', 'keepTab', 'states', 'wpTableSelection', 'wpTableFocus'); } public get EVENT() { @@ -56,7 +58,7 @@ export class WorkPackageStateLinksHandler implements TableEventHandler { let classIdentifier = row.data('classIdentifier'); let [index, _] = table.findRenderedRow(classIdentifier); - this.states.focusedWorkPackage.putValue(workPackageId); + this.wpTableFocus.updateFocus(workPackageId); // Update single selection if no modifier present this.wpTableSelection.setSelection(workPackageId, index); diff --git a/frontend/app/components/wp-fast-table/handlers/state/hierarchy-transformer.ts b/frontend/app/components/wp-fast-table/handlers/state/hierarchy-transformer.ts index e7f30d33a0..d28878e599 100644 --- a/frontend/app/components/wp-fast-table/handlers/state/hierarchy-transformer.ts +++ b/frontend/app/components/wp-fast-table/handlers/state/hierarchy-transformer.ts @@ -7,7 +7,7 @@ import {WorkPackageTableHierarchies} from "../../wp-table-hierarchies"; import {indicatorCollapsedClass} from "../../builders/modes/hierarchy/single-hierarchy-row-builder"; import {tableRowClassName} from '../../builders/rows/single-row-builder'; import {debugLog} from '../../../../helpers/debug_output'; -import { locateTableRow } from "core-components/wp-fast-table/helpers/wp-table-row-helpers"; +import { locateTableRow, scrollTableRowIntoView } from "core-components/wp-fast-table/helpers/wp-table-row-helpers"; export class HierarchyTransformer { public wpTableHierarchies:WorkPackageTableHierarchiesService; @@ -85,23 +85,7 @@ export class HierarchyTransformer { // Keep focused on the last element, if any. // Based on https://stackoverflow.com/a/3782959 if (state.last) { - try { - const element = locateTableRow(state.last); - const container = element.scrollParent(); - const containerTop = container.scrollTop(); - const containerBottom = containerTop + container.height(); - - const elemTop = element[0].offsetTop; - const elemBottom = elemTop + element.height(); - - if (elemTop < containerTop) { - container[0].scrollTop = elemTop; - } else if (elemBottom > containerBottom) { - container[0].scrollTop = elemBottom - container.height(); - } - } catch (e) { - console.warn("Can't scroll hierarchy element into view: " + e); - } + scrollTableRowIntoView(state.last); } diff --git a/frontend/app/components/wp-fast-table/handlers/state/selection-transformer.ts b/frontend/app/components/wp-fast-table/handlers/state/selection-transformer.ts index 6643219a49..c6d45f694d 100644 --- a/frontend/app/components/wp-fast-table/handlers/state/selection-transformer.ts +++ b/frontend/app/components/wp-fast-table/handlers/state/selection-transformer.ts @@ -1,42 +1,34 @@ -import {injectorBridge} from "../../../angular/angular-injector-bridge.functions"; +import {$injectFields} from "../../../angular/angular-injector-bridge.functions"; import {States} from "../../../states.service"; import {tableRowClassName} from "../../builders/rows/single-row-builder"; import {checkedClassName} from "../../builders/ui-state-link-builder"; -import {rowId, locateTableRow} from "../../helpers/wp-table-row-helpers"; +import {rowId, locateTableRow, scrollTableRowIntoView} from "../../helpers/wp-table-row-helpers"; import {WorkPackageTableSelection} from "../../state/wp-table-selection.service"; import {WorkPackageTable} from "../../wp-fast-table"; import {WPTableRowSelectionState} from "../../wp-table.interfaces"; +import {WorkPackageTableFocusService} from "core-components/wp-fast-table/state/wp-table-focus.service"; export class SelectionTransformer { public wpTableSelection:WorkPackageTableSelection; + public wpTableFocus:WorkPackageTableFocusService; public states:States; public FocusHelper:any; - // When first entering the page, the user - // wants to scroll to the focused work package in the table. - // We only want to do this once, so remember when we did the first focus - private hasFocusedOnElement = false; - constructor(table:WorkPackageTable) { - injectorBridge(this); + $injectFields(this, 'wpTableSelection', 'wpTableFocus', 'states', 'FocusHelper'); // Focus a single selection when active this.states.table.rendered.values$() .takeUntil(this.states.table.stopAllSubscriptions) .subscribe(() => { - const singleSelection = this.wpTableSelection.getSingleSelection; - if (singleSelection === null) { - return; - } - if (!this.hasFocusedOnElement) { - this.hasFocusedOnElement = true; - const element = locateTableRow(singleSelection); + this.wpTableFocus.ifShouldFocus((wpId:string) => { + const element = locateTableRow(wpId); if (element.length) { - element[0].scrollIntoView(); + scrollTableRowIntoView(wpId); this.FocusHelper.focusElement(element, true); } - } + }); }); @@ -75,4 +67,3 @@ export class SelectionTransformer { } } -SelectionTransformer.$inject = ['wpTableSelection', 'states', 'FocusHelper']; diff --git a/frontend/app/components/wp-fast-table/helpers/wp-table-row-helpers.ts b/frontend/app/components/wp-fast-table/helpers/wp-table-row-helpers.ts index 4eb3a5647f..50f062d32c 100644 --- a/frontend/app/components/wp-fast-table/helpers/wp-table-row-helpers.ts +++ b/frontend/app/components/wp-fast-table/helpers/wp-table-row-helpers.ts @@ -10,4 +10,24 @@ export function locateTableRow(workPackageId:string):JQuery { return jQuery('.' + rowId(workPackageId)); } +export function scrollTableRowIntoView(workPackageId:string):void { + try { + const element = locateTableRow(workPackageId); + const container = element.scrollParent(); + const containerTop = container.scrollTop(); + const containerBottom = containerTop + container.height(); + + const elemTop = element[0].offsetTop; + const elemBottom = elemTop + element.height(); + + if (elemTop < containerTop) { + container[0].scrollTop = elemTop; + } else if (elemBottom > containerBottom) { + container[0].scrollTop = elemBottom - container.height(); + } + } catch (e) { + console.warn("Can't scroll row element into view: " + e); + } +} + diff --git a/frontend/app/components/wp-fast-table/state/wp-table-focus.service.ts b/frontend/app/components/wp-fast-table/state/wp-table-focus.service.ts new file mode 100644 index 0000000000..005f188c8a --- /dev/null +++ b/frontend/app/components/wp-fast-table/state/wp-table-focus.service.ts @@ -0,0 +1,80 @@ +import {States} from '../../states.service'; +import {opServicesModule} from '../../../angular-modules'; +import {WorkPackageResource} from '../../api/api-v3/hal-resources/work-package-resource.service'; +import {InputState} from 'reactivestates'; +import {WorkPackageTableSelection} from 'core-components/wp-fast-table/state/wp-table-selection.service'; + +export interface WPFocusState { + workPackageId:string; + focusAfterRender:boolean; +} + +export class WorkPackageTableFocusService { + + public state:InputState; + + constructor(public states:States, + public wpTableSelection:WorkPackageTableSelection) { + this.state = states.focusedWorkPackage; + this.observeToUpdateFocused(); + } + + public isFocused(workPackageId:string) { + return this.focusedWorkPackage === workPackageId; + } + + public ifShouldFocus(callback:(workPackageId:string) => void) { + const value = this.state.value; + + if (value && value.focusAfterRender) { + callback(value.workPackageId); + value.focusAfterRender = false; + this.state.putValue(value, 'Setting focus to false after callback.'); + } + } + + public get focusedWorkPackage():string|null { + const value = this.state.value; + + if (value) { + return value.workPackageId; + } + + return null; + } + + public clear() { + this.state.clear(); + } + + public whenChanged() { + return this.state.values$() + .map((val:WPFocusState) => val.workPackageId) + .distinctUntilChanged(); + } + + public updateFocus(workPackageId:string, setFocusAfterRender:boolean = false) { + // Set the selection to this row, if nothing else is selected. + if (this.wpTableSelection.isEmpty) { + this.wpTableSelection.setRowState(workPackageId, true); + } + this.state.putValue({ workPackageId: workPackageId, focusAfterRender: setFocusAfterRender}); + } + + /** + * Put the first row that is eligible to be displayed in the details view into + * the focused state if no manual selection has been made yet. + */ + private observeToUpdateFocused() { + this + .states.table.rendered + .values$() + .map(state => _.find(state, (row:any) => row.workPackageId)) + .filter(fullRow => !!fullRow && this.wpTableSelection.isEmpty) + .subscribe(fullRow => { + this.updateFocus(fullRow!.workPackageId!); + }); + } +} + +opServicesModule.service('wpTableFocus', WorkPackageTableFocusService); diff --git a/frontend/app/components/wp-fast-table/state/wp-table-selection.service.ts b/frontend/app/components/wp-fast-table/state/wp-table-selection.service.ts index 79010dfc5b..b2bddfbc38 100644 --- a/frontend/app/components/wp-fast-table/state/wp-table-selection.service.ts +++ b/frontend/app/components/wp-fast-table/state/wp-table-selection.service.ts @@ -15,8 +15,6 @@ export class WorkPackageTableSelection { if (this.selectionState.isPristine()) { this.reset(); } - - this.observeToUpdateFocused(); } public isSelected(workPackageId:string) { @@ -84,31 +82,6 @@ export class WorkPackageTableSelection { return _.size(this.currentState.selected); } - public get isSingleSelection():boolean { - return this.getSingleSelection !== null; - } - - public get getSingleSelection():string|null { - const selected = _.pickBy(this.currentState.selected, (selected:boolean) => selected === true); - const selectedWps = _.keys(selected); - if (selectedWps.length === 1) { - return selectedWps[0]; - } - - return null; - } - - /** - * Switch the current focused work package to the given id, - * setting selection and focus on this WP. - */ - public focusOn(workPackgeId:string) { - let newState = this._emptyState; - newState.selected[workPackgeId] = true; - this.selectionState.putValue(newState); - this.states.focusedWorkPackage.putValue(workPackgeId); - } - /** * Toggle a single row selection state and update the state. * @param workPackageId @@ -150,15 +123,19 @@ export class WorkPackageTableSelection { public setMultiSelectionFrom(rows:RenderedRow[], wpId:string, position:number) { let state = this.currentState; - if (this.selectionCount === 0) { + // If there are no other selections, it does not matter what the index is + if (this.selectionCount === 0 || state.activeRowIndex === null) { + console.warn(`Selection count is empty, setting ${wpId} to selected.`); state.selected[wpId] = true; state.activeRowIndex = position; - } else if (state.activeRowIndex !== null) { + } else { + console.warn(`Active index is ${state.activeRowIndex}`); let start = Math.min(position, state.activeRowIndex); let end = Math.max(position, state.activeRowIndex); rows.forEach((row, i) => { if (row.workPackageId) { + console.warn(`Setting ${row.workPackageId} ? ${i >= start && i <= end}`); state.selected[row.workPackageId] = i >= start && i <= end; } }); @@ -174,21 +151,6 @@ export class WorkPackageTableSelection { activeRowIndex: null }; } - - /** - * Put the first row that is eligible to be displayed in the details view into - * the focused state if no manual selection has been made yet. - */ - private observeToUpdateFocused() { - this - .states.table.rendered - .values$() - .map(state => _.find(state, (row:any) => row.workPackageId)) - .filter(fullRow => !!fullRow && _.isEmpty(this.currentState.selected)) - .subscribe(fullRow => { - this.states.focusedWorkPackage.putValue(fullRow!.workPackageId!); - }); - } } opServicesModule.service('wpTableSelection', WorkPackageTableSelection); diff --git a/frontend/app/components/wp-fast-table/wp-table.interfaces.ts b/frontend/app/components/wp-fast-table/wp-table.interfaces.ts index 8713db5b60..06fc4a2936 100644 --- a/frontend/app/components/wp-fast-table/wp-table.interfaces.ts +++ b/frontend/app/components/wp-fast-table/wp-table.interfaces.ts @@ -28,8 +28,8 @@ export interface GroupableColumn { export interface WPTableRowSelectionState { // Map of selected rows - selected: {[workPackageId: string]: boolean}; + selected:{[workPackageId:string]:boolean}; // Index of current selection // required for shift-offsets - activeRowIndex: number | null; + activeRowIndex:number | null; } diff --git a/frontend/app/components/wp-inline-create/wp-inline-create.directive.ts b/frontend/app/components/wp-inline-create/wp-inline-create.directive.ts index e8cfef829c..c86db72aaf 100644 --- a/frontend/app/components/wp-inline-create/wp-inline-create.directive.ts +++ b/frontend/app/components/wp-inline-create/wp-inline-create.directive.ts @@ -48,6 +48,7 @@ import {TableRowEditContext} from '../wp-edit-form/table-row-edit-context'; import {WorkPackageChangeset} from '../wp-edit-form/work-package-changeset'; import {WorkPackageEditingService} from '../wp-edit-form/work-package-editing-service'; import {WorkPackageFilterValues} from '../wp-edit-form/work-package-filter-values'; +import {WorkPackageTableFocusService} from 'core-components/wp-fast-table/state/wp-table-focus.service'; export class WorkPackageInlineCreateController { @@ -74,6 +75,7 @@ export class WorkPackageInlineCreateController { public wpCreate:WorkPackageCreateService, public wpTableColumns:WorkPackageTableColumnsService, private wpTableFilters:WorkPackageTableFiltersService, + private wpTableFocus:WorkPackageTableFocusService, private AuthorisationService:any, private $q:ng.IQService, private I18n:op.I18n) { @@ -96,7 +98,7 @@ export class WorkPackageInlineCreateController { this.addWorkPackageRow(); // Focus on the last inserted id - this.states.focusedWorkPackage.putValue(wp.id, 'Added in inline create'); + this.wpTableFocus.updateFocus(wp.id); } else { // Remove current row this.table.editing.stopEditing('new'); diff --git a/spec/features/work_packages/select_work_package_row_spec.rb b/spec/features/work_packages/select_work_package_row_spec.rb index 5dd70a7583..25e7df2d9a 100644 --- a/spec/features/work_packages/select_work_package_row_spec.rb +++ b/spec/features/work_packages/select_work_package_row_spec.rb @@ -76,9 +76,9 @@ describe 'Select work package row', type: :feature, js:true, selenium: true do element = find(".work-package-table--container tr:nth-of-type(#{number}) .wp-table--cell-td.id") loading_indicator_saveguard - page.driver.browser.action.key_down(:control) + page.driver.browser.action.key_down(:meta) .click(element.native) - .key_up(:control) + .key_up(:meta) .perform end