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

@ -8,13 +8,17 @@ import {
} from '@chakra-ui/react'
import { ExpandIcon } from 'assets/icons'
import {
ConditionItem,
ConditionStep,
InputStepType,
IntegrationStepType,
LogicStepType,
Step,
StepIndices,
StepOptions,
TextBubbleStep,
Webhook,
WebhookStep,
} from 'models'
import { useRef } from 'react'
import {
@ -37,9 +41,9 @@ type Props = {
step: Exclude<Step, TextBubbleStep>
webhook?: Webhook
onExpandClick: () => void
onOptionsChange: (options: StepOptions) => void
onWebhookChange: (updates: Partial<Webhook>) => void
onStepChange: (updates: Partial<Step>) => void
onTestRequestClick: () => void
indices: StepIndices
}
export const SettingsPopoverContent = ({ onExpandClick, ...props }: Props) => {
@ -79,23 +83,35 @@ export const SettingsPopoverContent = ({ onExpandClick, ...props }: Props) => {
export const StepSettings = ({
step,
webhook,
onOptionsChange,
onWebhookChange,
onStepChange,
onTestRequestClick,
indices,
}: {
step: Step
webhook?: Webhook
onOptionsChange: (options: StepOptions) => void
onWebhookChange: (updates: Partial<Webhook>) => void
onStepChange: (step: Partial<Step>) => void
onTestRequestClick: () => void
indices: StepIndices
}) => {
const handleOptionsChange = (options: StepOptions) => {
onStepChange({ options } as Partial<Step>)
}
const handleWebhookChange = (updates: Partial<Webhook>) => {
onStepChange({
webhook: { ...(step as WebhookStep).webhook, ...updates },
} as Partial<Step>)
}
const handleItemChange = (updates: Partial<ConditionItem>) => {
onStepChange({
items: [{ ...(step as ConditionStep).items[0], ...updates }],
} as Partial<Step>)
}
switch (step.type) {
case InputStepType.TEXT: {
return (
<TextInputSettingsBody
options={step.options}
onOptionsChange={onOptionsChange}
onOptionsChange={handleOptionsChange}
/>
)
}
@ -103,7 +119,7 @@ export const StepSettings = ({
return (
<NumberInputSettingsBody
options={step.options}
onOptionsChange={onOptionsChange}
onOptionsChange={handleOptionsChange}
/>
)
}
@ -111,7 +127,7 @@ export const StepSettings = ({
return (
<EmailInputSettingsBody
options={step.options}
onOptionsChange={onOptionsChange}
onOptionsChange={handleOptionsChange}
/>
)
}
@ -119,7 +135,7 @@ export const StepSettings = ({
return (
<UrlInputSettingsBody
options={step.options}
onOptionsChange={onOptionsChange}
onOptionsChange={handleOptionsChange}
/>
)
}
@ -127,7 +143,7 @@ export const StepSettings = ({
return (
<DateInputSettingsBody
options={step.options}
onOptionsChange={onOptionsChange}
onOptionsChange={handleOptionsChange}
/>
)
}
@ -135,7 +151,7 @@ export const StepSettings = ({
return (
<PhoneNumberSettingsBody
options={step.options}
onOptionsChange={onOptionsChange}
onOptionsChange={handleOptionsChange}
/>
)
}
@ -143,7 +159,7 @@ export const StepSettings = ({
return (
<ChoiceInputSettingsBody
options={step.options}
onOptionsChange={onOptionsChange}
onOptionsChange={handleOptionsChange}
/>
)
}
@ -151,23 +167,20 @@ export const StepSettings = ({
return (
<SetVariableSettings
options={step.options}
onOptionsChange={onOptionsChange}
onOptionsChange={handleOptionsChange}
/>
)
}
case LogicStepType.CONDITION: {
return (
<ConditionSettingsBody
options={step.options}
onOptionsChange={onOptionsChange}
/>
<ConditionSettingsBody step={step} onItemChange={handleItemChange} />
)
}
case LogicStepType.REDIRECT: {
return (
<RedirectSettings
options={step.options}
onOptionsChange={onOptionsChange}
onOptionsChange={handleOptionsChange}
/>
)
}
@ -175,7 +188,7 @@ export const StepSettings = ({
return (
<GoogleSheetsSettingsBody
options={step.options}
onOptionsChange={onOptionsChange}
onOptionsChange={handleOptionsChange}
stepId={step.id}
/>
)
@ -184,18 +197,18 @@ export const StepSettings = ({
return (
<GoogleAnalyticsSettings
options={step.options}
onOptionsChange={onOptionsChange}
onOptionsChange={handleOptionsChange}
/>
)
}
case IntegrationStepType.WEBHOOK: {
return (
<WebhookSettings
options={step.options}
webhook={webhook as Webhook}
onOptionsChange={onOptionsChange}
onWebhookChange={onWebhookChange}
step={step}
onOptionsChange={handleOptionsChange}
onWebhookChange={handleWebhookChange}
onTestRequestClick={onTestRequestClick}
indices={indices}
/>
)
}

View File

@ -1,33 +1,40 @@
import { Flex } from '@chakra-ui/react'
import { DropdownList } from 'components/shared/DropdownList'
import { TableList } from 'components/shared/TableList'
import { Comparison, ConditionOptions, LogicalOperator, Table } from 'models'
import {
Comparison,
ConditionItem,
ConditionStep,
LogicalOperator,
} from 'models'
import React from 'react'
import { ComparisonItem } from './ComparisonsItem'
type ConditionSettingsBodyProps = {
options: ConditionOptions
onOptionsChange: (options: ConditionOptions) => void
step: ConditionStep
onItemChange: (updates: Partial<ConditionItem>) => void
}
export const ConditionSettingsBody = ({
options,
onOptionsChange,
step,
onItemChange,
}: ConditionSettingsBodyProps) => {
const handleComparisonsChange = (comparisons: Table<Comparison>) =>
onOptionsChange({ ...options, comparisons })
const itemContent = step.items[0].content
const handleComparisonsChange = (comparisons: Comparison[]) =>
onItemChange({ content: { ...itemContent, comparisons } })
const handleLogicalOperatorChange = (logicalOperator: LogicalOperator) =>
onOptionsChange({ ...options, logicalOperator })
onItemChange({ content: { ...itemContent, logicalOperator } })
return (
<TableList<Comparison>
initialItems={options.comparisons}
initialItems={itemContent.comparisons}
onItemsChange={handleComparisonsChange}
Item={ComparisonItem}
ComponentBetweenItems={() => (
<Flex justify="center">
<DropdownList<LogicalOperator>
currentItem={options.logicalOperator}
currentItem={itemContent.logicalOperator}
onItemSelect={handleLogicalOperatorChange}
items={Object.values(LogicalOperator)}
/>

View File

@ -6,14 +6,12 @@ import { useTypebot } from 'contexts/TypebotContext'
import { CredentialsType } from 'db'
import {
Cell,
defaultTable,
ExtractingCell,
GoogleSheetsAction,
GoogleSheetsGetOptions,
GoogleSheetsInsertRowOptions,
GoogleSheetsOptions,
GoogleSheetsUpdateRowOptions,
Table,
} from 'models'
import React, { useMemo } from 'react'
import {
@ -60,7 +58,7 @@ export const GoogleSheetsSettingsBody = ({
const newOptions: GoogleSheetsGetOptions = {
...options,
action,
cellsToExtract: defaultTable,
cellsToExtract: [],
}
return onOptionsChange({ ...newOptions })
}
@ -68,7 +66,7 @@ export const GoogleSheetsSettingsBody = ({
const newOptions: GoogleSheetsInsertRowOptions = {
...options,
action,
cellsToInsert: defaultTable,
cellsToInsert: [],
}
return onOptionsChange({ ...newOptions })
}
@ -76,7 +74,7 @@ export const GoogleSheetsSettingsBody = ({
const newOptions: GoogleSheetsUpdateRowOptions = {
...options,
action,
cellsToUpsert: defaultTable,
cellsToUpsert: [],
}
return onOptionsChange({ ...newOptions })
}
@ -155,16 +153,16 @@ const ActionOptions = ({
sheet: Sheet
onOptionsChange: (options: GoogleSheetsOptions) => void
}) => {
const handleInsertColumnsChange = (cellsToInsert: Table<Cell>) =>
const handleInsertColumnsChange = (cellsToInsert: Cell[]) =>
onOptionsChange({ ...options, cellsToInsert } as GoogleSheetsOptions)
const handleUpsertColumnsChange = (cellsToUpsert: Table<Cell>) =>
const handleUpsertColumnsChange = (cellsToUpsert: Cell[]) =>
onOptionsChange({ ...options, cellsToUpsert } as GoogleSheetsOptions)
const handleReferenceCellChange = (referenceCell: Cell) =>
onOptionsChange({ ...options, referenceCell } as GoogleSheetsOptions)
const handleExtractingCellsChange = (cellsToExtract: Table<ExtractingCell>) =>
const handleExtractingCellsChange = (cellsToExtract: ExtractingCell[]) =>
onOptionsChange({ ...options, cellsToExtract } as GoogleSheetsOptions)
const UpdatingCellItem = useMemo(
@ -194,9 +192,8 @@ const ActionOptions = ({
<Stack>
<Text>Row to select</Text>
<CellWithValueStack
id={'reference'}
columns={sheet.columns}
item={options.referenceCell ?? {}}
item={options.referenceCell ?? { id: 'reference' }}
onItemChange={handleReferenceCellChange}
/>
<Text>Cells to update</Text>
@ -213,9 +210,8 @@ const ActionOptions = ({
<Stack>
<Text>Row to select</Text>
<CellWithValueStack
id={'reference'}
columns={sheet.columns}
item={options.referenceCell ?? {}}
item={options.referenceCell ?? { id: 'reference' }}
onItemChange={handleReferenceCellChange}
/>
<Text>Cells to extract</Text>

View File

@ -1,5 +1,4 @@
import { FormLabel, Stack } from '@chakra-ui/react'
import { DebouncedInput } from 'components/shared/DebouncedInput'
import { SwitchWithLabel } from 'components/shared/SwitchWithLabel'
import { InputWithVariableButton } from 'components/shared/TextboxWithVariableButton'
import { RedirectOptions } from 'models'

View File

@ -20,7 +20,6 @@ export const HeadersInputs = (props: TableListItemProps<KeyValue>) => (
)
export const KeyValueInputs = ({
id,
item,
onItemChange,
keyPlaceholder,
@ -40,18 +39,18 @@ export const KeyValueInputs = ({
return (
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
<FormControl>
<FormLabel htmlFor={'key' + id}>Key:</FormLabel>
<FormLabel htmlFor={'key' + item.id}>Key:</FormLabel>
<InputWithVariableButton
id={'key' + id}
id={'key' + item.id}
initialValue={item.key ?? ''}
onChange={handleKeyChange}
placeholder={keyPlaceholder}
/>
</FormControl>
<FormControl>
<FormLabel htmlFor={'value' + id}>Value:</FormLabel>
<FormLabel htmlFor={'value' + item.id}>Value:</FormLabel>
<InputWithVariableButton
id={'value' + id}
id={'value' + item.id}
initialValue={item.value ?? ''}
onChange={handleValueChange}
placeholder={valuePlaceholder}

View File

@ -5,7 +5,6 @@ import { VariableSearchInput } from 'components/shared/VariableSearchInput'
import { VariableForTest, Variable } from 'models'
export const VariableForTestInputs = ({
id,
item,
onItemChange,
}: TableListItemProps<VariableForTest>) => {
@ -18,17 +17,17 @@ export const VariableForTestInputs = ({
return (
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
<FormControl>
<FormLabel htmlFor={'name' + id}>Variable name:</FormLabel>
<FormLabel htmlFor={'name' + item.id}>Variable name:</FormLabel>
<VariableSearchInput
id={'name' + id}
id={'name' + item.id}
initialVariableId={item.variableId}
onSelectVariable={handleVariableSelect}
/>
</FormControl>
<FormControl>
<FormLabel htmlFor={'value' + id}>Test value:</FormLabel>
<FormLabel htmlFor={'value' + item.id}>Test value:</FormLabel>
<DebouncedInput
id={'value' + id}
id={'value' + item.id}
initialValue={item.value ?? ''}
onChange={handleValueChange}
/>

View File

@ -15,11 +15,12 @@ import { useTypebot } from 'contexts/TypebotContext'
import {
HttpMethod,
KeyValue,
Table,
WebhookOptions,
VariableForTest,
Webhook,
ResponseVariableMapping,
WebhookStep,
StepIndices,
} from 'models'
import { DropdownList } from 'components/shared/DropdownList'
import { TableList, TableListItemProps } from 'components/shared/TableList'
@ -34,19 +35,19 @@ import { VariableForTestInputs } from './VariableForTestInputs'
import { DataVariableInputs } from './ResponseMappingInputs'
type Props = {
webhook: Webhook
options?: WebhookOptions
step: WebhookStep
onOptionsChange: (options: WebhookOptions) => void
onWebhookChange: (updates: Partial<Webhook>) => void
onTestRequestClick: () => void
indices: StepIndices
}
export const WebhookSettings = ({
options,
step: { webhook, options },
onOptionsChange,
webhook,
onWebhookChange,
onTestRequestClick,
indices,
}: Props) => {
const { typebot, save } = useTypebot()
const [isTestResponseLoading, setIsTestResponseLoading] = useState(false)
@ -62,23 +63,23 @@ export const WebhookSettings = ({
const handleMethodChange = (method: HttpMethod) => onWebhookChange({ method })
const handleQueryParamsChange = (queryParams: Table<KeyValue>) =>
const handleQueryParamsChange = (queryParams: KeyValue[]) =>
onWebhookChange({ queryParams })
const handleHeadersChange = (headers: Table<KeyValue>) =>
const handleHeadersChange = (headers: KeyValue[]) =>
onWebhookChange({ headers })
const handleBodyChange = (body: string) => onWebhookChange({ body })
const handleVariablesChange = (variablesForTest: Table<VariableForTest>) =>
options && onOptionsChange({ ...options, variablesForTest })
const handleVariablesChange = (variablesForTest: VariableForTest[]) =>
onOptionsChange({ ...options, variablesForTest })
const handleResponseMappingChange = (
responseVariableMapping: Table<ResponseVariableMapping>
) => options && onOptionsChange({ ...options, responseVariableMapping })
responseVariableMapping: ResponseVariableMapping[]
) => onOptionsChange({ ...options, responseVariableMapping })
const handleTestRequestClick = async () => {
if (!typebot || !webhook) return
if (!typebot) return
setIsTestResponseLoading(true)
onTestRequestClick()
await save()
@ -86,9 +87,10 @@ export const WebhookSettings = ({
typebot.id,
webhook.id,
convertVariableForTestToVariables(
options?.variablesForTest,
options.variablesForTest,
typebot.variables
)
),
indices
)
if (error) return toast({ title: error.name, description: error.message })
setTestResponse(JSON.stringify(data, undefined, 2))
@ -196,9 +198,7 @@ export const WebhookSettings = ({
</AccordionButton>
<AccordionPanel pb={4} as={Stack} spacing="6">
<TableList<ResponseVariableMapping>
initialItems={
options?.responseVariableMapping ?? { byId: {}, allIds: [] }
}
initialItems={options.responseVariableMapping}
onItemsChange={handleResponseMappingChange}
Item={ResponseMappingInputs}
addLabel="Add an entry"

View File

@ -4,21 +4,19 @@ import {
Popover,
PopoverTrigger,
useDisclosure,
useEventListener,
} from '@chakra-ui/react'
import React, { useEffect, useState } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import {
BubbleStep,
BubbleStepContent,
DraggableStep,
Step,
StepOptions,
TextBubbleContent,
TextBubbleStep,
Webhook,
} from 'models'
import { Coordinates, useGraph } from 'contexts/GraphContext'
import { useGraph } from 'contexts/GraphContext'
import { StepIcon } from 'components/editor/StepsSideBar/StepIcon'
import { isBubbleStep, isTextBubbleStep, isWebhookStep } from 'utils'
import { isBubbleStep, isTextBubbleStep, stepHasItems } from 'utils'
import { StepNodeContent } from './StepNodeContent/StepNodeContent'
import { useTypebot } from 'contexts/TypebotContext'
import { ContextMenu } from 'components/shared/ContextMenu'
@ -32,46 +30,41 @@ import { StepSettings } from './SettingsPopoverContent/SettingsPopoverContent'
import { TextBubbleEditor } from './TextBubbleEditor'
import { TargetEndpoint } from '../../Endpoints'
import { MediaBubblePopoverContent } from './MediaBubblePopoverContent'
import { NodePosition, useDragDistance } from 'contexts/GraphDndContext'
import { setMultipleRefs } from 'services/utils'
export const StepNode = ({
step,
isConnectable,
onMouseMoveBottomOfElement,
onMouseMoveTopOfElement,
indices,
onMouseDown,
}: {
step: Step
isConnectable: boolean
onMouseMoveBottomOfElement?: () => void
onMouseMoveTopOfElement?: () => void
onMouseDown?: (
stepNodePosition: { absolute: Coordinates; relative: Coordinates },
step: DraggableStep
) => void
indices: { stepIndex: number; blockIndex: number }
onMouseDown?: (stepNodePosition: NodePosition, step: DraggableStep) => void
}) => {
const { query } = useRouter()
const {
setConnectingIds,
connectingIds,
openedStepId,
setOpenedStepId,
blocksCoordinates,
} = useGraph()
const { detachStepFromBlock, updateStep, typebot, updateWebhook } =
useTypebot()
const { setConnectingIds, connectingIds, openedStepId, setOpenedStepId } =
useGraph()
const { updateStep } = useTypebot()
const [localStep, setLocalStep] = useState(step)
const [localWebhook, setLocalWebhook] = useState(
isWebhookStep(step)
? typebot?.webhooks.byId[step.options.webhookId ?? '']
: undefined
)
const [isConnecting, setIsConnecting] = useState(false)
const [isPopoverOpened, setIsPopoverOpened] = useState(
openedStepId === step.id
)
const stepRef = useRef<HTMLDivElement | null>(null)
const onDrag = (position: NodePosition) => {
if (step.type === 'start' || !onMouseDown) return
onMouseDown(position, step)
}
useDragDistance({
ref: stepRef,
onDrag,
isDisabled: !onMouseDown || step.type === 'start',
})
const [mouseDownEvent, setMouseDownEvent] =
useState<{ absolute: Coordinates; relative: Coordinates }>()
const [isEditing, setIsEditing] = useState<boolean>(
isTextBubbleStep(step) && step.content.plainText === ''
)
@ -98,15 +91,15 @@ export const StepNode = ({
}, [connectingIds, step.blockId, step.id])
const handleModalClose = () => {
updateStep(localStep.id, { ...localStep })
updateStep(indices, { ...localStep })
onModalClose()
}
const handleMouseEnter = () => {
if (connectingIds?.target)
if (connectingIds)
setConnectingIds({
...connectingIds,
target: { ...connectingIds.target, stepId: step.id },
target: { blockId: step.blockId, stepId: step.id },
})
}
@ -118,54 +111,16 @@ export const StepNode = ({
})
}
const handleMouseDown = (e: React.MouseEvent) => {
if (!onMouseDown) return
e.stopPropagation()
const element = e.currentTarget as HTMLDivElement
const rect = element.getBoundingClientRect()
const relativeX = e.clientX - rect.left
const relativeY = e.clientY - rect.top
setMouseDownEvent({
absolute: { x: e.clientX + relativeX, y: e.clientY + relativeY },
relative: { x: relativeX, y: relativeY },
})
}
const handleGlobalMouseUp = () => {
setMouseDownEvent(undefined)
}
useEventListener('mouseup', handleGlobalMouseUp)
const handleMouseUp = () => {
if (mouseDownEvent) {
setIsEditing(true)
}
}
const handleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
if (!onMouseMoveBottomOfElement || !onMouseMoveTopOfElement) return
const isMovingAndIsMouseDown =
mouseDownEvent &&
onMouseDown &&
(event.movementX > 0 || event.movementY > 0)
if (isMovingAndIsMouseDown && step.type !== 'start') {
onMouseDown(mouseDownEvent, step)
detachStepFromBlock(step.id)
setMouseDownEvent(undefined)
}
const element = event.currentTarget as HTMLDivElement
const rect = element.getBoundingClientRect()
const y = event.clientY - rect.top
if (y > rect.height / 2) onMouseMoveBottomOfElement()
else onMouseMoveTopOfElement()
}
const handleCloseEditor = () => {
const handleCloseEditor = (content: TextBubbleContent) => {
const updatedStep = { ...localStep, content } as Step
setLocalStep(updatedStep)
updateStep(indices, updatedStep)
setIsEditing(false)
}
const handleClick = (e: React.MouseEvent) => {
e.stopPropagation()
if (isTextBubbleStep(step)) setIsEditing(true)
setOpenedStepId(step.id)
}
@ -175,22 +130,16 @@ export const StepNode = ({
}
const updateOptions = () => {
updateStep(localStep.id, { ...localStep })
if (localWebhook) updateWebhook(localWebhook.id, { ...localWebhook })
updateStep(indices, { ...localStep })
}
const handleOptionsChange = (options: StepOptions) => {
setLocalStep({ ...localStep, options } as Step)
const handleStepChange = (updates: Partial<Step>) => {
setLocalStep({ ...localStep, ...updates } as Step)
}
const handleContentChange = (content: BubbleStepContent) =>
setLocalStep({ ...localStep, content } as Step)
const handleWebhookChange = (updates: Partial<Webhook>) => {
if (!localWebhook) return
setLocalWebhook({ ...localWebhook, ...updates })
}
useEffect(() => {
if (isPopoverOpened && openedStepId !== step.id) updateOptions()
setIsPopoverOpened(openedStepId === step.id)
@ -199,13 +148,12 @@ export const StepNode = ({
return isEditing && isTextBubbleStep(localStep) ? (
<TextBubbleEditor
stepId={localStep.id}
initialValue={localStep.content.richText}
onClose={handleCloseEditor}
/>
) : (
<ContextMenu<HTMLDivElement>
renderMenu={() => <StepNodeContextMenu stepId={step.id} />}
renderMenu={() => <StepNodeContextMenu indices={indices} />}
>
{(ref, isOpened) => (
<Popover
@ -217,14 +165,11 @@ export const StepNode = ({
<PopoverTrigger>
<Flex
pos="relative"
ref={ref}
onMouseMove={handleMouseMove}
onMouseDown={handleMouseDown}
ref={setMultipleRefs([ref, stepRef])}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onMouseUp={handleMouseUp}
onClick={handleClick}
data-testid={`step-${step.id}`}
data-testid={`step`}
w="full"
>
<HStack
@ -244,37 +189,34 @@ export const StepNode = ({
mt="1"
data-testid={`${localStep.id}-icon`}
/>
<StepNodeContent step={localStep} />
<StepNodeContent step={localStep} indices={indices} />
<TargetEndpoint
pos="absolute"
left="-32px"
top="19px"
stepId={localStep.id}
/>
{blocksCoordinates &&
isConnectable &&
hasDefaultConnector(localStep) && (
<SourceEndpoint
source={{
blockId: localStep.blockId,
stepId: localStep.id,
}}
pos="absolute"
right="15px"
bottom="18px"
/>
)}
{isConnectable && hasDefaultConnector(localStep) && (
<SourceEndpoint
source={{
blockId: localStep.blockId,
stepId: localStep.id,
}}
pos="absolute"
right="15px"
bottom="18px"
/>
)}
</HStack>
</Flex>
</PopoverTrigger>
{hasSettingsPopover(localStep) && (
<SettingsPopoverContent
step={localStep}
webhook={localWebhook}
onExpandClick={handleExpandClick}
onOptionsChange={handleOptionsChange}
onWebhookChange={handleWebhookChange}
onStepChange={handleStepChange}
onTestRequestClick={updateOptions}
indices={indices}
/>
)}
{isMediaBubbleStep(localStep) && (
@ -286,10 +228,9 @@ export const StepNode = ({
<SettingsModal isOpen={isModalOpen} onClose={handleModalClose}>
<StepSettings
step={localStep}
webhook={localWebhook}
onOptionsChange={handleOptionsChange}
onWebhookChange={handleWebhookChange}
onStepChange={handleStepChange}
onTestRequestClick={updateOptions}
indices={indices}
/>
</SettingsModal>
</Popover>

View File

@ -6,11 +6,11 @@ import {
InputStepType,
LogicStepType,
IntegrationStepType,
StepIndices,
} from 'models'
import { isInputStep } from 'utils'
import { ButtonNodesList } from '../../ButtonNode'
import { ItemNodesList } from '../../ItemNode'
import {
ConditionContent,
SetVariableContent,
TextBubbleContent,
VideoBubbleContent,
@ -23,9 +23,9 @@ import { PlaceholderContent } from './contents/PlaceholderContent'
type Props = {
step: Step | StartStep
isConnectable?: boolean
indices: StepIndices
}
export const StepNodeContent = ({ step }: Props) => {
export const StepNodeContent = ({ step, indices }: Props) => {
if (isInputStep(step) && step.options.variableId) {
return <WithVariableContent step={step} />
}
@ -52,13 +52,13 @@ export const StepNodeContent = ({ step }: Props) => {
return <Text color={'gray.500'}>Pick a date...</Text>
}
case InputStepType.CHOICE: {
return <ButtonNodesList step={step} />
return <ItemNodesList step={step} indices={indices} />
}
case LogicStepType.SET_VARIABLE: {
return <SetVariableContent step={step} />
}
case LogicStepType.CONDITION: {
return <ConditionContent step={step} />
return <ItemNodesList step={step} indices={indices} isReadOnly />
}
case LogicStepType.REDIRECT: {
return (

View File

@ -1,57 +0,0 @@
import { Flex, Stack, HStack, Tag, Text } from '@chakra-ui/react'
import { useTypebot } from 'contexts/TypebotContext'
import { ConditionStep } from 'models'
import { SourceEndpoint } from '../../../../Endpoints/SourceEndpoint'
export const ConditionContent = ({ step }: { step: ConditionStep }) => {
const { typebot } = useTypebot()
return (
<Flex>
{step.options?.comparisons.allIds.length === 0 ? (
<Text color={'gray.500'}>Configure...</Text>
) : (
<Stack>
{step.options?.comparisons.allIds.map((comparisonId, idx) => {
const comparison = step.options?.comparisons.byId[comparisonId]
const variable =
typebot?.variables.byId[comparison?.variableId ?? '']
return (
<HStack key={comparisonId} spacing={1}>
{idx > 0 && <Text>{step.options?.logicalOperator ?? ''}</Text>}
{variable?.name && (
<Tag bgColor="orange.400">{variable.name}</Tag>
)}
{comparison.comparisonOperator && (
<Text>{comparison?.comparisonOperator}</Text>
)}
{comparison?.value && (
<Tag bgColor={'green.400'}>{comparison.value}</Tag>
)}
</HStack>
)
})}
</Stack>
)}
<SourceEndpoint
source={{
blockId: step.blockId,
stepId: step.id,
conditionType: 'true',
}}
pos="absolute"
top="7px"
right="15px"
/>
<SourceEndpoint
source={{
blockId: step.blockId,
stepId: step.id,
conditionType: 'false',
}}
pos="absolute"
bottom="7px"
right="15px"
/>
</Flex>
)
}

View File

@ -1,12 +1,13 @@
import { Text } from '@chakra-ui/react'
import { useTypebot } from 'contexts/TypebotContext'
import { SetVariableStep } from 'models'
import { byId } from 'utils'
export const SetVariableContent = ({ step }: { step: SetVariableStep }) => {
const { typebot } = useTypebot()
const variableName =
typebot?.variables.byId[step.options?.variableId ?? '']?.name ?? ''
const expression = step.options?.expressionToEvaluate ?? ''
typebot?.variables.find(byId(step.options.variableId))?.name ?? ''
const expression = step.options.expressionToEvaluate ?? ''
return (
<Text color={'gray.500'}>
{variableName === '' && expression === ''

View File

@ -10,6 +10,7 @@ type Props = {
export const TextBubbleContent = ({ step }: Props) => {
const { typebot } = useTypebot()
if (!typebot) return <></>
return (
<Flex
flexDir={'column'}

View File

@ -1,18 +1,11 @@
import { Text } from '@chakra-ui/react'
import { useTypebot } from 'contexts/TypebotContext'
import { WebhookStep } from 'models'
import { useMemo } from 'react'
type Props = {
step: WebhookStep
}
export const WebhookContent = ({ step }: Props) => {
const { typebot } = useTypebot()
const webhook = useMemo(
() => typebot?.webhooks.byId[step.options?.webhookId ?? ''],
[step.options?.webhookId, typebot?.webhooks.byId]
)
export const WebhookContent = ({ step: { webhook } }: Props) => {
if (!webhook?.url) return <Text color="gray.500">Configure...</Text>
return (
<Text isTruncated pr="6">

View File

@ -2,6 +2,7 @@ import { InputStep } from 'models'
import { chakra, Text } from '@chakra-ui/react'
import React from 'react'
import { useTypebot } from 'contexts/TypebotContext'
import { byId } from 'utils'
type Props = {
step: InputStep
@ -9,8 +10,10 @@ type Props = {
export const WithVariableContent = ({ step }: Props) => {
const { typebot } = useTypebot()
const variableName =
typebot?.variables.byId[step.options.variableId as string].name
const variableName = typebot?.variables.find(
byId(step.options.variableId)
)?.name
return (
<Text>
Collect{' '}

View File

@ -1,4 +1,3 @@
export * from './ConditionContent'
export * from './SetVariableContent'
export * from './WithVariableContent'
export * from './VideoBubbleContent'

View File

@ -1,11 +1,13 @@
import { MenuList, MenuItem } from '@chakra-ui/react'
import { TrashIcon } from 'assets/icons'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import { StepIndices } from 'models'
export const StepNodeContextMenu = ({ stepId }: { stepId: string }) => {
type Props = { indices: StepIndices }
export const StepNodeContextMenu = ({ indices }: Props) => {
const { deleteStep } = useTypebot()
const handleDeleteClick = () => deleteStep(stepId)
const handleDeleteClick = () => deleteStep(indices)
return (
<MenuList>

View File

@ -1,12 +1,13 @@
import { StackProps, HStack } from '@chakra-ui/react'
import { StartStep, Step } from 'models'
import { StartStep, Step, StepIndices } from 'models'
import { StepIcon } from 'components/editor/StepsSideBar/StepIcon'
import { StepNodeContent } from './StepNodeContent/StepNodeContent'
export const StepNodeOverlay = ({
step,
indices,
...props
}: { step: Step | StartStep } & StackProps) => {
}: { step: Step | StartStep; indices: StepIndices } & StackProps) => {
return (
<HStack
p="3"
@ -20,7 +21,7 @@ export const StepNodeOverlay = ({
{...props}
>
<StepIcon type={step.type} />
<StepNodeContent step={step} />
<StepNodeContent step={step} indices={indices} />
</HStack>
)
}

View File

@ -1,105 +1,123 @@
import { useEventListener, Stack, Flex, Portal } from '@chakra-ui/react'
import { DraggableStep } from 'models'
import { useStepDnd } from 'contexts/StepDndContext'
import { DraggableStep, DraggableStepType, Step } from 'models'
import {
computeNearestPlaceholderIndex,
useStepDnd,
} from 'contexts/GraphDndContext'
import { Coordinates, useGraph } from 'contexts/GraphContext'
import { useMemo, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import { useTypebot } from 'contexts/TypebotContext'
import { StepNode } from './StepNode'
import { StepNodeOverlay } from './StepNodeOverlay'
type Props = {
blockId: string
steps: Step[]
blockIndex: number
blockRef: React.MutableRefObject<HTMLDivElement | null>
isStartBlock: boolean
}
export const StepNodesList = ({
blockId,
stepIds,
}: {
blockId: string
stepIds: string[]
}) => {
steps,
blockIndex,
blockRef,
isStartBlock,
}: Props) => {
const {
draggedStep,
setDraggedStep,
draggedStepType,
mouseOverBlockId,
mouseOverBlock,
setDraggedStepType,
setMouseOverBlockId,
} = useStepDnd()
const { typebot, createStep } = useTypebot()
const { typebot, createStep, detachStepFromBlock } = useTypebot()
const { isReadOnly } = useGraph()
const [expandedPlaceholderIndex, setExpandedPlaceholderIndex] = useState<
number | undefined
>()
const showSortPlaceholders = useMemo(
() => mouseOverBlockId === blockId && (draggedStep || draggedStepType),
[mouseOverBlockId, blockId, draggedStep, draggedStepType]
)
const placeholderRefs = useRef<HTMLDivElement[]>([])
const [position, setPosition] = useState({
x: 0,
y: 0,
})
const [relativeCoordinates, setRelativeCoordinates] = useState({ x: 0, y: 0 })
const [mousePositionInElement, setMousePositionInElement] = useState({
x: 0,
y: 0,
})
const isDraggingOnCurrentBlock =
(draggedStep || draggedStepType) && mouseOverBlock?.id === blockId
const showSortPlaceholders = !isStartBlock && (draggedStep || draggedStepType)
const handleStepMove = (event: MouseEvent) => {
if (!draggedStep) return
useEffect(() => {
if (mouseOverBlock?.id !== blockId) setExpandedPlaceholderIndex(undefined)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mouseOverBlock?.id])
const handleMouseMoveGlobal = (event: MouseEvent) => {
if (!draggedStep || draggedStep.blockId !== blockId) return
const { clientX, clientY } = event
setPosition({
...position,
x: clientX - relativeCoordinates.x,
y: clientY - relativeCoordinates.y,
x: clientX - mousePositionInElement.x,
y: clientY - mousePositionInElement.y,
})
}
useEventListener('mousemove', handleStepMove)
useEventListener('mousemove', handleMouseMoveGlobal)
const handleMouseMove = (event: React.MouseEvent) => {
if (!draggedStep) return
const element = event.currentTarget as HTMLDivElement
const rect = element.getBoundingClientRect()
const y = event.clientY - rect.top
if (y < 20) setExpandedPlaceholderIndex(0)
const handleMouseMoveOnBlock = (event: MouseEvent) => {
if (!isDraggingOnCurrentBlock) return
setExpandedPlaceholderIndex(
computeNearestPlaceholderIndex(event.pageY, placeholderRefs)
)
}
useEventListener('mousemove', handleMouseMoveOnBlock, blockRef.current)
const handleMouseUp = (e: React.MouseEvent<HTMLDivElement>) => {
if (expandedPlaceholderIndex === undefined) return
e.stopPropagation()
setMouseOverBlockId(undefined)
const handleMouseUpOnBlock = (e: MouseEvent) => {
setExpandedPlaceholderIndex(undefined)
if (!draggedStep && !draggedStepType) return
if (!isDraggingOnCurrentBlock) return
const stepIndex = computeNearestPlaceholderIndex(e.clientY, placeholderRefs)
createStep(
blockId,
draggedStep || draggedStepType,
expandedPlaceholderIndex
(draggedStep || draggedStepType) as DraggableStep | DraggableStepType,
{
blockIndex,
stepIndex,
}
)
setDraggedStep(undefined)
setDraggedStepType(undefined)
}
useEventListener(
'mouseup',
handleMouseUpOnBlock,
mouseOverBlock?.ref.current,
{
capture: true,
}
)
const handleStepMouseDown = (
{ absolute, relative }: { absolute: Coordinates; relative: Coordinates },
step: DraggableStep
) => {
if (isReadOnly) return
setPosition(absolute)
setRelativeCoordinates(relative)
setMouseOverBlockId(blockId)
setDraggedStep(step)
}
const handleStepMouseDown =
(stepIndex: number) =>
(
{ absolute, relative }: { absolute: Coordinates; relative: Coordinates },
step: DraggableStep
) => {
if (isReadOnly) return
detachStepFromBlock({ blockIndex, stepIndex })
setPosition(absolute)
setMousePositionInElement(relative)
setDraggedStep(step)
}
const handleMouseOnTopOfStep = (stepIndex: number) => () => {
if (!draggedStep && !draggedStepType) return
setExpandedPlaceholderIndex(stepIndex === 0 ? 0 : stepIndex)
}
const handleMouseOnBottomOfStep = (stepIndex: number) => () => {
if (!draggedStep && !draggedStepType) return
setExpandedPlaceholderIndex(stepIndex + 1)
}
const handlePushElementRef =
(idx: number) => (elem: HTMLDivElement | null) => {
elem && (placeholderRefs.current[idx] = elem)
}
return (
<Stack
spacing={1}
onMouseUpCapture={handleMouseUp}
onMouseMove={handleMouseMove}
transition="none"
>
<Stack spacing={1} transition="none">
<Flex
ref={handlePushElementRef(0)}
h={
showSortPlaceholders && expandedPlaceholderIndex === 0
? '50px'
@ -111,17 +129,17 @@ export const StepNodesList = ({
transition={showSortPlaceholders ? 'height 200ms' : 'none'}
/>
{typebot &&
stepIds.map((stepId, idx) => (
<Stack key={stepId} spacing={1}>
steps.map((step, idx) => (
<Stack key={step.id} spacing={1}>
<StepNode
key={stepId}
step={typebot.steps.byId[stepId]}
isConnectable={!isReadOnly && stepIds.length - 1 === idx}
onMouseMoveTopOfElement={handleMouseOnTopOfStep(idx)}
onMouseMoveBottomOfElement={handleMouseOnBottomOfStep(idx)}
onMouseDown={handleStepMouseDown}
key={step.id}
step={step}
indices={{ blockIndex, stepIndex: idx }}
isConnectable={!isReadOnly && steps.length - 1 === idx}
onMouseDown={handleStepMouseDown(idx)}
/>
<Flex
ref={handlePushElementRef(idx + 1)}
h={
showSortPlaceholders && expandedPlaceholderIndex === idx + 1
? '50px'
@ -138,6 +156,7 @@ export const StepNodesList = ({
<Portal>
<StepNodeOverlay
step={draggedStep}
indices={{ blockIndex, stepIndex: 0 }}
pos="fixed"
top="0"
left="0"

View File

@ -8,21 +8,19 @@ import {
withPlate,
} from '@udecode/plate-core'
import { editorStyle, platePlugins } from 'libs/plate'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import { BaseSelection, createEditor, Transforms } from 'slate'
import { ToolBar } from './ToolBar'
import { parseHtmlStringToPlainText } from 'services/utils'
import { TextBubbleStep, Variable } from 'models'
import { defaultTextBubbleContent, TextBubbleContent, Variable } from 'models'
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
import { ReactEditor } from 'slate-react'
type Props = {
stepId: string
initialValue: TDescendant[]
onClose: () => void
onClose: (newContent: TextBubbleContent) => void
}
export const TextBubbleEditor = ({ initialValue, stepId, onClose }: Props) => {
export const TextBubbleEditor = ({ initialValue, onClose }: Props) => {
const randomEditorId = useMemo(() => Math.random().toString(), [])
const editor = useMemo(
() =>
@ -30,7 +28,6 @@ export const TextBubbleEditor = ({ initialValue, stepId, onClose }: Props) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
)
const { updateStep } = useTypebot()
const [value, setValue] = useState(initialValue)
const varDropdownRef = useRef<HTMLDivElement | null>(null)
const rememberedSelection = useRef<BaseSelection | null>(null)
@ -38,12 +35,11 @@ export const TextBubbleEditor = ({ initialValue, stepId, onClose }: Props) => {
const textEditorRef = useRef<HTMLDivElement>(null)
const closeEditor = () => onClose(convertValueToStepContent(value))
useOutsideClick({
ref: textEditorRef,
handler: () => {
save(value)
onClose()
},
handler: closeEditor,
})
useEffect(() => {
@ -69,18 +65,16 @@ export const TextBubbleEditor = ({ initialValue, stepId, onClose }: Props) => {
}
}
const save = (value: unknown[]) => {
if (value.length === 0) return
const convertValueToStepContent = (value: unknown[]): TextBubbleContent => {
if (value.length === 0) defaultTextBubbleContent
const html = serializeHtml(editor, {
nodes: value,
})
updateStep(stepId, {
content: {
html,
richText: value,
plainText: parseHtmlStringToPlainText(html),
},
} as TextBubbleStep)
return {
html,
richText: value,
plainText: parseHtmlStringToPlainText(html),
}
}
const handleMouseDown = (e: React.MouseEvent) => {
@ -99,6 +93,11 @@ export const TextBubbleEditor = ({ initialValue, stepId, onClose }: Props) => {
setValue(val)
setIsVariableDropdownOpen(false)
}
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.shiftKey) return
if (e.key === 'Enter') closeEditor()
}
return (
<Stack
flex="1"
@ -126,6 +125,7 @@ export const TextBubbleEditor = ({ initialValue, stepId, onClose }: Props) => {
onBlur: () => {
rememberedSelection.current = editor.selection
},
onKeyDown: handleKeyDown,
}}
initialValue={
initialValue.length === 0