-
Notifications
You must be signed in to change notification settings - Fork 3.2k
[#74684] Extract BorderBoxListComponent
#23074
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
8f2cdab
c494c3b
08f3fec
e63c07f
20a4783
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,196 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| #-- copyright | ||
| # OpenProject is an open source project management software. | ||
| # Copyright (C) the OpenProject GmbH | ||
| # | ||
| # This program is free software; you can redistribute it and/or | ||
| # modify it under the terms of the GNU General Public License version 3. | ||
| # | ||
| # OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: | ||
| # Copyright (C) 2006-2013 Jean-Philippe Lang | ||
| # Copyright (C) 2010-2013 the ChiliProject Team | ||
| # | ||
| # This program is free software; you can redistribute it and/or | ||
| # modify it under the terms of the GNU General Public License | ||
| # as published by the Free Software Foundation; either version 2 | ||
| # of the License, or (at your option) any later version. | ||
| # | ||
| # This program is distributed in the hope that it will be useful, | ||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| # GNU General Public License for more details. | ||
| # | ||
| # You should have received a copy of the GNU General Public License | ||
| # along with this program; if not, write to the Free Software | ||
| # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||
| # | ||
| # See COPYRIGHT and LICENSE files for more details. | ||
| #++ | ||
|
|
||
| module OpenProject | ||
| module Common | ||
| # A Primer BorderBox-backed list composition with optional header, items, | ||
| # empty state, and footer. | ||
| # | ||
| # Use this component for compact lists that need consistent OpenProject | ||
| # header actions, collapsible behavior, and row rendering. | ||
| class BorderBoxListComponent < ApplicationComponent | ||
| include OpPrimer::ComponentHelpers | ||
|
|
||
| attr_reader :container, :current_user, :header_id, :footer_id | ||
|
|
||
| # Optional header row. | ||
| # | ||
| # @!parse | ||
| # # Adds the optional header row. | ||
| # # | ||
| # # @param system_arguments [Hash] forwarded to {Header}. List wiring | ||
| # # arguments are supplied internally. | ||
| # # @return [ViewComponent::Slot] | ||
| # def with_header(**system_arguments, &block) | ||
| # end | ||
| renders_one :header, ->(**system_arguments) { | ||
| system_arguments[:id] = header_id | ||
| system_arguments[:list_id] = list_id | ||
|
|
||
| Header.new(**system_arguments) | ||
| } | ||
|
|
||
| # List row content. | ||
| # | ||
| # Use: | ||
| # | ||
| # - `item` for generic row content. | ||
| # - `work_package_item` for rows backed by a work package card. | ||
| # | ||
| # @!parse | ||
| # # Adds a generic list row. | ||
| # # | ||
| # # @param system_arguments [Hash] forwarded to Primer's BorderBox row. | ||
| # # @return [ViewComponent::Slot] | ||
| # def with_item(**system_arguments, &block) | ||
| # end | ||
| # | ||
| # # Adds a work-package list row. | ||
| # # | ||
| # # @param work_package [WorkPackage] work package rendered by the row. | ||
| # # @param project [Project] project context for the work package. | ||
| # # @param params [Hash] request params used by specialized item classes. | ||
| # # @param component_klass [Class] item component class to instantiate. | ||
| # # @param item_arguments [Hash] forwarded to the item component. | ||
| # # @return [ViewComponent::Slot] | ||
| # def with_work_package_item( | ||
| # work_package:, | ||
| # project: work_package.project, | ||
| # params: {}, | ||
| # component_klass: WorkPackageItem, | ||
| # **item_arguments, | ||
| # &block | ||
| # ) | ||
| # end | ||
| renders_many :items, types: { | ||
| item: { | ||
| renders: ->(**system_arguments) { | ||
| Item.new(**system_arguments) | ||
| }, | ||
| as: :item | ||
| }, | ||
| work_package_item: { | ||
| renders: ->( | ||
| work_package:, | ||
| project: work_package.project, | ||
| params: {}, | ||
| component_klass: WorkPackageItem, | ||
| **item_arguments | ||
| ) { | ||
| component_klass.new( | ||
| work_package:, | ||
| project:, | ||
| params:, | ||
| container:, | ||
| current_user:, | ||
| **item_arguments | ||
| ) | ||
| }, | ||
| as: :work_package_item | ||
| } | ||
| } | ||
|
|
||
| # Optional empty-state content rendered when no items are present. | ||
| # | ||
| # @!parse | ||
| # # Adds empty-state content. | ||
| # # | ||
| # # @param title [String] empty-state title. | ||
| # # @param description [String, nil] optional supporting text. | ||
| # # @param icon [Symbol, nil] optional Primer icon. | ||
| # # @param system_arguments [Hash] forwarded to `Primer::Beta::Blankslate`. | ||
| # # @return [ViewComponent::Slot] | ||
| # def with_empty_state(title:, description: nil, icon: nil, **system_arguments) | ||
| # end | ||
| renders_one :empty_state, ->(title:, description: nil, icon: nil, **system_arguments) { | ||
| EmptyState.new(title:, description:, icon:, **system_arguments) | ||
| } | ||
|
|
||
| # Optional footer row. | ||
| # | ||
| # @!parse | ||
| # # Adds an optional footer row. | ||
| # # | ||
| # # @param system_arguments [Hash] forwarded to Primer's BorderBox | ||
| # # footer. The `id` is generated internally for collapsible header | ||
| # # wiring. | ||
| # # @return [ViewComponent::Slot] | ||
| # def with_footer(**system_arguments, &block) | ||
| # end | ||
| renders_one :footer, ->(**system_arguments) { | ||
| system_arguments[:id] = footer_id | ||
|
|
||
| Footer.new(**system_arguments) | ||
| } | ||
|
|
||
| # @param container [String, Symbol, Class, Object] value passed to | ||
| # `dom_target` to derive DOM ids for the list and related controls. | ||
| # @param current_user [User] user context passed to work-package items. | ||
| # @param system_arguments [Hash] forwarded to `Primer::Beta::BorderBox`. | ||
| # Pass `id:` to set the box id; related ids are derived from it. | ||
| def initialize(container:, current_user: User.current, **system_arguments) | ||
| super() | ||
|
|
||
| @container = container | ||
| @current_user = current_user | ||
| @system_arguments = system_arguments | ||
|
|
||
| @system_arguments[:id] ||= dom_target(container) | ||
| @system_arguments[:list_id] = dom_target(@system_arguments[:id], :list) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| @header_id = dom_target(@system_arguments[:id], :header) | ||
| @footer_id = dom_target(@system_arguments[:id], :footer) | ||
| end | ||
|
|
||
| def before_render | ||
| content | ||
| configure_header! | ||
| end | ||
|
|
||
| def render? | ||
| header? || items.any? || empty_state? || footer? | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def configure_header! | ||
| return unless header? | ||
|
|
||
| header.resolve_count!(items.size) | ||
| return unless footer? | ||
|
|
||
| header.collapsible_id = [list_id, footer_id].compact.join(" ") | ||
| end | ||
|
|
||
| def list_id | ||
| @system_arguments[:list_id] | ||
| end | ||
| end | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| //-- copyright | ||
| // OpenProject is an open source project management software. | ||
| // Copyright (C) the OpenProject GmbH | ||
| // | ||
| // This program is free software; you can redistribute it and/or | ||
| // modify it under the terms of the GNU General Public License version 3. | ||
| // | ||
| // See COPYRIGHT and LICENSE files for more details. | ||
| //++ | ||
|
|
||
| .op-border-box-list-header | ||
| display: grid | ||
| grid-template-columns: 1fr minmax(5rem, max-content) auto | ||
| grid-template-areas: "collapsible actions menu" | ||
| align-items: center | ||
|
|
||
| &--actions, | ||
| &--menu | ||
| margin-left: var(--stack-gap-normal) | ||
| align-self: flex-start | ||
| // Unfortunately, the invisible button style bites us here again. | ||
| margin-top: -6px |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| #-- copyright | ||
| # OpenProject is an open source project management software. | ||
| # Copyright (C) the OpenProject GmbH | ||
| # | ||
| # This program is free software; you can redistribute it and/or | ||
| # modify it under the terms of the GNU General Public License version 3. | ||
| # | ||
| # OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: | ||
| # Copyright (C) 2006-2013 Jean-Philippe Lang | ||
| # Copyright (C) 2010-2013 the ChiliProject Team | ||
| # | ||
| # This program is free software; you can redistribute it and/or | ||
| # modify it under the terms of the GNU General Public License | ||
| # as published by the Free Software Foundation; either version 2 | ||
| # of the License, or (at your option) any later version. | ||
| # | ||
| # This program is distributed in the hope that it will be useful, | ||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| # GNU General Public License for more details. | ||
| # | ||
| # You should have received a copy of the GNU General Public License | ||
| # along with this program; if not, write to the Free Software | ||
| # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||
| # | ||
| # See COPYRIGHT and LICENSE files for more details. | ||
| #++ | ||
|
|
||
| module OpenProject | ||
| module Common | ||
| class BorderBoxListComponent | ||
| # Empty-state content rendered as a Primer Blankslate. | ||
| # | ||
| # This component is part of {BorderBoxListComponent} and should not be | ||
| # used as a standalone component. | ||
| # | ||
| # The component announces changes politely by default while preserving | ||
| # caller-provided aria attributes. | ||
| class EmptyState < ApplicationComponent | ||
| include Primer::AttributesHelper | ||
|
|
||
| # @param title [String] empty-state heading. | ||
| # @param description [String, nil] optional supporting text. | ||
| # @param icon [Symbol, nil] optional Primer icon. | ||
| # @param system_arguments [Hash] forwarded to `Primer::Beta::Blankslate`. | ||
| def initialize(title:, description: nil, icon: nil, **system_arguments) | ||
| super() | ||
|
|
||
| @title = title | ||
| @description = description | ||
| @icon = icon | ||
|
|
||
| @system_arguments = system_arguments | ||
| @system_arguments[:role] = "status" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should every empty state be rendered as It might be more announcement-heavy than needed. Maybe this should be configurable, with the current behavior as an option? |
||
| @system_arguments[:aria] = merge_aria( | ||
| system_arguments, | ||
| aria: { live: "polite" } | ||
| ) | ||
| end | ||
|
|
||
| def call | ||
| blankslate = Primer::Beta::Blankslate.new(**@system_arguments) | ||
| blankslate.with_heading(tag: :h4).with_content(@title) | ||
| blankslate.with_description { @description } if @description | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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. |
||
| blankslate.with_visual_icon(icon: @icon) if @icon | ||
|
|
||
| render(blankslate) | ||
| end | ||
| end | ||
| end | ||
| end | ||
| end | ||
Uh oh!
There was an error while loading. Please reload this page.