Skip to content

Commit

Permalink
Merge pull request #17017 from opf/impl/57819-drill-down-hierarchy
Browse files Browse the repository at this point in the history
Implement drill up/down the hierarchy field
  • Loading branch information
mereghost authored Oct 29, 2024
2 parents ec6f8a7 + 8ae9df6 commit fbde563
Show file tree
Hide file tree
Showing 19 changed files with 265 additions and 198 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@ See COPYRIGHT and LICENSE files for more details.
++#%>

<%=
render(Primer::Alpha::Dialog.new(id: DIALOG_ID,
title: "Delete item",
data: { test_selector: TEST_SELECTOR })) do |dialog|
render(Primer::Alpha::Dialog.new(id: DIALOG_ID, title: "Delete item", test_selector: TEST_SELECTOR)) do |dialog|
dialog.with_header(variant: :large)
dialog.with_body do
"Are you sure you want to delete this item from the current hierarchy level?"
Expand All @@ -42,14 +40,12 @@ See COPYRIGHT and LICENSE files for more details.
end)

concat(primer_form_with(
model: @custom_field,
url: custom_field_item_path(custom_field_id: @custom_field.id, id: @hierarchy_item.id),
method: :delete,
data: { turbo: true }
) do
render(Primer::ButtonComponent.new(scheme: :danger,
type: :submit,
data: { "close-dialog-id": DIALOG_ID })) do
model: @custom_field,
url: custom_field_item_path(custom_field_id: @custom_field.id, id: @hierarchy_item.id),
method: :delete,
data: { turbo: true }
) do
render(Primer::ButtonComponent.new(scheme: :danger, type: :submit, data: { "close-dialog-id": DIALOG_ID })) do
I18n.t(:button_delete)
end
end)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,42 +28,44 @@ See COPYRIGHT and LICENSE files for more details.
++#%>

<%=
component_wrapper(data: { test_selector: "op-custom-fields--hierarchy-item" }) do
component_wrapper(tag: "turbo-frame", refresh: :morph) do
if show_edit_form?
render Admin::CustomFields::Hierarchy::ItemFormComponent.new(
custom_field: @custom_field,
hierarchy_item: @hierarchy_item,
url: custom_field_item_path(@custom_field, @hierarchy_item),
method: :put,
label: @edit_item_form_data.fetch(:label, nil),
short: @edit_item_form_data.fetch(:short, nil)
target_item: model,
url: custom_field_item_path(@root.custom_field_id, model),
method: :put
)
else
flex_layout(align_items: :center, justify_content: :space_between) do |item_container|
flex_layout(align_items: :center, justify_content: :space_between, test_selector: "op-custom-fields--hierarchy-item") do |item_container|
item_container.with_column(flex_layout: true) do |item_information|
item_information.with_column(mr: 2) do
render(Primer::Beta::Text.new(font_weight: :bold)) do
@hierarchy_item.label
render(Primer::Beta::Link.new(href: custom_field_item_path(@root.custom_field_id, model), underline: false)) do
render(Primer::Beta::Text.new(font_weight: :bold)) { model.label }
end
end

unless @hierarchy_item.short.nil?
if model.short.present?
item_information.with_column(mr: 2) do
render(Primer::Beta::Text.new(color: :subtle)) { short_text }
end
end

# Actions
item_container.with_column do
render(Primer::Alpha::ActionMenu.new(data: { test_selector: "op-hierarchy-item--action-menu" })) do |menu|
menu.with_show_button(icon: "kebab-horizontal",
scheme: :invisible,
"aria-label": I18n.t("custom_fields.admin.items.actions"))
edit_action_item(menu)
deletion_action_item(menu)
if model.children.any?
item_information.with_column(mr: 2) do
render(Primer::Beta::Text.new) { children_count }
end
end
end

