diff --git a/apps/builder/components/dashboard/FolderContent/TypebotButton.tsx b/apps/builder/components/dashboard/FolderContent/TypebotButton.tsx index fa74a936ca..fe0f1b527a 100644 --- a/apps/builder/components/dashboard/FolderContent/TypebotButton.tsx +++ b/apps/builder/components/dashboard/FolderContent/TypebotButton.tsx @@ -16,11 +16,13 @@ import { isMobile } from 'services/utils' import { MoreButton } from 'components/dashboard/FolderContent/MoreButton' import { ConfirmModal } from 'components/modals/ConfirmModal' import { GripIcon } from 'assets/icons' -import { deleteTypebot, duplicateTypebot } from 'services/typebots' +import { deleteTypebot, importTypebot, getTypebot } from 'services/typebots' import { Typebot } from 'models' import { useTypebotDnd } from 'contexts/TypebotDndContext' import { useDebounce } from 'use-debounce' import { TypebotIcon } from 'components/shared/TypebotHeader/TypebotIcon' +import { useUser } from 'contexts/UserContext' +import { Plan } from 'db' type ChatbotCardProps = { typebot: Pick @@ -36,6 +38,7 @@ export const TypebotButton = ({ onMouseDown, }: ChatbotCardProps) => { const router = useRouter() + const { user } = useUser() const { draggedTypebot } = useTypebotDnd() const [draggedTypebotDebounced] = useDebounce(draggedTypebot, 200) const { @@ -71,7 +74,13 @@ export const TypebotButton = ({ const handleDuplicateClick = async (e: React.MouseEvent) => { e.stopPropagation() - const { data: createdTypebot, error } = await duplicateTypebot(typebot.id) + const { data } = await getTypebot(typebot.id) + const typebotToDuplicate = data?.typebot + if (!typebotToDuplicate) return { error: new Error('Typebot not found') } + const { data: createdTypebot, error } = await importTypebot( + data.typebot, + user?.plan ?? Plan.FREE + ) if (error) return toast({ title: "Couldn't duplicate typebot", diff --git a/apps/builder/components/templates/CreateNewTypebotButtons.tsx b/apps/builder/components/templates/CreateNewTypebotButtons.tsx index 62d20534de..5345d58c49 100644 --- a/apps/builder/components/templates/CreateNewTypebotButtons.tsx +++ b/apps/builder/components/templates/CreateNewTypebotButtons.tsx @@ -11,7 +11,7 @@ import { useUser } from 'contexts/UserContext' import { Typebot } from 'models' import { useRouter } from 'next/router' import React, { useState } from 'react' -import { importTypebot, createTypebot } from 'services/typebots' +import { createTypebot, importTypebot } from 'services/typebots' import { ImportTypebotFromFileButton } from './ImportTypebotFromFileButton' import { TemplatesModal } from './TemplatesModal' @@ -46,7 +46,7 @@ export const CreateNewTypebotButtons = () => { }, }, }, - user + user.plan ) : await createTypebot({ folderId, diff --git a/apps/builder/services/typebots/typebots.ts b/apps/builder/services/typebots/typebots.ts index 463b5b34b3..f826c7a480 100644 --- a/apps/builder/services/typebots/typebots.ts +++ b/apps/builder/services/typebots/typebots.ts @@ -48,11 +48,11 @@ import { } from 'utils' import { dequal } from 'dequal' import { stringify } from 'qs' -import { isChoiceInput, isConditionStep, sendRequest, omit } from 'utils' +import { isChoiceInput, isConditionStep, sendRequest } from 'utils' import cuid from 'cuid' import { diff } from 'deep-object-diff' import { duplicateWebhook } from 'services/webhook' -import { Plan, User } from 'db' +import { Plan } from 'db' export type TypebotInDashboard = Pick< Typebot, @@ -96,91 +96,84 @@ export const createTypebot = async ({ }) } -export const importTypebot = async (typebot: Typebot, user: User) => { - const typebotToImport: Omit = omit( - { - ...typebot, - publishedTypebotId: null, - publicId: null, - customDomain: null, - }, - 'id', - 'updatedAt', - 'createdAt' - ) +export const importTypebot = async (typebot: Typebot, userPlan: Plan) => { return sendRequest({ url: `/api/typebots`, method: 'POST', - body: cleanUpTypebot(typebotToImport, user), + body: await duplicateTypebot(typebot, userPlan), }) } -export const duplicateTypebot = async (typebotId: string) => { - const { data } = await getTypebot(typebotId) - const typebotToDuplicate = data?.typebot - if (!typebotToDuplicate) return { error: new Error('Typebot not found') } - const duplicatedTypebot: Omit = - omit( - { - ...typebotToDuplicate, - name: `${typebotToDuplicate.name} copy`, - publishedTypebotId: null, - publicId: null, - customDomain: null, +const duplicateTypebot = async ( + typebot: Typebot, + userPlan: Plan +): Promise => { + const blockIdsMapping = generateOldNewIdsMapping(typebot.blocks) + const edgeIdsMapping = generateOldNewIdsMapping(typebot.edges) + return { + ...typebot, + id: cuid(), + name: `${typebot.name} copy`, + publishedTypebotId: null, + publicId: null, + customDomain: null, + blocks: await Promise.all( + typebot.blocks.map(async (b) => ({ + ...b, + id: blockIdsMapping.get(b.id) as string, + steps: await Promise.all( + b.steps.map(async (s) => { + const newIds = { + blockId: blockIdsMapping.get(s.blockId) as string, + outgoingEdgeId: s.outgoingEdgeId + ? edgeIdsMapping.get(s.outgoingEdgeId) + : undefined, + } + if (isWebhookStep(s)) { + const newWebhook = await duplicateWebhook(s.webhookId) + return { + ...s, + webhookId: newWebhook ? newWebhook.id : cuid(), + ...newIds, + } + } + return { + ...s, + ...newIds, + } + }) + ), + })) + ), + edges: typebot.edges.map((e) => ({ + ...e, + id: edgeIdsMapping.get(e.id) as string, + from: { + ...e.from, + blockId: blockIdsMapping.get(e.from.blockId) as string, }, - 'id', - 'updatedAt', - 'createdAt' - ) - return sendRequest({ - url: `/api/typebots`, - method: 'POST', - body: await cleanAndDuplicateWebhooks(duplicatedTypebot), - }) + to: { ...e.to, blockId: blockIdsMapping.get(e.to.blockId) as string }, + })), + settings: + typebot.settings.general.isBrandingEnabled === false && + userPlan === Plan.FREE + ? { + ...typebot.settings, + general: { ...typebot.settings.general, isBrandingEnabled: true }, + } + : typebot.settings, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + } } -const cleanUpTypebot = ( - typebot: Omit, - user: User -): Omit => ({ - ...typebot, - blocks: typebot.blocks.map((b) => ({ - ...b, - steps: b.steps.map((s) => - isWebhookStep(s) ? { ...s, webhookId: cuid() } : s - ), - })), - settings: - typebot.settings.general.isBrandingEnabled === false && - user.plan === Plan.FREE - ? { - ...typebot.settings, - general: { ...typebot.settings.general, isBrandingEnabled: true }, - } - : typebot.settings, -}) - -const cleanAndDuplicateWebhooks = async ( - typebot: Omit -) => ({ - ...typebot, - blocks: await Promise.all( - typebot.blocks.map(async (b) => ({ - ...b, - steps: await Promise.all( - b.steps.map(async (s) => { - if (isWebhookStep(s)) { - const newWebhook = await duplicateWebhook(s.webhookId) - return { ...s, webhookId: newWebhook ? newWebhook.id : cuid() } - } - return s - }) - ), - })) - ), -}) +const generateOldNewIdsMapping = (itemWithId: { id: string }[]) => { + const idsMapping: Map = new Map() + itemWithId.forEach((item) => idsMapping.set(item.id, cuid())) + return idsMapping +} -const getTypebot = (typebotId: string) => +export const getTypebot = (typebotId: string) => sendRequest<{ typebot: Typebot }>({ url: `/api/typebots/${typebotId}`, method: 'GET',