PhoenixKit.Users.CustomFields (phoenix_kit v2.15.1)

Copy Markdown View Source

Context for managing custom user field definitions.

This module provides functionality to define, manage, and validate custom fields that can be added to user profiles. Field definitions are stored as JSON in the settings table, and actual field values are stored in the user's custom_fields JSONB column.

Field Definition Structure

Each field definition is a map with the following keys:

  • key - Unique identifier for the field (string)
  • label - Display label for the field (string)
  • type - Field type: text, textarea, number, boolean, date, email, url, uuid, select, radio, checkbox
  • required - Whether the field is required (boolean)
  • position - Display order (integer)
  • enabled - Whether the field is active (boolean)
  • user_accessible - Whether users can edit this field from their settings page (boolean, default: true)
  • validation - Optional validation rules (map)
  • default - Default value (string)
  • options - For select/radio/checkbox types (list of strings)

Examples

# Get all field definitions
CustomFields.list_field_definitions()

# Add a new field
CustomFields.add_field_definition(%{
  "key" => "phone",
  "label" => "Phone Number",
  "type" => "text",
  "required" => false,
  "position" => 1,
  "enabled" => true
})

# Get system configuration
CustomFields.get_config()

Summary

Functions

Adds a new field definition.

Deletes a field definition by key.

Ensures field definitions exist for all keys in the given custom fields map.

Gets the current custom fields system configuration.

Gets a single field definition by key.

Gets the display text for a select field value (which is stored as index).

Infers the custom field type from a value.

Whether key is one of PhoenixKit's internal per-user state keys.

Returns only enabled field definitions, sorted by position.

Returns the list of all custom field definitions.

Returns only enabled field definitions that are user-accessible, sorted by position.

Renders a stored custom_fields value as a string that is safe to display.

Reorders field definitions by updating their position values.

Saves the complete list of field definitions.

Whether a stored value is structured (a map or a list) rather than a scalar.

Updates an existing field definition.

Validates a user's custom field value against a field definition.

Validates a field definition structure.

Validates all custom fields for a user against field definitions.

Functions

add_field_definition(field_def)

Adds a new field definition.

Validates the field structure and ensures the key is unique.

Examples

iex> add_field_definition(%{"key" => "phone", "label" => "Phone", "type" => "text"})
{:ok, _setting}

iex> add_field_definition(%{"key" => "phone"})
{:error, "Field with key 'phone' already exists"}

delete_field_definition(key)

Deletes a field definition by key.

Examples

iex> delete_field_definition("phone")
{:ok, _setting}

ensure_definitions_exist(custom_fields)

Ensures field definitions exist for all keys in the given custom fields map.

For any key that doesn't have a corresponding field definition, auto-creates one with a label derived from the key, type inferred from the value, and user_accessible: false (admin-only by default).

Two kinds of key are skipped silently, because a definition for either one produces an admin form that cannot render it (issue #780):

  • Structured values — a map or a list. Every supported type describes a scalar, so the closest infer_field_type/1 can offer is "text", and the edit form then hands the raw value to an <input>: a map raises Phoenix.HTML.Safe, a list is flattened into one concatenated run that the next save writes back over the stored list.
  • PhoenixKit's own internal keys (internal_key?/1) — per-user state, not profile data.

Returns :ok. Logs warnings for any definitions that fail to create.

get_config()

Gets the current custom fields system configuration.

Returns a map with field counts.

Examples

iex> get_config()
%{
  field_count: 5,
  enabled_field_count: 3
}

get_field_definition(key)

Gets a single field definition by key.

Returns nil if not found.

Examples

iex> get_field_definition("phone")
%{"key" => "phone", "label" => "Phone Number", ...}

iex> get_field_definition("nonexistent")
nil

get_option_text(field_key, index_value)

Gets the display text for a select field value (which is stored as index).

For select/radio/checkbox fields, the saved value is the index (0, 1, 2...) and this function returns the actual option text.

Examples

iex> get_option_text("favorite_color", "1")
"Blue"  # if options are ["Red", "Blue", "Green"]

iex> get_option_text("nonexistent", "1")
"1"  # fallback to raw value

infer_field_type(value)

Infers the custom field type from a value.

Returns one of: "boolean", "number", "uuid", "url", "email", "text".

internal_key?(key)

@spec internal_key?(term()) :: boolean()

Whether key is one of PhoenixKit's internal per-user state keys.

These live in the same custom_fields JSONB column as admin-defined profile fields, but they are written by features (the media browser's view mode, the etcher's palette, notification routing) rather than by an admin, and several of them hold a map or a list. They must never gain a field definition — see ensure_definitions_exist/1.

Examples

iex> PhoenixKit.Users.CustomFields.internal_key?("etcher_line_params")
true

iex> PhoenixKit.Users.CustomFields.internal_key?("notification_channel:telegram")
true

iex> PhoenixKit.Users.CustomFields.internal_key?("phone")
false

list_enabled_field_definitions()

Returns only enabled field definitions, sorted by position.

Examples

iex> list_enabled_field_definitions()
[%{"key" => "phone", "enabled" => true, ...}]

list_field_definitions()

Returns the list of all custom field definitions.

Examples

iex> list_field_definitions()
[
  %{
    "key" => "phone",
    "label" => "Phone Number",
    "type" => "text",
    ...
  }
]

list_user_accessible_field_definitions()

Returns only enabled field definitions that are user-accessible, sorted by position.

These are fields that users can view and edit from their own settings page. Admins can always see and edit all fields regardless of this setting.

Legacy fields without the user_accessible key default to true (accessible).

Examples

iex> list_user_accessible_field_definitions()
[%{"key" => "phone", "enabled" => true, "user_accessible" => true, ...}]

printable(value)

@spec printable(term()) :: String.t()

Renders a stored custom_fields value as a string that is safe to display.

to_string/1 raises Protocol.UndefinedError on a map, which took the whole user page down with a 500 for any user who had ever used a feature that stores one, and silently concatenates a list into one unreadable run. Maps render as JSON and lists as a comma-separated join instead.

Examples

iex> PhoenixKit.Users.CustomFields.printable(%{"dash" => "solid"})
~s({"dash":"solid"})

iex> PhoenixKit.Users.CustomFields.printable(["#fca5a5", "#fdba74"])
"#fca5a5, #fdba74"

iex> PhoenixKit.Users.CustomFields.printable(42)
"42"

reorder_field_definitions(ordered_keys)

Reorders field definitions by updating their position values.

Accepts a list of keys in the desired order.

Examples

iex> reorder_field_definitions(["email", "phone", "department"])
{:ok, _setting}

save_field_definitions(definitions)

Saves the complete list of field definitions.

Examples

iex> save_field_definitions([%{"key" => "phone", ...}])
{:ok, _setting}

structured_value?(value)

@spec structured_value?(term()) :: boolean()

Whether a stored value is structured (a map or a list) rather than a scalar.

custom_fields is free-form JSONB, so a value of either shape can sit under a key that has a definition. No field type can represent one, so callers render them read-only instead of into an editable input.

Examples

iex> PhoenixKit.Users.CustomFields.structured_value?(%{"width" => 2})
true

iex> PhoenixKit.Users.CustomFields.structured_value?("2")
false

update_field_definition(key, updates)

Updates an existing field definition.

Examples

iex> update_field_definition("phone", %{"label" => "Mobile Number"})
{:ok, _setting}

iex> update_field_definition("nonexistent", %{})
{:error, "Field with key 'nonexistent' not found"}

validate_custom_field_value(field_def, value)

Validates a user's custom field value against a field definition.

Examples

iex> validate_custom_field_value(%{"type" => "email", "required" => true}, "test@example.com")
:ok

iex> validate_custom_field_value(%{"type" => "email", "required" => true}, nil)
{:error, "Field is required"}

validate_field_definition(field_def)

Validates a field definition structure.

Checks for required keys, valid types, and proper structure.

Examples

iex> validate_field_definition(%{"key" => "phone", "type" => "text"})
:ok

iex> validate_field_definition(%{"key" => "phone", "type" => "invalid"})
{:error, "Invalid field type: invalid"}

validate_user_custom_fields(user)

Validates all custom fields for a user against field definitions.

Returns :ok if valid, or {:error, errors} with a map of field keys to error messages.

Examples

iex> validate_user_custom_fields(%User{custom_fields: %{"phone" => "555-1234"}})
:ok

iex> validate_user_custom_fields(%User{custom_fields: %{"email" => "invalid"}})
{:error, %{"email" => "Invalid email format"}}