Skip to content
60 changes: 60 additions & 0 deletions frontend/src/turbo/action-menu-morph-remount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* -- 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.
* ++
*/

/**
* Primer `<action-menu>` with `src` (via `Primer::Alpha::ActionMenu`) lazy loads
* the menu items via `<include-fragment>`.
* It registers `include-fragment-replaced` only in `connectedCallback`, while Turbo
* Idiomorph can leave the same `<action-menu>` host connected while swapping
* in a new `<include-fragment>`. Since the `<action-menu>` is not replaced, the fragment
* replaced event is never fired, the component will stay in a loading state after morph.
*
* Replacing each affected `<action-menu>` host forces `connectedCallback` and
* a correct listener. We hook `turbo:morph-element` so this applies to turbo-stream
* morph, frame morph, and full-page morph alike.
*/

interface TurboMorphElementDetail {
currentElement:HTMLElement;
newElement:HTMLElement;
}

export function registerActionMenuMorphRemount():void {
document.addEventListener('turbo:morph-element', (event:Event) => {
const { detail } = event as CustomEvent<TurboMorphElementDetail>;
const currentElement = detail?.currentElement;
if (!currentElement?.matches('action-menu:has(include-fragment[src])')) {
return;
}

const clone = currentElement.cloneNode(true);
currentElement.replaceWith(clone);
});
}
2 changes: 2 additions & 0 deletions frontend/src/turbo/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { debugLog, whenDebugging } from 'core-app/shared/helpers/debug_output';
import { TURBO_EVENTS } from './constants';
import { StreamActions } from '@hotwired/turbo';
import { addTurboAngularWrapper } from 'core-turbo/turbo-angular-wrapper';
import { registerActionMenuMorphRemount } from './action-menu-morph-remount';

Turbo.session.drive = true;
Turbo.config.drive.progressBarDelay = 100;
Expand All @@ -37,6 +38,7 @@ whenDebugging(() => {
// Register our own actions
addTurboEventListeners();
addTurboGlobalListeners();
registerActionMenuMorphRemount();
registerDialogStreamAction();
registerFlashStreamAction();
registerLiveRegionStreamAction();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ See COPYRIGHT and LICENSE files for more details.
),
tabindex: 0
) do %>
<%= render(Backlogs::StoryComponent.new(story:, project:, sprint:, max_position:)) %>
<%= render(Backlogs::StoryComponent.new(story:, project:, sprint:)) %>
<% end %>
<% end %>
<% end %>
Expand Down
4 changes: 0 additions & 4 deletions modules/backlogs/app/components/backlogs/backlog_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,6 @@ def folded?
current_user.backlogs_preference(:versions_default_fold_state) == "closed"
end

def max_position
stories.filter_map(&:position).max
end

def drop_target_config
{
generic_drag_and_drop_target: "container",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,7 @@ See COPYRIGHT and LICENSE files for more details.
<%= render Backlogs::InboxItemComponent.with_collection(
paginate? ? first_page : work_packages,
container: border_box,
project:,
max_position:,
open_sprints_exist:
project:
) %>

<% if paginate? %>
Expand Down Expand Up @@ -110,9 +108,7 @@ See COPYRIGHT and LICENSE files for more details.
<%= render Backlogs::InboxItemComponent.with_collection(
last_page,
container: border_box,
project:,
max_position:,
open_sprints_exist:
project:
) %>
<% end %>
<% end %>
Expand Down
9 changes: 2 additions & 7 deletions modules/backlogs/app/components/backlogs/inbox_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,15 @@ class InboxComponent < ApplicationComponent
FIRST_PAGE_SIZE = 50
LAST_PAGE_SIZE = 10

attr_reader :work_packages, :project, :current_user, :show_all, :open_sprints_exist
attr_reader :work_packages, :project, :current_user, :show_all

def initialize(work_packages:, project:, open_sprints_exist:, show_all: false, current_user: User.current, **system_arguments)
def initialize(work_packages:, project:, show_all: false, current_user: User.current, **system_arguments)
super()

@work_packages = work_packages
@project = project
@show_all = show_all
@current_user = current_user
@open_sprints_exist = open_sprints_exist

@system_arguments = system_arguments
@system_arguments[:id] = inbox_dom_id
Expand Down Expand Up @@ -100,9 +99,5 @@ def drop_target_config
target_allowed_drag_type: "story"
}
end

def max_position
work_packages.maximum(:position)
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,21 @@ See COPYRIGHT and LICENSE files for more details.
<% end %>
<% end %>
<% grid.with_area(:menu) do %>
<%= render Backlogs::InboxMenuComponent.new(
work_package:,
project:,
max_position:,
open_sprints_exist:
) %>
<%= render(
Primer::Alpha::ActionMenu.new(
menu_id: dom_target(work_package, :menu),
src: menu_project_inbox_path(project, work_package),
anchor_align: :end,
classes: "hide-when-print"
)
) do |menu| %>
<% menu.with_show_button(
scheme: :invisible,
icon: :"kebab-horizontal",
"aria-label": t(".label_actions"),
tooltip_direction: :se
) %>
<% end %>
<% end %>
<% grid.with_area(:subject) do %>
<%= render(Primer::Beta::Text.new(font_weight: :semibold)) { work_package.subject } %>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,14 @@ module Backlogs
class InboxItemComponent < ApplicationComponent
include OpPrimer::ComponentHelpers

attr_reader :work_package, :project, :container, :max_position, :current_user, :open_sprints_exist
attr_reader :work_package, :project, :container, :current_user

def initialize(inbox_item:, project:, container:, max_position:, open_sprints_exist:,
current_user: User.current)
def initialize(inbox_item:, project:, container:, current_user: User.current)
super()

