Context for dashboard pages and their widget layouts.
Persistence follows the phoenix_kit_crm precedent: the dashboard row holds a
JSONB layout list of widget instances, read and written whole. The host app's
repo is reached via PhoenixKit.RepoHelper.repo/0 — this module never owns a
repo of its own.
Summary
Functions
Add a widget instance for widget_key to a dashboard's layout, seeded with the
widget type's default size and settings — the grid placement takes the first
FREE cell on the home tier, the pixel geometry stacks below the existing
widgets. Returns the updated dashboard.
Add a widget at an explicit grid cell on bp (a catalog drag-out drop) —
x/y 0-based, clamped into the tier; a spot overlapping another widget is
refused with {:error, :occupied} (the drag hook only offers free cells).
Marks the breakpoint customized. Logs dashboard.widget_added.
Add a widget at an explicit pixel position on the free canvas (a catalog
drag-out drop) — fx/fy clamped to the top-left; size is the widget type's
default seed. Logs dashboard.widget_added.
Clone a dashboard into a new personal dashboard owned by user_uuid —
copies the layout (with fresh instance ids) and config. Lets a user take a
private, editable copy of a shared/system dashboard.
Update a single widget instance's config — its :settings map, its selected
:view, and/or its :min_override flag (opt out of the recommended minimum
size: the resize floor drops to 1x1 and view-switch growth is skipped) — in
one write. Logs a dashboard.widget_configured activity.
Create a dashboard. The slug (derived from the title) is auto-uniquified per
owner — a blank or repeated title gets a -2, -3, … suffix instead of failing
the [owner_user_uuid, slug] unique constraint. A free slug is picked by query
(so nil-owner/system dashboards uniquify too, where Postgres treats NULLs as
distinct) and a constraint clash under concurrency is retried.
Whether a breakpoint is a designed view: hand-customized, or the home tier (always designed — it's the seed + derivation anchor).
Delete a dashboard.
The designed breakpoint to display for a viewer whose screen best fits
screen_bp — that tier if it's designed, else the nearest designed tier (which
the caller scales to fit). Never a freshly-derived tier: on open we only ever
show views someone actually made.
Fetch one dashboard by uuid, or nil.
Return the user's default personal dashboard, creating an empty one on first access. This is the page shown at the module's landing route.
Show/hide a grid widget on bp (so smaller screens can drop non-essentials),
marking that breakpoint customized. A layout tweak — not activity-logged.
The dashboard's home breakpoint — the size it was designed at (seeded into,
and the anchor other tiers derive from). Set from the creator's screen on first
open; config["home_bp"], defaulting to the module default (desktop) for
dashboards created before the home concept (their widgets were seeded there).
Dashboards visible to a user: their own personal ones, all shared/system ones,
and any role-scoped ones whose role_uuid is in role_uuids (the user's
roles — empty by default, so /1 keeps the personal + system behaviour).
Place a grid widget at an explicit cell — x (column) / y (row), 0-based —
on bp, marking that breakpoint customized. The widget may go anywhere on the
grid (gaps are fine); x is clamped so the span stays within the columns, y
within Grid.max_rows/0. A spot overlapping another widget is refused with
{:error, :occupied} (the drag hook never offers one; this guards stale/
crafted events). A layout tweak — not activity-logged.
Free/pixel-canvas mode: place a widget at absolute pixels (fx, fy), each
clamped to >= 0 (top-left of the canvas). Pixel geometry is embedded so it
never disturbs the grid placement. A layout tweak — not activity-logged.
Record the dashboard's home breakpoint — only when it's still unset AND the dashboard is empty (a brand-new one on first open); otherwise a no-op, so an existing dashboard's home never moves under a different viewer's screen.
Remove a widget instance by its instance id.
Re-pack a breakpoint's grid to ordered_ids: every widget gets the first
free cell in that order (reflow + compact), so the ids' order becomes the
reading order. Unknown ids are filtered/deduped; unnamed widgets keep their
relative order after the named ones. Marks the breakpoint customized. A layout
tweak — not activity-logged.
Reset a breakpoint to auto — drops its stored per-widget placements + custom flag so it re-derives from the home tier. The home tier (the design anchor) can't be reset. Not activity-logged.
Set a grid widget's w/h span for bp, marking that breakpoint customized.
Clamped to the widget type's min/max — and to what actually FITS at the
widget's cell: it grows until blocked by a neighbouring widget or the grid
edge (Grid.fit_size/8), never onto another widget. A layout tweak — not
activity-logged.
Free/pixel-canvas mode: set a widget's absolute pixel size (fw, fh), each
clamped to [@free_min_px, @free_max_px]. A layout tweak — not activity-logged.
Whether a widget is currently hidden on bp (stored or derived).
The {item, placement} pairs to render at bp — the single render path shared
by the builder (explicit breakpoint) and the runtime (viewport-matched), so
preview and runtime never diverge. Every returned placement carries explicit
cells (x/y) plus w/h/hidden
The resolved grid placement for one widget on bp — exactly what
resolve_items/3 renders for it, or nil if the widget isn't in the layout.
Use this (not Layout.placement/2) anywhere that must match what's on screen —
e.g. the Settings modal's inputs, so a save doesn't overwrite a derived
placement with the default.
Restack a pixel widget: "front" puts it above every other widget's z,
"back" below. Overlap is allowed on the free canvas — z-order makes it
deliberate. A layout tweak — not activity-logged.
Persist a new full layout (the hot path while editing the grid).
Set the dashboard's layout mode ("grid" or "free"). Invalid values are
ignored. A presentation tweak — not activity-logged.
Update a dashboard's metadata.
Replace the settings map of a single widget instance.
Whether a dashboard is visible to a user — the single scope-visibility rule,
mirroring list_for_user/2's WHERE clause. Both LiveViews use this so the list
page and the builder never disagree about access.
Functions
@spec add_widget( PhoenixKitDashboards.Schemas.Dashboard.t(), widget_key :: String.t(), keyword() ) :: {:ok, PhoenixKitDashboards.Schemas.Dashboard.t()} | {:error, term()}
Add a widget instance for widget_key to a dashboard's layout, seeded with the
widget type's default size and settings — the grid placement takes the first
FREE cell on the home tier, the pixel geometry stacks below the existing
widgets. Returns the updated dashboard.
@spec add_widget_at( PhoenixKitDashboards.Schemas.Dashboard.t(), widget_key :: String.t(), String.t(), integer(), integer(), keyword() ) :: {:ok, PhoenixKitDashboards.Schemas.Dashboard.t()} | {:error, :unknown_widget | :occupied | Ecto.Changeset.t()}
Add a widget at an explicit grid cell on bp (a catalog drag-out drop) —
x/y 0-based, clamped into the tier; a spot overlapping another widget is
refused with {:error, :occupied} (the drag hook only offers free cells).
Marks the breakpoint customized. Logs dashboard.widget_added.
@spec add_widget_px( PhoenixKitDashboards.Schemas.Dashboard.t(), widget_key :: String.t(), integer(), integer(), keyword() ) :: {:ok, PhoenixKitDashboards.Schemas.Dashboard.t()} | {:error, :unknown_widget | Ecto.Changeset.t()}
Add a widget at an explicit pixel position on the free canvas (a catalog
drag-out drop) — fx/fy clamped to the top-left; size is the widget type's
default seed. Logs dashboard.widget_added.
@spec clone( PhoenixKitDashboards.Schemas.Dashboard.t(), user_uuid :: String.t(), keyword() ) :: {:ok, PhoenixKitDashboards.Schemas.Dashboard.t()} | {:error, Ecto.Changeset.t()}
Clone a dashboard into a new personal dashboard owned by user_uuid —
copies the layout (with fresh instance ids) and config. Lets a user take a
private, editable copy of a shared/system dashboard.
@spec configure_widget( PhoenixKitDashboards.Schemas.Dashboard.t(), instance_id :: String.t(), %{optional(:settings) => map(), optional(:view) => String.t() | nil}, keyword() ) :: {:ok, PhoenixKitDashboards.Schemas.Dashboard.t()} | {:error, Ecto.Changeset.t()}
Update a single widget instance's config — its :settings map, its selected
:view, and/or its :min_override flag (opt out of the recommended minimum
size: the resize floor drops to 1x1 and view-switch growth is skipped) — in
one write. Logs a dashboard.widget_configured activity.
@spec create( map(), keyword() ) :: {:ok, PhoenixKitDashboards.Schemas.Dashboard.t()} | {:error, Ecto.Changeset.t()}
Create a dashboard. The slug (derived from the title) is auto-uniquified per
owner — a blank or repeated title gets a -2, -3, … suffix instead of failing
the [owner_user_uuid, slug] unique constraint. A free slug is picked by query
(so nil-owner/system dashboards uniquify too, where Postgres treats NULLs as
distinct) and a constraint clash under concurrency is retried.
@spec customized?(PhoenixKitDashboards.Schemas.Dashboard.t(), String.t()) :: boolean()
Whether a breakpoint is a designed view: hand-customized, or the home tier (always designed — it's the seed + derivation anchor).
@spec delete( PhoenixKitDashboards.Schemas.Dashboard.t(), keyword() ) :: {:ok, PhoenixKitDashboards.Schemas.Dashboard.t()} | {:error, Ecto.Changeset.t()}
Delete a dashboard.
@spec display_bp(PhoenixKitDashboards.Schemas.Dashboard.t(), String.t()) :: String.t()
The designed breakpoint to display for a viewer whose screen best fits
screen_bp — that tier if it's designed, else the nearest designed tier (which
the caller scales to fit). Never a freshly-derived tier: on open we only ever
show views someone actually made.
@spec get(String.t()) :: PhoenixKitDashboards.Schemas.Dashboard.t() | nil
Fetch one dashboard by uuid, or nil.
@spec get_or_create_default(user_uuid :: String.t()) :: PhoenixKitDashboards.Schemas.Dashboard.t()
Return the user's default personal dashboard, creating an empty one on first access. This is the page shown at the module's landing route.
@spec hide_widget( PhoenixKitDashboards.Schemas.Dashboard.t(), instance_id :: String.t(), String.t(), boolean() ) :: {:ok, PhoenixKitDashboards.Schemas.Dashboard.t()} | {:error, Ecto.Changeset.t()}
Show/hide a grid widget on bp (so smaller screens can drop non-essentials),
marking that breakpoint customized. A layout tweak — not activity-logged.
@spec home_bp(PhoenixKitDashboards.Schemas.Dashboard.t()) :: String.t()
The dashboard's home breakpoint — the size it was designed at (seeded into,
and the anchor other tiers derive from). Set from the creator's screen on first
open; config["home_bp"], defaulting to the module default (desktop) for
dashboards created before the home concept (their widgets were seeded there).
@spec list_for_user(user_uuid :: String.t(), role_uuids :: [String.t()]) :: [ PhoenixKitDashboards.Schemas.Dashboard.t() ]
Dashboards visible to a user: their own personal ones, all shared/system ones,
and any role-scoped ones whose role_uuid is in role_uuids (the user's
roles — empty by default, so /1 keeps the personal + system behaviour).
@spec place_widget_grid( PhoenixKitDashboards.Schemas.Dashboard.t(), instance_id :: String.t(), String.t(), integer(), integer() ) :: {:ok, PhoenixKitDashboards.Schemas.Dashboard.t()} | {:error, :occupied | Ecto.Changeset.t()}
Place a grid widget at an explicit cell — x (column) / y (row), 0-based —
on bp, marking that breakpoint customized. The widget may go anywhere on the
grid (gaps are fine); x is clamped so the span stays within the columns, y
within Grid.max_rows/0. A spot overlapping another widget is refused with
{:error, :occupied} (the drag hook never offers one; this guards stale/
crafted events). A layout tweak — not activity-logged.
@spec place_widget_px( PhoenixKitDashboards.Schemas.Dashboard.t(), instance_id :: String.t(), integer(), integer() ) :: {:ok, PhoenixKitDashboards.Schemas.Dashboard.t()} | {:error, Ecto.Changeset.t()}
Free/pixel-canvas mode: place a widget at absolute pixels (fx, fy), each
clamped to >= 0 (top-left of the canvas). Pixel geometry is embedded so it
never disturbs the grid placement. A layout tweak — not activity-logged.
@spec put_home_bp(PhoenixKitDashboards.Schemas.Dashboard.t(), String.t()) :: {:ok, PhoenixKitDashboards.Schemas.Dashboard.t()} | {:error, Ecto.Changeset.t()}
Record the dashboard's home breakpoint — only when it's still unset AND the dashboard is empty (a brand-new one on first open); otherwise a no-op, so an existing dashboard's home never moves under a different viewer's screen.
@spec remove_widget( PhoenixKitDashboards.Schemas.Dashboard.t(), instance_id :: String.t(), keyword() ) :: {:ok, PhoenixKitDashboards.Schemas.Dashboard.t()} | {:error, Ecto.Changeset.t()}
Remove a widget instance by its instance id.
@spec reorder_widgets(PhoenixKitDashboards.Schemas.Dashboard.t(), String.t(), [ String.t() ]) :: {:ok, PhoenixKitDashboards.Schemas.Dashboard.t()} | {:error, Ecto.Changeset.t()}
Re-pack a breakpoint's grid to ordered_ids: every widget gets the first
free cell in that order (reflow + compact), so the ids' order becomes the
reading order. Unknown ids are filtered/deduped; unnamed widgets keep their
relative order after the named ones. Marks the breakpoint customized. A layout
tweak — not activity-logged.
@spec reset_breakpoint(PhoenixKitDashboards.Schemas.Dashboard.t(), String.t()) :: {:ok, PhoenixKitDashboards.Schemas.Dashboard.t()} | {:error, Ecto.Changeset.t()}
Reset a breakpoint to auto — drops its stored per-widget placements + custom flag so it re-derives from the home tier. The home tier (the design anchor) can't be reset. Not activity-logged.
@spec resize_widget( PhoenixKitDashboards.Schemas.Dashboard.t(), instance_id :: String.t(), String.t(), pos_integer(), pos_integer() ) :: {:ok, PhoenixKitDashboards.Schemas.Dashboard.t()} | {:error, Ecto.Changeset.t()}
Set a grid widget's w/h span for bp, marking that breakpoint customized.
Clamped to the widget type's min/max — and to what actually FITS at the
widget's cell: it grows until blocked by a neighbouring widget or the grid
edge (Grid.fit_size/8), never onto another widget. A layout tweak — not
activity-logged.
@spec resize_widget_px( PhoenixKitDashboards.Schemas.Dashboard.t(), instance_id :: String.t(), integer(), integer() ) :: {:ok, PhoenixKitDashboards.Schemas.Dashboard.t()} | {:error, Ecto.Changeset.t()}
Free/pixel-canvas mode: set a widget's absolute pixel size (fw, fh), each
clamped to [@free_min_px, @free_max_px]. A layout tweak — not activity-logged.
@spec resolve_items(PhoenixKitDashboards.Schemas.Dashboard.t(), String.t(), keyword()) :: [ {map(), map()} ]
The {item, placement} pairs to render at bp — the single render path shared
by the builder (explicit breakpoint) and the runtime (viewport-matched), so
preview and runtime never diverge. Every returned placement carries explicit
cells (x/y) plus w/h/hidden:
- a designed breakpoint renders its stored cells verbatim; stored
placements that predate explicit cells (order-only) are packed into the
remaining free cells in
posorder, so old layouts keep their arrangement without a migration (their first edit persists the cells); - an un-designed breakpoint is derived from the nearest designed tier by reflow + compact: its widgets in reading order, spans clamped to the target's columns, packed first-fit.
Ordered by reading order (y, then x); hidden widgets are included (the
builder dims them and they keep their cells; pass visible: true — or filter
placement["hidden"] — for the runtime).
@spec resolve_placement( PhoenixKitDashboards.Schemas.Dashboard.t(), instance_id :: String.t(), String.t() ) :: map() | nil
The resolved grid placement for one widget on bp — exactly what
resolve_items/3 renders for it, or nil if the widget isn't in the layout.
Use this (not Layout.placement/2) anywhere that must match what's on screen —
e.g. the Settings modal's inputs, so a save doesn't overwrite a derived
placement with the default.
@spec restack_widget_px( PhoenixKitDashboards.Schemas.Dashboard.t(), instance_id :: String.t(), String.t() ) :: {:ok, PhoenixKitDashboards.Schemas.Dashboard.t()} | {:error, Ecto.Changeset.t()}
Restack a pixel widget: "front" puts it above every other widget's z,
"back" below. Overlap is allowed on the free canvas — z-order makes it
deliberate. A layout tweak — not activity-logged.
@spec save_layout(PhoenixKitDashboards.Schemas.Dashboard.t(), [map()]) :: {:ok, PhoenixKitDashboards.Schemas.Dashboard.t()} | {:error, Ecto.Changeset.t()}
Persist a new full layout (the hot path while editing the grid).
@spec set_layout_mode(PhoenixKitDashboards.Schemas.Dashboard.t(), String.t()) :: {:ok, PhoenixKitDashboards.Schemas.Dashboard.t()} | {:error, Ecto.Changeset.t()}
Set the dashboard's layout mode ("grid" or "free"). Invalid values are
ignored. A presentation tweak — not activity-logged.
@spec update(PhoenixKitDashboards.Schemas.Dashboard.t(), map(), keyword()) :: {:ok, PhoenixKitDashboards.Schemas.Dashboard.t()} | {:error, Ecto.Changeset.t()}
Update a dashboard's metadata.
@spec update_widget_settings( PhoenixKitDashboards.Schemas.Dashboard.t(), instance_id :: String.t(), map(), keyword() ) :: {:ok, PhoenixKitDashboards.Schemas.Dashboard.t()} | {:error, Ecto.Changeset.t()}
Replace the settings map of a single widget instance.
@spec visible_to?( PhoenixKitDashboards.Schemas.Dashboard.t(), user_uuid :: String.t() | nil, role_uuids :: [String.t()] ) :: boolean()
Whether a dashboard is visible to a user — the single scope-visibility rule,
mirroring list_for_user/2's WHERE clause. Both LiveViews use this so the list
page and the builder never disagree about access.