chore: added more @spec

This commit is contained in:
2026-05-01 17:49:50 +02:00
parent abcae1dad7
commit 881056eb61
157 changed files with 6223 additions and 1647 deletions

View File

@@ -6,8 +6,10 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.DraftManagement do
alias BDS.Desktop.ShellLive.MenuEditor.PageCategory
alias BDS.Desktop.ShellLive.MenuEditor.TreeOps
@spec current_draft(term()) :: term()
def current_draft(assigns), do: Map.get(assigns.menu_editor_state || %{}, :draft)
@spec start_page_draft(term()) :: term()
def start_page_draft(state) do
item = %{
item_id: Ecto.UUID.generate(),
@@ -29,6 +31,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.DraftManagement do
}
end
@spec start_category_draft(term()) :: term()
def start_category_draft(state) do
item = %{
item_id: Ecto.UUID.generate(),
@@ -50,6 +53,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.DraftManagement do
}
end
@spec finalize_submenu_draft(term()) :: term()
def finalize_submenu_draft(%{draft: %{item_id: item_id, query: query}} = state) do
label =
if(String.trim(query) == "",
@@ -69,12 +73,19 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.DraftManagement do
def finalize_submenu_draft(state), do: state
@spec assign_page_to_draft(term(), term()) :: term()
def assign_page_to_draft(%{draft: %{item_id: item_id}} = state, post) do
%{
state
| items:
TreeOps.update_item(state.items, item_id, fn item ->
%{item | kind: :page, label: post.title, slug: PageCategory.blank_to_nil(post.slug), children: []}
%{
item
| kind: :page,
label: post.title,
slug: PageCategory.blank_to_nil(post.slug),
children: []
}
end),
draft: nil
}
@@ -82,6 +93,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.DraftManagement do
def assign_page_to_draft(state, _post), do: state
@spec assign_category_to_draft(term(), term()) :: term()
def assign_category_to_draft(%{draft: %{item_id: item_id}} = state, category) do
label = PageCategory.blank_to_nil(category.title) || category.name
@@ -97,6 +109,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.DraftManagement do
def assign_category_to_draft(state, _category), do: state
@spec cancel_draft(term()) :: term()
def cancel_draft(%{draft: %{item_id: item_id}} = state) do
items = TreeOps.remove_item(state.items, item_id)
%{state | items: items, selected_id: TreeOps.first_item_id(items), draft: nil}
@@ -104,6 +117,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.DraftManagement do
def cancel_draft(state), do: state
@spec confirm_category_draft(term(), term()) :: term()
def confirm_category_draft(socket, update_state_fun) do
project_id = socket.assigns.projects.active_project_id
draft = current_draft(socket.assigns)
@@ -117,8 +131,12 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.DraftManagement do
category =
cond do
category != nil -> category
normalized == "" -> %{name: "", title: ""}
category != nil ->
category
normalized == "" ->
%{name: "", title: ""}
true ->
{:ok, _metadata} = Metadata.add_category(project_id, normalized)
%{name: normalized, title: normalized}

View File

@@ -6,19 +6,26 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.PageCategory do
alias BDS.{Metadata, Repo}
alias BDS.Posts.Post
@spec page_posts(term()) :: term()
def page_posts(nil), do: []
def page_posts(project_id) do
Repo.all(from post in Post, where: post.project_id == ^project_id, order_by: [asc: post.title, asc: post.slug])
Repo.all(
from post in Post,
where: post.project_id == ^project_id,
order_by: [asc: post.title, asc: post.slug]
)
|> Enum.filter(&("page" in (&1.categories || [])))
end
@spec page_post(term(), term()) :: term()
def page_post(nil, _post_id), do: nil
def page_post(project_id, post_id) do
Enum.find(page_posts(project_id), &(&1.id == post_id))
end
@spec filter_page_posts(term(), term()) :: term()
def filter_page_posts(posts, query) do
normalized = query |> to_string() |> String.trim() |> String.downcase()
@@ -29,6 +36,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.PageCategory do
end)
end
@spec category_options(term()) :: term()
def category_options(nil), do: []
def category_options(project_id) do
@@ -40,6 +48,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.PageCategory do
end)
end
@spec filter_categories(term(), term()) :: term()
def filter_categories(categories, query) do
normalized = query |> to_string() |> String.trim() |> String.downcase()
@@ -50,7 +59,9 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.PageCategory do
end)
end
@spec blank_to_nil(term()) :: term()
def blank_to_nil(nil), do: nil
def blank_to_nil(value) do
trimmed = String.trim(to_string(value))
if trimmed == "", do: nil, else: trimmed

