Extend the WorkPackageCardComponent with new optionss

This commit is contained in:
Henriette Darge
2026-05-12 14:16:58 +02:00
parent 6a4030f337
commit 91de3310bd
14 changed files with 336 additions and 49 deletions
@@ -30,20 +30,44 @@ See COPYRIGHT and LICENSE files for more details.
<%= grid_layout(
"op-work-package-card",
tag: :article,
classes: { "op-work-package-card_with-metric": metric? }
classes: {
"op-work-package-card_with-drag-handle": show_drag_handle,
"op-work-package-card_with-footer": show_footer?
}
) do |grid| %>
<% grid.with_area(:info_line) do %>
<%# TODO(73089): allow callers to pass arguments through to InfoLineComponent (e.g. status presentation, variants). %>
<%= render(WorkPackages::InfoLineComponent.new(work_package:)) %>
<% end %>
<% if metric? %>
<% grid.with_area(:metric) do %>
<%= metric %>
<% if show_drag_handle %>
<% grid.with_area(:drag_handle) do %>
<%= render(Primer::Beta::Octicon.new(icon: :grabber, "aria-label": t(".drag_handle.label"))) %>
<% end %>
<% end %>
<% grid.with_area(:menu) do %>
<% grid.with_area(:info_line) do %>
<%= render(WorkPackages::InfoLineComponent.new(work_package:, status_scheme:)) %>
<% end %>
<% grid.with_area(:actions) do %>
<% if show_assignee && work_package.assigned_to %>
<%= render(
Users::AvatarComponent.new(
user: work_package.assigned_to, size: "mini", link: false, show_name: true,
name_classes: "op-work-package-card--assignee-name"
)
) %>
<% end %>
<% if metric? %>
<span class="op-work-package-card--metric"><%= metric %></span>
<% end %>
<% if show_priority && work_package.priority %>
<%= render(
Primer::Beta::Text.new(
tag: :span,
classes: "__hl_inline_priority_#{work_package.priority.id} __hl_inline__small_dot"
)
) { work_package.priority.name } %>
<% end %>
<% if menu? %>
<%= menu %>
<% else %>
@@ -58,6 +82,33 @@ See COPYRIGHT and LICENSE files for more details.
<% end %>
<% grid.with_area(:subject) do %>
<%= render(Primer::Beta::Text.new(font_weight: :semibold)) { work_package.subject } %>
<%= render(Primer::Beta::Link.new(href: work_package_path(work_package), font_weight: :semibold)) { work_package.subject } %>
<% end %>
<% if show_footer? %>
<% grid.with_area(:footer) do %>
<% flex_layout do |flex| %>
<% if show_parent_link && work_package.parent.present? %>
<% flex.with_row do %>
<%= render(
Primer::Beta::Link.new(
href: work_package_path(work_package.parent),
underline: false,
color: :default
)
) do %>
<%= render(Primer::Beta::Text.new(color: :subtle)) { "#{t('.parent')}: " } %>
<%= work_package.parent.subject %>
<% end %>
<% end %>
<% end %>
<% if bottom_line? %>
<% flex.with_row do %>
<%= bottom_line %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
@@ -43,17 +43,38 @@ module OpenProject
**system_arguments
)
}
renders_one :bottom_line, Primer::Content
attr_reader :work_package, :menu_src
attr_reader :work_package, :menu_src, :show_drag_handle, :show_assignee, :show_priority,
:show_parent_link, :status_scheme
# @param work_package [WorkPackage] the work package this card represents.
# @param menu_src [String, NilClass] optional lazy menu source. Prefer the
# `with_menu(src:)` slot for new call sites.
def initialize(work_package:, menu_src: nil)
# @param show_drag_handle [Boolean] whether to show a drag handle icon.
# @param show_assignee [Boolean] whether to show the assignee (icon + name when space allows).
# @param show_priority [Boolean] whether to show the priority badge.
# @param show_parent_link [Boolean] whether to show a link to the parent work package in row 3.
# Only rendered when the work package actually has a parent.
# @param status_scheme [Symbol] status label scheme for the info line. One of :default or :secondary.
def initialize(work_package:, menu_src: nil, show_drag_handle: false,
show_assignee: false, show_priority: false, show_parent_link: false,
status_scheme: :default)
super()
@work_package = work_package
@menu_src = menu_src
@show_drag_handle = show_drag_handle
@show_assignee = show_assignee
@show_priority = show_priority
@show_parent_link = show_parent_link
@status_scheme = status_scheme
end
private
def show_footer?
bottom_line? || (show_parent_link && work_package.parent.present?)
end
end
end
@@ -30,24 +30,61 @@
display: grid
grid-template-columns: 1fr auto
grid-template-rows: auto auto
grid-template-areas: "info_line menu" "subject subject"
grid-template-areas: "info_line actions" "subject subject"
align-items: center
margin-top: calc(-1 * var(--base-size-4))
margin-bottom: var(--base-size-4)
container-type: inline-size
.op-work-package-card_with-metric
grid-template-columns: 1fr minmax(2rem, max-content) auto
grid-template-areas: "info_line metric menu" "subject subject subject"
&_with-footer
grid-template-rows: auto auto auto
grid-template-areas: "info_line actions" "subject subject" "footer footer"
.op-work-package-card--metric
margin-left: var(--stack-gap-normal)
font-variant-numeric: tabular-nums
text-align: right
&_with-drag-handle
grid-template-columns: auto 1fr auto
grid-template-areas: "drag_handle info_line actions" ". subject subject"
.op-work-package-card--menu
margin-left: var(--stack-gap-normal)
&.op-work-package-card_with-footer
grid-template-rows: auto auto auto
grid-template-areas: "drag_handle info_line actions" ". subject subject" ". footer footer"
.op-work-package-card--subject
align-self: start // Align to top of second row
word-wrap: break-word
overflow-wrap: break-word
&--drag_handle
align-self: center
padding-right: var(--stack-gap-condensed)
cursor: grab
color: var(--fgColor-muted)
&--actions
display: flex
align-items: center
gap: var(--stack-gap-normal)
color: var(--fgColor-muted)
&--metric
font-variant-numeric: tabular-nums
text-align: right
// parent_link and bottom_line auto-place into implicit rows, spanning full width
&--parent_link,
&--bottom_line
grid-column: 1 / -1
padding-top: var(--base-size-4)
&_with-drag-handle
.op-work-package-card--parent_link,
.op-work-package-card--bottom_line
grid-column: 2 / -1
&--subject
align-self: start
word-wrap: break-word
overflow-wrap: break-word
// < 768px: hide text labels, show only icons
@container (width < 768px)
.op-work-package-card--assignee-name
display: none
.op-work-package-card--actions .__hl_inline__small_dot
font-size: 0
@@ -32,6 +32,7 @@ See COPYRIGHT and LICENSE files for more details.
scheme: :invisible,
icon: :"kebab-horizontal",
"aria-label": button_aria_label || t(".label_actions"),
tooltip_direction: :se
tooltip_direction: :se,
size: :small
) %>
<% end %>
@@ -21,7 +21,7 @@
if @show_status
flex.with_column(ml: 2) do
render WorkPackages::StatusBadgeComponent.new(status: @work_package.status)
render WorkPackages::StatusBadgeComponent.new(status: @work_package.status, scheme: @status_scheme)
end
end
@@ -35,6 +35,7 @@ class WorkPackages::InfoLineComponent < ApplicationComponent
show_project: false,
show_subject: false,
show_status: true,
status_scheme: :default,
font_size: :small,
**system_arguments)
super
@@ -44,6 +45,7 @@ class WorkPackages::InfoLineComponent < ApplicationComponent
@show_project = show_project
@show_subject = show_subject
@show_status = show_status
@status_scheme = status_scheme
@system_arguments = system_arguments
end
@@ -35,6 +35,12 @@ class WorkPackages::StatusBadgeComponent < ApplicationComponent
super
@status = status
@system_arguments = system_arguments.merge({ classes: "__hl_background_status_#{@status.id}" })
@system_arguments = system_arguments
unless @system_arguments[:scheme]
@system_arguments[:classes] = class_names(
@system_arguments[:classes],
"__hl_background_status_#{@status.id}"
)
end
end
end
+3
View File
@@ -4902,8 +4902,11 @@ en:
open_project:
common:
work_package_card_component:
drag_handle:
label: "Drag to reorder"
menu:
label_actions: "Work package actions"
parent: "Parent"
work_package_card_list_component:
header:
label_actions: "Open menu"
@@ -110,6 +110,11 @@
width: 10px
height: 10px
&.__hl_inline__small_dot:before
width: 6px
height: 6px
vertical-align: 1px
@mixin dot_border_width_style
[class^='__hl_inline_'],
[class*=' __hl_inline_']
@@ -0,0 +1,62 @@
The `WorkPackageCard` is a compact representation of a work package, designed for use in list and board views such as sprint and backlog views.
## Overview
<%= embed OpenProject::Common::WorkPackageCardComponentPreview, :default %>
## Anatomy
The card is structured in up to three rows:
**Row 1 — Metadata**
Contains the info line (type, ID, status), followed by optional elements: assignee, metric (e.g. story points), priority, and the actions menu. When `show_drag_handle: true`, a drag handle icon appears on the far left, spanning all rows, aligned to the top (row 1).
**Row 2 — Subject**
The work package title, always visible.
**Row 3 — Footer** *(only rendered when content is present)*
Shows a link to the parent work package (when `show_parent_link: true` and a parent exists) and/or the `with_bottom_line` slot.
## Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| `work_package` | `WorkPackage` | required | The work package to display |
| `menu_src` | `String` | `nil` | Optional lazy-load URL for the actions menu |
| `show_assignee` | `Boolean` | `false` | Show the assignee icon and name |
| `show_priority` | `Boolean` | `false` | Show a priority badge |
| `show_drag_handle` | `Boolean` | `false` | Show a drag handle icon |
| `show_parent_link` | `Boolean` | `false` | Show a link to the parent work package in row 3. Only rendered when `work_package.parent` is present. |
| `status_scheme` | `Symbol` | `:default` | Status label style: `:default` (highlighted) or `:secondary` (neutral label) |
## Slots
| Slot | Description |
|---|---|
| `with_metric` | Numeric value shown in row 1 (e.g. story points) |
| `with_menu` | Custom actions menu, replaces the default lazy menu |
| `with_bottom_line` | Additional content appended to row 3 |
## Variants
<%= embed OpenProject::Common::WorkPackageCardComponentPreview, :playground, panels: %i[params source] %>
## Code structure
```ruby
render OpenProject::Common::WorkPackageCardComponent.new(
work_package: @work_package,
show_assignee: true,
show_priority: true,
show_parent_link: true,
status_scheme: :secondary
) do |card|
card.with_metric { @work_package.story_points }
card.with_menu do |menu|
menu.with_item(label: "Open", href: work_package_path(@work_package))
end
card.with_bottom_line do
# Whatever else you want to show
end
end
```
@@ -32,37 +32,99 @@ module OpenProject
module Common
# @logical_path OpenProject/Common
class WorkPackageCardComponentPreview < ViewComponent::Preview
# See the [component documentation](/lookbook/pages/components/work_packages/card) for more details.
#
# @param show_assignee toggle
# @param show_priority toggle
# @param show_drag_handle toggle
# @param show_parent_link toggle
# @param show_metric toggle
# @param show_menu toggle
# @param show_bottom toggle
# @param status_scheme select [default, secondary]
def playground(show_assignee: false, show_priority: false, show_drag_handle: false,
show_parent_link: false, show_metric: false, show_menu: false,
show_bottom: false, status_scheme: :default)
work_package = WorkPackage.visible.where.not(parent_id: nil).first || WorkPackage.visible.first
return preview_message("No work packages in the database.") unless work_package
render_with_template(template: "open_project/common/work_package_card_component_preview/playground",
locals: {
work_package:,
show_assignee:,
show_priority:,
show_drag_handle:,
show_parent_link:,
show_metric:,
show_menu:,
show_bottom:,
status_scheme:
})
end
# Minimal card showing only the info line, subject and actions menu.
def default
work_package = WorkPackage.first
work_package = WorkPackage.visible.first
return preview_message("No work packages in the database.") unless work_package
render OpenProject::Common::WorkPackageCardComponent.new(work_package:)
end
# Card with a numeric metric (e.g. story points) in the top-right area.
def with_metric
work_package = WorkPackage.visible.first
return preview_message("No work packages in the database.") unless work_package
render OpenProject::Common::WorkPackageCardComponent.new(work_package:) do |card|
card.with_metric { (work_package.try(:story_points) || 8).to_s }
end
end
# Card with a custom actions menu.
def with_menu
work_package = WorkPackage.visible.first
return preview_message("No work packages in the database.") unless work_package
render OpenProject::Common::WorkPackageCardComponent.new(work_package:) do |card|
card.with_menu do |menu|
menu.with_item(label: "Open", href: "/work_packages/#{work_package.id}")
menu.with_item(label: "Edit", href: "/work_packages/#{work_package.id}/edit")
menu.with_divider
menu.with_item(label: "Delete", scheme: :danger)
end
end
end
# Card with a drag handle icon for reorderable lists.
def with_drag_handle
work_package = WorkPackage.visible.first
return preview_message("No work packages in the database.") unless work_package
render OpenProject::Common::WorkPackageCardComponent.new(
work_package:
work_package:,
show_drag_handle: true
)
end
def with_metric
work_package = WorkPackage.first
return preview_message("No work packages in the database.") unless work_package
# Card with show_parent_link enabled. Renders a link to the parent work package in row 3.
# Only visible when the work package actually has a parent.
def with_parent_link
work_package = WorkPackage.visible.where.not(parent_id: nil).first
return preview_message("No work packages with a parent found.") unless work_package
render OpenProject::Common::WorkPackageCardComponent.new(
work_package:
) do |card|
card.with_metric_content(10)
end
work_package:,
show_parent_link: true
)
end
def with_menu
work_package = WorkPackage.first
# Card with additional content in the bottom slot (row 3), rendered alongside the parent link.
def with_bottom_line
work_package = WorkPackage.visible.first
return preview_message("No work packages in the database.") unless work_package
render OpenProject::Common::WorkPackageCardComponent.new(
work_package:
) do |card|
card.with_menu do |menu|
menu.with_item(label: "Open", href: "/work_packages/#{work_package.id}")
end
end
render_with_template(template: "open_project/common/work_package_card_component_preview/with_bottom_line",
locals: { work_package: })
end
private
@@ -0,0 +1,25 @@
<%= render OpenProject::Common::WorkPackageCardComponent.new(
work_package:,
show_assignee:,
show_priority:,
show_drag_handle:,
show_parent_link:,
status_scheme: status_scheme.to_sym
) do |card|
card.with_metric { (work_package.try(:story_points) || 5).to_s } if show_metric
if show_menu
card.with_menu do |menu|
menu.with_item(label: "Open", href: "/work_packages/#{work_package.id}")
menu.with_item(label: "Edit", href: "/work_packages/#{work_package.id}/edit")
menu.with_divider
menu.with_item(label: "Delete", scheme: :danger)
end
end
if show_bottom
card.with_bottom_line do
render(Primer::Beta::Text.new(tag: :span, font_size: :small, color: :subtle)) { "Some bottom line" }
end
end
end %>
@@ -0,0 +1,5 @@
<%= render OpenProject::Common::WorkPackageCardComponent.new(work_package:) do |card|
card.with_bottom_line do
render(Primer::Beta::Text.new(tag: :span, font_size: :small, color: :subtle)) { "Some bottom line" }
end
end %>
@@ -54,7 +54,14 @@ module Backlogs
private
def card
@card ||= OpenProject::Common::WorkPackageCardComponent.new(work_package:, menu_src:)
@card ||= OpenProject::Common::WorkPackageCardComponent.new(
work_package:,
menu_src:,
show_assignee: true,
show_priority: true,
show_parent_link: true,
status_scheme: :secondary
)
end
def before_render