Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions studio-ui/docs/type-builder-forms-engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> 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
Last updated: 2026-08-05

---

Expand Down Expand Up @@ -646,7 +646,7 @@ Separate **completed design decisions** (`[x]`) from **remaining implementation
- [ ] Align project-plugin auto-wiring with TB2 catalog discovery (`site-config-tools.xml` vs `ui.xml`) and define FE1 package migration.
- [ ] Normalize plugin identity/locator vocabulary and resolve `file` vs `filename` across XML, frontend, docs, and backend.
- [ ] Fix the `hasJsController` / “Client-side Controller” UI path currently opening `controller.groovy` (and wire FE2 to consume the flag).
- [ ] **S3 / WebDAV capability stubs** — remote modules load but ops hard-fail via `unsupportedRemoteError` until `DataSourceServices` gains dedicated platform support.
- [ ] **S3 / WebDAV capability stubs** — All remote browse and upload modules work via `browseExternalAssets` / `uploadExternalAssets`. Remaining gap: `video-S3-transcoding` still hard-fails via `unsupportedRemoteError` until dedicated transcoding platform support exists.
- [ ] **Non-rendering control-map entries** — `disabled`, `internal-name`, `link-input`, `link-textarea` (and any other null map slots) need real FE2 controls or an explicit retire/alias decision.
- [ ] **FE2 Crafter-specific RTE plugin parity** — audit FE1 TinyMCE/Crafter plugins vs current `rteUtils` externals (`craftercms_paste`, `editform`, …) and implement missing FE2 equivalents.
- [ ] **Focused compatibility / plugin tests** — no Jest/Vitest harness in `ui/app` yet; need coverage for locator≠descriptor id, multi-control URL, registry conflicts, atomic registration failure, partial DS resolve, and TB plugin locator round-trip.
Expand All @@ -660,6 +660,14 @@ Separate **completed design decisions** (`[x]`) from **remaining implementation

Keep newest first. One short bullet per meaningful session.

