Packages
phoenix_kit
1.7.179
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/live/users/roles.html.heex
<PhoenixKitWeb.Components.LayoutWrapper.app_layout
socket={@socket}
flash={@flash}
phoenix_kit_current_scope={assigns[:phoenix_kit_current_scope]}
page_title={gettext("Role Management")}
page_subtitle={gettext("Create and manage user roles")}
current_path={@url_path}
project_title={@project_title}
current_locale={@current_locale}
>
<div class="container flex-col mx-auto px-4 py-6">
<%!-- Create Role Modal --%>
<.modal show={@show_create_form} on_close="hide_form" max_width="2xl">
<:title>{gettext("Create New Role")}</:title>
<form id="create-role-form" phx-submit="create_role" class="space-y-4">
<div class="form-control">
<.input
field={@create_role_form[:name]}
label={gettext("Role Name *")}
placeholder={gettext("Enter role name (e.g., Manager, Editor)")}
maxlength="50"
required
/>
<p class="label mt-1">{gettext("Must be unique, 1-50 characters.")}</p>
</div>
<div class="form-control">
<.textarea
field={@create_role_form[:description]}
label={gettext("Description")}
placeholder={gettext("Describe what this role can do (optional)")}
maxlength="500"
/>
<p class="label mt-1">{gettext("Optional, up to 500 characters.")}</p>
</div>
</form>
<:actions>
<button type="button" phx-click="hide_form" class="btn btn-outline">
{gettext("Cancel")}
</button>
<button type="submit" form="create-role-form" class="btn btn-primary">
{gettext("Create Role")}
</button>
</:actions>
</.modal>
<%!-- Edit Role Modal --%>
<.modal show={@show_edit_form} on_close="hide_form" max_width="2xl">
<:title>{gettext("Edit Role")}</:title>
<form id="edit-role-form" phx-submit="update_role" class="space-y-4">
<div class="form-control">
<.input
field={@edit_role_form[:name]}
label={gettext("Role Name *")}
placeholder={gettext("Enter role name (e.g., Manager, Editor)")}
maxlength="50"
required
/>
<p class="label mt-1">{gettext("Must be unique, 1-50 characters.")}</p>
</div>
<div class="form-control">
<.textarea
field={@edit_role_form[:description]}
label={gettext("Description")}
placeholder={gettext("Describe what this role can do (optional)")}
maxlength="500"
/>
<p class="label mt-1">{gettext("Optional, up to 500 characters.")}</p>
</div>
</form>
<:actions>
<button type="button" phx-click="hide_form" class="btn btn-outline">
{gettext("Cancel")}
</button>
<button type="submit" form="edit-role-form" class="btn btn-primary">
{gettext("Update Role")}
</button>
</:actions>
</.modal>
<%!-- Roles List --%>
<div class="bg-base-100">
<%= if length(@roles) > 0 do %>
<.table_default
id="roles-table"
variant="zebra"
toggleable={true}
items={@roles}
card_fields={
fn role ->
[
%{
label: gettext("Type"),
value: if(role.is_system_role, do: gettext("System"), else: gettext("Custom"))
},
%{label: gettext("Users"), value: Map.get(@role_stats, role.name, 0)},
%{label: gettext("Description"), value: role.description || gettext("—")}
]
end
}
>
<:toolbar_title>
<span class="text-sm text-base-content/60">
{length(@roles)} {gettext("roles")}
</span>
</:toolbar_title>
<:toolbar_actions>
<button phx-click="show_create_form" class="btn btn-primary btn-sm">
<.icon name="hero-plus" class="w-4 h-4" /> {gettext("Create New Role")}
</button>
</:toolbar_actions>
<:card_header :let={role}>
<div class="flex items-center gap-2">
<.role_badge role={role} />
<%= if role.is_system_role do %>
<span class="badge badge-outline badge-sm h-auto">{gettext("System")}</span>
<% else %>
<span class="badge badge-success badge-sm h-auto">{gettext("Custom")}</span>
<% end %>
</div>
</:card_header>
<:card_actions :let={role}>
<.table_row_menu id={"role-card-menu-#{role.uuid}"} label={gettext("Role actions")}>
<%= if !role.is_system_role do %>
<.table_row_menu_button
phx-click="show_edit_role"
phx-value-role_uuid={role.uuid}
icon="hero-pencil"
label={gettext("Edit")}
variant="primary"
/>
<.table_row_menu_divider />
<.table_row_menu_button
phx-click="request_delete_role"
phx-value-role_uuid={role.uuid}
phx-value-role_name={role.name}
icon="hero-trash"
label={gettext("Delete")}
variant="error"
/>
<% else %>
<li class="px-3 py-2 text-xs text-base-content/50 italic select-none">
{gettext("Protected")}
</li>
<% end %>
<%= if @can_manage_permissions && !MapSet.member?(@uneditable_role_uuids, to_string(role.uuid)) do %>
<.table_row_menu_divider />
<.table_row_menu_button
phx-click="show_permissions_editor"
phx-value-role_uuid={role.uuid}
icon="hero-key"
label={gettext("Permissions")}
variant="secondary"
/>
<% end %>
</.table_row_menu>
</:card_actions>
<.table_default_header>
<.table_default_row>
<.table_default_header_cell>{gettext("Role")}</.table_default_header_cell>
<.table_default_header_cell>{gettext("Type")}</.table_default_header_cell>
<.table_default_header_cell>{gettext("Description")}</.table_default_header_cell>
<.table_default_header_cell>{gettext("Users")}</.table_default_header_cell>
<.table_default_header_cell>{gettext("Actions")}</.table_default_header_cell>
</.table_default_row>
</.table_default_header>
<.table_default_body>
<%= for role <- @roles do %>
<.table_default_row>
<.table_default_cell>
<div class="flex items-center gap-2">
<.role_badge role={role} />
</div>
</.table_default_cell>
<.table_default_cell>
<%= if role.is_system_role do %>
<span class="badge badge-outline badge-sm h-auto">
{gettext("System Role")}
</span>
<% else %>
<span class="badge badge-success badge-sm h-auto">
{gettext("Custom Role")}
</span>
<% end %>
</.table_default_cell>
<.table_default_cell class="max-w-xs">
<div class="text-sm">
<%= if role.description do %>
{role.description}
<% else %>
<span class="text-base-content/50 italic">{gettext("No description")}</span>
<% end %>
</div>
</.table_default_cell>
<.table_default_cell>
<div class="flex items-center gap-2">
<.users_count role={role} stats={@role_stats} />
<span class="text-sm text-base-content/70">{gettext("users")}</span>
</div>
</.table_default_cell>
<.table_default_cell>
<.table_row_menu id={"role-menu-#{role.uuid}"} label={gettext("Role actions")}>
<%= if !role.is_system_role do %>
<.table_row_menu_button
phx-click="show_edit_role"
phx-value-role_uuid={role.uuid}
icon="hero-pencil"
label={gettext("Edit")}
variant="primary"
/>
<.table_row_menu_divider />
<.table_row_menu_button
phx-click="request_delete_role"
phx-value-role_uuid={role.uuid}
phx-value-role_name={role.name}
icon="hero-trash"
label={gettext("Delete")}
variant="error"
/>
<% else %>
<li class="px-3 py-2 text-xs text-base-content/50 italic select-none">
{gettext("Protected")}
</li>
<% end %>
<%= if @can_manage_permissions && !MapSet.member?(@uneditable_role_uuids, to_string(role.uuid)) do %>
<.table_row_menu_divider />
<.table_row_menu_button
phx-click="show_permissions_editor"
phx-value-role_uuid={role.uuid}
icon="hero-key"
label={gettext("Permissions")}
variant="secondary"
/>
<% end %>
</.table_row_menu>
</.table_default_cell>
</.table_default_row>
<% end %>
</.table_default_body>
</.table_default>
<% else %>
<%!-- Empty state --%>
<div class="text-center py-12">
<.icon name="hero-shield-check" class="h-16 w-16 mx-auto text-base-content/40 mb-4" />
<h3 class="text-lg font-medium text-base-content mb-2">
{gettext("No roles found")}
</h3>
<p class="text-base-content/70 mb-4">
{gettext("Start by creating your first custom role.")}
</p>
<button
phx-click="show_create_form"
class="btn btn-primary"
>
{gettext("Create First Role")}
</button>
</div>
<% end %>
</div>
<%!-- Role Statistics --%>
<div class="mt-8">
<h3 class="text-lg font-semibold text-base-content mb-6">{gettext("Role Statistics")}</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<%!-- System Roles Count --%>
<PhoenixKitWeb.Components.Core.StatCard.stat_card
compact={true}
rounded="xl"
value={Enum.count(@roles, & &1.is_system_role)}
title={gettext("System Roles")}
subtitle={gettext("Protected core roles")}
>
<:icon>
<.icon name="hero-lock-closed" class="w-5 h-5" />
</:icon>
</PhoenixKitWeb.Components.Core.StatCard.stat_card>
<%!-- Custom Roles Count --%>
<PhoenixKitWeb.Components.Core.StatCard.stat_card
compact={true}
rounded="xl"
value={Enum.count(@roles, &(!&1.is_system_role))}
title={gettext("Custom Roles")}
subtitle={gettext("User-defined roles")}
>
<:icon>
<.icon name="hero-cog-6-tooth" class="w-5 h-5" />
</:icon>
</PhoenixKitWeb.Components.Core.StatCard.stat_card>
<%!-- Total Roles Count --%>
<PhoenixKitWeb.Components.Core.StatCard.stat_card
compact={true}
rounded="xl"
value={length(@roles)}
title={gettext("Total Roles")}
subtitle={gettext("All available roles")}
>
<:icon>
<.icon name="hero-chart-bar" class="w-5 h-5" />
</:icon>
</PhoenixKitWeb.Components.Core.StatCard.stat_card>
</div>
</div>
<%!-- Delete Confirmation Modal --%>
<.confirm_modal
show={assigns[:delete_confirmation] && @delete_confirmation.show}
on_confirm="confirm_delete_role"
on_cancel="cancel_delete"
title={gettext("Delete Role")}
title_icon="hero-trash"
danger={true}
messages={[
{:warning,
gettext(
"Are you sure you want to delete the role \"%{role_name}\"? This action cannot be undone.",
role_name: Map.get(@delete_confirmation, :role_name, "")
)}
]}
confirm_text={gettext("Delete Role")}
/>
<%!-- Permissions Editor Modal --%>
<.modal
show={@show_permissions_editor && @permissions_role != nil}
on_close="hide_permissions_editor"
max_width="3xl"
>
<:title>
{gettext("Permissions for")}
<.role_badge role={@permissions_role || %{name: "", is_system_role: false}} />
</:title>
<p class="text-sm text-base-content/70 mb-4">
{gettext(
"Select which sections and modules this role can access. You can only grant permissions that your own role has."
)}
</p>
<%!-- Quick Actions --%>
<div class="flex gap-2 mb-4">
<button phx-click="grant_all_permissions" class="btn btn-xs btn-outline btn-success">
{gettext("Grant All")}
</button>
<button phx-click="revoke_all_permissions" class="btn btn-xs btn-outline btn-error">
{gettext("Revoke All")}
</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<%!-- Core Sections --%>
<div>
<h4 class="font-semibold text-sm text-base-content/80 mb-3 uppercase tracking-wide">
{gettext("Core Sections")}
</h4>
<div class="space-y-2">
<%= for key <- PhoenixKit.Users.Permissions.core_section_keys(), MapSet.member?(@permissions_grantable_keys, key) do %>
<label class="flex items-center gap-3 cursor-pointer p-2 rounded-lg hover:bg-base-200 transition-colors">
<input
type="checkbox"
class="checkbox checkbox-sm checkbox-primary"
checked={MapSet.member?(@permissions_role_keys, key)}
phx-click="toggle_permission"
phx-value-key={key}
/>
<span class="text-sm">{PhoenixKit.Users.Permissions.module_label(key)}</span>
</label>
<% end %>
</div>
</div>
<%!-- Feature Modules --%>
<div>
<h4 class="font-semibold text-sm text-base-content/80 mb-3 uppercase tracking-wide">
{gettext("Feature Modules")}
</h4>
<div class="space-y-2 max-h-80 overflow-y-auto">
<%= for key <- PhoenixKit.Users.Permissions.feature_module_keys(), MapSet.member?(@permissions_grantable_keys, key) do %>
<label class="flex items-center gap-3 cursor-pointer p-2 rounded-lg hover:bg-base-200 transition-colors">
<input
type="checkbox"
class="checkbox checkbox-sm checkbox-primary"
checked={MapSet.member?(@permissions_role_keys, key)}
phx-click="toggle_permission"
phx-value-key={key}
/>
<span class="text-sm">{PhoenixKit.Users.Permissions.module_label(key)}</span>
</label>
<% end %>
</div>
</div>
</div>
<%!-- Custom (permission keys registered by parent app) --%>
<%= if length(PhoenixKit.Users.Permissions.custom_keys()) > 0 do %>
<div class="md:col-span-2 mt-4">
<h4 class="font-semibold text-sm text-accent mb-3 uppercase tracking-wide">
{gettext("Custom")}
</h4>
<div class="columns-1 md:columns-2 gap-x-6">
<%= for key <- PhoenixKit.Users.Permissions.custom_keys(), MapSet.member?(@permissions_grantable_keys, key) do %>
<label class="flex items-center gap-3 cursor-pointer p-2 rounded-lg hover:bg-base-200 transition-colors break-inside-avoid">
<input
type="checkbox"
class="checkbox checkbox-sm checkbox-primary"
checked={MapSet.member?(@permissions_role_keys, key)}
phx-click="toggle_permission"
phx-value-key={key}
/>
<span class="text-sm">{PhoenixKit.Users.Permissions.module_label(key)}</span>
</label>
<% end %>
</div>
</div>
<% end %>
<%!-- Summary --%>
<div class="mt-4 p-3 bg-base-200 rounded-lg">
<span class="text-sm text-base-content/70">
{gettext("%{granted} of %{total} manageable permissions granted",
granted: MapSet.size(@permissions_role_keys),
total: MapSet.size(@permissions_grantable_keys)
)}
</span>
<%= if MapSet.size(@permissions_preserved_keys) > 0 do %>
<span class="text-sm text-base-content/50 ml-2">
{gettext("(%{count} for disabled modules, preserved on save)",
count: MapSet.size(@permissions_preserved_keys)
)}
</span>
<% end %>
</div>
<:actions>
<button type="button" phx-click="hide_permissions_editor" class="btn btn-outline">
{gettext("Cancel")}
</button>
<button phx-click="save_permissions" class="btn btn-primary">
{gettext("Save Permissions")}
</button>
</:actions>
</.modal>
</div>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>