Remove uiRouter from Boards module

This commit is contained in:
Henriette Darge
2026-04-02 14:49:05 +02:00
parent 850af3bb91
commit 4aa4f5221e
17 changed files with 222 additions and 215 deletions
+8
View File
@@ -39,6 +39,7 @@ import { OpSharedModule } from 'core-app/shared/shared.module';
import { OpSpotModule } from 'core-app/spot/spot.module';
import { OpDragScrollDirective } from 'core-app/shared/directives/op-drag-scroll/op-drag-scroll.directive';
import { OpenprojectWorkPackagesModule } from 'core-app/features/work-packages/openproject-work-packages.module';
import { OpenprojectBoardsModule } from 'core-app/features/boards/openproject-boards.module';
import { OpenprojectAttachmentsModule } from 'core-app/shared/components/attachments/openproject-attachments.module';
import { OpenprojectEditorModule } from 'core-app/shared/components/editor/openproject-editor.module';
import { OpenprojectGridsModule } from 'core-app/shared/components/grids/openproject-grids.module';
@@ -149,6 +150,9 @@ import {
import {
WorkPackageSplitViewEntryComponent,
} from 'core-app/features/work-packages/routing/wp-split-view/wp-split-view-entry.component';
import {
BoardEntryComponent,
} from 'core-app/features/boards/board/board-partitioned-page/board-entry.component';
import {
StorageLoginButtonComponent,
} from 'core-app/shared/components/storages/storage-login-button/storage-login-button.component';
@@ -295,6 +299,9 @@ export function runBootstrap(appRef:ApplicationRef) {
OpenprojectWorkPackagesModule,
OpenprojectWorkPackageRoutesModule,
// Boards
OpenprojectBoardsModule,
// Work packages in graph representation
OpenprojectWorkPackageGraphsModule,
// Calendar module
@@ -394,6 +401,7 @@ export class OpenProjectModule implements DoBootstrap {
registerCustomElement('opce-reminder-settings', ReminderSettingsPageComponent, { injector });
registerCustomElement('opce-notification-center', InAppNotificationCenterComponent, { injector });
registerCustomElement('opce-wp-split-view', WorkPackageSplitViewEntryComponent, { injector });
registerCustomElement('opce-board-view', BoardEntryComponent, { injector });
registerCustomElement('opce-wp-full-view', WorkPackageFullViewEntryComponent, { injector });
registerCustomElement('opce-wp-full-create', WorkPackageFullCreateEntryComponent, { injector });
registerCustomElement('opce-wp-full-copy', WorkPackageFullCopyEntryComponent, { injector });
@@ -247,6 +247,10 @@ export class PathHelperService {
return `${this.boardsPath(projectIdentifier)}/new`;
}
public boardDetailsPath(projectIdentifier:string|null, boardId:string|number, workPackageId:string|number) {
return `${this.boardsPath(projectIdentifier)}/${boardId}/details/${workPackageId}`;
}
public projectDashboardsPath(projectIdentifier:string) {
return `${this.projectPath(projectIdentifier)}/dashboards`;
}
@@ -68,12 +68,6 @@ export const OPENPROJECT_ROUTES:Ng2StateDeclaration[] = [
'!$default': { component: ApplicationBaseComponent },
},
},
{
name: 'boards.**',
parent: 'optional_project',
url: '/boards',
loadChildren: () => import('../../features/boards/openproject-boards.module').then((m) => m.OpenprojectBoardsModule),
},
{
name: 'bim.**',
parent: 'optional_project',
@@ -34,7 +34,6 @@ import { OpModalComponent } from 'core-app/shared/components/modal/modal.compone
import { OpModalLocalsToken } from 'core-app/shared/components/modal/modal.service';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { Board } from 'core-app/features/boards/board/board';
import { StateService } from '@uirouter/core';
import { BoardService } from 'core-app/features/boards/board/board.service';
import { BoardActionsRegistryService } from 'core-app/features/boards/board/board-actions/board-actions-registry.service';
import { BoardActionService } from 'core-app/features/boards/board/board-actions/board-action.service';
@@ -42,6 +41,7 @@ import { HalResourceNotificationService } from 'core-app/features/hal/services/h
import { tap } from 'rxjs/operators';
import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service';
import { CurrentProjectService } from 'core-app/core/current-project/current-project.service';
import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
import {
firstValueFrom,
Observable,
@@ -113,11 +113,11 @@ export class AddListModalComponent extends OpModalComponent implements OnInit {
readonly cdRef:ChangeDetectorRef,
readonly boardActions:BoardActionsRegistryService,
readonly halNotification:HalResourceNotificationService,
readonly state:StateService,
readonly boardService:BoardService,
readonly I18n:I18nService,
readonly apiV3Service:ApiV3Service,
readonly currentProject:CurrentProjectService) {
readonly currentProject:CurrentProjectService,
readonly pathHelper:PathHelperService) {
super(locals, cdRef, elementRef);
}
@@ -141,7 +141,7 @@ export class AddListModalComponent extends OpModalComponent implements OnInit {
.then((board) => {
this.inFlight = false;
this.closeMe();
void this.state.go('boards.partitioned.show', { board_id: board.id, isNew: true });
void Turbo.visit(`${this.pathHelper.boardsPath(this.currentProject.identifier)}/${board.id}`);
})
.catch(() => (this.inFlight = false));
}
@@ -7,7 +7,6 @@ import { HalResourceService } from 'core-app/features/hal/services/hal-resource.
import { WorkPackageViewFiltersService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-filters.service';
import { QueryFilterInstanceResource } from 'core-app/features/hal/resources/query-filter-instance-resource';
import { UrlParamsHelperService } from 'core-app/features/work-packages/components/wp-query/url-params-helper';
import { StateService } from '@uirouter/core';
import { debounceTime, skip, take } from 'rxjs/operators';
import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin';
import { Observable } from 'rxjs';
@@ -32,8 +31,7 @@ export class BoardFilterComponent extends UntilDestroyedMixin implements AfterVi
private readonly wpStatesInitialization:WorkPackageStatesInitializationService,
private readonly wpTableFilters:WorkPackageViewFiltersService,
private readonly urlParamsHelper:UrlParamsHelperService,
private readonly boardFilters:BoardFiltersService,
private readonly $state:StateService) {
private readonly boardFilters:BoardFiltersService) {
super();
}
@@ -71,9 +69,15 @@ export class BoardFilterComponent extends UntilDestroyedMixin implements AfterVi
const filterHash = this.urlParamsHelper.buildV3GetFilters(filters);
const query_props = JSON.stringify(filterHash);
this.boardFilters.filters.putValue(filterHash);
const url = new URL(window.location.href);
if (query_props) {
url.searchParams.set('query_props', query_props);
} else {
url.searchParams.delete('query_props');
}
window.history.pushState({}, '', url);
this.$state.go('.', { query_props }, { custom: { notify: false } });
this.boardFilters.filters.putValue(filterHash);
});
}
@@ -185,9 +185,9 @@ export class BoardListComponent extends AbstractWidgetComponent implements OnIni
this.resource.isNewWidget = false;
// Set initial selection if split view open
if (this.state.includes(`${this.state.current.data.baseRoute}.details`)) {
const wpId = this.state.params.workPackageId;
this.wpViewSelectionService.initializeSelection([wpId]);
const detailsMatch = window.location.pathname.match(/\/details\/(\d+)/);
if (detailsMatch) {
this.wpViewSelectionService.initializeSelection([detailsMatch[1]]);
}
// If this query space changes its focused or selected
@@ -495,15 +495,20 @@ export class BoardListComponent extends AbstractWidgetComponent implements OnIni
}
openStateLink(event:{ workPackageId:string; requestedState:string }) {
const params = { workPackageId: event.workPackageId };
if (event.requestedState === 'split') {
this.keepTab.goCurrentDetailsState(params);
this.goToSplitView(event.workPackageId);
} else {
this.keepTab.goCurrentShowState(params.workPackageId);
this.keepTab.goCurrentShowState(event.workPackageId);
}
}
private goToSplitView(workPackageId:string):void {
const base = this.pathHelper.boardDetailsPath(this.currentProject.identifier, this.board.id!, workPackageId);
const search = window.location.search;
const link = search ? `${base}${search}` : base;
Turbo.visit(link, { frame: 'content-bodyRight', action: 'advance' });
}
private schema(workPackage:WorkPackageResource) {
return this.schemaCache.of(workPackage);
}
@@ -1,20 +1,50 @@
import { Component, Injector } from '@angular/core';
//-- copyright
// OpenProject is an open source project management software.
// Copyright (C) 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 COPYRIGHT and LICENSE files for more details.
//++
import { ChangeDetectionStrategy, Component, ElementRef, Injector, Input } from '@angular/core';
import { populateInputsFromDataset } from 'core-app/shared/components/dataset-inputs';
import {
WorkPackageIsolatedQuerySpaceDirective,
} from 'core-app/features/work-packages/directives/query-space/wp-isolated-query-space.directive';
import { BoardConfigurationService } from 'core-app/features/boards/board/configuration-modal/board-configuration.service';
import { BoardActionsRegistryService } from 'core-app/features/boards/board/board-actions/board-actions-registry.service';
import { BoardStatusActionService } from 'core-app/features/boards/board/board-actions/status/status-action.service';
import { BoardVersionActionService } from 'core-app/features/boards/board/board-actions/version/version-action.service';
import { QueryUpdatedService } from 'core-app/features/boards/board/query-updated/query-updated.service';
import { BoardAssigneeActionService } from 'core-app/features/boards/board/board-actions/assignee/assignee-action.service';
import { BoardSubprojectActionService } from 'core-app/features/boards/board/board-actions/subproject/subproject-action.service';
import { BoardSubtasksActionService } from 'core-app/features/boards/board/board-actions/subtasks/board-subtasks-action.service';
import {
WorkPackageIsolatedQuerySpaceDirective,
} from 'core-app/features/work-packages/directives/query-space/wp-isolated-query-space.directive';
import { QueryUpdatedService } from 'core-app/features/boards/board/query-updated/query-updated.service';
@Component({
selector: 'boards-entry',
selector: 'board-entry',
hostDirectives: [WorkPackageIsolatedQuerySpaceDirective],
template: '<ui-view />',
template: `<board-partitioned-page [boardId]="boardId" />`,
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
BoardConfigurationService,
BoardStatusActionService,
@@ -26,11 +56,18 @@ import {
],
standalone: false,
})
export class BoardsRootComponent {
constructor(readonly injector:Injector) {
// Register action services
const registry = injector.get(BoardActionsRegistryService);
export class BoardEntryComponent {
@Input() boardId:string;
constructor(
readonly elementRef:ElementRef,
readonly injector:Injector,
) {
populateInputsFromDataset(this);
document.body.classList.add('router--boards-full-view');
const registry = injector.get(BoardActionsRegistryService);
registry.add('status', injector.get(BoardStatusActionService));
registry.add('assignee', injector.get(BoardAssigneeActionService));
registry.add('version', injector.get(BoardVersionActionService));
@@ -2,6 +2,7 @@ import {
ChangeDetectionStrategy,
Component,
ElementRef,
Input,
Injector,
OnInit,
QueryList,
@@ -36,11 +37,11 @@ import {
BoardActionsRegistryService,
} from 'core-app/features/boards/board/board-actions/board-actions-registry.service';
import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service';
import {
WorkPackageStatesInitializationService,
} from 'core-app/features/work-packages/components/wp-list/wp-states-initialization.service';
import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
import { CurrentProjectService } from 'core-app/core/current-project/current-project.service';
@Component({
selector: 'board-list-container',
templateUrl: './board-list-container.component.html',
styleUrls: ['./board-list-container.component.sass'],
providers: [
@@ -50,6 +51,7 @@ import {
standalone: false,
})
export class BoardListContainerComponent extends UntilDestroyedMixin implements OnInit {
@Input() boardId:string;
text = {
delete: this.I18n.t('js.button_delete'),
areYouSure: this.I18n.t('js.text_are_you_sure'),
@@ -90,7 +92,7 @@ export class BoardListContainerComponent extends UntilDestroyedMixin implements
private currentQueryUpdatedMonitoring:Subscription;
constructor(
readonly I18n:I18nService,
readonly I18n:I18nService,
readonly state:StateService,
readonly toastService:ToastService,
readonly halNotification:HalResourceNotificationService,
@@ -102,16 +104,17 @@ readonly I18n:I18nService,
readonly apiV3Service:ApiV3Service,
readonly Boards:BoardService,
readonly boardListCrossSelectionService:BoardListCrossSelectionService,
readonly wpStatesInitialization:WorkPackageStatesInitializationService,
readonly Drag:DragAndDropService,
readonly apiv3Service:ApiV3Service,
readonly QueryUpdated:QueryUpdatedService,
readonly pathHelper:PathHelperService,
readonly currentProject:CurrentProjectService,
) {
super();
}
ngOnInit():void {
const id:string = this.state.params.board_id.toString();
const id:string = this.boardId || this.state.params.board_id?.toString();
this.board$ = this
.apiV3Service
.boards
@@ -128,10 +131,11 @@ readonly I18n:I18nService,
.pipe(
this.untilDestroyed(),
filter((state) => state.focusedWorkPackage !== null),
filter(() => this.state.includes(`${this.state.current.data.baseRoute}.details`)),
filter(() => window.location.pathname.includes('/details/')),
).subscribe((selection) => {
// Update split screen
this.state.go(`${this.state.current.data.baseRoute}.details`, { workPackageId: selection.focusedWorkPackage });
// Update split screen
const link = this.pathHelper.boardDetailsPath(this.currentProject.identifier, id, selection.focusedWorkPackage!);
Turbo.visit(link, { frame: 'content-bodyRight', action: 'advance' });
});
}
@@ -0,0 +1,48 @@
<div class="work-packages-partitioned-query-space--container"
[ngClass]="currentPartition">
<op-breadcrumbs [items]="breadcrumbItems()"
[lastItemSection]="currentMenuSectionHeader()"
/>
<div class="toolbar-container -editable">
<div class="toolbar">
<h2 class="toolbar-title--container">
<editable-toolbar-title [title]="selectedTitle"
[inFlight]="toolbarDisabled"
[showSaveCondition]="showToolbarSaveButton"
(onSave)="changeChangesFromTitle($event)"
(onEmptySubmit)="updateTitleName('')"
[editable]="titleEditingEnabled" />
</h2>
@if (showToolbar) {
<ul class="toolbar-items hide-when-print"
>
@for (definition of toolbarButtonComponents; track definition) {
@if (!definition.show || definition.show()) {
<li class="toolbar-item"
[ngClass]="definition.containerClasses">
<ndc-dynamic [ndcDynamicComponent]="definition.component"
[ndcDynamicInputs]="definition.inputs"
[ndcDynamicInjector]="injector"
[ndcDynamicOutputs]="definition.outputs" />
</li>
}
}
</ul>
}
</div>
</div>
<div class="work-packages-partitioned-query-space--filter-area">
@if (filterContainerDefinition) {
<ndc-dynamic [ndcDynamicComponent]="filterContainerDefinition.component"
[ndcDynamicInputs]="filterContainerDefinition.inputs"
[ndcDynamicOutputs]="filterContainerDefinition.outputs"
[ndcDynamicInjector]="injector" />
}
</div>
<div class="work-packages-partitioned-page--content-container">
<!-- Board list content -->
<board-list-container [boardId]="boardId" />
</div>
</div>
@@ -2,7 +2,9 @@ import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
Injector, OnInit, OnDestroy,
Injector,
Input,
OnInit,
} from '@angular/core';
import {
DynamicComponentDefinition,
@@ -11,7 +13,6 @@ import {
} from 'core-app/features/work-packages/routing/partitioned-query-space-page/partitioned-query-space-page.component';
import {
StateService,
TransitionService,
} from '@uirouter/core';
import { BoardFilterComponent } from 'core-app/features/boards/board/board-filter/board-filter.component';
import { ToastService } from 'core-app/shared/components/toaster/toast.service';
@@ -24,17 +25,18 @@ import { BoardsMenuButtonComponent } from 'core-app/features/boards/board/toolba
import {
catchError,
finalize,
skip,
take,
} from 'rxjs/operators';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin';
import { QueryResource } from 'core-app/features/hal/resources/query-resource';
import { Ng2StateDeclaration } from '@uirouter/angular';
import { Board } from 'core-app/features/boards/board/board';
import { BoardFiltersService } from 'core-app/features/boards/board/board-filter/board-filters.service';
import { CardViewHandlerRegistry } from 'core-app/features/work-packages/components/wp-card-view/event-handler/card-view-handler-registry';
import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service';
import { OpTitleService } from 'core-app/core/html/op-title.service';
import { EMPTY } from 'rxjs';
import { EMPTY, ReplaySubject } from 'rxjs';
import { SubmenuService } from 'core-app/core/main-menu/submenu.service';
import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
import { CurrentProjectService } from 'core-app/core/current-project/current-project.service';
@@ -44,7 +46,8 @@ export function boardCardViewHandlerFactory(injector:Injector) {
}
@Component({
templateUrl: '../../../work-packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.html',
selector: 'board-partitioned-page',
templateUrl: './board-partitioned-page.component.html',
styleUrls: [
'../../../work-packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.sass',
'./board-partitioned-page.component.sass',
@@ -56,7 +59,8 @@ export function boardCardViewHandlerFactory(injector:Injector) {
],
standalone: false,
})
export class BoardPartitionedPageComponent extends UntilDestroyedMixin implements OnInit, OnDestroy {
export class BoardPartitionedPageComponent extends UntilDestroyedMixin implements OnInit {
@Input() boardId:string;
text = {
button_more: this.I18n.t('js.button_more'),
delete: this.I18n.t('js.button_delete'),
@@ -71,15 +75,8 @@ export class BoardPartitionedPageComponent extends UntilDestroyedMixin implement
unnamed_list: this.I18n.t('js.boards.label_unnamed_list'),
};
/** Board observable */
board$ = this
.apiV3Service
.boards
.id(this.state.params.board_id.toString())
.observe();
/** Whether this is a new board just created */
isNew = !!this.state.params.isNew;
/** Board subject */
board$ = new ReplaySubject<Board>(1);
/** Whether the board is editable */
editable:boolean;
@@ -95,10 +92,6 @@ export class BoardPartitionedPageComponent extends UntilDestroyedMixin implement
/** Do we currently have query props ? */
showToolbarSaveButton:boolean;
/** Listener callbacks */
// eslint-disable-next-line @typescript-eslint/ban-types
removeTransitionSubscription:Function;
/** Show a toolbar */
showToolbar = true;
@@ -139,7 +132,6 @@ export class BoardPartitionedPageComponent extends UntilDestroyedMixin implement
constructor(
readonly I18n:I18nService,
readonly cdRef:ChangeDetectorRef,
readonly $transitions:TransitionService,
readonly state:StateService,
readonly toastService:ToastService,
readonly halNotification:HalResourceNotificationService,
@@ -159,29 +151,28 @@ export class BoardPartitionedPageComponent extends UntilDestroyedMixin implement
// Ensure board is being loaded
this.Boards.loadAllBoards();
this.removeTransitionSubscription = this.$transitions.onSuccess({}, (transition):any => {
const toState = transition.to();
const params = transition.params('to');
const boardId = this.boardId || this.state.params.board_id?.toString();
this.apiV3Service.boards.id(boardId).observe()
.pipe(this.untilDestroyed())
.subscribe((board) => this.board$.next(board));
this.showToolbarSaveButton = !!params.query_props;
this.setPartition(toState);
this
.board$
.pipe(take(1))
.subscribe((board) => {
this.titleService.setFirstPart(board.name);
});
this.cdRef.detectChanges();
});
// React to filter changes (board-filter updates boardFilters after pushing URL)
this.boardFilters.filters.values$()
.pipe(
this.untilDestroyed(),
skip(1), // skip the initial empty default value
)
.subscribe(() => {
this.showToolbarSaveButton = !!new URLSearchParams(window.location.search).get('query_props');
this.cdRef.detectChanges();
});
this.board$
.pipe(
this.untilDestroyed(),
)
.subscribe((board) => {
const queryProps = this.state.params.query_props;
const queryProps = new URLSearchParams(window.location.search).get('query_props');
this.editable = board.editable;
this.selectedTitle = board.name;
this.titleService.setFirstPart(board.name);
@@ -191,11 +182,6 @@ export class BoardPartitionedPageComponent extends UntilDestroyedMixin implement
});
}
ngOnDestroy():void {
super.ngOnDestroy();
this.removeTransitionSubscription();
}
breadcrumbItems() {
return [
{ href: this.pathHelperService.projectPath(this.currentProject.identifier!), text: (this.currentProject.name) },
@@ -213,8 +199,10 @@ export class BoardPartitionedPageComponent extends UntilDestroyedMixin implement
board.name = newName;
board.filters = this.boardFilters.current;
const params = { isNew: false, query_props: null };
this.state.go('.', params, { custom: { notify: false } });
const url = new URL(window.location.href);
url.searchParams.delete('query_props');
window.history.pushState({}, '', url);
this.showToolbarSaveButton = false;
this.toolbarDisabled = true;
this.Boards
@@ -245,16 +233,6 @@ export class BoardPartitionedPageComponent extends UntilDestroyedMixin implement
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?.partition) ? state.data.partition : '-split';
}
private reloadSidemenu():void {
this.submenuService.reloadSubmenu(null);
}
@@ -30,9 +30,7 @@ import { NgModule } from '@angular/core';
import { OpSharedModule } from 'core-app/shared/shared.module';
import { OpenprojectWorkPackagesModule } from 'core-app/features/work-packages/openproject-work-packages.module';
import { OpenprojectModalModule } from 'core-app/shared/components/modal/modal.module';
import { UIRouterModule } from '@uirouter/angular';
import { BoardListComponent } from 'core-app/features/boards/board/board-list/board-list.component';
import { BoardsRootComponent } from 'core-app/features/boards/boards-root/boards-root.component';
import { BoardInlineAddAutocompleterComponent } from 'core-app/features/boards/board/inline-add/board-inline-add-autocompleter.component';
import { BoardsToolbarMenuDirective } from 'core-app/features/boards/board/toolbar-menu/boards-toolbar-menu.directive';
import { BoardConfigurationModalComponent } from 'core-app/features/boards/board/configuration-modal/board-configuration.modal';
@@ -43,9 +41,9 @@ import { BoardFilterComponent } from 'core-app/features/boards/board/board-filte
import { BoardListMenuComponent } from 'core-app/features/boards/board/board-list/board-list-menu.component';
import { VersionBoardHeaderComponent } from 'core-app/features/boards/board/board-actions/version/version-board-header.component';
import { DynamicModule } from 'ng-dynamic-component';
import { BOARDS_ROUTES, uiRouterBoardsConfiguration } from 'core-app/features/boards/openproject-boards.routes';
import { BoardPartitionedPageComponent } from 'core-app/features/boards/board/board-partitioned-page/board-partitioned-page.component';
import { BoardListContainerComponent } from 'core-app/features/boards/board/board-partitioned-page/board-list-container.component';
import { BoardEntryComponent } from 'core-app/features/boards/board/board-partitioned-page/board-entry.component';
import { BoardsMenuButtonComponent } from 'core-app/features/boards/board/toolbar-menu/boards-menu-button.component';
import { AssigneeBoardHeaderComponent } from 'core-app/features/boards/board/board-actions/assignee/assignee-board-header.component';
import { SubprojectBoardHeaderComponent } from 'core-app/features/boards/board/board-actions/subproject/subproject-board-header.component';
@@ -64,18 +62,12 @@ import { OpenprojectEnterpriseModule } from 'core-app/features/enterprise/openpr
// Dynamic Module for actions
DynamicModule,
// Routes for /boards
UIRouterModule.forChild({
states: BOARDS_ROUTES,
config: uiRouterBoardsConfiguration,
}),
],
declarations: [
BoardPartitionedPageComponent,
BoardListContainerComponent,
BoardEntryComponent,
BoardListComponent,
BoardsRootComponent,
BoardInlineAddAutocompleterComponent,
BoardHighlightingTabComponent,
BoardConfigurationModalComponent,
@@ -1,105 +0,0 @@
//-- copyright
// OpenProject is an open source project management software.
// Copyright (C) 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 COPYRIGHT and LICENSE files for more details.
//++
import { Ng2StateDeclaration, UIRouter } from '@uirouter/angular';
import { BoardsRootComponent } from 'core-app/features/boards/boards-root/boards-root.component';
import { BoardPartitionedPageComponent } from 'core-app/features/boards/board/board-partitioned-page/board-partitioned-page.component';
import { BoardListContainerComponent } from 'core-app/features/boards/board/board-partitioned-page/board-list-container.component';
import { makeSplitViewRoutes } from 'core-app/features/work-packages/routing/split-view-routes.template';
import { WorkPackageSplitViewComponent } from 'core-app/features/work-packages/routing/wp-split-view/wp-split-view.component';
export const menuItemClass = 'boards-menu-item';
export const sidemenuId = 'boards_sidemenu';
export const sideMenuOptions = {
sidemenuId,
hardReloadOnBaseRoute: true,
};
export const BOARDS_ROUTES:Ng2StateDeclaration[] = [
{
name: 'boards',
parent: 'optional_project',
// The trailing slash is important
// cf., https://community.openproject.com/wp/29754
url: '/boards/?query_props',
data: {
bodyClasses: 'router--boards-view-base',
menuItem: menuItemClass,
sideMenuOptions,
},
params: {
// Use custom encoder/decoder that ensures validity of URL string
query_props: { type: 'opQueryString', dynamic: true },
},
component: BoardsRootComponent,
},
{
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,
sideMenuOptions,
},
reloadOnSearch: false,
component: BoardPartitionedPageComponent,
redirectTo: 'boards.partitioned.show',
},
{
name: 'boards.partitioned.show',
url: '',
data: {
baseRoute: 'boards.partitioned.show',
sideMenuOptions,
},
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/`,
);
}
@@ -48,7 +48,7 @@
data-notification-selector='notification-scroll-container'
>
<ndc-dynamic [ndcDynamicComponent]="activeTabComponent"
[ndcDynamicInputs]="{ workPackage: workPackage, tab: tabIdentifier }" />
[ndcDynamicInputs]="{ workPackage: workPackage }" />
</div>
}
</edit-form>
@@ -3,13 +3,15 @@
module ::Boards
class BoardsController < BaseController
include Layout
include WorkPackages::WithSplitView
before_action :load_and_authorize_in_optional_project
before_action :find_board_for_deletion, only: %i[destroy]
before_action :find_board, only: %i[show split_view]
# The boards permission alone does not suffice
# to view work packages
before_action :authorize_work_package_permission, only: %i[show]
before_action :authorize_work_package_permission, only: %i[show split_view]
before_action :build_board_grid, only: %i[new]
before_action :load_query, only: %i[index]
@@ -21,7 +23,19 @@ module ::Boards
end
def show
render layout: "angular/angular"
render
end
def split_view
respond_to do |format|
format.html do
if turbo_frame_request?
render "work_packages/split_view", layout: false
else
render :show
end
end
end
end
def new; end
@@ -56,6 +70,10 @@ module ::Boards
private
def split_view_base_route
project_work_package_board_path(@project, params[:id], request.query_parameters)
end
def load_query
projects = @project || Project.allowed_to(User.current, :show_board_views)
@@ -64,6 +82,10 @@ module ::Boards
.where(project: projects)
end
def find_board
@board_grid = Boards::Grid.find_by!(id: params[:id], project: @project)
end
def find_board_for_deletion
@board_grid = Boards::Grid.find_by!(id: params[:id], project: @project)
end
@@ -27,4 +27,13 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
<% html_title(t("boards.label_boards")) -%>
<% html_title(@board_grid.name) -%>
<% content_for :content_body do %>
<%= angular_component_tag "opce-board-view", inputs: { boardId: @board_grid.id.to_s } %>
<% end %>
<% content_for :content_body_right do %>
<%= turbo_stream.set_title(title: page_title(*html_title_parts)) if turbo_frame_request? %>
<%= render(split_view_instance) if render_work_package_split_view? %>
<% end %>
+7
View File
@@ -12,6 +12,13 @@ Rails.application.routes.draw do
collection do
get "menu" => "boards/menus#show"
end
member do
get "details/:work_package_id(/:tab)",
action: :split_view,
defaults: { tab: :overview },
as: :details,
work_package_split_view: true
end
get "(/*state)" => "boards/boards#show", on: :member, as: "", constraints: { id: /\d+/ }
end
end
@@ -28,7 +28,7 @@ module OpenProject::Boards
settings: {} do
project_module :board_view, dependencies: :work_package_tracking, order: 80 do
permission :show_board_views,
{ "boards/boards": %i[index show],
{ "boards/boards": %i[index show split_view],
"boards/menus": %i[show] },
permissible_on: :project,
dependencies: :view_work_packages,