@work_package = inbox_item
@project = project
@container = container
@max_position = max_position
@open_sprints_exist = open_sprints_exist
@current_user = current_user
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,8 @@ See COPYRIGHT and LICENSE files for more details.
++# %>

<%=
render(Primer::Alpha::ActionMenu.new(**@system_arguments)) do |menu|
menu.with_show_button(
scheme: :invisible,
icon: :"kebab-horizontal",
"aria-label": t(".label_actions"),
tooltip_direction: :se
)

menu.with_item(
render(Primer::Alpha::ActionMenu::List.new(menu_id:)) do |list|
list.with_item(
id: dom_target(work_package, :menu, :open_details),
tag: :a,
label: t(:"js.button_open_details"),
Expand All @@ -46,7 +39,7 @@ See COPYRIGHT and LICENSE files for more details.
item.with_leading_visual_icon(icon: :"op-view-split")
end

menu.with_item(
list.with_item(
id: dom_target(work_package, :menu, :open_fullscreen),
tag: :a,
label: t(:"js.button_open_fullscreen"),
Expand All @@ -56,7 +49,7 @@ See COPYRIGHT and LICENSE files for more details.
item.with_leading_visual_icon(icon: :"screen-full")
end

menu.with_item(
list.with_item(
id: dom_target(work_package, :menu, :copy_url_to_clipboard),
tag: :"clipboard-copy",
label: t(".action_menu.copy_url_to_clipboard"),
Expand All @@ -65,7 +58,7 @@ See COPYRIGHT and LICENSE files for more details.
item.with_leading_visual_icon(icon: :copy)
end

menu.with_item(
list.with_item(
id: dom_target(work_package, :menu, :copy_work_package_id),
tag: :"clipboard-copy",
label: t(".action_menu.copy_work_package_id"),
Expand All @@ -75,9 +68,14 @@ See COPYRIGHT and LICENSE files for more details.
end

if show_move_submenu?
menu.with_divider

menu.with_sub_menu_item(label: t(".action_menu.move_menu")) do |move_menu|
list.with_divider

list.with_item(
component_klass: Primer::Alpha::ActionMenu::SubMenuItem,
label: t(".action_menu.move_menu"),
select_variant: :none,
form_arguments: {}
) do |move_menu|
move_menu.with_leading_visual_icon(icon: :"op-arrow-in")

with_item_group(move_menu) do
Expand Down
18 changes: 8 additions & 10 deletions modules/backlogs/app/components/backlogs/inbox_menu_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,27 +29,25 @@
#++

module Backlogs
# Renders Primer::Alpha::ActionMenu::List for the deferred menu (InboxController#menu).
# +menu_id+ must match the row ActionMenu in InboxItemComponent.
class InboxMenuComponent < ApplicationComponent
include OpPrimer::ComponentHelpers

attr_reader :work_package, :project, :max_position, :current_user, :open_sprints_exist

def initialize(work_package:, project:, max_position:, open_sprints_exist:, current_user: User.current, **system_arguments)
def initialize(work_package:, project:, max_position:, open_sprints_exist:, current_user: User.current)
super()

@work_package = work_package
@project = project
@max_position = max_position
@current_user = current_user
@max_position = max_position
@open_sprints_exist = open_sprints_exist
end

@system_arguments = system_arguments
@system_arguments[:menu_id] = dom_target(work_package, :menu)
@system_arguments[:anchor_align] = :end
@system_arguments[:classes] = class_names(
@system_arguments[:classes],
"hide-when-print"
)
def menu_id
dom_target(work_package, :menu)
end

private
Expand All @@ -60,7 +58,7 @@ def show_move_submenu?

def show_move_items?
allowed_to_manage_sprint_items? &&
!(first_item? && last_item?)
!(first_item? && last_item?)
end

def show_move_to_sprint?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ See COPYRIGHT and LICENSE files for more details.
data: story_data_attribute(story),
tabindex: 0
) do %>
<%= render(Backlogs::StoryComponent.new(story:, sprint:, project:, max_position:)) %>
<%= render(Backlogs::StoryComponent.new(story:, sprint:, project:)) %>
<% end %>
<% end %>
<% end %>
Expand Down
4 changes: 0 additions & 4 deletions modules/backlogs/app/components/backlogs/sprint_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,6 @@ def folded?
current_user.backlogs_preference(:versions_default_fold_state) == "closed"
end

def max_position
stories.filter_map(&:position).max
end

def drop_target_config
{
generic_drag_and_drop_target: "container",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,21 @@ See COPYRIGHT and LICENSE files for more details.
<% end %>

<% grid.with_area(:menu) do %>
<%= render(Backlogs::StoryMenuComponent.new(story:, sprint:, project:, max_position:)) %>
<%= render(
Primer::Alpha::ActionMenu.new(
menu_id: dom_target(story, :menu),
src: menu_backlogs_project_sprint_story_path(project, sprint, story),
anchor_align: :end,
classes: "hide-when-print"
)
) do |menu| %>
<% menu.with_show_button(
scheme: :invisible,
icon: :"kebab-horizontal",
"aria-label": t(".label_actions"),
tooltip_direction: :se
) %>
<% end %>
<% end %>

<% grid.with_area(:subject) do %>
Expand Down
5 changes: 2 additions & 3 deletions modules/backlogs/app/components/backlogs/story_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,14 @@ module Backlogs
class StoryComponent < ApplicationComponent
include OpPrimer::ComponentHelpers

attr_reader :story, :sprint, :project, :max_position, :current_user
attr_reader :story, :sprint, :project, :current_user

def initialize(story:, sprint:, project:, max_position:, current_user: User.current)
def initialize(story:, sprint:, project:, current_user: User.current)
super()

@story = story
@sprint = sprint
@project = project
@max_position = max_position
@current_user = current_user
end

Expand Down
Loading
Loading