Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 42 additions & 14 deletions backend/internal/data/repo/repo_entities.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ type (
ParentIDs []uuid.UUID `json:"parentIds"`
TagIDs []uuid.UUID `json:"tagIds"`
NegateTags bool `json:"negateTags"`
MatchAllTags bool `json:"matchAllTags"`
OnlyWithoutPhoto bool `json:"onlyWithoutPhoto"`
OnlyWithPhoto bool `json:"onlyWithPhoto"`
ParentItemIDs []uuid.UUID `json:"parentItemIds"`
Expand Down Expand Up @@ -605,25 +606,52 @@ func (r *EntityRepository) QueryByGroup(ctx context.Context, gid uuid.UUID, q En
tagRepo := &TagRepository{r.db, r.bus}
ctxDescendants, descSpan := entityTracer().Start(ctx, "repo.EntityRepository.QueryByGroup.tagDescendants",
trace.WithAttributes(attribute.Int("query.tag_ids.count", len(q.TagIDs))))
descendants, err := tagRepo.GetDescendantTagIDs(ctxDescendants, q.TagIDs)
if err != nil {
recordSpanError(descSpan, err)
log.Warn().Err(err).Msg("failed to get descendant tags, using only direct tags")
descendants = q.TagIDs
} else if len(descendants) == 0 {
descendants = q.TagIDs
descendantGroups := make([][]uuid.UUID, 0, len(q.TagIDs))
descendantCount := 0
if q.MatchAllTags && !q.NegateTags {
for _, tagID := range q.TagIDs {
descendants, err := tagRepo.GetDescendantTagIDs(ctxDescendants, []uuid.UUID{tagID})
if err != nil {
recordSpanError(descSpan, err)
log.Warn().Err(err).Msg("failed to get descendant tags, using only direct tag")
descendants = []uuid.UUID{tagID}
} else if len(descendants) == 0 {
descendants = []uuid.UUID{tagID}
}
descendantGroups = append(descendantGroups, descendants)
descendantCount += len(descendants)
}
} else {
descendants, err := tagRepo.GetDescendantTagIDs(ctxDescendants, q.TagIDs)
if err != nil {
recordSpanError(descSpan, err)
log.Warn().Err(err).Msg("failed to get descendant tags, using only direct tags")
descendants = q.TagIDs
} else if len(descendants) == 0 {
descendants = q.TagIDs
}
descendantGroups = append(descendantGroups, descendants)
descendantCount = len(descendants)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
descSpan.SetAttributes(attribute.Int("query.tag_descendants.count", len(descendants)))
descSpan.SetAttributes(attribute.Int("query.tag_descendants.count", descendantCount))
descSpan.End()

var tagPredicates []predicate.Entity
if !q.NegateTags {
tagPredicates = lo.Map(descendants, func(l uuid.UUID, _ int) predicate.Entity {
return entity.HasTagWith(tag.ID(l))
})
andPredicates = append(andPredicates, entity.Or(tagPredicates...))
if q.MatchAllTags {
for _, descendants := range descendantGroups {
tagPredicates := lo.Map(descendants, func(l uuid.UUID, _ int) predicate.Entity {
return entity.HasTagWith(tag.ID(l))
})
andPredicates = append(andPredicates, entity.Or(tagPredicates...))
}
} else {
tagPredicates := lo.Map(descendantGroups[0], func(l uuid.UUID, _ int) predicate.Entity {
return entity.HasTagWith(tag.ID(l))
})
andPredicates = append(andPredicates, entity.Or(tagPredicates...))
}
} else {
tagPredicates = lo.Map(descendants, func(l uuid.UUID, _ int) predicate.Entity {
tagPredicates := lo.Map(descendantGroups[0], func(l uuid.UUID, _ int) predicate.Entity {
return entity.Not(entity.HasTagWith(tag.ID(l)))
})
andPredicates = append(andPredicates, entity.And(tagPredicates...))
Expand Down
104 changes: 104 additions & 0 deletions backend/internal/data/repo/repo_entities_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,110 @@ func TestEntityRepository_Update_Tags(t *testing.T) {
}
}

func TestEntityRepository_QueryByGroup_MatchAllTags(t *testing.T) {
containerET := useContainerEntityType(t)
itemET := useItemEntityType(t)
tags := useTags(t, 2)
childTag1, err := tRepos.Tags.Create(context.Background(), tGroup.ID, TagCreate{
Name: fk.Str(10),
Description: fk.Str(100),
ParentID: tags[0].ID,
})
require.NoError(t, err)
childTag2, err := tRepos.Tags.Create(context.Background(), tGroup.ID, TagCreate{
Name: fk.Str(10),
Description: fk.Str(100),
ParentID: tags[1].ID,
})
require.NoError(t, err)
t.Cleanup(func() {
_ = tRepos.Tags.delete(context.Background(), childTag2.ID)
_ = tRepos.Tags.delete(context.Background(), childTag1.ID)
})

container, err := tRepos.Entities.Create(context.Background(), tGroup.ID, EntityCreate{
Name: fk.Str(10),
Description: fk.Str(100),
EntityTypeID: containerET.ID,
})
require.NoError(t, err)

wideMatch, err := tRepos.Entities.Create(context.Background(), tGroup.ID, EntityCreate{
Name: fk.Str(10),
Description: fk.Str(100),
ParentID: container.ID,
EntityTypeID: itemET.ID,
TagIDs: []uuid.UUID{tags[0].ID, tags[1].ID},
})
require.NoError(t, err)

narrowMatch, err := tRepos.Entities.Create(context.Background(), tGroup.ID, EntityCreate{
Name: fk.Str(10),
Description: fk.Str(100),
ParentID: container.ID,
EntityTypeID: itemET.ID,
TagIDs: []uuid.UUID{tags[0].ID},
})
require.NoError(t, err)

descendantWideMatch, err := tRepos.Entities.Create(context.Background(), tGroup.ID, EntityCreate{
Name: fk.Str(10),
Description: fk.Str(100),
ParentID: container.ID,
EntityTypeID: itemET.ID,
TagIDs: []uuid.UUID{childTag1.ID, childTag2.ID},
})
require.NoError(t, err)

descendantNarrowMatch, err := tRepos.Entities.Create(context.Background(), tGroup.ID, EntityCreate{
Name: fk.Str(10),
Description: fk.Str(100),
ParentID: container.ID,
EntityTypeID: itemET.ID,
TagIDs: []uuid.UUID{childTag1.ID},
})
require.NoError(t, err)

t.Cleanup(func() {
_ = tRepos.Entities.Delete(context.Background(), descendantNarrowMatch.ID)
_ = tRepos.Entities.Delete(context.Background(), descendantWideMatch.ID)
_ = tRepos.Entities.Delete(context.Background(), narrowMatch.ID)
_ = tRepos.Entities.Delete(context.Background(), wideMatch.ID)
_ = tRepos.Entities.Delete(context.Background(), container.ID)
})

query := EntityQuery{
Page: -1,
PageSize: -1,
TagIDs: []uuid.UUID{tags[0].ID, tags[1].ID},
}

results, err := tRepos.Entities.QueryByGroup(context.Background(), tGroup.ID, query)
require.NoError(t, err)

ids := make([]uuid.UUID, 0, len(results.Items))
for _, item := range results.Items {
ids = append(ids, item.ID)
}
assert.Contains(t, ids, wideMatch.ID)
assert.Contains(t, ids, narrowMatch.ID)
assert.Contains(t, ids, descendantWideMatch.ID)
assert.Contains(t, ids, descendantNarrowMatch.ID)

query.MatchAllTags = true
results, err = tRepos.Entities.QueryByGroup(context.Background(), tGroup.ID, query)
require.NoError(t, err)

ids = ids[:0]
for _, item := range results.Items {
ids = append(ids, item.ID)
}
assert.Contains(t, ids, wideMatch.ID)
assert.Contains(t, ids, descendantWideMatch.ID)
assert.NotContains(t, ids, narrowMatch.ID)
assert.NotContains(t, ids, descendantNarrowMatch.ID)
}

func TestEntityRepository_Update(t *testing.T) {
entities := useEntities(t, 3)

Expand Down
3 changes: 2 additions & 1 deletion frontend/lib/api/classes/items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export type ItemsQuery = {
parentIds?: string[];
tags?: string[];
negateTags?: boolean;
matchAllTags?: boolean;
onlyWithoutPhoto?: boolean;
onlyWithPhoto?: boolean;
q?: string;
Expand Down Expand Up @@ -203,7 +204,7 @@ export class ItemsApi extends BaseAPI {
return {
...resp,
data: resp.data?.items ?? [],
} as { data: EntitySummary[]; error: any; status: number };
} as { data: EntitySummary[]; error: unknown; status: number };
}

getTree(tq: TreeQuery = { withItems: false }) {
Expand Down
1 change: 1 addition & 0 deletions frontend/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,7 @@
"model_number": "Model Number",
"name": "Name",
"negate_tags": "Negate Selected Tags",
"match_all_tags": "Match All Selected Tags",
"next_page": "Next Page",
"no_attachments": "No attachments found",
"no_results": "No Items Found",
Expand Down
14 changes: 14 additions & 0 deletions frontend/pages/items.vue
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
const includeArchived = useOptionalRouteQuery("archived", false);
const fieldSelector = useOptionalRouteQuery("fieldSelector", false);
const negateTags = useOptionalRouteQuery("negateTags", false);
const matchAllTags = useOptionalRouteQuery("matchAllTags", false);
const onlyWithoutPhoto = useOptionalRouteQuery("onlyWithoutPhoto", false);
const onlyWithPhoto = useOptionalRouteQuery("onlyWithPhoto", false);
const orderBy = useOptionalRouteQuery("orderBy", "name");
Expand Down Expand Up @@ -207,6 +208,12 @@
}
});

watch(matchAllTags, (newV, oldV) => {
if (newV !== oldV) {
search();
}
});

watch(onlyWithoutPhoto, (newV, oldV) => {
if (newV && onlyWithPhoto.value) {
// this triggers the watch on onlyWithPhoto
Expand Down Expand Up @@ -275,6 +282,7 @@
archived: includeArchived.value,
fieldSelector: fieldSelector.value,
negateTags: negateTags.value,
matchAllTags: matchAllTags.value,
onlyWithoutPhoto: onlyWithoutPhoto.value,
onlyWithPhoto: onlyWithPhoto.value,
orderBy: orderBy.value,
Expand Down Expand Up @@ -312,6 +320,7 @@
parentIds: locIDs.value,
tags: tagIDs.value,
negateTags: negateTags.value,
matchAllTags: matchAllTags.value,
onlyWithoutPhoto: onlyWithoutPhoto.value,
onlyWithPhoto: onlyWithPhoto.value,
includeArchived: includeArchived.value,
Expand Down Expand Up @@ -426,6 +435,11 @@
<div class="grow" />
<span class="text-right"> {{ $t("items.negate_tags") }} </span>
</Label>
<Label class="flex cursor-pointer items-center">
<Switch v-model="matchAllTags" class="ml-auto" />
<div class="grow" />
<span class="text-right"> {{ $t("items.match_all_tags") }} </span>
</Label>
<Label class="flex cursor-pointer items-center">
<Switch v-model="onlyWithoutPhoto" class="ml-auto" />
<div class="grow" />
Expand Down
Loading