Cross-LV helpers shared by the staff admin LiveViews.
log_operation_error/3 — failure-side audit rows
Staff's documented architectural choice is that PhoenixKitStaff.Activity
is success-only at the call site (see CLAUDE.md "Activity logging").
The post-Apr pipeline expects every user-driven mutation to leave an
audit row on BOTH :ok AND :error branches so a DB outage / FK
violation / constraint failure can't silently erase admin clicks
from the activity feed.
The catalogue module's Batch 4 (canonical reference at
phoenix_kit_catalogue/lib/phoenix_kit_catalogue/web/helpers.ex)
resolves the tension by writing the failure-side row at the
LiveView layer, not in the context. Same intent here:
- One edit point in this helper instead of ~10 LV mutation sites
- Same action atom the success path would have used
(e.g.
staff.person_deleted) metadata.db_pending: trueso audit-feed readers can distinguish attempted-but-failed from completed actions- PII-safe metadata: changeset reasons land error-key field names
only (no values), atom reasons land the atom string, other shapes
get
error_kind: "other"
Helper only fires from handle_event {:error, _} branches —
validate cycles never reach it (they're handled by the form's
assign_form/2 cycle, no audit row needed for keystrokes).
Summary
Functions
Returns the user's in-flight record by applying the current changeset.
Reads the translations field off the current form's changeset and
returns the sub-map for current_lang (or %{}).
Writes a failure-side activity row for a destructive/mutating operation.
Flips :current_lang back to :primary_language when a save fails
with errors on translatable primary fields submitted from a
secondary tab. Without this, the inline error renders on the
primary tab (where the user can't see it) and the form re-renders
with no visible change after submit. translatable_fields is the
list of DB column names (atoms) from Schema.translatable_fields/0.
Folds secondary-language form params into the in-flight record's
translations JSONB and preserves primary-language column values
that the current secondary-tab DOM didn't render.
Functions
@spec in_flight_record(Phoenix.LiveView.Socket.t(), atom(), atom()) :: struct()
Returns the user's in-flight record by applying the current changeset.
When the user has been typing in a primary-tab field and switches to a
secondary tab, the server-side changeset already captures those primary
values from prior validate events. Re-using the pristine assign
would lose them because that struct is the pre-form-edit version.
Apply the changeset to get the baseline that has both the primary
fields AND the existing translations the user has already typed.
@spec lang_data(Phoenix.HTML.Form.t(), String.t() | nil) :: map()
Reads the translations field off the current form's changeset and
returns the sub-map for current_lang (or %{}).
Used as the lang_data attr on <.translatable_field> so secondary
tabs see in-flight overrides — without this, switching between two
secondary tabs would lose unsaved edits.
@spec log_operation_error(String.t(), Phoenix.LiveView.Socket.t(), keyword()) :: :ok | :activity_unavailable | {:ok, struct()} | {:error, any()}
Writes a failure-side activity row for a destructive/mutating operation.
Required opts
:resource_type— string, e.g."staff_person"/"department":reason— the{:error, reason}value from the context call
Optional opts
:resource_uuid— uuid of the record the operation targeted. Optional because failed CREATE submissions have no uuid yet — in that case the audit row records intent without a target.:target_uuid— second-party uuid for membership operations (the user being added/removed, etc.):metadata— extra metadata (must be PII-safe). Merged UNDER the helper's owndb_pending/error_kind/error_keys/error_atomkeys — caller-supplied collisions on those keys are ignored so the audit-feed contract stays stable.
Returns the underlying Activity.log/2 return value (:ok,
{:ok, _entry}, {:error, _}, :activity_unavailable); never
raises.
@spec maybe_switch_to_primary_on_error( Phoenix.LiveView.Socket.t(), Ecto.Changeset.t(), [atom()] ) :: Phoenix.LiveView.Socket.t()
Flips :current_lang back to :primary_language when a save fails
with errors on translatable primary fields submitted from a
secondary tab. Without this, the inline error renders on the
primary tab (where the user can't see it) and the form re-renders
with no visible change after submit. translatable_fields is the
list of DB column names (atoms) from Schema.translatable_fields/0.
Folds secondary-language form params into the in-flight record's
translations JSONB and preserves primary-language column values
that the current secondary-tab DOM didn't render.
primary_fields is the list of DB column names (as strings) that are
translatable — e.g. ["name", "description"] for Department,
["job_title", "bio", "skills", "notes"] for Person.