feat: primary images (#576)

* add support for primary images

* fix locked loading state issue

* add action to auto-set images
This commit is contained in:
Hayden 2023-10-06 21:51:08 -05:00 committed by GitHub
parent 63a966c526
commit 318b8be192
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 649 additions and 207 deletions

View file

@ -68,3 +68,16 @@ func (ctrl *V1Controller) HandleEnsureImportRefs() errchain.HandlerFunc {
func (ctrl *V1Controller) HandleItemDateZeroOut() errchain.HandlerFunc { func (ctrl *V1Controller) HandleItemDateZeroOut() errchain.HandlerFunc {
return actionHandlerFactory("zero out date time", ctrl.repo.Items.ZeroOutTimeFields) return actionHandlerFactory("zero out date time", ctrl.repo.Items.ZeroOutTimeFields)
} }
// HandleSetPrimaryPhotos godoc
//
// @Summary Set Primary Photos
// @Description Sets the first photo of each item as the primary photo
// @Tags Actions
// @Produce json
// @Success 200 {object} ActionAmountResult
// @Router /v1/actions/set-primary-photos [Post]
// @Security Bearer
func (ctrl *V1Controller) HandleSetPrimaryPhotos() errchain.HandlerFunc {
return actionHandlerFactory("ensure asset IDs", ctrl.repo.Items.SetPrimaryPhotos)
}

View file

@ -92,6 +92,7 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
r.Post(v1Base("/actions/ensure-asset-ids"), chain.ToHandlerFunc(v1Ctrl.HandleEnsureAssetID(), userMW...)) r.Post(v1Base("/actions/ensure-asset-ids"), chain.ToHandlerFunc(v1Ctrl.HandleEnsureAssetID(), userMW...))
r.Post(v1Base("/actions/zero-item-time-fields"), chain.ToHandlerFunc(v1Ctrl.HandleItemDateZeroOut(), userMW...)) r.Post(v1Base("/actions/zero-item-time-fields"), chain.ToHandlerFunc(v1Ctrl.HandleItemDateZeroOut(), userMW...))
r.Post(v1Base("/actions/ensure-import-refs"), chain.ToHandlerFunc(v1Ctrl.HandleEnsureImportRefs(), userMW...)) r.Post(v1Base("/actions/ensure-import-refs"), chain.ToHandlerFunc(v1Ctrl.HandleEnsureImportRefs(), userMW...))
r.Post(v1Base("/actions/set-primary-photos"), chain.ToHandlerFunc(v1Ctrl.HandleSetPrimaryPhotos(), userMW...))
r.Get(v1Base("/locations"), chain.ToHandlerFunc(v1Ctrl.HandleLocationGetAll(), userMW...)) r.Get(v1Base("/locations"), chain.ToHandlerFunc(v1Ctrl.HandleLocationGetAll(), userMW...))
r.Post(v1Base("/locations"), chain.ToHandlerFunc(v1Ctrl.HandleLocationCreate(), userMW...)) r.Post(v1Base("/locations"), chain.ToHandlerFunc(v1Ctrl.HandleLocationCreate(), userMW...))

View file

@ -68,6 +68,31 @@ const docTemplate = `{
} }
} }
}, },
"/v1/actions/set-primary-photos": {
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Sets the first photo of each item as the primary photo",
"produces": [
"application/json"
],
"tags": [
"Actions"
],
"summary": "Set Primary Photos",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/v1.ActionAmountResult"
}
}
}
}
},
"/v1/actions/zero-item-time-fields": { "/v1/actions/zero-item-time-fields": {
"post": { "post": {
"security": [ "security": [
@ -1879,6 +1904,9 @@ const docTemplate = `{
"id": { "id": {
"type": "string" "type": "string"
}, },
"primary": {
"type": "boolean"
},
"type": { "type": {
"type": "string" "type": "string"
}, },
@ -1890,6 +1918,9 @@ const docTemplate = `{
"repo.ItemAttachmentUpdate": { "repo.ItemAttachmentUpdate": {
"type": "object", "type": "object",
"properties": { "properties": {
"primary": {
"type": "boolean"
},
"title": { "title": {
"type": "string" "type": "string"
}, },
@ -1989,6 +2020,9 @@ const docTemplate = `{
"id": { "id": {
"type": "string" "type": "string"
}, },
"imageId": {
"type": "string"
},
"insured": { "insured": {
"type": "boolean" "type": "boolean"
}, },
@ -2096,6 +2130,9 @@ const docTemplate = `{
"id": { "id": {
"type": "string" "type": "string"
}, },
"imageId": {
"type": "string"
},
"insured": { "insured": {
"type": "boolean" "type": "boolean"
}, },
@ -2255,12 +2292,6 @@ const docTemplate = `{
"id": { "id": {
"type": "string" "type": "string"
}, },
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.ItemSummary"
}
},
"name": { "name": {
"type": "string" "type": "string"
}, },
@ -2322,12 +2353,6 @@ const docTemplate = `{
"id": { "id": {
"type": "string" "type": "string"
}, },
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.ItemSummary"
}
},
"name": { "name": {
"type": "string" "type": "string"
}, },

View file

@ -60,6 +60,31 @@
} }
} }
}, },
"/v1/actions/set-primary-photos": {
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Sets the first photo of each item as the primary photo",
"produces": [
"application/json"
],
"tags": [
"Actions"
],
"summary": "Set Primary Photos",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/v1.ActionAmountResult"
}
}
}
}
},
"/v1/actions/zero-item-time-fields": { "/v1/actions/zero-item-time-fields": {
"post": { "post": {
"security": [ "security": [
@ -1871,6 +1896,9 @@
"id": { "id": {
"type": "string" "type": "string"
}, },
"primary": {
"type": "boolean"
},
"type": { "type": {
"type": "string" "type": "string"
}, },
@ -1882,6 +1910,9 @@
"repo.ItemAttachmentUpdate": { "repo.ItemAttachmentUpdate": {
"type": "object", "type": "object",
"properties": { "properties": {
"primary": {
"type": "boolean"
},
"title": { "title": {
"type": "string" "type": "string"
}, },
@ -1981,6 +2012,9 @@
"id": { "id": {
"type": "string" "type": "string"
}, },
"imageId": {
"type": "string"
},
"insured": { "insured": {
"type": "boolean" "type": "boolean"
}, },
@ -2088,6 +2122,9 @@
"id": { "id": {
"type": "string" "type": "string"
}, },
"imageId": {
"type": "string"
},
"insured": { "insured": {
"type": "boolean" "type": "boolean"
}, },
@ -2247,12 +2284,6 @@
"id": { "id": {
"type": "string" "type": "string"
}, },
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.ItemSummary"
}
},
"name": { "name": {
"type": "string" "type": "string"
}, },
@ -2314,12 +2345,6 @@
"id": { "id": {
"type": "string" "type": "string"
}, },
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.ItemSummary"
}
},
"name": { "name": {
"type": "string" "type": "string"
}, },

View file

@ -52,6 +52,8 @@ definitions:
$ref: '#/definitions/repo.DocumentOut' $ref: '#/definitions/repo.DocumentOut'
id: id:
type: string type: string
primary:
type: boolean
type: type:
type: string type: string
updatedAt: updatedAt:
@ -59,6 +61,8 @@ definitions:
type: object type: object
repo.ItemAttachmentUpdate: repo.ItemAttachmentUpdate:
properties: properties:
primary:
type: boolean
title: title:
type: string type: string
type: type:
@ -126,6 +130,8 @@ definitions:
type: array type: array
id: id:
type: string type: string
imageId:
type: string
insured: insured:
type: boolean type: boolean
labels: labels:
@ -201,6 +207,8 @@ definitions:
type: string type: string
id: id:
type: string type: string
imageId:
type: string
insured: insured:
type: boolean type: boolean
labels: labels:
@ -312,10 +320,6 @@ definitions:
type: string type: string
id: id:
type: string type: string
items:
items:
$ref: '#/definitions/repo.ItemSummary'
type: array
name: name:
type: string type: string
updatedAt: updatedAt:
@ -356,10 +360,6 @@ definitions:
type: string type: string
id: id:
type: string type: string
items:
items:
$ref: '#/definitions/repo.ItemSummary'
type: array
name: name:
type: string type: string
parent: parent:
@ -742,6 +742,21 @@ paths:
summary: Ensures Import Refs summary: Ensures Import Refs
tags: tags:
- Actions - Actions
/v1/actions/set-primary-photos:
post:
description: Sets the first photo of each item as the primary photo
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/v1.ActionAmountResult'
security:
- Bearer: []
summary: Set Primary Photos
tags:
- Actions
/v1/actions/zero-item-time-fields: /v1/actions/zero-item-time-fields:
post: post:
description: Resets all item date fields to the beginning of the day description: Resets all item date fields to the beginning of the day

View file

@ -518,6 +518,8 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
@ -545,6 +547,8 @@ github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/olahol/melody v1.1.4 h1:RQHfKZkQmDxI0+SLZRNBCn4LiXdqxLKRGSkT8Dyoe/E= github.com/olahol/melody v1.1.4 h1:RQHfKZkQmDxI0+SLZRNBCn4LiXdqxLKRGSkT8Dyoe/E=
github.com/olahol/melody v1.1.4/go.mod h1:GgkTl6Y7yWj/HtfD48Q5vLKPVoZOH+Qqgfa7CvJgJM4= github.com/olahol/melody v1.1.4/go.mod h1:GgkTl6Y7yWj/HtfD48Q5vLKPVoZOH+Qqgfa7CvJgJM4=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
@ -618,7 +622,10 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasO
github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As= github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=

View file

@ -23,7 +23,7 @@ func (svc *ItemService) AttachmentPath(ctx context.Context, attachmentId uuid.UU
func (svc *ItemService) AttachmentUpdate(ctx Context, itemId uuid.UUID, data *repo.ItemAttachmentUpdate) (repo.ItemOut, error) { func (svc *ItemService) AttachmentUpdate(ctx Context, itemId uuid.UUID, data *repo.ItemAttachmentUpdate) (repo.ItemOut, error) {
// Update Attachment // Update Attachment
attachment, err := svc.repo.Attachments.Update(ctx, data.ID, attachment.Type(data.Type)) attachment, err := svc.repo.Attachments.Update(ctx, data.ID, data)
if err != nil { if err != nil {
return repo.ItemOut{}, err return repo.ItemOut{}, err
} }

View file

@ -26,6 +26,8 @@ type Attachment struct {
UpdatedAt time.Time `json:"updated_at,omitempty"` UpdatedAt time.Time `json:"updated_at,omitempty"`
// Type holds the value of the "type" field. // Type holds the value of the "type" field.
Type attachment.Type `json:"type,omitempty"` Type attachment.Type `json:"type,omitempty"`
// Primary holds the value of the "primary" field.
Primary bool `json:"primary,omitempty"`
// Edges holds the relations/edges for other nodes in the graph. // Edges holds the relations/edges for other nodes in the graph.
// The values are being populated by the AttachmentQuery when eager-loading is set. // The values are being populated by the AttachmentQuery when eager-loading is set.
Edges AttachmentEdges `json:"edges"` Edges AttachmentEdges `json:"edges"`
@ -76,6 +78,8 @@ func (*Attachment) scanValues(columns []string) ([]any, error) {
values := make([]any, len(columns)) values := make([]any, len(columns))
for i := range columns { for i := range columns {
switch columns[i] { switch columns[i] {
case attachment.FieldPrimary:
values[i] = new(sql.NullBool)
case attachment.FieldType: case attachment.FieldType:
values[i] = new(sql.NullString) values[i] = new(sql.NullString)
case attachment.FieldCreatedAt, attachment.FieldUpdatedAt: case attachment.FieldCreatedAt, attachment.FieldUpdatedAt:
@ -125,6 +129,12 @@ func (a *Attachment) assignValues(columns []string, values []any) error {
} else if value.Valid { } else if value.Valid {
a.Type = attachment.Type(value.String) a.Type = attachment.Type(value.String)
} }
case attachment.FieldPrimary:
if value, ok := values[i].(*sql.NullBool); !ok {
return fmt.Errorf("unexpected type %T for field primary", values[i])
} else if value.Valid {
a.Primary = value.Bool
}
case attachment.ForeignKeys[0]: case attachment.ForeignKeys[0]:
if value, ok := values[i].(*sql.NullScanner); !ok { if value, ok := values[i].(*sql.NullScanner); !ok {
return fmt.Errorf("unexpected type %T for field document_attachments", values[i]) return fmt.Errorf("unexpected type %T for field document_attachments", values[i])
@ -193,6 +203,9 @@ func (a *Attachment) String() string {
builder.WriteString(", ") builder.WriteString(", ")
builder.WriteString("type=") builder.WriteString("type=")
builder.WriteString(fmt.Sprintf("%v", a.Type)) builder.WriteString(fmt.Sprintf("%v", a.Type))
builder.WriteString(", ")
builder.WriteString("primary=")
builder.WriteString(fmt.Sprintf("%v", a.Primary))
builder.WriteByte(')') builder.WriteByte(')')
return builder.String() return builder.String()
} }

View file

@ -22,6 +22,8 @@ const (
FieldUpdatedAt = "updated_at" FieldUpdatedAt = "updated_at"
// FieldType holds the string denoting the type field in the database. // FieldType holds the string denoting the type field in the database.
FieldType = "type" FieldType = "type"
// FieldPrimary holds the string denoting the primary field in the database.
FieldPrimary = "primary"
// EdgeItem holds the string denoting the item edge name in mutations. // EdgeItem holds the string denoting the item edge name in mutations.
EdgeItem = "item" EdgeItem = "item"
// EdgeDocument holds the string denoting the document edge name in mutations. // EdgeDocument holds the string denoting the document edge name in mutations.
@ -50,6 +52,7 @@ var Columns = []string{
FieldCreatedAt, FieldCreatedAt,
FieldUpdatedAt, FieldUpdatedAt,
FieldType, FieldType,
FieldPrimary,
} }
// ForeignKeys holds the SQL foreign-keys that are owned by the "attachments" // ForeignKeys holds the SQL foreign-keys that are owned by the "attachments"
@ -81,6 +84,8 @@ var (
DefaultUpdatedAt func() time.Time DefaultUpdatedAt func() time.Time
// UpdateDefaultUpdatedAt holds the default value on update for the "updated_at" field. // UpdateDefaultUpdatedAt holds the default value on update for the "updated_at" field.
UpdateDefaultUpdatedAt func() time.Time UpdateDefaultUpdatedAt func() time.Time
// DefaultPrimary holds the default value on creation for the "primary" field.
DefaultPrimary bool
// DefaultID holds the default value on creation for the "id" field. // DefaultID holds the default value on creation for the "id" field.
DefaultID func() uuid.UUID DefaultID func() uuid.UUID
) )
@ -137,6 +142,11 @@ func ByType(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldType, opts...).ToFunc() return sql.OrderByField(FieldType, opts...).ToFunc()
} }
// ByPrimary orders the results by the primary field.
func ByPrimary(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldPrimary, opts...).ToFunc()
}
// ByItemField orders the results by item field. // ByItemField orders the results by item field.
func ByItemField(field string, opts ...sql.OrderTermOption) OrderOption { func ByItemField(field string, opts ...sql.OrderTermOption) OrderOption {
return func(s *sql.Selector) { return func(s *sql.Selector) {

View file

@ -66,6 +66,11 @@ func UpdatedAt(v time.Time) predicate.Attachment {
return predicate.Attachment(sql.FieldEQ(FieldUpdatedAt, v)) return predicate.Attachment(sql.FieldEQ(FieldUpdatedAt, v))
} }
// Primary applies equality check predicate on the "primary" field. It's identical to PrimaryEQ.
func Primary(v bool) predicate.Attachment {
return predicate.Attachment(sql.FieldEQ(FieldPrimary, v))
}
// CreatedAtEQ applies the EQ predicate on the "created_at" field. // CreatedAtEQ applies the EQ predicate on the "created_at" field.
func CreatedAtEQ(v time.Time) predicate.Attachment { func CreatedAtEQ(v time.Time) predicate.Attachment {
return predicate.Attachment(sql.FieldEQ(FieldCreatedAt, v)) return predicate.Attachment(sql.FieldEQ(FieldCreatedAt, v))
@ -166,6 +171,16 @@ func TypeNotIn(vs ...Type) predicate.Attachment {
return predicate.Attachment(sql.FieldNotIn(FieldType, vs...)) return predicate.Attachment(sql.FieldNotIn(FieldType, vs...))
} }
// PrimaryEQ applies the EQ predicate on the "primary" field.
func PrimaryEQ(v bool) predicate.Attachment {
return predicate.Attachment(sql.FieldEQ(FieldPrimary, v))
}
// PrimaryNEQ applies the NEQ predicate on the "primary" field.
func PrimaryNEQ(v bool) predicate.Attachment {
return predicate.Attachment(sql.FieldNEQ(FieldPrimary, v))
}
// HasItem applies the HasEdge predicate on the "item" edge. // HasItem applies the HasEdge predicate on the "item" edge.
func HasItem() predicate.Attachment { func HasItem() predicate.Attachment {
return predicate.Attachment(func(s *sql.Selector) { return predicate.Attachment(func(s *sql.Selector) {

View file

@ -65,6 +65,20 @@ func (ac *AttachmentCreate) SetNillableType(a *attachment.Type) *AttachmentCreat
return ac return ac
} }
// SetPrimary sets the "primary" field.
func (ac *AttachmentCreate) SetPrimary(b bool) *AttachmentCreate {
ac.mutation.SetPrimary(b)
return ac
}
// SetNillablePrimary sets the "primary" field if the given value is not nil.
func (ac *AttachmentCreate) SetNillablePrimary(b *bool) *AttachmentCreate {
if b != nil {
ac.SetPrimary(*b)
}
return ac
}
// SetID sets the "id" field. // SetID sets the "id" field.
func (ac *AttachmentCreate) SetID(u uuid.UUID) *AttachmentCreate { func (ac *AttachmentCreate) SetID(u uuid.UUID) *AttachmentCreate {
ac.mutation.SetID(u) ac.mutation.SetID(u)
@ -148,6 +162,10 @@ func (ac *AttachmentCreate) defaults() {
v := attachment.DefaultType v := attachment.DefaultType
ac.mutation.SetType(v) ac.mutation.SetType(v)
} }
if _, ok := ac.mutation.Primary(); !ok {
v := attachment.DefaultPrimary
ac.mutation.SetPrimary(v)
}
if _, ok := ac.mutation.ID(); !ok { if _, ok := ac.mutation.ID(); !ok {
v := attachment.DefaultID() v := attachment.DefaultID()
ac.mutation.SetID(v) ac.mutation.SetID(v)
@ -170,6 +188,9 @@ func (ac *AttachmentCreate) check() error {
return &ValidationError{Name: "type", err: fmt.Errorf(`ent: validator failed for field "Attachment.type": %w`, err)} return &ValidationError{Name: "type", err: fmt.Errorf(`ent: validator failed for field "Attachment.type": %w`, err)}
} }
} }
if _, ok := ac.mutation.Primary(); !ok {
return &ValidationError{Name: "primary", err: errors.New(`ent: missing required field "Attachment.primary"`)}
}
if _, ok := ac.mutation.ItemID(); !ok { if _, ok := ac.mutation.ItemID(); !ok {
return &ValidationError{Name: "item", err: errors.New(`ent: missing required edge "Attachment.item"`)} return &ValidationError{Name: "item", err: errors.New(`ent: missing required edge "Attachment.item"`)}
} }
@ -223,6 +244,10 @@ func (ac *AttachmentCreate) createSpec() (*Attachment, *sqlgraph.CreateSpec) {
_spec.SetField(attachment.FieldType, field.TypeEnum, value) _spec.SetField(attachment.FieldType, field.TypeEnum, value)
_node.Type = value _node.Type = value
} }
if value, ok := ac.mutation.Primary(); ok {
_spec.SetField(attachment.FieldPrimary, field.TypeBool, value)
_node.Primary = value
}
if nodes := ac.mutation.ItemIDs(); len(nodes) > 0 { if nodes := ac.mutation.ItemIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{ edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O, Rel: sqlgraph.M2O,

View file

@ -51,6 +51,20 @@ func (au *AttachmentUpdate) SetNillableType(a *attachment.Type) *AttachmentUpdat
return au return au
} }
// SetPrimary sets the "primary" field.
func (au *AttachmentUpdate) SetPrimary(b bool) *AttachmentUpdate {
au.mutation.SetPrimary(b)
return au
}
// SetNillablePrimary sets the "primary" field if the given value is not nil.
func (au *AttachmentUpdate) SetNillablePrimary(b *bool) *AttachmentUpdate {
if b != nil {
au.SetPrimary(*b)
}
return au
}
// SetItemID sets the "item" edge to the Item entity by ID. // SetItemID sets the "item" edge to the Item entity by ID.
func (au *AttachmentUpdate) SetItemID(id uuid.UUID) *AttachmentUpdate { func (au *AttachmentUpdate) SetItemID(id uuid.UUID) *AttachmentUpdate {
au.mutation.SetItemID(id) au.mutation.SetItemID(id)
@ -160,6 +174,9 @@ func (au *AttachmentUpdate) sqlSave(ctx context.Context) (n int, err error) {
if value, ok := au.mutation.GetType(); ok { if value, ok := au.mutation.GetType(); ok {
_spec.SetField(attachment.FieldType, field.TypeEnum, value) _spec.SetField(attachment.FieldType, field.TypeEnum, value)
} }
if value, ok := au.mutation.Primary(); ok {
_spec.SetField(attachment.FieldPrimary, field.TypeBool, value)
}
if au.mutation.ItemCleared() { if au.mutation.ItemCleared() {
edge := &sqlgraph.EdgeSpec{ edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O, Rel: sqlgraph.M2O,
@ -258,6 +275,20 @@ func (auo *AttachmentUpdateOne) SetNillableType(a *attachment.Type) *AttachmentU
return auo return auo
} }
// SetPrimary sets the "primary" field.
func (auo *AttachmentUpdateOne) SetPrimary(b bool) *AttachmentUpdateOne {
auo.mutation.SetPrimary(b)
return auo
}
// SetNillablePrimary sets the "primary" field if the given value is not nil.
func (auo *AttachmentUpdateOne) SetNillablePrimary(b *bool) *AttachmentUpdateOne {
if b != nil {
auo.SetPrimary(*b)
}
return auo
}
// SetItemID sets the "item" edge to the Item entity by ID. // SetItemID sets the "item" edge to the Item entity by ID.
func (auo *AttachmentUpdateOne) SetItemID(id uuid.UUID) *AttachmentUpdateOne { func (auo *AttachmentUpdateOne) SetItemID(id uuid.UUID) *AttachmentUpdateOne {
auo.mutation.SetItemID(id) auo.mutation.SetItemID(id)
@ -397,6 +428,9 @@ func (auo *AttachmentUpdateOne) sqlSave(ctx context.Context) (_node *Attachment,
if value, ok := auo.mutation.GetType(); ok { if value, ok := auo.mutation.GetType(); ok {
_spec.SetField(attachment.FieldType, field.TypeEnum, value) _spec.SetField(attachment.FieldType, field.TypeEnum, value)
} }
if value, ok := auo.mutation.Primary(); ok {
_spec.SetField(attachment.FieldPrimary, field.TypeBool, value)
}
if auo.mutation.ItemCleared() { if auo.mutation.ItemCleared() {
edge := &sqlgraph.EdgeSpec{ edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O, Rel: sqlgraph.M2O,

View file

@ -137,7 +137,6 @@ const (
CurrencyBrl Currency = "brl" CurrencyBrl Currency = "brl"
CurrencyCad Currency = "cad" CurrencyCad Currency = "cad"
CurrencyChf Currency = "chf" CurrencyChf Currency = "chf"
CurrencyCny Currency = "cny"
CurrencyCzk Currency = "czk" CurrencyCzk Currency = "czk"
CurrencyDkk Currency = "dkk" CurrencyDkk Currency = "dkk"
CurrencyEur Currency = "eur" CurrencyEur Currency = "eur"
@ -172,7 +171,7 @@ func (c Currency) String() string {
// CurrencyValidator is a validator for the "currency" field enum values. It is called by the builders before save. // CurrencyValidator is a validator for the "currency" field enum values. It is called by the builders before save.
func CurrencyValidator(c Currency) error { func CurrencyValidator(c Currency) error {
switch c { switch c {
case CurrencyAed, CurrencyAud, CurrencyBgn, CurrencyBrl, CurrencyCad, CurrencyChf, CurrencyCny, CurrencyCzk, CurrencyDkk, CurrencyEur, CurrencyGbp, CurrencyHkd, CurrencyIdr, CurrencyInr, CurrencyJpy, CurrencyKrw, CurrencyMxn, CurrencyNok, CurrencyNzd, CurrencyPln, CurrencyRmb, CurrencyRon, CurrencyRub, CurrencySar, CurrencySek, CurrencySgd, CurrencyThb, CurrencyTry, CurrencyUsd, CurrencyXag, CurrencyXau, CurrencyZar: case CurrencyAed, CurrencyAud, CurrencyBgn, CurrencyBrl, CurrencyCad, CurrencyChf, CurrencyCzk, CurrencyDkk, CurrencyEur, CurrencyGbp, CurrencyHkd, CurrencyIdr, CurrencyInr, CurrencyJpy, CurrencyKrw, CurrencyMxn, CurrencyNok, CurrencyNzd, CurrencyPln, CurrencyRmb, CurrencyRon, CurrencyRub, CurrencySar, CurrencySek, CurrencySgd, CurrencyThb, CurrencyTry, CurrencyUsd, CurrencyXag, CurrencyXau, CurrencyZar:
return nil return nil
default: default:
return fmt.Errorf("group: invalid enum value for currency field: %q", c) return fmt.Errorf("group: invalid enum value for currency field: %q", c)

View file

@ -14,6 +14,7 @@ var (
{Name: "created_at", Type: field.TypeTime}, {Name: "created_at", Type: field.TypeTime},
{Name: "updated_at", Type: field.TypeTime}, {Name: "updated_at", Type: field.TypeTime},
{Name: "type", Type: field.TypeEnum, Enums: []string{"photo", "manual", "warranty", "attachment", "receipt"}, Default: "attachment"}, {Name: "type", Type: field.TypeEnum, Enums: []string{"photo", "manual", "warranty", "attachment", "receipt"}, Default: "attachment"},
{Name: "primary", Type: field.TypeBool, Default: false},
{Name: "document_attachments", Type: field.TypeUUID}, {Name: "document_attachments", Type: field.TypeUUID},
{Name: "item_attachments", Type: field.TypeUUID}, {Name: "item_attachments", Type: field.TypeUUID},
} }
@ -25,13 +26,13 @@ var (
ForeignKeys: []*schema.ForeignKey{ ForeignKeys: []*schema.ForeignKey{
{ {
Symbol: "attachments_documents_attachments", Symbol: "attachments_documents_attachments",
Columns: []*schema.Column{AttachmentsColumns[4]}, Columns: []*schema.Column{AttachmentsColumns[5]},
RefColumns: []*schema.Column{DocumentsColumns[0]}, RefColumns: []*schema.Column{DocumentsColumns[0]},
OnDelete: schema.Cascade, OnDelete: schema.Cascade,
}, },
{ {
Symbol: "attachments_items_attachments", Symbol: "attachments_items_attachments",
Columns: []*schema.Column{AttachmentsColumns[5]}, Columns: []*schema.Column{AttachmentsColumns[6]},
RefColumns: []*schema.Column{ItemsColumns[0]}, RefColumns: []*schema.Column{ItemsColumns[0]},
OnDelete: schema.Cascade, OnDelete: schema.Cascade,
}, },
@ -116,7 +117,7 @@ var (
{Name: "created_at", Type: field.TypeTime}, {Name: "created_at", Type: field.TypeTime},
{Name: "updated_at", Type: field.TypeTime}, {Name: "updated_at", Type: field.TypeTime},
{Name: "name", Type: field.TypeString, Size: 255}, {Name: "name", Type: field.TypeString, Size: 255},
{Name: "currency", Type: field.TypeEnum, Enums: []string{"aed", "aud", "bgn", "brl", "cad", "chf", "cny", "czk", "dkk", "eur", "gbp", "hkd", "idr", "inr", "jpy", "krw", "mxn", "nok", "nzd", "pln", "rmb", "ron", "rub", "sar", "sek", "sgd", "thb", "try", "usd", "zar"}, Default: "usd"}, {Name: "currency", Type: field.TypeEnum, Enums: []string{"aed", "aud", "bgn", "brl", "cad", "chf", "czk", "dkk", "eur", "gbp", "hkd", "idr", "inr", "jpy", "krw", "mxn", "nok", "nzd", "pln", "rmb", "ron", "rub", "sar", "sek", "sgd", "thb", "try", "usd", "xag", "xau", "zar"}, Default: "usd"},
} }
// GroupsTable holds the schema information for the "groups" table. // GroupsTable holds the schema information for the "groups" table.
GroupsTable = &schema.Table{ GroupsTable = &schema.Table{

View file

@ -61,6 +61,7 @@ type AttachmentMutation struct {
created_at *time.Time created_at *time.Time
updated_at *time.Time updated_at *time.Time
_type *attachment.Type _type *attachment.Type
primary *bool
clearedFields map[string]struct{} clearedFields map[string]struct{}
item *uuid.UUID item *uuid.UUID
cleareditem bool cleareditem bool
@ -283,6 +284,42 @@ func (m *AttachmentMutation) ResetType() {
m._type = nil m._type = nil
} }
// SetPrimary sets the "primary" field.
func (m *AttachmentMutation) SetPrimary(b bool) {
m.primary = &b
}
// Primary returns the value of the "primary" field in the mutation.
func (m *AttachmentMutation) Primary() (r bool, exists bool) {
v := m.primary
if v == nil {
return
}
return *v, true
}
// OldPrimary returns the old "primary" field's value of the Attachment entity.
// If the Attachment object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *AttachmentMutation) OldPrimary(ctx context.Context) (v bool, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldPrimary is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldPrimary requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldPrimary: %w", err)
}
return oldValue.Primary, nil
}
// ResetPrimary resets all changes to the "primary" field.
func (m *AttachmentMutation) ResetPrimary() {
m.primary = nil
}
// SetItemID sets the "item" edge to the Item entity by id. // SetItemID sets the "item" edge to the Item entity by id.
func (m *AttachmentMutation) SetItemID(id uuid.UUID) { func (m *AttachmentMutation) SetItemID(id uuid.UUID) {
m.item = &id m.item = &id
@ -395,7 +432,7 @@ func (m *AttachmentMutation) Type() string {
// order to get all numeric fields that were incremented/decremented, call // order to get all numeric fields that were incremented/decremented, call
// AddedFields(). // AddedFields().
func (m *AttachmentMutation) Fields() []string { func (m *AttachmentMutation) Fields() []string {
fields := make([]string, 0, 3) fields := make([]string, 0, 4)
if m.created_at != nil { if m.created_at != nil {
fields = append(fields, attachment.FieldCreatedAt) fields = append(fields, attachment.FieldCreatedAt)
} }
@ -405,6 +442,9 @@ func (m *AttachmentMutation) Fields() []string {
if m._type != nil { if m._type != nil {
fields = append(fields, attachment.FieldType) fields = append(fields, attachment.FieldType)
} }
if m.primary != nil {
fields = append(fields, attachment.FieldPrimary)
}
return fields return fields
} }
@ -419,6 +459,8 @@ func (m *AttachmentMutation) Field(name string) (ent.Value, bool) {
return m.UpdatedAt() return m.UpdatedAt()
case attachment.FieldType: case attachment.FieldType:
return m.GetType() return m.GetType()
case attachment.FieldPrimary:
return m.Primary()
} }
return nil, false return nil, false
} }
@ -434,6 +476,8 @@ func (m *AttachmentMutation) OldField(ctx context.Context, name string) (ent.Val
return m.OldUpdatedAt(ctx) return m.OldUpdatedAt(ctx)
case attachment.FieldType: case attachment.FieldType:
return m.OldType(ctx) return m.OldType(ctx)
case attachment.FieldPrimary:
return m.OldPrimary(ctx)
} }
return nil, fmt.Errorf("unknown Attachment field %s", name) return nil, fmt.Errorf("unknown Attachment field %s", name)
} }
@ -464,6 +508,13 @@ func (m *AttachmentMutation) SetField(name string, value ent.Value) error {
} }
m.SetType(v) m.SetType(v)
return nil return nil
case attachment.FieldPrimary:
v, ok := value.(bool)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetPrimary(v)
return nil
} }
return fmt.Errorf("unknown Attachment field %s", name) return fmt.Errorf("unknown Attachment field %s", name)
} }
@ -522,6 +573,9 @@ func (m *AttachmentMutation) ResetField(name string) error {
case attachment.FieldType: case attachment.FieldType:
m.ResetType() m.ResetType()
return nil return nil
case attachment.FieldPrimary:
m.ResetPrimary()
return nil
} }
return fmt.Errorf("unknown Attachment field %s", name) return fmt.Errorf("unknown Attachment field %s", name)
} }

View file

@ -40,6 +40,10 @@ func init() {
attachment.DefaultUpdatedAt = attachmentDescUpdatedAt.Default.(func() time.Time) attachment.DefaultUpdatedAt = attachmentDescUpdatedAt.Default.(func() time.Time)
// attachment.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field. // attachment.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field.
attachment.UpdateDefaultUpdatedAt = attachmentDescUpdatedAt.UpdateDefault.(func() time.Time) attachment.UpdateDefaultUpdatedAt = attachmentDescUpdatedAt.UpdateDefault.(func() time.Time)
// attachmentDescPrimary is the schema descriptor for primary field.
attachmentDescPrimary := attachmentFields[1].Descriptor()
// attachment.DefaultPrimary holds the default value on creation for the primary field.
attachment.DefaultPrimary = attachmentDescPrimary.Default.(bool)
// attachmentDescID is the schema descriptor for id field. // attachmentDescID is the schema descriptor for id field.
attachmentDescID := attachmentMixinFields0[0].Descriptor() attachmentDescID := attachmentMixinFields0[0].Descriptor()
// attachment.DefaultID holds the default value on creation for the id field. // attachment.DefaultID holds the default value on creation for the id field.

View file

@ -24,6 +24,8 @@ func (Attachment) Fields() []ent.Field {
field.Enum("type"). field.Enum("type").
Values("photo", "manual", "warranty", "attachment", "receipt"). Values("photo", "manual", "warranty", "attachment", "receipt").
Default("attachment"), Default("attachment"),
field.Bool("primary").
Default(false),
} }
} }

View file

@ -0,0 +1,12 @@
-- Disable the enforcement of foreign-keys constraints
PRAGMA foreign_keys = off;
-- Create "new_attachments" table
CREATE TABLE `new_attachments` (`id` uuid NOT NULL, `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, `type` text NOT NULL DEFAULT 'attachment', `primary` bool NOT NULL DEFAULT false, `document_attachments` uuid NOT NULL, `item_attachments` uuid NOT NULL, PRIMARY KEY (`id`), CONSTRAINT `attachments_documents_attachments` FOREIGN KEY (`document_attachments`) REFERENCES `documents` (`id`) ON DELETE CASCADE, CONSTRAINT `attachments_items_attachments` FOREIGN KEY (`item_attachments`) REFERENCES `items` (`id`) ON DELETE CASCADE);
-- Copy rows from old table "attachments" to new temporary table "new_attachments"
INSERT INTO `new_attachments` (`id`, `created_at`, `updated_at`, `type`, `document_attachments`, `item_attachments`) SELECT `id`, `created_at`, `updated_at`, `type`, `document_attachments`, `item_attachments` FROM `attachments`;
-- Drop "attachments" table after copying rows
DROP TABLE `attachments`;
-- Rename temporary table "new_attachments" to "attachments"
ALTER TABLE `new_attachments` RENAME TO `attachments`;
-- Enable back the enforcement of foreign-keys constraints
PRAGMA foreign_keys = on;

View file

@ -1,4 +1,4 @@
h1:VjVLPBHzJ8N1Hiw+Aeitb0alnVn9UFilnajCzc+pie8= h1:sjJCTAqc9FG8BKBIzh5ZynYD/Ilz6vnLqM4XX83WQ4M=
20220929052825_init.sql h1:ZlCqm1wzjDmofeAcSX3jE4h4VcdTNGpRg2eabztDy9Q= 20220929052825_init.sql h1:ZlCqm1wzjDmofeAcSX3jE4h4VcdTNGpRg2eabztDy9Q=
20221001210956_group_invitations.sql h1:YQKJFtE39wFOcRNbZQ/d+ZlHwrcfcsZlcv/pLEYdpjw= 20221001210956_group_invitations.sql h1:YQKJFtE39wFOcRNbZQ/d+ZlHwrcfcsZlcv/pLEYdpjw=
20221009173029_add_user_roles.sql h1:vWmzAfgEWQeGk0Vn70zfVPCcfEZth3E0JcvyKTjpYyU= 20221009173029_add_user_roles.sql h1:vWmzAfgEWQeGk0Vn70zfVPCcfEZth3E0JcvyKTjpYyU=
@ -12,3 +12,4 @@ h1:VjVLPBHzJ8N1Hiw+Aeitb0alnVn9UFilnajCzc+pie8=
20230227024134_add_scheduled_date.sql h1:8qO5OBZ0AzsfYEQOAQQrYIjyhSwM+v1A+/ylLSoiyoc= 20230227024134_add_scheduled_date.sql h1:8qO5OBZ0AzsfYEQOAQQrYIjyhSwM+v1A+/ylLSoiyoc=
20230305065819_add_notifier_types.sql h1:r5xrgCKYQ2o9byBqYeAX1zdp94BLdaxf4vq9OmGHNl0= 20230305065819_add_notifier_types.sql h1:r5xrgCKYQ2o9byBqYeAX1zdp94BLdaxf4vq9OmGHNl0=
20230305071524_add_group_id_to_notifiers.sql h1:xDShqbyClcFhvJbwclOHdczgXbdffkxXNWjV61hL/t4= 20230305071524_add_group_id_to_notifiers.sql h1:xDShqbyClcFhvJbwclOHdczgXbdffkxXNWjV61hL/t4=
20231006213457_add_primary_attachment_flag.sql h1:J4tMSJQFa7vaj0jpnh8YKTssdyIjRyq6RXDXZIzDDu4=

View file

@ -7,6 +7,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/data/ent" "github.com/hay-kot/homebox/backend/internal/data/ent"
"github.com/hay-kot/homebox/backend/internal/data/ent/attachment" "github.com/hay-kot/homebox/backend/internal/data/ent/attachment"
"github.com/hay-kot/homebox/backend/internal/data/ent/item"
) )
// AttachmentRepo is a repository for Attachments table that links Items to Documents // AttachmentRepo is a repository for Attachments table that links Items to Documents
@ -24,12 +25,14 @@ type (
UpdatedAt time.Time `json:"updatedAt"` UpdatedAt time.Time `json:"updatedAt"`
Type string `json:"type"` Type string `json:"type"`
Document DocumentOut `json:"document"` Document DocumentOut `json:"document"`
Primary bool `json:"primary"`
} }
ItemAttachmentUpdate struct { ItemAttachmentUpdate struct {
ID uuid.UUID `json:"-"` ID uuid.UUID `json:"-"`
Type string `json:"type"` Type string `json:"type"`
Title string `json:"title"` Title string `json:"title"`
Primary bool `json:"primary"`
} }
) )
@ -39,6 +42,7 @@ func ToItemAttachment(attachment *ent.Attachment) ItemAttachment {
CreatedAt: attachment.CreatedAt, CreatedAt: attachment.CreatedAt,
UpdatedAt: attachment.UpdatedAt, UpdatedAt: attachment.UpdatedAt,
Type: attachment.Type.String(), Type: attachment.Type.String(),
Primary: attachment.Primary,
Document: DocumentOut{ Document: DocumentOut{
ID: attachment.Edges.Document.ID, ID: attachment.Edges.Document.ID,
Title: attachment.Edges.Document.Title, Title: attachment.Edges.Document.Title,
@ -64,10 +68,33 @@ func (r *AttachmentRepo) Get(ctx context.Context, id uuid.UUID) (*ent.Attachment
Only(ctx) Only(ctx)
} }
func (r *AttachmentRepo) Update(ctx context.Context, itemId uuid.UUID, typ attachment.Type) (*ent.Attachment, error) { func (r *AttachmentRepo) Update(ctx context.Context, itemId uuid.UUID, data *ItemAttachmentUpdate) (*ent.Attachment, error) {
itm, err := r.db.Attachment.UpdateOneID(itemId). // TODO: execute within Tx
SetType(typ). typ := attachment.Type(data.Type)
Save(ctx)
bldr := r.db.Attachment.UpdateOneID(itemId).
SetType(typ)
// Primary only applies to photos
if typ == attachment.TypePhoto {
bldr = bldr.SetPrimary(data.Primary)
} else {
bldr = bldr.SetPrimary(false)
}
itm, err := bldr.Save(ctx)
if err != nil {
return nil, err
}
// Ensure all other attachments are not primary
err = r.db.Attachment.Update().
Where(
attachment.HasItemWith(item.ID(itemId)),
attachment.IDNEQ(itm.ID),
).
SetPrimary(false).
Exec(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -110,7 +110,10 @@ func TestAttachmentRepo_Update(t *testing.T) {
for _, typ := range []attachment.Type{"photo", "manual", "warranty", "attachment"} { for _, typ := range []attachment.Type{"photo", "manual", "warranty", "attachment"} {
t.Run(string(typ), func(t *testing.T) { t.Run(string(typ), func(t *testing.T) {
_, err := tRepos.Attachments.Update(context.Background(), entity.ID, typ) _, err := tRepos.Attachments.Update(context.Background(), entity.ID, &ItemAttachmentUpdate{
Type: string(typ),
})
assert.NoError(t, err) assert.NoError(t, err)
updated, err := tRepos.Attachments.Get(context.Background(), entity.ID) updated, err := tRepos.Attachments.Get(context.Background(), entity.ID)

View file

@ -8,6 +8,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/core/services/reporting/eventbus" "github.com/hay-kot/homebox/backend/internal/core/services/reporting/eventbus"
"github.com/hay-kot/homebox/backend/internal/data/ent" "github.com/hay-kot/homebox/backend/internal/data/ent"
"github.com/hay-kot/homebox/backend/internal/data/ent/attachment"
"github.com/hay-kot/homebox/backend/internal/data/ent/group" "github.com/hay-kot/homebox/backend/internal/data/ent/group"
"github.com/hay-kot/homebox/backend/internal/data/ent/item" "github.com/hay-kot/homebox/backend/internal/data/ent/item"
"github.com/hay-kot/homebox/backend/internal/data/ent/itemfield" "github.com/hay-kot/homebox/backend/internal/data/ent/itemfield"
@ -125,6 +126,8 @@ type (
// Edges // Edges
Location *LocationSummary `json:"location,omitempty" extensions:"x-nullable,x-omitempty"` Location *LocationSummary `json:"location,omitempty" extensions:"x-nullable,x-omitempty"`
Labels []LabelSummary `json:"labels"` Labels []LabelSummary `json:"labels"`
ImageID *uuid.UUID `json:"imageId,omitempty"`
} }
ItemOut struct { ItemOut struct {
@ -174,6 +177,16 @@ func mapItemSummary(item *ent.Item) ItemSummary {
labels = mapEach(item.Edges.Label, mapLabelSummary) labels = mapEach(item.Edges.Label, mapLabelSummary)
} }
var imageID *uuid.UUID
if item.Edges.Attachments != nil {
for _, a := range item.Edges.Attachments {
if a.Primary && a.Edges.Document != nil {
imageID = &a.ID
break
}
}
}
return ItemSummary{ return ItemSummary{
ID: item.ID, ID: item.ID,
Name: item.Name, Name: item.Name,
@ -191,6 +204,7 @@ func mapItemSummary(item *ent.Item) ItemSummary {
// Warranty // Warranty
Insured: item.Insured, Insured: item.Insured,
ImageID: imageID,
} }
} }
@ -407,7 +421,13 @@ func (e *ItemsRepository) QueryByGroup(ctx context.Context, gid uuid.UUID, q Ite
qb = qb. qb = qb.
WithLabel(). WithLabel().
WithLocation() WithLocation().
WithAttachments(func(aq *ent.AttachmentQuery) {
aq.Where(
attachment.Primary(true),
).
WithDocument()
})
if q.Page != -1 || q.PageSize != -1 { if q.Page != -1 || q.PageSize != -1 {
qb = qb. qb = qb.
@ -533,13 +553,13 @@ func (e *ItemsRepository) Create(ctx context.Context, gid uuid.UUID, data ItemCr
} }
func (e *ItemsRepository) Delete(ctx context.Context, id uuid.UUID) error { func (e *ItemsRepository) Delete(ctx context.Context, id uuid.UUID) error {
err := e.db.Item.DeleteOneID(id).Exec(ctx) err := e.db.Item.DeleteOneID(id).Exec(ctx)
if err != nil { if err != nil {
return err return err
} }
e.publishMutationEvent(id) e.publishMutationEvent(id)
return nil return nil
} }
func (e *ItemsRepository) DeleteByGroup(ctx context.Context, gid, id uuid.UUID) error { func (e *ItemsRepository) DeleteByGroup(ctx context.Context, gid, id uuid.UUID) error {
@ -549,12 +569,11 @@ func (e *ItemsRepository) DeleteByGroup(ctx context.Context, gid, id uuid.UUID)
item.ID(id), item.ID(id),
item.HasGroupWith(group.ID(gid)), item.HasGroupWith(group.ID(gid)),
).Exec(ctx) ).Exec(ctx)
if err != nil {
if err != nil { return err
return err }
}
e.publishMutationEvent(gid) e.publishMutationEvent(gid)
return err return err
} }
@ -670,7 +689,7 @@ func (e *ItemsRepository) UpdateByGroup(ctx context.Context, GID uuid.UUID, data
} }
} }
e.publishMutationEvent(GID) e.publishMutationEvent(GID)
return e.GetOne(ctx, data.ID) return e.GetOne(ctx, data.ID)
} }
@ -709,7 +728,7 @@ func (e *ItemsRepository) Patch(ctx context.Context, GID, ID uuid.UUID, data Ite
q.SetQuantity(*data.Quantity) q.SetQuantity(*data.Quantity)
} }
e.publishMutationEvent(GID) e.publishMutationEvent(GID)
return q.Exec(ctx) return q.Exec(ctx)
} }
@ -822,3 +841,51 @@ func (e *ItemsRepository) ZeroOutTimeFields(ctx context.Context, GID uuid.UUID)
return updated, nil return updated, nil
} }
func (e *ItemsRepository) SetPrimaryPhotos(ctx context.Context, GID uuid.UUID) (int, error) {
// All items where there is no primary photo
itemIDs, err := e.db.Item.Query().
Where(
item.HasGroupWith(group.ID(GID)),
item.HasAttachmentsWith(
attachment.Not(
attachment.And(
attachment.Primary(true),
attachment.TypeEQ(attachment.TypePhoto),
),
),
),
).
IDs(ctx)
if err != nil {
return -1, err
}
updated := 0
for _, id := range itemIDs {
// Find the first photo attachment
a, err := e.db.Attachment.Query().
Where(
attachment.HasItemWith(item.ID(id)),
attachment.TypeEQ(attachment.TypePhoto),
attachment.Primary(false),
).
First(ctx)
if err != nil {
return updated, err
}
// Set it as primary
_, err = e.db.Attachment.UpdateOne(a).
SetPrimary(true).
Save(ctx)
if err != nil {
return updated, err
}
updated++
}
return updated, nil
}

View file

@ -8,7 +8,6 @@ import (
"github.com/hay-kot/homebox/backend/internal/core/services/reporting/eventbus" "github.com/hay-kot/homebox/backend/internal/core/services/reporting/eventbus"
"github.com/hay-kot/homebox/backend/internal/data/ent" "github.com/hay-kot/homebox/backend/internal/data/ent"
"github.com/hay-kot/homebox/backend/internal/data/ent/group" "github.com/hay-kot/homebox/backend/internal/data/ent/group"
"github.com/hay-kot/homebox/backend/internal/data/ent/item"
"github.com/hay-kot/homebox/backend/internal/data/ent/label" "github.com/hay-kot/homebox/backend/internal/data/ent/label"
"github.com/hay-kot/homebox/backend/internal/data/ent/predicate" "github.com/hay-kot/homebox/backend/internal/data/ent/predicate"
) )
@ -42,7 +41,6 @@ type (
LabelOut struct { LabelOut struct {
LabelSummary LabelSummary
Items []ItemSummary `json:"items"`
} }
) )
@ -64,7 +62,6 @@ var (
func mapLabelOut(label *ent.Label) LabelOut { func mapLabelOut(label *ent.Label) LabelOut {
return LabelOut{ return LabelOut{
LabelSummary: mapLabelSummary(label), LabelSummary: mapLabelSummary(label),
Items: mapEach(label.Edges.Items, mapItemSummary),
} }
} }
@ -78,9 +75,6 @@ func (r *LabelRepository) getOne(ctx context.Context, where ...predicate.Label)
return mapLabelOutErr(r.db.Label.Query(). return mapLabelOutErr(r.db.Label.Query().
Where(where...). Where(where...).
WithGroup(). WithGroup().
WithItems(func(iq *ent.ItemQuery) {
iq.Where(item.Archived(false))
}).
Only(ctx), Only(ctx),
) )
} }
@ -142,7 +136,7 @@ func (r *LabelRepository) UpdateByGroup(ctx context.Context, GID uuid.UUID, data
} }
// delete removes the label from the database. This should only be used when // delete removes the label from the database. This should only be used when
// the label's ownership is already confirmed/validated. // the label's ownership is already confirmed/validated.
func (r *LabelRepository) delete(ctx context.Context, id uuid.UUID) error { func (r *LabelRepository) delete(ctx context.Context, id uuid.UUID) error {
return r.db.Label.DeleteOneID(id).Exec(ctx) return r.db.Label.DeleteOneID(id).Exec(ctx)
} }