View File

@@ -7,6 +7,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.State do
alias BDS.Menu
alias BDS.Desktop.ShellLive.MenuEditor.{PageCategory, TreeOps, TreePredicates}
@spec ensure_state(term()) :: term()
def ensure_state(assigns) do
project_id = assigns.projects.active_project_id
@@ -16,11 +17,13 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.State do
end
end
@spec update_state(term(), term()) :: term()
def update_state(socket, updater) do
state = ensure_state(socket.assigns)
assign(socket, :menu_editor_state, updater.(state))
end
@spec build(term(), term()) :: term()
def build(_assigns, state) do
categories = PageCategory.category_options(state.project_id)
draft = state.draft
@@ -35,7 +38,8 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.State do
draft_query: draft_query,
filtered_pages:
if(match?(%{type: :page}, draft),
do: PageCategory.filter_page_posts(PageCategory.page_posts(state.project_id), draft_query),
do:
PageCategory.filter_page_posts(PageCategory.page_posts(state.project_id), draft_query),
else: []
),
filtered_categories:
@@ -53,6 +57,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.State do
}
end
@spec save(term(), term(), term()) :: term()
def save(socket, reload, append_output) do
state = socket.assigns.menu_editor_state
@@ -60,12 +65,22 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.State do
Menu.update_menu(state.project_id, Enum.map(state.items, &TreeOps.persisted_item/1))
socket
|> append_output.(translated("menuEditor.tabTitle"), translated("menuEditor.saved"), nil, "info")
|> append_output.(
translated("menuEditor.tabTitle"),
translated("menuEditor.saved"),
nil,
"info"
)
|> reload.(socket.assigns.workbench)
end
defp load_state(nil) do
%{project_id: nil, items: [TreeOps.home_item()], selected_id: TreeOps.home_item_id(), draft: nil}
%{
project_id: nil,
items: [TreeOps.home_item()],
selected_id: TreeOps.home_item_id(),
draft: nil
}
end
defp load_state(project_id) do

View File

