@ -9,6 +9,7 @@
|
|||||||
"lint": "dotenv -e ./.env -e ../../.env -- next lint",
|
"lint": "dotenv -e ./.env -e ../../.env -- next lint",
|
||||||
"test": "dotenv -e ./.env -e ../../.env -- pnpm playwright test",
|
"test": "dotenv -e ./.env -e ../../.env -- pnpm playwright test",
|
||||||
"test:show-report": "pnpm playwright show-report src/test/reporters",
|
"test:show-report": "pnpm playwright show-report src/test/reporters",
|
||||||
|
"test:ui": "dotenv -e ./.env -e ../../.env -- pnpm playwright test --ui",
|
||||||
"format:check": "prettier --check ./src"
|
"format:check": "prettier --check ./src"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -42,12 +43,12 @@
|
|||||||
"@typebot.io/env": "workspace:*",
|
"@typebot.io/env": "workspace:*",
|
||||||
"@typebot.io/js": "workspace:*",
|
"@typebot.io/js": "workspace:*",
|
||||||
"@typebot.io/nextjs": "workspace:*",
|
"@typebot.io/nextjs": "workspace:*",
|
||||||
"@udecode/plate-basic-marks": "21.1.5",
|
"@udecode/cn": "29.0.1",
|
||||||
"@udecode/plate-common": "21.1.5",
|
"@udecode/plate-basic-marks": "30.5.3",
|
||||||
"@udecode/plate-core": "21.1.5",
|
"@udecode/plate-common": "30.4.5",
|
||||||
"@udecode/plate-link": "21.2.0",
|
"@udecode/plate-core": "30.4.5",
|
||||||
"@udecode/plate-ui-link": "21.2.0",
|
"@udecode/plate-floating": "30.5.3",
|
||||||
"@udecode/plate-ui-toolbar": "21.1.5",
|
"@udecode/plate-link": "30.5.3",
|
||||||
"@uiw/codemirror-extensions-langs": "4.21.7",
|
"@uiw/codemirror-extensions-langs": "4.21.7",
|
||||||
"@uiw/codemirror-theme-github": "4.21.7",
|
"@uiw/codemirror-theme-github": "4.21.7",
|
||||||
"@uiw/codemirror-theme-tokyo-night": "4.21.7",
|
"@uiw/codemirror-theme-tokyo-night": "4.21.7",
|
||||||
@ -83,9 +84,6 @@
|
|||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
"slate": "0.94.1",
|
|
||||||
"slate-history": "0.93.0",
|
|
||||||
"slate-react": "0.94.2",
|
|
||||||
"sonner": "1.3.1",
|
"sonner": "1.3.1",
|
||||||
"stripe": "12.13.0",
|
"stripe": "12.13.0",
|
||||||
"svg-round-corners": "0.4.1",
|
"svg-round-corners": "0.4.1",
|
||||||
@ -97,7 +95,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@chakra-ui/styled-system": "2.9.1",
|
"@chakra-ui/styled-system": "2.9.1",
|
||||||
"@playwright/test": "1.36.0",
|
"@playwright/test": "1.41.2",
|
||||||
"@typebot.io/forge": "workspace:*",
|
"@typebot.io/forge": "workspace:*",
|
||||||
"@typebot.io/forge-repository": "workspace:*",
|
"@typebot.io/forge-repository": "workspace:*",
|
||||||
"@typebot.io/forge-schemas": "workspace:*",
|
"@typebot.io/forge-schemas": "workspace:*",
|
||||||
|
@ -651,3 +651,14 @@ export const LightBulbIcon = (props: IconProps) => (
|
|||||||
<path d="M10 22h4" />
|
<path d="M10 22h4" />
|
||||||
</Icon>
|
</Icon>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const UnlinkIcon = (props: IconProps) => (
|
||||||
|
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
|
||||||
|
<path d="m18.84 12.25 1.72-1.71h-.02a5.004 5.004 0 0 0-.12-7.07 5.006 5.006 0 0 0-6.95 0l-1.72 1.71" />
|
||||||
|
<path d="m5.17 11.75-1.71 1.71a5.004 5.004 0 0 0 .12 7.07 5.006 5.006 0 0 0 6.95 0l1.71-1.71" />
|
||||||
|
<line x1="8" x2="8" y1="2" y2="5" />
|
||||||
|
<line x1="2" x2="5" y1="8" y2="8" />
|
||||||
|
<line x1="16" x2="16" y1="19" y2="22" />
|
||||||
|
<line x1="19" x2="22" y1="16" y2="16" />
|
||||||
|
</Icon>
|
||||||
|
)
|
||||||
|
@ -1,175 +1,10 @@
|
|||||||
import {
|
import React, { useState } from 'react'
|
||||||
Flex,
|
import { Plate } from '@udecode/plate-core'
|
||||||
Popover,
|
import { platePlugins } from '@/lib/plate'
|
||||||
PopoverAnchor,
|
import { TElement } from '@udecode/plate-common'
|
||||||
PopoverContent,
|
import { TextEditorEditorContent } from './TextEditorEditorContent'
|
||||||
Portal,
|
|
||||||
Stack,
|
|
||||||
useColorModeValue,
|
|
||||||
} from '@chakra-ui/react'
|
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
||||||
import { Plate, PlateProvider, usePlateEditorRef } from '@udecode/plate-core'
|
|
||||||
import { editorStyle, platePlugins } from '@/lib/plate'
|
|
||||||
import { BaseEditor, BaseSelection, Transforms } from 'slate'
|
|
||||||
import { Variable } from '@typebot.io/schemas'
|
|
||||||
import { ReactEditor } from 'slate-react'
|
|
||||||
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
|
|
||||||
import { colors } from '@/lib/theme'
|
|
||||||
import { useOutsideClick } from '@/hooks/useOutsideClick'
|
|
||||||
import { selectEditor, TElement } from '@udecode/plate-common'
|
|
||||||
import { TextEditorToolBar } from './TextEditorToolBar'
|
|
||||||
import { useTranslate } from '@tolgee/react'
|
|
||||||
|
|
||||||
type TextBubbleEditorContentProps = {
|
type TextBubbleEditorContentProps = {
|
||||||
id: string
|
|
||||||
textEditorValue: TElement[]
|
|
||||||
onClose: (newContent: TElement[]) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const TextBubbleEditorContent = ({
|
|
||||||
id,
|
|
||||||
textEditorValue,
|
|
||||||
onClose,
|
|
||||||
}: TextBubbleEditorContentProps) => {
|
|
||||||
const { t } = useTranslate()
|
|
||||||
const editor = usePlateEditorRef()
|
|
||||||
const varDropdownRef = useRef<HTMLDivElement | null>(null)
|
|
||||||
const rememberedSelection = useRef<BaseSelection | null>(null)
|
|
||||||
const [isVariableDropdownOpen, setIsVariableDropdownOpen] = useState(false)
|
|
||||||
const [isFirstFocus, setIsFirstFocus] = useState(true)
|
|
||||||
|
|
||||||
const textEditorRef = useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
const closeEditor = () => onClose(textEditorValue)
|
|
||||||
|
|
||||||
useOutsideClick({
|
|
||||||
ref: textEditorRef,
|
|
||||||
handler: closeEditor,
|
|
||||||
})
|
|
||||||
|
|
||||||
const computeTargetCoord = useCallback(() => {
|
|
||||||
if (rememberedSelection.current) return { top: 0, left: 0 }
|
|
||||||
const selection = window.getSelection()
|
|
||||||
const relativeParent = textEditorRef.current
|
|
||||||
if (!selection || !relativeParent) return { top: 0, left: 0 }
|
|
||||||
const range = selection.getRangeAt(0)
|
|
||||||
const selectionBoundingRect = range.getBoundingClientRect()
|
|
||||||
const relativeRect = relativeParent.getBoundingClientRect()
|
|
||||||
return {
|
|
||||||
top: selectionBoundingRect.bottom - relativeRect.top,
|
|
||||||
left: selectionBoundingRect.left - relativeRect.left,
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isVariableDropdownOpen) return
|
|
||||||
const el = varDropdownRef.current
|
|
||||||
if (!el) return
|
|
||||||
const { top, left } = computeTargetCoord()
|
|
||||||
if (top === 0 && left === 0) return
|
|
||||||
el.style.top = `${top}px`
|
|
||||||
el.style.left = `${left}px`
|
|
||||||
}, [computeTargetCoord, isVariableDropdownOpen])
|
|
||||||
|
|
||||||
const handleVariableSelected = (variable?: Variable) => {
|
|
||||||
setIsVariableDropdownOpen(false)
|
|
||||||
if (!rememberedSelection.current || !variable) return
|
|
||||||
ReactEditor.focus(editor as unknown as ReactEditor)
|
|
||||||
Transforms.select(
|
|
||||||
editor as unknown as BaseEditor,
|
|
||||||
rememberedSelection.current
|
|
||||||
)
|
|
||||||
Transforms.insertText(
|
|
||||||
editor as unknown as BaseEditor,
|
|
||||||
'{{' + variable.name + '}}'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
||||||
if (e.shiftKey) return
|
|
||||||
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) closeEditor()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack
|
|
||||||
flex="1"
|
|
||||||
ref={textEditorRef}
|
|
||||||
borderWidth="2px"
|
|
||||||
borderColor="blue.400"
|
|
||||||
rounded="md"
|
|
||||||
pos="relative"
|
|
||||||
spacing={0}
|
|
||||||
cursor="text"
|
|
||||||
className="prevent-group-drag"
|
|
||||||
onContextMenuCapture={(e) => e.stopPropagation()}
|
|
||||||
sx={{
|
|
||||||
'.slate-ToolbarButton-active': {
|
|
||||||
color: useColorModeValue('blue.500', 'blue.300') + ' !important',
|
|
||||||
},
|
|
||||||
'[class^="PlateFloatingLink___Styled"]': {
|
|
||||||
'--tw-bg-opacity': useColorModeValue('1', '.1') + '!important',
|
|
||||||
backgroundColor: useColorModeValue('white', 'gray.800'),
|
|
||||||
borderRadius: 'md',
|
|
||||||
transitionProperty: 'background-color',
|
|
||||||
transitionDuration: 'normal',
|
|
||||||
},
|
|
||||||
'[class^="FloatingVerticalDivider___"]': {
|
|
||||||
'--tw-bg-opacity': useColorModeValue('1', '.4') + '!important',
|
|
||||||
},
|
|
||||||
'.slate-a': {
|
|
||||||
color: useColorModeValue('blue.500', 'blue.300'),
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TextEditorToolBar
|
|
||||||
onVariablesButtonClick={() => setIsVariableDropdownOpen(true)}
|
|
||||||
/>
|
|
||||||
<Plate
|
|
||||||
id={id}
|
|
||||||
editableProps={{
|
|
||||||
style: editorStyle(useColorModeValue('white', colors.gray[850])),
|
|
||||||
autoFocus: true,
|
|
||||||
onFocus: () => {
|
|
||||||
rememberedSelection.current = null
|
|
||||||
if (!isFirstFocus) return
|
|
||||||
if (editor.children.length === 0) return
|
|
||||||
selectEditor(editor, {
|
|
||||||
edge: 'end',
|
|
||||||
})
|
|
||||||
setIsFirstFocus(false)
|
|
||||||
},
|
|
||||||
'aria-label': `${t('editor.blocks.bubbles.textEditor.plate.label')}`,
|
|
||||||
onBlur: () => {
|
|
||||||
rememberedSelection.current = editor?.selection
|
|
||||||
},
|
|
||||||
onKeyDown: handleKeyDown,
|
|
||||||
onClick: () => {
|
|
||||||
setIsVariableDropdownOpen(false)
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Popover isOpen={isVariableDropdownOpen} isLazy>
|
|
||||||
<PopoverAnchor>
|
|
||||||
<Flex pos="absolute" ref={varDropdownRef} />
|
|
||||||
</PopoverAnchor>
|
|
||||||
<Portal>
|
|
||||||
<PopoverContent>
|
|
||||||
<VariableSearchInput
|
|
||||||
initialVariableId={undefined}
|
|
||||||
onSelectVariable={handleVariableSelected}
|
|
||||||
placeholder={t(
|
|
||||||
'editor.blocks.bubbles.textEditor.searchVariable.placeholder'
|
|
||||||
)}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</PopoverContent>
|
|
||||||
</Portal>
|
|
||||||
</Popover>
|
|
||||||
</Stack>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type TextBubbleEditorProps = {
|
|
||||||
id: string
|
id: string
|
||||||
initialValue: TElement[]
|
initialValue: TElement[]
|
||||||
onClose: (newContent: TElement[]) => void
|
onClose: (newContent: TElement[]) => void
|
||||||
@ -179,11 +14,14 @@ export const TextBubbleEditor = ({
|
|||||||
id,
|
id,
|
||||||
initialValue,
|
initialValue,
|
||||||
onClose,
|
onClose,
|
||||||
}: TextBubbleEditorProps) => {
|
}: TextBubbleEditorContentProps) => {
|
||||||
const [textEditorValue, setTextEditorValue] = useState(initialValue)
|
const [textEditorValue, setTextEditorValue] =
|
||||||
|
useState<TElement[]>(initialValue)
|
||||||
|
|
||||||
|
const closeEditor = () => onClose(textEditorValue)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PlateProvider
|
<Plate
|
||||||
id={id}
|
id={id}
|
||||||
plugins={platePlugins}
|
plugins={platePlugins}
|
||||||
initialValue={
|
initialValue={
|
||||||
@ -193,11 +31,7 @@ export const TextBubbleEditor = ({
|
|||||||
}
|
}
|
||||||
onChange={setTextEditorValue}
|
onChange={setTextEditorValue}
|
||||||
>
|
>
|
||||||
<TextBubbleEditorContent
|
<TextEditorEditorContent closeEditor={closeEditor} />
|
||||||
id={id}
|
</Plate>
|
||||||
textEditorValue={textEditorValue}
|
|
||||||
onClose={onClose}
|
|
||||||
/>
|
|
||||||
</PlateProvider>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,152 @@
|
|||||||
|
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
|
||||||
|
import { editorStyle } from '@/lib/plate'
|
||||||
|
import { colors } from '@/lib/theme'
|
||||||
|
import {
|
||||||
|
useColorModeValue,
|
||||||
|
Popover,
|
||||||
|
PopoverAnchor,
|
||||||
|
Flex,
|
||||||
|
Portal,
|
||||||
|
PopoverContent,
|
||||||
|
Stack,
|
||||||
|
} from '@chakra-ui/react'
|
||||||
|
import { Variable } from '@typebot.io/schemas'
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
import { TextEditorToolBar } from './TextEditorToolBar'
|
||||||
|
import { useTranslate } from '@tolgee/react'
|
||||||
|
import { PlateContent, useEditorRef } from '@udecode/plate-core'
|
||||||
|
import { focusEditor, insertText, selectEditor } from '@udecode/plate-common'
|
||||||
|
import { useOutsideClick } from '@/hooks/useOutsideClick'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
closeEditor: () => void
|
||||||
|
}
|
||||||
|
export const TextEditorEditorContent = ({ closeEditor }: Props) => {
|
||||||
|
const { t } = useTranslate()
|
||||||
|
const editor = useEditorRef()
|
||||||
|
const [isVariableDropdownOpen, setIsVariableDropdownOpen] = useState(false)
|
||||||
|
const [isFirstFocus, setIsFirstFocus] = useState(true)
|
||||||
|
|
||||||
|
const varDropdownRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
const rememberedSelection = useRef<typeof editor.selection | null>(null)
|
||||||
|
const textEditorRef = useRef<HTMLDivElement>(null)
|
||||||
|
const plateContentRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const handleVariableSelected = (variable?: Variable) => {
|
||||||
|
setIsVariableDropdownOpen(false)
|
||||||
|
if (!variable) return
|
||||||
|
focusEditor(editor)
|
||||||
|
insertText(editor, '{{' + variable.name + '}}')
|
||||||
|
}
|
||||||
|
|
||||||
|
useOutsideClick({
|
||||||
|
ref: textEditorRef,
|
||||||
|
handler: closeEditor,
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (e.shiftKey) return
|
||||||
|
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) closeEditor()
|
||||||
|
}
|
||||||
|
|
||||||
|
const computeTargetCoord = useCallback(() => {
|
||||||
|
if (rememberedSelection.current) return { top: 0, left: 0 }
|
||||||
|
const selection = window.getSelection()
|
||||||
|
const relativeParent = textEditorRef.current
|
||||||
|
if (!selection || !relativeParent) return { top: 0, left: 0 }
|
||||||
|
const range = selection.getRangeAt(0)
|
||||||
|
const selectionBoundingRect = range.getBoundingClientRect()
|
||||||
|
const relativeRect = relativeParent.getBoundingClientRect()
|
||||||
|
return {
|
||||||
|
top: selectionBoundingRect.bottom - relativeRect.top,
|
||||||
|
left: selectionBoundingRect.left - relativeRect.left,
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isVariableDropdownOpen) return
|
||||||
|
const el = varDropdownRef.current
|
||||||
|
if (!el) return
|
||||||
|
const { top, left } = computeTargetCoord()
|
||||||
|
if (top === 0 && left === 0) return
|
||||||
|
el.style.top = `${top}px`
|
||||||
|
el.style.left = `${left}px`
|
||||||
|
}, [computeTargetCoord, isVariableDropdownOpen])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
flex="1"
|
||||||
|
ref={textEditorRef}
|
||||||
|
borderWidth="2px"
|
||||||
|
borderColor="blue.400"
|
||||||
|
rounded="md"
|
||||||
|
pos="relative"
|
||||||
|
spacing={0}
|
||||||
|
cursor="text"
|
||||||
|
className="prevent-group-drag"
|
||||||
|
onContextMenuCapture={(e) => e.stopPropagation()}
|
||||||
|
sx={{
|
||||||
|
'.slate-ToolbarButton-active': {
|
||||||
|
color: useColorModeValue('blue.500', 'blue.300') + ' !important',
|
||||||
|
},
|
||||||
|
'[class^="PlateFloatingLink___Styled"]': {
|
||||||
|
'--tw-bg-opacity': useColorModeValue('1', '.1') + '!important',
|
||||||
|
backgroundColor: useColorModeValue('white', 'gray.800'),
|
||||||
|
borderRadius: 'md',
|
||||||
|
transitionProperty: 'background-color',
|
||||||
|
transitionDuration: 'normal',
|
||||||
|
},
|
||||||
|
'[class^="FloatingVerticalDivider___"]': {
|
||||||
|
'--tw-bg-opacity': useColorModeValue('1', '.4') + '!important',
|
||||||
|
},
|
||||||
|
'.slate-a': {
|
||||||
|
color: useColorModeValue('blue.500', 'blue.300'),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextEditorToolBar
|
||||||
|
onVariablesButtonClick={() => setIsVariableDropdownOpen(true)}
|
||||||
|
/>
|
||||||
|
<PlateContent
|
||||||
|
ref={plateContentRef}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
style={editorStyle(useColorModeValue('white', colors.gray[850]))}
|
||||||
|
autoFocus
|
||||||
|
onClick={() => {
|
||||||
|
setIsVariableDropdownOpen(false)
|
||||||
|
}}
|
||||||
|
onFocus={() => {
|
||||||
|
rememberedSelection.current = null
|
||||||
|
if (!isFirstFocus || !editor) return
|
||||||
|
if (editor.children.length === 0) return
|
||||||
|
selectEditor(editor, {
|
||||||
|
edge: 'end',
|
||||||
|
})
|
||||||
|
setIsFirstFocus(false)
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
if (!editor) return
|
||||||
|
rememberedSelection.current = editor.selection
|
||||||
|
}}
|
||||||
|
aria-label="Text editor"
|
||||||
|
/>
|
||||||
|
<Popover isOpen={isVariableDropdownOpen} isLazy>
|
||||||
|
<PopoverAnchor>
|
||||||
|
<Flex pos="absolute" ref={varDropdownRef} />
|
||||||
|
</PopoverAnchor>
|
||||||
|
<Portal>
|
||||||
|
<PopoverContent>
|
||||||
|
<VariableSearchInput
|
||||||
|
initialVariableId={undefined}
|
||||||
|
onSelectVariable={handleVariableSelected}
|
||||||
|
placeholder={t(
|
||||||
|
'editor.blocks.bubbles.textEditor.searchVariable.placeholder'
|
||||||
|
)}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Portal>
|
||||||
|
</Popover>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
@ -9,16 +9,17 @@ import {
|
|||||||
MARK_ITALIC,
|
MARK_ITALIC,
|
||||||
MARK_UNDERLINE,
|
MARK_UNDERLINE,
|
||||||
} from '@udecode/plate-basic-marks'
|
} from '@udecode/plate-basic-marks'
|
||||||
import { getPluginType, usePlateEditorRef } from '@udecode/plate-core'
|
import { getPluginType, useEditorRef } from '@udecode/plate-core'
|
||||||
import { LinkToolbarButton } from '@udecode/plate-ui-link'
|
|
||||||
import { MarkToolbarButton } from '@udecode/plate-ui-toolbar'
|
|
||||||
import {
|
import {
|
||||||
BoldIcon,
|
BoldIcon,
|
||||||
ItalicIcon,
|
ItalicIcon,
|
||||||
UnderlineIcon,
|
UnderlineIcon,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
LinkIcon,
|
LinkIcon,
|
||||||
UserIcon,
|
UserIcon,
|
||||||
} from '@/components/icons'
|
} from '@/components/icons'
|
||||||
|
import { MarkToolbarButton } from './plate/MarkToolbarButton'
|
||||||
|
import { LinkToolbarButton } from './plate/LinkToolbarButton'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onVariablesButtonClick: () => void
|
onVariablesButtonClick: () => void
|
||||||
@ -28,7 +29,8 @@ export const TextEditorToolBar = ({
|
|||||||
onVariablesButtonClick,
|
onVariablesButtonClick,
|
||||||
...props
|
...props
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const editor = usePlateEditorRef()
|
const editor = useEditorRef()
|
||||||
|
|
||||||
const handleVariablesButtonMouseDown = (e: React.MouseEvent) => {
|
const handleVariablesButtonMouseDown = (e: React.MouseEvent) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@ -52,24 +54,27 @@ export const TextEditorToolBar = ({
|
|||||||
/>
|
/>
|
||||||
<span data-testid="bold-button">
|
<span data-testid="bold-button">
|
||||||
<MarkToolbarButton
|
<MarkToolbarButton
|
||||||
type={getPluginType(editor, MARK_BOLD)}
|
nodeType={getPluginType(editor, MARK_BOLD)}
|
||||||
icon={<BoldIcon />}
|
icon={<BoldIcon />}
|
||||||
|
aria-label="Toggle bold"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span data-testid="italic-button">
|
<span data-testid="italic-button">
|
||||||
<MarkToolbarButton
|
<MarkToolbarButton
|
||||||
type={getPluginType(editor, MARK_ITALIC)}
|
nodeType={getPluginType(editor, MARK_ITALIC)}
|
||||||
icon={<ItalicIcon />}
|
icon={<ItalicIcon />}
|
||||||
|
aria-label="Toggle italic"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span data-testid="underline-button">
|
<span data-testid="underline-button">
|
||||||
<MarkToolbarButton
|
<MarkToolbarButton
|
||||||
type={getPluginType(editor, MARK_UNDERLINE)}
|
nodeType={getPluginType(editor, MARK_UNDERLINE)}
|
||||||
icon={<UnderlineIcon />}
|
icon={<UnderlineIcon />}
|
||||||
|
aria-label="Toggle underline"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span data-testid="link-button">
|
<span data-testid="link-button">
|
||||||
<LinkToolbarButton icon={<LinkIcon />} />
|
<LinkToolbarButton icon={<LinkIcon />} aria-label="Add link" />
|
||||||
</span>
|
</span>
|
||||||
</HStack>
|
</HStack>
|
||||||
)
|
)
|
||||||
|
@ -0,0 +1,149 @@
|
|||||||
|
import React, { useRef } from 'react'
|
||||||
|
import {
|
||||||
|
flip,
|
||||||
|
offset,
|
||||||
|
UseVirtualFloatingOptions,
|
||||||
|
} from '@udecode/plate-floating'
|
||||||
|
import {
|
||||||
|
LinkFloatingToolbarState,
|
||||||
|
useFloatingLinkEdit,
|
||||||
|
useFloatingLinkEditState,
|
||||||
|
useFloatingLinkInsert,
|
||||||
|
useFloatingLinkInsertState,
|
||||||
|
useFloatingLinkUrlInput,
|
||||||
|
} from '@udecode/plate-link'
|
||||||
|
import { LinkIcon, UnlinkIcon } from '@/components/icons'
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
HStack,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
InputGroup,
|
||||||
|
InputLeftElement,
|
||||||
|
Stack,
|
||||||
|
} from '@chakra-ui/react'
|
||||||
|
import { TextInputIcon } from '@/features/blocks/inputs/textInput/components/TextInputIcon'
|
||||||
|
|
||||||
|
const floatingOptions: UseVirtualFloatingOptions = {
|
||||||
|
placement: 'bottom-start',
|
||||||
|
middleware: [
|
||||||
|
offset(12),
|
||||||
|
flip({
|
||||||
|
padding: 12,
|
||||||
|
fallbackPlacements: ['bottom-end', 'top-start', 'top-end'],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LinkFloatingToolbarProps {
|
||||||
|
state?: LinkFloatingToolbarState
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LinkFloatingToolbar({ state }: LinkFloatingToolbarProps) {
|
||||||
|
const urlInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const insertState = useFloatingLinkInsertState({
|
||||||
|
...state,
|
||||||
|
floatingOptions: {
|
||||||
|
...floatingOptions,
|
||||||
|
...state?.floatingOptions,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const {
|
||||||
|
props: insertProps,
|
||||||
|
ref: insertRef,
|
||||||
|
hidden,
|
||||||
|
textInputProps,
|
||||||
|
} = useFloatingLinkInsert(insertState)
|
||||||
|
|
||||||
|
const { props } = useFloatingLinkUrlInput({
|
||||||
|
ref: urlInputRef,
|
||||||
|
})
|
||||||
|
|
||||||
|
const editState = useFloatingLinkEditState({
|
||||||
|
...state,
|
||||||
|
floatingOptions: {
|
||||||
|
...floatingOptions,
|
||||||
|
...state?.floatingOptions,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const {
|
||||||
|
props: editProps,
|
||||||
|
ref: editRef,
|
||||||
|
editButtonProps,
|
||||||
|
unlinkButtonProps,
|
||||||
|
} = useFloatingLinkEdit(editState)
|
||||||
|
|
||||||
|
if (hidden) return null
|
||||||
|
|
||||||
|
const input = (
|
||||||
|
<Stack
|
||||||
|
w="330px"
|
||||||
|
bgColor="white"
|
||||||
|
px="4"
|
||||||
|
py="2"
|
||||||
|
rounded="md"
|
||||||
|
borderWidth={1}
|
||||||
|
shadow="md"
|
||||||
|
>
|
||||||
|
<InputGroup>
|
||||||
|
<InputLeftElement pointerEvents="none">
|
||||||
|
<LinkIcon color="gray.300" />
|
||||||
|
</InputLeftElement>
|
||||||
|
<Input
|
||||||
|
ref={urlInputRef}
|
||||||
|
placeholder="Paste link"
|
||||||
|
defaultValue={props.defaultValue}
|
||||||
|
onChange={props.onChange}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<InputGroup>
|
||||||
|
<InputLeftElement pointerEvents="none">
|
||||||
|
<TextInputIcon color="gray.300" />
|
||||||
|
</InputLeftElement>
|
||||||
|
<Input placeholder="Text to display" {...textInputProps} />
|
||||||
|
</InputGroup>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
|
||||||
|
const editContent = editState.isEditing ? (
|
||||||
|
input
|
||||||
|
) : (
|
||||||
|
<HStack
|
||||||
|
bgColor="white"
|
||||||
|
p="2"
|
||||||
|
rounded="md"
|
||||||
|
borderWidth={1}
|
||||||
|
shadow="md"
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
|
<Button {...editButtonProps} size="sm">
|
||||||
|
Edit link
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Divider orientation="vertical" h="20px" />
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
icon={<UnlinkIcon />}
|
||||||
|
aria-label="Unlink"
|
||||||
|
size="sm"
|
||||||
|
{...unlinkButtonProps}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div ref={insertRef} {...insertProps}>
|
||||||
|
{input}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ref={editRef} {...editProps}>
|
||||||
|
{editContent}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { IconButton, IconButtonProps } from '@chakra-ui/react'
|
||||||
|
import {
|
||||||
|
useLinkToolbarButton,
|
||||||
|
useLinkToolbarButtonState,
|
||||||
|
} from '@udecode/plate-link'
|
||||||
|
|
||||||
|
type Props = IconButtonProps
|
||||||
|
|
||||||
|
export const LinkToolbarButton = ({ ...rest }: Props) => {
|
||||||
|
const state = useLinkToolbarButtonState()
|
||||||
|
const { props } = useLinkToolbarButton(state)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
variant={props.pressed ? 'outline' : 'ghost'}
|
||||||
|
colorScheme={props.pressed ? 'blue' : undefined}
|
||||||
|
{...props}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
useMarkToolbarButton,
|
||||||
|
useMarkToolbarButtonState,
|
||||||
|
} from '@udecode/plate-common'
|
||||||
|
import { IconButton, IconButtonProps } from '@chakra-ui/react'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
nodeType: string
|
||||||
|
clear?: string | string[]
|
||||||
|
} & IconButtonProps
|
||||||
|
|
||||||
|
export const MarkToolbarButton = ({ clear, nodeType, ...rest }: Props) => {
|
||||||
|
const state = useMarkToolbarButtonState({ clear, nodeType })
|
||||||
|
const { props } = useMarkToolbarButton(state)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
variant={props.pressed ? 'outline' : 'ghost'}
|
||||||
|
colorScheme={props.pressed ? 'blue' : undefined}
|
||||||
|
{...props}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
import { LinkFloatingToolbar } from '@/features/blocks/bubbles/textBubble/components/plate/LinkFloatingInput'
|
||||||
import {
|
import {
|
||||||
createBoldPlugin,
|
createBoldPlugin,
|
||||||
createItalicPlugin,
|
createItalicPlugin,
|
||||||
@ -5,13 +6,13 @@ import {
|
|||||||
} from '@udecode/plate-basic-marks'
|
} from '@udecode/plate-basic-marks'
|
||||||
import { createPlugins } from '@udecode/plate-core'
|
import { createPlugins } from '@udecode/plate-core'
|
||||||
import { createLinkPlugin, ELEMENT_LINK } from '@udecode/plate-link'
|
import { createLinkPlugin, ELEMENT_LINK } from '@udecode/plate-link'
|
||||||
import { PlateFloatingLink } from '@udecode/plate-ui-link'
|
|
||||||
|
|
||||||
export const editorStyle = (backgroundColor: string): React.CSSProperties => ({
|
export const editorStyle = (backgroundColor: string): React.CSSProperties => ({
|
||||||
flex: 1,
|
flex: 1,
|
||||||
padding: '1rem',
|
padding: '1rem',
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
borderRadius: '0.25rem',
|
borderRadius: '0.25rem',
|
||||||
|
outline: 'none',
|
||||||
})
|
})
|
||||||
|
|
||||||
export const platePlugins = createPlugins(
|
export const platePlugins = createPlugins(
|
||||||
@ -20,7 +21,8 @@ export const platePlugins = createPlugins(
|
|||||||
createItalicPlugin(),
|
createItalicPlugin(),
|
||||||
createUnderlinePlugin(),
|
createUnderlinePlugin(),
|
||||||
createLinkPlugin({
|
createLinkPlugin({
|
||||||
renderAfterEditable: PlateFloatingLink,
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
renderAfterEditable: LinkFloatingToolbar as any,
|
||||||
options: {
|
options: {
|
||||||
isUrl: (url: string) =>
|
isUrl: (url: string) =>
|
||||||
url.startsWith('http:') ||
|
url.startsWith('http:') ||
|
||||||
|
@ -19460,6 +19460,37 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"progressBar": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"isEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"backgroundColor": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"placement": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"Top",
|
||||||
|
"Bottom"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"thickness": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"position": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"fixed",
|
||||||
|
"absolute"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1380,6 +1380,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"description": "If the typebot contains dynamic avatars, dynamicTheme returns the new avatar URLs whenever their variables are updated."
|
"description": "If the typebot contains dynamic avatars, dynamicTheme returns the new avatar URLs whenever their variables are updated."
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "If progress bar is enabled, this field will return a number between 0 and 100 indicating the current progress based on the longest remaining path of the flow."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@ -1709,6 +1713,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"description": "If the typebot contains dynamic avatars, dynamicTheme returns the new avatar URLs whenever their variables are updated."
|
"description": "If the typebot contains dynamic avatars, dynamicTheme returns the new avatar URLs whenever their variables are updated."
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "If progress bar is enabled, this field will return a number between 0 and 100 indicating the current progress based on the longest remaining path of the flow."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@ -2127,6 +2135,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"description": "If the typebot contains dynamic avatars, dynamicTheme returns the new avatar URLs whenever their variables are updated."
|
"description": "If the typebot contains dynamic avatars, dynamicTheme returns the new avatar URLs whenever their variables are updated."
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "If progress bar is enabled, this field will return a number between 0 and 100 indicating the current progress based on the longest remaining path of the flow."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@ -6759,6 +6771,37 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"progressBar": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"isEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"backgroundColor": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"placement": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"Top",
|
||||||
|
"Bottom"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"thickness": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"position": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"fixed",
|
||||||
|
"absolute"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
"@typebot.io/schemas": "workspace:*",
|
"@typebot.io/schemas": "workspace:*",
|
||||||
"@typebot.io/tsconfig": "workspace:*",
|
"@typebot.io/tsconfig": "workspace:*",
|
||||||
"@typebot.io/variables": "workspace:*",
|
"@typebot.io/variables": "workspace:*",
|
||||||
"@udecode/plate-common": "21.1.5",
|
"@udecode/plate-common": "30.4.5",
|
||||||
"ai": "2.2.33",
|
"ai": "2.2.33",
|
||||||
"chrono-node": "2.7.5",
|
"chrono-node": "2.7.5",
|
||||||
"date-fns": "2.30.0",
|
"date-fns": "2.30.0",
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@stripe/stripe-js": "1.54.1",
|
"@stripe/stripe-js": "1.54.1",
|
||||||
"@udecode/plate-common": "21.1.5",
|
"@udecode/plate-common": "30.4.5",
|
||||||
"dompurify": "3.0.6",
|
"dompurify": "3.0.6",
|
||||||
"ky": "1.1.3",
|
"ky": "1.1.3",
|
||||||
"marked": "9.0.3",
|
"marked": "9.0.3",
|
||||||
|
@ -27,17 +27,17 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/nextjs": "7.77.0",
|
"@sentry/nextjs": "7.77.0",
|
||||||
"@trpc/server": "10.40.0",
|
"@trpc/server": "10.40.0",
|
||||||
"@udecode/plate-basic-marks": "21.1.5",
|
"@udecode/plate-basic-marks": "30.5.3",
|
||||||
"@udecode/plate-block-quote": "30.1.2",
|
"@udecode/plate-block-quote": "30.5.3",
|
||||||
"@udecode/plate-code-block": "30.1.2",
|
"@udecode/plate-code-block": "30.7.0",
|
||||||
"@udecode/plate-common": "21.1.5",
|
"@udecode/plate-common": "30.4.5",
|
||||||
"@udecode/plate-heading": "30.1.2",
|
"@udecode/plate-heading": "30.5.3",
|
||||||
"@udecode/plate-horizontal-rule": "30.1.2",
|
"@udecode/plate-horizontal-rule": "30.5.3",
|
||||||
"@udecode/plate-link": "21.2.0",
|
"@udecode/plate-link": "30.5.3",
|
||||||
"@udecode/plate-list": "30.1.2",
|
"@udecode/plate-list": "30.5.3",
|
||||||
"@udecode/plate-media": "30.1.2",
|
"@udecode/plate-media": "30.5.3",
|
||||||
"@udecode/plate-paragraph": "30.1.2",
|
"@udecode/plate-paragraph": "30.5.3",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "1.0.3",
|
||||||
"google-auth-library": "8.9.0",
|
"google-auth-library": "8.9.0",
|
||||||
"got": "12.6.0",
|
"got": "12.6.0",
|
||||||
"minio": "7.1.3",
|
"minio": "7.1.3",
|
||||||
|
@ -7,7 +7,7 @@ export const playwrightBaseConfig: PlaywrightTestConfig = {
|
|||||||
expect: {
|
expect: {
|
||||||
timeout: process.env.CI ? 10 * 1000 : 5 * 1000,
|
timeout: process.env.CI ? 10 * 1000 : 5 * 1000,
|
||||||
},
|
},
|
||||||
retries: process.env.NO_RETRIES ? 0 : 1,
|
retries: 0,
|
||||||
workers: process.env.CI ? 2 : 3,
|
workers: process.env.CI ? 2 : 3,
|
||||||
reporter: [
|
reporter: [
|
||||||
[process.env.CI ? 'github' : 'list'],
|
[process.env.CI ? 'github' : 'list'],
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@udecode/plate-common": "21.1.5"
|
"@udecode/plate-common": "30.4.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typebot.io/schemas": "workspace:*",
|
"@typebot.io/schemas": "workspace:*",
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@udecode/plate-common": "21.1.5",
|
"@udecode/plate-common": "30.4.5",
|
||||||
"zod": "3.22.4",
|
"zod": "3.22.4",
|
||||||
"zod-openapi": "^2.11.0"
|
"zod-openapi": "^2.11.0"
|
||||||
},
|
},
|
||||||
|
1517
pnpm-lock.yaml
generated
1517
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user