Skip to content

Commit 7898f76

Browse files
fix: keep flow connections intact when importing multiple flows (activepieces#10820)
1 parent fdc5a74 commit 7898f76

9 files changed

Lines changed: 235 additions & 74 deletions

File tree

packages/react-ui/src/app/components/sidebar/builder/flows-navigation.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
SidebarSkeleton,
2424
} from '@/components/ui/sidebar-shadcn';
2525
import { flowHooks } from '@/features/flows/lib/flow-hooks';
26+
import { ImportFlowButton } from '@/features/flows/lib/Import-flow-button';
2627
import { NewFlowButton } from '@/features/flows/lib/new-flow-button';
2728
import { CreateFolderDialog } from '@/features/folders/component/create-folder-dialog';
2829
import { FolderActions } from '@/features/folders/component/folder-actions';
@@ -236,6 +237,12 @@ function DefaultFolder({
236237
<Shapes className="size-3.5!" />
237238
<span>{t('Uncategorized')}</span>
238239
<div className="ml-auto relative">
240+
<ImportFlowButton
241+
folderId={UncategorizedFolderId}
242+
variant="small"
243+
className="opacity-0 group-hover/item:opacity-100"
244+
onRefresh={refetch}
245+
/>
239246
<NewFlowButton
240247
folderId={UncategorizedFolderId}
241248
variant="small"
@@ -292,6 +299,12 @@ function RegularFolder({
292299
<FolderOpen className="size-3.5! hidden group-data-[state=open]/collapsible:block" />
293300
<span className="truncate">{folder.displayName}</span>
294301
<div className="flex items-center justify-center ml-auto">
302+
<ImportFlowButton
303+
folderId={folder.id}
304+
variant="small"
305+
className="group-hover/item:opacity-100 opacity-0"
306+
onRefresh={refetch}
307+
/>
295308
<NewFlowButton
296309
folderId={folder.id}
297310
variant="small"

packages/react-ui/src/app/routes/templates/id/use-template-dialog.tsx

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,12 @@ import {
2222
SelectTrigger,
2323
SelectValue,
2424
} from '@/components/ui/select';
25-
import { flowsApi } from '@/features/flows/lib/flows-api';
25+
import { flowHooks } from '@/features/flows/lib/flow-hooks';
2626
import { foldersApi } from '@/features/folders/lib/folders-api';
2727
import { foldersHooks } from '@/features/folders/lib/folders-hooks';
2828
import { projectCollectionUtils } from '@/hooks/project-collection';
2929
import { authenticationSession } from '@/lib/authentication-session';
3030
import {
31-
FlowOperationType,
3231
PopulatedFlow,
3332
Template,
3433
UncategorizedFolderId,
@@ -87,24 +86,11 @@ export const UseTemplateDialog = ({
8786
folderName = folder.displayName;
8887
}
8988

90-
return Promise.all(
91-
flows.map(async (flowTemplate) => {
92-
const newFlow = await flowsApi.create({
93-
displayName: flowTemplate.displayName,
94-
projectId: projectId,
95-
folderName: folderName,
96-
});
97-
98-
return flowsApi.update(newFlow.id, {
99-
type: FlowOperationType.IMPORT_FLOW,
100-
request: {
101-
displayName: flowTemplate.displayName,
102-
trigger: flowTemplate.trigger,
103-
schemaVersion: flowTemplate.schemaVersion,
104-
},
105-
});
106-
}),
107-
);
89+
return await flowHooks.importFlowsFromTemplates({
90+
templates: [template],
91+
projectId,
92+
folderName,
93+
});
10894
},
10995
onSuccess: (flows) => {
11096
onOpenChange(false);

packages/react-ui/src/features/flows/components/import-flow-dialog.tsx

Lines changed: 20 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ import { templatesApi } from '@/features/templates/lib/templates-api';
3636
import { api } from '@/lib/api';
3737
import { authenticationSession } from '@/lib/authentication-session';
3838
import {
39-
FlowOperationType,
4039
isNil,
4140
PopulatedFlow,
4241
TelemetryEventName,
@@ -45,7 +44,7 @@ import {
4544
} from '@activepieces/shared';
4645

4746
import { FormError } from '../../../components/ui/form';
48-
import { flowsApi } from '../lib/flows-api';
47+
import { flowHooks } from '../lib/flow-hooks';
4948
import { templateUtils } from '../lib/template-parser';
5049

5150
export type ImportFlowDialogProps =
@@ -97,40 +96,27 @@ const ImportFlowDialog = (
9796
Template[]
9897
>({
9998
mutationFn: async (templates: Template[]) => {
100-
const importPromises = templates.flatMap(async (template) => {
101-
const flowImportPromises = (template.flows || []).map(
102-
async (templateFlow) => {
103-
let flow: PopulatedFlow | null = null;
104-
if (props.insideBuilder) {
105-
flow = await flowsApi.get(props.flowId);
106-
} else {
107-
const folder =
108-
!isNil(selectedFolderId) &&
109-
selectedFolderId !== UncategorizedFolderId
110-
? await foldersApi.get(selectedFolderId)
111-
: undefined;
112-
flow = await flowsApi.create({
113-
displayName: templateFlow.displayName,
114-
projectId: authenticationSession.getProjectId()!,
115-
folderName: folder?.displayName,
116-
});
117-
}
118-
return await flowsApi.update(flow.id, {
119-
type: FlowOperationType.IMPORT_FLOW,
120-
request: {
121-
displayName: templateFlow.displayName,
122-
trigger: templateFlow.trigger,
123-
schemaVersion: templateFlow.schemaVersion,
124-
},
125-
});
126-
},
127-
);
99+
if (props.insideBuilder) {
100+
if (templates.length === 0) {
101+
throw new Error('No template selected');
102+
}
103+
const flow = await flowHooks.importFlowIntoExisting({
104+
template: templates[0],
105+
existingFlowId: props.flowId,
106+
});
107+
return [flow];
108+
}
128109

129-
return Promise.all(flowImportPromises);
130-
});
110+
const folder =
111+
!isNil(selectedFolderId) && selectedFolderId !== UncategorizedFolderId
112+
? await foldersApi.get(selectedFolderId)
113+
: undefined;
131114

132-
const results = await Promise.all(importPromises);
133-
return results.flat();
115+
return flowHooks.importFlowsFromTemplates({
116+
templates,
117+
projectId: authenticationSession.getProjectId()!,
118+
folderName: folder?.displayName,
119+
});
134120
},
135121

136122
onSuccess: (flows: PopulatedFlow[]) => {

packages/react-ui/src/features/flows/components/share-template-dialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ const ShareTemplateDialog: React.FC<{
6161
summary: template.summary,
6262
tags: template.tags,
6363
blogUrl: template.blogUrl ?? undefined,
64-
metadata: null,
64+
metadata: template.metadata,
6565
author,
6666
categories: template.categories,
6767
type: template.type,
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { t } from 'i18next';
2+
import { Upload } from 'lucide-react';
3+
4+
import { PermissionNeededTooltip } from '@/components/custom/permission-needed-tooltip';
5+
import { useEmbedding } from '@/components/embed-provider';
6+
import { Button } from '@/components/ui/button';
7+
import {
8+
Tooltip,
9+
TooltipContent,
10+
TooltipTrigger,
11+
} from '@/components/ui/tooltip';
12+
import { useAuthorization } from '@/hooks/authorization-hooks';
13+
import { cn } from '@/lib/utils';
14+
import { Permission } from '@activepieces/shared';
15+
16+
import { ImportFlowDialog } from '../components/import-flow-dialog';
17+
18+
type ImportFlowButtonProps = {
19+
variant?: 'default' | 'small';
20+
className?: string;
21+
folderId: string;
22+
onRefresh?: () => void;
23+
};
24+
25+
export const ImportFlowButton = ({
26+
variant = 'default',
27+
className,
28+
folderId,
29+
onRefresh,
30+
}: ImportFlowButtonProps) => {
31+
const { checkAccess } = useAuthorization();
32+
const { embedState } = useEmbedding();
33+
const doesUserHavePermissionToWriteFlow = checkAccess(Permission.WRITE_FLOW);
34+
35+
if (embedState.hideExportAndImportFlow) {
36+
return null;
37+
}
38+
39+
return (
40+
<PermissionNeededTooltip hasPermission={doesUserHavePermissionToWriteFlow}>
41+
<Tooltip delayDuration={100}>
42+
<ImportFlowDialog
43+
insideBuilder={false}
44+
onRefresh={() => {
45+
if (onRefresh) onRefresh();
46+
}}
47+
folderId={folderId}
48+
>
49+
<TooltipTrigger asChild>
50+
<Button
51+
disabled={!doesUserHavePermissionToWriteFlow}
52+
variant={variant === 'small' ? 'ghost' : 'outline'}
53+
size={variant === 'small' ? 'icon' : 'default'}
54+
className={cn(
55+
variant === 'small' ? '!bg-transparent' : '',
56+
className,
57+
)}
58+
data-testid="import-flow-button"
59+
>
60+
{variant === 'small' ? (
61+
<Upload className="h-4 w-4" />
62+
) : (
63+
<>
64+
<Upload className="h-4 w-4 mr-2" />
65+
<span>{t('Import')}</span>
66+
</>
67+
)}
68+
</Button>
69+
</TooltipTrigger>
70+
</ImportFlowDialog>
71+
<TooltipContent side={variant === 'small' ? 'right' : 'bottom'}>
72+
{t('Import flow')}
73+
</TooltipContent>
74+
</Tooltip>
75+
</PermissionNeededTooltip>
76+
);
77+
};

packages/react-ui/src/features/flows/lib/flow-hooks.tsx

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
FlowStatus,
2222
FlowVersion,
2323
FlowVersionMetadata,
24+
FlowVersionTemplate,
2425
ListFlowsRequest,
2526
PopulatedFlow,
2627
FlowTrigger,
@@ -30,6 +31,7 @@ import {
3031
isNil,
3132
ErrorCode,
3233
SeekPage,
34+
Template,
3335
UncategorizedFolderId,
3436
} from '@activepieces/shared';
3537

@@ -333,6 +335,112 @@ export const flowHooks = {
333335
},
334336
});
335337
},
338+
importFlowIntoExisting: async ({
339+
template,
340+
existingFlowId,
341+
}: {
342+
template: Template;
343+
existingFlowId: string;
344+
}): Promise<PopulatedFlow> => {
345+
const flows = template.flows || [];
346+
if (flows.length === 0) {
347+
throw new Error('Template has no flows');
348+
}
349+
350+
const templateFlow = flows[0];
351+
const flow = await flowsApi.get(existingFlowId);
352+
353+
const oldExternalId = !isNil(template.metadata?.externalId)
354+
? (template.metadata['externalId'] as string)
355+
: flow.externalId;
356+
357+
const triggerString = JSON.stringify(templateFlow.trigger).replaceAll(
358+
oldExternalId,
359+
flow.externalId,
360+
);
361+
const updatedTrigger = JSON.parse(triggerString);
362+
363+
return await flowsApi.update(flow.id, {
364+
type: FlowOperationType.IMPORT_FLOW,
365+
request: {
366+
displayName: templateFlow.displayName,
367+
trigger: updatedTrigger,
368+
schemaVersion: templateFlow.schemaVersion,
369+
},
370+
});
371+
},
372+
importFlowsFromTemplates: async ({
373+
templates,
374+
projectId,
375+
folderName,
376+
}: {
377+
templates: Template[];
378+
projectId: string;
379+
folderName?: string;
380+
}): Promise<PopulatedFlow[]> => {
381+
if (templates.length === 0) {
382+
return [];
383+
}
384+
385+
const allFlowsToImport: Array<{
386+
flow: PopulatedFlow;
387+
templateFlow: FlowVersionTemplate;
388+
oldExternalId: string;
389+
}> = [];
390+
391+
for (const template of templates) {
392+
const flows = template.flows || [];
393+
if (flows.length === 0) {
394+
continue;
395+
}
396+
397+
for (const templateFlow of flows) {
398+
const flow = await flowsApi.create({
399+
displayName: templateFlow.displayName,
400+
projectId,
401+
folderName,
402+
});
403+
404+
const oldExternalId = !isNil(template.metadata?.externalId)
405+
? (template.metadata['externalId'] as string)
406+
: flow.externalId;
407+
408+
allFlowsToImport.push({
409+
flow,
410+
templateFlow,
411+
oldExternalId,
412+
});
413+
}
414+
}
415+
416+
const externalIdMap = new Map<string, string>();
417+
for (const { oldExternalId, flow } of allFlowsToImport) {
418+
externalIdMap.set(oldExternalId, flow.externalId);
419+
}
420+
421+
const importPromises = allFlowsToImport.map(
422+
async ({ flow, templateFlow }) => {
423+
let triggerString = JSON.stringify(templateFlow.trigger);
424+
425+
for (const [oldId, newId] of externalIdMap.entries()) {
426+
triggerString = triggerString.replaceAll(oldId, newId);
427+
}
428+
429+
const updatedTrigger = JSON.parse(triggerString);
430+
431+
return await flowsApi.update(flow.id, {
432+
type: FlowOperationType.IMPORT_FLOW,
433+
request: {
434+
displayName: templateFlow.displayName,
435+
trigger: updatedTrigger,
436+
schemaVersion: templateFlow.schemaVersion,
437+
},
438+
});
439+
},
440+
);
441+
442+
return await Promise.all(importPromises);
443+
},
336444
};
337445

338446
type UseChangeFlowStatusParams = {

packages/react-ui/src/features/flows/lib/use-flows-bulk-actions.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { MoveFlowDialog } from '../components/move-flow-dialog';
2525

2626
import { flowHooks } from './flow-hooks';
2727
import { flowsApi } from './flows-api';
28+
import { ImportFlowButton } from './Import-flow-button';
2829
import { NewFlowButton } from './new-flow-button';
2930

3031
export const useFlowsBulkActions = ({
@@ -177,6 +178,7 @@ export const useFlowsBulkActions = ({
177178
</ConfirmationDeleteDialog>
178179
</PermissionNeededTooltip>
179180
)}
181+
<ImportFlowButton folderId={folderId} onRefresh={refetch} />
180182
<NewFlowButton folderId={folderId} />
181183
</div>
182184
);

0 commit comments

Comments
 (0)