Skip to content
This repository was archived by the owner on Mar 31, 2026. It is now read-only.

Commit 16ded7b

Browse files
committed
feat: add export workflow and import workflow
1 parent deb814f commit 16ded7b

6 files changed

Lines changed: 469 additions & 17 deletions

File tree

packages/demo/pages/ui/+Page.tsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ export default function UI() {
119119

120120
const getTokenByclient = useCallback(async () => {
121121
const TOKEN =
122-
"Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6Ijg5QzAwNzc1RTlGNDhEQUZGN0QzQzZGMjBBNkZDQTdDN0FDMzNDQjIiLCJ4NXQiOiJpY0FIZGVuMGphXzMwOGJ5Q21fS2ZIckRQTEkiLCJ0eXAiOiJhdCtqd3QifQ.eyJpc3MiOiJodHRwczovL2F1dGgtc3RhdGlvbi1kZXYtc3RhZ2luZy5hZXZhdGFyLmFpLyIsImV4cCI6MTc2MTExNDQ1MCwiaWF0IjoxNzYwOTQxNjUxLCJhdWQiOiJBZXZhdGFyIiwic2NvcGUiOiJBZXZhdGFyIG9mZmxpbmVfYWNjZXNzIiwianRpIjoiNzVlNTAzNzQtMDAwYS00OWUzLWFlZDYtOGM0N2IwN2YzYzFjIiwic3ViIjoiNTdiYWIyMjAtOTVkZS1kNDYxLTFlODMtM2ExY2VlOGYwYzIzIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWZ2b2ppIiwiZW1haWwiOiJhZnZvamlAc25hcG1haWwuY2MiLCJyb2xlIjpbImJhc2ljVXNlciIsIjgwNmQzY2M2LTI2OTMtYTg0NS05Y2FhLTNhMWNlZThmNDY2ZV9Pd25lciIsIjVhN2VkOTI0LTM4NWYtMDA2YS04OWU2LTNhMWNlZThmNDU1NV9Pd25lciJdLCJwaG9uZV9udW1iZXJfdmVyaWZpZWQiOiJGYWxzZSIsImVtYWlsX3ZlcmlmaWVkIjoiRmFsc2UiLCJ1bmlxdWVfbmFtZSI6ImFmdm9qaSIsInNlY3VyaXR5X3N0YW1wIjoiR1NCNE4ySUIyTElBWk5CRzUyM0tGNVo0T0YyUUo3WDIiLCJvaV9wcnN0IjoiQWV2YXRhckF1dGhTZXJ2ZXIiLCJvaV9hdV9pZCI6ImFlNzQ5Yzk4LTg0YzgtODFmMy1lZTkzLTNhMWNlZThmMGUxMiIsImNsaWVudF9pZCI6IkFldmF0YXJBdXRoU2VydmVyIiwib2lfdGtuX2lkIjoiZGY1MTBmMWQtZWQ2ZS1kZmE5LTZlOTYtM2ExZDEyN2EwZjE1In0.LQp9Exri0cOToeXWrf8_eRl7CwBw0bCk1Wt5ypfIF-G6V7dG96I7jvaSRGub_a1EtQAz3yOZvqGyt9Qq-vayyzNITnFm05a2Ry4fkjCM_n7LS1HwDo3CRkkLkv0CsDKhL2-XI-2dmDXw9CX552UyUVC3ouqNY1VOsVATX1h29ZqozxInWHzm2MIN_KqOBDrWe36M1j5jaT0ZcHRxgIB7rr3PCw-IxrIsaUJUidVL1vuMiqRFmdtfse8F1tveeZLl6LP9UnrEsbjYnhAdguNEudUETmt4_NxYfO6zOKGJnhxvAWFE34Q7BmVuGULg2RURgDOTsUyEemmC3QNz62POZw";
122+
"Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6Ijg5QzAwNzc1RTlGNDhEQUZGN0QzQzZGMjBBNkZDQTdDN0FDMzNDQjIiLCJ4NXQiOiJpY0FIZGVuMGphXzMwOGJ5Q21fS2ZIckRQTEkiLCJ0eXAiOiJhdCtqd3QifQ.eyJpc3MiOiJodHRwczovL2F1dGgtc3RhdGlvbi1kZXYtc3RhZ2luZy5hZXZhdGFyLmFpLyIsImV4cCI6MTc2MTgwNTk2MywiaWF0IjoxNzYxNjMzMTY0LCJhdWQiOiJBZXZhdGFyIiwic2NvcGUiOiJBZXZhdGFyIG9mZmxpbmVfYWNjZXNzIiwianRpIjoiNzA2N2VkZWYtMGIwZi00MDNlLTkyNTQtNDBlZTBjNjJiMTdjIiwic3ViIjoiNTdiYWIyMjAtOTVkZS1kNDYxLTFlODMtM2ExY2VlOGYwYzIzIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWZ2b2ppIiwiZW1haWwiOiJhZnZvamlAc25hcG1haWwuY2MiLCJyb2xlIjpbImJhc2ljVXNlciIsIjgwNmQzY2M2LTI2OTMtYTg0NS05Y2FhLTNhMWNlZThmNDY2ZV9Pd25lciIsIjVhN2VkOTI0LTM4NWYtMDA2YS04OWU2LTNhMWNlZThmNDU1NV9Pd25lciJdLCJwaG9uZV9udW1iZXJfdmVyaWZpZWQiOiJGYWxzZSIsImVtYWlsX3ZlcmlmaWVkIjoiRmFsc2UiLCJ1bmlxdWVfbmFtZSI6ImFmdm9qaSIsInNlY3VyaXR5X3N0YW1wIjoiR1NCNE4ySUIyTElBWk5CRzUyM0tGNVo0T0YyUUo3WDIiLCJvaV9wcnN0IjoiQWV2YXRhckF1dGhTZXJ2ZXIiLCJvaV9hdV9pZCI6ImFlNzQ5Yzk4LTg0YzgtODFmMy1lZTkzLTNhMWNlZThmMGUxMiIsImNsaWVudF9pZCI6IkFldmF0YXJBdXRoU2VydmVyIiwib2lfdGtuX2lkIjoiNmRiNTQ2ZjQtZWE0My0wZDQyLTI5N2MtM2ExZDNiYjFiNGM3In0.Oe866mgfN96JSxM45iMa62dNosvYECMPSllj1cFPoC0DOxgNXZH9dFi77_fmAQAZgelHLFXHu8qy2NHEq-kKW22tQcbAvGxSQhnWgjAZ1xlWUTAp3kRybVJNNOlFm5G2EvLzTcTE9NhDf5Vm0_PQ3IgEFWxOdNwSM_11zCLLzesJeu6Yaz5ikFR2OVICbLPoYjSP0mK04edjFX0WVeZ-u19b-yfXXD4io3m2P-zztLL2McD42RLWR58qtEcEK-5qne5va6RU16qJyUjOWI1rtTzaVKPdJ_IkQaLRANSw6ch8VxNVO1mxyhzjdz4P6SWsXrhnoR7HQAnNA3phtim6bw";
123123
aevatarAI.fetchRequest.setHeaders({
124124
Authorization: TOKEN,
125125
});
@@ -268,12 +268,6 @@ export default function UI() {
268268
setStage(undefined);
269269
}}
270270
editWorkflow={editWorkflow}
271-
onDuplicateWorkflow={async (workflow) => {
272-
setStage(undefined);
273-
await sleep(1000);
274-
setEditWorkflow(workflow);
275-
onShowWorkflow();
276-
}}
277271
/>
278272
</FullScreen>
279273
)}

