Skip to content

Implementation/62608 add hovercard to gates #18543

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Apr 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/components/_index.sass
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
@import "open_project/common/submenu_component"
@import "filter/filters_component"
@import "projects/row_component"
@import "projects/phases/hover_card_component"
@import "op_primer/border_box_table_component"
@import "op_primer/form_helpers"
@import "work_packages/exports/modal_dialog_component"
Expand Down
10 changes: 8 additions & 2 deletions app/components/projects/phase_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<%=
flex_layout(align_items: :center, classes: "gap-2") do |type_container|
if display_start_gate?
type_container.with_column(classes: icon_color_class) do
type_container.with_column(
classes: icon_color_class,
data: hover_card_data_args(gate: :start)
) do
render Primer::Beta::Octicon.new(icon: gate_icon)
end
end
Expand All @@ -15,7 +18,10 @@
render(Primer::Beta::Text.new) { finish_date }
end
if display_finish_gate?
type_container.with_column(classes: icon_color_class) do
type_container.with_column(
classes: icon_color_class,
data: hover_card_data_args(gate: :finish)
) do
render Primer::Beta::Octicon.new(icon: gate_icon)
end
end
Expand Down
13 changes: 8 additions & 5 deletions app/components/projects/phase_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
module Projects
class PhaseComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
include Projects::Phases::Shared

def initialize(phase:, **)
@phase = phase
Expand Down Expand Up @@ -61,12 +62,14 @@ def display_finish_gate?
phase.finish_gate? && phase.finish_date.present?
end

def gate_icon
:"op-gate"
end
def hover_card_data_args(gate:)
raise ArgumentError, "gate must be either :start or :finish" unless %i[start finish].include?(gate)

def icon_color_class
helpers.hl_inline_class("project_phase_definition", phase.definition_id)
{
hover_card_trigger_target: "trigger",
hover_card_url: hover_card_project_phase_path(phase, gate:),
test_selector: "phase-#{phase.id}-#{gate}-gate"
}
end

private
Expand Down
58 changes: 58 additions & 0 deletions app/components/projects/phases/hover_card_component.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<%#-- 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.

++#%>

<%=
flex_layout(classes: "op-phase-gate-hover-card", data: { test_selector: "phase-gate-hover-card-#{phase.id}" }) do |flex|
flex.with_row do
flex_layout(classes: "op-phase-gate-hover-card--info", justify_content: :space_between) do |f|
f.with_column(classes: "op-phase-gate-hover-card--name flex-self-start") do
flex_layout do |fl|
fl.with_column(classes: icon_color_class, mr: 1) do
render Primer::Beta::Octicon.new(icon: gate_icon)
end
fl.with_column do
render(Primer::Beta::Text.new(data: { test_selector: "phase-gate-hover-card-name" })) { phase_gate_name }
end
end
end

f.with_column(classes: "op-phase-gate-hover-card--date flex-self-end") do
flex_layout do |fl|
fl.with_column(mr: 1) do
render Primer::Beta::Octicon.new(icon: :calendar, color: :muted)
end
fl.with_column do
render(Primer::Beta::Text.new(data: { test_selector: "phase-gate-hover-card-date" })) { phase_gate_date }
end
end
end
end
end
end
%>
69 changes: 69 additions & 0 deletions app/components/projects/phases/hover_card_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# 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.
#++

module Projects
module Phases
class HoverCardComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
include Projects::Phases::Shared

attr_reader :phase

def initialize(phase:, gate:)
raise ArgumentError, "gate must be either 'start' or 'finish'" unless %w[start finish].include?(gate)

super

@phase = phase
@gate = gate.to_sym
end

def phase_gate_name
case @gate
when :start
@phase.start_gate? ? @phase.start_gate_name : nil
else
@phase.finish_gate? ? @phase.finish_gate_name : nil
end
end

def phase_gate_date
date = case @gate
when :start
@phase.start_date
else
@phase.finish_date
end

