diff --git a/.changeset/spotty-sheep-dream.md b/.changeset/spotty-sheep-dream.md new file mode 100644 index 0000000000..cf345aeeaa --- /dev/null +++ b/.changeset/spotty-sheep-dream.md @@ -0,0 +1,5 @@ +--- +'@openproject/primer-view-components': minor +--- + +Support single select variant in FilterableTreeView diff --git a/app/components/primer/open_project/filterable_tree_view.rb b/app/components/primer/open_project/filterable_tree_view.rb index 8a984c8390..011ea064a9 100644 --- a/app/components/primer/open_project/filterable_tree_view.rb +++ b/app/components/primer/open_project/filterable_tree_view.rb @@ -230,19 +230,43 @@ def with_filter_mode(name:, **system_arguments) end def with_sub_tree(**system_arguments, &block) + system_arguments[:select_variant] ||= :multiple + + if system_arguments[:select_variant] != :multiple && system_arguments[:select_variant] != :single + raise ArgumentError, "FilterableTreeView only supports `:multiple` or `:single` as select_variant" + end + + if system_arguments[:select_variant] == :single + # In single selection, the include sub-items checkbox and the SegmentedControl make no sense + @include_sub_items_check_box_arguments[:hidden] = true + @include_sub_items_check_box_arguments[:checked] = false + @filter_mode_control_arguments[:hidden] = true + end + @tree_view.with_sub_tree( sub_tree_component_klass: SubTree, **system_arguments, - select_variant: :multiple, select_strategy: :self, &block ) end def with_leaf(**system_arguments, &block) + system_arguments[:select_variant] ||= :multiple + + if system_arguments[:select_variant] != :multiple && system_arguments[:select_variant] != :single + raise ArgumentError, "FilterableTreeView only supports `:multiple` or `:single` as select_variant" + end + + if system_arguments[:select_variant] == :single + # In single selection, the include sub-items checkbox and the SegmentedControl make no sense + @include_sub_items_check_box_arguments[:hidden] = true + @include_sub_items_check_box_arguments[:checked] = false + @filter_mode_control_arguments[:hidden] = true + end + @tree_view.with_leaf( **system_arguments, - select_variant: :multiple, &block ) end diff --git a/app/components/primer/open_project/filterable_tree_view/sub_tree.rb b/app/components/primer/open_project/filterable_tree_view/sub_tree.rb index a1e66ce0a1..77aad87d47 100644 --- a/app/components/primer/open_project/filterable_tree_view/sub_tree.rb +++ b/app/components/primer/open_project/filterable_tree_view/sub_tree.rb @@ -9,29 +9,39 @@ class FilterableTreeView # should not be used directly. class SubTree < Primer::Alpha::TreeView::SubTree def with_sub_tree(**system_arguments, &block) + system_arguments[:select_variant] ||= :multiple + + if system_arguments[:select_variant] != :multiple && system_arguments[:select_variant] != :single + raise ArgumentError, "FilterableTreeView only supports `:multiple` or `:single` as select_variant" + end + super( sub_tree_component_klass: self.class, **system_arguments, - select_variant: :multiple, select_strategy: :self, &block ) end def with_leaf(**system_arguments, &block) + system_arguments[:select_variant] ||= :multiple + + if system_arguments[:select_variant] != :multiple && system_arguments[:select_variant] != :single + raise ArgumentError, "FilterableTreeView only supports `:multiple` or `:single` as select_variant" + end + super( **system_arguments, - select_variant: :multiple, &block ) end def with_loading_spinner(**system_arguments) - raise ArgumentError, "FilteredTreeView does not support asynchronous loading" + raise ArgumentError, "FilterableTreeView does not support asynchronous loading" end def with_loading_skeleton(**system_arguments) - raise ArgumentError, "FilteredTreeView does not support asynchronous loading" + raise ArgumentError, "FilterableTreeView does not support asynchronous loading" end end end diff --git a/previews/primer/open_project/filterable_tree_view_preview.rb b/previews/primer/open_project/filterable_tree_view_preview.rb index d703a06c3c..704c39db80 100644 --- a/previews/primer/open_project/filterable_tree_view_preview.rb +++ b/previews/primer/open_project/filterable_tree_view_preview.rb @@ -7,11 +7,13 @@ class FilterableTreeViewPreview < ViewComponent::Preview # @label Playground # # @param expanded [Boolean] toggle + # @param select_variant [Symbol] select [multiple, single] # @param show_checkbox [Boolean] toggle # @param show_segmented_control [Boolean] toggle - def playground(expanded: true, show_checkbox: true, show_segmented_control: true) + def playground(expanded: true, select_variant: :multiple, show_checkbox: true, show_segmented_control: true) render_with_template(locals: { expanded: coerce_bool(expanded), + select_variant: select_variant.to_sym, show_checkbox: coerce_bool(show_checkbox), show_segmented_control: coerce_bool(show_segmented_control) }) @@ -29,9 +31,11 @@ def default(expanded: true) # @label Form input # + # @param select_variant [Symbol] select [multiple, single] # @param expanded [Boolean] toggle - def form_input(expanded: true) + def form_input(select_variant: :multiple, expanded: true) render_with_template(locals: { + select_variant: select_variant.to_sym, expanded: coerce_bool(expanded) }) end diff --git a/previews/primer/open_project/filterable_tree_view_preview/form_input.html.erb b/previews/primer/open_project/filterable_tree_view_preview/form_input.html.erb index 71b77cb5a6..fb32c4c087 100644 --- a/previews/primer/open_project/filterable_tree_view_preview/form_input.html.erb +++ b/previews/primer/open_project/filterable_tree_view_preview/form_input.html.erb @@ -1,30 +1,30 @@ <%= form_with(url: primer_view_components.generic_form_submission_path(format: :json)) do |f| %> <%= render(Primer::Alpha::Stack.new) do %> <%= render(Primer::OpenProject::FilterableTreeView.new(form_arguments: { builder: f, name: "characters" })) do |tree| %> - <% tree.with_sub_tree(label: "Students", expanded: expanded) do |hogwarts| %> - <% hogwarts.with_sub_tree(label: "Ravenclaw", expanded: expanded) do |ravenclaw| %> - <% ravenclaw.with_leaf(label: "Luna Lovegood") %> + <% tree.with_sub_tree(label: "Students", select_variant: select_variant, expanded: expanded) do |hogwarts| %> + <% hogwarts.with_sub_tree(label: "Ravenclaw", select_variant: select_variant, expanded: expanded) do |ravenclaw| %> + <% ravenclaw.with_leaf(label: "Luna Lovegood", select_variant: select_variant) %> <% end %> - <% hogwarts.with_sub_tree(label: "Slytherin", expanded: expanded) do |hufflepuff| %> - <% hufflepuff.with_leaf(label: "Draco Malfoy") %> + <% hogwarts.with_sub_tree(label: "Slytherin", select_variant: select_variant, expanded: expanded) do |hufflepuff| %> + <% hufflepuff.with_leaf(label: "Draco Malfoy", select_variant: select_variant) %> <% end %> - <% hogwarts.with_sub_tree(label: "Hufflepuff", expanded: expanded) do |hufflepuff| %> - <% hufflepuff.with_leaf(label: "Susan Bones") %> + <% hogwarts.with_sub_tree(label: "Hufflepuff", select_variant: select_variant, expanded: expanded) do |hufflepuff| %> + <% hufflepuff.with_leaf(label: "Susan Bones", select_variant: select_variant) %> <% end %> - <% hogwarts.with_sub_tree(label: "Gryffindor", expanded: expanded) do |hufflepuff| %> - <% hufflepuff.with_leaf(label: "Harry Potter") %> - <% hufflepuff.with_leaf(label: "Ronald Weasley") %> - <% hufflepuff.with_leaf(label: "Hermione Granger") %> + <% hogwarts.with_sub_tree(label: "Gryffindor", select_variant: select_variant, expanded: expanded) do |hufflepuff| %> + <% hufflepuff.with_leaf(label: "Harry Potter", select_variant: select_variant) %> + <% hufflepuff.with_leaf(label: "Ronald Weasley", select_variant: select_variant) %> + <% hufflepuff.with_leaf(label: "Hermione Granger", select_variant: select_variant) %> <% end %> <% end %> - <% tree.with_leaf(label: "Albus Dumbledore") %> - <% tree.with_leaf(label: "Minerva McGonagall") %> - <% tree.with_leaf(label: "Severus Snape") %> - <% tree.with_leaf(label: "Rubeus Hagrid") %> + <% tree.with_leaf(label: "Albus Dumbledore", select_variant: select_variant) %> + <% tree.with_leaf(label: "Minerva McGonagall", select_variant: select_variant) %> + <% tree.with_leaf(label: "Severus Snape", select_variant: select_variant) %> + <% tree.with_leaf(label: "Rubeus Hagrid", select_variant: select_variant) %> <% end %> <%= render(Primer::Alpha::SubmitButton.new(name: :submit, label: "Submit")) %> diff --git a/previews/primer/open_project/filterable_tree_view_preview/playground.html.erb b/previews/primer/open_project/filterable_tree_view_preview/playground.html.erb index b760fc3192..fc16896348 100644 --- a/previews/primer/open_project/filterable_tree_view_preview/playground.html.erb +++ b/previews/primer/open_project/filterable_tree_view_preview/playground.html.erb @@ -2,28 +2,28 @@ include_sub_items_check_box_arguments: { hidden: !show_checkbox }, filter_mode_control_arguments: { hidden: !show_segmented_control } )) do |tree| %> - <% tree.with_sub_tree(label: "Students", expanded: expanded) do |hogwarts| %> - <% hogwarts.with_sub_tree(label: "Ravenclaw", expanded: expanded) do |ravenclaw| %> - <% ravenclaw.with_leaf(label: "Luna Lovegood") %> + <% tree.with_sub_tree(label: "Students", select_variant: select_variant, expanded: expanded) do |hogwarts| %> + <% hogwarts.with_sub_tree(label: "Ravenclaw", select_variant: select_variant, expanded: expanded) do |ravenclaw| %> + <% ravenclaw.with_leaf(label: "Luna Lovegood", select_variant: select_variant) %> <% end %> - <% hogwarts.with_sub_tree(label: "Slytherin", expanded: expanded) do |hufflepuff| %> - <% hufflepuff.with_leaf(label: "Draco Malfoy") %> + <% hogwarts.with_sub_tree(label: "Slytherin", select_variant: select_variant, expanded: expanded) do |hufflepuff| %> + <% hufflepuff.with_leaf(label: "Draco Malfoy", select_variant: select_variant) %> <% end %> - <% hogwarts.with_sub_tree(label: "Hufflepuff", expanded: expanded) do |hufflepuff| %> - <% hufflepuff.with_leaf(label: "Susan Bones") %> + <% hogwarts.with_sub_tree(label: "Hufflepuff", select_variant: select_variant, expanded: expanded) do |hufflepuff| %> + <% hufflepuff.with_leaf(label: "Susan Bones", select_variant: select_variant) %> <% end %> - <% hogwarts.with_sub_tree(label: "Gryffindor", expanded: expanded) do |hufflepuff| %> - <% hufflepuff.with_leaf(label: "Harry Potter") %> - <% hufflepuff.with_leaf(label: "Ronald Weasley") %> - <% hufflepuff.with_leaf(label: "Hermione Granger") %> + <% hogwarts.with_sub_tree(label: "Gryffindor", select_variant: select_variant, expanded: expanded) do |hufflepuff| %> + <% hufflepuff.with_leaf(label: "Harry Potter", select_variant: select_variant) %> + <% hufflepuff.with_leaf(label: "Ronald Weasley", select_variant: select_variant) %> + <% hufflepuff.with_leaf(label: "Hermione Granger", select_variant: select_variant) %> <% end %> <% end %> - <% tree.with_leaf(label: "Albus Dumbledore") %> - <% tree.with_leaf(label: "Minerva McGonagall") %> - <% tree.with_leaf(label: "Severus Snape") %> - <% tree.with_leaf(label: "Rubeus Hagrid") %> + <% tree.with_leaf(label: "Albus Dumbledore", select_variant: select_variant) %> + <% tree.with_leaf(label: "Minerva McGonagall", select_variant: select_variant) %> + <% tree.with_leaf(label: "Severus Snape", select_variant: select_variant) %> + <% tree.with_leaf(label: "Rubeus Hagrid", select_variant: select_variant) %> <% end %> diff --git a/test/components/primer/open_project/filterable_tree_view_test.rb b/test/components/primer/open_project/filterable_tree_view_test.rb index 51f57963a5..3e523076d4 100644 --- a/test/components/primer/open_project/filterable_tree_view_test.rb +++ b/test/components/primer/open_project/filterable_tree_view_test.rb @@ -51,7 +51,7 @@ def test_filter_mode_control_label_can_be_overridden assert_selector("segmented-control [role=list][aria-label='Filtermodus']") end - def test_segmented_control_can_be_hidden + def test_filter_mode_control_can_be_hidden render_inline( Primer::OpenProject::FilterableTreeView.new( filter_mode_control_arguments: { hidden: true } @@ -93,6 +93,14 @@ def test_include_sub_items_can_have_a_different_default assert_selector("input[name=include_sub_items][checked=checked]", visible: :visible) end + def test_include_sub_items_and_filter_mode_are_hidden_for_single_select_variant + render_preview(:form_input, params: { select_variant: :single }) + + assert_selector(".FormControl-checkbox-wrap", visible: :hidden) + assert_selector("input[name=include_sub_items]:not([checked=false])", visible: :hidden) + assert_selector("segmented-control", visible: :hidden) + end + def test_has_filter_input render_preview(:default) @@ -119,6 +127,46 @@ def test_filter_input_arguments_can_be_overridden assert_selector("label[for='#{id}'].sr-only", text: "Filtern") end + def test_leaf_cannot_render_with_select_variant_none + error = assert_raises(ArgumentError) do + render_inline(Primer::OpenProject::FilterableTreeView.new) do |tree| + tree.with_leaf(label: "Foo", select_variant: :none) + end + end + + assert_equal error.message, "FilterableTreeView only supports `:multiple` or `:single` as select_variant" + + error = assert_raises(ArgumentError) do + render_inline(Primer::OpenProject::FilterableTreeView.new) do |tree| + tree.with_sub_tree(label: "Foo") do |sub_tree| + sub_tree.with_leaf(label: "Bar", select_variant: :none) + end + end + end + + assert_equal error.message, "FilterableTreeView only supports `:multiple` or `:single` as select_variant" + end + + def test_sub_trees_cannot_render_with_select_variant_none + error = assert_raises(ArgumentError) do + render_inline(Primer::OpenProject::FilterableTreeView.new) do |tree| + tree.with_sub_tree(label: "Foo", select_variant: :none) + end + end + + assert_equal error.message, "FilterableTreeView only supports `:multiple` or `:single` as select_variant" + + error = assert_raises(ArgumentError) do + render_inline(Primer::OpenProject::FilterableTreeView.new) do |tree| + tree.with_sub_tree(label: "Foo") do |sub_tree| + sub_tree.with_sub_tree(label: "Bar", select_variant: :none) + end + end + end + + assert_equal error.message, "FilterableTreeView only supports `:multiple` or `:single` as select_variant" + end + def test_sub_trees_cannot_load_with_async_spinner error = assert_raises(ArgumentError) do render_inline(Primer::OpenProject::FilterableTreeView.new) do |tree| @@ -128,7 +176,7 @@ def test_sub_trees_cannot_load_with_async_spinner end end - assert_equal error.message, "FilteredTreeView does not support asynchronous loading" + assert_equal error.message, "FilterableTreeView does not support asynchronous loading" end def test_sub_trees_cannot_load_with_async_skeleton @@ -140,7 +188,7 @@ def test_sub_trees_cannot_load_with_async_skeleton end end - assert_equal error.message, "FilteredTreeView does not support asynchronous loading" + assert_equal error.message, "FilterableTreeView does not support asynchronous loading" end def test_custom_filter_modes diff --git a/test/system/open_project/filterable_tree_view_test.rb b/test/system/open_project/filterable_tree_view_test.rb index b752875943..5321d85fb7 100644 --- a/test/system/open_project/filterable_tree_view_test.rb +++ b/test/system/open_project/filterable_tree_view_test.rb @@ -217,6 +217,23 @@ def test_automatically_expands_sub_trees_to_show_matching_items assert_path("Students", "Gryffindor", "Ronald Weasley") end + def test_form_submits_checked_nodes_for_single_select_variant + visit_preview(:form_input, select_variant: :single) + + activate_at_path("Students", "Gryffindor", "Harry Potter") + activate_at_path("Students", "Ravenclaw", "Luna Lovegood") + click_on "Submit" + + response = JSON.parse(find("pre").text) + assert character_list = response.dig("form_params", "characters") + + # Since we are in single select mode, only the last checked element is selected and send + assert_equal 1, character_list.size + + character = JSON.parse(character_list[0]) + assert_equal character["path"], ["Students", "Ravenclaw", "Luna Lovegood"] + end + def test_form_submits_checked_nodes visit_preview(:form_input)