style: format codebase
This commit is contained in:
@@ -31,6 +31,7 @@ defmodule ComponentsElixir.AprilTag do
|
||||
def valid_apriltag_id?(id) when is_integer(id) do
|
||||
id >= 0 and id < @tag36h11_count
|
||||
end
|
||||
|
||||
def valid_apriltag_id?(_), do: false
|
||||
|
||||
@doc """
|
||||
@@ -78,8 +79,8 @@ defmodule ComponentsElixir.AprilTag do
|
||||
def used_apriltag_ids do
|
||||
ComponentsElixir.Repo.all(
|
||||
from sl in ComponentsElixir.Inventory.StorageLocation,
|
||||
where: not is_nil(sl.apriltag_id),
|
||||
select: sl.apriltag_id
|
||||
where: not is_nil(sl.apriltag_id),
|
||||
select: sl.apriltag_id
|
||||
)
|
||||
end
|
||||
|
||||
@@ -130,10 +131,11 @@ defmodule ComponentsElixir.AprilTag do
|
||||
This should be run once during setup to pre-generate all AprilTag images.
|
||||
"""
|
||||
def generate_all_apriltag_svgs(opts \\ []) do
|
||||
static_dir = Path.join([
|
||||
Application.app_dir(:components_elixir, "priv/static"),
|
||||
"apriltags"
|
||||
])
|
||||
static_dir =
|
||||
Path.join([
|
||||
Application.app_dir(:components_elixir, "priv/static"),
|
||||
"apriltags"
|
||||
])
|
||||
|
||||
# Ensure directory exists
|
||||
File.mkdir_p!(static_dir)
|
||||
@@ -187,10 +189,12 @@ defmodule ComponentsElixir.AprilTag do
|
||||
"""
|
||||
def cleanup_apriltag_svg(apriltag_id) do
|
||||
filename = "tag36h11_id_#{String.pad_leading(to_string(apriltag_id), 3, "0")}.svg"
|
||||
file_path = Path.join([
|
||||
Application.app_dir(:components_elixir, "priv/static/apriltags"),
|
||||
filename
|
||||
])
|
||||
|
||||
file_path =
|
||||
Path.join([
|
||||
Application.app_dir(:components_elixir, "priv/static/apriltags"),
|
||||
filename
|
||||
])
|
||||
|
||||
if File.exists?(file_path) do
|
||||
File.rm(file_path)
|
||||
|
||||
@@ -16,6 +16,7 @@ defmodule ComponentsElixir.AprilTag.Tag36h11 do
|
||||
case Regex.run(~r/\(april\.tag\.Tag36h11, id = (\d+)\) <([0-9a-fA-F]+)> maketag/, line) do
|
||||
[_, id_str, hex_pattern] ->
|
||||
{String.to_integer(id_str), String.downcase(hex_pattern)}
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
@@ -23,26 +24,30 @@ defmodule ComponentsElixir.AprilTag.Tag36h11 do
|
||||
|
||||
# Extract patterns from PostScript file at compile time
|
||||
@all_patterns (
|
||||
path = Path.join([File.cwd!(), "apriltags.ps"])
|
||||
path = Path.join([File.cwd!(), "apriltags.ps"])
|
||||
|
||||
if File.exists?(path) do
|
||||
File.read!(path)
|
||||
|> String.split("\n")
|
||||
|> Enum.filter(&String.contains?(&1, "april.tag.Tag36h11"))
|
||||
|> Enum.map(fn line ->
|
||||
case Regex.run(~r/\(april\.tag\.Tag36h11, id = (\d+)\) <([0-9a-fA-F]+)> maketag/, line) do
|
||||
[_, id_str, hex_pattern] ->
|
||||
{String.to_integer(id_str), String.downcase(hex_pattern)}
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|> Map.new()
|
||||
else
|
||||
raise "Error: apriltags.ps file not found in project root. This file is required for AprilTag pattern generation."
|
||||
end
|
||||
)
|
||||
if File.exists?(path) do
|
||||
File.read!(path)
|
||||
|> String.split("\n")
|
||||
|> Enum.filter(&String.contains?(&1, "april.tag.Tag36h11"))
|
||||
|> Enum.map(fn line ->
|
||||
case Regex.run(
|
||||
~r/\(april\.tag\.Tag36h11, id = (\d+)\) <([0-9a-fA-F]+)> maketag/,
|
||||
line
|
||||
) do
|
||||
[_, id_str, hex_pattern] ->
|
||||
{String.to_integer(id_str), String.downcase(hex_pattern)}
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|> Map.new()
|
||||
else
|
||||
raise "Error: apriltags.ps file not found in project root. This file is required for AprilTag pattern generation."
|
||||
end
|
||||
)
|
||||
|
||||
# Sample of real tag36h11 hex patterns from AprilRobotics repository
|
||||
# This will be populated with patterns extracted from the PostScript file
|
||||
@@ -64,7 +69,8 @@ defmodule ComponentsElixir.AprilTag.Tag36h11 do
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|> Map.new()
|
||||
else
|
||||
%{} # Return empty map if file not found, will fall back to hardcoded patterns
|
||||
# Return empty map if file not found, will fall back to hardcoded patterns
|
||||
%{}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -76,6 +82,7 @@ defmodule ComponentsElixir.AprilTag.Tag36h11 do
|
||||
def get_hex_pattern(id) when is_integer(id) and id >= 0 and id < 587 do
|
||||
Map.get(@tag36h11_patterns, id)
|
||||
end
|
||||
|
||||
def get_hex_pattern(_), do: nil
|
||||
|
||||
@doc """
|
||||
@@ -97,6 +104,7 @@ defmodule ComponentsElixir.AprilTag.Tag36h11 do
|
||||
|
||||
# Each scanline is padded to a byte boundary: 3 bytes per 10 pixels (2 bpp)
|
||||
row_bytes = 3
|
||||
|
||||
rows =
|
||||
for row <- 0..9 do
|
||||
<<_::binary-size(row * row_bytes), r::binary-size(row_bytes), _::binary>> = bytes
|
||||
@@ -104,12 +112,23 @@ defmodule ComponentsElixir.AprilTag.Tag36h11 do
|
||||
|
||||
samples =
|
||||
[
|
||||
b0 >>> 6 &&& 0x3, b0 >>> 4 &&& 0x3, b0 >>> 2 &&& 0x3, b0 &&& 0x3,
|
||||
b1 >>> 6 &&& 0x3, b1 >>> 4 &&& 0x3, b1 >>> 2 &&& 0x3, b1 &&& 0x3,
|
||||
b2 >>> 6 &&& 0x3, b2 >>> 4 &&& 0x3, b2 >>> 2 &&& 0x3, b2 &&& 0x3
|
||||
b0 >>> 6 &&& 0x3,
|
||||
b0 >>> 4 &&& 0x3,
|
||||
b0 >>> 2 &&& 0x3,
|
||||
b0 &&& 0x3,
|
||||
b1 >>> 6 &&& 0x3,
|
||||
b1 >>> 4 &&& 0x3,
|
||||
b1 >>> 2 &&& 0x3,
|
||||
b1 &&& 0x3,
|
||||
b2 >>> 6 &&& 0x3,
|
||||
b2 >>> 4 &&& 0x3,
|
||||
b2 >>> 2 &&& 0x3,
|
||||
b2 &&& 0x3
|
||||
]
|
||||
|> Enum.take(10) # drop the 2 padding samples at end of row
|
||||
|> Enum.map(&(&1 == 0)) # 0 = black, 3 = white → boolean
|
||||
# drop the 2 padding samples at end of row
|
||||
|> Enum.take(10)
|
||||
# 0 = black, 3 = white → boolean
|
||||
|> Enum.map(&(&1 == 0))
|
||||
|
||||
samples
|
||||
end
|
||||
@@ -133,7 +152,8 @@ defmodule ComponentsElixir.AprilTag.Tag36h11 do
|
||||
Only black modules are drawn over a white background.
|
||||
"""
|
||||
def binary_matrix_to_svg(binary_matrix, opts \\ []) do
|
||||
size = Keyword.get(opts, :size, 200) # final CSS size in px
|
||||
# final CSS size in px
|
||||
size = Keyword.get(opts, :size, 200)
|
||||
id_text = Keyword.get(opts, :id_text, "")
|
||||
|
||||
# binary_matrix is 10x10 of booleans: true=black, false=white
|
||||
@@ -172,9 +192,11 @@ defmodule ComponentsElixir.AprilTag.Tag36h11 do
|
||||
|
||||
<!-- caption -->
|
||||
#{if id_text != "" do
|
||||
~s(<text x="#{modules_w/2}" y="#{modules_h + 1.4}" text-anchor="middle"
|
||||
~s(<text x="#{modules_w / 2}" y="#{modules_h + 1.4}" text-anchor="middle"
|
||||
font-family="Arial" font-size="0.9">#{id_text}</text>)
|
||||
else "" end}
|
||||
else
|
||||
""
|
||||
end}
|
||||
</svg>
|
||||
"""
|
||||
end
|
||||
@@ -197,11 +219,13 @@ defmodule ComponentsElixir.AprilTag.Tag36h11 do
|
||||
opts_with_id = Keyword.put(opts, :id_text, id_text)
|
||||
binary_matrix_to_svg(binary_matrix, opts_with_id)
|
||||
end
|
||||
end # Generate a placeholder pattern for IDs we don't have real data for yet
|
||||
end
|
||||
|
||||
# Generate a placeholder pattern for IDs we don't have real data for yet
|
||||
defp generate_placeholder_svg(id, opts) do
|
||||
size = Keyword.get(opts, :size, 200)
|
||||
margin = Keyword.get(opts, :margin, div(size, 10))
|
||||
square_size = size - (2 * margin)
|
||||
square_size = size - 2 * margin
|
||||
|
||||
"""
|
||||
<svg width="#{size}" height="#{size + 30}" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
@@ -28,6 +28,7 @@ defmodule ComponentsElixir.DatasheetDownloader do
|
||||
case URI.parse(url) do
|
||||
%URI{scheme: scheme} when scheme in ["http", "https"] ->
|
||||
{:ok, URI.parse(url)}
|
||||
|
||||
_ ->
|
||||
{:error, "Invalid URL scheme. Only HTTP and HTTPS are supported."}
|
||||
end
|
||||
@@ -36,9 +37,12 @@ defmodule ComponentsElixir.DatasheetDownloader do
|
||||
defp generate_filename(url) do
|
||||
# Try to extract a meaningful filename from the URL
|
||||
uri = URI.parse(url)
|
||||
|
||||
original_filename =
|
||||
case Path.basename(uri.path || "") do
|
||||
"" -> "datasheet"
|
||||
"" ->
|
||||
"datasheet"
|
||||
|
||||
basename ->
|
||||
# Remove extension and sanitize
|
||||
basename
|
||||
@@ -54,10 +58,14 @@ defmodule ComponentsElixir.DatasheetDownloader do
|
||||
|
||||
defp sanitize_filename(filename) do
|
||||
filename
|
||||
|> String.replace(~r/[^\w\-_]/, "_") # Replace non-word chars with underscores
|
||||
|> String.replace(~r/_+/, "_") # Replace multiple underscores with single
|
||||
|> String.trim("_") # Remove leading/trailing underscores
|
||||
|> String.slice(0, 50) # Limit length
|
||||
# Replace non-word chars with underscores
|
||||
|> String.replace(~r/[^\w\-_]/, "_")
|
||||
# Replace multiple underscores with single
|
||||
|> String.replace(~r/_+/, "_")
|
||||
# Remove leading/trailing underscores
|
||||
|> String.trim("_")
|
||||
# Limit length
|
||||
|> String.slice(0, 50)
|
||||
|> case do
|
||||
"" -> "datasheet"
|
||||
name -> name
|
||||
@@ -66,17 +74,19 @@ defmodule ComponentsElixir.DatasheetDownloader do
|
||||
|
||||
defp fetch_pdf(url) do
|
||||
case Req.get(url,
|
||||
redirect: true,
|
||||
max_redirects: 5,
|
||||
receive_timeout: 30_000,
|
||||
headers: [
|
||||
{"User-Agent", "ComponentSystem/1.0 DatasheetDownloader"}
|
||||
]
|
||||
) do
|
||||
redirect: true,
|
||||
max_redirects: 5,
|
||||
receive_timeout: 30_000,
|
||||
headers: [
|
||||
{"User-Agent", "ComponentSystem/1.0 DatasheetDownloader"}
|
||||
]
|
||||
) do
|
||||
{:ok, %Req.Response{status: 200} = response} ->
|
||||
{:ok, response}
|
||||
|
||||
{:ok, %Req.Response{status: status}} ->
|
||||
{:error, "HTTP error: #{status}"}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to download PDF from #{url}: #{inspect(reason)}")
|
||||
{:error, "Download failed: #{inspect(reason)}"}
|
||||
@@ -88,6 +98,7 @@ defmodule ComponentsElixir.DatasheetDownloader do
|
||||
case body do
|
||||
<<"%PDF", _rest::binary>> ->
|
||||
:ok
|
||||
|
||||
_ ->
|
||||
{:error, "Downloaded content is not a valid PDF file"}
|
||||
end
|
||||
@@ -105,10 +116,12 @@ defmodule ComponentsElixir.DatasheetDownloader do
|
||||
:ok ->
|
||||
Logger.info("Successfully saved datasheet: #{filename}")
|
||||
:ok
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to save datasheet file: #{inspect(reason)}")
|
||||
{:error, "Failed to save file: #{inspect(reason)}"}
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to create datasheets directory: #{inspect(reason)}")
|
||||
{:error, "Failed to create directory: #{inspect(reason)}"}
|
||||
@@ -129,6 +142,7 @@ defmodule ComponentsElixir.DatasheetDownloader do
|
||||
:ok ->
|
||||
Logger.info("Deleted datasheet file: #{filename}")
|
||||
:ok
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warning("Failed to delete datasheet file #{filename}: #{inspect(reason)}")
|
||||
{:error, reason}
|
||||
|
||||
@@ -19,7 +19,7 @@ defmodule ComponentsElixir.Inventory do
|
||||
locations =
|
||||
StorageLocation
|
||||
|> order_by([sl], asc: sl.name)
|
||||
|> preload([parent: [parent: [parent: [parent: :parent]]]])
|
||||
|> preload(parent: [parent: [parent: [parent: :parent]]])
|
||||
|> Repo.all()
|
||||
|
||||
# Ensure AprilTag SVGs exist for all locations
|
||||
@@ -162,7 +162,7 @@ defmodule ComponentsElixir.Inventory do
|
||||
"""
|
||||
def list_categories do
|
||||
Category
|
||||
|> preload([parent: [parent: [parent: [parent: :parent]]]])
|
||||
|> preload(parent: [parent: [parent: [parent: :parent]]])
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@@ -217,8 +217,15 @@ defmodule ComponentsElixir.Inventory do
|
||||
|
||||
# Verify the category exists before getting descendants
|
||||
case Enum.find(categories, &(&1.id == category_id)) do
|
||||
nil -> []
|
||||
_category -> ComponentsElixir.Inventory.Hierarchical.descendant_ids(categories, category_id, &(&1.parent_id))
|
||||
nil ->
|
||||
[]
|
||||
|
||||
_category ->
|
||||
ComponentsElixir.Inventory.Hierarchical.descendant_ids(
|
||||
categories,
|
||||
category_id,
|
||||
& &1.parent_id
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -233,13 +240,21 @@ defmodule ComponentsElixir.Inventory do
|
||||
for typical storage location tree sizes (hundreds of locations). For very large storage location trees,
|
||||
a recursive CTE query could be used instead.
|
||||
"""
|
||||
def get_storage_location_and_descendant_ids(storage_location_id) when is_integer(storage_location_id) do
|
||||
def get_storage_location_and_descendant_ids(storage_location_id)
|
||||
when is_integer(storage_location_id) do
|
||||
storage_locations = list_storage_locations()
|
||||
|
||||
# Verify the storage location exists before getting descendants
|
||||
case Enum.find(storage_locations, &(&1.id == storage_location_id)) do
|
||||
nil -> []
|
||||
_storage_location -> ComponentsElixir.Inventory.Hierarchical.descendant_ids(storage_locations, storage_location_id, &(&1.parent_id))
|
||||
nil ->
|
||||
[]
|
||||
|
||||
_storage_location ->
|
||||
ComponentsElixir.Inventory.Hierarchical.descendant_ids(
|
||||
storage_locations,
|
||||
storage_location_id,
|
||||
& &1.parent_id
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -306,7 +321,7 @@ defmodule ComponentsElixir.Inventory do
|
||||
}
|
||||
|
||||
defp get_sort_order(criteria) do
|
||||
Map.get(@sort_orders, criteria, [asc: :name, asc: :id])
|
||||
Map.get(@sort_orders, criteria, asc: :name, asc: :id)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -338,10 +353,12 @@ defmodule ComponentsElixir.Inventory do
|
||||
case ComponentsElixir.DatasheetDownloader.download_pdf_from_url(url) do
|
||||
{:ok, filename} ->
|
||||
Map.put(attrs, "datasheet_filename", filename)
|
||||
|
||||
{:error, _reason} ->
|
||||
# Continue without datasheet file if download fails
|
||||
attrs
|
||||
end
|
||||
|
||||
_ ->
|
||||
attrs
|
||||
end
|
||||
@@ -372,13 +389,18 @@ defmodule ComponentsElixir.Inventory do
|
||||
{:ok, filename} ->
|
||||
# Delete old datasheet file if it exists
|
||||
if component.datasheet_filename do
|
||||
ComponentsElixir.DatasheetDownloader.delete_datasheet_file(component.datasheet_filename)
|
||||
ComponentsElixir.DatasheetDownloader.delete_datasheet_file(
|
||||
component.datasheet_filename
|
||||
)
|
||||
end
|
||||
|
||||
Map.put(attrs, "datasheet_filename", filename)
|
||||
|
||||
{:error, _reason} ->
|
||||
# Keep existing filename if download fails
|
||||
attrs
|
||||
end
|
||||
|
||||
_ ->
|
||||
attrs
|
||||
end
|
||||
|
||||
@@ -37,7 +37,7 @@ defmodule ComponentsElixir.Inventory.Category do
|
||||
"""
|
||||
@impl true
|
||||
def full_path(%Category{} = category) do
|
||||
Hierarchical.full_path(category, &(&1.parent), path_separator())
|
||||
Hierarchical.full_path(category, & &1.parent, path_separator())
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
||||
@@ -30,7 +30,18 @@ defmodule ComponentsElixir.Inventory.Component do
|
||||
@doc false
|
||||
def changeset(component, attrs) do
|
||||
component
|
||||
|> cast(attrs, [:name, :description, :keywords, :position, :count, :datasheet_url, :datasheet_filename, :image_filename, :category_id, :storage_location_id])
|
||||
|> cast(attrs, [
|
||||
:name,
|
||||
:description,
|
||||
:keywords,
|
||||
:position,
|
||||
:count,
|
||||
:datasheet_url,
|
||||
:datasheet_filename,
|
||||
:image_filename,
|
||||
:category_id,
|
||||
:storage_location_id
|
||||
])
|
||||
|> validate_required([:name, :category_id])
|
||||
|> validate_length(:name, min: 1, max: 255)
|
||||
|> validate_length(:description, max: 2000)
|
||||
|
||||
@@ -29,10 +29,12 @@ defmodule ComponentsElixir.Inventory.Hierarchical do
|
||||
case parent_accessor_fn.(entity) do
|
||||
nil ->
|
||||
entity.name
|
||||
|
||||
%Ecto.Association.NotLoaded{} ->
|
||||
# Parent not loaded - fall back to database lookup
|
||||
# This is a fallback and should be rare if preloading is done correctly
|
||||
build_path_with_db_lookup(entity, separator)
|
||||
|
||||
parent ->
|
||||
"#{full_path(parent, parent_accessor_fn, separator)}#{separator}#{entity.name}"
|
||||
end
|
||||
@@ -52,12 +54,14 @@ defmodule ComponentsElixir.Inventory.Hierarchical do
|
||||
nil ->
|
||||
# This is a root entity, add its name and return the complete path
|
||||
[entity.name | path_so_far]
|
||||
|
||||
parent_id ->
|
||||
# Load parent from database
|
||||
case load_parent_entity(entity, parent_id) do
|
||||
nil ->
|
||||
# Parent not found (orphaned record), treat this as root
|
||||
[entity.name | path_so_far]
|
||||
|
||||
parent ->
|
||||
# Recursively get the path from the parent, then add current entity
|
||||
collect_path_from_root(parent, [entity.name | path_so_far])
|
||||
@@ -93,9 +97,9 @@ defmodule ComponentsElixir.Inventory.Hierarchical do
|
||||
entity_id = id_accessor_fn.(entity)
|
||||
|
||||
# Remove self-reference
|
||||
entity_id == editing_entity_id ||
|
||||
# Remove descendants (they would create a cycle)
|
||||
descendant?(entities, entity_id, editing_entity_id, parent_id_accessor_fn)
|
||||
entity_id == editing_entity_id ||
|
||||
descendant?(entities, entity_id, editing_entity_id, parent_id_accessor_fn)
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -114,13 +118,21 @@ defmodule ComponentsElixir.Inventory.Hierarchical do
|
||||
|
||||
defp descendant_recursive?(entities, entity, ancestor_id, parent_id_accessor_fn) do
|
||||
case parent_id_accessor_fn.(entity) do
|
||||
nil -> false
|
||||
^ancestor_id -> true
|
||||
nil ->
|
||||
false
|
||||
|
||||
^ancestor_id ->
|
||||
true
|
||||
|
||||
parent_id ->
|
||||
parent = Enum.find(entities, fn e -> e.id == parent_id end)
|
||||
|
||||
case parent do
|
||||
nil -> false
|
||||
parent_entity -> descendant_recursive?(entities, parent_entity, ancestor_id, parent_id_accessor_fn)
|
||||
nil ->
|
||||
false
|
||||
|
||||
parent_entity ->
|
||||
descendant_recursive?(entities, parent_entity, ancestor_id, parent_id_accessor_fn)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -182,15 +194,20 @@ defmodule ComponentsElixir.Inventory.Hierarchical do
|
||||
Includes proper filtering to prevent cycles and formatted display names.
|
||||
Results are sorted hierarchically for intuitive navigation.
|
||||
"""
|
||||
def parent_select_options(entities, editing_entity_id, parent_accessor_fn, nil_option_text \\ "No parent") do
|
||||
def parent_select_options(
|
||||
entities,
|
||||
editing_entity_id,
|
||||
parent_accessor_fn,
|
||||
nil_option_text \\ "No parent"
|
||||
) do
|
||||
available_entities =
|
||||
filter_parent_options(
|
||||
entities,
|
||||
editing_entity_id,
|
||||
&(&1.id),
|
||||
&(&1.parent_id)
|
||||
& &1.id,
|
||||
& &1.parent_id
|
||||
)
|
||||
|> sort_hierarchically(&(&1.parent_id))
|
||||
|> sort_hierarchically(& &1.parent_id)
|
||||
|> Enum.map(fn entity ->
|
||||
{display_name(entity, parent_accessor_fn), entity.id}
|
||||
end)
|
||||
@@ -205,7 +222,7 @@ defmodule ComponentsElixir.Inventory.Hierarchical do
|
||||
def select_options(entities, parent_accessor_fn, nil_option_text \\ nil) do
|
||||
sorted_entities =
|
||||
entities
|
||||
|> sort_hierarchically(&(&1.parent_id))
|
||||
|> sort_hierarchically(& &1.parent_id)
|
||||
|> Enum.map(fn entity ->
|
||||
{display_name(entity, parent_accessor_fn), entity.id}
|
||||
end)
|
||||
@@ -300,9 +317,10 @@ defmodule ComponentsElixir.Inventory.Hierarchical do
|
||||
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)
|
||||
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
|
||||
@@ -320,7 +338,13 @@ defmodule ComponentsElixir.Inventory.Hierarchical do
|
||||
- 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
|
||||
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
|
||||
|
||||
@@ -25,7 +25,7 @@ defmodule ComponentsElixir.Inventory.StorageLocation do
|
||||
timestamps(type: :naive_datetime_usec)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@doc false
|
||||
def changeset(storage_location, attrs) do
|
||||
storage_location
|
||||
|> cast(attrs, [:name, :description, :parent_id, :apriltag_id])
|
||||
@@ -40,7 +40,7 @@ defmodule ComponentsElixir.Inventory.StorageLocation do
|
||||
# HierarchicalSchema implementations
|
||||
@impl true
|
||||
def full_path(%StorageLocation{} = storage_location) do
|
||||
Hierarchical.full_path(storage_location, &(&1.parent), path_separator())
|
||||
Hierarchical.full_path(storage_location, & &1.parent, path_separator())
|
||||
end
|
||||
|
||||
@impl true
|
||||
@@ -80,11 +80,12 @@ defmodule ComponentsElixir.Inventory.StorageLocation do
|
||||
|
||||
defp get_next_available_apriltag_id do
|
||||
# Get all used AprilTag IDs
|
||||
used_ids = ComponentsElixir.Repo.all(
|
||||
from sl in ComponentsElixir.Inventory.StorageLocation,
|
||||
where: not is_nil(sl.apriltag_id),
|
||||
select: sl.apriltag_id
|
||||
)
|
||||
used_ids =
|
||||
ComponentsElixir.Repo.all(
|
||||
from sl in ComponentsElixir.Inventory.StorageLocation,
|
||||
where: not is_nil(sl.apriltag_id),
|
||||
select: sl.apriltag_id
|
||||
)
|
||||
|
||||
# Find the first available ID (0-586)
|
||||
0..586
|
||||
@@ -93,7 +94,9 @@ defmodule ComponentsElixir.Inventory.StorageLocation do
|
||||
nil ->
|
||||
# All IDs are used - this should be handled at the application level
|
||||
raise "All AprilTag IDs are in use"
|
||||
id -> id
|
||||
|
||||
id ->
|
||||
id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user