2
0

chore(editor): ♻️ Revert tables to arrays

Yet another refacto. I improved many many mechanisms on this one including dnd. It is now end 2 end tested 🎉
This commit is contained in:
Baptiste Arnaud
2022-02-04 19:00:08 +01:00
parent 8a350eee6c
commit 524ef0812c
123 changed files with 2998 additions and 3112 deletions

View File

@ -1,13 +1,11 @@
import { Edge, Table, Target } from 'models'
import { Edge, IdMap } from 'models'
import { AnchorsPositionProps } from 'components/shared/Graph/Edges/Edge'
import {
stubLength,
blockWidth,
blockAnchorsOffset,
ConnectingIds,
Endpoint,
Coordinates,
BlocksCoordinates,
} from 'contexts/GraphContext'
import { roundCorners } from 'svg-round-corners'
import { headerHeight } from 'components/shared/TypebotHeader'
@ -230,20 +228,11 @@ export const computeEdgePath = ({
}
export const computeConnectingEdgePath = ({
connectingIds,
sourceBlockCoordinates,
targetBlockCoordinates,
sourceTop,
targetTop,
blocksCoordinates,
}: {
connectingIds: Omit<ConnectingIds, 'target'> & { target: Target }
sourceTop: number
targetTop?: number
blocksCoordinates: BlocksCoordinates
}) => {
const sourceBlockCoordinates =
blocksCoordinates.byId[connectingIds.source.blockId]
const targetBlockCoordinates =
blocksCoordinates.byId[connectingIds.target.blockId]
}: GetAnchorsPositionParams) => {
const anchorsPosition = getAnchorsPosition({
sourceBlockCoordinates,
targetBlockCoordinates,
@ -254,23 +243,25 @@ export const computeConnectingEdgePath = ({
}
export const computeEdgePathToMouse = ({
blockPosition,
sourceBlockCoordinates,
mousePosition,
sourceTop,
}: {
blockPosition: Coordinates
sourceBlockCoordinates: Coordinates
mousePosition: Coordinates
sourceTop: number
}): string => {
const sourcePosition = {
x:
mousePosition.x - blockPosition.x > blockWidth / 2
? blockPosition.x + blockWidth - 40
: blockPosition.x + 40,
mousePosition.x - sourceBlockCoordinates.x > blockWidth / 2
? sourceBlockCoordinates.x + blockWidth - 40
: sourceBlockCoordinates.x + 40,
y: sourceTop,
}
const sourceType =
mousePosition.x - blockPosition.x > blockWidth / 2 ? 'right' : 'left'
mousePosition.x - sourceBlockCoordinates.x > blockWidth / 2
? 'right'
: 'left'
const segments = computeThreeSegments(
sourcePosition,
mousePosition,
@ -284,11 +275,11 @@ export const computeEdgePathToMouse = ({
export const getEndpointTopOffset = (
graphPosition: Coordinates,
endpoints: Table<Endpoint>,
endpoints: IdMap<Endpoint>,
endpointId?: string
): number | undefined => {
if (!endpointId) return
const endpointRef = endpoints.byId[endpointId]?.ref
const endpointRef = endpoints[endpointId]?.ref
if (!endpointRef) return
return (
8 +
@ -299,4 +290,4 @@ export const getEndpointTopOffset = (
}
export const getSourceEndpointId = (edge?: Edge) =>
edge?.from.buttonId ?? edge?.from.stepId + `${edge?.from.conditionType ?? ''}`
edge?.from.itemId ?? edge?.from.stepId

View File

@ -2,7 +2,7 @@ import { sendRequest } from 'utils'
import { stringify } from 'qs'
import useSWR from 'swr'
import { fetcher } from './utils'
import { Table, Variable, VariableForTest, WebhookResponse } from 'models'
import { StepIndices, Variable, VariableForTest, WebhookResponse } from 'models'
export const getGoogleSheetsConsentScreenUrl = (
redirectUrl: string,
@ -69,10 +69,11 @@ export const useSheets = ({
export const executeWebhook = (
typebotId: string,
webhookId: string,
variables: Table<Variable>
variables: Variable[],
{ blockIndex, stepIndex }: StepIndices
) =>
sendRequest<WebhookResponse>({
url: `/api/typebots/${typebotId}/webhooks/${webhookId}/execute`,
url: `/api/typebots/${typebotId}/blocks/${blockIndex}/steps/${stepIndex}/executeWebhook`,
method: 'POST',
body: {
variables,
@ -80,28 +81,21 @@ export const executeWebhook = (
})
export const convertVariableForTestToVariables = (
variablesForTest: Table<VariableForTest> | undefined,
variables: Table<Variable>
): Table<Variable> => {
if (!variablesForTest) return { byId: {}, allIds: [] }
return {
byId: {
...variables.byId,
...variablesForTest.allIds.reduce((obj, id) => {
const variableForTest = variablesForTest.byId[id]
if (!variableForTest.variableId) return {}
const variable = variables.byId[variableForTest.variableId ?? '']
return {
...obj,
[variableForTest.variableId]: {
...variable,
value: variableForTest.value,
},
}
variablesForTest: VariableForTest[],
variables: Variable[]
): Variable[] => {
if (!variablesForTest) return []
return [
...variables,
...variablesForTest
.filter((v) => v.variableId)
.map((variableForTest) => {
const variable = variables.find(
(v) => v.id === variableForTest.variableId
) as Variable
return { ...variable, value: variableForTest.value }
}, {}),
},
allIds: variables.allIds,
}
]
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@ -1,26 +1,39 @@
import { PublicTypebot, Typebot } from 'models'
import {
Block,
InputStep,
PublicBlock,
PublicStep,
PublicTypebot,
Step,
Typebot,
} from 'models'
import shortId from 'short-uuid'
import { HStack, Text } from '@chakra-ui/react'
import { CalendarIcon } from 'assets/icons'
import { StepIcon } from 'components/editor/StepsSideBar/StepIcon'
import { isInputStep, sendRequest } from 'utils'
import { isDefined } from '@udecode/plate-common'
export const parseTypebotToPublicTypebot = (
typebot: Typebot
): PublicTypebot => ({
...typebot,
id: shortId.generate(),
blocks: typebot.blocks,
steps: typebot.steps,
name: typebot.name,
typebotId: typebot.id,
theme: typebot.theme,
settings: typebot.settings,
publicId: typebot.publicId,
choiceItems: typebot.choiceItems,
variables: typebot.variables,
edges: typebot.edges,
blocks: parseBlocksToPublicBlocks(typebot.blocks),
})
const parseBlocksToPublicBlocks = (blocks: Block[]): PublicBlock[] =>
blocks.map((b) => ({
...b,
steps: b.steps.map(
(s) =>
('webhook' in s && isDefined(s.webhook)
? { ...s, webhook: s.webhook.id }
: s) as PublicStep
),
}))
export const createPublishedTypebot = async (
typebot: Omit<PublicTypebot, 'id'>
) =>
@ -41,12 +54,11 @@ export const updatePublishedTypebot = async (
})
export const parseSubmissionsColumns = (
typebot?: PublicTypebot
typebot: PublicTypebot
): {
Header: JSX.Element
accessor: string
}[] => {
if (!typebot) return []
return [
{
Header: (
@ -57,14 +69,14 @@ export const parseSubmissionsColumns = (
),
accessor: 'createdAt',
},
...typebot.blocks.allIds
.filter((blockId) => typebot && blockContainsInput(typebot, blockId))
.map((blockId) => {
const block = typebot.blocks.byId[blockId]
const inputStepId = block.stepIds.find((stepId) =>
isInputStep(typebot.steps.byId[stepId])
)
const inputStep = typebot.steps.byId[inputStepId as string]
...typebot.blocks
.filter(
(block) => typebot && block.steps.some((step) => isInputStep(step))
)
.map((block) => {
const inputStep = block.steps.find((step) =>
isInputStep(step)
) as InputStep
return {
Header: (
<HStack>
@ -72,16 +84,8 @@ export const parseSubmissionsColumns = (
<Text>{block.title}</Text>
</HStack>
),
accessor: blockId,
accessor: block.id,
}
}),
]
}
const blockContainsInput = (
typebot: PublicTypebot | Typebot,
blockId: string
) =>
typebot.blocks.byId[blockId].stepIds.some((stepId) =>
isInputStep(typebot.steps.byId[stepId])
)

View File

@ -24,18 +24,27 @@ import {
defaultUrlInputOptions,
defaultChoiceInputOptions,
defaultSetVariablesOptions,
defaultConditionOptions,
defaultRedirectOptions,
defaultGoogleSheetsOptions,
defaultGoogleAnalyticsOptions,
defaultWebhookOptions,
StepWithOptionsType,
defaultWebhookAttributes,
Webhook,
Item,
ItemType,
defaultConditionContent,
} from 'models'
import shortId, { generate } from 'short-uuid'
import { Typebot } from 'models'
import useSWR from 'swr'
import { fetcher, toKebabCase } from './utils'
import { isBubbleStepType, stepTypeHasOption } from 'utils'
import {
isBubbleStepType,
stepTypeHasItems,
stepTypeHasOption,
stepTypeHasWebhook,
} from 'utils'
import { deepEqual } from 'fast-equals'
import { stringify } from 'qs'
import { isChoiceInput, isConditionStep, sendRequest } from 'utils'
@ -125,9 +134,35 @@ export const parseNewStep = (
options: stepTypeHasOption(type)
? parseDefaultStepOptions(type)
: undefined,
webhook: stepTypeHasWebhook(type) ? parseDefaultWebhook() : undefined,
items: stepTypeHasItems(type) ? parseDefaultItems(type, id) : undefined,
} as DraggableStep
}
const parseDefaultWebhook = (): Webhook => ({
id: generate(),
...defaultWebhookAttributes,
})
const parseDefaultItems = (
type: LogicStepType.CONDITION | InputStepType.CHOICE,
stepId: string
): Item[] => {
switch (type) {
case InputStepType.CHOICE:
return [{ id: generate(), stepId, type: ItemType.BUTTON }]
case LogicStepType.CONDITION:
return [
{
id: generate(),
stepId,
type: ItemType.CONDITION,
content: defaultConditionContent,
},
]
}
}
const parseDefaultContent = (type: BubbleStepType): BubbleStepContent => {
switch (type) {
case BubbleStepType.TEXT:
@ -154,11 +189,9 @@ const parseDefaultStepOptions = (type: StepWithOptionsType): StepOptions => {
case InputStepType.URL:
return defaultUrlInputOptions
case InputStepType.CHOICE:
return { ...defaultChoiceInputOptions, itemIds: [generate()] }
return defaultChoiceInputOptions
case LogicStepType.SET_VARIABLE:
return defaultSetVariablesOptions
case LogicStepType.CONDITION:
return defaultConditionOptions
case LogicStepType.REDIRECT:
return defaultRedirectOptions
case IntegrationStepType.GOOGLE_SHEETS:
@ -166,7 +199,7 @@ const parseDefaultStepOptions = (type: StepWithOptionsType): StepOptions => {
case IntegrationStepType.GOOGLE_ANALYTICS:
return defaultGoogleAnalyticsOptions
case IntegrationStepType.WEBHOOK:
return { ...defaultWebhookOptions, webhookId: generate() }
return defaultWebhookOptions
}
}
@ -181,7 +214,6 @@ export const checkIfPublished = (
publicTypebot: PublicTypebot
) =>
deepEqual(typebot.blocks, publicTypebot.blocks) &&
deepEqual(typebot.steps, publicTypebot.steps) &&
typebot.name === publicTypebot.name &&
typebot.publicId === publicTypebot.publicId &&
deepEqual(typebot.settings, publicTypebot.settings) &&
@ -214,18 +246,15 @@ export const parseNewTypebot = ({
id: startBlockId,
title: 'Start',
graphCoordinates: { x: 0, y: 0 },
stepIds: [startStepId],
steps: [startStep],
}
return {
folderId,
name,
ownerId,
blocks: { byId: { [startBlockId]: startBlock }, allIds: [startBlockId] },
steps: { byId: { [startStepId]: startStep }, allIds: [startStepId] },
choiceItems: { byId: {}, allIds: [] },
variables: { byId: {}, allIds: [] },
edges: { byId: {}, allIds: [] },
webhooks: { byId: {}, allIds: [] },
blocks: [startBlock],
edges: [],
variables: [],
theme: defaultTheme,
settings: defaultSettings,
}

View File

@ -100,11 +100,8 @@ export const removeUndefinedFields = <T>(obj: T): T =>
export const stepHasOptions = (step: Step) => 'options' in step
export const parseVariableHighlight = (content: string, typebot?: Typebot) => {
if (!typebot) return content
const varNames = typebot.variables.allIds.map(
(varId) => typebot.variables.byId[varId].name
)
export const parseVariableHighlight = (content: string, typebot: Typebot) => {
const varNames = typebot.variables.map((v) => v.name)
return content.replace(/\{\{(.*?)\}\}/g, (fullMatch, foundVar) => {
if (varNames.some((val) => foundVar.includes(val))) {
return `<span style="background-color:#ff8b1a; color:#ffffff; padding: 0.125rem 0.25rem; border-radius: 0.35rem">${fullMatch.replace(
@ -115,3 +112,8 @@ export const parseVariableHighlight = (content: string, typebot?: Typebot) => {
return fullMatch
})
}
export const setMultipleRefs =
(refs: React.MutableRefObject<HTMLDivElement | null>[]) =>
(elem: HTMLDivElement) =>
refs.forEach((ref) => (ref.current = elem))