MishkaGervaz.Helpers (MishkaGervaz v0.0.2)

Copy Markdown View Source

Shared helper functions for MishkaGervaz.

Summary

Functions

Checks if an entity (filter, action, etc.) is accessible based on visibility and restrictions.

A row action's button label: its ui label, else its humanized name, translated in the caller's locale.

Drops nil values from a map and returns nil if the result is empty, otherwise returns the cleaned map.

Whether module defines fun/arity, loading the module first.

Unwrap a preload entry to its source atom. Preloads can be plain atoms or {source, alias} tuples — this returns the atom either way.

Unwraps a Spark singleton sub-entity wrapper into the entity itself.

The names of every entry that depends_on one of changed_parents, directly or down the chain.

Finds an entity in a list by its name field.

Formats a file size in bytes to a human-readable string.

Look up the Ash domain for a resource.

Fetch the domain-side defaults map for a given section (:form or :table). Returns %{} when the resource has no domain or the domain has no mishka_gervaz block.

Returns the resource's Ash attributes as a %{name => attribute_struct} map.

Extracts and resolves a label from a UI structure, with fallback to humanized name.

Filters columns based on their visibility setting.

Checks if a value is present (not nil, empty string, or empty list).

Converts an atom or string to a human-readable label.

Injects preload alias values into a record or list of records.

Invalidates dependent filter values and relation_filter_state when parent filters change.

Checks if a string name is a known entity name in the given state.

Whether name is a declared filter, step or upload on this state.

Fetch a key from a possibly-nil map, returning default when the key is missing or the value is nil.

Puts a key-value pair into a map only if the value is present and valid.

Returns true when the user has no tenant — i.e. is a master user.

Conditionally assigns a key-value pair to assigns only if the value is not nil.

Merges form-state relation field values into a params map.

The actions value names that resource does not have as a type action.

The compile error for the entries missing_actions/4 returned, each with an :origin of :resource, {:domain, domain} or :default.

Converts a module name to its snake_case short name.

Normalizes an Ash primary-key type to one of :uuid, :uuid_v7, :integer, or :string. Falls back to :uuid for anything else.

Normalizes a list of options for HTML select elements.

Normalizes selected values for multi-select components.

Returns the normalized type of resource's primary key as one of :uuid, :uuid_v7, :integer, or :string. Defaults to :uuid when introspection fails or the resource has no primary key.

Returns the normalized id-type for a :relation entity, defaulting to :uuid when the related resource cannot be found. A :relation entity with no related resource and its own options list is :string — its values are the list's, not a resource's keys.

Resolves the destination resource of a relation field/filter.

Resolves a bulk action's confirm.

Resolves an action's confirm — the sibling of resolve_label/1 for a message that needs the record.

Resolves a header, footer or notice text: a string (translated), fn -> text end, or fn state -> text end.

Resolves a label that may be a string or a zero-arity function.

Resolves a display value from a record using either an atom field or a function.

A relation option's label, with the live state available to the display_field.

Resolves options that may be a list or a zero-arity function returning a list.

resolve_options/1 for the options a resource declared: the labels of a list are translated in the caller's locale (translate_options/1), the result of a function is returned as it is.

Resolves a label from a nested UI structure.

value with every HTML tag taken out, by HtmlSanitizeEx.strip_tags/1, and its text as typed: &, < and > are not escaped, and an entity typed into it, such as &amp;, is kept as typed. Tags are taken out until none is left, so a tag hidden inside a tag goes too. Anything but a string is returned as it is.

Converts string boolean representations to actual booleans.

Translates the label of every option in the caller's locale (translate_text/1).

Translates a string a resource declared in its DSL, in the caller's locale.

Extracts the tenant value from a user map/struct (currently :site_id).

Validates a form and returns per-field errors for the currently-changing field only.

Functions

accessible?(element, state)

@spec accessible?(map(), map()) :: boolean()

Checks if an entity (filter, action, etc.) is accessible based on visibility and restrictions.

Handles:

  • restricted: true → accessible only when state.master_user? is true
  • visible: false → not accessible
  • visible: fn state -> boolean end → calls function

