feat: add search filter for items with no photo (#383)

* feat: add search filter for items without photos

* chore: configure Golang formatter for VSCode

* fix: displaying long filter labels for some locales

* feat: add search filter for items with photos

* test: fix linter errors

* chore: remove redundant height attribute

* fix: make with/without photo filters mutually exclusive

---------

Co-authored-by: zebrapurring <>
This commit is contained in:
zebrapurring
2025-01-28 00:00:24 +01:00
committed by GitHub
parent 743be2fb2c
commit b22a49a0fd
6 changed files with 94 additions and 28 deletions

View File

@@ -30,5 +30,8 @@
"editor.quickSuggestions": { "editor.quickSuggestions": {
"strings": true "strings": true
}, },
"tailwindCSS.experimental.configFile": "./frontend/tailwind.config.js" "tailwindCSS.experimental.configFile": "./frontend/tailwind.config.js",
"[go]": {
"editor.defaultFormatter": "golang.go"
},
} }

View File

@@ -56,16 +56,18 @@ func (ctrl *V1Controller) HandleItemsGetAll() errchain.HandlerFunc {
} }
v := repo.ItemQuery{ v := repo.ItemQuery{
Page: queryIntOrNegativeOne(params.Get("page")), Page: queryIntOrNegativeOne(params.Get("page")),
PageSize: queryIntOrNegativeOne(params.Get("pageSize")), PageSize: queryIntOrNegativeOne(params.Get("pageSize")),
Search: params.Get("q"), Search: params.Get("q"),
LocationIDs: queryUUIDList(params, "locations"), LocationIDs: queryUUIDList(params, "locations"),
LabelIDs: queryUUIDList(params, "labels"), LabelIDs: queryUUIDList(params, "labels"),
NegateLabels: queryBool(params.Get("negateLabels")), NegateLabels: queryBool(params.Get("negateLabels")),
ParentItemIDs: queryUUIDList(params, "parentIds"), OnlyWithoutPhoto: queryBool(params.Get("onlyWithoutPhoto")),
IncludeArchived: queryBool(params.Get("includeArchived")), OnlyWithPhoto: queryBool(params.Get("onlyWithPhoto")),
Fields: filterFieldItems(params["fields"]), ParentItemIDs: queryUUIDList(params, "parentIds"),
OrderBy: params.Get("orderBy"), IncludeArchived: queryBool(params.Get("includeArchived")),
Fields: filterFieldItems(params["fields"]),
OrderBy: params.Get("orderBy"),
} }
if strings.HasPrefix(v.Search, "#") { if strings.HasPrefix(v.Search, "#") {

View File

@@ -30,18 +30,20 @@ type (
} }
ItemQuery struct { ItemQuery struct {
Page int Page int
PageSize int PageSize int
Search string `json:"search"` Search string `json:"search"`
AssetID AssetID `json:"assetId"` AssetID AssetID `json:"assetId"`
LocationIDs []uuid.UUID `json:"locationIds"` LocationIDs []uuid.UUID `json:"locationIds"`
LabelIDs []uuid.UUID `json:"labelIds"` LabelIDs []uuid.UUID `json:"labelIds"`
NegateLabels bool `json:"negateLabels"` NegateLabels bool `json:"negateLabels"`
ParentItemIDs []uuid.UUID `json:"parentIds"` OnlyWithoutPhoto bool `json:"onlyWithoutPhoto"`
SortBy string `json:"sortBy"` OnlyWithPhoto bool `json:"onlyWithPhoto"`
IncludeArchived bool `json:"includeArchived"` ParentItemIDs []uuid.UUID `json:"parentIds"`
Fields []FieldQuery `json:"fields"` SortBy string `json:"sortBy"`
OrderBy string `json:"orderBy"` IncludeArchived bool `json:"includeArchived"`
Fields []FieldQuery `json:"fields"`
OrderBy string `json:"orderBy"`
} }
ItemField struct { ItemField struct {
@@ -385,6 +387,27 @@ func (e *ItemsRepository) QueryByGroup(ctx context.Context, gid uuid.UUID, q Ite
} }
} }
if q.OnlyWithoutPhoto {
andPredicates = append(andPredicates, item.Not(
item.HasAttachmentsWith(
attachment.And(
attachment.Primary(true),
attachment.TypeEQ(attachment.TypePhoto),
),
)),
)
}
if q.OnlyWithPhoto {
andPredicates = append(andPredicates, item.HasAttachmentsWith(
attachment.And(
attachment.Primary(true),
attachment.TypeEQ(attachment.TypePhoto),
),
),
)
}
if len(q.LocationIDs) > 0 { if len(q.LocationIDs) > 0 {
locationPredicates := make([]predicate.Item, 0, len(q.LocationIDs)) locationPredicates := make([]predicate.Item, 0, len(q.LocationIDs))
for _, l := range q.LocationIDs { for _, l := range q.LocationIDs {

View File

@@ -24,6 +24,8 @@ export type ItemsQuery = {
locations?: string[]; locations?: string[];
labels?: string[]; labels?: string[];
negateLabels?: boolean; negateLabels?: boolean;
onlyWithoutPhoto?: boolean;
onlyWithPhoto?: boolean;
parentIds?: string[]; parentIds?: string[];
q?: string; q?: string;
fields?: string[]; fields?: string[];

View File

@@ -179,6 +179,8 @@
"model_number": "Model Number", "model_number": "Model Number",
"name": "Name", "name": "Name",
"negate_labels": "Negate Selected Labels", "negate_labels": "Negate Selected Labels",
"only_without_photo": "Only items without photo",
"only_with_photo": "Only items with photo",
"next_page": "Next Page", "next_page": "Next Page",
"no_results": "No Items Found", "no_results": "No Items Found",
"notes": "Notes", "notes": "Notes",

View File

@@ -41,6 +41,8 @@
const includeArchived = useRouteQuery("archived", false); const includeArchived = useRouteQuery("archived", false);
const fieldSelector = useRouteQuery("fieldSelector", false); const fieldSelector = useRouteQuery("fieldSelector", false);
const negateLabels = useRouteQuery("negateLabels", false); const negateLabels = useRouteQuery("negateLabels", false);
const onlyWithoutPhoto = useRouteQuery("onlyWithoutPhoto", false);
const onlyWithPhoto = useRouteQuery("onlyWithPhoto", false);
const orderBy = useRouteQuery("orderBy", "name"); const orderBy = useRouteQuery("orderBy", "name");
const totalPages = computed(() => Math.ceil(total.value / pageSize.value)); const totalPages = computed(() => Math.ceil(total.value / pageSize.value));
@@ -177,6 +179,24 @@
} }
}); });
watch(onlyWithoutPhoto, (newV, oldV) => {
if (newV && onlyWithPhoto) {
onlyWithPhoto.value = false;
}
if (newV !== oldV) {
search();
}
});
watch(onlyWithPhoto, (newV, oldV) => {
if (newV && onlyWithoutPhoto) {
onlyWithoutPhoto.value = false;
}
if (newV !== oldV) {
search();
}
});
watch(orderBy, (newV, oldV) => { watch(orderBy, (newV, oldV) => {
if (newV !== oldV) { if (newV !== oldV) {
search(); search();
@@ -215,6 +235,8 @@
pageSize: pageSize.value, pageSize: pageSize.value,
includeArchived: includeArchived.value ? "true" : "false", includeArchived: includeArchived.value ? "true" : "false",
negateLabels: negateLabels.value ? "true" : "false", negateLabels: negateLabels.value ? "true" : "false",
onlyWithoutPhoto: onlyWithoutPhoto.value ? "true" : "false",
onlyWithPhoto: onlyWithPhoto.value ? "true" : "false",
orderBy: orderBy.value, orderBy: orderBy.value,
}, },
}); });
@@ -243,6 +265,8 @@
locations: locIDs.value, locations: locIDs.value,
labels: labIDs.value, labels: labIDs.value,
negateLabels: negateLabels.value, negateLabels: negateLabels.value,
onlyWithoutPhoto: onlyWithoutPhoto.value,
onlyWithPhoto: onlyWithPhoto.value,
includeArchived: includeArchived.value, includeArchived: includeArchived.value,
page: page.value, page: page.value,
pageSize: pageSize.value, pageSize: pageSize.value,
@@ -294,6 +318,8 @@
archived: includeArchived.value ? "true" : "false", archived: includeArchived.value ? "true" : "false",
fieldSelector: fieldSelector.value ? "true" : "false", fieldSelector: fieldSelector.value ? "true" : "false",
negateLabels: negateLabels.value ? "true" : "false", negateLabels: negateLabels.value ? "true" : "false",
onlyWithoutPhoto: onlyWithoutPhoto.value ? "true" : "false",
onlyWithPhoto: onlyWithPhoto.value ? "true" : "false",
orderBy: orderBy.value, orderBy: orderBy.value,
pageSize: pageSize.value, pageSize: pageSize.value,
page: page.value, page: page.value,
@@ -377,19 +403,27 @@
<label tabindex="0" class="btn btn-xs">{{ $t("items.options") }}</label> <label tabindex="0" class="btn btn-xs">{{ $t("items.options") }}</label>
<div <div
tabindex="0" tabindex="0"
class="dropdown-content mt-1 max-h-72 w-64 -translate-x-24 overflow-auto rounded-md bg-base-100 p-4 shadow" class="dropdown-content mt-1 w-72 -translate-x-24 overflow-auto rounded-md bg-base-100 p-4 shadow"
> >
<label class="label mr-auto cursor-pointer"> <label class="label mr-auto cursor-pointer">
<input v-model="includeArchived" type="checkbox" class="toggle toggle-primary toggle-sm" /> <input v-model="includeArchived" type="checkbox" class="toggle toggle-primary toggle-sm" />
<span class="label-text ml-4"> {{ $t("items.include_archive") }} </span> <span class="label-text ml-4 text-right"> {{ $t("items.include_archive") }} </span>
</label> </label>
<label class="label mr-auto cursor-pointer"> <label class="label mr-auto cursor-pointer">
<input v-model="fieldSelector" type="checkbox" class="toggle toggle-primary toggle-sm" /> <input v-model="fieldSelector" type="checkbox" class="toggle toggle-primary toggle-sm" />
<span class="label-text ml-4"> {{ $t("items.field_selector") }} </span> <span class="label-text ml-4 text-right"> {{ $t("items.field_selector") }} </span>
</label> </label>
<label class="label mr-auto cursor-pointer"> <label class="label mr-auto cursor-pointer">
<input v-model="negateLabels" type="checkbox" class="toggle toggle-primary toggle-sm" /> <input v-model="negateLabels" type="checkbox" class="toggle toggle-primary toggle-sm" />
<span class="label-text ml-4"> {{ $t("items.negate_labels") }} </span> <span class="label-text ml-4 text-right"> {{ $t("items.negate_labels") }} </span>
</label>
<label class="label mr-auto cursor-pointer">
<input v-model="onlyWithoutPhoto" type="checkbox" class="toggle toggle-primary toggle-sm" />
<span class="label-text ml-4 text-right"> {{ $t("items.only_without_photo") }} </span>
</label>
<label class="label mr-auto cursor-pointer">
<input v-model="onlyWithPhoto" type="checkbox" class="toggle toggle-primary toggle-sm" />
<span class="label-text ml-4 text-right"> {{ $t("items.only_with_photo") }} </span>
</label> </label>
<label class="label mr-auto cursor-pointer"> <label class="label mr-auto cursor-pointer">
<select v-model="orderBy" class="select select-bordered select-sm"> <select v-model="orderBy" class="select select-bordered select-sm">
@@ -397,7 +431,7 @@
<option value="createdAt">{{ $t("items.created_at") }}</option> <option value="createdAt">{{ $t("items.created_at") }}</option>
<option value="updatedAt">{{ $t("items.updated_at") }}</option> <option value="updatedAt">{{ $t("items.updated_at") }}</option>
</select> </select>
<span class="label-text ml-4"> {{ $t("items.order_by") }} </span> <span class="label-text ml-4 text-right"> {{ $t("items.order_by") }} </span>
</label> </label>
<hr class="my-2" /> <hr class="my-2" />
<BaseButton class="btn-sm btn-block" @click="reset"> {{ $t("items.reset_search") }} </BaseButton> <BaseButton class="btn-sm btn-block" @click="reset"> {{ $t("items.reset_search") }} </BaseButton>