Commit 13a54c3f authored by StyleZhang's avatar StyleZhang

block-selector edit

parent d58a1b13
'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<BlockSelectorContextValue>({
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<Placement>('top')
const [offsetValue, setOffsetValue] = useState<OffsetOptions>(0)
const [className, setClassName] = useState<string>('')
const callbackRef = useRef<OnSelect | undefined>(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<OnSelect>((type) => {
if (callbackRef.current)
callbackRef.current(type)
setOpen(v => !v)
}, [])
return (
<BlockSelectorContext.Provider value={{
from,
open,
setOpen,
handleToggle,
referenceRef: refs.setReference,
}}>
{children}
{
open && (
<FloatingPortal>
<div
ref={refs.setFloating}
style={floatingStyles}
{...getFloatingProps()}
className='z-[1000]'
>
<BlockSelector
className={className}
onSelect={handleSelect}
/>
</div>
</FloatingPortal>
)
}
</BlockSelectorContext.Provider>
)
}
import type { FC, ReactElement } from 'react' import type { FC } from 'react'
import { import { memo } from 'react'
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 Tabs from './tabs' import Tabs from './tabs'
import type { OnSelect } from './types'
import { SearchLg } from '@/app/components/base/icons/src/vender/line/general' import { SearchLg } from '@/app/components/base/icons/src/vender/line/general'
type NodeSelectorProps = { type NodeSelectorProps = {
placement?: Placement onSelect: OnSelect
offset?: OffsetOptions
className?: string className?: string
children: (props: any) => ReactElement
} }
const NodeSelector: FC<NodeSelectorProps> = ({ const NodeSelector: FC<NodeSelectorProps> = ({
placement = 'top', onSelect,
offset: offsetValue = 0,
className, 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 ( return (
<> <div className={`w-[256px] rounded-lg border-[0.5px] border-gray-200 bg-white shadow-lg ${className}`}>
{children({ ...getReferenceProps(), ref: refs.setReference, open })} <div className='px-2 pt-2'>
{ <div className='flex items-center px-2 rounded-lg bg-gray-100'>
open && ( <SearchLg className='shrink-0 ml-[1px] mr-[5px] w-3.5 h-3.5 text-gray-400' />
<FloatingPortal> <input
<div className='grow px-0.5 py-[7px] text-[13px] bg-transparent appearance-none outline-none'
ref={refs.setFloating} placeholder='Search block'
style={floatingStyles} />
{...getFloatingProps()} </div>
className='z-[1000]' </div>
> <Tabs onSelect={onSelect} />
<div className={`w-[256px] rounded-lg border-[0.5px] border-gray-200 bg-white shadow-lg ${className}`}> </div>
<div className='px-2 pt-2'>
<div className='flex items-center px-2 rounded-lg bg-gray-100'>
<SearchLg className='shrink-0 ml-[1px] mr-[5px] w-3.5 h-3.5 text-gray-400' />
<input
className='grow px-0.5 py-[7px] text-[13px] bg-transparent appearance-none outline-none'
placeholder='Search block'
/>
</div>
</div>
<Tabs />
</div>
</div>
</FloatingPortal>
)
}
</>
) )
} }
......
import type { FC } from 'react'
import { import {
memo, memo,
useState, useState,
} from 'react' } from 'react'
import { useNodeId } from 'reactflow'
import BlockIcon from '../block-icon' import BlockIcon from '../block-icon'
import { useWorkflowContext } from '../context' import type { OnSelect } from './types'
import { import {
BLOCK_CLASSIFICATIONS, BLOCK_CLASSIFICATIONS,
BLOCK_GROUP_BY_CLASSIFICATION, BLOCK_GROUP_BY_CLASSIFICATION,
TABS, TABS,
} from './constants' } from './constants'
const Tabs = () => { export type TabsProps = {
const { onSelect: OnSelect
nodes, }
handleAddNextNode, const Tabs: FC<TabsProps> = ({
} = useWorkflowContext() onSelect,
}) => {
const [activeTab, setActiveTab] = useState(TABS[0].key) const [activeTab, setActiveTab] = useState(TABS[0].key)
const nodeId = useNodeId()
const currentNode = nodes.find(node => node.id === nodeId)
return ( return (
<div> <div>
...@@ -59,7 +58,7 @@ const Tabs = () => { ...@@ -59,7 +58,7 @@ const Tabs = () => {
className='flex items-center px-3 h-8 rounded-lg hover:bg-gray-50 cursor-pointer' className='flex items-center px-3 h-8 rounded-lg hover:bg-gray-50 cursor-pointer'
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
handleAddNextNode(currentNode!, block.type) onSelect(block.type)
}} }}
> >
<BlockIcon <BlockIcon
......
import type { BlockEnum } from '../types'
export type OnSelect = (type: BlockEnum) => void
...@@ -21,6 +21,7 @@ import AppInfoPanel from './app-info-panel' ...@@ -21,6 +21,7 @@ import AppInfoPanel from './app-info-panel'
import ZoomInOut from './zoom-in-out' import ZoomInOut from './zoom-in-out'
import CustomEdge from './custom-edge' import CustomEdge from './custom-edge'
import type { Node } from './types' import type { Node } from './types'
import { BlockSelectorContextProvider } from './block-selector/context'
const nodeTypes = { const nodeTypes = {
custom: CustomNode, custom: CustomNode,
...@@ -93,7 +94,9 @@ const WorkflowWrap: FC<WorkflowWrapProps> = ({ ...@@ -93,7 +94,9 @@ const WorkflowWrap: FC<WorkflowWrapProps> = ({
handleAddNextNode, handleAddNextNode,
handleUpdateNodeData, handleUpdateNodeData,
}}> }}>
<Workflow /> <BlockSelectorContextProvider>
<Workflow />
</BlockSelectorContextProvider>
</WorkflowContext.Provider> </WorkflowContext.Provider>
) )
} }
......
...@@ -6,9 +6,12 @@ import { ...@@ -6,9 +6,12 @@ import {
} from 'react' } from 'react'
import { getOutgoers } from 'reactflow' import { getOutgoers } from 'reactflow'
import BlockIcon from '../../../block-icon' import BlockIcon from '../../../block-icon'
import type { Node } from '../../../types' import type {
BlockEnum,
Node,
} from '../../../types'
import { useWorkflowContext } from '../../../context' 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 { Plus } from '@/app/components/base/icons/src/vender/line/general'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
...@@ -18,49 +21,23 @@ type NextStepProps = { ...@@ -18,49 +21,23 @@ type NextStepProps = {
const NextStep: FC<NextStepProps> = ({ const NextStep: FC<NextStepProps> = ({
selectedNode, selectedNode,
}) => { }) => {
const {
from,
open,
referenceRef,
handleToggle,
} = useBlockSelectorContext()
const { const {
nodes, nodes,
edges, edges,
handleAddNextNode,
} = useWorkflowContext() } = useWorkflowContext()
const outgoers = useMemo(() => { const outgoers = useMemo(() => {
return getOutgoers(selectedNode, nodes, edges) return getOutgoers(selectedNode, nodes, edges)
}, [selectedNode, nodes, edges]) }, [selectedNode, nodes, edges])
const handleSelectBlock = useCallback((type: BlockEnum) => {
const renderBlockSelectorChildren = useCallback(({ open, ref, ...restProps }: any) => { handleAddNextNode(selectedNode, type)
return ( }, [selectedNode, handleAddNextNode])
<div
{...restProps}
ref={ref}
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 && '!bg-gray-100'}
`}
>
<div className='flex items-center justify-center mr-1.5 w-5 h-5 rounded-[5px] bg-gray-200'>
<Plus className='w-3 h-3' />
</div>
SELECT NEXT BLOCK
</div>
)
}, [])
const renderBlockSelectorButtonChildren = useCallback(({ open, ref, ...restProps }: any) => {
return (
<div
{...restProps}
ref={ref}
>
<Button
className={`
hidden group-hover:flex px-2 py-0 h-6 bg-white text-xs text-gray-700 font-medium rounded-md
${open && '!bg-gray-100 !flex'}
`}
>
Change
</Button>
</div>
)
}, [])
return ( return (
<div className='flex py-1'> <div className='flex py-1'>
...@@ -71,9 +48,26 @@ const NextStep: FC<NextStepProps> = ({ ...@@ -71,9 +48,26 @@ const NextStep: FC<NextStepProps> = ({
<div className='grow'> <div className='grow'>
{ {
!outgoers.length && ( !outgoers.length && (
<BlockSelector className='!w-[328px]'> <div
{renderBlockSelectorChildren} onClick={() => {
</BlockSelector> 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'}
`}
>
<div className='flex items-center justify-center mr-1.5 w-5 h-5 rounded-[5px] bg-gray-200'>
<Plus className='w-3 h-3' />
</div>
SELECT NEXT BLOCK
</div>
) )
} }
{ {
...@@ -87,12 +81,26 @@ const NextStep: FC<NextStepProps> = ({ ...@@ -87,12 +81,26 @@ const NextStep: FC<NextStepProps> = ({
className='shrink-0 mr-1.5' className='shrink-0 mr-1.5'
/> />
<div className='grow'>{outgoer.data.name}</div> <div className='grow'>{outgoer.data.name}</div>
<BlockSelector <div
placement='top-end' ref={from === 'panel' ? referenceRef : null}
offset={6} onClick={() => {
handleToggle({
from: 'panel',
className: 'w-[328px]',
placement: 'top-end',
offset: 6,
})
}}
> >
{renderBlockSelectorButtonChildren} <Button
</BlockSelector> className={`
hidden group-hover:flex px-2 py-0 h-6 bg-white text-xs text-gray-700 font-medium rounded-md
${open && '!bg-gray-100 !flex'}
`}
>
Change
</Button>
</div>
</div> </div>
)) ))
} }
......
...@@ -11,7 +11,8 @@ import { ...@@ -11,7 +11,8 @@ import {
import type { NodeProps } from 'reactflow' import type { NodeProps } from 'reactflow'
import { getOutgoers } from 'reactflow' import { getOutgoers } from 'reactflow'
import { useWorkflowContext } from '../../context' 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 NodeControl from '../../node-control'
import BlockIcon from '../../block-icon' import BlockIcon from '../../block-icon'
import { Plus02 } from '@/app/components/base/icons/src/vender/line/general' import { Plus02 } from '@/app/components/base/icons/src/vender/line/general'
...@@ -30,30 +31,23 @@ const BaseNode: FC<BaseNodeProps> = ({ ...@@ -30,30 +31,23 @@ const BaseNode: FC<BaseNodeProps> = ({
edges, edges,
selectedNodeId, selectedNodeId,
handleSelectedNodeIdChange, handleSelectedNodeIdChange,
handleAddNextNode,
} = useWorkflowContext() } = useWorkflowContext()
const {
from,
open,
referenceRef,
handleToggle,
} = useBlockSelectorContext()
const currentNode = useMemo(() => { const currentNode = useMemo(() => {
return nodes.find(node => node.id === nodeId) return nodes.find(node => node.id === nodeId)
}, [nodeId, nodes]) }, [nodeId, nodes])
const outgoers = useMemo(() => { const outgoers = useMemo(() => {
return getOutgoers(currentNode!, nodes, edges) return getOutgoers(currentNode!, nodes, edges)
}, [currentNode, nodes, edges]) }, [currentNode, nodes, edges])
const renderBlockSelectorChildren = useCallback(({ open, ref, ...restProps }: any) => { const handleSelectBlock = useCallback((type: BlockEnum) => {
return ( handleAddNextNode(currentNode!, type)
<div onClick={e => e.stopPropagation()}> }, [currentNode, handleAddNextNode])
<div
{...restProps}
ref={ref}
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 && '!flex'}
`}
>
<Plus02 className='w-2.5 h-2.5 text-white' />
</div>
</div>
)
}, [])
return ( return (
<div <div
...@@ -81,12 +75,24 @@ const BaseNode: FC<BaseNodeProps> = ({ ...@@ -81,12 +75,24 @@ const BaseNode: FC<BaseNodeProps> = ({
</div> </div>
{ {
!outgoers.length && ( !outgoers.length && (
<BlockSelector <div
placement='right' ref={from === 'node' ? referenceRef : null}
offset={6} onClick={(e) => {
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} <Plus02 className='w-2.5 h-2.5 text-white' />
</BlockSelector> </div>
) )
} }
</div> </div>
......
...@@ -10,7 +10,10 @@ import type { NodeProps } from 'reactflow' ...@@ -10,7 +10,10 @@ import type { NodeProps } from 'reactflow'
import { useWorkflowContext } from '../../context' import { useWorkflowContext } from '../../context'
import BlockIcon from '../../block-icon' import BlockIcon from '../../block-icon'
import NextStep from './components/next-step' 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' import { GitBranch01 } from '@/app/components/base/icons/src/vender/line/development'
type BasePanelProps = { type BasePanelProps = {
...@@ -37,12 +40,16 @@ const BasePanel: FC<BasePanelProps> = ({ ...@@ -37,12 +40,16 @@ const BasePanel: FC<BasePanelProps> = ({
size='md' size='md'
/> />
<div className='grow py-1 text-base text-gray-900 font-semibold '>{data.title}</div> <div className='grow py-1 text-base text-gray-900 font-semibold '>{data.title}</div>
<div className='shrink-0 flex items-center'> <div className='shrink-0 flex items-center text-gray-500'>
<div className='flex items-center justify-center w-6 h-6 cursor-pointer'>
<DotsHorizontal className='w-4 h-4' />
</div>
<div className='mx-3 w-[1px] h-3.5 bg-gray-200' />
<div <div
className='flex items-center justify-center w-6 h-6 cursor-pointer' className='flex items-center justify-center w-6 h-6 cursor-pointer'
onClick={() => handleSelectedNodeIdChange('')} onClick={() => handleSelectedNodeIdChange('')}
> >
<XClose className='w-4 h-4 text-gray-500' /> <XClose className='w-4 h-4' />
</div> </div>
</div> </div>
</div> </div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment