11<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- <div class =" flex justify-end items-center" >
4- <p
5- class =" hover:underline cursor-pointer text-blue-700 dark:text-blue-500 text-end"
6- @click =" isExpanded = !isExpanded"
7- >
8- {{ isExpanded ? 'Hide filters' : 'Show filters' }}
9- </p >
10- </div >
11- <div v-if =" isExpanded" class =" md:grid md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-6 gap-2 w-full" >
12- <div class =" flex flex-col" v-for =" c in columnsWithFilter" :key =" c" >
13- <div class =" min-w-48" >
14- <p class =" dark:text-gray-400 font-medium text-sm" >{{ c.label }}</p >
15- <Select
16- :teleportToBody =" true "
17- v-if =" c .foreignResource "
18- :multiple =" c .filterOptions .multiselect "
19- class="w-full"
20- :options =" columnOptions [c .name ] || []"
21- :searchDisabled =" ! c .foreignResource .searchableFields "
22- @scroll-near-end =" loadMoreOptions (c .name )"
23- @search =" (searchTerm ) => {
24- if (c .foreignResource .searchableFields && onSearchInput [c .name ]) {
25- onSearchInput [c .name ](searchTerm );
26- }
27- } "
28- @update :modelValue =" onFilterInput [c .name ]({ column: c , operator: c .filterOptions .multiselect ? ' in' : ' eq' , value: c .filterOptions .multiselect ? ($event .length ? $event : undefined ) : $event || undefined })"
29- :modelValue =" filtersStore .filters .find (f => f .field === c .name && f .operator === (c .filterOptions .multiselect ? ' in' : ' eq' ))?.value || (c .filterOptions .multiselect ? [] : ' ' )"
30- :teleportToTop =" true "
31- >
32- <template #extra-item v-if =" columnLoadingState [c .name ]?.loading " >
33- <div class =" text-center text-gray-400 dark:text-gray-300 py-2 flex items-center justify-center gap-2" >
34- <Spinner class="w-4 h-4" />
35- {{ $t('Loading...') }}
36- </div >
37- </template >
38- </Select >
39- <Select
40- :teleportToBody =" true "
41- :multiple =" c .filterOptions .multiselect "
42- class="w-full"
43- v-else-if =" c .type === ' boolean' "
44- :options =" [
45- { label: $t (' Yes' ), value: true },
46- { label: $t (' No' ), value: false },
47- // if field is not required, undefined might be there, and user might want to filter by it
48- ... (c .required ? [] : [ { label: $t (' Unset' ), value: undefined } ])
49- ] "
50- @update :modelValue =" onFilterInput [c .name ]({ column: c , operator: c .filterOptions .multiselect ? ' in' : ' eq' , value: c .filterOptions .multiselect ? ($event .length ? $event : undefined ) : $event })"
51- :modelValue =" filtersStore .filters .find (f => f .field === c .name && f .operator === (c .filterOptions .multiselect ? ' in' : ' eq' ))?.value !== undefined
52- ? filtersStore .filters .find (f => f .field === c .name && f .operator === (c .filterOptions .multiselect ? ' in' : ' eq' ))?.value
53- : (c .filterOptions .multiselect ? [] : ' ' ) "
54- :teleportToTop =" true "
55- />
56-
57- <Select
58- :teleportToBody =" true "
59- :multiple =" c .filterOptions .multiselect "
60- class="w-full"
61- v-else-if =" c .enum "
62- :options =" c .enum "
63- @update :modelValue =" onFilterInput [c .name ]({ column: c , operator: c .filterOptions .multiselect ? ' in' : ' eq' , value: c .filterOptions .multiselect ? ($event .length ? $event : undefined ) : $event || undefined })"
64- :modelValue =" filtersStore .filters .find (f => f .field === c .name && f .operator === (c .filterOptions .multiselect ? ' in' : ' eq' ))?.value || (c .filterOptions .multiselect ? [] : ' ' )"
65- :teleportToTop =" true "
66- />
67-
68- <Input
69- v-else-if =" [' string' , ' text' , ' json' , ' richtext' , ' unknown' ].includes (c .type ) "
70- type="text"
71- full-width
72- :placeholder =" $t (' Search' )"
73- @update :modelValue =" onFilterInput [c .name ]({ column: c , operator: c .filterOptions ?.substringSearch ? ' ilike' : ' eq' , value: $event || undefined })"
74- :modelValue =" getFilterItem ({ column: c , operator: c .filterOptions ?.substringSearch ? ' ilike' : ' eq' })"
75- />
76-
77- <CustomDateRangePicker
78- v-else-if =" [' datetime' , ' date' , ' time' ].includes (c .type ) "
79- :column =" c "
80- :valueStart =" filtersStore .filters .find (f => f .field === c .name && f .operator === ' gte' )?.value || undefined "
81- @update :valueStart =" onFilterInput [c .name ]({ column: c , operator: ' gte' , value: $event || undefined })"
82- :valueEnd =" filtersStore .filters .find (f => f .field === c .name && f .operator === ' lte' )?.value || undefined "
83- @update :valueEnd =" onFilterInput [c .name ]({ column: c , operator: ' lte' , value: $event || undefined })"
84- />
85-
86- <CustomRangePicker
87- v-else-if =" [' integer' , ' decimal' , ' float' ].includes (c .type ) && c .allowMinMaxQuery "
88- :valueStart =" getFilterItem ({ column: c , operator: ' gte' })"
89- @update :valueStart =" onFilterInput [c .name ]({ column: c , operator: ' gte' , value: ($event !== ' ' && $event !== null ) ? $event : undefined })"
90- :valueEnd =" getFilterItem ({ column: c , operator: ' lte' })"
91- @update :valueEnd =" onFilterInput [c .name ]({ column: c , operator: ' lte' , value: ($event !== ' ' && $event !== null ) ? $event : undefined })"
92- />
93-
94- <div v-else-if =" ['integer', 'decimal', 'float'].includes(c.type)" class =" flex gap-2" >
95- <Input
96- type="number"
97- full-width
98- aria-describedby="helper-text-explanation"
99- :placeholder =" $t (' From' )"
100- @update :modelValue =" onFilterInput [c .name ]({ column: c , operator: ' gte' , value: ($event !== ' ' && $event !== null ) ? $event : undefined })"
101- :modelValue =" getFilterItem ({ column: c , operator: ' gte' })"
102- />
103- <Input
104- type="number"
105- full-width
106- aria-describedby="helper-text-explanation"
107- :placeholder =" $t (' To' )"
108- @update :modelValue =" onFilterInput [c .name ]({ column: c , operator: ' lte' , value: ($event !== ' ' && $event !== null ) ? $event : undefined })"
109- :modelValue =" getFilterItem ({ column: c , operator: ' lte' })"
110- />
111- </div >
112- </div >
113- </div >
114- <template v-if =" freeCells > 0 " v-for =" n in freeCells - 1 " :key =" ' free-cell-' + n " >
115- <div ></div >
116- </template >
117- <template v-if =" freeCells === 0 " v-for =" n in cols - 1 " :key =" ' free-cell2-' + n " >
118- <div ></div >
119- </template >
120- <div class =" flex items-end justify-end" >
121- <Button
122- class="mt-6 max-w-24"
123- @click =" filtersStore .filters = [... filtersStore .filters .filter (f => filtersStore .shouldFilterBeHidden (f .field ))]"
124- :disabled =" filtersStore .filters .length === 0 "
125- >
126- Clear all
127- </Button >
2+ <div class =" flex gap-1" >
3+ <div v-for =" filter in meta.options" :key =" filter.name" >
4+ <UniversalSearchInput
5+ v-if =" filter .hasSearchInput "
6+ :meta =" {
7+ placeholder: ` Search ${filter .name } ` ,
8+ filterField: filter .name
9+ } "
10+ />
11+
12+ <div v-else >
13+ <QickFiltersSelect
14+ :filter =" filter "
15+ />
12816 </div >
12917 </div >
13018 </div >
19+
13120</template >
13221
13322<script lang="ts" setup>
134- import { onMounted , onUnmounted , computed , ref , reactive , watch } from ' vue' ;
135- import { useFiltersStore } from ' @/stores/filters' ;
136- import { callAdminForthApi , loadMoreForeignOptions , searchForeignOptions , createSearchInputHandlers } from ' @/utils' ;
137- import { useRouter } from ' vue-router' ;
138- import debounce from ' debounce' ;
139- import { Select , Input , Button } from ' @/afcl' ;
140- import CustomRangePicker from " @/components/CustomRangePicker.vue" ;
141- import CustomDateRangePicker from ' @/components/CustomDateRangePicker.vue' ;
142- import { useRoute } from ' vue-router' ;
143-
144- const router = useRouter ();
145- const route = useRoute ();
146- const columnLoadingState = reactive ({});
147- const columnOffsets = reactive ({});
148- const columnEmptyResultsCount = reactive ({});
149- const filtersStore = useFiltersStore ();
150- const columnsMinMax = ref ({});
151- const isExpanded = ref (true );
152- const freeCells = ref (0 );
153- const currentBreakpoint = ref (" base" );
154- const cols = ref (1 );
23+ import UniversalSearchInput from ' ./UniversalSearchInput.vue' ;
24+ import QickFiltersSelect from ' ./QickFiltersSelect.vue' ;
25+ import type { Filter } from ' ./types' ;
15526
15627const props = defineProps <{
157- meta: any ,
28+ meta: {
29+ pluginInstanceId: string ,
30+ resourceId: string ,
31+ options: Filter []
32+ },
15833 resource: any ,
15934 adminUser: any
16035}>();
16136
162- const columnOptions = ref ({});
163- const columnsWithFilter = computed (
164- () => sortFilters (props .resource .columns ?.filter (column => column .showIn .filter && props .meta .options .columns .some (c => c .column === column .name )), props .meta .options .columns ) || []
165- );
166-
167- onMounted (async () => {
168- // columnsMinMax.value = await callAdminForthApi({
169- // path: '/get_min_max_for_columns',
170- // method: 'POST',
171- // body: {
172- // resourceId: route.params.resourceId
173- // }
174- // });
175- currentBreakpoint .value = getBreakpoint (window .innerWidth );
176- calculateGridCols ();
177- window .addEventListener (' resize' , () => {
178- const newBreakpoint = getBreakpoint (window .innerWidth )
179- if (newBreakpoint !== currentBreakpoint .value ) {
180- currentBreakpoint .value = newBreakpoint ;
181- calculateGridCols ();
182- }
183- })
184- });
185-
186- onUnmounted (() => {
187- window .removeEventListener (' resize' , getBreakpoint )
188- })
189-
190- function getFilterItem({ column , operator }) {
191- const filterValue = filtersStore .filters .find (f => f .field === column .name && f .operator === operator )?.value ;
192- return filterValue !== undefined ? filterValue : ' ' ;
193- }
194-
195- async function loadMoreOptions(columnName , searchTerm = ' ' ) {
196- return loadMoreForeignOptions ({
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- async function searchOptions(columnName , searchTerm ) {
211- return searchForeignOptions ({
212- columnName ,
213- searchTerm ,
214- columns: props .resource .columns ,
215- resourceId: Array .isArray (router .currentRoute .value .params .resourceId )
216- ? router .currentRoute .value .params .resourceId [0 ]
217- : router .currentRoute .value .params .resourceId ,
218- columnOptions ,
219- columnLoadingState ,
220- columnOffsets ,
221- columnEmptyResultsCount
222- });
223- }
224-
225- const onSearchInput = computed (() => {
226- return createSearchInputHandlers (
227- props .resource .columns ,
228- searchOptions ,
229- (column ) => column .filterOptions ?.debounceTimeMs || 300
230- );
231- });
232-
233- const onFilterInput = computed (() => {
234- if (! props .resource .columns ) return {};
235-
236- return props .resource .columns .reduce ((acc , c ) => {
237- return {
238- ... acc ,
239- [c .name ]: debounce (({ column , operator , value }) => {
240- setFilterItem ({ column , operator , value });
241- }, c .filterOptions ?.debounceTimeMs || 10 ),
242- };
243- }, {});
244- });
245-
246- function setFilterItem({ column , operator , value }) {
247-
248- const index = filtersStore .filters .findIndex (f => f .field === column .name && f .operator === operator );
249- if (value === undefined || value === ' ' || value === null ) {
250- if (index !== - 1 ) {
251- filtersStore .filters .splice (index , 1 );
252- }
253- } else {
254- if (index === - 1 ) {
255- filtersStore .setFilter ({ field: column .name , value , operator });
256- } else {
257- filtersStore .setFilters ([... filtersStore .filters .slice (0 , index ), { field: column .name , value , operator }, ... filtersStore .filters .slice (index + 1 )])
258- }
259- }
260- }
261-
262-
263- function getBreakpoint(width ) {
264- if (width >= 1536 ) return ' 2xl'
265- if (width >= 1280 ) return ' xl'
266- if (width >= 1024 ) return ' lg'
267- if (width >= 768 ) return ' md'
268- if (width >= 640 ) return ' sm'
269- return ' base'
270- }
271-
272- function calculateGridCols() {
273- const size = currentBreakpoint .value ;
274- switch (size ) {
275- case ' 2xl' :
276- cols .value = 6 ;
277- break ;
278- case ' xl' :
279- cols .value = 3 ;
280- break ;
281- case ' lg' :
282- cols .value = 2 ;
283- break ;
284- case ' md' :
285- cols .value = 1 ;
286- break ;
287- default :
288- cols .value = 1 ;
289- }
290- const rows = Math .ceil (columnsWithFilter .value .length / cols .value );
291- freeCells .value = (cols .value * rows ) - columnsWithFilter .value .length ;
292- }
293-
294- function sortFilters(array , fieldsObject ) {
295- let desiredOrder = [];
296- fieldsObject .forEach (element => {
297- desiredOrder .push (element .column );
298- });
299- const sortedObjects = array .sort (
300- (a , b ) => desiredOrder .indexOf (a .name ) - desiredOrder .indexOf (b .name )
301- );
302- return sortedObjects ;
303- }
30437
30538 </script >
0 commit comments