item_container.with_column do
render(Primer::Alpha::ActionMenu.new(test_selector: "op-hierarchy-item--action-menu")) do |menu|
menu.with_show_button(icon: "kebab-horizontal",
scheme: :invisible,
"aria-label": I18n.t("custom_fields.admin.items.actions"))
edit_action_item(menu)
deletion_action_item(menu)
end
end
end
end
end
Expand Down
29 changes: 14 additions & 15 deletions app/components/admin/custom_fields/hierarchy/item_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,31 +35,31 @@ class ItemComponent < ApplicationComponent
include OpTurbo::Streamable
include OpPrimer::ComponentHelpers

def initialize(custom_field:, hierarchy_item:, edit_item_form_data: { show: false })
super
@custom_field = custom_field
@hierarchy_item = hierarchy_item
@edit_item_form_data = edit_item_form_data
def initialize(item:, show_edit_form: false)
super(item)
@show_edit_form = show_edit_form
@root = item.root
end

def short_text
"(#{@hierarchy_item.short})"
def wrapper_uniq_by
model.id
end

def wrapper_uniq_by
@hierarchy_item.id
def short_text
"(#{model.short})"
end

def show_edit_form?
@edit_item_form_data.fetch(:show, false)
def show_edit_form? = @show_edit_form

def children_count
I18n.t("custom_fields.admin.hierarchy.subitems", count: model.children.count)
end

def deletion_action_item(menu)
menu.with_item(label: I18n.t(:button_delete),
scheme: :danger,
tag: :a,
href: deletion_dialog_custom_field_item_path(custom_field_id: @custom_field.id,
id: @hierarchy_item.id),
href: deletion_dialog_custom_field_item_path(custom_field_id: @root.custom_field_id, id: model.id),
content_arguments: { data: { controller: "async-dialog" } }) do |item|
item.with_leading_visual_icon(icon: :trash)
end
Expand All @@ -68,8 +68,7 @@ def deletion_action_item(menu)
def edit_action_item(menu)
menu.with_item(label: I18n.t(:button_edit),
tag: :a,
content_arguments: { data: { turbo_stream: true } },
href: edit_custom_field_item_path(@custom_field, @hierarchy_item)) do |item|
href: edit_custom_field_item_path(@root.custom_field_id, model)) do |item|
item.with_leading_visual_icon(icon: :pencil)
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,7 @@ See COPYRIGHT and LICENSE files for more details.
++#%>

<%=
primer_form_with(url: @url, method: @method, data: { test_selector: "op-custom-fields--new-item-form" }) do |f|
render(CustomFields::Hierarchy::ItemForm.new(
f,
custom_field: @custom_field,
hierarchy_item: @hierarchy_item,
label: @label,
short: @short
))
primer_form_with(url: @url, method: @method, test_selector: "op-custom-fields--new-item-form") do |f|
render(CustomFields::Hierarchy::ItemForm.new(f, target_item: model))
end
%>
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
Expand Down Expand Up @@ -32,18 +34,10 @@ module Hierarchy
class ItemFormComponent < ApplicationComponent
include OpTurbo::Streamable

def initialize(custom_field:, hierarchy_item:, url:, method:, label: nil, short: nil)
super
@custom_field = custom_field
@hierarchy_item = hierarchy_item
def initialize(target_item:, url:, method:)
super(target_item)
@url = url
@method = method
@label = label || @hierarchy_item.label
@short = short || @hierarchy_item.short
end

def items_path
custom_field_items_path(@custom_field)
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,42 +28,33 @@ See COPYRIGHT and LICENSE files for more details.
++#%>

<%=
component_wrapper(tag: "turbo-frame") do
component_wrapper(tag: "turbo-frame", refresh: :morph, data: { turbo_action: :advance }) do
flex_layout do |container|
if items.empty? && !show_new_item_form?
container.with_row(mb: 3) do
render Primer::Beta::Blankslate.new(
border: true,
test_selector: "op-custom-fields--hierarchy-items-blankslate"
) do |component|
component.with_visual_icon(icon: "list-ordered")
component.with_heading(tag: :h3).with_content(I18n.t("custom_fields.admin.items.blankslate.title"))
component.with_description { I18n.t("custom_fields.admin.items.blankslate.description") }
end
end
else
container.with_row(mb: 3) do
render(Primer::Beta::BorderBox.new) do |item_box|
item_box.with_header { @custom_field.name }
container.with_row(mb: 3) do
render(Primer::Beta::BorderBox.new) do |box|
box.with_header { item_header }

