Packages

phoenix_kit

1.7.69
1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib phoenix_kit_web components dashboard admin_sidebar.ex
Raw

lib/phoenix_kit_web/components/dashboard/admin_sidebar.ex

defmodule PhoenixKitWeb.Components.Dashboard.AdminSidebar do
@moduledoc """
Admin sidebar component for the PhoenixKit admin panel.
Renders the admin navigation using registry-driven Tab structs instead of
hardcoded HEEX. Supports:
- Permission-gated tabs (filtered by Registry)
- Module-enabled filtering (filtered by Registry)
- Dynamic children for Entities and Publishing
- Subtab expand/collapse
- Full reuse of the TabItem component for consistent rendering
## Usage
<.admin_sidebar
current_path={@current_path}
scope={@phoenix_kit_current_scope}
locale={@current_locale}
/>
"""
use Phoenix.Component
require Logger
alias PhoenixKit.Dashboard.{Registry, Tab}
alias PhoenixKitWeb.Components.Dashboard.TabItem
import PhoenixKit.Dashboard.TabHelpers
import PhoenixKitWeb.Components.Core.Icon, only: [icon: 1]
@doc """
Renders the complete admin sidebar navigation.
## Attributes
- `current_path` - The current URL path for active state detection
- `scope` - The current authentication scope for permission filtering
- `locale` - The current locale for path generation
- `class` - Additional CSS classes
"""
attr :current_path, :string, default: "/admin"
attr :scope, :any, default: nil
attr :locale, :string, default: nil
attr :class, :string, default: ""
def admin_sidebar(assigns) do
# Get admin tabs, already filtered by level, permission, and module-enabled
# Expand dynamic children BEFORE active state so dynamic tabs get checked too
tabs =
:telemetry.span([:phoenix_kit, :admin_sidebar, :render], %{}, fn ->
result =
Registry.get_admin_tabs(scope: assigns.scope)
|> expand_dynamic_children(assigns.scope)
|> add_active_state(assigns.current_path)
{result, %{tab_count: length(result)}}
end)
# Group tabs
grouped_tabs = group_tabs(tabs)
groups = Registry.get_groups()
assigns =
assigns
|> assign(:tabs, tabs)
|> assign(:grouped_tabs, grouped_tabs)
|> assign(:groups, groups)
~H"""
<nav class={["space-y-2", @class]} role="navigation" aria-label="Admin navigation">
<%= for group <- sorted_groups(@groups, @grouped_tabs) do %>
<.admin_tab_group
group={group}
tabs={Map.get(@grouped_tabs, group.id, [])}
all_tabs={@tabs}
locale={@locale}
/>
<% end %>
<%!-- Render ungrouped tabs --%>
<%= for tab <- filter_top_level(Map.get(@grouped_tabs, nil, [])) do %>
<.admin_tab_with_subtabs
tab={tab}
all_tabs={@tabs}
locale={@locale}
/>
<% end %>
</nav>
"""
end
attr :group, :map, required: true
attr :tabs, :list, required: true
attr :all_tabs, :list, required: true
attr :locale, :string, default: nil
defp admin_tab_group(assigns) do
~H"""
<div class="space-y-1" data-group-id={@group.id}>
<%= if @group.label do %>
<div class="px-3 py-2 text-xs font-semibold text-base-content/50 uppercase tracking-wider">
<span class="flex items-center gap-2">
<%= if @group.icon do %>
<.icon name={@group.icon} class="w-3.5 h-3.5" />
<% end %>
{@group.label}
</span>
</div>
<% end %>
<%= for tab <- filter_top_level(@tabs) do %>
<.admin_tab_with_subtabs
tab={tab}
all_tabs={@all_tabs}
locale={@locale}
/>
<% end %>
</div>
"""
end
attr :tab, :any, required: true
attr :all_tabs, :list, required: true
attr :locale, :string, default: nil
defp admin_tab_with_subtabs(assigns) do
subtabs = get_subtabs_for(assigns.tab.id, assigns.all_tabs)
# Check all descendants (not just direct children) for active state
descendant_active = any_descendant_active?(assigns.tab.id, assigns.all_tabs)
show_subtabs =
Tab.show_subtabs?(assigns.tab, assigns.tab.active) or descendant_active
display_tab = maybe_redirect_to_first_subtab(assigns.tab, subtabs)
highlight_with_subtabs = Map.get(assigns.tab, :highlight_with_subtabs, false)
parent_active =
if descendant_active and not highlight_with_subtabs do
false
else
assigns.tab.active
end
assigns =
assigns
|> assign(:subtabs, subtabs)
|> assign(:show_subtabs, show_subtabs)
|> assign(:has_subtabs, subtabs != [])
|> assign(:display_tab, display_tab)
|> assign(:parent_active, parent_active)
~H"""
<div class="tab-with-subtabs" data-tab-id={@tab.id} data-has-subtabs={@has_subtabs}>
<TabItem.tab_item
tab={@display_tab}
active={@parent_active}
locale={@locale}
/>
<%= if @has_subtabs and @show_subtabs do %>
<div class="subtabs pl-1 border-l-2 border-base-300 ml-2 mt-1 space-y-0.5">
<%= for subtab <- @subtabs do %>
<.admin_subtab_item
subtab={subtab}
parent_tab={@tab}
all_tabs={@all_tabs}
locale={@locale}
/>
<% end %>
</div>
<% end %>
</div>
"""
end
attr :subtab, :any, required: true
attr :parent_tab, :any, required: true
attr :all_tabs, :list, required: true
attr :locale, :string, default: nil
defp admin_subtab_item(assigns) do
children = get_subtabs_for(assigns.subtab.id, assigns.all_tabs)
child_active = any_descendant_active?(assigns.subtab.id, assigns.all_tabs)
show_children =
children != [] and
(Tab.show_subtabs?(assigns.subtab, assigns.subtab.active) or child_active)
highlight_with_subtabs = Map.get(assigns.subtab, :highlight_with_subtabs, false)
subtab_active =
if child_active and not highlight_with_subtabs do
false
else
assigns.subtab.active
end
assigns =
assigns
|> assign(:children, children)
|> assign(:show_children, show_children)
|> assign(:subtab_active, subtab_active)
~H"""
<TabItem.tab_item
tab={@subtab}
active={@subtab_active}
locale={@locale}
parent_tab={@parent_tab}
/>
<%= if @show_children do %>
<div class="sub-subtabs pl-1 border-l-2 border-base-300 ml-2 mt-0.5 space-y-0.5">
<%= for child <- @children do %>
<TabItem.tab_item
tab={child}
active={child.active}
locale={@locale}
parent_tab={@subtab}
/>
<% end %>
</div>
<% end %>
"""
end
# --- Helpers ---
defp expand_dynamic_children(tabs, scope) do
# Find tabs with dynamic_children and expand them
{parents_with_dynamic, other_tabs} =
Enum.split_with(tabs, fn tab ->
is_function(tab.dynamic_children, 1)
end)
dynamic_children =
Enum.flat_map(parents_with_dynamic, fn parent ->
children =
try do
parent.dynamic_children.(scope)
rescue
error ->
Logger.warning(
"[AdminSidebar] dynamic_children for #{inspect(parent.id)} failed: #{Exception.message(error)}"
)
[]
end
# Ensure children have parent set, correct level, and resolved paths
Enum.map(children, fn child ->
child
|> Map.put(:parent, child.parent || parent.id)
|> Map.put(:level, :admin)
|> Tab.resolve_path(:admin)
end)
end)
# Active state is applied after this function by add_active_state/2
other_tabs ++ parents_with_dynamic ++ dynamic_children
end
# Recursively checks if any descendant (children, grandchildren, etc.) is active.
# Includes depth limit and cycle detection for safety with parent-app-registered tabs.
defp any_descendant_active?(parent_id, all_tabs, depth \\ 0, visited \\ %{})
defp any_descendant_active?(_parent_id, _all_tabs, depth, _visited) when depth > 5, do: false
defp any_descendant_active?(parent_id, all_tabs, depth, visited) do
if Map.has_key?(visited, parent_id) do
Logger.warning("[AdminSidebar] Circular tab reference detected: #{inspect(parent_id)}")
false
else
children = get_subtabs_for(parent_id, all_tabs)
new_visited = Map.put(visited, parent_id, true)
Enum.any?(children, fn child ->
child.active or any_descendant_active?(child.id, all_tabs, depth + 1, new_visited)
end)
end
end
defp maybe_redirect_to_first_subtab(%{redirect_to_first_subtab: true} = tab, [
first_subtab | _
]) do
%{tab | path: first_subtab.path}
end
defp maybe_redirect_to_first_subtab(tab, _subtabs), do: tab
end