@ -23,8 +23,8 @@ export const BackButton = ({ id }: { id: string | null }) => {
|
||||
href={id ? `/typebots/folders/${id}` : '/typebots'}
|
||||
leftIcon={<ChevronLeftIcon />}
|
||||
variant={'outline'}
|
||||
colorScheme={isTypebotOver ? 'blue' : 'gray'}
|
||||
borderWidth={isTypebotOver ? '3px' : '1px'}
|
||||
colorScheme={isTypebotOver || draggedTypebot ? 'blue' : 'gray'}
|
||||
borderWidth={isTypebotOver ? '2px' : '1px'}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
|
@ -4,6 +4,7 @@ import { useRouter } from 'next/router'
|
||||
import { stringify } from 'qs'
|
||||
import React from 'react'
|
||||
import { useTranslate } from '@tolgee/react'
|
||||
import { useTypebotDnd } from '../TypebotDndProvider'
|
||||
|
||||
export const CreateBotButton = ({
|
||||
folderId,
|
||||
@ -12,6 +13,7 @@ export const CreateBotButton = ({
|
||||
}: { folderId?: string; isFirstBot: boolean } & ButtonProps) => {
|
||||
const { t } = useTranslate()
|
||||
const router = useRouter()
|
||||
const { draggedTypebot } = useTypebotDnd()
|
||||
|
||||
const handleClick = () =>
|
||||
router.push(
|
||||
@ -28,6 +30,7 @@ export const CreateBotButton = ({
|
||||
paddingX={6}
|
||||
whiteSpace={'normal'}
|
||||
colorScheme="blue"
|
||||
opacity={draggedTypebot ? 0.3 : 1}
|
||||
{...props}
|
||||
>
|
||||
<VStack spacing="6">
|
||||
|
@ -21,22 +21,24 @@ import { FolderIcon, MoreVerticalIcon } from '@/components/icons'
|
||||
import { ConfirmModal } from '@/components/ConfirmModal'
|
||||
import { useTypebotDnd } from '../TypebotDndProvider'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useMemo } from 'react'
|
||||
import React, { memo, useMemo } from 'react'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
import { T, useTranslate } from '@tolgee/react'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
|
||||
export const FolderButton = ({
|
||||
folder,
|
||||
index,
|
||||
onFolderDeleted,
|
||||
onFolderRenamed,
|
||||
}: {
|
||||
type Props = {
|
||||
folder: DashboardFolder
|
||||
index: number
|
||||
onFolderDeleted: () => void
|
||||
onFolderRenamed: () => void
|
||||
}) => {
|
||||
}
|
||||
|
||||
const FolderButton = ({
|
||||
folder,
|
||||
index,
|
||||
onFolderDeleted,
|
||||
onFolderRenamed,
|
||||
}: Props) => {
|
||||
const { t } = useTranslate()
|
||||
const router = useRouter()
|
||||
const { draggedTypebot, setMouseOverFolderId, mouseOverFolderId } =
|
||||
@ -88,8 +90,9 @@ export const FolderButton = ({
|
||||
pos="relative"
|
||||
cursor="pointer"
|
||||
variant="outline"
|
||||
colorScheme={isTypebotOver ? 'blue' : 'gray'}
|
||||
borderWidth={isTypebotOver ? '3px' : '1px'}
|
||||
colorScheme={isTypebotOver || draggedTypebot ? 'blue' : 'gray'}
|
||||
borderWidth={isTypebotOver ? '2px' : '1px'}
|
||||
transition={'border-width 0.1s ease'}
|
||||
justifyContent="center"
|
||||
onClick={handleClick}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
@ -186,3 +189,8 @@ export const ButtonSkeleton = () => (
|
||||
</VStack>
|
||||
</Button>
|
||||
)
|
||||
|
||||
export default memo(
|
||||
FolderButton,
|
||||
(prev, next) => prev.folder.id === next.folder.id && prev.index === next.index
|
||||
)
|
||||
|
@ -10,23 +10,22 @@ import {
|
||||
Wrap,
|
||||
} from '@chakra-ui/react'
|
||||
import { useTypebotDnd } from '../TypebotDndProvider'
|
||||
import React, { useState } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { BackButton } from './BackButton'
|
||||
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
import { CreateBotButton } from './CreateBotButton'
|
||||
import { CreateFolderButton } from './CreateFolderButton'
|
||||
import { ButtonSkeleton, FolderButton } from './FolderButton'
|
||||
import { TypebotButton } from './TypebotButton'
|
||||
import FolderButton, { ButtonSkeleton } from './FolderButton'
|
||||
import TypebotButton from './TypebotButton'
|
||||
import { TypebotCardOverlay } from './TypebotButtonOverlay'
|
||||
import { useTypebots } from '@/features/dashboard/hooks/useTypebots'
|
||||
import { TypebotInDashboard } from '@/features/dashboard/types'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
import { NodePosition } from '@/features/graph/providers/GraphDndProvider'
|
||||
|
||||
type Props = { folder: DashboardFolder | null }
|
||||
|
||||
const dragDistanceTolerance = 20
|
||||
|
||||
export const FolderContent = ({ folder }: Props) => {
|
||||
const { workspace, currentRole } = useWorkspace()
|
||||
const [isCreatingFolder, setIsCreatingFolder] = useState(false)
|
||||
@ -36,14 +35,11 @@ export const FolderContent = ({ folder }: Props) => {
|
||||
mouseOverFolderId,
|
||||
setMouseOverFolderId,
|
||||
} = useTypebotDnd()
|
||||
const [mouseDownPosition, setMouseDownPosition] = useState({ x: 0, y: 0 })
|
||||
const [draggablePosition, setDraggablePosition] = useState({ x: 0, y: 0 })
|
||||
const [relativeDraggablePosition, setRelativeDraggablePosition] = useState({
|
||||
const [mousePositionInElement, setMousePositionInElement] = useState({
|
||||
x: 0,
|
||||
y: 0,
|
||||
})
|
||||
const [typebotDragCandidate, setTypebotDragCandidate] =
|
||||
useState<TypebotInDashboard>()
|
||||
|
||||
const { showToast } = useToast()
|
||||
|
||||
@ -121,40 +117,55 @@ 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: TypebotInDashboard) => (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 handleTypebotDrag =
|
||||
(typebot: TypebotInDashboard) =>
|
||||
({ absolute, relative }: NodePosition) => {
|
||||
if (draggedTypebot) return
|
||||
setMousePositionInElement(relative)
|
||||
setDraggablePosition({
|
||||
x: absolute.x - relative.x,
|
||||
y: absolute.y - relative.y,
|
||||
})
|
||||
setDraggedTypebot(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)
|
||||
if (!draggedTypebot) return
|
||||
const { clientX, clientY } = e
|
||||
setDraggablePosition({
|
||||
...draggablePosition,
|
||||
x: clientX - relativeDraggablePosition.x,
|
||||
y: clientY - relativeDraggablePosition.y,
|
||||
x: clientX - mousePositionInElement.x,
|
||||
y: clientY - mousePositionInElement.y,
|
||||
})
|
||||
}
|
||||
useEventListener('mousemove', handleMouseMove)
|
||||
|
||||
useEffect(() => {
|
||||
if (!draggablePosition || !draggedTypebot) return
|
||||
const { innerHeight } = window
|
||||
const scrollSpeed = 10
|
||||
const scrollMargin = 50
|
||||
const clientY = draggablePosition.y + mousePositionInElement.y
|
||||
const scrollY =
|
||||
clientY < scrollMargin
|
||||
? -scrollSpeed
|
||||
: clientY > innerHeight - scrollMargin
|
||||
? scrollSpeed
|
||||
: 0
|
||||
window.scrollBy(0, scrollY)
|
||||
const interval = setInterval(() => {
|
||||
window.scrollBy(0, scrollY)
|
||||
}, 5)
|
||||
|
||||
return () => {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, [draggablePosition, draggedTypebot, mousePositionInElement])
|
||||
|
||||
return (
|
||||
<Flex w="full" flex="1" justify="center">
|
||||
<Stack w="1000px" spacing={6} pt="4">
|
||||
@ -196,8 +207,9 @@ export const FolderContent = ({ folder }: Props) => {
|
||||
<TypebotButton
|
||||
key={typebot.id.toString()}
|
||||
typebot={typebot}
|
||||
draggedTypebot={draggedTypebot}
|
||||
onTypebotUpdated={refetchTypebots}
|
||||
onMouseDown={handleMouseDown(typebot)}
|
||||
onDrag={handleTypebotDrag(typebot)}
|
||||
/>
|
||||
))}
|
||||
</Wrap>
|
||||
@ -214,6 +226,7 @@ export const FolderContent = ({ folder }: Props) => {
|
||||
style={{
|
||||
transform: `translate(${draggablePosition.x}px, ${draggablePosition.y}px) rotate(-2deg)`,
|
||||
}}
|
||||
transformOrigin="0 0 0"
|
||||
/>
|
||||
</Portal>
|
||||
)}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import React, { memo } from 'react'
|
||||
import {
|
||||
Alert,
|
||||
AlertIcon,
|
||||
@ -16,7 +16,6 @@ import {
|
||||
import { useRouter } from 'next/router'
|
||||
import { ConfirmModal } from '@/components/ConfirmModal'
|
||||
import { GripIcon } from '@/components/icons'
|
||||
import { useTypebotDnd } from '../TypebotDndProvider'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
import { MoreButton } from './MoreButton'
|
||||
@ -26,29 +25,41 @@ import { TypebotInDashboard } from '@/features/dashboard/types'
|
||||
import { isMobile } from '@/helpers/isMobile'
|
||||
import { trpc, trpcVanilla } from '@/lib/trpc'
|
||||
import { duplicateName } from '@/features/typebot/helpers/duplicateName'
|
||||
import {
|
||||
NodePosition,
|
||||
useDragDistance,
|
||||
} from '@/features/graph/providers/GraphDndProvider'
|
||||
|
||||
type Props = {
|
||||
typebot: TypebotInDashboard
|
||||
isReadOnly?: boolean
|
||||
draggedTypebot: TypebotInDashboard | undefined
|
||||
onTypebotUpdated: () => void
|
||||
onMouseDown?: (e: React.MouseEvent<HTMLButtonElement>) => void
|
||||
onDrag: (position: NodePosition) => void
|
||||
}
|
||||
|
||||
export const TypebotButton = ({
|
||||
const TypebotButton = ({
|
||||
typebot,
|
||||
isReadOnly = false,
|
||||
draggedTypebot,
|
||||
onTypebotUpdated,
|
||||
onMouseDown,
|
||||
onDrag,
|
||||
}: Props) => {
|
||||
const { t } = useTranslate()
|
||||
const router = useRouter()
|
||||
const { draggedTypebot } = useTypebotDnd()
|
||||
const [draggedTypebotDebounced] = useDebounce(draggedTypebot, 200)
|
||||
const {
|
||||
isOpen: isDeleteOpen,
|
||||
onOpen: onDeleteOpen,
|
||||
onClose: onDeleteClose,
|
||||
} = useDisclosure()
|
||||
const buttonRef = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
useDragDistance({
|
||||
ref: buttonRef,
|
||||
onDrag,
|
||||
deps: [],
|
||||
})
|
||||
|
||||
const { showToast } = useToast()
|
||||
|
||||
@ -125,6 +136,7 @@ export const TypebotButton = ({
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={buttonRef}
|
||||
as={WrapItem}
|
||||
onClick={handleTypebotClick}
|
||||
display="flex"
|
||||
@ -134,8 +146,7 @@ export const TypebotButton = ({
|
||||
h="270px"
|
||||
rounded="lg"
|
||||
whiteSpace="normal"
|
||||
opacity={draggedTypebot?.id === typebot.id ? 0.2 : 1}
|
||||
onMouseDown={onMouseDown}
|
||||
opacity={draggedTypebot ? 0.3 : 1}
|
||||
cursor="pointer"
|
||||
>
|
||||
{typebot.publishedTypebotId && (
|
||||
@ -223,3 +234,11 @@ export const TypebotButton = ({
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(
|
||||
TypebotButton,
|
||||
(prev, next) =>
|
||||
prev.draggedTypebot?.id === next.draggedTypebot?.id &&
|
||||
prev.typebot.id === next.typebot.id &&
|
||||
prev.isReadOnly === next.isReadOnly
|
||||
)
|
||||
|
@ -110,7 +110,7 @@ export const useDragDistance = ({
|
||||
ref: React.MutableRefObject<HTMLDivElement | null>
|
||||
onDrag: (position: { absolute: Coordinates; relative: Coordinates }) => void
|
||||
distanceTolerance?: number
|
||||
isDisabled: boolean
|
||||
isDisabled?: boolean
|
||||
deps?: unknown[]
|
||||
}) => {
|
||||
const mouseDownPosition = useRef<{
|
||||
|
Reference in New Issue
Block a user