diff --git a/studio-ui/docs/type-builder-forms-engine.md b/studio-ui/docs/type-builder-forms-engine.md
index 985d95ae..4b3093e4 100644
--- a/studio-ui/docs/type-builder-forms-engine.md
+++ b/studio-ui/docs/type-builder-forms-engine.md
@@ -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
---
@@ -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.
@@ -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.
diff --git a/studio-ui/ui/app/src/components/FormsEngine/dataSources/moduleHelpers.ts b/studio-ui/ui/app/src/components/FormsEngine/dataSources/moduleHelpers.ts
index c6c0c22f..a8139a84 100644
--- a/studio-ui/ui/app/src/components/FormsEngine/dataSources/moduleHelpers.ts
+++ b/studio-ui/ui/app/src/components/FormsEngine/dataSources/moduleHelpers.ts
@@ -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) {
@@ -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);
+ }
+ };
+}
+
/**
* Factory for a standard search action (path expanded + recursive `/.+` suffix via {@link toSearchPath}).
*/
@@ -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.
diff --git a/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/imgS3Repo.ts b/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/imgS3Repo.ts
index ceeb0497..3ebe0430 100644
--- a/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/imgS3Repo.ts
+++ b/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/imgS3Repo.ts
@@ -14,4 +14,37 @@
* along with this program. If not, see .
*/
-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;
diff --git a/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/imgS3Upload.ts b/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/imgS3Upload.ts
index 9fb10b0d..6a15c710 100644
--- a/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/imgS3Upload.ts
+++ b/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/imgS3Upload.ts
@@ -14,4 +14,36 @@
* along with this program. If not, see .
*/
-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;
diff --git a/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/imgWebDAVRepo.ts b/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/imgWebDAVRepo.ts
index 245423c0..c2805aec 100644
--- a/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/imgWebDAVRepo.ts
+++ b/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/imgWebDAVRepo.ts
@@ -14,4 +14,37 @@
* along with this program. If not, see .
*/
-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;
diff --git a/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/imgWebDAVUpload.ts b/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/imgWebDAVUpload.ts
index 959c82e1..df2ec08d 100644
--- a/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/imgWebDAVUpload.ts
+++ b/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/imgWebDAVUpload.ts
@@ -14,4 +14,36 @@
* along with this program. If not, see .
*/
-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;
diff --git a/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/remoteStubs.ts b/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/remoteStubs.ts
deleted file mode 100644
index 1cf13777..00000000
--- a/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/remoteStubs.ts
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * Copyright (C) 2007-2026 Crafter Software Corporation. All Rights Reserved.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 3 as published by
- * the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-import { DATA_SOURCE_API_VERSION, type DataSourceCapability, type DataSourceModule } from '../types';
-import { createInstanceFromRecord, defineDataSourceModule } from '../defineModule';
-import { createRemoteStubAction, propString } from '../moduleHelpers';
-
-function remoteMediaModule(options: {
- type: string;
- interfaces: DataSourceModule['interfaces'];
- kind: 'browse' | 'upload';
- pathProp: 'path' | 'repoPath';
- defaultPath?: string;
- mimeTypes?: string[];
-}): DataSourceModule {
- const capabilities: DataSourceCapability[] = [options.kind];
- const module: DataSourceModule = defineDataSourceModule({
- apiVersion: DATA_SOURCE_API_VERSION,
- type: options.type,
- interfaces: options.interfaces,
- capabilities,
- create({ record }) {
- const path = propString(record, options.pathProp, options.defaultPath ?? '');
- const profileId = propString(record, 'profileId') || undefined;
- const label = options.kind === 'browse' ? 'Browse' : 'Upload';
- return createInstanceFromRecord(record, module, {
- capabilities,
- getActions() {
- return [
- createRemoteStubAction({
- id: options.kind,
- kind: options.kind,
- label: `${label} - ${record.title}`,
- type: options.type,
- operation: options.kind,
- meta: {
- path,
- profileId,
- mimeTypes: options.mimeTypes
- }
- })
- ];
- }
- });
- }
- });
- return module;
-}
-
-export const imgS3RepoDataSourceModule = remoteMediaModule({
- type: 'img-S3-repo',
- interfaces: ['image'],
- kind: 'browse',
- pathProp: 'path',
- mimeTypes: ['image/*']
-});
-
-export const imgS3UploadDataSourceModule = remoteMediaModule({
- type: 'img-S3-upload',
- interfaces: ['image'],
- kind: 'upload',
- pathProp: 'repoPath',
- mimeTypes: ['image/*']
-});
-
-export const imgWebDAVRepoDataSourceModule = remoteMediaModule({
- type: 'img-WebDAV-repo',
- interfaces: ['image'],
- kind: 'browse',
- pathProp: 'repoPath',
- mimeTypes: ['image/*']
-});
-
-export const imgWebDAVUploadDataSourceModule = remoteMediaModule({
- type: 'img-WebDAV-upload',
- interfaces: ['image'],
- kind: 'upload',
- pathProp: 'repoPath',
- mimeTypes: ['image/*']
-});
-
-export const videoS3RepoDataSourceModule = remoteMediaModule({
- type: 'video-S3-repo',
- interfaces: ['video'],
- kind: 'browse',
- pathProp: 'path',
- mimeTypes: ['video/*']
-});
-
-export const videoS3UploadDataSourceModule = remoteMediaModule({
- type: 'video-S3-upload',
- interfaces: ['video'],
- kind: 'upload',
- pathProp: 'repoPath',
- mimeTypes: ['video/*']
-});
-
-export const videoWebDAVRepoDataSourceModule = remoteMediaModule({
- type: 'video-WebDAV-repo',
- interfaces: ['video'],
- kind: 'browse',
- pathProp: 'repoPath',
- mimeTypes: ['video/*']
-});
-
-export const videoWebDAVUploadDataSourceModule = remoteMediaModule({
- type: 'video-WebDAV-upload',
- interfaces: ['video'],
- kind: 'upload',
- pathProp: 'repoPath',
- mimeTypes: ['video/*']
-});
-
-export const s3RepoDataSourceModule = remoteMediaModule({
- type: 'S3-repo',
- interfaces: ['item'],
- kind: 'browse',
- pathProp: 'path'
-});
-
-export const s3UploadDataSourceModule = remoteMediaModule({
- type: 'S3-upload',
- interfaces: ['item'],
- kind: 'upload',
- pathProp: 'repoPath'
-});
-
-export const webDavRepoDataSourceModule = remoteMediaModule({
- type: 'WebDAV-repo',
- interfaces: ['item'],
- kind: 'browse',
- pathProp: 'repoPath'
-});
-
-export const webDavUploadDataSourceModule = remoteMediaModule({
- type: 'WebDAV-upload',
- interfaces: ['item'],
- kind: 'upload',
- pathProp: 'repoPath'
-});
diff --git a/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/s3Repo.ts b/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/s3Repo.ts
index 17201fc3..e641e4bc 100644
--- a/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/s3Repo.ts
+++ b/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/s3Repo.ts
@@ -14,4 +14,35 @@
* along with this program. If not, see .
*/
-export { s3RepoDataSourceModule as default } from './remoteStubs';
+import { DATA_SOURCE_API_VERSION, type DataSourceModule } from '../types';
+import { createInstanceFromRecord, defineDataSourceModule } from '../defineModule';
+import { createExternalBrowseAction, propString } from '../moduleHelpers';
+
+export const s3RepoDataSourceModule: DataSourceModule = defineDataSourceModule({
+ apiVersion: DATA_SOURCE_API_VERSION,
+ type: 'S3-repo',
+ interfaces: ['item'],
+ capabilities: ['browse'],
+ create({ record }) {
+ const path = propString(record, 'path');
+ const profileId = propString(record, 'profileId');
+
+ return createInstanceFromRecord(record, s3RepoDataSourceModule, {
+ capabilities: ['browse'],
+ getActions() {
+ return [
+ createExternalBrowseAction({
+ label: `Browse - ${record.title}`,
+ path,
+ profileId,
+ profileType: 'aws',
+ selection: 'item',
+ meta: { path, profileId }
+ })
+ ];
+ }
+ });
+ }
+});
+
+export default s3RepoDataSourceModule;
diff --git a/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/s3Upload.ts b/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/s3Upload.ts
index b27acfd5..bc18f207 100644
--- a/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/s3Upload.ts
+++ b/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/s3Upload.ts
@@ -14,4 +14,35 @@
* along with this program. If not, see .
*/
-export { s3UploadDataSourceModule as default } from './remoteStubs';
+import { DATA_SOURCE_API_VERSION, type DataSourceModule } from '../types';
+import { createInstanceFromRecord, defineDataSourceModule } from '../defineModule';
+import { createExternalUploadAction, propString } from '../moduleHelpers';
+
+export const s3UploadDataSourceModule: DataSourceModule = defineDataSourceModule({
+ apiVersion: DATA_SOURCE_API_VERSION,
+ type: 'S3-upload',
+ interfaces: ['item'],
+ capabilities: ['upload'],
+ create({ record }) {
+ const path = propString(record, 'repoPath');
+ const profileId = propString(record, 'profileId');
+
+ return createInstanceFromRecord(record, s3UploadDataSourceModule, {
+ capabilities: ['upload'],
+ getActions() {
+ return [
+ createExternalUploadAction({
+ label: `Upload - ${record.title}`,
+ path,
+ profileId,
+ profileType: 'aws',
+ selection: 'item',
+ meta: { path, profileId }
+ })
+ ];
+ }
+ });
+ }
+});
+
+export default s3UploadDataSourceModule;
diff --git a/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/videoS3Repo.ts b/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/videoS3Repo.ts
index 8f9e5fb8..fca51fbb 100644
--- a/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/videoS3Repo.ts
+++ b/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/videoS3Repo.ts
@@ -14,4 +14,37 @@
* along with this program. If not, see .
*/
-export { videoS3RepoDataSourceModule as default } from './remoteStubs';
+import { DATA_SOURCE_API_VERSION, type DataSourceModule } from '../types';
+import { createInstanceFromRecord, defineDataSourceModule } from '../defineModule';
+import { createExternalBrowseAction, propString, VIDEO_MIME_TYPES } from '../moduleHelpers';
+
+export const videoS3RepoDataSourceModule: DataSourceModule = defineDataSourceModule({
+ apiVersion: DATA_SOURCE_API_VERSION,
+ type: 'video-S3-repo',
+ interfaces: ['video'],
+ capabilities: ['browse'],
+ create({ record }) {
+ const path = propString(record, 'path');
+ const profileId = propString(record, 'profileId');
+
+ return createInstanceFromRecord(record, videoS3RepoDataSourceModule, {
+ capabilities: ['browse'],
+ getActions() {
+ return [
+ createExternalBrowseAction({
+ label: `Browse - ${record.title}`,
+ path,
+ profileId,
+ profileType: 'aws',
+ type: 'video',
+ mimeTypes: VIDEO_MIME_TYPES,
+ selection: 'asset',
+ meta: { path, profileId, mimeTypes: VIDEO_MIME_TYPES }
+ })
+ ];
+ }
+ });
+ }
+});
+
+export default videoS3RepoDataSourceModule;
diff --git a/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/videoS3Upload.ts b/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/videoS3Upload.ts
index 30ee2cd1..ca4e6498 100644
--- a/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/videoS3Upload.ts
+++ b/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/videoS3Upload.ts
@@ -14,4 +14,36 @@
* along with this program. If not, see .
*/
-export { videoS3UploadDataSourceModule as default } from './remoteStubs';
+import { DATA_SOURCE_API_VERSION, type DataSourceModule } from '../types';
+import { createInstanceFromRecord, defineDataSourceModule } from '../defineModule';
+import { createExternalUploadAction, propString, VIDEO_MIME_TYPES } from '../moduleHelpers';
+
+export const videoS3UploadDataSourceModule: DataSourceModule = defineDataSourceModule({
+ apiVersion: DATA_SOURCE_API_VERSION,
+ type: 'video-S3-upload',
+ interfaces: ['video'],
+ capabilities: ['upload'],
+ create({ record }) {
+ const path = propString(record, 'repoPath');
+ const profileId = propString(record, 'profileId');
+
+ return createInstanceFromRecord(record, videoS3UploadDataSourceModule, {
+ capabilities: ['upload'],
+ getActions() {
+ return [
+ createExternalUploadAction({
+ label: `Upload - ${record.title}`,
+ path,
+ profileId,
+ profileType: 'aws',
+ fileTypes: VIDEO_MIME_TYPES,
+ selection: 'asset',
+ meta: { path, profileId, fileTypes: VIDEO_MIME_TYPES }
+ })
+ ];
+ }
+ });
+ }
+});
+
+export default videoS3UploadDataSourceModule;
diff --git a/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/videoWebDAVRepo.ts b/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/videoWebDAVRepo.ts
index aa8e2647..e225bef8 100644
--- a/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/videoWebDAVRepo.ts
+++ b/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/videoWebDAVRepo.ts
@@ -14,4 +14,37 @@
* along with this program. If not, see .
*/
-export { videoWebDAVRepoDataSourceModule as default } from './remoteStubs';
+import { DATA_SOURCE_API_VERSION, type DataSourceModule } from '../types';
+import { createInstanceFromRecord, defineDataSourceModule } from '../defineModule';
+import { createExternalBrowseAction, propString, VIDEO_MIME_TYPES } from '../moduleHelpers';
+
+export const videoWebDAVRepoDataSourceModule: DataSourceModule = defineDataSourceModule({
+ apiVersion: DATA_SOURCE_API_VERSION,
+ type: 'video-WebDAV-repo',
+ interfaces: ['video'],
+ capabilities: ['browse'],
+ create({ record }) {
+ const path = propString(record, 'repoPath');
+ const profileId = propString(record, 'profileId');
+
+ return createInstanceFromRecord(record, videoWebDAVRepoDataSourceModule, {
+ capabilities: ['browse'],
+ getActions() {
+ return [
+ createExternalBrowseAction({
+ label: `Browse - ${record.title}`,
+ path,
+ profileId,
+ profileType: 'webdav',
+ type: 'video',
+ mimeTypes: VIDEO_MIME_TYPES,
+ selection: 'asset',
+ meta: { path, profileId, mimeTypes: VIDEO_MIME_TYPES }
+ })
+ ];
+ }
+ });
+ }
+});
+
+export default videoWebDAVRepoDataSourceModule;
diff --git a/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/videoWebDAVUpload.ts b/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/videoWebDAVUpload.ts
index 834e97c9..1cadc75e 100644
--- a/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/videoWebDAVUpload.ts
+++ b/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/videoWebDAVUpload.ts
@@ -14,4 +14,36 @@
* along with this program. If not, see .
*/
-export { videoWebDAVUploadDataSourceModule as default } from './remoteStubs';
+import { DATA_SOURCE_API_VERSION, type DataSourceModule } from '../types';
+import { createInstanceFromRecord, defineDataSourceModule } from '../defineModule';
+import { createExternalUploadAction, propString, VIDEO_MIME_TYPES } from '../moduleHelpers';
+
+export const videoWebDAVUploadDataSourceModule: DataSourceModule = defineDataSourceModule({
+ apiVersion: DATA_SOURCE_API_VERSION,
+ type: 'video-WebDAV-upload',
+ interfaces: ['video'],
+ capabilities: ['upload'],
+ create({ record }) {
+ const path = propString(record, 'repoPath');
+ const profileId = propString(record, 'profileId');
+
+ return createInstanceFromRecord(record, videoWebDAVUploadDataSourceModule, {
+ capabilities: ['upload'],
+ getActions() {
+ return [
+ createExternalUploadAction({
+ label: `Upload - ${record.title}`,
+ path,
+ profileId,
+ profileType: 'webdav',
+ fileTypes: VIDEO_MIME_TYPES,
+ selection: 'asset',
+ meta: { path, profileId, fileTypes: VIDEO_MIME_TYPES }
+ })
+ ];
+ }
+ });
+ }
+});
+
+export default videoWebDAVUploadDataSourceModule;
diff --git a/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/webDavRepo.ts b/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/webDavRepo.ts
index aa9c5357..b0590cd5 100644
--- a/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/webDavRepo.ts
+++ b/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/webDavRepo.ts
@@ -14,4 +14,35 @@
* along with this program. If not, see .
*/
-export { webDavRepoDataSourceModule as default } from './remoteStubs';
+import { DATA_SOURCE_API_VERSION, type DataSourceModule } from '../types';
+import { createInstanceFromRecord, defineDataSourceModule } from '../defineModule';
+import { createExternalBrowseAction, propString } from '../moduleHelpers';
+
+export const webDavRepoDataSourceModule: DataSourceModule = defineDataSourceModule({
+ apiVersion: DATA_SOURCE_API_VERSION,
+ type: 'WebDAV-repo',
+ interfaces: ['item'],
+ capabilities: ['browse'],
+ create({ record }) {
+ const path = propString(record, 'repoPath');
+ const profileId = propString(record, 'profileId');
+
+ return createInstanceFromRecord(record, webDavRepoDataSourceModule, {
+ capabilities: ['browse'],
+ getActions() {
+ return [
+ createExternalBrowseAction({
+ label: `Browse - ${record.title}`,
+ path,
+ profileId,
+ profileType: 'webdav',
+ selection: 'item',
+ meta: { path, profileId }
+ })
+ ];
+ }
+ });
+ }
+});
+
+export default webDavRepoDataSourceModule;
diff --git a/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/webDavUpload.ts b/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/webDavUpload.ts
index 63b3ea49..d4c1533a 100644
--- a/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/webDavUpload.ts
+++ b/studio-ui/ui/app/src/components/FormsEngine/dataSources/modules/webDavUpload.ts
@@ -14,4 +14,35 @@
* along with this program. If not, see .
*/
-export { webDavUploadDataSourceModule as default } from './remoteStubs';
+import { DATA_SOURCE_API_VERSION, type DataSourceModule } from '../types';
+import { createInstanceFromRecord, defineDataSourceModule } from '../defineModule';
+import { createExternalUploadAction, propString } from '../moduleHelpers';
+
+export const webDavUploadDataSourceModule: DataSourceModule = defineDataSourceModule({
+ apiVersion: DATA_SOURCE_API_VERSION,
+ type: 'WebDAV-upload',
+ interfaces: ['item'],
+ capabilities: ['upload'],
+ create({ record }) {
+ const path = propString(record, 'repoPath');
+ const profileId = propString(record, 'profileId');
+
+ return createInstanceFromRecord(record, webDavUploadDataSourceModule, {
+ capabilities: ['upload'],
+ getActions() {
+ return [
+ createExternalUploadAction({
+ label: `Upload - ${record.title}`,
+ path,
+ profileId,
+ profileType: 'webdav',
+ selection: 'item',
+ meta: { path, profileId }
+ })
+ ];
+ }
+ });
+ }
+});
+
+export default webDavUploadDataSourceModule;
diff --git a/studio-ui/ui/app/src/components/FormsEngine/dataSources/services.ts b/studio-ui/ui/app/src/components/FormsEngine/dataSources/services.ts
index 57fc8007..0d3c551d 100644
--- a/studio-ui/ui/app/src/components/FormsEngine/dataSources/services.ts
+++ b/studio-ui/ui/app/src/components/FormsEngine/dataSources/services.ts
@@ -18,7 +18,13 @@ import type { Dispatch } from 'redux';
import type { BrowseFilesDialogProps } from '../../BrowseFilesDialog';
import type { SearchProps } from '../../Search';
import type { FormsEngineGlobalApiContextProps } from '../lib/formsEngineContext';
-import { showBrowseFilesDialog, showSearchDialog, showSingleFileUploadDialog } from '../lib/controlHelpers';
+import {
+ showBrowseExternalAssetDialog,
+ showBrowseFilesDialog,
+ showExternalAssetUploadDialog,
+ showSearchDialog,
+ showSingleFileUploadDialog
+} from '../lib/controlHelpers';
import type { DataSourceServices } from './types';
/** Redux dispatch + site + forms API needed to bridge dialog / `pushForm` UI into promise services. */
@@ -55,6 +61,23 @@ export function createDataSourceServices({
});
});
},
+ browseExternalAssets(request) {
+ return new Promise((resolve) => {
+ showBrowseExternalAssetDialog({
+ dispatch,
+ path: request.path,
+ profileId: request.profileId,
+ profileType: request.profileType,
+ type: request.type,
+ multiSelect: request.multiSelect,
+ preselectedPaths: request.preselectedPaths,
+ onClose: () => resolve([]),
+ onSuccess(items) {
+ resolve(Array.isArray(items) ? items : [items]);
+ }
+ });
+ });
+ },
search(request) {
return new Promise((resolve) => {
showSearchDialog({
@@ -81,6 +104,19 @@ export function createDataSourceServices({
});
});
},
+ uploadExternalAssets(request) {
+ return new Promise((resolve) => {
+ showExternalAssetUploadDialog({
+ dispatch,
+ path: request.path,
+ profileId: request.profileId,
+ profileType: request.profileType,
+ fileTypes: request.fileTypes,
+ onClose: () => resolve(null),
+ onUploadComplete: resolve
+ });
+ });
+ },
createContent(request) {
return new Promise((resolve) => {
let settled = false;
diff --git a/studio-ui/ui/app/src/components/FormsEngine/dataSources/types.ts b/studio-ui/ui/app/src/components/FormsEngine/dataSources/types.ts
index fd09db67..99ff096d 100644
--- a/studio-ui/ui/app/src/components/FormsEngine/dataSources/types.ts
+++ b/studio-ui/ui/app/src/components/FormsEngine/dataSources/types.ts
@@ -81,6 +81,20 @@ export interface DataSourceBrowseRequest {
initialParameters?: Record;
}
+/**
+ * Inputs for {@link DataSourceServices.browseExternalAssets}.
+ * Mirrors {@link BrowseExternalAssetDialog} props (S3 / WebDAV).
+ */
+export interface DataSourceBrowseExternalRequest {
+ path: string;
+ profileId: string;
+ profileType?: 'aws' | 'webdav';
+ /** API filter passed to list endpoints (e.g. `image`, `video`). */
+ type?: string;
+ multiSelect?: boolean;
+ preselectedPaths?: string[];
+}
+
/** Inputs for {@link DataSourceServices.search}. */
export interface DataSourceSearchRequest {
path: string;
@@ -95,6 +109,17 @@ export interface DataSourceUploadRequest {
fileTypes?: string[];
}
+/**
+ * Inputs for {@link DataSourceServices.uploadExternalAssets}.
+ * Mirrors {@link ExternalAssetUploadDialog} props (S3 / WebDAV).
+ */
+export interface DataSourceUploadExternalRequest {
+ path: string;
+ profileId: string;
+ profileType?: 'aws' | 'webdav';
+ fileTypes?: string[];
+}
+
/** Inputs for {@link DataSourceServices.createContent}. */
export interface DataSourceCreateRequest {
path: string;
@@ -114,8 +139,12 @@ export interface DataSourceSearchResult {
*/
export interface DataSourceServices {
browseFiles(request: DataSourceBrowseRequest): Promise;
+ /** Opens BrowseExternalAssetDialog for S3/WebDAV; cancel resolves to `[]`. */
+ browseExternalAssets(request: DataSourceBrowseExternalRequest): Promise;
search(request: DataSourceSearchRequest): Promise;
upload(request: DataSourceUploadRequest): Promise;
+ /** Opens ExternalAssetUploadDialog for S3/WebDAV; cancel resolves to `null`. */
+ uploadExternalAssets(request: DataSourceUploadExternalRequest): Promise;
createContent(request: DataSourceCreateRequest): Promise;
/** @deprecated Use createContent so the action can return the created selection. */
pushForm(props: FormsEngineProps): void;
diff --git a/studio-ui/ui/app/src/components/FormsEngine/lib/controlHelpers.tsx b/studio-ui/ui/app/src/components/FormsEngine/lib/controlHelpers.tsx
index 350bd17e..fd3c5d87 100644
--- a/studio-ui/ui/app/src/components/FormsEngine/lib/controlHelpers.tsx
+++ b/studio-ui/ui/app/src/components/FormsEngine/lib/controlHelpers.tsx
@@ -32,6 +32,8 @@ import { getFileNameFromPath } from '../../../utils/path';
import { ensureSingleSlash } from '../../../utils/string';
import { Dispatch as ReduxDispatch } from 'redux';
import { BrowseFilesDialogProps } from '../../BrowseFilesDialog';
+import type { BrowseExternalAssetDialogProps } from '../../BrowseS3Dialog';
+import type { ExternalAssetUploadDialogProps } from '../../ExternalAssetUploadDialog';
import { nanoid } from 'nanoid';
import { popDialog, pushDialog, pushNonDialog } from '../../../state/actions/dialogStack';
import { createComponentId } from '../../../utils/system';
@@ -262,6 +264,52 @@ export const showBrowseFilesDialog = ({
);
};
+export const showBrowseExternalAssetDialog = ({
+ dispatch,
+ onSuccess,
+ path,
+ profileId,
+ profileType = 'aws',
+ type,
+ multiSelect = true,
+ preselectedPaths = [],
+ onClose
+}: {
+ dispatch: ReduxDispatch;
+ onSuccess: BrowseExternalAssetDialogProps['onSuccess'];
+ path: string;
+ profileId: string;
+ profileType?: BrowseExternalAssetDialogProps['profileType'];
+ type?: string;
+ multiSelect?: boolean;
+ preselectedPaths?: string[];
+ onClose?(): void;
+}): void => {
+ const id = nanoid();
+ dispatch(
+ pushDialog({
+ id,
+ component: createComponentId('BrowseExternalAssetDialog'),
+ props: {
+ path,
+ profileId,
+ profileType,
+ type,
+ multiSelect,
+ preselectedPaths,
+ onClose: () => {
+ dispatch(popDialog({ id }));
+ onClose?.();
+ },
+ onSuccess(items) {
+ dispatch(popDialog({ id }));
+ onSuccess?.(items);
+ }
+ } as Partial
+ })
+ );
+};
+
export const showSearchDialog = ({
dispatch,
path,
@@ -346,6 +394,46 @@ export const showSingleFileUploadDialog = ({
);
};
+export const showExternalAssetUploadDialog = ({
+ dispatch,
+ path,
+ profileId,
+ profileType = 'aws',
+ fileTypes,
+ onUploadComplete,
+ onClose
+}: {
+ dispatch: ReduxDispatch;
+ path: string;
+ profileId: string;
+ profileType?: ExternalAssetUploadDialogProps['profileType'];
+ fileTypes?: string[];
+ onUploadComplete?: ExternalAssetUploadDialogProps['onUploadComplete'];
+ onClose?(): void;
+}): void => {
+ const id = nanoid();
+ dispatch(
+ pushDialog({
+ id,
+ component: createComponentId('ExternalAssetUploadDialog'),
+ props: {
+ path,
+ profileId,
+ profileType,
+ fileTypes,
+ onClose: () => {
+ dispatch(popDialog({ id }));
+ onClose?.();
+ },
+ onUploadComplete: (result) => {
+ dispatch(popDialog({ id }));
+ onUploadComplete?.(result);
+ }
+ } as Partial
+ })
+ );
+};
+
export const showImageCropDialog = ({
dispatch,
path,
diff --git a/studio-ui/ui/app/src/env/studioUI.ts b/studio-ui/ui/app/src/env/studioUI.ts
index 2fb6dbd3..7cd343c9 100644
--- a/studio-ui/ui/app/src/env/studioUI.ts
+++ b/studio-ui/ui/app/src/env/studioUI.ts
@@ -77,6 +77,7 @@ export const components = {
BasePathSelector: lazy(() => import('../components/BasePathSelector')),
BrokenReferencesDialog: lazy(() => import('../components/BrokenReferencesDialog')),
BrowseFilesDialog: lazy(() => import('../components/BrowseFilesDialog')),
+ BrowseExternalAssetDialog: lazy(() => import('../components/BrowseS3Dialog')),
BulkCancelPackageDialog: lazy(() => import('../components/BulkCancelPackageDialog')),
CancelPackageDialog: lazy(() => import('../components/CancelPackageDialog')),
ChangeContentTypeDialog: lazy(() => import('../components/ChangeContentTypeDialog')),
@@ -133,6 +134,7 @@ export const components = {
ErrorDialog: lazy(() => import('../components/ErrorDialog')),
ErrorState: lazy(() => import('../components/ErrorState')),
ExpiringDashlet: lazy(() => import('../components/ExpiringDashlet')),
+ ExternalAssetUploadDialog: lazy(() => import('../components/ExternalAssetUploadDialog')),
FieldInformationDialog: lazy(() => import('../components/FieldInformationDialog/FieldInformationDialog')),
FolderBrowserTreeView: lazy(() => import('../components/FolderBrowserTreeView')),
FolderMoveAlert: lazy(() => import('../components/FolderMoveAlert')),