feat: collapsable hierarchical view
This commit is contained in:
@@ -275,4 +275,60 @@ defmodule ComponentsElixir.Inventory.Hierarchical do
|
||||
[child | sort_children_recursively(entities, child.id, parent_id_accessor_fn)]
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculates component counts for an entity and all its descendants.
|
||||
Returns a tuple of {self_count, children_count, total_count}.
|
||||
|
||||
## Parameters
|
||||
- entity_id: The ID of the entity to count for
|
||||
- all_entities: List of all entities in the hierarchy
|
||||
- parent_id_accessor_fn: Function to get parent_id from an entity
|
||||
- count_fn: Function that takes an entity_id and returns the direct count for that entity
|
||||
|
||||
## Examples
|
||||
iex> count_fn = fn id -> MyRepo.count_components_for(id) end
|
||||
iex> Hierarchical.count_with_descendants(1, entities, &(&1.parent_id), count_fn)
|
||||
{3, 7, 10} # 3 in self, 7 in children, 10 total
|
||||
"""
|
||||
def count_with_descendants(entity_id, all_entities, parent_id_accessor_fn, count_fn) do
|
||||
# Get direct count for this entity
|
||||
self_count = count_fn.(entity_id)
|
||||
|
||||
# Get all descendant entity IDs (excluding self)
|
||||
all_descendant_ids = descendant_ids(all_entities, entity_id, parent_id_accessor_fn)
|
||||
descendant_ids_only = List.delete(all_descendant_ids, entity_id)
|
||||
|
||||
# Sum counts for all descendants
|
||||
children_count = Enum.reduce(descendant_ids_only, 0, fn id, acc ->
|
||||
acc + count_fn.(id)
|
||||
end)
|
||||
|
||||
{self_count, children_count, self_count + children_count}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Formats component count display based on expansion state.
|
||||
|
||||
When collapsed: Shows total count only: "10 components"
|
||||
When expanded: Shows breakdown: "10 components (3 self, 7 children)"
|
||||
|
||||
## Parameters
|
||||
- self_count: Number of components directly in this entity
|
||||
- children_count: Number of components in all descendant entities
|
||||
- is_expanded: Whether the entity is currently expanded
|
||||
- singular_noun: What to call a single item (default: "component")
|
||||
- plural_noun: What to call multiple items (default: "components")
|
||||
"""
|
||||
def format_count_display(self_count, children_count, is_expanded, singular_noun \\ "component", plural_noun \\ "components") do
|
||||
total_count = self_count + children_count
|
||||
|
||||
count_noun = if total_count == 1, do: singular_noun, else: plural_noun
|
||||
|
||||
if is_expanded and children_count > 0 do
|
||||
"#{total_count} #{count_noun} (#{self_count} self, #{children_count} children)"
|
||||
else
|
||||
"#{total_count} #{count_noun}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -20,6 +20,7 @@ defmodule ComponentsElixirWeb.CategoriesLive do
|
||||
|> assign(:show_edit_form, false)
|
||||
|> assign(:editing_category, nil)
|
||||
|> assign(:form, nil)
|
||||
|> assign(:expanded_categories, MapSet.new())
|
||||
|> assign(:page_title, "Category Management")}
|
||||
end
|
||||
end
|
||||
@@ -115,6 +116,19 @@ defmodule ComponentsElixirWeb.CategoriesLive do
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("toggle_expand", %{"id" => id}, socket) do
|
||||
category_id = String.to_integer(id)
|
||||
expanded_categories = socket.assigns.expanded_categories
|
||||
|
||||
new_expanded = if MapSet.member?(expanded_categories, category_id) do
|
||||
MapSet.delete(expanded_categories, category_id)
|
||||
else
|
||||
MapSet.put(expanded_categories, category_id)
|
||||
end
|
||||
|
||||
{:noreply, assign(socket, :expanded_categories, new_expanded)}
|
||||
end
|
||||
|
||||
defp reload_categories(socket) do
|
||||
categories = Inventory.list_categories()
|
||||
assign(socket, :categories, categories)
|
||||
@@ -156,6 +170,21 @@ defmodule ComponentsElixirWeb.CategoriesLive do
|
||||
_ -> {"w-3 h-3", "p-1", "text-sm", "h5"}
|
||||
end
|
||||
|
||||
children = child_categories(assigns.categories, assigns.category.id)
|
||||
has_children = !Enum.empty?(children)
|
||||
is_expanded = MapSet.member?(assigns.expanded_categories, assigns.category.id)
|
||||
|
||||
# Calculate component counts including descendants
|
||||
{self_count, children_count, _total_count} = Hierarchical.count_with_descendants(
|
||||
assigns.category.id,
|
||||
assigns.categories,
|
||||
&(&1.parent_id),
|
||||
&count_components_in_category/1
|
||||
)
|
||||
|
||||
# Format count display
|
||||
count_display = Hierarchical.format_count_display(self_count, children_count, is_expanded)
|
||||
|
||||
assigns = assigns
|
||||
|> assign(:margin_left, margin_left)
|
||||
|> assign(:border_class, border_class)
|
||||
@@ -163,50 +192,113 @@ defmodule ComponentsElixirWeb.CategoriesLive do
|
||||
|> assign(:button_size, button_size)
|
||||
|> assign(:text_size, text_size)
|
||||
|> assign(:title_tag, title_tag)
|
||||
|> assign(:children, child_categories(assigns.categories, assigns.category.id))
|
||||
|> assign(:children, children)
|
||||
|> assign(:has_children, has_children)
|
||||
|> assign(:is_expanded, is_expanded)
|
||||
|> assign(:count_display, count_display)
|
||||
|
||||
~H"""
|
||||
<div class={[@border_class, @depth > 0 && "mt-4"]} style={"margin-left: #{@margin_left}px"}>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center">
|
||||
<.icon name="hero-folder" class={"#{@icon_size} #{if @depth == 0, do: "text-primary", else: "text-base-content/60"} mr-3"} />
|
||||
<div>
|
||||
<%= if @title_tag == "h3" do %>
|
||||
<h3 class={"#{@text_size} font-medium #{if @depth == 0, do: "text-base-content", else: "text-base-content/80"}"}>{@category.name}</h3>
|
||||
<div class="flex items-start justify-between p-2 hover:bg-base-50 rounded-md">
|
||||
<div class="flex items-start flex-1 space-x-2">
|
||||
<!-- Expand/Collapse button - always aligned to top -->
|
||||
<%= if @has_children do %>
|
||||
<button
|
||||
phx-click="toggle_expand"
|
||||
phx-value-id={@category.id}
|
||||
class="flex-shrink-0 p-1 hover:bg-base-200 rounded mt-0.5"
|
||||
>
|
||||
<%= if @is_expanded do %>
|
||||
<.icon name="hero-chevron-down" class="w-4 h-4 text-base-content/60" />
|
||||
<% else %>
|
||||
<h4 class={"#{@text_size} font-medium #{if @depth == 0, do: "text-base-content", else: "text-base-content/80"}"}>{@category.name}</h4>
|
||||
<.icon name="hero-chevron-right" class="w-4 h-4 text-base-content/60" />
|
||||
<% end %>
|
||||
<%= if @category.description do %>
|
||||
<p class="text-sm text-base-content/60 mt-1">{@category.description}</p>
|
||||
<% end %>
|
||||
<p class="text-xs text-base-content/50 mt-1">
|
||||
{count_components_in_category(@category.id)} components
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
<% else %>
|
||||
<div class="w-6"></div> <!-- Spacer for alignment -->
|
||||
<% end %>
|
||||
|
||||
<.icon name="hero-folder" class={"#{@icon_size} #{if @depth == 0, do: "text-primary", else: "text-base-content/60"} mt-0.5"} />
|
||||
|
||||
<!-- Content area - always starts at same vertical position -->
|
||||
<div class="flex-1">
|
||||
<!-- Minimized view (default) -->
|
||||
<%= unless @is_expanded do %>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-3">
|
||||
<%= if @title_tag == "h3" do %>
|
||||
<h3 class={"#{@text_size} font-medium #{if @depth == 0, do: "text-base-content", else: "text-base-content/80"}"}>{@category.name}</h3>
|
||||
<% else %>
|
||||
<h4 class={"#{@text_size} font-medium #{if @depth == 0, do: "text-base-content", else: "text-base-content/80"}"}>{@category.name}</h4>
|
||||
<% end %>
|
||||
<span class="text-xs text-base-content/50">
|
||||
({@count_display})
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button
|
||||
phx-click="show_edit_form"
|
||||
phx-value-id={@category.id}
|
||||
class="inline-flex items-center p-1.5 border border-transparent rounded-full shadow-sm text-white bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
<.icon name="hero-pencil" class="w-3 h-3" />
|
||||
</button>
|
||||
<button
|
||||
phx-click="delete_category"
|
||||
phx-value-id={@category.id}
|
||||
data-confirm="Are you sure you want to delete this category? This action cannot be undone."
|
||||
class="inline-flex items-center p-1.5 border border-transparent rounded-full shadow-sm text-white bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
<.icon name="hero-trash" class="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Expanded view -->
|
||||
<%= if @is_expanded do %>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<%= if @title_tag == "h3" do %>
|
||||
<h3 class={"#{@text_size} font-medium #{if @depth == 0, do: "text-base-content", else: "text-base-content/80"}"}>{@category.name}</h3>
|
||||
<% else %>
|
||||
<h4 class={"#{@text_size} font-medium #{if @depth == 0, do: "text-base-content", else: "text-base-content/80"}"}>{@category.name}</h4>
|
||||
<% end %>
|
||||
<%= if @category.description do %>
|
||||
<p class="text-sm text-base-content/60 mt-1">{@category.description}</p>
|
||||
<% end %>
|
||||
<p class="text-xs text-base-content/50 mt-1">
|
||||
{@count_display}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button
|
||||
phx-click="show_edit_form"
|
||||
phx-value-id={@category.id}
|
||||
class={"inline-flex items-center #{@button_size} border border-transparent rounded-full shadow-sm text-white bg-blue-600 hover:bg-blue-700"}
|
||||
>
|
||||
<.icon name="hero-pencil" class={@icon_size} />
|
||||
</button>
|
||||
<button
|
||||
phx-click="delete_category"
|
||||
phx-value-id={@category.id}
|
||||
data-confirm="Are you sure you want to delete this category? This action cannot be undone."
|
||||
class={"inline-flex items-center #{@button_size} border border-transparent rounded-full shadow-sm text-white bg-red-600 hover:bg-red-700"}
|
||||
>
|
||||
<.icon name="hero-trash" class={@icon_size} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button
|
||||
phx-click="show_edit_form"
|
||||
phx-value-id={@category.id}
|
||||
class={"inline-flex items-center #{@button_size} border border-transparent rounded-full shadow-sm text-white bg-blue-600 hover:bg-blue-700"}
|
||||
>
|
||||
<.icon name="hero-pencil" class={@icon_size} />
|
||||
</button>
|
||||
<button
|
||||
phx-click="delete_category"
|
||||
phx-value-id={@category.id}
|
||||
data-confirm="Are you sure you want to delete this category? This action cannot be undone."
|
||||
class={"inline-flex items-center #{@button_size} border border-transparent rounded-full shadow-sm text-white bg-red-600 hover:bg-red-700"}
|
||||
>
|
||||
<.icon name="hero-trash" class={@icon_size} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Render children recursively -->
|
||||
<%= for child <- @children do %>
|
||||
<.category_item category={child} categories={@categories} depth={@depth + 1} />
|
||||
|
||||
<!-- Render children recursively (only when expanded) -->
|
||||
<%= if @is_expanded do %>
|
||||
<%= for child <- @children do %>
|
||||
<.category_item category={child} categories={@categories} expanded_categories={@expanded_categories} depth={@depth + 1} />
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
@@ -391,7 +483,7 @@ defmodule ComponentsElixirWeb.CategoriesLive do
|
||||
<!-- Recursive Category Tree -->
|
||||
<%= for category <- root_categories(@categories) do %>
|
||||
<div class="px-6 py-4">
|
||||
<.category_item category={category} categories={@categories} depth={0} />
|
||||
<.category_item category={category} categories={@categories} expanded_categories={@expanded_categories} depth={0} />
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -26,6 +26,7 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
|> assign(:form, nil)
|
||||
|> assign(:apriltag_scanner_open, false)
|
||||
|> assign(:scanned_tags, [])
|
||||
|> assign(:expanded_locations, MapSet.new())
|
||||
|> assign(:page_title, "Storage Location Management")}
|
||||
end
|
||||
end
|
||||
@@ -183,6 +184,19 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
{:noreply, assign(socket, :scanned_tags, [])}
|
||||
end
|
||||
|
||||
def handle_event("toggle_expand", %{"id" => id}, socket) do
|
||||
location_id = String.to_integer(id)
|
||||
expanded_locations = socket.assigns.expanded_locations
|
||||
|
||||
new_expanded = if MapSet.member?(expanded_locations, location_id) do
|
||||
MapSet.delete(expanded_locations, location_id)
|
||||
else
|
||||
MapSet.put(expanded_locations, location_id)
|
||||
end
|
||||
|
||||
{:noreply, assign(socket, :expanded_locations, new_expanded)}
|
||||
end
|
||||
|
||||
def handle_event("set_apriltag_mode", %{"mode" => mode}, socket) do
|
||||
{:noreply, assign(socket, :apriltag_mode, mode)}
|
||||
end
|
||||
@@ -290,6 +304,21 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
_ -> "hero-cube" # Box/Container
|
||||
end
|
||||
|
||||
children = child_storage_locations(assigns.storage_locations, assigns.location.id)
|
||||
has_children = !Enum.empty?(children)
|
||||
is_expanded = MapSet.member?(assigns.expanded_locations, assigns.location.id)
|
||||
|
||||
# Calculate component counts including descendants
|
||||
{self_count, children_count, _total_count} = Hierarchical.count_with_descendants(
|
||||
assigns.location.id,
|
||||
assigns.storage_locations,
|
||||
&(&1.parent_id),
|
||||
&count_components_in_location/1
|
||||
)
|
||||
|
||||
# Format count display
|
||||
count_display = Hierarchical.format_count_display(self_count, children_count, is_expanded)
|
||||
|
||||
assigns = assigns
|
||||
|> assign(:margin_left, margin_left)
|
||||
|> assign(:border_class, border_class)
|
||||
@@ -298,82 +327,152 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
|> assign(:text_size, text_size)
|
||||
|> assign(:title_tag, title_tag)
|
||||
|> assign(:icon_name, icon_name)
|
||||
|> assign(:children, child_storage_locations(assigns.storage_locations, assigns.location.id))
|
||||
|> assign(:children, children)
|
||||
|> assign(:has_children, has_children)
|
||||
|> assign(:is_expanded, is_expanded)
|
||||
|> assign(:count_display, count_display)
|
||||
|
||||
~H"""
|
||||
<div class={[@border_class, @depth > 0 && "mt-4"]} style={"margin-left: #{@margin_left}px"}>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center flex-1 space-x-4">
|
||||
<.icon name={@icon_name} class={"#{@icon_size} #{if @depth == 0, do: "text-primary", else: "text-base-content/60"}"} />
|
||||
<div class="flex-1">
|
||||
<%= if @title_tag == "h3" do %>
|
||||
<h3 class={"#{@text_size} font-medium #{if @depth == 0, do: "text-base-content", else: "text-base-content/80"}"}>{@location.name}</h3>
|
||||
<% else %>
|
||||
<h4 class={"#{@text_size} font-medium #{if @depth == 0, do: "text-base-content", else: "text-base-content/80"}"}>{@location.name}</h4>
|
||||
<% end %>
|
||||
<%= if @location.description do %>
|
||||
<p class="text-sm text-base-content/60 mt-1">{@location.description}</p>
|
||||
<% end %>
|
||||
<div class="flex items-center space-x-2 mt-1">
|
||||
<p class="text-xs text-base-content/50">
|
||||
{count_components_in_location(@location.id)} components
|
||||
</p>
|
||||
<%= if @location.apriltag_id do %>
|
||||
<span class="text-xs bg-base-200 text-base-content/80 px-2 py-1 rounded">
|
||||
AprilTag: {@location.apriltag_id}
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<%= if @location.apriltag_id do %>
|
||||
<div class="flex items-center space-x-3">
|
||||
<%= if get_apriltag_url(@location) do %>
|
||||
<div class="apriltag-container flex-shrink-0">
|
||||
<img
|
||||
src={get_apriltag_url(@location)}
|
||||
alt={"AprilTag for #{@location.name}"}
|
||||
class="w-16 h-auto border border-base-300 rounded bg-base-100"
|
||||
onerror="this.style.display='none'"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-start justify-between p-2 hover:bg-base-50 rounded-md">
|
||||
<div class="flex items-start flex-1 space-x-2">
|
||||
<!-- Expand/Collapse button - always aligned to top -->
|
||||
<%= if @has_children do %>
|
||||
<button
|
||||
phx-click="toggle_expand"
|
||||
phx-value-id={@location.id}
|
||||
class="flex-shrink-0 p-1 hover:bg-base-200 rounded mt-0.5"
|
||||
>
|
||||
<%= if @is_expanded do %>
|
||||
<.icon name="hero-chevron-down" class="w-4 h-4 text-base-content/60" />
|
||||
<% else %>
|
||||
<div class="w-16 h-16 border border-base-300 rounded bg-base-200 flex items-center justify-center flex-shrink-0">
|
||||
<.icon name="hero-qr-code" class="w-8 h-8 text-base-content/50" />
|
||||
</div>
|
||||
<.icon name="hero-chevron-right" class="w-4 h-4 text-base-content/60" />
|
||||
<% end %>
|
||||
<button
|
||||
phx-click="download_apriltag"
|
||||
phx-value-id={@location.id}
|
||||
class="inline-flex items-center px-3 py-1.5 border border-base-300 rounded-md shadow-sm text-sm font-medium text-base-content bg-base-100 hover:bg-base-200 flex-shrink-0"
|
||||
title="Download AprilTag"
|
||||
>
|
||||
<.icon name="hero-arrow-down-tray" class="w-4 h-4 mr-1.5" />
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
</button>
|
||||
<% else %>
|
||||
<div class="w-6"></div> <!-- Spacer for alignment -->
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 ml-4">
|
||||
<button
|
||||
phx-click="show_edit_form"
|
||||
phx-value-id={@location.id}
|
||||
class="inline-flex items-center p-2 border border-transparent rounded-full shadow-sm text-white bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
<.icon name="hero-pencil" class="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
phx-click="delete_location"
|
||||
phx-value-id={@location.id}
|
||||
data-confirm="Are you sure you want to delete this storage location? This action cannot be undone."
|
||||
class="inline-flex items-center p-2 border border-transparent rounded-full shadow-sm text-white bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
<.icon name="hero-trash" class="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<.icon name={@icon_name} class={"#{@icon_size} #{if @depth == 0, do: "text-primary", else: "text-base-content/60"} mt-0.5"} />
|
||||
|
||||
<!-- Content area - always starts at same vertical position -->
|
||||
<div class="flex-1">
|
||||
<!-- Minimized view (default) -->
|
||||
<%= unless @is_expanded do %>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-3">
|
||||
<%= if @title_tag == "h3" do %>
|
||||
<h3 class={"#{@text_size} font-medium #{if @depth == 0, do: "text-base-content", else: "text-base-content/80"}"}>{@location.name}</h3>
|
||||
<% else %>
|
||||
<h4 class={"#{@text_size} font-medium #{if @depth == 0, do: "text-base-content", else: "text-base-content/80"}"}>{@location.name}</h4>
|
||||
<% end %>
|
||||
<span class="text-xs text-base-content/50">
|
||||
({@count_display})
|
||||
</span>
|
||||
<%= if @location.apriltag_id do %>
|
||||
<span class="text-xs bg-base-200 text-base-content/80 px-2 py-1 rounded">
|
||||
AprilTag: {@location.apriltag_id}
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 ml-4">
|
||||
<button
|
||||
phx-click="show_edit_form"
|
||||
phx-value-id={@location.id}
|
||||
class="inline-flex items-center p-1.5 border border-transparent rounded-full shadow-sm text-white bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
<.icon name="hero-pencil" class="w-3 h-3" />
|
||||
</button>
|
||||
<button
|
||||
phx-click="delete_location"
|
||||
phx-value-id={@location.id}
|
||||
data-confirm="Are you sure you want to delete this storage location? This action cannot be undone."
|
||||
class="inline-flex items-center p-1.5 border border-transparent rounded-full shadow-sm text-white bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
<.icon name="hero-trash" class="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Expanded view -->
|
||||
<%= if @is_expanded do %>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<%= if @title_tag == "h3" do %>
|
||||
<h3 class={"#{@text_size} font-medium #{if @depth == 0, do: "text-base-content", else: "text-base-content/80"}"}>{@location.name}</h3>
|
||||
<% else %>
|
||||
<h4 class={"#{@text_size} font-medium #{if @depth == 0, do: "text-base-content", else: "text-base-content/80"}"}>{@location.name}</h4>
|
||||
<% end %>
|
||||
<%= if @location.description do %>
|
||||
<p class="text-sm text-base-content/60 mt-1">{@location.description}</p>
|
||||
<% end %>
|
||||
<div class="flex items-center space-x-2 mt-1">
|
||||
<p class="text-xs text-base-content/50">
|
||||
{@count_display}
|
||||
</p>
|
||||
<%= if @location.apriltag_id do %>
|
||||
<span class="text-xs bg-base-200 text-base-content/80 px-2 py-1 rounded">
|
||||
AprilTag: {@location.apriltag_id}
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start space-x-3 ml-4">
|
||||
<%= if @location.apriltag_id do %>
|
||||
<%= if get_apriltag_url(@location) do %>
|
||||
<div class="apriltag-container flex-shrink-0">
|
||||
<img
|
||||
src={get_apriltag_url(@location)}
|
||||
alt={"AprilTag for #{@location.name}"}
|
||||
class="w-16 h-auto border border-base-300 rounded bg-base-100"
|
||||
onerror="this.style.display='none'"
|
||||
/>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="w-16 h-16 border border-base-300 rounded bg-base-200 flex items-center justify-center flex-shrink-0">
|
||||
<.icon name="hero-qr-code" class="w-8 h-8 text-base-content/50" />
|
||||
</div>
|
||||
<% end %>
|
||||
<button
|
||||
phx-click="download_apriltag"
|
||||
phx-value-id={@location.id}
|
||||
class="inline-flex items-center px-3 py-1.5 border border-base-300 rounded-md shadow-sm text-sm font-medium text-base-content bg-base-100 hover:bg-base-200 flex-shrink-0"
|
||||
title="Download AprilTag"
|
||||
>
|
||||
<.icon name="hero-arrow-down-tray" class="w-4 h-4 mr-1.5" />
|
||||
Download
|
||||
</button>
|
||||
<% end %>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button
|
||||
phx-click="show_edit_form"
|
||||
phx-value-id={@location.id}
|
||||
class="inline-flex items-center p-2 border border-transparent rounded-full shadow-sm text-white bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
<.icon name="hero-pencil" class="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
phx-click="delete_location"
|
||||
phx-value-id={@location.id}
|
||||
data-confirm="Are you sure you want to delete this storage location? This action cannot be undone."
|
||||
class="inline-flex items-center p-2 border border-transparent rounded-full shadow-sm text-white bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
<.icon name="hero-trash" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Render children recursively -->
|
||||
<%= for child <- @children do %>
|
||||
<.location_item location={child} storage_locations={@storage_locations} depth={@depth + 1} />
|
||||
|
||||
<!-- Render children recursively (only when expanded) -->
|
||||
<%= if @is_expanded do %>
|
||||
<%= for child <- @children do %>
|
||||
<.location_item location={child} storage_locations={@storage_locations} expanded_locations={@expanded_locations} depth={@depth + 1} />
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
@@ -780,7 +879,7 @@ defmodule ComponentsElixirWeb.StorageLocationsLive do
|
||||
<!-- Recursive Storage Location Tree -->
|
||||
<%= for location <- root_storage_locations(@storage_locations) do %>
|
||||
<div class="px-6 py-4">
|
||||
<.location_item location={location} storage_locations={@storage_locations} depth={0} />
|
||||
<.location_item location={location} storage_locations={@storage_locations} expanded_locations={@expanded_locations} depth={0} />
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user