feat: add scroll to selected node button in workflow header (#24030)

Co-authored-by: zhangxuhe1 <xuhezhang6@gmail.com>
This commit is contained in:
lyzno1 2025-08-16 19:26:44 +08:00 committed by GitHub
parent ae25f90f34
commit f214eeb7b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 60 additions and 87 deletions

View File

@ -17,6 +17,7 @@ import RunAndHistory from './run-and-history'
import EditingTitle from './editing-title'
import EnvButton from './env-button'
import VersionHistoryButton from './version-history-button'
import ScrollToSelectedNodeButton from './scroll-to-selected-node-button'
export type HeaderInNormalProps = {
components?: {
@ -53,10 +54,13 @@ const HeaderInNormal = ({
}, [workflowStore, handleBackupDraft, selectedNode, handleNodeSelect, setShowWorkflowVersionHistoryPanel, setShowEnvPanel, setShowDebugAndPreviewPanel, setShowVariableInspectPanel, setShowChatVariablePanel])
return (
<>
<div className='flex w-full items-center justify-between'>
<div>
<EditingTitle />
</div>
<div>
<ScrollToSelectedNodeButton />
</div>
<div className='flex items-center gap-2'>
{components?.left}
<EnvButton disabled={nodesReadOnly} />
@ -65,7 +69,7 @@ const HeaderInNormal = ({
{components?.middle}
<VersionHistoryButton onClick={onStartRestoring} />
</div>
</>
</div>
)
}

View File

@ -0,0 +1,34 @@
import type { FC } from 'react'
import { useCallback } from 'react'
import { useNodes } from 'reactflow'
import { useTranslation } from 'react-i18next'
import type { CommonNodeType } from '../types'
import { scrollToWorkflowNode } from '../utils/node-navigation'
import cn from '@/utils/classnames'
const ScrollToSelectedNodeButton: FC = () => {
const { t } = useTranslation()
const nodes = useNodes<CommonNodeType>()
const selectedNode = nodes.find(node => node.data.selected)
const handleScrollToSelectedNode = useCallback(() => {
if (!selectedNode) return
scrollToWorkflowNode(selectedNode.id)
}, [selectedNode])
if (!selectedNode)
return null
return (
<div
className={cn(
'system-xs-medium flex h-6 cursor-pointer items-center justify-center whitespace-nowrap rounded-md border-[0.5px] border-effects-highlight bg-components-actionbar-bg px-3 text-text-tertiary shadow-lg backdrop-blur-sm transition-colors duration-200 hover:text-text-accent',
)}
onClick={handleScrollToSelectedNode}
>
{t('workflow.panel.scrollToSelectedNode')}
</div>
)
}
export default ScrollToSelectedNodeButton

View File

@ -1,63 +0,0 @@
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import { useShallow } from 'zustand/react/shallow'
import { RiCrosshairLine } from '@remixicon/react'
import { useReactFlow, useStore } from 'reactflow'
import TooltipPlus from '@/app/components/base/tooltip'
import { useNodesSyncDraft } from '@/app/components/workflow-app/hooks'
type NodePositionProps = {
nodeId: string
}
const NodePosition = ({
nodeId,
}: NodePositionProps) => {
const { t } = useTranslation()
const reactflow = useReactFlow()
const { doSyncWorkflowDraft } = useNodesSyncDraft()
const {
nodePosition,
nodeWidth,
nodeHeight,
} = useStore(useShallow((s) => {
const nodes = s.getNodes()
const currentNode = nodes.find(node => node.id === nodeId)!
return {
nodePosition: currentNode.position,
nodeWidth: currentNode.width,
nodeHeight: currentNode.height,
}
}))
const transform = useStore(s => s.transform)
if (!nodePosition || !nodeWidth || !nodeHeight) return null
const workflowContainer = document.getElementById('workflow-container')
const zoom = transform[2]
const { clientWidth, clientHeight } = workflowContainer!
const { setViewport } = reactflow
return (
<TooltipPlus
popupContent={t('workflow.panel.moveToThisNode')}
>
<div
className='mr-1 flex h-6 w-6 cursor-pointer items-center justify-center rounded-md hover:bg-state-base-hover'
onClick={() => {
setViewport({
x: (clientWidth - 400 - nodeWidth * zoom) / 2 - nodePosition.x * zoom,
y: (clientHeight - nodeHeight * zoom) / 2 - nodePosition.y * zoom,
zoom: transform[2],
})
doSyncWorkflowDraft()
}}
>
<RiCrosshairLine className='h-4 w-4 text-text-tertiary' />
</div>
</TooltipPlus>
)
}
export default memo(NodePosition)

View File

@ -19,7 +19,6 @@ import { useShallow } from 'zustand/react/shallow'
import { useTranslation } from 'react-i18next'
import NextStep from '../next-step'
import PanelOperator from '../panel-operator'
import NodePosition from '@/app/components/workflow/nodes/_base/components/node-position'
import HelpLink from '../help-link'
import {
DescriptionInput,
@ -362,7 +361,6 @@ const BasePanel: FC<BasePanelProps> = ({
</Tooltip>
)
}
<NodePosition nodeId={id}></NodePosition>
<HelpLink nodeType={data.type} />
<PanelOperator id={id} data={data} showHelpLink={false} />
<div className='mx-3 h-3.5 w-[1px] bg-divider-regular' />

View File

@ -315,13 +315,13 @@ const translation = {
checklistResolved: 'Alle Probleme wurden gelöst',
change: 'Ändern',
optional: '(optional)',
moveToThisNode: 'Bewege zu diesem Knoten',
selectNextStep: 'Nächsten Schritt auswählen',
addNextStep: 'Fügen Sie den nächsten Schritt in diesem Arbeitsablauf hinzu.',
organizeBlocks: 'Knoten organisieren',
changeBlock: 'Knoten ändern',
maximize: 'Maximiere die Leinwand',
minimize: 'Vollbildmodus beenden',
scrollToSelectedNode: 'Zum ausgewählten Knoten scrollen',
},
nodes: {
common: {

View File

@ -320,7 +320,6 @@ const translation = {
addNextStep: 'Add the next step in this workflow',
selectNextStep: 'Select Next Step',
runThisStep: 'Run this step',
moveToThisNode: 'Move to this node',
checklist: 'Checklist',
checklistTip: 'Make sure all issues are resolved before publishing',
checklistResolved: 'All issues are resolved',
@ -329,6 +328,7 @@ const translation = {
optional: '(optional)',
maximize: 'Maximize Canvas',
minimize: 'Exit Full Screen',
scrollToSelectedNode: 'Scroll to selected node',
},
nodes: {
common: {

View File

@ -315,13 +315,13 @@ const translation = {
checklistResolved: 'Se resolvieron todos los problemas',
change: 'Cambiar',
optional: '(opcional)',
moveToThisNode: 'Mueve a este nodo',
organizeBlocks: 'Organizar nodos',
addNextStep: 'Agrega el siguiente paso en este flujo de trabajo',
changeBlock: 'Cambiar Nodo',
selectNextStep: 'Seleccionar siguiente paso',
maximize: 'Maximizar Canvas',
minimize: 'Salir de pantalla completa',
scrollToSelectedNode: 'Desplácese hasta el nodo seleccionado',
},
nodes: {
common: {

View File

@ -315,13 +315,13 @@ const translation = {
checklistResolved: 'تمام مسائل حل شده‌اند',
change: 'تغییر',
optional: '(اختیاری)',
moveToThisNode: 'به این گره بروید',
selectNextStep: 'گام بعدی را انتخاب کنید',
changeBlock: 'تغییر گره',
organizeBlocks: 'گره‌ها را سازماندهی کنید',
addNextStep: 'مرحله بعدی را به این فرآیند اضافه کنید',
minimize: 'خروج از حالت تمام صفحه',
maximize: 'بیشینه‌سازی بوم',
scrollToSelectedNode: 'به گره انتخاب شده بروید',
},
nodes: {
common: {

View File

@ -315,13 +315,13 @@ const translation = {
checklistResolved: 'Tous les problèmes ont été résolus',
change: 'Modifier',
optional: '(facultatif)',
moveToThisNode: 'Déplacer vers ce nœud',
organizeBlocks: 'Organiser les nœuds',
addNextStep: 'Ajoutez la prochaine étape dans ce flux de travail',
selectNextStep: 'Sélectionner la prochaine étape',
changeBlock: 'Changer de nœud',
maximize: 'Maximiser le Canvas',
minimize: 'Sortir du mode plein écran',
scrollToSelectedNode: 'Faites défiler jusquau nœud sélectionné',
},
nodes: {
common: {

View File

@ -327,13 +327,13 @@ const translation = {
checklistResolved: 'सभी समस्याएं हल हो गई हैं',
change: 'बदलें',
optional: '(वैकल्पिक)',
moveToThisNode: 'इस नोड पर जाएं',
changeBlock: 'नोड बदलें',
addNextStep: 'इस कार्यप्रवाह में अगला कदम जोड़ें',
selectNextStep: 'अगला कदम चुनें',
organizeBlocks: 'नोड्स का आयोजन करें',
minimize: 'पूर्ण स्क्रीन से बाहर निकलें',
maximize: 'कैनवास का अधिकतम लाभ उठाएँ',
scrollToSelectedNode: 'चुने गए नोड पर स्क्रॉल करें',
},
nodes: {
common: {

View File

@ -330,13 +330,13 @@ const translation = {
checklistResolved: 'Tutti i problemi sono risolti',
change: 'Cambia',
optional: '(opzionale)',
moveToThisNode: 'Sposta a questo nodo',
changeBlock: 'Cambia Nodo',
selectNextStep: 'Seleziona il prossimo passo',
organizeBlocks: 'Organizzare i nodi',
addNextStep: 'Aggiungi il prossimo passo in questo flusso di lavoro',
minimize: 'Esci dalla modalità schermo intero',
maximize: 'Massimizza Canvas',
scrollToSelectedNode: 'Scorri fino al nodo selezionato',
},
nodes: {
common: {

View File

@ -326,9 +326,9 @@ const translation = {
organizeBlocks: 'ノード整理',
change: '変更',
optional: '(任意)',
moveToThisNode: 'このノードに移動する',
maximize: 'キャンバスを最大化する',
minimize: '全画面を終了する',
scrollToSelectedNode: '選択したノードまでスクロール',
},
nodes: {
common: {

View File

@ -336,13 +336,13 @@ const translation = {
checklistResolved: '모든 문제가 해결되었습니다',
change: '변경',
optional: '(선택사항)',
moveToThisNode: '이 노드로 이동',
organizeBlocks: '노드 정리하기',
selectNextStep: '다음 단계 선택',
changeBlock: '노드 변경',
addNextStep: '이 워크플로우에 다음 단계를 추가하세요.',
minimize: '전체 화면 종료',
maximize: '캔버스 전체 화면',
scrollToSelectedNode: '선택한 노드로 스크롤',
},
nodes: {
common: {

View File

@ -315,13 +315,13 @@ const translation = {
checklistResolved: 'Wszystkie problemy zostały rozwiązane',
change: 'Zmień',
optional: '(opcjonalne)',
moveToThisNode: 'Przenieś do tego węzła',
selectNextStep: 'Wybierz następny krok',
addNextStep: 'Dodaj następny krok w tym procesie roboczym',
changeBlock: 'Zmień węzeł',
organizeBlocks: 'Organizuj węzły',
minimize: 'Wyjdź z trybu pełnoekranowego',
maximize: 'Maksymalizuj płótno',
scrollToSelectedNode: 'Przewiń do wybranego węzła',
},
nodes: {
common: {

View File

@ -315,13 +315,13 @@ const translation = {
checklistResolved: 'Todos os problemas foram resolvidos',
change: 'Mudar',
optional: '(opcional)',
moveToThisNode: 'Mova-se para este nó',
changeBlock: 'Mudar Nó',
addNextStep: 'Adicione o próximo passo neste fluxo de trabalho',
organizeBlocks: 'Organizar nós',
selectNextStep: 'Selecione o próximo passo',
maximize: 'Maximize Canvas',
minimize: 'Sair do Modo Tela Cheia',
scrollToSelectedNode: 'Role até o nó selecionado',
},
nodes: {
common: {

View File

@ -315,13 +315,13 @@ const translation = {
checklistResolved: 'Toate problemele au fost rezolvate',
change: 'Schimbă',
optional: '(opțional)',
moveToThisNode: 'Mutați la acest nod',
organizeBlocks: 'Organizează nodurile',
addNextStep: 'Adăugați următorul pas în acest flux de lucru',
changeBlock: 'Schimbă nodul',
selectNextStep: 'Selectați Pasul Următor',
maximize: 'Maximize Canvas',
minimize: 'Iesi din modul pe tot ecranul',
scrollToSelectedNode: 'Derulați la nodul selectat',
},
nodes: {
common: {

View File

@ -315,13 +315,13 @@ const translation = {
checklistResolved: 'Все проблемы решены',
change: 'Изменить',
optional: '(необязательно)',
moveToThisNode: 'Перейдите к этому узлу',
selectNextStep: 'Выберите следующий шаг',
organizeBlocks: 'Организовать узлы',
addNextStep: 'Добавьте следующий шаг в этот рабочий процесс',
changeBlock: 'Изменить узел',
minimize: 'Выйти из полноэкранного режима',
maximize: 'Максимизировать холст',
scrollToSelectedNode: 'Прокрутите до выбранного узла',
},
nodes: {
common: {

View File

@ -318,7 +318,6 @@ const translation = {
runThisStep: 'Izvedi ta korak',
changeBlock: 'Spremeni vozlišče',
addNextStep: 'Dodajte naslednji korak v ta delovni potek',
moveToThisNode: 'Premakni se na to vozlišče',
checklistTip: 'Prepričajte se, da so vse težave rešene, preden objavite.',
selectNextStep: 'Izberi naslednji korak',
helpLink: 'Pomočna povezava',
@ -329,6 +328,7 @@ const translation = {
minimize: 'Izhod iz celotnega zaslona',
maximize: 'Maksimiziraj platno',
optional: '(neobvezno)',
scrollToSelectedNode: 'Pomaknite se do izbranega vozlišča',
},
nodes: {
common: {

View File

@ -315,13 +315,13 @@ const translation = {
checklistResolved: 'ปัญหาทั้งหมดได้รับการแก้ไขแล้ว',
change: 'เปลี่ยน',
optional: '(ไม่บังคับ)',
moveToThisNode: 'ย้ายไปที่โหนดนี้',
organizeBlocks: 'จัดระเบียบโหนด',
addNextStep: 'เพิ่มขั้นตอนถัดไปในกระบวนการทำงานนี้',
changeBlock: 'เปลี่ยนโหนด',
selectNextStep: 'เลือกขั้นตอนถัดไป',
minimize: 'ออกจากโหมดเต็มหน้าจอ',
maximize: 'เพิ่มประสิทธิภาพผ้าใบ',
scrollToSelectedNode: 'เลื่อนไปยังโหนดที่เลือก',
},
nodes: {
common: {

View File

@ -315,13 +315,13 @@ const translation = {
checklistResolved: 'Tüm sorunlar çözüldü',
change: 'Değiştir',
optional: '(isteğe bağlı)',
moveToThisNode: 'Bu düğüme geç',
changeBlock: 'Düğümü Değiştir',
addNextStep: 'Bu iş akışına bir sonraki adımı ekleyin',
organizeBlocks: 'Düğümleri düzenle',
selectNextStep: 'Sonraki Adımı Seç',
minimize: 'Tam Ekrandan Çık',
maximize: 'Kanvası Maksimize Et',
scrollToSelectedNode: 'Seçili düğüme kaydırma',
},
nodes: {
common: {

View File

@ -315,13 +315,13 @@ const translation = {
checklistResolved: 'Всі проблеми вирішені',
change: 'Змінити',
optional: '(необов\'язково)',
moveToThisNode: 'Перемістіть до цього вузла',
organizeBlocks: 'Організуйте вузли',
changeBlock: 'Змінити вузол',
selectNextStep: 'Виберіть наступний крок',
addNextStep: 'Додайте наступний крок у цей робочий процес',
minimize: 'Вийти з повноекранного режиму',
maximize: 'Максимізувати полотно',
scrollToSelectedNode: 'Прокрутіть до вибраного вузла',
},
nodes: {
common: {

View File

@ -315,13 +315,13 @@ const translation = {
checklistResolved: 'Tất cả các vấn đề đã được giải quyết',
change: 'Thay đổi',
optional: '(tùy chọn)',
moveToThisNode: 'Di chuyển đến nút này',
changeBlock: 'Thay đổi Node',
selectNextStep: 'Chọn bước tiếp theo',
organizeBlocks: 'Tổ chức các nút',
addNextStep: 'Thêm bước tiếp theo trong quy trình này',
maximize: 'Tối đa hóa Canvas',
minimize: 'Thoát chế độ toàn màn hình',
scrollToSelectedNode: 'Cuộn đến nút đã chọn',
},
nodes: {
common: {

View File

@ -326,9 +326,9 @@ const translation = {
organizeBlocks: '整理节点',
change: '更改',
optional: '(选填)',
moveToThisNode: '定位至此节点',
maximize: '最大化画布',
minimize: '退出最大化',
scrollToSelectedNode: '滚动至选中节点',
},
nodes: {
common: {

View File

@ -319,9 +319,9 @@ const translation = {
organizeBlocks: '整理節點',
change: '更改',
optional: '(選擇性)',
moveToThisNode: '定位至此節點',
minimize: '退出全螢幕',
maximize: '最大化畫布',
scrollToSelectedNode: '捲動至選取的節點',
},
nodes: {
common: {