Make boards a partitioned-page component and add split routes

pull/8230/head
Oliver Günther 5 years ago
parent eff5075c72
commit b33faeea7c
No known key found for this signature in database
GPG Key ID: A3A8BDAD7C0C552C
  1. 7
      app/assets/stylesheets/layout/_boards.sass
  2. 1
      app/assets/stylesheets/layout/_index.sass
  3. 2
      frontend/src/app/modules/boards/board/add-list-modal/add-list-modal.component.ts
  4. 52
      frontend/src/app/modules/boards/board/board-filter/board-filter.component.ts
  5. 16
      frontend/src/app/modules/boards/board/board-filter/board-filters.service.ts
  6. 1
      frontend/src/app/modules/boards/board/board-list/board-list.component.html
  7. 67
      frontend/src/app/modules/boards/board/board-list/board-list.component.ts
  8. 41
      frontend/src/app/modules/boards/board/board-partitioned-page/board-list-container.component.html
  9. 47
      frontend/src/app/modules/boards/board/board-partitioned-page/board-list-container.component.sass
  10. 223
      frontend/src/app/modules/boards/board/board-partitioned-page/board-list-container.component.ts
  11. 221
      frontend/src/app/modules/boards/board/board-partitioned-page/board-partitioned-page.component.ts
  12. 95
      frontend/src/app/modules/boards/board/board.component.html
  13. 2
      frontend/src/app/modules/boards/board/inline-add/board-inline-add-autocompleter.component.ts
  14. 25
      frontend/src/app/modules/boards/board/toolbar-menu/boards-menu-button.component.ts
  15. 2
      frontend/src/app/modules/boards/board/toolbar-menu/boards-toolbar-menu.directive.ts
  16. 2
      frontend/src/app/modules/boards/boards-root/boards-root.component.ts
  17. 2
      frontend/src/app/modules/boards/boards-sidebar/boards-menu.component.html
  18. 2
      frontend/src/app/modules/boards/index-page/boards-index-page.component.html
  19. 2
      frontend/src/app/modules/boards/new-board-modal/new-board-modal.component.ts
  20. 67
      frontend/src/app/modules/boards/openproject-boards.module.ts
  21. 107
      frontend/src/app/modules/boards/openproject-boards.routes.ts
  22. 27
      frontend/src/app/modules/work_packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.html
  23. 49
      frontend/src/app/modules/work_packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.ts
  24. 17
      modules/boards/spec/features/support/board_page.rb

@ -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%

@ -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

@ -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);
}