items.each do |item|
item_box.with_row do
render Admin::CustomFields::Hierarchy::ItemComponent.new(
custom_field: @custom_field,
hierarchy_item: item
)
if children.empty? && !show_new_item_form?
box.with_row do
render(Primer::Beta::Blankslate.new(test_selector: "op-custom-fields--hierarchy-items-blankslate")) do |component|
component.with_visual_icon(icon: "list-ordered")
component.with_heading(tag: :h3).with_content(I18n.t("custom_fields.admin.items.blankslate.title"))
component.with_description { I18n.t("custom_fields.admin.items.blankslate.description") }
end
end
else
children.each do |item|
box.with_row do
render Admin::CustomFields::Hierarchy::ItemComponent.new(item: item)
end
end

if show_new_item_form?
item_box.with_footer do
box.with_footer(test_selector: "op-custom-fields--new-item-form") do
render Admin::CustomFields::Hierarchy::ItemFormComponent.new(
custom_field: @custom_field,
hierarchy_item: CustomField::Hierarchy::Item.new,
url: custom_field_items_path(@custom_field),
method: :post,
label: @new_item_form_data[:label],
short: @new_item_form_data[:short]
target_item: @new_item,
url: new_child_custom_field_item_path(model.root.custom_field_id, model),
method: :post
)
end
end
Expand All @@ -74,8 +65,7 @@ See COPYRIGHT and LICENSE files for more details.
container.with_row do
render Primer::Beta::Button.new(scheme: :primary,
tag: :a,
data: { turbo_stream: true },
href: new_custom_field_item_path(@custom_field)) do |button|
href: new_child_custom_field_item_path(model.root.custom_field_id, model)) do |button|
button.with_leading_visual_icon(icon: :plus)
I18n.t(:label_item)
end
Expand Down
40 changes: 31 additions & 9 deletions app/components/admin/custom_fields/hierarchy/items_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,41 @@ class ItemsComponent < ApplicationComponent
include OpTurbo::Streamable
include OpPrimer::ComponentHelpers

def initialize(custom_field:, new_item_form_data: { show: false })
super
@custom_field = custom_field
@new_item_form_data = new_item_form_data
property :children

def initialize(item:, new_item: nil)
super(item)
@new_item = new_item
end

def items
# TODO: This must be context aware (breadcrumbs)
@custom_field.hierarchy_root.children
def show_new_item_form? = @new_item

def root
return model if model.root?

@root ||= model.root
end

def show_new_item_form?
@new_item_form_data[:show] || false
def item_header
render(Primer::Beta::Breadcrumbs.new) do |loaf|
slices.each do |slice|
loaf.with_item(href: slice[:href], target: nil) { slice[:label] }
end
end
end

private

def slices
nodes = ::CustomFields::Hierarchy::HierarchicalItemService.new.get_branch(item: model).value!

nodes.map do |item|
if item.root?
{ href: custom_field_items_path(root.custom_field_id), label: root.custom_field.name }
else
{ href: custom_field_item_path(root.custom_field_id, item), label: item.label }
end
end
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,7 @@ class UpdateItemContract < Dry::Validation::Contract
rule(:label) do
next unless key?

label_already_exists = CustomField::Hierarchy::Item
.where(parent_id: values[:item].parent_id, label: value)
.where.not(id: values[:item].id)
.exists?

if label_already_exists
if values[:item].siblings.where(label: value).any?
key.failure("must be unique at the same hierarchical level")
end
end
Expand Down
Loading

0 comments on commit fbde563

Please sign in to comment.