diff --git a/web/app/components/workflow/block-selector/context.tsx b/web/app/components/workflow/block-selector/context.tsx new file mode 100644 index 0000000000..e8261ff9f6 --- /dev/null +++ b/web/app/components/workflow/block-selector/context.tsx @@ -0,0 +1,123 @@ +'use client' + +import { useCallback, useRef, useState } from 'react' +import { createContext, useContext } from 'use-context-selector' +import type { + OffsetOptions, + Placement, +} from '@floating-ui/react' +import { + FloatingPortal, + flip, + offset, + shift, + useDismiss, + useFloating, + useInteractions, +} from '@floating-ui/react' +import type { OnSelect } from './types' +import BlockSelector from './index' + +type UpdateParams = { + from?: string + placement?: Placement + offset?: OffsetOptions + className?: string + callback?: OnSelect +} +export type BlockSelectorContextValue = { + from: string + open: boolean + setOpen: (open: boolean) => void + referenceRef: any + handleToggle: (v: UpdateParams) => void +} + +export const BlockSelectorContext = createContext({ + from: '', + open: false, + setOpen: () => {}, + referenceRef: null, + handleToggle: () => {}, +}) +export const useBlockSelectorContext = () => useContext(BlockSelectorContext) + +type BlockSelectorContextProviderProps = { + children: React.ReactNode +} +export const BlockSelectorContextProvider = ({ + children, +}: BlockSelectorContextProviderProps) => { + const [from, setFrom] = useState('node') + const [open, setOpen] = useState(false) + const [placement, setPlacement] = useState('top') + const [offsetValue, setOffsetValue] = useState(0) + const [className, setClassName] = useState('') + const callbackRef = useRef(undefined) + + const { refs, floatingStyles, context } = useFloating({ + placement, + strategy: 'fixed', + open, + onOpenChange: setOpen, + middleware: [ + flip(), + shift(), + offset(offsetValue), + ], + }) + const dismiss = useDismiss(context) + const { getFloatingProps } = useInteractions([ + dismiss, + ]) + + const handleToggle = useCallback(({ + from, + placement, + offset, + className, + callback, + }: UpdateParams) => { + setFrom(from || 'node') + setOpen(v => !v) + setPlacement(placement || 'top') + setOffsetValue(offset || 0) + setClassName(className || '') + callbackRef.current = callback + }, []) + + const handleSelect = useCallback((type) => { + if (callbackRef.current) + callbackRef.current(type) + setOpen(v => !v) + }, []) + + return ( + + {children} + { + open && ( + +
+ +
+
+ ) + } +
+ ) +} diff --git a/web/app/components/workflow/block-selector/index.tsx b/web/app/components/workflow/block-selector/index.tsx index a7db2772c6..4c47a850e2 100644 --- a/web/app/components/workflow/block-selector/index.tsx +++ b/web/app/components/workflow/block-selector/index.tsx @@ -1,87 +1,30 @@ -import type { FC, ReactElement } from 'react' -import { - memo, - useState, -} from 'react' -import type { - OffsetOptions, - Placement, -} from '@floating-ui/react' -import { - FloatingPortal, - flip, - offset, - shift, - useClick, - useDismiss, - useFloating, - useInteractions, -} from '@floating-ui/react' +import type { FC } from 'react' +import { memo } from 'react' import Tabs from './tabs' +import type { OnSelect } from './types' import { SearchLg } from '@/app/components/base/icons/src/vender/line/general' type NodeSelectorProps = { - placement?: Placement - offset?: OffsetOptions + onSelect: OnSelect className?: string - children: (props: any) => ReactElement } const NodeSelector: FC = ({ - placement = 'top', - offset: offsetValue = 0, + onSelect, className, - children, }) => { - const [open, setOpen] = useState(false) - const { refs, floatingStyles, context } = useFloating({ - placement, - strategy: 'fixed', - open, - onOpenChange: setOpen, - middleware: [ - flip(), - shift(), - offset(offsetValue), - ], - }) - const click = useClick(context) - const dismiss = useDismiss(context, { - bubbles: false, - }) - const { getReferenceProps, getFloatingProps } = useInteractions([ - click, - dismiss, - ]) - return ( - <> - {children({ ...getReferenceProps(), ref: refs.setReference, open })} - { - open && ( - -
-
-
-
- - -
-
- -
-
-
- ) - } - +
+
+
+ + +
+
+ +
) } diff --git a/web/app/components/workflow/block-selector/tabs.tsx b/web/app/components/workflow/block-selector/tabs.tsx index fbcc31dd59..ce197bf0b6 100644 --- a/web/app/components/workflow/block-selector/tabs.tsx +++ b/web/app/components/workflow/block-selector/tabs.tsx @@ -1,24 +1,23 @@ +import type { FC } from 'react' import { memo, useState, } from 'react' -import { useNodeId } from 'reactflow' import BlockIcon from '../block-icon' -import { useWorkflowContext } from '../context' +import type { OnSelect } from './types' import { BLOCK_CLASSIFICATIONS, BLOCK_GROUP_BY_CLASSIFICATION, TABS, } from './constants' -const Tabs = () => { - const { - nodes, - handleAddNextNode, - } = useWorkflowContext() +export type TabsProps = { + onSelect: OnSelect +} +const Tabs: FC = ({ + onSelect, +}) => { const [activeTab, setActiveTab] = useState(TABS[0].key) - const nodeId = useNodeId() - const currentNode = nodes.find(node => node.id === nodeId) return (
@@ -59,7 +58,7 @@ const Tabs = () => { className='flex items-center px-3 h-8 rounded-lg hover:bg-gray-50 cursor-pointer' onClick={(e) => { e.stopPropagation() - handleAddNextNode(currentNode!, block.type) + onSelect(block.type) }} > void diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index fe01903479..954a2949be 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -21,6 +21,7 @@ import AppInfoPanel from './app-info-panel' import ZoomInOut from './zoom-in-out' import CustomEdge from './custom-edge' import type { Node } from './types' +import { BlockSelectorContextProvider } from './block-selector/context' const nodeTypes = { custom: CustomNode, @@ -93,7 +94,9 @@ const WorkflowWrap: FC = ({ handleAddNextNode, handleUpdateNodeData, }}> - + + + ) } diff --git a/web/app/components/workflow/nodes/_base/components/next-step.tsx b/web/app/components/workflow/nodes/_base/components/next-step.tsx index ba4b0d63e4..20f7539698 100644 --- a/web/app/components/workflow/nodes/_base/components/next-step.tsx +++ b/web/app/components/workflow/nodes/_base/components/next-step.tsx @@ -6,9 +6,12 @@ import { } from 'react' import { getOutgoers } from 'reactflow' import BlockIcon from '../../../block-icon' -import type { Node } from '../../../types' +import type { + BlockEnum, + Node, +} from '../../../types' import { useWorkflowContext } from '../../../context' -import BlockSelector from '../../../block-selector' +import { useBlockSelectorContext } from '../../../block-selector/context' import { Plus } from '@/app/components/base/icons/src/vender/line/general' import Button from '@/app/components/base/button' @@ -18,49 +21,23 @@ type NextStepProps = { const NextStep: FC = ({ selectedNode, }) => { + const { + from, + open, + referenceRef, + handleToggle, + } = useBlockSelectorContext() const { nodes, edges, + handleAddNextNode, } = useWorkflowContext() const outgoers = useMemo(() => { return getOutgoers(selectedNode, nodes, edges) }, [selectedNode, nodes, edges]) - - const renderBlockSelectorChildren = useCallback(({ open, ref, ...restProps }: any) => { - return ( -
-
- -
- SELECT NEXT BLOCK -
- ) - }, []) - const renderBlockSelectorButtonChildren = useCallback(({ open, ref, ...restProps }: any) => { - return ( -
- -
- ) - }, []) + const handleSelectBlock = useCallback((type: BlockEnum) => { + handleAddNextNode(selectedNode, type) + }, [selectedNode, handleAddNextNode]) return (
@@ -71,9 +48,26 @@ const NextStep: FC = ({
{ !outgoers.length && ( - - {renderBlockSelectorChildren} - +
{ + handleToggle({ + from: 'panel', + className: 'w-[328px]', + callback: handleSelectBlock, + }) + }} + ref={from === 'panel' ? referenceRef : null} + className={` + flex items-center px-2 w-[328px] h-9 rounded-lg border border-dashed border-gray-200 bg-gray-50 + hover:bg-gray-100 text-xs text-gray-500 cursor-pointer + ${open && from === 'panel' && '!bg-gray-100'} + `} + > +
+ +
+ SELECT NEXT BLOCK +
) } { @@ -87,12 +81,26 @@ const NextStep: FC = ({ className='shrink-0 mr-1.5' />
{outgoer.data.name}
- { + handleToggle({ + from: 'panel', + className: 'w-[328px]', + placement: 'top-end', + offset: 6, + }) + }} > - {renderBlockSelectorButtonChildren} - + +
)) } diff --git a/web/app/components/workflow/nodes/_base/node.tsx b/web/app/components/workflow/nodes/_base/node.tsx index 599e1d7034..95c8d81baa 100644 --- a/web/app/components/workflow/nodes/_base/node.tsx +++ b/web/app/components/workflow/nodes/_base/node.tsx @@ -11,7 +11,8 @@ import { import type { NodeProps } from 'reactflow' import { getOutgoers } from 'reactflow' import { useWorkflowContext } from '../../context' -import BlockSelector from '../../block-selector' +import type { BlockEnum } from '../../types' +import { useBlockSelectorContext } from '../../block-selector/context' import NodeControl from '../../node-control' import BlockIcon from '../../block-icon' import { Plus02 } from '@/app/components/base/icons/src/vender/line/general' @@ -30,30 +31,23 @@ const BaseNode: FC = ({ edges, selectedNodeId, handleSelectedNodeIdChange, + handleAddNextNode, } = useWorkflowContext() + const { + from, + open, + referenceRef, + handleToggle, + } = useBlockSelectorContext() const currentNode = useMemo(() => { return nodes.find(node => node.id === nodeId) }, [nodeId, nodes]) const outgoers = useMemo(() => { return getOutgoers(currentNode!, nodes, edges) }, [currentNode, nodes, edges]) - const renderBlockSelectorChildren = useCallback(({ open, ref, ...restProps }: any) => { - return ( -
e.stopPropagation()}> - -
- ) - }, []) + const handleSelectBlock = useCallback((type: BlockEnum) => { + handleAddNextNode(currentNode!, type) + }, [currentNode, handleAddNextNode]) return (
= ({
{ !outgoers.length && ( - { + e.stopPropagation() + handleToggle({ + placement: 'right', + offset: 6, + callback: handleSelectBlock, + }) + }} + className={` + hidden absolute -bottom-2 left-1/2 -translate-x-1/2 items-center justify-center + w-4 h-4 rounded-full bg-primary-600 cursor-pointer z-10 group-hover:flex + ${open && from === 'node' && '!flex'} + `} > - {renderBlockSelectorChildren} - + +
) } diff --git a/web/app/components/workflow/nodes/_base/panel.tsx b/web/app/components/workflow/nodes/_base/panel.tsx index 77b8932b7c..9cb1d558b4 100644 --- a/web/app/components/workflow/nodes/_base/panel.tsx +++ b/web/app/components/workflow/nodes/_base/panel.tsx @@ -10,7 +10,10 @@ import type { NodeProps } from 'reactflow' import { useWorkflowContext } from '../../context' import BlockIcon from '../../block-icon' import NextStep from './components/next-step' -import { XClose } from '@/app/components/base/icons/src/vender/line/general' +import { + DotsHorizontal, + XClose, +} from '@/app/components/base/icons/src/vender/line/general' import { GitBranch01 } from '@/app/components/base/icons/src/vender/line/development' type BasePanelProps = { @@ -37,12 +40,16 @@ const BasePanel: FC = ({ size='md' />
{data.title}
-
+
+
+ +
+
handleSelectedNodeIdChange('')} > - +