Compare commits

...

16 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
1942b59eb3 Complete implementation of label parent/child relationships
Co-authored-by: tankerkiller125 <3457368+tankerkiller125@users.noreply.github.com>
2025-12-14 02:21:43 +00:00
copilot-swe-agent[bot]
bc7ca76ab5 Fix code review issues: migration comments and add missing translations
Co-authored-by: tankerkiller125 <3457368+tankerkiller125@users.noreply.github.com>
2025-12-14 02:19:19 +00:00
copilot-swe-agent[bot]
8463b70229 Add frontend support for label parent/child relationships
Co-authored-by: tankerkiller125 <3457368+tankerkiller125@users.noreply.github.com>
2025-12-14 02:16:49 +00:00
copilot-swe-agent[bot]
a23e0f5909 Update TypeScript types for label parent/child relationships
Co-authored-by: tankerkiller125 <3457368+tankerkiller125@users.noreply.github.com>
2025-12-14 02:14:23 +00:00
copilot-swe-agent[bot]
5da734c8f1 Generate swagger documentation for label parent/child features
Co-authored-by: tankerkiller125 <3457368+tankerkiller125@users.noreply.github.com>
2025-12-14 02:12:47 +00:00
copilot-swe-agent[bot]
422faffbe0 Add parent/child relationships for labels with tests
Co-authored-by: tankerkiller125 <3457368+tankerkiller125@users.noreply.github.com>
2025-12-14 02:11:32 +00:00
copilot-swe-agent[bot]
f6c0dc783c Initial plan for label parent/child relationships
Co-authored-by: tankerkiller125 <3457368+tankerkiller125@users.noreply.github.com>
2025-12-14 02:03:37 +00:00
copilot-swe-agent[bot]
e007b12a2d Initial plan 2025-12-14 01:56:59 +00:00
Sarun Nuntaviriyakul
2d1d3d927b Update log level options in configuration documentation (#1127) 2025-12-12 13:33:12 -05:00
Matthew Kilgore
540028a22e fix: broken docker.io attestation 2025-12-11 22:24:11 -05:00
Nelson Cabete
14b0d51894 Update docs to reference disable_https instead of disableSsl on Storage Configuration page (#1124)
Co-authored-by: Nelson Cabete <me@ncabete.com>
2025-12-09 20:56:05 -05:00
Matt
4334f926c0 Fix postgres nullable password migration to be at end 2025-12-09 14:44:53 -05:00
Robert Eggl
1088972ff0 docs: add missing barcode spider env var (#1114) 2025-12-08 20:17:45 -05:00
Matthew Kilgore
55e247ac71 Fix missing postgres OIDC migration 2025-12-08 20:10:36 -05:00
Matthew Kilgore
05a2700718 Merge remote-tracking branch 'origin/main' 2025-12-06 18:14:12 -05:00
Matthew Kilgore
06c11cdcd5 Ensure options are up to date in docs 2025-12-06 18:14:06 -05:00
37 changed files with 1631 additions and 134 deletions

View File

@@ -243,6 +243,6 @@ jobs:
uses: actions/attest-build-provenance@v1
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
with:
subject-name: ${{ env.DOCKERHUB_REPO }}
subject-name: docker.io/${{ env.DOCKERHUB_REPO }}
subject-digest: ${{ steps.push-dockerhub.outputs.digest }}
push-to-registry: true

View File

@@ -245,6 +245,6 @@ jobs:
uses: actions/attest-build-provenance@v1
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
with:
subject-name: ${{ env.DOCKERHUB_REPO }}
subject-name: docker.io/${{ env.DOCKERHUB_REPO }}
subject-digest: ${{ steps.push-dockerhub.outputs.digest }}
push-to-registry: true

View File

@@ -236,6 +236,6 @@ jobs:
uses: actions/attest-build-provenance@v1
if: (github.event_name == 'schedule' || startsWith(github.ref, 'refs/tags/'))
with:
subject-name: ${{ env.DOCKERHUB_REPO }}
subject-name: docker.io/${{ env.DOCKERHUB_REPO }}
subject-digest: ${{ steps.push-dockerhub.outputs.digest }}
push-to-registry: true

14
.scaffold/go.sum Normal file
View File

@@ -0,0 +1,14 @@
entgo.io/ent v0.14.5 h1:Rj2WOYJtCkWyFo6a+5wB3EfBRP0rnx1fMk6gGA0UUe4=
entgo.io/ent v0.14.5/go.mod h1:zTzLmWtPvGpmSwtkaayM2cm5m819NdM7z7tYPq3vN0U=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/sysadminsmedia/homebox/backend v0.0.0-20251212183312-2d1d3d927bfd h1:QULUJSgHc4rSlTjb2qYT6FIgwDWFCqEpnYqc/ltsrkk=
github.com/sysadminsmedia/homebox/backend v0.0.0-20251212183312-2d1d3d927bfd/go.mod h1:jB+tPmHtPDM1VnAjah0gvcRfP/s7c+rtQwpA8cvZD/U=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -3216,6 +3216,13 @@ const docTemplate = `{
"ent.LabelEdges": {
"type": "object",
"properties": {
"children": {
"description": "Children holds the value of the children edge.",
"type": "array",
"items": {
"$ref": "#/definitions/ent.Label"
}
},
"group": {
"description": "Group holds the value of the group edge.",
"allOf": [
@@ -3230,6 +3237,14 @@ const docTemplate = `{
"items": {
"$ref": "#/definitions/ent.Item"
}
},
"parent": {
"description": "Parent holds the value of the parent edge.",
"allOf": [
{
"$ref": "#/definitions/ent.Label"
}
]
}
}
},
@@ -4437,12 +4452,22 @@ const docTemplate = `{
"type": "string",
"maxLength": 255,
"minLength": 1
},
"parentId": {
"type": "string",
"x-nullable": true
}
}
},
"repo.LabelOut": {
"type": "object",
"properties": {
"children": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.LabelSummary"
}
},
"color": {
"type": "string"
},
@@ -4458,6 +4483,9 @@ const docTemplate = `{
"name": {
"type": "string"
},
"parent": {
"$ref": "#/definitions/repo.LabelSummary"
},
"updatedAt": {
"type": "string"
}

View File

@@ -3413,6 +3413,13 @@
"ent.LabelEdges": {
"type": "object",
"properties": {
"children": {
"description": "Children holds the value of the children edge.",
"type": "array",
"items": {
"$ref": "#/components/schemas/ent.Label"
}
},
"group": {
"description": "Group holds the value of the group edge.",
"allOf": [
@@ -3427,6 +3434,14 @@
"items": {
"$ref": "#/components/schemas/ent.Item"
}
},
"parent": {
"description": "Parent holds the value of the parent edge.",
"allOf": [
{
"$ref": "#/components/schemas/ent.Label"
}
]
}
}
},
@@ -4634,12 +4649,22 @@
"type": "string",
"maxLength": 255,
"minLength": 1
},
"parentId": {
"type": "string",
"nullable": true
}
}
},
"repo.LabelOut": {
"type": "object",
"properties": {
"children": {
"type": "array",
"items": {
"$ref": "#/components/schemas/repo.LabelSummary"
}
},
"color": {
"type": "string"
},
@@ -4655,6 +4680,9 @@
"name": {
"type": "string"
},
"parent": {
"$ref": "#/components/schemas/repo.LabelSummary"
},
"updatedAt": {
"type": "string"
}

View File

@@ -2112,6 +2112,11 @@ components:
ent.LabelEdges:
type: object
properties:
children:
description: Children holds the value of the children edge.
type: array
items:
$ref: "#/components/schemas/ent.Label"
group:
description: Group holds the value of the group edge.
allOf:
@@ -2121,6 +2126,10 @@ components:
type: array
items:
$ref: "#/components/schemas/ent.Item"
parent:
description: Parent holds the value of the parent edge.
allOf:
- $ref: "#/components/schemas/ent.Label"
ent.Location:
type: object
properties:
@@ -2956,9 +2965,16 @@ components:
type: string
maxLength: 255
minLength: 1
parentId:
type: string
nullable: true
repo.LabelOut:
type: object
properties:
children:
type: array
items:
$ref: "#/components/schemas/repo.LabelSummary"
color:
type: string
createdAt:
@@ -2969,6 +2985,8 @@ components:
type: string
name:
type: string
parent:
$ref: "#/components/schemas/repo.LabelSummary"
updatedAt:
type: string
repo.LabelSummary:

View File

@@ -3214,6 +3214,13 @@
"ent.LabelEdges": {
"type": "object",
"properties": {
"children": {
"description": "Children holds the value of the children edge.",
"type": "array",
"items": {
"$ref": "#/definitions/ent.Label"
}
},
"group": {
"description": "Group holds the value of the group edge.",
"allOf": [
@@ -3228,6 +3235,14 @@
"items": {
"$ref": "#/definitions/ent.Item"
}
},
"parent": {
"description": "Parent holds the value of the parent edge.",
"allOf": [
{
"$ref": "#/definitions/ent.Label"
}
]
}
}
},
@@ -4435,12 +4450,22 @@
"type": "string",
"maxLength": 255,
"minLength": 1
},
"parentId": {
"type": "string",
"x-nullable": true
}
}
},
"repo.LabelOut": {
"type": "object",
"properties": {
"children": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.LabelSummary"
}
},
"color": {
"type": "string"
},
@@ -4456,6 +4481,9 @@
"name": {
"type": "string"
},
"parent": {
"$ref": "#/definitions/repo.LabelSummary"
},
"updatedAt": {
"type": "string"
}

View File

@@ -534,6 +534,11 @@ definitions:
type: object
ent.LabelEdges:
properties:
children:
description: Children holds the value of the children edge.
items:
$ref: '#/definitions/ent.Label'
type: array
group:
allOf:
- $ref: '#/definitions/ent.Group'
@@ -543,6 +548,10 @@ definitions:
items:
$ref: '#/definitions/ent.Item'
type: array
parent:
allOf:
- $ref: '#/definitions/ent.Label'
description: Parent holds the value of the parent edge.
type: object
ent.Location:
properties:
@@ -1371,11 +1380,18 @@ definitions:
maxLength: 255
minLength: 1
type: string
parentId:
type: string
x-nullable: true
required:
- name
type: object
repo.LabelOut:
properties:
children:
items:
$ref: '#/definitions/repo.LabelSummary'
type: array
color:
type: string
createdAt:
@@ -1386,6 +1402,8 @@ definitions:
type: string
name:
type: string
parent:
$ref: '#/definitions/repo.LabelSummary'
updatedAt:
type: string
type: object

View File

@@ -325,8 +325,6 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/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.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
@@ -349,8 +347,6 @@ github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOF
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/olahol/melody v1.4.0 h1:Pa5SdeZL/zXPi1tJuMAPDbl4n3gQOThSL6G1p4qZ4SI=
github.com/olahol/melody v1.4.0/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/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU=
github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
@@ -393,10 +389,6 @@ github.com/shirou/gopsutil/v4 v4.25.11 h1:X53gB7muL9Gnwwo2evPSE+SfOrltMoR6V3xJAX
github.com/shirou/gopsutil/v4 v4.25.11/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
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/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=