@@ -3,12 +3,15 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
@home_item_id "menu-home"
@spec home_item_id() :: term()
def home_item_id, do: @home_item_id
@spec home_item() :: term()
def home_item do
%{item_id: @home_item_id, kind: :home, label: "Home", slug: nil, children: [], is_home: true}
end
@spec ui_item(term()) :: term()
def ui_item(%{kind: :home}), do: home_item()
def ui_item(item) do
@@ -24,25 +27,37 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
}
end
@spec persisted_item(term()) :: term()
def persisted_item(%{kind: :home}), do: %{kind: :home, label: "Home", slug: nil}
def persisted_item(%{kind: :submenu} = item) do
%{kind: :submenu, label: item.label, slug: nil, children: Enum.map(item.children || [], &persisted_item/1)}
%{
kind: :submenu,
label: item.label,
slug: nil,
children: Enum.map(item.children || [], &persisted_item/1)
}
end
def persisted_item(item) do
%{kind: item.kind, label: item.label, slug: item.slug}
end
@spec first_item_id(term()) :: term()
def first_item_id([item | _rest]), do: item.item_id
def first_item_id([]), do: nil
@spec insert_target(term(), term()) :: term()
def insert_target(items, nil), do: {[], length(items)}
def insert_target(items, selected_id) do
case find_path(items, selected_id) do
nil -> {[], length(items)}
[] -> {[], length(items)}
nil ->
{[], length(items)}
[] ->
{[], length(items)}
path ->
case item_at_path(items, path) do
%{kind: :submenu} -> {path, 0}
@@ -51,9 +66,12 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
end
end
@spec path_prefix?(term(), term()) :: term()
def path_prefix?(prefix, path) when length(prefix) > length(path), do: false
@spec path_prefix?(term(), term()) :: term()
def path_prefix?(prefix, path), do: Enum.take(path, length(prefix)) == prefix
@spec find_path(term(), term(), term()) :: term()
def find_path(items, item_id, path \\ []) do
Enum.find_value(Enum.with_index(items), fn {item, index} ->
next_path = path ++ [index]
@@ -71,6 +89,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
end)
end
@spec item_at_path(term(), term()) :: term()
def item_at_path(_items, []), do: nil
def item_at_path(items, [index]) do
@@ -84,6 +103,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
end
end
@spec items_at_path(term(), term()) :: term()
def items_at_path(items, []), do: items
def items_at_path(items, [index | rest]) do
@@ -93,6 +113,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
end
end
@spec replace_items_at_path(term(), term(), term()) :: term()
def replace_items_at_path(_items, [], replacement), do: replacement
def replace_items_at_path(items, [index | rest], replacement) do
@@ -101,6 +122,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
end)
end
@spec update_item(term(), term(), term()) :: term()
def update_item(items, item_id, updater) do
Enum.map(items, fn item ->
cond do
@@ -111,6 +133,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
end)
end
@spec insert_item(term(), term(), term(), term()) :: term()
def insert_item(items, [], index, item) do
List.insert_at(items, index, item)
end
@@ -121,10 +144,12 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
end)
end
@spec remove_item(term(), term()) :: term()
def remove_item(items, item_id) do
remove_item_with_value(items, item_id) |> elem(0)
end
@spec remove_item_with_value(term(), term()) :: term()
def remove_item_with_value(items, item_id) do
Enum.reduce_while(Enum.with_index(items), {items, nil}, fn {item, index}, _acc ->
cond do
@@ -135,7 +160,8 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
{next_children, removed_item} = remove_item_with_value(item.children, item_id)
if removed_item do
{:halt, {List.replace_at(items, index, %{item | children: next_children}), removed_item}}
{:halt,
{List.replace_at(items, index, %{item | children: next_children}), removed_item}}
else
{:cont, {items, nil}}
end
@@ -146,16 +172,23 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
end)
end
@spec append_child(term(), term(), term()) :: term()
def append_child(items, parent_item_id, child) do
update_item(items, parent_item_id, fn item ->
%{item | children: (item.children || []) ++ [child]}
end)
end
def move_selected(%{selected_id: selected_id} = state, direction) when direction in [:up, :down] do
@spec move_selected(term(), term()) :: term()
def move_selected(%{selected_id: selected_id} = state, direction)
when direction in [:up, :down] do
case find_path(state.items, selected_id) do
nil -> state
[] -> state
nil ->
state
[] ->
state
path ->
parent_path = Enum.drop(path, -1)
index = List.last(path)
@@ -175,10 +208,15 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
end
end
@spec indent_selected(term()) :: term()
def indent_selected(%{selected_id: selected_id} = state) do
case find_path(state.items, selected_id) do
nil -> state
[] -> state
nil ->
state
[] ->
state
path ->
parent_path = Enum.drop(path, -1)
index = List.last(path)
@@ -193,7 +231,9 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
case item_at_path(state.items, previous_sibling_path) do
%{kind: :submenu, item_id: sibling_id} ->
case remove_item_with_value(state.items, selected_id) do
{_next_items, nil} -> state
{_next_items, nil} ->
state
{next_items, removed_item} ->
%{
state
@@ -208,18 +248,27 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
end
end
@spec unindent_selected(term()) :: term()
def unindent_selected(%{selected_id: selected_id} = state) do
case find_path(state.items, selected_id) do
nil -> state
[] -> state
[_root_index] -> state
nil ->
state
[] ->
state
[_root_index] ->
state
path ->
parent_path = Enum.drop(path, -1)
parent_index = List.last(parent_path)
grand_parent_path = Enum.drop(parent_path, -1)
case remove_item_with_value(state.items, selected_id) do
{_next_items, nil} -> state
{_next_items, nil} ->
state
{next_items, removed_item} ->
%{
state
@@ -229,6 +278,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
end
end
@spec delete_selected(term()) :: term()
def delete_selected(%{selected_id: @home_item_id} = state), do: state
def delete_selected(%{selected_id: selected_id} = state) do
@@ -241,9 +291,11 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
state
end
def drop_selected(state, drag_item_id, target_item_id, _position) when drag_item_id == target_item_id,
do: state
def drop_selected(state, drag_item_id, target_item_id, _position)
when drag_item_id == target_item_id,
do: state
@spec drop_selected(term(), term(), term(), term()) :: term()
def drop_selected(state, drag_item_id, target_item_id, position) do
drag_path = find_path(state.items, drag_item_id)
target_path = find_path(state.items, target_item_id)
@@ -275,7 +327,11 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
defp insert_dropped_item(state, next_items, dragged_item, target_path, "inside") do
case item_at_path(next_items, target_path) do
%{kind: :submenu} ->
%{state | items: insert_item(next_items, target_path, 0, dragged_item), selected_id: dragged_item.item_id}
%{
state
| items: insert_item(next_items, target_path, 0, dragged_item),
selected_id: dragged_item.item_id
}
_other ->
state
@@ -285,12 +341,22 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreeOps do
defp insert_dropped_item(state, next_items, dragged_item, target_path, "before") do
parent_path = Enum.drop(target_path, -1)
index = List.last(target_path)
%{state | items: insert_item(next_items, parent_path, index, dragged_item), selected_id: dragged_item.item_id}
%{
state
| items: insert_item(next_items, parent_path, index, dragged_item),
selected_id: dragged_item.item_id
}
end
defp insert_dropped_item(state, next_items, dragged_item, target_path, _position) do
parent_path = Enum.drop(target_path, -1)
index = List.last(target_path) + 1
%{state | items: insert_item(next_items, parent_path, index, dragged_item), selected_id: dragged_item.item_id}
%{
state
| items: insert_item(next_items, parent_path, index, dragged_item),
selected_id: dragged_item.item_id
}
end
end

View File

@@ -3,6 +3,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreePredicates do
alias BDS.Desktop.ShellLive.MenuEditor.TreeOps
@spec can_move_up?(term(), term()) :: term()
def can_move_up?(items, selected_id) do
case TreeOps.find_path(items, selected_id) do
[_parent, index] -> index > 0
@@ -12,9 +13,12 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreePredicates do
end
end
@spec can_move_down?(term(), term()) :: term()
def can_move_down?(items, selected_id) do
case TreeOps.find_path(items, selected_id) do
nil -> false
nil ->
false
path ->
parent_path = Enum.drop(path, -1)
index = List.last(path)
@@ -22,10 +26,15 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreePredicates do
end
end
@spec can_indent?(term(), term()) :: term()
def can_indent?(items, selected_id) do
case TreeOps.find_path(items, selected_id) do
nil -> false
[] -> false
nil ->
false
[] ->
false
[_index] = path ->
index = List.last(path)
index > 0 and match?(%{kind: :submenu}, TreeOps.item_at_path(items, [index - 1]))
@@ -34,10 +43,14 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreePredicates do
index = List.last(path)
index > 0 and
match?(%{kind: :submenu}, TreeOps.item_at_path(items, Enum.drop(path, -1) ++ [index - 1]))
match?(
%{kind: :submenu},
TreeOps.item_at_path(items, Enum.drop(path, -1) ++ [index - 1])
)
end
end
@spec can_unindent?(term(), term()) :: term()
def can_unindent?(items, selected_id) do
case TreeOps.find_path(items, selected_id) do
[_index] -> false
@@ -46,9 +59,11 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.TreePredicates do
end
end
@spec can_delete?(term()) :: term()
def can_delete?(selected_id),
do: is_binary(selected_id) and selected_id != TreeOps.home_item_id()
@spec draft_item?(term(), term()) :: term()
def draft_item?(menu_editor, item_id) do
match?(%{item_id: ^item_id}, menu_editor.draft)
end