[#74684] Extract BorderBoxListComponent#23074
Conversation
There was a problem hiding this comment.
Pull request overview
This PR completes a rename/refactor of the work package “card box” UI into a WorkPackageCardListComponent, updates Backlogs to use the renamed component/item class, and introduces a reusable OpPrimer::BorderBoxListComponent wrapper to standardize BorderBox list rendering (header/rows/footer/empty rows).
Changes:
- Rename
OpenProject::Common::WorkPackageCardBoxComponenttoOpenProject::Common::WorkPackageCardListComponent(including nestedHeader/Item) and update associated styles and I18n keys. - Add
OpPrimer::BorderBoxListComponent(with content/empty/work-package row bridges) and wire the work package card list to render via this component. - Update Backlogs (bucket/sprint/inbox) rendering and specs to use the new list + item components and updated accessibility labels.
Reviewed changes
Copilot reviewed 27 out of 27 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| spec/components/open_project/common/work_package_card_list_component/item_spec.rb | Updates spec namespace for the renamed Item component and related test doubles. |
| spec/components/open_project/common/work_package_card_list_component/header_spec.rb | Updates spec namespace for the renamed Header component. |
| spec/components/open_project/common/work_package_card_list_component_spec.rb | Updates spec namespace for the renamed list component and removes coverage for removed manual-slot behavior. |
| spec/components/op_primer/border_box_list_component_spec.rb | Adds unit coverage for the new OpPrimer::BorderBoxListComponent behavior. |
| modules/backlogs/spec/support/pages/backlog.rb | Updates I18n key for counter accessible label; minor Capybara matcher adjustment. |
| modules/backlogs/spec/components/backlogs/work_package_card_list_item_component_spec.rb | Updates spec to the renamed Backlogs item component. |
| modules/backlogs/spec/components/backlogs/bucket_component_spec.rb | Updates I18n key used for the counter aria-label. |
| modules/backlogs/app/components/backlogs/work_package_card_list_item_component.rb | Renames Backlogs work package item component to inherit from the renamed common list item. |
| modules/backlogs/app/components/backlogs/sprint_component.html.erb | Switches sprint rendering to WorkPackageCardListComponent and renamed Backlogs item component. |
| modules/backlogs/app/components/backlogs/inbox_component.html.erb | Reworks inbox rendering to use OpPrimer::BorderBoxListComponent directly and updates CSS class name. |
| modules/backlogs/app/components/backlogs/bucket_component.html.erb | Switches bucket rendering to WorkPackageCardListComponent and renamed Backlogs item component. |
| lookbook/previews/open_project/common/work_package_card_list_component_preview.rb | Renames preview class and updates it to render the renamed list component; removes manual-item preview. |
| lookbook/previews/op_primer/border_box_list_component_preview.rb | Adds Lookbook preview coverage for the new BorderBoxListComponent. |
| config/locales/en.yml | Renames translation namespace from work_package_card_box_component to work_package_card_list_component. |
| app/components/open_project/common/work_package_card_list_component/item.rb | Renames outer component constant for the nested Item and keeps item row bridging behavior. |
| app/components/open_project/common/work_package_card_list_component/header.sass | Renames header CSS class from ...card-box... to ...card-list.... |
| app/components/open_project/common/work_package_card_list_component/header.rb | Renames outer component constant for the nested Header. |
| app/components/open_project/common/work_package_card_list_component/header.html.erb | Updates header wrapper class to the renamed CSS class. |
| app/components/open_project/common/work_package_card_list_component.sass | Renames base CSS class from ...card-box... to ...card-list.... |
| app/components/open_project/common/work_package_card_list_component.rb | Renames the component class and simplifies rendering/validation to “work_packages-driven” list rendering. |
| app/components/open_project/common/work_package_card_list_component.html.erb | Adds a dedicated template that renders via OpPrimer::BorderBoxListComponent. |
| app/components/op_primer/border_box_list_component/work_package_item.rb | Replaces incorrect/spec-like content with the actual WorkPackageItem bridge implementation. |
| app/components/op_primer/border_box_list_component/empty_item.rb | Moves/implements EmptyItem bridge under OpPrimer::BorderBoxListComponent. |
| app/components/op_primer/border_box_list_component/content_item.rb | Moves/implements ContentItem bridge under OpPrimer::BorderBoxListComponent. |
| app/components/op_primer/border_box_list_component.rb | Introduces the new BorderBox list wrapper component API. |
| app/components/op_primer/border_box_list_component.html.erb | Updates rendering to use rows and render header/footer via stored bridge objects. |
| app/components/_index.sass | Updates Sass imports to the renamed work package card list component styles. |
Comments suppressed due to low confidence (2)
app/components/open_project/common/work_package_card_list_component/item.rb:36
- The class-level comment still refers to a “card box”, but the constant is now
WorkPackageCardListComponent. Please update the wording (and any similar occurrences) to avoid confusion for future readers and to reflect the renamed component accurately.
app/components/open_project/common/work_package_card_list_component.rb:140 - The surrounding documentation for the
empty_stateslot still talks about “items” / “automatic item builds”, but the component now renders rows directly fromwork_packages(and validates based onwork_packages.empty?). Please update the comment to match the current behavior so it stays accurate.
EinLama
left a comment
There was a problem hiding this comment.
This change makes sense to me 👍
It would be good to wait for the next merge of the release branch and apply the removal of the mirrorContainer here, too.
20fd06f to
51ebb2f
Compare
WorkPackageCardListComponent, Extract BorderBoxListComponent
3005f6e to
1a753d1
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 37 out of 37 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
config/locales/en.yml:4913
- Renaming the I18n scope from
open_project.common.work_package_card_box_componentto...work_package_card_list_componentupdates onlyen.yml. All Crowdin locale files still definework_package_card_box_componentand have nowork_package_card_list_component, so non-English locales will start showing missing translations (or fallback to English) for the header count/labels. Consider keeping the old key as an alias (temporarily) and/or updating the Crowdin locale keys as part of this change.
work_package_card_list_component:
header:
label_actions: "Open menu"
label_work_package_count:
zero: "No work packages"
one: "%{count} work package"
other: "%{count} work packages"
1a753d1 to
a280bbb
Compare
WorkPackageCardListComponent, Extract BorderBoxListComponentBorderBoxListComponent
a280bbb to
f25a281
Compare
e46b196 to
39b5174
Compare
78c15af to
55f3271
Compare
55f3271 to
01097e0
Compare
01097e0 to
737bafc
Compare
Introduces a shared BorderBox-backed list component and moves the Backlogs-specific work package card list into the Backlogs module. Wires Backlogs callers to the new component API, keeps specialized card rendering behind an item factory hook, and replaces old OpPrimer work-package card list coverage with focused component specs. https://community.openproject.org/wp/74684
737bafc to
8f2cdab
Compare
Replaces `data: { test_selector: ... }` with the `test_selector:`
system argument in Backlogs component templates that instantiate
`BorderBoxListComponent`.
Adds a `count_label:` keyword to `Header` that defaults to `I18n.t(:label_x_items, count:)` and always sets `aria-live: polite` on the counter badge. Callers no longer need to wire up aria attributes manually. Uses `merge_aria` so additional caller-provided aria attributes are preserved. https://community.openproject.org/wp/74684
Adds `DEFAULT_ACTION_SCHEME` (`:default`) to `Header` so callers no longer need to pass `scheme:` for standard header buttons. Updates Lookbook preview and Backlogs sprint component to use the new default. https://community.openproject.org/wp/72945
| @icon = icon | ||
|
|
||
| @system_arguments = system_arguments | ||
| @system_arguments[:role] = "status" |
There was a problem hiding this comment.
Should every empty state be rendered as role="status" with aria-live="polite" by default?
It might be more announcement-heavy than needed. Maybe this should be configurable, with the current behavior as an option?
| merged = DEFAULT_COUNT_ARGUMENTS.merge(count_arguments).merge(count:) | ||
| merged[:aria] = merge_aria( | ||
| merged, | ||
| { aria: { label: count_label, live: "polite" } } |
There was a problem hiding this comment.
Should the counter always get aria-live="polite" by default?
For static initial rendering this may be unnecessary, and in Backlogs we may render several counters at once. Would it be safer to make the live behavior opt-in, or only apply it where the counter is expected to update dynamically?
There was a problem hiding this comment.
Is this new file imported into the compiled frontend styles. Could we import it?
| @system_arguments = system_arguments | ||
|
|
||
| @system_arguments[:id] ||= dom_target(container) | ||
| @system_arguments[:list_id] = dom_target(@system_arguments[:id], :list) |
There was a problem hiding this comment.
@system_arguments[:list_id] = dom_target(...) unconditionally overwrites any caller-provided list_id:. The spec ("derives the list id from the explicit box id") even asserts that an explicit list_id: is ignored. Same for footer id: at line 168 — the Footer stores the original in attr_reader :id but the rendered id is always the derived one. If these aren't meant to be caller-controlled, consider filtering them out or documenting they're reserved, rather than silently discarding them.
There was a problem hiding this comment.
deny_single_argument would make this easy to implement, but we would have to inherit from Primer::Component instead.
| include Primer::AttributesHelper | ||
| include HasMenu | ||
|
|
||
| DEFAULT_ACTION_SCHEME = :default |
There was a problem hiding this comment.
The old WorkPackageCardListComponent header never set a default button scheme, and sprint_component.rb explicitly passed scheme: :invisible on the start/finish sprint buttons. But here we remove those scheme: :invisible overrides, so the sprint buttons now render with :default scheme instead of :invisible. This is a user-visible UI change. Is it intentional? If so, worth calling out in the PR description.
| # @return [void] | ||
| def resolve_count!(item_count) | ||
| @count = item_count if count == true | ||
| @count_label ||= I18n.t(:label_x_items, count: @count) if render_count? |
There was a problem hiding this comment.
The default label is intentionally generic ("5 items"). The Backlogs wrapper compensates by passing :label_x_work_packages. A short inline comment noting "callers should supply count_label: when a domain-specific label is needed" would help future consumers of this generic component avoid accidentally shipping "5 items" when they meant "5 work packages" (or similar).
There was a problem hiding this comment.
@bsatarnejad count_label is documented on Header#initialize. Is this not enough?
# @param count_label [String, nil] accessible label for the counter
# badge. Defaults to `I18n.t(:label_x_items, count:)` when a count
# is rendered. Pass an explicit string to override.| <% end %> | ||
|
|
||
| <% if menu? %> | ||
| <% grid.with_area(:menu) do %> |
There was a problem hiding this comment.
The old header template always rendered the menu grid area. This now conditionally renders it with if menu? This is cleaner, but worth confirming no CSS or JS relied on the .op-border-box-list-header--menu area always being present in the grid layout.
| def call | ||
| blankslate = Primer::Beta::Blankslate.new(**@system_arguments) | ||
| blankslate.with_heading(tag: :h4).with_content(@title) | ||
| blankslate.with_description { @description } if @description |
There was a problem hiding this comment.
with_description { @description } works but the old code used with_description_content(description), which is the Primer documented API for passing a plain string. The block form is fine but slightly inconsistent with the chaining style used for with_heading two lines above.
Ticket
https://community.openproject.org/wp/74684
What are you trying to accomplish?
Extracts a reusable BorderBox-backed list component out of
WorkPackageCardListComponent(renamed in #23146), splitting the responsibility across two layers:OpenProject::Common::BorderBoxListComponent— A domain-aware list that rendersPrimer::Beta::BorderBoxdirectly. Providesheader,with_item,with_work_package_item,with_empty_state, andfooterslots. Owns container-derived DOM IDs. Supports manual item composition (callers control render order).Backlogs::WorkPackageCardListComponent— An internal auto-collection wrapper. Acceptswork_packages:, iterates the collection, handles empty-state and drag-and-drop. Moved toBacklogssince it's not yet ready for sharing with other teams.Backlogs
InboxComponentusesBorderBoxListComponentdirectly (manual composition for show-more interleaving).SprintComponentandBucketComponentuse the Backlogs wrapper.What approach did you choose and why?
BorderBoxListComponentrendersPrimer::Beta::BorderBoxdirectly with polymorphic item slots — generic content rows and work-package rows in the same collection.WorkPackageItemis a base class nested underBorderBoxListComponentthat Backlogs subclasses for drag-and-drop and story-specific behavior.Also switches Backlogs templates from
data: { test_selector: ... }to thetest_selector:Primer system argument.Merge checklist