|
20 | 20 | <div class="py-4 "> |
21 | 21 | <ul class="space-y-3 font-medium"> |
22 | 22 | <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> |
133 | 149 | </li> |
134 | 150 | </ul> |
135 | 151 | </div> |
@@ -162,6 +178,8 @@ import Input from '@/afcl/Input.vue'; |
162 | 178 | import Select from '@/afcl/Select.vue'; |
163 | 179 | import Spinner from '@/afcl/Spinner.vue'; |
164 | 180 | import debounce from 'debounce'; |
| 181 | +import { Tooltip } from '@/afcl'; |
| 182 | +import { IconMinusOutline, IconTrashBinSolid } from '@iconify-prerendered/vue-flowbite'; |
165 | 183 |
|
166 | 184 | const filtersStore = useFiltersStore(); |
167 | 185 | const { t } = useI18n(); |
|
0 commit comments