View File

@@ -1902,6 +1902,38 @@ func (c *LabelClient) QueryGroup(_m *Label) *GroupQuery {
return query
}
// QueryParent queries the parent edge of a Label.
func (c *LabelClient) QueryParent(_m *Label) *LabelQuery {
query := (&LabelClient{config: c.config}).Query()
query.path = func(context.Context) (fromV *sql.Selector, _ error) {
id := _m.ID
step := sqlgraph.NewStep(
sqlgraph.From(label.Table, label.FieldID, id),
sqlgraph.To(label.Table, label.FieldID),
sqlgraph.Edge(sqlgraph.M2O, true, label.ParentTable, label.ParentColumn),
)
fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step)
return fromV, nil
}
return query
}
// QueryChildren queries the children edge of a Label.
func (c *LabelClient) QueryChildren(_m *Label) *LabelQuery {
query := (&LabelClient{config: c.config}).Query()
query.path = func(context.Context) (fromV *sql.Selector, _ error) {
id := _m.ID
step := sqlgraph.NewStep(
sqlgraph.From(label.Table, label.FieldID, id),
sqlgraph.To(label.Table, label.FieldID),
sqlgraph.Edge(sqlgraph.O2M, false, label.ChildrenTable, label.ChildrenColumn),
)
fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step)
return fromV, nil
}
return query
}
// QueryItems queries the items edge of a Label.
func (c *LabelClient) QueryItems(_m *Label) *ItemQuery {
query := (&ItemClient{config: c.config}).Query()

View File

@@ -31,20 +31,25 @@ type Label struct {
Color string `json:"color,omitempty"`
// Edges holds the relations/edges for other nodes in the graph.
// The values are being populated by the LabelQuery when eager-loading is set.
Edges LabelEdges `json:"edges"`
group_labels *uuid.UUID
selectValues sql.SelectValues
Edges LabelEdges `json:"edges"`
group_labels *uuid.UUID
label_children *uuid.UUID
selectValues sql.SelectValues
}
// LabelEdges holds the relations/edges for other nodes in the graph.
type LabelEdges struct {
// Group holds the value of the group edge.
Group *Group `json:"group,omitempty"`
// Parent holds the value of the parent edge.
Parent *Label `json:"parent,omitempty"`
// Children holds the value of the children edge.
Children []*Label `json:"children,omitempty"`
// Items holds the value of the items edge.
Items []*Item `json:"items,omitempty"`
// loadedTypes holds the information for reporting if a
// type was loaded (or requested) in eager-loading or not.
loadedTypes [2]bool
loadedTypes [4]bool
}
// GroupOrErr returns the Group value or an error if the edge
@@ -58,10 +63,30 @@ func (e LabelEdges) GroupOrErr() (*Group, error) {
return nil, &NotLoadedError{edge: "group"}
}
// ParentOrErr returns the Parent value or an error if the edge
// was not loaded in eager-loading, or loaded but was not found.
func (e LabelEdges) ParentOrErr() (*Label, error) {
if e.Parent != nil {
return e.Parent, nil
} else if e.loadedTypes[1] {
return nil, &NotFoundError{label: label.Label}
}
return nil, &NotLoadedError{edge: "parent"}
}
// ChildrenOrErr returns the Children value or an error if the edge
// was not loaded in eager-loading.
func (e LabelEdges) ChildrenOrErr() ([]*Label, error) {
if e.loadedTypes[2] {
return e.Children, nil
}
return nil, &NotLoadedError{edge: "children"}
}
// ItemsOrErr returns the Items value or an error if the edge
// was not loaded in eager-loading.
func (e LabelEdges) ItemsOrErr() ([]*Item, error) {
if e.loadedTypes[1] {
if e.loadedTypes[3] {
return e.Items, nil
}
return nil, &NotLoadedError{edge: "items"}
@@ -80,6 +105,8 @@ func (*Label) scanValues(columns []string) ([]any, error) {
values[i] = new(uuid.UUID)
case label.ForeignKeys[0]: // group_labels
values[i] = &sql.NullScanner{S: new(uuid.UUID)}
case label.ForeignKeys[1]: // label_children
values[i] = &sql.NullScanner{S: new(uuid.UUID)}
default:
values[i] = new(sql.UnknownType)
}
@@ -138,6 +165,13 @@ func (_m *Label) assignValues(columns []string, values []any) error {
_m.group_labels = new(uuid.UUID)
*_m.group_labels = *value.S.(*uuid.UUID)
}
case label.ForeignKeys[1]:
if value, ok := values[i].(*sql.NullScanner); !ok {
return fmt.Errorf("unexpected type %T for field label_children", values[i])
} else if value.Valid {
_m.label_children = new(uuid.UUID)
*_m.label_children = *value.S.(*uuid.UUID)
}
default:
_m.selectValues.Set(columns[i], values[i])
}
@@ -156,6 +190,16 @@ func (_m *Label) QueryGroup() *GroupQuery {
return NewLabelClient(_m.config).QueryGroup(_m)
}
// QueryParent queries the "parent" edge of the Label entity.
func (_m *Label) QueryParent() *LabelQuery {
return NewLabelClient(_m.config).QueryParent(_m)
}
// QueryChildren queries the "children" edge of the Label entity.
func (_m *Label) QueryChildren() *LabelQuery {
return NewLabelClient(_m.config).QueryChildren(_m)
}
// QueryItems queries the "items" edge of the Label entity.
func (_m *Label) QueryItems() *ItemQuery {
return NewLabelClient(_m.config).QueryItems(_m)

View File

@@ -27,6 +27,10 @@ const (
FieldColor = "color"
// EdgeGroup holds the string denoting the group edge name in mutations.
EdgeGroup = "group"
// EdgeParent holds the string denoting the parent edge name in mutations.
EdgeParent = "parent"
// EdgeChildren holds the string denoting the children edge name in mutations.
EdgeChildren = "children"
// EdgeItems holds the string denoting the items edge name in mutations.
EdgeItems = "items"
// Table holds the table name of the label in the database.
@@ -38,6 +42,14 @@ const (
GroupInverseTable = "groups"
// GroupColumn is the table column denoting the group relation/edge.
GroupColumn = "group_labels"
// ParentTable is the table that holds the parent relation/edge.
ParentTable = "labels"
// ParentColumn is the table column denoting the parent relation/edge.
ParentColumn = "label_children"
// ChildrenTable is the table that holds the children relation/edge.
ChildrenTable = "labels"
// ChildrenColumn is the table column denoting the children relation/edge.
ChildrenColumn = "label_children"
// ItemsTable is the table that holds the items relation/edge. The primary key declared below.
ItemsTable = "label_items"
// ItemsInverseTable is the table name for the Item entity.
@@ -59,6 +71,7 @@ var Columns = []string{
// table and are not defined as standalone fields in the schema.
var ForeignKeys = []string{
"group_labels",
"label_children",
}
var (
@@ -139,6 +152,27 @@ func ByGroupField(field string, opts ...sql.OrderTermOption) OrderOption {
}
}
// ByParentField orders the results by parent field.
func ByParentField(field string, opts ...sql.OrderTermOption) OrderOption {
return func(s *sql.Selector) {
sqlgraph.OrderByNeighborTerms(s, newParentStep(), sql.OrderByField(field, opts...))
}
}
// ByChildrenCount orders the results by children count.
func ByChildrenCount(opts ...sql.OrderTermOption) OrderOption {
return func(s *sql.Selector) {
sqlgraph.OrderByNeighborsCount(s, newChildrenStep(), opts...)
}
}
// ByChildren orders the results by children terms.
func ByChildren(term sql.OrderTerm, terms ...sql.OrderTerm) OrderOption {
return func(s *sql.Selector) {
sqlgraph.OrderByNeighborTerms(s, newChildrenStep(), append([]sql.OrderTerm{term}, terms...)...)
}
}
// ByItemsCount orders the results by items count.
func ByItemsCount(opts ...sql.OrderTermOption) OrderOption {
return func(s *sql.Selector) {
@@ -159,6 +193,20 @@ func newGroupStep() *sqlgraph.Step {
sqlgraph.Edge(sqlgraph.M2O, true, GroupTable, GroupColumn),
)
}
func newParentStep() *sqlgraph.Step {
return sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.To(Table, FieldID),
sqlgraph.Edge(sqlgraph.M2O, true, ParentTable, ParentColumn),
)
}
func newChildrenStep() *sqlgraph.Step {
return sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.To(Table, FieldID),
sqlgraph.Edge(sqlgraph.O2M, false, ChildrenTable, ChildrenColumn),
)
}
func newItemsStep() *sqlgraph.Step {
return sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),

View File

@@ -399,6 +399,52 @@ func HasGroupWith(preds ...predicate.Group) predicate.Label {
})
}
// HasParent applies the HasEdge predicate on the "parent" edge.
func HasParent() predicate.Label {
return predicate.Label(func(s *sql.Selector) {
step := sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.Edge(sqlgraph.M2O, true, ParentTable, ParentColumn),
)
sqlgraph.HasNeighbors(s, step)
})
}
// HasParentWith applies the HasEdge predicate on the "parent" edge with a given conditions (other predicates).
func HasParentWith(preds ...predicate.Label) predicate.Label {
return predicate.Label(func(s *sql.Selector) {
step := newParentStep()
sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) {
for _, p := range preds {
p(s)
}
})
})
}
// HasChildren applies the HasEdge predicate on the "children" edge.
func HasChildren() predicate.Label {
return predicate.Label(func(s *sql.Selector) {
step := sqlgraph.NewStep(
sqlgraph.From(Table, FieldID),
sqlgraph.Edge(sqlgraph.O2M, false, ChildrenTable, ChildrenColumn),
)
sqlgraph.HasNeighbors(s, step)
})
}
// HasChildrenWith applies the HasEdge predicate on the "children" edge with a given conditions (other predicates).
func HasChildrenWith(preds ...predicate.Label) predicate.Label {
return predicate.Label(func(s *sql.Selector) {
step := newChildrenStep()
sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) {
for _, p := range preds {
p(s)
}
})
})
}
// HasItems applies the HasEdge predicate on the "items" edge.
func HasItems() predicate.Label {
return predicate.Label(func(s *sql.Selector) {

View File

@@ -110,6 +110,40 @@ func (_c *LabelCreate) SetGroup(v *Group) *LabelCreate {
return _c.SetGroupID(v.ID)
}
// SetParentID sets the "parent" edge to the Label entity by ID.
func (_c *LabelCreate) SetParentID(id uuid.UUID) *LabelCreate {
_c.mutation.SetParentID(id)
return _c
}
// SetNillableParentID sets the "parent" edge to the Label entity by ID if the given value is not nil.
func (_c *LabelCreate) SetNillableParentID(id *uuid.UUID) *LabelCreate {
if id != nil {
_c = _c.SetParentID(*id)
}
return _c
}
// SetParent sets the "parent" edge to the Label entity.
func (_c *LabelCreate) SetParent(v *Label) *LabelCreate {
return _c.SetParentID(v.ID)
}
// AddChildIDs adds the "children" edge to the Label entity by IDs.
func (_c *LabelCreate) AddChildIDs(ids ...uuid.UUID) *LabelCreate {
_c.mutation.AddChildIDs(ids...)
return _c
}
// AddChildren adds the "children" edges to the Label entity.
func (_c *LabelCreate) AddChildren(v ...*Label) *LabelCreate {
ids := make([]uuid.UUID, len(v))
for i := range v {
ids[i] = v[i].ID
}
return _c.AddChildIDs(ids...)
}
// AddItemIDs adds the "items" edge to the Item entity by IDs.
func (_c *LabelCreate) AddItemIDs(ids ...uuid.UUID) *LabelCreate {
_c.mutation.AddItemIDs(ids...)
@@ -275,6 +309,39 @@ func (_c *LabelCreate) createSpec() (*Label, *sqlgraph.CreateSpec) {
_node.group_labels = &nodes[0]
_spec.Edges = append(_spec.Edges, edge)
}
if nodes := _c.mutation.ParentIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
Inverse: true,
Table: label.ParentTable,
Columns: []string{label.ParentColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(label.FieldID, field.TypeUUID),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_node.label_children = &nodes[0]
_spec.Edges = append(_spec.Edges, edge)
}
if nodes := _c.mutation.ChildrenIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: false,
Table: label.ChildrenTable,
Columns: []string{label.ChildrenColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(label.FieldID, field.TypeUUID),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges = append(_spec.Edges, edge)
}
if nodes := _c.mutation.ItemsIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2M,

View File

@@ -22,13 +22,15 @@ import (
// LabelQuery is the builder for querying Label entities.
type LabelQuery struct {
config
ctx *QueryContext
order []label.OrderOption
inters []Interceptor
predicates []predicate.Label
withGroup *GroupQuery
withItems *ItemQuery
withFKs bool
ctx *QueryContext
order []label.OrderOption
inters []Interceptor
predicates []predicate.Label
withGroup *GroupQuery
withParent *LabelQuery
withChildren *LabelQuery
withItems *ItemQuery
withFKs bool
// intermediate query (i.e. traversal path).
sql *sql.Selector
path func(context.Context) (*sql.Selector, error)
@@ -87,6 +89,50 @@ func (_q *LabelQuery) QueryGroup() *GroupQuery {
return query
}
// QueryParent chains the current query on the "parent" edge.
func (_q *LabelQuery) QueryParent() *LabelQuery {
query := (&LabelClient{config: _q.config}).Query()
query.path = func(ctx context.Context) (fromU *sql.Selector, err error) {
if err := _q.prepareQuery(ctx); err != nil {
return nil, err
}
selector := _q.sqlQuery(ctx)
if err := selector.Err(); err != nil {
return nil, err
}
step := sqlgraph.NewStep(
sqlgraph.From(label.Table, label.FieldID, selector),
sqlgraph.To(label.Table, label.FieldID),
sqlgraph.Edge(sqlgraph.M2O, true, label.ParentTable, label.ParentColumn),
)
fromU = sqlgraph.SetNeighbors(_q.driver.Dialect(), step)
return fromU, nil
}
return query
}
// QueryChildren chains the current query on the "children" edge.
func (_q *LabelQuery) QueryChildren() *LabelQuery {
query := (&LabelClient{config: _q.config}).Query()
query.path = func(ctx context.Context) (fromU *sql.Selector, err error) {
if err := _q.prepareQuery(ctx); err != nil {
return nil, err
}
selector := _q.sqlQuery(ctx)
if err := selector.Err(); err != nil {
return nil, err
}
step := sqlgraph.NewStep(
sqlgraph.From(label.Table, label.FieldID, selector),
sqlgraph.To(label.Table, label.FieldID),
sqlgraph.Edge(sqlgraph.O2M, false, label.ChildrenTable, label.ChildrenColumn),
)
fromU = sqlgraph.SetNeighbors(_q.driver.Dialect(), step)
return fromU, nil
}
return query
}
// QueryItems chains the current query on the "items" edge.
func (_q *LabelQuery) QueryItems() *ItemQuery {
query := (&ItemClient{config: _q.config}).Query()
@@ -296,13 +342,15 @@ func (_q *LabelQuery) Clone() *LabelQuery {
return nil
}
return &LabelQuery{
config: _q.config,
ctx: _q.ctx.Clone(),
order: append([]label.OrderOption{}, _q.order...),
inters: append([]Interceptor{}, _q.inters...),
predicates: append([]predicate.Label{}, _q.predicates...),
withGroup: _q.withGroup.Clone(),
withItems: _q.withItems.Clone(),
config: _q.config,
ctx: _q.ctx.Clone(),
order: append([]label.OrderOption{}, _q.order...),
inters: append([]Interceptor{}, _q.inters...),
predicates: append([]predicate.Label{}, _q.predicates...),
withGroup: _q.withGroup.Clone(),
withParent: _q.withParent.Clone(),
withChildren: _q.withChildren.Clone(),
withItems: _q.withItems.Clone(),
// clone intermediate query.
sql: _q.sql.Clone(),
path: _q.path,
@@ -320,6 +368,28 @@ func (_q *LabelQuery) WithGroup(opts ...func(*GroupQuery)) *LabelQuery {
return _q
}
// WithParent tells the query-builder to eager-load the nodes that are connected to
// the "parent" edge. The optional arguments are used to configure the query builder of the edge.
func (_q *LabelQuery) WithParent(opts ...func(*LabelQuery)) *LabelQuery {
query := (&LabelClient{config: _q.config}).Query()
for _, opt := range opts {
opt(query)
}
_q.withParent = query
return _q
}
// WithChildren tells the query-builder to eager-load the nodes that are connected to
// the "children" edge. The optional arguments are used to configure the query builder of the edge.
func (_q *LabelQuery) WithChildren(opts ...func(*LabelQuery)) *LabelQuery {
query := (&LabelClient{config: _q.config}).Query()
for _, opt := range opts {
opt(query)
}
_q.withChildren = query
return _q
}
// WithItems tells the query-builder to eager-load the nodes that are connected to
// the "items" edge. The optional arguments are used to configure the query builder of the edge.
func (_q *LabelQuery) WithItems(opts ...func(*ItemQuery)) *LabelQuery {
@@ -410,12 +480,14 @@ func (_q *LabelQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Label,
nodes = []*Label{}
withFKs = _q.withFKs
_spec = _q.querySpec()
loadedTypes = [2]bool{
loadedTypes = [4]bool{
_q.withGroup != nil,
_q.withParent != nil,
_q.withChildren != nil,
_q.withItems != nil,
}
)
if _q.withGroup != nil {
if _q.withGroup != nil || _q.withParent != nil {
withFKs = true
}
if withFKs {
@@ -445,6 +517,19 @@ func (_q *LabelQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Label,
return nil, err
}
}
if query := _q.withParent; query != nil {
if err := _q.loadParent(ctx, query, nodes, nil,
func(n *Label, e *Label) { n.Edges.Parent = e }); err != nil {
return nil, err
}
}
if query := _q.withChildren; query != nil {
if err := _q.loadChildren(ctx, query, nodes,
func(n *Label) { n.Edges.Children = []*Label{} },
func(n *Label, e *Label) { n.Edges.Children = append(n.Edges.Children, e) }); err != nil {
return nil, err
}
}
if query := _q.withItems; query != nil {
if err := _q.loadItems(ctx, query, nodes,
func(n *Label) { n.Edges.Items = []*Item{} },
@@ -487,6 +572,69 @@ func (_q *LabelQuery) loadGroup(ctx context.Context, query *GroupQuery, nodes []
}
return nil
}
func (_q *LabelQuery) loadParent(ctx context.Context, query *LabelQuery, nodes []*Label, init func(*Label), assign func(*Label, *Label)) error {
ids := make([]uuid.UUID, 0, len(nodes))
nodeids := make(map[uuid.UUID][]*Label)
for i := range nodes {
if nodes[i].label_children == nil {
continue
}
fk := *nodes[i].label_children
if _, ok := nodeids[fk]; !ok {
ids = append(ids, fk)
}
nodeids[fk] = append(nodeids[fk], nodes[i])
}
if len(ids) == 0 {
return nil
}
query.Where(label.IDIn(ids...))
neighbors, err := query.All(ctx)
if err != nil {
return err
}
for _, n := range neighbors {
nodes, ok := nodeids[n.ID]
if !ok {
return fmt.Errorf(`unexpected foreign-key "label_children" returned %v`, n.ID)
}
for i := range nodes {
assign(nodes[i], n)
}
}
return nil
}
func (_q *LabelQuery) loadChildren(ctx context.Context, query *LabelQuery, nodes []*Label, init func(*Label), assign func(*Label, *Label)) error {
fks := make([]driver.Value, 0, len(nodes))
nodeids := make(map[uuid.UUID]*Label)
for i := range nodes {
fks = append(fks, nodes[i].ID)
nodeids[nodes[i].ID] = nodes[i]
if init != nil {
init(nodes[i])
}
}
query.withFKs = true
query.Where(predicate.Label(func(s *sql.Selector) {
s.Where(sql.InValues(s.C(label.ChildrenColumn), fks...))
}))
neighbors, err := query.All(ctx)
if err != nil {
return err
}
for _, n := range neighbors {
fk := n.label_children
if fk == nil {
return fmt.Errorf(`foreign-key "label_children" is nil for node %v`, n.ID)
}
node, ok := nodeids[*fk]
if !ok {
return fmt.Errorf(`unexpected referenced foreign-key "label_children" returned %v for node %v`, *fk, n.ID)
}
assign(node, n)
}
return nil
}
func (_q *LabelQuery) loadItems(ctx context.Context, query *ItemQuery, nodes []*Label, init func(*Label), assign func(*Label, *Item)) error {
edgeIDs := make([]driver.Value, len(nodes))
byID := make(map[uuid.UUID]*Label)

View File

@@ -102,6 +102,40 @@ func (_u *LabelUpdate) SetGroup(v *Group) *LabelUpdate {
return _u.SetGroupID(v.ID)
}
// SetParentID sets the "parent" edge to the Label entity by ID.
func (_u *LabelUpdate) SetParentID(id uuid.UUID) *LabelUpdate {
_u.mutation.SetParentID(id)
return _u
}
// SetNillableParentID sets the "parent" edge to the Label entity by ID if the given value is not nil.
func (_u *LabelUpdate) SetNillableParentID(id *uuid.UUID) *LabelUpdate {
if id != nil {
_u = _u.SetParentID(*id)
}
return _u
}
// SetParent sets the "parent" edge to the Label entity.
func (_u *LabelUpdate) SetParent(v *Label) *LabelUpdate {
return _u.SetParentID(v.ID)
}
// AddChildIDs adds the "children" edge to the Label entity by IDs.
func (_u *LabelUpdate) AddChildIDs(ids ...uuid.UUID) *LabelUpdate {
_u.mutation.AddChildIDs(ids...)
return _u
}
// AddChildren adds the "children" edges to the Label entity.
func (_u *LabelUpdate) AddChildren(v ...*Label) *LabelUpdate {
ids := make([]uuid.UUID, len(v))
for i := range v {
ids[i] = v[i].ID
}
return _u.AddChildIDs(ids...)
}
// AddItemIDs adds the "items" edge to the Item entity by IDs.
func (_u *LabelUpdate) AddItemIDs(ids ...uuid.UUID) *LabelUpdate {
_u.mutation.AddItemIDs(ids...)
@@ -128,6 +162,33 @@ func (_u *LabelUpdate) ClearGroup() *LabelUpdate {
return _u
}
// ClearParent clears the "parent" edge to the Label entity.
func (_u *LabelUpdate) ClearParent() *LabelUpdate {
_u.mutation.ClearParent()
return _u
}
// ClearChildren clears all "children" edges to the Label entity.
func (_u *LabelUpdate) ClearChildren() *LabelUpdate {
_u.mutation.ClearChildren()
return _u
}
// RemoveChildIDs removes the "children" edge to Label entities by IDs.
func (_u *LabelUpdate) RemoveChildIDs(ids ...uuid.UUID) *LabelUpdate {
_u.mutation.RemoveChildIDs(ids...)
return _u
}
// RemoveChildren removes "children" edges to Label entities.
func (_u *LabelUpdate) RemoveChildren(v ...*Label) *LabelUpdate {
ids := make([]uuid.UUID, len(v))
for i := range v {
ids[i] = v[i].ID
}
return _u.RemoveChildIDs(ids...)
}
// ClearItems clears all "items" edges to the Item entity.
func (_u *LabelUpdate) ClearItems() *LabelUpdate {
_u.mutation.ClearItems()
@@ -267,6 +328,80 @@ func (_u *LabelUpdate) sqlSave(ctx context.Context) (_node int, err error) {
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
if _u.mutation.ParentCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
Inverse: true,
Table: label.ParentTable,
Columns: []string{label.ParentColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(label.FieldID, field.TypeUUID),
},
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := _u.mutation.ParentIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
Inverse: true,
Table: label.ParentTable,
Columns: []string{label.ParentColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(label.FieldID, field.TypeUUID),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
if _u.mutation.ChildrenCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: false,
Table: label.ChildrenTable,
Columns: []string{label.ChildrenColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(label.FieldID, field.TypeUUID),
},
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := _u.mutation.RemovedChildrenIDs(); len(nodes) > 0 && !_u.mutation.ChildrenCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: false,
Table: label.ChildrenTable,
Columns: []string{label.ChildrenColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(label.FieldID, field.TypeUUID),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := _u.mutation.ChildrenIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: false,
Table: label.ChildrenTable,
Columns: []string{label.ChildrenColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(label.FieldID, field.TypeUUID),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
if _u.mutation.ItemsCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2M,
@@ -403,6 +538,40 @@ func (_u *LabelUpdateOne) SetGroup(v *Group) *LabelUpdateOne {
return _u.SetGroupID(v.ID)
}
// SetParentID sets the "parent" edge to the Label entity by ID.
func (_u *LabelUpdateOne) SetParentID(id uuid.UUID) *LabelUpdateOne {
_u.mutation.SetParentID(id)
return _u
}
// SetNillableParentID sets the "parent" edge to the Label entity by ID if the given value is not nil.
func (_u *LabelUpdateOne) SetNillableParentID(id *uuid.UUID) *LabelUpdateOne {
if id != nil {
_u = _u.SetParentID(*id)
}
return _u
}
// SetParent sets the "parent" edge to the Label entity.
func (_u *LabelUpdateOne) SetParent(v *Label) *LabelUpdateOne {
return _u.SetParentID(v.ID)
}
// AddChildIDs adds the "children" edge to the Label entity by IDs.
func (_u *LabelUpdateOne) AddChildIDs(ids ...uuid.UUID) *LabelUpdateOne {
_u.mutation.AddChildIDs(ids...)
return _u
}
// AddChildren adds the "children" edges to the Label entity.
func (_u *LabelUpdateOne) AddChildren(v ...*Label) *LabelUpdateOne {
ids := make([]uuid.UUID, len(v))
for i := range v {
ids[i] = v[i].ID
}
return _u.AddChildIDs(ids...)
}
// AddItemIDs adds the "items" edge to the Item entity by IDs.
func (_u *LabelUpdateOne) AddItemIDs(ids ...uuid.UUID) *LabelUpdateOne {
_u.mutation.AddItemIDs(ids...)
@@ -429,6 +598,33 @@ func (_u *LabelUpdateOne) ClearGroup() *LabelUpdateOne {
return _u
}
// ClearParent clears the "parent" edge to the Label entity.
func (_u *LabelUpdateOne) ClearParent() *LabelUpdateOne {
_u.mutation.ClearParent()
return _u
}
// ClearChildren clears all "children" edges to the Label entity.
func (_u *LabelUpdateOne) ClearChildren() *LabelUpdateOne {
_u.mutation.ClearChildren()
return _u
}
// RemoveChildIDs removes the "children" edge to Label entities by IDs.
func (_u *LabelUpdateOne) RemoveChildIDs(ids ...uuid.UUID) *LabelUpdateOne {
_u.mutation.RemoveChildIDs(ids...)
return _u
}
// RemoveChildren removes "children" edges to Label entities.
func (_u *LabelUpdateOne) RemoveChildren(v ...*Label) *LabelUpdateOne {
ids := make([]uuid.UUID, len(v))
for i := range v {
ids[i] = v[i].ID
}
return _u.RemoveChildIDs(ids...)
}
// ClearItems clears all "items" edges to the Item entity.
func (_u *LabelUpdateOne) ClearItems() *LabelUpdateOne {
_u.mutation.ClearItems()
@@ -598,6 +794,80 @@ func (_u *LabelUpdateOne) sqlSave(ctx context.Context) (_node *Label, err error)
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
if _u.mutation.ParentCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
Inverse: true,
Table: label.ParentTable,
Columns: []string{label.ParentColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(label.FieldID, field.TypeUUID),
},
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := _u.mutation.ParentIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2O,
Inverse: true,
Table: label.ParentTable,
Columns: []string{label.ParentColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(label.FieldID, field.TypeUUID),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
if _u.mutation.ChildrenCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: false,
Table: label.ChildrenTable,
Columns: []string{label.ChildrenColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(label.FieldID, field.TypeUUID),
},
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := _u.mutation.RemovedChildrenIDs(); len(nodes) > 0 && !_u.mutation.ChildrenCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: false,
Table: label.ChildrenTable,
Columns: []string{label.ChildrenColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(label.FieldID, field.TypeUUID),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Clear = append(_spec.Edges.Clear, edge)
}
if nodes := _u.mutation.ChildrenIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
Inverse: false,
Table: label.ChildrenTable,
Columns: []string{label.ChildrenColumn},
Bidi: false,
Target: &sqlgraph.EdgeTarget{
IDSpec: sqlgraph.NewFieldSpec(label.FieldID, field.TypeUUID),
},
}
for _, k := range nodes {
edge.Target.Nodes = append(edge.Target.Nodes, k)
}
_spec.Edges.Add = append(_spec.Edges.Add, edge)
}
if _u.mutation.ItemsCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.M2M,

View File

@@ -305,6 +305,7 @@ var (
{Name: "description", Type: field.TypeString, Nullable: true, Size: 1000},
{Name: "color", Type: field.TypeString, Nullable: true, Size: 255},
{Name: "group_labels", Type: field.TypeUUID},
{Name: "label_children", Type: field.TypeUUID, Nullable: true},
}
// LabelsTable holds the schema information for the "labels" table.
LabelsTable = &schema.Table{
@@ -318,6 +319,12 @@ var (
RefColumns: []*schema.Column{GroupsColumns[0]},
OnDelete: schema.Cascade,
},
{
Symbol: "labels_labels_children",
Columns: []*schema.Column{LabelsColumns[7]},
RefColumns: []*schema.Column{LabelsColumns[0]},
OnDelete: schema.Cascade,
},
},
}
// LocationsColumns holds the columns for the "locations" table.
@@ -549,6 +556,7 @@ func init() {
ItemTemplatesTable.ForeignKeys[0].RefTable = GroupsTable
ItemTemplatesTable.ForeignKeys[1].RefTable = LocationsTable
LabelsTable.ForeignKeys[0].RefTable = GroupsTable
LabelsTable.ForeignKeys[1].RefTable = LabelsTable
LocationsTable.ForeignKeys[0].RefTable = GroupsTable
LocationsTable.ForeignKeys[1].RefTable = LocationsTable
MaintenanceEntriesTable.ForeignKeys[0].RefTable = ItemsTable

View File

@@ -8692,23 +8692,28 @@ func (m *ItemTemplateMutation) ResetEdge(name string) error {
// LabelMutation represents an operation that mutates the Label nodes in the graph.
type LabelMutation struct {
config
op Op
typ string
id *uuid.UUID
created_at *time.Time
updated_at *time.Time
name *string
description *string
color *string
clearedFields map[string]struct{}
group *uuid.UUID
clearedgroup bool
items map[uuid.UUID]struct{}
removeditems map[uuid.UUID]struct{}
cleareditems bool
done bool
oldValue func(context.Context) (*Label, error)
predicates []predicate.Label
op Op
typ string
id *uuid.UUID
created_at *time.Time
updated_at *time.Time
name *string
description *string
color *string
clearedFields map[string]struct{}
group *uuid.UUID
clearedgroup bool
parent *uuid.UUID
clearedparent bool
children map[uuid.UUID]struct{}
removedchildren map[uuid.UUID]struct{}
clearedchildren bool
items map[uuid.UUID]struct{}
removeditems map[uuid.UUID]struct{}
cleareditems bool
done bool
oldValue func(context.Context) (*Label, error)
predicates []predicate.Label
}
var _ ent.Mutation = (*LabelMutation)(nil)
@@ -9060,6 +9065,99 @@ func (m *LabelMutation) ResetGroup() {
m.clearedgroup = false
}
// SetParentID sets the "parent" edge to the Label entity by id.
func (m *LabelMutation) SetParentID(id uuid.UUID) {
m.parent = &id
}
// ClearParent clears the "parent" edge to the Label entity.
func (m *LabelMutation) ClearParent() {
m.clearedparent = true
}
// ParentCleared reports if the "parent" edge to the Label entity was cleared.
func (m *LabelMutation) ParentCleared() bool {
return m.clearedparent
}
// ParentID returns the "parent" edge ID in the mutation.
func (m *LabelMutation) ParentID() (id uuid.UUID, exists bool) {
if m.parent != nil {
return *m.parent, true
}
return
}
// ParentIDs returns the "parent" edge IDs in the mutation.
// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use
// ParentID instead. It exists only for internal usage by the builders.
func (m *LabelMutation) ParentIDs() (ids []uuid.UUID) {
if id := m.parent; id != nil {
ids = append(ids, *id)
}
return
}
// ResetParent resets all changes to the "parent" edge.
func (m *LabelMutation) ResetParent() {
m.parent = nil
m.clearedparent = false
}
// AddChildIDs adds the "children" edge to the Label entity by ids.
func (m *LabelMutation) AddChildIDs(ids ...uuid.UUID) {
if m.children == nil {
m.children = make(map[uuid.UUID]struct{})
}
for i := range ids {
m.children[ids[i]] = struct{}{}
}
}
// ClearChildren clears the "children" edge to the Label entity.
func (m *LabelMutation) ClearChildren() {
m.clearedchildren = true
}
// ChildrenCleared reports if the "children" edge to the Label entity was cleared.
func (m *LabelMutation) ChildrenCleared() bool {
return m.clearedchildren
}
// RemoveChildIDs removes the "children" edge to the Label entity by IDs.
func (m *LabelMutation) RemoveChildIDs(ids ...uuid.UUID) {
if m.removedchildren == nil {
m.removedchildren = make(map[uuid.UUID]struct{})
}
for i := range ids {
delete(m.children, ids[i])
m.removedchildren[ids[i]] = struct{}{}
}
}
// RemovedChildren returns the removed IDs of the "children" edge to the Label entity.
func (m *LabelMutation) RemovedChildrenIDs() (ids []uuid.UUID) {
for id := range m.removedchildren {
ids = append(ids, id)
}
return
}
// ChildrenIDs returns the "children" edge IDs in the mutation.
func (m *LabelMutation) ChildrenIDs() (ids []uuid.UUID) {
for id := range m.children {
ids = append(ids, id)
}
return
}
// ResetChildren resets all changes to the "children" edge.
func (m *LabelMutation) ResetChildren() {
m.children = nil
m.clearedchildren = false
m.removedchildren = nil
}
// AddItemIDs adds the "items" edge to the Item entity by ids.
func (m *LabelMutation) AddItemIDs(ids ...uuid.UUID) {
if m.items == nil {
@@ -9330,10 +9428,16 @@ func (m *LabelMutation) ResetField(name string) error {
// AddedEdges returns all edge names that were set/added in this mutation.
func (m *LabelMutation) AddedEdges() []string {
edges := make([]string, 0, 2)
edges := make([]string, 0, 4)
if m.group != nil {
edges = append(edges, label.EdgeGroup)
}
if m.parent != nil {
edges = append(edges, label.EdgeParent)
}
if m.children != nil {
edges = append(edges, label.EdgeChildren)
}
if m.items != nil {
edges = append(edges, label.EdgeItems)
}
@@ -9348,6 +9452,16 @@ func (m *LabelMutation) AddedIDs(name string) []ent.Value {
if id := m.group; id != nil {
return []ent.Value{*id}
}
case label.EdgeParent:
if id := m.parent; id != nil {
return []ent.Value{*id}
}
case label.EdgeChildren:
ids := make([]ent.Value, 0, len(m.children))
for id := range m.children {
ids = append(ids, id)
}
return ids
case label.EdgeItems:
ids := make([]ent.Value, 0, len(m.items))
for id := range m.items {
@@ -9360,7 +9474,10 @@ func (m *LabelMutation) AddedIDs(name string) []ent.Value {
// RemovedEdges returns all edge names that were removed in this mutation.
func (m *LabelMutation) RemovedEdges() []string {
edges := make([]string, 0, 2)
edges := make([]string, 0, 4)
if m.removedchildren != nil {
edges = append(edges, label.EdgeChildren)
}
if m.removeditems != nil {
edges = append(edges, label.EdgeItems)
}
@@ -9371,6 +9488,12 @@ func (m *LabelMutation) RemovedEdges() []string {
// the given name in this mutation.
func (m *LabelMutation) RemovedIDs(name string) []ent.Value {
switch name {
case label.EdgeChildren:
ids := make([]ent.Value, 0, len(m.removedchildren))
for id := range m.removedchildren {
ids = append(ids, id)
}
return ids
case label.EdgeItems:
ids := make([]ent.Value, 0, len(m.removeditems))
for id := range m.removeditems {
@@ -9383,10 +9506,16 @@ func (m *LabelMutation) RemovedIDs(name string) []ent.Value {
// ClearedEdges returns all edge names that were cleared in this mutation.
func (m *LabelMutation) ClearedEdges() []string {
edges := make([]string, 0, 2)
edges := make([]string, 0, 4)
if m.clearedgroup {
edges = append(edges, label.EdgeGroup)
}
if m.clearedparent {
edges = append(edges, label.EdgeParent)
}
if m.clearedchildren {
edges = append(edges, label.EdgeChildren)
}
if m.cleareditems {
edges = append(edges, label.EdgeItems)
}
@@ -9399,6 +9528,10 @@ func (m *LabelMutation) EdgeCleared(name string) bool {
switch name {
case label.EdgeGroup:
return m.clearedgroup
case label.EdgeParent:
return m.clearedparent
case label.EdgeChildren:
return m.clearedchildren
case label.EdgeItems:
return m.cleareditems
}
@@ -9412,6 +9545,9 @@ func (m *LabelMutation) ClearEdge(name string) error {
case label.EdgeGroup:
m.ClearGroup()
return nil
case label.EdgeParent:
m.ClearParent()
return nil
}
return fmt.Errorf("unknown Label unique edge %s", name)
}
@@ -9423,6 +9559,12 @@ func (m *LabelMutation) ResetEdge(name string) error {
case label.EdgeGroup:
m.ResetGroup()
return nil
case label.EdgeParent:
m.ResetParent()
return nil
case label.EdgeChildren:
m.ResetChildren()
return nil
case label.EdgeItems:
m.ResetItems()
return nil

View File

@@ -2,6 +2,7 @@ package schema
import (
"entgo.io/ent"
"entgo.io/ent/dialect/entsql"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
"github.com/sysadminsmedia/homebox/backend/internal/data/ent/schema/mixins"
@@ -32,6 +33,12 @@ func (Label) Fields() []ent.Field {
// Edges of the Label.
func (Label) Edges() []ent.Edge {
return []ent.Edge{
edge.To("children", Label.Type).
Annotations(entsql.Annotation{
OnDelete: entsql.Cascade,
}).
From("parent").
Unique(),
edge.To("items", Item.Type),
}
}

View File

@@ -0,0 +1,2 @@
-- +goose Up
ALTER TABLE users ALTER COLUMN password DROP NOT NULL;

View File

@@ -0,0 +1,3 @@
-- +goose Up
-- Add label_children column to labels table for hierarchical label organization
ALTER TABLE labels ADD COLUMN label_children UUID REFERENCES labels(id) ON DELETE CASCADE;

View File

@@ -0,0 +1,3 @@
-- +goose Up
-- Add label_children column to labels table for hierarchical label organization
ALTER TABLE labels ADD COLUMN label_children TEXT REFERENCES labels(id) ON DELETE CASCADE;

View File

@@ -2,6 +2,7 @@ package repo
import (
"context"
"fmt"
"time"
"github.com/google/uuid"
@@ -19,9 +20,10 @@ type LabelRepository struct {
type (
LabelCreate struct {
Name string `json:"name" validate:"required,min=1,max=255"`
Description string `json:"description" validate:"max=1000"`
Color string `json:"color"`
Name string `json:"name" validate:"required,min=1,max=255"`
Description string `json:"description" validate:"max=1000"`
Color string `json:"color"`
ParentID uuid.UUID `json:"parentId" extensions:"x-nullable"`
}
LabelUpdate struct {
@@ -29,6 +31,7 @@ type (
Name string `json:"name" validate:"required,min=1,max=255"`
Description string `json:"description" validate:"max=1000"`
Color string `json:"color"`
ParentID uuid.UUID `json:"parentId" extensions:"x-nullable"`
}
LabelSummary struct {
@@ -41,7 +44,9 @@ type (
}
LabelOut struct {
Parent *LabelSummary `json:"parent,omitempty"`
LabelSummary
Children []LabelSummary `json:"children"`
}
)
@@ -62,7 +67,20 @@ var (
)
func mapLabelOut(label *ent.Label) LabelOut {
var parent *LabelSummary
if label.Edges.Parent != nil {
p := mapLabelSummary(label.Edges.Parent)
parent = &p
}
children := make([]LabelSummary, 0, len(label.Edges.Children))
for _, c := range label.Edges.Children {
children = append(children, mapLabelSummary(c))
}
return LabelOut{
Parent: parent,
Children: children,
LabelSummary: mapLabelSummary(label),
}
}
@@ -73,10 +91,59 @@ func (r *LabelRepository) publishMutationEvent(gid uuid.UUID) {
}
}
const maxLabelDepth = 20
// validateParentNotCircular checks if setting parentID as parent of labelID would create a circular reference
func (r *LabelRepository) validateParentNotCircular(ctx context.Context, labelID, parentID uuid.UUID) error {
// Check if label is being set as its own parent
if labelID == parentID {
return fmt.Errorf("label cannot be its own parent")
}
// Check if parent exists and get its parent chain
depth := 0
currentID := parentID
for currentID != uuid.Nil && depth < maxLabelDepth {
l, err := r.db.Label.Query().
Where(label.ID(currentID)).
WithParent().
Only(ctx)
if err != nil {
if ent.IsNotFound(err) {
return fmt.Errorf("parent label not found")
}
return err
}
// Check if we've reached the label we're trying to set a parent for (circular reference)
if l.ID == labelID {
return fmt.Errorf("circular reference detected in label hierarchy")
}
depth++
if depth >= maxLabelDepth {
return fmt.Errorf("label hierarchy exceeds maximum depth of 20")
}
if l.Edges.Parent != nil {
currentID = l.Edges.Parent.ID
} else {
currentID = uuid.Nil
}
}
return nil
}
func (r *LabelRepository) getOne(ctx context.Context, where ...predicate.Label) (LabelOut, error) {
return mapLabelOutErr(r.db.Label.Query().
Where(where...).
WithGroup().
WithParent().
WithChildren(func(lq *ent.LabelQuery) {
lq.Order(label.ByName())
}).
Only(ctx),
)
}
@@ -99,19 +166,62 @@ func (r *LabelRepository) GetAll(ctx context.Context, groupID uuid.UUID) ([]Labe
}
func (r *LabelRepository) Create(ctx context.Context, groupID uuid.UUID, data LabelCreate) (LabelOut, error) {
label, err := r.db.Label.Create().
// Validate parent if provided
if data.ParentID != uuid.Nil {
// Check that parent exists and belongs to same group
parent, err := r.db.Label.Query().
Where(label.ID(data.ParentID), label.HasGroupWith(group.ID(groupID))).
Only(ctx)
if err != nil {
if ent.IsNotFound(err) {
return LabelOut{}, fmt.Errorf("parent label not found or does not belong to the same group")
}
return LabelOut{}, err
}
// Validate parent hierarchy depth
depth := 0
currentID := parent.ID
for currentID != uuid.Nil && depth < maxLabelDepth {
l, err := r.db.Label.Query().
Where(label.ID(currentID)).
WithParent().
Only(ctx)
if err != nil {
return LabelOut{}, err
}
depth++
if depth >= maxLabelDepth {
return LabelOut{}, fmt.Errorf("label hierarchy exceeds maximum depth of 20")
}
if l.Edges.Parent != nil {
currentID = l.Edges.Parent.ID
} else {
currentID = uuid.Nil
}
}
}
q := r.db.Label.Create().
SetName(data.Name).
SetDescription(data.Description).
SetColor(data.Color).
SetGroupID(groupID).
Save(ctx)
SetGroupID(groupID)
if data.ParentID != uuid.Nil {
q.SetParentID(data.ParentID)
}
label, err := q.Save(ctx)
if err != nil {
return LabelOut{}, err
}
label.Edges.Group = &ent.Group{ID: groupID} // bootstrap group ID
r.publishMutationEvent(groupID)
return mapLabelOut(label), err
return mapLabelOut(label), nil
}
func (r *LabelRepository) update(ctx context.Context, data LabelUpdate, where ...predicate.Label) (int, error) {
@@ -119,15 +229,41 @@ func (r *LabelRepository) update(ctx context.Context, data LabelUpdate, where ..
panic("empty where not supported empty")
}
return r.db.Label.Update().
q := r.db.Label.Update().
Where(where...).
SetName(data.Name).
SetDescription(data.Description).
SetColor(data.Color).
Save(ctx)
SetColor(data.Color)
if data.ParentID != uuid.Nil {
q.SetParentID(data.ParentID)
} else {
q.ClearParent()
}
return q.Save(ctx)
}
func (r *LabelRepository) UpdateByGroup(ctx context.Context, gid uuid.UUID, data LabelUpdate) (LabelOut, error) {
// Validate parent if provided
if data.ParentID != uuid.Nil {
// Check that parent exists and belongs to same group
parent, err := r.db.Label.Query().
Where(label.ID(data.ParentID), label.HasGroupWith(group.ID(gid))).
Only(ctx)
if err != nil {
if ent.IsNotFound(err) {
return LabelOut{}, fmt.Errorf("parent label not found or does not belong to the same group")
}
return LabelOut{}, err
}
// Validate no circular reference
if err := r.validateParentNotCircular(ctx, data.ID, parent.ID); err != nil {
return LabelOut{}, err
}
}
_, err := r.update(ctx, data, label.ID(data.ID), label.HasGroupWith(group.ID(gid)))
if err != nil {
return LabelOut{}, err

View File

@@ -4,6 +4,7 @@ import (
"context"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -101,3 +102,208 @@ func TestLabelRepository_Delete(t *testing.T) {
_, err = tRepos.Labels.GetOne(context.Background(), loc.ID)
require.Error(t, err)
}
func TestLabelRepository_ParentChild_Create(t *testing.T) {
ctx := context.Background()
// Create parent label
parent, err := tRepos.Labels.Create(ctx, tGroup.ID, labelFactory())
require.NoError(t, err)
defer tRepos.Labels.delete(ctx, parent.ID)
// Create child label
childData := labelFactory()
childData.ParentID = parent.ID
child, err := tRepos.Labels.Create(ctx, tGroup.ID, childData)
require.NoError(t, err)
defer tRepos.Labels.delete(ctx, child.ID)
// Verify child has parent
foundChild, err := tRepos.Labels.GetOne(ctx, child.ID)
require.NoError(t, err)
require.NotNil(t, foundChild.Parent)
assert.Equal(t, parent.ID, foundChild.Parent.ID)
// Verify parent has child
foundParent, err := tRepos.Labels.GetOne(ctx, parent.ID)
require.NoError(t, err)
assert.Len(t, foundParent.Children, 1)
assert.Equal(t, child.ID, foundParent.Children[0].ID)
}
func TestLabelRepository_ParentChild_CascadeDelete(t *testing.T) {
ctx := context.Background()
// Create parent label
parent, err := tRepos.Labels.Create(ctx, tGroup.ID, labelFactory())
require.NoError(t, err)
// Create child labels
child1Data := labelFactory()
child1Data.ParentID = parent.ID
child1, err := tRepos.Labels.Create(ctx, tGroup.ID, child1Data)
require.NoError(t, err)
child2Data := labelFactory()
child2Data.ParentID = parent.ID
child2, err := tRepos.Labels.Create(ctx, tGroup.ID, child2Data)
require.NoError(t, err)
// Delete parent
err = tRepos.Labels.delete(ctx, parent.ID)
require.NoError(t, err)
// Verify children are also deleted (cascade)
_, err = tRepos.Labels.GetOne(ctx, child1.ID)
require.Error(t, err)
_, err = tRepos.Labels.GetOne(ctx, child2.ID)
require.Error(t, err)
}
func TestLabelRepository_ParentChild_CircularReference_Self(t *testing.T) {
ctx := context.Background()
// Create a label
label1, err := tRepos.Labels.Create(ctx, tGroup.ID, labelFactory())
require.NoError(t, err)
defer tRepos.Labels.delete(ctx, label1.ID)
// Try to set itself as parent - should fail
updateData := LabelUpdate{
ID: label1.ID,
Name: label1.Name,
Description: label1.Description,
Color: label1.Color,
ParentID: label1.ID,
}
_, err = tRepos.Labels.UpdateByGroup(ctx, tGroup.ID, updateData)
require.Error(t, err)
assert.Contains(t, err.Error(), "cannot be its own parent")
}
func TestLabelRepository_ParentChild_CircularReference_Chain(t *testing.T) {
ctx := context.Background()
// Create label chain: label1 -> label2 -> label3
label1, err := tRepos.Labels.Create(ctx, tGroup.ID, labelFactory())
require.NoError(t, err)
defer tRepos.Labels.delete(ctx, label1.ID)
label2Data := labelFactory()
label2Data.ParentID = label1.ID
label2, err := tRepos.Labels.Create(ctx, tGroup.ID, label2Data)
require.NoError(t, err)
defer tRepos.Labels.delete(ctx, label2.ID)
label3Data := labelFactory()
label3Data.ParentID = label2.ID
label3, err := tRepos.Labels.Create(ctx, tGroup.ID, label3Data)
require.NoError(t, err)
defer tRepos.Labels.delete(ctx, label3.ID)
// Try to set label3 as parent of label1 (would create circular reference)
updateData := LabelUpdate{
ID: label1.ID,
Name: label1.Name,
Description: label1.Description,
Color: label1.Color,
ParentID: label3.ID,
}
_, err = tRepos.Labels.UpdateByGroup(ctx, tGroup.ID, updateData)
require.Error(t, err)
assert.Contains(t, err.Error(), "circular reference")
}
func TestLabelRepository_ParentChild_MaxDepth(t *testing.T) {
ctx := context.Background()
// Create a chain of 20 labels
labels := make([]LabelOut, 20)
for i := 0; i < 20; i++ {
labelData := labelFactory()
if i > 0 {
labelData.ParentID = labels[i-1].ID
}
label, err := tRepos.Labels.Create(ctx, tGroup.ID, labelData)
require.NoError(t, err)
labels[i] = label
}
// Clean up
defer func() {
for i := len(labels) - 1; i >= 0; i-- {
_ = tRepos.Labels.delete(ctx, labels[i].ID)
}
}()
// Try to create 21st level - should fail
labelData := labelFactory()
labelData.ParentID = labels[19].ID
_, err := tRepos.Labels.Create(ctx, tGroup.ID, labelData)
require.Error(t, err)
assert.Contains(t, err.Error(), "maximum depth")
}
func TestLabelRepository_ParentChild_Update(t *testing.T) {
ctx := context.Background()
// Create labels
parent1, err := tRepos.Labels.Create(ctx, tGroup.ID, labelFactory())
require.NoError(t, err)
defer tRepos.Labels.delete(ctx, parent1.ID)
parent2, err := tRepos.Labels.Create(ctx, tGroup.ID, labelFactory())
require.NoError(t, err)
defer tRepos.Labels.delete(ctx, parent2.ID)
childData := labelFactory()
childData.ParentID = parent1.ID
child, err := tRepos.Labels.Create(ctx, tGroup.ID, childData)
require.NoError(t, err)
defer tRepos.Labels.delete(ctx, child.ID)
// Update child to have different parent
updateData := LabelUpdate{
ID: child.ID,
Name: child.Name,
Description: child.Description,
Color: child.Color,
ParentID: parent2.ID,
}
updated, err := tRepos.Labels.UpdateByGroup(ctx, tGroup.ID, updateData)
require.NoError(t, err)
require.NotNil(t, updated.Parent)
assert.Equal(t, parent2.ID, updated.Parent.ID)
}
func TestLabelRepository_ParentChild_ClearParent(t *testing.T) {
ctx := context.Background()
// Create parent and child
parent, err := tRepos.Labels.Create(ctx, tGroup.ID, labelFactory())
require.NoError(t, err)
defer tRepos.Labels.delete(ctx, parent.ID)
childData := labelFactory()
childData.ParentID = parent.ID
child, err := tRepos.Labels.Create(ctx, tGroup.ID, childData)
require.NoError(t, err)
defer tRepos.Labels.delete(ctx, child.ID)
// Clear parent by setting ParentID to uuid.Nil
updateData := LabelUpdate{
ID: child.ID,
Name: child.Name,
Description: child.Description,
Color: child.Color,
ParentID: uuid.Nil,
}
updated, err := tRepos.Labels.UpdateByGroup(ctx, tGroup.ID, updateData)
require.NoError(t, err)
assert.Nil(t, updated.Parent)
}

View File

@@ -3413,6 +3413,13 @@
"ent.LabelEdges": {
"type": "object",
"properties": {
"children": {
"description": "Children holds the value of the children edge.",
"type": "array",
"items": {
"$ref": "#/components/schemas/ent.Label"
}
},
"group": {
"description": "Group holds the value of the group edge.",
"allOf": [
@@ -3427,6 +3434,14 @@
"items": {
"$ref": "#/components/schemas/ent.Item"
}
},
"parent": {
"description": "Parent holds the value of the parent edge.",
"allOf": [
{
"$ref": "#/components/schemas/ent.Label"
}
]
}
}
},
@@ -4634,12 +4649,22 @@
"type": "string",
"maxLength": 255,
"minLength": 1
},
"parentId": {
"type": "string",
"nullable": true
}
}
},
"repo.LabelOut": {
"type": "object",
"properties": {
"children": {
"type": "array",
"items": {
"$ref": "#/components/schemas/repo.LabelSummary"
}
},
"color": {
"type": "string"
},
@@ -4655,6 +4680,9 @@
"name": {
"type": "string"
},
"parent": {
"$ref": "#/components/schemas/repo.LabelSummary"
},
"updatedAt": {
"type": "string"
}

View File

@@ -2112,6 +2112,11 @@ components:
ent.LabelEdges:
type: object
properties:
children:
description: Children holds the value of the children edge.
type: array
items:
$ref: "#/components/schemas/ent.Label"
group:
description: Group holds the value of the group edge.
allOf:
@@ -2121,6 +2126,10 @@ components:
type: array
items:
$ref: "#/components/schemas/ent.Item"
parent:
description: Parent holds the value of the parent edge.
allOf:
- $ref: "#/components/schemas/ent.Label"
ent.Location:
type: object
properties:
@@ -2956,9 +2965,16 @@ components:
type: string
maxLength: 255
minLength: 1
parentId:
type: string
nullable: true
repo.LabelOut:
type: object
properties:
children:
type: array
items:
$ref: "#/components/schemas/repo.LabelSummary"
color:
type: string
createdAt:
@@ -2969,6 +2985,8 @@ components:
type: string
name:
type: string
parent:
$ref: "#/components/schemas/repo.LabelSummary"
updatedAt:
type: string
repo.LabelSummary:

View File

@@ -3214,6 +3214,13 @@
"ent.LabelEdges": {
"type": "object",
"properties": {
"children": {
"description": "Children holds the value of the children edge.",
"type": "array",
"items": {
"$ref": "#/definitions/ent.Label"
}
},
"group": {
"description": "Group holds the value of the group edge.",
"allOf": [
@@ -3228,6 +3235,14 @@
"items": {
"$ref": "#/definitions/ent.Item"
}
},
"parent": {
"description": "Parent holds the value of the parent edge.",
"allOf": [
{
"$ref": "#/definitions/ent.Label"
}
]
}
}
},
@@ -4435,12 +4450,22 @@
"type": "string",
"maxLength": 255,
"minLength": 1
},
"parentId": {
"type": "string",
"x-nullable": true
}
}
},
"repo.LabelOut": {
"type": "object",
"properties": {
"children": {
"type": "array",
"items": {
"$ref": "#/definitions/repo.LabelSummary"
}
},
"color": {
"type": "string"
},
@@ -4456,6 +4481,9 @@
"name": {
"type": "string"
},
"parent": {
"$ref": "#/definitions/repo.LabelSummary"
},
"updatedAt": {
"type": "string"
}

View File

@@ -534,6 +534,11 @@ definitions:
type: object
ent.LabelEdges:
properties:
children:
description: Children holds the value of the children edge.
items:
$ref: '#/definitions/ent.Label'
type: array
group:
allOf:
- $ref: '#/definitions/ent.Group'
@@ -543,6 +548,10 @@ definitions:
items:
$ref: '#/definitions/ent.Item'
type: array
parent:
allOf:
- $ref: '#/definitions/ent.Label'
description: Parent holds the value of the parent edge.
type: object
ent.Location:
properties:
@@ -1371,11 +1380,18 @@ definitions:
maxLength: 255
minLength: 1
type: string
parentId:
type: string
x-nullable: true
required:
- name
type: object
repo.LabelOut:
properties:
children:
items:
$ref: '#/definitions/repo.LabelSummary'
type: array
color:
type: string
createdAt:
@@ -1386,6 +1402,8 @@ definitions:
type: string
name:
type: string
parent:
$ref: '#/definitions/repo.LabelSummary'
updatedAt:
type: string
type: object

View File

@@ -22,7 +22,7 @@ aside: false
| HBOX_WEB_IDLE_TIMEOUT | 30s | Idle timeout of HTTP server |
| HBOX_STORAGE_CONN_STRING | file:///./ | path to the data directory, do not change this if you're using docker |
| HBOX_STORAGE_PREFIX_PATH | .data | prefix path for the storage, if not set the storage will be used as is |
| HBOX_LOG_LEVEL | `info` | log level to use, can be one of `trace`, `debug`, `info`, `warn`, `error`, `critical` |
| HBOX_LOG_LEVEL | `info` | log level to use, can be one of `trace`, `debug`, `info`, `warn`, `error`, `fatal`, `panic` |
| HBOX_LOG_FORMAT | `text` | log format to use, can be one of: `text`, `json` |
| HBOX_MAILER_HOST | | email host to use, if not set no email provider will be used |
| HBOX_MAILER_PORT | 587 | email port to use |
@@ -71,6 +71,7 @@ aside: false
| HBOX_THUMBNAIL_ENABLED | true | enable thumbnail generation for images, supports PNG, JPEG, AVIF, WEBP, GIF file types |
| HBOX_THUMBNAIL_WIDTH | 500 | width for generated thumbnails in pixels |
| HBOX_THUMBNAIL_HEIGHT | 500 | height for generated thumbnails in pixels |
| HBOX_BARCODE_TOKEN_BARCODESPIDER | | API token for BarcodeSpider.com service used for barcode product lookups. If not set, barcode product lookups will not be performed. |
### HBOX_WEB_HOST examples
@@ -204,70 +205,79 @@ If you're deploying without docker you can use command line arguments to configu
for more information.
```sh
Usage: api [options] [arguments]
OPTIONS
--mode/$HBOX_MODE <string> (default: development)
--web-port/$HBOX_WEB_PORT <string> (default: 7745)
--web-host/$HBOX_WEB_HOST <string>
--web-max-upload-size/$HBOX_WEB_MAX_UPLOAD_SIZE <int> (default: 10)
--storage-conn-string/$HBOX_STORAGE_CONN_STRING <string> (default: file:///./)
--storage-prefix-path/$HBOX_STORAGE_PREFIX_PATH <string> (default: .data)
--log-level/$HBOX_LOG_LEVEL <string> (default: info)
--log-format/$HBOX_LOG_FORMAT <string> (default: text)
--mailer-host/$HBOX_MAILER_HOST <string>
--mailer-port/$HBOX_MAILER_PORT <int>
--mailer-username/$HBOX_MAILER_USERNAME <string>
--mailer-password/$HBOX_MAILER_PASSWORD <string>
--mailer-from/$HBOX_MAILER_FROM <string>
--demo/$HBOX_DEMO <bool>
--debug-enabled/$HBOX_DEBUG_ENABLED <bool> (default: false)
--debug-port/$HBOX_DEBUG_PORT <string> (default: 4000)
--database-driver/$HBOX_DATABASE_DRIVER <string> (default: sqlite3)
--database-sqlite-path/$HBOX_DATABASE_SQLITE_PATH <string> (default: ./.data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1&_time_format=sqlite)
--database-host/$HBOX_DATABASE_HOST <string>
--database-port/$HBOX_DATABASE_PORT <string>
--database-username/$HBOX_DATABASE_USERNAME <string>
--database-password/$HBOX_DATABASE_PASSWORD <string>
--database-database/$HBOX_DATABASE_DATABASE <string>
--database-ssl-mode/$HBOX_DATABASE_SSL_MODE <string> (default: prefer)
--options-allow-registration/$HBOX_OPTIONS_ALLOW_REGISTRATION <bool> (default: true)
--options-auto-increment-asset-id/$HBOX_OPTIONS_AUTO_INCREMENT_ASSET_ID <bool> (default: true)
--options-currency-config/$HBOX_OPTIONS_CURRENCY_CONFIG <string>
--options-github-release-check/$HBOX_OPTIONS_GITHUB_RELEASE_CHECK <bool> (default: true)
--options-allow-analytics/$HBOX_OPTIONS_ALLOW_ANALYTICS <bool> (default: false)
--options-allow-local-login/$HBOX_OPTIONS_ALLOW_LOCAL_LOGIN <bool> (default: true)
--options-trust-proxy/$HBOX_OPTIONS_TRUST_PROXY <bool> (default: false)
--options-hostname/$HBOX_OPTIONS_HOSTNAME <string>
--oidc-enabled/$HBOX_OIDC_ENABLED <bool> (default: false)
--oidc-issuer-url/$HBOX_OIDC_ISSUER_URL <string>
--oidc-client-id/$HBOX_OIDC_CLIENT_ID <string>
--oidc-client-secret/$HBOX_OIDC_CLIENT_SECRET <string>
--oidc-scope/$HBOX_OIDC_SCOPE <string> (default: openid profile email)
--oidc-allowed-groups/$HBOX_OIDC_ALLOWED_GROUPS <string>
--oidc-auto-redirect/$HBOX_OIDC_AUTO_REDIRECT <bool> (default: false)
--oidc-verify-email/$HBOX_OIDC_VERIFY_EMAIL <bool> (default: false)
--oidc-group-claim/$HBOX_OIDC_GROUP_CLAIM <string> (default: groups)
--oidc-email-claim/$HBOX_OIDC_EMAIL_CLAIM <string> (default: email)
--oidc-name-claim/$HBOX_OIDC_NAME_CLAIM <string> (default: name)
--oidc-email-verified-claim/$HBOX_OIDC_EMAIL_VERIFIED_CLAIM <string> (default: email_verified)
--oidc-button-text/$HBOX_OIDC_BUTTON_TEXT <string> (default: Sign in with OIDC)
--oidc-state-expiry/$HBOX_OIDC_STATE_EXPIRY <duration> (default: 10m)
--oidc-request-timeout/$HBOX_OIDC_REQUEST_TIMEOUT <duration> (default: 30s)
--label-maker-width/$HBOX_LABEL_MAKER_WIDTH <int> (default: 526)
--label-maker-height/$HBOX_LABEL_MAKER_HEIGHT <int> (default: 200)
--label-maker-padding/$HBOX_LABEL_MAKER_PADDING <int> (default: 32)
--label-maker-margin/$HBOX_LABEL_MAKER_MARGIN <int> (default: 32)
--label-maker-font-size/$HBOX_LABEL_MAKER_FONT_SIZE <float> (default: 32.0)
--label-maker-print-command/$HBOX_LABEL_MAKER_PRINT_COMMAND <string>
--label-maker-dynamic-length/$HBOX_LABEL_MAKER_DYNAMIC_LENGTH <bool> (default: true)
--label-maker-additional-information/$HBOX_LABEL_MAKER_ADDITIONAL_INFORMATION <string>
--label-maker-regular-font-path/$HBOX_LABEL_MAKER_REGULAR_FONT_PATH <string>
--label-maker-bold-font-path/$HBOX_LABEL_MAKER_BOLD_FONT_PATH <string>
--thumbnail-enabled/$HBOX_THUMBNAIL_ENABLED <bool> (default: true)
--thumbnail-width/$HBOX_THUMBNAIL_WIDTH <int> (default: 500)
--thumbnail-height/$HBOX_THUMBNAIL_HEIGHT <int> (default: 500)
--help/-h display this help message
Options:
--barcode-token-barcodespider <string>
--database-database <string>
--database-driver <string> (default: sqlite3)
--database-host <string>
--database-password <string>
--database-port <string>
--database-pub-sub-conn-string <string> (default: mem://{{ .Topic }})
--database-sqlite-path <string> (default: ./.data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1&_time_format=sqlite)
--database-ssl-cert <string>
--database-ssl-key <string>
--database-ssl-mode <string> (default: require)
--database-ssl-root-cert <string>
--database-username <string>
--debug-enabled <bool> (default: false)
--debug-port <string> (default: 4000)
--demo <bool>
-h, --help display this help message
--label-maker-additional-information <string>
--label-maker-bold-font-path <string>
--label-maker-dynamic-length <bool> (default: true)
--label-maker-font-size <float> (default: 32.0)
--label-maker-height <int> (default: 200)
--label-maker-label-service-timeout <int>
--label-maker-label-service-url <string>
--label-maker-margin <int> (default: 32)
--label-maker-padding <int> (default: 32)
--label-maker-print-command <string>
--label-maker-regular-font-path <string>
--label-maker-width <int> (default: 526)
--log-format <string> (default: text)
--log-level <string> (default: info)
--mailer-from <string>
--mailer-host <string>
--mailer-password <string>
--mailer-port <int>
--mailer-username <string>
--mode <string> (default: development)
--oidc-allowed-groups <string>
--oidc-auto-redirect <bool> (default: false)
--oidc-button-text <string> (default: Sign in with OIDC)
--oidc-client-id <string>
--oidc-client-secret <string>
--oidc-email-claim <string> (default: email)
--oidc-email-verified-claim <string> (default: email_verified)
--oidc-enabled <bool> (default: false)
--oidc-group-claim <string> (default: groups)
--oidc-issuer-url <string>
--oidc-name-claim <string> (default: name)
--oidc-request-timeout <duration> (default: 30s)
--oidc-scope <string> (default: openid profile email)
--oidc-state-expiry <duration> (default: 10m)
--oidc-verify-email <bool> (default: false)
--options-allow-analytics <bool> (default: false)
--options-allow-local-login <bool> (default: true)
--options-allow-registration <bool> (default: true)
--options-auto-increment-asset-id <bool> (default: true)
--options-currency-config <string>
--options-github-release-check <bool> (default: true)
--options-hostname <string>
--options-trust-proxy <bool> (default: false)
--storage-conn-string <string> (default: file:///./)
--storage-prefix-path <string> (default: .data)
--thumbnail-enabled <bool> (default: true)
--thumbnail-height <int> (default: 500)
--thumbnail-width <int> (default: 500)
-v, --version display version
--web-host <string>
--web-idle-timeout <duration> (default: 30s)
--web-max-upload-size <int> (default: 10)
--web-port <string> (default: 7745)
--web-read-timeout <duration> (default: 10s)
--web-write-timeout <duration> (default: 10s)
```
:::

View File

@@ -30,19 +30,19 @@ the bucket name in the connection string.
### S3-Compatible Storage
You can also use S3-compatible storage by setting the `HBOX_STORAGE_CONN_STRING` to
`s3://my-bucket?awssdk=v2&endpoint=http://my-s3-compatible-endpoint.tld&disableSSL=true&s3ForcePathStyle=true`.
`s3://my-bucket?awssdk=v2&endpoint=http://my-s3-compatible-endpoint.tld&disable_https=true&s3ForcePathStyle=true`.
This allows you to connect to S3-compatible services like MinIO, DigitalOcean Spaces, or any other service that supports
the S3 API. Configure the `disableSSL`, `s3ForcePathStyle`, and `endpoint` parameters as needed for your specific
the S3 API. Configure the `disable_https`, `s3ForcePathStyle`, and `endpoint` parameters as needed for your specific
service.
#### Tested S3-Compatible Storage
| Service | Working | Connection String |
|---------------------|---------|--------------------------------------------------------------------------------------------------------------------------|
| MinIO | Yes | `s3://my-bucket?awssdk=v2&endpoint=http://minio:9000&disableSSL=true&s3ForcePathStyle=true` |
| Cloudflare R2 | Yes | `s3://my-bucket?awssdk=v2&endpoint=https://<account-id>.r2.cloudflarestorage.com&disableSSL=false&s3ForcePathStyle=true` |
| Backblaze B2 | Yes | `s3://my-bucket?awssdk=v2&endpoint=https://s3.us-west-004.backblazeb2.com&disableSSL=false&s3ForcePathStyle=true` |
| MinIO | Yes | `s3://my-bucket?awssdk=v2&endpoint=http://minio:9000&disable_https=true&s3ForcePathStyle=true` |
| Cloudflare R2 | Yes | `s3://my-bucket?awssdk=v2&endpoint=https://<account-id>.r2.cloudflarestorage.com&disable_https=false&s3ForcePathStyle=true` |
| Backblaze B2 | Yes | `s3://my-bucket?awssdk=v2&endpoint=https://s3.us-west-004.backblazeb2.com&disable_https=false&s3ForcePathStyle=true` |
::: info
If you know of any other S3-compatible storage that works with Homebox, please let us know or create a pull request to update the table.
@@ -57,7 +57,7 @@ Additionally, the parameters in the URL can be used to configure specific S3 set
features.)
- `endpoint`: The custom endpoint for S3-compatible storage services.
- `s3ForcePathStyle`: Whether to force path-style access (set to `true` or `false`).
- `disableSSL`: Whether to disable SSL (set to `true` or `false`).
- `disable_https`: Whether to disable SSL (set to `true` or `false`).
- `sseType`: The server-side encryption type (e.g., `AES256` or `aws:kms` or `aws:kms:dsse`).
- `kmskeyid`: The KMS key ID for server-side encryption.
- `fips`: Whether to use FIPS endpoints (set to `true` or `false`).

View File

@@ -15,6 +15,11 @@
default: "md",
},
});
// Type guard to check if label is LabelOut (has parent/children)
const isLabelOut = (label: LabelOut | LabelSummary): label is LabelOut => {
return 'parent' in label || 'children' in label;
};
</script>
<template>
@@ -42,6 +47,10 @@
<MdiArrowUp class="hidden group-hover/label-chip:block" />
</div>
</div>
<template v-if="isLabelOut(label) && label.parent">
<span class="opacity-70">{{ label.parent.name }}</span>
<span class="opacity-50">/</span>
</template>
{{ label.name }}
</NuxtLink>
</template>

View File

@@ -15,6 +15,7 @@
:max-length="1000"
/>
<ColorSelector v-model="form.color" :label="$t('components.label.create_modal.label_color')" :show-hex="true" />
<LabelParentSelector v-model="form.parentId" :labels="labels" />
<div class="mt-4 flex flex-row-reverse">
<ButtonGroup>
<Button :disabled="loading" type="submit">{{ $t("global.create") }}</Button>
@@ -37,6 +38,8 @@
import FormTextField from "~/components/Form/TextField.vue";
import FormTextArea from "~/components/Form/TextArea.vue";
import { Button, ButtonGroup } from "~/components/ui/button";
import LabelParentSelector from "@/components/Label/ParentSelector.vue";
import type { LabelOut } from "~/lib/api/types/data-contracts";
const { t } = useI18n();
@@ -49,13 +52,25 @@
const form = reactive({
name: "",
description: "",
color: "", // Future!
color: "",
parentId: null as string | null,
});
const labels = ref<LabelOut[]>([]);
// Load labels for parent selection
onMounted(async () => {
const { data } = await api.labels.getAll();
if (data) {
labels.value = data;
}
});
function reset() {
form.name = "";
form.description = "";
form.color = "";
form.parentId = null;
focused.value = false;
loading.value = false;
}

View File

@@ -0,0 +1,40 @@
<template>
<div class="flex flex-col gap-1">
<Label :for="id">
{{ $t('components.label.parent_selector.label') }}
</Label>
<Select v-model="modelValue">
<SelectTrigger :id="id">
<SelectValue :placeholder="$t('components.label.parent_selector.placeholder')" />
</SelectTrigger>
<SelectContent>
<SelectItem value="">{{ $t('components.label.parent_selector.no_parent') }}</SelectItem>
<SelectItem v-for="label in props.labels" :key="label.id" :value="label.id">
{{ label.name }}
</SelectItem>
</SelectContent>
</Select>
</div>
</template>
<script setup lang="ts">
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import type { LabelOut } from "~/lib/api/types/data-contracts";
const id = useId();
const emit = defineEmits(["update:modelValue"]);
const props = defineProps({
modelValue: {
type: String as () => string | null,
default: null,
},
labels: {
type: Array as () => LabelOut[],
required: true,
},
});
const modelValue = useVModel(props, "modelValue", emit);
</script>

View File

@@ -381,10 +381,14 @@ export interface EntLabel {
}
export interface EntLabelEdges {
/** Children holds the value of the children edge. */
children: EntLabel[];
/** Group holds the value of the group edge. */
group: EntGroup;
/** Items holds the value of the items edge. */
items: EntItem[];
/** Parent holds the value of the parent edge. */
parent: EntLabel;
}
export interface EntLocation {
@@ -857,14 +861,17 @@ export interface LabelCreate {
* @maxLength 255
*/
name: string;
parentId?: string | null;
}
export interface LabelOut {
children: LabelSummary[];
color: string;
createdAt: Date | string;
description: string;
id: string;
name: string;
parent: LabelSummary;
updatedAt: Date | string;
}

View File

@@ -202,6 +202,11 @@
"label_name_too_long": "Label name must not be longer than 50 characters"
}
},
"parent_selector": {
"label": "Parent Label",
"no_parent": "No Parent",
"placeholder": "Select a parent label"
},
"selector": {
"select_labels": "Select Labels"
}
@@ -506,8 +511,10 @@
"warranty_expires": "Warranty Expires"
},
"labels": {
"child_labels": "Child Labels",
"label_delete_confirm": "Are you sure you want to delete this label? This action cannot be undone.",
"no_results": "No Labels Found",
"parent_label": "Parent Label",
"toast": {
"failed_delete_label": "Failed to delete label",
"failed_load_label": "Failed to load label",

View File

@@ -21,6 +21,8 @@
import PageQRCode from "~/components/global/PageQRCode.vue";
import Markdown from "~/components/global/Markdown.vue";
import ItemViewSelectable from "~/components/Item/View/Selectable.vue";
import LabelParentSelector from "@/components/Label/ParentSelector.vue";
import LabelChip from "@/components/Label/Chip.vue";
definePageMeta({
middleware: ["auth"],
@@ -69,12 +71,19 @@
name: "",
description: "",
color: "",
parentId: null as string | null,
});
const { data: allLabels } = useAsyncData("all-labels", async () => {
const { data } = await api.labels.getAll();
return data || [];
});
function openUpdate() {
updateData.name = label.value?.name || "";
updateData.description = label.value?.description || "";
updateData.color = "";
updateData.parentId = label.value?.parent?.id || null;
openDialog(DialogID.UpdateLabel);
}
@@ -151,6 +160,7 @@
:show-hex="true"
:starting-color="label.color"
/>
<LabelParentSelector v-if="allLabels" v-model="updateData.parentId" :labels="allLabels.filter(l => l.id !== labelId)" />
<DialogFooter>
<Button type="submit" :loading="updating"> {{ $t("global.update") }} </Button>
</DialogFooter>
@@ -204,6 +214,25 @@
</header>
<Separator v-if="label && label.description" />
<Markdown v-if="label && label.description" class="mt-3 text-base" :source="label.description" />
<!-- Display parent and children -->
<div v-if="label && (label.parent || (label.children && label.children.length > 0))" class="mt-3">
<Separator />
<div class="mt-3">
<div v-if="label.parent" class="mb-2">
<span class="text-sm font-medium">{{ $t("labels.parent_label") }}:</span>
<div class="mt-1">
<LabelChip :label="label.parent" size="sm" />
</div>
</div>
<div v-if="label.children && label.children.length > 0">
<span class="text-sm font-medium">{{ $t("labels.child_labels") }}:</span>
<div class="mt-1 flex flex-wrap gap-2">
<LabelChip v-for="child in label.children" :key="child.id" :label="child" size="sm" />
</div>
</div>
</div>
</div>
</Card>
<section v-if="label && items">
<ItemViewSelectable :items="items.items" @refresh="refreshItemList" />