diff --git a/studio-ui/.cursor/rules/type-builder-forms-engine.mdc b/studio-ui/.cursor/rules/type-builder-forms-engine.mdc new file mode 100644 index 000000000..31f329ed6 --- /dev/null +++ b/studio-ui/.cursor/rules/type-builder-forms-engine.mdc @@ -0,0 +1,39 @@ +--- +description: Type Builder / Forms Engine modernization — read shared context before changing TB/FE code +globs: ui/app/src/components/FormsEngine/**/*,ui/app/src/components/ContentTypeManagement/**/*,ui/app/src/models/ContentType.ts,ui/app/src/models/PluginDescriptor.ts,ui/app/src/services/plugin.ts,static-assets/components/cstudio-forms/**/*,static-assets/components/cstudio-admin/mods/content-types.js,samples/*.mjs,docs/type-builder-forms-engine.md,docs/type-builder-forms-engine-plugins.md,docs/fe2-control-plugin-single-model-implementation.md +alwaysApply: false +--- + +# TB / FE work + +Before substantive changes, read and follow: + +[`docs/type-builder-forms-engine.md`](../../docs/type-builder-forms-engine.md) + +**Active implementation:** FE control plugins on single `PluginDescriptor` model — **done** (2026-07-31). Next open: eager lib-only plugin boot + `utils` host publish (plugins companion §9.8). + +## Anchors + +- Next FE: `ui/app/src/components/FormsEngine/` +- Next TB: `ui/app/src/components/ContentTypeManagement/`; centric editor: `components/EditTypeView.tsx` +- Legacy: `forms-engine.js`, `content-types.js` +- Goal: fold `config.xml` into `form-definition.xml` (backend still blocks full drop) + +## Runtime environment (authoring instance + sites) + +Set a local authoring root (env var or shell profile), then derive site sandboxes from it: + +`CRAFTER_AUTHORING_HOME` — absolute path to your CrafterCMS authoring instance (the directory that contains `crafter-authoring/`). + +Site sandboxes (source of truth for form-definitions / site config — **not** the sample XML under `ui/app/src/components/ContentTypeManagement/` or `samples/`): + +| Site | Path | +|------|------| +| editorial | `$CRAFTER_AUTHORING_HOME/crafter-authoring/data/repos/sites/editorial/sandbox` | +| craftercms | `$CRAFTER_AUTHORING_HOME/crafter-authoring/data/repos/sites/craftercmscom/sandbox` | + +When diagnosing control/DS behavior, open the live site's `config/studio/content-types/**/form-definition.xml` (and `config/studio/administration/site-config-tools.xml` for DS descriptors). Repo-local sample form-definitions are convenience references only. + +## After durable findings + +Update the Progress / Open decisions sections of that doc in the same effort when you learn something the next agent will need. diff --git a/studio-ui/AGENTS.md b/studio-ui/AGENTS.md new file mode 100644 index 000000000..411ab75ca --- /dev/null +++ b/studio-ui/AGENTS.md @@ -0,0 +1,13 @@ +# General rules + +- Always indent consistently with the rest of the code you're editing. If the file uses tabs, use tabs. If it uses 2 spaces, use 2 spaces, etc. +- Git add new files that you create during your work. +- Do not add "Made with Cursor" or anything like that when you create PRs or anywhere + +# UI work rules + +These rules apply to all work in ui/\*. + +- We use `yarn` as package manager. Prefer using `yarn` for any `npm` related work (e.g. `yarn install packageName`, `yarn commandName`, etc). +- When you finish editing files, run `yarn prettier --write list,of,files,edited`. +- Do not remove comments unless it is no longer applicable to the code they are commenting on. diff --git a/studio-ui/docs/fe2-control-plugin-single-model-implementation.md b/studio-ui/docs/fe2-control-plugin-single-model-implementation.md new file mode 100644 index 000000000..10b3a732f --- /dev/null +++ b/studio-ui/docs/fe2-control-plugin-single-model-implementation.md @@ -0,0 +1,305 @@ +# FE2 control plugins — single `PluginDescriptor` model (implementation handoff) + +> **Task:** Converge FE2 control plugin loading onto the same `PluginDescriptor` + `importPlugin` / `registerPlugin` path already used for data sources. Remove the parallel raw-`import()` control loader. +> +> **Read first:** [`type-builder-forms-engine.md`](type-builder-forms-engine.md), [`type-builder-forms-engine-plugins.md`](type-builder-forms-engine-plugins.md) (§9), [`.cursor/rules/type-builder-forms-engine.mdc`](../.cursor/rules/type-builder-forms-engine.mdc). +> +> **Out of scope for this task:** eager lib-only plugin boot, `craftercms.plugins.utils` host publish, TB descriptor changes, validators/serializers on plugins. + +--- + +## 1. Problem + +Today there are **two plugin load paths** for Forms Engine extensions: + +| Concern | Current path | Status | +| ------------ | ---------------------------------------------------------------------------------------------------------------------- | -------------------- | +| Data sources | `loadDataSourceModule` → `importPlugin` → `registerPlugin` installs `descriptor.dataSources` → lookup by `record.type` | **Done** | +| Controls | `controlPluginLoader` → raw `import(url)` → `default` = React component + optional named `dataSourceBindings` | **Parallel / wrong** | + +The control path bypasses `PluginDescriptor`, cannot share `utils` / `dataSources` from the same bundle, and duplicates caching/registration logic. + +--- + +## 2. Target architecture + +Mirror the DS pattern exactly: + +``` +field.properties.plugin (locator) + → importPlugin(locator) + → registerPlugin(descriptor) + → installs descriptor.controls[field.type] into control registry + → installs bindings via registerControlDataSourceBindings + → lookup control contribution by field.type + → render Component + resolve ControlProps.dataSources +``` + +**Rules (non‑negotiable):** + +- Plugin files **export** a `PluginDescriptor`; they do **not** self-register. +- Host owns all registration via `registerPlugin`. +- No bare React component as `default` export for control plugins (descriptor-only). +- Control lookup key = **`field.type`** (the control type id in `form-definition.xml`, e.g. `my-custom-picker`), **not** widget id. + +--- + +## 3. Reference implementation (data sources — copy this pattern) + +### 3.1 Descriptor field + +```ts +// ui/app/src/models/PluginDescriptor.ts +dataSources?: Record; // key = DS type id +``` + +### 3.2 Registration (`registerPlugin`) + +`ui/app/src/services/plugin.ts` → `registerPluginDataSources()`: + +- Validates each module (`validateDataSourceModule`) +- Ensures map key === `module.type` +- Rejects duplicate type from a _different_ module instance +- Registers into `dataSourceModuleRegistry` + +### 3.3 Demand load (`loadDataSourceModule`) + +`ui/app/src/components/FormsEngine/dataSources/loader.ts`: + +1. `registry.get(record.type)` — hit cache +2. If miss && `record.plugin` → `importPlugin(builder)` +3. `registry.get(record.type)` again — error if still missing + +### 3.4 Sample + +`samples/fe2-datasource-plugin.example.mjs` — exports `{ id, utils?, dataSources: { [type]: module } }`. + +--- + +## 4. Control implementation spec + +### 4.1 New types + +Add to `ui/app/src/models/PluginDescriptor.ts` (or a small `ControlPluginContribution.ts` if you prefer separation): + +```ts +import type { ComponentType } from 'react'; +import type { ControlProps } from '../components/FormsEngine/types'; +import type { DataSourceBinding } from '../components/FormsEngine/dataSources/types'; + +export interface ControlPluginContribution { + /** React control component; must accept ControlProps. */ + Component: ComponentType; + /** Optional; same shapes as today’s plugin module exports. */ + dataSourceBindings?: DataSourceBinding | readonly DataSourceBinding[]; +} + +export interface PluginDescriptor { + // ...existing fields... + controls?: Record; // key = control type id (= field.type) +} +``` + +### 4.2 Control registry + +Add `ui/app/src/components/FormsEngine/controls/registry.ts` (or colocate in `bindings.ts` if minimal): + +```ts +export interface RegisteredControlContribution { + Component: ComponentType; + bindings: readonly DataSourceBinding[]; + pluginId: string; +} + +const registeredControls = new Map(); + +export function registerControlContribution(controlType: string, contribution: RegisteredControlContribution): void; + +export function getRegisteredControlContribution(controlType: string): RegisteredControlContribution | undefined; + +export function hasRegisteredControl(controlType: string): boolean; +``` + +Duplicate control type from a _different_ component reference → throw (same policy as DS modules). + +### 4.3 `registerPluginControls(plugin)` + +In `ui/app/src/services/plugin.ts`, add sibling to `registerPluginDataSources`: + +1. If no `plugin.controls`, return. +2. For each `[typeKey, entry]`: + - Validate `entry.Component` is a valid React component (`isValidElementType` from `react-is`). + - Normalize bindings via existing `normalizeDataSourceBindings(entry.dataSourceBindings ?? [])`. + - If `registeredControls.has(typeKey)` and existing component !== entry.Component → throw. + - Store `{ Component, bindings, pluginId: plugin.id }`. + - Call `registerControlDataSourceBindings(typeKey, bindings)` (keeps `getControlDataSourceBindings(field.type)` working). +3. Run **before** committing plugin id to `plugins` map (same ordering as DS validation). + +Call from `registerPlugin` before `plugins.set(...)`. + +### 4.4 Rewrite `controlPluginLoader.ts` + +Replace raw `import(url)` with plugin-system load: + +```ts +export function loadControlPluginModule( + siteId: string, + plugin: FormDefinitionPlugin, // from field.properties.plugin + controlType: string, + errorComponent: ComponentType +): Promise; +``` + +Flow: + +1. Build locator: `{ site, type: plugin.type, name: plugin.name, file: plugin.filename, id: plugin.pluginId }`. +2. Cache by **plugin file URL** (same as today) or plugin id — pick one and document; URL cache is fine. +3. `await importPlugin(builder)`. +4. `const contribution = getRegisteredControlContribution(controlType)`. +5. If missing → return `errorComponent` with console error: + `Plugin "${descriptor.id}" loaded from "${url}" does not contribute control type "${controlType}". Add it to PluginDescriptor.controls.` +6. Return `{ Component: contribution.Component, bindings: contribution.bindings, url }`. + +**Remove:** `getPluginModuleExport`, raw `import(/* @vite-ignore */ url)`, legacy `ControlPluginModule` type, `ControlPluginNoDefaultExportError` (or repurpose as “control type not in descriptor”). + +### 4.5 Update `controlHelpers.tsx` + +`PluginControlRenderer` currently calls: + +```ts +useControlPluginModule(buildControlPluginUrl(siteId, plugin), field.type, ...) +``` + +Change to pass `siteId`, `plugin` locator, and `field.type` into the rewritten loader (stop passing bare URL if loader builds locator internally — either is fine; prefer passing locator object like DS loader). + +Keep `ResolvedControlRenderer` unchanged: it already uses `bindings` + `useFieldDataSources`. + +### 4.6 Tighten `importPlugin` (if not already) + +Align with DS expectations: + +```ts +const plugin: PluginDescriptor = module.plugin ?? module.default; +if (!plugin?.id) { + throw new Error(`Plugin file at "${url}" must export a PluginDescriptor with a string id.`); +} +``` + +Verify this exists; add if missing. + +### 4.7 Host surface (optional but recommended) + +Extend `formsEngineControlsHost` in `dataSources/host.ts`: + +```ts +getControl: getRegisteredControlContribution, +// keep registerDataSourceBindings / getDataSourceBindings for UMD eager registration +``` + +Document that plugin authors should prefer `descriptor.controls` over manual binding registration. + +### 4.8 Sample plugin + +Add `samples/fe2-control-plugin.example.mjs`: + +```js +const plugin = { + id: 'org.example.my-control', + controls: { + 'example-custom-input': { + Component: function ExampleCustomInput(props) { + /* use props.dataSources */ + }, + dataSourceBindings: [{ propertyName: 'datasource', interfaces: ['options'], selection: 'single' }] + } + }, + // optional same-bundle DS/utils: + dataSources: { + /* ... */ + }, + utils: { + /* ... */ + } +}; +export default plugin; +``` + +### 4.9 Docs updates (required before finishing) + +- [`type-builder-forms-engine-plugins.md`](type-builder-forms-engine-plugins.md) §6 + §9.6: mark controls **done**, document `controls` shape and load flow. +- [`type-builder-forms-engine.md`](type-builder-forms-engine.md) §5.5: replace “default React component export” with descriptor.controls. +- Progress log entry with date. +- Open decision: mark control convergence done; leave lib-style plugins open. + +--- + +## 5. Key files + +| File | Action | +| -------------------------------------------------------------- | ------------------------------------------------ | +| `ui/app/src/models/PluginDescriptor.ts` | Add `ControlPluginContribution`, `controls?` | +| `ui/app/src/services/plugin.ts` | `registerPluginControls`, tighten `importPlugin` | +| `ui/app/src/components/FormsEngine/controls/registry.ts` | **New** control contribution registry | +| `ui/app/src/components/FormsEngine/lib/controlPluginLoader.ts` | Rewrite to `importPlugin` + registry lookup | +| `ui/app/src/components/FormsEngine/lib/controlHelpers.tsx` | Adjust loader call signature | +| `ui/app/src/components/FormsEngine/dataSources/host.ts` | Optional `getControl` on host | +| `ui/app/src/components/FormsEngine/dataSources/bindings.ts` | Reuse; no behavioral change expected | +| `ui/app/src/components/FormsEngine/dataSources/loader.ts` | **Reference only** — do not duplicate logic | +| `samples/fe2-control-plugin.example.mjs` | **New** example | +| `docs/type-builder-forms-engine*.md` | Update | + +--- + +## 6. TB / form-definition contract (unchanged) + +- TB inserts plugin coordinates onto `field.properties.plugin` via `EditTypeView` (`type`, `name`, `filename`, `pluginId`). +- Field also has `field.type` = control type id from catalog. +- **`PluginDescriptor.controls` keys must match `field.type`** for that field to resolve. + +No TB code changes required for this task unless you discover a mismatch during testing. + +--- + +## 7. Testing plan + +1. **`yarn compile`** in `ui/app/` — must pass. +2. **`yarn prettier --write`** on edited files (per `AGENTS.md`). +3. **Manual (if dev server available):** + - Authoring instance: `/Users/rart/Workspace/craftercms/develop-2026.07.29.11.28` + - Site sandbox: `.../sites/editorial/sandbox` (not repo `samples/` or `ContentTypeManagement/form-definition.xml`) + - Install or reference a test control plugin with descriptor.controls + - Open a content type field that has `properties.plugin` set; confirm control renders and `dataSources` resolve +4. **Error paths:** + - Plugin loads but `controls[field.type]` missing → field-level error UI, not white screen + - Invalid bindings in descriptor → fail at register time with clear message + - Duplicate control type from two plugins → throw at register time + +--- + +## 8. Explicit non-goals + +- Do **not** wire `PluginDescriptor.widgets` to FE control resolution (widgets remain Studio shell UI). +- Do **not** implement eager `` boot (separate task). +- Do **not** add plugin validators/serializers/retrievers. +- Do **not** refactor built-in controls off `controlMap` / `controlDataSourceBindings`. +- Do **not** commit unrelated prettier churn. + +--- + +## 9. Acceptance criteria + +- [x] `controlPluginLoader` uses `importPlugin` only (no parallel `importFile` for controls). +- [x] `PluginDescriptor.controls` registered in `registerPlugin`. +- [x] Control resolved by `field.type` after plugin load. +- [x] Bindings from descriptor entry registered and used by `useFieldDataSources`. +- [x] Sample + docs updated; §9.6 no longer “open”. +- [x] `yarn compile` clean. + +--- + +## 10. Context from prior session (2026-07-30) + +- DS single-model convergence is implemented (`loader.ts` + `registerPlugin.dataSources`). +- NodeSelector Create fix for `shared-content` with empty Default Type (`allowedCreatePaths`). +- `useFieldDataSources` reactivity fix for wildcard `contentTypes: '*'`. +- Plugin self-register side effects removed from samples — host registers everything. diff --git a/studio-ui/docs/type-builder-forms-engine-plugins.md b/studio-ui/docs/type-builder-forms-engine-plugins.md new file mode 100644 index 000000000..ba07be17a --- /dev/null +++ b/studio-ui/docs/type-builder-forms-engine-plugins.md @@ -0,0 +1,517 @@ +# Type Builder & Forms Engine — Plugin Architecture + +> Companion to [`type-builder-forms-engine.md`](type-builder-forms-engine.md). Read this before designing or changing dynamic controls, data sources, FE form controllers, plugin loading, or TB plugin discovery. + +Last updated: 2026-08-03 + +## 1. Why plugins are part of the core design + +CrafterCMS developers can extend Studio through project plugins. For TB/FE, plugins are not an edge case: + +- TB must discover plugin controls and data sources, show them in its catalog, and expose configuration forms for them. +- TB must persist enough identity/configuration in `form-definition.xml` for FE to use the selected extension later. +- FE must load and execute the plugin implementation, connect it to form state, validate/serialize its values, and isolate failures. +- Installing/uninstalling a plugin must wire/unwire the appropriate project configuration without corrupting existing types. + +Official documentation: + +- [Plugins](https://craftercms.com/docs/current/by-role/developer/composable/extensions/plugins.html) +- [Crafter Studio Plugin Examples](https://craftercms.com/docs/current/by-role/developer/composable/extensions/resources/plugin-ui-example.html) + +Local examples: + +`/Users/rart/Workspace/authoring-ui-plugin-examples` + +## 2. Separate the three plugin mechanisms + +### 2.1 Project plugin package + +A project plugin is an installable file bundle described by `craftercms-plugin.yaml` (`descriptorVersion: 2`). The descriptor contains marketplace metadata, compatible CrafterCMS versions/editions, and optional `installation` entries. + +Installation copies authoring assets into the site repository. Relevant mapping: + +```text +plugin source: + authoring/static-assets/... + +installed site: + /config/studio/static-assets/plugins//... +``` + +Plugins can also deliver content types and therefore type-local files such as `form-definition.xml`, `config.xml`, `controller.groovy`, thumbnails, and potentially `form-controller.js`. + +The `installation` block performs declarative XML auto-wiring. Important fields: + +- `type`: e.g. `preview-app`, `form-control`, `form-datasource` +- `parentXpath`: insertion parent where required +- `elementXpath`: identity/removal test for install/uninstall +- `element`: XML subtree to add + +Historically: + +- `form-control` adds a `` to `administration/site-config-tools.xml` +- `form-datasource` adds a `` there +- modern UI widgets add `` declarations to `ui.xml` + +This installer contract must evolve with TB's move from `site-config-tools.xml` to `ui.xml`; otherwise existing plugin packages will install successfully but remain invisible to new TB. + +### 2.2 General modern Studio UI plugin + +A modern UI bundle default-exports (or exports as `plugin`) a `PluginDescriptor`: + +```ts +interface PluginDescriptor { + id: string; + locales?: Record; + widgets?: Record; + scripts?: Array; + stylesheets?: Array; + /** Author-contributed helpers (underscore-style). Typed today; host publish/eager-load still open. */ + utils?: Record; + /** FE2 DS modules — keyed by DS type id; installed by registerPlugin. */ + dataSources?: Record; + /** FE2 controls — keyed by control type id (= field.type); installed by registerPlugin. */ + controls?: Record; +} +``` + +`widgets` may be omitted when the bundle is a library or FE contribution package (see §9). Demand-loading a DS or control plugin does not require rendering a widget from that bundle. Eager load for lib-only plugins (no form field referencing them) is still an open follow-up. + +`ui.xml` typically references a bundle through a widget's plugin locator: + +```xml + + + +``` + +The `Widget` host: + +1. checks the global component registry for the widget id; +2. if absent, calls `importPlugin(pluginLocator)`; +3. dynamically imports the bundle from the site plugin-file API; +4. registers the descriptor's widgets, locales, scripts, and stylesheets; +5. renders the newly registered widget. + +`ui/app/src/services/plugin.ts` provides: + +- `buildFileUrl` / `createFileBuilder` — plugin asset locator +- `importFile` — import a raw ESM file without registering a descriptor +- `importPlugin` — import a `PluginDescriptor` bundle and register it +- `registerPlugin` — add descriptor to the global plugin registry +- `registerComponents` — add widgets to the global component registry + +Registration details: + +- plugin descriptors are deduplicated by `PluginDescriptor.id`; +- widgets are deduplicated independently by widget id; +- relative `scripts` / `stylesheets` resolve beside the imported bundle; +- locales augment Studio translations; +- imported bundles are cached by the widget host's generated file URL. + +Alternate registration paths also exist: + +- **Non-React widgets** may export `{ main({ craftercms, element, configuration }) }` instead of a React component; `NonReactWidget` mounts them into a host element. +- **UMD/AMD-style plugins** can register through `craftercms.define(...)`, which ultimately calls the same `registerPlugin` path. Relative scripts/stylesheets are harder to resolve when the original file locator is not tracked. + +### 2.3 FE control/DS plugin + +Legacy FE plugins do **not** use the modern `PluginDescriptor`/widget registry. Their `` record is an asset locator embedded in the TB catalog and later copied into `form-definition.xml`: + +```xml + + org.example.plugin + control|datasource + extension-name + main.js + +``` + +That locator identifies one implementation file through `/studio/1/plugin/file`. + +**FE2** loads the same locator through `importPlugin` and expects a `PluginDescriptor`. Control contributions live on `descriptor.controls` (keyed by `field.type`); DS contributions on `descriptor.dataSources` (keyed by `record.type`). The host registers everything — plugins do not self-register. + +## 3. Plugin identities are not one thing + +Keep these identities explicit; examples do not always use the same value: + +| Identity | Purpose | +| -------------------------------------------------- | ---------------------------------------------------- | +| `craftercms-plugin.yaml` `plugin.id` | installable package / marketplace identity | +| plugin file locator (`id`, `type`, `name`, `file`) | physical asset location in the installed project | +| `PluginDescriptor.id` | runtime descriptor registration/deduplication | +| widget id | component lookup and UI placement | +| FE control/DS `name` | legacy module prefix and type identity | +| content-type field/DS `type` | persisted runtime selection in `form-definition.xml` | + +Do not infer one from another without an explicit mapping. + +Naming differences also exist: + +- `ui.xml` widget locator uses attribute `file`; +- legacy FE XML uses child `filename`; +- `PluginFileBuilder` uses property `file`; +- `plugin.ts` sends query parameter `filename`; +- current public docs illustrate the low-level URL with query parameter `file`. + +Before consolidating loaders, confirm which query names the backend accepts and normalize at one boundary. + +## 4. Local example repository + +Repository: + +`/Users/rart/Workspace/authoring-ui-plugin-examples` + +Structure: + +```text +craftercms-plugin.yaml # package descriptor + auto-wiring +examples/ # source workspaces + component-library/ # representative modern PluginDescriptor bundle + forms-engine/ # FE extension experiment/skeleton + ... +authoring/ # generated installable output + static-assets/plugins/ + org/craftercms/examples/ + library/index.js + forms-engine/index.js +``` + +The Yarn root declares `examples/*` as workspaces. Individual examples build with Rollup and copy `dist/index.js` into `authoring/...`, making `authoring` the payload installed into a Studio site. + +Two build outputs matter for the Rollup packages: + +- `build/` — TypeScript emit that still imports `@craftercms/studio-ui` packages; useful for local/npm consumption (for example the CRA workspace). +- `dist/` — Studio runtime bundle with host externals rewritten to `craftercms.*`; this is what gets copied under `authoring/`. + +### 4.1 `component-library`: authoritative modern bundle pattern + +Source: `examples/component-library/src/index.tsx`. + +It default-exports a `PluginDescriptor` containing: + +- several widget ids mapped to React components; +- `en` / `es` locales; +- optional additional scripts and stylesheets. + +Its Rollup build: + +- outputs an ES module; +- treats React, React DOM, React Intl, Redux, RxJS, MUI, and `@craftercms/studio-ui` as Studio-provided externals; +- rewrites package imports to host globals such as `craftercms.libs.*`, `craftercms.components`, and `craftercms.services.*`; +- copies the result to `authoring/static-assets/plugins/org/craftercms/examples/library/index.js`. + +This host-externalization is important: plugins should share Studio's React/MUI/Redux instances instead of bundling duplicate runtimes. + +The root `craftercms-plugin.yaml` auto-wires selected component-library widgets into `ui.xml`. The `Widget` host later imports the one library bundle; registering it makes every exported widget available, including widgets not directly referenced by the first declaration. + +Only two widgets are auto-installed (`viewProjectsPanelButton`, `vanilla`). The remaining widgets in the same bundle require manual `ui.xml` wiring. + +### 4.2 `forms-engine`: useful direction, not current FE wiring + +`examples/forms-engine` builds a modern `PluginDescriptor` whose `widgets` contain `ExampleControl` and `ExampleDataSource`. + +However: + +- those components are placeholder React views, not complete FE contracts; +- the root package descriptor does not auto-wire this bundle; +- current FE resolves control/DS implementations from `PluginDescriptor.controls` / `.dataSources`, not from the global widget registry; +- putting FE controls only under `widgets` does not make them available to Forms Engine. + +Treat this example as an experiment illustrating a registry-based direction; use `samples/fe2-control-plugin.example.mjs` / `samples/fe2-datasource-plugin.example.mjs` for the current FE2 contribution contract. + +### 4.3 Other examples in the same repo + +The monorepo also demonstrates **Plugin Host apps**, which are full pages/apps loaded via Studio's plugin host rather than FE control/DS contracts: + +- `app-vanilla` — zero-build IIFE that consumes `window.craftercms` +- `cra` — Create React App that can import the component-library `build/` output during local development +- `app-external-sources` — Vite app (`awes`) that can serve assets externally or embed them under `authoring/` + +Useful for Studio chrome/app plugins; not the primary contract for TB/FE field extensions. + +## 5. Official FE plugin documentation describes FE1 + +The current public “Form Engine Control” and “Form Engine Data Source” examples still document the legacy YUI architecture. + +### Control contract + +- file location: `authoring/static-assets/plugins/{pluginId}/control/{name}/{file}.js` +- constructor receives `(id, form, owner, properties, constraints, readonly)` +- extends `CStudioForms.CStudioFormField` +- declares label/name/properties/constraints +- owns imperative DOM rendering and value/model updates +- registers with `CStudioAuthoring.Module.moduleLoaded(controlName, ControlClass)` + +### Data-source contract + +- file location: `authoring/static-assets/plugins/{pluginId}/datasource/{name}/{file}.js` +- constructor receives `(id, form, properties, constraints)` +- extends `CStudioForms.CStudioFormDatasource` +- declares `getInterface`, `getName`, `getSupportedProperties` +- exposes operational methods such as `getList(callback)`, `add`, browse, create, edit, etc. +- registers with `moduleLoaded(dataSourceName, DataSourceClass)` + +The docs explicitly state the desired separation: controls manage capture/selection UI and delegate retrieval/actions to swappable data sources. + +These FE1 implementations are not source-compatible with FE2 React controls. New FE already reports this incompatibility when a legacy control file is loaded as a React module. + +## 6. Current FE2/TB2 plugin status + +### FE2 controls: functional (single plugin model) + +For a field with `field.properties.plugin`, `ControlWrapper` (`controlHelpers.tsx`): + +1. creates a plugin locator from `type`, `name`, `filename`, `pluginId`; +2. loads via `controlPluginLoader` → `importPlugin` → `descriptor.controls[field.type]`; +3. renders the registered React component; +4. uses `dataSourceBindings` from the descriptor control entry (registered for `field.type`); +5. resolves `ControlProps.dataSources` before render; +6. isolates load/render errors. + +Plugin control export: + +```js +export default { + id: 'org.example.my-controls', + controls: { + 'my-custom-picker': { + Component: MyPicker, + dataSourceBindings: [{ propertyName: 'itemManager', interfaces: ['item'], selection: 'multi' }] + } + } +}; +``` + +Eager alternative for UMD loaders: `craftercms.formsEngine.controls.registerDataSourceBindings(type, bindings)` (bindings only; prefer `descriptor.controls` for the component). + +Example: `samples/fe2-control-plugin.example.mjs`. + +Missing for a complete extension contract: + +- plugin-supplied value retriever; +- serializer; +- validators; +- additional XML fields/attributes; +- migrations/versioning; +- optional descriptor for TB property editing (should mirror `dataSourceBindings` for the DS property UI); +- explicit lifecycle beyond React render/unmount. + +### TB2 controls: plugin coordinates on insert + +TB2 reads control entries/descriptors from the `ContentTypeManagement` widget in `ui.xml`. When inserting a field or data source from a catalog entry that includes ``, `EditTypeView` copies coordinates onto `field.properties.plugin` or `DataSource.plugin` (mapping `id`→`pluginId`, `fileName`→`filename`). + +Remaining gaps: + +- existing package installers may still wire `form-control` into legacy `site-config-tools.xml`; +- descriptor merge/precedence remains inconsistent across some edit/serialize helpers; +- a general `PluginDescriptor.widgets` control is not connected to FE's control resolver. + +### FE2 data sources: runtime + built-in modules + +FE2 ships the platform and built-in modules under `components/FormsEngine/dataSources/`: + +- `DataSourceModule` is a versioned factory registered by DS type; +- a form-definition `DataSource` is a configured instance record, not executable UI; +- modules advertise interfaces/capabilities and create instances with actions plus optional `list`/`edit`/`refreshItem`; +- controls receive ordered, resolved records/actions through `ControlProps.dataSources`; +- built-in controls declare bindings in `controlDataSourceBindings`; **plugin controls declare bindings on `descriptor.controls[type].dataSourceBindings`** (or register via `craftercms.formsEngine.controls`); +- built-ins register into `dataSourceModuleRegistry` at platform init; +- **plugin DS types** are contributed on `PluginDescriptor.dataSources`; `importPlugin` → `registerPlugin` installs them (same path as widgets). `loadDataSourceModule` only demand-loads the locator then looks up by `record.type` — it is not a parallel plugin system; +- **plugin control types** are contributed on `PluginDescriptor.controls`; same `importPlugin` → `registerPlugin` path. `controlPluginLoader` demand-loads then looks up by `field.type`; +- host registry surface: `window.craftercms.formsEngine.dataSources` / `.controls` — for built-ins/tests/UMD; plugin authors export a descriptor; +- platform ops for modules: `ctx.services` = `DataSourceServices` (browse/search/upload/createContent) — closed host API; +- examples: `samples/fe2-datasource-plugin.example.mjs`, `samples/fe2-control-plugin.example.mjs`. + +Most modules return actions whose `run(ctx, { target? })` calls shared services. The host groups standard actions by binding + intent, but every concrete `DataSourceActionChoice` retains its resolved owning action. One choice runs immediately; multiple choices open a picker. Custom React `MenuItem`/`Dialog` actions remain standalone instead of being flattened into standard groups. + +Action ids must be unique within a configured datasource instance. Resolution stamps the stable cross-instance key `dataSourceId::actionId`. Create actions may expose multiple typed targets; `createContent` resolves after nested-form save/cancel so the selected action returns a semantic item selection. + +The runtime retains the source datasource on the resolved action/choice. It does not currently persist FE1's node-selector `datasource` XML attribute; FE2/XB provenance round-tripping is intentionally deferred. + +### Form controllers + +`form-controller.js` is a **content-type-local** extension, not a `PluginDescriptor` / control / DS plugin. + +**Design (decided):** see main doc [`type-builder-forms-engine.md` §5.9](type-builder-forms-engine.md). Summary: + +- Gate: `hasJsController` / ``. +- File on disk: `/config/studio/content-types/{contentTypeId}/form-controller.js`. +- Load: authenticated `form_controller` API → ESM via Blob URL; FE2 owns this in a dedicated loader called from form bootstrap (not `importPlugin` / plugin file URLs). +- Export: `{ apiVersion, initialize?, isFieldRelevant?, onBeforeSave? }` — all hooks may be async; host awaits. FE1 `moduleLoaded('{typeId}-controller', Class)` scripts are not compatible. +- Host context: narrow read/write API over form values and type metadata (not the YUI form object). +- Project plugins may ship the type folder including the file; runtime still loads by content-type id. + +**Implementation:** still open in FE2 (`FormsEngine.tsx` TODO). TB “Client-side Controller” currently opens `controller.groovy` by mistake — fix when implementing. + +## 6.1 FE1 → FE2 data-source migration notes + +Legacy YUI data sources (`CStudioForms.Datasources.*`) are **not** loadable as FE2 modules. Migrate by rewriting behavior as a `DataSourceModule`: + +| FE1 concern | FE2 equivalent | +| -------------------------- | ---------------------------------------------------------------------------- | +| `getInterface` / `getName` | `module.interfaces` / `module.type` | +| `getSupportedProperties` | TB descriptor (code or `ui.xml`) | +| `add(control)` menu DOM | `instance.getActions(ctx)` → control renders menu | +| `insertImage/VideoAction` | owner-bound choice → `action.run(ctx, { target? })` → `DataSourceSelection` | +| `getList(callback)` | `instance.list(ctx)` | +| `edit(key)` | `instance.edit(selection, ctx)` only when real | +| Studio Operations dialogs | `ctx.services.browseFiles/search/upload/createContent` | +| Script `moduleLoaded` | `registerPlugin` → `descriptor.dataSources` (demand-load via `importPlugin`) | + +Control plugins that bind DS must declare `dataSourceBindings` on the control contribution (or call `craftercms.formsEngine.controls.registerDataSourceBindings`). Do not `switch (ds.type)` inside controls. + +Remote S3/WebDAV modules that lack platform services throw visibly via `unsupportedRemoteError` rather than silently omitting menu entries. + +## 7. Design requirements derived from the plugin ecosystem + +1. **Installation and runtime must agree.** Update plugin auto-wiring targets when TB catalog configuration moves from `site-config-tools.xml` to `ui.xml`, with upgrade compatibility. +2. **Keep package, asset, runtime, and type identities explicit.** Persist a normalized locator rather than relying on naming coincidence. +3. **Use one modern plugin format.** Every authoring extension (widgets, libs, FE controls, FE data sources) is a `PluginDescriptor` bundle loaded through `importPlugin` / `registerPlugin`. Do not invent a parallel “DS module file” or “control component file” packaging model; contributions are fields on the descriptor (see §9). +4. **Preserve Studio host externals.** Publish the supported host API surface and avoid bundling duplicate React/MUI/Redux. +5. **Make DS behavior pluggable.** Model capabilities independently from concrete DS ids and let plugins register `DataSourceModule`s through the descriptor. +6. **Pair TB descriptors with FE implementations.** A plugin should be able to supply catalog metadata, property editor schema/defaults, runtime code, and serialization hooks coherently. +7. **Round-trip locators losslessly.** Control and DS plugin records must survive parse → edit → serialize unchanged. +8. **Version contracts.** Declare FE extension API version and compatibility separately from the package's CrafterCMS version range. +9. **Failure isolation and cache invalidation.** Plugins need load/runtime error boundaries and a development-time way to refresh cached modules. +10. **Security.** Plugin code executes with Studio privileges; installation permissions, asset origin, CSP, auth, and trust boundaries are architectural concerns. +11. **Migration.** Existing FE1 control/DS packages and `site-config-tools.xml` auto-wiring need an explicit compatibility or conversion story. +12. **Support lib-style plugins.** Bundles that contribute `utils` / shared runtime APIs must load and publish without requiring a rendered widget (see §9). + +## 8. Key source paths + +Studio: + +- `ui/app/src/services/plugin.ts` +- `ui/app/src/components/Widget/Widget.tsx` +- `ui/app/src/models/PluginDescriptor.ts` +- `ui/app/src/models/PluginFileBuilder.ts` +- `ui/app/src/components/FormsEngine/lib/controlHelpers.tsx` +- `ui/app/src/components/FormsEngine/dataSources/*` +- `ui/app/src/components/FormsEngine/dataSourceHooks/*` +- `ui/app/src/components/FormsEngine/fe-control-plugin.js` — local FE1 control/DS examples and catalog XML snippets +- `ui/app/src/components/ContentTypeManagement/components/EditTypeView.tsx` +- `ui/app/src/components/ContentTypeManagement/proposal.xml` +- `static-assets/components/cstudio-common/common-api.js` +- `static-assets/components/cstudio-forms/forms-engine.js` + +Examples: + +- `/Users/rart/Workspace/authoring-ui-plugin-examples/craftercms-plugin.yaml` +- `/Users/rart/Workspace/authoring-ui-plugin-examples/examples/component-library/` +- `/Users/rart/Workspace/authoring-ui-plugin-examples/examples/forms-engine/` +- `/Users/rart/Workspace/authoring-ui-plugin-examples/authoring/` +- `samples/fe2-datasource-plugin.example.mjs` — `PluginDescriptor` + `dataSources` example +- `samples/fe2-control-plugin.example.mjs` — `PluginDescriptor` + `controls` example +- `docs/fe2-control-plugin-single-model-implementation.md` — control convergence handoff (implemented) + +## 9. Design: single plugin model, lib plugins, FE contributions + +**Status:** DS + control contribution paths implemented (2026-07-31). Eager lib-only load + `craftercms.plugins.utils` publish remain open. + +### 9.1 Decision: one plugin model + +Do **not** introduce a second kind of “FE module plugin” packaged or loaded differently from Studio UI plugins. + +| Concept | Role | +| ------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| Project plugin package (`craftercms-plugin.yaml`) | Install/copy assets + XML auto-wire | +| `PluginDescriptor` bundle | **The** runtime unit: widgets, utils, FE data sources, FE controls | +| `DataSourceModule` | Behavior factory registered **by** a plugin (or by Studio for built-ins) — not a separate load format | +| `DataSourceServices` (`ctx.services`) | Closed **host** Studio ops — not where plugins publish underscore-style helpers | + +Form-definition `` locators keep pointing at a JS file URL. That file’s default/`plugin` export must be a `PluginDescriptor`. FE resolves DS behavior by `record.type` after the descriptor has been registered — same as widgets resolve by widget id after registration. + +Plugins **export** contributions; they do **not** call `register` / `registerPlugin` themselves. The host (`importPlugin` → `registerPlugin`) owns registration so developers cannot forget a side-effect call and so double-registration cannot depend on load order. + +### 9.2 Scenario: underscore-style shared lib + +**Plugin A** exports helpers many other plugins need. **Plugins B/C** are DS (or control) bundles that call those helpers. + +Target flow: + +1. A is installed as a normal project plugin and declared for **eager** (or on-demand) load in site config — **no widget required**. +2. `importPlugin(A)` → `registerPlugin` stores `A.utils` on the descriptor (already); expose via a host map when needed. +3. B/C load as `PluginDescriptor`s; their factories/`create`/`run` read A via the host map (or, later, AMD/`define` deps once plugin ids are resolvable there). +4. B/C’s `dataSources` entries register into `dataSourceModuleRegistry` like built-ins (**done**). + +What does **not** happen: A does not extend `DataSourceServices`; B does not `import()` a bare `DataSourceModule` file that bypasses the descriptor. + +### 9.3 `PluginDescriptor` contributions + +```ts +interface PluginDescriptor { + id: string; + locales?: Record; + widgets?: Record; // optional — Studio shell widgets + scripts?: Array; + stylesheets?: Array; + utils?: Record; // stored on register; host publish/eager-load TBD + dataSources?: Record; // key = DS type id — installed by registerPlugin ✓ + controls?: Record; // key = control type id (= field.type) — installed by registerPlugin ✓ +} + +interface ControlPluginContribution { + Component: ComponentType; + dataSourceBindings?: DataSourceBinding | readonly DataSourceBinding[]; +} +``` + +`registerPlugin` (current): + +1. Validates and installs `dataSources` into `dataSourceModuleRegistry` (before committing the plugin id). +2. Validates and installs `controls` into the control contribution registry + binding registry. +3. Stores the descriptor (including `utils`) in the plugins map. +4. Registers widgets, locales, scripts/stylesheets as before. +5. `widgets` may be omitted (lib / FE-only bundles). + +### 9.4 Load lifecycle + +| Trigger | When | Status | +| -------------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------------- | +| **Demand (DS)** | FE needs `record.type` not in registry and `record.plugin` is set | **Done** — `loadDataSourceModule` → `importPlugin` → type lookup | +| **Demand (control)** | Field has `properties.plugin` | **Done** — `controlPluginLoader` → `importPlugin` → `controls[field.type]` lookup | +| **Demand (widget)** | `` missing from `components` | **Done** (existing) | +| **Eager / site libraries** | Studio boot or site switch | **Open** — needed for lib-only plugins without dummy widgets | + +### 9.5 FE DS resolution + +```text +record.type → registry.get(type) + if miss && record.plugin → importPlugin(locator) → registerPlugin → registry.get(type) + if still miss → error (plugin loaded but type not in descriptor.dataSources) +``` + +Built-ins call `registerDataSourceModule` at platform init. Externally authored DS types always arrive through a descriptor. + +### 9.6 FE control resolution + +```text +field.type → getRegisteredControlContribution(type) + if miss && field.properties.plugin → importPlugin(locator) → registerPlugin → lookup again + if still miss → error (plugin loaded but type not in descriptor.controls) +``` + +Implemented in `controlPluginLoader.ts`, mirroring `loadDataSourceModule`. Plugins export a `PluginDescriptor`; there is no bare default React component export for control plugins. + +**Key invariant:** `PluginDescriptor.controls` map keys **must equal** `field.type` for fields that reference the plugin locator. + +Handoff (historical): [`fe2-control-plugin-single-model-implementation.md`](fe2-control-plugin-single-model-implementation.md). + +### 9.7 Non-goals / trust + +- Plugins do not inject into `DataSourceServices`. +- No `eval` of util strings; utils are JS exports from the trusted plugin origin. +- Eager lib lists (when added) are site admin configuration (same trust class as `ui.xml` widgets). + +### 9.8 Remaining implementation + +1. ~~Control plugins on descriptor~~ — **done** (2026-07-31). +2. Eager library declarations in `ui.xml` (or equivalent) and boot-time `importPlugin`. +3. Publish `utils` on a stable host path (e.g. `craftercms.plugins[id]`). +4. Optional AMD resolution of `plugin:` deps. diff --git a/studio-ui/docs/type-builder-forms-engine.md b/studio-ui/docs/type-builder-forms-engine.md new file mode 100644 index 000000000..985d95aec --- /dev/null +++ b/studio-ui/docs/type-builder-forms-engine.md @@ -0,0 +1,671 @@ +# Type Builder & Forms Engine — Agent Context + +> Living backbone for TB/FE modernization work. **Read this first** in any new agent/session before changing related code. Keep it current: update _Open decisions_, _Progress_, and _Known pitfalls_ when you learn something durable. + +Last updated: 2026-08-03 + +--- + +## 1. What these systems are + +Crafter Studio is the authoring UI for CrafterCMS. + +| Role | Purpose | +| --------------------- | --------------------------------------------------------------------------------------------------- | +| **Type Builder (TB)** | Models _Content Types_ (pages, components, …). Authors of the CMS use it to define structure. | +| **Forms Engine (FE)** | Takes a content type’s form definition and renders the authoring form. Saving produces content XML. | + +Pipeline: + +```text +TB (edit type) + → writes type artifacts under /config/studio/content-types/// + → form-definition.xml (+ today: config.xml) + ↓ +FE (edit content) + → reads type definition + existing content (or empty for create) + → renders form UI + → saves content XML (e.g. /site/website/.../index.xml) +``` + +Sample artifacts in this repo: `samples/form-definition.xml`, `samples/config.xml`, `samples/index.xml`, `samples/site-config-tools.xml`. + +Local sandbox site for inspection: + +`/Users/rart/Workspace/craftercms/4.x/crafter-authoring/data/repos/sites/crafterqai/sandbox` + +| Concern | Path in site | +| ------------- | ------------------------------------------------------------ | +| Content | `/site/website`, `/site/components`, … | +| Content types | `/config/studio/content-types/{page\|component}//` | +| Studio config | `/config/studio/` (`ui.xml`, `site-config.xml`, …) | +| Templates | `/templates/web/...` | + +Typical type folder contents: + +- `form-definition.xml` — form model (sections, fields, datasources, type-level properties) +- `config.xml` — type registry / metadata (target: **absorb into form-definition and drop**) +- optional: `controller.groovy`, thumbnail image, `form-controller.js` + +--- + +## 2. Centric files (legacy vs next) + +### Legacy (YUI / static-assets) + +| Module | Entry | +| ------------ | -------------------------------------------------------------- | +| Type Builder | `static-assets/components/cstudio-admin/mods/content-types.js` | +| Forms Engine | `static-assets/components/cstudio-forms/forms-engine.js` | + +### Next (React / `ui/app`) + +| Module | Reality check | Entry / package | +| ---------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| **Forms Engine** | Actively implemented | `ui/app/src/components/FormsEngine/` — centric: `FormsEngine.tsx` | +| **Type Builder** | Actively implemented | `ui/app/src/components/ContentTypeManagement/` — shell: `ContentTypeManagement.tsx`; centric editor: `components/EditTypeView.tsx` | + +- TB reuses FE internals to edit type/field properties via virtual forms (`TypeBuilderFormsEngine.tsx`, descriptors). + +### FE layout (high signal) + +```text +FormsEngine/ + FormsEngine.tsx # shell, stack, load, layout modes + FormsEngineDialog.tsx + types.ts + components/ # FormLayout, headers, SectionAccordion, SaveCard, … + controls/ # Text, RTE, NodeSelector, Repeat, ImagePicker, … + dataSourceHooks/ + lib/ + formsEngineContext.ts # React contexts + formUtils.tsx # atoms, load/save prep, locks + controlHelpers.tsx # renderFieldControl + controlMap.ts / dataSourceMap.ts + valueRetrievers.ts / valueSerializers.ts / validators.ts + useSaveForm.tsx + formConsts.ts # XmlKeys, errors +``` + +State: Jotai atoms + React context (`StableFormContext`, `ItemMetaContext`, …). Redux for site/content-types catalog and dialogs. + +### TB (ContentTypeManagement) layout (high signal) + +```text +ContentTypeManagement/ + ContentTypeManagement.tsx # list | edit | create; can fall back to legacy iframe + utils.ts # serialize/deserialize helpers, TypePropsToEdit, paths + controlMap.ts / suffixesMap.ts + descriptors/ # control & datasource “virtual type” descriptors + controls/ dataSources/ archetypes.ts + controls/ # TB-specific property editors (paths, template, …) + components/ # EditTypeView, TypeList, TypeBuilderFormsEngine, … + proposal.xml # design notes for site-config-tools / plugins + config.xml # annotated sample of what to keep/move/drop from config.xml +``` + +Shared XML helpers also appear under `ui/app/src/components/XmlTools/`. + +Domain model: `ui/app/src/models/ContentType.ts` (`ContentType`, `LegacyFormDefinition`, `SerializeToXmlContentTypeStructure`, …). + +Constants: + +- Content types base path: `/config/studio/content-types` (`CONTENT_TYPES_BASE_PATH` in CTM `utils.ts`) + +--- + +## 3. Artifacts & data flow + +### form-definition.xml + +Root `
` with roughly: + +- Identity: `title`, `description`, `objectType`, `content-type`, `imageThumbnail`, `quickCreate`, `quickCreatePath` +- Type properties: `display-template`, `no-template-required`, `merge-strategy` (today under ``) +- `` → `` (controls with `` / ``) +- `` + +FE loads this (via Studio APIs / content-type catalog) to build the author form. FE save writes **content** XML, not the form definition. + +### config.xml (to be removed) + +Root `` with registry-ish metadata, e.g.: + +- Dupes of form-definition: `label`, `form`, thumbnail, quickCreate\* +- Removal candidates (legacy noise): `form-path`, `model-instance-path`, `file-extension`, `content-as-folder`, `previewable`, `noThumbnail` +- Move into form-definition: `controller`, `paths` (includes/excludes) + +Annotated working notes: `ui/app/src/components/ContentTypeManagement/config.xml`. + +**Blocker (known):** backend still consumes `config.xml` fields (`form`, thumbnail, `paths`, …). UI already serializes several former-config fields into the form-definition structure (`SerializeToXmlContentTypeStructure` marks `controller`, `paths`, deps, `previewable`), but **Allowed Destinations** UI is gated until config.xml can be dropped (see comments in `descriptors/controls/commonDescriptors.ts`). + +### Content XML (FE output) + +Example: `samples/index.xml`. Root is `page` / `component` / etc. Includes system fields (`content-type`, `display-template`, `objectId`, `file-name`, `internal-name`, …) plus author fields. Embedded components may be inlined; shared components referenced via includes. + +Field id suffixes (`_s`, `_html`, `_o`, `_dt`, …) matter for indexing/Engine — see `suffixesMap.ts` / FE serializers. + +--- + +## 4. Strategic objectives (current effort) + +1. **Modern FE** — React FormsEngine replacing `forms-engine.js` (substantial progress). +2. **Modern TB** — ContentTypeManagement replacing `content-types.js` (in progress; legacy toggle still exists). +3. **Single type artifact** — fold `config.xml` into `form-definition.xml` and delete `config.xml` (requires Studio **backend** alignment, not UI-only). +4. **Descriptors / site-config-tools** — control & datasource listing + property forms driven by code defaults + optional XML overrides/plugins (`proposal.xml`, crawl → walk → run). + +Out of scope unless asked: Engine delivery / Freemarker templates beyond how TB stores `display-template`. + +--- + +## 5. How TB and FE couple + +- TB **produces** type XML; FE **consumes** it for authoring. +- TB **embeds FE** to edit the type itself and each field’s properties: descriptors define virtual content types; `TypeBuilderFormsEngine` hosts FE controls. +- Shared concerns: `XmlKeys`, value serializers/retrievers, control maps, validation keys. +- Changes to form-definition shape or field XML must stay coherent across: + - CTM serialize (`prepareSerializeToXmlTypeObject` / `buildContentTypeXml`) + - content-type parse / `ContentType` model + - FE value retrieve/serialize/validate + - (eventually) backend type APIs that today still read `config.xml` + +### 5.1 Do not conflate these extension concerns + +There are four related but distinct mechanisms: + +1. **Control catalog / descriptor** — what TB lists and which property form it presents when modeling a field. +2. **Control runtime** — the implementation FE renders when an author edits content. +3. **Data source catalog / descriptor vs runtime** — TB edits a DS definition, while FE must execute/interpret that definition for a consuming control. +4. **Form controller** — content-type-specific behavior (`form-controller.js`), separate from a control/DS plugin and from the server-side `controller.groovy`. + +### 5.2 Legacy Type Builder: catalog and plugin discovery + +Verified configuration source: `/config/studio/administration/site-config-tools.xml` (the repo sample is `samples/site-config-tools.xml`). + +Flow: + +1. `static-assets/components/cstudio-admin/base.js` calls `lookupConfigurtion(..., '/administration/site-config-tools.xml')`. +2. `buildModules` loads the `content-types` tool and passes that tool's complete XML-derived config object into `content-types.js`. +3. `renderContentTypeTools(config)` reads `config.controls.control` and `config.datasources.datasource`. +4. Each catalog entry is resolved by `CStudioAuthoring.Utils.form.getPluginInfo`: + - built-in control: `/static-assets/components/cstudio-forms/controls/.js` + - built-in DS: `/static-assets/components/cstudio-forms/data-sources/.js` + - plugin: `/studio/1/plugin/file?...` assembled from `type`, `name`, `filename`, optional `pluginId` +5. `CStudioAuthoring.Module.requireModule` injects the script, waits for `moduleLoaded(prefix, Class)`, and caches the class globally in `loadedModules`. +6. TB instantiates a fake control/DS to introspect behavior: + - controls: `getName`, `getLabel`, `getSupportedProperties`, `getSupportedConstraints`, `getSupportedPostFixes`, etc. + - data sources: `getName`, `getLabel`, `getInterface`, `getSupportedProperties`. +7. Dragging an item into a type copies its executable object's defaults plus the catalog's `` metadata into the in-memory form definition. Saving writes that plugin declaration into `form-definition.xml`. + +`site-config-tools.xml` is therefore primarily the **legacy TB palette/catalog**, not the legacy FE's runtime registry. It chooses what TB offers and provides the plugin coordinates TB persists into the type. + +### 5.3 Legacy Forms Engine: runtime controls and data sources + +The type's `form-definition.xml` is the runtime manifest. + +#### Controls + +For every field, `_renderField`: + +1. Calls `getPluginInfo(field, CONTROL_URL, 'control')`. +2. Resolves either the built-in script from `field.type` or the plugin URL from `field.plugin`. +3. Obtains the constructor through the global module registry. +4. Instantiates a control with the common constructor contract: + `(fieldId, form, section, properties, constraints, readOnly, pencilMode)`. +5. Calls `initialize`, then sets the model/default value. + +Plugin controls and built-ins participate in essentially the same runtime protocol. The plugin declaration travels with the field, so FE does not need the original TB catalog to execute it. + +#### Data sources + +Before fields render, `_loadDatasources` iterates `form.definition.datasources`, dynamically loads each built-in/plugin class, instantiates it as: + +`new DataSourceClass(datasourceId, form, properties)` + +and registers the object in `form.datasourceMap[id]`. It publishes `/datasource/loaded` for controls waiting on an asynchronous DS. + +The base `CStudioFormDatasource` is intentionally small (`getInterface`, `getName`, `getLabel`, `getSupportedProperties`, path macro processing). Concrete DS objects expose operational capabilities consumed by controls, for example: + +- item DS: `getList(callback)` and sometimes `add(control)` +- media/repository DS: browse/upload/search-specific methods +- `interface` (`item`, `image`, `video`, `audio`, …) lets TB filter compatible DS choices + +This is dynamic but not a perfectly uniform abstraction: controls still know capability methods and some DS families. Its key virtue is that behavior lives on the loaded DS object instead of in a central switch over every DS type. + +Load-mechanism quirks (legacy FE): + +- built-in/plugin **controls** typically go through `requireModule`, with external plugin controls also using dynamic `import('/studio...')` +- **datasources** typically load via `jQuery.getScript` (or a cached module) rather than the same control path +- TB palette always uses `requireModule` for both + +#### Legacy plugin transport + +`getPluginInfo` is shared by TB and FE. A declaration contains: + +- `type` +- `name` +- `filename` +- optional `pluginId` + +The plugin script must register the expected module prefix through `CStudioAuthoring.Module.moduleLoaded`. Missing coordinates are reported; loaded modules are cached. + +TB availability checks for plugin controls compare against `control.plugin.name` (not the outer ``), so palette identity and persisted `field.type` must stay aligned. + +#### Form controllers + +Legacy FE loads `/content-types//config.xml`; when `true`: + +1. It fetches the type's `form-controller.js` through the authenticated form-controller API. +2. It executes the returned script through a Blob-backed `