diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 29f45fb5..47e5ad51 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,2 @@ +open_collective: homebox github: [tankerkiller125,katosdev,tonyaellie] diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 7ae658b3..fe8a4f94 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -58,6 +58,17 @@ body: - Other validations: required: true + - type: dropdown + id: database + attributes: + label: Database Type + description: What database backend are you using? + multiple: false + options: + - SQLite + - PostgreSQL + validations: + required: true - type: dropdown id: arch attributes: 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/configure/index.md b/docs/en/configure/index.md index f1878b09..d3fc2cde 100644 --- a/docs/en/configure/index.md +++ b/docs/en/configure/index.md @@ -14,7 +14,7 @@ aside: false | HBOX_WEB_HOST | | host to run the web server on, if you're using docker do not change this. see below for examples | | HBOX_OPTIONS_ALLOW_REGISTRATION | true | allow users to register themselves | | HBOX_OPTIONS_AUTO_INCREMENT_ASSET_ID | true | auto-increments the asset_id field for new items | -| HBOX_OPTIONS_CURRENCY_CONFIG | | json configuration file containing additional currencie | +| HBOX_OPTIONS_CURRENCY_CONFIG | | json configuration file containing additional currencies | | HBOX_OPTIONS_ALLOW_ANALYTICS | false | Allows the homebox team to view extremely basic information about the system that your running on. This helps make decisions regarding builds and other general decisions. | | HBOX_WEB_MAX_UPLOAD_SIZE | 10 | maximum file upload size supported in MB | | HBOX_WEB_READ_TIMEOUT | 10s | Read timeout of HTTP sever | 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..c1ee68a9 100644 --- a/frontend/components/Item/Card.vue +++ b/frontend/components/Item/Card.vue @@ -1,8 +1,30 @@