Update selections across board lists

pull/8230/head
Oliver Günther 5 years ago
parent aefc645675
commit b01e475edc
No known key found for this signature in database
GPG Key ID: A3A8BDAD7C0C552C
  1. 3
      frontend/src/app/components/wp-card-view/wp-single-card/wp-single-card.component.ts
  2. 52
      frontend/src/app/modules/boards/board/board-list/board-list-cross-selection.service.ts
  3. 64
      frontend/src/app/modules/boards/board/board-list/board-list.component.ts
  4. 18
      frontend/src/app/modules/boards/board/board-partitioned-page/board-list-container.component.ts
  5. 2
      frontend/src/app/modules/boards/board/board-partitioned-page/board-partitioned-page.component.ts
  6. 16
      frontend/src/app/modules/work_packages/routing/wp-view-base/view-services/wp-view-selection.service.ts

@ -18,6 +18,7 @@ import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {CardHighlightingMode} from "core-components/wp-fast-table/builders/highlighting/highlighting-mode.const"; import {CardHighlightingMode} from "core-components/wp-fast-table/builders/highlighting/highlighting-mode.const";
import {CardViewOrientation} from "core-components/wp-card-view/wp-card-view.component"; import {CardViewOrientation} from "core-components/wp-card-view/wp-card-view.component";
import {UntilDestroyedMixin} from "core-app/helpers/angular/until-destroyed.mixin"; import {UntilDestroyedMixin} from "core-app/helpers/angular/until-destroyed.mixin";
import {WorkPackageViewFocusService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-focus.service";
@Component({ @Component({
@ -49,6 +50,7 @@ export class WorkPackageSingleCardComponent extends UntilDestroyedMixin implemen
readonly I18n:I18nService, readonly I18n:I18nService,
readonly $state:StateService, readonly $state:StateService,
readonly wpTableSelection:WorkPackageViewSelectionService, readonly wpTableSelection:WorkPackageViewSelectionService,
readonly wpTableFocus:WorkPackageViewFocusService,
readonly cardView:WorkPackageCardViewService, readonly cardView:WorkPackageCardViewService,
readonly cdRef:ChangeDetectorRef) { readonly cdRef:ChangeDetectorRef) {
super(); super();
@ -72,6 +74,7 @@ export class WorkPackageSingleCardComponent extends UntilDestroyedMixin implemen
public openSplitScreen(wp:WorkPackageResource) { public openSplitScreen(wp:WorkPackageResource) {
let classIdentifier = this.classIdentifier(wp); let classIdentifier = this.classIdentifier(wp);
this.wpTableSelection.setSelection(wp.id!, this.cardView.findRenderedCard(classIdentifier)); this.wpTableSelection.setSelection(wp.id!, this.cardView.findRenderedCard(classIdentifier));
this.wpTableFocus.updateFocus(wp.id!);
this.$state.go( this.$state.go(
this.$state.current.data.baseRoute + '.details', this.$state.current.data.baseRoute + '.details',
{ workPackageId: wp.id! } { workPackageId: wp.id! }

@ -0,0 +1,52 @@
import {Observable, Subject} from "rxjs";
import {filter} from "rxjs/operators";
export interface BoardSelection {
/** The query that the selection happened in */
withinQuery:string;
/** The focused selected work package */
focusedWorkPackage:string;
/** Array of selected work packages */
allSelected:string[];
}
/**
* Responsible for keeping selected items across all lists of a board,
* selections in one list will propagate to other lists as well.
*/
export class BoardListCrossSelectionService {
private selections$ = new Subject<BoardSelection>();
/**
* Marks the selection of one or multiple cards within a list
* by a user.
*
* The primary selected should be open in split screen (if open).
*
*/
updateSelection(selection:BoardSelection) {
this.selections$.next(selection);
}
/**
* Returns an observable for a given query that fires
* when its selection should be updated.
*
* @param id
*/
selectionsForQuery(id:string):Observable<BoardSelection> {
return this
.selections$
.pipe(
filter(selection => selection.withinQuery !== id)
);
}
selections():Observable<BoardSelection> {
return this.selections$;
}
}

@ -49,6 +49,12 @@ import {WorkPackageCardDragAndDropService} from "core-components/wp-card-view/se
import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset"; import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset";
import {componentDestroyed} from "@w11k/ngx-componentdestroyed"; import {componentDestroyed} from "@w11k/ngx-componentdestroyed";
import {BoardFiltersService} from "core-app/modules/boards/board/board-filter/board-filters.service"; import {BoardFiltersService} from "core-app/modules/boards/board/board-filter/board-filters.service";
import {StateService, TransitionService} from "@uirouter/core";
import {WorkPackageViewFocusService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-focus.service";
import {WorkPackageViewSelectionService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-selection.service";
import {BoardListCrossSelectionService} from "core-app/modules/boards/board/board-list/board-list-cross-selection.service";
import {debounceTime} from "rxjs/operators";
import {combineLatest} from "rxjs";
export interface DisabledButtonPlaceholder { export interface DisabledButtonPlaceholder {
text:string; text:string;
@ -123,13 +129,18 @@ export class BoardListComponent extends AbstractWidgetComponent implements OnIni
constructor(readonly QueryDm:QueryDmService, constructor(readonly QueryDm:QueryDmService,
readonly I18n:I18nService, readonly I18n:I18nService,
readonly state:StateService,
readonly cdRef:ChangeDetectorRef, readonly cdRef:ChangeDetectorRef,
readonly transitions:TransitionService,
readonly boardCache:BoardCacheService, readonly boardCache:BoardCacheService,
readonly boardFilters:BoardFiltersService, readonly boardFilters:BoardFiltersService,
readonly notifications:NotificationsService, readonly notifications:NotificationsService,
readonly querySpace:IsolatedQuerySpace, readonly querySpace:IsolatedQuerySpace,
readonly halNotification:HalResourceNotificationService, readonly halNotification:HalResourceNotificationService,
readonly wpStatesInitialization:WorkPackageStatesInitializationService, readonly wpStatesInitialization:WorkPackageStatesInitializationService,
readonly wpViewFocusService:WorkPackageViewFocusService,
readonly wpViewSelectionService:WorkPackageViewSelectionService,
readonly boardListCrossSelectionService:BoardListCrossSelectionService,
readonly authorisationService:AuthorisationService, readonly authorisationService:AuthorisationService,
readonly wpInlineCreate:WorkPackageInlineCreateService, readonly wpInlineCreate:WorkPackageInlineCreateService,
readonly injector:Injector, readonly injector:Injector,
@ -147,6 +158,12 @@ export class BoardListComponent extends AbstractWidgetComponent implements OnIni
this.initiallyFocused = this.resource.isNewWidget; this.initiallyFocused = this.resource.isNewWidget;
this.resource.isNewWidget = false; this.resource.isNewWidget = false;
// Set initial selection if split view open
if (this.state.includes(this.state.current.data.baseRoute + '.details')) {
let wpId = this.state.params.workPackageId;
this.wpViewSelectionService.setMultiSelection([wpId]);
}
// Update permission on model updates // Update permission on model updates
this.authorisationService this.authorisationService
.observeUntil(componentDestroyed(this)) .observeUntil(componentDestroyed(this))
@ -157,6 +174,41 @@ export class BoardListComponent extends AbstractWidgetComponent implements OnIni
} }
}); });
let lastSelection:string[];
// If this query space changes its focused or selected
// work packages, update the board cross selection
combineLatest([
this.wpViewFocusService.state.values$(),
this.wpViewSelectionService.selection$()
]).pipe(
debounceTime(100),
this.untilDestroyed()
).subscribe(([focusedState, selectionState]) => {
let selected = Object.keys(_.pickBy(selectionState.selected, (selected, _) => selected === true));
if (_.isEqual(selected, lastSelection)) {
return;
}
this.boardListCrossSelectionService.updateSelection({
withinQuery: this.queryId,
focusedWorkPackage: focusedState.workPackageId,
allSelected: selected
});
});
// Apply focus and selection when changed in cross service
this.boardListCrossSelectionService
.selectionsForQuery(this.queryId)
.pipe(
this.untilDestroyed()
)
.subscribe(selection => {
lastSelection = selection.allSelected;
this.wpViewSelectionService.setMultiSelection(selection.allSelected);
});
// Update query on filter change // Update query on filter change
this.boardFilters this.boardFilters
.filters .filters
@ -181,6 +233,10 @@ export class BoardListComponent extends AbstractWidgetComponent implements OnIni
this.updateQuery(); this.updateQuery();
} }
ngOnDestroy() {
super.ngOnDestroy();
}
public get errorMessage() { public get errorMessage() {
return this.I18n.t('js.boards.error_loading_the_list', { error_message: this.loadingError }); return this.I18n.t('js.boards.error_loading_the_list', { error_message: this.loadingError });
} }
@ -340,10 +396,12 @@ export class BoardListComponent extends AbstractWidgetComponent implements OnIni
} }
} }
private loadQuery(visibly = true) { private get queryId():string {
const queryId:string = (this.resource.options.queryId as number|string).toString(); return (this.resource.options.queryId as number|string).toString();
}
let observable = this.QueryDm.stream(this.columnsQueryProps, queryId); private loadQuery(visibly = true) {
let observable = this.QueryDm.stream(this.columnsQueryProps, this.queryId);
// Spread arguments on pipe does not work: // Spread arguments on pipe does not work:
// https://github.com/ReactiveX/rxjs/issues/3989 // https://github.com/ReactiveX/rxjs/issues/3989

@ -19,10 +19,15 @@ import {GridWidgetResource} from "core-app/modules/hal/resources/grid-widget-res
import {BoardPartitionedPageComponent} from "core-app/modules/boards/board/board-partitioned-page/board-partitioned-page.component"; import {BoardPartitionedPageComponent} from "core-app/modules/boards/board/board-partitioned-page/board-partitioned-page.component";
import {AddListModalComponent} from "core-app/modules/boards/board/add-list-modal/add-list-modal.component"; import {AddListModalComponent} from "core-app/modules/boards/board/add-list-modal/add-list-modal.component";
import {I18nService} from "core-app/modules/common/i18n/i18n.service"; import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {BoardListCrossSelectionService} from "core-app/modules/boards/board/board-list/board-list-cross-selection.service";
import {filter} from "rxjs/operators";
@Component({ @Component({
templateUrl: './board-list-container.component.html', templateUrl: './board-list-container.component.html',
styleUrls: ['./board-list-container.component.sass'] styleUrls: ['./board-list-container.component.sass'],
providers: [
BoardListCrossSelectionService
]
}) })
export class BoardListContainerComponent extends UntilDestroyedMixin implements OnInit { export class BoardListContainerComponent extends UntilDestroyedMixin implements OnInit {
@ -74,6 +79,7 @@ export class BoardListContainerComponent extends UntilDestroyedMixin implements
readonly BoardCache:BoardCacheService, readonly BoardCache:BoardCacheService,
readonly Boards:BoardService, readonly Boards:BoardService,
readonly Banner:BannersService, readonly Banner:BannersService,
readonly boardListCrossSelectionService:BoardListCrossSelectionService,
readonly Drag:DragAndDropService, readonly Drag:DragAndDropService,
readonly QueryUpdated:QueryUpdatedService) { readonly QueryUpdated:QueryUpdatedService) {
super(); super();
@ -91,6 +97,16 @@ export class BoardListContainerComponent extends UntilDestroyedMixin implements
.subscribe(board => { .subscribe(board => {
this.setupQueryUpdatedMonitoring(board); this.setupQueryUpdatedMonitoring(board);
}); });
this.boardListCrossSelectionService
.selections()
.pipe(
this.untilDestroyed(),
filter(() => this.state.includes(this.state.current.data.baseRoute + '.details'))
).subscribe(selection => {
// Update split screen
this.state.go(this.state.current.data.baseRoute + '.details', { workPackageId: selection.focusedWorkPackage });
});
} }
moveList(board:Board, event:CdkDragDrop<GridWidgetResource[]>) { moveList(board:Board, event:CdkDragDrop<GridWidgetResource[]>) {

@ -24,7 +24,6 @@ import {UntilDestroyedMixin} from "core-app/helpers/angular/until-destroyed.mixi
import {QueryResource} from "core-app/modules/hal/resources/query-resource"; import {QueryResource} from "core-app/modules/hal/resources/query-resource";
import {Ng2StateDeclaration} from "@uirouter/angular"; import {Ng2StateDeclaration} from "@uirouter/angular";
import {BoardFiltersService} from "core-app/modules/boards/board/board-filter/board-filters.service"; import {BoardFiltersService} from "core-app/modules/boards/board/board-filter/board-filters.service";
import {WorkPackageViewHandlerToken} from "core-app/modules/work_packages/routing/wp-view-base/event-handling/event-handler-registry";
import {CardViewHandlerRegistry} from "core-components/wp-card-view/event-handler/card-view-handler-registry"; import {CardViewHandlerRegistry} from "core-components/wp-card-view/event-handler/card-view-handler-registry";
export function boardCardViewHandlerFactory(injector:Injector) { export function boardCardViewHandlerFactory(injector:Injector) {
@ -40,7 +39,6 @@ export function boardCardViewHandlerFactory(injector:Injector) {
providers: [ providers: [
DragAndDropService, DragAndDropService,
BoardFiltersService, BoardFiltersService,
{ provide: WorkPackageViewHandlerToken, useFactory: boardCardViewHandlerFactory, deps: [Injector] },
] ]
}) })
export class BoardPartitionedPageComponent extends UntilDestroyedMixin { export class BoardPartitionedPageComponent extends UntilDestroyedMixin {

@ -9,10 +9,10 @@ import {RenderedWorkPackage} from "core-app/modules/work_packages/render-info/re
export interface WorkPackageViewSelectionState { export interface WorkPackageViewSelectionState {
// Map of selected rows // Map of selected rows
selected:{[workPackageId:string]:boolean}; selected:{ [workPackageId:string]:boolean };
// Index of current selection // Index of current selection
// required for shift-offsets // required for shift-offsets
activeRowIndex:number | null; activeRowIndex:number|null;
} }
@Injectable() @Injectable()
@ -128,11 +128,19 @@ export class WorkPackageViewSelectionService implements OnDestroy {
* Override current selection with the given work package id. * Override current selection with the given work package id.
*/ */
public setSelection(wpId:string, position:number) { public setSelection(wpId:string, position:number) {
this.setMultiSelection([wpId], position);
}
/**
* Select a number of work packages
*/
public setMultiSelection(selectedWorkPackageIds:string[], activeRowIndex:number|null = null) {
let state:WorkPackageViewSelectionState = { let state:WorkPackageViewSelectionState = {
selected: {}, selected: {},
activeRowIndex: position activeRowIndex: activeRowIndex
}; };
state.selected[wpId] = true;
selectedWorkPackageIds.forEach(id => state.selected[id] = true);
this.selectionState.putValue(state); this.selectionState.putValue(state);
} }

Loading…
Cancel
Save