PhoenixKitProjects.Statuses (PhoenixKitProjects v0.11.0)

Copy Markdown View Source

User-defined project workflow statuses, configured through the optional phoenix_kit_entities module and cemented locally when a project starts.

Two layers:

  • Catalog (entities). A shared project_status entity (auto-seeded with a default vocabulary) and/or full custom per-project entities the user owns. Templates and not-yet-started projects read this catalog live, so edits to the vocabulary flow straight through.
  • Cemented (local). When a project starts, its chosen catalog statuses are snapshotted into phoenix_kit_project_statuses (PhoenixKitProjects.Schemas.ProjectStatus). The running project then uses its own frozen, independently-editable copy — later catalog edits don't touch it. Mirrors the module's template→instance philosophy.

The selected status is addressed by its slug (current_status_slug on the project), a stable identity that resolves against the live catalog before start and the cemented local rows after.

Optional dependency

phoenix_kit_entities is optional. Every public function degrades gracefully when it's absent or disabled: reads return []/nil, provisioning returns {:error, :entities_not_available}, and cement_project_statuses/2 becomes a no-op. The guard scaffolding (@compile {:no_warn_undefined, …} + safe_call/2 + available?/0) mirrors PhoenixKitProjects.Translations.

Summary

Types

A normalized status row, identical whether it came from the live catalog or a cemented local row. uuid is the source row's uuid (entity_data uuid pre-start, phoenix_kit_project_statuses uuid post-start); slug is the stable cross-boundary identity.

Functions

Adds a cemented status row to a started project.

Is the entities-backed status feature usable right now? True when the optional phoenix_kit_entities plugin is loaded AND enabled at runtime.

Snapshots a project's chosen catalog statuses into local phoenix_kit_project_statuses rows. Called inside start_project/2's transaction. Idempotent: a no-op if the project already has cemented rows. A no-op (returns :ok) when entities is unavailable — a started project simply has no workflow statuses until the module is wired up.

Creates a new default status entity, seeded with the default vocabulary, and returns {:ok, entity} (or {:error, :entities_not_available}).

The currently-selected status for a project as a normalized map, or nil when unset / unresolvable (e.g. the slug points at a trashed catalog row, or entities is unavailable).

Base name for generated default status entities ("project_statuses").

The default seeded status vocabulary (for tests / inspection).

Provisions a dedicated custom status entity for project (named project_status_<uuid>), seeds it with the defaults as a starting point, points the project's status_entity_uuid at it, and returns {:ok, project} with the updated project.

Fetches a cemented status row by uuid, scoped to a project.

The admin-chosen global default status entity uuid (the projects_default_status_entity_uuid setting), or nil when unset. This is what a project's "Shared default" resolves to. Set on the projects Settings page; nothing is auto-created.

The global default for status-title translation display (setting, default true).

Live catalog statuses for a given entity uuid (normalized). [] when entities is unavailable.

Cemented local status rows for a project, ordered by position.

Entities selectable as a project's status source, grouped for a picker: [{"Status lists", [{name, uuid}]}, {"Other entities", [{name, uuid}]}]. Status-tagged catalogs (the shared entity + per-project opt-ins, marked settings["source"] = "phoenix_kit_projects") come first; every other entity follows, since any entity's records can serve as statuses (record title = label). Empty groups are omitted. [] when entities is unavailable.

Server-side mate to the edit form's locked status-source picker: a started project's statuses were cemented at started_at and are frozen, so this drops any status_entity_uuid from incoming update attrs once started?/1. Unstarted projects and templates pass through unchanged (the source is still a live, pre-start choice). Handles string- and atom-keyed attrs.

Per-project custom status entity name: project_status_<32 hex> (the full project UUID with hyphens stripped — 47 chars, under the 50-char entity-name limit, and collision-free unlike a UUIDv7 prefix).

Clear + re-copy the chosen catalog into local rows, atomically. Used when a started project switches its status source (from the show-page picker). Existing local edits are intentionally discarded — the user chose a different list. Returns the (possibly slug-reconciled) project, or {:error, changeset} if the reconciling write fails. No-op (returns {:ok, project}) when entities is unavailable.

Deletes a cemented status row. If the deleted row was the project's currently-selected status, current_status_slug is cleared too (and a :project_status_changed broadcast fires) so the selection never dangles at a slug with no matching row. Slugs are unique per project, so deleting the row removes the only match.

