Skip to content

Commit 2bd6ff5

Browse files
authored
Add option to wipe inventory
2 parents 4557df8 + 3594158 commit 2bd6ff5

File tree

10 files changed

+345
-3
lines changed

10 files changed

+345
-3
lines changed

.scaffold/go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
88
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
99
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
1010
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
11-
github.com/sysadminsmedia/homebox/backend v0.0.0-20251212183312-2d1d3d927bfd h1:QULUJSgHc4rSlTjb2qYT6FIgwDWFCqEpnYqc/ltsrkk=
12-
github.com/sysadminsmedia/homebox/backend v0.0.0-20251212183312-2d1d3d927bfd/go.mod h1:jB+tPmHtPDM1VnAjah0gvcRfP/s7c+rtQwpA8cvZD/U=
11+
github.com/sysadminsmedia/homebox/backend v0.0.0-20251228052820-4557df86eddb h1:nRu1qr3gceoIIDJolCRnd/Eo5VLAMoH9CYnyKCCVBuA=
12+
github.com/sysadminsmedia/homebox/backend v0.0.0-20251228052820-4557df86eddb/go.mod h1:9zHHw5TNttw5Kn4Wks+SxwXmJPz6PgGNbnB4BtF1Z4c=
1313
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
1414
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

backend/app/api/handlers/v1/v1_ctrl_actions.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package v1
22