View file

@ -9,7 +9,6 @@ import (
"github.com/hay-kot/homebox/backend/internal/core/services/reporting/eventbus" "github.com/hay-kot/homebox/backend/internal/core/services/reporting/eventbus"
"github.com/hay-kot/homebox/backend/internal/data/ent" "github.com/hay-kot/homebox/backend/internal/data/ent"
"github.com/hay-kot/homebox/backend/internal/data/ent/group" "github.com/hay-kot/homebox/backend/internal/data/ent/group"
"github.com/hay-kot/homebox/backend/internal/data/ent/item"
"github.com/hay-kot/homebox/backend/internal/data/ent/location" "github.com/hay-kot/homebox/backend/internal/data/ent/location"
"github.com/hay-kot/homebox/backend/internal/data/ent/predicate" "github.com/hay-kot/homebox/backend/internal/data/ent/predicate"
) )
@ -49,7 +48,6 @@ type (
LocationOut struct { LocationOut struct {
Parent *LocationSummary `json:"parent,omitempty"` Parent *LocationSummary `json:"parent,omitempty"`
LocationSummary LocationSummary
Items []ItemSummary `json:"items"`
Children []LocationSummary `json:"children"` Children []LocationSummary `json:"children"`
} }
) )
@ -88,7 +86,6 @@ func mapLocationOut(location *ent.Location) LocationOut {
CreatedAt: location.CreatedAt, CreatedAt: location.CreatedAt,
UpdatedAt: location.UpdatedAt, UpdatedAt: location.UpdatedAt,
}, },
Items: mapEach(location.Edges.Items, mapItemSummary),
} }
} }
@ -164,11 +161,6 @@ func (r *LocationRepository) getOne(ctx context.Context, where ...predicate.Loca
return mapLocationOutErr(r.db.Location.Query(). return mapLocationOutErr(r.db.Location.Query().
Where(where...). Where(where...).
WithGroup(). WithGroup().
WithItems(func(iq *ent.ItemQuery) {
iq.Where(item.Archived(false)).
Order(ent.Asc(item.FieldName)).
WithLabel()
}).
WithParent(). WithParent().
WithChildren(). WithChildren().
Only(ctx)) Only(ctx))

View file

@ -60,6 +60,31 @@
} }
} }
}, },
"/v1/actions/set-primary-photos": {
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Sets the first photo of each item as the primary photo",
"produces": [
"application/json"
],
"tags": [
"Actions"
],
"summary": "Set Primary Photos",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/v1.ActionAmountResult"
}
}
}
}
},
"/v1/actions/zero-item-time-fields": { "/v1/actions/zero-item-time-fields": {
"post": { "post": {
"security": [ "security": [
@ -1871,6 +1896,9 @@
"id": { "id": {
"type": "string" "type": "string"
}, },
"primary": {
"type": "boolean"
},
"type": { "type": {
"type": "string" "type": "string"
}, },
@ -1882,6 +1910,9 @@
"repo.ItemAttachmentUpdate": { "repo.ItemAttachmentUpdate": {
"type": "object", "type": "object",
"properties": { "properties": {
"primary": {
"type": "boolean"
},
"title": { "title": {
"type": "string" "type": "string"
}, },
@ -1981,6 +2012,9 @@
"id": { "id": {
"type": "string" "type": "string"
}, },
"imageId": {
"type": "string"
},
"insured": { "insured": {
"type": "boolean" "type": "boolean"
}, },
@ -2088,6 +2122,9 @@
"id": { "id": {
"type": "string" "type": "string"
}, },
"imageId": {
"type": "string"
},
"insured": { "insured": {
"type": "boolean" "type": "boolean"
}, },
@ -2247,12 +2284,6 @@
"id": { "id": {
"type": "string" "type": "string"
}, },
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.ItemSummary"
}
},
"name": { "name": {
"type": "string" "type": "string"
}, },
@ -2314,12 +2345,6 @@
"id": { "id": {
"type": "string" "type": "string"
}, },
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.ItemSummary"
}
},
"name": { "name": {
"type": "string" "type": "string"
}, },

View file

@ -1,21 +1,21 @@
<template> <template>
<NuxtLink class="group card rounded-md" :to="`/item/${item.id}`"> <NuxtLink class="group card rounded-md border border-gray-300" :to="`/item/${item.id}`">
<div class="rounded-t flex flex-col justify-center bg-neutral text-neutral-content p-5"> <div class="relative h-[200px]">
<h2 class="text-lg mb-1 last:mb-0 font-bold two-line">{{ item.name }}</h2> <img v-if="imageUrl" class="h-[200px] w-full object-cover rounded-t shadow-sm border-gray-300" :src="imageUrl" />
<div> <div class="absolute bottom-1 left-1">
<NuxtLink v-if="item.location" class="text-sm hover:link" :to="`/location/${item.location.id}`"> <NuxtLink
v-if="item.location"
class="text-sm hover:link badge shadow-md rounded-md"
:to="`/location/${item.location.id}`"
>
{{ item.location.name }} {{ item.location.name }}
</NuxtLink> </NuxtLink>
<span class="flex-1"></span>
</div> </div>
</div> </div>
<div class="rounded-b p-4 pt-2 flex-grow col-span-4 flex flex-col gap-y-2 bg-base-100"> <div class="rounded-b p-4 pt-2 flex-grow col-span-4 flex flex-col gap-y-1 bg-base-100">
<h2 class="text-lg font-bold two-line">{{ item.name }}</h2>
<div class="divider my-0"></div>
<div class="flex justify-between gap-2"> <div class="flex justify-between gap-2">
<div class="mr-auto tooltip tooltip-tip" data-tip="Purchase Price">
<span v-if="item.purchasePrice != '0'" class="badge badge-sm badge-ghost h-5">
<Currency :amount="item.purchasePrice" />
</span>
</div>
<div v-if="item.insured" class="tooltip z-10" data-tip="Insured"> <div v-if="item.insured" class="tooltip z-10" data-tip="Insured">
<Icon class="h-5 w-5 text-primary" name="mdi-shield-check" /> <Icon class="h-5 w-5 text-primary" name="mdi-shield-check" />
</div> </div>
@ -26,7 +26,6 @@
</div> </div>
</div> </div>
<Markdown class="mb-2 text-clip three-line" :source="item.description" /> <Markdown class="mb-2 text-clip three-line" :source="item.description" />
<div class="flex gap-2 flex-wrap -mr-1 mt-auto justify-end"> <div class="flex gap-2 flex-wrap -mr-1 mt-auto justify-end">
<LabelChip v-for="label in top3" :key="label.id" :label="label" size="sm" /> <LabelChip v-for="label in top3" :key="label.id" :label="label" size="sm" />
</div> </div>
@ -37,6 +36,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { ItemOut, ItemSummary } from "~~/lib/api/types/data-contracts"; import { ItemOut, ItemSummary } from "~~/lib/api/types/data-contracts";
const api = useUserApi();
const imageUrl = computed(() => {
if (!props.item.imageId) {
return "/no-image.jpg";
}
return api.authURL(`/items/${props.item.id}/attachments/${props.item.imageId}`);
});
const top3 = computed(() => { const top3 = computed(() => {
return props.item.labels.slice(0, 3) || []; return props.item.labels.slice(0, 3) || [];
}); });

View file

@ -33,7 +33,12 @@
v-if="detail.copyable" v-if="detail.copyable"
class="opacity-0 group-hover:opacity-100 ml-4 my-0 duration-75 transition-opacity" class="opacity-0 group-hover:opacity-100 ml-4 my-0 duration-75 transition-opacity"
> >
<CopyText :text="detail.text.toString()" :icon-size="16" class="btn btn-xs btn-ghost btn-circle" /> <CopyText
v-if="detail.text.toString()"
:text="detail.text.toString()"
:icon-size="16"
class="btn btn-xs btn-ghost btn-circle"
/>
</span> </span>
</span> </span>
</template> </template>

View file

@ -19,4 +19,10 @@ export class ActionsAPI extends BaseAPI {
url: route("/actions/ensure-import-refs"), url: route("/actions/ensure-import-refs"),
}); });
} }
setPrimaryPhotos() {
return this.http.post<void, ActionAmountResult>({
url: route("/actions/set-primary-photos"),
});
}
} }

