fix: improve flow board mind tools
This commit is contained in:
parent
9a562db15a
commit
1b239eb2c7
@ -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<string>();
|
||||
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<ReturnType<typeof setTimeout> | null>(null);
|
||||
const boardRef = useRef<PlaitBoard | null>(null);
|
||||
const mindMapFileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const selectedMindElementsRef = useRef<MindElement[]>([]);
|
||||
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({
|
||||
</FloatingSidebarShell>
|
||||
|
||||
<FloatingSidebarShell
|
||||
className="absolute left-6 top-[92px] z-20"
|
||||
className="absolute top-9 right-[180px] z-20"
|
||||
flowBoardControl
|
||||
>
|
||||
<FloatingSidebarRail className="max-h-[calc(100vh-172px)]">
|
||||
<FloatingSidebarRail orientation="horizontal">
|
||||
<FloatingSidebarAction
|
||||
disabled={isSavingNow}
|
||||
icon={Save}
|
||||
@ -1406,9 +1532,14 @@ export function DrawnixWorkspace({
|
||||
onClick={() => {
|
||||
setIsFilePanelOpen(false);
|
||||
setIsMarkerPanelOpen((open) => !open);
|
||||
refreshMarkerSelectionState();
|
||||
refreshMarkerSelectionState({ preserveFallback: true });
|
||||
}}
|
||||
/>
|
||||
<FloatingSidebarAction
|
||||
icon={GitMerge}
|
||||
label="合并汇总"
|
||||
onClick={handleInsertFlowBoardAbstract}
|
||||
/>
|
||||
<FloatingSidebarAction
|
||||
icon={Plus}
|
||||
label="新建"
|
||||
@ -1519,6 +1650,9 @@ export function DrawnixWorkspace({
|
||||
<aside
|
||||
className="absolute top-20 right-4 bottom-5 z-30 flex w-[330px] max-w-[calc(100vw-32px)] flex-col overflow-hidden rounded-[28px] border border-black/10 bg-white shadow-[0_24px_80px_rgba(17,24,39,0.16)]"
|
||||
data-flow-board-shell-control="true"
|
||||
onMouseDownCapture={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between border-black/5 border-b px-5 py-4">
|
||||
<div>
|
||||
@ -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({
|
||||
<button
|
||||
className="flex h-10 w-full items-center justify-center rounded-2xl border border-black/10 bg-white font-semibold text-[#4f566b] text-sm transition-colors hover:bg-[#f7f8fb] disabled:cursor-not-allowed disabled:opacity-50"
|
||||
disabled={markerSelectionState.count === 0}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onClick={handleClearFlowBoardMarkers}
|
||||
type="button"
|
||||
>
|
||||
|
||||
@ -101,16 +101,22 @@ export function FloatingLogoPill({
|
||||
type FloatingSidebarRailProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
orientation?: "vertical" | "horizontal";
|
||||
};
|
||||
|
||||
export function FloatingSidebarRail({
|
||||
children,
|
||||
className,
|
||||
orientation = "vertical",
|
||||
}: FloatingSidebarRailProps) {
|
||||
const isHorizontal = orientation === "horizontal";
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-auto flex w-[56px] flex-col items-center gap-1 overflow-y-auto rounded-[28px] border border-black/10 bg-white px-1.5 py-2.5 shadow",
|
||||
"pointer-events-auto flex items-center gap-1 border border-black/10 bg-white shadow",
|
||||
isHorizontal
|
||||
? "h-[52px] w-auto flex-row overflow-hidden rounded-full px-2.5 py-1.5"
|
||||
: "w-[56px] flex-col overflow-y-auto rounded-[28px] px-1.5 py-2.5",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@ -146,6 +152,10 @@ export function FloatingSidebarAction({
|
||||
"bg-[#111827] text-white shadow-sm hover:bg-[#1f2937] hover:text-white"
|
||||
)}
|
||||
disabled={disabled || loading}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onClick={onClick}
|
||||
type="button"
|
||||
>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user