parent
eff5075c72
commit
b33faeea7c
@ -0,0 +1,7 @@ |
||||
// Let board list span whole screen |
||||
.router--boards-full-view |
||||
@include extended-content--bottom |
||||
@include extended-content--right |
||||
|
||||
#content |
||||
height: 100% |
@ -0,0 +1,16 @@ |
||||
import {ApiV3Filter} from "core-components/api/api-v3/api-v3-filter-builder"; |
||||
import {input} from "reactivestates"; |
||||
|
||||
export class BoardFiltersService { |
||||
/** |
||||
* We need to remember the current filter, that may either come |
||||
* from the saved board, or were assigned by the user. |
||||
* |
||||
* This is due to the fact we do not work on an query object here. |
||||
*/ |
||||
filters = input<ApiV3Filter[]>([]); |
||||
|
||||
get current():ApiV3Filter[] { |
||||
return this.filters.getValueOr([]); |
||||
} |
||||
} |
@ -0,0 +1,41 @@ |
||||
<ng-container *ngIf="(board$ | async) as board"> |
||||
<div class="boards-list--container" |
||||
#container |
||||
*ngIf="showBoardListView()" |
||||
cdkDropList |
||||
[cdkDropListDisabled]="!board.editable" |
||||
cdkDropListOrientation="horizontal" |
||||
(cdkDropListDropped)="moveList(board, $event)" |
||||
> |
||||
<div *ngFor="let queryWidget of board.queries; trackBy:trackByQueryId" |
||||
class="boards-list--item" |
||||
wp-isolated-query-space |
||||
cdkDrag |
||||
vsDragScroll |
||||
[cdkDragData]="queryWidget" |
||||
[vsDragScrollContainer]="_container"> |
||||
<span *ngIf="board.editable" |
||||
class="boards-list-item-handle icon icon-drag-handle" |
||||
cdkDragHandle></span> |
||||
<board-list [resource]="queryWidget" |
||||
[board]="board" |
||||
(onRemove)="removeList(board, queryWidget)"></board-list> |
||||
</div> |
||||
|
||||
<div class="boards-list--add-item -no-text-select" |
||||
*ngIf="board.editable" |
||||
(click)="addList(board)"> |
||||
<div class="boards-list--add-item-text"> |
||||
<op-icon icon-classes="icon-add icon-context"></op-icon> |
||||
<span [textContent]="text.addList"></span> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<enterprise-banner *ngIf="!showBoardListView()" |
||||
[leftMargin]="true" |
||||
[linkMessage]="text.upsaleCheckOutLink" |
||||
[textMessage]="text.upsaleBoards" |
||||
[opReferrer]="opReferrer(board)"> |
||||
</enterprise-banner> |
||||
</ng-container> |
@ -0,0 +1,221 @@ |
||||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, Injector} from "@angular/core"; |
||||
import { |
||||
DynamicComponentDefinition, |
||||
ToolbarButtonComponentDefinition, |
||||
ViewPartitionState |
||||
} from "core-app/modules/work_packages/routing/partitioned-query-space-page/partitioned-query-space-page.component"; |
||||
import {StateService, TransitionService} from "@uirouter/core"; |
||||
import {BoardFilterComponent} from "core-app/modules/boards/board/board-filter/board-filter.component"; |
||||
import {Board} from "core-app/modules/boards/board/board"; |
||||
import {NotificationsService} from "core-app/modules/common/notifications/notifications.service"; |
||||
import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; |
||||
import {BoardCacheService} from "core-app/modules/boards/board/board-cache.service"; |
||||
import {BoardService} from "core-app/modules/boards/board/board.service"; |
||||
import {DragAndDropService} from "core-app/modules/common/drag-and-drop/drag-and-drop.service"; |
||||
import {WorkPackageFilterButtonComponent} from "core-components/wp-buttons/wp-filter-button/wp-filter-button.component"; |
||||
import {ZenModeButtonComponent} from "core-components/wp-buttons/zen-mode-toggle-button/zen-mode-toggle-button.component"; |
||||
import {BoardsMenuButtonComponent} from "core-app/modules/boards/board/toolbar-menu/boards-menu-button.component"; |
||||
import {RequestSwitchmap} from "core-app/helpers/rxjs/request-switchmap"; |
||||
import {from} from "rxjs"; |
||||
import {componentDestroyed} from "@w11k/ngx-componentdestroyed"; |
||||
import {take} from "rxjs/operators"; |
||||
import {I18nService} from "core-app/modules/common/i18n/i18n.service"; |
||||
import {UntilDestroyedMixin} from "core-app/helpers/angular/until-destroyed.mixin"; |
||||
import {QueryResource} from "core-app/modules/hal/resources/query-resource"; |
||||
import {Ng2StateDeclaration} from "@uirouter/angular"; |
||||
import {BoardFiltersService} from "core-app/modules/boards/board/board-filter/board-filters.service"; |
||||
|
||||
@Component({ |
||||
templateUrl: '/app/modules/work_packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.html', |
||||
styleUrls: [ |
||||
'/app/modules/work_packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.sass' |
||||
], |
||||
changeDetection: ChangeDetectionStrategy.OnPush, |
||||
providers: [ |
||||
DragAndDropService, |
||||
BoardFiltersService, |
||||
] |
||||
}) |
||||
export class BoardPartitionedPageComponent extends UntilDestroyedMixin { |
||||
|
||||
text = { |
||||
button_more: this.I18n.t('js.button_more'), |
||||
delete: this.I18n.t('js.button_delete'), |
||||
areYouSure: this.I18n.t('js.text_are_you_sure'), |
||||
deleteSuccessful: this.I18n.t('js.notice_successful_delete'), |
||||
updateSuccessful: this.I18n.t('js.notice_successful_update'), |
||||
unnamedBoard: this.I18n.t('js.boards.label_unnamed_board'), |
||||
loadingError: 'No such board found', |
||||
addList: this.I18n.t('js.boards.add_list'), |
||||
upsaleBoards: this.I18n.t('js.boards.upsale.teaser_text'), |
||||
upsaleCheckOutLink: this.I18n.t('js.work_packages.table_configuration.upsale.check_out_link'), |
||||
unnamed_list: this.I18n.t('js.boards.label_unnamed_list'), |
||||
}; |
||||
|
||||
/** Board observable */ |
||||
board$ = this.BoardCache.observe(this.state.params.board_id.toString()); |
||||
|
||||
/** Whether this is a new board just created */ |
||||
isNew:boolean = !!this.state.params.isNew; |
||||
|
||||
/** Whether the board is editable */ |
||||
editable:boolean; |
||||
|
||||
/** Go back to boards using back-button */ |
||||
backButtonCallback = () => this.state.go('boards'); |
||||
|
||||
/** Current query title to render */ |
||||
selectedTitle?:string; |
||||
currentQuery:QueryResource|undefined; |
||||
|
||||
/** Whether we're saving the board */ |
||||
toolbarDisabled:boolean = false; |
||||
|
||||
/** Do we currently have query props ? */ |
||||
showToolbarSaveButton:boolean; |
||||
|
||||
/** Listener callbacks */ |
||||
removeTransitionSubscription:Function; |
||||
|
||||
showToolbar = true; |
||||
|
||||
/** Whether filtering is allowed */ |
||||
filterAllowed:boolean = true; |
||||
|
||||
/** We need to pass the correct partition state to the view to manage the grid */ |
||||
currentPartition:ViewPartitionState = '-split'; |
||||
|
||||
/** We need to apply our own board filter component */ |
||||
/** Which filter container component to mount */ |
||||
filterContainerDefinition:DynamicComponentDefinition = { |
||||
component: BoardFilterComponent, |
||||
inputs: { |
||||
board$: this.board$ |
||||
}, |
||||
}; |
||||
|
||||
// We remember when we want to update the board
|
||||
boardSaver = new RequestSwitchmap( |
||||
(board:Board) => { |
||||
this.toolbarDisabled = true; |
||||
const promise = this.Boards |
||||
.save(board) |
||||
.then(board => { |
||||
this.toolbarDisabled = false; |
||||
return board; |
||||
}) |
||||
.catch((error) => { |
||||
this.toolbarDisabled = false; |
||||
throw error; |
||||
}); |
||||
|
||||
return from(promise); |
||||
} |
||||
); |
||||
|
||||
toolbarButtonComponents:ToolbarButtonComponentDefinition[] = [ |
||||
{ |
||||
component: WorkPackageFilterButtonComponent, |
||||
containerClasses: 'hidden-for-mobile' |
||||
}, |
||||
{ |
||||
component: ZenModeButtonComponent, |
||||
containerClasses: 'hidden-for-mobile' |
||||
}, |
||||
{ |
||||
component: BoardsMenuButtonComponent, |
||||
containerClasses: 'hidden-for-mobile', |
||||
show: () => this.editable, |
||||
inputs: { |
||||
board$: this.board$ |
||||
} |
||||
} |
||||
]; |
||||
|
||||
constructor(readonly I18n:I18nService, |
||||
readonly cdRef:ChangeDetectorRef, |
||||
readonly $transitions:TransitionService, |
||||
readonly state:StateService, |
||||
readonly notifications:NotificationsService, |
||||
readonly halNotification:HalResourceNotificationService, |
||||
readonly injector:Injector, |
||||
readonly BoardCache:BoardCacheService, |
||||
readonly boardFilters:BoardFiltersService, |
||||
readonly Boards:BoardService) { |
||||
super(); |
||||
} |
||||
|
||||
ngOnInit():void { |
||||
// Ensure board is being loaded
|
||||
this.Boards.loadAllBoards(); |
||||
|
||||
this.boardSaver |
||||
.observe(componentDestroyed(this)) |
||||
.subscribe( |
||||
(board:Board) => { |
||||
this.BoardCache.update(board); |
||||
this.notifications.addSuccess(this.text.updateSuccessful); |
||||
}, |
||||
(error:unknown) => this.halNotification.handleRawError(error) |
||||
); |
||||
|
||||
this.removeTransitionSubscription = this.$transitions.onSuccess({}, (transition):any => { |
||||
const toState = transition.to(); |
||||
const params = transition.params('to'); |
||||
|
||||
this.showToolbarSaveButton = !!params.query_props |
||||
this.setPartition(toState); |
||||
this.cdRef.detectChanges(); |
||||
}); |
||||
|
||||
this.board$ |
||||
.pipe( |
||||
this.untilDestroyed() |
||||
) |
||||
.subscribe(board => { |
||||
let queryProps = this.state.params.query_props; |
||||
this.editable = board.editable; |
||||
this.selectedTitle = board.name; |
||||
this.boardFilters.filters.putValue(queryProps ? JSON.parse(queryProps) : board.filters); |
||||
this.cdRef.detectChanges(); |
||||
}); |
||||
} |
||||
|
||||
ngOnDestroy():void { |
||||
super.ngOnDestroy(); |
||||
this.removeTransitionSubscription(); |
||||
} |
||||
|
||||
changeChangesFromTitle(newName:string) { |
||||
this.board$ |
||||
.pipe(take(1)) |
||||
.subscribe(board => { |
||||
board.name = newName; |
||||
board.filters = this.boardFilters.current; |
||||
|
||||
let params = { isNew: false, query_props: null }; |
||||
this.state.go('.', params, { custom: { notify: false } }); |
||||
|
||||
this.boardSaver.request(board); |
||||
}); |
||||
} |
||||
|
||||
updateTitleName(val:string) { |
||||
this.changeChangesFromTitle(val); |
||||
} |
||||
|
||||
/** Whether the title can be edited */ |
||||
get titleEditingEnabled():boolean { |
||||
return this.editable; |
||||
} |
||||
|
||||
/** |
||||
* We need to set the current partition to the grid to ensure |
||||
* either side gets expanded to full width if we're not in '-split' mode. |
||||
* |
||||
* @param state The current or entering state |
||||
*/ |
||||
protected setPartition(state:Ng2StateDeclaration) { |
||||
this.currentPartition = (state.data && state.data.partition) ? state.data.partition : '-split'; |
||||
} |
||||
} |
@ -1,95 +0,0 @@ |
||||
<div *ngIf="board" |
||||
[ngClass]="{ '-editable': board.editable, '-free' : board.isFree}" |
||||
class="board--container"> |
||||
|
||||
<ng-container wp-isolated-query-space> |
||||
<div class="toolbar-container -editable"> |
||||
<div id="toolbar"> |
||||
<div class="title-container board--header-container"> |
||||
|
||||
<back-button linkClass="board--back-button" |
||||
[customBackMethod]="goBack.bind(this)"> |
||||
</back-button> |
||||
|
||||
<editable-toolbar-title [title]="board.name" |
||||
[inFlight]="inFlight" |
||||
[initialFocus]="isNew" |
||||
(onSave)="saveWithNameAndFilters(board, $event)" |
||||
[editable]="board.editable" |
||||
[showSaveCondition]="!!state.params.query_props"> |
||||
</editable-toolbar-title> |
||||
|
||||
<ul class="toolbar-items" |
||||
*ngIf="showBoardListView()"> |
||||
|
||||
<li class="toolbar-item hidden-for-mobile"> |
||||
<wp-filter-button> |
||||
</wp-filter-button> |
||||
</li> |
||||
|
||||
<li class="toolbar-item hidden-for-mobile"> |
||||
<zen-mode-toggle-button></zen-mode-toggle-button> |
||||
</li> |
||||
<li *ngIf="board.editable" |
||||
class="toolbar-item hidden-for-mobile"> |
||||
<button title="{{ text.button_more }}" |
||||
class="button last board--settings-dropdown toolbar-icon" |
||||
boardsToolbarMenu |
||||
[boardsToolbarMenu-resource]="board"> |
||||
<op-icon icon-classes="button--icon icon-show-more"></op-icon> |
||||
</button> |
||||
</li> |
||||
</ul> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="boards-filters-container"> |
||||
<board-filter [board]="board" |
||||
[filters]="filters" |
||||
(onFiltersChanged)="updateFilters($event)"></board-filter> |
||||
</div> |
||||
|
||||
</ng-container> |
||||
|
||||
<div class="boards-list--container" |
||||
#container |
||||
*ngIf="showBoardListView()" |
||||
cdkDropList |
||||
[cdkDropListDisabled]="!board.editable" |
||||
cdkDropListOrientation="horizontal" |
||||
(cdkDropListDropped)="moveList(board, $event)" |
||||
> |
||||
<div *ngFor="let queryWidget of board.queries; trackBy:trackByQueryId" |
||||
class="boards-list--item" |
||||
wp-isolated-query-space |
||||
cdkDrag |
||||
vsDragScroll |
||||
[cdkDragData]="queryWidget" |
||||
[vsDragScrollContainer]="_container"> |
||||
<span *ngIf="board.editable" |
||||
class="boards-list-item-handle icon icon-drag-handle" |
||||
cdkDragHandle></span> |
||||
<board-list [resource]="queryWidget" |
||||
[board]="board" |
||||
(onRemove)="removeList(board, queryWidget)" |
||||
[filters]="filters"></board-list> |
||||
</div> |
||||
|
||||
<div class="boards-list--add-item -no-text-select" |
||||
*ngIf="board.editable" |
||||
(click)="addList(board)"> |
||||
<div class="boards-list--add-item-text"> |
||||
<op-icon icon-classes="icon-add icon-context"></op-icon> |
||||
<span [textContent]="text.addList"></span> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<enterprise-banner *ngIf="!showBoardListView()" |
||||
[leftMargin]="true" |
||||
[linkMessage]="text.upsaleCheckOutLink" |
||||
[textMessage]="text.upsaleBoards" |
||||
[opReferrer]="opReferrer(board)"> |
||||
</enterprise-banner> |
||||
</div> |
@ -0,0 +1,25 @@ |
||||
import {Component, Input} from "@angular/core"; |
||||
import {I18nService} from "core-app/modules/common/i18n/i18n.service"; |
||||
import {Board} from "core-app/modules/boards/board/board"; |
||||
import {Observable} from "rxjs"; |
||||
|
||||
@Component({ |
||||
template: ` |
||||
<button title="{{ text.button_more }}" |
||||
class="button last board--settings-dropdown toolbar-icon" |
||||
boardsToolbarMenu |
||||
[boardsToolbarMenu-resource]="board$ | async"> |
||||
<op-icon icon-classes="button--icon icon-show-more"></op-icon> |
||||
</button> |
||||
` |
||||
}) |
||||
export class BoardsMenuButtonComponent { |
||||
@Input() board$:Observable<Board>; |
||||
|
||||
text = { |
||||
button_more: this.I18n.t('js.button_more'), |
||||
}; |
||||
|
||||
constructor(readonly I18n:I18nService) { |
||||
} |
||||
} |
@ -0,0 +1,107 @@ |
||||
// -- 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 {Ng2StateDeclaration, UIRouter} from "@uirouter/angular"; |
||||
import {BoardsRootComponent} from "core-app/modules/boards/boards-root/boards-root.component"; |
||||
import {BoardsIndexPageComponent} from "core-app/modules/boards/index-page/boards-index-page.component"; |
||||
import {BoardPartitionedPageComponent} from "core-app/modules/boards/board/board-partitioned-page/board-partitioned-page.component"; |
||||
import {BoardListContainerComponent} from "core-app/modules/boards/board/board-partitioned-page/board-list-container.component"; |
||||
import {makeSplitViewRoutes} from "core-app/modules/work_packages/routing/split-view-routes.template"; |
||||
import {WorkPackageSplitViewComponent} from "core-app/modules/work_packages/routing/wp-split-view/wp-split-view.component"; |
||||
|
||||
export const menuItemClass = 'board-view-menu-item'; |
||||
|
||||
export const BOARDS_ROUTES:Ng2StateDeclaration[] = [ |
||||
{ |
||||
name: 'boards', |
||||
parent: 'root', |
||||
// The trailing slash is important
|
||||
// cf., https://community.openproject.com/wp/29754
|
||||
url: '/boards/?query_props', |
||||
data: { |
||||
bodyClasses: 'router--boards-view-base', |
||||
menuItem: menuItemClass |
||||
}, |
||||
params: { |
||||
// Use custom encoder/decoder that ensures validity of URL string
|
||||
query_props: { type: 'opQueryString', dynamic: true } |
||||
}, |
||||
redirectTo: 'boards.list', |
||||
component: BoardsRootComponent |
||||
}, |
||||
{ |
||||
name: 'boards.list', |
||||
component: BoardsIndexPageComponent, |
||||
data: { |
||||
parent: 'boards', |
||||
bodyClasses: 'router--boards-list-view', |
||||
menuItem: menuItemClass |
||||
} |
||||
}, |
||||
{ |
||||
name: 'boards.partitioned', |
||||
url: '{board_id}', |
||||
params: { |
||||
board_id: { type: 'int' }, |
||||
isNew: { type: 'bool', inherit: false, dynamic: true } |
||||
}, |
||||
data: { |
||||
parent: 'boards', |
||||
bodyClasses: 'router--boards-full-view', |
||||
menuItem: menuItemClass |
||||
}, |
||||
reloadOnSearch: false, |
||||
component: BoardPartitionedPageComponent, |
||||
redirectTo: 'boards.partitioned.show', |
||||
}, |
||||
{ |
||||
name: 'boards.partitioned.show', |
||||
url: '', |
||||
data: { |
||||
baseRoute: 'boards.partitioned.show' |
||||
}, |
||||
views: { |
||||
'content-left': { component: BoardListContainerComponent } |
||||
} |
||||
}, |
||||
...makeSplitViewRoutes( |
||||
'boards.partitioned.show', |
||||
menuItemClass, |
||||
WorkPackageSplitViewComponent |
||||
) |
||||
]; |
||||
|
||||
export function uiRouterBoardsConfiguration(uiRouter:UIRouter) { |
||||
// Ensure boards/ are being redirected correctly
|
||||
// cf., https://community.openproject.com/wp/29754
|
||||
uiRouter.urlService.rules |
||||
.when( |
||||
new RegExp("^/projects/(.*)/boards$"), |
||||
match => `/projects/${match[1]}/boards/` |
||||
); |
||||
} |
Loading…
Reference in new issue