This is an automated email from the ASF dual-hosted git repository.

guoqqqi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-dashboard.git


The following commit(s) were added to refs/heads/master by this push:
     new 33b81ba6c fix: wire the form TOC refresh intentionally (#3439)
33b81ba6c is described below

commit 33b81ba6c9eb4954795878ed9f971136e1f42a25
Author: Yuhan <[email protected]>
AuthorDate: Thu Jul 23 09:50:26 2026 +0800

    fix: wire the form TOC refresh intentionally (#3439)
---
 .../form.toc-refresh-on-dynamic-sections.spec.ts   | 59 ++++++++++++++++++++++
 src/components/form-slice/FormSection/index.tsx    | 22 +++++---
 2 files changed, 74 insertions(+), 7 deletions(-)

diff --git a/e2e/tests/regression/form.toc-refresh-on-dynamic-sections.spec.ts 
b/e2e/tests/regression/form.toc-refresh-on-dynamic-sections.spec.ts
new file mode 100644
index 000000000..6f364aa4f
--- /dev/null
+++ b/e2e/tests/regression/form.toc-refresh-on-dynamic-sections.spec.ts
@@ -0,0 +1,59 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Regression for a UX item of apache/apisix-dashboard#3417: the form
+// table of contents never refreshed when sections appeared or
+// disappeared dynamically — `refreshTOC` built a debounced function on
+// every call and discarded it without invoking, so Mantine's
+// TableOfContents reinitialize hook was never actually called. Enabling
+// the health-check switch rendered the new sections in the form while
+// the TOC kept showing the stale initial list.
+
+import { upstreamsPom } from '@e2e/pom/upstreams';
+import { test } from '@e2e/utils/test';
+import { expect } from '@playwright/test';
+
+test('TOC gains and loses entries as sections toggle', async ({ page }) => {
+  await upstreamsPom.toAdd(page);
+  await upstreamsPom.isAddPage(page);
+
+  const toc = page.locator('.mantine-TableOfContents-root');
+  await expect(toc).toBeVisible();
+  // the always-present Nodes section proves the TOC scanned the form
+  await expect(toc.getByText('Nodes', { exact: true })).toBeVisible();
+  await expect(toc.getByText('Active', { exact: true })).toBeHidden();
+
+  const checksTrack = page
+    .getByTestId('checksEnabled')
+    .locator('..')
+    .locator('.mantine-Switch-track');
+
+  // enabling health checks mounts the Active section in the form…
+  await checksTrack.click();
+  await expect(
+    page.getByRole('group', { name: 'Active', exact: true })
+  ).toBeVisible();
+  // …and the TOC must pick it up (debounced refresh, auto-retried here)
+  await expect(toc.getByText('Active', { exact: true })).toBeVisible();
+
+  // and drop it again when the section unmounts
+  await checksTrack.click();
+  await expect(
+    page.getByRole('group', { name: 'Active', exact: true })
+  ).toBeHidden();
+  await expect(toc.getByText('Active', { exact: true })).toBeHidden();
+});
diff --git a/src/components/form-slice/FormSection/index.tsx 
b/src/components/form-slice/FormSection/index.tsx
index 2e4ce2a9d..d2047f70e 100644
--- a/src/components/form-slice/FormSection/index.tsx
+++ b/src/components/form-slice/FormSection/index.tsx
@@ -28,7 +28,6 @@ import {
   createContext,
   type PropsWithChildren,
   type ReactNode,
-  useCallback,
   useContext,
   useMemo,
   useRef,
@@ -88,8 +87,12 @@ export const FormSection = (props: FormSectionProps) => {
     [legend, depth]
   );
 
-  // refresh TOC when children changes
-  useShallowEffect(refreshTOC, [children]);
+  // refresh TOC when children change, and again on unmount — a section
+  // that disappears must also drop out of the TOC
+  useShallowEffect(() => {
+    refreshTOC();
+    return refreshTOC;
+  }, [children]);
 
   return (
     <SectionDepthProvider value={depth}>
@@ -146,10 +149,15 @@ export type FormTOCBoxProps = PropsWithChildren;
 export const FormTOCBox = (props: FormTOCBoxProps) => {
   const { children } = props;
   const reinitializeRef = useRef(() => {});
-  const refreshTOC = useCallback(
-    () => debounce(reinitializeRef.current, 200),
-    []
-  );
+  // one stable debounced function, invoked through the ref so it always
+  // reaches Mantine's latest reinitialize. The previous version built a
+  // debounced wrapper on every call and discarded it — the TOC only kept
+  // refreshing because React treated that discarded return value as an
+  // effect CLEANUP and invoked it on the next change (see #3417).
+  const refreshTOC = useMemo(() => {
+    const run = debounce(() => reinitializeRef.current(), 200);
+    return () => run(undefined);
+  }, []);
 
   return (
     <Group

Reply via email to