Packages
phoenix_kit
1.7.38
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
Current section
Files
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
alias PhoenixKit.Dashboard.{Registry, Tab}
alias PhoenixKitWeb.Components.Dashboard.TabItem
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 =
Registry.get_admin_tabs(scope: assigns.scope)
|> expand_dynamic_children(assigns.scope)
|> add_active_state(assigns.current_path)
# 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 add_active_state(tabs, current_path) do
Enum.map(tabs, fn tab ->
Map.put(tab, :active, Tab.matches_path?(tab, current_path))
end)
end
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
_ -> []
end
# Ensure children have parent set and correct level
Enum.map(children, fn child ->
child
|> Map.put(:parent, child.parent || parent.id)
|> Map.put(:level, :admin)
end)
end)
# Active state is applied after this function by add_active_state/2
other_tabs ++ parents_with_dynamic ++ dynamic_children
end
defp group_tabs(tabs) do
Enum.group_by(tabs, & &1.group)
end
defp sorted_groups(groups, grouped_tabs) do
group_ids_with_tabs = Map.keys(grouped_tabs) |> Enum.reject(&is_nil/1)
groups
|> Enum.filter(&(&1.id in group_ids_with_tabs))
|> Enum.sort_by(& &1.priority)
end
defp filter_top_level(tabs) do
Enum.filter(tabs, &Tab.top_level?/1)
end
defp get_subtabs_for(parent_id, all_tabs) do
Enum.filter(all_tabs, fn tab ->
tab.parent == parent_id
end)
|> Enum.sort_by(& &1.priority)
end
# Recursively checks if any descendant (children, grandchildren, etc.) is active
defp any_descendant_active?(parent_id, all_tabs) do
children = get_subtabs_for(parent_id, all_tabs)
Enum.any?(children, fn child ->
child.active or any_descendant_active?(child.id, all_tabs)
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