diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index ea3e421..f02a8a0 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -1422,7 +1422,6 @@ const en = { openRegion: "Open", kindPhysical: "Physical", kindCustom: "Custom", - regionNodeHint: "{{count}}P", layerNodeHint: "{{count}} NE(s)", layerNodeEmpty: "Click to create & open", rootCanvasHint: "Pick a root from the left tree.", diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 50a2894..fd465e2 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -1412,7 +1412,6 @@ const zh = { openRegion: "打开", kindPhysical: "物理拓扑", kindCustom: "自定义图", - regionNodeHint: "{{count}}P", layerNodeHint: "{{count}} 个网元", layerNodeEmpty: "点击创建并打开", rootCanvasHint: "从左侧选择根进入目录。", diff --git a/web/src/pages/TopologyPage.tsx b/web/src/pages/TopologyPage.tsx index ead1c28..f8c1a5e 100644 --- a/web/src/pages/TopologyPage.tsx +++ b/web/src/pages/TopologyPage.tsx @@ -1,4 +1,5 @@ import { + Fragment, createContext, memo, useCallback, @@ -136,7 +137,7 @@ function indexFoldersById( return acc; } -/** Self + ancestors (rootward), for expanding the left tree path. */ +/** Self + ancestors (leaf → rootward), for expanding the left tree path. */ function folderPathIds( folders: TopologyTreeFolderItem[], folderId: string, @@ -157,6 +158,19 @@ function folderPathIds( return path; } +/** Ancestors + self (rootward → leaf) for breadcrumb display. */ +function folderPathFolders( + folders: TopologyTreeFolderItem[], + folderId: string, +): TopologyTreeFolderItem[] { + if (!folderId) return []; + const byId = indexFoldersById(folders); + return folderPathIds(folders, folderId) + .map((id) => byId.get(id)) + .filter((f): f is TopologyTreeFolderItem => Boolean(f)) + .reverse(); +} + /** UME World container only — hex nav, not a canvas. */ function isUmeWorldContainer(folder: TopologyTreeFolderItem | null | undefined): boolean { if (!folder) return false; @@ -1844,36 +1858,45 @@ export function TopologyPage() { const goBackBrowse = useCallback(() => { if (!confirmDiscardIfDirty()) return; const folder = findFolderInTree(regions, selectedFolderId); - // Leaving a region canvas under UME World → container hex nav. - if (folder) { - const path = folderPathIds(regions, folder.id); - const container = regions.find((r) => isUmeWorldContainer(r)); - if (container && (folder.id === container.id || path.includes(container.id))) { - setSelectedFolderId(container.id); + const parentId = String(folder?.parent_id || "").trim(); + const parent = parentId ? findFolderInTree(regions, parentId) : null; + // One level up: parent canvas if nested, else parent hex nav, else roots. + if (parent) { + setSelectedFolderId(parent.id); + expandFolderPath(parent.id); + if (isRegionCanvasFolder(parent, String(treeRoot?.id || ""))) { setWorldFocusFolderId(""); - setMapId(""); - clearDirty(); - setNodes([]); - setEdges([]); - return; - } - } - // Leaving other region canvases → parent nav container, else root hex. - if (isRegionCanvasFolder(folder, String(treeRoot?.id || "")) || isUmeWorldNavFolder(folder)) { - const parentId = String(folder?.parent_id || "").trim(); - const parent = parentId ? findFolderInTree(regions, parentId) : null; - if (parent && !isRegionCanvasFolder(parent, String(treeRoot?.id || ""))) { - setSelectedFolderId(parent.id); - } else { - setSelectedFolderId(""); + const view = primaryViewOfFolder(parent); + if (view) { + setMapId(view.id); + clearDirty(); + return; + } } + setWorldFocusFolderId(""); + setMapId(""); + clearDirty(); + setNodes([]); + setEdges([]); + return; } + setSelectedFolderId(""); setWorldFocusFolderId(""); setMapId(""); clearDirty(); setNodes([]); setEdges([]); - }, [confirmDiscardIfDirty, clearDirty, setNodes, setEdges, regions, selectedFolderId, treeRoot?.id]); + }, [ + confirmDiscardIfDirty, + clearDirty, + setNodes, + setEdges, + regions, + selectedFolderId, + treeRoot?.id, + expandFolderPath, + primaryViewOfFolder, + ]); // Deep link from classify search: /topology?view=&ne= useEffect(() => { @@ -2296,7 +2319,6 @@ export function TopologyPage() { const focused = viewActiveUnder || (isSelected && Boolean(mapId)); const regionActive = isSelected && !mapId; const hot = hotBrowseKey === `region:${folder.id}`; - const childCount = kids.length; const renderViewRow = (v: TopologyTreeViewItem) => { const isPhysical = String(v.kind) === "physical"; @@ -2346,9 +2368,6 @@ export function TopologyPage() { {displayViewName(v.name, t)} {viewActive && dirty ? " *" : ""} - {!umeNav ? ( - {v.node_count || 0}N - ) : null} {!umeNav ? ( @@ -2433,24 +2452,6 @@ export function TopologyPage() { {regionDisplayName(folder, t)} - - {(() => { - if (containerFolder) { - return t("topology.regionNodeHint").replace( - "{{count}}", - String(kids.length + visibleViews.length), - ); - } - const neCount = (folder.views || []).reduce( - (sum, v) => sum + (Number(v.node_count) || 0), - 0, - ); - // Prefer canvas NE count; fall back to child-region count. - if (neCount > 0) return `${neCount}N`; - if (childCount > 0) return String(childCount); - return ""; - })()} - {(() => { @@ -4063,6 +4064,12 @@ export function TopologyPage() { return displayViewName(graphQuery.data?.view?.name, t); }, [mapId, activeView, graphQuery.data?.view?.name, t]); + /** Full folder chain for breadcrumb (rootward → leaf). */ + const breadcrumbFolders = useMemo( + () => folderPathFolders(regions, selectedFolderId), + [regions, selectedFolderId], + ); + const titleText = useMemo(() => { if (canvasMode) return activeLeafName || t("topology.selectMap"); if (activeRegion) return regionDisplayName(activeRegion, t); @@ -4338,27 +4345,37 @@ export function TopologyPage() { > {t("topology.rootName")} - {activeRegion ? ( - <> - / - {activeView && - isRegionCanvasFolder(activeRegion, rootFolderId) && - primaryViewOfFolder(activeRegion)?.id === activeView.id ? ( - - {regionDisplayName(activeRegion, t)} - {dirty ? " *" : ""} - - ) : ( - - )} - - ) : null} + {breadcrumbFolders.map((folder, idx) => { + const isLast = idx === breadcrumbFolders.length - 1; + const primaryView = primaryViewOfFolder(folder); + const showExtraView = + isLast && + Boolean(activeView) && + !( + isRegionCanvasFolder(folder, rootFolderId) && + primaryView?.id === activeView?.id + ); + const asCurrent = isLast && !showExtraView; + return ( + + / + {asCurrent ? ( + + {regionDisplayName(folder, t)} + {dirty ? " *" : ""} + + ) : ( + + )} + + ); + })} {activeView && !( activeRegion && @@ -4368,7 +4385,7 @@ export function TopologyPage() { <> / - {activeView.name} + {displayViewName(activeView.name, t)} {dirty ? " *" : ""} @@ -4614,8 +4631,27 @@ export function TopologyPage() { > {t("topology.rootName")} - / - {regionDisplayName(hexBrowseRegion, t)} + {breadcrumbFolders.map((folder, idx) => { + const isLast = idx === breadcrumbFolders.length - 1; + return ( + + / + {isLast ? ( + + {regionDisplayName(folder, t)} + + ) : ( + + )} + + ); + })} ) : ( {titleText} @@ -4715,16 +4751,6 @@ export function TopologyPage() { {regionDisplayName(region, t)} - - {t("topology.regionNodeHint").replace( - "{{count}}", - String( - isUmeWorldContainer(region) - ? (region.children?.length || 0) + (region.views?.length || 0) - : region.views?.length || 0, - ), - )} - ))} @@ -4769,12 +4795,6 @@ export function TopologyPage() { {regionDisplayName(umeWorldHexModules.drill, t)} - - {t("topology.regionNodeHint").replace( - "{{count}}", - String(umeWorldHexModules.drill.children?.length || 0), - )} - ) : null} @@ -4805,12 +4825,6 @@ export function TopologyPage() { {umeWorldHexModules.flatView.name} - - {t("topology.regionNodeHint").replace( - "{{count}}", - String(umeWorldHexModules.flatView.node_count || 0), - )} - ) : null} @@ -4836,12 +4850,6 @@ export function TopologyPage() { {regionDisplayName(region, t)} - - {t("topology.regionNodeHint").replace( - "{{count}}", - String(region.views?.length || region.children?.length || 0), - )} - ))} @@ -4867,7 +4875,6 @@ export function TopologyPage() { {displayViewName(v.name, t)} - {v.node_count || 0}N );