2
0

🚸 Automatically create variables when pasting groups to a new typebot

Closes #1587
This commit is contained in:
Baptiste Arnaud
2024-06-19 14:09:18 +02:00
parent 67f37c02a4
commit 4ab1803d39
7 changed files with 148 additions and 20 deletions

View File

@ -131,6 +131,7 @@
"next-runtime-env": "1.6.2", "next-runtime-env": "1.6.2",
"superjson": "1.12.4", "superjson": "1.12.4",
"typescript": "5.4.5", "typescript": "5.4.5",
"zod": "3.22.4" "zod": "3.22.4",
"@typebot.io/variables": "workspace:*"
} }
} }

View File

@ -7,6 +7,7 @@ import {
Edge, Edge,
GroupV6, GroupV6,
TypebotV6, TypebotV6,
Variable,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import { SetTypebot } from '../TypebotProvider' import { SetTypebot } from '../TypebotProvider'
import { import {
@ -15,9 +16,10 @@ import {
duplicateBlockDraft, duplicateBlockDraft,
} from './blocks' } from './blocks'
import { byId, isEmpty } from '@typebot.io/lib' 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 { Coordinates, CoordinatesMap } from '@/features/graph/types'
import { parseUniqueKey } from '@typebot.io/lib/parseUniqueKey' import { parseUniqueKey } from '@typebot.io/lib/parseUniqueKey'
import { extractVariableIdsFromObject } from '@typebot.io/variables/extractVariablesFromObject'
export type GroupsActions = { export type GroupsActions = {
createGroup: ( createGroup: (
@ -34,6 +36,7 @@ export type GroupsActions = {
pasteGroups: ( pasteGroups: (
groups: GroupV6[], groups: GroupV6[],
edges: Edge[], edges: Edge[],
variables: Pick<Variable, 'id' | 'name'>[],
oldToNewIdsMapping: Map<string, string> oldToNewIdsMapping: Map<string, string>
) => void ) => void
updateGroupsCoordinates: (newCoord: CoordinatesMap) => void updateGroupsCoordinates: (newCoord: CoordinatesMap) => void
@ -131,12 +134,29 @@ const groupsActions = (setTypebot: SetTypebot): GroupsActions => ({
pasteGroups: ( pasteGroups: (
groups: GroupV6[], groups: GroupV6[],
edges: Edge[], edges: Edge[],
variables: Omit<Variable, 'value'>[],
oldToNewIdsMapping: Map<string, string> oldToNewIdsMapping: Map<string, string>
) => { ) => {
const createdGroups: GroupV6[] = [] const createdGroups: GroupV6[] = []
setTypebot((typebot) => setTypebot((typebot) =>
produce(typebot, (typebot) => { produce(typebot, (typebot) => {
const edgesToCreate: Edge[] = [] const edgesToCreate: Edge[] = []
const variablesToCreate: Omit<Variable, 'value'>[] = []
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) => { groups.forEach((group) => {
const groupTitle = isEmpty(group.title) const groupTitle = isEmpty(group.title)
? '' ? ''
@ -151,6 +171,25 @@ const groupsActions = (setTypebot: SetTypebot): GroupsActions => ({
const newBlock = { ...block } const newBlock = { ...block }
const blockId = createId() const blockId = createId()
oldToNewIdsMapping.set(newBlock.id, blockId) 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)) { if (blockHasItems(newBlock)) {
newBlock.items = newBlock.items?.map((item) => { newBlock.items = newBlock.items?.map((item) => {
const id = createId() const id = createId()
@ -224,6 +263,10 @@ const groupsActions = (setTypebot: SetTypebot): GroupsActions => ({
} }
typebot.edges.push(newEdge) typebot.edges.push(newEdge)
}) })
variablesToCreate.forEach((variableToCreate) => {
typebot.variables.unshift(variableToCreate)
})
}) })
) )
}, },

View File

@ -13,10 +13,14 @@ import { useRef, useState } from 'react'
import { useGroupsStore } from '../hooks/useGroupsStore' import { useGroupsStore } from '../hooks/useGroupsStore'
import { toast } from 'sonner' import { toast } from 'sonner'
import { createId } from '@paralleldrive/cuid2' 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 { Coordinates } from '../types'
import { useShallow } from 'zustand/react/shallow' import { useShallow } from 'zustand/react/shallow'
import { projectMouse } from '../helpers/projectMouse' import { projectMouse } from '../helpers/projectMouse'
import {
extractVariableIdReferencesInObject,
extractVariableIdsFromObject,
} from '@typebot.io/variables/extractVariablesFromObject'
type Props = { type Props = {
graphPosition: Coordinates & { scale: number } graphPosition: Coordinates & { scale: number }
@ -62,10 +66,19 @@ export const GroupSelectionMenu = ({
const edges = typebot.edges.filter((edge) => const edges = typebot.edges.filter((edge) =>
groups.find((g) => g.id === edge.to.groupId) groups.find((g) => g.id === edge.to.groupId)
) )
copyGroups(groups, edges) const variables = extractVariablesFromCopiedGroups(
groups,
typebot.variables
)
copyGroups({
groups,
edges,
variables,
})
return { return {
groups, groups,
edges, edges,
variables,
} }
} }
@ -77,6 +90,7 @@ export const GroupSelectionMenu = ({
const handlePaste = (overrideClipBoard?: { const handlePaste = (overrideClipBoard?: {
groups: GroupV6[] groups: GroupV6[]
edges: Edge[] edges: Edge[]
variables: Omit<Variable, 'value'>[]
}) => { }) => {
if (!groupsInClipboard || isReadOnly || !mousePosition) return if (!groupsInClipboard || isReadOnly || !mousePosition) return
const clipboard = overrideClipBoard ?? groupsInClipboard const clipboard = overrideClipBoard ?? groupsInClipboard
@ -87,7 +101,12 @@ export const GroupSelectionMenu = ({
groups.forEach((group) => { groups.forEach((group) => {
updateGroupCoordinates(group.id, group.graphCoordinates) updateGroupCoordinates(group.id, group.graphCoordinates)
}) })
pasteGroups(groups, clipboard.edges, oldToNewIdsMapping) pasteGroups(
groups,
clipboard.edges,
clipboard.variables,
oldToNewIdsMapping
)
setFocusedGroups(groups.map((g) => g.id)) setFocusedGroups(groups.map((g) => g.id))
} }
@ -194,3 +213,33 @@ const parseGroupsToPaste = (
oldToNewIdsMapping, oldToNewIdsMapping,
} }
} }
export const extractVariablesFromCopiedGroups = (
groups: GroupV6[],
existingVariables: Variable[]
): Omit<Variable, 'value'>[] => {
const groupsStr = JSON.stringify(groups)
if (!groupsStr) return []
const calledVariablesId = extractVariableIdReferencesInObject(
groups,
existingVariables
)
const variableIdsInOptions = extractVariableIdsFromObject(groups)
return [...variableIdsInOptions, ...calledVariablesId].reduce<
Omit<Variable, 'value'>[]
>((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,
},
]
}, [])
}

View File

@ -1,13 +1,19 @@
import { createWithEqualityFn } from 'zustand/traditional' import { createWithEqualityFn } from 'zustand/traditional'
import { Coordinates, CoordinatesMap } from '../types' 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 { subscribeWithSelector } from 'zustand/middleware'
import { share } from 'shared-zustand' import { share } from 'shared-zustand'
type Store = { type Store = {
focusedGroups: string[] focusedGroups: string[]
groupsCoordinates: CoordinatesMap | undefined groupsCoordinates: CoordinatesMap | undefined
groupsInClipboard: { groups: GroupV6[]; edges: Edge[] } | undefined groupsInClipboard:
| {
groups: GroupV6[]
edges: Edge[]
variables: Omit<Variable, 'value'>[]
}
| undefined
isDraggingGraph: boolean 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). // 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 getGroupsCoordinates: () => CoordinatesMap | undefined
@ -17,7 +23,11 @@ type Store = {
setFocusedGroups: (groupIds: string[]) => void setFocusedGroups: (groupIds: string[]) => void
setGroupsCoordinates: (groups: Group[] | undefined) => void setGroupsCoordinates: (groups: Group[] | undefined) => void
updateGroupCoordinates: (groupId: string, newCoord: Coordinates) => void updateGroupCoordinates: (groupId: string, newCoord: Coordinates) => void
copyGroups: (groups: GroupV6[], edges: Edge[]) => void copyGroups: (args: {
groups: GroupV6[]
edges: Edge[]
variables: Omit<Variable, 'value'>[]
}) => void
setIsDraggingGraph: (isDragging: boolean) => void setIsDraggingGraph: (isDragging: boolean) => void
} }
@ -83,12 +93,9 @@ export const useGroupsStore = createWithEqualityFn<Store>()(
}, },
})) }))
}, },
copyGroups: (groups, edges) => copyGroups: (groupsInClipboard) =>
set({ set({
groupsInClipboard: { groupsInClipboard,
groups,
edges,
},
}), }),
setIsDraggingGraph: (isDragging) => set({ isDraggingGraph: isDragging }), setIsDraggingGraph: (isDragging) => set({ isDraggingGraph: isDragging }),
})) }))

