mirror of
https://github.com/langgenius/dify.git
synced 2026-01-04 13:37:22 +00:00
block-selector edit
This commit is contained in:
123
web/app/components/workflow/block-selector/context.tsx
Normal file
123
web/app/components/workflow/block-selector/context.tsx
Normal file
@@ -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<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>
|
||||
)
|
||||
}
|
||||
@@ -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<NodeSelectorProps> = ({
|
||||
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 && (
|
||||
<FloatingPortal>
|
||||
<div
|
||||
ref={refs.setFloating}
|
||||
style={floatingStyles}
|
||||
{...getFloatingProps()}
|
||||
className='z-[1000]'
|
||||
>
|
||||
<div className={`w-[256px] rounded-lg border-[0.5px] border-gray-200 bg-white shadow-lg ${className}`}>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
</>
|
||||
<div className={`w-[256px] rounded-lg border-[0.5px] border-gray-200 bg-white shadow-lg ${className}`}>
|
||||
<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 onSelect={onSelect} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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<TabsProps> = ({
|
||||
onSelect,
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState(TABS[0].key)
|
||||
const nodeId = useNodeId()
|
||||
const currentNode = nodes.find(node => node.id === nodeId)
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -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)
|
||||
}}
|
||||
>
|
||||
<BlockIcon
|
||||
|
||||
3
web/app/components/workflow/block-selector/types.ts
Normal file
3
web/app/components/workflow/block-selector/types.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import type { BlockEnum } from '../types'
|
||||
|
||||
export type OnSelect = (type: BlockEnum) => void
|
||||
@@ -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<WorkflowWrapProps> = ({
|
||||
handleAddNextNode,
|
||||
handleUpdateNodeData,
|
||||
}}>
|
||||
<Workflow />
|
||||
<BlockSelectorContextProvider>
|
||||
<Workflow />
|
||||
</BlockSelectorContextProvider>
|
||||
</WorkflowContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<NextStepProps> = ({
|
||||
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 (
|
||||
<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>
|
||||
)
|
||||
}, [])
|
||||
const handleSelectBlock = useCallback((type: BlockEnum) => {
|
||||
handleAddNextNode(selectedNode, type)
|
||||
}, [selectedNode, handleAddNextNode])
|
||||
|
||||
return (
|
||||
<div className='flex py-1'>
|
||||
@@ -71,9 +48,26 @@ const NextStep: FC<NextStepProps> = ({
|
||||
<div className='grow'>
|
||||
{
|
||||
!outgoers.length && (
|
||||
<BlockSelector className='!w-[328px]'>
|
||||
{renderBlockSelectorChildren}
|
||||
</BlockSelector>
|
||||
<div
|
||||
onClick={() => {
|
||||
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> = ({
|
||||
className='shrink-0 mr-1.5'
|
||||
/>
|
||||
<div className='grow'>{outgoer.data.name}</div>
|
||||
<BlockSelector
|
||||
placement='top-end'
|
||||
offset={6}
|
||||
<div
|
||||
ref={from === 'panel' ? referenceRef : null}
|
||||
onClick={() => {
|
||||
handleToggle({
|
||||
from: 'panel',
|
||||
className: 'w-[328px]',
|
||||
placement: 'top-end',
|
||||
offset: 6,
|
||||
})
|
||||
}}
|
||||
>
|
||||
{renderBlockSelectorButtonChildren}
|
||||
</BlockSelector>
|
||||
<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>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
|
||||
@@ -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<BaseNodeProps> = ({
|
||||
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 (
|
||||
<div onClick={e => e.stopPropagation()}>
|
||||
<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>
|
||||
)
|
||||
}, [])
|
||||
const handleSelectBlock = useCallback((type: BlockEnum) => {
|
||||
handleAddNextNode(currentNode!, type)
|
||||
}, [currentNode, handleAddNextNode])
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -81,12 +75,24 @@ const BaseNode: FC<BaseNodeProps> = ({
|
||||
</div>
|
||||
{
|
||||
!outgoers.length && (
|
||||
<BlockSelector
|
||||
placement='right'
|
||||
offset={6}
|
||||
<div
|
||||
ref={from === 'node' ? referenceRef : null}
|
||||
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}
|
||||
</BlockSelector>
|
||||
<Plus02 className='w-2.5 h-2.5 text-white' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -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<BasePanelProps> = ({
|
||||
size='md'
|
||||
/>
|
||||
<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
|
||||
className='flex items-center justify-center w-6 h-6 cursor-pointer'
|
||||
onClick={() => handleSelectedNodeIdChange('')}
|
||||
>
|
||||
<XClose className='w-4 h-4 text-gray-500' />
|
||||
<XClose className='w-4 h-4' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user