From 7aaaa346abe7ed5022e9f47ffdfd0e8329b7e7b1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 28 Dec 2025 15:41:13 +0000 Subject: [PATCH] Add wipe inventory options for labels/locations and owner-only restriction - Added WipeInventoryDialog component with checkboxes for wiping labels and locations - Modified backend WipeInventory method to accept wipeLabels and wipeLocations parameters - Added owner check in HandleWipeInventory to restrict action to group owners only - Updated frontend API client to send wipe options - Added new translation keys for checkbox labels and owner note - Integrated dialog into app layout and updated tools.vue to use new dialog Co-authored-by: katosdev <7927609+katosdev@users.noreply.github.com> --- .../app/api/handlers/v1/v1_ctrl_actions.go | 32 ++++++- backend/internal/data/repo/repo_items.go | 22 ++++- frontend/components/WipeInventoryDialog.vue | 85 +++++++++++++++++++ .../components/ui/dialog-provider/utils.ts | 2 + frontend/layouts/default.vue | 2 + frontend/lib/api/classes/actions.ts | 5 +- frontend/locales/en.json | 3 + frontend/pages/tools.vue | 33 ++++--- 8 files changed, 166 insertions(+), 18 deletions(-) create mode 100644 frontend/components/WipeInventoryDialog.vue diff --git a/backend/app/api/handlers/v1/v1_ctrl_actions.go b/backend/app/api/handlers/v1/v1_ctrl_actions.go index e9500abc..ef1f85a2 100644 --- a/backend/app/api/handlers/v1/v1_ctrl_actions.go +++ b/backend/app/api/handlers/v1/v1_ctrl_actions.go @@ -96,12 +96,19 @@ func (ctrl *V1Controller) HandleCreateMissingThumbnails() errchain.HandlerFunc { return actionHandlerFactory("create missing thumbnails", ctrl.repo.Attachments.CreateMissingThumbnails) } +// WipeInventoryOptions represents the options for wiping inventory +type WipeInventoryOptions struct { + WipeLabels bool `json:"wipeLabels"` + WipeLocations bool `json:"wipeLocations"` +} + // HandleWipeInventory godoc // // @Summary Wipe Inventory // @Description Deletes all items in the inventory // @Tags Actions // @Produce json +// @Param options body WipeInventoryOptions false "Wipe options" // @Success 200 {object} ActionAmountResult // @Router /v1/actions/wipe-inventory [Post] // @Security Bearer @@ -111,6 +118,29 @@ func (ctrl *V1Controller) HandleWipeInventory() errchain.HandlerFunc { return validate.NewRequestError(errors.New("wipe inventory is not allowed in demo mode"), http.StatusForbidden) } - return actionHandlerFactory("wipe inventory", ctrl.repo.Items.WipeInventory)(w, r) + ctx := services.NewContext(r.Context()) + + // Check if user is owner + if !ctx.User.IsOwner { + return validate.NewRequestError(errors.New("only group owners can wipe inventory"), http.StatusForbidden) + } + + // Parse options from request body + var options WipeInventoryOptions + if err := server.Decode(r, &options); err != nil { + // If no body provided, use default (false for both) + options = WipeInventoryOptions{ + WipeLabels: false, + WipeLocations: false, + } + } + + totalCompleted, err := ctrl.repo.Items.WipeInventory(ctx, ctx.GID, options.WipeLabels, options.WipeLocations) + if err != nil { + log.Err(err).Str("action_ref", "wipe inventory").Msg("failed to run action") + return validate.NewRequestError(err, http.StatusInternalServerError) + } + + return server.JSON(w, http.StatusOK, ActionAmountResult{Completed: totalCompleted}) } } diff --git a/backend/internal/data/repo/repo_items.go b/backend/internal/data/repo/repo_items.go index 2ac67c61..388a0537 100644 --- a/backend/internal/data/repo/repo_items.go +++ b/backend/internal/data/repo/repo_items.go @@ -809,7 +809,7 @@ func (e *ItemsRepository) DeleteByGroup(ctx context.Context, gid, id uuid.UUID) return err } -func (e *ItemsRepository) WipeInventory(ctx context.Context, gid uuid.UUID) (int, error) { +func (e *ItemsRepository) WipeInventory(ctx context.Context, gid uuid.UUID, wipeLabels bool, wipeLocations bool) (int, error) { // Get all items for the group items, err := e.db.Item.Query(). Where(item.HasGroupWith(group.ID(gid))). @@ -850,6 +850,26 @@ func (e *ItemsRepository) WipeInventory(ctx context.Context, gid uuid.UUID) (int deleted++ } + // Wipe labels if requested + if wipeLabels { + labelCount, err := e.db.Label.Delete().Where(label.HasGroupWith(group.ID(gid))).Exec(ctx) + if err != nil { + log.Err(err).Msg("failed to delete labels during wipe inventory") + } else { + log.Info().Int("count", labelCount).Msg("deleted labels during wipe inventory") + } + } + + // Wipe locations if requested + if wipeLocations { + locationCount, err := e.db.Location.Delete().Where(location.HasGroupWith(group.ID(gid))).Exec(ctx) + if err != nil { + log.Err(err).Msg("failed to delete locations during wipe inventory") + } else { + log.Info().Int("count", locationCount).Msg("deleted locations during wipe inventory") + } + } + e.publishMutationEvent(gid) return deleted, nil } diff --git a/frontend/components/WipeInventoryDialog.vue b/frontend/components/WipeInventoryDialog.vue new file mode 100644 index 00000000..dd218c7c --- /dev/null +++ b/frontend/components/WipeInventoryDialog.vue @@ -0,0 +1,85 @@ + + + + {{ $t("tools.actions_set.wipe_inventory") }} + + + + {{ $t("tools.actions_set.wipe_inventory_confirm") }} + + + + + + + {{ $t("tools.actions_set.wipe_inventory_labels") }} + + + + + + + {{ $t("tools.actions_set.wipe_inventory_locations") }} + + + + + + {{ $t("tools.actions_set.wipe_inventory_note") }} + + + + + {{ $t("global.cancel") }} + + {{ $t("global.confirm") }} + + + + + + diff --git a/frontend/components/ui/dialog-provider/utils.ts b/frontend/components/ui/dialog-provider/utils.ts index 8cc992cc..ef5b6b13 100644 --- a/frontend/components/ui/dialog-provider/utils.ts +++ b/frontend/components/ui/dialog-provider/utils.ts @@ -26,6 +26,7 @@ export enum DialogID { UpdateLocation = "update-location", UpdateTemplate = "update-template", ItemChangeDetails = "item-table-updater", + WipeInventory = "wipe-inventory", } /** @@ -71,6 +72,7 @@ export type DialogResultMap = { [DialogID.ItemImage]?: { action: "delete"; id: string }; [DialogID.EditMaintenance]?: boolean; [DialogID.ItemChangeDetails]?: boolean; + [DialogID.WipeInventory]?: { wipeLabels: boolean; wipeLocations: boolean }; }; /** Helpers to split IDs by requirement */ diff --git a/frontend/layouts/default.vue b/frontend/layouts/default.vue index 0d5d14db..b4b2fbee 100644 --- a/frontend/layouts/default.vue +++ b/frontend/layouts/default.vue @@ -8,6 +8,7 @@ + @@ -216,6 +217,7 @@ import ModalConfirm from "~/components/ModalConfirm.vue"; import OutdatedModal from "~/components/App/OutdatedModal.vue"; import ItemCreateModal from "~/components/Item/CreateModal.vue"; + import WipeInventoryDialog from "~/components/WipeInventoryDialog.vue"; import LabelCreateModal from "~/components/Label/CreateModal.vue"; import LocationCreateModal from "~/components/Location/CreateModal.vue"; diff --git a/frontend/lib/api/classes/actions.ts b/frontend/lib/api/classes/actions.ts index 01f6ed0f..958428a5 100644 --- a/frontend/lib/api/classes/actions.ts +++ b/frontend/lib/api/classes/actions.ts @@ -32,9 +32,10 @@ export class ActionsAPI extends BaseAPI { }); } - wipeInventory() { - return this.http.post({ + wipeInventory(options?: { wipeLabels?: boolean; wipeLocations?: boolean }) { + return this.http.post<{ wipeLabels?: boolean; wipeLocations?: boolean }, ActionAmountResult>({ url: route("/actions/wipe-inventory"), + body: options || {}, }); } } diff --git a/frontend/locales/en.json b/frontend/locales/en.json index 05ca503f..a7abe21b 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -738,6 +738,9 @@ "wipe_inventory": "Wipe Inventory", "wipe_inventory_button": "Wipe Inventory", "wipe_inventory_confirm": "Are you sure you want to wipe your entire inventory? This will delete all items and cannot be undone.", + "wipe_inventory_labels": "Also wipe all labels (tags)", + "wipe_inventory_locations": "Also wipe all locations", + "wipe_inventory_note": "Note: Only group owners can perform this action.", "wipe_inventory_sub": "Permanently deletes all items in your inventory. This action is irreversible and will remove all item data including attachments and photos.", "zero_datetimes": "Zero Item Date Times", "zero_datetimes_button": "Zero Item Date Times", diff --git a/frontend/pages/tools.vue b/frontend/pages/tools.vue index e86794f3..33bb7503 100644 --- a/frontend/pages/tools.vue +++ b/frontend/pages/tools.vue @@ -228,20 +228,25 @@ } async function wipeInventory() { - const { isCanceled } = await confirm.open(t("tools.actions_set.wipe_inventory_confirm")); - - if (isCanceled) { - return; - } - - const result = await api.actions.wipeInventory(); - - if (result.error) { - toast.error(t("tools.toast.failed_wipe_inventory")); - return; - } - - toast.success(t("tools.toast.wipe_inventory_success", { results: result.data.completed })); + openDialog(DialogID.WipeInventory, { + onClose: async (result) => { + if (!result) { + return; + } + + const apiResult = await api.actions.wipeInventory({ + wipeLabels: result.wipeLabels, + wipeLocations: result.wipeLocations, + }); + + if (apiResult.error) { + toast.error(t("tools.toast.failed_wipe_inventory")); + return; + } + + toast.success(t("tools.toast.wipe_inventory_success", { results: apiResult.data.completed })); + }, + }); }
+ {{ $t("tools.actions_set.wipe_inventory_confirm") }} +
+ {{ $t("tools.actions_set.wipe_inventory_note") }} +