perf(e2e): ⚡️ Migrate to Playwright
This commit is contained in:
@ -1,7 +1,7 @@
|
||||
import { Flex } from '@chakra-ui/react'
|
||||
import React from 'react'
|
||||
import Graph from './graph/Graph'
|
||||
import { DndContext } from 'contexts/DndContext'
|
||||
import { StepDndContext } from 'contexts/StepDndContext'
|
||||
import { StepTypesList } from './StepTypesList'
|
||||
import { PreviewDrawer } from './preview/PreviewDrawer'
|
||||
import { RightPanel, useEditor } from 'contexts/EditorContext'
|
||||
@ -12,14 +12,14 @@ export const Board = () => {
|
||||
const { rightPanel } = useEditor()
|
||||
return (
|
||||
<Flex flex="1" pos="relative" bgColor="gray.50" h="full">
|
||||
<DndContext>
|
||||
<StepDndContext>
|
||||
<StepTypesList />
|
||||
<GraphProvider>
|
||||
<Graph flex="1" />
|
||||
<BoardMenuButton pos="absolute" right="40px" top="20px" />
|
||||
{rightPanel === RightPanel.PREVIEW && <PreviewDrawer />}
|
||||
</GraphProvider>
|
||||
</DndContext>
|
||||
</StepDndContext>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Flex, HStack, StackProps, Text } from '@chakra-ui/react'
|
||||
import { StepType, DraggableStepType } from 'models'
|
||||
import { useDnd } from 'contexts/DndContext'
|
||||
import { useStepDnd } from 'contexts/StepDndContext'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { StepIcon } from './StepIcon'
|
||||
import { StepTypeLabel } from './StepTypeLabel'
|
||||
@ -12,7 +12,7 @@ export const StepCard = ({
|
||||
type: DraggableStepType
|
||||
onMouseDown: (e: React.MouseEvent, type: DraggableStepType) => void
|
||||
}) => {
|
||||
const { draggedStepType } = useDnd()
|
||||
const { draggedStepType } = useStepDnd()
|
||||
const [isMouseDown, setIsMouseDown] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -13,12 +13,12 @@ import {
|
||||
IntegrationStepType,
|
||||
LogicStepType,
|
||||
} from 'models'
|
||||
import { useDnd } from 'contexts/DndContext'
|
||||
import { useStepDnd } from 'contexts/StepDndContext'
|
||||
import React, { useState } from 'react'
|
||||
import { StepCard, StepCardOverlay } from './StepCard'
|
||||
|
||||
export const StepTypesList = () => {
|
||||
const { setDraggedStepType, draggedStepType } = useDnd()
|
||||
const { setDraggedStepType, draggedStepType } = useStepDnd()
|
||||
const [position, setPosition] = useState({
|
||||
x: 0,
|
||||
y: 0,
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { Block } from 'models'
|
||||
import { useGraph } from 'contexts/GraphContext'
|
||||
import { useDnd } from 'contexts/DndContext'
|
||||
import { useStepDnd } from 'contexts/StepDndContext'
|
||||
import { StepsList } from './StepsList'
|
||||
import { filterTable, isDefined } from 'utils'
|
||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||
@ -22,8 +22,8 @@ type Props = {
|
||||
export const BlockNode = ({ block }: Props) => {
|
||||
const { connectingIds, setConnectingIds, previewingEdgeId } = useGraph()
|
||||
const { typebot, updateBlock } = useTypebot()
|
||||
const { setMouseOverBlockId } = useDnd()
|
||||
const { draggedStep, draggedStepType } = useDnd()
|
||||
const { setMouseOverBlockId } = useStepDnd()
|
||||
const { draggedStep, draggedStepType } = useStepDnd()
|
||||
const [isMouseDown, setIsMouseDown] = useState(false)
|
||||
const [isConnecting, setIsConnecting] = useState(false)
|
||||
const isPreviewing = useMemo(() => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Flex, Portal, Stack, Text, useEventListener } from '@chakra-ui/react'
|
||||
import { useDnd } from 'contexts/DndContext'
|
||||
import { useStepDnd } from 'contexts/StepDndContext'
|
||||
import { Coordinates } from 'contexts/GraphContext'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { ChoiceInputStep, ChoiceItem } from 'models'
|
||||
@ -19,7 +19,7 @@ export const ChoiceItemsList = ({ step }: ChoiceItemsListProps) => {
|
||||
mouseOverBlockId,
|
||||
setDraggedChoiceItem,
|
||||
setMouseOverBlockId,
|
||||
} = useDnd()
|
||||
} = useStepDnd()
|
||||
const showSortPlaceholders = useMemo(
|
||||
() => mouseOverBlockId === step.blockId && draggedChoiceItem,
|
||||
[draggedChoiceItem, mouseOverBlockId, step.blockId]
|
||||
|
@ -28,7 +28,6 @@ export const VideoUploadContent = ({ content, onSubmit }: Props) => {
|
||||
placeholder="Paste the video link..."
|
||||
initialValue={content?.url ?? ''}
|
||||
onChange={handleUrlChange}
|
||||
delay={100}
|
||||
/>
|
||||
<Text fontSize="sm" color="gray.400" textAlign="center">
|
||||
Works with Youtube, Vimeo and others
|
||||
|
@ -31,13 +31,12 @@ export const ChoiceInputSettingsBody = ({
|
||||
/>
|
||||
{options?.isMultipleChoice && (
|
||||
<Stack>
|
||||
<FormLabel mb="0" htmlFor="send">
|
||||
<FormLabel mb="0" htmlFor="button">
|
||||
Button label:
|
||||
</FormLabel>
|
||||
<DebouncedInput
|
||||
id="send"
|
||||
id="button"
|
||||
initialValue={options?.buttonLabel ?? 'Send'}
|
||||
delay={100}
|
||||
onChange={handleButtonLabelChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -40,7 +40,6 @@ export const ComparisonItem = ({
|
||||
/>
|
||||
{item.comparisonOperator !== ComparisonOperators.IS_SET && (
|
||||
<InputWithVariableButton
|
||||
delay={100}
|
||||
initialValue={item.value ?? ''}
|
||||
onChange={handleChangeValue}
|
||||
placeholder="Type a value..."
|
||||
|
@ -21,6 +21,7 @@ export const ConditionSettingsBody = ({
|
||||
|
||||
return (
|
||||
<TableList<Comparison>
|
||||
initialItems={options.comparisons}
|
||||
onItemsChange={handleComparisonsChange}
|
||||
Item={ComparisonItem}
|
||||
ComponentBetweenItems={() => (
|
||||
|
@ -49,7 +49,6 @@ export const DateInputSettingsBody = ({
|
||||
<DebouncedInput
|
||||
id="from"
|
||||
initialValue={options.labels.from}
|
||||
delay={100}
|
||||
onChange={handleFromChange}
|
||||
/>
|
||||
</Stack>
|
||||
@ -62,7 +61,6 @@ export const DateInputSettingsBody = ({
|
||||
<DebouncedInput
|
||||
id="to"
|
||||
initialValue={options.labels.to}
|
||||
delay={100}
|
||||
onChange={handleToChange}
|
||||
/>
|
||||
</Stack>
|
||||
@ -74,7 +72,6 @@ export const DateInputSettingsBody = ({
|
||||
<DebouncedInput
|
||||
id="button"
|
||||
initialValue={options.labels.button}
|
||||
delay={100}
|
||||
onChange={handleButtonLabelChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -29,7 +29,6 @@ export const EmailInputSettingsBody = ({
|
||||
<DebouncedInput
|
||||
id="placeholder"
|
||||
initialValue={options.labels.placeholder}
|
||||
delay={100}
|
||||
onChange={handlePlaceholderChange}
|
||||
/>
|
||||
</Stack>
|
||||
@ -40,7 +39,6 @@ export const EmailInputSettingsBody = ({
|
||||
<DebouncedInput
|
||||
id="button"
|
||||
initialValue={options.labels.button}
|
||||
delay={100}
|
||||
onChange={handleButtonLabelChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -51,7 +51,6 @@ export const GoogleAnalyticsSettings = ({
|
||||
id="tracking-id"
|
||||
initialValue={options?.trackingId ?? ''}
|
||||
placeholder="G-123456..."
|
||||
delay={100}
|
||||
onChange={handleTrackingIdChange}
|
||||
/>
|
||||
</Stack>
|
||||
@ -63,7 +62,6 @@ export const GoogleAnalyticsSettings = ({
|
||||
id="category"
|
||||
initialValue={options?.category ?? ''}
|
||||
placeholder="Example: Typebot"
|
||||
delay={100}
|
||||
onChange={handleCategoryChange}
|
||||
/>
|
||||
</Stack>
|
||||
@ -75,7 +73,6 @@ export const GoogleAnalyticsSettings = ({
|
||||
id="action"
|
||||
initialValue={options?.action ?? ''}
|
||||
placeholder="Example: Submit email"
|
||||
delay={100}
|
||||
onChange={handleActionChange}
|
||||
/>
|
||||
</Stack>
|
||||
@ -98,7 +95,6 @@ export const GoogleAnalyticsSettings = ({
|
||||
id="label"
|
||||
initialValue={options?.label ?? ''}
|
||||
placeholder="Example: Campaign Z"
|
||||
delay={100}
|
||||
onChange={handleLabelChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -149,7 +149,7 @@ const ActionOptions = ({
|
||||
case GoogleSheetsAction.INSERT_ROW:
|
||||
return (
|
||||
<TableList<Cell>
|
||||
initialItems={options.cellsToInsert}
|
||||
initialItems={options.cellsToInsert ?? { byId: {}, allIds: [] }}
|
||||
onItemsChange={handleInsertColumnsChange}
|
||||
Item={UpdatingCellItem}
|
||||
addLabel="Add a value"
|
||||
@ -167,7 +167,7 @@ const ActionOptions = ({
|
||||
/>
|
||||
<Text>Cells to update</Text>
|
||||
<TableList<Cell>
|
||||
initialItems={options.cellsToUpsert}
|
||||
initialItems={options.cellsToUpsert ?? { byId: {}, allIds: [] }}
|
||||
onItemsChange={handleUpsertColumnsChange}
|
||||
Item={UpdatingCellItem}
|
||||
addLabel="Add a value"
|
||||
@ -186,7 +186,7 @@ const ActionOptions = ({
|
||||
/>
|
||||
<Text>Cells to extract</Text>
|
||||
<TableList<ExtractingCell>
|
||||
initialItems={options.cellsToExtract}
|
||||
initialItems={options.cellsToExtract ?? { byId: {}, allIds: [] }}
|
||||
onItemsChange={handleExtractingCellsChange}
|
||||
Item={ExtractingCellItem}
|
||||
addLabel="Add a value"
|
||||
|
@ -30,8 +30,8 @@ export const SheetsDropdown = ({
|
||||
selectedItem={currentSheet?.name}
|
||||
items={(sheets ?? []).map((s) => s.name)}
|
||||
onValueChange={handleSpreadsheetSelect}
|
||||
placeholder={isLoading ? 'Loading...' : 'Select the sheet'}
|
||||
isDisabled={isLoading}
|
||||
placeholder={'Select the sheet'}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -28,8 +28,8 @@ export const SpreadsheetsDropdown = ({
|
||||
selectedItem={currentSpreadsheet?.name}
|
||||
items={(spreadsheets ?? []).map((s) => s.name)}
|
||||
onValueChange={handleSpreadsheetSelect}
|
||||
placeholder={isLoading ? 'Loading...' : 'Search for spreadsheet'}
|
||||
isDisabled={isLoading}
|
||||
placeholder={'Search for spreadsheet'}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -37,7 +37,6 @@ export const NumberInputSettingsBody = ({
|
||||
<DebouncedInput
|
||||
id="placeholder"
|
||||
initialValue={options.labels.placeholder}
|
||||
delay={100}
|
||||
onChange={handlePlaceholderChange}
|
||||
/>
|
||||
</Stack>
|
||||
@ -48,7 +47,6 @@ export const NumberInputSettingsBody = ({
|
||||
<DebouncedInput
|
||||
id="button"
|
||||
initialValue={options?.labels?.button ?? 'Send'}
|
||||
delay={100}
|
||||
onChange={handleButtonLabelChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -29,7 +29,6 @@ export const PhoneNumberSettingsBody = ({
|
||||
<DebouncedInput
|
||||
id="placeholder"
|
||||
initialValue={options.labels.placeholder}
|
||||
delay={100}
|
||||
onChange={handlePlaceholderChange}
|
||||
/>
|
||||
</Stack>
|
||||
@ -40,7 +39,6 @@ export const PhoneNumberSettingsBody = ({
|
||||
<DebouncedInput
|
||||
id="button"
|
||||
initialValue={options.labels.button}
|
||||
delay={100}
|
||||
onChange={handleButtonLabelChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -1,6 +1,7 @@
|
||||
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'
|
||||
import React from 'react'
|
||||
|
||||
@ -21,11 +22,10 @@ export const RedirectSettings = ({ options, onOptionsChange }: Props) => {
|
||||
<FormLabel mb="0" htmlFor="tracking-id">
|
||||
Url:
|
||||
</FormLabel>
|
||||
<DebouncedInput
|
||||
<InputWithVariableButton
|
||||
id="tracking-id"
|
||||
initialValue={options.url ?? ''}
|
||||
placeholder="Type a URL..."
|
||||
delay={100}
|
||||
onChange={handleUrlChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -37,7 +37,6 @@ export const SetVariableSettingsBody = ({
|
||||
<DebouncedTextarea
|
||||
id="expression"
|
||||
initialValue={options.expressionToEvaluate ?? ''}
|
||||
delay={100}
|
||||
onChange={handleExpressionChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -38,7 +38,6 @@ export const TextInputSettingsBody = ({
|
||||
<DebouncedInput
|
||||
id="placeholder"
|
||||
initialValue={options.labels.placeholder}
|
||||
delay={100}
|
||||
onChange={handlePlaceholderChange}
|
||||
/>
|
||||
</Stack>
|
||||
@ -49,7 +48,6 @@ export const TextInputSettingsBody = ({
|
||||
<DebouncedInput
|
||||
id="button"
|
||||
initialValue={options.labels.button}
|
||||
delay={100}
|
||||
onChange={handleButtonLabelChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -29,7 +29,6 @@ export const UrlInputSettingsBody = ({
|
||||
<DebouncedInput
|
||||
id="placeholder"
|
||||
initialValue={options.labels.placeholder}
|
||||
delay={100}
|
||||
onChange={handlePlaceholderChange}
|
||||
/>
|
||||
</Stack>
|
||||
@ -40,7 +39,6 @@ export const UrlInputSettingsBody = ({
|
||||
<DebouncedInput
|
||||
id="button"
|
||||
initialValue={options.labels.button}
|
||||
delay={100}
|
||||
onChange={handleButtonLabelChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -29,7 +29,6 @@ export const VariableForTestInputs = ({
|
||||
<FormLabel htmlFor={'value' + id}>Test value:</FormLabel>
|
||||
<DebouncedInput
|
||||
id={'value' + id}
|
||||
delay={100}
|
||||
initialValue={item.value ?? ''}
|
||||
onChange={handleValueChange}
|
||||
/>
|
||||
|
@ -130,7 +130,7 @@ export const WebhookSettings = ({
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||
<TableList<KeyValue>
|
||||
initialItems={webhook?.queryParams}
|
||||
initialItems={webhook?.queryParams ?? { byId: {}, allIds: [] }}
|
||||
onItemsChange={handleQueryParamsChange}
|
||||
Item={QueryParamsInputs}
|
||||
addLabel="Add a param"
|
||||
@ -144,7 +144,7 @@ export const WebhookSettings = ({
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||
<TableList<KeyValue>
|
||||
initialItems={webhook?.headers}
|
||||
initialItems={webhook?.headers ?? { byId: {}, allIds: [] }}
|
||||
onItemsChange={handleHeadersChange}
|
||||
Item={HeadersInputs}
|
||||
addLabel="Add a value"
|
||||
@ -171,7 +171,9 @@ export const WebhookSettings = ({
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||
<TableList<VariableForTest>
|
||||
initialItems={options?.variablesForTest}
|
||||
initialItems={
|
||||
options?.variablesForTest ?? { byId: {}, allIds: [] }
|
||||
}
|
||||
onItemsChange={handleVariablesChange}
|
||||
Item={VariableForTestInputs}
|
||||
addLabel="Add an entry"
|
||||
@ -194,7 +196,9 @@ export const WebhookSettings = ({
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||
<TableList<ResponseVariableMapping>
|
||||
initialItems={options?.responseVariableMapping}
|
||||
initialItems={
|
||||
options?.responseVariableMapping ?? { byId: {}, allIds: [] }
|
||||
}
|
||||
onItemsChange={handleResponseMappingChange}
|
||||
Item={ResponseMappingInputs}
|
||||
/>
|
||||
|
@ -8,10 +8,9 @@ import {
|
||||
} from '@chakra-ui/react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { BubbleStep, DraggableStep, Step, TextBubbleStep } from 'models'
|
||||
import { useGraph } from 'contexts/GraphContext'
|
||||
import { Coordinates, useGraph } from 'contexts/GraphContext'
|
||||
import { StepIcon } from 'components/board/StepTypesList/StepIcon'
|
||||
import { isBubbleStep, isTextBubbleStep } from 'utils'
|
||||
import { Coordinates } from '@dnd-kit/core/dist/types'
|
||||
import { TextEditor } from './TextEditor/TextEditor'
|
||||
import { StepNodeContent } from './StepNodeContent/StepNodeContent'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
@ -43,7 +42,8 @@ export const StepNode = ({
|
||||
) => void
|
||||
}) => {
|
||||
const { query } = useRouter()
|
||||
const { setConnectingIds, connectingIds } = useGraph()
|
||||
const { setConnectingIds, connectingIds, openedStepId, setOpenedStepId } =
|
||||
useGraph()
|
||||
const { moveStep } = useTypebot()
|
||||
const [isConnecting, setIsConnecting] = useState(false)
|
||||
const [mouseDownEvent, setMouseDownEvent] =
|
||||
@ -57,6 +57,11 @@ export const StepNode = ({
|
||||
onClose: onModalClose,
|
||||
} = useDisclosure()
|
||||
|
||||
useEffect(() => {
|
||||
if (query.stepId?.toString() === step.id) setOpenedStepId(step.id)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [query])
|
||||
|
||||
useEffect(() => {
|
||||
setIsConnecting(
|
||||
connectingIds?.target?.blockId === step.blockId &&
|
||||
@ -126,6 +131,16 @@ export const StepNode = ({
|
||||
setIsEditing(false)
|
||||
}
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
setOpenedStepId(step.id)
|
||||
}
|
||||
|
||||
const handleExpandClick = () => {
|
||||
setOpenedStepId(undefined)
|
||||
onModalOpen()
|
||||
}
|
||||
|
||||
return isEditing && isTextBubbleStep(step) ? (
|
||||
<TextEditor
|
||||
stepId={step.id}
|
||||
@ -137,11 +152,7 @@ export const StepNode = ({
|
||||
renderMenu={() => <StepNodeContextMenu stepId={step.id} />}
|
||||
>
|
||||
{(ref, isOpened) => (
|
||||
<Popover
|
||||
placement="left"
|
||||
isLazy
|
||||
defaultIsOpen={query.stepId?.toString() === step.id}
|
||||
>
|
||||
<Popover placement="left" isLazy isOpen={openedStepId === step.id}>
|
||||
<PopoverTrigger>
|
||||
<Flex
|
||||
pos="relative"
|
||||
@ -151,6 +162,7 @@ export const StepNode = ({
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onMouseUp={handleMouseUp}
|
||||
onClick={handleClick}
|
||||
data-testid={`step-${step.id}`}
|
||||
w="full"
|
||||
>
|
||||
@ -166,7 +178,11 @@ export const StepNode = ({
|
||||
align="flex-start"
|
||||
w="full"
|
||||
>
|
||||
<StepIcon type={step.type} mt="1" />
|
||||
<StepIcon
|
||||
type={step.type}
|
||||
mt="1"
|
||||
data-testid={`${step.id}-icon`}
|
||||
/>
|
||||
<StepNodeContent step={step} />
|
||||
<TargetEndpoint
|
||||
pos="absolute"
|
||||
@ -189,7 +205,10 @@ export const StepNode = ({
|
||||
</Flex>
|
||||
</PopoverTrigger>
|
||||
{hasSettingsPopover(step) && (
|
||||
<SettingsPopoverContent step={step} onExpandClick={onModalOpen} />
|
||||
<SettingsPopoverContent
|
||||
step={step}
|
||||
onExpandClick={handleExpandClick}
|
||||
/>
|
||||
)}
|
||||
{hasContentPopover(step) && <ContentPopover step={step} />}
|
||||
<SettingsModal isOpen={isModalOpen} onClose={onModalClose}>
|
||||
|
@ -7,26 +7,31 @@ export const ConditionNodeContent = ({ step }: { step: ConditionStep }) => {
|
||||
const { typebot } = useTypebot()
|
||||
return (
|
||||
<Flex>
|
||||
<Stack color={'gray.500'}>
|
||||
{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>
|
||||
{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,
|
||||
|
@ -80,11 +80,7 @@ export const StepNodeContent = ({ step }: Props) => {
|
||||
)
|
||||
}
|
||||
case InputStepType.DATE: {
|
||||
return (
|
||||
<Text color={'gray.500'}>
|
||||
{step.options?.labels?.from ?? 'Pick a date...'}
|
||||
</Text>
|
||||
)
|
||||
return <Text color={'gray.500'}>Pick a date...</Text>
|
||||
}
|
||||
case InputStepType.PHONE: {
|
||||
return (
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useEventListener, Stack, Flex, Portal } from '@chakra-ui/react'
|
||||
import { DraggableStep, Step, Table } from 'models'
|
||||
import { useDnd } from 'contexts/DndContext'
|
||||
import { useStepDnd } from 'contexts/StepDndContext'
|
||||
import { Coordinates } from 'contexts/GraphContext'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { StepNode, StepNodeOverlay } from './StepNode'
|
||||
@ -20,7 +20,7 @@ export const StepsList = ({
|
||||
mouseOverBlockId,
|
||||
setDraggedStepType,
|
||||
setMouseOverBlockId,
|
||||
} = useDnd()
|
||||
} = useStepDnd()
|
||||
const { createStep } = useTypebot()
|
||||
const [expandedPlaceholderIndex, setExpandedPlaceholderIndex] = useState<
|
||||
number | undefined
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Flex, FlexProps, useEventListener } from '@chakra-ui/react'
|
||||
import React, { useRef, useMemo } from 'react'
|
||||
import React, { useRef, useMemo, useEffect } from 'react'
|
||||
import { blockWidth, useGraph } from 'contexts/GraphContext'
|
||||
import { BlockNode } from './BlockNode/BlockNode'
|
||||
import { useDnd } from 'contexts/DndContext'
|
||||
import { useStepDnd } from 'contexts/StepDndContext'
|
||||
import { Edges } from './Edges'
|
||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||
import { headerHeight } from 'components/shared/TypebotHeader/TypebotHeader'
|
||||
@ -10,16 +10,23 @@ import { DraggableStepType } from 'models'
|
||||
|
||||
const Graph = ({ ...props }: FlexProps) => {
|
||||
const { draggedStepType, setDraggedStepType, draggedStep, setDraggedStep } =
|
||||
useDnd()
|
||||
useStepDnd()
|
||||
const graphContainerRef = useRef<HTMLDivElement | null>(null)
|
||||
const editorContainerRef = useRef<HTMLDivElement | null>(null)
|
||||
const { createBlock, typebot } = useTypebot()
|
||||
const { graphPosition, setGraphPosition } = useGraph()
|
||||
const { graphPosition, setGraphPosition, setOpenedStepId } = useGraph()
|
||||
const transform = useMemo(
|
||||
() =>
|
||||
`translate(${graphPosition.x}px, ${graphPosition.y}px) scale(${graphPosition.scale})`,
|
||||
[graphPosition]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
editorContainerRef.current = document.getElementById(
|
||||
'editor-container'
|
||||
) as HTMLDivElement
|
||||
}, [])
|
||||
|
||||
const handleMouseWheel = (e: WheelEvent) => {
|
||||
e.preventDefault()
|
||||
const isPinchingTrackpad = e.ctrlKey
|
||||
@ -57,6 +64,9 @@ const Graph = ({ ...props }: FlexProps) => {
|
||||
}
|
||||
useEventListener('mousedown', handleMouseDown, undefined, { capture: true })
|
||||
|
||||
const handleClick = () => setOpenedStepId(undefined)
|
||||
useEventListener('click', handleClick, editorContainerRef.current)
|
||||
|
||||
if (!typebot) return <></>
|
||||
return (
|
||||
<Flex ref={graphContainerRef} {...props}>
|
||||
|
@ -1,26 +1,20 @@
|
||||
import { DashboardFolder } from '.prisma/client'
|
||||
import { Typebot } from 'models'
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
Heading,
|
||||
HStack,
|
||||
Portal,
|
||||
Skeleton,
|
||||
Stack,
|
||||
useEventListener,
|
||||
useToast,
|
||||
Wrap,
|
||||
} from '@chakra-ui/react'
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
DragOverlay,
|
||||
DragStartEvent,
|
||||
MouseSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core'
|
||||
import { FolderPlusIcon } from 'assets/icons'
|
||||
import React, { useState } from 'react'
|
||||
import { useTypebotDnd } from 'contexts/TypebotDndContext'
|
||||
import { Typebot } from 'models'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { createFolder, useFolders } from 'services/folders'
|
||||
import { patchTypebot, useTypebots } from 'services/typebots'
|
||||
import { BackButton } from './FolderContent/BackButton'
|
||||
@ -31,16 +25,24 @@ import { TypebotCardOverlay } from './FolderContent/TypebotButtonOverlay'
|
||||
|
||||
type Props = { folder: DashboardFolder | null }
|
||||
|
||||
const dragDistanceTolerance = 20
|
||||
|
||||
export const FolderContent = ({ folder }: Props) => {
|
||||
const [isCreatingFolder, setIsCreatingFolder] = useState(false)
|
||||
const [draggedTypebot, setDraggedTypebot] = useState<Typebot | undefined>()
|
||||
const sensors = useSensors(
|
||||
useSensor(MouseSensor, {
|
||||
activationConstraint: {
|
||||
distance: 20,
|
||||
},
|
||||
})
|
||||
)
|
||||
const {
|
||||
setDraggedTypebot,
|
||||
draggedTypebot,
|
||||
mouseOverFolderId,
|
||||
setMouseOverFolderId,
|
||||
} = useTypebotDnd()
|
||||
const [mouseDownPosition, setMouseDownPosition] = useState({ x: 0, y: 0 })
|
||||
const [draggablePosition, setDraggablePosition] = useState({ x: 0, y: 0 })
|
||||
const [relativeDraggablePosition, setRelativeDraggablePosition] = useState({
|
||||
x: 0,
|
||||
y: 0,
|
||||
})
|
||||
const [typebotDragCandidate, setTypebotDragCandidate] = useState<Typebot>()
|
||||
|
||||
const toast = useToast({
|
||||
position: 'top-right',
|
||||
status: 'error',
|
||||
@ -67,19 +69,6 @@ export const FolderContent = ({ folder }: Props) => {
|
||||
},
|
||||
})
|
||||
|
||||
const handleDragStart = (event: DragStartEvent) => {
|
||||
if (!typebots) return
|
||||
setDraggedTypebot(typebots.find((c) => c.id === event.active.id))
|
||||
}
|
||||
|
||||
const handleDragEnd = async (event: DragEndEvent) => {
|
||||
if (!typebots) return
|
||||
const { over } = event
|
||||
if (over?.id && draggedTypebot?.id)
|
||||
await moveTypebotToFolder(draggedTypebot.id, over.id)
|
||||
setDraggedTypebot(undefined)
|
||||
}
|
||||
|
||||
const moveTypebotToFolder = async (typebotId: string, folderId: string) => {
|
||||
if (!typebots) return
|
||||
const { error } = await patchTypebot(typebotId, {
|
||||
@ -118,63 +107,103 @@ export const FolderContent = ({ folder }: Props) => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleMouseUp = async () => {
|
||||
if (mouseOverFolderId !== undefined && draggedTypebot)
|
||||
await moveTypebotToFolder(draggedTypebot.id, mouseOverFolderId ?? 'root')
|
||||
setTypebotDragCandidate(undefined)
|
||||
setMouseOverFolderId(undefined)
|
||||
setDraggedTypebot(undefined)
|
||||
}
|
||||
useEventListener('mouseup', handleMouseUp)
|
||||
|
||||
const handleMouseDown = (typebot: Typebot) => (e: React.MouseEvent) => {
|
||||
const element = e.currentTarget as HTMLDivElement
|
||||
const rect = element.getBoundingClientRect()
|
||||
setDraggablePosition({ x: rect.left, y: rect.top })
|
||||
const x = e.clientX - rect.left
|
||||
const y = e.clientY - rect.top
|
||||
setRelativeDraggablePosition({ x, y })
|
||||
setMouseDownPosition({ x: e.screenX, y: e.screenY })
|
||||
setTypebotDragCandidate(typebot)
|
||||
}
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!typebotDragCandidate) return
|
||||
const { clientX, clientY, screenX, screenY } = e
|
||||
if (
|
||||
Math.abs(mouseDownPosition.x - screenX) > dragDistanceTolerance ||
|
||||
Math.abs(mouseDownPosition.y - screenY) > dragDistanceTolerance
|
||||
)
|
||||
setDraggedTypebot(typebotDragCandidate)
|
||||
setDraggablePosition({
|
||||
...draggablePosition,
|
||||
x: clientX - relativeDraggablePosition.x,
|
||||
y: clientY - relativeDraggablePosition.y,
|
||||
})
|
||||
}
|
||||
useEventListener('mousemove', handleMouseMove)
|
||||
|
||||
return (
|
||||
<Flex w="full" justify="center" align="center" pt={4}>
|
||||
<DndContext
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
sensors={sensors}
|
||||
>
|
||||
<Stack w="1000px" spacing={6}>
|
||||
<Skeleton isLoaded={folder?.name !== undefined}>
|
||||
<Heading as="h1">{folder?.name}</Heading>
|
||||
</Skeleton>
|
||||
<Stack>
|
||||
<HStack>
|
||||
{folder && <BackButton id={folder.parentFolderId} />}
|
||||
<Button
|
||||
leftIcon={<FolderPlusIcon />}
|
||||
onClick={handleCreateFolder}
|
||||
isLoading={isCreatingFolder || isFolderLoading}
|
||||
>
|
||||
Create a folder
|
||||
</Button>
|
||||
</HStack>
|
||||
<Wrap spacing={4}>
|
||||
<CreateBotButton
|
||||
folderId={folder?.id}
|
||||
isLoading={isTypebotLoading}
|
||||
/>
|
||||
{isFolderLoading && <ButtonSkeleton />}
|
||||
{folders &&
|
||||
folders.map((folder) => (
|
||||
<FolderButton
|
||||
key={folder.id.toString()}
|
||||
folder={folder}
|
||||
onFolderDeleted={() => handleFolderDeleted(folder.id)}
|
||||
onFolderRenamed={(newName: string) =>
|
||||
handleFolderRenamed(folder.id, newName)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{isTypebotLoading && <ButtonSkeleton />}
|
||||
{typebots &&
|
||||
typebots.map((typebot) => (
|
||||
<TypebotButton
|
||||
key={typebot.id.toString()}
|
||||
typebot={typebot}
|
||||
onTypebotDeleted={() => handleTypebotDeleted(typebot.id)}
|
||||
/>
|
||||
))}
|
||||
<DragOverlay dropAnimation={null}>
|
||||
{draggedTypebot && (
|
||||
<TypebotCardOverlay typebot={draggedTypebot} />
|
||||
)}
|
||||
</DragOverlay>
|
||||
</Wrap>
|
||||
</Stack>
|
||||
<Flex w="full" flex="1" justify="center" pt={4}>
|
||||
<Stack w="1000px" spacing={6}>
|
||||
<Skeleton isLoaded={folder?.name !== undefined}>
|
||||
<Heading as="h1">{folder?.name}</Heading>
|
||||
</Skeleton>
|
||||
<Stack>
|
||||
<HStack>
|
||||
{folder && <BackButton id={folder.parentFolderId} />}
|
||||
<Button
|
||||
leftIcon={<FolderPlusIcon />}
|
||||
onClick={handleCreateFolder}
|
||||
isLoading={isCreatingFolder || isFolderLoading}
|
||||
>
|
||||
Create a folder
|
||||
</Button>
|
||||
</HStack>
|
||||
<Wrap spacing={4}>
|
||||
<CreateBotButton
|
||||
folderId={folder?.id}
|
||||
isLoading={isTypebotLoading}
|
||||
/>
|
||||
{isFolderLoading && <ButtonSkeleton />}
|
||||
{folders &&
|
||||
folders.map((folder) => (
|
||||
<FolderButton
|
||||
key={folder.id.toString()}
|
||||
folder={folder}
|
||||
onFolderDeleted={() => handleFolderDeleted(folder.id)}
|
||||
onFolderRenamed={(newName: string) =>
|
||||
handleFolderRenamed(folder.id, newName)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{isTypebotLoading && <ButtonSkeleton />}
|
||||
{typebots &&
|
||||
typebots.map((typebot) => (
|
||||
<TypebotButton
|
||||
key={typebot.id.toString()}
|
||||
typebot={typebot}
|
||||
onTypebotDeleted={() => handleTypebotDeleted(typebot.id)}
|
||||
onMouseDown={handleMouseDown(typebot)}
|
||||
/>
|
||||
))}
|
||||
</Wrap>
|
||||
</Stack>
|
||||
</DndContext>
|
||||
</Stack>
|
||||
{draggedTypebot && (
|
||||
<Portal>
|
||||
<TypebotCardOverlay
|
||||
typebot={draggedTypebot}
|
||||
onMouseUp={handleMouseUp}
|
||||
pos="fixed"
|
||||
top="0"
|
||||
left="0"
|
||||
style={{
|
||||
transform: `translate(${draggablePosition.x}px, ${draggablePosition.y}px) rotate(-2deg)`,
|
||||
}}
|
||||
/>
|
||||
</Portal>
|
||||
)}
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
@ -1,22 +1,30 @@
|
||||
import { Button } from '@chakra-ui/react'
|
||||
import { useDroppable } from '@dnd-kit/core'
|
||||
import { ChevronLeftIcon } from 'assets/icons'
|
||||
import { NextChakraLink } from 'components/nextChakra/NextChakraLink'
|
||||
import React from 'react'
|
||||
import { useTypebotDnd } from 'contexts/TypebotDndContext'
|
||||
import React, { useMemo } from 'react'
|
||||
|
||||
export const BackButton = ({ id }: { id: string | null }) => {
|
||||
const { setNodeRef, isOver } = useDroppable({
|
||||
id: id?.toString() ?? 'root',
|
||||
})
|
||||
const { draggedTypebot, setMouseOverFolderId, mouseOverFolderId } =
|
||||
useTypebotDnd()
|
||||
|
||||
const isTypebotOver = useMemo(
|
||||
() => draggedTypebot && mouseOverFolderId === id,
|
||||
[draggedTypebot, id, mouseOverFolderId]
|
||||
)
|
||||
|
||||
const handleMouseEnter = () => setMouseOverFolderId(id)
|
||||
const handleMouseLeave = () => setMouseOverFolderId(undefined)
|
||||
return (
|
||||
<Button
|
||||
as={NextChakraLink}
|
||||
href={id ? `/typebots/folders/${id}` : '/typebots'}
|
||||
leftIcon={<ChevronLeftIcon />}
|
||||
variant={'outline'}
|
||||
colorScheme={isOver ? 'blue' : 'gray'}
|
||||
borderWidth={isOver ? '3px' : '1px'}
|
||||
ref={setNodeRef}
|
||||
colorScheme={isTypebotOver ? 'blue' : 'gray'}
|
||||
borderWidth={isTypebotOver ? '3px' : '1px'}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
|
@ -17,11 +17,11 @@ import {
|
||||
SkeletonCircle,
|
||||
WrapItem,
|
||||
} from '@chakra-ui/react'
|
||||
import { useDroppable } from '@dnd-kit/core'
|
||||
import { FolderIcon, MoreVerticalIcon } from 'assets/icons'
|
||||
import { ConfirmModal } from 'components/modals/ConfirmModal'
|
||||
import { useTypebotDnd } from 'contexts/TypebotDndContext'
|
||||
import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
import { deleteFolder, updateFolder } from 'services/folders'
|
||||
|
||||
export const FolderButton = ({
|
||||
@ -34,9 +34,12 @@ export const FolderButton = ({
|
||||
onFolderRenamed: (newName: string) => void
|
||||
}) => {
|
||||
const router = useRouter()
|
||||
const { setNodeRef, isOver } = useDroppable({
|
||||
id: folder.id.toString(),
|
||||
})
|
||||
const { draggedTypebot, setMouseOverFolderId, mouseOverFolderId } =
|
||||
useTypebotDnd()
|
||||
const isTypebotOver = useMemo(
|
||||
() => draggedTypebot && mouseOverFolderId === folder.id,
|
||||
[draggedTypebot, folder.id, mouseOverFolderId]
|
||||
)
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const toast = useToast({
|
||||
position: 'top-right',
|
||||
@ -65,31 +68,33 @@ export const FolderButton = ({
|
||||
router.push(`/typebots/folders/${folder.id}`)
|
||||
}
|
||||
|
||||
const handleMouseEnter = () => setMouseOverFolderId(folder.id)
|
||||
const handleMouseLeave = () => setMouseOverFolderId(undefined)
|
||||
return (
|
||||
<Button
|
||||
as={WrapItem}
|
||||
ref={setNodeRef}
|
||||
style={{ width: '225px', height: '270px' }}
|
||||
paddingX={6}
|
||||
whiteSpace={'normal'}
|
||||
pos="relative"
|
||||
cursor="pointer"
|
||||
variant="outline"
|
||||
colorScheme={isOver ? 'blue' : 'gray'}
|
||||
borderWidth={isOver ? '3px' : '1px'}
|
||||
colorScheme={isTypebotOver ? 'blue' : 'gray'}
|
||||
borderWidth={isTypebotOver ? '3px' : '1px'}
|
||||
justifyContent="center"
|
||||
onClick={handleClick}
|
||||
data-testid="folder-button"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<Menu>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
icon={<MoreVerticalIcon />}
|
||||
aria-label="Show folder menu"
|
||||
aria-label={`Show ${folder.name} menu`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
colorScheme="blue"
|
||||
variant="ghost"
|
||||
size="lg"
|
||||
colorScheme="gray"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
pos="absolute"
|
||||
top="5"
|
||||
right="5"
|
||||
|
@ -16,10 +16,11 @@ export const MoreButton = ({ children, ...props }: Props) => {
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
icon={<MoreVerticalIcon />}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
colorScheme="blue"
|
||||
variant="ghost"
|
||||
size="lg"
|
||||
onMouseUp={(e) => e.stopPropagation()}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
colorScheme="gray"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
{...props}
|
||||
/>
|
||||
<MenuList>{children}</MenuList>
|
@ -2,6 +2,7 @@ import React from 'react'
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
IconButton,
|
||||
MenuItem,
|
||||
Text,
|
||||
useDisclosure,
|
||||
@ -9,28 +10,28 @@ import {
|
||||
VStack,
|
||||
WrapItem,
|
||||
} from '@chakra-ui/react'
|
||||
import { useDraggable } from '@dnd-kit/core'
|
||||
import { useRouter } from 'next/router'
|
||||
import { isMobile } from 'services/utils'
|
||||
import { MoreButton } from 'components/MoreButton'
|
||||
import { MoreButton } from 'components/dashboard/FolderContent/MoreButton'
|
||||
import { ConfirmModal } from 'components/modals/ConfirmModal'
|
||||
import { GlobeIcon, ToolIcon } from 'assets/icons'
|
||||
import { GlobeIcon, GripIcon, ToolIcon } from 'assets/icons'
|
||||
import { deleteTypebot, duplicateTypebot } from 'services/typebots'
|
||||
import { Typebot } from 'models'
|
||||
import { useTypebotDnd } from 'contexts/TypebotDndContext'
|
||||
|
||||
type ChatbotCardProps = {
|
||||
typebot: Typebot
|
||||
onTypebotDeleted: () => void
|
||||
onMouseDown: (e: React.MouseEvent<HTMLButtonElement>) => void
|
||||
}
|
||||
|
||||
export const TypebotButton = ({
|
||||
typebot,
|
||||
onTypebotDeleted,
|
||||
onMouseDown,
|
||||
}: ChatbotCardProps) => {
|
||||
const router = useRouter()
|
||||
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
|
||||
id: typebot.id.toString(),
|
||||
})
|
||||
const { draggedTypebot } = useTypebotDnd()
|
||||
const {
|
||||
isOpen: isDeleteOpen,
|
||||
onOpen: onDeleteOpen,
|
||||
@ -43,6 +44,7 @@ export const TypebotButton = ({
|
||||
})
|
||||
|
||||
const handleTypebotClick = () => {
|
||||
if (draggedTypebot) return
|
||||
router.push(
|
||||
isMobile
|
||||
? `/typebots/${typebot.id}/results/responses`
|
||||
@ -73,7 +75,7 @@ export const TypebotButton = ({
|
||||
return (
|
||||
<Button
|
||||
as={WrapItem}
|
||||
onClick={handleTypebotClick}
|
||||
onMouseUp={handleTypebotClick}
|
||||
display="flex"
|
||||
flexDir="column"
|
||||
variant="outline"
|
||||
@ -84,17 +86,26 @@ export const TypebotButton = ({
|
||||
mb={6}
|
||||
rounded="lg"
|
||||
whiteSpace="normal"
|
||||
data-testid={`typebot-button-${typebot.id}`}
|
||||
opacity={isDragging ? 0.2 : 1}
|
||||
ref={setNodeRef}
|
||||
{...listeners}
|
||||
{...attributes}
|
||||
opacity={draggedTypebot?.id === typebot.id ? 0.2 : 1}
|
||||
onMouseDown={onMouseDown}
|
||||
cursor="pointer"
|
||||
>
|
||||
<IconButton
|
||||
icon={<GripIcon />}
|
||||
pos="absolute"
|
||||
top="20px"
|
||||
left="20px"
|
||||
aria-label="Drag"
|
||||
cursor="grab"
|
||||
variant="ghost"
|
||||
colorScheme="blue"
|
||||
size="sm"
|
||||
/>
|
||||
<MoreButton
|
||||
pos="absolute"
|
||||
top="10px"
|
||||
right="10px"
|
||||
aria-label="Show typebot menu"
|
||||
top="20px"
|
||||
right="20px"
|
||||
aria-label={`Show ${typebot.name} menu`}
|
||||
>
|
||||
<MenuItem onClick={handleDuplicateClick}>Duplicate</MenuItem>
|
||||
<MenuItem
|
||||
|
@ -1,43 +1,47 @@
|
||||
import { Button, Flex, Text, VStack } from '@chakra-ui/react'
|
||||
import { Box, BoxProps, Flex, Text, VStack } from '@chakra-ui/react'
|
||||
import { GlobeIcon, ToolIcon } from 'assets/icons'
|
||||
import { Typebot } from 'models'
|
||||
|
||||
type Props = {
|
||||
typebot: Typebot
|
||||
}
|
||||
} & BoxProps
|
||||
|
||||
export const TypebotCardOverlay = ({ typebot }: Props) => {
|
||||
export const TypebotCardOverlay = ({ typebot, ...props }: Props) => {
|
||||
return (
|
||||
<div
|
||||
className="sm:mr-6 mb-6 focus:outline-none rounded-lg "
|
||||
style={{ width: '225px', height: '270px' }}
|
||||
<Box
|
||||
display="flex"
|
||||
flexDir="column"
|
||||
variant="outline"
|
||||
justifyContent="center"
|
||||
w="225px"
|
||||
h="270px"
|
||||
whiteSpace="normal"
|
||||
transition="none"
|
||||
pointerEvents="none"
|
||||
borderWidth={1}
|
||||
rounded="md"
|
||||
bgColor="white"
|
||||
shadow="lg"
|
||||
opacity={0.7}
|
||||
{...props}
|
||||
>
|
||||
<Button
|
||||
display="flex"
|
||||
flexDir="column"
|
||||
variant="outline"
|
||||
w="full"
|
||||
h="full"
|
||||
whiteSpace="normal"
|
||||
>
|
||||
<VStack spacing={4}>
|
||||
<Flex
|
||||
boxSize="45px"
|
||||
rounded="full"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
bgColor={typebot.publishedTypebotId ? 'blue.500' : 'gray.400'}
|
||||
color="white"
|
||||
>
|
||||
{typebot.publishedTypebotId ? (
|
||||
<GlobeIcon fill="white" fontSize="20px" />
|
||||
) : (
|
||||
<ToolIcon fill="white" fontSize="20px" />
|
||||
)}
|
||||
</Flex>
|
||||
<Text>{typebot.name}</Text>
|
||||
</VStack>
|
||||
</Button>
|
||||
</div>
|
||||
<VStack spacing={4}>
|
||||
<Flex
|
||||
boxSize="45px"
|
||||
rounded="full"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
bgColor={typebot.publishedTypebotId ? 'blue.500' : 'gray.400'}
|
||||
color="white"
|
||||
>
|
||||
{typebot.publishedTypebotId ? (
|
||||
<GlobeIcon fill="white" fontSize="20px" />
|
||||
) : (
|
||||
<ToolIcon fill="white" fontSize="20px" />
|
||||
)}
|
||||
</Flex>
|
||||
<Text>{typebot.name}</Text>
|
||||
</VStack>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
@ -92,7 +92,6 @@ export const MetadataForm = ({
|
||||
<InputWithVariableButton
|
||||
id="title"
|
||||
initialValue={metadata.title ?? typebotName}
|
||||
delay={100}
|
||||
onChange={handleTitleChange}
|
||||
/>
|
||||
</Stack>
|
||||
@ -103,7 +102,6 @@ export const MetadataForm = ({
|
||||
<TextareaWithVariableButton
|
||||
id="description"
|
||||
initialValue={metadata.description}
|
||||
delay={100}
|
||||
onChange={handleDescriptionChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -6,27 +6,24 @@ import {
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
|
||||
type Props = Omit<InputProps, 'onChange' | 'value'> & {
|
||||
delay: number
|
||||
initialValue: string
|
||||
onChange: (debouncedValue: string) => void
|
||||
}
|
||||
|
||||
export const DebouncedInput = forwardRef(
|
||||
(
|
||||
{ delay, onChange, initialValue, ...props }: Props,
|
||||
{ onChange, initialValue, ...props }: Props,
|
||||
ref: ForwardedRef<HTMLInputElement>
|
||||
) => {
|
||||
const [currentValue, setCurrentValue] = useState(initialValue)
|
||||
const [currentValueDebounced] = useDebounce(currentValue, delay)
|
||||
|
||||
useEffect(() => {
|
||||
if (currentValueDebounced === initialValue) return
|
||||
onChange(currentValueDebounced)
|
||||
if (currentValue === initialValue) return
|
||||
onChange(currentValue)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentValueDebounced])
|
||||
}, [currentValue])
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setCurrentValue(e.target.value)
|
||||
|
@ -1,27 +1,23 @@
|
||||
import { Textarea, TextareaProps } from '@chakra-ui/react'
|
||||
import { ChangeEvent, useEffect, useState } from 'react'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
|
||||
type Props = Omit<TextareaProps, 'onChange' | 'value'> & {
|
||||
delay: number
|
||||
initialValue: string
|
||||
onChange: (debouncedValue: string) => void
|
||||
}
|
||||
|
||||
export const DebouncedTextarea = ({
|
||||
delay,
|
||||
onChange,
|
||||
initialValue,
|
||||
...props
|
||||
}: Props) => {
|
||||
const [currentValue, setCurrentValue] = useState(initialValue)
|
||||
const [currentValueDebounced] = useDebounce(currentValue, delay)
|
||||
|
||||
useEffect(() => {
|
||||
if (currentValueDebounced === initialValue) return
|
||||
onChange(currentValueDebounced)
|
||||
if (currentValue === initialValue) return
|
||||
onChange(currentValue)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentValueDebounced])
|
||||
}, [currentValue])
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setCurrentValue(e.target.value)
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
MenuButtonProps,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Portal,
|
||||
Stack,
|
||||
} from '@chakra-ui/react'
|
||||
import { ChevronLeftIcon } from 'assets/icons'
|
||||
@ -41,22 +42,24 @@ export const DropdownList = <T,>({
|
||||
>
|
||||
{currentItem ?? placeholder}
|
||||
</MenuButton>
|
||||
<MenuList maxW="500px" shadow="lg">
|
||||
<Stack maxH={'35vh'} overflowY="scroll" spacing="0">
|
||||
{items.map((item) => (
|
||||
<MenuItem
|
||||
key={item as unknown as string}
|
||||
maxW="500px"
|
||||
overflow="hidden"
|
||||
whiteSpace="nowrap"
|
||||
textOverflow="ellipsis"
|
||||
onClick={handleMenuItemClick(item)}
|
||||
>
|
||||
{item}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Stack>
|
||||
</MenuList>
|
||||
<Portal>
|
||||
<MenuList maxW="500px" shadow="lg" zIndex={1500}>
|
||||
<Stack maxH={'35vh'} overflowY="scroll" spacing="0">
|
||||
{items.map((item) => (
|
||||
<MenuItem
|
||||
key={item as unknown as string}
|
||||
maxW="500px"
|
||||
overflow="hidden"
|
||||
whiteSpace="nowrap"
|
||||
textOverflow="ellipsis"
|
||||
onClick={handleMenuItemClick(item)}
|
||||
>
|
||||
{item}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Stack>
|
||||
</MenuList>
|
||||
</Portal>
|
||||
</Menu>
|
||||
</>
|
||||
)
|
||||
|
@ -17,12 +17,14 @@ type Props = {
|
||||
selectedItem?: string
|
||||
items: string[]
|
||||
onValueChange?: (value: string) => void
|
||||
isLoading?: boolean
|
||||
} & InputProps
|
||||
|
||||
export const SearchableDropdown = ({
|
||||
selectedItem,
|
||||
items,
|
||||
onValueChange,
|
||||
isLoading = false,
|
||||
...inputProps
|
||||
}: Props) => {
|
||||
const { onOpen, onClose, isOpen } = useDisclosure()
|
||||
@ -47,6 +49,7 @@ export const SearchableDropdown = ({
|
||||
)
|
||||
.slice(0, 50),
|
||||
])
|
||||
if (inputRef.current === document.activeElement) onOpen()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [items])
|
||||
|
||||
@ -97,6 +100,7 @@ export const SearchableDropdown = ({
|
||||
value={inputValue}
|
||||
onChange={onInputChange}
|
||||
onClick={onOpen}
|
||||
type="text"
|
||||
{...inputProps}
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
@ -130,7 +134,9 @@ export const SearchableDropdown = ({
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<Text p={4}>Not found.</Text>
|
||||
<Text p={4} color="gray.500">
|
||||
{isLoading ? 'Loading...' : 'Not found.'}
|
||||
</Text>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
@ -13,7 +13,7 @@ export type TableListItemProps<T> = {
|
||||
}
|
||||
|
||||
type Props<T> = {
|
||||
initialItems?: Table<T>
|
||||
initialItems: Table<T>
|
||||
onItemsChange: (items: Table<T>) => void
|
||||
addLabel?: string
|
||||
Item: (props: TableListItemProps<T>) => JSX.Element
|
||||
@ -27,7 +27,7 @@ export const TableList = <T,>({
|
||||
Item,
|
||||
ComponentBetweenItems = () => <></>,
|
||||
}: Props<T>) => {
|
||||
const [items, setItems] = useImmer(initialItems ?? { byId: {}, allIds: [] })
|
||||
const [items, setItems] = useImmer(initialItems)
|
||||
const [showDeleteId, setShowDeleteId] = useState<string | undefined>()
|
||||
|
||||
useEffect(() => {
|
||||
@ -80,6 +80,7 @@ export const TableList = <T,>({
|
||||
pos="relative"
|
||||
onMouseEnter={handleMouseEnter(itemId)}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
mt={idx !== 0 && ComponentBetweenItems ? 4 : 0}
|
||||
>
|
||||
<Item
|
||||
id={itemId}
|
||||
|
@ -13,13 +13,11 @@ import {
|
||||
import { UserIcon } from 'assets/icons'
|
||||
import { Variable } from 'models'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
import { VariableSearchInput } from '../VariableSearchInput'
|
||||
|
||||
export type TextBoxWithVariableButtonProps = {
|
||||
initialValue: string
|
||||
onChange: (value: string) => void
|
||||
delay?: number
|
||||
TextBox:
|
||||
| ComponentWithAs<'textarea', TextareaProps>
|
||||
| ComponentWithAs<'input', InputProps>
|
||||
@ -28,7 +26,6 @@ export type TextBoxWithVariableButtonProps = {
|
||||
export const TextBoxWithVariableButton = ({
|
||||
initialValue,
|
||||
onChange,
|
||||
delay,
|
||||
TextBox,
|
||||
...props
|
||||
}: TextBoxWithVariableButtonProps) => {
|
||||
@ -36,13 +33,12 @@ export const TextBoxWithVariableButton = ({
|
||||
null
|
||||
)
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const [debouncedValue] = useDebounce(value, delay ?? 100)
|
||||
const [carretPosition, setCarretPosition] = useState<number>(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedValue !== initialValue) onChange(debouncedValue)
|
||||
if (value !== initialValue) onChange(value)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [debouncedValue])
|
||||
}, [value])
|
||||
|
||||
const handleVariableSelected = (variable?: Variable) => {
|
||||
if (!textBoxRef.current || !variable) return
|
||||
|
@ -118,6 +118,7 @@ export const VariableSearchInput = ({
|
||||
value={inputValue}
|
||||
onChange={onInputChange}
|
||||
onClick={onOpen}
|
||||
placeholder={inputProps.placeholder ?? 'Select a variable'}
|
||||
{...inputProps}
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
|
@ -42,7 +42,7 @@ export const ColorPicker = ({ initialColor, onColorChange }: Props) => {
|
||||
setColor(e.target.value)
|
||||
|
||||
return (
|
||||
<Popover variant="picker">
|
||||
<Popover variant="picker" placement="right" isLazy>
|
||||
<PopoverTrigger>
|
||||
<Button
|
||||
aria-label={'Pick a color'}
|
||||
|
Reference in New Issue
Block a user