♻️ (builder) Change to features-centric folder structure
This commit is contained in:
committed by
Baptiste Arnaud
parent
3686465a85
commit
643571fe7d
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { TypebotProvider, useTypebot } from './TypebotProvider'
|
||||
Reference in New Issue
Block a user