Examples

iex> MishkaGervaz.Helpers.accessible?(%{restricted: true}, %{master_user?: false})
false

iex> MishkaGervaz.Helpers.accessible?(%{restricted: true}, %{master_user?: true})
true

iex> MishkaGervaz.Helpers.accessible?(%{restricted: true}, %{})
false

iex> MishkaGervaz.Helpers.accessible?(%{visible: false}, %{})
false

iex> MishkaGervaz.Helpers.accessible?(%{visible: true}, %{})
true

action_label(action)

@spec action_label(map()) :: String.t()

A row action's button label: its ui label, else its humanized name, translated in the caller's locale.

Examples

iex> MishkaGervaz.Helpers.action_label(%{name: :mark_done, ui: %{label: "Finish"}})
"Finish"

iex> MishkaGervaz.Helpers.action_label(%{name: :mark_done, ui: nil})
"Mark Done"

compact_to_nil(map)

@spec compact_to_nil(map() | nil) :: map() | nil

Drops nil values from a map and returns nil if the result is empty, otherwise returns the cleaned map.

Used by transformers that build optional sub-config blocks where "no overrides set" should compile down to nil rather than an empty map. Idempotent on nil input.

dynamic_component(assigns)

@spec dynamic_component(map()) :: Phoenix.LiveView.Rendered.t()

Dynamic component wrapper using apply/3 for proper Phoenix lifecycle. See: https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#module-dynamic-component-rendering

exports?(module, fun, arity)

@spec exports?(module() | nil, atom(), arity()) :: boolean()

Whether module defines fun/arity, loading the module first.

Prefer this over a bare function_exported?/3, which only sees modules already in memory.

Returns false for nil, for a module that does not exist, and for anything that is not an atom.

iex> MishkaGervaz.Helpers.exports?(Enum, :map, 2)
true

iex> MishkaGervaz.Helpers.exports?(nil, :map, 2)
false

extract_preload_source(source)

@spec extract_preload_source(atom() | {atom(), atom()}) :: atom()

Unwrap a preload entry to its source atom. Preloads can be plain atoms or {source, alias} tuples — this returns the atom either way.

extract_singleton_entity(map, key)

@spec extract_singleton_entity(map(), atom()) :: map()

Unwraps a Spark singleton sub-entity wrapper into the entity itself.

Spark stores DSL singleton entities under key as a single-element list during parse. After the parent entity's transform/1 runs, the list is collapsed to the entity struct (or nil if absent). Used by every entity that owns one or more singleton sub-entities (e.g. :ui, :preload, plus submit's :create / :update / :cancel).

Idempotent: if the value is already a struct (transform already ran) or absent, the map is returned unchanged.

find_all_dependents(changed_parents, filters)

@spec find_all_dependents(MapSet.t(), list()) :: MapSet.t()

The names of every entry that depends_on one of changed_parents, directly or down the chain.

entries are filters or form fields: maps with :name and :depends_on. The parents themselves are left out unless another parent depends on them.

Examples

iex> fields = [
...>   %{name: :site_id, depends_on: nil},
...>   %{name: :workspace, depends_on: :site_id},
...>   %{name: :version_id, depends_on: :workspace},
...>   %{name: :title, depends_on: nil}
...> ]
iex> MishkaGervaz.Helpers.find_all_dependents(MapSet.new([:site_id]), fields)
MapSet.new([:workspace, :version_id])

find_by_name(list, name)

@spec find_by_name(list() | nil, atom()) :: map() | nil

Finds an entity in a list by its name field.

Examples

iex> MishkaGervaz.Helpers.find_by_name([%{name: :foo}, %{name: :bar}], :bar)
%{name: :bar}

iex> MishkaGervaz.Helpers.find_by_name([%{name: :foo}], :missing)
nil

iex> MishkaGervaz.Helpers.find_by_name(nil, :foo)
nil

format_filesize(size)

@spec format_filesize(integer() | nil) :: String.t()

Formats a file size in bytes to a human-readable string.

Examples

iex> MishkaGervaz.Helpers.format_filesize(500)
"500 B"