33
import (
44
"context"
5+
"errors"
56
"net/http"
67

78
"github.com/google/uuid"
@@ -94,3 +95,54 @@ func (ctrl *V1Controller) HandleSetPrimaryPhotos() errchain.HandlerFunc {
9495
func (ctrl *V1Controller) HandleCreateMissingThumbnails() errchain.HandlerFunc {
9596
return actionHandlerFactory("create missing thumbnails", ctrl.repo.Attachments.CreateMissingThumbnails)
9697
}
98+
99+
// WipeInventoryOptions represents the options for wiping inventory
100+
type WipeInventoryOptions struct {
101+
WipeLabels bool `json:"wipeLabels"`
102+
WipeLocations bool `json:"wipeLocations"`
103+
WipeMaintenance bool `json:"wipeMaintenance"`
104+
}
105+
106+
// HandleWipeInventory godoc
107+
//
108+
// @Summary Wipe Inventory
109+
// @Description Deletes all items in the inventory
110+
// @Tags Actions
111+
// @Produce json
112+
// @Param options body WipeInventoryOptions false "Wipe options"
113+
// @Success 200 {object} ActionAmountResult
114+
// @Router /v1/actions/wipe-inventory [Post]
115+
// @Security Bearer
116+
func (ctrl *V1Controller) HandleWipeInventory() errchain.HandlerFunc {
117+
return func(w http.ResponseWriter, r *http.Request) error {
118+
if ctrl.isDemo {
119+
return validate.NewRequestError(errors.New("wipe inventory is not allowed in demo mode"), http.StatusForbidden)
120+
}
121+
122+
ctx := services.NewContext(r.Context())
123+
124+
// Check if user is owner
125+
if !ctx.User.IsOwner {
126+
return validate.NewRequestError(errors.New("only group owners can wipe inventory"), http.StatusForbidden)
127+
}
128+
129+
// Parse options from request body
130+
var options WipeInventoryOptions
131+
if err := server.Decode(r, &options); err != nil {
132+
// If no body provided, use default (false for all)
133+
options = WipeInventoryOptions{
134+
WipeLabels: false,
135+
WipeLocations: false,
136+
WipeMaintenance: false,
137+
}
138+
}
139+
140+
totalCompleted, err := ctrl.repo.Items.WipeInventory(ctx, ctx.GID, options.WipeLabels, options.WipeLocations, options.WipeMaintenance)
141+
if err != nil {
142+
log.Err(err).Str("action_ref", "wipe inventory").Msg("failed to run action")
143+
return validate.NewRequestError(err, http.StatusInternalServerError)
144+
}
145+
146+
return server.JSON(w, http.StatusOK, ActionAmountResult{Completed: totalCompleted})
147+
}
148+
}

backend/app/api/routes.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
108108
r.Post("/actions/ensure-import-refs", chain.ToHandlerFunc(v1Ctrl.HandleEnsureImportRefs(), userMW...))
109109
r.Post("/actions/set-primary-photos", chain.ToHandlerFunc(v1Ctrl.HandleSetPrimaryPhotos(), userMW...))
110110
r.Post("/actions/create-missing-thumbnails", chain.ToHandlerFunc(v1Ctrl.HandleCreateMissingThumbnails(), userMW...))
111+
r.Post("/actions/wipe-inventory", chain.ToHandlerFunc(v1Ctrl.HandleWipeInventory(), userMW...))
111112

112113
r.Get("/locations", chain.ToHandlerFunc(v1Ctrl.HandleLocationGetAll(), userMW...))
113114
r.Post("/locations", chain.ToHandlerFunc(v1Ctrl.HandleLocationCreate(), userMW...))

backend/internal/data/repo/repo_items.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -809,6 +809,87 @@ func (e *ItemsRepository) DeleteByGroup(ctx context.Context, gid, id uuid.UUID)
809809
return err
810810
}
811811

812+
func (e *ItemsRepository) WipeInventory(ctx context.Context, gid uuid.UUID, wipeLabels bool, wipeLocations bool, wipeMaintenance bool) (int, error) {
813+
// Get all items for the group
814+
items, err := e.db.Item.Query().
815+
Where(item.HasGroupWith(group.ID(gid))).
816+
WithAttachments().
817+
All(ctx)
818+
if err != nil {
819+
return 0, err
820+
}
821+
822+
deleted := 0
823+
// Delete each item with its attachments
824+
// Note: We manually delete attachments and items instead of calling DeleteByGroup
825+
// to continue processing remaining items even if some deletions fail
826+
for _, itm := range items {
827+
// Delete all attachments first
828+
for _, att := range itm.Edges.Attachments {
829+
err := e.attachments.Delete(ctx, gid, itm.ID, att.ID)
830+
if err != nil {
831+
log.Err(err).Str("attachment_id", att.ID.String()).Msg("failed to delete attachment during wipe inventory")
832+
// Continue with other attachments even if one fails
833+
}
834+
}
835+
836+
// Delete the item
837+
_, err = e.db.Item.
838+
Delete().
839+
Where(
840+
item.ID(itm.ID),
841+
item.HasGroupWith(group.ID(gid)),
842+
).Exec(ctx)
843+
if err != nil {
844+
log.Err(err).Str("item_id", itm.ID.String()).Msg("failed to delete item during wipe inventory")
845+
// Skip to next item without incrementing counter
846+
continue
847+
}
848+
849+
// Only increment counter if deletion succeeded
850+
deleted++
851+
}
852+
853+
// Wipe labels if requested
854+
if wipeLabels {
855+
labelCount, err := e.db.Label.Delete().Where(label.HasGroupWith(group.ID(gid))).Exec(ctx)
856+
if err != nil {
857+
log.Err(err).Msg("failed to delete labels during wipe inventory")
858+
} else {
859+
log.Info().Int("count", labelCount).Msg("deleted labels during wipe inventory")
860+
deleted += labelCount
861+
}
862+
}
863+
864+
// Wipe locations if requested
865+
if wipeLocations {
866+
locationCount, err := e.db.Location.Delete().Where(location.HasGroupWith(group.ID(gid))).Exec(ctx)
867+
if err != nil {
868+
log.Err(err).Msg("failed to delete locations during wipe inventory")
869+
} else {
870+
log.Info().Int("count", locationCount).Msg("deleted locations during wipe inventory")
871+
deleted += locationCount
872+
}
873+
}
874+
875+
// Wipe maintenance records if requested
876+
if wipeMaintenance {
877+
// Maintenance entries are linked to items, so we query by items in the group
878+
maintenanceCount, err := e.db.MaintenanceEntry.Delete().
879+
Where(maintenanceentry.HasItemWith(item.HasGroupWith(group.ID(gid)))).
880+
Exec(ctx)
881+
if err != nil {
882+
log.Err(err).Msg("failed to delete maintenance entries during wipe inventory")
883+
} else {
884+
log.Info().Int("count", maintenanceCount).Msg("deleted maintenance entries during wipe inventory")
885+
deleted += maintenanceCount
886+
}
887+
}
888+
889+
e.publishMutationEvent(gid)
890+
return deleted, nil
891+
}
892+
812893
func (e *ItemsRepository) UpdateByGroup(ctx context.Context, gid uuid.UUID, data ItemUpdate) (ItemOut, error) {
813894
q := e.db.Item.Update().Where(item.ID(data.ID), item.HasGroupWith(group.ID(gid))).
814895
SetName(data.Name).
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
<template>
2+
<AlertDialog :open="dialog" @update:open="handleOpenChange">
3+
<AlertDialogContent>
4+
<AlertDialogHeader>
5+
<AlertDialogTitle>{{ $t("tools.actions_set.wipe_inventory") }}</AlertDialogTitle>
6+
<AlertDialogDescription>
7+
{{ $t("tools.actions_set.wipe_inventory_confirm") }}
8+
</AlertDialogDescription>
9+
</AlertDialogHeader>
10+
11+
<div class="space-y-2">
12+
<div class="flex items-center space-x-2">
13+
<input
14+
id="wipe-labels-checkbox"
15+
v-model="wipeLabels"
16+
type="checkbox"
17+
class="size-4 rounded border-gray-300"
18+
/>
19+
<label for="wipe-labels-checkbox" class="cursor-pointer text-sm font-medium">
20+
{{ $t("tools.actions_set.wipe_inventory_labels") }}
21+
</label>
22+
</div>
23+
24+
<div class="flex items-center space-x-2">
25+
<input
26+
id="wipe-locations-checkbox"
27+
v-model="wipeLocations"
28+
type="checkbox"
29+
class="size-4 rounded border-gray-300"
30+
/>
31+
<label for="wipe-locations-checkbox" class="cursor-pointer text-sm font-medium">
32+
{{ $t("tools.actions_set.wipe_inventory_locations") }}
33+
</label>
34+
</div>
35+
36+
<div class="flex items-center space-x-2">
37+
<input
38+
id="wipe-maintenance-checkbox"
39+
v-model="wipeMaintenance"
40+
type="checkbox"
41+
class="size-4 rounded border-gray-300"
42+
/>
43+
<label for="wipe-maintenance-checkbox" class="cursor-pointer text-sm font-medium">
44+
{{ $t("tools.actions_set.wipe_inventory_maintenance") }}
45+
</label>
46+
</div>
47+
</div>
48+
49+
<p class="text-sm text-gray-600">
50+
{{ $t("tools.actions_set.wipe_inventory_note") }}
51+
</p>
52+
53+
<AlertDialogFooter>
54+
<AlertDialogCancel @click="close">
55+
{{ $t("global.cancel") }}
56+
</AlertDialogCancel>
57+
<AlertDialogAction @click="confirm">
58+
{{ $t("global.confirm") }}
59+
</AlertDialogAction>
60+
</AlertDialogFooter>
61+
</AlertDialogContent>
62+
</AlertDialog>
63+
</template>
64+
65+
<script setup lang="ts">
66+
import { DialogID } from "~/components/ui/dialog-provider/utils";
67+
import { useDialog } from "~/components/ui/dialog-provider";
68+
import {
69+
AlertDialog,
70+
AlertDialogAction,
71+
AlertDialogCancel,
72+
AlertDialogContent,
73+
AlertDialogDescription,
74+
AlertDialogFooter,
75+
AlertDialogHeader,
76+
AlertDialogTitle,
77+
} from "@/components/ui/alert-dialog";
78+
79+
const { registerOpenDialogCallback, closeDialog, addAlert, removeAlert } = useDialog();
80+
81+
const dialog = ref(false);
82+
const wipeLabels = ref(false);
83+
const wipeLocations = ref(false);
84+
const wipeMaintenance = ref(false);
85+
86+
let onCloseCallback:
87+
| ((result?: { wipeLabels: boolean; wipeLocations: boolean; wipeMaintenance: boolean } | undefined) => void)
88+
| undefined;
89+
90+
registerOpenDialogCallback(
91+
DialogID.WipeInventory,
92+
(params?: {
93+
onClose?: (
94+
result?: { wipeLabels: boolean; wipeLocations: boolean; wipeMaintenance: boolean } | undefined
95+
) => void;
96+
}) => {
97+
dialog.value = true;
98+
wipeLabels.value = false;
99+
wipeLocations.value = false;
100+
wipeMaintenance.value = false;
101+
onCloseCallback = params?.onClose;
102+
}
103+
);
104+
105+
watch(
106+
dialog,
107+
val => {
108+
if (val) {
109+
addAlert("wipe-inventory-dialog");
110+
} else {
111+
removeAlert("wipe-inventory-dialog");
112+
}
113+
},
114+
{ immediate: true }
115+
);
116+
117+
function handleOpenChange(open: boolean) {
118+
if (!open) {
119+
close();
120+
}
121+
}
122+
123+
function close() {
124+
dialog.value = false;
125+
closeDialog(DialogID.WipeInventory, undefined);
126+
onCloseCallback?.(undefined);
127+
}
128+
129+
function confirm() {
130+
dialog.value = false;
131+
const result = {
132+
wipeLabels: wipeLabels.value,
133+
wipeLocations: wipeLocations.value,
134+
wipeMaintenance: wipeMaintenance.value,
135+
};
136+
closeDialog(DialogID.WipeInventory, result);
137+
onCloseCallback?.(result);
138+
}
139+
</script>

frontend/components/ui/dialog-provider/utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export enum DialogID {
2626
UpdateLocation = "update-location",
2727
UpdateTemplate = "update-template",
2828
ItemChangeDetails = "item-table-updater",
29+
WipeInventory = "wipe-inventory",
2930
}
3031

3132
/**
@@ -71,6 +72,7 @@ export type DialogResultMap = {
7172
[DialogID.ItemImage]?: { action: "delete"; id: string };
7273
[DialogID.EditMaintenance]?: boolean;
7374
[DialogID.ItemChangeDetails]?: boolean;
75+
[DialogID.WipeInventory]?: { wipeLabels: boolean; wipeLocations: boolean; wipeMaintenance: boolean };
7476
};
7577

7678
/** Helpers to split IDs by requirement */

frontend/layouts/default.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<ModalConfirm />
99
<OutdatedModal v-if="status" :status="status" />
1010
<ItemCreateModal />
11+
<WipeInventoryDialog />
1112
<LabelCreateModal />
1213
<LocationCreateModal />
1314
<ItemBarcodeModal />
@@ -216,6 +217,7 @@
216217
import ModalConfirm from "~/components/ModalConfirm.vue";
217218
import OutdatedModal from "~/components/App/OutdatedModal.vue";
218219
import ItemCreateModal from "~/components/Item/CreateModal.vue";
220+
import WipeInventoryDialog from "~/components/WipeInventoryDialog.vue";
219221
220222
import LabelCreateModal from "~/components/Label/CreateModal.vue";
221223
import LocationCreateModal from "~/components/Location/CreateModal.vue";

frontend/lib/api/classes/actions.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,14 @@ export class ActionsAPI extends BaseAPI {
3131
url: route("/actions/create-missing-thumbnails"),
3232
});
3333
}
34+
35+
wipeInventory(options?: { wipeLabels?: boolean; wipeLocations?: boolean; wipeMaintenance?: boolean }) {
36+
return this.http.post<
37+
{ wipeLabels?: boolean; wipeLocations?: boolean; wipeMaintenance?: boolean },
38+
ActionAmountResult
39+
>({
40+
url: route("/actions/wipe-inventory"),
41+
body: options || {},
42+
});
43+
}
3444
}

