From b33faeea7cffb7e94f298ad19300bf44033e4178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Mon, 30 Mar 2020 10:00:33 +0200 Subject: [PATCH] Make boards a partitioned-page component and add split routes --- app/assets/stylesheets/layout/_boards.sass | 7 + app/assets/stylesheets/layout/_index.sass | 1 + .../add-list-modal.component.ts | 2 +- .../board-filter/board-filter.component.ts | 64 ++--- .../board-filter/board-filters.service.ts | 16 ++ .../board-list/board-list.component.html | 1 + .../board/board-list/board-list.component.ts | 67 +++--- .../board-list-container.component.html | 41 ++++ .../board-list-container.component.sass} | 47 +--- .../board-list-container.component.ts} | 225 ++++++------------ .../board-partitioned-page.component.ts | 221 +++++++++++++++++ .../modules/boards/board/board.component.html | 95 -------- ...oard-inline-add-autocompleter.component.ts | 2 +- .../boards-menu-button.component.ts | 25 ++ .../boards-toolbar-menu.directive.ts | 2 +- .../boards-root/boards-root.component.ts | 2 +- .../boards-sidebar/boards-menu.component.html | 2 +- .../boards-index-page.component.html | 2 +- .../new-board-modal.component.ts | 2 +- .../boards/openproject-boards.module.ts | 67 +----- .../boards/openproject-boards.routes.ts | 107 +++++++++ ...artitioned-query-space-page.component.html | 27 ++- .../partitioned-query-space-page.component.ts | 49 ++-- .../spec/features/support/board_page.rb | 17 +- 24 files changed, 629 insertions(+), 462 deletions(-) create mode 100644 app/assets/stylesheets/layout/_boards.sass create mode 100644 frontend/src/app/modules/boards/board/board-filter/board-filters.service.ts create mode 100644 frontend/src/app/modules/boards/board/board-partitioned-page/board-list-container.component.html rename frontend/src/app/modules/boards/board/{board.component.sass => board-partitioned-page/board-list-container.component.sass} (63%) rename frontend/src/app/modules/boards/board/{board.component.ts => board-partitioned-page/board-list-container.component.ts} (54%) create mode 100644 frontend/src/app/modules/boards/board/board-partitioned-page/board-partitioned-page.component.ts delete mode 100644 frontend/src/app/modules/boards/board/board.component.html create mode 100644 frontend/src/app/modules/boards/board/toolbar-menu/boards-menu-button.component.ts create mode 100644 frontend/src/app/modules/boards/openproject-boards.routes.ts diff --git a/app/assets/stylesheets/layout/_boards.sass b/app/assets/stylesheets/layout/_boards.sass new file mode 100644 index 0000000000..75faa622df --- /dev/null +++ b/app/assets/stylesheets/layout/_boards.sass @@ -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% diff --git a/app/assets/stylesheets/layout/_index.sass b/app/assets/stylesheets/layout/_index.sass index 6ee1c3a539..ef29575265 100644 --- a/app/assets/stylesheets/layout/_index.sass +++ b/app/assets/stylesheets/layout/_index.sass @@ -29,6 +29,7 @@ @import layout/admin @import layout/base @import layout/base_mobile +@import layout/boards @import layout/grid @import layout/tree_menu @import layout/warning_bar diff --git a/frontend/src/app/modules/boards/board/add-list-modal/add-list-modal.component.ts b/frontend/src/app/modules/boards/board/add-list-modal/add-list-modal.component.ts index e7cf0f3716..31236e6a89 100644 --- a/frontend/src/app/modules/boards/board/add-list-modal/add-list-modal.component.ts +++ b/frontend/src/app/modules/boards/board/add-list-modal/add-list-modal.component.ts @@ -133,7 +133,7 @@ export class AddListModalComponent extends OpModalComponent implements OnInit { this.inFlight = false; this.closeMe(); this.boardCache.update(board); - this.state.go('boards.show', { board_id: board.id, isNew: true }); + this.state.go('boards.partitioned.show', { board_id: board.id, isNew: true }); }) .catch(() => this.inFlight = false); } diff --git a/frontend/src/app/modules/boards/board/board-filter/board-filter.component.ts b/frontend/src/app/modules/boards/board/board-filter/board-filter.component.ts index 510c3e3828..454822ca11 100644 --- a/frontend/src/app/modules/boards/board/board-filter/board-filter.component.ts +++ b/frontend/src/app/modules/boards/board/board-filter/board-filter.component.ts @@ -1,4 +1,4 @@ -import {Component, Input, Output} from "@angular/core"; +import {AfterViewInit, Component, Input} from "@angular/core"; import {Board} from "core-app/modules/boards/board/board"; import {CurrentProjectService} from "core-components/projects/current-project.service"; import {QueryFormDmService} from "core-app/modules/hal/dm-services/query-form-dm.service"; @@ -11,27 +11,18 @@ import {WorkPackageViewFiltersService} from "core-app/modules/work_packages/rout import {QueryFilterInstanceResource} from "core-app/modules/hal/resources/query-filter-instance-resource"; import {UrlParamsHelperService} from "core-components/wp-query/url-params-helper"; import {StateService} from "@uirouter/core"; -import {DebouncedEventEmitter} from "core-components/angular/debounced-event-emitter"; -import {skip} from "rxjs/internal/operators"; -import {ApiV3Filter} from "core-components/api/api-v3/api-v3-filter-builder"; +import {debounceTime, skip, take} from "rxjs/internal/operators"; import {UntilDestroyedMixin} from "core-app/helpers/angular/until-destroyed.mixin"; -import {componentDestroyed} from "@w11k/ngx-componentdestroyed"; +import {Observable} from "rxjs"; +import {BoardFiltersService} from "core-app/modules/boards/board/board-filter/board-filters.service"; @Component({ selector: 'board-filter', templateUrl: './board-filter.component.html' }) -export class BoardFilterComponent extends UntilDestroyedMixin { +export class BoardFilterComponent extends UntilDestroyedMixin implements AfterViewInit { /** Current active */ - @Input() public board:Board; - - /** Transient set of active filters - * Either from saved board (then filters === board.filters) - * or from the unsaved query props - */ - @Input() public filters:ApiV3Filter[]; - - @Output() public onFiltersChanged = new DebouncedEventEmitter(componentDestroyed(this)); + @Input() public board$:Observable; initialized = false; @@ -41,37 +32,31 @@ export class BoardFilterComponent extends UntilDestroyedMixin { private readonly wpStatesInitialization:WorkPackageStatesInitializationService, private readonly wpTableFilters:WorkPackageViewFiltersService, private readonly urlParamsHelper:UrlParamsHelperService, + private readonly boardFilters:BoardFiltersService, private readonly $state:StateService, private readonly queryFormDm:QueryFormDmService) { super(); } - /** - * Avoid initializing onInit to avoid loading the form earlier - * than other parts of the board. - * - * Instead, the board component will instrument this method - * when children are loaded. - */ - public doInitialize():void { - if (this.initialized) { + ngAfterViewInit():void { + if (!this.board$) { return; } - // Since we're being called from the board component - // ensure this happens only once. - this.initialized = true; - - // Initially load the form once to be able to render filters - this.loadQueryForm(); + this.board$ + .pipe(take(1)) + .subscribe(board => { + // Initially load the form once to be able to render filters + this.loadQueryForm(); - // Update checksum service whenever filters change - this.updateChecksumOnFilterChanges(); + // Update checksum service whenever filters change + this.updateChecksumOnFilterChanges(); - // Remove action attribute from filter service - if (this.board.isAction) { - this.wpTableFilters.hidden.push(this.board.actionAttribute!); - } + // Remove action attribute from filter service + if (board.isAction) { + this.wpTableFilters.hidden.push(board.actionAttribute!); + } + }); } private updateChecksumOnFilterChanges() { @@ -79,7 +64,8 @@ export class BoardFilterComponent extends UntilDestroyedMixin { .live$() .pipe( this.untilDestroyed(), - skip(1) + skip(1), + debounceTime(250) ) .subscribe(() => { @@ -87,7 +73,7 @@ export class BoardFilterComponent extends UntilDestroyedMixin { let filterHash = this.urlParamsHelper.buildV3GetFilters(filters); let query_props = JSON.stringify(filterHash); - this.onFiltersChanged.emit(filterHash); + this.boardFilters.filters.putValue(filterHash); this.$state.go('.', { query_props: query_props }, { custom: { notify: false } }); }); @@ -96,7 +82,7 @@ export class BoardFilterComponent extends UntilDestroyedMixin { private loadQueryForm() { this.queryFormDm .loadWithParams( - { filters: JSON.stringify(this.filters) }, + { filters: JSON.stringify(this.boardFilters.current) }, undefined, this.currentProjectService.id ) diff --git a/frontend/src/app/modules/boards/board/board-filter/board-filters.service.ts b/frontend/src/app/modules/boards/board/board-filter/board-filters.service.ts new file mode 100644 index 0000000000..e0b58652a4 --- /dev/null +++ b/frontend/src/app/modules/boards/board/board-filter/board-filters.service.ts @@ -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([]); + + get current():ApiV3Filter[] { + return this.filters.getValueOr([]); + } +} \ No newline at end of file diff --git a/frontend/src/app/modules/boards/board/board-list/board-list.component.html b/frontend/src/app/modules/boards/board/board-list/board-list.component.html index 2cb5372838..2bfbac69c9 100644 --- a/frontend/src/app/modules/boards/board/board-list/board-list.component.html +++ b/frontend/src/app/modules/boards/board/board-list/board-list.component.html @@ -53,6 +53,7 @@ [dragInto]="canDragInto" [workPackageAddedHandler]="workPackageAddedHandler" [cardsRemovable]="board.isFree && canDragOutOf" + [showInfoButton]="true" [highlightingMode]="board.highlightingMode" [showStatusButton]="showCardStatusButton()"> diff --git a/frontend/src/app/modules/boards/board/board-list/board-list.component.ts b/frontend/src/app/modules/boards/board/board-list/board-list.component.ts index f4486d910b..8bf4e4d9eb 100644 --- a/frontend/src/app/modules/boards/board/board-list/board-list.component.ts +++ b/frontend/src/app/modules/boards/board/board-list/board-list.component.ts @@ -1,14 +1,14 @@ import { + ChangeDetectionStrategy, + ChangeDetectorRef, Component, ElementRef, EventEmitter, Injector, Input, - OnChanges, OnDestroy, OnInit, Output, - SimpleChanges, ViewChild } from "@angular/core"; import {QueryDmService} from "core-app/modules/hal/dm-services/query-dm.service"; @@ -48,6 +48,7 @@ import {debugLog} from "core-app/helpers/debug_output"; import {WorkPackageCardDragAndDropService} from "core-components/wp-card-view/services/wp-card-drag-and-drop.service"; import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset"; import {componentDestroyed} from "@w11k/ngx-componentdestroyed"; +import {BoardFiltersService} from "core-app/modules/boards/board/board-filter/board-filters.service"; export interface DisabledButtonPlaceholder { text:string; @@ -58,22 +59,20 @@ export interface DisabledButtonPlaceholder { selector: 'board-list', templateUrl: './board-list.component.html', styleUrls: ['./board-list.component.sass'], + changeDetection: ChangeDetectionStrategy.OnPush, providers: [ { provide: WorkPackageInlineCreateService, useClass: BoardInlineCreateService }, BoardListMenuComponent, WorkPackageCardDragAndDropService ] }) -export class BoardListComponent extends AbstractWidgetComponent implements OnInit, OnDestroy, OnChanges { +export class BoardListComponent extends AbstractWidgetComponent implements OnInit, OnDestroy { /** Output fired upon query removal */ @Output() onRemove = new EventEmitter(); /** Access to the board resource */ @Input() public board:Board; - /** Access the filters of the board */ - @Input() public filters:ApiV3Filter[]; - /** Access to the loading indicator element */ @ViewChild('loadingIndicator', { static: true }) indicator:ElementRef; @@ -122,22 +121,24 @@ export class BoardListComponent extends AbstractWidgetComponent implements OnIni public buttonPlaceholder:DisabledButtonPlaceholder|undefined; - constructor(private readonly QueryDm:QueryDmService, - private readonly I18n:I18nService, - private readonly boardCache:BoardCacheService, - private readonly notifications:NotificationsService, - private readonly querySpace:IsolatedQuerySpace, - private readonly halNotification:HalResourceNotificationService, - private readonly wpStatesInitialization:WorkPackageStatesInitializationService, - private readonly authorisationService:AuthorisationService, - private readonly wpInlineCreate:WorkPackageInlineCreateService, - protected readonly injector:Injector, - private readonly halEditing:HalResourceEditingService, - private readonly loadingIndicator:LoadingIndicatorService, - private readonly wpCacheService:WorkPackageCacheService, - private readonly boardService:BoardService, - private readonly boardActionRegistry:BoardActionsRegistryService, - private readonly causedUpdates:CausedUpdatesService) { + constructor(readonly QueryDm:QueryDmService, + readonly I18n:I18nService, + readonly cdRef:ChangeDetectorRef, + readonly boardCache:BoardCacheService, + readonly boardFilters:BoardFiltersService, + readonly notifications:NotificationsService, + readonly querySpace:IsolatedQuerySpace, + readonly halNotification:HalResourceNotificationService, + readonly wpStatesInitialization:WorkPackageStatesInitializationService, + readonly authorisationService:AuthorisationService, + readonly wpInlineCreate:WorkPackageInlineCreateService, + readonly injector:Injector, + readonly halEditing:HalResourceEditingService, + readonly loadingIndicator:LoadingIndicatorService, + readonly wpCacheService:WorkPackageCacheService, + readonly boardService:BoardService, + readonly boardActionRegistry:BoardActionsRegistryService, + readonly causedUpdates:CausedUpdatesService) { super(I18n, injector); } @@ -152,9 +153,19 @@ export class BoardListComponent extends AbstractWidgetComponent implements OnIni .subscribe(() => { if (!this.board.isAction) { this.showAddButton = this.canDragInto && (this.wpInlineCreate.canAdd || this.canReference); + this.cdRef.detectChanges(); } }); + // Update query on filter change + this.boardFilters + .filters + .values$() + .pipe( + this.untilDestroyed() + ) + .subscribe(() => this.updateQuery(true)); + this.querySpace.query .values$() .pipe( @@ -164,19 +175,12 @@ export class BoardListComponent extends AbstractWidgetComponent implements OnIni this.query = query; this.canDragOutOf = !!this.query.updateOrderedWorkPackages; this.loadActionAttribute(query); + this.cdRef.detectChanges(); }); this.updateQuery(); } - ngOnChanges(changes:SimpleChanges) { - // When the changes were caused by an actual filter change - // and not by a change in lists - if (changes.filters && !changes.resource) { - this.updateQuery(); - } - } - public get errorMessage() { return this.I18n.t('js.boards.error_loading_the_list', { error_message: this.loadingError }); } @@ -266,7 +270,7 @@ export class BoardListComponent extends AbstractWidgetComponent implements OnIni } public updateQuery(visibly = true) { - this.setQueryProps(this.filters); + this.setQueryProps(this.boardFilters.current); this.loadQuery(visibly); } @@ -295,6 +299,7 @@ export class BoardListComponent extends AbstractWidgetComponent implements OnIni const canWriteAttribute = await this.actionService.canAddToQuery(query); this.showAddButton = this.canDragInto && this.wpInlineCreate.canAdd && canWriteAttribute; + this.cdRef.detectChanges(); }); } diff --git a/frontend/src/app/modules/boards/board/board-partitioned-page/board-list-container.component.html b/frontend/src/app/modules/boards/board/board-partitioned-page/board-list-container.component.html new file mode 100644 index 0000000000..44842bc034 --- /dev/null +++ b/frontend/src/app/modules/boards/board/board-partitioned-page/board-list-container.component.html @@ -0,0 +1,41 @@ + +
+
+ + +
+ +
+
+ + +
+
+
+ + + +
\ No newline at end of file diff --git a/frontend/src/app/modules/boards/board/board.component.sass b/frontend/src/app/modules/boards/board/board-partitioned-page/board-list-container.component.sass similarity index 63% rename from frontend/src/app/modules/boards/board/board.component.sass rename to frontend/src/app/modules/boards/board/board-partitioned-page/board-list-container.component.sass index d7672adff6..208925e4e2 100644 --- a/frontend/src/app/modules/boards/board/board.component.sass +++ b/frontend/src/app/modules/boards/board/board-partitioned-page/board-list-container.component.sass @@ -2,40 +2,16 @@ $board-list-max-width: 300px -// Let board list span whole screen -.router--boards-full-view - @include extended-content--bottom - @include extended-content--left - - #content - height: 100% - .boards-list--container display: flex flex-direction: row flex-wrap: nowrap flex-grow: 1 + height: 100% // Make it scrollable overflow-y: auto - -.boards-list--item - flex: 0 0 $board-list-max-width - max-width: $board-list-max-width - position: relative - - &:first-of-type - margin-left: 5px - - &.cdk-drag-placeholder - @include modifying--placeholder - -.board--header-container - display: flex - align-items: flex-start - margin-bottom: 0 - .boards-list-item-handle position: absolute font-size: 14px @@ -45,9 +21,6 @@ $board-list-max-width: 300px opacity: 0 cursor: grab - .board--container.-free & - top: 13px - &:before padding: 0 color: var(--light-gray) @@ -65,18 +38,16 @@ $board-list-max-width: 300px cursor: pointer color: var(--gray-dark) - .board--container.-free & - margin-top: 0 - &:hover color: var(--body-font-color) -.board--container +.boards-list--item + flex: 0 0 $board-list-max-width + max-width: $board-list-max-width position: relative - display: flex - flex-direction: column - height: 100% -.boards-filters-container .advanced-filters--container - margin-bottom: 1rem - margin-left: 20px + &:first-of-type + margin-left: 5px + + &.cdk-drag-placeholder + @include modifying--placeholder diff --git a/frontend/src/app/modules/boards/board/board.component.ts b/frontend/src/app/modules/boards/board/board-partitioned-page/board-list-container.component.ts similarity index 54% rename from frontend/src/app/modules/boards/board/board.component.ts rename to frontend/src/app/modules/boards/board/board-partitioned-page/board-list-container.component.ts index 10b5b5d35f..bd275809c1 100644 --- a/frontend/src/app/modules/boards/board/board.component.ts +++ b/frontend/src/app/modules/boards/board/board-partitioned-page/board-list-container.component.ts @@ -1,56 +1,47 @@ -import { - Component, - ElementRef, - Injector, - OnDestroy, - OnInit, - QueryList, - ViewChild, - ViewChildren, - ViewEncapsulation -} from "@angular/core"; +import {Component, ElementRef, Injector, OnInit, QueryList, ViewChild, ViewChildren} from "@angular/core"; +import {Observable, Subscription} from "rxjs"; +import {QueryResource} from "core-app/modules/hal/resources/query-resource"; +import {BoardListComponent} from "core-app/modules/boards/board/board-list/board-list.component"; +import {StateService} from "@uirouter/core"; import {NotificationsService} from "core-app/modules/common/notifications/notifications.service"; -import {I18nService} from "core-app/modules/common/i18n/i18n.service"; +import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; import {BoardListsService} from "core-app/modules/boards/board/board-list/board-lists.service"; +import {OpModalService} from "core-components/op-modals/op-modal.service"; import {BoardCacheService} from "core-app/modules/boards/board/board-cache.service"; import {BoardService} from "core-app/modules/boards/board/board.service"; -import {Board} from "core-app/modules/boards/board/board"; -import {StateService} from "@uirouter/core"; -import {GridWidgetResource} from "core-app/modules/hal/resources/grid-widget-resource"; -import {CdkDragDrop, moveItemInArray} from "@angular/cdk/drag-drop"; -import {BoardListComponent} from "core-app/modules/boards/board/board-list/board-list.component"; -import {OpModalService} from "core-components/op-modals/op-modal.service"; -import {AddListModalComponent} from "core-app/modules/boards/board/add-list-modal/add-list-modal.component"; import {BannersService} from "core-app/modules/common/enterprise/banners.service"; -import {ApiV3Filter} from "core-components/api/api-v3/api-v3-filter-builder"; -import {RequestSwitchmap} from "core-app/helpers/rxjs/request-switchmap"; -import {from, Subscription} from "rxjs"; -import {BoardFilterComponent} from "core-app/modules/boards/board/board-filter/board-filter.component"; -import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; import {DragAndDropService} from "core-app/modules/common/drag-and-drop/drag-and-drop.service"; import {QueryUpdatedService} from "core-app/modules/boards/board/query-updated/query-updated.service"; -import {QueryResource} from "core-app/modules/hal/resources/query-resource"; -import {componentDestroyed} from "@w11k/ngx-componentdestroyed"; import {UntilDestroyedMixin} from "core-app/helpers/angular/until-destroyed.mixin"; +import {Board} from "core-app/modules/boards/board/board"; +import {CdkDragDrop, moveItemInArray} from "@angular/cdk/drag-drop"; +import {GridWidgetResource} from "core-app/modules/hal/resources/grid-widget-resource"; +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 {I18nService} from "core-app/modules/common/i18n/i18n.service"; @Component({ - selector: 'board', - templateUrl: './board.component.html', - styleUrls: ['./board.component.sass'], - // Necessary to let the board span the complete height of the screen - encapsulation: ViewEncapsulation.None, - providers: [ - DragAndDropService, - ] + templateUrl: './board-list-container.component.html', + styleUrls: ['./board-list-container.component.sass'] }) -export class BoardComponent extends UntilDestroyedMixin implements OnInit, OnDestroy { +export class BoardListContainerComponent extends UntilDestroyedMixin implements OnInit { - /** Reference all query children to extract current actions */ - @ViewChildren(BoardListComponent) lists:QueryList; + 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'), + 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'), + }; - public _container:HTMLElement; /** Container reference */ + public _container:HTMLElement; @ViewChild('container') set container(v:ElementRef|undefined) { // ViewChild reference may be undefined initially @@ -63,128 +54,53 @@ export class BoardComponent extends UntilDestroyedMixin implements OnInit, OnDes } } - /** Reference to the filter component */ - @ViewChild(BoardFilterComponent) - set content(v:BoardFilterComponent|undefined) { - // ViewChild reference may be undefined initially - // due to ngIf - if (v !== undefined) { - setTimeout(() => v.doInitialize()); - } - } - - /** Board observable */ - public board:Board; - - /** Whether this is a new board just created */ - public isNew:boolean = !!this.state.params.isNew; - - /** Whether we're in flight of updating the board */ - public inFlight = false; - - /** Board filter */ - public filters:ApiV3Filter[]; - - public 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'), - }; + /** Reference all query children to extract current actions */ + @ViewChildren(BoardListComponent) lists:QueryList; - // We remember when we want to update the board - private boardSaver = new RequestSwitchmap( - (board:Board) => { - this.inFlight = true; - const promise = this.Boards - .save(board) - .then(board => { - this.inFlight = false; - return board; - }) - .catch((error) => { - this.inFlight = false; - throw error; - }); + trackByQueryId = (index:number, widget:GridWidgetResource) => widget.options.queryId; - return from(promise); - } - ); + board$:Observable; - trackByQueryId = (index:number, widget:GridWidgetResource) => widget.options.queryId; + private currentQueryUpdatedMonitoring:Subscription; - constructor(public readonly state:StateService, - private readonly I18n:I18nService, - private readonly notifications:NotificationsService, - private readonly halNotification:HalResourceNotificationService, - private readonly BoardList:BoardListsService, - private readonly opModalService:OpModalService, - private readonly injector:Injector, - private readonly BoardCache:BoardCacheService, - private readonly Boards:BoardService, - private readonly Banner:BannersService, - private readonly Drag:DragAndDropService, - private readonly QueryUpdated:QueryUpdatedService) { + constructor(readonly I18n:I18nService, + readonly state:StateService, + readonly notifications:NotificationsService, + readonly halNotification:HalResourceNotificationService, + readonly boardComponent:BoardPartitionedPageComponent, + readonly BoardList:BoardListsService, + readonly opModalService:OpModalService, + readonly injector:Injector, + readonly BoardCache:BoardCacheService, + readonly Boards:BoardService, + readonly Banner:BannersService, + readonly Drag:DragAndDropService, + readonly QueryUpdated:QueryUpdatedService) { super(); } - goBack() { - this.state.go('^'); - } - ngOnInit():void { const id:string = this.state.params.board_id.toString(); - // 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.board$ = this.BoardCache.observe(id); - this.BoardCache - .observe(id) + this.board$ .pipe( this.untilDestroyed() ) .subscribe(board => { - this.board = board; - let queryProps = this.state.params.query_props; - this.filters = queryProps ? JSON.parse(queryProps) : this.board.filters; - - this.setupQueryUpdatedMonitoring(); + this.setupQueryUpdatedMonitoring(board); }); } - saveWithNameAndFilters(board:Board, newName:string) { - board.name = newName; - board.filters = this.filters; - - let params = { isNew: false, query_props: null }; - this.state.go('.', params, { custom: { notify: false } }); - + moveList(board:Board, event:CdkDragDrop) { + moveItemInArray(board.queries, event.previousIndex, event.currentIndex); this.saveBoard(board); } - showError(text = this.text.loadingError) { - this.notifications.addError(text); - } - - saveBoard(board:Board):void { - this.boardSaver.request(board); + removeList(board:Board, query:GridWidgetResource) { + board.removeQuery(query); + this.saveBoard(board); } addList(board:Board):any { @@ -206,44 +122,36 @@ export class BoardComponent extends UntilDestroyedMixin implements OnInit, OnDes } } - moveList(board:Board, event:CdkDragDrop) { - moveItemInArray(board.queries, event.previousIndex, event.currentIndex); - this.saveBoard(board); - } - - removeList(board:Board, query:GridWidgetResource) { - board.removeQuery(query); - this.saveBoard(board); - } - - public showBoardListView() { + showBoardListView() { return !this.Banner.eeShowBanners; } - public opReferrer(board:Board) { + opReferrer(board:Board) { return board.isFree ? 'boards#free' : 'boards#status'; } - public updateFilters(filters:ApiV3Filter[]) { - this.filters = filters; + saveBoard(board:Board):void { + this.boardComponent.boardSaver.request(board); } - private currentQueryUpdatedMonitoring:Subscription; - - private setupQueryUpdatedMonitoring() { + private setupQueryUpdatedMonitoring(board:Board) { if (this.currentQueryUpdatedMonitoring) { this.currentQueryUpdatedMonitoring.unsubscribe(); } this.currentQueryUpdatedMonitoring = this .QueryUpdated - .monitor(this.board.queries.map((widget) => widget.options.queryId as string)) + .monitor(board.queries.map((widget) => widget.options.queryId as string)) .pipe( this.untilDestroyed() ) .subscribe((collection) => this.requestRefreshOfUpdatedLists(collection.elements)); } + private showError(text = this.text.loadingError) { + this.notifications.addError(text); + } + private requestRefreshOfUpdatedLists(queries:QueryResource[]) { queries.forEach((query) => { this @@ -257,4 +165,5 @@ export class BoardComponent extends UntilDestroyedMixin implements OnInit, OnDes .forEach((listComponent) => listComponent.refreshQueryUnlessCaused(query, false)); }); } -} + +} \ No newline at end of file diff --git a/frontend/src/app/modules/boards/board/board-partitioned-page/board-partitioned-page.component.ts b/frontend/src/app/modules/boards/board/board-partitioned-page/board-partitioned-page.component.ts new file mode 100644 index 0000000000..83d867aa68 --- /dev/null +++ b/frontend/src/app/modules/boards/board/board-partitioned-page/board-partitioned-page.component.ts @@ -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'; + } +} diff --git a/frontend/src/app/modules/boards/board/board.component.html b/frontend/src/app/modules/boards/board/board.component.html deleted file mode 100644 index cdad782a58..0000000000 --- a/frontend/src/app/modules/boards/board/board.component.html +++ /dev/null @@ -1,95 +0,0 @@ -
- - -
-
-
- - - - - - - -
    - -
  • - - -
  • - -
  • - -
  • -
  • - -
  • -
-
-
-
- -
- -
- -
- -
-
- - -
- -
-
- - -
-
-
- - - -
diff --git a/frontend/src/app/modules/boards/board/inline-add/board-inline-add-autocompleter.component.ts b/frontend/src/app/modules/boards/board/inline-add/board-inline-add-autocompleter.component.ts index 7d8cd931ef..b14cd1c727 100644 --- a/frontend/src/app/modules/boards/board/inline-add/board-inline-add-autocompleter.component.ts +++ b/frontend/src/app/modules/boards/board/inline-add/board-inline-add-autocompleter.component.ts @@ -65,7 +65,7 @@ export class BoardInlineAddAutocompleterComponent implements AfterViewInit { placeholder: this.I18n.t('js.relations_autocomplete.placeholder') }; - @Input() appendToContainer:string = '.board--container'; + @Input() appendToContainer:string = '.work-packages-partitioned-query-space--container'; @ViewChild(NgSelectComponent) public ngSelectComponent:NgSelectComponent; @Output() onCancel = new EventEmitter(); diff --git a/frontend/src/app/modules/boards/board/toolbar-menu/boards-menu-button.component.ts b/frontend/src/app/modules/boards/board/toolbar-menu/boards-menu-button.component.ts new file mode 100644 index 0000000000..74941059e7 --- /dev/null +++ b/frontend/src/app/modules/boards/board/toolbar-menu/boards-menu-button.component.ts @@ -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: ` + + ` +}) +export class BoardsMenuButtonComponent { + @Input() board$:Observable; + + text = { + button_more: this.I18n.t('js.button_more'), + }; + + constructor(readonly I18n:I18nService) { + } +} \ No newline at end of file diff --git a/frontend/src/app/modules/boards/board/toolbar-menu/boards-toolbar-menu.directive.ts b/frontend/src/app/modules/boards/board/toolbar-menu/boards-toolbar-menu.directive.ts index 8b588fb12f..fd4ab77dcd 100644 --- a/frontend/src/app/modules/boards/board/toolbar-menu/boards-toolbar-menu.directive.ts +++ b/frontend/src/app/modules/boards/board/toolbar-menu/boards-toolbar-menu.directive.ts @@ -93,7 +93,7 @@ export class BoardsToolbarMenuDirective extends OpContextMenuTrigger { icon: 'icon-edit', onClick: ($event:JQuery.TriggeredEvent) => { if (!!this.board.grid.updateImmediately) { - jQuery(`.board--header-container .editable-toolbar-title--input`).trigger(triggerEditingEvent); + jQuery(`.toolbar-container .editable-toolbar-title--input`).trigger(triggerEditingEvent); } return true; diff --git a/frontend/src/app/modules/boards/boards-root/boards-root.component.ts b/frontend/src/app/modules/boards/boards-root/boards-root.component.ts index 032fe66b87..e3fbafc116 100644 --- a/frontend/src/app/modules/boards/boards-root/boards-root.component.ts +++ b/frontend/src/app/modules/boards/boards-root/boards-root.component.ts @@ -7,7 +7,7 @@ import {QueryUpdatedService} from "core-app/modules/boards/board/query-updated/q @Component({ selector: 'boards-entry', - template: '', + template: '', providers: [ BoardConfigurationService, BoardStatusActionService, diff --git a/frontend/src/app/modules/boards/boards-sidebar/boards-menu.component.html b/frontend/src/app/modules/boards/boards-sidebar/boards-menu.component.html index 4328e66880..b83130832e 100644 --- a/frontend/src/app/modules/boards/boards-sidebar/boards-menu.component.html +++ b/frontend/src/app/modules/boards/boards-sidebar/boards-menu.component.html @@ -2,7 +2,7 @@