From 4ab1803d3985c214bcc6c9d448349e6beef9808e Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Wed, 19 Jun 2024 14:09:18 +0200 Subject: [PATCH] :children_crossing: Automatically create variables when pasting groups to a new typebot Closes #1587 --- apps/builder/package.json | 3 +- .../editor/providers/typebotActions/groups.ts | 45 ++++++++++++++- .../graph/components/GroupSelectionMenu.tsx | 55 ++++++++++++++++++- .../features/graph/hooks/useGroupsStore.ts | 23 +++++--- packages/schemas/helpers.ts | 10 +--- .../variables/extractVariablesFromObject.ts | 29 ++++++++++ pnpm-lock.yaml | 3 + 7 files changed, 148 insertions(+), 20 deletions(-) create mode 100644 packages/variables/extractVariablesFromObject.ts diff --git a/apps/builder/package.json b/apps/builder/package.json index fb762ca32..bcf9be87e 100644 --- a/apps/builder/package.json +++ b/apps/builder/package.json @@ -131,6 +131,7 @@ "next-runtime-env": "1.6.2", "superjson": "1.12.4", "typescript": "5.4.5", - "zod": "3.22.4" + "zod": "3.22.4", + "@typebot.io/variables": "workspace:*" } } diff --git a/apps/builder/src/features/editor/providers/typebotActions/groups.ts b/apps/builder/src/features/editor/providers/typebotActions/groups.ts index ff5428b94..6ca12ace7 100644 --- a/apps/builder/src/features/editor/providers/typebotActions/groups.ts +++ b/apps/builder/src/features/editor/providers/typebotActions/groups.ts @@ -7,6 +7,7 @@ import { Edge, GroupV6, TypebotV6, + Variable, } from '@typebot.io/schemas' import { SetTypebot } from '../TypebotProvider' import { @@ -15,9 +16,10 @@ import { duplicateBlockDraft, } from './blocks' import { byId, isEmpty } from '@typebot.io/lib' -import { blockHasItems } from '@typebot.io/schemas/helpers' +import { blockHasItems, blockHasOptions } from '@typebot.io/schemas/helpers' import { Coordinates, CoordinatesMap } from '@/features/graph/types' import { parseUniqueKey } from '@typebot.io/lib/parseUniqueKey' +import { extractVariableIdsFromObject } from '@typebot.io/variables/extractVariablesFromObject' export type GroupsActions = { createGroup: ( @@ -34,6 +36,7 @@ export type GroupsActions = { pasteGroups: ( groups: GroupV6[], edges: Edge[], + variables: Pick[], oldToNewIdsMapping: Map ) => void updateGroupsCoordinates: (newCoord: CoordinatesMap) => void @@ -131,12 +134,29 @@ const groupsActions = (setTypebot: SetTypebot): GroupsActions => ({ pasteGroups: ( groups: GroupV6[], edges: Edge[], + variables: Omit[], oldToNewIdsMapping: Map ) => { const createdGroups: GroupV6[] = [] setTypebot((typebot) => produce(typebot, (typebot) => { const edgesToCreate: Edge[] = [] + const variablesToCreate: Omit[] = [] + variables.forEach((variable) => { + const existingVariable = typebot.variables.find( + (v) => v.name === variable.name + ) + if (existingVariable) { + oldToNewIdsMapping.set(variable.id, existingVariable.id) + return + } + const id = createId() + oldToNewIdsMapping.set(variable.id, id) + variablesToCreate.push({ + ...variable, + id, + }) + }) groups.forEach((group) => { const groupTitle = isEmpty(group.title) ? '' @@ -151,6 +171,25 @@ const groupsActions = (setTypebot: SetTypebot): GroupsActions => ({ const newBlock = { ...block } const blockId = createId() oldToNewIdsMapping.set(newBlock.id, blockId) + console.log(JSON.stringify(newBlock), blockHasOptions(newBlock)) + if (blockHasOptions(newBlock) && newBlock.options) { + const variableIdsToReplace = extractVariableIdsFromObject( + newBlock.options + ).filter((v) => oldToNewIdsMapping.has(v)) + console.log( + JSON.stringify(newBlock.options), + variableIdsToReplace + ) + if (variableIdsToReplace.length > 0) { + let optionsStr = JSON.stringify(newBlock.options) + variableIdsToReplace.forEach((variableId) => { + const newId = oldToNewIdsMapping.get(variableId) + if (!newId) return + optionsStr = optionsStr.replace(variableId, newId) + }) + newBlock.options = JSON.parse(optionsStr) + } + } if (blockHasItems(newBlock)) { newBlock.items = newBlock.items?.map((item) => { const id = createId() @@ -224,6 +263,10 @@ const groupsActions = (setTypebot: SetTypebot): GroupsActions => ({ } typebot.edges.push(newEdge) }) + + variablesToCreate.forEach((variableToCreate) => { + typebot.variables.unshift(variableToCreate) + }) }) ) }, diff --git a/apps/builder/src/features/graph/components/GroupSelectionMenu.tsx b/apps/builder/src/features/graph/components/GroupSelectionMenu.tsx index cb11ddc3a..ab14a672b 100644 --- a/apps/builder/src/features/graph/components/GroupSelectionMenu.tsx +++ b/apps/builder/src/features/graph/components/GroupSelectionMenu.tsx @@ -13,10 +13,14 @@ import { useRef, useState } from 'react' import { useGroupsStore } from '../hooks/useGroupsStore' import { toast } from 'sonner' import { createId } from '@paralleldrive/cuid2' -import { Edge, GroupV6 } from '@typebot.io/schemas' +import { Edge, GroupV6, Variable } from '@typebot.io/schemas' import { Coordinates } from '../types' import { useShallow } from 'zustand/react/shallow' import { projectMouse } from '../helpers/projectMouse' +import { + extractVariableIdReferencesInObject, + extractVariableIdsFromObject, +} from '@typebot.io/variables/extractVariablesFromObject' type Props = { graphPosition: Coordinates & { scale: number } @@ -62,10 +66,19 @@ export const GroupSelectionMenu = ({ const edges = typebot.edges.filter((edge) => groups.find((g) => g.id === edge.to.groupId) ) - copyGroups(groups, edges) + const variables = extractVariablesFromCopiedGroups( + groups, + typebot.variables + ) + copyGroups({ + groups, + edges, + variables, + }) return { groups, edges, + variables, } } @@ -77,6 +90,7 @@ export const GroupSelectionMenu = ({ const handlePaste = (overrideClipBoard?: { groups: GroupV6[] edges: Edge[] + variables: Omit[] }) => { if (!groupsInClipboard || isReadOnly || !mousePosition) return const clipboard = overrideClipBoard ?? groupsInClipboard @@ -87,7 +101,12 @@ export const GroupSelectionMenu = ({ groups.forEach((group) => { updateGroupCoordinates(group.id, group.graphCoordinates) }) - pasteGroups(groups, clipboard.edges, oldToNewIdsMapping) + pasteGroups( + groups, + clipboard.edges, + clipboard.variables, + oldToNewIdsMapping + ) setFocusedGroups(groups.map((g) => g.id)) } @@ -194,3 +213,33 @@ const parseGroupsToPaste = ( oldToNewIdsMapping, } } + +export const extractVariablesFromCopiedGroups = ( + groups: GroupV6[], + existingVariables: Variable[] +): Omit[] => { + const groupsStr = JSON.stringify(groups) + if (!groupsStr) return [] + const calledVariablesId = extractVariableIdReferencesInObject( + groups, + existingVariables + ) + const variableIdsInOptions = extractVariableIdsFromObject(groups) + + return [...variableIdsInOptions, ...calledVariablesId].reduce< + Omit[] + >((acc, id) => { + if (!id) return acc + if (acc.find((v) => v.id === id)) return acc + const variable = existingVariables.find((v) => v.id === id) + if (!variable) return acc + return [ + ...acc, + { + id: variable.id, + name: variable.name, + isSessionVariable: variable.isSessionVariable, + }, + ] + }, []) +} diff --git a/apps/builder/src/features/graph/hooks/useGroupsStore.ts b/apps/builder/src/features/graph/hooks/useGroupsStore.ts index 318838c41..34ec5f9f6 100644 --- a/apps/builder/src/features/graph/hooks/useGroupsStore.ts +++ b/apps/builder/src/features/graph/hooks/useGroupsStore.ts @@ -1,13 +1,19 @@ import { createWithEqualityFn } from 'zustand/traditional' import { Coordinates, CoordinatesMap } from '../types' -import { Edge, Group, GroupV6 } from '@typebot.io/schemas' +import { Edge, Group, GroupV6, Variable } from '@typebot.io/schemas' import { subscribeWithSelector } from 'zustand/middleware' import { share } from 'shared-zustand' type Store = { focusedGroups: string[] groupsCoordinates: CoordinatesMap | undefined - groupsInClipboard: { groups: GroupV6[]; edges: Edge[] } | undefined + groupsInClipboard: + | { + groups: GroupV6[] + edges: Edge[] + variables: Omit[] + } + | undefined isDraggingGraph: boolean // TO-DO: remove once Typebot provider is migrated to a Zustand store. We will be able to get it internally in the store (if mutualized). getGroupsCoordinates: () => CoordinatesMap | undefined @@ -17,7 +23,11 @@ type Store = { setFocusedGroups: (groupIds: string[]) => void setGroupsCoordinates: (groups: Group[] | undefined) => void updateGroupCoordinates: (groupId: string, newCoord: Coordinates) => void - copyGroups: (groups: GroupV6[], edges: Edge[]) => void + copyGroups: (args: { + groups: GroupV6[] + edges: Edge[] + variables: Omit[] + }) => void setIsDraggingGraph: (isDragging: boolean) => void } @@ -83,12 +93,9 @@ export const useGroupsStore = createWithEqualityFn()( }, })) }, - copyGroups: (groups, edges) => + copyGroups: (groupsInClipboard) => set({ - groupsInClipboard: { - groups, - edges, - }, + groupsInClipboard, }), setIsDraggingGraph: (isDragging) => set({ isDraggingGraph: isDragging }), })) diff --git a/packages/schemas/helpers.ts b/packages/schemas/helpers.ts index b1980456e..ad0f4c189 100644 --- a/packages/schemas/helpers.ts +++ b/packages/schemas/helpers.ts @@ -14,6 +14,7 @@ import { IntegrationBlock, HttpRequestBlock, BlockWithOptionsType, + BlockWithOptions, } from './features/blocks' import { BubbleBlockType } from './features/blocks/bubbles/constants' import { defaultChoiceInputOptions } from './features/blocks/inputs/choice/constants' @@ -78,13 +79,8 @@ export const isBubbleBlockType = ( ): type is BubbleBlockType => (Object.values(BubbleBlockType) as string[]).includes(type) -export const blockTypeHasOption = ( - type: Block['type'] -): type is BlockWithOptionsType => - (Object.values(InputBlockType) as string[]) - .concat(Object.values(LogicBlockType)) - .concat(Object.values(IntegrationBlockType)) - .includes(type) +export const blockHasOptions = (block: Block): block is BlockWithOptions => + 'options' in block export const blockTypeHasItems = ( type: Block['type'] diff --git a/packages/variables/extractVariablesFromObject.ts b/packages/variables/extractVariablesFromObject.ts new file mode 100644 index 000000000..664c31bc7 --- /dev/null +++ b/packages/variables/extractVariablesFromObject.ts @@ -0,0 +1,29 @@ +import { Variable } from './types' + +const variableNameRegex = /\{\{([^{}]+)\}\}/g + +export const extractVariableIdReferencesInObject = ( + obj: any, + existingVariables: Variable[] +): string[] => + [...(JSON.stringify(obj).match(variableNameRegex) ?? [])].reduce( + (acc, match) => { + const varName = match.slice(2, -2) + const id = existingVariables.find((v) => v.name === varName)?.id + if (!id || acc.find((accId) => accId === id)) return acc + return acc.concat(id) + }, + [] + ) + +const variableIdRegex = /"\w*variableid":"([^"]+)"/gi + +export const extractVariableIdsFromObject = (obj: any): string[] => + [...(JSON.stringify(obj).match(variableIdRegex) ?? [])].reduce( + (acc, match) => { + const id = variableIdRegex.exec(match)?.[1] + if (!id || acc.find((accId) => accId === id)) return acc + return acc.concat(id) + }, + [] + ) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd73006f0..72de93590 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -324,6 +324,9 @@ importers: '@typebot.io/tsconfig': specifier: workspace:* version: link:../../packages/tsconfig + '@typebot.io/variables': + specifier: workspace:* + version: link:../../packages/variables '@types/canvas-confetti': specifier: 1.6.0 version: 1.6.0