No description provided.
SSR Node Editor #49
Merged
ernstla
merged 27 commits from 2026年07月03日 20:21:48 +02:00
rewrite/ssr-field-types into main
ernstla
commented 2026年07月03日 19:18:43 +02:00
ernstla
added 25 commits 2026年07月03日 19:18:44 +02:00
Load the node (or blueprint) payload in the panel editor controller and render the editor as an SSR form: Boiler views per control name under panel/views/field/, a shared field wrapper with locale variants, and global styles ported from the island's scoped components. The Svelte island host, its bootstrap payload and the legacy /panel API wiring are gone from the editor view. Read-only milestone: saving comes next.
ValidatorFactory passed the raw label into sire, which requires a string — saving any node with an unlabeled field crashed with a TypeError. Field name as fallback; labels only feed validation messages.
minLength/maxLength are not sire rules (minlen/maxlen are); validating the fixture threw ValueError. Never noticed because no test saved a TestDocument until the SSR editor save tests did.
POST {panel}/collection/{slug}/{uid} feeds the submitted form through
FormPatch: per-field replacement over the stored node payload, casts by
control name (checkbox presence marker, number to float, repeater index
normalization, group key merge), rich fields as one JSON blob under
[json]. Unsubmitted fields, extra locales and metas survive untouched.
Success answers with out-of-band status fragments (hx-swap=none keeps
the form and focus intact); validation errors surface in an OOB error
box; non-htmx requests follow PRG. Login's form parsing fallback moves
to the Panel base as formData().
Editor pages carry a #cosray-system-data JSON script (locales, prefix, assets, allowedFiles — the SSR replacement for /panel/boot) and element controls render as <cosray-host> tags with their value, meta, field and locales payload embedded as JSON. The node uid threads through the field wrapper for the element contract.
cosray-host is a form-associated custom element: it loads the control module, assigns the unchanged element contract and mirrors every cosray-change into the form value as one JSON string for the merge patch. panel.ts drops the island wiring; on editor pages it configures the runtime from the embedded payload and installs a standalone window.Cosray bridge with plain-DOM modal chrome and toasts.
Three delegated document-level behaviors replace the island's reactivity: locale tabs toggle server-rendered variants and hand the locale to element hosts; the unsaved-changes guard anchors dirty state to the form element itself (a swapped-out form cannot leak stale state) and stands down when the OOB save status reports data-saved; repeater add/remove clones the server-rendered __i__ template and renumbers names, ids and labels. The save fragment marks success for the guard.
The topbar gains publish (save-and-publish submit), preview and a confirmed delete form; the headline shows the published badge (updated out-of-band after saves). Content and settings live as two panes inside one form: the settings pane submits handle, per-locale route paths and the published/hidden switches through the same merge patch, and a debounced htmx fragment previews generated route paths as you type. Create mode posts the blueprint form and 303-redirects to the fresh node's editor; delete redirects to the collection (Store::delete gains a requireJson opt-out for the form path). Preview saves first, then overlays the rendered page in an iframe swapped in out-of-band.
#[When('multiDay')] ties a field to a sibling's value (truthy, eq,
neq, in, empty, notEmpty). The editor behavior evaluates the condition
against form state on every edit — hiding keeps inputs (and typed
values) in the form and suspends required. Read-time enforcement lives
in the hydrator: an inactive field hydrates with empty data, so every
frontend/API consumer sees it empty by construction while the stored
value survives; Field::raw() is the deliberate bypass. One declaration,
two lockstep evaluators (Cosray\Field\Condition and behaviors/when.ts)
sharing form-normalized semantics.
Group sub-controls may declare a width percentage: sized sub-controls share a row and stack at full container width via a container query — the date-range composite (two 50% dates) works with zero JavaScript. data-derive mirrors a sibling field into an input (optionally slugified) until it is edited manually; data-count-of renders live character counts. Both are the closed end of the behavior set — richer client logic belongs in element controls.
Field::metaControl() describes the field's meta map as a group whose sub-control keys name the meta entries. The wrapper shows a Meta button opening a native per-field dialog; entries submit as content[field][meta][key][zxx] and the merge patch replaces only the keys the group knows — stored meta the form never rendered survives. Element controls keep managing meta themselves via cosray-change.
The elements' shared system store now populates from window.Cosray instead of the island's boot fetch — locale tabs and media modals inside elements read live locale data again. Input drops the vestigial island dirty call (edits reach the form through cosray-change), ModalLink browses node media only when a caller can provide fields and content (element controls cannot yet — media tabs hide instead of rendering empty), ModalOptions moves into the dependency-free bridge module, and the runtime shrinks to the panel base path.
The SSR form replaced every use of the Svelte island: panel/src/editor, the island-only shell components (editor chrome, content interpreter, primitives dispatcher, toasts, modal shell), the shared dirty/modal stores and the legacy request plumbing (req, qs, boot/api runtime URLs) are gone. What survives under panel/src is exactly what the element bundles reach: the entries/blocks internals, media components, the bridge, the host, and the panel behaviors. node-placeholder styles and the island fallback CSS go with it; docs describe the SSR rendering.
field/input.php referenced $attrs before the null-coalesce, so any control that does not pass attrs (date, time, datetime, hidden) blew up with 'Undefined variable' — no test fixture rendered one. Guard the optional unwraps, give the conditional fixture date/time fields and assert their rendered inputs.
htmx4 dispatches before:request on the request's source element with the context under detail.ctx — the guard read detail.sourceElement, never matched, and prompted for the form's own requests (route-path preview after typing, save). Exempt by event target with ctx fallback.
The delete form action and the paths preview URL were built by appending '/delete' and '/paths' to edit(), whose value already carries the editor query string — the suffix landed inside the query, so the preview 400ed and the delete button posted to the save route instead. CollectionUrls grows paths()/delete() builders.
The elements' sync effect wrote the map state and read it back through the locale-slot default, tripping Svelte's effect_update_depth_exceeded guard on every mount (latent since the element conversion; first real browser run surfaced it). The slot default now runs untracked.
Required translated fields rendered required on every locale variant;
the hidden non-default variant was empty, and the browser refused to
submit ('invalid form control is not focusable') — saving was
impossible. The form is novalidate now (hidden locale variants, panes
and meta dialogs are legitimate, which native validation cannot
express; the server validates and reports out-of-band) and the required
attribute only lands on the default locale variant, matching the
server-side shape.
richtext.css, code.css, grid.css and util.css reached the page through the island chunk's editor.css, which the dissolution deleted — the richtext toolbar collapsed into an unstyled icon pile and the source textarea sat permanently visible. panel.css now carries them.
The wrappers synced their value maps in an effect, which runs after children mount — ProseMirror and CodeMirror read their content at mount and came up empty (blocks hid the gap behind a template guard). The maps now initialize synchronously from the host-assigned props; the effect remains for later re-assignments and locale switches, and the sync helpers mutate the raw prop object so the untrack workaround from the loop fix becomes unnecessary.
onUpdate notified before assigning the bound value, so every cosray-change carried the state one keystroke behind and the last edit never reached the form value. Under the island this only toggled the dirty flag; the element world serializes the map at notify time.
The editor island's dirty badge was dropped in the dissolution; dirty.ts tracked the state but rendered nothing. Re-add the headline badge, toggle it from dirty.ts on edit/save, and restore its style.
The editor is one htmx-boosted form and field controls render into its light DOM, so any <button> without type="button" defaulted to submit and silently saved the node (block width buttons were the visible symptom). Add a capture-phase submit guard so only the explicit save/publish/preview actions submit, and set type=button on the block control buttons.
The generated-path preview re-posted the whole form on every input, so it fired on richtext and unrelated fields yet stayed a no-op (and missed custom-element fields). Mark exactly the inputs the route template references — the handle, the path inputs, and the content fields named in the template (via new RoutePathGenerator::referencedFields) — and render the initial preview server-side. Editing those refreshes the preview; nothing else does.
MediaList reordered assets via SortableJS but never notified, and notify was never passed to it, so in the push-based element model the new order never reached the form value and was lost on save (add/remove persisted because they ran through Upload, which notifies). Thread notify into MediaList and call it after reorder and after the in-grid alt/title edit.
ernstla
added 1 commit 2026年07月03日 19:54:20 +02:00
Create mode passed an empty node id and skipped the route-path preview, so a new node could neither hold uploaded media nor preview its URL. The create form now carries the blueprint's pre-generated uid (a hidden field and the fields' node id), and store() adopts that submitted uid, so media uploaded to node/<uid>/ before saving resolves to the persisted node. The preview is wired for create mode via a blueprint-based create-paths endpoint, so the generated URL updates live as the route's fields are edited.
ernstla
added 1 commit 2026年07月03日 20:21:13 +02:00
ernstla
merged commit 37d651c199 into main 2026年07月03日 20:21:48 +02:00
ernstla
deleted branch rewrite/ssr-field-types 2026年07月03日 20:21:48 +02:00
Sign in to join this conversation.
No reviewers
Labels
Clear labels
bug
Something isn't working
dependencies
Pull requests that update a dependency file
documentation
Improvements or additions to documentation
duplicate
This issue or pull request already exists
enhancement
New feature or request
good first issue
Good for newcomers
help wanted
Extra attention is needed
invalid
This doesn't seem right
javascript
Pull requests that update javascript code
question
Further information is requested
wontfix
This will not be worked on
No labels
bug
dependencies
documentation
duplicate
enhancement
good first issue
help wanted
invalid
javascript
question
wontfix
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".
No due date set.
Dependencies
No dependencies set.
Reference
cosray/cms!49
Loading...
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "rewrite/ssr-field-types"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?