Skip to content

Commit 6a38590

Browse files
Merge pull request #109 from devforth/add-column-filter-options
feat: add filterOptions field to columns
2 parents 99dd2ed + 41af0f9 commit 6a38590

File tree

4 files changed

+95
-12
lines changed

4 files changed

+95
-12
lines changed

adminforth/documentation/docs/tutorial/03-Customization/13-standardPagesTuning.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -491,4 +491,37 @@ export default {
491491
],
492492
```
493493

494-
This way, when creating or editing a record you will be able to choose value for this field from a dropdown selector and on list and show pages this field will be displayed as a link to a foreign resource.
494+
This way, when creating or editing a record you will be able to choose value for this field from a dropdown selector and on list and show pages this field will be displayed as a link to a foreign resource.
495+
496+
## Filtering
497+
498+
### Filter Options
499+
500+
You can specify the delay between filtering requests and filtering operator for a column using `filterOptions` field.
501+
502+
```typescript title="./resources/adminuser.ts"
503+
export default {
504+
name: 'adminuser',
505+
columns: [
506+
...
507+
{
508+
name: "title",
509+
required: true,
510+
maxLength: 255,
511+
minLength: 3,
512+
//diff-add
513+
filterOptions: {
514+
//diff-add
515+
debounceTimeMs: 500,
516+
//diff-add
517+
substringSearch: false,
518+
//diff-add
519+
},
520+
},
521+
],
522+
},
523+
...
524+
],
525+
```
526+
`debounceTimeMs` field dictates how long (in milliseconds) to wait between inputs to send updated data request. By increasing this value, you can reduce the amount of requests set to backend. Default value for this field is set to 10ms.
527+
`substringSearch` sets what comparison operator to use for text field. By default this field is set to `true`, which results in using case-insensitive `ILIKE` operator, that will look for records that have filter string anywhere inside field value. Setting this `substringSearch` to `false` will result in using more strict `EQ` operator, that will look for exact full-string matches.

adminforth/modules/configValidator.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,30 @@ export default class ConfigValidator implements IConfigValidator {
374374
//define default sortable
375375
if (!Object.keys(col).includes('sortable')) { col.sortable = !col.virtual; }
376376

377+
// define default filter options
378+
if (!Object.keys(col).includes('filterOptions')) {
379+
col.filterOptions = {
380+
debounceTimeMs: 10,
381+
substringSearch: true,
382+
};
383+
} else {
384+
if (col.filterOptions.debounceTimeMs !== undefined) {
385+
if (typeof col.filterOptions.debounceTimeMs !== 'number') {
386+
errors.push(`Resource "${res.resourceId}" column "${col.name}" filterOptions.debounceTimeMs must be a number`);
387+
}
388+
} else {
389+
col.filterOptions.debounceTimeMs = 10;
390+
}
391+
392+
if (col.filterOptions.substringSearch !== undefined) {
393+
if (typeof col.filterOptions.substringSearch !== 'boolean') {
394+
errors.push(`Resource "${res.resourceId}" column "${col.name}" filterOptions.substringSearch must be a boolean`);
395+
}
396+
} else {
397+
col.filterOptions.substringSearch = true;
398+
}
399+
}
400+
377401
col.showIn = this.validateAndNormalizeShowIn(resInput, inCol, errors, warnings);
378402

379403
// check col.required is boolean or object

adminforth/spa/src/components/Filters.vue

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
multiple
2828
class="w-full"
2929
:options="columnOptions[c.name] || []"
30-
@update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event.length ? $event : undefined })"
30+
@update:modelValue="onFilterInput[c.name]({ column: c, operator: 'in', value: $event.length ? $event : undefined })"
3131
:modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
3232
/>
3333
<Select
@@ -40,7 +40,7 @@
4040
// if field is not required, undefined might be there, and user might want to filter by it
4141
...(c.required ? [] : [ { label: $t('Unset'), value: undefined } ])
4242
]"
43-
@update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event.length ? $event : undefined })"
43+
@update:modelValue="onFilterInput[c.name]({ column: c, operator: 'in', value: $event.length ? $event : undefined })"
4444
:modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
4545
/>
4646

@@ -49,7 +49,7 @@
4949
class="w-full"
5050
v-else-if="c.enum"
5151
:options="c.enum"
52-
@update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event.length ? $event : undefined })"
52+
@update:modelValue="onFilterInput[c.name]({ column: c, operator: 'in', value: $event.length ? $event : undefined })"
5353
:modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
5454
/>
5555

@@ -58,42 +58,42 @@
5858
type="text"
5959
full-width
6060
:placeholder="$t('Search')"
61-
@update:modelValue="setFilterItem({ column: c, operator: 'ilike', value: $event || undefined })"
62-
:modelValue="getFilterItem({ column: c, operator: 'ilike' })"
61+
@update:modelValue="onFilterInput[c.name]({ column: c, operator: c.filterOptions?.substringSearch ? 'ilike' : 'eq', value: $event || undefined })"
62+
:modelValue="getFilterItem({ column: c, operator: c.filterOptions?.substringSearch ? 'ilike' : 'eq' })"
6363
/>
6464

6565
<CustomDateRangePicker
6666
v-else-if="['datetime', 'date', 'time'].includes(c.type)"
6767
:column="c"
6868
:valueStart="filtersStore.filters.find(f => f.field === c.name && f.operator === 'gte')?.value || undefined"
69-
@update:valueStart="setFilterItem({ column: c, operator: 'gte', value: $event || undefined })"
69+
@update:valueStart="onFilterInput[c.name]({ column: c, operator: 'gte', value: $event || undefined })"
7070
:valueEnd="filtersStore.filters.find(f => f.field === c.name && f.operator === 'lte')?.value || undefined"
71-
@update:valueEnd="setFilterItem({ column: c, operator: 'lte', value: $event || undefined })"
71+
@update:valueEnd="onFilterInput[c.name]({ column: c, operator: 'lte', value: $event || undefined })"
7272
/>
7373

7474
<CustomRangePicker
7575
v-else-if="['integer', 'decimal', 'float'].includes(c.type) && c.allowMinMaxQuery"
7676
:min="getFilterMinValue(c.name)"
7777
:max="getFilterMaxValue(c.name)"
7878
:valueStart="getFilterItem({ column: c, operator: 'gte' })"
79-
@update:valueStart="setFilterItem({ column: c, operator: 'gte', value: $event || undefined })"
79+
@update:valueStart="onFilterInput[c.name]({ column: c, operator: 'gte', value: $event || undefined })"
8080
:valueEnd="getFilterItem({ column: c, operator: 'lte' })"
81-
@update:valueEnd="setFilterItem({ column: c, operator: 'lte', value: $event || undefined })"
81+
@update:valueEnd="onFilterInput[c.name]({ column: c, operator: 'lte', value: $event || undefined })"
8282
/>
8383

8484
<div v-else-if="['integer', 'decimal', 'float'].includes(c.type)" class="flex gap-2">
8585
<Input
8686
type="number"
8787
aria-describedby="helper-text-explanation"
8888
:placeholder="$t('From')"
89-
@update:modelValue="setFilterItem({ column: c, operator: 'gte', value: $event || undefined })"
89+
@update:modelValue="onFilterInput[c.name]({ column: c, operator: 'gte', value: $event || undefined })"
9090
:modelValue="getFilterItem({ column: c, operator: 'gte' })"
9191
/>
9292
<Input
9393
type="number"
9494
aria-describedby="helper-text-explanation"
9595
:placeholder="$t('To')"
96-
@update:modelValue="setFilterItem({ column: c, operator: 'lte', value: $event|| undefined })"
96+
@update:modelValue="onFilterInput[c.name]({ column: c, operator: 'lte', value: $event|| undefined })"
9797
:modelValue="getFilterItem({ column: c, operator: 'lte' })"
9898
/>
9999
</div>
@@ -127,6 +127,7 @@ import CustomRangePicker from "@/components/CustomRangePicker.vue";
127127
import { useFiltersStore } from '@/stores/filters';
128128
import Input from '@/afcl/Input.vue';
129129
import Select from '@/afcl/Select.vue';
130+
import debounce from 'debounce';
130131
131132
const filtersStore = useFiltersStore();
132133
@@ -186,6 +187,19 @@ watch(() => props.show, (show) => {
186187
// operator: 'like'
187188
// }
188189
190+
const onFilterInput = computed(() => {
191+
if (!props.columns) return {};
192+
193+
return props.columns.reduce((acc, c) => {
194+
return {
195+
...acc,
196+
[c.name]: debounce(({ column, operator, value }) => {
197+
setFilterItem({ column, operator, value });
198+
}, c.filterOptions?.debounceTimeMs || 10),
199+
};
200+
}, {});
201+
});
202+
189203
function setFilterItem({ column, operator, value }) {
190204
191205
const index = filtersStore.filters.findIndex(f => f.field === column.name && f.operator === operator);

adminforth/types/Common.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -784,6 +784,18 @@ export interface AdminForthResourceColumnInputCommon {
784784

785785
sortable?: boolean,
786786

787+
788+
filterOptions?: {
789+
/**
790+
* Decrease number of requests by adding debounce time to filter requests.
791+
*/
792+
debounceTimeMs?: number,
793+
/**
794+
* If true - will force EQ operator for filter instead of ILIKE.
795+
*/
796+
substringSearch?: boolean,
797+
},
798+
787799
/**
788800
* if true field will !not be passed to UI under no circumstances, but will be presented in hooks
789801
*/

0 commit comments

Comments
 (0)