diff --git a/.scaffold/go.sum b/.scaffold/go.sum
index 40d03dd1..36f3018c 100644
--- a/.scaffold/go.sum
+++ b/.scaffold/go.sum
@@ -8,7 +8,7 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
-github.com/sysadminsmedia/homebox/backend v0.0.0-20251228163253-2bd6ff580a7f h1:+5m3FRu/Ja3kH2XgFn0GYCWOKIe4O+7PbLawLXvb4gA=
-github.com/sysadminsmedia/homebox/backend v0.0.0-20251228163253-2bd6ff580a7f/go.mod h1:9zHHw5TNttw5Kn4Wks+SxwXmJPz6PgGNbnB4BtF1Z4c=
+github.com/sysadminsmedia/homebox/backend v0.0.0-20251228172914-2a6773d1d610 h1:kNLtnxaPaOryBUZ7RgUHPQVWxIExXYR/q9pYCbum5Vk=
+github.com/sysadminsmedia/homebox/backend v0.0.0-20251228172914-2a6773d1d610/go.mod h1:9zHHw5TNttw5Kn4Wks+SxwXmJPz6PgGNbnB4BtF1Z4c=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/backend/app/api/handlers/v1/v1_ctrl_actions.go b/backend/app/api/handlers/v1/v1_ctrl_actions.go
index 9766ed96..9f3ccd3b 100644
--- a/backend/app/api/handlers/v1/v1_ctrl_actions.go
+++ b/backend/app/api/handlers/v1/v1_ctrl_actions.go
@@ -10,6 +10,7 @@ import (
"github.com/hay-kot/httpkit/server"
"github.com/rs/zerolog/log"
"github.com/sysadminsmedia/homebox/backend/internal/core/services"
+ "github.com/sysadminsmedia/homebox/backend/internal/core/services/reporting/eventbus"
"github.com/sysadminsmedia/homebox/backend/internal/sys/validate"
)
@@ -143,6 +144,16 @@ func (ctrl *V1Controller) HandleWipeInventory() errchain.HandlerFunc {
return validate.NewRequestError(err, http.StatusInternalServerError)
}
+ // Publish mutation events for wiped resources
+ if ctrl.bus != nil {
+ if options.WipeLabels {
+ ctrl.bus.Publish(eventbus.EventLabelMutation, eventbus.GroupMutationEvent{GID: ctx.GID})
+ }
+ if options.WipeLocations {
+ ctrl.bus.Publish(eventbus.EventLocationMutation, eventbus.GroupMutationEvent{GID: ctx.GID})
+ }
+ }
+
return server.JSON(w, http.StatusOK, ActionAmountResult{Completed: totalCompleted})
}
}
diff --git a/frontend/components/WipeInventoryDialog.vue b/frontend/components/WipeInventoryDialog.vue
index 64bf33c2..29eca418 100644
--- a/frontend/components/WipeInventoryDialog.vue
+++ b/frontend/components/WipeInventoryDialog.vue
@@ -54,9 +54,9 @@
{{ $t("global.cancel") }}
-
+
+
@@ -67,7 +67,6 @@
import { useDialog } from "~/components/ui/dialog-provider";
import {
AlertDialog,
- AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
@@ -75,6 +74,7 @@
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
+ import { Button } from "@/components/ui/button";
const { registerOpenDialogCallback, closeDialog, addAlert, removeAlert } = useDialog();
@@ -82,12 +82,14 @@
const wipeLabels = ref(false);
const wipeLocations = ref(false);
const wipeMaintenance = ref(false);
+ const isConfirming = ref(false);
registerOpenDialogCallback(DialogID.WipeInventory, () => {
dialog.value = true;
wipeLabels.value = false;
wipeLocations.value = false;
wipeMaintenance.value = false;
+ isConfirming.value = false;
});
watch(
@@ -103,7 +105,7 @@
);
function handleOpenChange(open: boolean) {
- if (!open) {
+ if (!open && !isConfirming.value) {
close();
}
}
@@ -114,12 +116,14 @@
}
function confirm() {
- dialog.value = false;
+ isConfirming.value = true;
const result = {
wipeLabels: wipeLabels.value,
wipeLocations: wipeLocations.value,
wipeMaintenance: wipeMaintenance.value,
};
closeDialog(DialogID.WipeInventory, result);
+ dialog.value = false;
+ isConfirming.value = false;
}
diff --git a/frontend/test/e2e/wipe-inventory.browser.spec.ts b/frontend/test/e2e/wipe-inventory.browser.spec.ts
new file mode 100644
index 00000000..d7952466
--- /dev/null
+++ b/frontend/test/e2e/wipe-inventory.browser.spec.ts
@@ -0,0 +1,129 @@
+import { expect, test } from "@playwright/test";
+
+test.describe("Wipe Inventory E2E Test", () => {
+ test.beforeEach(async ({ page }) => {
+ // Login as demo user (owner with permissions)
+ await page.goto("/");
+ await page.fill("input[type='text']", "demo@example.com");
+ await page.fill("input[type='password']", "demo");
+ await page.click("button[type='submit']");
+ await expect(page).toHaveURL("/home");
+ });
+
+ test("should open wipe inventory dialog with all options", async ({ page }) => {
+ // Navigate to Tools page
+ await page.goto("/tools");
+ await page.waitForLoadState("networkidle");
+
+ // Scroll to the bottom where wipe inventory is located
+ await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
+ await page.waitForTimeout(500);
+
+ // Find and click the Wipe Inventory button
+ const wipeButton = page.locator("button", { hasText: "Wipe Inventory" }).last();
+ await expect(wipeButton).toBeVisible();
+ await wipeButton.click();
+
+ // Wait for dialog to appear
+ await page.waitForTimeout(1000);
+
+ // Verify dialog title is visible
+ await expect(page.locator("text=Wipe Inventory").first()).toBeVisible();
+
+ // Verify all checkboxes are present
+ await expect(page.locator("input#wipe-labels-checkbox")).toBeVisible();
+ await expect(page.locator("input#wipe-locations-checkbox")).toBeVisible();
+ await expect(page.locator("input#wipe-maintenance-checkbox")).toBeVisible();
+
+ // Verify labels for checkboxes
+ await expect(page.locator("label[for='wipe-labels-checkbox']")).toBeVisible();
+ await expect(page.locator("label[for='wipe-locations-checkbox']")).toBeVisible();
+ await expect(page.locator("label[for='wipe-maintenance-checkbox']")).toBeVisible();
+
+ // Verify both Cancel and Confirm buttons are present
+ await expect(page.locator("button", { hasText: "Cancel" })).toBeVisible();
+ const confirmButton = page.locator("button", { hasText: "Confirm" });
+ await expect(confirmButton).toBeVisible();
+
+ // Take screenshot of the modal
+ await page.screenshot({
+ path: "/tmp/playwright-logs/wipe-inventory-modal-initial.png",
+ });
+ console.log("✅ Screenshot saved: wipe-inventory-modal-initial.png");
+
+ // Check all three options
+ await page.check("input#wipe-labels-checkbox");
+ await page.check("input#wipe-locations-checkbox");
+ await page.check("input#wipe-maintenance-checkbox");
+ await page.waitForTimeout(500);
+
+ // Verify checkboxes are checked
+ await expect(page.locator("input#wipe-labels-checkbox")).toBeChecked();
+ await expect(page.locator("input#wipe-locations-checkbox")).toBeChecked();
+ await expect(page.locator("input#wipe-maintenance-checkbox")).toBeChecked();
+
+ // Take screenshot with all options checked
+ await page.screenshot({
+ path: "/tmp/playwright-logs/wipe-inventory-modal-options-checked.png",
+ });
+ console.log("✅ Screenshot saved: wipe-inventory-modal-options-checked.png");
+
+ // Click Confirm button
+ await confirmButton.click();
+ await page.waitForTimeout(2000);
+
+ // Wait for the dialog to close (verify button is no longer visible)
+ await expect(confirmButton).not.toBeVisible({ timeout: 5000 });
+
+ // Check for success toast notification
+ // The toast should contain text about items being deleted
+ const toastLocator = page.locator("[role='status'], [class*='toast'], [class*='sonner']");
+ await expect(toastLocator.first()).toBeVisible({ timeout: 10000 });
+
+ // Take screenshot of the page after confirmation
+ await page.screenshot({
+ path: "/tmp/playwright-logs/after-wipe-confirmation.png",
+ fullPage: true,
+ });
+ console.log("✅ Screenshot saved: after-wipe-confirmation.png");
+
+ console.log("✅ Test completed successfully!");
+ console.log("✅ Wipe Inventory dialog opened correctly");
+ console.log("✅ All three options (labels, locations, maintenance) are available");
+ console.log("✅ Confirm button triggers the action");
+ console.log("✅ Dialog closes after confirmation");
+ });
+
+ test("should cancel wipe inventory operation", async ({ page }) => {
+ // Navigate to Tools page
+ await page.goto("/tools");
+ await page.waitForLoadState("networkidle");
+
+ // Scroll to wipe inventory section
+ await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
+ await page.waitForTimeout(500);
+
+ // Click Wipe Inventory button
+ const wipeButton = page.locator("button", { hasText: "Wipe Inventory" }).last();
+ await wipeButton.click();
+ await page.waitForTimeout(1000);
+
+ // Verify dialog is open
+ await expect(page.locator("text=Wipe Inventory").first()).toBeVisible();
+
+ // Click Cancel button
+ const cancelButton = page.locator("button", { hasText: "Cancel" });
+ await cancelButton.click();
+ await page.waitForTimeout(1000);
+
+ // Verify dialog is closed
+ await expect(page.locator("text=Wipe Inventory").first()).not.toBeVisible({ timeout: 5000 });
+
+ // Take screenshot after cancel
+ await page.screenshot({
+ path: "/tmp/playwright-logs/after-cancel.png",
+ });
+ console.log("✅ Screenshot saved: after-cancel.png");
+ console.log("✅ Cancel button works correctly");
+ });
+});