iex> MishkaGervaz.Helpers.format_filesize(1024)
"1.0 KB"

iex> MishkaGervaz.Helpers.format_filesize(1048576)
"1.0 MB"

iex> MishkaGervaz.Helpers.format_filesize(nil)
"-"

get_domain(resource)

@spec get_domain(module()) :: {:ok, module()} | :error

Look up the Ash domain for a resource.

get_domain_defaults(resource, section)

@spec get_domain_defaults(module(), atom()) :: map()

Fetch the domain-side defaults map for a given section (:form or :table). Returns %{} when the resource has no domain or the domain has no mishka_gervaz block.

get_resource_attributes(resource)

@spec get_resource_attributes(module()) :: map()

Returns the resource's Ash attributes as a %{name => attribute_struct} map.

Used by builders that need O(1) attribute lookup by name (FieldBuilder, ColumnBuilder).

get_ui_label(entity)

@spec get_ui_label(map() | struct()) :: String.t()

Extracts and resolves a label from a UI structure, with fallback to humanized name.

Similar to resolve_ui_label/1 but falls back to humanizing the :name field when no UI label is found. The humanized name is translated in the caller's locale.

Examples

iex> MishkaGervaz.Helpers.get_ui_label(%{ui: %{label: "Custom"}, name: :field})
"Custom"

iex> MishkaGervaz.Helpers.get_ui_label(%{name: :user_name})
"User Name"

iex> MishkaGervaz.Helpers.get_ui_label(%{ui: nil, name: :created_at})
"Created At"

get_visible_columns(columns, state)

@spec get_visible_columns([map()], map()) :: [map()]

Filters columns based on their visibility setting.

Evaluates the visible field of each column:

  • Function with arity 1: calls with state and uses the result
  • Boolean: uses the value directly
  • Any other value: defaults to true (visible)

Examples

iex> columns = [%{name: :id, visible: true}, %{name: :secret, visible: false}]
iex> MishkaGervaz.Helpers.get_visible_columns(columns, %{})
[%{name: :id, visible: true}]

iex> columns = [%{name: :admin_only, visible: fn state -> state.master_user? end}]
iex> MishkaGervaz.Helpers.get_visible_columns(columns, %{master_user?: true})
[%{name: :admin_only, visible: _}]

iex> MishkaGervaz.Helpers.get_visible_columns(columns, %{master_user?: false})
[]

has_value?(_)

@spec has_value?(any()) :: boolean()

Checks if a value is present (not nil, empty string, or empty list).

Examples

iex> MishkaGervaz.Helpers.has_value?(nil)
false

iex> MishkaGervaz.Helpers.has_value?("")
false

iex> MishkaGervaz.Helpers.has_value?([])
false

iex> MishkaGervaz.Helpers.has_value?("test")
true

iex> MishkaGervaz.Helpers.has_value?(["a", "b"])
true

humanize(atom)

@spec humanize(atom() | String.t()) :: String.t()

Converts an atom or string to a human-readable label.

Examples

iex> MishkaGervaz.Helpers.humanize(:first_name)
"First Name"

iex> MishkaGervaz.Helpers.humanize(:user_id)
"User Id"

iex> MishkaGervaz.Helpers.humanize("already_formatted")
"already_formatted"

inject_preload_aliases(record_or_records, aliases)

@spec inject_preload_aliases(struct() | [struct()], map() | nil) ::
  struct() | [struct()]

Injects preload alias values into a record or list of records.

When using master/tenant preload patterns (e.g., master_media_category aliased to media_category), this function copies the loaded relationship data from the source field to the alias field.

Examples

iex> record = %{id: 1, master_media_category: %{name: "Photos"}}
iex> MishkaGervaz.Helpers.inject_preload_aliases(record, %{media_category: :master_media_category})
%{id: 1, master_media_category: %{name: "Photos"}, media_category: %{name: "Photos"}}

iex> MishkaGervaz.Helpers.inject_preload_aliases([%{id: 1, source: "val"}], %{alias: :source})
[%{id: 1, source: "val", alias: "val"}]

