Harden topology tree against bad view filters and allow deleting UME World.

Coerce non-dict filter JSON so /tree no longer 500s, surface API failure details, and let users remove the UME World container for local resets.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-08-07 09:58:20 +08:00
parent 35a3980dc0
commit 961d523479
7 changed files with 89 additions and 30 deletions

View file

@ -1441,6 +1441,8 @@ const en = {
regionDeleted: "Deleted",
deleteRegion: "Delete",
deleteRegionConfirm: "Delete \"{{name}}\"? Its maps will also be deleted.",
deleteUmeWorldConfirm:
"Delete \"{{name}}\"? This cascades all UME regions and maps under it. A later UME topology apply may recreate it.",
renameRegion: "Rename",
renameRegionPrompt: "Name",
regionRenamed: "Renamed",

View file

@ -1431,6 +1431,8 @@ const zh = {
regionDeleted: "已删除",
deleteRegion: "删除",
deleteRegionConfirm: "删除「{{name}}」?其下画布将一并删除。",
deleteUmeWorldConfirm:
"删除「{{name}}」?将级联删除其下全部 UME 区域与画布。之后若再跑 UME 拓扑同步/应用,可能重新生成。",
renameRegion: "重命名",
renameRegionPrompt: "名称",
regionRenamed: "已重命名",

View file

@ -2318,30 +2318,38 @@ export function TopologyPage() {
</span>
</span>
</button>
{!umeNav ? (
{!umeNav || containerFolder ? (
<div className="topo-map-list__actions">
<button
type="button"
className="topo-map-list__icon"
title={t("topology.renameRegion")}
aria-label={t("topology.renameRegion")}
disabled={!!folder.is_system || renameRegionMut.isPending}
onClick={() => promptRenameRegion(folder.id, folder.name)}
>
<PencilIcon />
</button>
{!containerFolder ? (
<button
type="button"
className="topo-map-list__icon"
title={t("topology.renameRegion")}
aria-label={t("topology.renameRegion")}
disabled={!!folder.is_system || renameRegionMut.isPending}
onClick={() => promptRenameRegion(folder.id, folder.name)}
>
<PencilIcon />
</button>
) : null}
<button
type="button"
className="topo-map-list__icon"
title={t("topology.deleteRegion")}
aria-label={t("topology.deleteRegion")}
disabled={!!folder.is_system}
disabled={
deleteFolderMut.isPending ||
(!!folder.is_system && !containerFolder)
}
onClick={() => {
const msg = t("topology.deleteRegionConfirm").replace(
"{{name}}",
folder.name,
);
if (window.confirm(msg)) deleteFolderMut.mutate(folder.id);
const msg = (
containerFolder
? t("topology.deleteUmeWorldConfirm")
: t("topology.deleteRegionConfirm")
).replace("{{name}}", folder.name);
if (window.confirm(msg)) {
deleteFolderMut.mutate(folder.id);
}
}}
>
<CloseIcon />
@ -4116,7 +4124,10 @@ export function TopologyPage() {
</>
) : treeFailed ? (
<>
{t("topology.treeLoadFailed")}{" "}
{t("topology.treeLoadFailed")}
{treeQuery.error ? (
<span className="muted"> ({String(treeQuery.error)})</span>
) : null}{" "}
<button
type="button"
className="btn btn--sm btn--ghost"