| 
									
										
										
										
											2024-04-08 18:51:46 +08:00
										 |  |  | import { | 
					
						
							| 
									
										
										
										
											2024-07-08 21:56:09 +08:00
										 |  |  |   Fragment, | 
					
						
							| 
									
										
										
										
											2024-04-08 18:51:46 +08:00
										 |  |  |   memo, | 
					
						
							|  |  |  |   useCallback, | 
					
						
							|  |  |  |   useState, | 
					
						
							|  |  |  | } from 'react' | 
					
						
							|  |  |  | import ReactDOM from 'react-dom' | 
					
						
							|  |  |  | import { | 
					
						
							|  |  |  |   flip, | 
					
						
							|  |  |  |   offset, | 
					
						
							|  |  |  |   shift, | 
					
						
							|  |  |  |   useFloating, | 
					
						
							|  |  |  | } from '@floating-ui/react' | 
					
						
							|  |  |  | import type { TextNode } from 'lexical' | 
					
						
							|  |  |  | import type { MenuRenderFn } from '@lexical/react/LexicalTypeaheadMenuPlugin' | 
					
						
							|  |  |  | import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' | 
					
						
							|  |  |  | import { LexicalTypeaheadMenuPlugin } from '@lexical/react/LexicalTypeaheadMenuPlugin' | 
					
						
							|  |  |  | import type { | 
					
						
							|  |  |  |   ContextBlockType, | 
					
						
							|  |  |  |   ExternalToolBlockType, | 
					
						
							|  |  |  |   HistoryBlockType, | 
					
						
							|  |  |  |   QueryBlockType, | 
					
						
							|  |  |  |   VariableBlockType, | 
					
						
							|  |  |  |   WorkflowVariableBlockType, | 
					
						
							|  |  |  | } from '../../types' | 
					
						
							|  |  |  | import { useBasicTypeaheadTriggerMatch } from '../../hooks' | 
					
						
							|  |  |  | import { INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND } from '../workflow-variable-block' | 
					
						
							|  |  |  | import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from '../variable-block' | 
					
						
							|  |  |  | import { $splitNodeContainingQuery } from '../../utils' | 
					
						
							|  |  |  | import { useOptions } from './hooks' | 
					
						
							| 
									
										
										
										
											2024-07-08 21:56:09 +08:00
										 |  |  | import type { PickerBlockMenuOption } from './menu' | 
					
						
							| 
									
										
										
										
											2024-04-08 18:51:46 +08:00
										 |  |  | import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars' | 
					
						
							|  |  |  | import { useEventEmitterContextContext } from '@/context/event-emitter' | 
					
						
							| 
									
										
										
										
											2025-04-22 11:07:18 +08:00
										 |  |  | import { KEY_ESCAPE_COMMAND } from 'lexical' | 
					
						
							| 
									
										
										
										
											2024-04-08 18:51:46 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | type ComponentPickerProps = { | 
					
						
							|  |  |  |   triggerString: string | 
					
						
							|  |  |  |   contextBlock?: ContextBlockType | 
					
						
							|  |  |  |   queryBlock?: QueryBlockType | 
					
						
							|  |  |  |   historyBlock?: HistoryBlockType | 
					
						
							|  |  |  |   variableBlock?: VariableBlockType | 
					
						
							|  |  |  |   externalToolBlock?: ExternalToolBlockType | 
					
						
							|  |  |  |   workflowVariableBlock?: WorkflowVariableBlockType | 
					
						
							| 
									
										
										
										
											2024-10-21 10:32:37 +08:00
										 |  |  |   isSupportFileVar?: boolean | 
					
						
							| 
									
										
										
										
											2024-04-08 18:51:46 +08:00
										 |  |  | } | 
					
						
							|  |  |  | const ComponentPicker = ({ | 
					
						
							|  |  |  |   triggerString, | 
					
						
							|  |  |  |   contextBlock, | 
					
						
							|  |  |  |   queryBlock, | 
					
						
							|  |  |  |   historyBlock, | 
					
						
							|  |  |  |   variableBlock, | 
					
						
							|  |  |  |   externalToolBlock, | 
					
						
							|  |  |  |   workflowVariableBlock, | 
					
						
							| 
									
										
										
										
											2024-10-21 10:32:37 +08:00
										 |  |  |   isSupportFileVar, | 
					
						
							| 
									
										
										
										
											2024-04-08 18:51:46 +08:00
										 |  |  | }: ComponentPickerProps) => { | 
					
						
							|  |  |  |   const { eventEmitter } = useEventEmitterContextContext() | 
					
						
							| 
									
										
										
										
											2024-07-08 21:56:09 +08:00
										 |  |  |   const { refs, floatingStyles, isPositioned } = useFloating({ | 
					
						
							| 
									
										
										
										
											2024-04-08 18:51:46 +08:00
										 |  |  |     placement: 'bottom-start', | 
					
						
							|  |  |  |     middleware: [ | 
					
						
							| 
									
										
										
										
											2024-06-20 11:19:43 +08:00
										 |  |  |       offset(0), // fix hide cursor
 | 
					
						
							| 
									
										
										
										
											2024-07-08 21:56:09 +08:00
										 |  |  |       shift({ | 
					
						
							|  |  |  |         padding: 8, | 
					
						
							|  |  |  |       }), | 
					
						
							| 
									
										
										
										
											2024-04-08 18:51:46 +08:00
										 |  |  |       flip(), | 
					
						
							|  |  |  |     ], | 
					
						
							|  |  |  |   }) | 
					
						
							|  |  |  |   const [editor] = useLexicalComposerContext() | 
					
						
							|  |  |  |   const checkForTriggerMatch = useBasicTypeaheadTriggerMatch(triggerString, { | 
					
						
							|  |  |  |     minLength: 0, | 
					
						
							|  |  |  |     maxLength: 0, | 
					
						
							|  |  |  |   }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const [queryString, setQueryString] = useState<string | null>(null) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   eventEmitter?.useSubscription((v: any) => { | 
					
						
							|  |  |  |     if (v.type === INSERT_VARIABLE_VALUE_BLOCK_COMMAND) | 
					
						
							|  |  |  |       editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${v.payload}}}`) | 
					
						
							|  |  |  |   }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const { | 
					
						
							| 
									
										
										
										
											2024-07-08 21:56:09 +08:00
										 |  |  |     allFlattenOptions, | 
					
						
							| 
									
										
										
										
											2024-04-08 18:51:46 +08:00
										 |  |  |     workflowVariableOptions, | 
					
						
							|  |  |  |   } = useOptions( | 
					
						
							|  |  |  |     contextBlock, | 
					
						
							|  |  |  |     queryBlock, | 
					
						
							|  |  |  |     historyBlock, | 
					
						
							|  |  |  |     variableBlock, | 
					
						
							|  |  |  |     externalToolBlock, | 
					
						
							|  |  |  |     workflowVariableBlock, | 
					
						
							|  |  |  |   ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const onSelectOption = useCallback( | 
					
						
							|  |  |  |     ( | 
					
						
							| 
									
										
										
										
											2024-07-08 21:56:09 +08:00
										 |  |  |       selectedOption: PickerBlockMenuOption, | 
					
						
							| 
									
										
										
										
											2024-04-08 18:51:46 +08:00
										 |  |  |       nodeToRemove: TextNode | null, | 
					
						
							|  |  |  |       closeMenu: () => void, | 
					
						
							|  |  |  |     ) => { | 
					
						
							|  |  |  |       editor.update(() => { | 
					
						
							|  |  |  |         if (nodeToRemove && selectedOption?.key) | 
					
						
							|  |  |  |           nodeToRemove.remove() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-08 21:56:09 +08:00
										 |  |  |         selectedOption.onSelectMenuOption() | 
					
						
							| 
									
										
										
										
											2024-04-08 18:51:46 +08:00
										 |  |  |         closeMenu() | 
					
						
							|  |  |  |       }) | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     [editor], | 
					
						
							|  |  |  |   ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const handleSelectWorkflowVariable = useCallback((variables: string[]) => { | 
					
						
							|  |  |  |     editor.update(() => { | 
					
						
							|  |  |  |       const needRemove = $splitNodeContainingQuery(checkForTriggerMatch(triggerString, editor)!) | 
					
						
							|  |  |  |       if (needRemove) | 
					
						
							|  |  |  |         needRemove.remove() | 
					
						
							|  |  |  |     }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (variables[1] === 'sys.query' || variables[1] === 'sys.files') | 
					
						
							|  |  |  |       editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, [variables[1]]) | 
					
						
							|  |  |  |     else | 
					
						
							|  |  |  |       editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, variables) | 
					
						
							|  |  |  |   }, [editor, checkForTriggerMatch, triggerString]) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-22 11:07:18 +08:00
										 |  |  |   const handleClose = useCallback(() => { | 
					
						
							| 
									
										
										
										
											2025-05-06 09:48:53 +08:00
										 |  |  |     const escapeEvent = new KeyboardEvent('keydown', { key: 'Escape' }) | 
					
						
							|  |  |  |     editor.dispatchCommand(KEY_ESCAPE_COMMAND, escapeEvent) | 
					
						
							| 
									
										
										
										
											2025-04-22 11:07:18 +08:00
										 |  |  |   }, [editor]) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-08 21:56:09 +08:00
										 |  |  |   const renderMenu = useCallback<MenuRenderFn<PickerBlockMenuOption>>(( | 
					
						
							| 
									
										
										
										
											2024-04-08 18:51:46 +08:00
										 |  |  |     anchorElementRef, | 
					
						
							| 
									
										
										
										
											2024-07-08 21:56:09 +08:00
										 |  |  |     { options, selectedIndex, selectOptionAndCleanUp, setHighlightedIndex }, | 
					
						
							| 
									
										
										
										
											2024-04-08 18:51:46 +08:00
										 |  |  |   ) => { | 
					
						
							| 
									
										
										
										
											2024-07-08 21:56:09 +08:00
										 |  |  |     if (!(anchorElementRef.current && (allFlattenOptions.length || workflowVariableBlock?.show))) | 
					
						
							|  |  |  |       return null | 
					
						
							| 
									
										
										
										
											2025-05-06 09:48:53 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     setTimeout(() => { | 
					
						
							|  |  |  |       if (anchorElementRef.current) | 
					
						
							|  |  |  |         refs.setReference(anchorElementRef.current) | 
					
						
							|  |  |  |     }, 0) | 
					
						
							| 
									
										
										
										
											2024-07-08 21:56:09 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     return ( | 
					
						
							|  |  |  |       <> | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           ReactDOM.createPortal( | 
					
						
							|  |  |  |             // The `LexicalMenu` will try to calculate the position of the floating menu based on the first child.
 | 
					
						
							|  |  |  |             // Since we use floating ui, we need to wrap it with a div to prevent the position calculation being affected.
 | 
					
						
							|  |  |  |             // See https://github.com/facebook/lexical/blob/ac97dfa9e14a73ea2d6934ff566282d7f758e8bb/packages/lexical-react/src/shared/LexicalMenu.ts#L493
 | 
					
						
							| 
									
										
										
										
											2025-03-21 17:41:03 +08:00
										 |  |  |             <div className='h-0 w-0'> | 
					
						
							| 
									
										
										
										
											2024-07-08 21:56:09 +08:00
										 |  |  |               <div | 
					
						
							| 
									
										
										
										
											2025-03-21 17:41:03 +08:00
										 |  |  |                 className='w-[260px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg' | 
					
						
							| 
									
										
										
										
											2024-07-08 21:56:09 +08:00
										 |  |  |                 style={{ | 
					
						
							|  |  |  |                   ...floatingStyles, | 
					
						
							|  |  |  |                   visibility: isPositioned ? 'visible' : 'hidden', | 
					
						
							|  |  |  |                 }} | 
					
						
							|  |  |  |                 ref={refs.setFloating} | 
					
						
							|  |  |  |               > | 
					
						
							|  |  |  |                 { | 
					
						
							| 
									
										
										
										
											2025-04-22 11:07:18 +08:00
										 |  |  |                   workflowVariableBlock?.show && ( | 
					
						
							|  |  |  |                     <div className='p-1'> | 
					
						
							|  |  |  |                       <VarReferenceVars | 
					
						
							|  |  |  |                         searchBoxClassName='mt-1' | 
					
						
							|  |  |  |                         vars={workflowVariableOptions} | 
					
						
							|  |  |  |                         onChange={(variables: string[]) => { | 
					
						
							|  |  |  |                           handleSelectWorkflowVariable(variables) | 
					
						
							|  |  |  |                         }} | 
					
						
							|  |  |  |                         maxHeightClass='max-h-[34vh]' | 
					
						
							|  |  |  |                         isSupportFileVar={isSupportFileVar} | 
					
						
							|  |  |  |                         onClose={handleClose} | 
					
						
							|  |  |  |                         onBlur={handleClose} | 
					
						
							|  |  |  |                       /> | 
					
						
							|  |  |  |                     </div> | 
					
						
							|  |  |  |                   ) | 
					
						
							| 
									
										
										
										
											2024-07-08 21:56:09 +08:00
										 |  |  |                 } | 
					
						
							|  |  |  |                 { | 
					
						
							| 
									
										
										
										
											2025-04-22 11:07:18 +08:00
										 |  |  |                   workflowVariableBlock?.show && !!options.length && ( | 
					
						
							|  |  |  |                     <div className='my-1 h-px w-full -translate-x-1 bg-divider-subtle'></div> | 
					
						
							| 
									
										
										
										
											2024-07-08 21:56:09 +08:00
										 |  |  |                   ) | 
					
						
							|  |  |  |                 } | 
					
						
							| 
									
										
										
										
											2025-05-06 09:48:53 +08:00
										 |  |  |                 <div> | 
					
						
							| 
									
										
										
										
											2025-04-22 11:07:18 +08:00
										 |  |  |                   { | 
					
						
							|  |  |  |                     options.map((option, index) => ( | 
					
						
							|  |  |  |                       <Fragment key={option.key}> | 
					
						
							|  |  |  |                         { | 
					
						
							|  |  |  |                           // Divider
 | 
					
						
							|  |  |  |                           index !== 0 && options.at(index - 1)?.group !== option.group && ( | 
					
						
							|  |  |  |                             <div className='my-1 h-px w-full -translate-x-1 bg-divider-subtle'></div> | 
					
						
							|  |  |  |                           ) | 
					
						
							|  |  |  |                         } | 
					
						
							|  |  |  |                         {option.renderMenuOption({ | 
					
						
							|  |  |  |                           queryString, | 
					
						
							|  |  |  |                           isSelected: selectedIndex === index, | 
					
						
							|  |  |  |                           onSelect: () => { | 
					
						
							|  |  |  |                             selectOptionAndCleanUp(option) | 
					
						
							|  |  |  |                           }, | 
					
						
							|  |  |  |                           onSetHighlight: () => { | 
					
						
							|  |  |  |                             setHighlightedIndex(index) | 
					
						
							|  |  |  |                           }, | 
					
						
							|  |  |  |                         })} | 
					
						
							|  |  |  |                       </Fragment> | 
					
						
							|  |  |  |                     )) | 
					
						
							|  |  |  |                   } | 
					
						
							|  |  |  |                 </div> | 
					
						
							| 
									
										
										
										
											2024-07-08 21:56:09 +08:00
										 |  |  |               </div> | 
					
						
							|  |  |  |             </div>, | 
					
						
							|  |  |  |             anchorElementRef.current, | 
					
						
							|  |  |  |           ) | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       </> | 
					
						
							|  |  |  |     ) | 
					
						
							| 
									
										
										
										
											2025-04-22 11:07:18 +08:00
										 |  |  |   }, [allFlattenOptions.length, workflowVariableBlock?.show, refs, isPositioned, floatingStyles, queryString, workflowVariableOptions, handleSelectWorkflowVariable, handleClose, isSupportFileVar]) | 
					
						
							| 
									
										
										
										
											2024-04-08 18:51:46 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |   return ( | 
					
						
							|  |  |  |     <LexicalTypeaheadMenuPlugin | 
					
						
							| 
									
										
										
										
											2024-07-08 21:56:09 +08:00
										 |  |  |       options={allFlattenOptions} | 
					
						
							| 
									
										
										
										
											2024-04-08 18:51:46 +08:00
										 |  |  |       onQueryChange={setQueryString} | 
					
						
							|  |  |  |       onSelectOption={onSelectOption} | 
					
						
							| 
									
										
										
										
											2024-07-08 21:56:09 +08:00
										 |  |  |       // 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)]' | 
					
						
							| 
									
										
										
										
											2024-04-08 18:51:46 +08:00
										 |  |  |       menuRenderFn={renderMenu} | 
					
						
							|  |  |  |       triggerFn={checkForTriggerMatch} | 
					
						
							|  |  |  |     /> | 
					
						
							|  |  |  |   ) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export default memo(ComponentPicker) |