iex> MishkaGervaz.Helpers.inject_preload_aliases(%{id: 1}, nil)
%{id: 1}

iex> MishkaGervaz.Helpers.inject_preload_aliases(%{id: 1}, %{})
%{id: 1}

invalidate_dependents(new_filter_values, old_filter_values, state)

@spec invalidate_dependents(map(), map(), map()) :: {map(), map()}

Invalidates dependent filter values and relation_filter_state when parent filters change.

Compares old vs new filter values to find changed parents, then recursively finds all children whose depends_on points to a changed filter. Removes those children from both filter_values and relation_filter_state.

Returns {cleaned_filter_values, cleaned_relation_filter_state}.

Examples

iex> filters = [%{name: :region, depends_on: nil}, %{name: :city, depends_on: :region}]
iex> old = %{region: "us", city: "ny"}
iex> new = %{region: "eu", city: "ny"}
iex> state = %{static: %{filters: filters}, relation_filter_state: %{}}
iex> {cleaned_fv, cleaned_rfs} = MishkaGervaz.Helpers.invalidate_dependents(new, old, state)
iex> cleaned_fv
%{region: "eu"}

known_name?(name, arg2)

@spec known_name?(String.t(), map()) :: boolean()

Checks if a string name is a known entity name in the given state.

Pattern matches on the state structure to auto-detect the context:

  • Form state (static.fields) — checks field names
  • Table state (static.columns) — checks column names
  • Table state (static.filters) — checks filter names (with explicit :filters)
  • Form state (static.steps) — checks step names (with explicit :steps)
  • Form state (static.uploads) — checks upload names (with explicit :uploads)

Examples

iex> state = %{static: %{fields: [%{name: :title}, %{name: :tags}]}}
iex> MishkaGervaz.Helpers.known_name?("tags", state)
true

iex> MishkaGervaz.Helpers.known_name?("unknown", state)
false

iex> state = %{static: %{columns: [%{name: :id}, %{name: :status}]}}
iex> MishkaGervaz.Helpers.known_name?("status", state)
true

known_name?(name, arg2, kind)

@spec known_name?(String.t(), map(), :filters | :steps | :uploads) :: boolean()

Whether name is a declared filter, step or upload on this state.

The guard event handlers use before acting on a name that arrived from the client, so a crafted phx-value cannot reach an entity the resource never declared.

known_name?("status", state, :filters)

map_get(map, key, default)

@spec map_get(map() | nil, atom(), term()) :: term()

Fetch a key from a possibly-nil map, returning default when the key is missing or the value is nil.

Used by the Resource.Info.{Form,Table} and Domain.Info.{Form,Table} introspection modules to absorb the repeating case map do %{key: v} -> v; _ -> default end shape.

map_put_if_set(map, key, value)

@spec map_put_if_set(map(), atom(), {:ok, any()} | {:error, any()} | :error | any()) ::
  map()

Puts a key-value pair into a map only if the value is present and valid.

Designed for building configuration maps where optional values should only be included when explicitly set. Supports multiple input formats commonly used with Spark DSL introspection functions.

Supported Formats

  • {:ok, value} - Spark/Ash introspection result (value must not be nil)
  • {:error, _} - Ignored, returns original map
  • :error - Ignored, returns original map
  • Direct value - Added if not nil

Examples

iex> %{} |> MishkaGervaz.Helpers.map_put_if_set(:name, {:ok, "John"})
%{name: "John"}

iex> %{} |> MishkaGervaz.Helpers.map_put_if_set(:name, {:ok, nil})
%{}

iex> %{} |> MishkaGervaz.Helpers.map_put_if_set(:name, :error)
%{}

iex> %{} |> MishkaGervaz.Helpers.map_put_if_set(:name, {:error, :not_found})
%{}

iex> %{} |> MishkaGervaz.Helpers.map_put_if_set(:name, "John")
%{name: "John"}

iex> %{} |> MishkaGervaz.Helpers.map_put_if_set(:name, nil)
%{}

iex> %{a: 1} |> MishkaGervaz.Helpers.map_put_if_set(:b, {:ok, 2}) |> MishkaGervaz.Helpers.map_put_if_set(:c, {:ok, 3})
%{a: 1, b: 2, c: 3}

