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-01-28 15:05:51 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/agama-web-ui (Old) and /work/SRC/openSUSE:Factory/.agama-web-ui.new.1928 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "agama-web-ui" Wed Jan 28 15:05:51 2026 rev:33 rq:1329459 version:0 Changes: -------- --- /work/SRC/openSUSE:Factory/agama-web-ui/agama-web-ui.changes 2026-01-26 10:43:50.687546942 +0100 +++ /work/SRC/openSUSE:Factory/.agama-web-ui.new.1928/agama-web-ui.changes 2026-01-28 15:06:18.655914354 +0100 @@ -1,0 +2,32 @@ +Fri Jan 23 15:34:08 UTC 2026 - Ladislav Slezák <[email protected]> + +- Fixed removing automatically selected recommended patterns + (gh#agama-project/agama#3072) + +------------------------------------------------------------------- +Thu Jan 22 16:19:20 UTC 2026 - Imobach Gonzalez Sosa <[email protected]> + +- Allow registering add-ons (gh#agama-project/agama#3061). +- Improve error reporting in the registration page. +- Do not block the UI when registering the system (bsc#1257105). +- Do not allow to change the product when it is already registered. + +------------------------------------------------------------------- +Thu Jan 22 15:25:25 UTC 2026 - Ancor Gonzalez Sosa <[email protected]> + +- Track state (collapsed/open) of the lists of logical volumes + (gh#agama-project/agama#3066). + +------------------------------------------------------------------- +Thu Jan 22 14:11:19 UTC 2026 - Ancor Gonzalez Sosa <[email protected]> + +- Avoid inconsistent state (collapsed/open) in the lists of + partitions (gh#agama-project/agama#3063). + +------------------------------------------------------------------- +Thu Jan 22 03:57:13 UTC 2026 - David Diaz <[email protected]> + +- Updated top header install button label to clarify navigation to + the overview page (gh#agama-project/agama#3060). + +------------------------------------------------------------------- ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ _service ++++++ --- /var/tmp/diff_new_pack.iJAbv4/_old 2026-01-28 15:06:32.196478600 +0100 +++ /var/tmp/diff_new_pack.iJAbv4/_new 2026-01-28 15:06:32.200478766 +0100 @@ -8,7 +8,7 @@ <param name="scm">git</param> <!-- the revision might be changed to "release" branch or a git tag by the .github/workflows/obs-staging-shared.yml action when submitting to OBS --> - <param name="revision">fix-registration</param> + <param name="revision">master</param> <param name="subdir">web</param> <param name="without-version">enable</param> <param name="extract">package-lock.json</param> ++++++ 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-01-22 08:37:34.000000000 +0100 +++ new/agama/package/agama-web-ui.changes 2026-01-26 15:26:53.000000000 +0100 @@ -1,4 +1,36 @@ ------------------------------------------------------------------- +Fri Jan 23 15:34:08 UTC 2026 - Ladislav Slezák <[email protected]> + +- Fixed removing automatically selected recommended patterns + (gh#agama-project/agama#3072) + +------------------------------------------------------------------- +Thu Jan 22 16:19:20 UTC 2026 - Imobach Gonzalez Sosa <[email protected]> + +- Allow registering add-ons (gh#agama-project/agama#3061). +- Improve error reporting in the registration page. +- Do not block the UI when registering the system (bsc#1257105). +- Do not allow to change the product when it is already registered. + +------------------------------------------------------------------- +Thu Jan 22 15:25:25 UTC 2026 - Ancor Gonzalez Sosa <[email protected]> + +- Track state (collapsed/open) of the lists of logical volumes + (gh#agama-project/agama#3066). + +------------------------------------------------------------------- +Thu Jan 22 14:11:19 UTC 2026 - Ancor Gonzalez Sosa <[email protected]> + +- Avoid inconsistent state (collapsed/open) in the lists of + partitions (gh#agama-project/agama#3063). + +------------------------------------------------------------------- +Thu Jan 22 03:57:13 UTC 2026 - David Diaz <[email protected]> + +- Updated top header install button label to clarify navigation to + the overview page (gh#agama-project/agama#3060). + +------------------------------------------------------------------- Wed Jan 21 10:48:33 UTC 2026 - Imobach Gonzalez Sosa <[email protected]> - Adapt to the changes in the question about trusting diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/App.tsx new/agama/src/App.tsx --- old/agama/src/App.tsx 2026-01-22 08:37:34.000000000 +0100 +++ new/agama/src/App.tsx 2026-01-26 15:26:53.000000000 +0100 @@ -24,7 +24,7 @@ import { Outlet, useLocation } from "react-router"; import { useStatusChanges, useStatus } from "~/hooks/model/status"; import { useSystemChanges } from "~/hooks/model/system"; -import { useProposalChanges } from "~/hooks/model/proposal"; +import { useProposal, useProposalChanges } from "~/hooks/model/proposal"; import { useIssuesChanges } from "~/hooks/model/issue"; import { useProductInfo } from "~/hooks/model/config/product"; import { useQueryClient } from "@tanstack/react-query"; @@ -37,6 +37,13 @@ * necessary before rendering the nested route content via the <Outlet />. */ const Content = () => { + // FIXME: we need to force TanStack query to retrieve the proposal to make + // sure it is refreshed after being invalidated. Related to useProgressTracking + // and useTrackQueriesRefetch. + // + // https://tanstack.com/query/latest/docs/framework/react/guides/query-invalidation + useProposal(); + const location = useLocation(); const product = useProductInfo(); const { progresses, stage } = useStatus(); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/core/ChangeProductOption.tsx new/agama/src/components/core/ChangeProductOption.tsx --- old/agama/src/components/core/ChangeProductOption.tsx 2026-01-22 08:37:34.000000000 +0100 +++ new/agama/src/components/core/ChangeProductOption.tsx 2026-01-26 15:26:53.000000000 +0100 @@ -33,14 +33,13 @@ * DropdownItem Option for navigating to the selection product. */ export default function ChangeProductOption({ children, ...props }: Omit<DropdownItemProps, "to">) { - const { products } = useSystem(); + const { products, software } = useSystem(); const { stage } = useStatus(); - // const registration = useRegistration(); const currentLocation = useLocation(); const to = useHref(PATHS.changeProduct); if (products.length <= 1) return null; - // if (registration?.registered) return null; + if (software?.registration) return null; if (SIDE_PATHS.includes(currentLocation.pathname)) return null; if (stage !== "configuring") return null; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/core/InstallButton.test.tsx new/agama/src/components/core/InstallButton.test.tsx --- old/agama/src/components/core/InstallButton.test.tsx 2026-01-22 08:37:34.000000000 +0100 +++ new/agama/src/components/core/InstallButton.test.tsx 1970-01-01 01:00:00.000000000 +0100 @@ -1,60 +0,0 @@ -/* - * Copyright (c) [2022-2026] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 of the License, or (at your option) - * any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import React from "react"; -import { screen } from "@testing-library/react"; -import { installerRender, mockRoutes } from "~/test-utils"; -import { InstallButton } from "~/components/core"; -import { PRODUCT, ROOT, STORAGE } from "~/routes/paths"; - -describe("InstallButton", () => { - describe("when not in an extended side paths", () => { - beforeEach(() => { - mockRoutes(STORAGE.addPartition); - }); - - it("renders the button with Install label ", () => { - installerRender(<InstallButton />); - screen.getByRole("button", { name: "Install" }); - }); - }); - - describe.each([ - ["overview", ROOT.root], - ["overview (full route)", ROOT.overview], - ["login", ROOT.login], - ["product selection", PRODUCT.changeProduct], - ["installation progress", ROOT.installationProgress], - ["installation finished", ROOT.installationFinished], - ["installation exit", ROOT.installationExit], - ["storage progress", STORAGE.progress], - ])(`when rendering %s screen`, (_, path) => { - beforeEach(() => { - mockRoutes(path); - }); - - it("renders nothing", () => { - const { container } = installerRender(<InstallButton />); - expect(container).toBeEmptyDOMElement(); - }); - }); -}); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/core/InstallButton.tsx new/agama/src/components/core/InstallButton.tsx --- old/agama/src/components/core/InstallButton.tsx 2026-01-22 08:37:34.000000000 +0100 +++ new/agama/src/components/core/InstallButton.tsx 1970-01-01 01:00:00.000000000 +0100 @@ -1,66 +0,0 @@ -/* - * Copyright (c) [2022-2026] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 of the License, or (at your option) - * any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import React, { useId } from "react"; -import { Button, ButtonProps } from "@patternfly/react-core"; -import { useLocation, useNavigate } from "react-router"; -import { EXTENDED_SIDE_PATHS, ROOT } from "~/routes/paths"; -import { _ } from "~/i18n"; - -/** - * A call-to-action button that navigates users to the overview page containing - * the actual installation button. - * - * Despite its name, this component doesn't trigger installation directly. - * Instead, it serves as a prominent navigation element to guide users toward - * the overview page where the real installation button resides. The "Install" - * label is intentionally simple and action-oriented to match user intent. - * - * @todo Refactor component name and behavior - * - Rename to better reflect its navigation purpose - * - Replace route-based visibility logic with explicit prop-based control now - * that pages manage their own layouts - */ -const InstallButton = ( - props: Omit<ButtonProps, "onClick"> & { onClickWithIssues?: () => void }, -) => { - const labelId = useId(); - const navigate = useNavigate(); - const location = useLocation(); - - if (EXTENDED_SIDE_PATHS.includes(location.pathname)) return; - - const navigateToConfirmation = () => navigate(ROOT.overview); - - const { onClickWithIssues, ...buttonProps } = props; - - // TRANSLATORS: The install button label - const buttonText = _("Install"); - - return ( - <Button variant="primary" {...buttonProps} onClick={navigateToConfirmation}> - <span id={labelId}>{buttonText}</span> - </Button> - ); -}; - -export default InstallButton; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/core/ReviewAndInstallButton.test.tsx new/agama/src/components/core/ReviewAndInstallButton.test.tsx --- old/agama/src/components/core/ReviewAndInstallButton.test.tsx 1970-01-01 01:00:00.000000000 +0100 +++ new/agama/src/components/core/ReviewAndInstallButton.test.tsx 2026-01-26 15:26:53.000000000 +0100 @@ -0,0 +1,60 @@ +/* + * Copyright (c) [2022-2026] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { screen } from "@testing-library/react"; +import { installerRender, mockRoutes } from "~/test-utils"; +import { PRODUCT, ROOT, STORAGE } from "~/routes/paths"; +import ReviewAndInstallButton from "./ReviewAndInstallButton"; + +describe("InstallButton", () => { + describe("when not in an extended side paths", () => { + beforeEach(() => { + mockRoutes(STORAGE.addPartition); + }); + + it("renders the button with 'Review and install' label ", () => { + installerRender(<ReviewAndInstallButton />); + screen.getByRole("button", { name: "Review and install" }); + }); + }); + + describe.each([ + ["overview", ROOT.root], + ["overview (full route)", ROOT.overview], + ["login", ROOT.login], + ["product selection", PRODUCT.changeProduct], + ["installation progress", ROOT.installationProgress], + ["installation finished", ROOT.installationFinished], + ["installation exit", ROOT.installationExit], + ["storage progress", STORAGE.progress], + ])(`when rendering %s screen`, (_, path) => { + beforeEach(() => { + mockRoutes(path); + }); + + it("renders nothing", () => { + const { container } = installerRender(<ReviewAndInstallButton />); + expect(container).toBeEmptyDOMElement(); + }); + }); +}); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/core/ReviewAndInstallButton.tsx new/agama/src/components/core/ReviewAndInstallButton.tsx --- old/agama/src/components/core/ReviewAndInstallButton.tsx 1970-01-01 01:00:00.000000000 +0100 +++ new/agama/src/components/core/ReviewAndInstallButton.tsx 2026-01-26 15:26:53.000000000 +0100 @@ -0,0 +1,63 @@ +/* + * Copyright (c) [2022-2026] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { Button, ButtonProps } from "@patternfly/react-core"; +import { useLocation, useNavigate } from "react-router"; +import { EXTENDED_SIDE_PATHS, ROOT } from "~/routes/paths"; +import { _ } from "~/i18n"; + +/** + * A call-to-action button that directs users to the overview page, which + * contains the actual installation button. + * + * This button does not trigger installation directly. Instead, it serves as a + * navigation element that guides users to the overview page where they can + * review installation details before proceeding. The label "Review and Install" + * is intentional, indicating that users will first be presented with a summary + * screen before they can proceed with the installation. + * + * @todo Refactor component behavior + * - Replace route-based visibility logic with explicit prop-based control now + * that pages manage their own layouts + */ +export default function ReviewAndInstallButton( + props: Omit<ButtonProps, "onClick"> & { onClickWithIssues?: () => void }, +) { + const navigate = useNavigate(); + const location = useLocation(); + + if (EXTENDED_SIDE_PATHS.includes(location.pathname)) return; + + const navigateToConfirmation = () => navigate(ROOT.overview); + + const { onClickWithIssues, ...buttonProps } = props; + + // TRANSLATORS: The review and install button label + const buttonText = _("Review and install"); + + return ( + <Button {...buttonProps} onClick={navigateToConfirmation}> + {buttonText} + </Button> + ); +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/core/index.ts new/agama/src/components/core/index.ts --- old/agama/src/components/core/index.ts 2026-01-22 08:37:34.000000000 +0100 +++ new/agama/src/components/core/index.ts 2026-01-26 15:26:53.000000000 +0100 @@ -27,7 +27,7 @@ export { default as InstallationFinished } from "./InstallationFinished"; export { default as InstallationProgress } from "./InstallationProgress"; export { default as InstallationExit } from "./InstallationExit"; -export { default as InstallButton } from "./InstallButton"; +export { default as ReviewAndInstallButton } from "./ReviewAndInstallButton"; export { default as IssuesAlert } from "./IssuesAlert"; export { default as ListSearch } from "./ListSearch"; export { default as LoginPage } from "./LoginPage"; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/layout/Header.test.tsx new/agama/src/components/layout/Header.test.tsx --- old/agama/src/components/layout/Header.test.tsx 2026-01-22 08:37:34.000000000 +0100 +++ new/agama/src/components/layout/Header.test.tsx 2026-01-26 15:26:53.000000000 +0100 @@ -55,7 +55,9 @@ }; jest.mock("~/components/core/InstallerOptions", () => () => <div>Installer Options Mock</div>); -jest.mock("~/components/core/InstallButton", () => () => <div>Install Button Mock</div>); +jest.mock("~/components/core/ReviewAndInstallButton", () => () => ( + <div>ReviewAndInstall Button Mock</div> +)); jest.mock("~/hooks/model/system", () => ({ ...jest.requireActual("~/hooks/model/system"), @@ -85,7 +87,7 @@ it("mounts the Install button", () => { plainRender(<Header />); - screen.getByText("Install Button Mock"); + screen.getByText("ReviewAndInstall Button Mock"); }); it("mounts installer options by default", () => { 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-01-22 08:37:34.000000000 +0100 +++ new/agama/src/components/layout/Header.tsx 2026-01-26 15:26:53.000000000 +0100 @@ -37,7 +37,12 @@ ToolbarItem, } from "@patternfly/react-core"; import { Icon } from "~/components/layout"; -import { ChangeProductOption, InstallerOptions, InstallButton, SkipTo } from "~/components/core"; +import { + ChangeProductOption, + InstallerOptions, + ReviewAndInstallButton, + SkipTo, +} from "~/components/core"; import ProgressStatusMonitor from "~/components/core/ProgressStatusMonitor"; import Breadcrumbs from "~/components/core/Breadcrumbs"; import { useProductInfo } from "~/hooks/model/config/product"; @@ -166,7 +171,7 @@ </ToolbarItem> )} <ToolbarItem> - <InstallButton /> + <ReviewAndInstallButton /> </ToolbarItem> <ToolbarItem> <OptionsDropdown /> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/product/ProductRegistrationPage.test.tsx new/agama/src/components/product/ProductRegistrationPage.test.tsx --- old/agama/src/components/product/ProductRegistrationPage.test.tsx 2026-01-22 08:37:34.000000000 +0100 +++ new/agama/src/components/product/ProductRegistrationPage.test.tsx 2026-01-26 15:26:53.000000000 +0100 @@ -22,10 +22,14 @@ import React from "react"; import { screen, within } from "@testing-library/react"; -import { installerRender } from "~/test-utils"; +import { installerRender, mockProduct, mockProductConfig } from "~/test-utils"; import ProductRegistrationPage from "./ProductRegistrationPage"; -import { AddonInfo, Product, RegisteredAddonInfo, RegistrationInfo } from "~/types/software"; -import { useAddons, useProduct, useRegisteredAddons, useRegistration } from "~/queries/software"; +import { Product } from "~/model/system"; +import { RegistrationInfo } from "~/model/system/software"; +import { Config } from "~/model/config"; +import { putConfig } from "~/api"; +import { Issue } from "~/model/issue"; +import { cloneDeep } from "radashi"; const tw: Product = { id: "Tumbleweed", @@ -39,57 +43,74 @@ registration: true, }; -let selectedProduct: Product; -let staticHostnameMock: string; -let registrationInfoMock: RegistrationInfo; -let addonInfoMock: AddonInfo[] = []; -let registeredAddonInfoMock: RegisteredAddonInfo[] = []; -const registerMutationMock = jest.fn(); - -jest.mock("~/queries/software", () => ({ - ...jest.requireActual("~/queries/software"), - useRegisterMutation: () => ({ mutate: registerMutationMock }), - useRegistration: (): ReturnType<typeof useRegistration> => registrationInfoMock, - useAddons: (): ReturnType<typeof useAddons> => addonInfoMock, - useRegisteredAddons: (): ReturnType<typeof useRegisteredAddons> => registeredAddonInfoMock, - useProduct: (): ReturnType<typeof useProduct> => { - return { - products: [tw, sle], - selectedProduct, - }; - }, +let mockSelectedProduct: Product | undefined; +let mockStaticHostname: string; +let mockRegistrationInfo: RegistrationInfo | undefined; +let mockConfig: Config; +let mockIssues: Issue[] = []; + +jest.mock("~/hooks/model/system", () => ({ + useSystem: () => ({ l10n: { locale: "en_US" } }), +})); + +jest.mock("~/hooks/model/system/software", () => ({ + useSystem: () => ({ registration: mockRegistrationInfo }), })); -jest.mock("~/hooks/model/proposal/hostname", () => ({ - ...jest.requireActual("~/hooks/model/proposal"), - useHostname: () => ({ transient: "testing-node", static: staticHostnameMock }), +jest.mock("~/hooks/model/config", () => ({ + useConfig: () => mockConfig, })); -it.todo("Adapt test to new hooks"); -describe.skip("ProductRegistrationPage", () => { +jest.mock("~/hooks/model/issue", () => ({ + useIssues: () => mockIssues, +})); + +jest.mock("~/api", () => ({ + putConfig: jest.fn(), + patchConfig: jest.fn(), +})); + +jest.mock("~/hooks/model/proposal", () => ({ + useProposal: () => ({ + hostname: { hostname: "testing-node", static: mockStaticHostname }, + }), +})); + +describe("ProductRegistrationPage", () => { + beforeEach(() => { + mockConfig = { product: { id: "sle", registrationCode: "" } }; + mockIssues = []; + mockProductConfig(mockConfig.product); + // @ts-ignore + mockProduct(mockSelectedProduct); + }); + describe("when the selected product is not registrable", () => { beforeEach(() => { - selectedProduct = tw; - registrationInfoMock = { registered: false, key: "", email: "", url: "" }; + mockSelectedProduct = tw; + // @ts-ignore + mockProduct(mockSelectedProduct); + mockRegistrationInfo = undefined; }); - it("renders nothing", () => { - const { container } = installerRender(<ProductRegistrationPage />, { withL10n: true }); - expect(container).toBeEmptyDOMElement(); + it("renders the registration page", () => { + installerRender(<ProductRegistrationPage />, { withL10n: true }); + screen.getByText("Registration"); }); }); describe("when the selected product is registrable and not yet registered", () => { beforeEach(() => { - selectedProduct = sle; - registrationInfoMock = { registered: false, key: "", email: "", url: "" }; + mockSelectedProduct = sle; + // @ts-ignore + mockProduct(mockSelectedProduct); + mockRegistrationInfo = undefined; }); describe("and the static hostname is not set", () => { it("renders a custom alert using the transient hostname", () => { installerRender(<ProductRegistrationPage />, { withL10n: true }); - screen.getByText("Custom alert:"); screen.getByText('The product will be registered with "testing-node" hostname'); screen.getByRole("link", { name: "hostname" }); }); @@ -97,13 +118,12 @@ describe("and the static hostname is set", () => { beforeEach(() => { - staticHostnameMock = "testing-server"; + mockStaticHostname = "testing-server"; }); it("renders a custom alert using the static hostname", () => { installerRender(<ProductRegistrationPage />, { withL10n: true }); - screen.getByText("Custom alert:"); screen.getByText('The product will be registered with "testing-server" hostname'); screen.getByRole("link", { name: "hostname" }); }); @@ -118,14 +138,15 @@ await user.click(submitButton); - expect(registerMutationMock).toHaveBeenCalledWith( - { - url: "", - key: "INTERNAL-USE-ONLY-1234-5678", - email: "", + expect(putConfig).toHaveBeenCalledWith({ + ...mockConfig, + product: { + id: "sle", + registrationCode: "INTERNAL-USE-ONLY-1234-5678", + registrationEmail: undefined, + registrationUrl: undefined, }, - expect.anything(), - ); + }); }); it("allows registering the product with an email address", async () => { @@ -145,14 +166,15 @@ await user.click(submitButton); - expect(registerMutationMock).toHaveBeenCalledWith( - { - url: "", - email: "[email protected]", - key: "INTERNAL-USE-ONLY-1234-5678", + expect(putConfig).toHaveBeenCalledWith({ + ...mockConfig, + product: { + id: "sle", + registrationCode: "INTERNAL-USE-ONLY-1234-5678", + registrationEmail: "[email protected]", + registrationUrl: undefined, }, - expect.anything(), - ); + }); }); it("renders an error when email input is enabled but left empty", async () => { @@ -167,7 +189,7 @@ await user.click(provideEmailCheckbox); await user.click(submitButton); - expect(registerMutationMock).not.toHaveBeenCalled(); + expect(putConfig).not.toHaveBeenCalled(); screen.getByText("Warning alert:"); screen.getByText("Enter an email"); }); @@ -183,14 +205,15 @@ const submitButton = screen.getByRole("button", { name: "Register" }); await user.click(submitButton); - expect(registerMutationMock).toHaveBeenCalledWith( - { - url: "https://custom-server.test", - key: "", - email: "", + expect(putConfig).toHaveBeenCalledWith({ + ...mockConfig, + product: { + id: "sle", + registrationUrl: "https://custom-server.test", + registrationCode: undefined, + registrationEmail: undefined, }, - expect.anything(), - ); + }); }); describe("if registering with the default server", () => { @@ -200,7 +223,7 @@ await user.click(submitButton); - expect(registerMutationMock).not.toHaveBeenCalled(); + expect(putConfig).not.toHaveBeenCalled(); screen.getByText("Warning alert:"); screen.getByText("Enter a registration code"); }); @@ -218,7 +241,7 @@ const submitButton = screen.getByRole("button", { name: "Register" }); await user.click(submitButton); - expect(registerMutationMock).not.toHaveBeenCalled(); + expect(putConfig).not.toHaveBeenCalled(); screen.getByText("Warning alert:"); screen.getByText("Enter a server URL"); }); @@ -243,14 +266,15 @@ const submitButton = screen.getByRole("button", { name: "Register" }); await user.click(submitButton); - expect(registerMutationMock).toHaveBeenCalledWith( - { - url: "https://custom-server.test", - key: "INTERNAL-USE-ONLY-1234-5678", - email: "", + expect(putConfig).toHaveBeenCalledWith({ + ...mockConfig, + product: { + id: "sle", + registrationUrl: "https://custom-server.test", + registrationCode: "INTERNAL-USE-ONLY-1234-5678", + registrationEmail: undefined, }, - expect.anything(), - ); + }); expect(screen.queryByText("Enter a registration code")).toBeNull(); }); @@ -271,33 +295,23 @@ const submitButton = screen.getByRole("button", { name: "Register" }); await user.click(submitButton); - expect(registerMutationMock).not.toHaveBeenCalled(); + expect(putConfig).not.toHaveBeenCalled(); screen.getByText("Warning alert:"); screen.queryByText("Enter a registration code."); }); }); }); - // Marked as pending until know how to properly trigger the mutation#onError - xit("handles and renders errors returned by the registration server", async () => { - registerMutationMock.mockRejectedValue({ - response: { data: { message: "Unauthorized code" } }, - }); - - const { user } = installerRender(<ProductRegistrationPage />, { withL10n: true }); - const registrationCodeInput = screen.getByLabelText("Registration code"); - const submitButton = screen.getByRole("button", { name: "Register" }); - await user.type(registrationCodeInput, "INTERNAL-USE-ONLY-1234-5678"); - await user.click(submitButton); - - expect(registerMutationMock).toHaveBeenCalledWith( + it("handles and renders errors returned by the registration server", async () => { + mockIssues = [ { - url: "", - email: "", - key: "INTERNAL-USE-ONLY-1234-5678", + scope: "software", + class: "system_registration_failed", + description: "Unauthorized code", }, - expect.anything(), - ); + ]; + + installerRender(<ProductRegistrationPage />, { withL10n: true }); screen.getByText("Warning alert:"); screen.getByText("Unauthorized code"); @@ -306,12 +320,13 @@ describe("when selected product is registrable and already registered", () => { beforeEach(() => { - selectedProduct = sle; - registrationInfoMock = { - registered: true, - key: "INTERNAL-USE-ONLY-1234-5678", + mockSelectedProduct = sle; + // @ts-ignore + mockProduct(mockSelectedProduct); + mockRegistrationInfo = { + code: "INTERNAL-USE-ONLY-1234-5678", email: "[email protected]", - url: "", + addons: [], }; }); @@ -333,11 +348,11 @@ describe("if registered with a custom server", () => { beforeEach(() => { - registrationInfoMock = { - registered: true, - key: "INTERNAL-USE-ONLY-1234-5678", + mockRegistrationInfo = { + code: "INTERNAL-USE-ONLY-1234-5678", email: "[email protected]", url: "https://custom-server.test", + addons: [], }; }); @@ -366,11 +381,10 @@ describe("if not using a resgistration code", () => { beforeEach(() => { - registrationInfoMock = { - registered: true, - key: "", + mockRegistrationInfo = { + code: "", email: "", - url: "", + addons: [], }; }); @@ -390,11 +404,10 @@ describe("if no email address is provided", () => { beforeEach(() => { - registrationInfoMock = { - registered: true, - key: "", + mockRegistrationInfo = { + code: "", email: "", - url: "", + addons: [], }; }); @@ -407,21 +420,26 @@ describe("when extensions are available", () => { beforeEach(() => { - addonInfoMock = [ - { - id: "sle-ha", - version: "16.0", - label: "SUSE Linux Enterprise High Availability Extension 16.0 x86_64 (BETA)", - available: true, - free: false, - recommended: false, - description: "SUSE Linux High Availability Extension provides...", - release: "beta", - registration: { - status: "notRegistered", + mockRegistrationInfo = { + code: "INTERNAL-USE-ONLY-1234-5678", + email: "[email protected]", + addons: [ + { + id: "sle-ha", + version: "16.0", + status: "available", + label: "SUSE Linux Enterprise High Availability Extension 16.0 x86_64 (BETA)", + available: true, + free: false, + recommended: false, + description: "SUSE Linux High Availability Extension provides...", + release: "beta", + registration: { + status: "notRegistered", + }, }, - }, - ]; + ], + }; }); it("renders them", async () => { @@ -432,10 +450,10 @@ name: /SUSE Linux Enterprise High Availability Extension 16.0 x86_64/, level: 4, }); - const extensionNode = title.parentElement; + const extensionNode = title.parentElement!; // description is displayed - within(extensionNode).getByText(addonInfoMock[0].description); + within(extensionNode).getByText(mockRegistrationInfo!.addons[0].description); // registration input field is displayed within(extensionNode).getByLabelText("Registration code"); @@ -446,13 +464,9 @@ describe("and they are registered", () => { beforeEach(() => { - registeredAddonInfoMock = [ - { - id: "sle-ha", - version: "16.0", - registrationCode: "INTERNAL-USE-ONLY-1234-ad42", - }, - ]; + const addons = cloneDeep(mockRegistrationInfo!.addons); + addons[0].registration = { status: "registered", code: "INTERNAL-USE-ONLY-1234-ad42" }; + mockRegistrationInfo!.addons = addons; }); it("renders them with its registration code partially hidden", async () => { @@ -465,12 +479,12 @@ // only the end of the code is displayed screen.getByText(/\*+ad42/); // not the full code - expect(screen.queryByText(registeredAddonInfoMock[0].registrationCode)).toBeNull(); + expect(screen.queryByText("INTERNAL-USE-ONLY-1234-ad42")).toBeNull(); // after pressing the "Show" button await user.click(visibilityCodeToggler); // the full code is visible - screen.getByText(registeredAddonInfoMock[0].registrationCode); + screen.getByText("INTERNAL-USE-ONLY-1234-ad42"); }); }); }); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/product/ProductRegistrationPage.tsx new/agama/src/components/product/ProductRegistrationPage.tsx --- old/agama/src/components/product/ProductRegistrationPage.tsx 2026-01-22 08:37:34.000000000 +0100 +++ new/agama/src/components/product/ProductRegistrationPage.tsx 2026-01-26 15:26:53.000000000 +0100 @@ -61,9 +61,9 @@ import { useProduct, useProductInfo } from "~/hooks/model/config/product"; import { useIssues } from "~/hooks/model/issue"; import { patchConfig, putConfig } from "~/api"; -import { Issue } from "~/model/issue"; +import type { Issue } from "~/model/issue"; +import type { Addon } from "~/model/config/product"; import { useConfig } from "~/hooks/model/config"; -import { Addon } from "~/model/config/product"; const FORM_ID = "productRegistration"; const SERVER_LABEL = N_("Registration server"); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/product/ProductSelectionPage.tsx new/agama/src/components/product/ProductSelectionPage.tsx --- old/agama/src/components/product/ProductSelectionPage.tsx 2026-01-22 08:37:34.000000000 +0100 +++ new/agama/src/components/product/ProductSelectionPage.tsx 2026-01-26 15:26:53.000000000 +0100 @@ -298,6 +298,30 @@ isSubmitted: boolean; }; +const ProductFormLabel = ({ products, currentProduct }) => { + // Calculate the number of available products, excluding the current product if selected + const availableProductCount = currentProduct ? products.length - 1 : products.length; + + // TODO: Refactor once mode selection is implemented + // When mode selection is added, check if there is only one product left, + // and if so, handle the display to allow switching between modes for that product. + // + // if (availableProductCount === 0) { + // return sprintf(_("Switch to one of %d other modes"), products[0].modes.length); + // } + + return sprintf( + n_( + "Switch to another available product", + // TODO: One modes is implemented, the label should reflect switching to + // available products or their modes + "Choose from %d available products", + availableProductCount, + ), + availableProductCount, + ); +}; + /** * Form for selecting a product. * @@ -323,17 +347,15 @@ }; return ( - <Form id="productSelectionForm" onSubmit={onFormSubmission}> + <Form + id="productSelectionForm" + onSubmit={onFormSubmission} + // @ts-expect-error: https://www.codegenes.net/blog/error-when-using-inert-attribute-with-typescript/ + inert={isSubmitted ? "" : undefined} + > <FormGroup role="radiogroup" - label={sprintf( - n_( - "Switch to other available product", - "Choose from %d available products", - products.length - 1, - ), - products.length - 1, - )} + label={<ProductFormLabel products={products} currentProduct={currentProduct} />} > <List isPlain> {products.map((product, index) => { @@ -368,13 +390,14 @@ isDisabled={isSelectionDisabled} isLoading={isSubmitted} variant={isSubmitted ? "secondary" : "primary"} + style={{ maxInlineSize: "30dvw", overflow: "hidden", textWrap: "balance" }} > <ProductFormSubmitLabel currentProduct={currentProduct} selectedProduct={selectedProduct} /> </Page.Submit> - {currentProduct && ( + {currentProduct && !isSubmitted && ( <Link to={ROOT.overview} size="lg" variant="link"> {_("Cancel")} </Link> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/product/RegistrationExtension.tsx new/agama/src/components/product/RegistrationExtension.tsx --- old/agama/src/components/product/RegistrationExtension.tsx 2026-01-22 08:37:34.000000000 +0100 +++ new/agama/src/components/product/RegistrationExtension.tsx 2026-01-26 15:26:53.000000000 +0100 @@ -35,8 +35,8 @@ import { mask } from "~/utils"; import { _ } from "~/i18n"; import RegistrationCodeInput from "./RegistrationCodeInput"; -import { Issue } from "~/model/issue"; -import { Addon } from "~/model/config/product"; +import type { Issue } from "~/model/issue"; +import type { Addon } from "~/model/config/product"; import { isEmpty } from "radashi"; /** diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/software/SoftwarePatternsSelection.tsx new/agama/src/components/software/SoftwarePatternsSelection.tsx --- old/agama/src/components/software/SoftwarePatternsSelection.tsx 2026-01-22 08:37:34.000000000 +0100 +++ new/agama/src/components/software/SoftwarePatternsSelection.tsx 2026-01-26 15:26:53.000000000 +0100 @@ -115,13 +115,17 @@ .filter((p) => selection[p.name] === SelectedBy.USER && p.name !== name) .map((p) => p.name); const remove = patterns - .filter((p) => selection[p.name] === SelectedBy.NONE && p.name !== name) + .filter((p) => selection[p.name] === SelectedBy.REMOVED && p.name !== name) .map((p) => p.name); if (selected) { add.push(name); } else { - remove.push(name); + // add the pattern to the "remove" list only if it was autoselected by dependencies, otherwise + // it was selected by user and it is enough to remove it from the "add" list above + if (selection[name] === SelectedBy.AUTO) { + remove.push(name); + } } patchConfig({ software: { patterns: { add, remove } } }); @@ -141,7 +145,7 @@ // TODO: extract to a DataListSelector component or so. const selector = sortGroups(groups).map((groupName) => { const selectedIds = groups[groupName] - .filter((p) => selection[p.name] !== SelectedBy.NONE) + .filter((p) => [SelectedBy.USER, SelectedBy.AUTO].includes(selection[p.name])) .map((p) => p.name); return ( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/storage/PartitionsSection.tsx new/agama/src/components/storage/PartitionsSection.tsx --- old/agama/src/components/storage/PartitionsSection.tsx 2026-01-22 08:37:34.000000000 +0100 +++ new/agama/src/components/storage/PartitionsSection.tsx 2026-01-26 15:26:53.000000000 +0100 @@ -34,7 +34,11 @@ ExpandableSectionToggle, ExpandableSectionProps, } from "@patternfly/react-core"; -import { useStorageUiState } from "~/context/storage-ui-state"; +import { + useStorageUiState, + isExpandedInState, + toggleExpandedInState, +} from "~/context/storage-ui-state"; import Text from "~/components/core/Text"; import MenuButton from "~/components/core/MenuButton"; import MountPathMenuItem from "~/components/storage/MountPathMenuItem"; @@ -49,7 +53,6 @@ import { IconProps } from "../layout/Icon"; import { sprintf } from "sprintf-js"; import spacingStyles from "@patternfly/react-styles/css/utilities/Spacing/spacing"; -import { toggle } from "radashi"; import configModel from "~/model/storage/config-model"; import type { ConfigModel } from "~/model/storage/config-model"; @@ -223,17 +226,12 @@ const contentId = useId(); const device = usePartitionable(collection, index); const uiIndex = `${collection[0]}${index}`; - const expanded = uiState.get("expanded")?.split(","); - const isExpanded = expanded?.includes(uiIndex); + const isExpanded = isExpandedInState(uiState, uiIndex); const newPartitionPath = generateEncodedPath(PATHS.addPartition, { collection, index }); const hasPartitions = device.partitions.some((p) => p.mountPath); const onToggle = () => { - setUiState((state) => { - const nextExpanded = toggle(expanded, uiIndex); - state.set("expanded", nextExpanded.join(",")); - return state; - }); + setUiState((state) => toggleExpandedInState(state, uiIndex)); }; const iconName: IconProps["name"] = isExpanded ? "unfold_less" : "unfold_more"; const commonProps: Pick<ExpandableSectionProps, "toggleId" | "contentId" | "isExpanded"> = { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/storage/VolumeGroupEditor.tsx new/agama/src/components/storage/VolumeGroupEditor.tsx --- old/agama/src/components/storage/VolumeGroupEditor.tsx 2026-01-22 08:37:34.000000000 +0100 +++ new/agama/src/components/storage/VolumeGroupEditor.tsx 2026-01-26 15:26:53.000000000 +0100 @@ -20,7 +20,7 @@ * find current contact information at www.suse.com. */ -import React, { forwardRef, useId, useState } from "react"; +import React, { forwardRef, useId } from "react"; import { Button, Content, @@ -38,6 +38,11 @@ Title, } from "@patternfly/react-core"; import { useNavigate } from "react-router"; +import { + useStorageUiState, + isExpandedInState, + toggleExpandedInState, +} from "~/context/storage-ui-state"; import * as partitionUtils from "~/components/storage/utils/partition"; import { NestedContent } from "../core"; import Text from "~/components/core/Text"; @@ -252,10 +257,15 @@ const LogicalVolumes = ({ vg }: { vg: ConfigModel.VolumeGroup }) => { const toggleId = useId(); const contentId = useId(); - const [isExpanded, setIsExpanded] = useState(false); + const { uiState, setUiState } = useStorageUiState(); + const uiIndex = `vg${vg.vgName}`; + const isExpanded = isExpandedInState(uiState, uiIndex); const menuAriaLabel = sprintf(_("Logical volumes for %s"), vg.vgName); - const toggle = () => setIsExpanded(!isExpanded); + const onToggle = () => { + setUiState((state) => toggleExpandedInState(state, uiIndex)); + }; + const iconName: IconProps["name"] = isExpanded ? "unfold_less" : "unfold_more"; const commonProps: Pick<ExpandableSectionProps, "toggleId" | "contentId" | "isExpanded"> = { toggleId, @@ -277,7 +287,7 @@ <Flex direction={{ default: "column" }}> <ExpandableSectionToggle {...commonProps} - onToggle={toggle} + onToggle={onToggle} className="no-default-icon" style={{ marginBlock: 0 }} > diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/context/storage-ui-state.tsx new/agama/src/context/storage-ui-state.tsx --- old/agama/src/context/storage-ui-state.tsx 2026-01-22 08:37:34.000000000 +0100 +++ new/agama/src/context/storage-ui-state.tsx 2026-01-26 15:26:53.000000000 +0100 @@ -21,6 +21,7 @@ */ import React, { useCallback, useState } from "react"; +import { toggle } from "radashi"; const StorageUiStateContext = React.createContext(null); @@ -94,4 +95,18 @@ ); } -export { StorageUiStateProvider, useStorageUiState }; +/* Check whether an element is marked as expanded in the given state */ +function isExpandedInState(uiState, uiIndex) { + const expanded = uiState.get("expanded")?.split(","); + return !!expanded?.includes(uiIndex); +} + +/* Change the information about an element in the given state */ +function toggleExpandedInState(uiState, uiIndex) { + const expanded = uiState.get("expanded")?.split(","); + const nextExpanded = toggle(expanded, uiIndex); + uiState.set("expanded", nextExpanded.join(",")); + return uiState; +} + +export { StorageUiStateProvider, useStorageUiState, isExpandedInState, toggleExpandedInState }; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/model/proposal/software.ts new/agama/src/model/proposal/software.ts --- old/agama/src/model/proposal/software.ts 2026-01-22 08:37:34.000000000 +0100 +++ new/agama/src/model/proposal/software.ts 2026-01-26 15:26:53.000000000 +0100 @@ -36,6 +36,8 @@ AUTO = "auto", /** No selected */ NONE = "none", + /** Explicitly removed by user */ + REMOVED = "removed", } export type { Proposal, PatternsSelection }; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/model/system/software.ts new/agama/src/model/system/software.ts --- old/agama/src/model/system/software.ts 2026-01-22 08:37:34.000000000 +0100 +++ new/agama/src/model/system/software.ts 2026-01-26 15:26:53.000000000 +0100 @@ -75,12 +75,6 @@ registration: AddonRegistered | AddonUnregistered; }; -type AddonConfig = { - id: string; - version?: string; - registrationCode?: string; -}; - type AddonRegistered = { status: "registered"; code?: string; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/test-utils.tsx new/agama/src/test-utils.tsx --- old/agama/src/test-utils.tsx 2026-01-22 08:37:34.000000000 +0100 +++ new/agama/src/test-utils.tsx 2026-01-26 15:26:53.000000000 +0100 @@ -43,6 +43,7 @@ import { Question } from "~/model/question"; import type { Product } from "~/types/software"; +import type { Config as ProductConfig } from "~/model/config/product"; /** * Internal mock for manipulating routes, using ["/"] by default @@ -167,6 +168,8 @@ registration: false, }); +const mockUseProduct = jest.fn().mockReturnValue(null); + /** * Allows mocking useProductInfo for testing purpose * @@ -181,8 +184,15 @@ */ const mockProduct = (product: Product) => mockUseProductInfo.mockReturnValue(product); +/** + * Allows mocking useProduct for testing purpose + */ +const mockProductConfig = (product: ProductConfig | null) => + mockUseProduct.mockReturnValue(product); + jest.mock("~/hooks/model/config/product", () => ({ useProductInfo: () => mockUseProductInfo(), + useProduct: () => mockUseProduct(), })); /** @@ -383,5 +393,6 @@ mockProgresses, mockStage, mockProduct, + mockProductConfig, mockQuestions, }; ++++++ agama.obsinfo ++++++ --- /var/tmp/diff_new_pack.iJAbv4/_old 2026-01-28 15:06:33.756543609 +0100 +++ /var/tmp/diff_new_pack.iJAbv4/_new 2026-01-28 15:06:33.760543776 +0100 @@ -1,5 +1,5 @@ name: agama -version: 19.pre+1110.2ad621a9f -mtime: 1769067454 -commit: 2ad621a9fc69a13f71732e29131becf58240543e +version: 19.pre+1189.efc3a4978 +mtime: 1769437613 +commit: efc3a497809aa4a54ee0169dd81aa92f5eab5e64
