From 30cc00bed281fac0e7747530358b874b8d6451cc Mon Sep 17 00:00:00 2001 From: chanx <1243304602@qq.com> Date: Wed, 15 Jul 2026 14:33:36 +0800 Subject: [PATCH] feat(dataflow-result): scroll to and highlight newly created chunk (#16937) --- .../components/parse-editer/index.tsx | 2 + .../components/parse-editer/interface.ts | 2 + .../components/parse-editer/json-parser.tsx | 39 ++++++++++++++++++- web/src/pages/dataflow-result/parser.tsx | 9 +++++ 4 files changed, 50 insertions(+), 2 deletions(-) diff --git a/web/src/pages/dataflow-result/components/parse-editer/index.tsx b/web/src/pages/dataflow-result/components/parse-editer/index.tsx index b9754002bc..cd5bdba0cd 100644 --- a/web/src/pages/dataflow-result/components/parse-editer/index.tsx +++ b/web/src/pages/dataflow-result/components/parse-editer/index.tsx @@ -13,6 +13,7 @@ const FormatPreserveEditor = ({ textMode, clickChunk, isReadonly, + newChunkIndex, }: FormatPreserveEditorProps) => { console.log('initialValue', initialValue); @@ -41,6 +42,7 @@ const FormatPreserveEditor = ({ textMode={textMode} isChunck={isChunck} clickChunk={clickChunk} + newChunkIndex={newChunkIndex} /> )} diff --git a/web/src/pages/dataflow-result/components/parse-editer/interface.ts b/web/src/pages/dataflow-result/components/parse-editer/interface.ts index af527bc00b..c24893b2fe 100644 --- a/web/src/pages/dataflow-result/components/parse-editer/interface.ts +++ b/web/src/pages/dataflow-result/components/parse-editer/interface.ts @@ -20,6 +20,7 @@ export interface FormatPreserveEditorProps { textMode?: ChunkTextMode; clickChunk: (chunk: IChunk) => void; isReadonly: boolean; + newChunkIndex?: number; } export type IJsonContainerProps = { @@ -47,6 +48,7 @@ export type IJsonContainerProps = { textMode?: ChunkTextMode; clickChunk: (chunk: IChunk) => void; isReadonly: boolean; + newChunkIndex?: number; }; export type IObjContainerProps = { diff --git a/web/src/pages/dataflow-result/components/parse-editer/json-parser.tsx b/web/src/pages/dataflow-result/components/parse-editer/json-parser.tsx index 92f79bfc0c..e93081e974 100644 --- a/web/src/pages/dataflow-result/components/parse-editer/json-parser.tsx +++ b/web/src/pages/dataflow-result/components/parse-editer/json-parser.tsx @@ -1,7 +1,7 @@ import { Checkbox } from '@/components/ui/checkbox'; import { cn } from '@/lib/utils'; import { isArray } from 'lodash'; -import { useCallback, useEffect, useMemo } from 'react'; +import { useCallback, useEffect, useMemo, useRef } from 'react'; import { ChunkTextMode } from '../../constant'; import styles from '../../index.module.less'; import { IChunk } from '../../interface'; @@ -23,11 +23,14 @@ export const ArrayContainer = (props: IJsonContainerProps) => { textMode, clickChunk, isReadonly, + newChunkIndex, } = props; const { content, activeEditIndex, setActiveEditIndex, editDivRef } = useParserInit({ initialValue }); + const sectionRefs = useRef<(HTMLElement | null)[]>([]); + const parserKey = useMemo(() => { const key = content.key === 'chunks' && content.params.field_name @@ -76,6 +79,32 @@ export const ArrayContainer = (props: IJsonContainerProps) => { } }, [editDivRef, activeEditIndex, content, parserKey]); + useEffect(() => { + if (newChunkIndex === undefined) return; + const target = sectionRefs.current[newChunkIndex]; + if (!target) return; + + // Scroll only the nearest scrollable ancestor so the whole page doesn't shift + let scrollContainer: HTMLElement | null = target.parentElement; + while (scrollContainer) { + const { overflowY } = window.getComputedStyle(scrollContainer); + if (overflowY === 'auto' || overflowY === 'scroll') break; + scrollContainer = scrollContainer.parentElement; + } + + if (scrollContainer) { + const containerRect = scrollContainer.getBoundingClientRect(); + const targetRect = target.getBoundingClientRect(); + const offsetTop = + targetRect.top - containerRect.top + scrollContainer.scrollTop; + const targetCenter = offsetTop + target.clientHeight / 2; + scrollContainer.scrollTo({ + top: Math.max(0, targetCenter - scrollContainer.clientHeight / 2), + behavior: 'smooth', + }); + } + }, [newChunkIndex, content]); + return ( <> {isArray(content.value) && @@ -90,11 +119,17 @@ export const ArrayContainer = (props: IJsonContainerProps) => { return (
{ + sectionRefs.current[index] = el; + }} className={cn( isChunck - ? 'bg-bg-card my-2 p-2 rounded-lg flex gap-1 items-start' + ? 'bg-bg-card my-2 p-2 rounded-lg flex gap-1 items-start transition-shadow duration-300' : '', activeEditIndex === index && isChunck ? 'bg-bg-title' : '', + newChunkIndex === index && isChunck + ? 'ring-2 ring-accent-primary' + : '', )} > {isChunck && !isReadonly && ( diff --git a/web/src/pages/dataflow-result/parser.tsx b/web/src/pages/dataflow-result/parser.tsx index efa2bbc843..4995ea9e0e 100644 --- a/web/src/pages/dataflow-result/parser.tsx +++ b/web/src/pages/dataflow-result/parser.tsx @@ -36,6 +36,7 @@ const ParserContainer = (props: IProps) => { } = props; const { t } = useTranslation(); const [selectedChunkIds, setSelectedChunkIds] = useState([]); + const [newChunkIndex, setNewChunkIndex] = useState(); const { changeChunkTextMode, textMode } = useChangeChunkTextMode(); const initialValue = useMemo(() => { const outputs = data?.value?.obj?.params?.outputs; @@ -136,6 +137,7 @@ const ParserContainer = (props: IProps) => { const handleCreateChunk = useCallback( (text: string) => { const newText = [...initialText.value, { text: text || ' ' }]; + setNewChunkIndex(newText.length - 1); setInitialText({ ...initialText, value: newText as any, @@ -144,6 +146,12 @@ const ParserContainer = (props: IProps) => { [initialText], ); + useEffect(() => { + if (newChunkIndex === undefined) return; + const timer = setTimeout(() => setNewChunkIndex(undefined), 3000); + return () => clearTimeout(timer); + }, [newChunkIndex]); + return ( <> {isChange && !isReadonly && ( @@ -220,6 +228,7 @@ const ParserContainer = (props: IProps) => { clickChunk={clickChunk} handleCheckboxClick={handleCheckboxClick} selectedChunkIds={selectedChunkIds} + newChunkIndex={newChunkIndex} /> )}