View File

@ -14,6 +14,7 @@ import {
IntegrationBlock, IntegrationBlock,
HttpRequestBlock, HttpRequestBlock,
BlockWithOptionsType, BlockWithOptionsType,
BlockWithOptions,
} from './features/blocks' } from './features/blocks'
import { BubbleBlockType } from './features/blocks/bubbles/constants' import { BubbleBlockType } from './features/blocks/bubbles/constants'
import { defaultChoiceInputOptions } from './features/blocks/inputs/choice/constants' import { defaultChoiceInputOptions } from './features/blocks/inputs/choice/constants'
@ -78,13 +79,8 @@ export const isBubbleBlockType = (
): type is BubbleBlockType => ): type is BubbleBlockType =>
(Object.values(BubbleBlockType) as string[]).includes(type) (Object.values(BubbleBlockType) as string[]).includes(type)
export const blockTypeHasOption = ( export const blockHasOptions = (block: Block): block is BlockWithOptions =>
type: Block['type'] 'options' in block
): type is BlockWithOptionsType =>
(Object.values(InputBlockType) as string[])
.concat(Object.values(LogicBlockType))
.concat(Object.values(IntegrationBlockType))
.includes(type)
export const blockTypeHasItems = ( export const blockTypeHasItems = (
type: Block['type'] type: Block['type']

View File

@ -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<string[]>(
(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<string[]>(
(acc, match) => {
const id = variableIdRegex.exec(match)?.[1]
if (!id || acc.find((accId) => accId === id)) return acc
return acc.concat(id)
},
[]
)

3
pnpm-lock.yaml generated
View File

@ -324,6 +324,9 @@ importers:
'@typebot.io/tsconfig': '@typebot.io/tsconfig':
specifier: workspace:* specifier: workspace:*
version: link:../../packages/tsconfig version: link:../../packages/tsconfig
'@typebot.io/variables':
specifier: workspace:*
version: link:../../packages/variables
'@types/canvas-confetti': '@types/canvas-confetti':
specifier: 1.6.0 specifier: 1.6.0
version: 1.6.0 version: 1.6.0