master_user?(_)

@spec master_user?(map() | struct() | nil) :: boolean()

Returns true when the user has no tenant — i.e. is a master user.

Canonical default for master_user?/1 across the framework. The tenant attribute is currently :site_id.

maybe_assign(assigns, key, value)

@spec maybe_assign(map(), atom(), any()) :: map()

Conditionally assigns a key-value pair to assigns only if the value is not nil.

This is useful when building assigns for components where you want to allow upstream defaults (via assign_new) to take effect when no value is provided.

Examples

iex> %{} |> Phoenix.Component.assign(:foo, "bar") |> MishkaGervaz.Helpers.maybe_assign(:icon, nil)
%{foo: "bar"}

iex> %{} |> Phoenix.Component.assign(:foo, "bar") |> MishkaGervaz.Helpers.maybe_assign(:icon, "hero-trash")
%{foo: "bar", icon: "hero-trash"}

merge_relation_field_values(params, state)

@spec merge_relation_field_values(map(), map()) :: map()

Merges form-state relation field values into a params map.

For each relation field with a value in state.field_values, places the value at the string-keyed slot in params. The sentinel "__nil__" is rewritten to a real nil so AshPhoenix can clear the relation. Empty string and nil values are ignored.

Used by validation/submit handlers to ensure relation selections survive param re-merging.

missing_actions(resource, key, value, type)

@spec missing_actions(
  module() | map(),
  atom(),
  atom() | {atom(), atom()} | nil,
  atom()
) :: [map()]

The actions value names that resource does not have as a type action.

value is an action name, a {master_action, tenant_action} tuple or nil. resource is a resource module or a resource's DSL state. Each entry is a map with :key, :value, :name, :type and :found — nil when the resource has no action with that name, otherwise the type of the action that has it.

missing_actions(MyApp.Post, :update, {:master_update, :update}, :update)
#=> [%{key: :update, value: {:master_update, :update}, name: :master_update,
#      type: :update, found: nil}]

missing_actions_message(resource, what, missing, fix)

@spec missing_actions_message(module(), String.t(), [map()], String.t()) :: String.t()

The compile error for the entries missing_actions/4 returned, each with an :origin of :resource, {:domain, domain} or :default.

what names the DSL block ("form", "table") and fix is the closing paragraph telling the reader where to name the actions instead.

module_to_snake(module, suffix \\ "")

@spec module_to_snake(module(), String.t()) :: String.t()

Converts a module name to its snake_case short name.

Takes the last part of a module name and converts it to snake_case. Optionally appends a suffix.

Examples

iex> MishkaGervaz.Helpers.module_to_snake(MyApp.Users.User)
"user"

iex> MishkaGervaz.Helpers.module_to_snake(MyApp.BlogPost)
"blog_post"

iex> MishkaGervaz.Helpers.module_to_snake(MyApp.Users.User, "_stream")
"user_stream"

iex> MishkaGervaz.Helpers.module_to_snake(MyApp.BlogPost, "_table")
"blog_post_table"

normalize_id_type(type)

@spec normalize_id_type(any()) :: :uuid | :uuid_v7 | :integer | :string

Normalizes an Ash primary-key type to one of :uuid, :uuid_v7, :integer, or :string. Falls back to :uuid for anything else.

Recognises the bare atom forms (:uuid, :integer, :string), the Ash.Type.* modules, and name variants such as Ash.Type.UUIDv7, which are matched by name string.

normalize_options(options)

@spec normalize_options(list() | nil) :: [{String.t(), String.t()}]

Normalizes a list of options for HTML select elements.

Converts various option formats to {label, value} tuples with string values, ensuring compatibility with Phoenix HTML attributes.

Examples

iex> MishkaGervaz.Helpers.normalize_options([{"API Only", :api_only}, {"Hybrid", :hybrid}])
[{"API Only", "api_only"}, {"Hybrid", "hybrid"}]

iex> MishkaGervaz.Helpers.normalize_options([:active, :inactive])
[{"Active", "active"}, {"Inactive", "inactive"}]

