From fbe38a2ef45868e4e6d1108471170d856ecf3c11 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Fri, 17 Jan 2025 13:20:06 +0100 Subject: [PATCH 01/10] just log when the UI button is clicked WIP --- web/src/components/storage/DriveEditor.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/web/src/components/storage/DriveEditor.tsx b/web/src/components/storage/DriveEditor.tsx index 396184bba3..e6c6626257 100644 --- a/web/src/components/storage/DriveEditor.tsx +++ b/web/src/components/storage/DriveEditor.tsx @@ -568,10 +568,14 @@ const PartitionsNoContentSelector = () => { }; const PartitionsWithContentSelector = ({ drive }) => { + console.log("DRIVEVV:", drive); const menuRef = useRef(); const toggleMenuRef = useRef(); const [isOpen, setIsOpen] = useState(false); const onToggle = () => setIsOpen(!isOpen); + const onDelete = () => { + console.log("DELETE CLICKED YAY"); + }; return ( { icon={} actionId={`delete-${partition.mountPath}`} aria-label={`Delete ${partition.mountPath}`} + onClick={onDelete} /> } From ba30864ad51138e9f18d712dfa6b197e14e5d0eb Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Fri, 17 Jan 2025 16:08:30 +0100 Subject: [PATCH 02/10] Clicking the trash can icon always deletes a proposed partition that is, without asking for confirmation or checking if it is mandatory. split out PartitionMenuItem, because eslint said: 607:40 error React Hook "usePartition" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function react-hooks/rules-of-hooks --- web/src/components/storage/DriveEditor.tsx | 63 ++++++++++++---------- web/src/queries/storage/config-model.ts | 43 +++++++++++++++ 2 files changed, 78 insertions(+), 28 deletions(-) diff --git a/web/src/components/storage/DriveEditor.tsx b/web/src/components/storage/DriveEditor.tsx index e6c6626257..37a67c925a 100644 --- a/web/src/components/storage/DriveEditor.tsx +++ b/web/src/components/storage/DriveEditor.tsx @@ -29,7 +29,7 @@ import { useAvailableDevices } from "~/queries/storage"; import { configModel } from "~/api/storage/types"; import { StorageDevice } from "~/types/storage"; import { STORAGE as PATHS } from "~/routes/paths"; -import { useDrive } from "~/queries/storage/config-model"; +import { useDrive, usePartition } from "~/queries/storage/config-model"; import * as driveUtils from "~/components/storage/utils/drive"; import { typeDescription, contentDescription } from "~/components/storage/utils/device"; import { Icon } from "../layout"; @@ -567,15 +567,41 @@ const PartitionsNoContentSelector = () => { ); }; +const PartitionMenuItem = ({ driveName, mountPath }) => { + const { onDelete } = usePartition(driveName, mountPath); + + return ( + + } + actionId={`edit-${mountPath}`} + aria-label={`Edit ${mountPath}`} + /> + } + actionId={`delete-${mountPath}`} + aria-label={`Delete ${mountPath}`} + onClick={onDelete} + /> + + } + > + {mountPath} + + ); +}; + const PartitionsWithContentSelector = ({ drive }) => { - console.log("DRIVEVV:", drive); const menuRef = useRef(); const toggleMenuRef = useRef(); const [isOpen, setIsOpen] = useState(false); const onToggle = () => setIsOpen(!isOpen); - const onDelete = () => { - console.log("DELETE CLICKED YAY"); - }; return ( { .filter((p) => p.mountPath) .map((partition) => { return ( - - } - actionId={`edit-${partition.mountPath}`} - aria-label={`Edit ${partition.mountPath}`} - /> - } - actionId={`delete-${partition.mountPath}`} - aria-label={`Delete ${partition.mountPath}`} - onClick={onDelete} - /> - - } - > - {partition.mountPath} - + driveName={drive.name} + mountPath={partition.mountPath} + /> ); })} diff --git a/web/src/queries/storage/config-model.ts b/web/src/queries/storage/config-model.ts index 0d0e27046d..93f18a1a3b 100644 --- a/web/src/queries/storage/config-model.ts +++ b/web/src/queries/storage/config-model.ts @@ -62,6 +62,18 @@ function isUsedDrive(model: configModel.Config, driveName: string) { return drive.partitions?.some((p) => isNewPartition(p) || isReusedPartition(p)); } +function findPartition( + model: configModel.Config, + driveName: string, + mountPath: string, +): configModel.Partition | undefined { + const drive = findDrive(model, driveName); + if (drive === undefined) return undefined; + + const partitions = drive.partitions || []; + return partitions.find((p) => p.mountPath === mountPath); +} + function isBoot(model: configModel.Config, driveName: string): boolean { return model.boot?.configure && driveName === model.boot?.device?.name; } @@ -104,6 +116,20 @@ function disableBoot(originalModel: configModel.Config): configModel.Config { return setBoot(originalModel, { configure: false }); } +function deletePartition( + originalModel: configModel.Config, + driveName: string, + mountPath: string, +): configModel.Config { + const model = copyModel(originalModel); + const drive = findDrive(model, driveName); + if (drive === undefined) return; + + const partitions = (drive.partitions || []).filter((p) => p.mountPath !== mountPath); + drive.partitions = partitions; + return model; +} + function switchDrive( originalModel: configModel.Config, driveName: string, @@ -252,6 +278,23 @@ export function useBoot(): BootHook { }; } +export type PartitionHook = { + onDelete: () => void; +}; + +// driveName, like "/dev/sda" +// mountPath, like "/" or "swap" +export function usePartition(driveName: string, mountPath: string): PartitionHook | undefined { + const model = useConfigModel(); + const { mutate } = useConfigModelMutation(); + + if (findPartition(model, driveName, mountPath) === undefined) return; + + return { + onDelete: () => mutate(deletePartition(model, driveName, mountPath)), + }; +} + export type DriveHook = { isBoot: boolean; isExplicitBoot: boolean; From f2f32f31f45a7781a264752671aacb9650eb8a96 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Fri, 17 Jan 2025 16:09:38 +0100 Subject: [PATCH 03/10] fix aria-label=Delete apparently a copy-paste bug in the prototype BTW shouldn't we use _(translations) more? --- web/src/components/storage/DriveEditor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/storage/DriveEditor.tsx b/web/src/components/storage/DriveEditor.tsx index 37a67c925a..8ff050d508 100644 --- a/web/src/components/storage/DriveEditor.tsx +++ b/web/src/components/storage/DriveEditor.tsx @@ -584,7 +584,7 @@ const PartitionMenuItem = ({ driveName, mountPath }) => { /> } + icon={} actionId={`delete-${mountPath}`} aria-label={`Delete ${mountPath}`} onClick={onDelete} From 104195f70becf415eff6150906d52eea3ded1366 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Wed, 22 Jan 2025 09:43:10 +0100 Subject: [PATCH 04/10] rename to PartitionHook.delete and use `deletePartition` as variable name not to collide with the `delete` operator --- web/src/components/storage/DriveEditor.tsx | 4 ++-- web/src/queries/storage/config-model.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/web/src/components/storage/DriveEditor.tsx b/web/src/components/storage/DriveEditor.tsx index 8ff050d508..53614a867d 100644 --- a/web/src/components/storage/DriveEditor.tsx +++ b/web/src/components/storage/DriveEditor.tsx @@ -568,7 +568,7 @@ const PartitionsNoContentSelector = () => { }; const PartitionMenuItem = ({ driveName, mountPath }) => { - const { onDelete } = usePartition(driveName, mountPath); + const { delete: deletePartition } = usePartition(driveName, mountPath); return ( { icon={} actionId={`delete-${mountPath}`} aria-label={`Delete ${mountPath}`} - onClick={onDelete} + onClick={deletePartition} /> } diff --git a/web/src/queries/storage/config-model.ts b/web/src/queries/storage/config-model.ts index 93f18a1a3b..3a11d25455 100644 --- a/web/src/queries/storage/config-model.ts +++ b/web/src/queries/storage/config-model.ts @@ -279,7 +279,7 @@ export function useBoot(): BootHook { } export type PartitionHook = { - onDelete: () => void; + delete: () => void; }; // driveName, like "/dev/sda" @@ -291,7 +291,7 @@ export function usePartition(driveName: string, mountPath: string): PartitionHoo if (findPartition(model, driveName, mountPath) === undefined) return; return { - onDelete: () => mutate(deletePartition(model, driveName, mountPath)), + delete: () => mutate(deletePartition(model, driveName, mountPath)), }; } From b64ff55f25bb5973b0ac565110fe61e0d7313434 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Wed, 22 Jan 2025 13:36:40 +0100 Subject: [PATCH 05/10] WIP test having to set up mocks for the whole DriveEditor is not what I expected :-/ --- .../components/storage/DriveEditor.test.tsx | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 web/src/components/storage/DriveEditor.test.tsx diff --git a/web/src/components/storage/DriveEditor.test.tsx b/web/src/components/storage/DriveEditor.test.tsx new file mode 100644 index 0000000000..d5d114a33d --- /dev/null +++ b/web/src/components/storage/DriveEditor.test.tsx @@ -0,0 +1,132 @@ +/* + * Copyright (c) [2025] 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, within } from "@testing-library/react"; +import { plainRender } from "~/test-utils"; +import { installerRender } from "~/test-utils"; +import * as ConfigModel from "~/api/storage/types/config-model"; + +import DriveEditor from "~/components/storage/DriveEditor"; + +// TODO: copied from ExpandableSelector.test.tsx +// TODO: no idea if it fits my purpose +const sda: any = { + sid: "59", + isDrive: true, + type: "disk", + vendor: "Micron", + model: "Micron 1100 SATA", + driver: ["ahci", "mmcblk"], + bus: "IDE", + busId: "", + transport: "usb", + dellBOSS: false, + sdCard: true, + active: true, + name: "/dev/sda", + size: 1024, + shrinking: { unsupported: ["Resizing is not supported"] }, + systems: [], + udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"], + udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"], +}; + +const sda1 = { + sid: "60", + isDrive: false, + type: "", + active: true, + name: "/dev/sda1", + size: 512, + shrinking: { supported: 128 }, + systems: [], + udevIds: [], + udevPaths: [], +}; + +const sda2 = { + sid: "61", + isDrive: false, + type: "", + active: true, + name: "/dev/sda2", + size: 512, + shrinking: { unsupported: ["Resizing is not supported"] }, + systems: [], + udevIds: [], + udevPaths: [], +}; + +sda.partitionTable = { + type: "gpt", + partitions: [sda1, sda2], + unpartitionedSize: 512, +}; + +const mockDrive = { + name: "/dev/sda", + //spacePolicy: "delete", + partitions: [ + { + mountPath: "swap", + size: { + min: 2_000_000_000, + default: false, // WTF does default mean?? + } + }, + ] +} + +const mockConfig = { drives: [mockDrive] as ConfigModel.Drive[] }; + +// TODO: why does "~/queries/storage" work elsewhere?? +jest.mock("~/queries/storage/config-model", () => ({ + ...jest.requireActual("~/queries/storage/config-model"), + useConfigModel: () => mockConfig, + useDrive: (name) => mockDrive, +})); + + +describe("PartitionMenuItem", () => { + it("does something when the Delete icon is clicked", async () => { + // oh fun, cannot use DriveEditorProps as it is not exported? any works + let props: any = { + // configModel.Drive + drive: mockDrive, + // StorageDevice + driveDevice: sda, + }; + // if I try to inline it in mockDrive, weird error, string is not SpacePolicy(?) + props.drive.spacePolicy = "delete"; + + //const { user } = installerRender(); + const { user } = plainRender(); + + // How do I find this? There is no role attribute + // MenuItemAction actionId="delete-swap" aria-label="Delete swap" + const button = screen.getByRole("button", { name: /Delete swap/ }); + // Oh, the UI I worked on will only be revealed once we click this: + // A new partition will be created for "swap" (at least 1.86 GiB) + // but how do we identify it? + }); +}); From f292d2c463ae87a7b003964bd4e275d165e07565 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Thu, 23 Jan 2025 01:28:04 +0000 Subject: [PATCH 06/10] fix(web): extend partitions menu markup and add a test Markup for adding roles and labels to make the interface a bit more accessible and easing unit testing with React Testing Library. Still needing love, thougth. But this is a kind of starting point / guidance commit. --- .../components/storage/DriveEditor.test.tsx | 93 ++++++------------- web/src/components/storage/DriveEditor.tsx | 28 ++++-- 2 files changed, 45 insertions(+), 76 deletions(-) diff --git a/web/src/components/storage/DriveEditor.test.tsx b/web/src/components/storage/DriveEditor.test.tsx index d5d114a33d..6daf9fb68b 100644 --- a/web/src/components/storage/DriveEditor.test.tsx +++ b/web/src/components/storage/DriveEditor.test.tsx @@ -23,15 +23,12 @@ import React from "react"; import { screen, within } from "@testing-library/react"; import { plainRender } from "~/test-utils"; -import { installerRender } from "~/test-utils"; +import DriveEditor, { DriveEditorProps } from "~/components/storage/DriveEditor"; import * as ConfigModel from "~/api/storage/types/config-model"; +import { StorageDevice } from "~/types/storage"; -import DriveEditor from "~/components/storage/DriveEditor"; - -// TODO: copied from ExpandableSelector.test.tsx -// TODO: no idea if it fits my purpose -const sda: any = { - sid: "59", +const sda: StorageDevice = { + sid: 59, isDrive: true, type: "disk", vendor: "Micron", @@ -49,84 +46,48 @@ const sda: any = { systems: [], udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"], udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"], + description: "", }; -const sda1 = { - sid: "60", - isDrive: false, - type: "", - active: true, - name: "/dev/sda1", - size: 512, - shrinking: { supported: 128 }, - systems: [], - udevIds: [], - udevPaths: [], -}; - -const sda2 = { - sid: "61", - isDrive: false, - type: "", - active: true, - name: "/dev/sda2", - size: 512, - shrinking: { unsupported: ["Resizing is not supported"] }, - systems: [], - udevIds: [], - udevPaths: [], -}; - -sda.partitionTable = { - type: "gpt", - partitions: [sda1, sda2], - unpartitionedSize: 512, -}; - -const mockDrive = { +const mockDrive: ConfigModel.Drive = { name: "/dev/sda", - //spacePolicy: "delete", + spacePolicy: "delete", partitions: [ { mountPath: "swap", size: { min: 2_000_000_000, default: false, // WTF does default mean?? - } + }, }, - ] -} - -const mockConfig = { drives: [mockDrive] as ConfigModel.Drive[] }; + ], +}; +const mockDeletePartition = jest.fn(); // TODO: why does "~/queries/storage" work elsewhere?? jest.mock("~/queries/storage/config-model", () => ({ ...jest.requireActual("~/queries/storage/config-model"), - useConfigModel: () => mockConfig, - useDrive: (name) => mockDrive, + useConfigModel: () => ({ drives: [mockDrive] }), + useDrive: () => mockDrive, + usePartition: () => ({ delete: mockDeletePartition }), })); +const props: DriveEditorProps = { + drive: mockDrive, + driveDevice: sda, +}; describe("PartitionMenuItem", () => { - it("does something when the Delete icon is clicked", async () => { - // oh fun, cannot use DriveEditorProps as it is not exported? any works - let props: any = { - // configModel.Drive - drive: mockDrive, - // StorageDevice - driveDevice: sda, - }; - // if I try to inline it in mockDrive, weird error, string is not SpacePolicy(?) - props.drive.spacePolicy = "delete"; - - //const { user } = installerRender(); + it("allows users delete a the partition", async () => { const { user } = plainRender(); - // How do I find this? There is no role attribute - // MenuItemAction actionId="delete-swap" aria-label="Delete swap" - const button = screen.getByRole("button", { name: /Delete swap/ }); - // Oh, the UI I worked on will only be revealed once we click this: - // A new partition will be created for "swap" (at least 1.86 GiB) - // but how do we identify it? + const partitionsButton = screen.getByRole("button", { name: "Partitions" }); + await user.click(partitionsButton); + const partitionsMenu = screen.getByRole("menu"); + const deleteSwapButton = within(partitionsMenu).getByRole("button", { + name: "Delete swap", + }); + await user.click(deleteSwapButton); + expect(mockDeletePartition).toHaveBeenCalled(); }); }); diff --git a/web/src/components/storage/DriveEditor.tsx b/web/src/components/storage/DriveEditor.tsx index 53614a867d..a8e06d56c1 100644 --- a/web/src/components/storage/DriveEditor.tsx +++ b/web/src/components/storage/DriveEditor.tsx @@ -20,7 +20,7 @@ * find current contact information at www.suse.com. */ -import React, { useRef, useState } from "react"; +import React, { useId, useRef, useState } from "react"; import { useNavigate, generatePath } from "react-router-dom"; import { _, formatList } from "~/i18n"; import { sprintf } from "sprintf-js"; @@ -52,7 +52,7 @@ import { MenuToggle, } from "@patternfly/react-core"; -type DriveEditorProps = { drive: configModel.Drive; driveDevice: StorageDevice }; +export type DriveEditorProps = { drive: configModel.Drive; driveDevice: StorageDevice }; // FIXME: Presentation is quite poor const SpacePolicySelectorIntro = ({ device }) => { @@ -520,7 +520,8 @@ const DriveHeader = ({ drive, driveDevice }: DriveEditorProps) => { ); }; -const PartitionsNoContentSelector = () => { +const PartitionsNoContentSelector = ({ toggleAriaLabel }) => { + const menuId = useId(); const menuRef = useRef(); const toggleMenuRef = useRef(); const [isOpen, setIsOpen] = useState(false); @@ -538,8 +539,10 @@ const PartitionsNoContentSelector = () => { onClick={onToggle} isExpanded={isOpen} className="menu-toggle-inline" + aria-label={toggleAriaLabel} + aria-controls={menuId} > - + {_("No additional partitions will be created")} @@ -547,13 +550,14 @@ const PartitionsNoContentSelector = () => { } menuRef={menuRef} menu={ - + {_("Add or use partition")} @@ -574,6 +578,7 @@ const PartitionMenuItem = ({ driveName, mountPath }) => { { ); }; -const PartitionsWithContentSelector = ({ drive }) => { +const PartitionsWithContentSelector = ({ drive, toggleAriaLabel }) => { + const menuId = useId(); const menuRef = useRef(); const toggleMenuRef = useRef(); const [isOpen, setIsOpen] = useState(false); @@ -615,8 +621,10 @@ const PartitionsWithContentSelector = ({ drive }) => { onClick={onToggle} isExpanded={isOpen} className="menu-toggle-inline" + aria-label={toggleAriaLabel} + aria-controls={menuId} > - + {driveUtils.contentDescription(drive)} @@ -624,7 +632,7 @@ const PartitionsWithContentSelector = ({ drive }) => { } menuRef={menuRef} menu={ - + {drive.partitions @@ -658,10 +666,10 @@ const PartitionsWithContentSelector = ({ drive }) => { const PartitionsSelector = ({ drive }) => { if (drive.partitions.some((p) => p.mountPath)) { - return ; + return ; } - return ; + return ; }; export default function DriveEditor({ drive, driveDevice }: DriveEditorProps) { From 2d8b27e54bd7d687706e3b298f2ba2e7a6dbe982 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Thu, 23 Jan 2025 14:06:54 +0100 Subject: [PATCH 07/10] adjust test after merging with upstream --- web/src/components/storage/DriveEditor.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/storage/DriveEditor.test.tsx b/web/src/components/storage/DriveEditor.test.tsx index 6daf9fb68b..812984c876 100644 --- a/web/src/components/storage/DriveEditor.test.tsx +++ b/web/src/components/storage/DriveEditor.test.tsx @@ -84,7 +84,7 @@ describe("PartitionMenuItem", () => { const partitionsButton = screen.getByRole("button", { name: "Partitions" }); await user.click(partitionsButton); const partitionsMenu = screen.getByRole("menu"); - const deleteSwapButton = within(partitionsMenu).getByRole("button", { + const deleteSwapButton = within(partitionsMenu).getByRole("menuitem", { name: "Delete swap", }); await user.click(deleteSwapButton); From 84c37e01d2f09b15ac24dcfe44a9baf1ba008944 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Thu, 23 Jan 2025 16:05:07 +0100 Subject: [PATCH 08/10] comment usePartition better --- web/src/queries/storage/config-model.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/web/src/queries/storage/config-model.ts b/web/src/queries/storage/config-model.ts index 3a11d25455..a1d24ffc30 100644 --- a/web/src/queries/storage/config-model.ts +++ b/web/src/queries/storage/config-model.ts @@ -282,8 +282,10 @@ export type PartitionHook = { delete: () => void; }; -// driveName, like "/dev/sda" -// mountPath, like "/" or "swap" +/** + * @param driveName like "/dev/sda" + * @param mountPath like "/" or "swap" + */ export function usePartition(driveName: string, mountPath: string): PartitionHook | undefined { const model = useConfigModel(); const { mutate } = useConfigModelMutation(); From 3c52b0821e6c71344b3d1401947caef9a09a7e4c Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Thu, 23 Jan 2025 16:05:38 +0100 Subject: [PATCH 09/10] comment Size.default better even better it belongs to storage.model.schema.json --- web/src/components/storage/DriveEditor.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/storage/DriveEditor.test.tsx b/web/src/components/storage/DriveEditor.test.tsx index 812984c876..a278cc1ad8 100644 --- a/web/src/components/storage/DriveEditor.test.tsx +++ b/web/src/components/storage/DriveEditor.test.tsx @@ -57,7 +57,7 @@ const mockDrive: ConfigModel.Drive = { mountPath: "swap", size: { min: 2_000_000_000, - default: false, // WTF does default mean?? + default: false, // false: user provided, true: calculated }, }, ], From 50f677df9637861add39954008c8f63766a0f4d3 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Thu, 23 Jan 2025 16:11:50 +0100 Subject: [PATCH 10/10] changelog --- web/package/agama-web-ui.changes | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/web/package/agama-web-ui.changes b/web/package/agama-web-ui.changes index 63ec4ae1c9..c9939d909d 100644 --- a/web/package/agama-web-ui.changes +++ b/web/package/agama-web-ui.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Thu Jan 23 15:09:09 UTC 2025 - Martin Vidner + +- Make the trash can icon (Delete) of a proposed partition work + gh#agama-project/agama#1915) + ------------------------------------------------------------------- Mon Jan 20 16:45:18 UTC 2025 - Ladislav Slezák