mirror of
https://github.com/sysadminsmedia/homebox.git
synced 2025-12-21 13:23:14 +01:00
feat: improved duplicate (#927)
* feat: improved duplicate * feat: enhance item duplication process with transaction handling and error logging for attachments and fields * feat: add error logging during transaction rollback in item duplication process for better debugging * feat: don't try and rollback is the commit succeeded * feat: add customizable duplication options for items, including prefix and field copying settings in API and UI * fix: simplify duplication checks for custom fields, attachments, and maintenance entries in ItemsRepository duplication method * refactor: import DuplicateSettings type from composables and sort import issues
This commit is contained in:
@@ -254,6 +254,25 @@ func (ctrl *V1Controller) HandleItemPatch() errchain.HandlerFunc {
|
|||||||
return adapters.ActionID("id", fn, http.StatusOK)
|
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
|
// HandleGetAllCustomFieldNames godocs
|
||||||
//
|
//
|
||||||
// @Summary Get All Custom Field Names
|
// @Summary Get All Custom Field Names
|
||||||
|
|||||||
@@ -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.Put("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemUpdate(), userMW...))
|
||||||
r.Patch("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemPatch(), userMW...))
|
r.Patch("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemPatch(), userMW...))
|
||||||
r.Delete("/items/{id}", chain.ToHandlerFunc(v1Ctrl.HandleItemDelete(), 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.Post("/items/{id}/attachments", chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentCreate(), userMW...))
|
||||||
r.Put("/items/{id}/attachments/{attachment_id}", chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentUpdate(), userMW...))
|
r.Put("/items/{id}/attachments/{attachment_id}", chain.ToHandlerFunc(v1Ctrl.HandleItemAttachmentUpdate(), userMW...))
|
||||||
|
|||||||
@@ -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": {
|
"/v1/items/{id}/maintenance": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"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": {
|
"repo.Group": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -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": {
|
"/v1/items/{id}/maintenance": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -3127,6 +3169,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"repo.DuplicateOptions": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"copyAttachments": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"copyCustomFields": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"copyMaintenance": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"copyPrefix": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"repo.Group": {
|
"repo.Group": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -667,6 +667,17 @@ definitions:
|
|||||||
search_engine_name:
|
search_engine_name:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
repo.DuplicateOptions:
|
||||||
|
properties:
|
||||||
|
copyAttachments:
|
||||||
|
type: boolean
|
||||||
|
copyCustomFields:
|
||||||
|
type: boolean
|
||||||
|
copyMaintenance:
|
||||||
|
type: boolean
|
||||||
|
copyPrefix:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
repo.Group:
|
repo.Group:
|
||||||
properties:
|
properties:
|
||||||
createdAt:
|
createdAt:
|
||||||
@@ -1968,6 +1979,32 @@ paths:
|
|||||||
summary: Update Item Attachment
|
summary: Update Item Attachment
|
||||||
tags:
|
tags:
|
||||||
- Items Attachments
|
- 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:
|
/v1/items/{id}/maintenance:
|
||||||
get:
|
get:
|
||||||
parameters:
|
parameters:
|
||||||
|
|||||||
@@ -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/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 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
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 h1:n7UlKiQnxVrgxKoM0d7usZiN+Z0y2lVENtYLgKtXS6s=
|
||||||
github.com/olahol/melody v1.3.0/go.mod h1:GgkTl6Y7yWj/HtfD48Q5vLKPVoZOH+Qqgfa7CvJgJM4=
|
github.com/olahol/melody v1.3.0/go.mod h1:GgkTl6Y7yWj/HtfD48Q5vLKPVoZOH+Qqgfa7CvJgJM4=
|
||||||
github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU=
|
github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU=
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ func (svc *ItemService) Create(ctx Context, item repo.ItemCreate) (repo.ItemOut,
|
|||||||
return svc.repo.Items.Create(ctx, ctx.GID, item)
|
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) {
|
func (svc *ItemService) EnsureAssetID(ctx context.Context, gid uuid.UUID) (int, error) {
|
||||||
items, err := svc.repo.Items.GetAllZeroAssetID(ctx, gid)
|
items, err := svc.repo.Items.GetAllZeroAssetID(ctx, gid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"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/core/services/reporting/eventbus"
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent"
|
"github.com/sysadminsmedia/homebox/backend/internal/data/ent"
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/attachment"
|
"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/itemfield"
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/label"
|
"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/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/ent/predicate"
|
||||||
"github.com/sysadminsmedia/homebox/backend/internal/data/types"
|
"github.com/sysadminsmedia/homebox/backend/internal/data/types"
|
||||||
)
|
)
|
||||||
@@ -46,6 +48,13 @@ type (
|
|||||||
OrderBy string `json:"orderBy"`
|
OrderBy string `json:"orderBy"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DuplicateOptions struct {
|
||||||
|
CopyMaintenance bool `json:"copyMaintenance"`
|
||||||
|
CopyAttachments bool `json:"copyAttachments"`
|
||||||
|
CopyCustomFields bool `json:"copyCustomFields"`
|
||||||
|
CopyPrefix string `json:"copyPrefix"`
|
||||||
|
}
|
||||||
|
|
||||||
ItemField struct {
|
ItemField struct {
|
||||||
ID uuid.UUID `json:"id,omitempty"`
|
ID uuid.UUID `json:"id,omitempty"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
@@ -1004,3 +1013,164 @@ func (e *ItemsRepository) SetPrimaryPhotos(ctx context.Context, gid uuid.UUID) (
|
|||||||
|
|
||||||
return updated, nil
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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": {
|
"/v1/items/{id}/maintenance": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -3127,6 +3169,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"repo.DuplicateOptions": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"copyAttachments": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"copyCustomFields": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"copyMaintenance": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"copyPrefix": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"repo.Group": {
|
"repo.Group": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -667,6 +667,17 @@ definitions:
|
|||||||
search_engine_name:
|
search_engine_name:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
repo.DuplicateOptions:
|
||||||
|
properties:
|
||||||
|
copyAttachments:
|
||||||
|
type: boolean
|
||||||
|
copyCustomFields:
|
||||||
|
type: boolean
|
||||||
|
copyMaintenance:
|
||||||
|
type: boolean
|
||||||
|
copyPrefix:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
repo.Group:
|
repo.Group:
|
||||||
properties:
|
properties:
|
||||||
createdAt:
|
createdAt:
|
||||||
@@ -1968,6 +1979,32 @@ paths:
|
|||||||
summary: Update Item Attachment
|
summary: Update Item Attachment
|
||||||
tags:
|
tags:
|
||||||
- Items Attachments
|
- 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:
|
/v1/items/{id}/maintenance:
|
||||||
get:
|
get:
|
||||||
parameters:
|
parameters:
|
||||||
|
|||||||
83
frontend/components/Item/DuplicateSettings.vue
Normal file
83
frontend/components/Item/DuplicateSettings.vue
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import type { DuplicateSettings } from "~/composables/use-preferences";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
modelValue: DuplicateSettings;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Emits = {
|
||||||
|
(e: "update:modelValue", value: DuplicateSettings): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const enableCustomPrefix = ref(props.modelValue.copyPrefixOverride !== null);
|
||||||
|
const prefix = ref(props.modelValue.copyPrefixOverride ?? "");
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
const settings = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: value => emit("update:modelValue", value),
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Switch id="copy-maintenance" v-model="settings.copyMaintenance" />
|
||||||
|
<Label for="copy-maintenance">
|
||||||
|
{{ $t("items.duplicate.copy_maintenance") }}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Switch id="copy-attachments" v-model="settings.copyAttachments" />
|
||||||
|
<Label for="copy-attachments">
|
||||||
|
{{ $t("items.duplicate.copy_attachments") }}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Switch id="copy-custom-fields" v-model="settings.copyCustomFields" />
|
||||||
|
<Label for="copy-custom-fields">
|
||||||
|
{{ $t("items.duplicate.copy_custom_fields") }}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
id="copy-prefix"
|
||||||
|
v-model="enableCustomPrefix"
|
||||||
|
@update:model-value="
|
||||||
|
v => {
|
||||||
|
settings.copyPrefixOverride = v ? prefix : null;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<Label for="copy-prefix">{{ $t("items.duplicate.enable_custom_prefix") }}</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<Label for="copy-prefix" :class="{ 'opacity-50': !enableCustomPrefix }">
|
||||||
|
{{ $t("items.duplicate.custom_prefix") }}
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="copy-prefix"
|
||||||
|
v-model="prefix"
|
||||||
|
:disabled="!enableCustomPrefix"
|
||||||
|
:placeholder="$t('items.duplicate.prefix')"
|
||||||
|
class="w-full"
|
||||||
|
@input="settings.copyPrefixOverride = prefix"
|
||||||
|
/>
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
{{ $t("items.duplicate.prefix_instructions") }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -10,6 +10,8 @@ export enum DialogID {
|
|||||||
CreateLocation = 'create-location',
|
CreateLocation = 'create-location',
|
||||||
CreateLabel = 'create-label',
|
CreateLabel = 'create-label',
|
||||||
CreateNotifier = 'create-notifier',
|
CreateNotifier = 'create-notifier',
|
||||||
|
DuplicateSettings = 'duplicate-settings',
|
||||||
|
DuplicateTemporarySettings = 'duplicate-temporary-settings',
|
||||||
EditMaintenance = 'edit-maintenance',
|
EditMaintenance = 'edit-maintenance',
|
||||||
Import = 'import',
|
Import = 'import',
|
||||||
ItemImage = 'item-image',
|
ItemImage = 'item-image',
|
||||||
|
|||||||
@@ -4,6 +4,13 @@ import type { DaisyTheme } from "~~/lib/data/themes";
|
|||||||
|
|
||||||
export type ViewType = "table" | "card" | "tree";
|
export type ViewType = "table" | "card" | "tree";
|
||||||
|
|
||||||
|
export type DuplicateSettings = {
|
||||||
|
copyMaintenance: boolean;
|
||||||
|
copyAttachments: boolean;
|
||||||
|
copyCustomFields: boolean;
|
||||||
|
copyPrefixOverride: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type LocationViewPreferences = {
|
export type LocationViewPreferences = {
|
||||||
showDetails: boolean;
|
showDetails: boolean;
|
||||||
showEmpty: boolean;
|
showEmpty: boolean;
|
||||||
@@ -15,6 +22,7 @@ export type LocationViewPreferences = {
|
|||||||
displayLegacyHeader: boolean;
|
displayLegacyHeader: boolean;
|
||||||
language?: string;
|
language?: string;
|
||||||
overrideFormatLocale?: string;
|
overrideFormatLocale?: string;
|
||||||
|
duplicateSettings: DuplicateSettings;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,6 +42,12 @@ export function useViewPreferences(): Ref<LocationViewPreferences> {
|
|||||||
displayLegacyHeader: false,
|
displayLegacyHeader: false,
|
||||||
language: null,
|
language: null,
|
||||||
overrideFormatLocale: null,
|
overrideFormatLocale: null,
|
||||||
|
duplicateSettings: {
|
||||||
|
copyMaintenance: false,
|
||||||
|
copyAttachments: true,
|
||||||
|
copyCustomFields: true,
|
||||||
|
copyPrefixOverride: null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{ mergeDefaults: true }
|
{ mergeDefaults: true }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -153,6 +153,26 @@ export class ItemsApi extends BaseAPI {
|
|||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
duplicate(
|
||||||
|
id: string,
|
||||||
|
options: {
|
||||||
|
copyMaintenance?: boolean;
|
||||||
|
copyAttachments?: boolean;
|
||||||
|
copyCustomFields?: boolean;
|
||||||
|
copyPrefix?: string;
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
|
return this.http.post<typeof options, ItemOut>({
|
||||||
|
url: route(`/items/${id}/duplicate`),
|
||||||
|
body: {
|
||||||
|
copyMaintenance: options.copyMaintenance,
|
||||||
|
copyAttachments: options.copyAttachments,
|
||||||
|
copyCustomFields: options.copyCustomFields,
|
||||||
|
copyPrefix: options.copyPrefix,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
import(file: File | Blob) {
|
import(file: File | Blob) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("csv", file);
|
formData.append("csv", file);
|
||||||
|
|||||||
@@ -454,10 +454,6 @@ export interface EntUserEdges {
|
|||||||
export interface BarcodeProduct {
|
export interface BarcodeProduct {
|
||||||
barcode: string;
|
barcode: string;
|
||||||
imageBase64: string;
|
imageBase64: string;
|
||||||
/**
|
|
||||||
* TODO: add image attachement
|
|
||||||
* TODO: add asin?
|
|
||||||
*/
|
|
||||||
imageURL: string;
|
imageURL: string;
|
||||||
item: ItemCreate;
|
item: ItemCreate;
|
||||||
manufacturer: string;
|
manufacturer: string;
|
||||||
|
|||||||
@@ -289,6 +289,18 @@
|
|||||||
"delete_attachment_confirm": "Are you sure you want to delete this attachment?",
|
"delete_attachment_confirm": "Are you sure you want to delete this attachment?",
|
||||||
"delete_item_confirm": "Are you sure you want to delete this item?",
|
"delete_item_confirm": "Are you sure you want to delete this item?",
|
||||||
"description": "Description",
|
"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",
|
"details": "Details",
|
||||||
"drag_and_drop": "Drag and drop files here or click to select files",
|
"drag_and_drop": "Drag and drop files here or click to select files",
|
||||||
"edit": {
|
"edit": {
|
||||||
|
|||||||
@@ -42,6 +42,13 @@
|
|||||||
const itemId = computed<string>(() => route.params.id as string);
|
const itemId = computed<string>(() => route.params.id as string);
|
||||||
const preferences = useViewPreferences();
|
const preferences = useViewPreferences();
|
||||||
|
|
||||||
|
const temporaryDuplicateSettings = ref<DuplicateSettings>({
|
||||||
|
copyMaintenance: preferences.value.duplicateSettings.copyMaintenance,
|
||||||
|
copyAttachments: preferences.value.duplicateSettings.copyAttachments,
|
||||||
|
copyCustomFields: preferences.value.duplicateSettings.copyCustomFields,
|
||||||
|
copyPrefixOverride: preferences.value.duplicateSettings.copyPrefixOverride,
|
||||||
|
});
|
||||||
|
|
||||||
const hasNested = computed<boolean>(() => {
|
const hasNested = computed<boolean>(() => {
|
||||||
return route.fullPath.split("/").at(-1) !== itemId.value;
|
return route.fullPath.split("/").at(-1) !== itemId.value;
|
||||||
});
|
});
|
||||||
@@ -473,43 +480,43 @@
|
|||||||
return resp.data.items;
|
return resp.data.items;
|
||||||
});
|
});
|
||||||
|
|
||||||
async function duplicateItem() {
|
async function duplicateItem(settings?: DuplicateSettings) {
|
||||||
if (!item.value) {
|
if (!item.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { error, data } = await api.items.create({
|
const duplicateSettings = settings
|
||||||
name: `${item.value.name} Copy`,
|
? {
|
||||||
description: item.value.description,
|
copyMaintenance: settings.copyMaintenance,
|
||||||
quantity: item.value.quantity,
|
copyAttachments: settings.copyAttachments,
|
||||||
locationId: item.value.location!.id,
|
copyCustomFields: settings.copyCustomFields,
|
||||||
parentId: item.value.parent?.id,
|
copyPrefix: settings.copyPrefixOverride ?? t("items.duplicate.prefix"),
|
||||||
labelIds: item.value.labels.map(l => l.id),
|
}
|
||||||
});
|
: {
|
||||||
|
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) {
|
if (error) {
|
||||||
toast.error(t("items.toast.failed_duplicate_item"));
|
toast.error(t("items.toast.failed_duplicate_item"));
|
||||||
return;
|
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}`);
|
navigateTo(`/item/${data.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleDuplicateClick(event: MouseEvent) {
|
||||||
|
if (event.shiftKey) {
|
||||||
|
openDialog(DialogID.DuplicateTemporarySettings);
|
||||||
|
} else {
|
||||||
|
duplicateItem();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
|
|
||||||
async function deleteItem() {
|
async function deleteItem() {
|
||||||
@@ -545,6 +552,25 @@
|
|||||||
<!-- set page title -->
|
<!-- set page title -->
|
||||||
<Title>{{ item.name }}</Title>
|
<Title>{{ item.name }}</Title>
|
||||||
|
|
||||||
|
<Dialog :dialog-id="DialogID.DuplicateTemporarySettings">
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{{ $t("items.duplicate.temporary_title") }}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<ItemDuplicateSettings v-model="temporaryDuplicateSettings" />
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
@click="
|
||||||
|
closeDialog(DialogID.DuplicateTemporarySettings);
|
||||||
|
duplicateItem(temporaryDuplicateSettings);
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ $t("global.duplicate") }}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<Dialog :dialog-id="DialogID.ItemImage">
|
<Dialog :dialog-id="DialogID.ItemImage">
|
||||||
<DialogContent class="w-auto border-transparent bg-transparent p-0" disable-close>
|
<DialogContent class="w-auto border-transparent bg-transparent p-0" disable-close>
|
||||||
<picture>
|
<picture>
|
||||||
@@ -623,7 +649,7 @@
|
|||||||
<MdiPlus />
|
<MdiPlus />
|
||||||
<span class="hidden md:inline">{{ $t("global.create_subitem") }}</span>
|
<span class="hidden md:inline">{{ $t("global.create_subitem") }}</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button class="w-9 md:w-auto" :aria-label="$t('global.duplicate')" @click="duplicateItem">
|
<Button class="w-9 md:w-auto" :aria-label="$t('global.duplicate')" @click="handleDuplicateClick">
|
||||||
<MdiContentCopy />
|
<MdiContentCopy />
|
||||||
<span class="hidden md:inline">{{ $t("global.duplicate") }}</span>
|
<span class="hidden md:inline">{{ $t("global.duplicate") }}</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -305,6 +305,18 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<Dialog :dialog-id="DialogID.DuplicateSettings">
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{{ $t("items.duplicate.title") }}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<ItemDuplicateSettings v-model="preferences.duplicateSettings" />
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
{{ $t("items.duplicate.override_instructions") }}
|
||||||
|
</p>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<Dialog :dialog-id="DialogID.ChangePassword">
|
<Dialog :dialog-id="DialogID.ChangePassword">
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -376,6 +388,9 @@
|
|||||||
{{ $t("profile.change_password") }}
|
{{ $t("profile.change_password") }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="secondary" size="sm" @click="generateToken"> {{ $t("profile.gen_invite") }} </Button>
|
<Button variant="secondary" size="sm" @click="generateToken"> {{ $t("profile.gen_invite") }} </Button>
|
||||||
|
<Button variant="secondary" size="sm" @click="openDialog(DialogID.DuplicateSettings)">
|
||||||
|
{{ $t("items.duplicate.title") }}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="token" class="flex items-center gap-2 pl-1 pt-4">
|
<div v-if="token" class="flex items-center gap-2 pl-1 pt-4">
|
||||||
<CopyText :text="tokenUrl" />
|
<CopyText :text="tokenUrl" />
|
||||||
|
|||||||
Reference in New Issue
Block a user