diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 1c97c54808f..1bca3a907a7 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -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 }); diff --git a/frontend/src/app/core/routing/openproject.routes.ts b/frontend/src/app/core/routing/openproject.routes.ts index 59250162984..e36af84a380 100644 --- a/frontend/src/app/core/routing/openproject.routes.ts +++ b/frontend/src/app/core/routing/openproject.routes.ts @@ -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, ]; /** diff --git a/frontend/src/app/features/team-planner/team-planner/add-work-packages/add-existing-pane.component.ts b/frontend/src/app/features/team-planner/team-planner/add-work-packages/add-existing-pane.component.ts index 1636b2e96c3..6ef92b88a1b 100644 --- a/frontend/src/app/features/team-planner/team-planner/add-work-packages/add-existing-pane.component.ts +++ b/frontend/src/app/features/team-planner/team-planner/add-work-packages/add-existing-pane.component.ts @@ -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) { diff --git a/frontend/src/app/features/team-planner/team-planner/page/team-planner-page.component.ts b/frontend/src/app/features/team-planner/team-planner/page/team-planner-page.component.ts index d77dd42af8a..9a960c1922e 100644 --- a/frontend/src/app/features/team-planner/team-planner/page/team-planner-page.component.ts +++ b/frontend/src/app/features/team-planner/team-planner/page/team-planner-page.component.ts @@ -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( diff --git a/frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.ts b/frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.ts index 87846cdfd3e..569a70aac64 100644 --- a/frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.ts +++ b/frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.ts @@ -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 { diff --git a/frontend/src/app/features/team-planner/team-planner/team-planner-entry.component.ts b/frontend/src/app/features/team-planner/team-planner/team-planner-entry.component.ts new file mode 100644 index 00000000000..a54474a0a69 --- /dev/null +++ b/frontend/src/app/features/team-planner/team-planner/team-planner-entry.component.ts @@ -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: '', + 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'); + } +} diff --git a/frontend/src/app/features/team-planner/team-planner/team-planner.lazy-routes.ts b/frontend/src/app/features/team-planner/team-planner/team-planner.lazy-routes.ts deleted file mode 100644 index d9449ae243a..00000000000 --- a/frontend/src/app/features/team-planner/team-planner/team-planner.lazy-routes.ts +++ /dev/null @@ -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), - }, -]; diff --git a/frontend/src/app/features/team-planner/team-planner/team-planner.module.ts b/frontend/src/app/features/team-planner/team-planner/team-planner.module.ts index 3d3d2c1e744..769e1259220 100644 --- a/frontend/src/app/features/team-planner/team-planner/team-planner.module.ts +++ b/frontend/src/app/features/team-planner/team-planner/team-planner.module.ts @@ -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, diff --git a/frontend/src/app/features/team-planner/team-planner/team-planner.routes.ts b/frontend/src/app/features/team-planner/team-planner/team-planner.routes.ts deleted file mode 100644 index 405fe948a81..00000000000 --- a/frontend/src/app/features/team-planner/team-planner/team-planner.routes.ts +++ /dev/null @@ -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, - ), -]; diff --git a/frontend/src/app/features/work-packages/components/wp-new/wp-new-split-view.component.ts b/frontend/src/app/features/work-packages/components/wp-new/wp-new-split-view.component.ts index 48a3ef0eafb..baab2d08835 100644 --- a/frontend/src/app/features/work-packages/components/wp-new/wp-new-split-view.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-new/wp-new-split-view.component.ts @@ -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 }, + }, + } : {}), }, }; } diff --git a/frontend/src/app/features/work-packages/routing/wp-split-create/wp-split-create-entry.component.ts b/frontend/src/app/features/work-packages/routing/wp-split-create/wp-split-create-entry.component.ts index 45b2a759e25..29da7e5f034 100644 --- a/frontend/src/app/features/work-packages/routing/wp-split-create/wp-split-create-entry.component.ts +++ b/frontend/src/app/features/work-packages/routing/wp-split-create/wp-split-create-entry.component.ts @@ -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'); } } diff --git a/modules/team_planner/app/controllers/team_planner/team_planner_controller.rb b/modules/team_planner/app/controllers/team_planner/team_planner_controller.rb index 2e02e579725..167fb37cf6c 100644 --- a/modules/team_planner/app/controllers/team_planner/team_planner_controller.rb +++ b/modules/team_planner/app/controllers/team_planner/team_planner_controller.rb @@ -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 diff --git a/modules/team_planner/app/views/team_planner/team_planner/show.html.erb b/modules/team_planner/app/views/team_planner/team_planner/show.html.erb index 157af1b2b0e..21bd5a6398e 100644 --- a/modules/team_planner/app/views/team_planner/team_planner/show.html.erb +++ b/modules/team_planner/app/views/team_planner/team_planner/show.html.erb @@ -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 %> diff --git a/modules/team_planner/config/routes.rb b/modules/team_planner/config/routes.rb index 45b91bc3547..6937a39f73e 100644 --- a/modules/team_planner/config/routes.rb +++ b/modules/team_planner/config/routes.rb @@ -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 diff --git a/modules/team_planner/lib/open_project/team_planner/engine.rb b/modules/team_planner/lib/open_project/team_planner/engine.rb index c4c51f2303e..1a5a5c91875 100644 --- a/modules/team_planner/lib/open_project/team_planner/engine.rb +++ b/modules/team_planner/lib/open_project/team_planner/engine.rb @@ -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],