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 h-7 my-1" >{{ c.label }}</p >
26+ <Tooltip v-if =" filtersStore .filters .find (f => f .field === c .name )" >
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+ :disabled =" !filtersStore.filters.find(f => f.field === c.name)"
30+ @click =" filtersStore.clearFilter(c.name);"
31+ >
32+ <IconCloseOutline />
33+ </button >
34+ <template #tooltip >
35+ Clear filter
36+ </template >
37+ </Tooltip >
38+ </div >
39+ <component
40+ v-if =" c.components?.filter"
41+ :is =" getCustomComponent(c.components.filter)"
42+ :meta =" c?.components?.list?.meta"
43+ :column =" c"
44+ class =" w-full"
45+ @update:modelValue =" (filtersArray) => {
46+ filtersStore.filters = filtersStore.filters.filter(f => f.field !== c.name);
47+
48+ for (const f of filtersArray) {
49+ filtersStore.filters.push({ field: c.name, ...f });
50+ }
51+ console.log('filtersStore.filters', filtersStore.filters);
52+ emits('update:filters', [...filtersStore.filters]);
53+ }"
54+ :modelValue =" filtersStore.filters.filter(f => f.field === c.name)"
55+ />
56+ <Select
57+ v-else-if =" c .foreignResource "
58+ :multiple =" c .filterOptions .multiselect "
59+ class="w-full"
60+ :options =" columnOptions [c .name ] || []"
61+ :searchDisabled =" ! c .foreignResource .searchableFields "
62+ @scroll-near-end =" loadMoreOptions (c .name )"
63+ @search =" (searchTerm ) => {
64+ if (c .foreignResource .searchableFields && onSearchInput [c .name ]) {
65+ onSearchInput [c .name ](searchTerm );
66+ }
67+ } "
68+ @update :modelValue =" onFilterInput [c .name ]({ column: c , operator: c .filterOptions .multiselect ? ' in' : ' eq' , value: c .filterOptions .multiselect ? ($event .length ? $event : undefined ) : $event || undefined })"
69+ :modelValue =" filtersStore .filters .find (f => f .field === c .name && f .operator === (c .filterOptions .multiselect ? ' in' : ' eq' ))?.value || (c .filterOptions .multiselect ? [] : ' ' )"
70+ >
71+ <template #extra-item v-if =" columnLoadingState [c .name ]?.loading " >
72+ <div class =" text-center text-gray-400 dark:text-gray-300 py-2 flex items-center justify-center gap-2" >
73+ <Spinner class="w-4 h-4" />
74+ {{ $t('Loading...') }}
75+ </div >
76+ </template >
77+ </Select >
78+ <Select
79+ :multiple =" c .filterOptions .multiselect "
80+ class="w-full"
81+ v-else-if =" c .type === ' boolean' "
82+ :options =" [
83+ { label: $t (' Yes' ), value: true },
84+ { label: $t (' No' ), value: false },
85+ // if field is not required, undefined might be there, and user might want to filter by it
86+ ... (c .required ? [] : [ { label: $t (' Unset' ), value: undefined } ])
87+ ] "
88+ @update :modelValue =" onFilterInput [c .name ]({ column: c , operator: c .filterOptions .multiselect ? ' in' : ' eq' , value: c .filterOptions .multiselect ? ($event .length ? $event : undefined ) : $event })"
89+ :modelValue =" filtersStore .filters .find (f => f .field === c .name && f .operator === (c .filterOptions .multiselect ? ' in' : ' eq' ))?.value !== undefined
90+ ? filtersStore .filters .find (f => f .field === c .name && f .operator === (c .filterOptions .multiselect ? ' in' : ' eq' ))?.value
91+ : (c .filterOptions .multiselect ? [] : ' ' ) "
92+ />
93+
94+ <Select
95+ :multiple =" c .filterOptions .multiselect "
96+ class="w-full"
97+ v-else-if =" c .enum "
98+ :options =" c .enum "
99+ @update :modelValue =" onFilterInput [c .name ]({ column: c , operator: c .filterOptions .multiselect ? ' in' : ' eq' , value: c .filterOptions .multiselect ? ($event .length ? $event : undefined ) : $event || undefined })"
100+ :modelValue =" filtersStore .filters .find (f => f .field === c .name && f .operator === (c .filterOptions .multiselect ? ' in' : ' eq' ))?.value || (c .filterOptions .multiselect ? [] : ' ' )"
101+ />
102+
103+ <Input
104+ v-else-if =" [' string' , ' text' , ' json' , ' richtext' , ' unknown' ].includes (c .type ) "
105+ type="text"
106+ full-width
107+ :placeholder =" $t (' Search' )"
108+ @update :modelValue =" onFilterInput [c .name ]({ column: c , operator: c .filterOptions ?.substringSearch ? ' ilike' : ' eq' , value: $event || undefined })"
109+ :modelValue =" getFilterItem ({ column: c , operator: c .filterOptions ?.substringSearch ? ' ilike' : ' eq' })"
110+ />
111+
112+ <CustomDateRangePicker
113+ v-else-if =" [' datetime' , ' date' , ' time' ].includes (c .type ) "
114+ :column =" c "
115+ :valueStart =" filtersStore .filters .find (f => f .field === c .name && f .operator === ' gte' )?.value || undefined "
116+ @update :valueStart =" onFilterInput [c .name ]({ column: c , operator: ' gte' , value: $event || undefined })"
117+ :valueEnd =" filtersStore .filters .find (f => f .field === c .name && f .operator === ' lte' )?.value || undefined "
118+ @update :valueEnd =" onFilterInput [c .name ]({ column: c , operator: ' lte' , value: $event || undefined })"
119+ />
120+
121+ <CustomRangePicker
122+ v-else-if =" [' integer' , ' decimal' , ' float' ].includes (c .type ) && c .allowMinMaxQuery "
123+ :min =" getFilterMinValue (c .name )"
124+ :max =" getFilterMaxValue (c .name )"
125+ :valueStart =" getFilterItem ({ column: c , operator: ' gte' })"
126+ @update :valueStart =" onFilterInput [c .name ]({ column: c , operator: ' gte' , value: ($event !== ' ' && $event !== null ) ? $event : undefined })"
127+ :valueEnd =" getFilterItem ({ column: c , operator: ' lte' })"
128+ @update :valueEnd =" onFilterInput [c .name ]({ column: c , operator: ' lte' , value: ($event !== ' ' && $event !== null ) ? $event : undefined })"
129+ />
130+
131+ <div v-else-if =" ['integer', 'decimal', 'float'].includes(c.type)" class =" flex gap-2" >
132+ <Input
133+ type="number"
134+ aria-describedby="helper-text-explanation"
135+ :placeholder =" $t (' From' )"
136+ @update :modelValue =" onFilterInput [c .name ]({ column: c , operator: ' gte' , value: ($event !== ' ' && $event !== null ) ? $event : undefined })"
137+ :modelValue =" getFilterItem ({ column: c , operator: ' gte' })"
138+ />
139+ <Input
140+ type="number"
141+ aria-describedby="helper-text-explanation"
142+ :placeholder =" $t (' To' )"
143+ @update :modelValue =" onFilterInput [c .name ]({ column: c , operator: ' lte' , value: ($event !== ' ' && $event !== null ) ? $event : undefined })"
144+ :modelValue =" getFilterItem ({ column: c , operator: ' lte' })"
145+ />
146+ </div >
147+ </div >
133148 </li >
134149 </ul >
135150 </div >
138153 <button
139154 :disabled =" !filtersStore.visibleFiltersCount"
140155 type =" button"
141- class =" flex items-center py-1 px -3 text-sm font-medium text-lightFiltersClearAllButtonText focus:outline-none bg-lightFiltersClearAllButtonBackground rounded border border-lightFiltersClearAllButtonBorder hover:bg-lightFiltersClearAllButtonBackgroundHover hover:text-lightFiltersClearAllButtonTextHover focus:z-10 focus:ring-4 focus:ring-lightFiltersClearAllButtonFocus dark:focus:ring-darkFiltersClearAllButtonFocus dark:bg-darkFiltersClearAllButtonBackground dark:text-darkFiltersClearAllButtonText dark:border-darkFiltersClearAllButtonBorder dark:hover:text-darkFiltersClearAllButtonTextHover dark:hover:bg-darkFiltersClearAllButtonBackgroundHover disabled:opacity-50 disabled:cursor-not-allowed"
142- @click =" clear" >{{ $t('Clear all') }}</button >
156+ class =" flex gap-1 items-center py-1 pr -3 text-sm font-medium text-lightFiltersClearAllButtonText focus:outline-none bg-lightFiltersClearAllButtonBackground rounded border border-lightFiltersClearAllButtonBorder hover:bg-lightFiltersClearAllButtonBackgroundHover hover:text-lightFiltersClearAllButtonTextHover focus:z-10 focus:ring-4 focus:ring-lightFiltersClearAllButtonFocus dark:focus:ring-darkFiltersClearAllButtonFocus dark:bg-darkFiltersClearAllButtonBackground dark:text-darkFiltersClearAllButtonText dark:border-darkFiltersClearAllButtonBorder dark:hover:text-darkFiltersClearAllButtonTextHover dark:hover:bg-darkFiltersClearAllButtonBackgroundHover disabled:opacity-50 disabled:cursor-not-allowed"
157+ @click =" clear" >< IconCloseOutline class="ml-3"/> {{ $t('Clear all') }}</button >
143158
144159 </div >
145160 </div >
@@ -162,6 +177,8 @@ import Input from '@/afcl/Input.vue';
162177import Select from ' @/afcl/Select.vue' ;
163178import Spinner from ' @/afcl/Spinner.vue' ;
164179import debounce from ' debounce' ;
180+ import { Tooltip } from ' @/afcl' ;
181+ import { IconCloseOutline } from ' @iconify-prerendered/vue-flowbite' ;
165182
166183const filtersStore = useFiltersStore ();
167184const { t } = useI18n ();
0 commit comments