Content
View differences
Updated by Alexander Coles 22 days ago
### Goal
Extract a reusable BorderBox-backed list component primitive into `OpenProject::Common::BorderBoxListComponent`, collapsing the previously proposed `OpPrimer` `OpPrimer::BorderBoxListComponent`, move generic header/action/menu chrome into that primitive, and `OpenProject::Common` layers into a single domain-aware component. Move the automatic collection wrapper (`WorkPackageCardListComponent`) into the `Backlogs` namespace as an internal component. keep OpenProject domain-specific item support outside `OpPrimer`.
This supersedes the three-layer design implementation is a static component extraction. Generic header actions, header menus, and item menus are in PR #23074. ~~A new branch off~~ `~~dev~~` ~~will be used.~~ scope as reusable list chrome. Sorting, reordering, drag-and-drop ordering APIs, and action-menu abstractions beyond these explicit slots are out of scope for this work package.
### Component API
`OpenProject::Common::BorderBoxListComponent` renders `Primer::Beta::BorderBox` directly with `OpPrimer::BorderBoxListComponent` should provide these generic slots:
* - `header` — structured
- `with_item`
- `with_empty_item`
- `footer`
The public collection slot should be called `items`, to distinguish the wrapper API from the underlying `Primer::Beta::BorderBox` row implementation. Callers are responsible for calling item slot methods in the order they want items rendered. The component should render through Primer BorderBox under the hood and forward relevant system arguments such as ids, list ids, padding, classes, data attributes, and ARIA attributes.
The generic header with `title:`, should support title, optional `count:`, `with_description`, `with_action_button` (plural), count, description, action buttons, and `with_menu` (kebab via `HasMenu` module)
* `with_item` — a menu slot. The generic item should render arbitrary content row, arbitrary block content, no and expose an optional menu sub-slot
* `with_work_package_item` — domain-specific row accepting `work_package:`, `project:`, `params:`, optional `component_klass:` (defaults to `BorderBoxListComponent::WorkPackageItem`). slot. The list injects `container:` and `current_user:` automatically.
* `with_empty_state` — rendered when items collection is empty. Accepts `title:`, `description:`, `icon:`. Always renders a `Primer::Beta::Blankslate`.
* `footer` — proper `Primer::Beta::BorderBox` footer
Constructor: `initialize(container:, current_user: User.current, **system_arguments)`
`container:` is required and derives box, list, and header DOM IDs via `dom_target`. Callers should pass structured references (model, symbol, or array) continue to render as a BorderBox row rather than pre-computed strings. a BorderBox footer, preserving the existing list-body DOM contract.
Items are polymorphic (`with_item` `OpPrimer::BorderBoxListComponent` must not expose work-package-specific slots or classes.
### OpenProject list specialization
Add `OpenProject::Common::BorderBoxListComponent` as the manual OpenProject-specialized layer around `OpPrimer::BorderBoxListComponent`.
It should compose an `OpPrimer::BorderBoxListComponent` instance, delegate the generic slot methods to it, and provide the generic list slots plus:
- `with_work_package_item`
`with_work_package_item` provides a work-package item bridge through a configurable item/card component. This keeps work-package support in the same collection). Callers control render order. No auto-iteration — this `OpenProject::Common`, while still allowing manual callers to interleave work-package items with custom items. This component never accepts must not accept `work_packages:` as a collection. and must not perform automatic collection iteration.
### WorkPackageItem base The work-package item component class
`BorderBoxListComponent::WorkPackageItem` is a base class nested under should be configurable once on the list component. It renders through a basic work-package card via `WorkPackageCardComponent` without module-specific behavior. `Backlogs::WorkPackageCardListItemComponent` inherits from it and adds drag-and-drop, story data, and backlogs-specific menu URLs. constructor-level `work_package_item_component:` default, with per-item `component_klass:` override support.
This component is also the future home for other explicit OpenProject-specialized item slots, such as meeting items, if those become useful.
### Backlogs Work package wrapper
`Backlogs::WorkPackageCardListComponent` (moved from `OpenProject::Common`) is `OpenProject::Common::WorkPackageCardListComponent` should remain the automatic collection-oriented wrapper. Internal to Backlogs, not yet ready for sharing.
It accepts `project:`, `container:`, `work_packages:`, `drag_and_drop:`, `params:`, `current_user:`. Hardcodes `Backlogs::WorkPackageCardListItemComponent` during auto-iteration. Delegates `with_header` (with fold-state defaults), `with_footer`, and `with_empty_state` should continue to support:
- `project:`
- `container:`
- `work_packages:`
- `drag_and_drop:`
- `item_component_klass:`
- `params:`
- `current_user:`
- delegated `header`
- delegated `footer`
- `empty_state`
When `work_packages:` are passed, `WorkPackageCardListComponent` should iterate the collection itself and add work-package items through `OpenProject::Common::BorderBoxListComponent`. When no work packages are rendered, it should render `empty_state` through the underlying `BorderBoxListComponent`. empty item slot.
`WorkPackageCardListComponent` should not expose manual item composition. Manual interleaving belongs to `OpenProject::Common::BorderBoxListComponent`.
### Backlogs adoption
`Backlogs::InboxComponent` uses should use `OpenProject::Common::BorderBoxListComponent` directly (manual composition for directly, because it interleaves a custom show-more interleaving). item between work-package items.
`Backlogs::SprintComponent` and `Backlogs::BucketComponent` use `Backlogs::WorkPackageCardListComponent` (automatic collection). should stay on `OpenProject::Common::WorkPackageCardListComponent`, because they render automatic work-package collections.
### Compatibility expectations
Existing backlogs rendering should keep the relevant DOM contract preserved: container/list/header IDs via `dom_target`, contract:
- container id from `dom_target(container)`
- list id from `dom_target(container, :list)`
- header id and collapsible linkage
- work package item IDs/classes/data ids, classes, data attributes, and card rendering
- empty item marker
- footer item styling
- drag-and-drop data, collapsible header linkage, empty state rendering. data on the box
### Tests
Focused Add or update focused component specs for `OpenProject::Common::BorderBoxListComponent` `OpPrimer::BorderBoxListComponent` covering:
- header, generic item, empty item, item menu, and footer rendering
- forwarding of system arguments to the underlying BorderBox
- header (title, count, description, action buttons, actions, menu, collapsible), generic items, work-package items (default and overridden `component_klass:`, injected `container:`/`current_user:`), count behavior
- empty state as Blankslate, footer, container-derived DOM IDs, system argument forwarding. item data merging
- no work-package references under `OpPrimer`
Focused Add focused component specs for `Backlogs::WorkPackageCardListComponent` `OpenProject::Common::BorderBoxListComponent` covering: auto-iteration,
- generic item pass-through through delegated slots
- constructor-level `work_package_item_component:` default
- per-item `component_klass:` override
- work-package item rendering through a supplied component class
- work-package item customization blocks
- no automatic collection iteration
Update/keep `WorkPackageCardListComponent` specs covering:
- automatic `work_packages:` iteration
- delegated header with fold-state, footer, and footer rendering
- `empty_state` rendering through the empty state, item slot
- container/list/header ids
- existing backlogs item data/classes and drag-and-drop data, DOM IDs. data
Updated Update backlogs caller component specs for InboxComponent, SprintComponent, BucketComponent, to ensure the inbox show-more item remains interleaved and WorkPackageCardListItemComponent inheritance chain. sprint/bucket DOM contracts remain stable.
Extract a reusable BorderBox-backed list component
This supersedes the three-layer design
### Component API
`OpenProject::Common::BorderBoxListComponent` renders `Primer::Beta::BorderBox` directly with
*
- `with_item`
- `with_empty_item`
- `footer`
The public collection slot should be called `items`, to distinguish the wrapper API from the underlying `Primer::Beta::BorderBox` row implementation. Callers are responsible for calling item slot methods in the order they want items rendered. The component should render through Primer BorderBox under the hood and forward relevant system arguments such as ids, list ids, padding, classes, data attributes, and ARIA attributes.
The generic
* `with_item` —
* `with_work_package_item` — domain-specific row accepting `work_package:`, `project:`, `params:`, optional `component_klass:` (defaults to `BorderBoxListComponent::WorkPackageItem`).
* `with_empty_state` — rendered when items collection is empty. Accepts `title:`, `description:`, `icon:`. Always renders a `Primer::Beta::Blankslate`.
* `footer` — proper `Primer::Beta::BorderBox` footer
Constructor: `initialize(container:, current_user: User.current, **system_arguments)`
`container:` is required and derives box, list, and header DOM IDs via `dom_target`. Callers should pass structured references (model, symbol, or array)
Items are polymorphic (`with_item`
### OpenProject list specialization
Add `OpenProject::Common::BorderBoxListComponent` as the manual OpenProject-specialized layer around `OpPrimer::BorderBoxListComponent`.
It should compose an `OpPrimer::BorderBoxListComponent` instance, delegate the generic slot methods to it,
-
`with_work_package_item` provides a work-package item bridge through a configurable item/card component. This keeps work-package support
### WorkPackageItem base
`BorderBoxListComponent::WorkPackageItem` is a base class nested under
`Backlogs::WorkPackageCardListComponent` (moved from `OpenProject::Common`) is
It accepts `project:`, `container:`, `work_packages:`, `drag_and_drop:`, `params:`, `current_user:`. Hardcodes `Backlogs::WorkPackageCardListItemComponent` during auto-iteration. Delegates `with_header` (with fold-state defaults), `with_footer`, and `with_empty_state`
- `project:`
- `container:`
- `work_packages:`
- `drag_and_drop:`
- `item_component_klass:`
- `params:`
- `current_user:`
- delegated `header`
- delegated `footer`
- `empty_state`
When `work_packages:` are passed, `WorkPackageCardListComponent` should iterate
`Backlogs::InboxComponent` uses
`Backlogs::SprintComponent` and `Backlogs::BucketComponent` use `Backlogs::WorkPackageCardListComponent` (automatic collection).
### Compatibility expectations
Existing
- container id from `dom_target(container)`
- list id from `dom_target(container, :list)`
- header id and collapsible linkage
-
- empty item marker
- footer item styling
-
### Tests
Focused
- header, generic item, empty item, item menu, and footer rendering
- forwarding of system arguments to the underlying BorderBox
-
-
- no work-package references under `OpPrimer`
Focused
- generic item pass-through through
- constructor-level `work_package_item_component:` default
- per-item `component_klass:` override
- work-package item rendering through a supplied component class
- work-package item customization blocks
- no automatic collection iteration
Update/keep `WorkPackageCardListComponent` specs covering:
- automatic `work_packages:` iteration
- delegated
- `empty_state` rendering through the
- container/list/header ids
- existing backlogs item data/classes and
Updated