Remove uiRouter from TeamPlanner

This commit is contained in:
Henriette Darge
2026-04-15 09:02:17 +02:00
parent 22a6d4e8b4
commit acdc78f5ca
15 changed files with 129 additions and 202 deletions
+5
View File
@@ -151,6 +151,8 @@ import {
BoardEntryComponent,
} from 'core-app/features/boards/board/board-partitioned-page/board-entry.component';
import { CalendarEntryComponent } from 'core-app/features/calendar/calendar-entry.component';
import { TeamPlannerEntryComponent } from 'core-app/features/team-planner/team-planner/team-planner-entry.component';
import { TeamPlannerModule } from 'core-app/features/team-planner/team-planner/team-planner.module';
import {
StorageLoginButtonComponent,
} from 'core-app/shared/components/storages/storage-login-button/storage-login-button.component';
@@ -304,6 +306,8 @@ export function runBootstrap(appRef:ApplicationRef) {
OpenprojectWorkPackageGraphsModule,
// Calendar module
OpenprojectCalendarModule,
// Team Planner module
TeamPlannerModule,
// MyPage
OpenprojectMyPageModule,
@@ -399,6 +403,7 @@ export class OpenProjectModule implements DoBootstrap {
registerCustomElement('opce-wp-split-create', WorkPackageSplitCreateEntryComponent, { injector });
registerCustomElement('opce-board-view', BoardEntryComponent, { injector });
registerCustomElement('opce-calendar-view', CalendarEntryComponent, { injector });
registerCustomElement('opce-team-planner-view', TeamPlannerEntryComponent, { injector });
registerCustomElement('opce-wp-full-view', WorkPackageFullViewEntryComponent, { injector });
registerCustomElement('opce-wp-full-create', WorkPackageFullCreateEntryComponent, { injector });
registerCustomElement('opce-wp-full-copy', WorkPackageFullCopyEntryComponent, { injector });
@@ -39,7 +39,6 @@ import {
mobileGuardActivated,
redirectToMobileAlternative,
} from 'core-app/shared/helpers/routing/mobile-guard.helper';
import { TEAM_PLANNER_LAZY_ROUTES } from 'core-app/features/team-planner/team-planner/team-planner.lazy-routes';
export const OPENPROJECT_ROUTES:Ng2StateDeclaration[] = [
{
@@ -73,7 +72,6 @@ export const OPENPROJECT_ROUTES:Ng2StateDeclaration[] = [
url: '/bcf',
loadChildren: () => import('../../features/bim/ifc_models/openproject-ifc-models.module').then((m) => m.OpenprojectIFCModelsModule),
},
...TEAM_PLANNER_LAZY_ROUTES,
];
/**
@@ -32,12 +32,9 @@ import { UrlParamsHelperService } from 'core-app/features/work-packages/componen
import { IsolatedQuerySpace } from 'core-app/features/work-packages/directives/query-space/isolated-query-space';
import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin';
import { CalendarDragDropService } from 'core-app/features/team-planner/team-planner/calendar-drag-drop.service';
import { splitViewRoute } from 'core-app/features/work-packages/routing/split-view-routes.helper';
import { StateService } from '@uirouter/core';
import { ActionsService } from 'core-app/core/state/actions/actions.service';
import { teamPlannerEventRemoved } from 'core-app/features/team-planner/team-planner/planner/team-planner.actions';
import { WorkPackageViewFiltersService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-filters.service';
import { OpCalendarService } from 'core-app/features/calendar/op-calendar.service';
import { OpWorkPackagesCalendarService } from 'core-app/features/calendar/op-work-packages-calendar.service';
@Component({
@@ -121,7 +118,6 @@ export class AddExistingPaneComponent extends UntilDestroyedMixin implements OnI
private readonly urlParamsHelper:UrlParamsHelperService,
private readonly workPackagesCalendar:OpWorkPackagesCalendarService,
private readonly calendarDrag:CalendarDragDropService,
private readonly $state:StateService,
private readonly actions$:ActionsService,
private readonly wpFilters:WorkPackageViewFiltersService,
) {
@@ -214,10 +210,7 @@ export class AddExistingPaneComponent extends UntilDestroyedMixin implements OnI
}
openStateLink(event:{ workPackageId:string; requestedState:string }):void {
void this.$state.go(
`${splitViewRoute(this.$state)}.tabs`,
{ workPackageId: event.workPackageId, tabIdentifier: 'overview' },
);
this.workPackagesCalendar.openSplitView(event.workPackageId);
}
private addExistingFilters(filters:ApiV3FilterBuilder) {
@@ -31,7 +31,8 @@ import { OpWorkPackagesCalendarService } from 'core-app/features/calendar/op-wor
import { OpCalendarService } from 'core-app/features/calendar/op-calendar.service';
@Component({
templateUrl: '../../../work-packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.html',
selector: 'op-team-planner-page',
templateUrl: './team-planner-page.component.html',
styleUrls: [
'../../../work-packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.sass',
],
@@ -98,6 +99,17 @@ export class TeamPlannerPageComponent extends PartitionedQuerySpacePageComponent
public ngOnInit():void {
super.ngOnInit();
// Fix showToolbarSaveButton from actual URL params (not uiRouter state)
this.showToolbarSaveButton = !!new URLSearchParams(window.location.search).get('query_props');
// Update save button reactively when query_props changes via pushState
this.wpListChecksumService.visibleChecksum$
.pipe(this.untilDestroyed())
.subscribe((checksum) => {
this.showToolbarSaveButton = !!checksum;
this.cdRef.detectChanges();
});
registerEffectCallbacks(this, this.untilDestroyed());
this.wpTableFilters.hidden.push(
@@ -40,7 +40,6 @@ import {
import {
CalendarOptions,
DateSelectArg,
DatesSetArg,
EventApi,
EventDropArg,
EventInput,
@@ -64,7 +63,6 @@ import {
take,
withLatestFrom,
} from 'rxjs/operators';
import { StateService } from '@uirouter/angular';
import resourceTimelinePlugin from '@fullcalendar/resource-timeline';
import interactionPlugin, {
EventDragStartArg,
@@ -78,8 +76,6 @@ import { ConfigurationService } from 'core-app/core/config/configuration.service
import { WorkPackageViewFiltersService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-filters.service';
import { IsolatedQuerySpace } from 'core-app/features/work-packages/directives/query-space/isolated-query-space';
import { CurrentProjectService } from 'core-app/core/current-project/current-project.service';
import { splitViewRoute } from 'core-app/features/work-packages/routing/split-view-routes.helper';
import { isClickedWithModifier } from 'core-app/shared/helpers/link-handling/link-handling';
import { QueryFilterInstanceResource } from 'core-app/features/hal/resources/query-filter-instance-resource';
import { PrincipalsResourceService } from 'core-app/core/state/principals/principals.service';
import {
@@ -98,7 +94,6 @@ import { MAGIC_PAGE_NUMBER } from 'core-app/core/apiv3/helpers/get-paginated-res
import { CalendarDragDropService } from 'core-app/features/team-planner/team-planner/calendar-drag-drop.service';
import { StatusResource } from 'core-app/features/hal/resources/status-resource';
import { ResourceChangeset } from 'core-app/shared/components/fields/changeset/resource-changeset';
import { KeepTabService } from 'core-app/features/work-packages/components/wp-single-view-tabs/keep-tab/keep-tab.service';
import { HalError } from 'core-app/features/hal/services/hal-error';
import { ActionsService } from 'core-app/core/state/actions/actions.service';
import {
@@ -409,7 +404,6 @@ export class TeamPlannerComponent extends UntilDestroyedMixin implements OnInit,
};
constructor(
private $state:StateService,
private configuration:ConfigurationService,
private principalsResourceService:PrincipalsResourceService,
private capabilitiesResourceService:CapabilitiesResourceService,
@@ -425,7 +419,6 @@ export class TeamPlannerComponent extends UntilDestroyedMixin implements OnInit,
readonly schemaCache:SchemaCacheService,
readonly apiV3Service:ApiV3Service,
readonly calendarDrag:CalendarDragDropService,
readonly keepTab:KeepTabService,
readonly actions$:ActionsService,
readonly toastService:ToastService,
readonly loadingIndicatorService:LoadingIndicatorService,
@@ -514,19 +507,6 @@ export class TeamPlannerComponent extends UntilDestroyedMixin implements OnInit,
.then(() => {
this.calendarOptions$.next(
this.workPackagesCalendar.calendarOptions({
// Override datesSet to persist cdate/cview via uiRouter instead of pushState,
// because uiRouter manages the TeamPlanner URL and would otherwise strip these params.
// Remove once uiRouter is removed.
datesSet: (dates:DatesSetArg) => {
void this.$state.go(
'.',
{
cdate: this.workPackagesCalendar.timezoneService.formattedISODate(dates.view.calendar.getDate()),
cview: (dates.view as unknown as { type:string }).type,
},
{ custom: { notify: false } },
);
},
locales: allLocales,
locale: this.I18n.locale,
schedulerLicenseKey: 'GPL-My-Project-Is-Open-Source',
@@ -843,48 +823,29 @@ export class TeamPlannerComponent extends UntilDestroyedMixin implements OnInit,
['$event.detail.start', '$event.detail.end', '$event.detail.assignee'],
)
openNewSplitCreate(start:string, end:string, resourceHref:string, nonWorkingDays?:boolean):void {
const defaults = {
startDate: start,
dueDate: end,
_links: {
assignee: {
href: resourceHref,
},
},
ignoreNonWorkingDays: nonWorkingDays,
};
void this.$state.go(
splitViewRoute(this.$state, 'new'),
{
defaults,
tabIdentifier: 'overview',
},
);
const basePath = window.location.pathname.replace(/\/details\/.*$/, '');
const search = new URLSearchParams(window.location.search);
search.set('startDate', start);
search.set('dueDate', end);
if (resourceHref) {
search.set('assignee_href', resourceHref);
}
if (nonWorkingDays) {
search.set('ignoreNonWorkingDays', 'true');
}
Turbo.visit(`${basePath}/details/new?${search.toString()}`, { frame: 'content-bodyRight', action: 'advance' });
}
openStateLink(event:{ workPackageId:string; requestedState:string }):void {
const params = { workPackageId: event.workPackageId };
if (event.requestedState === 'split') {
this.keepTab.goCurrentDetailsState(params);
this.workPackagesCalendar.openSplitView(event.workPackageId);
} else {
this.keepTab.goCurrentShowState(params.workPackageId);
this.workPackagesCalendar.openFullView(event.workPackageId);
}
}
onCardClicked({ workPackageId, event }:{ workPackageId:string, event:MouseEvent }):void {
if (isClickedWithModifier(event)) {
return;
}
// Only switch the split view if it is already open
if (!window.location.pathname.includes('/details/')) {
return;
}
this.workPackagesCalendar.wpTableSelection.setSelection(workPackageId, -1);
this.keepTab.goCurrentDetailsState({ workPackageId });
this.workPackagesCalendar.onCardClicked({ workPackageId, event });
}
shouldShowAsGhost(id:string, globalDraggingId:string|undefined):boolean {
@@ -0,0 +1,27 @@
import {
ChangeDetectionStrategy,
Component,
ElementRef,
OnDestroy,
} 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';
@Component({
hostDirectives: [WorkPackageIsolatedQuerySpaceDirective],
template: '<op-team-planner-page />',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class TeamPlannerEntryComponent implements OnDestroy {
constructor(readonly elementRef:ElementRef) {
populateInputsFromDataset(this);
document.body.classList.add('router--team-planner');
}
ngOnDestroy():void {
document.body.classList.remove('router--team-planner');
}
}
@@ -1,38 +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 } from '@uirouter/angular';
export const TEAM_PLANNER_LAZY_ROUTES:Ng2StateDeclaration[] = [
{
name: 'team_planner.**',
parent: 'optional_project',
url: '/team_planner',
loadChildren: () => import('./team-planner.module').then((m) => m.TeamPlannerModule),
},
];
@@ -1,16 +1,15 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { UIRouterModule } from '@uirouter/angular';
import { DynamicModule } from 'ng-dynamic-component';
import { FullCalendarModule } from '@fullcalendar/angular';
import { IconModule } from 'core-app/shared/components/icon/icon.module';
import { OpenprojectAutocompleterModule } from 'core-app/shared/components/autocompleter/openproject-autocompleter.module';
import { OpenprojectPrincipalRenderingModule } from 'core-app/shared/components/principal/principal-rendering.module';
import { OpenprojectWorkPackagesModule } from 'core-app/features/work-packages/openproject-work-packages.module';
import { TEAM_PLANNER_ROUTES } from 'core-app/features/team-planner/team-planner/team-planner.routes';
import { TeamPlannerComponent } from 'core-app/features/team-planner/team-planner/planner/team-planner.component';
import { AddAssigneeComponent } from 'core-app/features/team-planner/team-planner/assignee/add-assignee.component';
import { TeamPlannerPageComponent } from 'core-app/features/team-planner/team-planner/page/team-planner-page.component';
import { TeamPlannerEntryComponent } from 'core-app/features/team-planner/team-planner/team-planner-entry.component';
import { OpSharedModule } from 'core-app/shared/shared.module';
import { AddExistingPaneComponent } from './add-work-packages/add-existing-pane.component';
import { OpenprojectContentLoaderModule } from 'core-app/shared/components/op-content-loader/openproject-content-loader.module';
@@ -20,16 +19,13 @@ import { TeamPlannerViewSelectMenuDirective } from 'core-app/features/team-plann
declarations: [
TeamPlannerComponent,
TeamPlannerPageComponent,
TeamPlannerEntryComponent,
AddAssigneeComponent,
AddExistingPaneComponent,
TeamPlannerViewSelectMenuDirective,
],
imports: [
OpSharedModule,
// Routes for /team_planner
UIRouterModule.forChild({
states: TEAM_PLANNER_ROUTES,
}),
DynamicModule,
CommonModule,
IconModule,
@@ -1,84 +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 } from '@uirouter/angular';
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';
import { WorkPackagesBaseComponent } from 'core-app/features/work-packages/routing/wp-base/wp--base.component';
import { TeamPlannerPageComponent } from 'core-app/features/team-planner/team-planner/page/team-planner-page.component';
import { TeamPlannerComponent } from 'core-app/features/team-planner/team-planner/planner/team-planner.component';
export const sidemenuId = 'team_planner_sidemenu';
export const sideMenuOptions = {
sidemenuId,
hardReloadOnBaseRoute: true,
defaultQuery: 'new',
};
export const TEAM_PLANNER_ROUTES:Ng2StateDeclaration[] = [
{
name: 'team_planner',
parent: 'optional_project',
url: '/team_planners/:query_id?query_props&cdate&cview',
redirectTo: 'team_planner.page',
views: {
'!$default': { component: WorkPackagesBaseComponent },
},
params: {
query_id: { type: 'opQueryId', dynamic: true },
cdate: { type: 'string', dynamic: true },
cview: { type: 'string', dynamic: true },
// Use custom encoder/decoder that ensures validity of URL string
query_props: { type: 'opQueryString' },
},
},
{
name: 'team_planner.page',
component: TeamPlannerPageComponent,
redirectTo: 'team_planner.page.show',
data: {
bodyClasses: 'router--team-planner',
sideMenuOptions,
},
},
{
name: 'team_planner.page.show',
data: {
baseRoute: 'team_planner.page.show',
sideMenuOptions,
},
views: {
'content-left': { component: TeamPlannerComponent },
},
},
...makeSplitViewRoutes(
'team_planner.page.show',
undefined,
WorkPackageSplitViewComponent,
),
];
@@ -63,19 +63,27 @@ export class WorkPackageNewSplitViewComponent extends WorkPackageCreateComponent
);
}
// Apply date defaults passed via URL params (e.g. when dragging to create on the calendar).
// Apply defaults passed via URL params (e.g. when dragging to create on the calendar/team planner).
const startDate = params.get('startDate');
const dueDate = params.get('dueDate');
const ignoreNonWorkingDays = params.get('ignoreNonWorkingDays');
if (startDate || dueDate || ignoreNonWorkingDays) {
const assigneeHref = params.get('assignee_href');
if (startDate || dueDate || ignoreNonWorkingDays || assigneeHref) {
const existingDefaults = this.stateParams?.defaults;
this.stateParams = {
...this.stateParams,
defaults: {
_links: {},
...this.stateParams?.defaults,
...existingDefaults,
...(startDate ? { startDate } : {}),
...(dueDate ? { dueDate } : {}),
...(ignoreNonWorkingDays ? { ignoreNonWorkingDays: true } : {}),
...(assigneeHref ? {
_links: {
...(existingDefaults?._links || {}),
assignee: { href: assigneeHref },
},
} : {}),
},
};
}
@@ -62,7 +62,7 @@ export class WorkPackageSplitCreateEntryComponent implements AfterViewInit, OnDe
constructor(readonly elementRef:ElementRef) {
populateInputsFromDataset(this);
document.body.classList.add('\'router--work-packages-partitioned-split-view-new');
document.body.classList.add('router--work-packages-partitioned-split-view-new');
}
ngAfterViewInit():void {
@@ -73,6 +73,6 @@ export class WorkPackageSplitCreateEntryComponent implements AfterViewInit, OnDe
}
ngOnDestroy():void {
document.body.classList.remove('\'router--work-packages-partitioned-split-view-new');
document.body.classList.remove('router--work-packages-partitioned-split-view-new');
}
}
@@ -1,10 +1,15 @@
# frozen_string_literal: true
module ::TeamPlanner
class TeamPlannerController < BaseController
include EnterpriseHelper
include Layout
include WorkPackages::WithSplitView
before_action :load_and_authorize_in_optional_project
before_action :build_plan_view, only: %i[new]
before_action :find_plan_view, only: %i[destroy]
before_action :find_plan_view, only: %i[destroy split_view]
authorize_with_permission :add_work_packages, only: %i[split_create]
guard_enterprise_feature(:team_planner_view, except: %i[index overview]) do
redirect_to action: :index
@@ -21,6 +26,7 @@ module ::TeamPlanner
render layout: "global"
end
def show; end
def new; end
def create
@@ -37,8 +43,28 @@ module ::TeamPlanner
end
end
def show
render layout: "angular/angular"
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 split_create
respond_to do |format|
format.html do
if turbo_frame_request?
render "work_packages/split_create", layout: false
else
render :show
end
end
end
end
def upsell; end
@@ -63,12 +89,16 @@ module ::TeamPlanner
private
def split_view_base_route
project_team_planner_path(@project, @view, request.query_parameters)
end
def create_service_class
TeamPlanner::Views::GlobalCreateService
end
def plan_view_params
params.require(:query).permit(:name, :public, :starred).merge(project_id: @project&.id)
params.expect(query: %i[name public starred]).merge(project_id: @project&.id)
end
def build_plan_view
@@ -1 +1,11 @@
<% html_title(t("team_planner.label_team_planner")) -%>
<% content_for :content_body do %>
<%= angular_component_tag "opce-team-planner-view" %>
<% 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? %>
<%= render(split_create_instance) if render_work_package_split_create? %>
<% end %>
+9
View File
@@ -19,6 +19,15 @@ Rails.application.routes.draw do
end
member do
get "details/new",
action: :split_create,
as: :split_create,
work_package_split_create: true
get "details/:work_package_id(/:tab)",
action: :split_view,
defaults: { tab: :overview },
as: :details,
work_package_split_view: true
get "(/*state)" => "team_planner/team_planner#show", as: ""
end
end
@@ -40,7 +40,7 @@ module OpenProject::TeamPlanner
dependencies: :work_package_tracking,
enterprise_feature: "team_planner_view" do
permission :view_team_planner,
{ "team_planner/team_planner": %i[index show upsell overview],
{ "team_planner/team_planner": %i[index show split_view split_create upsell overview],
"team_planner/menus": %i[show] },
permissible_on: :project,
dependencies: %i[view_work_packages],