diff --git a/backend/app/api/handlers/v1/v1_ctrl_items.go b/backend/app/api/handlers/v1/v1_ctrl_items.go index 0deecea8..5b38bda7 100644 --- a/backend/app/api/handlers/v1/v1_ctrl_items.go +++ b/backend/app/api/handlers/v1/v1_ctrl_items.go @@ -254,6 +254,25 @@ func (ctrl *V1Controller) HandleItemPatch() errchain.HandlerFunc { return adapters.ActionID("id", fn, http.StatusOK) } +// HandleItemDuplicate godocs +// +// @Summary Duplicate Item +// @Tags Items +// @Produce json +// @Param id path string true "Item ID" +// @Param payload body repo.DuplicateOptions true "Duplicate Options" +// @Success 201 {object} repo.ItemOut +// @Router /v1/items/{id}/duplicate [POST] +// @Security Bearer +func (ctrl *V1Controller) HandleItemDuplicate() errchain.HandlerFunc { + fn := func(r *http.Request, ID uuid.UUID, options repo.DuplicateOptions) (repo.ItemOut, error) { + ctx := services.NewContext(r.Context()) + return ctrl.svc.Items.Duplicate(ctx, ctx.GID, ID, options) + } + + return adapters.ActionID("id", fn, http.StatusCreated) +} + // HandleGetAllCustomFieldNames godocs // // @Summary Get All Custom Field Names diff --git a/backend/app/api/routes.go b/backend/app/api/routes.go index 74f8595f..5f6a5ca6 100644 --- a/backend/app/api/routes.go +++ b/backend/app/api/routes.go @@ -129,6 +129,7 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR r.Put("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemUpdate(), userMW...)) r.Patch("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemPatch(), userMW...)) r.Delete("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemDelete(), userMW...)) + r.Post("/items/{id}/duplicate", chain.ToHandlerFunc(v1Ctrl.HandleItemDuplicate(), userMW...)) r.Post("/items/{id}/attachments", chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentCreate(), userMW...)) r.Put("/items/{id}/attachments/{attachment_id}", chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentUpdate(), userMW...)) diff --git a/backend/app/api/static/docs/docs.go b/backend/app/api/static/docs/docs.go index cbf1d5a8..843aebf9 100644 --- a/backend/app/api/static/docs/docs.go +++ b/backend/app/api/static/docs/docs.go @@ -943,6 +943,48 @@ const docTemplate = `{ } } }, + "/v1/items/{id}/duplicate": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Items" + ], + "summary": "Duplicate Item", + "parameters": [ + { + "type": "string", + "description": "Item ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Duplicate Options", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/repo.DuplicateOptions" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/repo.ItemOut" + } + } + } + } + }, "/v1/items/{id}/maintenance": { "get": { "security": [ @@ -3129,6 +3171,23 @@ const docTemplate = `{ } } }, + "repo.DuplicateOptions": { + "type": "object", + "properties": { + "copyAttachments": { + "type": "boolean" + }, + "copyCustomFields": { + "type": "boolean" + }, + "copyMaintenance": { + "type": "boolean" + }, + "copyPrefix": { + "type": "string" + } + } + }, "repo.Group": { "type": "object", "properties": { diff --git a/backend/app/api/static/docs/swagger.json b/backend/app/api/static/docs/swagger.json index 75c84cd7..c62359b9 100644 --- a/backend/app/api/static/docs/swagger.json +++ b/backend/app/api/static/docs/swagger.json @@ -941,6 +941,48 @@ } } }, + "/v1/items/{id}/duplicate": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Items" + ], + "summary": "Duplicate Item", + "parameters": [ + { + "type": "string", + "description": "Item ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Duplicate Options", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/repo.DuplicateOptions" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/repo.ItemOut" + } + } + } + } + }, "/v1/items/{id}/maintenance": { "get": { "security": [ @@ -3127,6 +3169,23 @@ } } }, + "repo.DuplicateOptions": { + "type": "object", + "properties": { + "copyAttachments": { + "type": "boolean" + }, + "copyCustomFields": { + "type": "boolean" + }, + "copyMaintenance": { + "type": "boolean" + }, + "copyPrefix": { + "type": "string" + } + } + }, "repo.Group": { "type": "object", "properties": { diff --git a/backend/app/api/static/docs/swagger.yaml b/backend/app/api/static/docs/swagger.yaml index e47ad0df..8289cc18 100644 --- a/backend/app/api/static/docs/swagger.yaml +++ b/backend/app/api/static/docs/swagger.yaml @@ -667,6 +667,17 @@ definitions: search_engine_name: type: string type: object + repo.DuplicateOptions: + properties: + copyAttachments: + type: boolean + copyCustomFields: + type: boolean + copyMaintenance: + type: boolean + copyPrefix: + type: string + type: object repo.Group: properties: createdAt: @@ -1968,6 +1979,32 @@ paths: summary: Update Item Attachment tags: - Items Attachments + /v1/items/{id}/duplicate: + post: + parameters: + - description: Item ID + in: path + name: id + required: true + type: string + - description: Duplicate Options + in: body + name: payload + required: true + schema: + $ref: '#/definitions/repo.DuplicateOptions' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/repo.ItemOut' + security: + - Bearer: [] + summary: Duplicate Item + tags: + - Items /v1/items/{id}/maintenance: get: parameters: diff --git a/backend/go.sum b/backend/go.sum index fe5062e6..b24c7400 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -337,6 +337,7 @@ github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/olahol/melody v1.3.0 h1:n7UlKiQnxVrgxKoM0d7usZiN+Z0y2lVENtYLgKtXS6s= github.com/olahol/melody v1.3.0/go.mod h1:GgkTl6Y7yWj/HtfD48Q5vLKPVoZOH+Qqgfa7CvJgJM4= github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU= diff --git a/backend/internal/core/services/service_items.go b/backend/internal/core/services/service_items.go index 650ce6a6..95bb0fb7 100644 --- a/backend/internal/core/services/service_items.go +++ b/backend/internal/core/services/service_items.go @@ -38,6 +38,10 @@ func (svc *ItemService) Create(ctx Context, item repo.ItemCreate) (repo.ItemOut, return svc.repo.Items.Create(ctx, ctx.GID, item) } +func (svc *ItemService) Duplicate(ctx Context, gid, id uuid.UUID, options repo.DuplicateOptions) (repo.ItemOut, error) { + return svc.repo.Items.Duplicate(ctx, gid, id, options) +} + func (svc *ItemService) EnsureAssetID(ctx context.Context, gid uuid.UUID) (int, error) { items, err := svc.repo.Items.GetAllZeroAssetID(ctx, gid) if err != nil { diff --git a/backend/internal/data/repo/repo_items.go b/backend/internal/data/repo/repo_items.go index b3a5d75a..4fb50255 100644 --- a/backend/internal/data/repo/repo_items.go +++ b/backend/internal/data/repo/repo_items.go @@ -6,6 +6,7 @@ import ( "time" "github.com/google/uuid" + "github.com/rs/zerolog/log" "github.com/sysadminsmedia/homebox/backend/internal/core/services/reporting/eventbus" "github.com/sysadminsmedia/homebox/backend/internal/data/ent" "github.com/sysadminsmedia/homebox/backend/internal/data/ent/attachment" @@ -14,6 +15,7 @@ import ( "github.com/sysadminsmedia/homebox/backend/internal/data/ent/itemfield" "github.com/sysadminsmedia/homebox/backend/internal/data/ent/label" "github.com/sysadminsmedia/homebox/backend/internal/data/ent/location" + "github.com/sysadminsmedia/homebox/backend/internal/data/ent/maintenanceentry" "github.com/sysadminsmedia/homebox/backend/internal/data/ent/predicate" "github.com/sysadminsmedia/homebox/backend/internal/data/types" ) @@ -46,6 +48,13 @@ type ( OrderBy string `json:"orderBy"` } + DuplicateOptions struct { + CopyMaintenance bool `json:"copyMaintenance"` + CopyAttachments bool `json:"copyAttachments"` + CopyCustomFields bool `json:"copyCustomFields"` + CopyPrefix string `json:"copyPrefix"` + } + ItemField struct { ID uuid.UUID `json:"id,omitempty"` Type string `json:"type"` @@ -1004,3 +1013,164 @@ func (e *ItemsRepository) SetPrimaryPhotos(ctx context.Context, gid uuid.UUID) ( return updated, nil } + +// Duplicate creates a copy of an item with configurable options for what data to copy. +// The new item will have the next available asset ID and a customizable prefix in the name. +func (e *ItemsRepository) Duplicate(ctx context.Context, gid, id uuid.UUID, options DuplicateOptions) (ItemOut, error) { + tx, err := e.db.Tx(ctx) + if err != nil { + return ItemOut{}, err + } + committed := false + defer func() { + if !committed { + if err := tx.Rollback(); err != nil { + log.Warn().Err(err).Msg("failed to rollback transaction during item duplication") + } + } + }() + + // Get the original item with all its data + originalItem, err := e.getOne(ctx, item.ID(id), item.HasGroupWith(group.ID(gid))) + if err != nil { + return ItemOut{}, err + } + + nextAssetID, err := e.GetHighestAssetID(ctx, gid) + if err != nil { + return ItemOut{}, err + } + nextAssetID++ + + // Set default copy prefix if not provided + if options.CopyPrefix == "" { + options.CopyPrefix = "Copy of " + } + + // Create the new item directly in the transaction + newItemID := uuid.New() + itemBuilder := tx.Item.Create(). + SetID(newItemID). + SetName(options.CopyPrefix + originalItem.Name). + SetDescription(originalItem.Description). + SetQuantity(originalItem.Quantity). + SetLocationID(originalItem.Location.ID). + SetGroupID(gid). + SetAssetID(int(nextAssetID)). + SetSerialNumber(originalItem.SerialNumber). + SetModelNumber(originalItem.ModelNumber). + SetManufacturer(originalItem.Manufacturer). + SetLifetimeWarranty(originalItem.LifetimeWarranty). + SetWarrantyExpires(originalItem.WarrantyExpires.Time()). + SetWarrantyDetails(originalItem.WarrantyDetails). + SetPurchaseTime(originalItem.PurchaseTime.Time()). + SetPurchaseFrom(originalItem.PurchaseFrom). + SetPurchasePrice(originalItem.PurchasePrice). + SetSoldTime(originalItem.SoldTime.Time()). + SetSoldTo(originalItem.SoldTo). + SetSoldPrice(originalItem.SoldPrice). + SetSoldNotes(originalItem.SoldNotes). + SetNotes(originalItem.Notes). + SetInsured(originalItem.Insured). + SetArchived(originalItem.Archived). + SetSyncChildItemsLocations(originalItem.SyncChildItemsLocations) + + if originalItem.Parent != nil { + itemBuilder.SetParentID(originalItem.Parent.ID) + } + + // Add labels + if len(originalItem.Labels) > 0 { + labelIDs := make([]uuid.UUID, len(originalItem.Labels)) + for i, label := range originalItem.Labels { + labelIDs[i] = label.ID + } + itemBuilder.AddLabelIDs(labelIDs...) + } + + _, err = itemBuilder.Save(ctx) + if err != nil { + return ItemOut{}, err + } + + // Copy custom fields if requested + if options.CopyCustomFields { + for _, field := range originalItem.Fields { + _, err = tx.ItemField.Create(). + SetItemID(newItemID). + SetType(itemfield.Type(field.Type)). + SetName(field.Name). + SetTextValue(field.TextValue). + SetNumberValue(field.NumberValue). + SetBooleanValue(field.BooleanValue). + Save(ctx) + if err != nil { + log.Warn().Err(err).Str("field_name", field.Name).Msg("failed to copy custom field during duplication") + continue + } + } + } + + // Copy attachments if requested + if options.CopyAttachments { + for _, att := range originalItem.Attachments { + // Get the original attachment file + originalAttachment, err := tx.Attachment.Query(). + Where(attachment.ID(att.ID)). + Only(ctx) + if err != nil { + // Log error but continue to copy other attachments + log.Warn().Err(err).Str("attachment_id", att.ID.String()).Msg("failed to find attachment during duplication") + continue + } + + // Create a copy of the attachment with the same file path + // Since files are stored with hash-based paths, this is safe + _, err = tx.Attachment.Create(). + SetItemID(newItemID). + SetType(originalAttachment.Type). + SetTitle(originalAttachment.Title). + SetPath(originalAttachment.Path). + SetMimeType(originalAttachment.MimeType). + SetPrimary(originalAttachment.Primary). + Save(ctx) + if err != nil { + log.Warn().Err(err).Str("original_attachment_id", att.ID.String()).Msg("failed to copy attachment during duplication") + continue + } + } + } + + // Copy maintenance entries if requested + if options.CopyMaintenance { + maintenanceEntries, err := tx.MaintenanceEntry.Query(). + Where(maintenanceentry.HasItemWith(item.ID(id))). + All(ctx) + if err == nil { + for _, entry := range maintenanceEntries { + _, err = tx.MaintenanceEntry.Create(). + SetItemID(newItemID). + SetDate(entry.Date). + SetScheduledDate(entry.ScheduledDate). + SetName(entry.Name). + SetDescription(entry.Description). + SetCost(entry.Cost). + Save(ctx) + if err != nil { + log.Warn().Err(err).Str("maintenance_entry_id", entry.ID.String()).Msg("failed to copy maintenance entry during duplication") + continue + } + } + } + } + + if err := tx.Commit(); err != nil { + return ItemOut{}, err + } + committed = true + + e.publishMutationEvent(gid) + + // Get the final item with all copied data + return e.GetOne(ctx, newItemID) +} diff --git a/docs/en/api/openapi-2.0.json b/docs/en/api/openapi-2.0.json index 75c84cd7..c62359b9 100644 --- a/docs/en/api/openapi-2.0.json +++ b/docs/en/api/openapi-2.0.json @@ -941,6 +941,48 @@ } } }, + "/v1/items/{id}/duplicate": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Items" + ], + "summary": "Duplicate Item", + "parameters": [ + { + "type": "string", + "description": "Item ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Duplicate Options", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/repo.DuplicateOptions" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/repo.ItemOut" + } + } + } + } + }, "/v1/items/{id}/maintenance": { "get": { "security": [ @@ -3127,6 +3169,23 @@ } } }, + "repo.DuplicateOptions": { + "type": "object", + "properties": { + "copyAttachments": { + "type": "boolean" + }, + "copyCustomFields": { + "type": "boolean" + }, + "copyMaintenance": { + "type": "boolean" + }, + "copyPrefix": { + "type": "string" + } + } + }, "repo.Group": { "type": "object", "properties": { diff --git a/docs/en/api/openapi-2.0.yaml b/docs/en/api/openapi-2.0.yaml index e47ad0df..8289cc18 100644 --- a/docs/en/api/openapi-2.0.yaml +++ b/docs/en/api/openapi-2.0.yaml @@ -667,6 +667,17 @@ definitions: search_engine_name: type: string type: object + repo.DuplicateOptions: + properties: + copyAttachments: + type: boolean + copyCustomFields: + type: boolean + copyMaintenance: + type: boolean + copyPrefix: + type: string + type: object repo.Group: properties: createdAt: @@ -1968,6 +1979,32 @@ paths: summary: Update Item Attachment tags: - Items Attachments + /v1/items/{id}/duplicate: + post: + parameters: + - description: Item ID + in: path + name: id + required: true + type: string + - description: Duplicate Options + in: body + name: payload + required: true + schema: + $ref: '#/definitions/repo.DuplicateOptions' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/repo.ItemOut' + security: + - Bearer: [] + summary: Duplicate Item + tags: + - Items /v1/items/{id}/maintenance: get: parameters: diff --git a/frontend/components/Item/DuplicateSettings.vue b/frontend/components/Item/DuplicateSettings.vue new file mode 100644 index 00000000..2f1505e0 --- /dev/null +++ b/frontend/components/Item/DuplicateSettings.vue @@ -0,0 +1,83 @@ + + + diff --git a/frontend/components/ui/dialog-provider/utils.ts b/frontend/components/ui/dialog-provider/utils.ts index f3b1856e..b3043f10 100644 --- a/frontend/components/ui/dialog-provider/utils.ts +++ b/frontend/components/ui/dialog-provider/utils.ts @@ -10,6 +10,8 @@ export enum DialogID { CreateLocation = 'create-location', CreateLabel = 'create-label', CreateNotifier = 'create-notifier', + DuplicateSettings = 'duplicate-settings', + DuplicateTemporarySettings = 'duplicate-temporary-settings', EditMaintenance = 'edit-maintenance', Import = 'import', ItemImage = 'item-image', diff --git a/frontend/composables/use-preferences.ts b/frontend/composables/use-preferences.ts index 4a19d2a2..e9f99cee 100644 --- a/frontend/composables/use-preferences.ts +++ b/frontend/composables/use-preferences.ts @@ -4,6 +4,13 @@ import type { DaisyTheme } from "~~/lib/data/themes"; export type ViewType = "table" | "card" | "tree"; +export type DuplicateSettings = { + copyMaintenance: boolean; + copyAttachments: boolean; + copyCustomFields: boolean; + copyPrefixOverride: string | null; +}; + export type LocationViewPreferences = { showDetails: boolean; showEmpty: boolean; @@ -15,6 +22,7 @@ export type LocationViewPreferences = { displayLegacyHeader: boolean; language?: string; overrideFormatLocale?: string; + duplicateSettings: DuplicateSettings; }; /** @@ -34,6 +42,12 @@ export function useViewPreferences(): Ref { displayLegacyHeader: false, language: null, overrideFormatLocale: null, + duplicateSettings: { + copyMaintenance: false, + copyAttachments: true, + copyCustomFields: true, + copyPrefixOverride: null, + }, }, { mergeDefaults: true } ); diff --git a/frontend/lib/api/classes/items.ts b/frontend/lib/api/classes/items.ts index a649b288..3639b79e 100644 --- a/frontend/lib/api/classes/items.ts +++ b/frontend/lib/api/classes/items.ts @@ -153,6 +153,26 @@ export class ItemsApi extends BaseAPI { return resp; } + duplicate( + id: string, + options: { + copyMaintenance?: boolean; + copyAttachments?: boolean; + copyCustomFields?: boolean; + copyPrefix?: string; + } = {} + ) { + return this.http.post({ + url: route(`/items/${id}/duplicate`), + body: { + copyMaintenance: options.copyMaintenance, + copyAttachments: options.copyAttachments, + copyCustomFields: options.copyCustomFields, + copyPrefix: options.copyPrefix, + }, + }); + } + import(file: File | Blob) { const formData = new FormData(); formData.append("csv", file); diff --git a/frontend/lib/api/types/data-contracts.ts b/frontend/lib/api/types/data-contracts.ts index 279c9a24..460b84c7 100644 --- a/frontend/lib/api/types/data-contracts.ts +++ b/frontend/lib/api/types/data-contracts.ts @@ -454,10 +454,6 @@ export interface EntUserEdges { export interface BarcodeProduct { barcode: string; imageBase64: string; - /** - * TODO: add image attachement - * TODO: add asin? - */ imageURL: string; item: ItemCreate; manufacturer: string; diff --git a/frontend/locales/en.json b/frontend/locales/en.json index 66c471af..bc36d7ee 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -289,6 +289,18 @@ "delete_attachment_confirm": "Are you sure you want to delete this attachment?", "delete_item_confirm": "Are you sure you want to delete this item?", "description": "Description", + "duplicate": { + "prefix": "Copy of ", + "copy_maintenance": "Copy Maintenance", + "copy_attachments": "Copy Attachments", + "copy_custom_fields": "Copy Custom Fields", + "custom_prefix": "Copy Prefix", + "enable_custom_prefix": "Enable Custom Prefix", + "prefix_instructions": "This prefix will be added to the beginning of the duplicated item's name. Include a space at the end of the prefix to add a space between the prefix and the item name.", + "temporary_title": "Temporary Settings", + "title": "Duplicate Settings", + "override_instructions": "Hold shift when clicking the duplicate button to override these settings." + }, "details": "Details", "drag_and_drop": "Drag and drop files here or click to select files", "edit": { diff --git a/frontend/pages/item/[id]/index.vue b/frontend/pages/item/[id]/index.vue index 1acc3cbc..9ceafa30 100644 --- a/frontend/pages/item/[id]/index.vue +++ b/frontend/pages/item/[id]/index.vue @@ -42,6 +42,13 @@ const itemId = computed(() => route.params.id as string); const preferences = useViewPreferences(); + const temporaryDuplicateSettings = ref({ + copyMaintenance: preferences.value.duplicateSettings.copyMaintenance, + copyAttachments: preferences.value.duplicateSettings.copyAttachments, + copyCustomFields: preferences.value.duplicateSettings.copyCustomFields, + copyPrefixOverride: preferences.value.duplicateSettings.copyPrefixOverride, + }); + const hasNested = computed(() => { return route.fullPath.split("/").at(-1) !== itemId.value; }); @@ -473,43 +480,43 @@ return resp.data.items; }); - async function duplicateItem() { + async function duplicateItem(settings?: DuplicateSettings) { if (!item.value) { return; } - const { error, data } = await api.items.create({ - name: `${item.value.name} Copy`, - description: item.value.description, - quantity: item.value.quantity, - locationId: item.value.location!.id, - parentId: item.value.parent?.id, - labelIds: item.value.labels.map(l => l.id), - }); + const duplicateSettings = settings + ? { + copyMaintenance: settings.copyMaintenance, + copyAttachments: settings.copyAttachments, + copyCustomFields: settings.copyCustomFields, + copyPrefix: settings.copyPrefixOverride ?? t("items.duplicate.prefix"), + } + : { + copyMaintenance: preferences.value.duplicateSettings.copyMaintenance, + copyAttachments: preferences.value.duplicateSettings.copyAttachments, + copyCustomFields: preferences.value.duplicateSettings.copyCustomFields, + copyPrefix: preferences.value.duplicateSettings.copyPrefixOverride ?? t("items.duplicate.prefix"), + }; + + const { error, data } = await api.items.duplicate(itemId.value, duplicateSettings); if (error) { toast.error(t("items.toast.failed_duplicate_item")); return; } - // add extra fields - const { error: updateError } = await api.items.update(data.id, { - ...item.value, - id: data.id, - labelIds: data.labels.map(l => l.id), - locationId: data.location!.id, - name: data.name, - assetId: data.assetId, - }); - - if (updateError) { - toast.error(t("items.toast.failed_duplicate_item")); - return; - } - navigateTo(`/item/${data.id}`); } + function handleDuplicateClick(event: MouseEvent) { + if (event.shiftKey) { + openDialog(DialogID.DuplicateTemporarySettings); + } else { + duplicateItem(); + } + } + const confirm = useConfirm(); async function deleteItem() { @@ -545,6 +552,25 @@ {{ item.name }} + + + + {{ $t("items.duplicate.temporary_title") }} + + + + + + + + @@ -623,7 +649,7 @@ - diff --git a/frontend/pages/profile.vue b/frontend/pages/profile.vue index 3e826e92..a63dcb91 100644 --- a/frontend/pages/profile.vue +++ b/frontend/pages/profile.vue @@ -305,6 +305,18 @@