# `PhoenixKitManufacturing.Web.MachineFormLive`
[🔗](https://github.com/BeamLabEU/phoenix_kit_manufacturing/blob/0.4.1/lib/phoenix_kit_manufacturing/web/machine_form_live.ex#L1)

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

# `on_mount`

---

*Consult [api-reference.md](api-reference.md) for complete listing*
