PhoenixKitProjects.Features (PhoenixKitProjects v0.21.1)

Copy Markdown View Source

Per-project feature flags — Level 2 of the hub's granularity model (Level 1 is extension enablement in PhoenixKitProjects.Extensions).

Every flag belongs to an extension: the built-in Tasks extension declares the pre-hub feature set (assignees, estimates, statuses, …) and providers declare their own via feature_flags on their descriptor — one channel, no special-cased built-ins.

Resolution (frozen semantics — panel amendment #6)

on?(project, key) is true iff ALL of:

  1. the flag exists in the catalog (unknown keys are fail-closed false);
  2. the flag's OWNING extension is effectively enabled for the project (Extensions.enabled?/2);
  3. every flag in requires resolves on (recursive, cycle-guarded) — the GitLab-style dependency rule: view_timeline requires scheduling, scheduling requires estimates;
  4. the project's explicit value — settings["features"][key] — when present; otherwise the flag's declared default.

Absence semantics are frozen: a missing settings key ALWAYS means "inherit the flag's catalog default", never a latent choice. Presets and the Modules panel write explicit booleans; pre-hub projects have no keys and every pre-hub flag defaults true — behavior-preserving with no backfill migration.

Presets

Named flag bundles applied at creation (or any time before a team relies on the current shape): "simple" (a todo list — everything off but the list), "standard" (the pre-hub defaults), "full" (everything on). apply_preset/3 writes the bundle as EXPLICIT values. The site default for new projects is the projects_default_preset setting ("standard").

Summary

Functions

Applies a preset's flag bundle to a project (explicit writes via set_flags/3). Unknown preset keys are a no-op {:ok, project} — a stale site-default setting must not break project creation.

The full flag catalog: %{flag_key => %{key, label, default, requires, ext_key}} — every extension's declared flags, stamped with their owner. First declaration wins on key collisions (logged).

Flags grouped per extension, for the Modules panel: [{%Extension{}, [flag]}].

The promotable creation-page blocks (key + label), for the settings UI.

The block keys the site promoted to top level (default: none). A plain list, not a MapSet — dialyzer's opaqueness false positive on literal MapSet construction (the resolve/3 precedent in this module).

Gate map for surfaces with NO project yet (the :new forms): the catalog defaults — i.e. what a fresh project would resolve. All pre-hub flags default true, so this is all-true today and stays correct if a default ever changes.

The site-default preset key for new projects (projects_default_preset).

The resolved gate map LiveViews assign as @fx: the tasks extension plus every built-in task flag, resolved for one project in one call. Rebuild it on :project_features_changed / :project_modules_changed.

Look up a preset by key.

Whether a flag is on for a project. See the moduledoc for the frozen resolution order. Accepts a %Project{} (preferred — no extra reads) or a project uuid (loads the settings map).

Named flag bundles. flags are written as EXPLICIT values by apply_preset/3; keys absent from a preset keep resolving by default.

Persist the promoted block set (unknown keys dropped).

Writes explicit flag values (%{"flag_key" => boolean}) into the project's settings["features"], whitelisted against the catalog — unknown keys and non-boolean values are dropped, known keys merge over the stored map. Logs one projects.feature_toggled activity row with the changed keys; broadcasts :project_features_changed.

Functions

apply_preset(project, preset_key, opts \\ [])

Applies a preset's flag bundle to a project (explicit writes via set_flags/3). Unknown preset keys are a no-op {:ok, project} — a stale site-default setting must not break project creation.

catalog()

@spec catalog() :: %{required(String.t()) => map()}

The full flag catalog: %{flag_key => %{key, label, default, requires, ext_key}} — every extension's declared flags, stamped with their owner. First declaration wins on key collisions (logged).

catalog_by_extension()

@spec catalog_by_extension() :: [
  {PhoenixKitProjects.Extensions.Extension.t(), [map()]}
]

Flags grouped per extension, for the Modules panel: [{%Extension{}, [flag]}].

creation_blocks()

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

The promotable creation-page blocks (key + label), for the settings UI.

creation_top_blocks()

@spec creation_top_blocks() :: [String.t()]

The block keys the site promoted to top level (default: none). A plain list, not a MapSet — dialyzer's opaqueness false positive on literal MapSet construction (the resolve/3 precedent in this module).

default_gates()

@spec default_gates() :: %{required(atom()) => boolean()}

Gate map for surfaces with NO project yet (the :new forms): the catalog defaults — i.e. what a fresh project would resolve. All pre-hub flags default true, so this is all-true today and stays correct if a default ever changes.

default_preset_key()

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

The site-default preset key for new projects (projects_default_preset).

gates(project_or_uuid)

@spec gates(PhoenixKitProjects.Schemas.Project.t() | map() | binary()) :: %{
  required(atom()) => boolean()
}

The resolved gate map LiveViews assign as @fx: the tasks extension plus every built-in task flag, resolved for one project in one call. Rebuild it on :project_features_changed / :project_modules_changed.

get_preset(key)

@spec get_preset(String.t()) :: map() | nil

Look up a preset by key.

on?(project_or_uuid, flag_key)

Whether a flag is on for a project. See the moduledoc for the frozen resolution order. Accepts a %Project{} (preferred — no extra reads) or a project uuid (loads the settings map).

presets()

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

Named flag bundles. flags are written as EXPLICIT values by apply_preset/3; keys absent from a preset keep resolving by default.

set_creation_top_blocks(keys)

@spec set_creation_top_blocks([String.t()]) :: {:ok, term()} | {:error, term()}

Persist the promoted block set (unknown keys dropped).

set_flags(project, flags, opts \\ [])

Writes explicit flag values (%{"flag_key" => boolean}) into the project's settings["features"], whitelisted against the catalog — unknown keys and non-boolean values are dropped, known keys merge over the stored map. Logs one projects.feature_toggled activity row with the changed keys; broadcasts :project_features_changed.