Packages
phoenix_kit
1.7.38
1.7.209
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/dashboard/registry.ex
defmodule PhoenixKit.Dashboard.Registry do
@moduledoc """
Registry for managing dashboard tabs across the application.
The Registry provides both compile-time configuration via application config
and runtime registration for dynamic tabs. Tabs are stored in an ETS table
for efficient access.
## Configuration
Tabs can be configured in your application config:
config :phoenix_kit, :user_dashboard_tabs, [
%{
id: :orders,
label: "Orders",
icon: "hero-shopping-bag",
path: "/dashboard/orders",
priority: 100
},
%{
id: :settings,
label: "Settings",
icon: "hero-cog-6-tooth",
path: "/dashboard/settings",
priority: 900
}
]
## Runtime Registration
Parent applications can register tabs at runtime:
PhoenixKit.Dashboard.Registry.register(:my_app, [
Tab.new!(id: :custom, label: "Custom", path: "/dashboard/custom", priority: 150)
])
## Groups
Tabs can be organized into groups:
config :phoenix_kit, :user_dashboard_tab_groups, [
%{id: :main, label: nil, priority: 100},
%{id: :farm, label: "Farm Management", priority: 200, icon: "hero-cube"},
%{id: :account, label: "Account", priority: 900}
]
Then assign tabs to groups:
%{id: :printers, label: "Printers", path: "/dashboard/printers", group: :farm}
## PubSub Integration
The registry can broadcast tab updates:
PhoenixKit.Dashboard.Registry.update_tab_badge(:notifications, Badge.count(5))
LiveViews subscribed to "phoenix_kit:dashboard:tabs" will receive updates.
"""
use GenServer
require Logger
alias PhoenixKit.Dashboard.{AdminTabs, Badge, Tab}
alias PhoenixKit.PubSubHelper
alias PhoenixKit.Users.Permissions
# Suppress warnings about optional modules (loaded conditionally)
@compile {:no_warn_undefined, PhoenixKit.Modules.Tickets}
@compile {:no_warn_undefined, PhoenixKit.Modules.Billing}
@compile {:no_warn_undefined, PhoenixKit.Modules.Shop}
@ets_table :phoenix_kit_dashboard_tabs
@pubsub_topic "phoenix_kit:dashboard:tabs"
# Client API
@doc """
Starts the Registry GenServer.
This is typically called by the PhoenixKit supervisor.
"""
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@doc """
Registers tabs for an application namespace.
## Examples
Registry.register(:my_app, [
Tab.new!(id: :home, label: "Home", path: "/dashboard", icon: "hero-home"),
Tab.new!(id: :orders, label: "Orders", path: "/dashboard/orders")
])
# Register a single tab
Registry.register(:my_app, Tab.new!(id: :settings, label: "Settings", path: "/dashboard/settings"))
"""
@spec register(atom(), Tab.t() | [Tab.t()]) :: :ok
def register(namespace, %Tab{} = tab) do
register(namespace, [tab])
end
def register(namespace, tabs) when is_atom(namespace) and is_list(tabs) do
GenServer.call(__MODULE__, {:register, namespace, tabs})
end
@doc """
Registers tabs from a map/keyword configuration.
Useful for registering tabs from config files.
## Examples
Registry.register_from_config(:my_app, [
%{id: :home, label: "Home", path: "/dashboard", icon: "hero-home"},
%{id: :orders, label: "Orders", path: "/dashboard/orders"}
])
"""
@spec register_from_config(atom(), [map()] | [keyword()]) :: :ok | {:error, term()}
def register_from_config(namespace, config) when is_atom(namespace) and is_list(config) do
tabs =
Enum.reduce_while(config, {:ok, []}, fn item, {:ok, acc} ->
case Tab.new(item) do
{:ok, tab} -> {:cont, {:ok, [tab | acc]}}
{:error, reason} -> {:halt, {:error, reason}}
end
end)
case tabs do
{:ok, tab_list} ->
register(namespace, Enum.reverse(tab_list))
:ok
{:error, reason} ->
{:error, reason}
end
end
@doc """
Unregisters all tabs for a namespace.
"""
@spec unregister(atom()) :: :ok
def unregister(namespace) when is_atom(namespace) do
GenServer.call(__MODULE__, {:unregister, namespace})
end
@doc """
Unregisters a specific tab by ID.
"""
@spec unregister_tab(atom()) :: :ok
def unregister_tab(tab_id) when is_atom(tab_id) do
GenServer.call(__MODULE__, {:unregister_tab, tab_id})
end
@doc """
Gets all registered tabs, sorted by priority.
## Options
- `:scope` - The current scope (for visibility filtering)
- `:level` - Filter by tab level: `:admin`, `:user`, or nil for all
- `:path` - The current path (for active state detection)
- `:include_hidden` - Include tabs that would be hidden (default: false)
## Examples
Registry.get_tabs()
Registry.get_tabs(scope: scope, level: :user)
Registry.get_tabs(scope: scope, level: :admin)
"""
@spec get_tabs(keyword()) :: [Tab.t()]
def get_tabs(opts \\ []) do
scope = opts[:scope]
level = opts[:level]
include_hidden = opts[:include_hidden] || false
all_tabs()
|> maybe_filter_level(level)
|> maybe_filter_enabled()
|> maybe_filter_permission(scope)
|> maybe_filter_visibility(scope, include_hidden)
|> sort_tabs()
end
@doc """
Gets admin-level tabs, filtered by permission and module-enabled status.
## Options
- `:scope` - The current scope (for permission and visibility filtering)
- `:include_hidden` - Include tabs that would be hidden (default: false)
"""
@spec get_admin_tabs(keyword()) :: [Tab.t()]
def get_admin_tabs(opts \\ []) do
get_tabs(Keyword.put(opts, :level, :admin))
end
@doc """
Gets user-level tabs.
## Options
- `:scope` - The current scope (for visibility filtering)
- `:include_hidden` - Include tabs that would be hidden (default: false)
"""
@spec get_user_tabs(keyword()) :: [Tab.t()]
def get_user_tabs(opts \\ []) do
get_tabs(Keyword.put(opts, :level, :user))
end
@doc """
Updates an existing tab's attributes.
## Examples
Registry.update_tab(:admin_dashboard, %{label: "Home", icon: "hero-house"})
"""
@spec update_tab(atom(), map()) :: :ok
def update_tab(tab_id, attrs) when is_atom(tab_id) and is_map(attrs) do
GenServer.call(__MODULE__, {:update_tab, tab_id, attrs})
end
@doc """
Gets a specific tab by ID.
"""
@spec get_tab(atom()) :: Tab.t() | nil
def get_tab(tab_id) when is_atom(tab_id) do
if initialized?() do
case :ets.lookup(@ets_table, {:tab, tab_id}) do
[{_, tab}] -> tab
[] -> nil
end
else
nil
end
end
@doc """
Gets all tabs in a specific group.
"""
@spec get_tabs_in_group(atom(), keyword()) :: [Tab.t()]
def get_tabs_in_group(group_id, opts \\ []) do
get_tabs(opts)
|> Enum.filter(&(&1.group == group_id))
end
@doc """
Gets all subtabs for a given parent tab ID.
## Examples
Registry.get_subtabs(:orders)
# => [%Tab{id: :pending_orders, parent: :orders, ...}, ...]
"""
@spec get_subtabs(atom(), keyword()) :: [Tab.t()]
def get_subtabs(parent_id, opts \\ []) when is_atom(parent_id) do
get_tabs(opts)
|> Enum.filter(&(&1.parent == parent_id))
end
@doc """
Gets only top-level tabs (tabs without a parent).
## Options
Same as `get_tabs/1`.
## Examples
Registry.get_top_level_tabs()
# => [%Tab{id: :orders, parent: nil, ...}, ...]
"""
@spec get_top_level_tabs(keyword()) :: [Tab.t()]
def get_top_level_tabs(opts \\ []) do
get_tabs(opts)
|> Enum.filter(&Tab.top_level?/1)
end
@doc """
Checks if a tab has any subtabs.
## Examples
Registry.has_subtabs?(:orders)
# => true
"""
@spec has_subtabs?(atom()) :: boolean()
def has_subtabs?(tab_id) when is_atom(tab_id) do
get_subtabs(tab_id) |> Enum.any?()
end
@doc """
Gets all registered groups, sorted by priority.
"""
@spec get_groups() :: [map()]
def get_groups do
if initialized?() do
case :ets.lookup(@ets_table, :groups) do
[{:groups, groups}] -> Enum.sort_by(groups, & &1.priority)
[] -> []
end
else
[]
end
end
@doc """
Registers tab groups.
## Examples
Registry.register_groups([
%{id: :main, label: nil, priority: 100},
%{id: :farm, label: "Farm Management", priority: 200},
%{id: :account, label: "Account", priority: 900}
])
"""
@spec register_groups([map()]) :: :ok
def register_groups(groups) when is_list(groups) do
GenServer.call(__MODULE__, {:register_groups, groups})
end
@doc """
Updates a tab's badge.
This broadcasts an update to all subscribed LiveViews.
## Examples
Registry.update_tab_badge(:notifications, Badge.count(5))
Registry.update_tab_badge(:printers, Badge.count(3, color: :warning))
"""
@spec update_tab_badge(atom(), Badge.t() | map() | nil) :: :ok
def update_tab_badge(tab_id, badge) do
GenServer.call(__MODULE__, {:update_badge, tab_id, badge})
end
@doc """
Sets an attention animation on a tab.
## Examples
Registry.set_tab_attention(:alerts, :pulse)
Registry.set_tab_attention(:notifications, :bounce)
"""
@spec set_tab_attention(atom(), atom()) :: :ok
def set_tab_attention(tab_id, attention)
when attention in [nil, :pulse, :bounce, :shake, :glow] do
GenServer.call(__MODULE__, {:set_attention, tab_id, attention})
end
@doc """
Clears attention animation from a tab.
"""
@spec clear_tab_attention(atom()) :: :ok
def clear_tab_attention(tab_id) do
set_tab_attention(tab_id, nil)
end
@doc """
Gets the PubSub topic for tab updates.
LiveViews can subscribe to this topic to receive real-time tab updates.
## Example
def mount(_params, _session, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(PubSubHelper.pubsub(), Registry.pubsub_topic())
end
{:ok, socket}
end
def handle_info({:tab_updated, tab}, socket) do
# Handle tab update
{:noreply, socket}
end
"""
@spec pubsub_topic() :: String.t()
def pubsub_topic, do: @pubsub_topic
@doc """
Broadcasts a tab update to all subscribers.
"""
@spec broadcast_update(Tab.t()) :: :ok
def broadcast_update(%Tab{} = tab) do
Phoenix.PubSub.broadcast(PubSubHelper.pubsub(), @pubsub_topic, {:tab_updated, tab})
:ok
rescue
_ -> :ok
end
@doc """
Broadcasts a full tab list refresh to all subscribers.
"""
@spec broadcast_refresh() :: :ok
def broadcast_refresh do
Phoenix.PubSub.broadcast(PubSubHelper.pubsub(), @pubsub_topic, :tabs_refreshed)
:ok
rescue
_ -> :ok
end
@doc """
Checks if the registry has been initialized.
"""
@spec initialized?() :: boolean()
def initialized? do
case :ets.info(@ets_table) do
:undefined -> false
_ -> true
end
end
@doc """
Gets all tabs with their active state for the given path.
Returns tabs with an additional `:active` key set based on path matching.
"""
@spec get_tabs_with_active(String.t(), keyword()) :: [map()]
def get_tabs_with_active(current_path, opts \\ []) do
get_tabs(opts)
|> Enum.map(fn tab ->
Map.put(tab, :active, Tab.matches_path?(tab, current_path))
end)
end
@doc """
Loads the default PhoenixKit tabs (Dashboard, Settings).
Called during initialization and can be used to reset to defaults.
"""
@spec load_defaults() :: :ok
def load_defaults do
GenServer.call(__MODULE__, :load_defaults)
end
@doc """
Loads tabs from application configuration.
Reads from `:phoenix_kit, :user_dashboard_tabs` config key.
"""
@spec load_from_config() :: :ok
def load_from_config do
GenServer.call(__MODULE__, :load_from_config)
end
@doc """
Loads admin default tabs.
Called during initialization.
"""
@spec load_admin_defaults() :: :ok
def load_admin_defaults do
GenServer.call(__MODULE__, :load_admin_defaults)
end
# GenServer Callbacks
@impl true
def init(_opts) do
# Create ETS table for tab storage
:ets.new(@ets_table, [:named_table, :set, :public, read_concurrency: true])
# Load user dashboard defaults and config
load_defaults_internal()
load_from_config_internal()
# Load admin dashboard defaults and config
load_admin_defaults_internal()
load_admin_from_config_internal()
{:ok, %{namespaces: MapSet.new([:phoenix_kit, :phoenix_kit_admin])}}
end
@impl true
def handle_call({:register, namespace, tabs}, _from, state) do
Enum.each(tabs, fn tab ->
:ets.insert(@ets_table, {{:tab, tab.id}, tab})
:ets.insert(@ets_table, {{:namespace, namespace, tab.id}, true})
end)
broadcast_refresh()
{:reply, :ok, %{state | namespaces: MapSet.put(state.namespaces, namespace)}}
end
@impl true
def handle_call({:unregister, namespace}, _from, state) do
# Find and remove all tabs for this namespace
pattern = {{:namespace, namespace, :_}, :_}
:ets.match_object(@ets_table, pattern)
|> Enum.each(fn {{:namespace, ^namespace, tab_id}, _} ->
:ets.delete(@ets_table, {:tab, tab_id})
:ets.delete(@ets_table, {:namespace, namespace, tab_id})
end)
broadcast_refresh()
{:reply, :ok, %{state | namespaces: MapSet.delete(state.namespaces, namespace)}}
end
@impl true
def handle_call({:unregister_tab, tab_id}, _from, state) do
:ets.delete(@ets_table, {:tab, tab_id})
# Remove from all namespace mappings
:ets.match_delete(@ets_table, {{:namespace, :_, tab_id}, :_})
broadcast_refresh()
{:reply, :ok, state}
end
@impl true
def handle_call({:register_groups, groups}, _from, state) do
:ets.insert(@ets_table, {:groups, groups})
broadcast_refresh()
{:reply, :ok, state}
end
@impl true
def handle_call({:update_badge, tab_id, badge}, _from, state) do
case get_tab(tab_id) do
nil ->
{:reply, :ok, state}
tab ->
updated_tab = Tab.update_badge(tab, badge)
:ets.insert(@ets_table, {{:tab, tab_id}, updated_tab})
broadcast_update(updated_tab)
{:reply, :ok, state}
end
end
@impl true
def handle_call({:set_attention, tab_id, attention}, _from, state) do
case get_tab(tab_id) do
nil ->
{:reply, :ok, state}
tab ->
updated_tab = Tab.set_attention(tab, attention)
:ets.insert(@ets_table, {{:tab, tab_id}, updated_tab})
broadcast_update(updated_tab)
{:reply, :ok, state}
end
end
@impl true
def handle_call(:load_defaults, _from, state) do
load_defaults_internal()
broadcast_refresh()
{:reply, :ok, state}
end
@impl true
def handle_call(:load_from_config, _from, state) do
load_from_config_internal()
broadcast_refresh()
{:reply, :ok, state}
end
@impl true
def handle_call(:load_admin_defaults, _from, state) do
load_admin_defaults_internal()
broadcast_refresh()
{:reply, :ok, state}
end
@impl true
def handle_call({:update_tab, tab_id, attrs}, _from, state) do
case get_tab(tab_id) do
nil ->
{:reply, :ok, state}
tab ->
updated_tab =
Enum.reduce(attrs, tab, fn
{:label, v}, acc -> %{acc | label: v}
{:icon, v}, acc -> %{acc | icon: v}
{:path, v}, acc -> %{acc | path: v}
{:priority, v}, acc -> %{acc | priority: v}
{:visible, v}, acc -> %{acc | visible: v}
{:permission, v}, acc -> %{acc | permission: v}
{:group, v}, acc -> %{acc | group: v}
{:metadata, v}, acc -> %{acc | metadata: Map.merge(acc.metadata, v)}
_, acc -> acc
end)
:ets.insert(@ets_table, {{:tab, tab_id}, updated_tab})
broadcast_update(updated_tab)
{:reply, :ok, state}
end
end
# Private helpers
defp all_tabs do
:ets.match_object(@ets_table, {{:tab, :_}, :_})
|> Enum.map(fn {_, tab} -> tab end)
rescue
_ -> []
end
defp maybe_filter_visibility(tabs, nil, _include_hidden), do: tabs
defp maybe_filter_visibility(tabs, _scope, true), do: tabs
defp maybe_filter_visibility(tabs, scope, false) do
Enum.filter(tabs, &Tab.visible?(&1, scope))
end
defp sort_tabs(tabs) do
Enum.sort_by(tabs, & &1.priority)
end
# Filter tabs by level. :admin returns admin+all, :user returns user+all
defp maybe_filter_level(tabs, nil), do: tabs
defp maybe_filter_level(tabs, :admin) do
Enum.filter(tabs, fn tab ->
level = Map.get(tab, :level, :user)
level in [:admin, :all]
end)
end
defp maybe_filter_level(tabs, :user) do
Enum.filter(tabs, fn tab ->
level = Map.get(tab, :level, :user)
level in [:user, :all]
end)
end
defp maybe_filter_level(tabs, _), do: tabs
# Filter out tabs whose associated module is disabled.
# Precomputes enabled state per unique permission key to avoid
# redundant DB queries (e.g., 5 "publishing" tabs = 1 query, not 5).
defp maybe_filter_enabled(tabs) do
enabled_cache =
tabs
|> Enum.map(& &1.permission)
|> Enum.reject(&is_nil/1)
|> Enum.uniq()
|> Map.new(fn perm ->
enabled =
try do
Permissions.feature_enabled?(perm)
rescue
_ -> false
end
{perm, enabled}
end)
Enum.filter(tabs, fn tab ->
case tab.permission do
nil -> true
perm -> Map.get(enabled_cache, perm, true)
end
end)
end
# Filter tabs by permission access
defp maybe_filter_permission(tabs, nil), do: tabs
defp maybe_filter_permission(tabs, scope) do
Enum.filter(tabs, &Tab.permission_granted?(&1, scope))
end
defp clear_namespace_tabs(namespace) do
# Find and remove all tabs for this namespace
pattern = {{:namespace, namespace, :_}, :_}
:ets.match_object(@ets_table, pattern)
|> Enum.each(fn {{:namespace, ^namespace, tab_id}, _} ->
:ets.delete(@ets_table, {:tab, tab_id})
:ets.delete(@ets_table, {:namespace, namespace, tab_id})
end)
rescue
_ -> :ok
end
defp load_defaults_internal do
# Clear existing phoenix_kit namespace tabs first
clear_namespace_tabs(:phoenix_kit)
# Default PhoenixKit tabs
defaults = [
Tab.new!(
id: :dashboard_home,
label: "Dashboard",
icon: "hero-home",
path: "/dashboard",
priority: 100,
match: :exact,
group: :main
),
Tab.new!(
id: :dashboard_settings,
label: "Settings",
icon: "hero-cog-6-tooth",
path: "/dashboard/settings",
priority: 900,
match: :prefix,
group: :account
)
]
# Add tickets tab if module is enabled (checking at runtime)
defaults =
if tickets_enabled?() do
ticket_tab =
Tab.new!(
id: :dashboard_tickets,
label: "My Tickets",
icon: "hero-ticket",
path: "/dashboard/tickets",
priority: 800,
match: :prefix,
group: :account
)
defaults ++ [ticket_tab]
else
defaults
end
# Add billing tabs if module is enabled
defaults =
if billing_enabled?() do
billing_tabs = [
Tab.new!(
id: :dashboard_orders,
label: "My Orders",
icon: "hero-shopping-bag",
path: "/dashboard/orders",
priority: 200,
match: :prefix,
group: :main
),
Tab.new!(
id: :dashboard_billing_profiles,
label: "Billing Profiles",
icon: "hero-identification",
path: "/dashboard/billing-profiles",
priority: 850,
match: :prefix,
group: :account
)
]
defaults ++ billing_tabs
else
defaults
end
# Add shop tabs if module is enabled
defaults =
if shop_enabled?() do
shop_tabs = [
Tab.new!(
id: :dashboard_shop,
label: "Shop",
icon: "hero-building-storefront",
path: "/shop",
priority: 300,
match: :prefix,
group: :shop
),
Tab.new!(
id: :dashboard_cart,
label: "My Cart",
icon: "hero-shopping-cart",
path: "/cart",
priority: 310,
match: :prefix,
group: :shop
)
]
defaults ++ shop_tabs
else
defaults
end
# Default groups
groups = [
%{id: :main, label: nil, priority: 100},
%{id: :shop, label: nil, priority: 200},
%{id: :account, label: nil, priority: 900}
]
Enum.each(defaults, fn tab ->
:ets.insert(@ets_table, {{:tab, tab.id}, tab})
:ets.insert(@ets_table, {{:namespace, :phoenix_kit, tab.id}, true})
end)
:ets.insert(@ets_table, {:groups, groups})
end
defp load_from_config_internal do
# Load from user_dashboard_categories (admin-style config)
load_from_categories_config()
# Load tab configuration (flat list style)
case Application.get_env(:phoenix_kit, :user_dashboard_tabs) do
nil ->
:ok
tabs when is_list(tabs) ->
Enum.each(tabs, fn tab_config ->
case Tab.new(tab_config) do
{:ok, tab} ->
:ets.insert(@ets_table, {{:tab, tab.id}, tab})
:ets.insert(@ets_table, {{:namespace, :config, tab.id}, true})
{:error, _reason} ->
# Log error but continue
:ok
end
end)
end
# Load group configuration
case Application.get_env(:phoenix_kit, :user_dashboard_tab_groups) do
nil ->
:ok
groups when is_list(groups) ->
:ets.insert(@ets_table, {:groups, groups})
end
end
# Load from user_dashboard_categories config (admin-style format)
defp load_from_categories_config do
alias PhoenixKit.Config.UserDashboardCategories
categories = UserDashboardCategories.get_categories()
if categories != [] do
# Convert categories to tabs and register them
tabs = UserDashboardCategories.to_tabs(categories)
Enum.each(tabs, fn tab_config ->
case Tab.new(tab_config) do
{:ok, tab} ->
:ets.insert(@ets_table, {{:tab, tab.id}, tab})
:ets.insert(@ets_table, {{:namespace, :categories, tab.id}, true})
{:error, _reason} ->
# Log error but continue
:ok
end
end)
# Convert categories to groups and register them
groups = UserDashboardCategories.to_groups(categories)
existing_groups = get_groups()
merged_groups = merge_groups(existing_groups, groups)
:ets.insert(@ets_table, {:groups, merged_groups})
end
end
# Merge groups, preferring existing groups with same ID
defp merge_groups(existing, new) do
existing_ids = MapSet.new(Enum.map(existing, & &1.id))
new_groups =
Enum.reject(new, fn group ->
MapSet.member?(existing_ids, group.id)
end)
(existing ++ new_groups)
|> Enum.sort_by(& &1.priority)
end
defp tickets_enabled? do
Code.ensure_loaded?(PhoenixKit.Modules.Tickets) and
function_exported?(PhoenixKit.Modules.Tickets, :enabled?, 0) and
call_tickets_enabled()
rescue
_ -> false
end
defp billing_enabled? do
Code.ensure_loaded?(PhoenixKit.Modules.Billing) and
function_exported?(PhoenixKit.Modules.Billing, :enabled?, 0) and
call_billing_enabled()
rescue
_ -> false
end
defp shop_enabled? do
Code.ensure_loaded?(PhoenixKit.Modules.Shop) and
function_exported?(PhoenixKit.Modules.Shop, :enabled?, 0) and
call_shop_enabled()
rescue
_ -> false
end
# credo:disable-for-next-line Credo.Check.Design.AliasUsage
defp call_tickets_enabled, do: PhoenixKit.Modules.Tickets.enabled?()
# credo:disable-for-next-line Credo.Check.Design.AliasUsage
defp call_billing_enabled, do: PhoenixKit.Modules.Billing.enabled?()
# credo:disable-for-next-line Credo.Check.Design.AliasUsage
defp call_shop_enabled, do: PhoenixKit.Modules.Shop.enabled?()
# --- Admin Tab Loading ---
defp load_admin_defaults_internal do
clear_namespace_tabs(:phoenix_kit_admin)
tabs = AdminTabs.default_tabs()
groups = AdminTabs.default_groups()
Enum.each(tabs, fn tab ->
:ets.insert(@ets_table, {{:tab, tab.id}, tab})
:ets.insert(@ets_table, {{:namespace, :phoenix_kit_admin, tab.id}, true})
end)
# Merge admin groups with existing groups
existing_groups = get_groups()
merged = merge_groups(existing_groups, groups)
:ets.insert(@ets_table, {:groups, merged})
end
defp load_admin_from_config_internal do
# Load legacy AdminDashboardCategories config and convert to admin-level tabs
load_legacy_admin_categories()
# Load new :admin_dashboard_tabs config (highest precedence)
case Application.get_env(:phoenix_kit, :admin_dashboard_tabs) do
nil ->
:ok
tabs when is_list(tabs) ->
Enum.each(tabs, fn tab_config ->
# Auto-set level to :admin for admin dashboard tabs
tab_config = Map.put_new(tab_config, :level, :admin)
case Tab.new(tab_config) do
{:ok, tab} ->
:ets.insert(@ets_table, {{:tab, tab.id}, tab})
:ets.insert(@ets_table, {{:namespace, :admin_config, tab.id}, true})
{:error, _reason} ->
:ok
end
end)
end
end
# Convert legacy AdminDashboardCategories to admin-level Tab structs
defp load_legacy_admin_categories do
alias PhoenixKit.Config.AdminDashboardCategories
categories = AdminDashboardCategories.get_categories()
if categories != [] do
Logger.info(
"[PhoenixKit] Legacy :admin_dashboard_categories config detected. " <>
"Consider migrating to :admin_dashboard_tabs format."
)
# Convert each category to admin-level tabs
categories
|> Enum.with_index()
|> Enum.each(fn {category, cat_idx} ->
# Create parent tab from category
cat_id = :"admin_custom_#{cat_idx}"
first_url =
case category.subsections do
[first | _] -> Map.get(first, :url, "/admin")
_ -> "/admin"
end
parent = %Tab{
id: cat_id,
label: category.title,
icon: category.icon || "hero-folder",
path: first_url,
priority: 700 + cat_idx * 10,
level: :admin,
match: :prefix,
group: :admin_modules,
subtab_display: :when_active,
highlight_with_subtabs: false
}
:ets.insert(@ets_table, {{:tab, parent.id}, parent})
:ets.insert(@ets_table, {{:namespace, :admin_legacy, parent.id}, true})
# Create child tabs from subsections
category.subsections
|> Enum.with_index()
|> Enum.each(fn {subsection, sub_idx} ->
child_id = :"admin_custom_#{cat_idx}_#{sub_idx}"
child = %Tab{
id: child_id,
label: subsection.title,
icon: subsection.icon || "hero-document-text",
path: subsection.url,
priority: 701 + cat_idx * 10 + sub_idx,
level: :admin,
match: :prefix,
parent: cat_id
}
:ets.insert(@ets_table, {{:tab, child.id}, child})
:ets.insert(@ets_table, {{:namespace, :admin_legacy, child.id}, true})
end)
end)
end
end
end