Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package agama-web-ui for openSUSE:Factory checked in at 2026-07-17 18:47:25 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/agama-web-ui (Old) and /work/SRC/openSUSE:Factory/.agama-web-ui.new.24530 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "agama-web-ui" Fri Jul 17 18:47:25 2026 rev:48 rq:1366195 version:0 Changes: -------- --- /work/SRC/openSUSE:Factory/agama-web-ui/agama-web-ui.changes 2026-07-15 16:30:00.194746833 +0200 +++ /work/SRC/openSUSE:Factory/.agama-web-ui.new.24530/agama-web-ui.changes 2026-07-17 18:47:49.729911606 +0200 @@ -1,0 +2,21 @@ +Thu Jul 16 07:06:02 UTC 2026 - David Diaz <[email protected]> + +- Prevent the partition and logical volume forms from flickering + when changing the mount point or file system + (gh#agama-project/agama#3728, bsc#1271524). + +------------------------------------------------------------------- +Tue Jul 14 16:59:31 UTC 2026 - David Diaz <[email protected]> + +- Require translated text in user-facing component props, ensuring + users get the interface in their language + (gh#agama-project/agama#3696, bsc#1271404). + +------------------------------------------------------------------- +Tue Jul 14 12:38:47 UTC 2026 - David Diaz <[email protected]> + +- Align paths and navigation with the Installation breadcrumb + instead of the internal overview concept + (gh#agama-project/agama#3722). + +------------------------------------------------------------------- ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ agama.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/package/agama-web-ui.changes new/agama/package/agama-web-ui.changes --- old/agama/package/agama-web-ui.changes 2026-07-14 10:45:10.000000000 +0200 +++ new/agama/package/agama-web-ui.changes 2026-07-16 14:18:35.000000000 +0200 @@ -1,4 +1,25 @@ ------------------------------------------------------------------- +Thu Jul 16 07:06:02 UTC 2026 - David Diaz <[email protected]> + +- Prevent the partition and logical volume forms from flickering + when changing the mount point or file system + (gh#agama-project/agama#3728, bsc#1271524). + +------------------------------------------------------------------- +Tue Jul 14 16:59:31 UTC 2026 - David Diaz <[email protected]> + +- Require translated text in user-facing component props, ensuring + users get the interface in their language + (gh#agama-project/agama#3696, bsc#1271404). + +------------------------------------------------------------------- +Tue Jul 14 12:38:47 UTC 2026 - David Diaz <[email protected]> + +- Align paths and navigation with the Installation breadcrumb + instead of the internal overview concept + (gh#agama-project/agama#3722). + +------------------------------------------------------------------- Mon Jul 13 14:14:41 UTC 2026 - Ancor Gonzalez Sosa <[email protected]> - Fixed check for BIOS RAIDs (bsc#1271120). diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/core/InstallerL10nOptions.tsx new/agama/src/components/core/InstallerL10nOptions.tsx --- old/agama/src/components/core/InstallerL10nOptions.tsx 2026-07-14 10:45:10.000000000 +0200 +++ new/agama/src/components/core/InstallerL10nOptions.tsx 2026-07-16 14:18:35.000000000 +0200 @@ -51,6 +51,8 @@ import { useInstallerL10n } from "~/context/installerL10n"; import { localConnection } from "~/utils"; import { _ } from "~/i18n"; + +import type { TranslatedString } from "~/i18n"; import supportedLanguages from "~/languages.json"; import { ROOT, L10N } from "~/routes/paths"; import { useProductInfo } from "~/hooks/model/config/product"; @@ -266,7 +268,7 @@ type TextWithLinkToL10nProps = { /** The text containing a bracketed substring for the link. */ - text: string; + text: TranslatedString; /** * Optional handler triggered when the user activates the link. Useful for * performing side effects, such as closing the dialog. Navigation may not occur diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/core/ResizeHandle.tsx new/agama/src/components/core/ResizeHandle.tsx --- old/agama/src/components/core/ResizeHandle.tsx 2026-07-14 10:45:10.000000000 +0200 +++ new/agama/src/components/core/ResizeHandle.tsx 2026-07-16 14:18:35.000000000 +0200 @@ -20,6 +20,8 @@ import React, { useCallback, useRef } from "react"; import Icon from "~/components/layout/Icon"; +import type { TranslatedString } from "~/i18n"; + /** Pixels each arrow-key press adds to or removes from the panel height. */ const KEYBOARD_STEP = 32; @@ -35,7 +37,7 @@ */ onStep: (delta: number) => void; /** Accessible label describing what the handle resizes. */ - label: string; + label: TranslatedString; }; /** diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/core/SelectableDataTable.tsx new/agama/src/components/core/SelectableDataTable.tsx --- old/agama/src/components/core/SelectableDataTable.tsx 2026-07-14 10:45:10.000000000 +0200 +++ new/agama/src/components/core/SelectableDataTable.tsx 2026-07-16 14:18:35.000000000 +0200 @@ -207,7 +207,7 @@ * Used as the `aria-label` for the row action menu's trigger to improve * accessibility. */ - itemActionsLabel?: (d: T) => string | string; + itemActionsLabel?: ((d: T) => TranslatedString) | TranslatedString; /** * Custom component to use for rendering the actions cell. When not provided, @@ -216,7 +216,7 @@ * The component will receive the actions for the row and the resolved * accessible label from `itemActionsLabel`. */ - itemActionsComponent?: React.ComponentType<{ items: IAction[]; label: string }>; + itemActionsComponent?: React.ComponentType<{ items: IAction[]; label: TranslatedString }>; /** * Array of currently selected items. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/core/SimpleDropdown.test.tsx new/agama/src/components/core/SimpleDropdown.test.tsx --- old/agama/src/components/core/SimpleDropdown.test.tsx 2026-07-14 10:45:10.000000000 +0200 +++ new/agama/src/components/core/SimpleDropdown.test.tsx 2026-07-16 14:18:35.000000000 +0200 @@ -23,6 +23,7 @@ import React from "react"; import { screen } from "@testing-library/react"; import { installerRender } from "~/test-utils"; +import { _ } from "~/i18n"; import SimpleDropdown from "./SimpleDropdown"; const mockItems = [ @@ -37,18 +38,18 @@ }); it("renders the toggle button with the given label", () => { - installerRender(<SimpleDropdown items={mockItems} label="Actions for 0.0.0160" />); + installerRender(<SimpleDropdown items={mockItems} label={_("Actions for 0.0.0160")} />); screen.getByRole("button", { name: "Actions for 0.0.0160" }); }); it("does not show the menu items initially", () => { - installerRender(<SimpleDropdown items={mockItems} label="Actions for 0.0.0160" />); + installerRender(<SimpleDropdown items={mockItems} label={_("Actions for 0.0.0160")} />); expect(screen.queryByRole("menuitem", { name: "Activate" })).toBeNull(); }); it("shows the menu items when the toggle is clicked", async () => { const { user } = installerRender( - <SimpleDropdown items={mockItems} label="Actions for 0.0.0160" />, + <SimpleDropdown items={mockItems} label={_("Actions for 0.0.0160")} />, ); const toggle = screen.getByRole("button", { name: "Actions for 0.0.0160" }); @@ -61,7 +62,7 @@ it("shows the group label when the menu is open", async () => { const { user } = installerRender( - <SimpleDropdown items={mockItems} label="Actions for 0.0.0160" />, + <SimpleDropdown items={mockItems} label={_("Actions for 0.0.0160")} />, ); await user.click(screen.getByRole("button", { name: "Actions for 0.0.0160" })); screen.getByText("Actions for 0.0.0160"); @@ -69,7 +70,7 @@ it("calls onClick when a menu item is clicked", async () => { const { user } = installerRender( - <SimpleDropdown items={mockItems} label="Actions for 0.0.0160" />, + <SimpleDropdown items={mockItems} label={_("Actions for 0.0.0160")} />, ); await user.click(screen.getByRole("button", { name: "Actions for 0.0.0160" })); await user.click(screen.getByRole("menuitem", { name: "Activate" })); @@ -78,7 +79,7 @@ it("closes the menu after clicking an item", async () => { const { user } = installerRender( - <SimpleDropdown items={mockItems} label="Actions for 0.0.0160" />, + <SimpleDropdown items={mockItems} label={_("Actions for 0.0.0160")} />, ); await user.click(screen.getByRole("button", { name: "Actions for 0.0.0160" })); await user.click(screen.getByRole("menuitem", { name: "Activate" })); @@ -87,7 +88,7 @@ it("renders danger items", async () => { const { user } = installerRender( - <SimpleDropdown items={mockItems} label="Actions for 0.0.0160" />, + <SimpleDropdown items={mockItems} label={_("Actions for 0.0.0160")} />, ); await user.click(screen.getByRole("button", { name: "Actions for 0.0.0160" })); screen.getByRole("menuitem", { name: "Format" }); @@ -98,7 +99,7 @@ const { user } = installerRender( <SimpleDropdown items={mockItems} - label="Actions for 0.0.0160" + label={_("Actions for 0.0.0160")} popperProps={{ position: "left" }} />, ); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/core/SimpleDropdown.tsx new/agama/src/components/core/SimpleDropdown.tsx --- old/agama/src/components/core/SimpleDropdown.tsx 2026-07-14 10:45:10.000000000 +0200 +++ new/agama/src/components/core/SimpleDropdown.tsx 2026-07-16 14:18:35.000000000 +0200 @@ -31,6 +31,8 @@ } from "@patternfly/react-core"; import Icon from "~/components/layout/Icon"; +import type { TranslatedString } from "~/i18n"; + /** * Represents a single action item in a {@link SimpleDropdown} menu. */ @@ -65,7 +67,7 @@ * providing visual and accessible context about which row or entity * the actions belong to. */ - label: string; + label: TranslatedString; /** * Props to pass to the PF Dropdown popper for controlling positioning. * Defaults to `{ position: "right" }` to align the menu to the right diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/form/CheckboxField.test.tsx new/agama/src/components/form/CheckboxField.test.tsx --- old/agama/src/components/form/CheckboxField.test.tsx 2026-07-14 10:45:10.000000000 +0200 +++ new/agama/src/components/form/CheckboxField.test.tsx 2026-07-16 14:18:35.000000000 +0200 @@ -24,6 +24,7 @@ import { screen } from "@testing-library/react"; import { installerRender } from "~/test-utils"; import { useAppForm } from "~/hooks/form"; +import { _ } from "~/i18n"; function CheckboxFieldForm({ initChecked = false }: { initChecked?: boolean }) { const form = useAppForm({ defaultValues: { licenseAccepted: initChecked } }); @@ -31,7 +32,7 @@ return ( <form.AppForm> <form.AppField name="licenseAccepted"> - {(field) => <field.CheckboxField label="Accept license" />} + {(field) => <field.CheckboxField label={_("Accept license")} />} </form.AppField> </form.AppForm> ); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/form/CheckboxField.tsx new/agama/src/components/form/CheckboxField.tsx --- old/agama/src/components/form/CheckboxField.tsx 2026-07-14 10:45:10.000000000 +0200 +++ new/agama/src/components/form/CheckboxField.tsx 2026-07-16 14:18:35.000000000 +0200 @@ -24,14 +24,16 @@ import { Checkbox } from "@patternfly/react-core"; import { useFieldContext } from "~/hooks/form-contexts"; +import type { TranslatedString } from "~/i18n"; + type CheckboxFieldProps = { - label: string; + label: TranslatedString; description?: React.ReactNode; /** * Literal accessible name for the checkbox. Replaces the visible label as the * accessible name; use only when the visible label is not descriptive enough. */ - "aria-label"?: string; + "aria-label"?: TranslatedString; /** * IDs of elements that name the checkbox. Replaces the visible label as the * accessible name; use when other on-screen elements describe it. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/form/Fieldset.test.tsx new/agama/src/components/form/Fieldset.test.tsx --- old/agama/src/components/form/Fieldset.test.tsx 2026-07-14 10:45:10.000000000 +0200 +++ new/agama/src/components/form/Fieldset.test.tsx 2026-07-16 14:18:35.000000000 +0200 @@ -23,12 +23,13 @@ import React from "react"; import { screen } from "@testing-library/react"; import { installerRender } from "~/test-utils"; +import { _ } from "~/i18n"; import { Fieldset } from "./Fieldset"; describe("Fieldset", () => { it("renders the legend", () => { installerRender( - <Fieldset legend="Network Settings"> + <Fieldset legend={_("Network Settings")}> <input type="text" /> </Fieldset>, ); @@ -37,7 +38,7 @@ it("renders children", () => { installerRender( - <Fieldset legend="Settings"> + <Fieldset legend={_("Settings")}> <input aria-label="Test input" /> </Fieldset>, ); @@ -46,7 +47,7 @@ it("renders description when provided", () => { installerRender( - <Fieldset legend="Advanced" description="Configure advanced options"> + <Fieldset legend={_("Advanced")} description="Configure advanced options"> <input type="text" /> </Fieldset>, ); @@ -55,7 +56,7 @@ it("passes through native fieldset attributes", () => { installerRender( - <Fieldset legend="Disabled Fieldset" disabled data-testid="test-fieldset"> + <Fieldset legend={_("Disabled Fieldset")} disabled data-testid="test-fieldset"> <input aria-label="Disabled input" /> </Fieldset>, ); @@ -65,7 +66,7 @@ it("supports className attribute", () => { installerRender( - <Fieldset legend="Styled" className="custom-class" data-testid="styled-fieldset"> + <Fieldset legend={_("Styled")} className="custom-class" data-testid="styled-fieldset"> <input type="text" /> </Fieldset>, ); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/form/Fieldset.tsx new/agama/src/components/form/Fieldset.tsx --- old/agama/src/components/form/Fieldset.tsx 2026-07-14 10:45:10.000000000 +0200 +++ new/agama/src/components/form/Fieldset.tsx 2026-07-16 14:18:35.000000000 +0200 @@ -24,11 +24,13 @@ import NestedContent from "~/components/core/NestedContent"; import Text from "~/components/core/Text"; +import type { TranslatedString } from "~/i18n"; + export interface FieldsetProps extends React.FieldsetHTMLAttributes<HTMLFieldSetElement> { /** * Text displayed as the fieldset legend */ - legend: string; + legend: TranslatedString; /** * Optional ID for the legend element. * diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/form/conventions.md new/agama/src/components/form/conventions.md --- old/agama/src/components/form/conventions.md 2026-07-14 10:45:10.000000000 +0200 +++ new/agama/src/components/form/conventions.md 2026-07-16 14:18:35.000000000 +0200 @@ -41,6 +41,10 @@ - [Why shake Lives in useUpdateConfig, Not the Caller](#why-shake-lives-in-useupdateconfig-not-the-caller) - [Putting It Together](#putting-it-together) - [Trade-offs and When Not to Use This Pattern](#trade-offs-and-when-not-to-use-this-pattern) +- [Derived Suspense Data Without Flicker](#derived-suspense-data-without-flicker) + - [The Problem](#the-problem-1) + - [The Solution: useDeferredValue + a Local Suspense Boundary](#the-solution-usedeferredvalue--a-local-suspense-boundary) + - [When to Use This Pattern](#when-to-use-this-pattern) - [Code Organization](#code-organization) - [Directory Structure](#directory-structure) - [File Naming](#file-naming) @@ -1339,6 +1343,107 @@ --- +## Derived Suspense Data Without Flicker + +`withFrozenQuery` protects a form from refetches of its **initial** data. A +different problem appears when a field derives read-only data from a +`useSuspenseQuery` that is **re-keyed by what the user types**: size hints +solved by the backend, availability checks, previews, etc. + +The running example is the size hint in `shared/SizeFields.tsx`, which solves +min/max sizes for the chosen mount point and filesystem. + +### The Problem + +The derived query key depends on a field value: + +```tsx +// key changes every time the mount point or filesystem changes +useSuspenseQuery({ queryKey: ["solvedStorageModel", JSON.stringify(config)], ... }); +``` + +When the value changes, the key changes, and there is no cached data for the +new key. `useSuspenseQuery` suspends. If the nearest `<Suspense>` boundary is +the page shell (`Page.Content`), React swaps the **entire form** for the +loading fallback and back on every change: the form visibly blinks. + +> Note: `placeholderData: keepPreviousData` (TanStack Query v5's replacement +> for the removed `keepPreviousData: true` option, see the +> [v5 migration guide](https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5#removed-keeppreviousdata-in-favor-of-placeholderdata-identity-function)) +> does not solve this: `useSuspenseQuery` ignores the `placeholderData` option +> (verified against v5.101.2, which hard-codes it to `undefined`) and suspends on every key change anyway. Honoring +> it would mean switching to plain `useQuery` with manual loading handling; the +> pattern below keeps suspense instead. + +### The Solution: useDeferredValue + a Local Suspense Boundary + +Two pieces, each solving one half of the problem: + +1. **`useDeferredValue`** on the inputs that drive the query. The urgent render + uses the previous (cached) value, so it never suspends. React then re-renders + in the background with the new value; if that suspends, React keeps the + already-committed content on screen instead of the fallback. This makes every + change **after the first** show stale data until the fresh data is ready: no + fallback, no blink. + +2. **A local `<Suspense>` boundary** wrapping only the suspending content. The + first time the content mounts there is nothing to defer to, so it does + suspend once. The local boundary contains that first suspension to the small + derived area instead of letting it reach the page shell. + +Isolate the suspending work into its own component so the boundary sits directly +above it (a boundary cannot catch a component suspending in its own body), and +so that sibling fields that do not need the derived data never suspend: + +```tsx +// Only this component consults the solver, and it defers its inputs. +function AutomaticSizeNote({ committedMountPoint, filesystem, useSolvedSizes }) { + const deferredMountPoint = useDeferredValue(committedMountPoint); + const deferredFilesystem = useDeferredValue(filesystem); + const volume = useVolumeTemplate(deferredMountPoint); + const solvedSizes = useSolvedSizes(deferredMountPoint, deferredFilesystem); + // ...render the note from volume + solvedSizes +} + +// The boundary lives right above the suspending component. Fallback is null: +// only the first solve reaches it, and the derived content has no fixed shape +// to placeholder without misrepresenting it. +case SIZE_MODE.AUTO: + return ( + <Suspense fallback={null}> + <AutomaticSizeNote + committedMountPoint={committedMountPoint} + filesystem={filesystem} + useSolvedSizes={useSolvedSizes} + /> + </Suspense> + ); +``` + +Choose the fallback deliberately. Because `useDeferredValue` keeps previous +content on screen for every change after the first, the fallback is only ever +seen once (initial mount). Prefer `null` when the derived content has no fixed +shape; use a skeleton only when the final content has a single, stable layout. + +### When to Use This Pattern + +- Read-only data derived from a `useSuspenseQuery` whose key changes as the user + edits a field, **and** the nearest Suspense boundary wraps enough UI that + re-suspending is visually disruptive. + +Do not reach for it when: + +- The derived query key is stable (it will not re-suspend after the first load). +- The data is editable form state, not read-only derived data (use TanStack Form + listeners and field synchronization instead). + +**References:** + +- React: [`useDeferredValue` — showing stale content while fresh content loads](https://react.dev/reference/react/useDeferredValue#showing-stale-content-while-fresh-content-is-loading) +- React: [`<Suspense>`](https://react.dev/reference/react/Suspense) + +--- + ## Code Organization The following conventions apply to all forms using TanStack Form across the diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/form/validation-helpers.ts new/agama/src/components/form/validation-helpers.ts --- old/agama/src/components/form/validation-helpers.ts 2026-07-14 10:45:10.000000000 +0200 +++ new/agama/src/components/form/validation-helpers.ts 2026-07-16 14:18:35.000000000 +0200 @@ -38,6 +38,8 @@ * */ +import type { TranslatedString } from "~/i18n"; + /** Types */ /** @@ -111,7 +113,10 @@ * requiredString(" ", "Name is required") // "Name is required" * requiredString("foo", "Name is required") // undefined */ -export const requiredString = (value: string, message: string): string | undefined => { +export const requiredString = ( + value: string, + message: TranslatedString, +): TranslatedString | undefined => { return value.trim().length === 0 ? message : undefined; }; @@ -129,8 +134,8 @@ value: number | undefined, min: number, max: number, - message: string, -): string | undefined => { + message: TranslatedString, +): TranslatedString | undefined => { if (value === undefined) return undefined; return value >= min && value <= max ? undefined : message; }; @@ -147,9 +152,9 @@ value: number | undefined, min: number, max: number, - emptyMessage: string, - invalidMessage: string, -): string | undefined => { + emptyMessage: TranslatedString, + invalidMessage: TranslatedString, +): TranslatedString | undefined => { if (value === undefined || value === null) return emptyMessage; return intRange(value, min, max, invalidMessage); }; @@ -166,8 +171,8 @@ value: number | undefined, min: number, max: number, - message: string, -): string | undefined => { + message: TranslatedString, +): TranslatedString | undefined => { return intRange(value, min, max, message); }; @@ -185,9 +190,9 @@ export const requiredValidList = ( value: string[], predicate: (item: string) => boolean, - emptyMessage: string, - invalidMessage: string, -): string | undefined => { + emptyMessage: TranslatedString, + invalidMessage: TranslatedString, +): TranslatedString | undefined => { if (value.length === 0) return emptyMessage; return value.every(predicate) ? undefined : invalidMessage; }; @@ -203,8 +208,8 @@ export const optionalValidList = ( value: string[], predicate: (item: string) => boolean, - invalidMessage: string, -): string | undefined => { + invalidMessage: TranslatedString, +): TranslatedString | undefined => { return value.every(predicate) ? undefined : invalidMessage; }; @@ -219,9 +224,9 @@ export const requiredValidString = ( value: string, predicate: (s: string) => boolean, - emptyMessage: string, - invalidMessage: string, -): string | undefined => { + emptyMessage: TranslatedString, + invalidMessage: TranslatedString, +): TranslatedString | undefined => { if (value.length === 0) return emptyMessage; return predicate(value) ? undefined : invalidMessage; }; @@ -237,8 +242,8 @@ export const optionalValidString = ( value: string, predicate: (s: string) => boolean, - invalidMessage: string, -): string | undefined => { + invalidMessage: TranslatedString, +): TranslatedString | undefined => { if (value === "") return undefined; return predicate(value) ? undefined : invalidMessage; }; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/layout/Header.tsx new/agama/src/components/layout/Header.tsx --- old/agama/src/components/layout/Header.tsx 2026-07-14 10:45:10.000000000 +0200 +++ new/agama/src/components/layout/Header.tsx 2026-07-16 14:18:35.000000000 +0200 @@ -113,7 +113,7 @@ if (product && breadcrumbs && !hideSummaryLink) { items.push({ isEditorial: true, - path: ROOT.overview, + path: ROOT.root, // TRANSLATORS: First breadcrumb item, linking back to the main page // where the whole installation can be reviewed. label: _("Installation"), diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/layout/Loading.test.tsx new/agama/src/components/layout/Loading.test.tsx --- old/agama/src/components/layout/Loading.test.tsx 2026-07-14 10:45:10.000000000 +0200 +++ new/agama/src/components/layout/Loading.test.tsx 2026-07-16 14:18:35.000000000 +0200 @@ -33,7 +33,7 @@ }); it("uses provided aria-label", async () => { - plainRender(<Loading aria-label="Loading something" />); + plainRender(<Loading aria-label={_("Loading something")} />); const icon = await screen.findByLabelText("Loading something"); expect(icon).toHaveRole("progressbar"); }); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/layout/Loading.tsx new/agama/src/components/layout/Loading.tsx --- old/agama/src/components/layout/Loading.tsx 2026-07-14 10:45:10.000000000 +0200 +++ new/agama/src/components/layout/Loading.tsx 2026-07-16 14:18:35.000000000 +0200 @@ -31,7 +31,7 @@ /** Text to be rendered alongside the spinner */ text?: TranslatedString; /** Accessible text, required when rendering only the spinner */ - "aria-label"?: string; + "aria-label"?: TranslatedString; /** Loader style, full screen or inline */ variant?: "full-screen" | "inline"; /** Size for the spinner icon */ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/network/ConnectionsTable.tsx new/agama/src/components/network/ConnectionsTable.tsx --- old/agama/src/components/network/ConnectionsTable.tsx 2026-07-14 10:45:10.000000000 +0200 +++ new/agama/src/components/network/ConnectionsTable.tsx 2026-07-16 14:18:35.000000000 +0200 @@ -383,7 +383,15 @@ }, ].filter(Boolean); }} - itemActionsLabel={(c: Connection) => `Actions for ${c.id}`} + itemActionsLabel={(c: Connection) => + sprintf( + // TRANSLATORS: accessible label for the connection actions toggle + // button. %s is replaced by connection id, e.g. "Actions for + // Ethernet 2" + _("Actions for %s"), + c.id, + ) + } emptyState={ <EmptyState headingLevel="h2" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/network/connection-form/validations.ts new/agama/src/components/network/connection-form/validations.ts --- old/agama/src/components/network/connection-form/validations.ts 2026-07-14 10:45:10.000000000 +0200 +++ new/agama/src/components/network/connection-form/validations.ts 2026-07-16 14:18:35.000000000 +0200 @@ -54,6 +54,8 @@ import { ADDRESS_REQUIRED_MODES, BridgeStpMode, FormIpMode } from "./fields"; import { _, formatList } from "~/i18n"; +import type { TranslatedString } from "~/i18n"; + import type { ValidationResult, FieldsValidationResult, @@ -87,8 +89,8 @@ mode: FormIpModeType, addresses: string[], isValid: (s: string) => boolean, - emptyMessage: string, - invalidMessage: string, + emptyMessage: TranslatedString, + invalidMessage: TranslatedString, ) => ADDRESS_REQUIRED_MODES.includes(mode) ? requiredValidList(addresses, isValid, emptyMessage, invalidMessage) @@ -105,8 +107,8 @@ mode: FormIpModeType, gateway: string, isValid: (s: string) => boolean, - emptyMessage: string, - invalidMessage: string, + emptyMessage: TranslatedString, + invalidMessage: TranslatedString, ) => { if (mode === FormIpMode.MANUAL) return requiredValidString(gateway, isValid, emptyMessage, invalidMessage); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/overview/SystemInformationSection.tsx new/agama/src/components/overview/SystemInformationSection.tsx --- old/agama/src/components/overview/SystemInformationSection.tsx 2026-07-14 10:45:10.000000000 +0200 +++ new/agama/src/components/overview/SystemInformationSection.tsx 2026-07-16 14:18:35.000000000 +0200 @@ -39,9 +39,11 @@ import textStyles from "@patternfly/react-styles/css/utilities/Text/text"; +import type { TranslatedString } from "~/i18n"; + type ItemProps = { /** The label/term for this field */ - label: string; + label: TranslatedString; /** The value/description for this field */ children: React.ReactNode; }; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/questions/RegistrationCertificateQuestion.tsx new/agama/src/components/questions/RegistrationCertificateQuestion.tsx --- old/agama/src/components/questions/RegistrationCertificateQuestion.tsx 2026-07-14 10:45:10.000000000 +0200 +++ new/agama/src/components/questions/RegistrationCertificateQuestion.tsx 2026-07-16 14:18:35.000000000 +0200 @@ -34,10 +34,12 @@ import { Popup } from "~/components/core"; import QuestionActions from "~/components/questions/QuestionActions"; import { _ } from "~/i18n"; + +import type { TranslatedString } from "~/i18n"; import type { AnswerCallback, Question } from "~/model/question"; type QuestionDataProps = { - label: string; + label: TranslatedString; value?: string; }; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/questions/UnsupportedAutoYaST.tsx new/agama/src/components/questions/UnsupportedAutoYaST.tsx --- old/agama/src/components/questions/UnsupportedAutoYaST.tsx 2026-07-14 10:45:10.000000000 +0200 +++ new/agama/src/components/questions/UnsupportedAutoYaST.tsx 2026-07-16 14:18:35.000000000 +0200 @@ -34,6 +34,8 @@ import QuestionActions from "~/components/questions/QuestionActions"; import { sprintf } from "sprintf-js"; import { _ } from "~/i18n"; + +import type { TranslatedString } from "~/i18n"; import type { AnswerCallback, Question } from "~/model/question"; const UnsupportedElements = ({ @@ -42,8 +44,8 @@ description, }: { elements: string[]; - title: string; - description: string; + title: TranslatedString; + description: TranslatedString; }) => { if (elements.length === 0) { return undefined; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/software/SoftwarePage.tsx new/agama/src/components/software/SoftwarePage.tsx --- old/agama/src/components/software/SoftwarePage.tsx 2026-07-14 10:45:10.000000000 +0200 +++ new/agama/src/components/software/SoftwarePage.tsx 2026-07-16 14:18:35.000000000 +0200 @@ -53,6 +53,8 @@ import { isPatternSelected } from "~/utils/software"; import { SOFTWARE as PATHS } from "~/routes/paths"; import { _, n_ } from "~/i18n"; + +import type { TranslatedString } from "~/i18n"; import { PROPOSAL_QUERY_KEY, EXTENDED_CONFIG_QUERY_KEY } from "~/hooks/model/proposal"; import type { Pattern } from "~/model/system/software"; @@ -68,8 +70,8 @@ buttonText, }: { to: string; - body: string; - buttonText: string; + body: TranslatedString; + buttonText: TranslatedString; }) => ( // TRANSLATORS: empty state title for a software section with nothing selected <EmptyState headingLevel="h4" titleText={_("None selected")} variant="sm"> @@ -87,7 +89,7 @@ /** * Informational empty state shown when patterns are not available. */ -const NoAvailable = ({ title, body }: { title: string; body: string }) => ( +const NoAvailable = ({ title, body }: { title: TranslatedString; body: TranslatedString }) => ( <EmptyState headingLevel="h4" titleText={title} variant="sm"> <EmptyStateBody>{body}</EmptyStateBody> </EmptyState> @@ -208,11 +210,11 @@ type SoftwareSectionProps = { /** Section heading. */ - title: string; + title: TranslatedString; /** Optional explanatory text rendered below the heading. */ description?: React.ReactNode; /** Label for the link that opens the pattern selection page. */ - buttonText: string; + buttonText: TranslatedString; /** Total number of patterns in this group, selected or not. */ totalCount: number; /** Patterns in this group that are currently selected. */ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/storage/dasd/DASDTable.tsx new/agama/src/components/storage/dasd/DASDTable.tsx --- old/agama/src/components/storage/dasd/DASDTable.tsx 2026-07-14 10:45:10.000000000 +0200 +++ new/agama/src/components/storage/dasd/DASDTable.tsx 2026-07-16 14:18:35.000000000 +0200 @@ -682,7 +682,12 @@ filterByDevice: true, }) } - itemActionsLabel={(d: Device) => `Actions for ${d.channel}`} + itemActionsLabel={(d: Device) => + // TRANSLATORS: accessible label for the connection actions toggle + // button. %s is replaced by device channel identifier, e.g. "Actions + // for 0.0.0150" + sprintf(_("Actions for %s"), d.channel) + } itemActionsComponent={SimpleDropdown} emptyState={ <EmptyState diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/storage/shared/SizeFields.tsx new/agama/src/components/storage/shared/SizeFields.tsx --- old/agama/src/components/storage/shared/SizeFields.tsx 2026-07-14 10:45:10.000000000 +0200 +++ new/agama/src/components/storage/shared/SizeFields.tsx 2026-07-16 14:18:35.000000000 +0200 @@ -20,7 +20,7 @@ * find current contact information at www.suse.com. */ -import React from "react"; +import React, { Suspense, useDeferredValue } from "react"; import { sprintf } from "sprintf-js"; import { Flex } from "@patternfly/react-core"; import Text from "~/components/core/Text"; @@ -269,6 +269,54 @@ ); /** + * Automatic size note: the only content that depends on the backend size + * solver. + * + * Isolated into its own component (rendered inside a Suspense boundary by + * {@link SizeFieldsContent}) so that solving never suspends unrelated UI. The + * solve is a suspense query keyed by mount point + filesystem, so changing + * either creates a new query with no cached data and would normally trigger the + * nearest Suspense fallback. + * + * useDeferredValue lets React render first with the previous (cached) values, + * so the urgent update does not suspend. The new solve then runs in the + * background while React keeps the previously committed note visible instead of + * showing the Suspense fallback. Only the initial solve (when no cached result + * exists) reaches the fallback. + */ +function AutomaticSizeNote({ + committedMountPoint, + filesystem, + useSolvedSizes, +}: { + committedMountPoint: string; + filesystem: string; + useSolvedSizes: UseSolvedSizes; +}) { + const deferredMountPoint = useDeferredValue(committedMountPoint); + const deferredFilesystem = useDeferredValue(filesystem); + + // Use committedMountPoint (not live mountPoint) to avoid reacting to incomplete input. + // This prevents showing misleading size hints while user types "/ho..." and avoids + // expensive useVolumeTemplate recalculations on every keystroke. + const volume = useVolumeTemplate(deferredMountPoint); + + // Calculate solved sizes - only recalculates when committedMountPoint or filesystem change. + // The host form provides the size-solving logic (partition vs logical volume). + const solvedSizes = useSolvedSizes(deferredMountPoint, deferredFilesystem); + const automaticSizeNote = useAutomaticSizeNote(volume, solvedSizes); + + return ( + <Flex direction={{ default: "column" }} gap={{ default: "gapXs" }}> + <Text isBold textStyle="textColorSubtle"> + {automaticSizeNote.sizeLabel} + </Text> + <Text component="small">{automaticSizeNote.rationale}</Text> + </Flex> + ); +} + +/** * Inner component that renders size mode-specific inputs and info notes. */ const SizeFieldsContent = withForm({ @@ -279,25 +327,21 @@ sizeMode: SIZE_MODE.AUTO, } as SizeFieldsContentProps, render: function Render({ form, committedMountPoint, filesystem, sizeMode, useSolvedSizes }) { - // Use committedMountPoint (not live mountPoint) to avoid reacting to incomplete input. - // This prevents showing misleading size hints while user types "/ho..." and avoids - // expensive useVolumeTemplate recalculations on every keystroke. - const volume = useVolumeTemplate(committedMountPoint); - - // Calculate solved sizes - only recalculates when committedMountPoint or filesystem change. - // The host form provides the size-solving logic (partition vs logical volume). - const solvedSizes = useSolvedSizes(committedMountPoint, filesystem); - const automaticSizeNote = useAutomaticSizeNote(volume, solvedSizes); - switch (sizeMode) { case SIZE_MODE.AUTO: + // Only AUTO consults the backend solver. It renders inside a local + // Suspense boundary so the first solve stays contained here (see + // AutomaticSizeNote, which defers its inputs via useDeferredValue to keep + // the previous note visible on later changes). Fallback is null: only the + // first solve reaches it and the note has no fixed shape to placeholder. return ( - <Flex direction={{ default: "column" }} gap={{ default: "gapXs" }}> - <Text isBold textStyle="textColorSubtle"> - {automaticSizeNote.sizeLabel} - </Text> - <Text component="small">{automaticSizeNote.rationale}</Text> - </Flex> + <Suspense fallback={null}> + <AutomaticSizeNote + committedMountPoint={committedMountPoint} + filesystem={filesystem} + useSolvedSizes={useSolvedSizes} + /> + </Suspense> ); case SIZE_MODE.FIXED: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/storage/zfcp/ZFCPDevicesTable.tsx new/agama/src/components/storage/zfcp/ZFCPDevicesTable.tsx --- old/agama/src/components/storage/zfcp/ZFCPDevicesTable.tsx 2026-07-14 10:45:10.000000000 +0200 +++ new/agama/src/components/storage/zfcp/ZFCPDevicesTable.tsx 2026-07-16 14:18:35.000000000 +0200 @@ -413,7 +413,12 @@ itemActions={(device: System.Device) => buildActions(device, deviceConfig(device), addDevices, removeDevices, checkLunScan) } - itemActionsLabel={(d: System.Device) => `Actions for ${d.lun}`} + itemActionsLabel={(d: System.Device) => + // TRANSLATORS: accessible label for the connection actions toggle + // button. %s is replaced with the zFCP LUN, e.g. "Actions for + // 0x4010403200000000" + sprintf(_("Actions for %s"), d.lun) + } emptyState={ <EmptyState headingLevel="h2" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/users/authentication-form/Form.test.tsx new/agama/src/components/users/authentication-form/Form.test.tsx --- old/agama/src/components/users/authentication-form/Form.test.tsx 2026-07-14 10:45:10.000000000 +0200 +++ new/agama/src/components/users/authentication-form/Form.test.tsx 2026-07-16 14:18:35.000000000 +0200 @@ -190,7 +190,7 @@ expect(mockPutConfig).not.toHaveBeenCalled(); screen.getByText("No changes to apply"); const link = screen.getByRole("link", { name: "installation" }); - expect(link).toHaveAttribute("href", "/overview"); + expect(link).toHaveAttribute("href", "/"); }); it("creates first user when checkbox is checked", async () => { @@ -405,7 +405,7 @@ await screen.findByText("Changes successfully applied"); const link = screen.getByRole("link", { name: "installation" }); - expect(link).toHaveAttribute("href", "/overview"); + expect(link).toHaveAttribute("href", "/"); }); }); }); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/hooks/use-form-submit.tsx new/agama/src/hooks/use-form-submit.tsx --- old/agama/src/hooks/use-form-submit.tsx 2026-07-14 10:45:10.000000000 +0200 +++ new/agama/src/hooks/use-form-submit.tsx 2026-07-16 14:18:35.000000000 +0200 @@ -325,7 +325,7 @@ } > {(linkText) => ( - <Link isInline variant="link" to={ROOT.overview}> + <Link isInline variant="link" to={ROOT.root}> {linkText} </Link> )} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/i18n.ts new/agama/src/i18n.ts --- old/agama/src/i18n.ts 2026-07-14 10:45:10.000000000 +0200 +++ new/agama/src/i18n.ts 2026-07-16 14:18:35.000000000 +0200 @@ -150,11 +150,16 @@ * Wrapper around Intl.ListFormat to get a language-specific representation of the given list of * strings. * - * @param {string[]} list iterable list of strings to represent - * @param {object} options passed to the Intl.ListFormat constructor - * @return {string} concatenation of the original strings with the correct language-specific + * Preserves the input type: formatting a list of {@link TranslatedString} + * values produces a {@link TranslatedString}, while a list of plain strings + * (e.g. device names or mount paths) stays a plain string. + * + * @param list iterable list of strings to represent + * @param options passed to the Intl.ListFormat constructor + * @return concatenation of the original strings with the correct language-specific * separators according to the currently selected language for the Agama UI */ -const formatList = (list, options = {}) => agama.formatList(list, options); +const formatList = <T extends string | TranslatedString>(list: T[], options: object = {}): T => + agama.formatList(list, options) as T; export { _, n_, N_, Nn_, formatList }; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/router.tsx new/agama/src/router.tsx --- old/agama/src/router.tsx 2026-07-14 10:45:10.000000000 +0200 +++ new/agama/src/router.tsx 2026-07-16 14:18:35.000000000 +0200 @@ -52,6 +52,10 @@ // bookmarked URLs and external links to naturally transition. const legacyRedirects = () => [ { + path: "/overview", + element: <Navigate to={PATHS.root} replace />, + }, + { path: "/hostname", element: <Navigate to={SYSTEM.root} replace />, }, @@ -71,10 +75,6 @@ const rootRoutes = () => [ { - path: "/overview", - element: <OverviewPage />, - }, - { path: SYSTEM.root, element: <SystemPage />, handle: { name: N_("System"), icon: "server" }, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/routes/paths.ts new/agama/src/routes/paths.ts --- old/agama/src/routes/paths.ts 2026-07-14 10:45:10.000000000 +0200 +++ new/agama/src/routes/paths.ts 2026-07-16 14:18:35.000000000 +0200 @@ -49,7 +49,6 @@ const ROOT = { root: "/", login: "/login", - overview: "/overview", installation: "/installation", installationProgress: "/installation/progress", installationFinished: "/installation/finished", ++++++ agama.obsinfo ++++++ --- /var/tmp/diff_new_pack.qine92/_old 2026-07-17 18:47:57.962189164 +0200 +++ /var/tmp/diff_new_pack.qine92/_new 2026-07-17 18:47:57.978189703 +0200 @@ -1,5 +1,5 @@ name: agama -version: 22+398.d23cf880d -mtime: 1784018710 -commit: d23cf880d8ccae5e227b611b1b1ece43b8dc3d72 +version: 22+456.ce0fb8baf +mtime: 1784204315 +commit: ce0fb8baf906e6dc037106481ae0640cc743cee2
