From 4d82769baa4639ed20f3599fa407cd201d216340 Mon Sep 17 00:00:00 2001 From: zhsama Date: Sat, 31 Jan 2026 21:26:32 +0800 Subject: [PATCH] fix: Fix null safety issues in workflow variable components --- .../workflow/comment/mention-input.tsx | 76 ++++++++++++------- .../variable/var-reference-picker.tsx | 6 +- .../workflow/nodes/variable-assigner/hooks.ts | 4 +- 3 files changed, 57 insertions(+), 29 deletions(-) diff --git a/web/app/components/workflow/comment/mention-input.tsx b/web/app/components/workflow/comment/mention-input.tsx index ed51feb90d..acd5768ebb 100644 --- a/web/app/components/workflow/comment/mention-input.tsx +++ b/web/app/components/workflow/comment/mention-input.tsx @@ -58,15 +58,35 @@ const MentionInputInner = forwardRef(({ const actionContainerRef = useRef(null) const actionRightRef = useRef(null) const baseTextareaHeightRef = useRef(null) + const mentionTimerRef = useRef(null) + const focusTimerRef = useRef(null) + const layoutRafRef = useRef(null) // Expose textarea ref to parent component useImperativeHandle(forwardedRef, () => textareaRef.current!, []) + useEffect(() => { + return () => { + if (mentionTimerRef.current !== null) { + window.clearTimeout(mentionTimerRef.current) + mentionTimerRef.current = null + } + if (focusTimerRef.current !== null) { + window.clearTimeout(focusTimerRef.current) + focusTimerRef.current = null + } + if (layoutRafRef.current !== null) { + window.cancelAnimationFrame(layoutRafRef.current) + layoutRafRef.current = null + } + } + }, []) + const workflowStore = useWorkflowStore() const mentionUsersFromStore = useStore(state => ( appId ? state.mentionableUsersCache[appId] : undefined )) - const mentionUsers = mentionUsersFromStore ?? [] + const mentionUsers = useMemo(() => mentionUsersFromStore ?? [], [mentionUsersFromStore]) const [showMentionDropdown, setShowMentionDropdown] = useState(false) const [mentionQuery, setMentionQuery] = useState('') @@ -229,6 +249,17 @@ const MentionInputInner = forwardRef(({ setPaddingBottom(prev => (prev === nextBottom ? prev : nextBottom)) }, [shouldReserveButtonGap, shouldReserveHorizontalSpace, paddingRight, paddingBottom]) + const scheduleLayoutSync = useCallback(() => { + if (typeof window === 'undefined') + return + if (layoutRafRef.current !== null) + window.cancelAnimationFrame(layoutRafRef.current) + layoutRafRef.current = window.requestAnimationFrame(() => { + evaluateContentLayout() + syncHighlightScroll() + }) + }, [evaluateContentLayout, syncHighlightScroll]) + const setActionContainerRef = useCallback((node: HTMLDivElement | null) => { actionContainerRef.current = node @@ -326,7 +357,9 @@ const MentionInputInner = forwardRef(({ const handleContentChange = useCallback((newValue: string) => { onChange(newValue) - setTimeout(() => { + if (mentionTimerRef.current !== null) + window.clearTimeout(mentionTimerRef.current) + mentionTimerRef.current = window.setTimeout(() => { const cursorPosition = textareaRef.current?.selectionStart || 0 const textBeforeCursor = newValue.slice(0, cursorPosition) const mentionMatch = textBeforeCursor.match(/@(\w*)$/) @@ -341,14 +374,9 @@ const MentionInputInner = forwardRef(({ setShowMentionDropdown(false) } - if (typeof window !== 'undefined') { - window.requestAnimationFrame(() => { - evaluateContentLayout() - syncHighlightScroll() - }) - } + scheduleLayoutSync() }, 0) - }, [onChange, evaluateContentLayout, syncHighlightScroll]) + }, [onChange, scheduleLayoutSync]) const handleMentionButtonClick = useCallback((e: React.MouseEvent) => { e.preventDefault() @@ -371,7 +399,9 @@ const MentionInputInner = forwardRef(({ onChange(newContent) - setTimeout(() => { + if (mentionTimerRef.current !== null) + window.clearTimeout(mentionTimerRef.current) + mentionTimerRef.current = window.setTimeout(() => { const newCursorPos = cursorPosition + 1 textarea.setSelectionRange(newCursorPos, newCursorPos) textarea.focus() @@ -381,14 +411,9 @@ const MentionInputInner = forwardRef(({ setShowMentionDropdown(true) setSelectedMentionIndex(0) - if (typeof window !== 'undefined') { - window.requestAnimationFrame(() => { - evaluateContentLayout() - syncHighlightScroll() - }) - } + scheduleLayoutSync() }, 0) - }, [value, onChange, evaluateContentLayout, syncHighlightScroll, showMentionDropdown]) + }, [value, onChange, scheduleLayoutSync, showMentionDropdown]) const insertMention = useCallback((user: UserProfile) => { const textarea = textareaRef.current @@ -408,19 +433,16 @@ const MentionInputInner = forwardRef(({ const newMentionedUserIds = [...mentionedUserIds, user.id] setMentionedUserIds(newMentionedUserIds) - setTimeout(() => { + if (mentionTimerRef.current !== null) + window.clearTimeout(mentionTimerRef.current) + mentionTimerRef.current = window.setTimeout(() => { const extraSpace = needsSpaceBefore ? 1 : 0 const newCursorPos = mentionPosition + extraSpace + user.name.length + 2 // (space) + @ + name + space textarea.setSelectionRange(newCursorPos, newCursorPos) textarea.focus() - if (typeof window !== 'undefined') { - window.requestAnimationFrame(() => { - evaluateContentLayout() - syncHighlightScroll() - }) - } + scheduleLayoutSync() }, 0) - }, [value, mentionPosition, onChange, mentionedUserIds, evaluateContentLayout, syncHighlightScroll]) + }, [value, mentionPosition, onChange, mentionedUserIds, scheduleLayoutSync]) const handleSubmit = useCallback(async (e?: React.MouseEvent) => { if (e) { @@ -497,7 +519,9 @@ const MentionInputInner = forwardRef(({ useEffect(() => { if (autoFocus && textareaRef.current) { const textarea = textareaRef.current - setTimeout(() => { + if (focusTimerRef.current !== null) + window.clearTimeout(focusTimerRef.current) + focusTimerRef.current = window.setTimeout(() => { textarea.focus() const length = textarea.value.length textarea.setSelectionRange(length, length) diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx index d4455da743..ca050d2a2c 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx @@ -162,7 +162,7 @@ const VarReferencePicker: FC = ({ const [open, setOpen] = useState(false) useEffect(() => { onOpen() - }, [open]) + }, [open, onOpen]) const hasValue = !isConstant && value.length > 0 const isIterationVar = useMemo(() => { @@ -275,10 +275,12 @@ const VarReferencePicker: FC = ({ return const workflowContainer = document.getElementById('workflow-container') + if (!workflowContainer) + return const { clientWidth, clientHeight, - } = workflowContainer! + } = workflowContainer const { setViewport, } = reactflow diff --git a/web/app/components/workflow/nodes/variable-assigner/hooks.ts b/web/app/components/workflow/nodes/variable-assigner/hooks.ts index 5c2fe36922..85f2a4c487 100644 --- a/web/app/components/workflow/nodes/variable-assigner/hooks.ts +++ b/web/app/components/workflow/nodes/variable-assigner/hooks.ts @@ -31,7 +31,9 @@ export const useVariableAssigner = () => { const handleAssignVariableValueChange = useCallback((nodeId: string, value: ValueSelector, varDetail: Var, groupId?: string) => { const { getNodes } = store.getState() const nodes = getNodes() - const node: Node = nodes.find(node => node.id === nodeId)! + const node = nodes.find(node => node.id === nodeId) as Node | undefined + if (!node) + return let payload if (groupId && groupId !== 'target') {