From 6cd9e2779fdb3cec87e12745c9ee229b0f21972a Mon Sep 17 00:00:00 2001 From: Tonya Date: Wed, 24 Sep 2025 02:37:38 +0100 Subject: [PATCH 1/5] Use Tanstack table for Selectable Table, quick actions (#998) * feat: implement example of data table * feat: load item data into table * chore: begin switching dialogs * feat: implement old dialog for controlling headers and page size * feat: get table into relatively usable state * feat: enhance dropdown actions for multi-selection and CSV download * feat: enhance table cell and dropdown button styles for better usability * feat: json download for table * feat: add expanded row component for item details in data table * chore: add translation support * feat: restore table on home page * fix: oops need ids * feat: move card view to use tanstack to allow for pagination * feat: switch the items search to use ItemViewSelectable * fix: update pagination handling and improve button click logic * feat: improve selectable table * feat: add indeterminate to checkbox * feat: overhaul maintenance dialog to use new system and add maintenance options to table * feat: add label ids and location id to item patch api * feat: change location and labels in table view * feat: add quick actions preference and enable toggle in table settings * fix: lint * fix: remove sized 1 pages * fix: attempt to fix type error * fix: various issues * fix: remove * fix: refactor item fetching logic to use useAsyncData for improved reactivity and improve use confirm * fix: sort backend issues * fix: enhance CSV export functionality by escaping fields to prevent formula injection * fix: put aria sort on th not button * chore: update api types --- backend/app/api/static/docs/docs.go | 13 + backend/app/api/static/docs/openapi-3.json | 13 + backend/app/api/static/docs/openapi-3.yaml | 10 + backend/app/api/static/docs/swagger.json | 13 + backend/app/api/static/docs/swagger.yaml | 10 + backend/internal/data/repo/repo_items.go | 98 ++++- docs/en/api/openapi-3.0.json | 13 + docs/en/api/openapi-3.0.yaml | 10 + docs/en/api/swagger-2.0.json | 13 + docs/en/api/swagger-2.0.yaml | 10 + docs/en/user-guide/tips-tricks.md | 8 + frontend/components/Base/SectionHeader.vue | 1 + frontend/components/Item/BarcodeModal.vue | 4 +- frontend/components/Item/Card.vue | 17 +- .../Item/View/ItemChangeDetails.vue | 169 +++++++++ frontend/components/Item/View/Selectable.vue | 70 +++- frontend/components/Item/View/Table.types.ts | 13 - frontend/components/Item/View/Table.vue | 340 +----------------- frontend/components/Item/View/pagination.ts | 8 + .../components/Item/View/table/card-view.vue | 66 ++++ .../components/Item/View/table/columns.ts | 270 ++++++++++++++ .../Item/View/table/data-table-controls.vue | 93 +++++ .../Item/View/table/data-table-dropdown.vue | 273 ++++++++++++++ .../View/table/data-table-expanded-row.vue | 45 +++ .../components/Item/View/table/data-table.vue | 252 +++++++++++++ .../components/Item/View/table/table-view.vue | 73 ++++ frontend/components/Label/Selector.vue | 7 +- frontend/components/Maintenance/EditModal.vue | 140 +++----- frontend/components/Maintenance/ListView.vue | 97 ++++- frontend/components/ModalConfirm.vue | 11 +- .../global/DetailsSection/DetailsSection.vue | 2 - frontend/components/ui/checkbox/Checkbox.vue | 36 +- .../components/ui/dialog-provider/utils.ts | 16 +- frontend/components/ui/table/TableCell.vue | 43 ++- frontend/composables/use-confirm.ts | 19 +- frontend/composables/use-preferences.ts | 17 +- frontend/eslint.config.mjs | 1 + frontend/layouts/default.vue | 2 +- frontend/lib/api/types/data-contracts.ts | 11 +- frontend/lib/utils.ts | 8 + frontend/locales/en.json | 39 +- frontend/package.json | 1 + frontend/pages/home/index.vue | 4 +- frontend/pages/item/[id]/index.vue | 38 +- frontend/pages/items.vue | 71 +--- frontend/pages/label/[id].vue | 50 +-- frontend/pages/location/[id].vue | 38 +- frontend/pnpm-lock.yaml | 20 ++ 48 files changed, 1959 insertions(+), 617 deletions(-) create mode 100644 frontend/components/Item/View/ItemChangeDetails.vue delete mode 100644 frontend/components/Item/View/Table.types.ts create mode 100644 frontend/components/Item/View/pagination.ts create mode 100644 frontend/components/Item/View/table/card-view.vue create mode 100644 frontend/components/Item/View/table/columns.ts create mode 100644 frontend/components/Item/View/table/data-table-controls.vue create mode 100644 frontend/components/Item/View/table/data-table-dropdown.vue create mode 100644 frontend/components/Item/View/table/data-table-expanded-row.vue create mode 100644 frontend/components/Item/View/table/data-table.vue create mode 100644 frontend/components/Item/View/table/table-view.vue diff --git a/backend/app/api/static/docs/docs.go b/backend/app/api/static/docs/docs.go index 6525c53a..1cbb82bf 100644 --- a/backend/app/api/static/docs/docs.go +++ b/backend/app/api/static/docs/docs.go @@ -3483,6 +3483,19 @@ const docTemplate = `{ "id": { "type": "string" }, + "labelIds": { + "type": "array", + "items": { + "type": "string" + }, + "x-nullable": true, + "x-omitempty": true + }, + "locationId": { + "type": "string", + "x-nullable": true, + "x-omitempty": true + }, "quantity": { "type": "integer", "x-nullable": true, diff --git a/backend/app/api/static/docs/openapi-3.json b/backend/app/api/static/docs/openapi-3.json index b0bcc2d8..2ae3681d 100644 --- a/backend/app/api/static/docs/openapi-3.json +++ b/backend/app/api/static/docs/openapi-3.json @@ -3661,6 +3661,19 @@ "id": { "type": "string" }, + "labelIds": { + "type": "array", + "items": { + "type": "string" + }, + "x-omitempty": true, + "nullable": true + }, + "locationId": { + "type": "string", + "x-omitempty": true, + "nullable": true + }, "quantity": { "type": "integer", "x-omitempty": true, diff --git a/backend/app/api/static/docs/openapi-3.yaml b/backend/app/api/static/docs/openapi-3.yaml index 9224a088..bc56d4a2 100644 --- a/backend/app/api/static/docs/openapi-3.yaml +++ b/backend/app/api/static/docs/openapi-3.yaml @@ -2293,6 +2293,16 @@ components: properties: id: type: string + labelIds: + type: array + items: + type: string + x-omitempty: true + nullable: true + locationId: + type: string + x-omitempty: true + nullable: true quantity: type: integer x-omitempty: true diff --git a/backend/app/api/static/docs/swagger.json b/backend/app/api/static/docs/swagger.json index 5c5e7aa2..3fd96322 100644 --- a/backend/app/api/static/docs/swagger.json +++ b/backend/app/api/static/docs/swagger.json @@ -3481,6 +3481,19 @@ "id": { "type": "string" }, + "labelIds": { + "type": "array", + "items": { + "type": "string" + }, + "x-nullable": true, + "x-omitempty": true + }, + "locationId": { + "type": "string", + "x-nullable": true, + "x-omitempty": true + }, "quantity": { "type": "integer", "x-nullable": true, diff --git a/backend/app/api/static/docs/swagger.yaml b/backend/app/api/static/docs/swagger.yaml index ac352ed2..1fc5f05e 100644 --- a/backend/app/api/static/docs/swagger.yaml +++ b/backend/app/api/static/docs/swagger.yaml @@ -875,6 +875,16 @@ definitions: properties: id: type: string + labelIds: + items: + type: string + type: array + x-nullable: true + x-omitempty: true + locationId: + type: string + x-nullable: true + x-omitempty: true quantity: type: integer x-nullable: true diff --git a/backend/internal/data/repo/repo_items.go b/backend/internal/data/repo/repo_items.go index 4fb50255..455fb21f 100644 --- a/backend/internal/data/repo/repo_items.go +++ b/backend/internal/data/repo/repo_items.go @@ -120,9 +120,11 @@ type ( } ItemPatch struct { - ID uuid.UUID `json:"id"` - Quantity *int `json:"quantity,omitempty" extensions:"x-nullable,x-omitempty"` - ImportRef *string `json:"-,omitempty" extensions:"x-nullable,x-omitempty"` + ID uuid.UUID `json:"id"` + Quantity *int `json:"quantity,omitempty" extensions:"x-nullable,x-omitempty"` + ImportRef *string `json:"-,omitempty" extensions:"x-nullable,x-omitempty"` + LocationID uuid.UUID `json:"locationId" extensions:"x-nullable,x-omitempty"` + LabelIDs []uuid.UUID `json:"labelIds" extensions:"x-nullable,x-omitempty"` } ItemSummary struct { @@ -814,7 +816,20 @@ func (e *ItemsRepository) GetAllZeroImportRef(ctx context.Context, gid uuid.UUID } func (e *ItemsRepository) Patch(ctx context.Context, gid, id uuid.UUID, data ItemPatch) error { - q := e.db.Item.Update(). + tx, err := e.db.Tx(ctx) + if err != nil { + return err + } + committed := false + defer func() { + if !committed { + if err := tx.Rollback(); err != nil { + log.Warn().Err(err).Msg("failed to rollback transaction during item patch") + } + } + }() + + q := tx.Item.Update(). Where( item.ID(id), item.HasGroupWith(group.ID(gid)), @@ -828,8 +843,81 @@ func (e *ItemsRepository) Patch(ctx context.Context, gid, id uuid.UUID, data Ite q.SetQuantity(*data.Quantity) } + if data.LocationID != uuid.Nil { + q.SetLocationID(data.LocationID) + } + + err = q.Exec(ctx) + if err != nil { + return err + } + + if data.LabelIDs != nil { + currentLabels, err := tx.Item.Query().Where(item.ID(id), item.HasGroupWith(group.ID(gid))).QueryLabel().All(ctx) + if err != nil { + return err + } + set := newIDSet(currentLabels) + + addLabels := []uuid.UUID{} + for _, l := range data.LabelIDs { + if set.Contains(l) { + set.Remove(l) + } else { + addLabels = append(addLabels, l) + } + } + + if len(addLabels) > 0 { + if err := tx.Item.Update(). + Where(item.ID(id), item.HasGroupWith(group.ID(gid))). + AddLabelIDs(addLabels...). + Exec(ctx); err != nil { + return err + } + } + if set.Len() > 0 { + if err := tx.Item.Update(). + Where(item.ID(id), item.HasGroupWith(group.ID(gid))). + RemoveLabelIDs(set.Slice()...). + Exec(ctx); err != nil { + return err + } + } + } + + if data.LocationID != uuid.Nil { + itemEnt, err := tx.Item.Query().Where(item.ID(id), item.HasGroupWith(group.ID(gid))).Only(ctx) + if err != nil { + return err + } + if itemEnt.SyncChildItemsLocations { + children, err := tx.Item.Query().Where(item.ID(id), item.HasGroupWith(group.ID(gid))).QueryChildren().All(ctx) + if err != nil { + return err + } + for _, child := range children { + childLocation, err := child.QueryLocation().First(ctx) + if err != nil { + return err + } + if data.LocationID != childLocation.ID { + err = child.Update().SetLocationID(data.LocationID).Exec(ctx) + if err != nil { + return err + } + } + } + } + } + + if err := tx.Commit(); err != nil { + return err + } + committed = true + e.publishMutationEvent(gid) - return q.Exec(ctx) + return nil } func (e *ItemsRepository) GetAllCustomFieldValues(ctx context.Context, gid uuid.UUID, name string) ([]string, error) { diff --git a/docs/en/api/openapi-3.0.json b/docs/en/api/openapi-3.0.json index b0bcc2d8..2ae3681d 100644 --- a/docs/en/api/openapi-3.0.json +++ b/docs/en/api/openapi-3.0.json @@ -3661,6 +3661,19 @@ "id": { "type": "string" }, + "labelIds": { + "type": "array", + "items": { + "type": "string" + }, + "x-omitempty": true, + "nullable": true + }, + "locationId": { + "type": "string", + "x-omitempty": true, + "nullable": true + }, "quantity": { "type": "integer", "x-omitempty": true, diff --git a/docs/en/api/openapi-3.0.yaml b/docs/en/api/openapi-3.0.yaml index 9224a088..bc56d4a2 100644 --- a/docs/en/api/openapi-3.0.yaml +++ b/docs/en/api/openapi-3.0.yaml @@ -2293,6 +2293,16 @@ components: properties: id: type: string + labelIds: + type: array + items: + type: string + x-omitempty: true + nullable: true + locationId: + type: string + x-omitempty: true + nullable: true quantity: type: integer x-omitempty: true diff --git a/docs/en/api/swagger-2.0.json b/docs/en/api/swagger-2.0.json index 5c5e7aa2..3fd96322 100644 --- a/docs/en/api/swagger-2.0.json +++ b/docs/en/api/swagger-2.0.json @@ -3481,6 +3481,19 @@ "id": { "type": "string" }, + "labelIds": { + "type": "array", + "items": { + "type": "string" + }, + "x-nullable": true, + "x-omitempty": true + }, + "locationId": { + "type": "string", + "x-nullable": true, + "x-omitempty": true + }, "quantity": { "type": "integer", "x-nullable": true, diff --git a/docs/en/api/swagger-2.0.yaml b/docs/en/api/swagger-2.0.yaml index ac352ed2..1fc5f05e 100644 --- a/docs/en/api/swagger-2.0.yaml +++ b/docs/en/api/swagger-2.0.yaml @@ -875,6 +875,16 @@ definitions: properties: id: type: string + labelIds: + items: + type: string + type: array + x-nullable: true + x-omitempty: true + locationId: + type: string + x-nullable: true + x-omitempty: true quantity: type: integer x-nullable: true diff --git a/docs/en/user-guide/tips-tricks.md b/docs/en/user-guide/tips-tricks.md index 25265dbf..8b28839c 100644 --- a/docs/en/user-guide/tips-tricks.md +++ b/docs/en/user-guide/tips-tricks.md @@ -102,3 +102,11 @@ The copy to clipboard functionality requires a secure context (HTTPS or localhos To enable this feature: - Use HTTPS by setting up a reverse proxy (like Nginx or Caddy) - OR access Homebox through localhost + +## Open Multiple Items in New Tabs + +By default browsers prevent opening multiple tabs with one click, to allow for the `View Items` button to work you therefore need to enable a setting usually called `Allow pop-ups and redirects` or similar for the domain you're using Homebox on. + +- Chrome: [Block or allow pop-ups in Chrome](https://support.google.com/chrome/answer/95472?hl=en-GB&co=GENIE.Platform%3DDesktop#zippy=%2Callow-pop-ups-and-redirects-from-a-site) +- Firefox: [Pop-up blocker settings, exceptions and troubleshooting](https://support.mozilla.org/en-US/kb/pop-blocker-settings-exceptions-troubleshooting) +- Safari: [Block pop-up ads and windows in Safari](https://support.apple.com/en-gb/102524) diff --git a/frontend/components/Base/SectionHeader.vue b/frontend/components/Base/SectionHeader.vue index 10d9e144..fccdca8a 100644 --- a/frontend/components/Base/SectionHeader.vue +++ b/frontend/components/Base/SectionHeader.vue @@ -3,6 +3,7 @@ + diff --git a/frontend/components/Item/BarcodeModal.vue b/frontend/components/Item/BarcodeModal.vue index dc00febf..a489ca34 100644 --- a/frontend/components/Item/BarcodeModal.vue +++ b/frontend/components/Item/BarcodeModal.vue @@ -115,7 +115,6 @@ import MdiAlertCircleOutline from "~icons/mdi/alert-circle-outline"; import MdiBarcode from "~icons/mdi/barcode"; import MdiLoading from "~icons/mdi/loading"; - import type { TableData } from "~/components/Item/View/Table.types"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Separator } from "@/components/ui/separator"; @@ -225,7 +224,8 @@ } } - function extractValue(data: TableData, value: string) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + function extractValue(data: Record, value: string) { const parts = value.split("."); let current = data; for (const part of parts) { diff --git a/frontend/components/Item/Card.vue b/frontend/components/Item/Card.vue index b95be4b0..5becbecd 100644 --- a/frontend/components/Item/Card.vue +++ b/frontend/components/Item/Card.vue @@ -1,5 +1,13 @@