mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-12 20:19:12 +08:00
refactor: revamp picker block (#4227)
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
This commit is contained in:
parent
68b1d063f7
commit
0046ef7707
@ -1,85 +0,0 @@
|
|||||||
import { memo } from 'react'
|
|
||||||
import { MenuOption } from '@lexical/react/LexicalTypeaheadMenuPlugin'
|
|
||||||
|
|
||||||
export class VariableOption extends MenuOption {
|
|
||||||
title: string
|
|
||||||
icon?: JSX.Element
|
|
||||||
extraElement?: JSX.Element
|
|
||||||
keywords: Array<string>
|
|
||||||
keyboardShortcut?: string
|
|
||||||
onSelect: (queryString: string) => void
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
title: string,
|
|
||||||
options: {
|
|
||||||
icon?: JSX.Element
|
|
||||||
extraElement?: JSX.Element
|
|
||||||
keywords?: Array<string>
|
|
||||||
keyboardShortcut?: string
|
|
||||||
onSelect: (queryString: string) => void
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
super(title)
|
|
||||||
this.title = title
|
|
||||||
this.keywords = options.keywords || []
|
|
||||||
this.icon = options.icon
|
|
||||||
this.extraElement = options.extraElement
|
|
||||||
this.keyboardShortcut = options.keyboardShortcut
|
|
||||||
this.onSelect = options.onSelect.bind(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type VariableMenuItemProps = {
|
|
||||||
isSelected: boolean
|
|
||||||
onClick: () => void
|
|
||||||
onMouseEnter: () => void
|
|
||||||
option: VariableOption
|
|
||||||
queryString: string | null
|
|
||||||
}
|
|
||||||
export const VariableMenuItem = memo(({
|
|
||||||
isSelected,
|
|
||||||
onClick,
|
|
||||||
onMouseEnter,
|
|
||||||
option,
|
|
||||||
queryString,
|
|
||||||
}: VariableMenuItemProps) => {
|
|
||||||
const title = option.title
|
|
||||||
let before = title
|
|
||||||
let middle = ''
|
|
||||||
let after = ''
|
|
||||||
|
|
||||||
if (queryString) {
|
|
||||||
const regex = new RegExp(queryString, 'i')
|
|
||||||
const match = regex.exec(option.title)
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
before = title.substring(0, match.index)
|
|
||||||
middle = match[0]
|
|
||||||
after = title.substring(match.index + match[0].length)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={option.key}
|
|
||||||
className={`
|
|
||||||
flex items-center px-3 h-6 rounded-md hover:bg-primary-50 cursor-pointer
|
|
||||||
${isSelected && 'bg-primary-50'}
|
|
||||||
`}
|
|
||||||
tabIndex={-1}
|
|
||||||
ref={option.setRefElement}
|
|
||||||
onMouseEnter={onMouseEnter}
|
|
||||||
onClick={onClick}>
|
|
||||||
<div className='mr-2'>
|
|
||||||
{option.icon}
|
|
||||||
</div>
|
|
||||||
<div className='grow text-[13px] text-gray-900 truncate' title={option.title}>
|
|
||||||
{before}
|
|
||||||
<span className='text-[#2970FF]'>{middle}</span>
|
|
||||||
{after}
|
|
||||||
</div>
|
|
||||||
{option.extraElement}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
VariableMenuItem.displayName = 'VariableMenuItem'
|
|
@ -15,8 +15,9 @@ import { INSERT_HISTORY_BLOCK_COMMAND } from '../history-block'
|
|||||||
import { INSERT_QUERY_BLOCK_COMMAND } from '../query-block'
|
import { INSERT_QUERY_BLOCK_COMMAND } from '../query-block'
|
||||||
import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from '../variable-block'
|
import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from '../variable-block'
|
||||||
import { $createCustomTextNode } from '../custom-text/node'
|
import { $createCustomTextNode } from '../custom-text/node'
|
||||||
import { PromptOption } from './prompt-option'
|
import { PromptMenuItem } from './prompt-option'
|
||||||
import { VariableOption } from './variable-option'
|
import { VariableMenuItem } from './variable-option'
|
||||||
|
import { PickerBlockMenuOption } from './menu'
|
||||||
import { File05 } from '@/app/components/base/icons/src/vender/solid/files'
|
import { File05 } from '@/app/components/base/icons/src/vender/solid/files'
|
||||||
import {
|
import {
|
||||||
MessageClockCircle,
|
MessageClockCircle,
|
||||||
@ -35,62 +36,111 @@ export const usePromptOptions = (
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [editor] = useLexicalComposerContext()
|
const [editor] = useLexicalComposerContext()
|
||||||
|
|
||||||
return useMemo(() => {
|
const promptOptions: PickerBlockMenuOption[] = []
|
||||||
return [
|
if (contextBlock?.show) {
|
||||||
...contextBlock?.show
|
promptOptions.push(new PickerBlockMenuOption({
|
||||||
? [
|
key: t('common.promptEditor.context.item.title'),
|
||||||
new PromptOption(t('common.promptEditor.context.item.title'), {
|
group: 'prompt context',
|
||||||
icon: <File05 className='w-4 h-4 text-[#6938EF]' />,
|
render: ({ isSelected, onSelect, onSetHighlight }) => {
|
||||||
onSelect: () => {
|
return <PromptMenuItem
|
||||||
if (!contextBlock?.selectable)
|
title={t('common.promptEditor.context.item.title')}
|
||||||
return
|
icon={<File05 className='w-4 h-4 text-[#6938EF]' />}
|
||||||
editor.dispatchCommand(INSERT_CONTEXT_BLOCK_COMMAND, undefined)
|
disabled={!contextBlock.selectable}
|
||||||
},
|
isSelected={isSelected}
|
||||||
disabled: !contextBlock?.selectable,
|
onClick={onSelect}
|
||||||
}),
|
onMouseEnter={onSetHighlight}
|
||||||
]
|
/>
|
||||||
: [],
|
},
|
||||||
...queryBlock?.show
|
onSelect: () => {
|
||||||
? [
|
if (!contextBlock?.selectable)
|
||||||
new PromptOption(t('common.promptEditor.query.item.title'), {
|
return
|
||||||
icon: <UserEdit02 className='w-4 h-4 text-[#FD853A]' />,
|
editor.dispatchCommand(INSERT_CONTEXT_BLOCK_COMMAND, undefined)
|
||||||
onSelect: () => {
|
},
|
||||||
if (!queryBlock?.selectable)
|
}))
|
||||||
return
|
}
|
||||||
editor.dispatchCommand(INSERT_QUERY_BLOCK_COMMAND, undefined)
|
|
||||||
},
|
if (queryBlock?.show) {
|
||||||
disabled: !queryBlock?.selectable,
|
promptOptions.push(
|
||||||
}),
|
new PickerBlockMenuOption({
|
||||||
]
|
key: t('common.promptEditor.query.item.title'),
|
||||||
: [],
|
group: 'prompt query',
|
||||||
...historyBlock?.show
|
render: ({ isSelected, onSelect, onSetHighlight }) => {
|
||||||
? [
|
return (
|
||||||
new PromptOption(t('common.promptEditor.history.item.title'), {
|
<PromptMenuItem
|
||||||
icon: <MessageClockCircle className='w-4 h-4 text-[#DD2590]' />,
|
title={t('common.promptEditor.query.item.title')}
|
||||||
onSelect: () => {
|
icon={<UserEdit02 className='w-4 h-4 text-[#FD853A]' />}
|
||||||
if (!historyBlock?.selectable)
|
disabled={!queryBlock.selectable}
|
||||||
return
|
isSelected={isSelected}
|
||||||
editor.dispatchCommand(INSERT_HISTORY_BLOCK_COMMAND, undefined)
|
onClick={onSelect}
|
||||||
},
|
onMouseEnter={onSetHighlight}
|
||||||
disabled: !historyBlock?.selectable,
|
/>
|
||||||
}),
|
)
|
||||||
]
|
},
|
||||||
: [],
|
onSelect: () => {
|
||||||
]
|
if (!queryBlock?.selectable)
|
||||||
}, [contextBlock, editor, historyBlock, queryBlock, t])
|
return
|
||||||
|
editor.dispatchCommand(INSERT_QUERY_BLOCK_COMMAND, undefined)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (historyBlock?.show) {
|
||||||
|
promptOptions.push(
|
||||||
|
new PickerBlockMenuOption({
|
||||||
|
key: t('common.promptEditor.history.item.title'),
|
||||||
|
group: 'prompt history',
|
||||||
|
render: ({ isSelected, onSelect, onSetHighlight }) => {
|
||||||
|
return (
|
||||||
|
<PromptMenuItem
|
||||||
|
title={t('common.promptEditor.history.item.title')}
|
||||||
|
icon={<MessageClockCircle className='w-4 h-4 text-[#DD2590]' />}
|
||||||
|
disabled={!historyBlock.selectable
|
||||||
|
}
|
||||||
|
isSelected={isSelected}
|
||||||
|
onClick={onSelect}
|
||||||
|
onMouseEnter={onSetHighlight}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onSelect: () => {
|
||||||
|
if (!historyBlock?.selectable)
|
||||||
|
return
|
||||||
|
editor.dispatchCommand(INSERT_HISTORY_BLOCK_COMMAND, undefined)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return promptOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useVariableOptions = (
|
export const useVariableOptions = (
|
||||||
variableBlock?: VariableBlockType,
|
variableBlock?: VariableBlockType,
|
||||||
queryString?: string,
|
queryString?: string,
|
||||||
) => {
|
): PickerBlockMenuOption[] => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [editor] = useLexicalComposerContext()
|
const [editor] = useLexicalComposerContext()
|
||||||
|
|
||||||
const options = useMemo(() => {
|
const options = useMemo(() => {
|
||||||
const baseOptions = (variableBlock?.variables || []).map((item) => {
|
if (!variableBlock?.variables)
|
||||||
return new VariableOption(item.value, {
|
return []
|
||||||
icon: <BracketsX className='w-[14px] h-[14px] text-[#2970FF]' />,
|
|
||||||
|
const baseOptions = (variableBlock.variables).map((item) => {
|
||||||
|
return new PickerBlockMenuOption({
|
||||||
|
key: item.value,
|
||||||
|
group: 'prompt variable',
|
||||||
|
render: ({ queryString, isSelected, onSelect, onSetHighlight }) => {
|
||||||
|
return (
|
||||||
|
<VariableMenuItem
|
||||||
|
title={item.value}
|
||||||
|
icon={<BracketsX className='w-[14px] h-[14px] text-[#2970FF]' />}
|
||||||
|
queryString={queryString}
|
||||||
|
isSelected={isSelected}
|
||||||
|
onClick={onSelect}
|
||||||
|
onMouseEnter={onSetHighlight}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${item.value}}}`)
|
editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${item.value}}}`)
|
||||||
},
|
},
|
||||||
@ -101,12 +151,25 @@ export const useVariableOptions = (
|
|||||||
|
|
||||||
const regex = new RegExp(queryString, 'i')
|
const regex = new RegExp(queryString, 'i')
|
||||||
|
|
||||||
return baseOptions.filter(option => regex.test(option.title) || option.keywords.some(keyword => regex.test(keyword)))
|
return baseOptions.filter(option => regex.test(option.key))
|
||||||
}, [editor, queryString, variableBlock])
|
}, [editor, queryString, variableBlock])
|
||||||
|
|
||||||
const addOption = useMemo(() => {
|
const addOption = useMemo(() => {
|
||||||
return new VariableOption(t('common.promptEditor.variable.modal.add'), {
|
return new PickerBlockMenuOption({
|
||||||
icon: <BracketsX className='mr-2 w-[14px] h-[14px] text-[#2970FF]' />,
|
key: t('common.promptEditor.variable.modal.add'),
|
||||||
|
group: 'prompt variable',
|
||||||
|
render: ({ queryString, isSelected, onSelect, onSetHighlight }) => {
|
||||||
|
return (
|
||||||
|
<VariableMenuItem
|
||||||
|
title={t('common.promptEditor.variable.modal.add')}
|
||||||
|
icon={<BracketsX className='mr-2 w-[14px] h-[14px] text-[#2970FF]' />}
|
||||||
|
queryString={queryString}
|
||||||
|
isSelected={isSelected}
|
||||||
|
onClick={onSelect}
|
||||||
|
onMouseEnter={onSetHighlight}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
const prefixNode = $createCustomTextNode('{{')
|
const prefixNode = $createCustomTextNode('{{')
|
||||||
@ -131,16 +194,31 @@ export const useExternalToolOptions = (
|
|||||||
const [editor] = useLexicalComposerContext()
|
const [editor] = useLexicalComposerContext()
|
||||||
|
|
||||||
const options = useMemo(() => {
|
const options = useMemo(() => {
|
||||||
const baseToolOptions = (externalToolBlockType?.externalTools || []).map((item) => {
|
if (!externalToolBlockType?.externalTools)
|
||||||
return new VariableOption(item.name, {
|
return []
|
||||||
icon: (
|
const baseToolOptions = (externalToolBlockType.externalTools).map((item) => {
|
||||||
<AppIcon
|
return new PickerBlockMenuOption({
|
||||||
className='!w-[14px] !h-[14px]'
|
key: item.name,
|
||||||
icon={item.icon}
|
group: 'external tool',
|
||||||
background={item.icon_background}
|
render: ({ queryString, isSelected, onSelect, onSetHighlight }) => {
|
||||||
/>
|
return (
|
||||||
),
|
<VariableMenuItem
|
||||||
extraElement: <div className='text-xs text-gray-400'>{item.variableName}</div>,
|
title={item.name}
|
||||||
|
icon={
|
||||||
|
<AppIcon
|
||||||
|
className='!w-[14px] !h-[14px]'
|
||||||
|
icon={item.icon}
|
||||||
|
background={item.icon_background}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
extraElement={<div className='text-xs text-gray-400'>{item.variableName}</div>}
|
||||||
|
queryString={queryString}
|
||||||
|
isSelected={isSelected}
|
||||||
|
onClick={onSelect}
|
||||||
|
onMouseEnter={onSetHighlight}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${item.variableName}}}`)
|
editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${item.variableName}}}`)
|
||||||
},
|
},
|
||||||
@ -151,16 +229,28 @@ export const useExternalToolOptions = (
|
|||||||
|
|
||||||
const regex = new RegExp(queryString, 'i')
|
const regex = new RegExp(queryString, 'i')
|
||||||
|
|
||||||
return baseToolOptions.filter(option => regex.test(option.title) || option.keywords.some(keyword => regex.test(keyword)))
|
return baseToolOptions.filter(option => regex.test(option.key))
|
||||||
}, [editor, queryString, externalToolBlockType])
|
}, [editor, queryString, externalToolBlockType])
|
||||||
|
|
||||||
const addOption = useMemo(() => {
|
const addOption = useMemo(() => {
|
||||||
return new VariableOption(t('common.promptEditor.variable.modal.addTool'), {
|
return new PickerBlockMenuOption({
|
||||||
icon: <Tool03 className='mr-2 w-[14px] h-[14px] text-[#444CE7]' />,
|
key: t('common.promptEditor.variable.modal.addTool'),
|
||||||
extraElement: <ArrowUpRight className='w-3 h-3 text-gray-400' />,
|
group: 'external tool',
|
||||||
|
render: ({ queryString, isSelected, onSelect, onSetHighlight }) => {
|
||||||
|
return (
|
||||||
|
<VariableMenuItem
|
||||||
|
title={t('common.promptEditor.variable.modal.addTool')}
|
||||||
|
icon={<Tool03 className='mr-2 w-[14px] h-[14px] text-[#444CE7]' />}
|
||||||
|
extraElement={< ArrowUpRight className='w-3 h-3 text-gray-400' />}
|
||||||
|
queryString={queryString}
|
||||||
|
isSelected={isSelected}
|
||||||
|
onClick={onSelect}
|
||||||
|
onMouseEnter={onSetHighlight}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
if (externalToolBlockType?.onAddExternalTool)
|
externalToolBlockType?.onAddExternalTool?.()
|
||||||
externalToolBlockType.onAddExternalTool()
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}, [externalToolBlockType, t])
|
}, [externalToolBlockType, t])
|
||||||
@ -191,11 +281,8 @@ export const useOptions = (
|
|||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
return {
|
return {
|
||||||
promptOptions,
|
|
||||||
variableOptions,
|
|
||||||
externalToolOptions,
|
|
||||||
workflowVariableOptions,
|
workflowVariableOptions,
|
||||||
allOptions: [...promptOptions, ...variableOptions, ...externalToolOptions],
|
allFlattenOptions: [...promptOptions, ...variableOptions, ...externalToolOptions],
|
||||||
}
|
}
|
||||||
}, [promptOptions, variableOptions, externalToolOptions, workflowVariableOptions])
|
}, [promptOptions, variableOptions, externalToolOptions, workflowVariableOptions])
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
|
Fragment,
|
||||||
memo,
|
memo,
|
||||||
useCallback,
|
useCallback,
|
||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import {
|
import {
|
||||||
FloatingPortal,
|
|
||||||
flip,
|
flip,
|
||||||
offset,
|
offset,
|
||||||
shift,
|
shift,
|
||||||
@ -27,11 +27,8 @@ import { useBasicTypeaheadTriggerMatch } from '../../hooks'
|
|||||||
import { INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND } from '../workflow-variable-block'
|
import { INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND } from '../workflow-variable-block'
|
||||||
import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from '../variable-block'
|
import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from '../variable-block'
|
||||||
import { $splitNodeContainingQuery } from '../../utils'
|
import { $splitNodeContainingQuery } from '../../utils'
|
||||||
import type { PromptOption } from './prompt-option'
|
|
||||||
import PromptMenu from './prompt-menu'
|
|
||||||
import VariableMenu from './variable-menu'
|
|
||||||
import type { VariableOption } from './variable-option'
|
|
||||||
import { useOptions } from './hooks'
|
import { useOptions } from './hooks'
|
||||||
|
import type { PickerBlockMenuOption } from './menu'
|
||||||
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
|
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
|
||||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||||
|
|
||||||
@ -54,11 +51,13 @@ const ComponentPicker = ({
|
|||||||
workflowVariableBlock,
|
workflowVariableBlock,
|
||||||
}: ComponentPickerProps) => {
|
}: ComponentPickerProps) => {
|
||||||
const { eventEmitter } = useEventEmitterContextContext()
|
const { eventEmitter } = useEventEmitterContextContext()
|
||||||
const { refs, floatingStyles, elements } = useFloating({
|
const { refs, floatingStyles, isPositioned } = useFloating({
|
||||||
placement: 'bottom-start',
|
placement: 'bottom-start',
|
||||||
middleware: [
|
middleware: [
|
||||||
offset(0), // fix hide cursor
|
offset(0), // fix hide cursor
|
||||||
shift(),
|
shift({
|
||||||
|
padding: 8,
|
||||||
|
}),
|
||||||
flip(),
|
flip(),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
@ -76,10 +75,7 @@ const ComponentPicker = ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const {
|
const {
|
||||||
allOptions,
|
allFlattenOptions,
|
||||||
promptOptions,
|
|
||||||
variableOptions,
|
|
||||||
externalToolOptions,
|
|
||||||
workflowVariableOptions,
|
workflowVariableOptions,
|
||||||
} = useOptions(
|
} = useOptions(
|
||||||
contextBlock,
|
contextBlock,
|
||||||
@ -92,18 +88,15 @@ const ComponentPicker = ({
|
|||||||
|
|
||||||
const onSelectOption = useCallback(
|
const onSelectOption = useCallback(
|
||||||
(
|
(
|
||||||
selectedOption: PromptOption | VariableOption,
|
selectedOption: PickerBlockMenuOption,
|
||||||
nodeToRemove: TextNode | null,
|
nodeToRemove: TextNode | null,
|
||||||
closeMenu: () => void,
|
closeMenu: () => void,
|
||||||
matchingString: string,
|
|
||||||
) => {
|
) => {
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
if (nodeToRemove && selectedOption?.key)
|
if (nodeToRemove && selectedOption?.key)
|
||||||
nodeToRemove.remove()
|
nodeToRemove.remove()
|
||||||
|
|
||||||
if (selectedOption?.onSelect)
|
selectedOption.onSelectMenuOption()
|
||||||
selectedOption.onSelect(matchingString)
|
|
||||||
|
|
||||||
closeMenu()
|
closeMenu()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -123,157 +116,93 @@ const ComponentPicker = ({
|
|||||||
editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, variables)
|
editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, variables)
|
||||||
}, [editor, checkForTriggerMatch, triggerString])
|
}, [editor, checkForTriggerMatch, triggerString])
|
||||||
|
|
||||||
const renderMenu = useCallback<MenuRenderFn<PromptOption | VariableOption>>((
|
const renderMenu = useCallback<MenuRenderFn<PickerBlockMenuOption>>((
|
||||||
anchorElementRef,
|
anchorElementRef,
|
||||||
{ selectedIndex, selectOptionAndCleanUp, setHighlightedIndex },
|
{ options, selectedIndex, selectOptionAndCleanUp, setHighlightedIndex },
|
||||||
) => {
|
) => {
|
||||||
if (anchorElementRef.current && (allOptions.length || workflowVariableBlock?.show)) {
|
if (!(anchorElementRef.current && (allFlattenOptions.length || workflowVariableBlock?.show)))
|
||||||
return (
|
return null
|
||||||
<>
|
refs.setReference(anchorElementRef.current)
|
||||||
{
|
|
||||||
ReactDOM.createPortal(
|
|
||||||
<div ref={refs.setReference}></div>,
|
|
||||||
anchorElementRef.current,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
elements.reference && (
|
|
||||||
<FloatingPortal id='typeahead-menu'>
|
|
||||||
<div
|
|
||||||
className='w-[260px] bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg overflow-y-auto'
|
|
||||||
style={{
|
|
||||||
...floatingStyles,
|
|
||||||
maxHeight: 'calc(1 / 3 * 100vh)',
|
|
||||||
}}
|
|
||||||
ref={refs.setFloating}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
!!promptOptions.length && (
|
|
||||||
<>
|
|
||||||
<PromptMenu
|
|
||||||
startIndex={0}
|
|
||||||
selectedIndex={selectedIndex}
|
|
||||||
options={promptOptions}
|
|
||||||
onClick={(index, option) => {
|
|
||||||
if (option.disabled)
|
|
||||||
return
|
|
||||||
setHighlightedIndex(index)
|
|
||||||
selectOptionAndCleanUp(option)
|
|
||||||
}}
|
|
||||||
onMouseEnter={(index, option) => {
|
|
||||||
if (option.disabled)
|
|
||||||
return
|
|
||||||
setHighlightedIndex(index)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
!!variableOptions.length && (
|
|
||||||
<>
|
|
||||||
{
|
|
||||||
!!promptOptions.length && (
|
|
||||||
<div className='h-[1px] bg-gray-100'></div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<VariableMenu
|
|
||||||
startIndex={promptOptions.length}
|
|
||||||
selectedIndex={selectedIndex}
|
|
||||||
options={variableOptions}
|
|
||||||
onClick={(index, option) => {
|
|
||||||
if (option.disabled)
|
|
||||||
return
|
|
||||||
setHighlightedIndex(index)
|
|
||||||
selectOptionAndCleanUp(option)
|
|
||||||
}}
|
|
||||||
onMouseEnter={(index, option) => {
|
|
||||||
if (option.disabled)
|
|
||||||
return
|
|
||||||
setHighlightedIndex(index)
|
|
||||||
}}
|
|
||||||
queryString={queryString}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
!!externalToolOptions.length && (
|
|
||||||
<>
|
|
||||||
{
|
|
||||||
(!!promptOptions.length || !!variableOptions.length) && (
|
|
||||||
<div className='h-[1px] bg-gray-100'></div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<VariableMenu
|
|
||||||
startIndex={promptOptions.length + variableOptions.length}
|
|
||||||
selectedIndex={selectedIndex}
|
|
||||||
options={externalToolOptions}
|
|
||||||
onClick={(index, option) => {
|
|
||||||
if (option.disabled)
|
|
||||||
return
|
|
||||||
setHighlightedIndex(index)
|
|
||||||
selectOptionAndCleanUp(option)
|
|
||||||
}}
|
|
||||||
onMouseEnter={(index, option) => {
|
|
||||||
if (option.disabled)
|
|
||||||
return
|
|
||||||
setHighlightedIndex(index)
|
|
||||||
}}
|
|
||||||
queryString={queryString}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
workflowVariableBlock?.show && (
|
|
||||||
<>
|
|
||||||
{
|
|
||||||
(!!promptOptions.length || !!variableOptions.length || !!externalToolOptions.length) && (
|
|
||||||
<div className='h-[1px] bg-gray-100'></div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<div className='p-1'>
|
|
||||||
<VarReferenceVars
|
|
||||||
hideSearch
|
|
||||||
vars={workflowVariableOptions}
|
|
||||||
onChange={(variables: string[]) => {
|
|
||||||
handleSelectWorkflowVariable(variables)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</FloatingPortal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
return (
|
||||||
}, [
|
<>
|
||||||
allOptions,
|
{
|
||||||
promptOptions,
|
ReactDOM.createPortal(
|
||||||
variableOptions,
|
// The `LexicalMenu` will try to calculate the position of the floating menu based on the first child.
|
||||||
externalToolOptions,
|
// Since we use floating ui, we need to wrap it with a div to prevent the position calculation being affected.
|
||||||
queryString,
|
// See https://github.com/facebook/lexical/blob/ac97dfa9e14a73ea2d6934ff566282d7f758e8bb/packages/lexical-react/src/shared/LexicalMenu.ts#L493
|
||||||
workflowVariableBlock?.show,
|
<div className='w-0 h-0'>
|
||||||
workflowVariableOptions,
|
<div
|
||||||
handleSelectWorkflowVariable,
|
className='p-1 w-[260px] bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg overflow-y-auto overflow-x-hidden'
|
||||||
elements,
|
style={{
|
||||||
floatingStyles,
|
...floatingStyles,
|
||||||
refs,
|
visibility: isPositioned ? 'visible' : 'hidden',
|
||||||
])
|
maxHeight: 'calc(1 / 3 * 100vh)',
|
||||||
|
}}
|
||||||
|
ref={refs.setFloating}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
options.map((option, index) => (
|
||||||
|
<Fragment key={option.key}>
|
||||||
|
{
|
||||||
|
// Divider
|
||||||
|
index !== 0 && options.at(index - 1)?.group !== option.group && (
|
||||||
|
<div className='h-px bg-gray-100 my-1 w-screen -translate-x-1'></div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{option.renderMenuOption({
|
||||||
|
queryString,
|
||||||
|
isSelected: selectedIndex === index,
|
||||||
|
onSelect: () => {
|
||||||
|
selectOptionAndCleanUp(option)
|
||||||
|
},
|
||||||
|
onSetHighlight: () => {
|
||||||
|
setHighlightedIndex(index)
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
</Fragment>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
{
|
||||||
|
workflowVariableBlock?.show && (
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
(!!options.length) && (
|
||||||
|
<div className='h-px bg-gray-100 my-1 w-screen -translate-x-1'></div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<div className='p-1'>
|
||||||
|
<VarReferenceVars
|
||||||
|
hideSearch
|
||||||
|
vars={workflowVariableOptions}
|
||||||
|
onChange={(variables: string[]) => {
|
||||||
|
handleSelectWorkflowVariable(variables)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
anchorElementRef.current,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}, [allFlattenOptions.length, workflowVariableBlock?.show, refs, isPositioned, floatingStyles, queryString, workflowVariableOptions, handleSelectWorkflowVariable])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LexicalTypeaheadMenuPlugin
|
<LexicalTypeaheadMenuPlugin
|
||||||
options={allOptions as any}
|
options={allFlattenOptions}
|
||||||
onQueryChange={setQueryString}
|
onQueryChange={setQueryString}
|
||||||
onSelectOption={onSelectOption}
|
onSelectOption={onSelectOption}
|
||||||
anchorClassName='z-[999999]'
|
// The `translate` class is used to workaround the issue that the `typeahead-menu` menu is not positioned as expected.
|
||||||
|
// See also https://github.com/facebook/lexical/blob/772520509308e8ba7e4a82b6cd1996a78b3298d0/packages/lexical-react/src/shared/LexicalMenu.ts#L498
|
||||||
|
//
|
||||||
|
// We no need the position function of the `LexicalTypeaheadMenuPlugin`,
|
||||||
|
// so the reference anchor should be positioned based on the range of the trigger string, and the menu will be positioned by the floating ui.
|
||||||
|
anchorClassName='z-[999999] translate-y-[calc(-100%-3px)]'
|
||||||
menuRenderFn={renderMenu}
|
menuRenderFn={renderMenu}
|
||||||
triggerFn={checkForTriggerMatch}
|
triggerFn={checkForTriggerMatch}
|
||||||
/>
|
/>
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
import { MenuOption } from '@lexical/react/LexicalTypeaheadMenuPlugin'
|
||||||
|
import { Fragment } from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Corresponds to the `MenuRenderFn` type from `@lexical/react/LexicalTypeaheadMenuPlugin`.
|
||||||
|
*/
|
||||||
|
type MenuOptionRenderProps = {
|
||||||
|
isSelected: boolean
|
||||||
|
onSelect: () => void
|
||||||
|
onSetHighlight: () => void
|
||||||
|
queryString: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PickerBlockMenuOption extends MenuOption {
|
||||||
|
public group?: string
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private data: {
|
||||||
|
key: string
|
||||||
|
group?: string
|
||||||
|
onSelect?: () => void
|
||||||
|
render: (menuRenderProps: MenuOptionRenderProps) => JSX.Element
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
super(data.key)
|
||||||
|
this.group = data.group
|
||||||
|
}
|
||||||
|
|
||||||
|
public onSelectMenuOption = () => this.data.onSelect?.()
|
||||||
|
public renderMenuOption = (menuRenderProps: MenuOptionRenderProps) => <Fragment key={this.data.key}>{this.data.render(menuRenderProps)}</Fragment>
|
||||||
|
}
|
@ -1,37 +0,0 @@
|
|||||||
import { memo } from 'react'
|
|
||||||
import { PromptMenuItem } from './prompt-option'
|
|
||||||
|
|
||||||
type PromptMenuProps = {
|
|
||||||
startIndex: number
|
|
||||||
selectedIndex: number | null
|
|
||||||
options: any[]
|
|
||||||
onClick: (index: number, option: any) => void
|
|
||||||
onMouseEnter: (index: number, option: any) => void
|
|
||||||
}
|
|
||||||
const PromptMenu = ({
|
|
||||||
startIndex,
|
|
||||||
selectedIndex,
|
|
||||||
options,
|
|
||||||
onClick,
|
|
||||||
onMouseEnter,
|
|
||||||
}: PromptMenuProps) => {
|
|
||||||
return (
|
|
||||||
<div className='p-1'>
|
|
||||||
{
|
|
||||||
options.map((option, index: number) => (
|
|
||||||
<PromptMenuItem
|
|
||||||
startIndex={startIndex}
|
|
||||||
index={index}
|
|
||||||
isSelected={selectedIndex === index + startIndex}
|
|
||||||
onClick={onClick}
|
|
||||||
onMouseEnter={onMouseEnter}
|
|
||||||
key={option.key}
|
|
||||||
option={option}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(PromptMenu)
|
|
@ -1,64 +1,44 @@
|
|||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
import { MenuOption } from '@lexical/react/LexicalTypeaheadMenuPlugin'
|
|
||||||
|
|
||||||
export class PromptOption extends MenuOption {
|
|
||||||
title: string
|
|
||||||
icon?: JSX.Element
|
|
||||||
keywords: Array<string>
|
|
||||||
keyboardShortcut?: string
|
|
||||||
onSelect: (queryString: string) => void
|
|
||||||
disabled?: boolean
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
title: string,
|
|
||||||
options: {
|
|
||||||
icon?: JSX.Element
|
|
||||||
keywords?: Array<string>
|
|
||||||
keyboardShortcut?: string
|
|
||||||
onSelect: (queryString: string) => void
|
|
||||||
disabled?: boolean
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
super(title)
|
|
||||||
this.title = title
|
|
||||||
this.keywords = options.keywords || []
|
|
||||||
this.icon = options.icon
|
|
||||||
this.keyboardShortcut = options.keyboardShortcut
|
|
||||||
this.onSelect = options.onSelect.bind(this)
|
|
||||||
this.disabled = options.disabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type PromptMenuItemMenuItemProps = {
|
type PromptMenuItemMenuItemProps = {
|
||||||
startIndex: number
|
icon: JSX.Element
|
||||||
index: number
|
title: string
|
||||||
|
disabled?: boolean
|
||||||
isSelected: boolean
|
isSelected: boolean
|
||||||
onClick: (index: number, option: PromptOption) => void
|
onClick: () => void
|
||||||
onMouseEnter: (index: number, option: PromptOption) => void
|
onMouseEnter: () => void
|
||||||
option: PromptOption
|
setRefElement?: (element: HTMLDivElement) => void
|
||||||
}
|
}
|
||||||
export const PromptMenuItem = memo(({
|
export const PromptMenuItem = memo(({
|
||||||
startIndex,
|
icon,
|
||||||
index,
|
title,
|
||||||
|
disabled,
|
||||||
isSelected,
|
isSelected,
|
||||||
onClick,
|
onClick,
|
||||||
onMouseEnter,
|
onMouseEnter,
|
||||||
option,
|
setRefElement,
|
||||||
}: PromptMenuItemMenuItemProps) => {
|
}: PromptMenuItemMenuItemProps) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={option.key}
|
|
||||||
className={`
|
className={`
|
||||||
flex items-center px-3 h-6 cursor-pointer hover:bg-gray-50 rounded-md
|
flex items-center px-3 h-6 cursor-pointer hover:bg-gray-50 rounded-md
|
||||||
${isSelected && !option.disabled && '!bg-gray-50'}
|
${isSelected && !disabled && '!bg-gray-50'}
|
||||||
${option.disabled ? 'cursor-not-allowed opacity-30' : 'hover:bg-gray-50 cursor-pointer'}
|
${disabled ? 'cursor-not-allowed opacity-30' : 'hover:bg-gray-50 cursor-pointer'}
|
||||||
`}
|
`}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
ref={option.setRefElement}
|
ref={setRefElement}
|
||||||
onMouseEnter={() => onMouseEnter(index + startIndex, option)}
|
onMouseEnter={() => {
|
||||||
onClick={() => onClick(index + startIndex, option)}>
|
if (disabled)
|
||||||
{option.icon}
|
return
|
||||||
<div className='ml-1 text-[13px] text-gray-900'>{option.title}</div>
|
onMouseEnter()
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
if (disabled)
|
||||||
|
return
|
||||||
|
onClick()
|
||||||
|
}}>
|
||||||
|
{icon}
|
||||||
|
<div className='ml-1 text-[13px] text-gray-900'>{title}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -1,40 +0,0 @@
|
|||||||
import { memo } from 'react'
|
|
||||||
import { VariableMenuItem } from './variable-option'
|
|
||||||
|
|
||||||
type VariableMenuProps = {
|
|
||||||
startIndex: number
|
|
||||||
selectedIndex: number | null
|
|
||||||
options: any[]
|
|
||||||
onClick: (index: number, option: any) => void
|
|
||||||
onMouseEnter: (index: number, option: any) => void
|
|
||||||
queryString: string | null
|
|
||||||
}
|
|
||||||
const VariableMenu = ({
|
|
||||||
startIndex,
|
|
||||||
selectedIndex,
|
|
||||||
options,
|
|
||||||
onClick,
|
|
||||||
onMouseEnter,
|
|
||||||
queryString,
|
|
||||||
}: VariableMenuProps) => {
|
|
||||||
return (
|
|
||||||
<div className='p-1'>
|
|
||||||
{
|
|
||||||
options.map((option, index: number) => (
|
|
||||||
<VariableMenuItem
|
|
||||||
startIndex={startIndex}
|
|
||||||
index={index}
|
|
||||||
isSelected={selectedIndex === index + startIndex}
|
|
||||||
onClick={onClick}
|
|
||||||
onMouseEnter={onMouseEnter}
|
|
||||||
key={option.key}
|
|
||||||
option={option}
|
|
||||||
queryString={queryString}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(VariableMenu)
|
|
@ -1,60 +1,32 @@
|
|||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
import { MenuOption } from '@lexical/react/LexicalTypeaheadMenuPlugin'
|
|
||||||
|
|
||||||
export class VariableOption extends MenuOption {
|
type VariableMenuItemProps = {
|
||||||
title: string
|
title: string
|
||||||
icon?: JSX.Element
|
icon?: JSX.Element
|
||||||
extraElement?: JSX.Element
|
extraElement?: JSX.Element
|
||||||
keywords: Array<string>
|
|
||||||
keyboardShortcut?: string
|
|
||||||
onSelect: (queryString: string) => void
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
title: string,
|
|
||||||
options: {
|
|
||||||
icon?: JSX.Element
|
|
||||||
extraElement?: JSX.Element
|
|
||||||
keywords?: Array<string>
|
|
||||||
keyboardShortcut?: string
|
|
||||||
onSelect: (queryString: string) => void
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
super(title)
|
|
||||||
this.title = title
|
|
||||||
this.keywords = options.keywords || []
|
|
||||||
this.icon = options.icon
|
|
||||||
this.extraElement = options.extraElement
|
|
||||||
this.keyboardShortcut = options.keyboardShortcut
|
|
||||||
this.onSelect = options.onSelect.bind(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type VariableMenuItemProps = {
|
|
||||||
startIndex: number
|
|
||||||
index: number
|
|
||||||
isSelected: boolean
|
isSelected: boolean
|
||||||
onClick: (index: number, option: VariableOption) => void
|
|
||||||
onMouseEnter: (index: number, option: VariableOption) => void
|
|
||||||
option: VariableOption
|
|
||||||
queryString: string | null
|
queryString: string | null
|
||||||
|
onClick: () => void
|
||||||
|
onMouseEnter: () => void
|
||||||
|
setRefElement?: (element: HTMLDivElement) => void
|
||||||
}
|
}
|
||||||
export const VariableMenuItem = memo(({
|
export const VariableMenuItem = memo(({
|
||||||
startIndex,
|
title,
|
||||||
index,
|
icon,
|
||||||
|
extraElement,
|
||||||
isSelected,
|
isSelected,
|
||||||
|
queryString,
|
||||||
onClick,
|
onClick,
|
||||||
onMouseEnter,
|
onMouseEnter,
|
||||||
option,
|
setRefElement,
|
||||||
queryString,
|
|
||||||
}: VariableMenuItemProps) => {
|
}: VariableMenuItemProps) => {
|
||||||
const title = option.title
|
|
||||||
let before = title
|
let before = title
|
||||||
let middle = ''
|
let middle = ''
|
||||||
let after = ''
|
let after = ''
|
||||||
|
|
||||||
if (queryString) {
|
if (queryString) {
|
||||||
const regex = new RegExp(queryString, 'i')
|
const regex = new RegExp(queryString, 'i')
|
||||||
const match = regex.exec(option.title)
|
const match = regex.exec(title)
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
before = title.substring(0, match.index)
|
before = title.substring(0, match.index)
|
||||||
@ -65,24 +37,23 @@ export const VariableMenuItem = memo(({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={option.key}
|
|
||||||
className={`
|
className={`
|
||||||
flex items-center px-3 h-6 rounded-md hover:bg-primary-50 cursor-pointer
|
flex items-center px-3 h-6 rounded-md hover:bg-primary-50 cursor-pointer
|
||||||
${isSelected && 'bg-primary-50'}
|
${isSelected && 'bg-primary-50'}
|
||||||
`}
|
`}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
ref={option.setRefElement}
|
ref={setRefElement}
|
||||||
onMouseEnter={() => onMouseEnter(index + startIndex, option)}
|
onMouseEnter={onMouseEnter}
|
||||||
onClick={() => onClick(index + startIndex, option)}>
|
onClick={onClick}>
|
||||||
<div className='mr-2'>
|
<div className='mr-2'>
|
||||||
{option.icon}
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
<div className='grow text-[13px] text-gray-900 truncate' title={option.title}>
|
<div className='grow text-[13px] text-gray-900 truncate' title={title}>
|
||||||
{before}
|
{before}
|
||||||
<span className='text-[#2970FF]'>{middle}</span>
|
<span className='text-[#2970FF]'>{middle}</span>
|
||||||
{after}
|
{after}
|
||||||
</div>
|
</div>
|
||||||
{option.extraElement}
|
{extraElement}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user