iex> MishkaGervaz.Helpers.normalize_options(["foo", "bar"])
[{"foo", "foo"}, {"bar", "bar"}]

iex> MishkaGervaz.Helpers.normalize_options([[label: "Admin", value: "admin"]])
[{"Admin", "admin"}]

iex> MishkaGervaz.Helpers.normalize_options(nil)
[]

normalize_selected_values(values)

@spec normalize_selected_values(list() | any() | nil) :: [String.t()]

Normalizes selected values for multi-select components.

Converts various input formats to a list of non-empty string values, filtering out empty strings and "nil" string representations.

Examples

iex> MishkaGervaz.Helpers.normalize_selected_values(nil)
[]

iex> MishkaGervaz.Helpers.normalize_selected_values(["a", "b", "c"])
["a", "b", "c"]

iex> MishkaGervaz.Helpers.normalize_selected_values([:foo, :bar])
["foo", "bar"]

iex> MishkaGervaz.Helpers.normalize_selected_values(["valid", "", nil, "nil"])
["valid"]

iex> MishkaGervaz.Helpers.normalize_selected_values("single")
["single"]

primary_key_type(resource)

@spec primary_key_type(module()) :: :uuid | :uuid_v7 | :integer | :string

Returns the normalized type of resource's primary key as one of :uuid, :uuid_v7, :integer, or :string. Defaults to :uuid when introspection fails or the resource has no primary key.

relation_id_type(entity, parent)

@spec relation_id_type(map(), module() | nil) ::
  :uuid | :uuid_v7 | :integer | :string | nil

Returns the normalized id-type for a :relation entity, defaulting to :uuid when the related resource cannot be found. A :relation entity with no related resource and its own options list is :string — its values are the list's, not a resource's keys.

Combines relation_target_resource/2 and primary_key_type/1. For non-relation entities returns nil.

relation_target_resource(arg1, parent)

@spec relation_target_resource(map(), module() | nil) :: module() | nil

Resolves the destination resource of a relation field/filter.

Accepts any map with :name / :source / :resource keys (the shape Form.Field and Table.Filter both have):

  • If :resource is set on the entity, returns it directly.
  • Otherwise looks up parent_resource's Ash relationships for the one whose source_attribute matches entity.source || entity.name and returns its :destination.
  • Returns nil when no match is found, when parent_resource is nil, or on any introspection failure.

resolve_bulk_confirm(confirm, count)

@spec resolve_bulk_confirm(boolean() | String.t() | nil, non_neg_integer() | nil) ::
  boolean() | String.t() | nil

Resolves a bulk action's confirm.

A string is translated in the caller's locale (translate_text/1), then every {count} in it becomes count. With no count, the text keeps {count} as written. Anything else is returned as it is.

Examples

iex> MishkaGervaz.Helpers.resolve_bulk_confirm("Delete {count} posts?", 3)
"Delete 3 posts?"

iex> MishkaGervaz.Helpers.resolve_bulk_confirm("Delete {count} posts?", nil)
"Delete {count} posts?"

iex> MishkaGervaz.Helpers.resolve_bulk_confirm("Sure?", 3)
"Sure?"

iex> MishkaGervaz.Helpers.resolve_bulk_confirm(nil, 3)
nil

resolve_confirm(confirm, record)

@spec resolve_confirm(String.t() | (map() -> String.t()) | nil, map()) ::
  String.t() | nil

Resolves an action's confirm — the sibling of resolve_label/1 for a message that needs the record.

confirm is declared as String.t() | (map() -> String.t()) | nil; the function form lets a prompt name what it is about to act on ("Delete the draft “Autosave”?"). A string is translated in the caller's locale (translate_text/1); a function's result is returned as it is.

Examples

iex> MishkaGervaz.Helpers.resolve_confirm("Sure?", %{id: 1})
"Sure?"

iex> MishkaGervaz.Helpers.resolve_confirm(fn r -> "Delete #{r.name}?" end, %{name: "Cat"})
"Delete Cat?"

