Skip to content

Commit 8ff2169

Browse files
committed
feat: add search in lists
1 parent 9ac4a6c commit 8ff2169

3 files changed

Lines changed: 202 additions & 2 deletions

File tree

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
<template>
2+
<div class="pantry-filter">
3+
<NcTextField
4+
v-model="localQuery"
5+
:placeholder="strings.placeholder"
6+
:show-trailing-button="localQuery.length > 0"
7+
trailing-button-icon="close"
8+
@trailing-button-click="localQuery = ''"
9+
>
10+
<template #icon>
11+
<MagnifyIcon :size="18" />
12+
</template>
13+
</NcTextField>
14+
15+
<div v-if="categoryOptions.length > 0" class="pantry-filter__categories">
16+
<NcChip
17+
:variant="selectedIds.length === 0 ? 'primary' : 'secondary'"
18+
class="pantry-filter__chip"
19+
no-close
20+
@click="$emit('update:selectedCategoryIds', [])"
21+
>
22+
<template #icon>
23+
<CheckIcon v-if="selectedIds.length === 0" :size="16" />
24+
</template>
25+
<span class="pantry-filter__chip-content">
26+
{{ strings.all }}
27+
<NcCounterBubble :count="totalCount" />
28+
</span>
29+
</NcChip>
30+
<NcChip
31+
v-for="opt in categoryOptions"
32+
:key="opt.category.id"
33+
:variant="selectedIds.includes(opt.category.id) ? 'primary' : 'secondary'"
34+
class="pantry-filter__chip"
35+
no-close
36+
@click="toggleCategory(opt.category.id)"
37+
>
38+
<template #icon>
39+
<component
40+
:is="iconFor(opt.category.icon)"
41+
:size="16"
42+
:style="{ color: opt.category.color }"
43+
/>
44+
</template>
45+
<span class="pantry-filter__chip-content">
46+
{{ opt.category.name }}
47+
<NcCounterBubble :count="opt.count" />
48+
</span>
49+
</NcChip>
50+
</div>
51+
</div>
52+
</template>
53+
54+
<script setup lang="ts">
55+
import { computed } from 'vue'
56+
import { t } from '@nextcloud/l10n'
57+
import NcTextField from '@nextcloud/vue/components/NcTextField'
58+
import NcChip from '@nextcloud/vue/components/NcChip'
59+
import NcCounterBubble from '@nextcloud/vue/components/NcCounterBubble'
60+
import MagnifyIcon from '@icons/Magnify.vue'
61+
import CheckIcon from '@icons/Check.vue'
62+
import { categoryIconComponent } from '@/components/CategoryPicker/categoryIcons'
63+
import type { Category, ChecklistItem } from '@/api/types'
64+
65+
const props = defineProps<{
66+
query: string
67+
selectedCategoryIds: number[]
68+
items: ChecklistItem[]
69+
categories: Category[]
70+
}>()
71+
72+
const emit = defineEmits<{
73+
(e: 'update:query', v: string): void
74+
(e: 'update:selectedCategoryIds', v: number[]): void
75+
}>()
76+
77+
const localQuery = computed({
78+
get: () => props.query,
79+
set: (v: string) => emit('update:query', v),
80+
})
81+
82+
const totalCount = computed(() => props.items.length)
83+
84+
interface CategoryOption {
85+
category: Category
86+
count: number
87+
}
88+
89+
const categoryOptions = computed<CategoryOption[]>(() => {
90+
const counts = new Map<number, number>()
91+
for (const item of props.items) {
92+
if (item.categoryId != null) {
93+
counts.set(item.categoryId, (counts.get(item.categoryId) ?? 0) + 1)
94+
}
95+
}
96+
return props.categories
97+
.filter((c) => counts.has(c.id))
98+
.map((c) => ({ category: c, count: counts.get(c.id)! }))
99+
})
100+
101+
const selectedIds = computed(() => props.selectedCategoryIds)
102+
103+
function toggleCategory(id: number) {
104+
const current = selectedIds.value
105+
if (current.includes(id)) {
106+
emit(
107+
'update:selectedCategoryIds',
108+
current.filter((cid) => cid !== id),
109+
)
110+
} else {
111+
emit('update:selectedCategoryIds', [...current, id])
112+
}
113+
}
114+
115+
function iconFor(key: string) {
116+
return categoryIconComponent(key)
117+
}
118+
119+
const strings = {
120+
placeholder: t('pantry', 'Type to filter …'),
121+
all: t('pantry', 'All'),
122+
}
123+
</script>
124+
125+
<style scoped lang="scss">
126+
.pantry-filter {
127+
display: flex;
128+
flex-direction: column;
129+
gap: 0.5rem;
130+
131+
&__categories {
132+
display: flex;
133+
flex-wrap: wrap;
134+
gap: 0.25rem;
135+
align-items: center;
136+
}
137+
138+
&__chip :deep(*) {
139+
cursor: pointer;
140+
}
141+
142+
&__chip#{&}__chip {
143+
transition: background-color 0.15s ease;
144+
}
145+
146+
&__chip#{&}__chip:hover {
147+
background-color: color-mix(
148+
in srgb,
149+
var(--color-primary-element) 50%,
150+
var(--color-background-hover)
151+
);
152+
}
153+
154+
&__chip-content {
155+
display: inline-flex;
156+
align-items: center;
157+
gap: 0.5rem;
158+
}
159+
}
160+
</style>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as ChecklistFilter } from './ChecklistFilter.vue'

