From 1b239eb2c7688738517deb9ff6dafaccbe117c70 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 12 Apr 2026 01:37:27 +0800 Subject: [PATCH] fix: improve flow board mind tools --- components/flow-board/drawnix-workspace.tsx | 176 ++++++++++++++++++-- components/home/floating-sidebar-shell.tsx | 12 +- 2 files changed, 170 insertions(+), 18 deletions(-) diff --git a/components/flow-board/drawnix-workspace.tsx b/components/flow-board/drawnix-workspace.tsx index fd2f41141..d1f274712 100644 --- a/components/flow-board/drawnix-workspace.tsx +++ b/components/flow-board/drawnix-workspace.tsx @@ -17,8 +17,10 @@ import { } from "@plait/core"; import { DrawTransforms, MemorizeKey } from "@plait/draw"; import { + canSetAbstract, getSelectedMindElements, type MindElement, + MindTransforms, } from "@plait/mind"; import type { BoardChangeData } from "@plait-board/react-board"; import { @@ -40,6 +42,7 @@ import { FileText, FolderOpen, Flag, + GitMerge, Loader2, type LucideIcon, Plus, @@ -424,23 +427,38 @@ function getFlowBoardElementEmojiNames(element: MindElement) { .filter(Boolean); } +function getExistingFlowBoardMindElements( + board: PlaitBoard, + elements: MindElement[] +) { + return elements.filter((element) => { + try { + PlaitBoard.findPath(board, element); + return true; + } catch (_error) { + return false; + } + }); +} + function getFlowBoardMarkerSelectionState( - board: PlaitBoard | null + board: PlaitBoard | null, + selectedElements?: MindElement[] ): FlowBoardMarkerSelectionState { if (!board) { return { count: 0, names: [] }; } - const selectedElements = getSelectedMindElements(board); + const targetElements = selectedElements || getSelectedMindElements(board); const names = new Set(); - selectedElements.forEach((element) => { + targetElements.forEach((element) => { getFlowBoardElementEmojiNames(element).forEach((name) => { names.add(name); }); }); return { - count: selectedElements.length, + count: targetElements.length, names: Array.from(names), }; } @@ -496,6 +514,29 @@ function getFlowBoardPastePoint(board: PlaitBoard): Point { ); } +function isFlowBoardEditablePasteTarget(target: HTMLElement | null) { + if (!target) { + return false; + } + + return Boolean( + target.isContentEditable || + target.closest( + [ + "input", + "textarea", + "select", + "[contenteditable='true']", + "[role='textbox']", + "[data-slate-editor='true']", + ".slate-editable-container", + ".plait-text-container", + ".ttd-dialog-input", + ].join(", ") + ) + ); +} + function getFlowBoardImageDimensions(file: File) { return new Promise<{ width: number; height: number }>((resolve) => { const url = URL.createObjectURL(file); @@ -557,6 +598,7 @@ export function DrawnixWorkspace({ const saveTimerRef = useRef | null>(null); const boardRef = useRef(null); const mindMapFileInputRef = useRef(null); + const selectedMindElementsRef = useRef([]); const [isMarkerPanelOpen, setIsMarkerPanelOpen] = useState(false); const [isImportingMindMap, setIsImportingMindMap] = useState(false); const [markerSelectionState, setMarkerSelectionState] = @@ -572,14 +614,70 @@ export function DrawnixWorkspace({ return siteLogo || siteLogoDark; }, [resolvedTheme, siteLogo, siteLogoDark]); - const refreshMarkerSelectionState = useCallback(() => { - setMarkerSelectionState(getFlowBoardMarkerSelectionState(boardRef.current)); + const getActiveMarkerElements = useCallback(() => { + const board = boardRef.current; + if (!board) { + selectedMindElementsRef.current = []; + return [] as MindElement[]; + } + + const selectedElements = getSelectedMindElements(board); + if (selectedElements.length > 0) { + selectedMindElementsRef.current = selectedElements; + return selectedElements; + } + + const fallbackElements = getExistingFlowBoardMindElements( + board, + selectedMindElementsRef.current + ); + selectedMindElementsRef.current = fallbackElements; + return fallbackElements; }, []); + const refreshMarkerSelectionState = useCallback( + (options: { preserveFallback?: boolean } = {}) => { + const board = boardRef.current; + if (!board) { + selectedMindElementsRef.current = []; + setMarkerSelectionState({ count: 0, names: [] }); + return; + } + + const selectedElements = getSelectedMindElements(board); + if (selectedElements.length > 0) { + selectedMindElementsRef.current = selectedElements; + setMarkerSelectionState( + getFlowBoardMarkerSelectionState(board, selectedElements) + ); + return; + } + + if (!options.preserveFallback) { + selectedMindElementsRef.current = []; + setMarkerSelectionState({ count: 0, names: [] }); + return; + } + + const fallbackElements = getExistingFlowBoardMindElements( + board, + selectedMindElementsRef.current + ); + selectedMindElementsRef.current = fallbackElements; + setMarkerSelectionState( + getFlowBoardMarkerSelectionState(board, fallbackElements) + ); + }, + [] + ); + const handleDrawnixInit = useCallback((board: PlaitBoard) => { boardRef.current = board; seedFlowBoardDefaultTextColor(); - setMarkerSelectionState(getFlowBoardMarkerSelectionState(board)); + selectedMindElementsRef.current = getSelectedMindElements(board); + setMarkerSelectionState( + getFlowBoardMarkerSelectionState(board, selectedMindElementsRef.current) + ); }, []); useEffect(() => { @@ -644,6 +742,7 @@ export function DrawnixWorkspace({ : null ); setMarkerSelectionState({ count: 0, names: [] }); + selectedMindElementsRef.current = []; updateBrowserFileParam(file.id); }, [updateBrowserFileParam] @@ -736,6 +835,7 @@ export function DrawnixWorkspace({ setCurrentTitle("流程白板"); setLastSavedAt(null); setMarkerSelectionState({ count: 0, names: [] }); + selectedMindElementsRef.current = []; updateBrowserFileParam(null); } return; @@ -849,7 +949,7 @@ export function DrawnixWorkspace({ return; } - const selectedElements = getSelectedMindElements(board); + const selectedElements = getActiveMarkerElements(); if (selectedElements.length === 0) { toast.info("请先选中一个思维导图节点"); return; @@ -858,9 +958,9 @@ export function DrawnixWorkspace({ selectedElements.forEach((element) => { updateFlowBoardElementMarker(board, element, group, marker); }); - refreshMarkerSelectionState(); + refreshMarkerSelectionState({ preserveFallback: true }); }, - [refreshMarkerSelectionState] + [getActiveMarkerElements, refreshMarkerSelectionState] ); const handleClearFlowBoardMarkers = useCallback(() => { @@ -869,7 +969,7 @@ export function DrawnixWorkspace({ return; } - const selectedElements = getSelectedMindElements(board); + const selectedElements = getActiveMarkerElements(); if (selectedElements.length === 0) { toast.info("请先选中一个思维导图节点"); return; @@ -904,9 +1004,31 @@ export function DrawnixWorkspace({ PlaitBoard.findPath(board, element) ); }); - refreshMarkerSelectionState(); + refreshMarkerSelectionState({ preserveFallback: true }); toast.success("已清空所选节点标记"); - }, [refreshMarkerSelectionState]); + }, [getActiveMarkerElements, refreshMarkerSelectionState]); + + const handleInsertFlowBoardAbstract = useCallback(() => { + const board = boardRef.current; + if (!board) { + return; + } + + const selectedElements = getActiveMarkerElements().filter(canSetAbstract); + if (selectedElements.length === 0) { + toast.info("请先选中要汇总的思维导图节点"); + return; + } + + try { + MindTransforms.insertAbstract(board, selectedElements); + refreshMarkerSelectionState({ preserveFallback: true }); + toast.success("已添加合并汇总"); + } catch (error) { + console.error("[FlowBoard] Failed to insert abstract:", error); + toast.error("合并汇总失败,请选择同一组思维导图分支节点"); + } + }, [getActiveMarkerElements, refreshMarkerSelectionState]); const handleFlowBoardPastedImages = useCallback(async ( board: PlaitBoard, @@ -967,7 +1089,10 @@ export function DrawnixWorkspace({ } const target = event.target instanceof HTMLElement ? event.target : null; - if (target?.closest("[data-flow-board-shell-control='true']")) { + if ( + target?.closest("[data-flow-board-shell-control='true']") || + isFlowBoardEditablePasteTarget(target) + ) { return; } @@ -1305,6 +1430,7 @@ export function DrawnixWorkspace({ setCurrentTitle("流程白板"); setLastSavedAt(null); setMarkerSelectionState({ count: 0, names: [] }); + selectedMindElementsRef.current = []; updateBrowserFileParam(null); } catch (error) { console.error("[FlowBoard] Failed to delete cloud file:", error); @@ -1376,10 +1502,10 @@ export function DrawnixWorkspace({ - + { setIsFilePanelOpen(false); setIsMarkerPanelOpen((open) => !open); - refreshMarkerSelectionState(); + refreshMarkerSelectionState({ preserveFallback: true }); }} /> + { + event.stopPropagation(); + }} >
@@ -1585,6 +1719,10 @@ export function DrawnixWorkspace({ : "border-black/10" }`} key={`${group.id}-${marker.name}`} + onMouseDown={(event) => { + event.preventDefault(); + event.stopPropagation(); + }} onClick={() => handleApplyFlowBoardMarker(group, marker) } @@ -1605,6 +1743,10 @@ export function DrawnixWorkspace({