Skip to content

Commit

Permalink
🚸 Automatically create variables when pasting groups to a new typebot
Browse files Browse the repository at this point in the history
Closes #1587
  • Loading branch information
baptisteArno committed Jun 19, 2024
1 parent 67f37c0 commit 4ab1803
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 20 deletions.
3 changes: 2 additions & 1 deletion apps/builder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Edge,
GroupV6,
TypebotV6,
Variable,
} from '@typebot.io/schemas'
import { SetTypebot } from '../TypebotProvider'
import {
Expand All @@ -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: (
Expand All @@ -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
Expand Down Expand Up @@ -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)
? ''
Expand All @@ -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()
Expand Down Expand Up @@ -224,6 +263,10 @@ const groupsActions = (setTypebot: SetTypebot): GroupsActions => ({
}
typebot.edges.push(newEdge)
})

variablesToCreate.forEach((variableToCreate) => {
typebot.variables.unshift(variableToCreate)
})
})
)
},
Expand Down
55 changes: 52 additions & 3 deletions apps/builder/src/features/graph/components/GroupSelectionMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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,
}
}

Expand All @@ -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
Expand All @@ -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))
}

Expand Down Expand Up @@ -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,
},
]
}, [])
}
23 changes: 15 additions & 8 deletions apps/builder/src/features/graph/hooks/useGroupsStore.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
}

Expand Down Expand Up @@ -83,12 +93,9 @@ export const useGroupsStore = createWithEqualityFn<Store>()(
},
}))
},
copyGroups: (groups, edges) =>
copyGroups: (groupsInClipboard) =>
set({
groupsInClipboard: {
groups,
edges,
},
groupsInClipboard,
}),
setIsDraggingGraph: (isDragging) => set({ isDraggingGraph: isDragging }),
}))
Expand Down
10 changes: 3 additions & 7 deletions packages/schemas/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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']
Expand Down
29 changes: 29 additions & 0 deletions packages/variables/extractVariablesFromObject.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 4ab1803

Please sign in to comment.