src/views/ChecklistDetail.vue

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@
4242
<div class="pantry-detail__body">
4343
<ChecklistAddForm :house-id="houseIdNum" :adding="adding" @add="handleAdd" />
4444

45+
<ChecklistFilter
46+
v-if="items.length > 0"
47+
v-model:query="filterQuery"
48+
v-model:selected-category-ids="filterCategoryIds"
49+
:items="items"
50+
:categories="categories.items.value"
51+
class="pantry-detail__filter"
52+
/>
53+
4554
<div v-if="loading" class="pantry-detail__center">
4655
<NcLoadingIcon :size="36" />
4756
</div>
@@ -197,6 +206,7 @@ import RadioboxMarkedIcon from '@icons/RadioboxMarked.vue'
197206
import TagIcon from '@icons/Tag.vue'
198207
import PageToolbar from '@/components/PageToolbar'
199208
import { ChecklistAddForm } from '@/components/ChecklistAddForm'
209+
import { ChecklistFilter } from '@/components/ChecklistFilter'
200210
import { ChecklistItemRow } from '@/components/ChecklistItemRow'
201211
import { ChecklistItemEditDialog } from '@/components/ChecklistItemEditDialog'
202212
import { ChecklistItemViewDialog } from '@/components/ChecklistItemViewDialog'
@@ -282,6 +292,28 @@ watch(
282292
},
283293
)
284294
295+
// ----- Filter -----
296+
297+
const filterQuery = ref('')
298+
const filterCategoryIds = ref<number[]>([])
299+
300+
const filteredItems = computed(() => {
301+
let result = items.value
302+
const catIds = filterCategoryIds.value
303+
if (catIds.length > 0) {
304+
result = result.filter((i) => i.categoryId != null && catIds.includes(i.categoryId))
305+
}
306+
const q = filterQuery.value.trim().toLowerCase()
307+
if (q) {
308+
result = result.filter(
309+
(i) =>
310+
i.name.toLowerCase().includes(q) ||
311+
(i.description && i.description.toLowerCase().includes(q)),
312+
)
313+
}
314+
return result
315+
})
316+
285317
// ----- Partitioned items -----
286318
287319
function sortWithinPartition(arr: ChecklistItem[]): ChecklistItem[] {
@@ -292,8 +324,10 @@ function sortWithinPartition(arr: ChecklistItem[]): ChecklistItem[] {
292324
}
293325
294326
const isCustomSort = computed(() => currentSort.value === 'custom')
295-
const uncheckedItems = computed(() => sortWithinPartition(items.value.filter((i) => !i.done)))
296-
const checkedItems = computed(() => sortWithinPartition(items.value.filter((i) => i.done)))
327+
const uncheckedItems = computed(() =>
328+
sortWithinPartition(filteredItems.value.filter((i) => !i.done)),
329+
)
330+
const checkedItems = computed(() => sortWithinPartition(filteredItems.value.filter((i) => i.done)))
297331
298332
// ----- Drag/drop reorder (custom sort, per partition) -----
299333
@@ -598,6 +632,11 @@ const strings = {
598632
margin: 0 auto;
599633
}
600634
635+
&__filter {
636+
margin-top: 1rem;
637+
margin-bottom: 1.5rem;
638+
}
639+
601640
&__center {
602641
display: flex;
603642
justify-content: center;

0 commit comments

Comments
 (0)