@ -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<ApiV3Filter[]>(componentDestroyed(this));
@Input() public board$:Observable<Board>;
initialized = false;
@ -41,27 +32,20 @@ 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;
this.board$
.pipe(take(1))
.subscribe(board => {
// Initially load the form once to be able to render filters
this.loadQueryForm();
@ -69,9 +53,10 @@ export class BoardFilterComponent extends UntilDestroyedMixin {
this.updateChecksumOnFilterChanges();
// Remove action attribute from filter service
if (this.board.isAction) {
this.wpTableFilters.hidden.push(this.board.actionAttribute!);
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
)

@ -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([]);
}
}

@ -53,6 +53,7 @@
[dragInto]="canDragInto"
[workPackageAddedHandler]="workPackageAddedHandler"
[cardsRemovable]="board.isFree && canDragOutOf"
[showInfoButton]="true"
[highlightingMode]="board.highlightingMode"
[showStatusButton]="showCardStatusButton()">

@ -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<void>();
/** 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();
});
}

@ -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>

@ -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

@ -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<BoardListComponent>;
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<BoardListComponent>;
// 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<Board>;
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<GridWidgetResource[]>) {
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<GridWidgetResource[]>) {
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));
});
}
}

@ -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>

@ -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<undefined>();

@ -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) {
}
}

@ -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;

@ -7,7 +7,7 @@ import {QueryUpdatedService} from "core-app/modules/boards/board/query-updated/q
@Component({
selector: 'boards-entry',
template: '<ui-view></ui-view>',
template: '<ui-view wp-isolated-query-space></ui-view>',
providers: [
BoardConfigurationService,
BoardStatusActionService,

@ -2,7 +2,7 @@
<ul class="main-menu--children boards--menu-items">
<li *ngFor="let board of boards;trackBy:trackById">
<a [textContent]="board.name"
uiSref="boards.show"
uiSref="boards.partitioned.show"
[uiOptions]="{ reload: true }"
[uiParams]="{ board_id: board.id, query_props: '', projects: 'projects', projectPath: currentProjectIdentifier }"
class="main-menu--children-sub-item ellipsis">

@ -80,7 +80,7 @@
<tr *ngFor="let board of boards">
<td class="name">
<a [textContent]="board.name"
uiSref="boards.show"
uiSref="boards.partitioned.show"
[uiParams]="{ board_id: board.id }">
</a>
</td>

@ -96,7 +96,7 @@ export class NewBoardModalComponent extends OpModalComponent {
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((error:unknown) => {
this.inFlight = false;

@ -29,8 +29,7 @@
import {NgModule} from '@angular/core';
import {OpenprojectCommonModule} from "core-app/modules/common/openproject-common.module";
import {OpenprojectWorkPackagesModule} from "core-app/modules/work_packages/openproject-work-packages.module";
import {Ng2StateDeclaration, UIRouter, UIRouterModule} from "@uirouter/angular";
import {BoardComponent} from "core-app/modules/boards/board/board.component";
import {UIRouterModule} from "@uirouter/angular";
import {BoardListComponent} from "core-app/modules/boards/board/board-list/board-list.component";
import {BoardsRootComponent} from "core-app/modules/boards/boards-root/boards-root.component";
import {BoardInlineAddAutocompleterComponent} from "core-app/modules/boards/board/inline-add/board-inline-add-autocompleter.component";
@ -47,62 +46,10 @@ import {DragScrollModule} from "cdk-drag-scroll";
import {BoardListMenuComponent} from "core-app/modules/boards/board/board-list/board-list-menu.component";
import {VersionBoardHeaderComponent} from "core-app/modules/boards/board/board-actions/version/version-board-header.component";
import {DynamicModule} from "ng-dynamic-component";
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.show',
url: '{board_id}',
params: {
board_id: {type: 'int'},
isNew: {type: 'bool', inherit: false, dynamic: true}
},
reloadOnSearch: false,
component: BoardComponent,
data: {
parent: 'boards',
bodyClasses: 'router--boards-full-view',
menuItem: menuItemClass
}
}
];
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/`
);
}
import {BOARDS_ROUTES, uiRouterBoardsConfiguration} from "core-app/modules/boards/openproject-boards.routes";
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 {BoardsMenuButtonComponent} from "core-app/modules/boards/board/toolbar-menu/boards-menu-button.component";
@NgModule({
imports: [
@ -121,7 +68,8 @@ export function uiRouterBoardsConfiguration(uiRouter:UIRouter) {
],
declarations: [
BoardsIndexPageComponent,
BoardComponent,
BoardPartitionedPageComponent,
BoardListContainerComponent,
BoardListComponent,
BoardsRootComponent,
BoardInlineAddAutocompleterComponent,
@ -129,6 +77,7 @@ export function uiRouterBoardsConfiguration(uiRouter:UIRouter) {
BoardHighlightingTabComponent,
BoardConfigurationModal,
BoardsToolbarMenuDirective,
BoardsMenuButtonComponent,
NewBoardModalComponent,
AddListModalComponent,
AddCardDropdownMenuDirective,

@ -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/`
);
}

@ -2,19 +2,26 @@
[ngClass]="currentPartition">
<div class="toolbar-container -editable">
<div class="toolbar">
<back-button *ngIf="backButtonCallback"
linkClass="back-button"
[customBackMethod]="backButtonCallback">
</back-button>
<editable-toolbar-title [title]="selectedTitle"
[inFlight]="querySaving"
[showSaveCondition]="hasQueryProps"
(onSave)="saveQueryFromTitle($event)"
(onEmptySubmit)="updateQueryName('')"
[inFlight]="toolbarDisabled"
[showSaveCondition]="showToolbarSaveButton"
(onSave)="changeChangesFromTitle($event)"
(onEmptySubmit)="updateTitleName('')"
[editable]="titleEditingEnabled">
</editable-toolbar-title>
<ul class="toolbar-items hide-when-print"
*ngIf="tableInformationLoaded">
*ngIf="showToolbar">
<ng-container *ngFor="let definition of toolbarButtonComponents">
<li class="toolbar-item" *ngIf="!definition.show || definition.show()" [ngClass]="definition.containerClasses">
<li class="toolbar-item"
*ngIf="!definition.show || definition.show()"
[ngClass]="definition.containerClasses">
<ndc-dynamic [ndcDynamicComponent]="definition.component"
[ndcDynamicInputs]="definition.inputs"
[ndcDynamicInjector]="injector"
@ -26,7 +33,13 @@
</div>
</div>
<filter-container></filter-container>
<div class="work-packages-partitioned-query-space--filter-area">
<ndc-dynamic [ndcDynamicComponent]="filterContainerDefinition.component"
[ndcDynamicInputs]="filterContainerDefinition.inputs"
[ndcDynamicOutputs]="filterContainerDefinition.outputs"
[ndcDynamicInjector]="injector">
</ndc-dynamic>
</div>
<div class="work-packages-partitioned-page--content-container">
<!-- Left content view -->

@ -37,15 +37,20 @@ import {QueryParamListenerService} from "core-components/wp-query/query-param-li
import {InjectField} from "core-app/helpers/angular/inject-field.decorator";
import {ComponentType} from "@angular/cdk/overlay";
import {Ng2StateDeclaration} from "@uirouter/angular";
import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {WorkPackageFilterContainerComponent} from "core-components/filters/filter-container/filter-container.directive";
export interface ToolbarButtonComponentDefinition {
export interface DynamicComponentDefinition {
component:ComponentType<any>;
containerClasses?:string;
show?:() => boolean;
inputs?:{ [inputName:string]:any };
outputs?:{ [outputName:string]:Function };
}
export interface ToolbarButtonComponentDefinition extends DynamicComponentDefinition {
containerClasses?:string;
show?:() => boolean;
}
export type ViewPartitionState = '-split'|'-left-only'|'-right-only';
@Component({
@ -60,6 +65,7 @@ export type ViewPartitionState = '-split'|'-left-only'|'-right-only';
]
})
export class PartitionedQuerySpacePageComponent extends WorkPackagesViewBase implements OnInit, OnDestroy {
@InjectField() I18n:I18nService;
@InjectField() titleService:OpTitleService;
@InjectField() queryParamListener:QueryParamListenerService;
@ -76,17 +82,17 @@ export class PartitionedQuerySpacePageComponent extends WorkPackagesViewBase imp
currentQuery:QueryResource|undefined;
/** Whether we're saving the query */
querySaving:boolean;
toolbarDisabled:boolean;
/** Do we currently have query props ? */
hasQueryProps:boolean;
showToolbarSaveButton:boolean;
/** Listener callbacks */
unRegisterTitleListener:Function;
removeTransitionSubscription:Function;
/** Determine when query is initially loaded */
tableInformationLoaded = false;
showToolbar = false;
/** The toolbar buttons to render */
toolbarButtonComponents:ToolbarButtonComponentDefinition[] = [];
@ -97,15 +103,23 @@ export class PartitionedQuerySpacePageComponent extends WorkPackagesViewBase imp
/** We need to pass the correct partition state to the view to manage the grid */
currentPartition:ViewPartitionState = '-split';
/** What route (if any) should we go back to using the back button left of the title? */
backButtonCallback:Function|undefined;
/** Which filter container component to mount */
filterContainerDefinition:DynamicComponentDefinition = {
component: WorkPackageFilterContainerComponent
};
ngOnInit() {
super.ngOnInit();
this.hasQueryProps = !!this.$state.params.query_props;
this.showToolbarSaveButton = !!this.$state.params.query_props;
this.setPartition(this.$state.current);
this.removeTransitionSubscription = this.$transitions.onSuccess({}, (transition):any => {
const params = transition.params('to');
const toState = transition.to();
this.hasQueryProps = !!params.query_props;
this.showToolbarSaveButton = !!params.query_props;
this.setPartition(toState);
});
@ -156,7 +170,7 @@ export class PartitionedQuerySpacePageComponent extends WorkPackagesViewBase imp
.values$()
.pipe(take(1))
.subscribe(() => {
this.tableInformationLoaded = true;
this.showToolbar = true;
this.cdRef.detectChanges();
});
}
@ -176,26 +190,25 @@ export class PartitionedQuerySpacePageComponent extends WorkPackagesViewBase imp
this.queryParamListener.removeQueryChangeListener();
}
public saveQueryFromTitle(val:string) {
public changeChangesFromTitle(val:string) {
if (this.currentQuery && this.currentQuery.persisted) {
this.updateQueryName(val);
this.updateTitleName(val);
} else {
this.wpListService
.create(this.currentQuery!, val)
.then(() => this.querySaving = false)
.catch(() => this.querySaving = false);
.then(() => this.toolbarDisabled = false)
.catch(() => this.toolbarDisabled = false);
}
}
updateQueryName(val:string) {
this.querySaving = true;
updateTitleName(val:string) {
this.toolbarDisabled = true;
this.currentQuery!.name = val;
this.wpListService.save(this.currentQuery)
.then(() => this.querySaving = false)
.catch(() => this.querySaving = false);
.then(() => this.toolbarDisabled = false)
.catch(() => this.toolbarDisabled = false);
}
updateTitle(query?:QueryResource) {
// Too early for loaded query

@ -103,7 +103,7 @@ module Pages
select_autocomplete(page.find('.wp-inline-create--reference-autocompleter'),
query: work_package.subject,
results_selector: '.board--container',
results_selector: '.work-packages-partitioned-query-space--container',
select_text: "##{work_package.id}")
expect_card(list_name, work_package.subject)
@ -118,7 +118,7 @@ module Pages
target_dropdown = search_autocomplete(page.find('.wp-inline-create--reference-autocompleter'),
query: work_package.subject,
results_selector: '.board--container')
results_selector: '.work-packages-partitioned-query-space--container')
expect(target_dropdown).to have_no_selector('.ui-menu-item', text: work_package.subject)
end
@ -265,13 +265,10 @@ module Pages
end
def back_to_index
find('.board--back-button').click
find('.back-button').click
end
def expect_editable_board(editable)
# Editable / draggable check
expect(page).to have_conditional_selector(editable, '.board--container.-editable')
# Settings dropdown
expect(page).to have_conditional_selector(editable, '.board--settings-dropdown')
@ -292,12 +289,12 @@ module Pages
def rename_board(new_name, through_dropdown: false)
if through_dropdown
click_dropdown_entry 'Rename view'
expect(page).to have_focus_on('.board--header-container .editable-toolbar-title--input')
input = page.find('.board--header-container .editable-toolbar-title--input')
expect(page).to have_focus_on('.toolbar-container .editable-toolbar-title--input')
input = page.find('.toolbar-container .editable-toolbar-title--input')
input.set new_name
input.send_keys :enter
else
page.within('.board--header-container') do
page.within('.toolbar-container') do
input = page.find('.editable-toolbar-title--input').click
input.set new_name
input.send_keys :enter
@ -306,7 +303,7 @@ module Pages
expect_and_dismiss_notification message: I18n.t('js.notice_successful_update')
page.within('.board--header-container') do
page.within('.toolbar-container') do
expect(page).to have_field('editable-toolbar-title', with: new_name)
end
end

Loading…
Cancel
Save