Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,43 @@ 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: layout_classes
) do |grid| %>
<% if show_drag_handle %>
<% grid.with_area(:drag_handle, classes: "hide-when-print") do %>
<%= render(Primer::OpenProject::DragHandle.new) %>
<% end %>
<% end %>

<% 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:)) %>
<%= render(WorkPackages::InfoLineComponent.new(work_package:, status_scheme:)) %>
<% end %>

<% if metric? %>
<% grid.with_area(:metric) do %>
<%= metric %>
<% 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"
)
) do %>
<span class="op-work-package-card--priority-name"><%= work_package.priority.name %></span>
<% end %>
<% end %>
<% end %>

<% grid.with_area(:menu) do %>
Comment thread
HDinger marked this conversation as resolved.
<% if menu? %>
<%= menu %>
<% else %>
Expand All @@ -58,6 +81,39 @@ 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 } %>
<% if link_subject %>
<%= render(Primer::Beta::Link.new(href: work_package_path(work_package), font_weight: :semibold)) { work_package.subject } %>
<% else %>
<%= render(Primer::Beta::Text.new(font_weight: :semibold)) { work_package.subject } %>
<% end %>
<% end %>

<% if show_footer? %>
<% grid.with_area(:footer) do %>
<% flex_layout do |flex| %>
<% if show_parent? %>
<% flex.with_row(classes: "ellipsis") do %>
<%= render(Primer::Beta::Text.new(color: :muted, font_size: :small)) do %>
<%= "#{t('.parent')}: " %>
<%= render(
Primer::Beta::Link.new(
href: work_package_path(work_package.parent),
underline: false,
color: :default
)
) do %>
<%= work_package.parent.subject %>
<% end %>
<% end %>
<% end %>
<% end %>

<% if additional_details? %>
<% flex.with_row do %>
<%= additional_details %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
41 changes: 39 additions & 2 deletions app/components/open_project/common/work_package_card_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,54 @@ class WorkPackageCardComponent < ApplicationComponent
**system_arguments
)
}
renders_one :additional_details, Primer::Content

attr_reader :work_package, :menu_src
attr_reader :work_package, :menu_src, :show_drag_handle, :show_assignee, :show_priority,
:show_parent, :link_subject, :status_scheme

alias_method :show_drag_handle?, :show_drag_handle

# @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 [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 link_subject [Boolean] whether to link the subject to the WP or render as plain text instead.
# @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: false, link_subject: true,
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 = show_parent
@link_subject = link_subject
@status_scheme = status_scheme
end

private

def layout_classes
{
"op-work-package-card_with-drag-handle": show_drag_handle?,
"op-work-package-card_with-footer": show_footer?,
"op-work-package-card_with-menu": menu? || menu_src.present?
}
end

def show_parent?
show_parent && work_package.parent&.visible?
end

def show_footer?
additional_details? || show_parent?
end
end
end
Expand Down
80 changes: 65 additions & 15 deletions app/components/open_project/common/work_package_card_component.sass
Original file line number Diff line number Diff line change
Expand Up @@ -30,24 +30,74 @@
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))
grid-row-gap: var(--base-size-4)
margin-top: 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
// --------- START: Menu positioning ---------
// This is a hack to avoid that the IconButton of the menu blows up the height of the first row
// resulting in different spacing between the rows.
// By positioning the menu absolutely, we can ensure that the height of the first row is not affected.
&_with-menu
.op-work-package-card--actions
position: relative
// 1rem column gap + 2rem button width - 0.5rem visual gap of the invisible button to it's icon
padding-right: 2.5rem

&--menu
position: absolute
right: 0
// --------- END: Menu positioning ---------

&--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 additional_details auto-place into implicit rows, spanning full width
&--parent_link,
&--additional_details
grid-column: 1 / -1
padding-top: var(--base-size-4)

&_with-drag-handle
.op-work-package-card--parent_link,
.op-work-package-card--additional_details
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,
.op-work-package-card--priority-name
display: none
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def initialize(work_package:, src: nil, button_aria_label: nil, **system_argumen
system_arguments[:anchor_align] ||= :end
system_arguments[:classes] = class_names(
system_arguments[:classes],
"op-work-package-card--menu",
"hide-when-print"
)
@menu = Primer::Alpha::ActionMenu.new(**system_arguments)
Expand Down
2 changes: 1 addition & 1 deletion app/components/work_packages/info_line_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment thread
HDinger marked this conversation as resolved.
end
end

Expand Down
2 changes: 2 additions & 0 deletions app/components/work_packages/info_line_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def initialize(work_package:,
show_project: false,
show_subject: false,
show_status: true,
status_scheme: :default,
Comment thread
HDinger marked this conversation as resolved.
font_size: :small,
**system_arguments)
super
Expand All @@ -44,6 +45,7 @@ def initialize(work_package:,
@show_project = show_project
@show_subject = show_subject
@show_status = show_status
@status_scheme = status_scheme

@system_arguments = system_arguments
end
Expand Down
9 changes: 8 additions & 1 deletion app/components/work_packages/status_badge_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ def initialize(status:, **system_arguments)
super

@status = status
@system_arguments = system_arguments.merge({ classes: "__hl_background_status_#{@status.id}" })
@system_arguments = system_arguments
if @system_arguments[:scheme].nil? || @system_arguments[:scheme] == :default
@system_arguments.delete(:scheme)
@system_arguments[:classes] = class_names(
@system_arguments[:classes],
"__hl_background_status_#{@status.id}"
)
end
end
end
3 changes: 3 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4936,8 +4936,11 @@ en:
open_project:
common:
work_package_card_component:
drag_handle:
label: "Drag to reorder"
menu:
label_actions: "Work package actions"
parent: "Parent"

permission_add_work_package_comments: "Add comments"
permission_add_work_packages: "Add work packages"
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/global_styles/layout/_colors.sass
Original file line number Diff line number Diff line change
Expand Up @@ -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_']
Expand Down
62 changes: 62 additions & 0 deletions lookbook/docs/components/work-packages/card.md.erb
Original file line number Diff line number Diff line change
@@ -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: true` and a parent exists) and/or the `with_additional_details` 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` | `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_additional_details` | 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: 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_additional_details do
# Whatever else you want to show
end
end
```
Loading
Loading