iex> MishkaGervaz.Helpers.resolve_confirm(nil, %{id: 1})
nil

resolve_dynamic(value, state)

@spec resolve_dynamic(term(), map()) :: term()

Resolves a header, footer or notice text: a string (translated), fn -> text end, or fn state -> text end.

Examples

iex> MishkaGervaz.Helpers.resolve_dynamic("A heading", %{})
"A heading"

iex> MishkaGervaz.Helpers.resolve_dynamic(fn state -> state.title end, %{title: "From state"})
"From state"

iex> MishkaGervaz.Helpers.resolve_dynamic(nil, %{})
nil

resolve_label(label)

@spec resolve_label(String.t() | (-> String.t()) | nil) :: String.t() | nil

Resolves a label that may be a string or a zero-arity function.

A string is translated in the caller's locale (translate_text/1). A function is called and its result returned as it is, so fn -> gettext("...") end defers the lookup to runtime.

Examples

iex> MishkaGervaz.Helpers.resolve_label("Static Label")
"Static Label"

iex> MishkaGervaz.Helpers.resolve_label(fn -> "Dynamic Label" end)
"Dynamic Label"

iex> MishkaGervaz.Helpers.resolve_label(nil)
nil

resolve_label(record, display_field)

@spec resolve_label(
  struct(),
  atom() | (struct() -> String.t()) | (struct(), map() -> String.t())
) ::
  String.t()

Resolves a display value from a record using either an atom field or a function.

Supports:

  • 2-arity: resolve_label(record, display_field) - for atom or 1-arity function
  • 3-arity: resolve_label(record, display_field, state) - for 2-arity function

Examples

iex> MishkaGervaz.Helpers.resolve_label(%{name: "Test"}, :name)
"Test"

iex> MishkaGervaz.Helpers.resolve_label(%{name: "Cat", site: %{name: "Site1"}}, fn r -> "#{r.name} - #{r.site.name}" end)
"Cat - Site1"

iex> MishkaGervaz.Helpers.resolve_label(%{id: 123, name: nil}, :name)
"123"

resolve_label(record, display_field, state)

@spec resolve_label(struct(), (struct(), map() -> String.t()), map()) :: String.t()

A relation option's label, with the live state available to the display_field.

Accepts every shape display_field may take — an attribute name, fn record -> …, or fn record, state -> … for a label that differs by role.

resolve_label(site, :name, state)

resolve_label(cat, fn cat, state ->
  if state.master_user?, do: cat.name <> " — " <> cat.site.name, else: cat.name
end, state)

resolve_options(opts)

@spec resolve_options(list() | (-> list()) | nil) :: list()

Resolves options that may be a list or a zero-arity function returning a list.

This enables dynamic options (e.g., from a database query) in DSL fields by allowing users to pass fn -> query_options() end which defers execution to runtime.

Examples

iex> MishkaGervaz.Helpers.resolve_options([{"A", "a"}, {"B", "b"}])
[{"A", "a"}, {"B", "b"}]

iex> MishkaGervaz.Helpers.resolve_options(fn -> [{"X", "x"}] end)
[{"X", "x"}]

iex> MishkaGervaz.Helpers.resolve_options(nil)
[]

resolve_translated_options(opts)

@spec resolve_translated_options(list() | (-> list()) | nil) :: list()

resolve_options/1 for the options a resource declared: the labels of a list are translated in the caller's locale (translate_options/1), the result of a function is returned as it is.

Examples

iex> MishkaGervaz.Helpers.resolve_translated_options([{"A", "a"}])
[{"A", "a"}]

iex> MishkaGervaz.Helpers.resolve_translated_options(fn -> [{"X", "x"}] end)
[{"X", "x"}]

iex> MishkaGervaz.Helpers.resolve_translated_options(nil)
[]

resolve_ui_label(_)

@spec resolve_ui_label(map() | struct() | nil) :: String.t() | nil

Resolves a label from a nested UI structure.

Extracts the label from entities that have a ui field containing a label. Supports both map and struct formats, with labels that can be strings (translated in the caller's locale) or zero-arity functions (called, their result returned as it is).

Examples

iex> MishkaGervaz.Helpers.resolve_ui_label(%{ui: %{label: "Name"}})
"Name"

iex> MishkaGervaz.Helpers.resolve_ui_label(%{ui: %{label: fn -> "Dynamic" end}})
"Dynamic"

iex> MishkaGervaz.Helpers.resolve_ui_label(%{ui: nil})
nil

iex> MishkaGervaz.Helpers.resolve_ui_label(%{other: "field"})
nil

iex> MishkaGervaz.Helpers.resolve_ui_label(nil)
nil

strip_tags(value)

@spec strip_tags(term()) :: term()

value with every HTML tag taken out, by HtmlSanitizeEx.strip_tags/1, and its text as typed: &, < and > are not escaped, and an entity typed into it, such as &amp;, is kept as typed. Tags are taken out until none is left, so a tag hidden inside a tag goes too. Anything but a string is returned as it is.

Examples

iex> MishkaGervaz.Helpers.strip_tags(~s(Tom & Jerry's <b>"cat"</b>))
~s(Tom & Jerry's "cat")

iex> MishkaGervaz.Helpers.strip_tags("<<b>b>")
""

iex> MishkaGervaz.Helpers.strip_tags(42)
42

to_boolean(_)

@spec to_boolean(String.t() | boolean() | nil) :: boolean() | nil

Converts string boolean representations to actual booleans.

Safely handles values coming from URL params, form inputs, or other string-based sources. Follows Phoenix's normalize_value/2 pattern.

Examples

iex> MishkaGervaz.Helpers.to_boolean("true")
true

iex> MishkaGervaz.Helpers.to_boolean("false")
false

iex> MishkaGervaz.Helpers.to_boolean(true)
true

iex> MishkaGervaz.Helpers.to_boolean(nil)
nil

iex> MishkaGervaz.Helpers.to_boolean("")
nil

translate_options(options)

@spec translate_options(list()) :: list()

Translates the label of every option in the caller's locale (translate_text/1).

Takes the shapes a select accepts: {label, value}, [label: label, value: value], a bare value that is its own label, and {group_label, options} for a group, whose label and options are translated. A bare value or keyword option becomes {label, value}. The value of an option is never translated, and a shape this does not know is returned as it is.

Examples

iex> MishkaGervaz.Helpers.translate_options([{"Open", :open}, :on_hold, [label: "Gone", value: "gone"]])
[{"Open", :open}, {"On Hold", "on_hold"}, {"Gone", "gone"}]

iex> MishkaGervaz.Helpers.translate_options([{"Group", [{"A", "a"}]}])
[{"Group", [{"A", "a"}]}]

translate_text(text)

@spec translate_text(term()) :: term()

Translates a string a resource declared in its DSL, in the caller's locale.

See MishkaGervaz.Messages.translate_text/1.

Examples

iex> MishkaGervaz.Helpers.translate_text("Static Label")
"Static Label"

iex> MishkaGervaz.Helpers.translate_text(nil)
nil

user_tenant(_)

@spec user_tenant(map() | struct() | nil) :: any()

Extracts the tenant value from a user map/struct (currently :site_id).

Returns nil for nil users or users without the tenant attribute.

validate_field_errors(ash_form, params, current_errors \\ %{})

@spec validate_field_errors(AshPhoenix.Form.t(), map(), map() | nil) :: {map(), map()}

Validates a form and returns per-field errors for the currently-changing field only.

Designed for use in on_validate hooks to provide live inline errors without showing unrelated required-field errors for untouched fields.

Extracts _target from params to identify the current field, validates via AshPhoenix.Form.validate/3, then filters errors to only that field.

Returns {params, errors} ready to return directly from an on_validate hook.

Examples

hooks do
  on_validate fn params, state ->
    MishkaGervaz.Helpers.validate_field_errors(state.form.source, params)
  end
end

# With param mutation before validation:
hooks do
  on_validate fn params, state ->
    updated = put_in(params, ["form", "slug"], slugify(params["form"]["title"]))
    MishkaGervaz.Helpers.validate_field_errors(state.form.source, updated)
  end
end