frontend/locales/en.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -735,12 +735,23 @@
735735
"set_primary_photo_button": "Set Primary Photo",
736736
"set_primary_photo_confirm": "Are you sure you want to set primary photos? This can take a while and cannot be undone.",
737737
"set_primary_photo_sub": "In version v0.10.0 of Homebox, the primary image field was added to attachments of type photo. This action will set the primary image field to the first image in the attachments array in the database, if it is not already set. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/pull/576\">'See GitHub PR #576'</a>'",
738+
"wipe_inventory": "Wipe Inventory",
739+
"wipe_inventory_button": "Wipe Inventory",
740+
"wipe_inventory_confirm": "Are you sure you want to wipe your entire inventory? This will delete all items and cannot be undone.",
741+
"wipe_inventory_labels": "Also wipe all labels (tags)",
742+
"wipe_inventory_locations": "Also wipe all locations",
743+
"wipe_inventory_maintenance": "Also wipe all maintenance records",
744+
"wipe_inventory_note": "Note: Only group owners can perform this action.",
745+
"wipe_inventory_sub": "Permanently deletes all items in your inventory. This action is irreversible and will remove all item data including attachments and photos.",
738746
"zero_datetimes": "Zero Item Date Times",
739747
"zero_datetimes_button": "Zero Item Date Times",
740748
"zero_datetimes_confirm": "Are you sure you want to reset all date and time values? This can take a while and cannot be undone.",
741749
"zero_datetimes_sub": "Resets the time value for all date time fields in your inventory to the beginning of the date. This is to fix a bug that was introduced early on in the development of the site that caused the time value to be stored with the time which caused issues with date fields displaying accurate values. '<a class=\"link\" href=\"https://github.com/hay-kot/homebox/issues/236\" target=\"_blank\">'See Github Issue #236 for more details.'</a>'"
742750
},
743751
"actions_sub": "Apply Actions to your inventory in bulk. These are irreversible actions. '<b>'Be careful.'</b>'",
752+
"demo_mode_error": {
753+
"wipe_inventory": "Inventory, labels, locations and maintenance records cannot be wiped whilst Homebox is in demo mode. Please ensure that you are not in demo mode and try again."
754+
},
744755
"import_export": "Import/Export",
745756
"import_export_set": {
746757
"export": "Export Inventory",
@@ -768,7 +779,9 @@
768779
"failed_ensure_ids": "Failed to ensure asset IDs.",
769780
"failed_ensure_import_refs": "Failed to ensure import refs.",
770781
"failed_set_primary_photos": "Failed to set primary photos.",
771-
"failed_zero_datetimes": "Failed to reset date and time values."
782+
"failed_wipe_inventory": "Failed to wipe inventory.",
783+
"failed_zero_datetimes": "Failed to reset date and time values.",
784+
"wipe_inventory_success": "Successfully wiped inventory. { results } items deleted."
772785
}
773786
}
774787
}

0 commit comments

Comments
 (0)