Feature/group fold button (#8863)
* add fold/unfold groups button * wip * introduce service to communicate fold state * fix colspan on folding/unfolding groups * move state handling into service * refreshAllGroupHeaderCells when allGroupsChanged too * have correct order for states to update * rely on refreshView to trigger collapsedRowRefreshing The refreshView method is called whenever a group is folded/expanded. Therefore, the listeners for the states are just extra and lead to the code being executed twice. * remove code made obsolete by commit before * show group toggle button only when grouping * use plus/minus icon for button * shorten code * remove duplicated code * linting * dropdown for expand/collapse * add icon to button * alter button position and switch icons * remove text from group toggle button * have unique html id on fold button * Rerendering only group rows when grouping changes * Disable unavailable menu collapse option + merge old collapse state * refreshAllGroupHeaderCells when allGroupsChanged too * Disable collapse options handling mixed scenarios * Working with different grouping & updating grouping options disable * Code climate fixes * Fix + Types with headers extracted to service * Test fixes * CodeClimate fixes Co-authored-by: Aleix Suau <info@macrofonoestudio.es>pull/8878/head
parent
b6784e90d4
commit
7a738cebc6
@ -0,0 +1,89 @@ |
||||
//-- copyright
|
||||
// OpenProject is an open source project management software.
|
||||
// Copyright (C) 2012-2020 the OpenProject GmbH
|
||||
//
|
||||
// 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 docs/COPYRIGHT.rdoc for more details.
|
||||
//++
|
||||
|
||||
import {OPContextMenuService} from "core-components/op-context-menu/op-context-menu.service"; |
||||
import {Directive, ElementRef} from "@angular/core"; |
||||
import {OpContextMenuTrigger} from "core-components/op-context-menu/handlers/op-context-menu-trigger.directive"; |
||||
import {I18nService} from "core-app/modules/common/i18n/i18n.service"; |
||||
import { |
||||
WorkPackageViewDisplayRepresentationService, |
||||
wpDisplayCardRepresentation, |
||||
wpDisplayListRepresentation |
||||
} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-display-representation.service"; |
||||
import {WorkPackageViewTimelineService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-timeline.service"; |
||||
import {WorkPackageViewCollapsedGroupsService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-collapsed-groups.service"; |
||||
|
||||
@Directive({ |
||||
selector: '[wpGroupToggleDropdown]' |
||||
}) |
||||
export class WorkPackageGroupToggleDropdownMenuDirective extends OpContextMenuTrigger { |
||||
constructor(readonly elementRef:ElementRef, |
||||
readonly opContextMenu:OPContextMenuService, |
||||
readonly I18n:I18nService, |
||||
readonly wpViewCollapsedGroups:WorkPackageViewCollapsedGroupsService) { |
||||
super(elementRef, opContextMenu); |
||||
} |
||||
|
||||
protected open(evt:JQuery.TriggeredEvent) { |
||||
this.buildItems(); |
||||
this.opContextMenu.show(this, evt); |
||||
} |
||||
|
||||
public get locals() { |
||||
return { |
||||
items: this.items, |
||||
contextMenuId: 'wp-group-fold-context-menu' |
||||
}; |
||||
} |
||||
|
||||
private buildItems() { |
||||
this.items = [ |
||||
{ |
||||
disabled: this.wpViewCollapsedGroups.allGroupsAreCollapsed, |
||||
linkText: this.I18n.t('js.button_collapse_all'), |
||||
icon: 'icon-minus2', |
||||
onClick: (evt:JQuery.TriggeredEvent) => { |
||||
this.wpViewCollapsedGroups.setAllGroupsCollapseStateTo(true); |
||||
|
||||
return true; |
||||
} |
||||
}, |
||||
{ |
||||
disabled: this.wpViewCollapsedGroups.allGroupsAreExpanded, |
||||
linkText: this.I18n.t('js.button_expand_all'), |
||||
icon: 'icon-plus', |
||||
onClick: (evt:JQuery.TriggeredEvent) => { |
||||
this.wpViewCollapsedGroups.setAllGroupsCollapseStateTo(false); |
||||
|
||||
return true; |
||||
} |
||||
} |
||||
]; |
||||
} |
||||
} |
||||
|
@ -0,0 +1,46 @@ |
||||
// -- copyright
|
||||
// OpenProject is an open source project management software.
|
||||
// Copyright (C) 2012-2020 the OpenProject GmbH
|
||||
//
|
||||
// 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 docs/COPYRIGHT.rdoc for more details.
|
||||
// ++
|
||||
|
||||
import {ChangeDetectionStrategy, Component} from '@angular/core'; |
||||
import {I18nService} from "core-app/modules/common/i18n/i18n.service"; |
||||
|
||||
@Component({ |
||||
template: ` |
||||
<button class="button" |
||||
id="wp-fold-toggle-button" |
||||
wpGroupToggleDropdown> |
||||
<op-icon icon-classes="button--icon icon-outline"></op-icon> |
||||
<span class="button--text"></span> |
||||
<op-icon icon-classes="button--icon icon-small icon-pulldown"></op-icon> |
||||
</button> |
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush, |
||||
selector: 'wp-fold-toggle-view-button' |
||||
}) |
||||
export class WorkPackageFoldToggleButtonComponent { |
||||
} |
@ -0,0 +1,23 @@ |
||||
import {Injector} from '@angular/core'; |
||||
import {distinctUntilChanged, takeUntil} from 'rxjs/operators'; |
||||
import {IsolatedQuerySpace} from "core-app/modules/work_packages/query-space/isolated-query-space"; |
||||
import {WorkPackageTable} from "core-components/wp-fast-table/wp-fast-table"; |
||||
import {InjectField} from "core-app/helpers/angular/inject-field.decorator"; |
||||
import {WorkPackageViewCollapsedGroupsService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-collapsed-groups.service"; |
||||
|
||||
export class GroupFoldTransformer { |
||||
|
||||
@InjectField() public workPackageViewCollapsedGroupsService:WorkPackageViewCollapsedGroupsService; |
||||
@InjectField() public querySpace:IsolatedQuerySpace; |
||||
|
||||
constructor(public readonly injector:Injector, |
||||
table:WorkPackageTable) { |
||||
this.workPackageViewCollapsedGroupsService |
||||
.updates$() |
||||
.pipe( |
||||
takeUntil(this.querySpace.stopAllSubscriptions), |
||||
distinctUntilChanged() |
||||
) |
||||
.subscribe((groupsCollapseEvent) => table.setGroupsCollapseState(groupsCollapseEvent.state)); |
||||
} |
||||
} |
@ -0,0 +1,8 @@ |
||||
interface IGroupsCollapseEvent { |
||||
state:{[identifier:string]:boolean}; |
||||
allGroupsAreCollapsed:boolean; |
||||
allGroupsAreExpanded:boolean; |
||||
lastChangedGroup:string|null; |
||||
allGroupsChanged:boolean; |
||||
groupedBy:string|null; |
||||
} |
@ -0,0 +1,159 @@ |
||||
// -- copyright
|
||||
// OpenProject is an open source project management software.
|
||||
// Copyright (C) 2012-2020 the OpenProject GmbH
|
||||
//
|
||||
// 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 docs/COPYRIGHT.rdoc for more details.
|
||||
// ++
|
||||
|
||||
import {QueryResource} from 'core-app/modules/hal/resources/query-resource'; |
||||
import {WorkPackageViewBaseService} from './wp-view-base.service'; |
||||
import {Injectable} from '@angular/core'; |
||||
import {WorkPackageViewGroupByService} from 'core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-group-by.service'; |
||||
import {IsolatedQuerySpace} from 'core-app/modules/work_packages/query-space/isolated-query-space'; |
||||
import {take} from 'rxjs/operators'; |
||||
import {GroupObject, WorkPackageCollectionResource} from 'core-app/modules/hal/resources/wp-collection-resource'; |
||||
import {QuerySchemaResource} from 'core-app/modules/hal/resources/query-schema-resource'; |
||||
import {QueryGroupByResource} from 'core-app/modules/hal/resources/query-group-by-resource'; |
||||
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; |
||||
import {SchemaCacheService} from 'core-components/schemas/schema-cache.service'; |
||||
|
||||
@Injectable() |
||||
export class WorkPackageViewCollapsedGroupsService extends WorkPackageViewBaseService<IGroupsCollapseEvent> { |
||||
readonly wpTypesToShowInCollapsedGroupHeaders:((wp:WorkPackageResource) => boolean)[]; |
||||
readonly groupTypesWithHeaderCellsWhenCollapsed = ['project']; |
||||
|
||||
get config():IGroupsCollapseEvent { |
||||
return this.updatesState.getValueOr(this.getDefaultState()); |
||||
} |
||||
|
||||
get currentGroups():GroupObject[] { |
||||
return this.querySpace.groups.value!; |
||||
} |
||||
|
||||
get allGroupsAreCollapsed():boolean { |
||||
return this.config.allGroupsAreCollapsed; |
||||
} |
||||
|
||||
get allGroupsAreExpanded():boolean { |
||||
return this.config.allGroupsAreExpanded; |
||||
} |
||||
|
||||
get currentGroupedBy():QueryGroupByResource|null { |
||||
return this.workPackageViewGroupByService.current; |
||||
} |
||||
|
||||
constructor( |
||||
protected readonly querySpace:IsolatedQuerySpace, |
||||
readonly workPackageViewGroupByService:WorkPackageViewGroupByService, |
||||
private schemaCacheService:SchemaCacheService, |
||||
) { |
||||
super(querySpace); |
||||
this.wpTypesToShowInCollapsedGroupHeaders = [this.isMilestone]; |
||||
} |
||||
|
||||
// Every time the groupedBy changes, this services is initialized
|
||||
private getDefaultState():IGroupsCollapseEvent { |
||||
return { |
||||
state: this.querySpace.collapsedGroups.value || {}, |
||||
allGroupsChanged: false, |
||||
lastChangedGroup: null, |
||||
groupedBy: this.currentGroupedBy?.id || null, |
||||
...this.getAllGroupsCollapsedState(this.currentGroups, this.querySpace.collapsedGroups.value!), |
||||
}; |
||||
} |
||||
|
||||
isMilestone = (workPackage:WorkPackageResource):boolean => { |
||||
return this.schemaCacheService.of(workPackage)?.isMilestone; |
||||
} |
||||
|
||||
toggleGroupCollapseState(groupIdentifier:string):void { |
||||
const newCollapsedState = !this.config.state[groupIdentifier]; |
||||
const state = { |
||||
...this.config.state, |
||||
[groupIdentifier]: newCollapsedState |
||||
}; |
||||
const newState = { |
||||
...this.config, |
||||
state, |
||||
lastChangedGroup: groupIdentifier, |
||||
...this.getAllGroupsCollapsedState(this.currentGroups, state), |
||||
}; |
||||
|
||||
this.update(newState); |
||||
} |
||||
|
||||
setAllGroupsCollapseStateTo(collapsedState:boolean):void { |
||||
const groupUpdatedState = this.currentGroups.reduce((updatedState:{[key:string]:boolean}, group) => { |
||||
return { |
||||
...updatedState, |
||||
[group.identifier]:collapsedState, |
||||
}; |
||||
}, {}); |
||||
const newState = { |
||||
...this.config, |
||||
state: { |
||||
...this.config.state, |
||||
...groupUpdatedState, |
||||
}, |
||||
lastChangedGroup: null, |
||||
allGroupsAreCollapsed: collapsedState, |
||||
allGroupsAreExpanded: !collapsedState, |
||||
allGroupsChanged: true, |
||||
}; |
||||
|
||||
this.update(newState); |
||||
} |
||||
|
||||
getAllGroupsCollapsedState(groups:GroupObject[], currentCollapsedGroupsState:IGroupsCollapseEvent['state']) { |
||||
let allGroupsAreCollapsed = false; |
||||
let allGroupsAreExpanded = true; |
||||
|
||||
if (currentCollapsedGroupsState && groups?.length) { |
||||
const firstGroupIdentifier = groups[0].identifier; |
||||
const firstGroupCollapsedState = currentCollapsedGroupsState[firstGroupIdentifier]; |
||||
const allGroupsHaveTheSameCollapseState = groups.every((group) => { |
||||
return currentCollapsedGroupsState[group.identifier] != null && |
||||
currentCollapsedGroupsState[group.identifier] === currentCollapsedGroupsState[firstGroupIdentifier]; |
||||
}); |
||||
|
||||
allGroupsAreCollapsed = allGroupsHaveTheSameCollapseState && firstGroupCollapsedState; |
||||
allGroupsAreExpanded = allGroupsHaveTheSameCollapseState && !firstGroupCollapsedState; |
||||
} |
||||
|
||||
return {allGroupsAreCollapsed, allGroupsAreExpanded}; |
||||
} |
||||
|
||||
initialize(query:QueryResource, results:WorkPackageCollectionResource, schema?:QuerySchemaResource) { |
||||
// When this service is initialized (first time the table is loaded and very time the groupBy changes),
|
||||
// we need to wait until the table is ready to emit the collapseStatus. Otherwise the groups are not
|
||||
// ready in the DOM and can't be collapsed/expanded.
|
||||
this.querySpace.tableRendered.values$().pipe(take(1)).subscribe(() => this.update({ ...this.config, allGroupsChanged: true })); |
||||
} |
||||
|
||||
valueFromQuery(query:QueryResource, results:WorkPackageCollectionResource) { |
||||
return this.getDefaultState(); |
||||
} |
||||
|
||||
applyToQuery(query:QueryResource) { return; } |
||||
} |
Loading…
Reference in new issue