Resolves which catalog entity a project/template draws from: its own status_entity_uuid (per-project choice), else the admin-chosen global default (the projects_default_status_entity_uuid setting, picked on the projects Settings page). Returns {:ok, uuid} or {:error, :no_status_entity} when neither is set — nothing is auto-provisioned.

Counts projects/templates currently sourcing their status list from the given catalog entity. This is the callback a host registers via config :phoenix_kit_entities, reverse_references: [{"project_status", &PhoenixKitProjects.Statuses.reverse_reference_count/1}] to power the entities admin's "Used by N" hint. Started projects no longer reference the catalog (they're cemented), which is the intended semantics.

Sets a project's current workflow status by slug (or clears it with nil). Validates the slug against the project's resolved status list before writing. Delegates the write to PhoenixKitProjects.Projects.set_current_status_slug/2 so the single PubSub broadcast fires. Returns {:ok, project} or {:error, reason}.

Sets (or clears with nil) the global default status entity.

Points a project at entity_uuid as its status source (nil = the shared default). For an already-started project this re-cements immediately — the chosen entity's statuses are snapshotted into fresh local rows (replacing any existing cemented rows), per the "cement on selection" rule — so existing/running projects get a usable, frozen status set. Unstarted projects just record the choice and cement at start as usual.

Statuses from the shared catalog entity if it already exists — does NOT provision it (unlike resolve_catalog_entity_uuid/1). Used by the list view's status filter, which shouldn't seed the entity as a side effect of rendering. [] when the shared entity hasn't been created yet or entities is unavailable.

True once a project has started (has a started_at).

The status list for a project — cemented local rows once it has started, otherwise the live catalog list it draws from. [] when entities is unavailable and the project hasn't started.

Batched current-status lookup for a list of projects. Returns %{project_uuid => status() | nil}. Groups started projects by a single local query and unstarted projects by their resolved catalog entity (one list_by_entity per distinct entity) to avoid an N+1. Empty map when entities is unavailable.

Updates a cemented status row.

Updates a project (arbitrary form attrs) and, when a started project's status source changed, re-cements its local rows — all in one transaction. This is the atomic edit-form entry point: update_project/2 followed by a separate re-cement would leave a failure window where the project points at a new entity with stale local rows. Returns the (possibly slug-reconciled) project. Unstarted projects just record the choice and cement at start.

Whether status titles display in the viewer's content locale for this project: the per-project override if set, else the global projects_use_status_translations setting (default true). Translations are always captured; this only gates display.

Types

status()

@type status() :: %{
  uuid: String.t() | nil,
  label: String.t(),
  slug: String.t(),
  color: String.t() | nil,
  position: integer()
}

A normalized status row, identical whether it came from the live catalog or a cemented local row. uuid is the source row's uuid (entity_data uuid pre-start, phoenix_kit_project_statuses uuid post-start); slug is the stable cross-boundary identity.

Functions

add_project_status(project, attrs)

Adds a cemented status row to a started project.

available?()

@spec available?() :: boolean()

Is the entities-backed status feature usable right now? True when the optional phoenix_kit_entities plugin is loaded AND enabled at runtime.

cement_project_statuses(project, opts \\ [])

@spec cement_project_statuses(
  PhoenixKitProjects.Schemas.Project.t(),
  keyword()
) :: :ok

Snapshots a project's chosen catalog statuses into local phoenix_kit_project_statuses rows. Called inside start_project/2's transaction. Idempotent: a no-op if the project already has cemented rows. A no-op (returns :ok) when entities is unavailable — a started project simply has no workflow statuses until the module is wired up.

Runs inside the caller's transaction, so a raised error rolls the whole start back.

create_default_status_entity(opts \\ [])

@spec create_default_status_entity(keyword()) :: {:ok, struct()} | {:error, term()}

Creates a new default status entity, seeded with the default vocabulary, and returns {:ok, entity} (or {:error, :entities_not_available}).

Named project_statuses; if that's already taken it auto-increments — project_statuses_2, project_statuses_3, … — so generating a default again always produces a fresh, independent list rather than reusing the existing one.

current_status(project)

@spec current_status(PhoenixKitProjects.Schemas.Project.t()) :: status() | nil

