mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 02:00:46 +08:00
Allow deleting UME-synced sub-regions like manual regions.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
467079d6ca
commit
8fa4b2e4ea
3 changed files with 41 additions and 8 deletions
|
|
@ -549,9 +549,11 @@ def update_folder(db: Session, folder_id: str, body: TopologyFolderUpdate) -> To
|
||||||
def delete_folder(db: Session, folder_id: str, *, force: bool = False) -> dict[str, Any]:
|
def delete_folder(db: Session, folder_id: str, *, force: bool = False) -> dict[str, Any]:
|
||||||
"""Delete a region and cascade-delete nested regions + maps.
|
"""Delete a region and cascade-delete nested regions + maps.
|
||||||
|
|
||||||
System L2「根图」may be removed only as part of cascading from its parent「根」.
|
System L2「根图」(manual, no UME external_ref) may be removed only as part of
|
||||||
UME World container may be deleted explicitly (cascades all synced SBN folders);
|
cascading from its parent「根」.
|
||||||
a later UME apply / world ensure can recreate it.
|
|
||||||
|
UME World and UME-synced SBN folders (``external_ref`` set) are user-deletable —
|
||||||
|
they are synced inventory, not structural locks; a later UME apply can recreate them.
|
||||||
"""
|
"""
|
||||||
row = _get_folder_or_404(db, folder_id)
|
row = _get_folder_or_404(db, folder_id)
|
||||||
if str(row.kind or "") == "root":
|
if str(row.kind or "") == "root":
|
||||||
|
|
@ -559,9 +561,10 @@ def delete_folder(db: Session, folder_id: str, *, force: bool = False) -> dict[s
|
||||||
|
|
||||||
from .ume_topology_world import is_ume_world_container
|
from .ume_topology_world import is_ume_world_container
|
||||||
|
|
||||||
# UME World container is system-marked but user-deletable (cascades SBN tree).
|
ext = str(getattr(row, "external_ref", None) or "").strip()
|
||||||
# Manual「根图」and other system folders stay protected except via parent cascade.
|
ume_synced = bool(ext) # ume:world / ume:world:drill / SBN id
|
||||||
if bool(row.is_system) and not is_ume_world_container(row):
|
# Protect manual system folders (根图); allow UME World + synced children.
|
||||||
|
if bool(row.is_system) and not is_ume_world_container(row) and not ume_synced:
|
||||||
raise HTTPException(status_code=400, detail="cannot_delete_system_folder")
|
raise HTTPException(status_code=400, detail="cannot_delete_system_folder")
|
||||||
_ = force
|
_ = force
|
||||||
from .topology_region_canvas import remove_region_canvas_placements
|
from .topology_region_canvas import remove_region_canvas_placements
|
||||||
|
|
|
||||||
|
|
@ -413,6 +413,34 @@ class UmeHierarchyTests(unittest.TestCase):
|
||||||
row = self.db.get(TopoFolder, out.id)
|
row = self.db.get(TopoFolder, out.id)
|
||||||
self.assertEqual(row.parent_id, drill.id)
|
self.assertEqual(row.parent_id, drill.id)
|
||||||
|
|
||||||
|
def test_ume_synced_sbn_folder_is_deletable(self):
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
from netx_api.topology_views_tree import delete_folder
|
||||||
|
from netx_api.ume_topology_world import get_world_drill_folder
|
||||||
|
|
||||||
|
self._seed_tree()
|
||||||
|
apply_ume_topology_to_fabric(self.db)
|
||||||
|
ensure_ume_world_and_sbn_folders(self.db)
|
||||||
|
drill = get_world_drill_folder(self.db)
|
||||||
|
self.assertIsNotNone(drill)
|
||||||
|
sbns = (
|
||||||
|
self.db.query(TopoFolder)
|
||||||
|
.filter(TopoFolder.parent_id == drill.id, TopoFolder.kind == "region")
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
self.assertGreater(len(sbns), 0)
|
||||||
|
target = next((f for f in sbns if str(f.external_ref or "").strip()), sbns[0])
|
||||||
|
self.assertTrue(bool(target.is_system))
|
||||||
|
self.assertTrue(str(target.external_ref or "").strip())
|
||||||
|
out = delete_folder(self.db, target.id)
|
||||||
|
self.assertTrue(out.get("ok"))
|
||||||
|
self.assertIsNone(self.db.get(TopoFolder, target.id))
|
||||||
|
# Manual system folder without UME ref stays protected.
|
||||||
|
with self.assertRaises(HTTPException) as ctx:
|
||||||
|
delete_folder(self.db, "nope-missing")
|
||||||
|
self.assertEqual(ctx.exception.status_code, 404)
|
||||||
|
|
||||||
def test_flat_slots_do_not_overlap(self):
|
def test_flat_slots_do_not_overlap(self):
|
||||||
self._seed_tree()
|
self._seed_tree()
|
||||||
apply_ume_topology_to_fabric(self.db)
|
apply_ume_topology_to_fabric(self.db)
|
||||||
|
|
|
||||||
|
|
@ -2395,7 +2395,7 @@ export function TopologyPage() {
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
{!umeNav || containerFolder ? (
|
{!umeNav || containerFolder || Boolean(String(folder.external_ref || "").trim()) ? (
|
||||||
<div className="topo-map-list__actions">
|
<div className="topo-map-list__actions">
|
||||||
{!containerFolder ? (
|
{!containerFolder ? (
|
||||||
<button
|
<button
|
||||||
|
|
@ -2416,7 +2416,9 @@ export function TopologyPage() {
|
||||||
aria-label={t("topology.deleteRegion")}
|
aria-label={t("topology.deleteRegion")}
|
||||||
disabled={
|
disabled={
|
||||||
deleteFolderMut.isPending ||
|
deleteFolderMut.isPending ||
|
||||||
(!!folder.is_system && !containerFolder)
|
(!!folder.is_system &&
|
||||||
|
!containerFolder &&
|
||||||
|
!String(folder.external_ref || "").trim())
|
||||||
}
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const msg = (
|
const msg = (
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue