diff --git a/netx_api/biz_state/compare_rules.py b/netx_api/biz_state/compare_rules.py index 104382d..97e454b 100644 --- a/netx_api/biz_state/compare_rules.py +++ b/netx_api/biz_state/compare_rules.py @@ -157,31 +157,32 @@ def effective_display_fields( compare_fields: Sequence[str], display_fields: Sequence[str] | None = None, ) -> list[str]: - """Result-table columns: explicit display, else key+compare (legacy). + """Result-table columns in Key → Compare → Display-only order. - Keys always lead; remaining display/compare fields follow in given order - without duplicates. + Keys always lead; compare fields follow in template order; remaining + display (context) fields come last. Compare fields are included even if + the UI forgot to tick display. """ keys = [str(x).strip() for x in (key_fields or []) if str(x).strip()] key_set = set(keys) - compare = [str(x).strip() for x in (compare_fields or []) if str(x).strip()] - raw_disp = display_fields - if raw_disp is None: - # Legacy templates: show key + compare only - extra = [f for f in compare if f not in key_set] - return keys + extra - disp = [str(x).strip() for x in raw_disp if str(x).strip()] - # Force keys first (always visible) + compare = [ + str(x).strip() + for x in (compare_fields or []) + if str(x).strip() and str(x).strip() not in key_set + ] + if display_fields is None: + return keys + compare + disp = [str(x).strip() for x in display_fields if str(x).strip()] + compare_set = set(compare) out: list[str] = list(keys) seen = set(keys) - for f in disp: + for f in compare: if f in seen: continue out.append(f) seen.add(f) - # Ensure compare fields appear even if UI forgot to tick display - for f in compare: - if f in seen or f in key_set: + for f in disp: + if f in seen or f in key_set or f in compare_set: continue out.append(f) seen.add(f) diff --git a/netx_api/biz_state/compare_service.py b/netx_api/biz_state/compare_service.py index 65004da..29fab80 100644 --- a/netx_api/biz_state/compare_service.py +++ b/netx_api/biz_state/compare_service.py @@ -1560,14 +1560,14 @@ def _csv_cell(v: Any) -> str: def _sheet_csv(sheet: dict[str, Any]) -> str: keys = list(sheet.get("key_fields") or []) key_set = set(keys) - compare = list(sheet.get("compare_fields") or []) + compare = [f for f in list(sheet.get("compare_fields") or []) if f not in key_set] compare_set = set(compare) display = effective_display_fields( key_fields=keys, compare_fields=compare, display_fields=list(sheet.get("display_fields") or []) or None, ) - # Non-key display columns: compare fields get pre/post; display-only get single value col + # Non-key columns already ordered Key→Compare→Display by effective_display_fields extra = [f for f in display if f not in key_set] headers = ["kind", *keys] for f in extra: diff --git a/tests/test_biz_state_compare.py b/tests/test_biz_state_compare.py index 8f8c23d..43b2ecc 100644 --- a/tests/test_biz_state_compare.py +++ b/tests/test_biz_state_compare.py @@ -289,8 +289,8 @@ class CompareRulesTests(unittest.TestCase): compare_fields=["mac"], display_fields=["vrf", "age", "ip"], ) - # keys first, then display order extras, compare forced if missing - self.assertEqual(explicit, ["ip", "iface", "vrf", "age", "mac"]) + # Key → Compare → Display-only (compare forced even if not ticked) + self.assertEqual(explicit, ["ip", "iface", "mac", "vrf", "age"]) def test_display_only_field_does_not_change(self) -> None: """Context column in display but not compare → value drift ignored.""" diff --git a/web/src/App.tsx b/web/src/App.tsx index 601e088..8c99625 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -144,11 +144,14 @@ function ProtectedApp() { element={} /> } /> - } /> + } + /> } /> } + element={} /> } /> diff --git a/web/src/pages/network/BizComparePage.tsx b/web/src/pages/network/BizComparePage.tsx index 107476b..6406f27 100644 --- a/web/src/pages/network/BizComparePage.tsx +++ b/web/src/pages/network/BizComparePage.tsx @@ -754,6 +754,11 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage const { showOk, showError } = useToast(); const [pageTab, setPageTab] = useState(pageMode === "templates" ? "templates" : "jobs"); + // Route-bound modes must follow pageMode; internal tabs only when pageMode === "all". + // Without this, React reuses the same component instance across + // /biz-compare ↔ /compare-templates and pageTab stays stuck. + const activeTab: PageTab = + pageMode === "templates" ? "templates" : pageMode === "jobs" ? "jobs" : pageTab; const showTabSwitch = pageMode === "all"; const [busy, setBusy] = useState(false); @@ -838,6 +843,20 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage // eslint-disable-next-line react-hooks/exhaustive-deps }, [refresh]); + // Keep local tab + dismiss overlays when route mode flips (component may be reused). + useEffect(() => { + if (pageMode === "templates") setPageTab("templates"); + else if (pageMode === "jobs") setPageTab("jobs"); + setTplOpen(false); + setJobCreateOpen(false); + setCreateStep(0); + setJobId(""); + setRuns([]); + setRunDetail(null); + setResultSheetId(""); + setBoardFs(false); + }, [pageMode]); + useEffect(() => { void (async () => { if (!beforeTaskId) { @@ -1046,12 +1065,12 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage let display = (activeRunSheet?.display_fields || []).filter(Boolean); if (!display.length) { display = [...keys, ...compare]; - } else { - // keys first, then rest of display order - const rest = display.filter((f) => !keySet.has(f)); - display = [...keys, ...rest]; } - let extras = display.filter((f) => !keySet.has(f)); + // Column order: Key → Compare → Display-only (context) + // Compare fields always appear (even if display unticked), matching backend. + const compareCols = [...compare]; + const displayOnly = display.filter((f) => !keySet.has(f) && !compareSet.has(f)); + let extras = [...compareCols, ...displayOnly]; // Presence / sparse display: if no value columns, pull fields from sample // added/removed rows so 缺失/多余 still show side data. if (!extras.length && pagedDiffs.length) { @@ -1066,7 +1085,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage } return { keys, - compare, + compare: compareCols, extras, compareSet, presence: !(activeRunSheet?.compare_fields || []).length, @@ -1922,7 +1941,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage {pageMode === "templates" ? t("bizCompare.templates") : t("bizCompare.title")}
- {pageTab === "templates" ? ( + {activeTab === "templates" ? ( <>
- {pageTab === "templates" ? ( + {activeTab === "templates" ? (

{t("bizCompare.templateImportHint")}