Packages
phoenix_kit
1.7.23
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/context_selector.ex
defmodule PhoenixKit.Dashboard.ContextSelector do
@moduledoc """
Configuration and helpers for the dashboard context selector.
The context selector allows users to switch between multiple contexts
(organizations, farms, teams, workspaces, etc.) in the dashboard.
Users with only one context won't see the selector.
## Configuration
Configure in your `config/config.exs`:
config :phoenix_kit, :dashboard_context_selector,
loader: {MyApp.Farms, :list_for_user},
display_name: fn farm -> farm.name end,
id_field: :id,
label: "Farm",
icon: "hero-building-office",
position: :sidebar,
sub_position: :end,
empty_behavior: :hide,
session_key: "dashboard_context_id",
tab_loader: {MyApp.Farms, :get_tabs_for_context}
## Configuration Options
- `:loader` - Required. A `{Module, :function}` tuple that takes a user ID
and returns a list of context items. Example: `{MyApp.Farms, :list_for_user}`
- `:display_name` - Required. A function that takes a context item and returns
the display string. Example: `fn farm -> farm.name end`
- `:id_field` - Optional. The field to use as the unique identifier.
Defaults to `:id`. Can be an atom or a function.
- `:label` - Optional. The label shown in the UI (e.g., "Farm", "Organization").
Defaults to `"Context"`.
- `:icon` - Optional. Heroicon name for the selector. Defaults to `"hero-building-office"`.
- `:position` - Optional. Which area to show the selector in.
Options: `:header` (default), `:sidebar`.
- `:sub_position` - Optional. Where within the area to place the selector.
For header: `:start` (left, after logo), `:end` (right, before user menu),
or `{:priority, N}` to sort among other header items.
For sidebar: `:start` (top), `:end` (pinned to very bottom),
or `{:priority, N}` to sort among tabs.
Defaults to `:start`.
- `:empty_behavior` - Optional. What to do when user has no contexts.
Options: `:hide` (default), `:show_empty`, `{:redirect, "/path"}`.
- `:separator` - Optional. Separator shown between logo and selector in header
(only applies to `position: :header, sub_position: :start`).
Defaults to `"/"`. Set to `false` or `nil` to disable. Can be any string
like `"›"`, `"|"`, or `"·"`.
Note: The separator may appear slightly off-center due to internal padding
in the selector dropdown. This is a visual quirk and can be adjusted by
customizing the layout template if precise alignment is required.
- `:session_key` - Optional. The session key for storing the selected context ID.
Defaults to `"dashboard_context_id"`.
- `:tab_loader` - Optional. A `{Module, :function}` tuple that takes a context
item and returns a list of tab definitions. Enables dynamic tabs that change
based on the selected context. Example: `{MyApp.Farms, :get_tabs_for_context}`
## Usage in LiveViews
The `ContextProvider` on_mount hook automatically sets these assigns:
- `@dashboard_contexts` - List of all contexts for the user
- `@current_context` - The currently selected context item
- `@show_context_selector` - Boolean, true only if user has 2+ contexts
- `@dashboard_tabs` - (Optional) List of Tab structs when `tab_loader` is configured
Access the current context in your LiveView:
def mount(_params, _session, socket) do
context = socket.assigns.current_context
items = MyApp.Items.list_for_context(context.id)
{:ok, assign(socket, items: items)}
end
Or use the helper functions:
context_id = PhoenixKit.Dashboard.current_context_id(socket)
"""
alias PhoenixKit.Config
defstruct [
:loader,
:display_name,
:id_field,
:label,
:icon,
:position,
:sub_position,
:empty_behavior,
:session_key,
:tab_loader,
:separator,
enabled: false
]
@type sub_position :: :start | :end | {:priority, integer()}
@type t :: %__MODULE__{
loader: {module(), atom()} | nil,
display_name: (any() -> String.t()) | nil,
id_field: atom() | (any() -> any()),
label: String.t(),
icon: String.t() | nil,
position: :header | :sidebar,
sub_position: sub_position() | nil,
empty_behavior: :hide | :show_empty | {:redirect, String.t()},
session_key: String.t(),
tab_loader: {module(), atom()} | nil,
separator: String.t() | false | nil,
enabled: boolean()
}
@default_label "Context"
@default_icon "hero-building-office"
@default_empty_behavior :hide
@default_session_key "dashboard_context_id"
@default_separator "/"
@default_id_field :id
@doc """
Gets the context selector configuration.
Returns a validated `%ContextSelector{}` struct if configured,
or a disabled struct if not configured.
## Examples
iex> PhoenixKit.Dashboard.ContextSelector.get_config()
%ContextSelector{enabled: true, loader: {MyApp.Farms, :list_for_user}, ...}
iex> PhoenixKit.Dashboard.ContextSelector.get_config()
%ContextSelector{enabled: false}
"""
@spec get_config() :: t()
def get_config do
case Config.get(:dashboard_context_selector) do
{:ok, config} when is_map(config) or is_list(config) ->
validate_config(config)
_ ->
%__MODULE__{enabled: false}
end
end
@doc """
Checks if the context selector feature is enabled.
Returns `true` if the feature is configured with a valid loader.
## Examples
iex> PhoenixKit.Dashboard.ContextSelector.enabled?()
true
"""
@spec enabled?() :: boolean()
def enabled? do
get_config().enabled
end
@doc """
Loads contexts for a user using the configured loader.
Returns an empty list if the feature is not enabled or the loader fails.
## Examples
iex> PhoenixKit.Dashboard.ContextSelector.load_contexts(user_id)
[%Farm{id: 1, name: "My Farm"}, %Farm{id: 2, name: "Other Farm"}]
"""
@spec load_contexts(any()) :: list()
def load_contexts(user_id) do
config = get_config()
if config.enabled do
call_loader(config.loader, user_id)
else
[]
end
end
@doc """
Gets the display name for a context item using the configured function.
## Examples
iex> PhoenixKit.Dashboard.ContextSelector.get_display_name(farm)
"My Farm"
"""
@spec get_display_name(any()) :: String.t()
def get_display_name(nil), do: ""
def get_display_name(item) do
config = get_config()
if config.enabled and is_function(config.display_name, 1) do
case config.display_name.(item) do
nil -> ""
result -> result
end
else
to_string(item)
end
rescue
_ -> to_string(item)
end
@doc """
Gets the ID for a context item using the configured id_field.
## Examples
iex> PhoenixKit.Dashboard.ContextSelector.get_id(%{id: 123})
123
"""
@spec get_id(any()) :: any()
def get_id(nil), do: nil
def get_id(item) do
config = get_config()
cond do
is_function(config.id_field, 1) ->
config.id_field.(item)
is_atom(config.id_field) ->
get_field(item, config.id_field)
true ->
get_field(item, :id)
end
rescue
_ -> nil
end
@doc """
Finds a context by ID from a list of contexts.
Handles both string and integer ID comparison.
## Examples
iex> PhoenixKit.Dashboard.ContextSelector.find_by_id(contexts, "123")
%Farm{id: 123, ...}
"""
@spec find_by_id(list(), any()) :: any() | nil
def find_by_id(contexts, id) when is_list(contexts) do
Enum.find(contexts, fn item ->
item_id = get_id(item)
ids_match?(item_id, id)
end)
end
def find_by_id(_, _), do: nil
@doc """
Gets the session key for storing the context ID.
"""
@spec session_key() :: String.t()
def session_key do
get_config().session_key
end
@doc """
Loads tabs for the given context using the configured tab_loader.
Returns an empty list if no tab_loader is configured or if the loader fails.
## Examples
iex> PhoenixKit.Dashboard.ContextSelector.load_tabs(context)
[%{id: :overview, label: "Overview", ...}, ...]
"""
@spec load_tabs(any()) :: list()
def load_tabs(context) do
config = get_config()
if config.enabled and config.tab_loader do
call_tab_loader(config.tab_loader, context)
else
[]
end
end
defp call_tab_loader({module, function}, context) do
apply(module, function, [context])
rescue
_ -> []
end
# Private functions
defp validate_config(config) when is_list(config) do
validate_config(Map.new(config))
end
defp validate_config(config) when is_map(config) do
loader = get_config_value(config, :loader)
display_name = get_config_value(config, :display_name)
if valid_loader?(loader) and is_function(display_name, 1) do
build_enabled_config(config, loader, display_name)
else
%__MODULE__{enabled: false}
end
end
defp validate_config(_), do: %__MODULE__{enabled: false}
defp build_enabled_config(config, loader, display_name) do
tab_loader = get_config_value(config, :tab_loader)
raw_position = get_config_value(config, :position)
raw_sub_position = get_config_value(config, :sub_position)
{position, sub_position} = parse_position_and_sub(raw_position, raw_sub_position)
%__MODULE__{
enabled: true,
loader: loader,
display_name: display_name,
id_field: get_config_value(config, :id_field, @default_id_field),
label: get_config_value(config, :label, @default_label),
icon: get_config_value(config, :icon, @default_icon),
position: position,
sub_position: sub_position,
empty_behavior: config |> get_config_value(:empty_behavior) |> parse_empty_behavior(),
session_key: get_config_value(config, :session_key, @default_session_key),
tab_loader: validate_tab_loader(tab_loader),
separator: parse_separator(get_config_value(config, :separator, @default_separator))
}
end
defp validate_tab_loader({module, function}) when is_atom(module) and is_atom(function) do
{module, function}
end
defp validate_tab_loader(_), do: nil
defp get_config_value(config, key, default \\ nil) do
config[key] || config[to_string(key)] || default
end
defp valid_loader?({module, function}) when is_atom(module) and is_atom(function) do
true
end
defp valid_loader?(_), do: false
defp call_loader({module, function}, user_id) do
apply(module, function, [user_id])
rescue
_ -> []
end
# Parse position and sub_position
# Returns {position, sub_position} tuple with defaults applied
defp parse_position_and_sub(position, sub_position) do
case normalize_position(position) do
{:header, default_sub} ->
{:header, parse_sub_position(sub_position, default_sub)}
{:sidebar, default_sub} ->
{:sidebar, parse_sub_position(sub_position, default_sub)}
end
end
# Normalize position values to {area, default_sub_position}
defp normalize_position(:header), do: {:header, :start}
defp normalize_position("header"), do: {:header, :start}
defp normalize_position(:sidebar), do: {:sidebar, :start}
defp normalize_position("sidebar"), do: {:sidebar, :start}
defp normalize_position(_), do: {:header, :start}
# Parse sub_position, falling back to default if not specified
defp parse_sub_position(nil, default), do: default
defp parse_sub_position(:start, _default), do: :start
defp parse_sub_position("start", _default), do: :start
defp parse_sub_position(:end, _default), do: :end
defp parse_sub_position("end", _default), do: :end
defp parse_sub_position({:priority, n}, _default) when is_integer(n), do: {:priority, n}
defp parse_sub_position(_, default), do: default
defp parse_empty_behavior(:hide), do: :hide
defp parse_empty_behavior(:show_empty), do: :show_empty
defp parse_empty_behavior({:redirect, path}) when is_binary(path), do: {:redirect, path}
defp parse_empty_behavior("hide"), do: :hide
defp parse_empty_behavior("show_empty"), do: :show_empty
defp parse_empty_behavior(_), do: @default_empty_behavior
# Parse separator - can be a string, false/nil to disable, or default "/"
defp parse_separator(false), do: nil
defp parse_separator(nil), do: nil
defp parse_separator(""), do: nil
defp parse_separator(sep) when is_binary(sep), do: sep
defp parse_separator(_), do: @default_separator
defp get_field(item, field) when is_map(item), do: Map.get(item, field)
defp get_field(item, field) when is_atom(field), do: Map.get(item, field)
defp get_field(_, _), do: nil
defp ids_match?(id1, id2) when is_integer(id1) and is_binary(id2) do
id1 == String.to_integer(id2)
rescue
_ -> false
end
defp ids_match?(id1, id2) when is_binary(id1) and is_integer(id2) do
String.to_integer(id1) == id2
rescue
_ -> false
end
defp ids_match?(id1, id2), do: id1 == id2
end