From a886fa86ca6ac3d75be1e77766f55d111299f51b Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Mon, 31 Oct 2022 23:30:42 -0800 Subject: [PATCH] feat: add archive item options (#122) Add archive option feature. Archived items can only be seen on the items page when including archived is selected. Archived items are excluded from the count and from other views --- backend/app/api/handlers/v1/v1_ctrl_items.go | 19 +++++-- backend/app/api/static/docs/docs.go | 9 +++ backend/app/api/static/docs/swagger.json | 9 +++ backend/app/api/static/docs/swagger.yaml | 6 ++ backend/internal/data/ent/item.go | 13 ++++- backend/internal/data/ent/item/item.go | 5 ++ backend/internal/data/ent/item/where.go | 21 +++++++ backend/internal/data/ent/item_create.go | 29 ++++++++++ backend/internal/data/ent/item_update.go | 42 ++++++++++++++ backend/internal/data/ent/migrate/schema.go | 16 ++++-- backend/internal/data/ent/mutation.go | 56 ++++++++++++++++++- backend/internal/data/ent/runtime.go | 20 ++++--- backend/internal/data/ent/schema/item.go | 3 + .../20221101041931_add_archived_field.sql | 22 ++++++++ .../data/migrations/migrations/atlas.sum | 3 +- backend/internal/data/repo/repo_items.go | 32 ++++++++--- backend/internal/data/repo/repo_labels.go | 5 +- backend/internal/data/repo/repo_locations.go | 4 +- frontend/components/Form/Checkbox.vue | 4 +- frontend/components/Item/Card.vue | 1 + frontend/components/global/Spacer.vue | 3 + frontend/composables/use-item-search.ts | 8 ++- frontend/lib/api/classes/items.ts | 1 + frontend/lib/api/types/data-contracts.ts | 3 + frontend/pages/item/[id]/edit.vue | 5 ++ frontend/pages/item/[id]/index.vue | 8 +-- frontend/pages/items.vue | 16 +++++- 27 files changed, 325 insertions(+), 38 deletions(-) create mode 100644 backend/internal/data/migrations/migrations/20221101041931_add_archived_field.sql create mode 100644 frontend/components/global/Spacer.vue diff --git a/backend/app/api/handlers/v1/v1_ctrl_items.go b/backend/app/api/handlers/v1/v1_ctrl_items.go index b43333a..732600f 100644 --- a/backend/app/api/handlers/v1/v1_ctrl_items.go +++ b/backend/app/api/handlers/v1/v1_ctrl_items.go @@ -47,15 +47,24 @@ func (ctrl *V1Controller) HandleItemsGetAll() server.HandlerFunc { return i } + getBool := func(s string) bool { + b, err := strconv.ParseBool(s) + if err != nil { + return false + } + return b + } + extractQuery := func(r *http.Request) repo.ItemQuery { params := r.URL.Query() return repo.ItemQuery{ - Page: intOrNegativeOne(params.Get("page")), - PageSize: intOrNegativeOne(params.Get("perPage")), - Search: params.Get("q"), - LocationIDs: uuidList(params, "locations"), - LabelIDs: uuidList(params, "labels"), + Page: intOrNegativeOne(params.Get("page")), + PageSize: intOrNegativeOne(params.Get("perPage")), + Search: params.Get("q"), + LocationIDs: uuidList(params, "locations"), + LabelIDs: uuidList(params, "labels"), + IncludeArchived: getBool(params.Get("includeArchived")), } } diff --git a/backend/app/api/static/docs/docs.go b/backend/app/api/static/docs/docs.go index 7cad594..7d19c80 100644 --- a/backend/app/api/static/docs/docs.go +++ b/backend/app/api/static/docs/docs.go @@ -1274,6 +1274,9 @@ const docTemplate = `{ "repo.ItemOut": { "type": "object", "properties": { + "archived": { + "type": "boolean" + }, "attachments": { "type": "array", "items": { @@ -1383,6 +1386,9 @@ const docTemplate = `{ "repo.ItemSummary": { "type": "object", "properties": { + "archived": { + "type": "boolean" + }, "createdAt": { "type": "string" }, @@ -1421,6 +1427,9 @@ const docTemplate = `{ "repo.ItemUpdate": { "type": "object", "properties": { + "archived": { + "type": "boolean" + }, "description": { "type": "string" }, diff --git a/backend/app/api/static/docs/swagger.json b/backend/app/api/static/docs/swagger.json index b5e97e8..64bbf41 100644 --- a/backend/app/api/static/docs/swagger.json +++ b/backend/app/api/static/docs/swagger.json @@ -1266,6 +1266,9 @@ "repo.ItemOut": { "type": "object", "properties": { + "archived": { + "type": "boolean" + }, "attachments": { "type": "array", "items": { @@ -1375,6 +1378,9 @@ "repo.ItemSummary": { "type": "object", "properties": { + "archived": { + "type": "boolean" + }, "createdAt": { "type": "string" }, @@ -1413,6 +1419,9 @@ "repo.ItemUpdate": { "type": "object", "properties": { + "archived": { + "type": "boolean" + }, "description": { "type": "string" }, diff --git a/backend/app/api/static/docs/swagger.yaml b/backend/app/api/static/docs/swagger.yaml index d2cf02a..eb80be3 100644 --- a/backend/app/api/static/docs/swagger.yaml +++ b/backend/app/api/static/docs/swagger.yaml @@ -85,6 +85,8 @@ definitions: type: object repo.ItemOut: properties: + archived: + type: boolean attachments: items: $ref: '#/definitions/repo.ItemAttachment' @@ -161,6 +163,8 @@ definitions: type: object repo.ItemSummary: properties: + archived: + type: boolean createdAt: type: string description: @@ -187,6 +191,8 @@ definitions: type: object repo.ItemUpdate: properties: + archived: + type: boolean description: type: string fields: diff --git a/backend/internal/data/ent/item.go b/backend/internal/data/ent/item.go index d9d66b4..802acff 100644 --- a/backend/internal/data/ent/item.go +++ b/backend/internal/data/ent/item.go @@ -35,6 +35,8 @@ type Item struct { Quantity int `json:"quantity,omitempty"` // Insured holds the value of the "insured" field. Insured bool `json:"insured,omitempty"` + // Archived holds the value of the "archived" field. + Archived bool `json:"archived,omitempty"` // SerialNumber holds the value of the "serial_number" field. SerialNumber string `json:"serial_number,omitempty"` // ModelNumber holds the value of the "model_number" field. @@ -170,7 +172,7 @@ func (*Item) scanValues(columns []string) ([]any, error) { values := make([]any, len(columns)) for i := range columns { switch columns[i] { - case item.FieldInsured, item.FieldLifetimeWarranty: + case item.FieldInsured, item.FieldArchived, item.FieldLifetimeWarranty: values[i] = new(sql.NullBool) case item.FieldPurchasePrice, item.FieldSoldPrice: values[i] = new(sql.NullFloat64) @@ -257,6 +259,12 @@ func (i *Item) assignValues(columns []string, values []any) error { } else if value.Valid { i.Insured = value.Bool } + case item.FieldArchived: + if value, ok := values[j].(*sql.NullBool); !ok { + return fmt.Errorf("unexpected type %T for field archived", values[j]) + } else if value.Valid { + i.Archived = value.Bool + } case item.FieldSerialNumber: if value, ok := values[j].(*sql.NullString); !ok { return fmt.Errorf("unexpected type %T for field serial_number", values[j]) @@ -443,6 +451,9 @@ func (i *Item) String() string { builder.WriteString("insured=") builder.WriteString(fmt.Sprintf("%v", i.Insured)) builder.WriteString(", ") + builder.WriteString("archived=") + builder.WriteString(fmt.Sprintf("%v", i.Archived)) + builder.WriteString(", ") builder.WriteString("serial_number=") builder.WriteString(i.SerialNumber) builder.WriteString(", ") diff --git a/backend/internal/data/ent/item/item.go b/backend/internal/data/ent/item/item.go index 7592f5b..c2991da 100644 --- a/backend/internal/data/ent/item/item.go +++ b/backend/internal/data/ent/item/item.go @@ -29,6 +29,8 @@ const ( FieldQuantity = "quantity" // FieldInsured holds the string denoting the insured field in the database. FieldInsured = "insured" + // FieldArchived holds the string denoting the archived field in the database. + FieldArchived = "archived" // FieldSerialNumber holds the string denoting the serial_number field in the database. FieldSerialNumber = "serial_number" // FieldModelNumber holds the string denoting the model_number field in the database. @@ -125,6 +127,7 @@ var Columns = []string{ FieldNotes, FieldQuantity, FieldInsured, + FieldArchived, FieldSerialNumber, FieldModelNumber, FieldManufacturer, @@ -188,6 +191,8 @@ var ( DefaultQuantity int // DefaultInsured holds the default value on creation for the "insured" field. DefaultInsured bool + // DefaultArchived holds the default value on creation for the "archived" field. + DefaultArchived bool // SerialNumberValidator is a validator for the "serial_number" field. It is called by the builders before save. SerialNumberValidator func(string) error // ModelNumberValidator is a validator for the "model_number" field. It is called by the builders before save. diff --git a/backend/internal/data/ent/item/where.go b/backend/internal/data/ent/item/where.go index cb4b0d9..2897e35 100644 --- a/backend/internal/data/ent/item/where.go +++ b/backend/internal/data/ent/item/where.go @@ -138,6 +138,13 @@ func Insured(v bool) predicate.Item { }) } +// Archived applies equality check predicate on the "archived" field. It's identical to ArchivedEQ. +func Archived(v bool) predicate.Item { + return predicate.Item(func(s *sql.Selector) { + s.Where(sql.EQ(s.C(FieldArchived), v)) + }) +} + // SerialNumber applies equality check predicate on the "serial_number" field. It's identical to SerialNumberEQ. func SerialNumber(v string) predicate.Item { return predicate.Item(func(s *sql.Selector) { @@ -873,6 +880,20 @@ func InsuredNEQ(v bool) predicate.Item { }) } +// ArchivedEQ applies the EQ predicate on the "archived" field. +func ArchivedEQ(v bool) predicate.Item { + return predicate.Item(func(s *sql.Selector) { + s.Where(sql.EQ(s.C(FieldArchived), v)) + }) +} + +// ArchivedNEQ applies the NEQ predicate on the "archived" field. +func ArchivedNEQ(v bool) predicate.Item { + return predicate.Item(func(s *sql.Selector) { + s.Where(sql.NEQ(s.C(FieldArchived), v)) + }) +} + // SerialNumberEQ applies the EQ predicate on the "serial_number" field. func SerialNumberEQ(v string) predicate.Item { return predicate.Item(func(s *sql.Selector) { diff --git a/backend/internal/data/ent/item_create.go b/backend/internal/data/ent/item_create.go index 5b0842d..f4de18e 100644 --- a/backend/internal/data/ent/item_create.go +++ b/backend/internal/data/ent/item_create.go @@ -130,6 +130,20 @@ func (ic *ItemCreate) SetNillableInsured(b *bool) *ItemCreate { return ic } +// SetArchived sets the "archived" field. +func (ic *ItemCreate) SetArchived(b bool) *ItemCreate { + ic.mutation.SetArchived(b) + return ic +} + +// SetNillableArchived sets the "archived" field if the given value is not nil. +func (ic *ItemCreate) SetNillableArchived(b *bool) *ItemCreate { + if b != nil { + ic.SetArchived(*b) + } + return ic +} + // SetSerialNumber sets the "serial_number" field. func (ic *ItemCreate) SetSerialNumber(s string) *ItemCreate { ic.mutation.SetSerialNumber(s) @@ -528,6 +542,10 @@ func (ic *ItemCreate) defaults() { v := item.DefaultInsured ic.mutation.SetInsured(v) } + if _, ok := ic.mutation.Archived(); !ok { + v := item.DefaultArchived + ic.mutation.SetArchived(v) + } if _, ok := ic.mutation.LifetimeWarranty(); !ok { v := item.DefaultLifetimeWarranty ic.mutation.SetLifetimeWarranty(v) @@ -583,6 +601,9 @@ func (ic *ItemCreate) check() error { if _, ok := ic.mutation.Insured(); !ok { return &ValidationError{Name: "insured", err: errors.New(`ent: missing required field "Item.insured"`)} } + if _, ok := ic.mutation.Archived(); !ok { + return &ValidationError{Name: "archived", err: errors.New(`ent: missing required field "Item.archived"`)} + } if v, ok := ic.mutation.SerialNumber(); ok { if err := item.SerialNumberValidator(v); err != nil { return &ValidationError{Name: "serial_number", err: fmt.Errorf(`ent: validator failed for field "Item.serial_number": %w`, err)} @@ -720,6 +741,14 @@ func (ic *ItemCreate) createSpec() (*Item, *sqlgraph.CreateSpec) { }) _node.Insured = value } + if value, ok := ic.mutation.Archived(); ok { + _spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{ + Type: field.TypeBool, + Value: value, + Column: item.FieldArchived, + }) + _node.Archived = value + } if value, ok := ic.mutation.SerialNumber(); ok { _spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{ Type: field.TypeString, diff --git a/backend/internal/data/ent/item_update.go b/backend/internal/data/ent/item_update.go index 86dd6dd..4307051 100644 --- a/backend/internal/data/ent/item_update.go +++ b/backend/internal/data/ent/item_update.go @@ -121,6 +121,20 @@ func (iu *ItemUpdate) SetNillableInsured(b *bool) *ItemUpdate { return iu } +// SetArchived sets the "archived" field. +func (iu *ItemUpdate) SetArchived(b bool) *ItemUpdate { + iu.mutation.SetArchived(b) + return iu +} + +// SetNillableArchived sets the "archived" field if the given value is not nil. +func (iu *ItemUpdate) SetNillableArchived(b *bool) *ItemUpdate { + if b != nil { + iu.SetArchived(*b) + } + return iu +} + // SetSerialNumber sets the "serial_number" field. func (iu *ItemUpdate) SetSerialNumber(s string) *ItemUpdate { iu.mutation.SetSerialNumber(s) @@ -795,6 +809,13 @@ func (iu *ItemUpdate) sqlSave(ctx context.Context) (n int, err error) { Column: item.FieldInsured, }) } + if value, ok := iu.mutation.Archived(); ok { + _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ + Type: field.TypeBool, + Value: value, + Column: item.FieldArchived, + }) + } if value, ok := iu.mutation.SerialNumber(); ok { _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ Type: field.TypeString, @@ -1387,6 +1408,20 @@ func (iuo *ItemUpdateOne) SetNillableInsured(b *bool) *ItemUpdateOne { return iuo } +// SetArchived sets the "archived" field. +func (iuo *ItemUpdateOne) SetArchived(b bool) *ItemUpdateOne { + iuo.mutation.SetArchived(b) + return iuo +} + +// SetNillableArchived sets the "archived" field if the given value is not nil. +func (iuo *ItemUpdateOne) SetNillableArchived(b *bool) *ItemUpdateOne { + if b != nil { + iuo.SetArchived(*b) + } + return iuo +} + // SetSerialNumber sets the "serial_number" field. func (iuo *ItemUpdateOne) SetSerialNumber(s string) *ItemUpdateOne { iuo.mutation.SetSerialNumber(s) @@ -2091,6 +2126,13 @@ func (iuo *ItemUpdateOne) sqlSave(ctx context.Context) (_node *Item, err error) Column: item.FieldInsured, }) } + if value, ok := iuo.mutation.Archived(); ok { + _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ + Type: field.TypeBool, + Value: value, + Column: item.FieldArchived, + }) + } if value, ok := iuo.mutation.SerialNumber(); ok { _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ Type: field.TypeString, diff --git a/backend/internal/data/ent/migrate/schema.go b/backend/internal/data/ent/migrate/schema.go index 5aa35b5..aeb751e 100644 --- a/backend/internal/data/ent/migrate/schema.go +++ b/backend/internal/data/ent/migrate/schema.go @@ -170,6 +170,7 @@ var ( {Name: "notes", Type: field.TypeString, Nullable: true, Size: 1000}, {Name: "quantity", Type: field.TypeInt, Default: 1}, {Name: "insured", Type: field.TypeBool, Default: false}, + {Name: "archived", Type: field.TypeBool, Default: false}, {Name: "serial_number", Type: field.TypeString, Nullable: true, Size: 255}, {Name: "model_number", Type: field.TypeString, Nullable: true, Size: 255}, {Name: "manufacturer", Type: field.TypeString, Nullable: true, Size: 255}, @@ -195,19 +196,19 @@ var ( ForeignKeys: []*schema.ForeignKey{ { Symbol: "items_groups_items", - Columns: []*schema.Column{ItemsColumns[22]}, + Columns: []*schema.Column{ItemsColumns[23]}, RefColumns: []*schema.Column{GroupsColumns[0]}, OnDelete: schema.Cascade, }, { Symbol: "items_items_children", - Columns: []*schema.Column{ItemsColumns[23]}, + Columns: []*schema.Column{ItemsColumns[24]}, RefColumns: []*schema.Column{ItemsColumns[0]}, OnDelete: schema.SetNull, }, { Symbol: "items_locations_items", - Columns: []*schema.Column{ItemsColumns[24]}, + Columns: []*schema.Column{ItemsColumns[25]}, RefColumns: []*schema.Column{LocationsColumns[0]}, OnDelete: schema.Cascade, }, @@ -221,16 +222,21 @@ var ( { Name: "item_manufacturer", Unique: false, - Columns: []*schema.Column{ItemsColumns[11]}, + Columns: []*schema.Column{ItemsColumns[12]}, }, { Name: "item_model_number", Unique: false, - Columns: []*schema.Column{ItemsColumns[10]}, + Columns: []*schema.Column{ItemsColumns[11]}, }, { Name: "item_serial_number", Unique: false, + Columns: []*schema.Column{ItemsColumns[10]}, + }, + { + Name: "item_archived", + Unique: false, Columns: []*schema.Column{ItemsColumns[9]}, }, }, diff --git a/backend/internal/data/ent/mutation.go b/backend/internal/data/ent/mutation.go index c245719..73ccbde 100644 --- a/backend/internal/data/ent/mutation.go +++ b/backend/internal/data/ent/mutation.go @@ -4133,6 +4133,7 @@ type ItemMutation struct { quantity *int addquantity *int insured *bool + archived *bool serial_number *string model_number *string manufacturer *string @@ -4623,6 +4624,42 @@ func (m *ItemMutation) ResetInsured() { m.insured = nil } +// SetArchived sets the "archived" field. +func (m *ItemMutation) SetArchived(b bool) { + m.archived = &b +} + +// Archived returns the value of the "archived" field in the mutation. +func (m *ItemMutation) Archived() (r bool, exists bool) { + v := m.archived + if v == nil { + return + } + return *v, true +} + +// OldArchived returns the old "archived" field's value of the Item entity. +// If the Item 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 *ItemMutation) OldArchived(ctx context.Context) (v bool, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldArchived is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldArchived requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldArchived: %w", err) + } + return oldValue.Archived, nil +} + +// ResetArchived resets all changes to the "archived" field. +func (m *ItemMutation) ResetArchived() { + m.archived = nil +} + // SetSerialNumber sets the "serial_number" field. func (m *ItemMutation) SetSerialNumber(s string) { m.serial_number = &s @@ -5613,7 +5650,7 @@ func (m *ItemMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *ItemMutation) Fields() []string { - fields := make([]string, 0, 21) + fields := make([]string, 0, 22) if m.created_at != nil { fields = append(fields, item.FieldCreatedAt) } @@ -5638,6 +5675,9 @@ func (m *ItemMutation) Fields() []string { if m.insured != nil { fields = append(fields, item.FieldInsured) } + if m.archived != nil { + fields = append(fields, item.FieldArchived) + } if m.serial_number != nil { fields = append(fields, item.FieldSerialNumber) } @@ -5701,6 +5741,8 @@ func (m *ItemMutation) Field(name string) (ent.Value, bool) { return m.Quantity() case item.FieldInsured: return m.Insured() + case item.FieldArchived: + return m.Archived() case item.FieldSerialNumber: return m.SerialNumber() case item.FieldModelNumber: @@ -5752,6 +5794,8 @@ func (m *ItemMutation) OldField(ctx context.Context, name string) (ent.Value, er return m.OldQuantity(ctx) case item.FieldInsured: return m.OldInsured(ctx) + case item.FieldArchived: + return m.OldArchived(ctx) case item.FieldSerialNumber: return m.OldSerialNumber(ctx) case item.FieldModelNumber: @@ -5843,6 +5887,13 @@ func (m *ItemMutation) SetField(name string, value ent.Value) error { } m.SetInsured(v) return nil + case item.FieldArchived: + v, ok := value.(bool) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetArchived(v) + return nil case item.FieldSerialNumber: v, ok := value.(string) if !ok { @@ -6127,6 +6178,9 @@ func (m *ItemMutation) ResetField(name string) error { case item.FieldInsured: m.ResetInsured() return nil + case item.FieldArchived: + m.ResetArchived() + return nil case item.FieldSerialNumber: m.ResetSerialNumber() return nil diff --git a/backend/internal/data/ent/runtime.go b/backend/internal/data/ent/runtime.go index 3929dab..af5dc22 100644 --- a/backend/internal/data/ent/runtime.go +++ b/backend/internal/data/ent/runtime.go @@ -271,36 +271,40 @@ func init() { itemDescInsured := itemFields[3].Descriptor() // item.DefaultInsured holds the default value on creation for the insured field. item.DefaultInsured = itemDescInsured.Default.(bool) + // itemDescArchived is the schema descriptor for archived field. + itemDescArchived := itemFields[4].Descriptor() + // item.DefaultArchived holds the default value on creation for the archived field. + item.DefaultArchived = itemDescArchived.Default.(bool) // itemDescSerialNumber is the schema descriptor for serial_number field. - itemDescSerialNumber := itemFields[4].Descriptor() + itemDescSerialNumber := itemFields[5].Descriptor() // item.SerialNumberValidator is a validator for the "serial_number" field. It is called by the builders before save. item.SerialNumberValidator = itemDescSerialNumber.Validators[0].(func(string) error) // itemDescModelNumber is the schema descriptor for model_number field. - itemDescModelNumber := itemFields[5].Descriptor() + itemDescModelNumber := itemFields[6].Descriptor() // item.ModelNumberValidator is a validator for the "model_number" field. It is called by the builders before save. item.ModelNumberValidator = itemDescModelNumber.Validators[0].(func(string) error) // itemDescManufacturer is the schema descriptor for manufacturer field. - itemDescManufacturer := itemFields[6].Descriptor() + itemDescManufacturer := itemFields[7].Descriptor() // item.ManufacturerValidator is a validator for the "manufacturer" field. It is called by the builders before save. item.ManufacturerValidator = itemDescManufacturer.Validators[0].(func(string) error) // itemDescLifetimeWarranty is the schema descriptor for lifetime_warranty field. - itemDescLifetimeWarranty := itemFields[7].Descriptor() + itemDescLifetimeWarranty := itemFields[8].Descriptor() // item.DefaultLifetimeWarranty holds the default value on creation for the lifetime_warranty field. item.DefaultLifetimeWarranty = itemDescLifetimeWarranty.Default.(bool) // itemDescWarrantyDetails is the schema descriptor for warranty_details field. - itemDescWarrantyDetails := itemFields[9].Descriptor() + itemDescWarrantyDetails := itemFields[10].Descriptor() // item.WarrantyDetailsValidator is a validator for the "warranty_details" field. It is called by the builders before save. item.WarrantyDetailsValidator = itemDescWarrantyDetails.Validators[0].(func(string) error) // itemDescPurchasePrice is the schema descriptor for purchase_price field. - itemDescPurchasePrice := itemFields[12].Descriptor() + itemDescPurchasePrice := itemFields[13].Descriptor() // item.DefaultPurchasePrice holds the default value on creation for the purchase_price field. item.DefaultPurchasePrice = itemDescPurchasePrice.Default.(float64) // itemDescSoldPrice is the schema descriptor for sold_price field. - itemDescSoldPrice := itemFields[15].Descriptor() + itemDescSoldPrice := itemFields[16].Descriptor() // item.DefaultSoldPrice holds the default value on creation for the sold_price field. item.DefaultSoldPrice = itemDescSoldPrice.Default.(float64) // itemDescSoldNotes is the schema descriptor for sold_notes field. - itemDescSoldNotes := itemFields[16].Descriptor() + itemDescSoldNotes := itemFields[17].Descriptor() // item.SoldNotesValidator is a validator for the "sold_notes" field. It is called by the builders before save. item.SoldNotesValidator = itemDescSoldNotes.Validators[0].(func(string) error) // itemDescID is the schema descriptor for id field. diff --git a/backend/internal/data/ent/schema/item.go b/backend/internal/data/ent/schema/item.go index 17021a5..f7799f4 100644 --- a/backend/internal/data/ent/schema/item.go +++ b/backend/internal/data/ent/schema/item.go @@ -28,6 +28,7 @@ func (Item) Indexes() []ent.Index { index.Fields("manufacturer"), index.Fields("model_number"), index.Fields("serial_number"), + index.Fields("archived"), } } @@ -45,6 +46,8 @@ func (Item) Fields() []ent.Field { Default(1), field.Bool("insured"). Default(false), + field.Bool("archived"). + Default(false), // ------------------------------------ // item identification diff --git a/backend/internal/data/migrations/migrations/20221101041931_add_archived_field.sql b/backend/internal/data/migrations/migrations/20221101041931_add_archived_field.sql new file mode 100644 index 0000000..db3962b --- /dev/null +++ b/backend/internal/data/migrations/migrations/20221101041931_add_archived_field.sql @@ -0,0 +1,22 @@ +-- disable the enforcement of foreign-keys constraints +PRAGMA foreign_keys = off; +-- create "new_items" table +CREATE TABLE `new_items` (`id` uuid NOT NULL, `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, `name` text NOT NULL, `description` text NULL, `import_ref` text NULL, `notes` text NULL, `quantity` integer NOT NULL DEFAULT 1, `insured` bool NOT NULL DEFAULT false, `archived` bool NOT NULL DEFAULT false, `serial_number` text NULL, `model_number` text NULL, `manufacturer` text NULL, `lifetime_warranty` bool NOT NULL DEFAULT false, `warranty_expires` datetime NULL, `warranty_details` text NULL, `purchase_time` datetime NULL, `purchase_from` text NULL, `purchase_price` real NOT NULL DEFAULT 0, `sold_time` datetime NULL, `sold_to` text NULL, `sold_price` real NOT NULL DEFAULT 0, `sold_notes` text NULL, `group_items` uuid NOT NULL, `item_children` uuid NULL, `location_items` uuid NULL, PRIMARY KEY (`id`), CONSTRAINT `items_groups_items` FOREIGN KEY (`group_items`) REFERENCES `groups` (`id`) ON DELETE CASCADE, CONSTRAINT `items_items_children` FOREIGN KEY (`item_children`) REFERENCES `items` (`id`) ON DELETE SET NULL, CONSTRAINT `items_locations_items` FOREIGN KEY (`location_items`) REFERENCES `locations` (`id`) ON DELETE CASCADE); +-- copy rows from old table "items" to new temporary table "new_items" +INSERT INTO `new_items` (`id`, `created_at`, `updated_at`, `name`, `description`, `import_ref`, `notes`, `quantity`, `insured`, `serial_number`, `model_number`, `manufacturer`, `lifetime_warranty`, `warranty_expires`, `warranty_details`, `purchase_time`, `purchase_from`, `purchase_price`, `sold_time`, `sold_to`, `sold_price`, `sold_notes`, `group_items`, `item_children`, `location_items`) SELECT `id`, `created_at`, `updated_at`, `name`, `description`, `import_ref`, `notes`, `quantity`, `insured`, `serial_number`, `model_number`, `manufacturer`, `lifetime_warranty`, `warranty_expires`, `warranty_details`, `purchase_time`, `purchase_from`, `purchase_price`, `sold_time`, `sold_to`, `sold_price`, `sold_notes`, `group_items`, `item_children`, `location_items` FROM `items`; +-- drop "items" table after copying rows +DROP TABLE `items`; +-- rename temporary table "new_items" to "items" +ALTER TABLE `new_items` RENAME TO `items`; +-- create index "item_name" to table: "items" +CREATE INDEX `item_name` ON `items` (`name`); +-- create index "item_manufacturer" to table: "items" +CREATE INDEX `item_manufacturer` ON `items` (`manufacturer`); +-- create index "item_model_number" to table: "items" +CREATE INDEX `item_model_number` ON `items` (`model_number`); +-- create index "item_serial_number" to table: "items" +CREATE INDEX `item_serial_number` ON `items` (`serial_number`); +-- create index "item_archived" to table: "items" +CREATE INDEX `item_archived` ON `items` (`archived`); +-- enable back the enforcement of foreign-keys constraints +PRAGMA foreign_keys = on; diff --git a/backend/internal/data/migrations/migrations/atlas.sum b/backend/internal/data/migrations/migrations/atlas.sum index 04a34fb..2916627 100644 --- a/backend/internal/data/migrations/migrations/atlas.sum +++ b/backend/internal/data/migrations/migrations/atlas.sum @@ -1,5 +1,6 @@ -h1:mYTnmyrnBDST/r93NGJM33mIJqhp/U9qR440zI99eqQ= +h1:i76VRMDIPdcmQtXTe9bzrgITAzLGjjVy9y8XaXIchAs= 20220929052825_init.sql h1:ZlCqm1wzjDmofeAcSX3jE4h4VcdTNGpRg2eabztDy9Q= 20221001210956_group_invitations.sql h1:YQKJFtE39wFOcRNbZQ/d+ZlHwrcfcsZlcv/pLEYdpjw= 20221009173029_add_user_roles.sql h1:vWmzAfgEWQeGk0Vn70zfVPCcfEZth3E0JcvyKTjpYyU= 20221020043305_allow_nesting_types.sql h1:4AyJpZ7l7SSJtJAQETYY802FHJ64ufYPJTqvwdiGn3M= +20221101041931_add_archived_field.sql h1:L2WxiOh1svRn817cNURgqnEQg6DIcodZ1twK4tvxW94= diff --git a/backend/internal/data/repo/repo_items.go b/backend/internal/data/repo/repo_items.go index 8eefb5f..71619fa 100644 --- a/backend/internal/data/repo/repo_items.go +++ b/backend/internal/data/repo/repo_items.go @@ -20,12 +20,13 @@ type ItemsRepository struct { type ( ItemQuery struct { - Page int - PageSize int - Search string `json:"search"` - LocationIDs []uuid.UUID `json:"locationIds"` - LabelIDs []uuid.UUID `json:"labelIds"` - SortBy string `json:"sortBy"` + Page int + PageSize int + Search string `json:"search"` + LocationIDs []uuid.UUID `json:"locationIds"` + LabelIDs []uuid.UUID `json:"labelIds"` + SortBy string `json:"sortBy"` + IncludeArchived bool `json:"includeArchived"` } ItemField struct { @@ -55,6 +56,7 @@ type ( Description string `json:"description"` Quantity int `json:"quantity"` Insured bool `json:"insured"` + Archived bool `json:"archived"` // Edges LocationID uuid.UUID `json:"locationId"` @@ -93,6 +95,7 @@ type ( Description string `json:"description"` Quantity int `json:"quantity"` Insured bool `json:"insured"` + Archived bool `json:"archived"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` @@ -157,6 +160,7 @@ func mapItemSummary(item *ent.Item) ItemSummary { Quantity: item.Quantity, CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt, + Archived: item.Archived, // Edges Location: location, @@ -276,7 +280,20 @@ func (e *ItemsRepository) GetOneByGroup(ctx context.Context, gid, id uuid.UUID) // QueryByGroup returns a list of items that belong to a specific group based on the provided query. func (e *ItemsRepository) QueryByGroup(ctx context.Context, gid uuid.UUID, q ItemQuery) (PaginationResult[ItemSummary], error) { - qb := e.db.Item.Query().Where(item.HasGroupWith(group.ID(gid))) + qb := e.db.Item.Query().Where( + item.HasGroupWith(group.ID(gid)), + ) + + if q.IncludeArchived { + qb = qb.Where( + item.Or( + item.Archived(true), + item.Archived(false), + ), + ) + } else { + qb = qb.Where(item.Archived(false)) + } if len(q.LabelIDs) > 0 { labels := make([]predicate.Item, 0, len(q.LabelIDs)) @@ -384,6 +401,7 @@ func (e *ItemsRepository) UpdateByGroup(ctx context.Context, gid uuid.UUID, data SetSerialNumber(data.SerialNumber). SetModelNumber(data.ModelNumber). SetManufacturer(data.Manufacturer). + SetArchived(data.Archived). SetPurchaseTime(data.PurchaseTime). SetPurchaseFrom(data.PurchaseFrom). SetPurchasePrice(data.PurchasePrice). diff --git a/backend/internal/data/repo/repo_labels.go b/backend/internal/data/repo/repo_labels.go index 353df17..9d5b11a 100644 --- a/backend/internal/data/repo/repo_labels.go +++ b/backend/internal/data/repo/repo_labels.go @@ -7,6 +7,7 @@ import ( "github.com/google/uuid" "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/item" "github.com/hay-kot/homebox/backend/internal/data/ent/label" "github.com/hay-kot/homebox/backend/internal/data/ent/predicate" ) @@ -68,7 +69,9 @@ func (r *LabelRepository) getOne(ctx context.Context, where ...predicate.Label) return mapLabelOutErr(r.db.Label.Query(). Where(where...). WithGroup(). - WithItems(). + WithItems(func(iq *ent.ItemQuery) { + iq.Where(item.Archived(false)) + }). Only(ctx), ) } diff --git a/backend/internal/data/repo/repo_locations.go b/backend/internal/data/repo/repo_locations.go index 9e5f36d..394a757 100644 --- a/backend/internal/data/repo/repo_locations.go +++ b/backend/internal/data/repo/repo_locations.go @@ -7,6 +7,7 @@ import ( "github.com/google/uuid" "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/item" "github.com/hay-kot/homebox/backend/internal/data/ent/location" "github.com/hay-kot/homebox/backend/internal/data/ent/predicate" ) @@ -105,6 +106,7 @@ func (r *LocationRepository) GetAll(ctx context.Context, groupId uuid.UUID) ([]L items WHERE items.location_items = locations.id + AND items.archived = false ) as item_count FROM locations @@ -139,7 +141,7 @@ func (r *LocationRepository) getOne(ctx context.Context, where ...predicate.Loca Where(where...). WithGroup(). WithItems(func(iq *ent.ItemQuery) { - iq.WithLabel() + iq.Where(item.Archived(false)).WithLabel() }). WithParent(). WithChildren(). diff --git a/frontend/components/Form/Checkbox.vue b/frontend/components/Form/Checkbox.vue index d1cf6ff..5bd6c36 100644 --- a/frontend/components/Form/Checkbox.vue +++ b/frontend/components/Form/Checkbox.vue @@ -2,7 +2,7 @@
@@ -11,7 +11,7 @@ {{ label }} - +
diff --git a/frontend/components/Item/Card.vue b/frontend/components/Item/Card.vue index 6be75a9..7dfb62a 100644 --- a/frontend/components/Item/Card.vue +++ b/frontend/components/Item/Card.vue @@ -7,6 +7,7 @@

{{ item.name }} +

{{ description }}

diff --git a/frontend/components/global/Spacer.vue b/frontend/components/global/Spacer.vue new file mode 100644 index 0000000..2d9e3cd --- /dev/null +++ b/frontend/components/global/Spacer.vue @@ -0,0 +1,3 @@ + diff --git a/frontend/composables/use-item-search.ts b/frontend/composables/use-item-search.ts index 9944edd..b9d1eff 100644 --- a/frontend/composables/use-item-search.ts +++ b/frontend/composables/use-item-search.ts @@ -10,13 +10,19 @@ export function useItemSearch(client: UserClient, opts?: SearchOptions) { const locations = ref([]); const labels = ref([]); const results = ref([]); + const includeArchived = ref(false); watchDebounced(query, search, { debounce: 250, maxWait: 1000 }); async function search() { const locIds = locations.value.map(l => l.id); const labelIds = labels.value.map(l => l.id); - const { data, error } = await client.items.getAll({ q: query.value, locations: locIds, labels: labelIds }); + const { data, error } = await client.items.getAll({ + q: query.value, + locations: locIds, + labels: labelIds, + includeArchived: includeArchived.value, + }); if (error) { return; } diff --git a/frontend/lib/api/classes/items.ts b/frontend/lib/api/classes/items.ts index cd268ff..a97152d 100644 --- a/frontend/lib/api/classes/items.ts +++ b/frontend/lib/api/classes/items.ts @@ -11,6 +11,7 @@ import { import { AttachmentTypes, PaginationResult } from "../types/non-generated"; export type ItemsQuery = { + includeArchived?: boolean; page?: number; pageSize?: number; locations?: string[]; diff --git a/frontend/lib/api/types/data-contracts.ts b/frontend/lib/api/types/data-contracts.ts index e1bc8dd..e4304e3 100644 --- a/frontend/lib/api/types/data-contracts.ts +++ b/frontend/lib/api/types/data-contracts.ts @@ -63,6 +63,7 @@ export interface ItemField { } export interface ItemOut { + archived: boolean; attachments: ItemAttachment[]; children: ItemSummary[]; createdAt: Date; @@ -107,6 +108,7 @@ export interface ItemOut { } export interface ItemSummary { + archived: boolean; createdAt: Date; description: string; id: string; @@ -121,6 +123,7 @@ export interface ItemSummary { } export interface ItemUpdate { + archived: boolean; description: string; fields: ItemField[]; id: string; diff --git a/frontend/pages/item/[id]/edit.vue b/frontend/pages/item/[id]/edit.vue index 48b3123..ede78b8 100644 --- a/frontend/pages/item/[id]/edit.vue +++ b/frontend/pages/item/[id]/edit.vue @@ -115,6 +115,11 @@ label: "Insured", ref: "insured", }, + { + type: "checkbox", + label: "Archived", + ref: "archived", + }, ]; const purchaseFields: FormField[] = [ diff --git a/frontend/pages/item/[id]/index.vue b/frontend/pages/item/[id]/index.vue index 7fe51c2..03ddfc5 100644 --- a/frontend/pages/item/[id]/index.vue +++ b/frontend/pages/item/[id]/index.vue @@ -1,5 +1,5 @@