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')),