🚸 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",
|
||||
"superjson": "1.12.4",
|
||||
"typescript": "5.4.5",
|
||||
"zod": "3.22.4"
|
||||
"zod": "3.22.4",
|
||||
"@typebot.io/variables": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
@ -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<Variable, 'id' | 'name'>[],
|
||||
oldToNewIdsMapping: Map<string, string>
|
||||
) => void
|
||||
updateGroupsCoordinates: (newCoord: CoordinatesMap) => void
|
||||
@ -131,12 +134,29 @@ const groupsActions = (setTypebot: SetTypebot): GroupsActions => ({
|
||||
pasteGroups: (
|
||||
groups: GroupV6[],
|
||||
edges: Edge[],
|
||||
variables: Omit<Variable, 'value'>[],
|
||||
oldToNewIdsMapping: Map<string, string>
|
||||
) => {
|
||||
const createdGroups: GroupV6[] = []
|
||||
setTypebot((typebot) =>
|
||||
produce(typebot, (typebot) => {
|
||||
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) => {
|
||||
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)
|
||||
})
|
||||
})
|
||||
)
|
||||
},
|
||||
|
@ -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<Variable, 'value'>[]
|
||||
}) => {
|
||||
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<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 { 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<Variable, 'value'>[]
|
||||
}
|
||||
| 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<Variable, 'value'>[]
|
||||
}) => void
|
||||
setIsDraggingGraph: (isDragging: boolean) => void
|
||||
}
|
||||
|
||||
@ -83,12 +93,9 @@ export const useGroupsStore = createWithEqualityFn<Store>()(
|
||||
},
|
||||
}))
|
||||
},
|
||||
copyGroups: (groups, edges) =>
|
||||
copyGroups: (groupsInClipboard) =>
|
||||
set({
|
||||
groupsInClipboard: {
|
||||
groups,
|
||||
edges,
|
||||
},
|
||||
groupsInClipboard,
|
||||
}),
|
||||
setIsDraggingGraph: (isDragging) => set({ isDraggingGraph: isDragging }),
|
||||
}))
|
||||
|
@ -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']
|
||||
|
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':
|
||||
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
|
||||
|
Reference in New Issue
Block a user