packages/ui-react/src/components/WorkflowList/index.tsx

Lines changed: 131 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
} from "react";
99
import { v4 as uuidv4 } from "uuid";
1010
import WorkflowListInner from "../WorkflowListInner";
11-
import { aevatarAI } from "../../utils";
11+
import { aevatarAI, validateWorkflowData } from "../../utils";
1212
import type {
1313
IAgentInfoDetail,
1414
IWorkflowCoordinatorState,
@@ -24,6 +24,7 @@ import Loading from "../../assets/svg/loading.svg?react";
2424
import { IS_NULL_ID } from "../../constants";
2525
import type { IWorkflowConfigurationState } from "../WorkflowConfiguration";
2626
import { useToastLoading } from "../../hooks/useToastLoading";
27+
import dayjs from "dayjs";
2728

2829
export interface IWorkflowListProps {
2930
className?: string;
@@ -260,10 +261,9 @@ export default forwardRef(function WorkflowList(
260261
}
261262
}, [onDelete, toast]);
262263

263-
const onDuplicateWorkflow = useCallback(
264-
async (workflow: IWorkflowCoordinatorState & IAgentInfoDetail) => {
265-
console.log("workflowId===", workflow);
266-
const randomId = uuidv4().replace(/-/g, '').substring(0, 6)
264+
const copyWorkflowViewData = useCallback(
265+
(workflow: IWorkflowCoordinatorState & IAgentInfoDetail) => {
266+
const randomId = uuidv4().replace(/-/g, "").substring(0, 6);
267267
const workflowName = `${workflow.name}_${randomId}`;
268268
const workflowNodeList = workflow.properties?.workflowNodeList?.map(
269269
(item) => {
@@ -282,6 +282,16 @@ export default forwardRef(function WorkflowList(
282282
name: workflowName,
283283
},
284284
};
285+
return workflowViewData;
286+
},
287+
[]
288+
);
289+
290+
const onDuplicateWorkflow = useCallback(
291+
async (workflow: IWorkflowCoordinatorState & IAgentInfoDetail) => {
292+
console.log("workflowId===", workflow);
293+
294+
const workflowViewData = copyWorkflowViewData(workflow);
285295
const { dismiss } = toastLoading("Copying workflow");
286296

287297
try {
@@ -297,7 +307,120 @@ export default forwardRef(function WorkflowList(
297307
});
298308
}
299309
},
300-
[onEditWorkflow, toastLoading, toast]
310+
[onEditWorkflow, toastLoading, toast, copyWorkflowViewData]
311+
);
312+
313+
const onExportWorkflow = useCallback(
314+
async (workflow: IWorkflowCoordinatorState & IAgentInfoDetail) => {
315+
const workflowViewData = copyWorkflowViewData(workflow);
316+
317+
try {
318+
// Convert workflow data to JSON string
319+
const jsonString = JSON.stringify(workflowViewData, null, 2);
320+
321+
// Create Blob and download link
322+
const blob = new Blob([jsonString], { type: "application/json" });
323+
const url = URL.createObjectURL(blob);
324+
const link = document.createElement("a");
325+
link.href = url;
326+
327+
// Generate filename with workflow name and timestamp
328+
const timestamp = dayjs().format("YYYYMMDDHHmmss");
329+
const filename = `${workflow.name}_${timestamp}.json`;
330+
link.download = filename;
331+
332+
// Trigger download
333+
document.body.appendChild(link);
334+
link.click();
335+
336+
// Cleanup
337+
document.body.removeChild(link);
338+
URL.revokeObjectURL(url);
339+
340+
// Show success toast
341+
toast({
342+
description: `Workflow exported successfully as ${filename}`,
343+
duration: 3000,
344+
});
345+
} catch (error) {
346+
toast({
347+
title: "error",
348+
description: handleErrorMessage(error, "Failed to export workflow."),
349+
duration: 3000,
350+
});
351+
}
352+
},
353+
[copyWorkflowViewData, toast]
354+
);
355+
356+
const onImportWorkflow = useCallback(
357+
async (file: File) => {
358+
try {
359+
// Read and parse file
360+
const text = await file.text();
361+
let workflowData: any;
362+
363+
try {
364+
workflowData = JSON.parse(text);
365+
} catch {
366+
toast({
367+
title: "error",
368+
description: "Invalid JSON file format. Please ensure the file is valid JSON.",
369+
duration: 3000,
370+
});
371+
return;
372+
}
373+
374+
// Validate workflow data structure
375+
const validation = validateWorkflowData(workflowData);
376+
if (!validation.isValid) {
377+
toast({
378+
title: "error",
379+
description: validation.error || "Invalid workflow file format.",
380+
duration: 3000,
381+
});
382+
return;
383+
}
384+
385+
// Show loading toast
386+
const { dismiss } = toastLoading("Importing workflow");
387+
388+
try {
389+
// Create workflow from imported data
390+
const result = await aevatarAI.services.workflow.createWorkflowViewData(
391+
workflowData as IWorkflowViewDataParams
392+
);
393+
dismiss();
394+
395+
// Show success toast and refresh list
396+
toast({
397+
description: `Workflow imported successfully as ${workflowData.name}`,
398+
duration: 3000,
399+
});
400+
401+
// Refresh workflow list
402+
await sleep(500);
403+
getWorkflowsLoop();
404+
405+
// Navigate to edit page if onEditWorkflow is provided
406+
if (result.id) onEditWorkflow?.(result.id);
407+
} catch (error) {
408+
dismiss();
409+
toast({
410+
title: "error",
411+
description: handleErrorMessage(error, "Failed to import workflow."),
412+
duration: 3000,
413+
});
414+
}
415+
} catch (error) {
416+
toast({
417+
title: "error",
418+
description: handleErrorMessage(error, "Failed to import workflow."),
419+
duration: 3000,
420+
});
421+
}
422+
},
423+
[toast, toastLoading, getWorkflowsLoop, onEditWorkflow]
301424
);
302425

303426
return (
@@ -310,6 +433,8 @@ export default forwardRef(function WorkflowList(
310433
onEditWorkflow={onEditWorkflow}
311434
onDeleteWorkflow={onDeleteWorkflow}
312435
onDuplicateWorkflow={onDuplicateWorkflow}
436+
onExportWorkflow={onExportWorkflow}
437+
onImportWorkflow={onImportWorkflow}
313438
/>
314439
<DeleteGAevatarConfirm
315440
open={deleteOpen}

packages/ui-react/src/components/WorkflowListInner/index.tsx

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import type {
1212
import { workflowColumns } from "./columns";
1313

1414
import { Select, SelectContent, SelectItem, SelectTrigger } from "../ui/select";
15-
import { Ellipsis } from "lucide-react";
15+
import { Ellipsis, Import } from "lucide-react";
16+
import Dropzone from "react-dropzone";
17+
import { toast } from "../../hooks/use-toast";
1618

1719
interface WorkflowListInnerProps {
1820
workflowsList: (IWorkflowCoordinatorState & IAgentInfoDetail)[];
@@ -25,7 +27,13 @@ interface WorkflowListInnerProps {
2527
) => void;
2628
onViewExecutions?: (workflowId: string) => void;
2729
onNewWorkflow: () => void;
28-
onDuplicateWorkflow: (workflow: IWorkflowCoordinatorState & IAgentInfoDetail) => void;
30+
onDuplicateWorkflow: (
31+
workflow: IWorkflowCoordinatorState & IAgentInfoDetail
32+
) => void;
33+
onExportWorkflow: (
34+
workflow: IWorkflowCoordinatorState & IAgentInfoDetail
35+
) => void;
36+
onImportWorkflow: (file: File) => void;
2937
}
3038

3139
const actionItemCls =
@@ -49,6 +57,7 @@ enum ActionType {
4957
ViewExecutions = "ViewExecutions",
5058
DuplicateWorkflow = "DuplicateWorkflow",
5159
DeleteWorkflow = "deleteWorkflow",
60+
ExportWorkflow = "exportWorkflow",
5261
}
5362

5463
export default function WorkflowListInner({
@@ -61,6 +70,8 @@ export default function WorkflowListInner({
6170
onViewExecutions,
6271
onNewWorkflow,
6372
onDuplicateWorkflow,
73+
onExportWorkflow,
74+
onImportWorkflow,
6475
}: WorkflowListInnerProps) {
6576
const [deleteItemId, setDeleteItemId] = useState<string | null>(null);
6677
const tableData = useMemo(
@@ -90,6 +101,8 @@ export default function WorkflowListInner({
90101
onViewExecutions?.(item.id);
91102
} else if (value === ActionType.DeleteWorkflow) {
92103
setDeleteItemId(item.id);
104+
} else if (value === ActionType.ExportWorkflow) {
105+
onExportWorkflow?.(item);
93106
}
94107
}}>
95108
<SelectTrigger
@@ -130,6 +143,14 @@ export default function WorkflowListInner({
130143
value={ActionType.DuplicateWorkflow}>
131144
Duplicate
132145
</SelectItem>
146+
<SelectItem
147+
className={clsx(
148+
actionItemCls,
149+
"aevatar-workflow-action-select-export"
150+
)}
151+
value={ActionType.ExportWorkflow}>
152+
Export
153+
</SelectItem>
133154
<SelectItem
134155
className={clsx(
135156
actionItemCls,
@@ -145,7 +166,13 @@ export default function WorkflowListInner({
145166
</div>
146167
),
147168
})),
148-
[workflowsList, onEditWorkflow, onViewExecutions, onDuplicateWorkflow]
169+
[
170+
workflowsList,
171+
onEditWorkflow,
172+
onViewExecutions,
173+
onDuplicateWorkflow,
174+
onExportWorkflow,
175+
]
149176
);
150177

151178
return (
@@ -163,7 +190,39 @@ export default function WorkflowListInner({
163190
id="node-6202_82361">
164191
<p>Workflows</p>
165192
</div>
166-
<div>
193+
<div className="sdk:flex sdk:items-center sdk:gap-[10px]">
194+
<Dropzone
195+
accept={{
196+
"application/json": [".json"],
197+
}}
198+
onDropAccepted={async (acceptedFiles) => {
199+
onImportWorkflow(acceptedFiles[0]);
200+
}}
201+
onDropRejected={(error) => {
202+
toast({
203+
description:
204+
error[0]?.errors?.[0]?.message ?? "Upload file error",
205+
});
206+
}}
207+
multiple={false}>
208+
{({ getRootProps, getInputProps }) => (
209+
<div
210+
{...getRootProps({
211+
className: clsx(
212+
"sdk:flex sdk:items-center sdk:justify-center sdk:cursor-pointer"
213+
),
214+
"data-testid": "dropzone-workflow-import-id",
215+
})}>
216+
<input {...getInputProps()} />
217+
<Button
218+
variant="outline"
219+
className="sdk:text-[12px] sdk:font-geist sdk:font-semibold sdk:flex sdk:items-center sdk:gap-[5px] sdk:cursor-pointer">
220+
<Import style={{ width: 14, height: 14 }} />
221+
Import Workflow
222+
</Button>
223+
</div>
224+
)}
225+
</Dropzone>
167226
<Button
168227
variant="primary"
169228
className="sdk:text-[12px] sdk:font-geist sdk:font-semibold sdk:flex sdk:items-center sdk:gap-[5px] sdk:cursor-pointer"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from "./aevatarAI";
22
export * from "./typeUtils";
3+
export * from "./workflowValidation";

0 commit comments

Comments
 (0)