From 41ec2b73dbd351c06e3eee1a914e186db0daef18 Mon Sep 17 00:00:00 2001 From: Ashish Kolhe Date: Mon, 2 Nov 2026 16:17:46 +0530 Subject: [PATCH] fix(autoobserve): never show a previous org's held total in the suggestions drawer A total shrink holds the old total while the drawer moves back to the last page. An org switch now drops that hold, and an older clamp can no longer clear or overwrite the newer org's. --- .../live/AllSuggestionsDrawer.spec.ts | 56 +++++++++++++++++++ .../autoobserve/live/AllSuggestionsDrawer.vue | 7 ++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/web/src/components/autoobserve/live/AllSuggestionsDrawer.spec.ts b/web/src/components/autoobserve/live/AllSuggestionsDrawer.spec.ts index 07138af4c9..e340748ba9 100644 --- a/web/src/components/autoobserve/live/AllSuggestionsDrawer.spec.ts +++ b/web/src/components/autoobserve/live/AllSuggestionsDrawer.spec.ts @@ -244,4 +244,60 @@ describe("AllSuggestionsDrawer (journey 3)", () => { expect(wrapper.find(COUNT).text()).toBe("24"); expect(wrapper.findComponent(OTable).props("loading")).toBe(false); }); + + it("never shows the previous org's held total when the org switches mid-clamp", async () => { + api.mockResolvedValueOnce(page(["a1"], 30)); + const wrapper = mountDrawer(); + await flushPromises(); + api.mockResolvedValueOnce(page([], 24)); + let release: (value: unknown) => void = () => undefined; + api.mockReturnValueOnce(new Promise((resolve) => (release = resolve)) as never); + wrapper.findComponent(OTable).vm.$emit("pagination-change", { page: 2, pageSize: 25 }); + await flushPromises(); + expect(wrapper.find(COUNT).text()).toBe("24"); + api.mockResolvedValueOnce(page(["b1"], 1)); + setOrg("org-b"); + await flushPromises(); + expect(rows(wrapper).map((r) => r.id)).toEqual(["b1"]); + expect(wrapper.find(COUNT).text()).toBe("1"); + expect(wrapper.findComponent(OTable).props("loading")).toBe(false); + release(page(["a1"], 24)); + await flushPromises(); + expect(rows(wrapper).map((r) => r.id)).toEqual(["b1"]); + expect(wrapper.find(COUNT).text()).toBe("1"); + expect(wrapper.findComponent(OTable).props("loading")).toBe(false); + }); + + it("keeps the new org's own clamp when the old org's clamp refetch settles late", async () => { + const held = () => { + let release: (value: unknown) => void = () => undefined; + const promise = new Promise((resolve) => (release = resolve)); + return { promise: promise as never, release: (v: unknown) => release(v) }; + }; + const toPage2 = () => + wrapper.findComponent(OTable).vm.$emit("pagination-change", { page: 2, pageSize: 25 }); + api.mockResolvedValueOnce(page(["a1"], 30)); + const wrapper = mountDrawer(); + await flushPromises(); + const orgA = held(); + api.mockResolvedValueOnce(page([], 24)).mockReturnValueOnce(orgA.promise); + toPage2(); + await flushPromises(); + api.mockResolvedValueOnce(page(["b1"], 30)); + setOrg("org-b"); + await flushPromises(); + const orgB = held(); + api.mockResolvedValueOnce(page([], 20)).mockReturnValueOnce(orgB.promise); + toPage2(); + await flushPromises(); + expect(wrapper.find(COUNT).text()).toBe("20"); + orgA.release(page(["a1"], 24)); + await flushPromises(); + expect(wrapper.find(COUNT).text()).toBe("20"); + expect(wrapper.findComponent(OTable).props("loading")).toBe(true); + orgB.release(page(["b1"], 20)); + await flushPromises(); + expect(wrapper.find(COUNT).text()).toBe("20"); + expect(wrapper.findComponent(OTable).props("loading")).toBe(false); + }); }); diff --git a/web/src/components/autoobserve/live/AllSuggestionsDrawer.vue b/web/src/components/autoobserve/live/AllSuggestionsDrawer.vue index 2f161c8ca6..1bfa275288 100644 --- a/web/src/components/autoobserve/live/AllSuggestionsDrawer.vue +++ b/web/src/components/autoobserve/live/AllSuggestionsDrawer.vue @@ -177,6 +177,8 @@ const rows = computed(() => toSuggestionRows(t, zone.value, query.data.value?.suggestions ?? []), ); const clampTotal = ref(null); +// An org switch voids the hold, so an older org's clamp can never set or clear the newer org's total. +let clampRun = 0; const total = computed(() => clampTotal.value ?? query.data.value?.total ?? 0); const skeleton = useHeldSkeleton(() => props.open && state.value.status === "loading"); const paging = computed(() => query.isPlaceholderData.value && query.isFetching.value); @@ -202,16 +204,19 @@ watch( watch(org, () => { page.value = 1; + clampRun += 1; + clampTotal.value = null; }); // The page it lands on may be cached from before the total shrank, so it is fetched again under the new total. watch(pastEnd, async (past) => { if (!past) return; + const run = ++clampRun; clampTotal.value = total.value; page.value = Math.max(1, Math.ceil(clampTotal.value / DRAWER_PAGE)); await nextTick(); if (!query.isPlaceholderData.value) await query.refetch(); - clampTotal.value = null; + if (run === clampRun) clampTotal.value = null; }); const columns = computed[]>(() => [