The currently-selected status for a project as a normalized map, or nil when unset / unresolvable (e.g. the slug points at a trashed catalog row, or entities is unavailable).

default_entity_base()

@spec default_entity_base() :: String.t()

Base name for generated default status entities ("project_statuses").

default_statuses()

@spec default_statuses() :: [map()]

The default seeded status vocabulary (for tests / inspection).

ensure_project_status_entity(project, opts \\ [])

@spec ensure_project_status_entity(
  PhoenixKitProjects.Schemas.Project.t(),
  keyword()
) :: {:ok, PhoenixKitProjects.Schemas.Project.t()} | {:error, term()}

Provisions a dedicated custom status entity for project (named project_status_<uuid>), seeds it with the defaults as a starting point, points the project's status_entity_uuid at it, and returns {:ok, project} with the updated project.

This is the per-project opt-in. The user fully owns the resulting entity afterward (rename, restructure fields, edit statuses) in the entities admin. No-op-with-error when entities is unavailable.

get_project_status(project, uuid)

Fetches a cemented status row by uuid, scoped to a project.

global_default_status_entity_uuid()

@spec global_default_status_entity_uuid() :: String.t() | nil

The admin-chosen global default status entity uuid (the projects_default_status_entity_uuid setting), or nil when unset. This is what a project's "Shared default" resolves to. Set on the projects Settings page; nothing is auto-created.

global_use_status_translations?()

@spec global_use_status_translations?() :: boolean()

The global default for status-title translation display (setting, default true).

list_catalog_statuses(entity_uuid)

@spec list_catalog_statuses(String.t()) :: [status()]

Live catalog statuses for a given entity uuid (normalized). [] when entities is unavailable.

list_project_statuses(project)

@spec list_project_statuses(PhoenixKitProjects.Schemas.Project.t()) :: [status()]

Cemented local status rows for a project, ordered by position.

list_status_source_entities()

@spec list_status_source_entities() :: [{String.t(), [{String.t(), String.t()}]}]

Entities selectable as a project's status source, grouped for a picker: [{"Status lists", [{name, uuid}]}, {"Other entities", [{name, uuid}]}]. Status-tagged catalogs (the shared entity + per-project opt-ins, marked settings["source"] = "phoenix_kit_projects") come first; every other entity follows, since any entity's records can serve as statuses (record title = label). Empty groups are omitted. [] when entities is unavailable.

lock_status_source(attrs, project)

@spec lock_status_source(map(), PhoenixKitProjects.Schemas.Project.t()) :: map()

Server-side mate to the edit form's locked status-source picker: a started project's statuses were cemented at started_at and are frozen, so this drops any status_entity_uuid from incoming update attrs once started?/1. Unstarted projects and templates pass through unchanged (the source is still a live, pre-start choice). Handles string- and atom-keyed attrs.

per_project_entity_name(project)

@spec per_project_entity_name(PhoenixKitProjects.Schemas.Project.t()) :: String.t()

Per-project custom status entity name: project_status_<32 hex> (the full project UUID with hyphens stripped — 47 chars, under the 50-char entity-name limit, and collision-free unlike a UUIDv7 prefix).

recement_project_statuses(project)

@spec recement_project_statuses(PhoenixKitProjects.Schemas.Project.t()) ::
  {:ok, PhoenixKitProjects.Schemas.Project.t()} | {:error, term()}

Clear + re-copy the chosen catalog into local rows, atomically. Used when a started project switches its status source (from the show-page picker). Existing local edits are intentionally discarded — the user chose a different list. Returns the (possibly slug-reconciled) project, or {:error, changeset} if the reconciling write fails. No-op (returns {:ok, project}) when entities is unavailable.

remove_project_status(row)

Deletes a cemented status row. If the deleted row was the project's currently-selected status, current_status_slug is cleared too (and a :project_status_changed broadcast fires) so the selection never dangles at a slug with no matching row. Slugs are unique per project, so deleting the row removes the only match.

resolve_catalog_entity_uuid(project)

@spec resolve_catalog_entity_uuid(PhoenixKitProjects.Schemas.Project.t()) ::
  {:ok, String.t()} | {:error, term()}

Resolves which catalog entity a project/template draws from: its own status_entity_uuid (per-project choice), else the admin-chosen global default (the projects_default_status_entity_uuid setting, picked on the projects Settings page). Returns {:ok, uuid} or {:error, :no_status_entity} when neither is set — nothing is auto-provisioned.

