mirror of
https://github.com/sysadminsmedia/homebox.git
synced 2025-12-21 13:23:14 +01:00
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:
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -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"
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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, "#") {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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[];
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user