feat(merge): 以官方 main 为基底融合 PR#530 多维表格与本地定制

- 多维表格(canvas-batch-table-node/use-canvas-batch-table/canvas-batch-table 等)以 PR#530 实现为主
- 保留官方 main 全部新功能:banner/画布版本历史/提示词编辑/方舟 Agent Plan/支付导出
- 移植本地定制:ddcat 多参考图校验、万有引力 lxmone 连接错误提示、Description v20(v24)、自建插件(a6api/metaso-h3/antigravity-proxy/lxmone)、seccomp、Windows 脚本
- 迁移版本升至 v24(官方 v20-23 banner/版本历史 + 我们的 Description 列)
- 修复官方缺失的 markdownPlainText import
This commit is contained in:
Codex committed 2026-09-18 14:09:50 +08:00
1 parent 4eaa28b807
commit 9b9a7f4a72
63 files changed
+9087 -892

No files matched your search

@@ -9,6 +9,7 @@ export type ChannelModelFormValues = {
providerModelKey?: string;
displayName?: string;
icon?: string;
description?: string;
capability: ModelCapabilityChoice;
protocol?: string;
priceTiers: PriceTierFormValues[];
@@ -31,6 +32,7 @@ export function initialChannelModelValues(item: ChannelModel | null, protocols:
providerModelKey: upstreamModel,
displayName: item?.displayName || "",
icon: item?.icon || "",
description: item?.description || "",
capability,
protocol,
priceTiers: item ? (item.priceTiers?.length ? item.priceTiers.map(priceTierToForm) : [legacyPriceTierToForm(item)]) : [defaultPriceTier()],
@@ -107,6 +107,7 @@ export function ChannelModelEditor({
providerModelKey: upstreamModel,
displayName: values.displayName?.trim() || values.modelKey.trim(),
icon: values.icon?.trim() || "",
description: values.description?.trim() || "",
capability: values.capability,
protocol: values.protocol,
priceTiers: values.priceTiers.map((tier) => ({
@@ -263,6 +264,9 @@ export function ChannelModelEditor({
<Form.Item name="icon" label="模型 Logo">
<ModelIconPicker />
</Form.Item>
<Form.Item name="description" label="模型说明" tooltip="展示在前台模型选择器里,用来说明该模型的能力边界与计费口径。">
<Input.TextArea rows={2} maxLength={600} showCount placeholder="例如:固定 720P,支持 5/10/15 秒,最多 9 张参考图,按次计费。" />
</Form.Item>
</div>
{tiersWithOwnUpstream.length ? (
<Alert
+28 -32
View File
@@ -68,7 +68,6 @@ import { CanvasZoomControls } from "@/components/canvas/canvas-zoom-controls";
import { CanvasShareModal } from "@/components/canvas/canvas-share-modal";
import { CanvasScriptEditor, CanvasScriptNodeContent } from "@/components/canvas/canvas-script-node";
import { CanvasBatchTableNodeContent } from "@/components/canvas/canvas-batch-table-node";
import { promoteLegacyBatchTableSize } from "@/lib/canvas/canvas-batch-table";
import { STORYBOARD_HEADER_HEIGHT, STORYBOARD_ROW_HEIGHT, storyboardMinNodeHeight, storyboardTableHeight } from "@/lib/canvas/canvas-storyboard-layout";
import { CanvasDirectorNodePanel } from "@/components/canvas/director/canvas-director-node-panel";
import { CanvasVersionCompareModal } from "@/components/canvas/canvas-version-compare-modal";
@@ -97,6 +96,7 @@ import { createCanvasNode, getInputSummary, isHiddenBatchChild } from "@/lib/can
import { stampCanvasNodeChanges, updateCanvasNode, updateCanvasNodes } from "@/lib/canvas/canvas-node-timestamps";
import { canvasAssetHandoffAttempt, finalizeCanvasAssetHandoff, uninsertedCanvasAssetHandoffPayloads } from "@/lib/canvas/canvas-asset-handoff";
import { batchSourceRestriction } from "@/lib/canvas/canvas-batch-connection";
import { batchReferenceColumns } from "@/lib/canvas/canvas-batch-table";
import { deriveStoryboardPipelineProgress } from "@/lib/canvas/canvas-storyboard-progress";
import { CanvasOperationChangeToast, CanvasMergeStatusToast, CanvasUploadStatusToast } from "./canvas-project-feedback";
import { backendProviderConfig, getGenerationCount } from "@/lib/canvas/canvas-project-generation";
@@ -1888,7 +1888,7 @@ function InfiniteCanvasPage() {
handleGenerateNode,
});
const { addReferenceColumn: addBatchReferenceColumn, addRow: addBatchRow, fillRowsFromConnections, generateRows: generateBatchRows, moveReferenceCell: moveBatchReferenceCell, patchTable: patchBatchTable, removeReferenceColumn: removeBatchReferenceColumn, removeRow: removeBatchRow, reorderReferenceColumns: reorderBatchReferenceColumns, syncRowsFromConnections, updateRow: updateBatchRow } = useCanvasBatchTable({
const { addReferenceColumn: addBatchReferenceColumn, addTextColumn: addBatchTextColumn, addRow: addBatchRow, fillRowsFromConnections, generateRows: generateBatchRows, moveReferenceCell: moveBatchReferenceCell, patchTable: patchBatchTable, removeRow: removeBatchRow, reorderReferenceColumns: reorderBatchReferenceColumns, syncRowsFromConnections, updateRow: updateBatchRow } = useCanvasBatchTable({
nodesRef,
connectionsRef,
setNodes,
@@ -1899,19 +1899,27 @@ function InfiniteCanvasPage() {
useEffect(() => {
if (!projectLoaded) return;
setNodes((current) => {
let changed = false;
const next = current.map((node) => {
const promoted = promoteLegacyBatchTableSize(node);
if (promoted !== node) changed = true;
return promoted;
});
return changed ? next : current;
});
nodesRef.current.filter((node) => node.type === CanvasNodeType.BatchTable).forEach((node) => {
syncRowsFromConnections(node.id, true);
});
}, [connections, projectLoaded, setNodes, syncRowsFromConnections]);
}, [connections, projectLoaded, syncRowsFromConnections]);
const handleUploadBatchReference = useCallback(async (tableNodeId: string, rowId: string, columnIndex: number, file: File) => {
const tableNode = nodesRef.current.find((item) => item.id === tableNodeId);
const table = tableNode?.metadata?.batchTable;
const row = table?.rows.find((item) => item.id === rowId);
if (!tableNode || !table || !row) return;
const rowIndex = table.rows.findIndex((item) => item.id === rowId);
const uploadedNodeId = await createFileNode(file, {
x: tableNode.position.x - 240,
y: tableNode.position.y + 170 + Math.max(0, rowIndex) * 220,
});
if (!uploadedNodeId) return;
const inputNodeIds = Array.from({ length: Math.max(batchReferenceColumns(table).length, columnIndex + 1) }, (_, index) => row.inputNodeIds[index] || "");
inputNodeIds[columnIndex] = uploadedNodeId;
updateBatchRow(tableNodeId, rowId, { inputNodeIds });
message.success(`已上传并填入参考图 ${columnIndex + 1}`);
}, [createFileNode, message, nodesRef, updateBatchRow]);
const { addScriptRow, createAndGenerateScriptVideos, createScriptActionBoards, createScriptImageNodes, createScriptVideoNodes, generateScriptImages, generateScriptRows, generateScriptVideos, removeScriptRow, replaceScriptRows, updateScriptRow } =
useCanvasStoryboard({
@@ -2123,26 +2131,11 @@ function InfiniteCanvasPage() {
onGenerate={(rowIds) => void generateBatchRows(contentNode.id, rowIds)}
onRetryItem={(batchId, itemId) => retryFailedBatchItems(contentNode.id, batchId, itemId)}
onAddReferenceColumn={() => addBatchReferenceColumn(contentNode.id)}
onRemoveReferenceColumn={() => removeBatchReferenceColumn(contentNode.id)}
onFocusOutput={(nodeId) => focusCanvasImageNode(nodeId)}
onAddTextColumn={() => addBatchTextColumn(contentNode.id)}
onReorderReferenceColumns={(fromColumnId, toColumnId) => reorderBatchReferenceColumns(contentNode.id, fromColumnId, toColumnId)}
onMoveReferenceCell={(sourceRowId, sourceColumnIndex, targetRowId, targetColumnIndex) => moveBatchReferenceCell(contentNode.id, sourceRowId, sourceColumnIndex, targetRowId, targetColumnIndex)}
onUploadReference={(rowId, columnIndex, file) => {
const row = contentNode.metadata?.batchTable?.rows.find((item) => item.id === rowId);
const existingId = row?.inputNodeIds[columnIndex];
if (existingId) {
void replaceNodeMedia(existingId, file);
return;
}
void createFileNode(file, { x: contentNode.position.x - 180, y: contentNode.position.y + columnIndex * 90 }).then((insertedId) => {
if (!insertedId) return;
const currentRow = nodesRef.current.find((item) => item.id === contentNode.id)?.metadata?.batchTable?.rows.find((item) => item.id === rowId);
const ids = [...(currentRow?.inputNodeIds || [])];
while (ids.length <= columnIndex) ids.push("");
ids[columnIndex] = insertedId;
updateBatchRow(contentNode.id, rowId, { inputNodeIds: ids });
});
}}
onReplaceReference={replaceCanvasNodeMedia}
onUploadReference={(rowId, columnIndex, file) => { void handleUploadBatchReference(contentNode.id, rowId, columnIndex, file); }}
onConnectStart={(event, handleId) => handleConnectStart(event, contentNode.id, "target", handleId)}
onConnectDrop={(event, handleId) => handleConnectDrop(event, contentNode.id, handleId)}
/>
@@ -2217,6 +2210,7 @@ function InfiniteCanvasPage() {
},
[
addBatchReferenceColumn,
addBatchTextColumn,
addBatchRow,
addScriptRow,
configInputsById,
@@ -2227,11 +2221,12 @@ function InfiniteCanvasPage() {
createScriptVideoNodes,
currentProject?.directorScenes,
fillRowsFromConnections,
focusCanvasImageNode,
generateBatchRows,
generateScriptImages,
generateScriptRows,
generateScriptVideos,
handleUploadBatchReference,
moveBatchReferenceCell,
handleConfigNodeChange,
handleConnectDrop,
handleConnectStart,
@@ -2242,8 +2237,9 @@ function InfiniteCanvasPage() {
openDirectorWorkbench,
openStoryInput,
patchBatchTable,
removeBatchReferenceColumn,
removeBatchRow,
reorderBatchReferenceColumns,
replaceCanvasNodeMedia,
removeScriptRow,
retryFailedBatchItems,
runningNodeId,
+1 -1
View File
@@ -238,7 +238,7 @@ export default function SharedCanvasPage() {
}));
const renderSharedNode = useCallback((node: CanvasNodeData): ReactNode => {
if (node.type === CanvasNodeType.Script) return <SharedScriptNode node={node} onUnauthorized={unauthorized} />;
if (node.type === CanvasNodeType.BatchTable) return <CanvasBatchTableNodeContent node={node} nodes={nodes} connections={connections} batch={node.metadata?.generationBatches?.at(-1)} theme={theme} readOnly onPatchTable={() => {}} onAddRow={() => {}} onRemoveRow={() => {}} onUpdateRow={() => {}} onFillRows={() => {}} onGenerate={() => {}} onRetryItem={() => {}} onAddReferenceColumn={() => {}} onConnectStart={() => {}} />;
if (node.type === CanvasNodeType.BatchTable) return <CanvasBatchTableNodeContent node={node} nodes={nodes} connections={connections} batch={node.metadata?.generationBatches?.at(-1)} theme={theme} readOnly onPatchTable={() => {}} onAddRow={() => {}} onRemoveRow={() => {}} onUpdateRow={() => {}} onFillRows={() => {}} onGenerate={() => {}} onRetryItem={() => {}} onAddReferenceColumn={() => {}} onAddTextColumn={() => {}} onReorderReferenceColumns={() => {}} onMoveReferenceCell={() => {}} onReplaceReference={() => {}} onUploadReference={() => {}} onConnectStart={() => {}} />;
return <SharedConfigNode node={node} onUnauthorized={unauthorized} />;
}, [connections, nodes, theme, unauthorized]);
const toolbarNodeKey = selectedNodeId;
+33 -31
View File
@@ -3,7 +3,7 @@ import { App } from "antd";
import { nanoid } from "nanoid";
import { NODE_DEFAULT_SIZE } from "@/constant/canvas";
import { batchInputColumns, batchPromptForRow, batchReferenceColumns, batchReferenceHandleId, createBatchRowsFromColumns, createInheritedBatchRow, moveBatchReferenceCell, removeLastBatchReferenceColumn, reorderBatchReferenceColumns } from "@/lib/canvas/canvas-batch-table";
import { batchInputColumns, batchPromptForRow, batchReferenceColumns, batchTextInputColumns, createBatchRow, createBatchRowsFromColumns, moveBatchReferenceCell, reorderBatchReferenceColumns } from "@/lib/canvas/canvas-batch-table";
import { createCanvasNode } from "@/lib/canvas/canvas-project-domain";
import { buildGenerationConfig, resetGenerationTaskMetadata } from "@/lib/canvas/canvas-project-generation";
import { navigateToSettings } from "@/lib/settings-navigation";
@@ -25,7 +25,7 @@ export function useCanvasBatchTable({ nodesRef, connectionsRef, setNodes, setCon
const isAiConfigReady = useConfigStore((state) => state.isAiConfigReady);
const patchTable = useCallback((nodeId: string, patch: Partial<CanvasBatchTableData>) => {
setNodes((current) => current.map((node) => node.id !== nodeId ? node : { ...node, metadata: { ...node.metadata, batchTable: { operation: "try_on", concurrency: 10, rows: [], ...node.metadata?.batchTable, ...patch } } }));
setNodes((current) => current.map((node) => node.id !== nodeId ? node : { ...node, metadata: { ...node.metadata, batchTable: { operation: "try_on", concurrency: 3, rows: [], ...node.metadata?.batchTable, ...patch } } }));
}, [setNodes]);
const updateRow = useCallback((nodeId: string, rowId: string, patch: Partial<CanvasBatchRow>) => {
@@ -37,7 +37,7 @@ export function useCanvasBatchTable({ nodesRef, connectionsRef, setNodes, setCon
const addRow = useCallback((nodeId: string) => {
const table = nodesRef.current.find((item) => item.id === nodeId)?.metadata?.batchTable;
if (!table) return;
patchTable(nodeId, { rows: [...table.rows, createInheritedBatchRow(table.operation, table.rows)] });
patchTable(nodeId, { rows: [...table.rows, createBatchRow(table.operation)] });
}, [nodesRef, patchTable]);
const removeRow = useCallback((nodeId: string, rowId: string) => {
@@ -55,19 +55,13 @@ export function useCanvasBatchTable({ nodesRef, connectionsRef, setNodes, setCon
patchTable(nodeId, { referenceColumns: [...columns, { id: `reference-${nanoid()}`, label: `参考图 ${nextIndex}` }] });
}, [message, nodesRef, patchTable]);
const removeReferenceColumn = useCallback((nodeId: string) => {
const addTextColumn = useCallback((nodeId: string) => {
const table = nodesRef.current.find((item) => item.id === nodeId)?.metadata?.batchTable;
if (!table) return;
const columns = batchReferenceColumns(table);
const nextTable = removeLastBatchReferenceColumn(table);
if (!nextTable) return message.info("至少保留 1 组参考图");
const removed = columns.at(-1);
patchTable(nodeId, nextTable);
if (removed) {
const handleId = batchReferenceHandleId(removed.id);
setConnections((current) => current.filter((connection) => !(connection.toNodeId === nodeId && connection.toHandleId === handleId)));
}
}, [message, nodesRef, patchTable, setConnections]);
const columns = table.textColumns || [];
if (columns.length >= 4) return message.info("最多支持 4 组文字");
patchTable(nodeId, { textColumns: [...columns, { id: `text-${nanoid()}`, label: `文字 ${columns.length + 1}` }] });
}, [message, nodesRef, patchTable]);
const syncRowsFromConnections = useCallback((nodeId: string, silent = false) => {
const node = nodesRef.current.find((item) => item.id === nodeId);
@@ -83,16 +77,25 @@ export function useCanvasBatchTable({ nodesRef, connectionsRef, setNodes, setCon
return false;
}
const rows = createBatchRowsFromColumns(table.operation, columns, table.rows);
const textColumns = batchTextInputColumns(node, connectionsRef.current).map((column) => column.filter((inputNodeId) => {
const input = nodeById.get(inputNodeId);
return input?.type === CanvasNodeType.Text && Boolean(input.metadata?.content || input.metadata?.prompt);
}));
const rowsWithText = rows.map((row, index) => ({
...row,
textNodeIds: textColumns.flatMap((column) => {
const input = column.length === 1 ? column[0] : column[index];
return input ? [input] : [];
}),
}));
if (!rows.length) {
if (!silent) message.warning("批量换装至少需要一张人物图和一张服装图");
return false;
}
if (JSON.stringify(rows) === JSON.stringify(table.rows)) return true;
patchTable(nodeId, { rows });
if (!silent) {
const added = Math.max(0, rows.length - table.rows.length);
message.success(added ? `已同步连线并新增 ${added} 行,原有任务均已保留` : "已同步最新连线,原有任务均已保留");
}
const rowsChanged = JSON.stringify(table.rows) !== JSON.stringify(rowsWithText);
if (!rowsChanged) return false;
patchTable(nodeId, { rows: rowsWithText });
if (!silent) message.success(`已按连线创建 ${rows.length} 行任务`);
return true;
}, [connectionsRef, message, nodesRef, patchTable]);
@@ -126,15 +129,11 @@ export function useCanvasBatchTable({ nodesRef, connectionsRef, setNodes, setCon
const activeNodeIds = new Set((sourceNode.metadata?.generationBatches || []).filter((batch) => batch.mode === "batch_image").flatMap((batch) => batch.items.filter((item) => ["waiting", "submitting", "queued", "running"].includes(item.status)).map((item) => item.nodeId)));
const requested = requestedRowIds?.length ? new Set(requestedRowIds) : null;
const rows = table.rows.filter((row) => {
const inputNodeIds = row.inputNodeIds.filter(Boolean);
if (!row.enabled || (requested && !requested.has(row.id)) || !batchPromptForRow(table, row).trim()) return false;
if (table.operation === "try_on" && inputNodeIds.length < 2) return false;
if (!inputNodeIds.length || inputNodeIds.some((id) => !nodesRef.current.some((node) => node.id === id && node.type === CanvasNodeType.Image && Boolean(node.metadata?.content || node.metadata?.storageKey)))) return false;
if (row.enabled === false || (requested && !requested.has(row.id)) || !batchPromptForRow(table, row).trim()) return false;
if (table.operation === "try_on" && row.inputNodeIds.length < 2) return false;
if (!row.inputNodeIds.length || row.inputNodeIds.some((id) => !nodesRef.current.some((node) => node.id === id && node.type === CanvasNodeType.Image && Boolean(node.metadata?.content || node.metadata?.storageKey)))) return false;
const output = row.outputNodeId ? nodesRef.current.find((node) => node.id === row.outputNodeId) : undefined;
if (output && activeNodeIds.has(output.id)) return false;
// 操作列的单行生成允许对已完成结果重新生成;顶部批量按钮仍只提交未完成项。
if (requested) return true;
return !output?.metadata?.content;
return !output?.metadata?.content && (!output || !activeNodeIds.has(output.id));
});
if (!rows.length) return message.info("没有可提交的未完成任务,请检查参考图和提示词");
const confirmed = await new Promise<boolean>((resolve) => modal.confirm({
@@ -159,7 +158,10 @@ export function useCanvasBatchTable({ nodesRef, connectionsRef, setNodes, setCon
const metadata = {
...(existingIndex >= 0 ? resetGenerationTaskMetadata(nextNodes[existingIndex].metadata) : {}),
prompt,
composerContent: prompt,
composerContent: [prompt, ...(row.textNodeIds || []).map((textNodeId) => {
const textNode = nodesRef.current.find((node) => node.id === textNodeId);
return textNode?.metadata?.content || textNode?.metadata?.prompt || "";
})].filter(Boolean).join("\n\n"),
model: buildGenerationConfig(effectiveConfig, undefined, "image").model,
generationMode: "image" as const,
generationType: "edit" as const,
@@ -178,7 +180,7 @@ export function useCanvasBatchTable({ nodesRef, connectionsRef, setNodes, setCon
if (existingIndex >= 0) nextNodes[existingIndex] = output;
else nextNodes.push(output);
nextConnections = nextConnections.filter((connection) => connection.toNodeId !== output.id);
row.inputNodeIds.filter(Boolean).forEach((inputNodeId) => nextConnections.push({ id: nanoid(), fromNodeId: inputNodeId, toNodeId: output.id }));
row.inputNodeIds.forEach((inputNodeId) => nextConnections.push({ id: nanoid(), fromNodeId: inputNodeId, toNodeId: output.id }));
nextConnections.push({ id: nanoid(), fromNodeId: sourceNode.id, toNodeId: output.id, relation: "batch-output", storyboardRowId: row.id });
outputByRowId.set(row.id, output.id);
targets.push({ rowId: row.id, nodeId: output.id });
@@ -193,5 +195,5 @@ export function useCanvasBatchTable({ nodesRef, connectionsRef, setNodes, setCon
if (enqueueGenerationBatch(nodeId, "batch_image", targets, { concurrency: table.concurrency })) message.success(`${targets.length} 个任务已加入并发队列`);
}, [connectionsRef, effectiveConfig, enqueueGenerationBatch, isAiConfigReady, message, modal, nodesRef, setConnections, setNodes, setSelectedNodeIds]);
return { addReferenceColumn, addRow, fillRowsFromConnections, generateRows, moveReferenceCell, patchTable, removeReferenceColumn, removeRow, reorderReferenceColumns, syncRowsFromConnections, updateRow };
return { addReferenceColumn, addTextColumn, addRow, fillRowsFromConnections, generateRows, moveReferenceCell, patchTable, removeRow, reorderReferenceColumns, syncRowsFromConnections, updateRow };
}
+8 -2
View File
@@ -46,7 +46,6 @@ const NODE_STATUS_SUCCESS = "success" as const;
const BATCH_UPLOAD_COLUMNS = 3;
const BATCH_UPLOAD_COLUMN_GAP = 380;
const BATCH_UPLOAD_ROW_GAP = 300;
const CANVAS_BATCH_TABLE_SELECTOR = "[data-canvas-batch-table]";
function isBatchTableDragEvent(event: DragEvent<HTMLElement>) {
@@ -544,11 +543,13 @@ export function useCanvasUpload({
const handleDrop = useCallback((event: DragEvent<HTMLDivElement>) => {
if (isBatchTableDragEvent(event)) {
event.preventDefault();
event.stopPropagation();
fileDragDepthRef.current = 0;
setFileDropActive(false);
return;
}
event.preventDefault();
event.stopPropagation();
fileDragDepthRef.current = 0;
setFileDropActive(false);
const chapterPayload = parseProjectChapterPayload(event.dataTransfer.getData(CANVAS_PROJECT_CHAPTER_DND_TYPE));
@@ -590,12 +591,14 @@ export function useCanvasUpload({
const handleFileDragEnter = useCallback((event: DragEvent<HTMLDivElement>) => {
if (isBatchTableDragEvent(event)) {
event.preventDefault();
event.stopPropagation();
fileDragDepthRef.current = 0;
setFileDropActive(false);
return;
}
if (!hasDraggedFiles(event)) return;
event.preventDefault();
event.stopPropagation();
fileDragDepthRef.current += 1;
setFileDropActive(true);
}, []);
@@ -603,18 +606,21 @@ export function useCanvasUpload({
const handleFileDragOver = useCallback((event: DragEvent<HTMLDivElement>) => {
if (isBatchTableDragEvent(event)) {
event.preventDefault();
event.stopPropagation();
fileDragDepthRef.current = 0;
setFileDropActive(false);
return;
}
if (!hasDraggedFiles(event) && !Array.from(event.dataTransfer.types).includes(CANVAS_PROJECT_CHAPTER_DND_TYPE)) return;
event.preventDefault();
event.stopPropagation();
event.dataTransfer.dropEffect = "copy";
}, []);
const handleFileDragLeave = useCallback((event: DragEvent<HTMLDivElement>) => {
if (isBatchTableDragEvent(event)) {
event.preventDefault();
event.stopPropagation();
fileDragDepthRef.current = 0;
setFileDropActive(false);
return;
@@ -732,6 +738,7 @@ export function useCanvasUpload({
return {
assetPickerOpen,
closeAssetPicker,
createFileNode,
createVideoNodeFromBlob,
createAssetPayloadNode,
createImageAssetNode,
@@ -752,7 +759,6 @@ export function useCanvasUpload({
pasteAssistantImage,
pasteSystemClipboard,
replaceNodeMedia,
createFileNode,
startUploadStatus,
uploadModalOpen,
uploadStatus,