helpers.format_date(date)
end
end
end
end
6 changes: 6 additions & 0 deletions app/components/projects/phases/hover_card_component.sass
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.op-hover-card:has(.op-phase-gate-hover-card)
max-width: 380px

.op-phase-gate-hover-card
&--name
@include text-shortener()
41 changes: 41 additions & 0 deletions app/components/projects/phases/shared.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# 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.
#++

module Projects
module Phases
module Shared
def gate_icon = :"op-gate"

def icon_color_class
helpers.hl_inline_class("project_phase_definition", phase.definition_id)
end
end
end
end
71 changes: 71 additions & 0 deletions app/controllers/project_phases/hover_card_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# 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.
#++

module ProjectPhases
class HoverCardController < ApplicationController
before_action :authorize
before_action :check_feature_flag
before_action :assign_gate
before_action :find_phase
before_action :check_access

layout false

def show; end

private

def check_feature_flag
return if OpenProject::FeatureDecisions.stages_and_gates_active?

render json: { error: "Not found" }, status: :not_found
end

def check_access
return if User.current.allowed_in_project?(:view_project_phases, @phase.project)

render json: { error: "Forbidden" }, status: :forbidden
end

def assign_gate
@gate = params[:gate]
return if @gate.in?(%w[start finish])

render json: { error: "Invalid gate parameter" }, status: :unprocessable_entity
end

def find_phase
@phase = Project::Phase.where(active: true).eager_load(:definition).find_by(id: params[:id])
return if @phase

render json: { error: "Invalid id parameter" }, status: :unprocessable_entity
end
end
end
3 changes: 3 additions & 0 deletions app/views/project_phases/hover_card/show.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<turbo-frame id="op-hover-card-body">
<%= render Projects::Phases::HoverCardComponent.new(phase: @phase, gate: @gate) %>
</turbo-frame>
4 changes: 3 additions & 1 deletion config/initializers/permissions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,9 @@
require: :member

map.permission :view_project_phases,
{},
{
"project_phases/hover_card": :show
},
permissible_on: :project,
dependencies: :view_project,
visible: -> { OpenProject::FeatureDecisions.stages_and_gates_active? }
Expand Down
6 changes: 6 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,12 @@
end
end

resources :project_phases, only: [] do
member do
get "/hover_card" => "project_phases/hover_card#show", as: "hover_card"
end
end

resources :admin, controller: :admin, only: :index do
collection do
get :plugins
Expand Down
15 changes: 13 additions & 2 deletions frontend/src/stimulus/controllers/hover-card-trigger.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ import { computePosition, flip, limitShift, shift } from '@floating-ui/dom';
export default class HoverCardTriggerController extends ApplicationController {
static targets = ['trigger', 'card'];

private readonly triggerTargets:HTMLElement[];

private mouseInModal = false;
private hoverTimeout:number|null = null;
private closeTimeout:number|null = null;
Expand Down Expand Up @@ -112,10 +114,19 @@ export default class HoverCardTriggerController extends ApplicationController {
e.preventDefault();
e.stopPropagation();

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const el = e.target as HTMLElement;
let el = e.target as HTMLElement;
if (!el) { return; }

// If the trigger contains other elements, one of them might have triggered the event. We want to only refer to
// the original trigger as this makes event and state handling easier. Find the correct target element:
if (!this.triggerTargets.some((trigger) => trigger === el)) {
// If the element is not a trigger itself, one of its parents must be. Find the correct one.
const trigger = el.closest('[data-hover-card-trigger-target="trigger"]') as HTMLElement;
if (!trigger) { return; }

el = trigger;
}

if (this.previousTarget && this.previousTarget === el) {
// Re-entering the trigger counts as hovering over the card:
this.mouseInModal = true;
Expand Down
1 change: 1 addition & 0 deletions lookbook/docs/components/hover-cards.md.erb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ The HoverCard always consists of two basic parts:

- WorkPackage preview when linking via `#ID`
- User preview on usernames and avatars
- Phase gate details on the phase gate icon

## Technical notes

Expand Down
Loading
Loading