View file

@ -42,11 +42,13 @@ export interface ItemAttachment {
createdAt: Date | string; createdAt: Date | string;
document: DocumentOut; document: DocumentOut;
id: string; id: string;
primary: boolean;
type: string; type: string;
updatedAt: Date | string; updatedAt: Date | string;
} }
export interface ItemAttachmentUpdate { export interface ItemAttachmentUpdate {
primary: boolean;
title: string; title: string;
type: string; type: string;
} }
@ -84,6 +86,7 @@ export interface ItemOut {
description: string; description: string;
fields: ItemField[]; fields: ItemField[];
id: string; id: string;
imageId: string;
insured: boolean; insured: boolean;
labels: LabelSummary[]; labels: LabelSummary[];
/** Warranty */ /** Warranty */
@ -124,6 +127,7 @@ export interface ItemSummary {
createdAt: Date | string; createdAt: Date | string;
description: string; description: string;
id: string; id: string;
imageId: string;
insured: boolean; insured: boolean;
labels: LabelSummary[]; labels: LabelSummary[];
/** Edges */ /** Edges */
@ -187,7 +191,6 @@ export interface LabelOut {
createdAt: Date | string; createdAt: Date | string;
description: string; description: string;
id: string; id: string;
items: ItemSummary[];
name: string; name: string;
updatedAt: Date | string; updatedAt: Date | string;
} }
@ -211,7 +214,6 @@ export interface LocationOut {
createdAt: Date | string; createdAt: Date | string;
description: string; description: string;
id: string; id: string;
items: ItemSummary[];
name: string; name: string;
parent: LocationSummary; parent: LocationSummary;
updatedAt: Date | string; updatedAt: Date | string;

View file

@ -307,6 +307,7 @@
id: "", id: "",
title: "", title: "",
type: "", type: "",
primary: false,
}); });
const attachmentOpts = Object.entries(AttachmentTypes).map(([key, value]) => ({ const attachmentOpts = Object.entries(AttachmentTypes).map(([key, value]) => ({
@ -318,6 +319,7 @@
editState.id = attachment.id; editState.id = attachment.id;
editState.title = attachment.document.title; editState.title = attachment.document.title;
editState.type = attachment.type; editState.type = attachment.type;
editState.primary = attachment.primary;
editState.modal = true; editState.modal = true;
editState.obj = attachmentOpts.find(o => o.value === attachment.type) || attachmentOpts[0]; editState.obj = attachmentOpts.find(o => o.value === attachment.type) || attachmentOpts[0];
@ -328,6 +330,7 @@
const { error, data } = await api.items.attachments.update(itemId.value, editState.id, { const { error, data } = await api.items.attachments.update(itemId.value, editState.id, {
title: editState.title, title: editState.title,
type: editState.type, type: editState.type,
primary: editState.primary,
}); });
if (error) { if (error) {
@ -407,7 +410,6 @@
<template #title> Attachment Edit </template> <template #title> Attachment Edit </template>
<FormTextField v-model="editState.title" label="Attachment Title" /> <FormTextField v-model="editState.title" label="Attachment Title" />
{{ editState.type }}
<FormSelect <FormSelect
v-model:value="editState.type" v-model:value="editState.type"
label="Attachment Type" label="Attachment Type"
@ -415,6 +417,14 @@
name="text" name="text"
:items="attachmentOpts" :items="attachmentOpts"
/> />
<div v-if="editState.type == 'photo'" class="flex gap-2 mt-3">
<input v-model="editState.primary" type="checkbox" class="checkbox" />
<p class="text-sm">
<span class="font-semibold">Primary Photo</span>
This options is only available for photos. Only one photo can be primary. If you select this option, the
current primary photo, if any will be unselected.
</p>
</div>
<div class="modal-action"> <div class="modal-action">
<BaseButton :loading="editState.loading" @click="updateAttachment"> Update </BaseButton> <BaseButton :loading="editState.loading" @click="updateAttachment"> Update </BaseButton>
</div> </div>

View file

@ -59,6 +59,7 @@
const { error, data } = await api.labels.update(labelId.value, updateData); const { error, data } = await api.labels.update(labelId.value, updateData);
if (error) { if (error) {
updating.value = false;
toast.error("Failed to update label"); toast.error("Failed to update label");
return; return;
} }
@ -68,6 +69,23 @@
updateModal.value = false; updateModal.value = false;
updating.value = false; updating.value = false;
} }
const items = computedAsync(async () => {
if (!label.value) {
return [];
}
const resp = await api.items.getAll({
labels: [label.value.id],
});
if (resp.error) {
toast.error("Failed to load items");
return [];
}
return resp.data.items;
});
</script> </script>
<template> <template>
@ -83,51 +101,47 @@
</form> </form>
</BaseModal> </BaseModal>
<BaseContainer v-if="label" class="space-y-6 mb-16"> <BaseContainer v-if="label">
<section> <div class="bg-white rounded p-3">
<BaseSectionHeader v-if="label"> <header class="mb-2">
<Icon name="mdi-package-variant" class="mr-2 -mt-1 text-base-content" /> <div class="flex flex-wrap items-end gap-2">
<span class="text-base-content"> <div class="avatar placeholder mb-auto">
{{ label ? label.name : "" }} <div class="bg-neutral-focus text-neutral-content rounded-full w-12">
</span> <Icon name="mdi-package-variant" class="h-7 w-7" />
</div>
<template #description> </div>
<Markdown class="text-lg" :source="label.description"> </Markdown> <div>
</template> <h1 class="text-2xl pb-1">
</BaseSectionHeader> {{ label ? label.name : "" }}
</h1>
<div class="flex gap-3 flex-wrap mb-6 text-sm italic"> <div class="flex gap-1 flex-wrap text-xs">
<div> <div>
Created Created
<DateTime :date="label?.createdAt" /> <DateTime :date="label?.createdAt" />
</div>
</div>
</div>
<div class="ml-auto mt-2 flex flex-wrap items-center justify-between gap-3">
<div class="btn-group">
<PageQRCode class="dropdown-left" />
<BaseButton size="sm" @click="openUpdate">
<Icon class="mr-1" name="mdi-pencil" />
Edit
</BaseButton>
</div>
<BaseButton class="btn btn-sm" @click="confirmDelete()">
<Icon name="mdi-delete" class="mr-2" />
Delete
</BaseButton>
</div>
</div> </div>
<div> </header>
<Icon name="mdi-circle-small" /> <div class="divider my-0 mb-1"></div>
</div> <Markdown v-if="label && label.description" class="text-base" :source="label.description"> </Markdown>
<div> </div>
Last Updated <section v-if="label && items">
<DateTime :date="label?.updatedAt" /> <ItemViewSelectable :items="items" />
</div>
</div>
<div class="flex flex-wrap items-center justify-between mb-6 mt-3">
<div class="btn-group">
<PageQRCode class="dropdown-right" />
<BaseButton class="ml-auto" size="sm" @click="openUpdate">
<Icon class="mr-1" name="mdi-pencil" />
Edit
</BaseButton>
</div>
<BaseButton class="btn btn-sm" @click="confirmDelete()">
<Icon name="mdi-delete" class="mr-2" />
Delete
</BaseButton>
</div>
</section> </section>
</BaseContainer> </BaseContainer>
<section v-if="label && label.items">
<ItemViewSelectable :items="label.items" />
</section>
</BaseContainer> </BaseContainer>
</template> </template>

View file

@ -68,6 +68,7 @@
const { error, data } = await api.locations.update(locationId.value, updateData); const { error, data } = await api.locations.update(locationId.value, updateData);
if (error) { if (error) {
updating.value = false;
toast.error("Failed to update location"); toast.error("Failed to update location");
return; return;
} }
@ -82,6 +83,23 @@
const locations = computed(() => locationStore.allLocations); const locations = computed(() => locationStore.allLocations);
const parent = ref<LocationSummary | any>({}); const parent = ref<LocationSummary | any>({});
const items = computedAsync(async () => {
if (!location.value) {
return [];
}
const resp = await api.items.getAll({
locations: [location.value.id],
});
if (resp.error) {
toast.error("Failed to load items");
return [];
}
return resp.data.items;
});
</script> </script>
<template> <template>
@ -99,65 +117,54 @@
</form> </form>
</BaseModal> </BaseModal>
<BaseContainer v-if="location" class="space-y-6 mb-16"> <BaseContainer v-if="location">
<section> <div class="bg-white rounded p-3">
<BaseSectionHeader v-if="location"> <header class="mb-2">
<Icon name="mdi-package-variant" class="mr-2 -mt-1 text-base-content" /> <div class="flex flex-wrap items-end gap-2">
<span class="text-base-content"> <div class="avatar placeholder mb-auto">
{{ location ? location.name : "" }} <div class="bg-neutral-focus text-neutral-content rounded-full w-12">
</span> <Icon name="mdi-package-variant" class="h-7 w-7" />
</div>
<div v-if="location?.parent" class="text-sm breadcrumbs pb-0"> </div>
<ul class="text-base-content/70"> <div>
<li> <div v-if="location?.parent" class="text-sm breadcrumbs pt-0 pb-0">
<NuxtLink :to="`/location/${location.parent.id}`"> {{ location.parent.name }}</NuxtLink> <ul class="text-base-content/70">
</li> <li>
<li>{{ location.name }}</li> <NuxtLink :to="`/location/${location.parent.id}`"> {{ location.parent.name }}</NuxtLink>
</ul> </li>
<li>{{ location.name }}</li>
</ul>
</div>
<h1 class="text-2xl pb-1">
{{ location ? location.name : "" }}
</h1>
<div class="flex gap-1 flex-wrap text-xs">
<div>
Created
<DateTime :date="location?.createdAt" />
</div>
</div>
</div>
<div class="ml-auto mt-2 flex flex-wrap items-center justify-between gap-3">
<div class="btn-group">
<PageQRCode class="dropdown-left" />
<BaseButton size="sm" @click="openUpdate">
<Icon class="mr-1" name="mdi-pencil" />
Edit
</BaseButton>
</div>
<BaseButton class="btn btn-sm" @click="confirmDelete()">
<Icon name="mdi-delete" class="mr-2" />
Delete
</BaseButton>
</div>
</div> </div>
<template #description> </header>
<Markdown class="text-lg" :source="location.description"> </Markdown> <div class="divider my-0 mb-1"></div>
</template> <Markdown v-if="location && location.description" class="text-base" :source="location.description"> </Markdown>
</BaseSectionHeader> </div>
<section v-if="location && items">
<div class="flex gap-3 flex-wrap mb-6 text-sm italic"> <ItemViewSelectable :items="items" />
<div>
Created
<DateTime :date="location?.createdAt" />
</div>
<div>
<Icon name="mdi-circle-small" />
</div>
<div>
Last Updated
<DateTime :date="location?.updatedAt" />
</div>
</div>
<div class="flex flex-wrap items-center justify-between mb-6 mt-3">
<div class="btn-group">
<PageQRCode class="dropdown-right" />
<BaseButton class="ml-auto" size="sm" @click="openUpdate">
<Icon class="mr-1" name="mdi-pencil" />
Edit
</BaseButton>
</div>
<BaseButton class="btn btn-sm" @click="confirmDelete()">
<Icon name="mdi-delete" class="mr-2" />
Delete
</BaseButton>
</div>
</section>
<template v-if="location && location.items.length > 0">
<ItemViewSelectable :items="location.items" />
</template>
<section v-if="location && location.children.length > 0">
<BaseSectionHeader class="mb-5"> Child Locations </BaseSectionHeader>
<div class="grid gap-2 grid-cols-1 sm:grid-cols-3">
<LocationCard v-for="item in location.children" :key="item.id" :location="item" />
</div>
</section> </section>
</BaseContainer> </BaseContainer>
</div> </div>

View file

@ -82,6 +82,12 @@
See Github Issue #236 for more details. See Github Issue #236 for more details.
</a> </a>
</DetailAction> </DetailAction>
<DetailAction @action="setPrimaryPhotos">
<template #title> Set Primary Photos </template>
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>
</DetailAction>
</div> </div>
</BaseCard> </BaseCard>
</BaseContainer> </BaseContainer>
@ -173,6 +179,25 @@
notify.success(`${result.data.completed} assets have been updated.`); notify.success(`${result.data.completed} assets have been updated.`);
} }
async function setPrimaryPhotos() {
const { isCanceled } = await confirm.open(
"Are you sure you want to set primary photos? This can take a while and cannot be undone."
);
if (isCanceled) {
return;
}
const result = await api.actions.setPrimaryPhotos();
if (result.error) {
notify.error("Failed to set primary photos.");
return;
}
notify.success(`${result.data.completed} assets have been updated.`);
}
</script> </script>
<style scoped></style> <style scoped></style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB