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>
This commit is contained in:
copilot-swe-agent[bot]
2025-12-28 15:41:13 +00:00
parent 27309e61da
commit 7aaaa346ab
8 changed files with 166 additions and 18 deletions

View File

@@ -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})
}
}

View File

@@ -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
}

View File

@@ -0,0 +1,85 @@
<template>
<BaseModal v-model="dialog" max-width="600px">
<template #title>
<span>{{ $t("tools.actions_set.wipe_inventory") }}</span>
</template>
<div class="space-y-4">
<p class="text-base">
{{ $t("tools.actions_set.wipe_inventory_confirm") }}
</p>
<div class="space-y-2">
<div class="flex items-center space-x-2">
<input
id="wipe-labels-checkbox"
v-model="wipeLabels"
type="checkbox"
class="h-4 w-4 rounded border-gray-300"
/>
<label for="wipe-labels-checkbox" class="text-sm font-medium cursor-pointer">
{{ $t("tools.actions_set.wipe_inventory_labels") }}
</label>
</div>
<div class="flex items-center space-x-2">
<input
id="wipe-locations-checkbox"
v-model="wipeLocations"
type="checkbox"
class="h-4 w-4 rounded border-gray-300"
/>
<label for="wipe-locations-checkbox" class="text-sm font-medium cursor-pointer">
{{ $t("tools.actions_set.wipe_inventory_locations") }}
</label>
</div>
</div>
<p class="text-sm text-gray-600">
{{ $t("tools.actions_set.wipe_inventory_note") }}
</p>
</div>
<template #actions>
<BaseButton @click="close"> {{ $t("global.cancel") }} </BaseButton>
<BaseButton type="primary" @click="confirm">
{{ $t("global.confirm") }}
</BaseButton>
</template>
</BaseModal>
</template>
<script setup lang="ts">
import { DialogID } from "~/components/ui/dialog-provider/utils";
import { useDialog } from "~/components/ui/dialog-provider";
const { registerOpenDialogCallback, closeDialog } = useDialog();
const dialog = ref(false);
const wipeLabels = ref(false);
const wipeLocations = ref(false);
let onCloseCallback: ((result?: { wipeLabels: boolean; wipeLocations: boolean } | undefined) => void) | undefined;
registerOpenDialogCallback(DialogID.WipeInventory, (params?: { onClose?: (result?: { wipeLabels: boolean; wipeLocations: boolean } | undefined) => void }) => {
dialog.value = true;
wipeLabels.value = false;
wipeLocations.value = false;
onCloseCallback = params?.onClose;
});
function close() {
dialog.value = false;
closeDialog(DialogID.WipeInventory, undefined);
onCloseCallback?.(undefined);
}
function confirm() {
dialog.value = false;
const result = {
wipeLabels: wipeLabels.value,
wipeLocations: wipeLocations.value,
};
closeDialog(DialogID.WipeInventory, result);
onCloseCallback?.(result);
}
</script>

View File

@@ -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 */

View File

@@ -8,6 +8,7 @@
<ModalConfirm />
<OutdatedModal v-if="status" :status="status" />
<ItemCreateModal />
<WipeInventoryDialog />
<LabelCreateModal />
<LocationCreateModal />
<ItemBarcodeModal />
@@ -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";

View File

@@ -32,9 +32,10 @@ export class ActionsAPI extends BaseAPI {
});
}
wipeInventory() {
return this.http.post<void, ActionAmountResult>({
wipeInventory(options?: { wipeLabels?: boolean; wipeLocations?: boolean }) {
return this.http.post<{ wipeLabels?: boolean; wipeLocations?: boolean }, ActionAmountResult>({
url: route("/actions/wipe-inventory"),
body: options || {},
});
}
}

View File

@@ -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",

View File

@@ -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 }));
},
});
}
</script>