No description provided.
Dynamic node editor and plugin arch #48
Merged
ernstla
merged 34 commits from 2026年07月03日 11:07:20 +02:00
epic/plugins into main
ernstla
commented 2026年07月03日 09:38:37 +02:00
ernstla
added 31 commits 2026年07月03日 09:38:37 +02:00
getField()/getFields() are pure reflection reads with no instance state. Making them static removes the ad-hoc empty-registry hydrators in Column and ValidatorFactory and drops the hydrator constructor params from Node, Serializer and ViewRenderer, which only forwarded it for reads. Node\Factory now holds the only hydrator instance.
The optional param silently fell back to an empty registry, skipping all attribute handlers. Types now exposes its registry for callers that need to construct or share the node schema registry.
Plugin now constructs the field schema registry once and exposes it together with the node schema registry and a new Field\Services bag (schemas + types) as container entries, giving external code one live registration point instead of scattered withDefaults() copies.
Field::init(Services, ?ReflectionProperty) replaces initSchema and throws instead of silently building a private registry copy. The hydrator, node Factory and Cms now require Services, so every hydration path shares the container-bound registries and Types. Entries no longer builds its own Types instance.
The class is the CMS boot procedure, not a user-facing plugin. This frees the plugin name for the upcoming runtime plugin interface; App::plugin() becomes App::bootstrap().
Field\Index registers field types and their string aliases, replacing the hardcoded alias table in NodeContentNormalizer. Bound in the container so plugins can register custom field types whose aliases reach content normalization.
Plugins implement Cosray\Plugin\Plugin (id + register) and are registered via App::plugin() or the plugins config key. Bootstrap runs each plugin's register() before renderers, collect() and database(), so plugin fields, schemas, nodes, collections, services, migration/sql dirs and frontend routes land in the normal pipeline. The Registrar is the write-only facade handed to plugins.
Integration test shows a plugin-shipped field type hydrates, serializes and exposes editor properties with no core changes. README gains a Plugins section covering the interface, Registrar surface and migration conventions.
Field\Control is an immutable builder over a fixed named vocabulary (text, number, option, richtext, group, repeater, element, ...). Every field emits its descriptor via control(), serialized into properties() as 'control', so the panel can render fields without knowing field type classes. The element control is the web-component escape hatch for plugin-supplied UIs.
Registrar::assets(dir) maps a plugin's prebuilt asset dir to
{panel}/vendor/{pluginId}/..., served path-jailed with a wider
extension whitelist (js modules, fonts, images). This is the
delivery path for element controls shipped by plugins.
The island no longer knows field type classes: Control.svelte dispatches on the server-provided control descriptor name and controls.ts is keyed by the fixed vocabulary. Adds Datetime, radio display for Option, number props (step/min/max), structural Group/ Repeater controls with a primitive SubControl, and the Element escape hatch that dynamically imports plugin web components from the vendor asset route. Decimal, DateTime and Radio fields render real controls for the first time.
docs/controls.md specifies the value shape per control name and the element (web component) contract. E2E test asserts the node API payload carries control descriptors end-to-end.
Block\Type classes own their panel descriptor (control + init payload) and server render; Block\Registry replaces the closed match in Value\Blocks. Blocks field payloads expose blockTypes for a data-driven picker, restrictable via allow()/#[Allows]. Plugins register types through Registrar::blockType(). Heading levels render legacy content but stay hidden from the picker.
The picker list, initial payloads and per-block component dispatch now come from field.blockTypes; the hardcoded id maps and add() branching are gone. BlockElement hosts plugin web components inside blocks via the element control.
Collection config moves from static props to class attributes (#[Label], #[Handle], #[Icon], #[Badge], #[Permission], #[Hidden], #[Order], #[Listing], #[Blueprints]) resolved through a collection schema registry mirroring the node schemas. Navigation stores Collection\Refs resolved without instantiation; Wire creation in the controllers is the only instantiation path. List/row/hierarchy logic is hoisted into Collection\Listing. Plugins can register schema handlers via Registrar::collectionSchema().
Plugins can register template namespaces (addressed as pluginId:path; the cosray dir stays first so un-namespaced layouts keep resolving), panel pages inside the auth/session/renderer pipeline, NavLink nav entries for arbitrary URLs, and extra panel stylesheets/scripts. The fixture plugin now exercises the whole app story end-to-end: chrome-wrapped page, nav link with active state, injected css and path-jailed vendor assets.
The embed island entry is referenced nowhere and is not a Vite input.
Field\Control\Registry maps named rich controls to {tag, module};
Control::resolve() serializes registered names as element descriptors
(recursing into group/repeater), so the island will only interpret
primitives, structural controls and elements. Control::named() lets
fields reference custom registered controls. Plugins register via
Registrar::control() with plugin-prefixed module paths; later
registrations win so a plugin can replace a built-in editor. The
default registry is empty, so payloads are unchanged until controls
are converted stage by stage.
The modal moves from a Svelte context (unreachable outside the component tree) to a $lib/modal store with an openElement() variant that renders plain DOM — the door for web components and the upcoming window.Cosray bridge. All eight consumers migrate to module imports.
The island installs a versioned public runtime for custom-element controls: system info, media upload (wrapping the CSRF-aware request helper), DOM-based modals and toasts. Cosray's own elements and plugin elements use the same API. LocaleTabs and ensureLocales accept explicit locale lists so they keep working when compiled into element bundles that cannot reach the island's system store.
ElementHost is the bare loader/property-assigner/event-listener with
no island store imports; Element.svelte becomes the field wrapper
that owns the locale tabs and feeds the host. The contract grows
node, meta and a live locale property (detail becomes {value, meta?})
— fixing the previously dead locale tabs on element fields. Module
values resolve via lib/elements.ts: cosray:{entry} (reserved id,
panel build or dev server), URLs, or plugin vendor paths. Control
threads an onchange callback so container bundles can supply their
own dirty signal later.
First converted control: cosray-richtext wraps RichTextEditor (compiled as a shadowless custom element via a second Vite pass emitting build/elements/*), owns the per-locale value map and dispatches cosray-change. RichTextEditor loses its island store imports: dirty signal becomes a notify prop, the link modal opens through the window.Cosray bridge. The control registry resolves 'richtext' to the element, so the island no longer maps it; the island RichText control is deleted. The bridge accessor is split from its installer so element bundles stay free of island stores.
cosray-code carries the syntax select and CodeMirror editor,
exercising the meta side of the contract: syntax changes dispatch
cosray-change with {value, meta}. CodeEditor's dirty signal becomes
a notify prop. The registry resolves 'code'; the island Code control
is deleted.
cosray-image/-file/-video share one module wrapping the existing Upload/MediaList machinery. Upload derives its endpoint from the bridge (node prop replaces path), uploads and toasts go through window.Cosray, dialogs and the image edit/preview modals mount via the bridge modal, and the dirty signal is a notify prop. Island media field controls and FileLike are deleted; block media controls pass node + setDirty until Stage C converts them.
Primitive.svelte renders the whole primitive vocabulary (text, textarea, iframe, number, checkbox, option, date, time, datetime, hidden) from descriptors — one component instead of ten. Control dispatches inline; the controls.ts map is gone. Group, Repeater and SubControl thread the onchange callback instead of importing the island dirty store, readying them for container element bundles. node-editor.js drops from 803k to 481k.
Each entry type ships init content built from its fields' structure(); the editor clones it when adding an entry. This deletes the client typeMap — the last place the island named Cosray field classes.
cosray-blocks owns the whole blocks editing surface: the panel and all built-in block controls move into the element bundle, dispatch edits through an instance-scoped notify context, and open the add/ remove modals via the bridge. Plugin block types keep loading as elements through BlockElement inside the bundle. Built-in block controls stay internal Svelte components rather than becoming elements themselves — converting their bind-based editing to the event contract would risk regressions for no isolation gain, since the island already only sees 'element'. The elements build emits a single style.css the loader links once; the island bundle is down to 108k from the original 803k.
cosray-entries hosts the entry list; nested fields render through the generic dispatcher compiled into the bundle, so nested rich fields resolve as nested elements. Entry controls and the remove confirmation use the shared notify channel and the bridge modal. The island dispatcher now knows only primitives, group/repeater and element — zero built-in rich controls.
types/fields.ts loses the per-control field interfaces the island no longer needs; docs describe the final split: primitives interpreted by the island, named rich controls resolved server-side to cosray custom elements, plugins registering their own via Registrar::control().
ernstla
added 3 commits 2026年07月03日 11:01:45 +02:00
Only one island survived the element refactor — the node editor's boot layer — so the islands namespace said nothing anymore. The directory becomes src/editor/ (main.ts entry, mount.ts runtime, Editor.svelte orchestrator, editor.css); the lazy chunk is emitted as main.js. The DOM/SSR contract (data-cosray-node-editor markers, window.CosrayNodeEditor, mountEditor) is unchanged; remaining island wording in comments and docs now says editor.
A boosted swap nests a fresh #main inside the previous one, so the stale outer wrapper keeps the last full page's class and the :has() scroll rules read the wrong page mode. De-nest to a single #main after each swap so client-side navigation matches a full page load.
The editor mounts a Svelte component that registers document-level beforeunload/htmx guards but was never unmounted on a boosted swap, so the component (and its guards, reading the shared dirty store) leaked. Every node left behind another guard, prompting 'unsaved changes' once per leak. Track the mounted instance and unmount it once its host leaves the DOM; let the panel wrapper hand off on hostless pages too so the release runs when navigating from a node to a collection.
ernstla
merged commit d76ccee8a5 into main 2026年07月03日 11:07:20 +02:00
ernstla
deleted branch epic/plugins 2026年07月03日 11:07:20 +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!48
Loading...
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "epic/plugins"
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?