diff --git a/demo/lib/demo/user.ex b/demo/lib/demo/user.ex index accdd1bd5..ac321195b 100644 --- a/demo/lib/demo/user.ex +++ b/demo/lib/demo/user.ex @@ -15,7 +15,7 @@ defmodule Demo.User do field :full_name, :string, virtual: true field :age, :integer field :role, Ecto.Enum, values: [:user, :admin] - field :permissions, {:array, :string} + field :permissions, {:array, :string}, default: [] field :avatar, :string field :deleted_at, :utc_datetime diff --git a/demo/lib/demo_web/live/user_live.ex b/demo/lib/demo_web/live/user_live.ex index 3dea7f29c..baffa9e28 100644 --- a/demo/lib/demo_web/live/user_live.ex +++ b/demo/lib/demo_web/live/user_live.ex @@ -122,6 +122,16 @@ defmodule DemoWeb.UserLive do options: [Admin: "admin", User: "user"], prompt: "Choose role..." }, + permissions: %{ + module: Backpex.Fields.Checkgroup, + label: "Permissions", + options: [ + {"Create Posts", "create_posts"}, + {"Edit Posts", "edit_posts"}, + {"Delete Posts", "delete_posts"}, + {"Manage Users", "manage_users"} + ] + }, posts: %{ module: Backpex.Fields.HasMany, label: "Posts", @@ -195,15 +205,6 @@ defmodule DemoWeb.UserLive do label: "Notes" } ] - }, - permissions: %{ - module: Backpex.Fields.MultiSelect, - label: "Permissions", - options: [ - {"Can access admin panel", "can_access_admin_panel"}, - {"Item actions", [{"Delete", "delete"}, {"Edit", "edit"}, {"Show", "show"}]}, - {"Other actions", [{"Can send email", "can_send_email"}]} - ] } ] end diff --git a/demo/priv/repo/migrations/20251205230743_set_default_for_permissions.exs b/demo/priv/repo/migrations/20251205230743_set_default_for_permissions.exs new file mode 100644 index 000000000..401a9023f --- /dev/null +++ b/demo/priv/repo/migrations/20251205230743_set_default_for_permissions.exs @@ -0,0 +1,15 @@ +defmodule Demo.Repo.Migrations.SetDefaultForPermissions do + use Ecto.Migration + + def up do + # Set default for new records + execute "ALTER TABLE users ALTER COLUMN permissions SET DEFAULT '{}'::text[]" + + # Update existing NULL values to empty array + execute "UPDATE users SET permissions = '{}' WHERE permissions IS NULL" + end + + def down do + execute "ALTER TABLE users ALTER COLUMN permissions DROP DEFAULT" + end +end diff --git a/guides/fields/what-is-a-field.md b/guides/fields/what-is-a-field.md index 1d9ee1e6b..a866e6315 100644 --- a/guides/fields/what-is-a-field.md +++ b/guides/fields/what-is-a-field.md @@ -10,6 +10,7 @@ Backpex provides the following built-in field types: - `Backpex.Fields.BelongsTo` - `Backpex.Fields.Boolean` +- `Backpex.Fields.Checkgroup` - `Backpex.Fields.Currency` - `Backpex.Fields.DateTime` - `Backpex.Fields.Date` diff --git a/lib/backpex/fields/checkgroup.ex b/lib/backpex/fields/checkgroup.ex new file mode 100644 index 000000000..a17b97d50 --- /dev/null +++ b/lib/backpex/fields/checkgroup.ex @@ -0,0 +1,109 @@ +defmodule Backpex.Fields.Checkgroup do + @config_schema [ + options: [ + doc: "List of options or function that receives the assigns.", + type: {:or, [{:list, :any}, {:fun, 1}]}, + required: true + ], + readonly: [ + doc: "Sets the field to readonly. Also see the [panels](/guides/fields/readonly.md) guide.", + type: {:or, [:boolean, {:fun, 1}]} + ] + ] + + @moduledoc """ + A field for handling multiple checkboxes with predefined options. + + This field stores selected values as an array. + + > #### Schema and migration defaults {: .warning} + > + > You **must** declare `default: []` on the `{:array, _}` schema field *and* set a matching + > SQL default (`DEFAULT '{}'::text[]` in PostgreSQL). Otherwise unchecking every box will + > persist `NULL` instead of an empty array. The reason is Ecto's `filter_empty_values/3`: + > the hidden sentinel input submits the scalar `""`, which Ecto treats as empty and replaces + > with the schema default. + > + > # Ecto schema + > field :roles, {:array, :string}, default: [] + > + > # Migration + > alter table(:users) do + > modify :roles, {:array, :string}, default: [] + > end + + ## Field-specific options + + See `Backpex.Field` for general field options. + + #{NimbleOptions.docs(@config_schema)} + + ## Example + + @impl Backpex.LiveResource + def fields do + [ + roles: %{ + module: Backpex.Fields.Checkgroup, + label: "Roles", + options: [{"Admin", "admin"}, {"User", "user"}, {"Editor", "editor"}] + } + ] + end + """ + use Backpex.Field, config_schema: @config_schema + + @impl Backpex.Field + def render_value(assigns) do + options = get_options(assigns) + labels = get_labels(assigns.value, options) + + assigns = assign(assigns, :labels, labels) + + ~H""" +
+ {if @labels == [], do: raw("—"), else: @labels |> Enum.map(&HTML.pretty_value/1) |> Enum.join(", ")} +
+ """ + end + + @impl Backpex.Field + def render_form(assigns) do + options = get_options(assigns) + + assigns = assign(assigns, :options, options) + + ~H""" ++
{render_slot(@inner_block)}
"""