Create rails based split create

This commit is contained in:
Henriette Darge
2026-04-14 12:31:52 +02:00
parent cbf0fac8d3
commit 22a6d4e8b4
14 changed files with 245 additions and 3 deletions
@@ -0,0 +1,2 @@
<%= helpers.angular_component_tag "opce-wp-split-create",
inputs: { projectIdentifier: @project_identifier } %>
@@ -0,0 +1,37 @@
# frozen_string_literal: true
#-- 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.
#++
class WorkPackages::SplitCreateComponent < ApplicationComponent
def initialize(project_identifier:)
super
@project_identifier = project_identifier
end
end
@@ -33,6 +33,14 @@ module WorkPackages::SplitViewHelper
params[:work_package_split_view].present?
end
def render_work_package_split_create?
params[:work_package_split_create].present?
end
def split_create_instance
WorkPackages::SplitCreateComponent.new(project_identifier: params[:project_id])
end
def split_view_instance
WorkPackages::SplitViewComponent.new(id: params[:work_package_id],
tab: params[:tab],
@@ -0,0 +1,3 @@
<%= turbo_frame_tag "content-bodyRight" do %>
<%= render(split_create_instance) %>
<% end %>
+4
View File
@@ -144,6 +144,9 @@ import {
import {
WorkPackageSplitViewEntryComponent,
} from 'core-app/features/work-packages/routing/wp-split-view/wp-split-view-entry.component';
import {
WorkPackageSplitCreateEntryComponent,
} from 'core-app/features/work-packages/routing/wp-split-create/wp-split-create-entry.component';
import {
BoardEntryComponent,
} from 'core-app/features/boards/board/board-partitioned-page/board-entry.component';
@@ -393,6 +396,7 @@ export class OpenProjectModule implements DoBootstrap {
registerCustomElement('opce-notification-center', InAppNotificationCenterComponent, { injector });
registerCustomElement('opce-wp-split-view', WorkPackageSplitViewEntryComponent, { injector });
registerCustomElement('opce-wp-split-create', WorkPackageSplitCreateEntryComponent, { injector });
registerCustomElement('opce-board-view', BoardEntryComponent, { injector });
registerCustomElement('opce-calendar-view', CalendarEntryComponent, { injector });
registerCustomElement('opce-wp-full-view', WorkPackageFullViewEntryComponent, { injector });
@@ -289,6 +289,12 @@ export class OpWorkPackagesCalendarService extends UntilDestroyedMixin {
Turbo.visit(link, { frame: 'content-bodyRight', action: 'advance' });
}
public openSplitCreate():void {
const basePath = window.location.pathname.replace(/\/details\/.*$/, '');
const link = `${basePath}/details/new${window.location.search}`;
Turbo.visit(link, { frame: 'content-bodyRight', action: 'advance' });
}
public openFullView(id:string):void {
this.wpTableSelection.setSelection(id, -1);
Turbo.visit(this.pathHelper.workPackagePath(id));
@@ -27,7 +27,10 @@
//++
import { WorkPackageCreateComponent } from 'core-app/features/work-packages/components/wp-new/wp-create.component';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { firstValueFrom } from 'rxjs';
import { WorkPackagesListService } from 'core-app/features/work-packages/components/wp-list/wp-list.service';
@Component({
selector: 'wp-new-split-view',
@@ -36,4 +39,82 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
standalone: false,
})
export class WorkPackageNewSplitViewComponent extends WorkPackageCreateComponent {
private readonly wpListService = inject(WorkPackagesListService);
/**
* Before creating the new WP form, load the current query (with its active filters)
* into the isolated query space so that WorkPackageCreateService.defaultsFromFilters()
* can pre-populate the form fields automatically — no manual filter mapping needed.
*/
protected override async createdWorkPackage() {
if (!this.routedFromAngular) {
const params = new URLSearchParams(window.location.search);
// Load the active query into the isolated query space so that
// WorkPackageCreateService.defaultsFromFilters() can pre-populate filter-based fields.
const queryId = params.get('query_id');
const queryProps = params.get('query_props');
if (queryId || queryProps) {
await firstValueFrom(
this.wpListService.fromQueryParams(
{ query_id: queryId ?? undefined, query_props: queryProps ?? undefined },
this.currentProjectService.identifier ?? undefined,
),
);
}
// Apply date defaults passed via URL params (e.g. when dragging to create on the calendar).
const startDate = params.get('startDate');
const dueDate = params.get('dueDate');
const ignoreNonWorkingDays = params.get('ignoreNonWorkingDays');
if (startDate || dueDate || ignoreNonWorkingDays) {
this.stateParams = {
...this.stateParams,
defaults: {
_links: {},
...this.stateParams?.defaults,
...(startDate ? { startDate } : {}),
...(dueDate ? { dueDate } : {}),
...(ignoreNonWorkingDays ? { ignoreNonWorkingDays: true } : {}),
},
};
}
}
return super.createdWorkPackage();
}
public override cancelAndBack():void {
if (this.routedFromAngular) {
super.cancelAndBack();
return;
}
this.wpCreate.cancelCreation();
// Close the split panel by navigating to the base URL (strips /details/new),
// replacing the history entry so back-navigation skips the create state.
const basePath = window.location.pathname.replace(/\/details\/.*$/, '');
Turbo.visit(basePath + window.location.search, { frame: 'content-bodyRight', action: 'replace' });
}
public override onSaved(params:{ savedResource:WorkPackageResource, isInitial:boolean }):void {
if (this.routedFromAngular) {
super.onSaved(params);
return;
}
const { savedResource, isInitial } = params;
this.editForm?.cancel(false);
this.notificationService.showSave(savedResource, isInitial);
window.OpenProject.pageState = 'submitted';
// Open the newly created WP in the split panel.
const basePath = window.location.pathname.replace(/\/details\/.*$/, '');
Turbo.visit(`${basePath}/details/${savedResource.id}${window.location.search}`, {
frame: 'content-bodyRight',
action: 'advance',
});
}
}
@@ -406,6 +406,9 @@ import {
import { WorkPackageFullCopyEntryComponent } from 'core-app/features/work-packages/routing/wp-full-copy/wp-full-copy-entry.component';
import { WorkPackageFullCreateEntryComponent } from 'core-app/features/work-packages/routing/wp-full-create/wp-full-create-entry.component';
import { WorkPackageFullViewEntryComponent } from 'core-app/features/work-packages/routing/wp-full-view/wp-full-view-entry.component';
import {
WorkPackageSplitCreateEntryComponent,
} from 'core-app/features/work-packages/routing/wp-split-create/wp-split-create-entry.component';
@NgModule({
imports: [
@@ -590,6 +593,7 @@ import { WorkPackageFullViewEntryComponent } from 'core-app/features/work-packag
WorkPackageDetailsViewButtonComponent,
WorkPackageSplitViewComponent,
WorkPackageSplitViewEntryComponent,
WorkPackageSplitCreateEntryComponent,
WorkPackageBreadcrumbComponent,
WorkPackageSplitViewToolbarComponent,
WorkPackageWatcherButtonComponent,
@@ -0,0 +1,78 @@
//-- 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 {
AfterViewInit,
ChangeDetectionStrategy,
Component,
ElementRef,
Input,
OnDestroy,
} from '@angular/core';
import {
WorkPackageIsolatedQuerySpaceDirective,
} from 'core-app/features/work-packages/directives/query-space/wp-isolated-query-space.directive';
import { populateInputsFromDataset } from 'core-app/shared/components/dataset-inputs';
/**
* An entry component to be rendered by Rails which opens an isolated query space
* for the work package split create (create form in the split panel).
*/
@Component({
hostDirectives: [WorkPackageIsolatedQuerySpaceDirective],
standalone: false,
template: `
<div class="op-work-package-split-view">
<wp-new-split-view
[stateParams]="{ projectPath: projectIdentifier, type: type }"
[routedFromAngular]="false"
/>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class WorkPackageSplitCreateEntryComponent implements AfterViewInit, OnDestroy {
@Input() projectIdentifier?:string;
@Input() type?:string;
constructor(readonly elementRef:ElementRef) {
populateInputsFromDataset(this);
document.body.classList.add('\'router--work-packages-partitioned-split-view-new');
}
ngAfterViewInit():void {
// wp-new-split-view sets pageState = 'edited' unconditionally on mount,
// which would block Turbo navigation in the split panel context.
// Reset it here after all children have initialized.
window.OpenProject.pageState = 'pristine';
}
ngOnDestroy():void {
document.body.classList.remove('\'router--work-packages-partitioned-split-view-new');
}
}
@@ -36,6 +36,7 @@ module ::Calendar
before_action :authorize_global, only: %i[index create]
before_action :authorize_new, only: %i[new]
authorization_checked! :new
authorize_with_permission :add_work_packages, only: %i[split_create]
before_action :find_calendar, only: %i[show split_view destroy]
menu_item :calendar_view
@@ -66,6 +67,18 @@ module ::Calendar
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 new
# In a project context, show the calendar view with an unsaved query.
# In the global context (no project), show the form so the user can select a project.
@@ -36,4 +36,5 @@ See COPYRIGHT and LICENSE files for more details.
<% 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 %>
+4
View File
@@ -9,6 +9,10 @@ Rails.application.routes.draw do
end
get "/ical" => "calendar/ical#show", on: :member, as: "ical"
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 },
@@ -28,7 +28,7 @@ module OpenProject::Calendar
settings: {} do
project_module :calendar_view, dependencies: :work_package_tracking do
permission :view_calendar,
{ "calendar/calendars": %i[index show split_view new],
{ "calendar/calendars": %i[index show split_view split_create new],
"calendar/menus": %i[show] },
permissible_on: :project,
dependencies: %i[view_work_packages],
@@ -130,8 +130,9 @@ RSpec.describe "Team planner add existing work packages",
# Select work package in add existing
add_existing_pane.card(second_wp).click
split_screen = Pages::SplitWorkPackage.new second_wp
split_screen.expect_subject
# Wait for navigation to complete before checking the split panel DOM
expect(page).to have_current_path /\/details\/#{second_wp.id}/
split_screen.expect_subject
end
it "allows to add work packages via drag&drop from the left hand shortlist" do