PhoenixKitDashboards.Dashboards (PhoenixKitDashboards v0.1.0)

Copy Markdown View Source

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

add_widget(dashboard, widget_key, opts \\ [])

@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.

add_widget_at(dashboard, widget_key, bp, x, y, opts \\ [])

@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.

add_widget_px(dashboard, widget_key, fx, fy, opts \\ [])

@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.

clone(source, user_uuid, opts \\ [])

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.

configure_widget(dashboard, instance_id, attrs, opts \\ [])

@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.

create(attrs, opts \\ [])

@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.

customized?(dashboard, bp)

Whether a breakpoint is a designed view: hand-customized, or the home tier (always designed — it's the seed + derivation anchor).

delete(dashboard, opts \\ [])

Delete a dashboard.

display_bp(dashboard, screen_bp)

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.

get(uuid)

Fetch one dashboard by uuid, or nil.

get_or_create_default(user_uuid)

@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.

hide_widget(dashboard, instance_id, bp, hidden)

@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.

home_bp(dashboard)

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).

list_for_user(user_uuid, role_uuids \\ [])

@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).

place_widget_grid(dashboard, instance_id, bp, x, y)

@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.

place_widget_px(dashboard, instance_id, fx, fy)

@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.

put_home_bp(dashboard, bp)

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_widget(dashboard, instance_id, opts \\ [])

@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.

reorder_widgets(dashboard, bp, ordered_ids)

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_breakpoint(dashboard, bp)

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.

resize_widget(dashboard, instance_id, bp, w, h)

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.

resize_widget_px(dashboard, instance_id, fw, fh)

@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.

resolve_hidden?(dashboard, id, bp)

@spec resolve_hidden?(
  PhoenixKitDashboards.Schemas.Dashboard.t(),
  instance_id :: String.t(),
  String.t()
) ::
  boolean()

Whether a widget is currently hidden on bp (stored or derived).

resolve_items(dashboard, bp, opts \\ [])

@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 pos order, 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).

resolve_placement(dashboard, id, bp)

@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.

restack_widget_px(dashboard, instance_id, dir)

@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.

save_layout(dashboard, layout)

Persist a new full layout (the hot path while editing the grid).

set_layout_mode(dashboard, mode)

Set the dashboard's layout mode ("grid" or "free"). Invalid values are ignored. A presentation tweak — not activity-logged.

update(dashboard, attrs, opts \\ [])

Update a dashboard's metadata.

update_widget_settings(dashboard, instance_id, settings, opts \\ [])

@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.

visible_to?(dashboard, user_uuid, role_uuids \\ [])

@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.