Packages
phoenix_kit
2.52.2
2.62.2
2.62.1
2.62.0
2.61.0
2.60.3
2.60.2
2.60.1
2.60.0
2.59.0
2.58.0
2.57.1
2.57.0
2.56.1
2.56.0
2.55.1
2.55.0
2.54.2
2.54.1
2.54.0
2.53.0
2.52.2
2.52.1
2.52.0
2.51.0
2.50.0
2.49.1
2.49.0
2.48.0
2.47.0
2.46.0
2.45.0
2.44.0
2.43.1
2.43.0
2.42.1
2.42.0
2.41.6
2.41.4
2.41.3
2.41.2
2.41.1
2.41.0
2.40.1
2.40.0
2.39.0
2.38.1
2.38.0
2.37.5
2.37.4
2.37.3
2.37.2
2.37.1
2.37.0
2.36.1
2.36.0
2.35.0
2.34.0
2.33.0
2.32.1
2.32.0
2.31.1
2.31.0
2.30.0
2.29.1
2.29.0
2.28.2
2.28.1
2.28.0
2.27.2
2.27.1
2.27.0
2.26.1
2.26.0
2.25.0
2.24.0
2.23.3
2.23.2
2.23.1
2.23.0
2.22.24
2.22.23
2.22.22
2.22.21
2.22.20
2.22.19
2.22.18
2.22.17
2.22.16
2.22.15
2.22.14
2.22.13
2.22.12
2.22.11
2.22.10
2.22.9
2.22.8
2.22.7
2.22.6
2.22.5
2.22.4
2.22.3
2.22.2
2.22.1
2.22.0
2.21.5
2.21.4
2.21.3
2.21.2
2.21.1
2.21.0
2.20.0
2.19.0
2.18.1
2.18.0
2.17.0
2.16.0
2.15.1
2.15.0
2.14.2
2.14.1
2.14.0
2.13.19
2.13.18
2.13.17
2.13.16
2.13.15
2.13.13
2.13.12
2.13.11
2.13.10
2.13.9
2.13.8
2.13.7
2.13.6
2.13.5
2.13.4
2.13.3
2.13.2
2.13.1
2.13.0
2.12.1
2.12.0
2.11.0
2.10.0
2.9.0
2.8.1
2.8.0
2.7.0
2.6.0
2.5.0
2.4.0
2.3.0
2.2.0
2.1.0
2.0.1
2.0.0
1.7.236
1.7.235
1.7.234
1.7.233
1.7.232
1.7.231
1.7.230
1.7.229
1.7.228
1.7.227
1.7.226
1.7.225
1.7.224
1.7.223
1.7.222
1.7.221
1.7.220
1.7.219
1.7.218
1.7.217
1.7.216
1.7.215
1.7.214
1.7.213
1.7.212
1.7.211
1.7.210
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_web/components/profile_settings_tabs.ex
defmodule PhoenixKitWeb.Components.ProfileSettingsTabs do
@moduledoc """
The tabs of the signed-in user's own settings, `/profile/settings/<tab>`.
One URL per tab, and a tab is offered only when it has something to show
for this user:
* `account` — who you are: avatar and name, custom fields, email, the
start page, and the annotation-tools reset
* `security` — password and connected sign-in accounts
* `sessions` — signed-in devices and recent sign-in attempts
* `notifications` — which notifications you get (when any types exist)
* `integrations` — your own service connections, for holders of the
opt-in `integrations` permission. Its own LiveView at
`/profile/settings/integrations`.
* `media` — your storage libraries and their members
(`PhoenixKitWeb.Live.Components.LibrarySettings`), while user
libraries are on and you hold the `"storage"` permission. The
end-user surface for them is `phoenix_kit_photos`; this tab is where
they are set up.
`/profile/settings` opens the first tab. Every section of
`PhoenixKitWeb.Live.Components.UserSettings.default_sections/0` is on
exactly one tab (a section on none would be one nobody can reach).
## Hiding sections
An admin can hide sections a site does not use (Settings → Users →
Profile page). What is stored is the list of HIDDEN sections, in the
`user_settings_hidden_sections` setting — so a section added in a later
release shows up on existing sites instead of staying invisible. A tab
whose sections are all hidden is not offered. `:identity` (name and
avatar) cannot be hidden. `:google_email` is the Google address field
inside the identity form, hideable on its own.
"""
use PhoenixKitWeb, :html
require Logger
alias PhoenixKit.Modules.Storage.Libraries
alias PhoenixKit.Notifications.Types, as: NotificationTypes
alias PhoenixKit.Users.Auth.Scope
alias PhoenixKit.Utils.Routes
# The `UserSettings` sections each tab renders, in page order.
@sections %{
"account" => [:identity, :custom_fields, :email, :start_page, :etcher],
"security" => [:password, :oauth],
"sessions" => [:sessions],
"notifications" => [:notifications]
}
@order ~w(account security sessions notifications integrations media)
@hidden_key "user_settings_hidden_sections"
# What an admin may hide, in the order the settings page lists them.
@hideable [
:google_email,
:custom_fields,
:email,
:start_page,
:etcher,
:password,
:oauth,
:sessions,
:notifications
]
@doc "The sections (and the `:google_email` field) an admin may hide."
@spec hideable_sections() :: [atom()]
def hideable_sections, do: @hideable
@doc "The sections an admin has hidden. Unknown names are ignored."
@spec hidden_sections() :: [atom()]
def hidden_sections do
@hidden_key
|> PhoenixKit.Settings.get_json_setting_cached(%{"hidden" => []})
|> stored_hidden()
rescue
# Fails open (every section shows) — but on the record. An unreachable DB
# raises on an unowned checkout but exits on a dead pool, so both are caught.
error ->
Logger.warning(
"[ProfileSettingsTabs] hidden sections unreadable: #{Exception.message(error)}"
)
[]
catch
:exit, reason ->
Logger.warning("[ProfileSettingsTabs] hidden sections unreadable: #{inspect(reason)}")
[]
end
defp stored_hidden(%{"hidden" => names}) when is_list(names),
do: Enum.filter(@hideable, &(Atom.to_string(&1) in names))
defp stored_hidden(_value), do: []
@doc """
Hides or shows `section` for every user. `section` must be hideable.
`opts` reach the setting write (`actor_uuid:`, `source:`), so the change
is attributed like any other settings save.
"""
@spec set_section_hidden(atom(), boolean(), keyword()) :: :ok | {:error, term()}
#
# A read-modify-write of one list, so it runs under a transaction lock on
# the key and reads the stored value, not the cache: two admins toggling
# different sections at once must both land.
def set_section_hidden(section, hidden?, opts \\ [])
def set_section_hidden(section, hidden?, opts) when section in @hideable do
repo = PhoenixKit.RepoHelper.repo()
repo.transaction(fn ->
repo.query!("SELECT pg_advisory_xact_lock(hashtext($1))", [@hidden_key])
names =
@hidden_key
|> PhoenixKit.Settings.get_json_setting(%{"hidden" => []})
|> stored_hidden()
|> then(&if(hidden?, do: Enum.uniq(&1 ++ [section]), else: List.delete(&1, section)))
|> Enum.map(&Atom.to_string/1)
case PhoenixKit.Settings.update_json_setting(@hidden_key, %{"hidden" => names}, opts) do
{:ok, _} -> :ok
{:error, reason} -> repo.rollback(reason)
end
end)
|> case do
{:ok, :ok} ->
# The write invalidated the cache before the transaction committed,
# so a reader in between could have cached the OLD list under the
# new generation. Invalidate again now that the new row is visible.
PhoenixKit.Cache.invalidate(:settings, @hidden_key)
:ok
{:error, reason} ->
{:error, reason}
end
end
def set_section_hidden(_section, _hidden?, _opts), do: {:error, :not_hideable}
@doc "Whether the Google address field shows in the identity form."
@spec google_email_shown?() :: boolean()
def google_email_shown?, do: :google_email not in hidden_sections()
@doc "The tab `/profile/settings` opens on."
@spec default_tab() :: String.t()
def default_tab, do: "account"
@doc """
The `UserSettings` sections a tab rendered by `ProfileSettings` shows, or
nil for a tab that is not one of them (integrations has a page of its own).
"""
@spec sections(String.t()) :: [atom()] | nil
def sections(tab) do
case Map.get(@sections, tab) do
nil -> nil
sections -> sections -- hidden_sections()
end
end
@doc "Whether `ProfileSettings` renders the tab itself (every tab but integrations)."
@spec rendered_here?(String.t()) :: boolean()
def rendered_here?(tab), do: Map.has_key?(@sections, tab) or tab == "media"
@doc "Every section some tab shows."
@spec all_sections() :: [atom()]
def all_sections, do: @sections |> Map.values() |> List.flatten()
@doc "The ids of the tabs `scope` is offered, in order."
@spec tab_ids(Scope.t() | nil) :: [String.t()]
def tab_ids(scope), do: Enum.filter(@order, &visible?(&1, scope))
defp visible?("notifications", _scope),
do: NotificationTypes.list() != [] and sections("notifications") != []
defp visible?("integrations", scope),
do: not is_nil(scope) and Scope.has_module_access?(scope, "integrations")
defp visible?("media", scope), do: Libraries.may_use_libraries?(scope)
# A tab of hidden sections only has nothing to show.
defp visible?(tab, _scope) when is_map_key(@sections, tab), do: sections(tab) != []
defp visible?(_tab, _scope), do: true
@doc "The path of a tab."
@spec path(String.t()) :: String.t()
def path(tab), do: Routes.path("/profile/settings/#{tab}")
@doc """
The tab strip. `active` is the tab being shown; `scope` decides which tabs
are offered. Tabs rendered by the same LiveView patch; the integrations
tab, a LiveView of its own, navigates.
"""
attr :active, :string, required: true
attr :scope, :any, required: true
def profile_tabs(assigns) do
assigns =
assign(assigns, :tabs, Enum.map(tab_ids(assigns.scope), &tab(&1, assigns.active)))
~H"""
<.nav_tabs active_tab={@active} tabs={@tabs} variant={:border} class="mb-6" />
"""
end
# From inside `ProfileSettings`, the tabs it renders itself patch and the
# integrations tab navigates; from the integrations page every tab
# navigates back into `ProfileSettings`.
defp tab(id, active) do
link =
if id != "integrations" and rendered_here?(active),
do: [patch: path(id)],
else: [navigate: path(id)]
Map.merge(%{id: id, label: tab_label(id), icon: tab_icon(id)}, Map.new(link))
end
defp tab_label("account"), do: gettext("Account")
defp tab_label("security"), do: gettext("Security")
defp tab_label("sessions"), do: gettext("Sessions")
defp tab_label("notifications"), do: gettext("Notifications")
defp tab_label("integrations"), do: gettext("Integrations")
defp tab_label("media"), do: gettext("Media")
defp tab_icon("account"), do: "hero-user-circle"
defp tab_icon("security"), do: "hero-lock-closed"
defp tab_icon("sessions"), do: "hero-computer-desktop"
defp tab_icon("notifications"), do: "hero-bell"
defp tab_icon("integrations"), do: "hero-link"
defp tab_icon("media"), do: "hero-rectangle-stack"
end