From 1c629811f5338943b799cd6a04e9b02bcb76792f Mon Sep 17 00:00:00 2001 From: Jose Antonio Alatorre Date: Tue, 10 Nov 2026 11:46:01 +0100 Subject: [PATCH] fix(sdk): preserve selection until the search query changes Avoid a delayed mount-time search reset clearing selection and destroying an open confirmation during live localization. Exercise the elapsed timer in the browser regression and retain the current record, form, selection, and focus. Co-authored-by: Codex --- command-center-sdk/CHANGELOG.md | 2 ++ command-center-sdk/docs/localization.md | 4 ++-- command-center-sdk/src/views/README.md | 5 +++++ command-center-sdk/src/views/ResourceListPage.tsx | 7 +++++-- .../tests/browser/localization-controls.pw.ts | 6 ++++++ 5 files changed, 20 insertions(+), 4 deletions(-) diff --git a/command-center-sdk/CHANGELOG.md b/command-center-sdk/CHANGELOG.md index 56c844a9..50a29728 100644 --- a/command-center-sdk/CHANGELOG.md +++ b/command-center-sdk/CHANGELOG.md @@ -6,6 +6,8 @@ hosts default to English; locale updates preserve authorization and child state. - Add public `/localization` and `/localization/react` message adapters, English defaults and live localization of resource controls and accessibility messages. +- Preserve selection and open confirmations when the normalized search query has + not changed; a delayed mount-time debounce must not clear them during a locale update. - Keep existing schema IDs, protocol versions and component overrides. External strict validators receive the legacy payload unless their child advertises `locale-context-v1`; locale-aware validators adopt the updated schema. No SDK-owned diff --git a/command-center-sdk/docs/localization.md b/command-center-sdk/docs/localization.md index 41bc7cfb..f1d4d29e 100644 --- a/command-center-sdk/docs/localization.md +++ b/command-center-sdk/docs/localization.md @@ -8,9 +8,9 @@ at `@dev-mainsequence/command-center-sdk/localization/react`. import { CommandCenterLocalizationProvider } from '@dev-mainsequence/command-center-sdk/localization/react'; import type { TranslateSdkMessage } from '@dev-mainsequence/command-center-sdk/localization'; import { useTranslation } from 'react-i18next'; -import { useCallback } from 'react'; +import { useCallback, type ReactNode } from 'react'; -function LocalizedApplication({ children }) { +function LocalizedApplication({ children }: { children: ReactNode }) { const { i18n } = useTranslation('sdk'); const locale = i18n.resolvedLanguage ?? i18n.language; const translate = useCallback((key, values) => diff --git a/command-center-sdk/src/views/README.md b/command-center-sdk/src/views/README.md index 9281caf0..31c6fa52 100644 --- a/command-center-sdk/src/views/README.md +++ b/command-center-sdk/src/views/README.md @@ -144,3 +144,8 @@ title; the host sizes it, and a lead that renders nothing takes no space. No vie Shared message defaults now come from `/localization/react`; wrap views in `CommandCenterLocalizationProvider` to inject application catalogs. Explicit label props retain precedence. See `docs/localization.md` for interpolation/plurals. + +`ResourceListPage` resets selection only when the normalized debounced search +query changes. Mounting, locale updates, and whitespace-only edits to the same +query preserve selection and any open confirmation. Keep translation callbacks +out of transport/query dependencies. diff --git a/command-center-sdk/src/views/ResourceListPage.tsx b/command-center-sdk/src/views/ResourceListPage.tsx index 2d44622d..77735af9 100644 --- a/command-center-sdk/src/views/ResourceListPage.tsx +++ b/command-center-sdk/src/views/ResourceListPage.tsx @@ -394,8 +394,11 @@ export function ResourceListPage({ ]); useEffect(() => { + const nextSearch = searchInput.trim(); + // Mounting, a locale update, or whitespace around the same query must not clear selection. + if (nextSearch === search) return undefined; const timeout = window.setTimeout(() => { - setSearch(searchInput.trim()); + setSearch(nextSearch); setPageIndex(0); setAllMatching(false); selection.clearSelection(); @@ -403,7 +406,7 @@ export function ResourceListPage({ return () => window.clearTimeout(timeout); // Selection callbacks intentionally stay outside dependencies: the query value is the reset boundary. // eslint-disable-next-line react-hooks/exhaustive-deps - }, [searchInput]); + }, [searchInput, search]); useEffect(() => { setPageIndex(0); diff --git a/command-center-sdk/tests/browser/localization-controls.pw.ts b/command-center-sdk/tests/browser/localization-controls.pw.ts index d29344b3..e9353b09 100644 --- a/command-center-sdk/tests/browser/localization-controls.pw.ts +++ b/command-center-sdk/tests/browser/localization-controls.pw.ts @@ -16,6 +16,9 @@ test.beforeAll(async () => { test("localizes resource selection, long pagination labels and an open confirmation in place", async ({ page }) => { await page.setContent(`
`); + await page.clock.install(); + const pageErrors: string[] = []; + page.on("pageerror", error => pageErrors.push(error.message)); await page.addScriptTag({ content: bundle }); await expect(page.getByRole("button", { name: "Weiter zur nächsten Ergebnisseite" })).toBeVisible(); const pagination = page.getByRole("navigation", { name: "records pagination" }); @@ -34,6 +37,9 @@ test("localizes resource selection, long pagination labels and an open confirmat await dialog.getByLabel("Bestätigungswort").fill("ARC"); // An application-owned update can arrive while focus is inside a modal. await page.getByRole("button", { name: "Change language" }).evaluate((button: HTMLButtonElement) => button.click()); + // A delayed mount-time search reset used to clear selection and destroy the open dialog. + await page.clock.fastForward(1_000); + expect(pageErrors).toEqual([]); await expect(dialog.getByRole("button", { name: "Cancel", exact: true })).toBeVisible(); await expect(confirmationInput).toHaveAttribute("aria-label", "Confirmation word"); await expect(confirmationInput).toHaveValue("ARC");