PhoenixKitManufacturing. Web. MachineFormLive
(PhoenixKitManufacturing v0.3.2)
Copy Markdown
View Source
Create/edit form for machines.
Machine fields (name, code, manufacturer…) are plain identifiers, so this
form uses core inputs rather than the multilang translatable fields used
for machine types. Type links are managed with a click-to-toggle picker
held in a MapSet and synced to the join table after the machine saves.
Admin chrome
Self-wraps with LayoutWrapper.app_layout (:self_wrapped_layout on_mount
below) instead of relying on PhoenixKit's automatic admin layout, so
page_title/page_subtitle render in the global admin header rather than
an in-page one — same pattern as PhoenixKitManufacturing.Web.MachinesLive.
Tabs
Once a machine exists, its card is split into in-page tabs (tabs tabs-border, <.link patch={...}>, same recipe as
PhoenixKitWarehouse.Web.InternalOrderFormLive): General (passport,
location, types, template fields), Operations, Files, Comments. Each tab
has its own hidden CRUD route (:operations/:files/:comments in
PhoenixKitManufacturing.admin_tabs/0, visible: false — same
hidden_crud_tabs convention as the warehouse) so it's directly
linkable/bookmarkable and survives a refresh, but never appears in the
sidebar. Switching tabs is a patch, not a navigate — the LiveView
process stays alive, so handle_params/3 (not mount/3) does the actual
per-action loading. mount/3 only sets up tab-independent scaffolding
(upload config, locale); handle_params/3 loads the machine and, like
InternalOrderFormLive's load_order_into_socket/3, only rebuilds the
pending, unsaved edit buffer (changeset, linked types/operations,
Attachments scope state) the first time a given uuid is seen — a bare tab
switch (same_machine? true) leaves it untouched, so toggling a type,
picking a featured image, or setting an operation override survives
navigating to another tab before hitting Save.
A :new machine has no uuid yet and stays a single, tab-less page
(General only, no tab bar) — Operations/Files/Comments only become
reachable once the machine has been saved once, mirroring how
InternalOrderFormLive's :new never really renders past its own
auto-create redirect.
Save/Cancel live inside the shared <.form>, which wraps every tab
except Comments — General, Operations, and Files all hold state that's
still pending at save time (passport/types/template are changeset-backed;
operation overrides and the featured-image pick are separate socket
assigns synced at save, see below); Comments persists immediately through
its own component and has nothing to save.
Location (soft link, not a form field)
location_uuid/space_uuid are picked via
PhoenixKitLocations.Web.Components.PlacePicker, a LiveComponent
rendered in its own card outside the main <.form phx-change="validate">
— see the comment on that card in render/1 for why. The picked uuids
live in @location_uuid/@space_uuid (updated from the component's
{:place_picker_select, ...} message) and are merged into the params in
save_machine/3, not read off the form.
Dynamic metadata fields
Machine types can define a field_template — stored in the machine_type
entity-data record's metadata["field_template"] (see EntitiesRegistry's
"Record shape" moduledoc section for why it lives in metadata rather
than data); Machines.merged_field_template/1 merges the templates of
every linked, published type into @merged_template, rendered as extra
inputs named machine[metadata][KEY] (raw name=, not @form[:atom] —
metadata here is this machine's freeform value map, keyed by whatever
the linked types define, not a fixed changeset field — a different
metadata from the type record's own). Recomputed whenever the type
selection changes. Each type badge carries a pencil icon-link to
Web.MachineTypeTemplateLive, the hidden-route mini-editor for that
type's own field_template — the generic entities admin UI has no widget
for it (see that module's moduledoc for why).
Operations
Every published operation in the directory (see
PhoenixKitManufacturing.EntitiesRegistry) can be linked to this machine,
each link optionally overriding the operation's own
base_time_norm_seconds for this machine specifically.
@operation_overrides is a %{operation_uuid => time_norm_seconds | nil}
map — its key set is exactly which operations are linked, the same
shape Machines.linked_operation_overrides/1 returns and
Machines.sync_machine_operations/3 takes as its desired "after" state,
so the form assign doubles as the sync payload with no translation step
(mirrors how @linked_type_uuids doubles as sync_machine_types/3's
payload). toggle_operation adds/removes a key (nil override — "use
the operation's base norm" — until the user types one);
set_operation_override (fired on the override input's phx-blur, not
the enclosing <.form>'s phx-change — see that handler for why)
updates an existing key's value, guarded with Map.replace/3 so a stray
blur event for a row the user has since unchecked can't silently
re-link it. Both links are synced together in sync_and_redirect/3.
Files & featured image
Wired through PhoenixKitManufacturing.Attachments, the same
folder-scoped pattern used by PhoenixKitLocations.Attachments (see
that module's doc for the general mechanics) and rendered with
PhoenixKitManufacturing.Web.Components.FilesCard on the Files tab
(see "Tabs" above — only reachable once the machine has a uuid). This
form only ever has one Attachments scope — the literal string
"machine" — so every Attachments event handler below hardcodes it
rather than reading phx-value-scope off the event params.
Attachments.maybe_rename_pending_folder_for/2 in save_machine/3's
:new clause is a leftover safety net from before the Files tab
existed (a :new machine could upload before its first save, landing
in a "pending" folder renamed post-save) — now a no-op in practice
since :new never renders the Files tab, but left in place rather than
torn out, since it's harmless and the fallback stays correct if that
ever changes.
One deliberate exception to "folder-scoped": the featured-image picker
itself (the MediaSelectorModal in render/1) browses the full
media library, not just this machine's folder — see the comment there
for why. Everything else (attached-files upload, storage, detach) stays
folder-scoped exactly as described above.
Comments
Only rendered on the Comments tab (@active_tab == :comments, which —
see "Tabs" above — only exists once the machine has a uuid) and only
when PhoenixKitManufacturing.Comments.available?/0 is true. Like the
Location card, this is rendered in its own card outside the main
<.form> — CommentsComponent renders its own internal <.form phx-target={@myself}> for the composer, and nesting that inside this
form's <.form> would produce invalid nested <form> elements. use PhoenixKitComments.Embed (below) forwards the rich-text composer's
{:leaf_changed, ...} messages to the component — without it, posting
a comment silently no-ops (see PhoenixKitComments.Embed moduledoc).