mirror of
https://github.com/opf/openproject.git
synced 2026-06-13 19:20:00 +00:00
Extend the WorkPackageCardComponent with new optionss
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+25
@@ -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 %>
|
||||
+5
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user