diff --git a/modules/calendar/app/views/calendar/calendars/new.html.erb b/modules/calendar/app/views/calendar/calendars/new.html.erb
index 0de4281096f4..78979bb28bc9 100644
--- a/modules/calendar/app/views/calendar/calendars/new.html.erb
+++ b/modules/calendar/app/views/calendar/calendars/new.html.erb
@@ -39,9 +39,48 @@ See COPYRIGHT and LICENSE files for more details.
end
%>
-<%= labelled_tabular_form_for @view, url: { controller: "/calendar/calendars", action: "create" }, html: { id: "calendar-form" } do |f| -%>
- <%= render partial: "form", locals: { f: f } %>
- <%= styled_button_tag t(:button_create), class: "-primary" %>
- <%= link_to t(:button_cancel), { controller: "calendar/calendars", action: "index" },
- class: "button" %>
+<%= primer_form_with(model: @view, scope: :query, url: calendars_path) do |f| %>
+ <%= render_inline_form(f) do |form|
+ project_id_value = @project&.id
+ form.text_field(
+ name: :name,
+ label: helpers.t(:label_title),
+ required: true,
+ input_width: :large
+ )
+
+ form.project_autocompleter(
+ name: :project_id,
+ label: Query.human_attribute_name(:project),
+ required: true,
+ caption: helpers.t("help_texts.views.project",
+ singular: helpers.t(:label_calendar).downcase,
+ plural: helpers.t(:label_calendar_plural)),
+ input_width: :large,
+ autocomplete_options: {
+ focusDirectly: false,
+ dropdownPosition: "bottom",
+ inputValue: project_id_value,
+ filters: [{ name: "user_action", operator: "=", values: ["calendars/create"] }],
+ data: { "test-selector": "project_id" }
+ }
+ )
+
+ form.check_box(
+ name: :public,
+ label: Query.human_attribute_name(:public),
+ caption: helpers.t("help_texts.views.public")
+ )
+
+ form.check_box(
+ name: :starred,
+ label: helpers.t(:label_favorite),
+ caption: helpers.t("help_texts.views.favoured")
+ )
+
+ form.group(layout: :horizontal) do |button_group|
+ button_group.submit(label: helpers.t(:button_create), name: :submit, scheme: :primary)
+ button_group.button(tag: :a, href: helpers.calendars_path, label: helpers.t(:button_cancel), name: :cancel)
+ end
+ end %>
<% end %>
diff --git a/modules/calendar/app/views/calendar/calendars/show.html.erb b/modules/calendar/app/views/calendar/calendars/show.html.erb
index 71d247033717..8672b487dccc 100644
--- a/modules/calendar/app/views/calendar/calendars/show.html.erb
+++ b/modules/calendar/app/views/calendar/calendars/show.html.erb
@@ -27,4 +27,13 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
-<% html_title(t(:label_calendar_plural)) -%>
+<% html_title(@view&.name.presence || t(:label_calendar_plural)) -%>
+
+<% content_for :content_body do %>
+ <%= angular_component_tag "opce-calendar-view", inputs: { queryId: @view&.id.to_s } %>
+<% end %>
+
+<% content_for :content_body_right do %>
+ <%= 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/calendar/app/views/calendar/menus/_menu.html.erb b/modules/calendar/app/views/calendar/menus/_menu.html.erb
index 4ecb73e0680d..dfce2ac8225d 100644
--- a/modules/calendar/app/views/calendar/menus/_menu.html.erb
+++ b/modules/calendar/app/views/calendar/menus/_menu.html.erb
@@ -1,5 +1,5 @@
<%= turbo_frame_tag "calendar_sidemenu",
src: menu_project_calendars_path(@project, **params.permit(:id)),
target: "_top",
- data: { turbo: false },
+ data: { turbo: false, query_param: "id" },
loading: :lazy %>
diff --git a/modules/calendar/config/routes.rb b/modules/calendar/config/routes.rb
index ec673b259fc0..1a3090c2fc76 100644
--- a/modules/calendar/config/routes.rb
+++ b/modules/calendar/config/routes.rb
@@ -2,14 +2,30 @@
scope "projects/:project_id", as: "project" do
resources :calendars,
controller: "calendar/calendars",
- only: %i[index destroy],
+ only: %i[index show new create destroy],
as: :calendars do
collection do
get "menu" => "calendar/menus#show"
+ get "new/details/new",
+ action: :split_create,
+ work_package_split_create: true
+ get "new/details/:work_package_id(/:tab)",
+ action: :split_view,
+ defaults: { tab: :overview },
+ work_package_split_view: true
end
- get "/new" => "calendar/calendars#show", on: :collection, as: "new"
get "/ical" => "calendar/ical#show", on: :member, as: "ical"
- get "(/*state)" => "calendar/calendars#show", on: :member, as: ""
+ 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
+ end
end
end
diff --git a/modules/calendar/lib/open_project/calendar/engine.rb b/modules/calendar/lib/open_project/calendar/engine.rb
index fc0f08a0e75a..031234fdd6d5 100644
--- a/modules/calendar/lib/open_project/calendar/engine.rb
+++ b/modules/calendar/lib/open_project/calendar/engine.rb
@@ -28,7 +28,7 @@ class Engine < ::Rails::Engine
settings: {} do
project_module :calendar_view, dependencies: :work_package_tracking do
permission :view_calendar,
- { "calendar/calendars": %i[index show],
+ { "calendar/calendars": %i[index show split_view split_create new],
"calendar/menus": %i[show] },
permissible_on: :project,
dependencies: %i[view_work_packages],
diff --git a/modules/calendar/spec/features/calendars_spec.rb b/modules/calendar/spec/features/calendars_spec.rb
index 9c559cc5606e..8f32f4f2aa5b 100644
--- a/modules/calendar/spec/features/calendars_spec.rb
+++ b/modules/calendar/spec/features/calendars_spec.rb
@@ -63,7 +63,7 @@
due_date: Time.zone.today.at_beginning_of_month.next_month + 18.days)
end
let(:filters) { Components::WorkPackages::Filters.new }
- let(:current_wp_split_screen) { Pages::SplitWorkPackage.new(current_work_package, project) }
+ let(:current_wp_split_screen) { Pages::PrimerizedSplitWorkPackage.new(current_work_package, project) }
before do
login_as(user)
@@ -83,10 +83,11 @@
find('[data-test-selector="add-calendar-button"]', text: "Calendar").click
loading_indicator_saveguard
+ expect_angular_frontend_initialized
# should open the calendar with the current month displayed
expect(page)
- .to have_css ".fc-event-title", text: current_work_package.subject
+ .to have_css ".fc-event-title", text: current_work_package.subject, wait: 20
expect(page)
.to have_css ".fc-event-title", text: another_current_work_package.subject
expect(page)
@@ -172,8 +173,9 @@
# go back a month by using the browser back functionality
page.execute_script("window.history.back()")
+ expect_angular_frontend_initialized
expect(page)
- .to have_css ".fc-event-title", text: current_work_package.subject
+ .to have_css ".fc-event-title", text: current_work_package.subject, wait: 20
expect(page)
.to have_css ".fc-event-title", text: another_current_work_package.subject
expect(page)
@@ -183,21 +185,30 @@
# click goes to work package split screen
page.find(".fc-event-title", text: current_work_package.subject).click
- current_wp_split_screen.expect_open
+
+ wait_for_turbo_frame do
+ expect(page).to have_current_path("/projects/#{project.identifier}/calendars/new/details/#{current_work_package.id}", ignore_query: true)
+ current_wp_split_screen.expect_open
+ end
# Going back in browser history will lead us back to the calendar
# Regression #29664
- page.go_back
- expect(page)
- .to have_css(".fc-event-title", text: current_work_package.subject, wait: 20)
- current_wp_split_screen.expect_closed
+ retry_block do
+ page.go_back
+ expect_angular_frontend_initialized
+ expect(page)
+ .to have_css(".fc-event-title", text: current_work_package.subject, wait: 20)
+ current_wp_split_screen.expect_closed
+ end
# After go_back, the app may not be fully initialized even though the
# calendar events are visible. Clicking too early can cause an "not
# authorized" error on the split screen API call. Retry to handle this.
retry_block do
page.find(".fc-event-title", text: current_work_package.subject).click
- current_wp_split_screen.expect_open
+ wait_for_turbo_frame do
+ current_wp_split_screen.expect_open
+ end
end
# click back goes back to calendar
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 2e02e5797252..19469857dc42 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 @@ def overview
render layout: "global"
end
+ def show; end
def new; end
def create
@@ -37,8 +43,28 @@ def create
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,23 @@ def destroy
private
+ def split_view_base_route
+ # Unsaved team planners use the /new collection path (no :id).
+ # In that case @view is nil and we return the /new path as the base route
+ # so that the split view close button navigates back correctly.
+ if @view
+ project_team_planner_path(@project, @view, request.query_parameters)
+ else
+ new_project_team_planners_path(@project, request.query_parameters)
+ end
+ 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
@@ -76,6 +113,11 @@ def build_plan_view
end
def find_plan_view
+ # The split_view action is also reachable via the /new collection path
+ # (e.g. /team_planners/new/details/:wp_id) which carries no :id.
+ # In that case @view remains nil and split_view_base_route handles it.
+ return if params[:id].blank?
+
@view = Query
.visible(current_user)
.find(params[:id])
diff --git a/modules/team_planner/app/views/team_planner/menus/_menu.html.erb b/modules/team_planner/app/views/team_planner/menus/_menu.html.erb
index d27316de1e35..a5b5bd887535 100644
--- a/modules/team_planner/app/views/team_planner/menus/_menu.html.erb
+++ b/modules/team_planner/app/views/team_planner/menus/_menu.html.erb
@@ -1,5 +1,5 @@
<%= turbo_frame_tag "team_planner_sidemenu",
src: menu_project_team_planners_path(@project, **params.permit(:id)),
target: "_top",
- data: { turbo: false },
+ data: { turbo: false, query_param: "id" },
loading: :lazy %>
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 157af1b2b0ef..4621ccf2bc3d 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,10 @@
<% 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 %>
+ <%= 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 45b91bc3547d..cb40015d1239 100644
--- a/modules/team_planner/config/routes.rb
+++ b/modules/team_planner/config/routes.rb
@@ -15,10 +15,26 @@
as: :team_planners do
collection do
get "menu" => "team_planner/menus#show"
+ get "new/details/new",
+ action: :split_create,
+ work_package_split_create: true
+ get "new/details/:work_package_id(/:tab)",
+ action: :split_view,
+ defaults: { tab: "overview" },
+ work_package_split_view: true
get "/new", to: "team_planner/team_planner#show", as: :new
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 c4c51f2303ee..1a5a5c918752 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 @@ class Engine < ::Rails::Engine
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],
diff --git a/modules/team_planner/spec/features/team_planner_add_existing_work_packages_spec.rb b/modules/team_planner/spec/features/team_planner_add_existing_work_packages_spec.rb
index c6d85903af1d..628161313428 100644
--- a/modules/team_planner/spec/features/team_planner_add_existing_work_packages_spec.rb
+++ b/modules/team_planner/spec/features/team_planner_add_existing_work_packages_spec.rb
@@ -130,8 +130,9 @@
# 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
@@ -169,7 +170,10 @@
# New events are directly clickable
split_view = team_planner.open_split_view_by_info_icon(third_wp)
- split_view.expect_open
+ wait_for_turbo_frame do
+ expect(page).to have_current_path /\/details\/#{third_wp.id}/
+ split_view.expect_subject
+ end
end
context "with non-working days" do
diff --git a/modules/team_planner/spec/features/team_planner_dates_spec.rb b/modules/team_planner/spec/features/team_planner_dates_spec.rb
index 1f653a368f82..ef8bb3f3a6f9 100644
--- a/modules/team_planner/spec/features/team_planner_dates_spec.rb
+++ b/modules/team_planner/spec/features/team_planner_dates_spec.rb
@@ -42,6 +42,7 @@
it 'hides sat and sun in the "Work week" view andd renders sat and sun as non working in the "1-week" view' do
team_planner.visit!
+ team_planner.wait_for_loaded
team_planner.expect_empty_state
team_planner.add_assignee user.name
diff --git a/modules/team_planner/spec/features/team_planner_error_handling_spec.rb b/modules/team_planner/spec/features/team_planner_error_handling_spec.rb
index 9b1bc621c4b3..0c91f557db9f 100644
--- a/modules/team_planner/spec/features/team_planner_error_handling_spec.rb
+++ b/modules/team_planner/spec/features/team_planner_error_handling_spec.rb
@@ -107,7 +107,7 @@
.perform
end
- team_planner.expect_toast(type: :error, message: I18n.t("api_v3.errors.code_409"))
+ expect_flash(type: :error, message: I18n.t("notice_locking_conflict_danger"))
work_package.reload
expect(work_package.start_date).to eq(Time.zone.today.beginning_of_week.next_occurring(:tuesday))
diff --git a/modules/team_planner/spec/features/team_planner_spec.rb b/modules/team_planner/spec/features/team_planner_spec.rb
index b4e11253832d..20a1171f4be0 100644
--- a/modules/team_planner/spec/features/team_planner_spec.rb
+++ b/modules/team_planner/spec/features/team_planner_spec.rb
@@ -46,6 +46,7 @@
expect(page).to have_content "There is currently nothing to display."
page.find_test_selector("add-team-planner-button").click
+ expect_angular_frontend_initialized
team_planner.expect_title
filters.expect_filter_count("1")
@@ -241,6 +242,7 @@
it "can add and remove assignees" do
team_planner.visit!
+ team_planner.wait_for_loaded
team_planner.expect_empty_state
team_planner.expect_assignee(user, present: false)
team_planner.expect_assignee(other_user, present: false)
diff --git a/modules/team_planner/spec/features/team_planner_view_modes_spec.rb b/modules/team_planner/spec/features/team_planner_view_modes_spec.rb
index c7e19f938989..51251f46016b 100644
--- a/modules/team_planner/spec/features/team_planner_view_modes_spec.rb
+++ b/modules/team_planner/spec/features/team_planner_view_modes_spec.rb
@@ -38,6 +38,7 @@
it "allows switching of view modes", with_settings: { working_days: [1, 2, 3, 4, 5] } do
team_planner.visit!
+ team_planner.wait_for_loaded
team_planner.expect_empty_state
team_planner.add_assignee user.name
diff --git a/modules/team_planner/spec/routing/team_planner_routing_spec.rb b/modules/team_planner/spec/routing/team_planner_routing_spec.rb
index c8efea61ed93..be48e18b5a5b 100644
--- a/modules/team_planner/spec/routing/team_planner_routing_spec.rb
+++ b/modules/team_planner/spec/routing/team_planner_routing_spec.rb
@@ -69,11 +69,18 @@
.to(controller: "team_planner/team_planner", action: :create)
end
- it "routes to team_planner#show with state" do
+ it "routes to team_planner#split_view" do
expect(subject)
.to route(:get, "/projects/foobar/team_planners/1234/details/555")
- .to(controller: "team_planner/team_planner", action: :show, project_id: "foobar", id: "1234",
- state: "details/555")
+ .to(controller: "team_planner/team_planner", action: :split_view, project_id: "foobar", id: "1234",
+ work_package_id: "555", tab: "overview", work_package_split_view: true)
+ end
+
+ it "routes to team_planner#split_create" do
+ expect(subject)
+ .to route(:get, "/projects/foobar/team_planners/1234/details/new")
+ .to(controller: "team_planner/team_planner", action: :split_create, project_id: "foobar", id: "1234",
+ work_package_split_create: true)
end
it "routes to team_planner#destroy" do
diff --git a/spec/features/work_packages/table/context_menu/context_menu_shared_examples.rb b/spec/features/work_packages/table/context_menu/context_menu_shared_examples.rb
index 5dcbc4d33fc1..ab2c1af9acd6 100644
--- a/spec/features/work_packages/table/context_menu/context_menu_shared_examples.rb
+++ b/spec/features/work_packages/table/context_menu/context_menu_shared_examples.rb
@@ -56,7 +56,7 @@
open_context_menu.call
menu.choose("Create new child")
expect(page).to have_css(".inline-edit--container.subject input")
- expect(current_url).to match(/.*\/create_new\?.*(&)*parent_id=#{work_package.id}/)
+ expect(current_url).to match(/.*\/(create_new|details\/new)\?.*(&)*parent_id=#{work_package.id}/)
find_by_id("work-packages--edit-actions-cancel").click
expect(page).to have_no_css(".inline-edit--container.subject input")
@@ -82,7 +82,7 @@
open_context_menu.call
menu.choose("Create new child")
expect(page).to have_css(".inline-edit--container.subject input")
- expect(current_url).to match(/.*\/create_new\?.*(&)*parent_id=#{work_package.id}/)
+ expect(current_url).to match(/.*\/(create_new|details\/new)\?.*(&)*parent_id=#{work_package.id}/)
split_view = Pages::SplitWorkPackageCreate.new project: work_package.project
subject = split_view.edit_field(:subject)
diff --git a/spec/support/pages/work_packages/work_package_cards.rb b/spec/support/pages/work_packages/work_package_cards.rb
index 388e39e8c7e8..325b1e697cbc 100644
--- a/spec/support/pages/work_packages/work_package_cards.rb
+++ b/spec/support/pages/work_packages/work_package_cards.rb
@@ -83,7 +83,7 @@ def open_split_view_by_info_icon(work_package)
# The offset is needed to ensure that the resizer does not catch the click, instead of the info icon
element.hover.find('[data-test-selector="op-wp-single-card--details-button"]').click(x: -5, y: 0)
- ::Pages::SplitWorkPackage.new(work_package, project)
+ ::Pages::PrimerizedSplitWorkPackage.new(work_package, project)
end
def drag_and_drop_work_package(from:, to:)