🚸 Automatically create variables when pasting groups to a new typebot
Closes #1587
This commit is contained in:
@ -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:*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
@ -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,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
|
@ -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 }),
|
||||||
}))
|
}))
|
||||||
|
@ -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']
|
||||||
|
29
packages/variables/extractVariablesFromObject.ts
Normal file
29
packages/variables/extractVariablesFromObject.ts
Normal 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
3
pnpm-lock.yaml
generated
@ -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
|
||||||
|
Reference in New Issue
Block a user