1+ <template >
2+ <div v-if =" columnsWithFilter && columnsWithFilter.length > 0" class =" flex flex-col w-full p-4 mb-4 rounded-lg border border-gray-100 dark:border-gray-700 shadow-sm dark:shadow-lg text-gray-900 dark:text-white" >
3+ <p
4+ class =" hover:underline cursor-pointer text-blue-700 dark:text-blue-500 text-end"
5+ @click =" isExpanded = !isExpanded"
6+ >
7+ {{ isExpanded ? 'Hide filters' : 'Show filters' }}
8+ </p >
9+ <div v-if =" isExpanded" class =" md:grid md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-6 gap-4 w-full" >
10+ <div class =" flex flex-col" v-for =" c in columnsWithFilter" :key =" c" >
11+ <div class =" min-w-48" >
12+ <p class =" dark:text-gray-400" >{{ c.label }}</p >
13+ <Select
14+ v-if =" c.foreignResource"
15+ :multiple =" c.filterOptions.multiselect"
16+ class =" w-full"
17+ :options =" columnOptions[c.name] || []"
18+ :searchDisabled =" !c.foreignResource.searchableFields"
19+ @scroll-near-end =" loadMoreOptions(c.name)"
20+ @search =" (searchTerm) => {
21+ if (c.foreignResource.searchableFields && onSearchInput[c.name]) {
22+ onSearchInput[c.name](searchTerm);
23+ }
24+ }"
25+ @update:modelValue =" onFilterInput[c.name]({ column: c, operator: c.filterOptions.multiselect ? 'in' : 'eq', value: c.filterOptions.multiselect ? ($event.length ? $event : undefined) : $event || undefined })"
26+ :modelValue =" filtersStore.filters.find(f => f.field === c.name && f.operator === (c.filterOptions.multiselect ? 'in' : 'eq'))?.value || (c.filterOptions.multiselect ? [] : '')"
27+ >
28+ <template #extra-item v-if =" columnLoadingState [c .name ]?.loading " >
29+ <div class =" text-center text-gray-400 dark:text-gray-300 py-2 flex items-center justify-center gap-2" >
30+ <Spinner class =" w-4 h-4" />
31+ {{ $t('Loading...') }}
32+ </div >
33+ </template >
34+ </Select >
35+ <Select
36+ :multiple =" c.filterOptions.multiselect"
37+ class =" w-full"
38+ v-else-if =" c.type === 'boolean'"
39+ :options =" [
40+ { label: $t('Yes'), value: true },
41+ { label: $t('No'), value: false },
42+ // if field is not required, undefined might be there, and user might want to filter by it
43+ ...(c.required ? [] : [ { label: $t('Unset'), value: undefined } ])
44+ ]"
45+ @update:modelValue =" onFilterInput[c.name]({ column: c, operator: c.filterOptions.multiselect ? 'in' : 'eq', value: c.filterOptions.multiselect ? ($event.length ? $event : undefined) : $event })"
46+ :modelValue =" filtersStore.filters.find(f => f.field === c.name && f.operator === (c.filterOptions.multiselect ? 'in' : 'eq'))?.value !== undefined
47+ ? filtersStore.filters.find(f => f.field === c.name && f.operator === (c.filterOptions.multiselect ? 'in' : 'eq'))?.value
48+ : (c.filterOptions.multiselect ? [] : '')"
49+ />
50+
51+ <Select
52+ :multiple =" c.filterOptions.multiselect"
53+ class =" w-full"
54+ v-else-if =" c.enum"
55+ :options =" c.enum"
56+ @update:modelValue =" onFilterInput[c.name]({ column: c, operator: c.filterOptions.multiselect ? 'in' : 'eq', value: c.filterOptions.multiselect ? ($event.length ? $event : undefined) : $event || undefined })"
57+ :modelValue =" filtersStore.filters.find(f => f.field === c.name && f.operator === (c.filterOptions.multiselect ? 'in' : 'eq'))?.value || (c.filterOptions.multiselect ? [] : '')"
58+ />
59+
60+ <Input
61+ v-else-if =" ['string', 'text', 'json', 'richtext', 'unknown'].includes(c.type)"
62+ type =" text"
63+ full-width
64+ :placeholder =" $t('Search')"
65+ @update:modelValue =" onFilterInput[c.name]({ column: c, operator: c.filterOptions?.substringSearch ? 'ilike' : 'eq', value: $event || undefined })"
66+ :modelValue =" getFilterItem({ column: c, operator: c.filterOptions?.substringSearch ? 'ilike' : 'eq' })"
67+ />
68+
69+ <CustomDateRangePicker
70+ v-else-if =" ['datetime', 'date', 'time'].includes(c.type)"
71+ :column =" c"
72+ :valueStart =" filtersStore.filters.find(f => f.field === c.name && f.operator === 'gte')?.value || undefined"
73+ @update:valueStart =" onFilterInput[c.name]({ column: c, operator: 'gte', value: $event || undefined })"
74+ :valueEnd =" filtersStore.filters.find(f => f.field === c.name && f.operator === 'lte')?.value || undefined"
75+ @update:valueEnd =" onFilterInput[c.name]({ column: c, operator: 'lte', value: $event || undefined })"
76+ />
77+
78+ <CustomRangePicker
79+ v-else-if =" ['integer', 'decimal', 'float'].includes(c.type) && c.allowMinMaxQuery"
80+ :min =" getFilterMinValue(c.name)"
81+ :max =" getFilterMaxValue(c.name)"
82+ :valueStart =" getFilterItem({ column: c, operator: 'gte' })"
83+ @update:valueStart =" onFilterInput[c.name]({ column: c, operator: 'gte', value: ($event !== '' && $event !== null) ? $event : undefined })"
84+ :valueEnd =" getFilterItem({ column: c, operator: 'lte' })"
85+ @update:valueEnd =" onFilterInput[c.name]({ column: c, operator: 'lte', value: ($event !== '' && $event !== null) ? $event : undefined })"
86+ />
87+
88+ <div v-else-if =" ['integer', 'decimal', 'float'].includes(c.type)" class =" flex gap-2" >
89+ <Input
90+ type =" number"
91+ full-width
92+ aria-describedby =" helper-text-explanation"
93+ :placeholder =" $t('From')"
94+ @update:modelValue =" onFilterInput[c.name]({ column: c, operator: 'gte', value: ($event !== '' && $event !== null) ? $event : undefined })"
95+ :modelValue =" getFilterItem({ column: c, operator: 'gte' })"
96+ />
97+ <Input
98+ type =" number"
99+ full-width
100+ aria-describedby =" helper-text-explanation"
101+ :placeholder =" $t('To')"
102+ @update:modelValue =" onFilterInput[c.name]({ column: c, operator: 'lte', value: ($event !== '' && $event !== null) ? $event : undefined })"
103+ :modelValue =" getFilterItem({ column: c, operator: 'lte' })"
104+ />
105+ </div >
106+ </div >
107+ </div >
108+ </div >
109+ <div v-if =" isExpanded" class =" flex w-full justify-end" >
110+ <Button
111+ class =" mt-4 max-w-24"
112+ @click =" filtersStore.filters = [...filtersStore.filters.filter(f => filtersStore.shouldFilterBeHidden(f.field))]"
113+ :disabled =" filtersStore.filters.length === 0"
114+ >
115+ Clear all
116+ </Button >
117+ </div >
118+ </div >
119+ </template >
120+
121+ <script lang="ts" setup>
122+ import { onMounted , computed , ref , reactive } from ' vue' ;
123+ import { useFiltersStore } from ' @/stores/filters' ;
124+ import { callAdminForthApi , loadMoreForeignOptions , searchForeignOptions , createSearchInputHandlers } from ' @/utils' ;
125+ import { useRouter } from ' vue-router' ;
126+ import debounce from ' debounce' ;
127+ import { Select , Input , Button } from ' @/afcl' ;
128+ import CustomRangePicker from " @/components/CustomRangePicker.vue" ;
129+ import CustomDateRangePicker from ' @/components/CustomDateRangePicker.vue' ;
130+ import { useRoute } from ' vue-router' ;
131+
132+ const router = useRouter ();
133+ const route = useRoute ();
134+ const columnLoadingState = reactive ({});
135+ const columnOffsets = reactive ({});
136+ const columnEmptyResultsCount = reactive ({});
137+ const filtersStore = useFiltersStore ();
138+ const columnsMinMax = ref ({});
139+ const isExpanded = ref (false );
140+
141+ const props = defineProps <{
142+ meta: any ,
143+ resource: any ,
144+ adminUser: any
145+ }>();
146+
147+ const columnOptions = ref ({});
148+ const columnsWithFilter = computed (
149+ () => props .resource .columns ?.filter (column => column .showIn .filter && props .meta .options .columns .some (c => c .column === column .name )) || []
150+ );
151+
152+ onMounted (async () => {
153+ console .log (' FiltersArea mounted' , props .resource );
154+ columnsMinMax .value = await callAdminForthApi ({
155+ path: ' /get_min_max_for_columns' ,
156+ method: ' POST' ,
157+ body: {
158+ resourceId: route .params .resourceId
159+ }
160+ });
161+ console .log (' Fetched columnsMinMax:' , columnsMinMax .value );
162+ });
163+
164+ function getFilterItem({ column , operator }) {
165+ const filterValue = filtersStore .filters .find (f => f .field === column .name && f .operator === operator )?.value ;
166+ return filterValue !== undefined ? filterValue : ' ' ;
167+ }
168+
169+ function getFilterMinValue(columnName ) {
170+ if (columnsMinMax .value && columnsMinMax .value [columnName ]) {
171+ return columnsMinMax .value [columnName ]?.min
172+ }
173+ }
174+
175+ function getFilterMaxValue(columnName ) {
176+ if (columnsMinMax .value && columnsMinMax .value [columnName ]) {
177+ return columnsMinMax .value [columnName ]?.max
178+ }
179+ }
180+ async function loadMoreOptions(columnName , searchTerm = ' ' ) {
181+ return loadMoreForeignOptions ({
182+ columnName ,
183+ searchTerm ,
184+ columns: props .resource .columns ,
185+ resourceId: Array .isArray (router .currentRoute .value .params .resourceId )
186+ ? router .currentRoute .value .params .resourceId [0 ]
187+ : router .currentRoute .value .params .resourceId ,
188+ columnOptions ,
189+ columnLoadingState ,
190+ columnOffsets ,
191+ columnEmptyResultsCount
192+ });
193+ }
194+
195+ async function searchOptions(columnName , searchTerm ) {
196+ return searchForeignOptions ({
197+ columnName ,
198+ searchTerm ,
199+ columns: props .resource .columns ,
200+ resourceId: Array .isArray (router .currentRoute .value .params .resourceId )
201+ ? router .currentRoute .value .params .resourceId [0 ]
202+ : router .currentRoute .value .params .resourceId ,
203+ columnOptions ,
204+ columnLoadingState ,
205+ columnOffsets ,
206+ columnEmptyResultsCount
207+ });
208+ }
209+
210+ const onSearchInput = computed (() => {
211+ return createSearchInputHandlers (
212+ props .resource .columns ,
213+ searchOptions ,
214+ (column ) => column .filterOptions ?.debounceTimeMs || 300
215+ );
216+ });
217+
218+ const onFilterInput = computed (() => {
219+ if (! props .resource .columns ) return {};
220+
221+ return props .resource .columns .reduce ((acc , c ) => {
222+ return {
223+ ... acc ,
224+ [c .name ]: debounce (({ column , operator , value }) => {
225+ setFilterItem ({ column , operator , value });
226+ }, c .filterOptions ?.debounceTimeMs || 10 ),
227+ };
228+ }, {});
229+ });
230+
231+ function setFilterItem({ column , operator , value }) {
232+
233+ const index = filtersStore .filters .findIndex (f => f .field === column .name && f .operator === operator );
234+ if (value === undefined || value === ' ' || value === null ) {
235+ if (index !== - 1 ) {
236+ filtersStore .filters .splice (index , 1 );
237+ }
238+ } else {
239+ if (index === - 1 ) {
240+ filtersStore .setFilter ({ field: column .name , value , operator });
241+ } else {
242+ filtersStore .setFilters ([... filtersStore .filters .slice (0 , index ), { field: column .name , value , operator }, ... filtersStore .filters .slice (index + 1 )])
243+ }
244+ }
245+ }
246+
247+ </script >
0 commit comments