♻️ (builder) Change to features-centric folder structure

This commit is contained in:
Baptiste Arnaud
2022-11-15 09:35:48 +01:00
committed by Baptiste Arnaud
parent 3686465a85
commit 643571fe7d
683 changed files with 3907 additions and 3643 deletions

View File

@@ -0,0 +1,41 @@
import {
createContext,
Dispatch,
ReactNode,
SetStateAction,
useContext,
useState,
} from 'react'
export enum RightPanel {
PREVIEW,
}
const editorContext = createContext<{
rightPanel?: RightPanel
setRightPanel: Dispatch<SetStateAction<RightPanel | undefined>>
startPreviewAtGroup: string | undefined
setStartPreviewAtGroup: Dispatch<SetStateAction<string | undefined>>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
}>({})
export const EditorProvider = ({ children }: { children: ReactNode }) => {
const [rightPanel, setRightPanel] = useState<RightPanel>()
const [startPreviewAtGroup, setStartPreviewAtGroup] = useState<string>()
return (
<editorContext.Provider
value={{
rightPanel,
setRightPanel,
startPreviewAtGroup,
setStartPreviewAtGroup,
}}
>
{children}
</editorContext.Provider>
)
}
export const useEditor = () => useContext(editorContext)

View File

@@ -0,0 +1,364 @@
import {
LogicBlockType,
PublicTypebot,
ResultsTablePreferences,
Settings,
Theme,
Typebot,
Webhook,
} from 'models'
import { Router, useRouter } from 'next/router'
import {
createContext,
ReactNode,
useContext,
useEffect,
useMemo,
useState,
} from 'react'
import { isDefined, isNotDefined, omit } from 'utils'
import { GroupsActions, groupsActions } from './actions/groups'
import { blocksAction, BlocksActions } from './actions/blocks'
import { variablesAction, VariablesActions } from './actions/variables'
import { edgesAction, EdgesActions } from './actions/edges'
import { itemsAction, ItemsActions } from './actions/items'
import { dequal } from 'dequal'
import cuid from 'cuid'
import { useToast } from '@/hooks/useToast'
import { useTypebotQuery } from '@/hooks/useTypebotQuery'
import useUndo from '../../hooks/useUndo'
import { useLinkedTypebots } from '@/hooks/useLinkedTypebots'
import { updateTypebotQuery } from '../../queries/updateTypebotQuery'
import { preventUserFromRefreshing } from '@/utils/helpers'
import { updatePublishedTypebotQuery } from '@/features/publish'
import { saveWebhookQuery } from '@/features/blocks/integrations/webhook/queries/saveWebhookQuery'
import {
createPublishedTypebotQuery,
deletePublishedTypebotQuery,
checkIfTypebotsAreEqual,
checkIfPublished,
parseTypebotToPublicTypebot,
parseDefaultPublicId,
parsePublicTypebotToTypebot,
} from '@/features/publish'
import { useAutoSave } from '@/hooks/useAutoSave'
const autoSaveTimeout = 10000
type UpdateTypebotPayload = Partial<{
theme: Theme
settings: Settings
publicId: string
name: string
publishedTypebotId: string
icon: string
customDomain: string
resultsTablePreferences: ResultsTablePreferences
isClosed: boolean
}>
export type SetTypebot = (
newPresent: Typebot | ((current: Typebot) => Typebot)
) => void
const typebotContext = createContext<
{
typebot?: Typebot
publishedTypebot?: PublicTypebot
linkedTypebots?: Typebot[]
webhooks: Webhook[]
isReadOnly?: boolean
isPublished: boolean
isPublishing: boolean
isSavingLoading: boolean
save: () => Promise<void>
undo: () => void
redo: () => void
canRedo: boolean
canUndo: boolean
updateWebhook: (
webhookId: string,
webhook: Partial<Webhook>
) => Promise<void>
updateTypebot: (updates: UpdateTypebotPayload) => void
publishTypebot: () => void
unpublishTypebot: () => void
restorePublishedTypebot: () => void
} & GroupsActions &
BlocksActions &
ItemsActions &
VariablesActions &
EdgesActions
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
>({})
export const TypebotProvider = ({
children,
typebotId,
}: {
children: ReactNode
typebotId: string
}) => {
const router = useRouter()
const { showToast } = useToast()
const { typebot, publishedTypebot, webhooks, isReadOnly, isLoading, mutate } =
useTypebotQuery({
typebotId,
})
const [
{ present: localTypebot },
{
redo,
undo,
flush,
canRedo,
canUndo,
set: setLocalTypebot,
presentRef: currentTypebotRef,
},
] = useUndo<Typebot | undefined>(undefined)
const linkedTypebotIds = localTypebot?.groups
.flatMap((b) => b.blocks)
.reduce<string[]>(
(typebotIds, block) =>
block.type === LogicBlockType.TYPEBOT_LINK &&
isDefined(block.options.typebotId)
? [...typebotIds, block.options.typebotId]
: typebotIds,
[]
)
const { typebots: linkedTypebots } = useLinkedTypebots({
workspaceId: localTypebot?.workspaceId ?? undefined,
typebotId,
typebotIds: linkedTypebotIds,
onError: (error) =>
showToast({
title: 'Error while fetching linkedTypebots',
description: error.message,
}),
})
useEffect(() => {
if (!typebot || !currentTypebotRef.current) return
if (typebotId !== currentTypebotRef.current.id) {
setLocalTypebot({ ...typebot }, { updateDate: false })
flush()
} else if (
new Date(typebot.updatedAt) >
new Date(currentTypebotRef.current.updatedAt)
) {
setLocalTypebot({ ...typebot })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [typebot])
const saveTypebot = async () => {
if (!currentTypebotRef.current || !typebot) return
const typebotToSave = { ...currentTypebotRef.current }
if (dequal(omit(typebot, 'updatedAt'), omit(typebotToSave, 'updatedAt')))
return
setIsSavingLoading(true)
const { error } = await updateTypebotQuery(typebotToSave.id, typebotToSave)
setIsSavingLoading(false)
if (error) {
showToast({ title: error.name, description: error.message })
return
}
mutate({
typebot: typebotToSave,
publishedTypebot,
webhooks: webhooks ?? [],
})
window.removeEventListener('beforeunload', preventUserFromRefreshing)
}
const savePublishedTypebot = async (newPublishedTypebot: PublicTypebot) => {
if (!localTypebot) return
setIsPublishing(true)
const { error } = await updatePublishedTypebotQuery(
newPublishedTypebot.id,
newPublishedTypebot,
localTypebot.workspaceId
)
setIsPublishing(false)
if (error)
return showToast({ title: error.name, description: error.message })
mutate({
typebot: currentTypebotRef.current as Typebot,
publishedTypebot: newPublishedTypebot,
webhooks: webhooks ?? [],
})
}
useAutoSave(
{
handler: saveTypebot,
item: localTypebot,
debounceTimeout: autoSaveTimeout,
},
[typebot, publishedTypebot, webhooks]
)
useEffect(() => {
Router.events.on('routeChangeStart', saveTypebot)
return () => {
Router.events.off('routeChangeStart', saveTypebot)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [typebot, publishedTypebot, webhooks])
const [isSavingLoading, setIsSavingLoading] = useState(false)
const [isPublishing, setIsPublishing] = useState(false)
const isPublished = useMemo(
() =>
isDefined(localTypebot) &&
isDefined(publishedTypebot) &&
checkIfPublished(localTypebot, publishedTypebot),
[localTypebot, publishedTypebot]
)
useEffect(() => {
if (!localTypebot || !typebot) return
currentTypebotRef.current = localTypebot
if (!checkIfTypebotsAreEqual(localTypebot, typebot)) {
window.removeEventListener('beforeunload', preventUserFromRefreshing)
window.addEventListener('beforeunload', preventUserFromRefreshing)
} else {
window.removeEventListener('beforeunload', preventUserFromRefreshing)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [localTypebot])
useEffect(() => {
if (isLoading) return
if (!typebot) {
showToast({ status: 'info', description: "Couldn't find typebot" })
router.replace('/typebots')
return
}
setLocalTypebot({ ...typebot })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoading])
const updateLocalTypebot = (updates: UpdateTypebotPayload) =>
localTypebot && setLocalTypebot({ ...localTypebot, ...updates })
const publishTypebot = async () => {
if (!localTypebot) return
const publishedTypebotId = cuid()
const newLocalTypebot = { ...localTypebot }
if (publishedTypebot && isNotDefined(localTypebot.publishedTypebotId)) {
updateLocalTypebot({ publishedTypebotId: publishedTypebot.id })
await saveTypebot()
}
if (!publishedTypebot) {
const newPublicId = parseDefaultPublicId(
localTypebot.name,
localTypebot.id
)
updateLocalTypebot({ publicId: newPublicId, publishedTypebotId })
newLocalTypebot.publicId = newPublicId
await saveTypebot()
}
if (publishedTypebot) {
await savePublishedTypebot({
...parseTypebotToPublicTypebot(newLocalTypebot),
id: publishedTypebot.id,
})
} else {
setIsPublishing(true)
const { data, error } = await createPublishedTypebotQuery(
{
...parseTypebotToPublicTypebot(newLocalTypebot),
id: publishedTypebotId,
},
localTypebot.workspaceId
)
setIsPublishing(false)
if (error)
return showToast({ title: error.name, description: error.message })
mutate({
typebot: localTypebot,
publishedTypebot: data,
webhooks: webhooks ?? [],
})
}
}
const unpublishTypebot = async () => {
if (!publishedTypebot || !localTypebot) return
setIsPublishing(true)
const { error } = await deletePublishedTypebotQuery({
publishedTypebotId: publishedTypebot.id,
typebotId: localTypebot.id,
})
setIsPublishing(false)
if (error) showToast({ description: error.message })
mutate({
typebot: localTypebot,
webhooks: webhooks ?? [],
})
}
const restorePublishedTypebot = () => {
if (!publishedTypebot || !localTypebot) return
setLocalTypebot(parsePublicTypebotToTypebot(publishedTypebot, localTypebot))
return saveTypebot()
}
const updateWebhook = async (
webhookId: string,
updates: Partial<Webhook>
) => {
if (!typebot) return
const { data } = await saveWebhookQuery(webhookId, updates)
if (data)
mutate({
typebot,
publishedTypebot,
webhooks: (webhooks ?? []).map((w) =>
w.id === webhookId ? data.webhook : w
),
})
}
return (
<typebotContext.Provider
value={{
typebot: localTypebot,
publishedTypebot,
linkedTypebots,
webhooks: webhooks ?? [],
isReadOnly,
isSavingLoading,
save: saveTypebot,
undo,
redo,
canUndo,
canRedo,
publishTypebot,
unpublishTypebot,
isPublishing,
isPublished,
updateTypebot: updateLocalTypebot,
restorePublishedTypebot,
updateWebhook,
...groupsActions(setLocalTypebot as SetTypebot),
...blocksAction(setLocalTypebot as SetTypebot),
...variablesAction(setLocalTypebot as SetTypebot),
...edgesAction(setLocalTypebot as SetTypebot),
...itemsAction(setLocalTypebot as SetTypebot),
}}
>
{children}
</typebotContext.Provider>
)
}
export const useTypebot = () => useContext(typebotContext)

View File

@@ -0,0 +1,165 @@
import {
Block,
Typebot,
DraggableBlock,
DraggableBlockType,
BlockIndices,
} from 'models'
import { removeEmptyGroups } from './groups'
import { WritableDraft } from 'immer/dist/types/types-external'
import { SetTypebot } from '../TypebotProvider'
import produce from 'immer'
import { cleanUpEdgeDraft, deleteEdgeDraft } from './edges'
import cuid from 'cuid'
import { byId, isWebhookBlock, blockHasItems } from 'utils'
import { duplicateItemDraft } from './items'
import { parseNewBlock } from '@/features/graph'
export type BlocksActions = {
createBlock: (
groupId: string,
block: DraggableBlock | DraggableBlockType,
indices: BlockIndices
) => void
updateBlock: (
indices: BlockIndices,
updates: Partial<Omit<Block, 'id' | 'type'>>
) => void
duplicateBlock: (indices: BlockIndices) => void
detachBlockFromGroup: (indices: BlockIndices) => void
deleteBlock: (indices: BlockIndices) => void
}
const blocksAction = (setTypebot: SetTypebot): BlocksActions => ({
createBlock: (
groupId: string,
block: DraggableBlock | DraggableBlockType,
indices: BlockIndices
) =>
setTypebot((typebot) =>
produce(typebot, (typebot) => {
createBlockDraft(typebot, block, groupId, indices)
})
),
updateBlock: (
{ groupIndex, blockIndex }: BlockIndices,
updates: Partial<Omit<Block, 'id' | 'type'>>
) =>
setTypebot((typebot) =>
produce(typebot, (typebot) => {
const block = typebot.groups[groupIndex].blocks[blockIndex]
typebot.groups[groupIndex].blocks[blockIndex] = { ...block, ...updates }
})
),
duplicateBlock: ({ groupIndex, blockIndex }: BlockIndices) =>
setTypebot((typebot) =>
produce(typebot, (typebot) => {
const block = { ...typebot.groups[groupIndex].blocks[blockIndex] }
const newBlock = duplicateBlockDraft(block.groupId)(block)
typebot.groups[groupIndex].blocks.splice(blockIndex + 1, 0, newBlock)
})
),
detachBlockFromGroup: (indices: BlockIndices) =>
setTypebot((typebot) => produce(typebot, removeBlockFromGroup(indices))),
deleteBlock: ({ groupIndex, blockIndex }: BlockIndices) =>
setTypebot((typebot) =>
produce(typebot, (typebot) => {
const removingBlock = typebot.groups[groupIndex].blocks[blockIndex]
removeBlockFromGroup({ groupIndex, blockIndex })(typebot)
cleanUpEdgeDraft(typebot, removingBlock.id)
removeEmptyGroups(typebot)
})
),
})
const removeBlockFromGroup =
({ groupIndex, blockIndex }: BlockIndices) =>
(typebot: WritableDraft<Typebot>) => {
typebot.groups[groupIndex].blocks.splice(blockIndex, 1)
}
const createBlockDraft = (
typebot: WritableDraft<Typebot>,
block: DraggableBlock | DraggableBlockType,
groupId: string,
{ groupIndex, blockIndex }: BlockIndices
) => {
const blocks = typebot.groups[groupIndex].blocks
if (
blockIndex === blocks.length &&
blockIndex > 0 &&
blocks[blockIndex - 1].outgoingEdgeId
)
deleteEdgeDraft(typebot, blocks[blockIndex - 1].outgoingEdgeId as string)
typeof block === 'string'
? createNewBlock(typebot, block, groupId, { groupIndex, blockIndex })
: moveBlockToGroup(typebot, block, groupId, { groupIndex, blockIndex })
removeEmptyGroups(typebot)
}
const createNewBlock = async (
typebot: WritableDraft<Typebot>,
type: DraggableBlockType,
groupId: string,
{ groupIndex, blockIndex }: BlockIndices
) => {
const newBlock = parseNewBlock(type, groupId)
typebot.groups[groupIndex].blocks.splice(blockIndex ?? 0, 0, newBlock)
}
const moveBlockToGroup = (
typebot: WritableDraft<Typebot>,
block: DraggableBlock,
groupId: string,
{ groupIndex, blockIndex }: BlockIndices
) => {
const newBlock = { ...block, groupId }
const items = blockHasItems(block) ? block.items : []
items.forEach((item) => {
const edgeIndex = typebot.edges.findIndex(byId(item.outgoingEdgeId))
if (edgeIndex === -1) return
typebot.edges[edgeIndex].from.groupId = groupId
})
if (block.outgoingEdgeId) {
if (typebot.groups[groupIndex].blocks.length > blockIndex ?? 0) {
deleteEdgeDraft(typebot, block.outgoingEdgeId)
newBlock.outgoingEdgeId = undefined
} else {
const edgeIndex = typebot.edges.findIndex(byId(block.outgoingEdgeId))
edgeIndex !== -1
? (typebot.edges[edgeIndex].from.groupId = groupId)
: (newBlock.outgoingEdgeId = undefined)
}
}
typebot.groups[groupIndex].blocks.splice(blockIndex ?? 0, 0, newBlock)
}
const duplicateBlockDraft =
(groupId: string) =>
(block: Block): Block => {
const blockId = cuid()
if (blockHasItems(block))
return {
...block,
groupId,
id: blockId,
items: block.items.map(duplicateItemDraft(blockId)),
outgoingEdgeId: undefined,
} as Block
if (isWebhookBlock(block))
return {
...block,
groupId,
id: blockId,
webhookId: cuid(),
outgoingEdgeId: undefined,
}
return {
...block,
groupId,
id: blockId,
outgoingEdgeId: undefined,
}
}
export { blocksAction, createBlockDraft, duplicateBlockDraft }

View File

@@ -0,0 +1,149 @@
import {
Typebot,
Edge,
BlockWithItems,
BlockIndices,
ItemIndices,
Block,
} from 'models'
import { WritableDraft } from 'immer/dist/types/types-external'
import { SetTypebot } from '../TypebotProvider'
import { produce } from 'immer'
import { byId, isDefined, blockHasItems } from 'utils'
import cuid from 'cuid'
export type EdgesActions = {
createEdge: (edge: Omit<Edge, 'id'>) => void
updateEdge: (edgeIndex: number, updates: Partial<Omit<Edge, 'id'>>) => void
deleteEdge: (edgeId: string) => void
}
export const edgesAction = (setTypebot: SetTypebot): EdgesActions => ({
createEdge: (edge: Omit<Edge, 'id'>) =>
setTypebot((typebot) =>
produce(typebot, (typebot) => {
const newEdge = {
...edge,
id: cuid(),
}
removeExistingEdge(typebot, edge)
typebot.edges.push(newEdge)
const groupIndex = typebot.groups.findIndex(byId(edge.from.groupId))
const blockIndex = typebot.groups[groupIndex].blocks.findIndex(
byId(edge.from.blockId)
)
const itemIndex = edge.from.itemId
? (
typebot.groups[groupIndex].blocks[blockIndex] as BlockWithItems
).items.findIndex(byId(edge.from.itemId))
: null
isDefined(itemIndex) && itemIndex !== -1
? addEdgeIdToItem(typebot, newEdge.id, {
groupIndex,
blockIndex,
itemIndex,
})
: addEdgeIdToBlock(typebot, newEdge.id, {
groupIndex,
blockIndex,
})
})
),
updateEdge: (edgeIndex: number, updates: Partial<Omit<Edge, 'id'>>) =>
setTypebot((typebot) =>
produce(typebot, (typebot) => {
const currentEdge = typebot.edges[edgeIndex]
typebot.edges[edgeIndex] = {
...currentEdge,
...updates,
}
})
),
deleteEdge: (edgeId: string) =>
setTypebot((typebot) =>
produce(typebot, (typebot) => {
deleteEdgeDraft(typebot, edgeId)
})
),
})
const addEdgeIdToBlock = (
typebot: WritableDraft<Typebot>,
edgeId: string,
{ groupIndex, blockIndex }: BlockIndices
) => {
typebot.groups[groupIndex].blocks[blockIndex].outgoingEdgeId = edgeId
}
const addEdgeIdToItem = (
typebot: WritableDraft<Typebot>,
edgeId: string,
{ groupIndex, blockIndex, itemIndex }: ItemIndices
) =>
((typebot.groups[groupIndex].blocks[blockIndex] as BlockWithItems).items[
itemIndex
].outgoingEdgeId = edgeId)
export const deleteEdgeDraft = (
typebot: WritableDraft<Typebot>,
edgeId: string
) => {
const edgeIndex = typebot.edges.findIndex(byId(edgeId))
if (edgeIndex === -1) return
deleteOutgoingEdgeIdProps(typebot, edgeId)
typebot.edges.splice(edgeIndex, 1)
}
const deleteOutgoingEdgeIdProps = (
typebot: WritableDraft<Typebot>,
edgeId: string
) => {
const edge = typebot.edges.find(byId(edgeId))
if (!edge) return
const fromGroupIndex = typebot.groups.findIndex(byId(edge.from.groupId))
const fromBlockIndex = typebot.groups[fromGroupIndex].blocks.findIndex(
byId(edge.from.blockId)
)
const block = typebot.groups[fromGroupIndex].blocks[fromBlockIndex] as
| Block
| undefined
const fromItemIndex =
edge.from.itemId && block && blockHasItems(block)
? block.items.findIndex(byId(edge.from.itemId))
: -1
if (fromBlockIndex !== -1)
typebot.groups[fromGroupIndex].blocks[fromBlockIndex].outgoingEdgeId =
undefined
if (fromItemIndex !== -1)
(
typebot.groups[fromGroupIndex].blocks[fromBlockIndex] as BlockWithItems
).items[fromItemIndex].outgoingEdgeId = undefined
}
export const cleanUpEdgeDraft = (
typebot: WritableDraft<Typebot>,
deletedNodeId: string
) => {
const edgesToDelete = typebot.edges.filter((edge) =>
[
edge.from.groupId,
edge.from.blockId,
edge.from.itemId,
edge.to.groupId,
edge.to.blockId,
].includes(deletedNodeId)
)
edgesToDelete.forEach((edge) => deleteEdgeDraft(typebot, edge.id))
}
const removeExistingEdge = (
typebot: WritableDraft<Typebot>,
edge: Omit<Edge, 'id'>
) => {
typebot.edges = typebot.edges.filter((e) =>
edge.from.itemId
? e.from.itemId !== edge.from.itemId
: isDefined(e.from.itemId) || e.from.blockId !== edge.from.blockId
)
}

View File

@@ -0,0 +1,102 @@
import cuid from 'cuid'
import { produce } from 'immer'
import { WritableDraft } from 'immer/dist/internal'
import {
Group,
DraggableBlock,
DraggableBlockType,
BlockIndices,
Typebot,
} from 'models'
import { SetTypebot } from '../TypebotProvider'
import { cleanUpEdgeDraft } from './edges'
import { createBlockDraft, duplicateBlockDraft } from './blocks'
import { Coordinates } from '@/features/graph'
export type GroupsActions = {
createGroup: (
props: Coordinates & {
id: string
block: DraggableBlock | DraggableBlockType
indices: BlockIndices
}
) => void
updateGroup: (groupIndex: number, updates: Partial<Omit<Group, 'id'>>) => void
duplicateGroup: (groupIndex: number) => void
deleteGroup: (groupIndex: number) => void
}
const groupsActions = (setTypebot: SetTypebot): GroupsActions => ({
createGroup: ({
id,
block,
indices,
...graphCoordinates
}: Coordinates & {
id: string
block: DraggableBlock | DraggableBlockType
indices: BlockIndices
}) =>
setTypebot((typebot) =>
produce(typebot, (typebot) => {
const newGroup: Group = {
id,
graphCoordinates,
title: `Group #${typebot.groups.length}`,
blocks: [],
}
typebot.groups.push(newGroup)
createBlockDraft(typebot, block, newGroup.id, indices)
})
),
updateGroup: (groupIndex: number, updates: Partial<Omit<Group, 'id'>>) =>
setTypebot((typebot) =>
produce(typebot, (typebot) => {
const block = typebot.groups[groupIndex]
typebot.groups[groupIndex] = { ...block, ...updates }
})
),
duplicateGroup: (groupIndex: number) =>
setTypebot((typebot) =>
produce(typebot, (typebot) => {
const group = typebot.groups[groupIndex]
const id = cuid()
const newGroup: Group = {
...group,
title: `${group.title} copy`,
id,
blocks: group.blocks.map(duplicateBlockDraft(id)),
graphCoordinates: {
x: group.graphCoordinates.x + 200,
y: group.graphCoordinates.y + 100,
},
}
typebot.groups.splice(groupIndex + 1, 0, newGroup)
})
),
deleteGroup: (groupIndex: number) =>
setTypebot((typebot) =>
produce(typebot, (typebot) => {
deleteGroupDraft(typebot)(groupIndex)
})
),
})
const deleteGroupDraft =
(typebot: WritableDraft<Typebot>) => (groupIndex: number) => {
cleanUpEdgeDraft(typebot, typebot.groups[groupIndex].id)
typebot.groups.splice(groupIndex, 1)
}
const removeEmptyGroups = (typebot: WritableDraft<Typebot>) => {
const emptyGroupsIndices = typebot.groups.reduce<number[]>(
(arr, group, idx) => {
group.blocks.length === 0 && arr.push(idx)
return arr
},
[]
)
emptyGroupsIndices.forEach(deleteGroupDraft(typebot))
}
export { groupsActions, removeEmptyGroups }

View File

@@ -0,0 +1,96 @@
import {
ItemIndices,
Item,
InputBlockType,
BlockWithItems,
ButtonItem,
} from 'models'
import { SetTypebot } from '../TypebotProvider'
import produce from 'immer'
import { cleanUpEdgeDraft } from './edges'
import { byId, blockHasItems } from 'utils'
import cuid from 'cuid'
export type ItemsActions = {
createItem: (
item: ButtonItem | Omit<ButtonItem, 'id'>,
indices: ItemIndices
) => void
updateItem: (indices: ItemIndices, updates: Partial<Omit<Item, 'id'>>) => void
detachItemFromBlock: (indices: ItemIndices) => void
deleteItem: (indices: ItemIndices) => void
}
const itemsAction = (setTypebot: SetTypebot): ItemsActions => ({
createItem: (
item: ButtonItem | Omit<ButtonItem, 'id'>,
{ groupIndex, blockIndex, itemIndex }: ItemIndices
) =>
setTypebot((typebot) =>
produce(typebot, (typebot) => {
const block = typebot.groups[groupIndex].blocks[blockIndex]
if (block.type !== InputBlockType.CHOICE) return
const newItem = {
...item,
blockId: block.id,
id: 'id' in item ? item.id : cuid(),
}
if (item.outgoingEdgeId) {
const edgeIndex = typebot.edges.findIndex(byId(item.outgoingEdgeId))
edgeIndex !== -1
? (typebot.edges[edgeIndex].from = {
groupId: block.groupId,
blockId: block.id,
itemId: newItem.id,
})
: (newItem.outgoingEdgeId = undefined)
}
block.items.splice(itemIndex, 0, newItem)
})
),
updateItem: (
{ groupIndex, blockIndex, itemIndex }: ItemIndices,
updates: Partial<Omit<Item, 'id'>>
) =>
setTypebot((typebot) =>
produce(typebot, (typebot) => {
const block = typebot.groups[groupIndex].blocks[blockIndex]
if (!blockHasItems(block)) return
;(
typebot.groups[groupIndex].blocks[blockIndex] as BlockWithItems
).items[itemIndex] = {
...block.items[itemIndex],
...updates,
} as Item
})
),
detachItemFromBlock: ({ groupIndex, blockIndex, itemIndex }: ItemIndices) =>
setTypebot((typebot) =>
produce(typebot, (typebot) => {
const block = typebot.groups[groupIndex].blocks[
blockIndex
] as BlockWithItems
block.items.splice(itemIndex, 1)
})
),
deleteItem: ({ groupIndex, blockIndex, itemIndex }: ItemIndices) =>
setTypebot((typebot) =>
produce(typebot, (typebot) => {
const block = typebot.groups[groupIndex].blocks[
blockIndex
] as BlockWithItems
const removingItem = block.items[itemIndex]
block.items.splice(itemIndex, 1)
cleanUpEdgeDraft(typebot, removingItem.id)
})
),
})
const duplicateItemDraft = (blockId: string) => (item: Item) => ({
...item,
id: cuid(),
blockId,
outgoingEdgeId: undefined,
})
export { itemsAction, duplicateItemDraft }

View File

@@ -0,0 +1,47 @@
import { Typebot, Variable } from 'models'
import { WritableDraft } from 'immer/dist/types/types-external'
import { SetTypebot } from '../TypebotProvider'
import { produce } from 'immer'
export type VariablesActions = {
createVariable: (variable: Variable) => void
updateVariable: (
variableId: string,
updates: Partial<Omit<Variable, 'id'>>
) => void
deleteVariable: (variableId: string) => void
}
export const variablesAction = (setTypebot: SetTypebot): VariablesActions => ({
createVariable: (newVariable: Variable) =>
setTypebot((typebot) =>
produce(typebot, (typebot) => {
typebot.variables.push(newVariable)
})
),
updateVariable: (
variableId: string,
updates: Partial<Omit<Variable, 'id'>>
) =>
setTypebot((typebot) =>
produce(typebot, (typebot) => {
typebot.variables = typebot.variables.map((v) =>
v.id === variableId ? { ...v, ...updates } : v
)
})
),
deleteVariable: (itemId: string) =>
setTypebot((typebot) =>
produce(typebot, (typebot) => {
deleteVariableDraft(typebot, itemId)
})
),
})
export const deleteVariableDraft = (
typebot: WritableDraft<Typebot>,
variableId: string
) => {
const index = typebot.variables.findIndex((v) => v.id === variableId)
typebot.variables.splice(index, 1)
}

View File

@@ -0,0 +1 @@
export { TypebotProvider, useTypebot } from './TypebotProvider'