Skip to content

Commit 4786356

Browse files
committed
feat: add clear filter icon for each filter
1 parent 417d33b commit 4786356

File tree

1 file changed

+128
-110
lines changed

1 file changed

+128
-110
lines changed

adminforth/spa/src/components/Filters.vue

Lines changed: 128 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -20,116 +20,132 @@
2020
<div class="py-4 ">
2121
<ul class="space-y-3 font-medium">
2222
<li v-for="c in columnsWithFilter" :key="c">
23-
<p class="dark:text-gray-400">{{ c.label }}</p>
24-
<component
25-
v-if="c.components?.filter"
26-
:is="getCustomComponent(c.components.filter)"
27-
:meta="c?.components?.list?.meta"
28-
:column="c"
29-
class="w-full"
30-
@update:modelValue="(filtersArray) => {
31-
filtersStore.filters = filtersStore.filters.filter(f => f.field !== c.name);
32-
33-
for (const f of filtersArray) {
34-
filtersStore.filters.push({ field: c.name, ...f });
35-
}
36-
console.log('filtersStore.filters', filtersStore.filters);
37-
emits('update:filters', [...filtersStore.filters]);
38-
}"
39-
:modelValue="filtersStore.filters.filter(f => f.field === c.name)"
40-
/>
41-
<Select
42-
v-else-if="c.foreignResource"
43-
:multiple="c.filterOptions.multiselect"
44-
class="w-full"
45-
:options="columnOptions[c.name] || []"
46-
:searchDisabled="!c.foreignResource.searchableFields"
47-
@scroll-near-end="loadMoreOptions(c.name)"
48-
@search="(searchTerm) => {
49-
if (c.foreignResource.searchableFields && onSearchInput[c.name]) {
50-
onSearchInput[c.name](searchTerm);
51-
}
52-
}"
53-
@update:modelValue="onFilterInput[c.name]({ column: c, operator: c.filterOptions.multiselect ? 'in' : 'eq', value: c.filterOptions.multiselect ? ($event.length ? $event : undefined) : $event || undefined })"
54-
:modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === (c.filterOptions.multiselect ? 'in' : 'eq'))?.value || (c.filterOptions.multiselect ? [] : '')"
55-
>
56-
<template #extra-item v-if="columnLoadingState[c.name]?.loading">
57-
<div class="text-center text-gray-400 dark:text-gray-300 py-2 flex items-center justify-center gap-2">
58-
<Spinner class="w-4 h-4" />
59-
{{ $t('Loading...') }}
60-
</div>
61-
</template>
62-
</Select>
63-
<Select
64-
:multiple="c.filterOptions.multiselect"
65-
class="w-full"
66-
v-else-if="c.type === 'boolean'"
67-
:options="[
68-
{ label: $t('Yes'), value: true },
69-
{ label: $t('No'), value: false },
70-
// if field is not required, undefined might be there, and user might want to filter by it
71-
...(c.required ? [] : [ { label: $t('Unset'), value: undefined } ])
72-
]"
73-
@update:modelValue="onFilterInput[c.name]({ column: c, operator: c.filterOptions.multiselect ? 'in' : 'eq', value: c.filterOptions.multiselect ? ($event.length ? $event : undefined) : $event })"
74-
:modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === (c.filterOptions.multiselect ? 'in' : 'eq'))?.value !== undefined
75-
? filtersStore.filters.find(f => f.field === c.name && f.operator === (c.filterOptions.multiselect ? 'in' : 'eq'))?.value
76-
: (c.filterOptions.multiselect ? [] : '')"
77-
/>
78-
79-
<Select
80-
:multiple="c.filterOptions.multiselect"
81-
class="w-full"
82-
v-else-if="c.enum"
83-
:options="c.enum"
84-
@update:modelValue="onFilterInput[c.name]({ column: c, operator: c.filterOptions.multiselect ? 'in' : 'eq', value: c.filterOptions.multiselect ? ($event.length ? $event : undefined) : $event || undefined })"
85-
:modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === (c.filterOptions.multiselect ? 'in' : 'eq'))?.value || (c.filterOptions.multiselect ? [] : '')"
86-
/>
87-
88-
<Input
89-
v-else-if="['string', 'text', 'json', 'richtext', 'unknown'].includes(c.type)"
90-
type="text"
91-
full-width
92-
:placeholder="$t('Search')"
93-
@update:modelValue="onFilterInput[c.name]({ column: c, operator: c.filterOptions?.substringSearch ? 'ilike' : 'eq', value: $event || undefined })"
94-
:modelValue="getFilterItem({ column: c, operator: c.filterOptions?.substringSearch ? 'ilike' : 'eq' })"
95-
/>
96-
97-
<CustomDateRangePicker
98-
v-else-if="['datetime', 'date', 'time'].includes(c.type)"
99-
:column="c"
100-
:valueStart="filtersStore.filters.find(f => f.field === c.name && f.operator === 'gte')?.value || undefined"
101-
@update:valueStart="onFilterInput[c.name]({ column: c, operator: 'gte', value: $event || undefined })"
102-
:valueEnd="filtersStore.filters.find(f => f.field === c.name && f.operator === 'lte')?.value || undefined"
103-
@update:valueEnd="onFilterInput[c.name]({ column: c, operator: 'lte', value: $event || undefined })"
104-
/>
105-
106-
<CustomRangePicker
107-
v-else-if="['integer', 'decimal', 'float'].includes(c.type) && c.allowMinMaxQuery"
108-
:min="getFilterMinValue(c.name)"
109-
:max="getFilterMaxValue(c.name)"
110-
:valueStart="getFilterItem({ column: c, operator: 'gte' })"
111-
@update:valueStart="onFilterInput[c.name]({ column: c, operator: 'gte', value: ($event !== '' && $event !== null) ? $event : undefined })"
112-
:valueEnd="getFilterItem({ column: c, operator: 'lte' })"
113-
@update:valueEnd="onFilterInput[c.name]({ column: c, operator: 'lte', value: ($event !== '' && $event !== null) ? $event : undefined })"
114-
/>
115-
116-
<div v-else-if="['integer', 'decimal', 'float'].includes(c.type)" class="flex gap-2">
117-
<Input
118-
type="number"
119-
aria-describedby="helper-text-explanation"
120-
:placeholder="$t('From')"
121-
@update:modelValue="onFilterInput[c.name]({ column: c, operator: 'gte', value: ($event !== '' && $event !== null) ? $event : undefined })"
122-
:modelValue="getFilterItem({ column: c, operator: 'gte' })"
123-
/>
124-
<Input
125-
type="number"
126-
aria-describedby="helper-text-explanation"
127-
:placeholder="$t('To')"
128-
@update:modelValue="onFilterInput[c.name]({ column: c, operator: 'lte', value: ($event !== '' && $event !== null) ? $event : undefined })"
129-
:modelValue="getFilterItem({ column: c, operator: 'lte' })"
130-
/>
131-
</div>
132-
23+
<div class="flex flex-col">
24+
<div class="flex justify-between items-center">
25+
<p class="dark:text-gray-400">{{ c.label }}</p>
26+
<Tooltip>
27+
<button
28+
class=" flex items-center justify-center w-7 h-7 my-1 hover:border rounded-md hover:bg-gray-100 dark:hover:bg-gray-700"
29+
:class="filtersStore.filters.find(f => f.field === c.name) ?? 'opacity-50'"
30+
:disabled="!filtersStore.filters.find(f => f.field === c.name)"
31+
@click="filtersStore.clearFilter(c.name); console.log('Filter setted to empty');"
32+
>
33+
<IconMinusOutline />
34+
</button>
35+
<template #tooltip>
36+
Clear filter
37+
</template>
38+
</Tooltip>
39+
</div>
40+
<component
41+
v-if="c.components?.filter"
42+
:is="getCustomComponent(c.components.filter)"
43+
:meta="c?.components?.list?.meta"
44+
:column="c"
45+
class="w-full"
46+
@update:modelValue="(filtersArray) => {
47+
filtersStore.filters = filtersStore.filters.filter(f => f.field !== c.name);
48+
49+
for (const f of filtersArray) {
50+
filtersStore.filters.push({ field: c.name, ...f });
51+
}
52+
console.log('filtersStore.filters', filtersStore.filters);
53+
emits('update:filters', [...filtersStore.filters]);
54+
}"
55+
:modelValue="filtersStore.filters.filter(f => f.field === c.name)"
56+
/>
57+
<Select
58+
v-else-if="c.foreignResource"
59+
:multiple="c.filterOptions.multiselect"
60+
class="w-full"
61+
:options="columnOptions[c.name] || []"
62+
:searchDisabled="!c.foreignResource.searchableFields"
63+
@scroll-near-end="loadMoreOptions(c.name)"
64+
@search="(searchTerm) => {
65+
if (c.foreignResource.searchableFields && onSearchInput[c.name]) {
66+
onSearchInput[c.name](searchTerm);
67+
}
68+
}"
69+
@update:modelValue="onFilterInput[c.name]({ column: c, operator: c.filterOptions.multiselect ? 'in' : 'eq', value: c.filterOptions.multiselect ? ($event.length ? $event : undefined) : $event || undefined })"
70+
:modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === (c.filterOptions.multiselect ? 'in' : 'eq'))?.value || (c.filterOptions.multiselect ? [] : '')"
71+
>
72+
<template #extra-item v-if="columnLoadingState[c.name]?.loading">
73+
<div class="text-center text-gray-400 dark:text-gray-300 py-2 flex items-center justify-center gap-2">
74+
<Spinner class="w-4 h-4" />
75+
{{ $t('Loading...') }}
76+
</div>
77+
</template>
78+
</Select>
79+
<Select
80+
:multiple="c.filterOptions.multiselect"
81+
class="w-full"
82+
v-else-if="c.type === 'boolean'"
83+
:options="[
84+
{ label: $t('Yes'), value: true },
85+
{ label: $t('No'), value: false },
86+
// if field is not required, undefined might be there, and user might want to filter by it
87+
...(c.required ? [] : [ { label: $t('Unset'), value: undefined } ])
88+
]"
89+
@update:modelValue="onFilterInput[c.name]({ column: c, operator: c.filterOptions.multiselect ? 'in' : 'eq', value: c.filterOptions.multiselect ? ($event.length ? $event : undefined) : $event })"
90+
:modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === (c.filterOptions.multiselect ? 'in' : 'eq'))?.value !== undefined
91+
? filtersStore.filters.find(f => f.field === c.name && f.operator === (c.filterOptions.multiselect ? 'in' : 'eq'))?.value
92+
: (c.filterOptions.multiselect ? [] : '')"
93+
/>
94+
95+
<Select
96+
:multiple="c.filterOptions.multiselect"
97+
class="w-full"
98+
v-else-if="c.enum"
99+
:options="c.enum"
100+
@update:modelValue="onFilterInput[c.name]({ column: c, operator: c.filterOptions.multiselect ? 'in' : 'eq', value: c.filterOptions.multiselect ? ($event.length ? $event : undefined) : $event || undefined })"
101+
:modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === (c.filterOptions.multiselect ? 'in' : 'eq'))?.value || (c.filterOptions.multiselect ? [] : '')"
102+
/>
103+
104+
<Input
105+
v-else-if="['string', 'text', 'json', 'richtext', 'unknown'].includes(c.type)"
106+
type="text"
107+
full-width
108+
:placeholder="$t('Search')"
109+
@update:modelValue="onFilterInput[c.name]({ column: c, operator: c.filterOptions?.substringSearch ? 'ilike' : 'eq', value: $event || undefined })"
110+
:modelValue="getFilterItem({ column: c, operator: c.filterOptions?.substringSearch ? 'ilike' : 'eq' })"
111+
/>
112+
113+
<CustomDateRangePicker
114+
v-else-if="['datetime', 'date', 'time'].includes(c.type)"
115+
:column="c"
116+
:valueStart="filtersStore.filters.find(f => f.field === c.name && f.operator === 'gte')?.value || undefined"
117+
@update:valueStart="onFilterInput[c.name]({ column: c, operator: 'gte', value: $event || undefined })"
118+
:valueEnd="filtersStore.filters.find(f => f.field === c.name && f.operator === 'lte')?.value || undefined"
119+
@update:valueEnd="onFilterInput[c.name]({ column: c, operator: 'lte', value: $event || undefined })"
120+
/>
121+
122+
<CustomRangePicker
123+
v-else-if="['integer', 'decimal', 'float'].includes(c.type) && c.allowMinMaxQuery"
124+
:min="getFilterMinValue(c.name)"
125+
:max="getFilterMaxValue(c.name)"
126+
:valueStart="getFilterItem({ column: c, operator: 'gte' })"
127+
@update:valueStart="onFilterInput[c.name]({ column: c, operator: 'gte', value: ($event !== '' && $event !== null) ? $event : undefined })"
128+
:valueEnd="getFilterItem({ column: c, operator: 'lte' })"
129+
@update:valueEnd="onFilterInput[c.name]({ column: c, operator: 'lte', value: ($event !== '' && $event !== null) ? $event : undefined })"
130+
/>
131+
132+
<div v-else-if="['integer', 'decimal', 'float'].includes(c.type)" class="flex gap-2">
133+
<Input
134+
type="number"
135+
aria-describedby="helper-text-explanation"
136+
:placeholder="$t('From')"
137+
@update:modelValue="onFilterInput[c.name]({ column: c, operator: 'gte', value: ($event !== '' && $event !== null) ? $event : undefined })"
138+
:modelValue="getFilterItem({ column: c, operator: 'gte' })"
139+
/>
140+
<Input
141+
type="number"
142+
aria-describedby="helper-text-explanation"
143+
:placeholder="$t('To')"
144+
@update:modelValue="onFilterInput[c.name]({ column: c, operator: 'lte', value: ($event !== '' && $event !== null) ? $event : undefined })"
145+
:modelValue="getFilterItem({ column: c, operator: 'lte' })"
146+
/>
147+
</div>
148+
</div>
133149
</li>
134150
</ul>
135151
</div>
@@ -162,6 +178,8 @@ import Input from '@/afcl/Input.vue';
162178
import Select from '@/afcl/Select.vue';
163179
import Spinner from '@/afcl/Spinner.vue';
164180
import debounce from 'debounce';
181+
import { Tooltip } from '@/afcl';
182+
import { IconMinusOutline, IconTrashBinSolid } from '@iconify-prerendered/vue-flowbite';
165183
166184
const filtersStore = useFiltersStore();
167185
const { t } = useI18n();

0 commit comments

Comments
 (0)