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
 

Reply via email to