1
0
Fork
You've already forked cms
0

SSR Node Editor #49

Merged
ernstla merged 27 commits from rewrite/ssr-field-types into main 2026年07月03日 20:21:48 +02:00
No description provided.
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.
Break the value-sync effect loop in blocks and entries
All checks were successful
ci / ci (push) Successful in 3m4s
ci / build-and-release (push) Has been skipped
ci / release (push) Successful in 0s
256f4a317b
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.
Write the richtext bind before notifying
All checks were successful
ci / ci (push) Successful in 3m3s
ci / build-and-release (push) Has been skipped
ci / release (push) Successful in 0s
60f25c0f65
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.
Persist image reorder and in-grid edits
All checks were successful
ci / ci (push) Successful in 3m3s
ci / build-and-release (push) Has been skipped
ci / release (push) Successful in 0s
5a1009727c
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.
Make new nodes fully editable before the first save
All checks were successful
ci / ci (push) Successful in 2m55s
ci / build-and-release (push) Has been skipped
ci / release (push) Successful in 0s
fbc82e911b
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.
Sanitize media upload filename and reject uid path traversal
All checks were successful
ci / ci (push) Successful in 2m50s
ci / build-and-release (push) Successful in 1m24s
ci / release (push) Successful in 0s
37d651c199
ernstla deleted branch rewrite/ssr-field-types 2026年07月03日 20:21:48 +02:00
Sign in to join this conversation.
No reviewers
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
Reference in a new issue
cosray/cms
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?