defmodule SelectoComponents.Views do alias SelectoComponents.Param @moduledoc """ View tuple helpers and shared parameter-shaping helpers. ## View Registration Register views in your LiveView as `{id, module, name, options}` tuples: ```elixir views = [ SelectoComponents.Views.spec( :aggregate, SelectoComponents.Views.Aggregate, "Aggregate View", %{drill_down: :detail} ), SelectoComponents.Views.spec(:detail, SelectoComponents.Views.Detail, "Detail View", %{}) ] state = get_initial_state(views, selecto) socket = assign(socket, views: views) ``` ## Formal View Interface The preferred interface is `SelectoComponents.Views.System`: - `initial_state/2` - `param_to_state/2` - `view/5` - `form_component/0` - `result_component/0` Built-in views use this behavior (`Aggregate`, `Detail`, `Graph`, `Map`). """ @type view_id :: atom() @type view_module :: module() @type view_name :: String.t() @type view_options :: map() @type view_tuple :: {view_id(), view_module(), view_name(), view_options()} @doc """ Canonical constructor for a view tuple used by SelectoComponents. views = [ SelectoComponents.Views.spec( :workflow, SelectoComponentsWorkflow.Views.Workflow, "Workflow View", %{drill_down: :detail} ) ] """ @spec spec(view_id(), view_module(), view_name(), view_options()) :: view_tuple() def spec(id, module, name, options \\ %{}) when is_atom(id) and is_atom(module) and is_binary(name) and is_map(options) do {id, module, name, options} end @doc """ Append extension-provided views for a Selecto/domain source. Existing views take precedence when IDs collide. """ @spec with_extensions([view_tuple()], map()) :: [view_tuple()] def with_extensions(base_views, selecto_or_domain) when is_list(base_views) and is_map(selecto_or_domain) do extension_specs = Selecto.Extensions.from_source(selecto_or_domain) extension_views = Selecto.Extensions.components_views(selecto_or_domain, extension_specs) existing_ids = base_views |> Enum.map(fn {id, _, _, _} -> id end) |> MapSet.new() additional_views = extension_views |> Enum.reject(fn {id, _, _, _} -> MapSet.member?(existing_ids, id) end) base_views ++ additional_views end def with_extensions(base_views, _), do: base_views ## Agg and Det forms use a common format for their subsections. This function reformats the parameters to use as state for form drawing def view_param_process(params, item_name, section) do Map.get(params, item_name, %{}) |> Enum.reduce([], fn {u, f}, acc -> stable_id = case f do %{"uuid" => uuid} when is_binary(uuid) and uuid != "" -> uuid %{uuid: uuid} when is_binary(uuid) and uuid != "" -> uuid _ -> u end acc ++ [{stable_id, f[section], Map.put_new(f, "uuid", stable_id)}] end) |> Enum.sort_by(fn {_uuid, _section, config} -> Param.integer(Map.get(config, "index")) end) end end