- **2026-08-05** — Implemented WebDAV uploads (`img-WebDAV-upload`, `video-WebDAV-upload`, `WebDAV-upload`) on `uploadExternalAssets` (`profileType: 'webdav'`). Removed emptied `remoteStubs.ts`; only `video-S3-transcoding` remains as a hard-fail stub.
- **2026-08-05** — Implemented `S3-upload` FE2 upload (`item` selection, no file-type filter) on the shared `uploadExternalAssets` path.
- **2026-08-05** — Implemented `img-S3-upload` FE2 upload (`IMAGE_MIME_TYPES`, `profileType: 'aws'`) on the shared `uploadExternalAssets` path.
- **2026-08-05** — Implemented `video-S3-upload`: registered `ExternalAssetUploadDialog`, added `uploadExternalAssets` / `createExternalUploadAction`, and mapped S3 response `item.url` into asset selections. Remaining remote upload stubs still open.
- **2026-08-05** — Implemented `WebDAV-repo` (item) and `video-WebDAV-repo` (asset, `type: 'video'`) FE2 browse (`profileType: 'webdav'`, `repoPath` + `profileId`). All remote browse stubs are now real; only upload/transcoding stubs remain.
- **2026-08-05** — Implemented `video-S3-repo` (asset, `type: 'video'`) and `S3-repo` (item, no type filter) FE2 browse on the shared `browseExternalAssets` path.
- **2026-08-04** — Implemented `img-WebDAV-repo` FE2 browse on the same `browseExternalAssets` / `BrowseExternalAssetDialog` path as S3 (`profileType: 'webdav'`, `repoPath` + `profileId`, `type: 'image'`).
- **2026-08-04** — Implemented `img-S3-repo` FE2 browse: registered `BrowseExternalAssetDialog` in `studioUI`, added `showBrowseExternalAssetDialog` + `DataSourceServices.browseExternalAssets` + `createExternalBrowseAction`, and replaced the stub module with a real browse action (`profileType: 'aws'`, `type: 'image'`). Remaining S3/WebDAV stubs still open.
- **2026-08-03** — Convergence-gap audit reflected in §8: remaining work includes S3/WebDAV stubs, null control-map entries (`disabled` / `internal-name` / `link-input` / `link-textarea`), FE2 RTE plugin parity, and focused compatibility tests. Control-plugin `PluginDescriptor.id` ownership checks and atomic `registerPlugin` preflight were already implemented — recorded under completed design decisions, not left as open gaps.
- **2026-08-03** — Refined §5.9: all `FormController` hooks may be async (host awaits); clarified on-disk path, form_controller API, and FE2 loader call site (`formControllerLoader` from form bootstrap — not `importPlugin`).
- **2026-08-03** — Decided FE2 form-controller design (§5.9): keep type-local `form-controller.js` gated by `hasJsController`; load via authenticated form_controller API + ESM Blob import; export `FormController` hooks (`initialize`, `isFieldRelevant`, `onBeforeSave`) — **not** a `PluginDescriptor`. FE1 YUI controllers are incompatible (migrate by rewrite). TB must fix Client-side Controller to edit `form-controller.js` instead of Groovy. Implementation still TODO.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,19 @@ export function toAssetSelection(item: unknown): DataSourceAssetSelection {
// Upload result (Uppy FileUpload)
if (candidate.meta && typeof candidate.meta === 'object') {
const meta = candidate.meta as { path?: string; name?: string; type?: string };
const response = candidate.response as { body?: { item?: { url?: string; name?: string } } } | undefined;
const remoteUrl = response?.body?.item?.url;
// S3/WebDAV upload APIs return the public/remote URL on the response item.
if (typeof remoteUrl === 'string' && remoteUrl) {
const name = response?.body?.item?.name ?? meta.name ?? '';
return {
kind: 'asset',
relativeUrl: remoteUrl,
previewUrl: remoteUrl,
fileExtension: fileExtensionFromPath(remoteUrl) || fileExtensionFromPath(name),
mimeType: meta.type ?? (typeof candidate.type === 'string' ? candidate.type : undefined)
};
}
const name = meta.name ?? (typeof candidate.name === 'string' ? candidate.name : '');
const path = meta.path ?? '';
if (!path && !name) {
Expand Down Expand Up @@ -282,6 +295,53 @@ export function createBrowseAction(options: {
};
}

/**
* Factory for browsing S3/WebDAV
* (opens BrowseExternalAssetDialog).
*/
export function createExternalBrowseAction(options: {
id?: string;
label?: string;
path: string;
profileId: string;
profileType?: 'aws' | 'webdav';
/** API filter passed to list endpoints (e.g. `image`, `video`). */
type?: string;
mimeTypes?: string[];
selection: 'item' | 'asset';
meta?: DataSourceActionMeta;
}): DataSourceAction {
const { path, profileId, profileType = 'aws', type, mimeTypes, selection } = options;
return {
id: options.id ?? 'browse',
kind: 'browse',
label: options.label ?? 'Browse',
meta: {
path,
mimeTypes,
profileId,
profileType,
type,
...options.meta
},
async run(ctx) {
if (!profileId) {
throw new Error('External browse requires a profileId on the data source.');
}
const expanded = expandPathOrRaw(ctx, path);
const items = await ctx.services.browseExternalAssets({
path: expanded,
profileId,
profileType,
type,
multiSelect: (ctx.remainingCapacity ?? 2) !== 1
});
if (!items.length) return null;
return selection === 'asset' ? toAssetSelections(items) : toItemSelections(items);
Comment thread
jvega190 marked this conversation as resolved.
}
};
}

/**
* Factory for a standard search action (path expanded + recursive `/.+` suffix via {@link toSearchPath}).
*/
Expand Down Expand Up @@ -351,6 +411,50 @@ export function createUploadAction(options: {
};
}

/**
* Factory for uploading to S3/WebDAV via {@link DataSourceServices.uploadExternalAssets}
* (opens ExternalAssetUploadDialog).
*/
export function createExternalUploadAction(options: {
id?: string;
label?: string;
path: string;
profileId: string;
profileType?: 'aws' | 'webdav';
fileTypes?: string[];
selection: 'item' | 'asset';
meta?: DataSourceActionMeta;
}): DataSourceAction {
const { path, profileId, profileType = 'aws', fileTypes, selection } = options;
return {
id: options.id ?? 'upload',
kind: 'upload',
label: options.label ?? 'Upload',
meta: {
path,
fileTypes,
profileId,
profileType,
...options.meta
},
async run(ctx) {
if (!profileId) {
throw new Error('External upload requires a profileId on the data source.');
}
const expanded = expandPathOrRaw(ctx, path);
const result = await ctx.services.uploadExternalAssets({
path: expanded,
profileId,
profileType,
fileTypes
});
if (!result) return null;
const mapped = selection === 'asset' ? mapUploadResultToAssets(result) : mapUploadResultToItems(result);
return mapped.length ? mapped : null;
}
};
}

/**
* Factory for a standard create action. When multiple targets exist, the control must pass a create
* target via `runOptions.target` — avoids ambiguous create.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,37 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

export { imgS3RepoDataSourceModule as default } from './remoteStubs';
import { DATA_SOURCE_API_VERSION, type DataSourceModule } from '../types';
import { createInstanceFromRecord, defineDataSourceModule } from '../defineModule';
import { createExternalBrowseAction, IMAGE_MIME_TYPES, propString } from '../moduleHelpers';

export const imgS3RepoDataSourceModule: DataSourceModule = defineDataSourceModule({
apiVersion: DATA_SOURCE_API_VERSION,
type: 'img-S3-repo',
interfaces: ['image'],
capabilities: ['browse'],
create({ record }) {
const path = propString(record, 'path');
const profileId = propString(record, 'profileId');

return createInstanceFromRecord(record, imgS3RepoDataSourceModule, {
capabilities: ['browse'],
getActions() {
return [
createExternalBrowseAction({
label: `Browse - ${record.title}`,
path,
profileId,
profileType: 'aws',
type: 'image',
mimeTypes: IMAGE_MIME_TYPES,
selection: 'asset',
meta: { path, profileId, mimeTypes: IMAGE_MIME_TYPES }
})
];
}
});
}
});

export default imgS3RepoDataSourceModule;
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,36 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

export { imgS3UploadDataSourceModule as default } from './remoteStubs';
import { DATA_SOURCE_API_VERSION, type DataSourceModule } from '../types';
import { createInstanceFromRecord, defineDataSourceModule } from '../defineModule';
import { createExternalUploadAction, IMAGE_MIME_TYPES, propString } from '../moduleHelpers';

export const imgS3UploadDataSourceModule: DataSourceModule = defineDataSourceModule({
apiVersion: DATA_SOURCE_API_VERSION,
type: 'img-S3-upload',
interfaces: ['image'],
capabilities: ['upload'],
create({ record }) {
const path = propString(record, 'repoPath');
const profileId = propString(record, 'profileId');

return createInstanceFromRecord(record, imgS3UploadDataSourceModule, {
capabilities: ['upload'],
getActions() {
return [
createExternalUploadAction({
label: `Upload - ${record.title}`,
path,
profileId,
profileType: 'aws',
fileTypes: IMAGE_MIME_TYPES,
selection: 'asset',
meta: { path, profileId, fileTypes: IMAGE_MIME_TYPES }
})
];
}
});
}
});

export default imgS3UploadDataSourceModule;
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,37 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

export { imgWebDAVRepoDataSourceModule as default } from './remoteStubs';
import { DATA_SOURCE_API_VERSION, type DataSourceModule } from '../types';
import { createInstanceFromRecord, defineDataSourceModule } from '../defineModule';
import { createExternalBrowseAction, IMAGE_MIME_TYPES, propString } from '../moduleHelpers';

export const imgWebDAVRepoDataSourceModule: DataSourceModule = defineDataSourceModule({
apiVersion: DATA_SOURCE_API_VERSION,
type: 'img-WebDAV-repo',
interfaces: ['image'],
capabilities: ['browse'],
create({ record }) {
const path = propString(record, 'repoPath');
const profileId = propString(record, 'profileId');

return createInstanceFromRecord(record, imgWebDAVRepoDataSourceModule, {
capabilities: ['browse'],
getActions() {
return [
createExternalBrowseAction({
label: `Browse - ${record.title}`,
path,
profileId,
profileType: 'webdav',
type: 'image',
mimeTypes: IMAGE_MIME_TYPES,
selection: 'asset',
meta: { path, profileId, mimeTypes: IMAGE_MIME_TYPES }
})
];
}
});
}
});

export default imgWebDAVRepoDataSourceModule;
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,36 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

export { imgWebDAVUploadDataSourceModule as default } from './remoteStubs';
import { DATA_SOURCE_API_VERSION, type DataSourceModule } from '../types';
import { createInstanceFromRecord, defineDataSourceModule } from '../defineModule';
import { createExternalUploadAction, IMAGE_MIME_TYPES, propString } from '../moduleHelpers';

export const imgWebDAVUploadDataSourceModule: DataSourceModule = defineDataSourceModule({
apiVersion: DATA_SOURCE_API_VERSION,
type: 'img-WebDAV-upload',
interfaces: ['image'],
capabilities: ['upload'],
create({ record }) {
const path = propString(record, 'repoPath');
const profileId = propString(record, 'profileId');

return createInstanceFromRecord(record, imgWebDAVUploadDataSourceModule, {
capabilities: ['upload'],
getActions() {
return [
createExternalUploadAction({
label: `Upload - ${record.title}`,
path,
profileId,
profileType: 'webdav',
fileTypes: IMAGE_MIME_TYPES,
selection: 'asset',
meta: { path, profileId, fileTypes: IMAGE_MIME_TYPES }
})
];
}
});
}
});

export default imgWebDAVUploadDataSourceModule;
Loading