reverse_reference_count(entity_uuid)

@spec reverse_reference_count(String.t()) :: non_neg_integer()

Counts projects/templates currently sourcing their status list from the given catalog entity. This is the callback a host registers via config :phoenix_kit_entities, reverse_references: [{"project_status", &PhoenixKitProjects.Statuses.reverse_reference_count/1}] to power the entities admin's "Used by N" hint. Started projects no longer reference the catalog (they're cemented), which is the intended semantics.

set_current_status(project, slug, opts \\ [])

@spec set_current_status(
  PhoenixKitProjects.Schemas.Project.t(),
  String.t() | nil,
  keyword()
) ::
  {:ok, PhoenixKitProjects.Schemas.Project.t()} | {:error, term()}

Sets a project's current workflow status by slug (or clears it with nil). Validates the slug against the project's resolved status list before writing. Delegates the write to PhoenixKitProjects.Projects.set_current_status_slug/2 so the single PubSub broadcast fires. Returns {:ok, project} or {:error, reason}.

set_default_status_entity(uuid)

@spec set_default_status_entity(String.t() | nil) :: term()

Sets (or clears with nil) the global default status entity.

set_status_entity(project, entity_uuid, opts \\ [])

@spec set_status_entity(
  PhoenixKitProjects.Schemas.Project.t(),
  String.t() | nil,
  keyword()
) ::
  {:ok, PhoenixKitProjects.Schemas.Project.t()} | {:error, term()}

Points a project at entity_uuid as its status source (nil = the shared default). For an already-started project this re-cements immediately — the chosen entity's statuses are snapshotted into fresh local rows (replacing any existing cemented rows), per the "cement on selection" rule — so existing/running projects get a usable, frozen status set. Unstarted projects just record the choice and cement at start as usual.

shared_catalog_statuses()

@spec shared_catalog_statuses() :: [status()]

Statuses from the shared catalog entity if it already exists — does NOT provision it (unlike resolve_catalog_entity_uuid/1). Used by the list view's status filter, which shouldn't seed the entity as a side effect of rendering. [] when the shared entity hasn't been created yet or entities is unavailable.

started?(arg1)

True once a project has started (has a started_at).

statuses_for(project)

@spec statuses_for(PhoenixKitProjects.Schemas.Project.t()) :: [status()]

The status list for a project — cemented local rows once it has started, otherwise the live catalog list it draws from. [] when entities is unavailable and the project hasn't started.

statuses_for_projects(projects)

@spec statuses_for_projects([PhoenixKitProjects.Schemas.Project.t()]) :: %{
  optional(String.t()) => status() | nil
}

Batched current-status lookup for a list of projects. Returns %{project_uuid => status() | nil}. Groups started projects by a single local query and unstarted projects by their resolved catalog entity (one list_by_entity per distinct entity) to avoid an N+1. Empty map when entities is unavailable.

update_project_status_row(row, attrs)

@spec update_project_status_row(PhoenixKitProjects.Schemas.ProjectStatus.t(), map()) ::
  {:ok, PhoenixKitProjects.Schemas.ProjectStatus.t()}
  | {:error, Ecto.Changeset.t()}

Updates a cemented status row.

update_project_with_statuses(project, attrs)

@spec update_project_with_statuses(PhoenixKitProjects.Schemas.Project.t(), map()) ::
  {:ok, PhoenixKitProjects.Schemas.Project.t()} | {:error, term()}

Updates a project (arbitrary form attrs) and, when a started project's status source changed, re-cements its local rows — all in one transaction. This is the atomic edit-form entry point: update_project/2 followed by a separate re-cement would leave a failure window where the project points at a new entity with stale local rows. Returns the (possibly slug-reconciled) project. Unstarted projects just record the choice and cement at start.

use_status_translations?(project)

@spec use_status_translations?(PhoenixKitProjects.Schemas.Project.t()) :: boolean()

Whether status titles display in the viewer's content locale for this project: the per-project override if set, else the global projects_use_status_translations setting (default true). Translations are always captured; this only gates display.

use_status_translations?(project, global_default)

@spec use_status_translations?(PhoenixKitProjects.Schemas.Project.t(), boolean()) ::
  boolean()