adminforth 2.27.0-next.5 → 2.27.0-next.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/commands/callTsProxy.js +10 -5
  2. package/commands/createApp/templates/api.ts.hbs +12 -2
  3. package/commands/proxy.ts +18 -10
  4. package/dist/commands/proxy.js +14 -10
  5. package/dist/commands/proxy.js.map +1 -1
  6. package/dist/dataConnectors/clickhouse.d.ts +5 -2
  7. package/dist/dataConnectors/clickhouse.d.ts.map +1 -1
  8. package/dist/dataConnectors/clickhouse.js +73 -9
  9. package/dist/dataConnectors/clickhouse.js.map +1 -1
  10. package/dist/index.d.ts +4 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +17 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/modules/configValidator.d.ts.map +1 -1
  15. package/dist/modules/configValidator.js +16 -9
  16. package/dist/modules/configValidator.js.map +1 -1
  17. package/dist/modules/restApi.d.ts.map +1 -1
  18. package/dist/modules/restApi.js +515 -9
  19. package/dist/modules/restApi.js.map +1 -1
  20. package/dist/modules/styles.js +1 -1
  21. package/dist/modules/utils.d.ts +1 -1
  22. package/dist/modules/utils.d.ts.map +1 -1
  23. package/dist/modules/utils.js +3 -5
  24. package/dist/modules/utils.js.map +1 -1
  25. package/dist/servers/express.d.ts +2 -6
  26. package/dist/servers/express.d.ts.map +1 -1
  27. package/dist/servers/express.js +46 -1
  28. package/dist/servers/express.js.map +1 -1
  29. package/dist/servers/openapi.d.ts +25 -0
  30. package/dist/servers/openapi.d.ts.map +1 -0
  31. package/dist/servers/openapi.js +126 -0
  32. package/dist/servers/openapi.js.map +1 -0
  33. package/dist/spa/package-lock.json +41 -0
  34. package/dist/spa/package.json +4 -0
  35. package/dist/spa/pnpm-lock.yaml +38 -0
  36. package/dist/spa/src/App.vue +77 -76
  37. package/dist/spa/src/afcl/Button.vue +2 -3
  38. package/dist/spa/src/afcl/Dialog.vue +1 -1
  39. package/dist/spa/src/afcl/Input.vue +1 -1
  40. package/dist/spa/src/afcl/Select.vue +8 -2
  41. package/dist/spa/src/afcl/Skeleton.vue +5 -0
  42. package/dist/spa/src/afcl/Spinner.vue +1 -1
  43. package/dist/spa/src/components/CallActionWrapper.vue +1 -1
  44. package/dist/spa/src/components/ColumnValueInput.vue +16 -3
  45. package/dist/spa/src/components/ColumnValueInputWrapper.vue +25 -2
  46. package/dist/spa/src/components/CustomRangePicker.vue +16 -14
  47. package/dist/spa/src/components/Filters.vue +95 -63
  48. package/dist/spa/src/components/GroupsTable.vue +9 -6
  49. package/dist/spa/src/components/MenuLink.vue +2 -2
  50. package/dist/spa/src/components/ResourceForm.vue +101 -7
  51. package/dist/spa/src/components/ResourceListTable.vue +14 -8
  52. package/dist/spa/src/components/ShowTable.vue +1 -1
  53. package/dist/spa/src/components/Sidebar.vue +29 -8
  54. package/dist/spa/src/components/ThreeDotsMenu.vue +25 -10
  55. package/dist/spa/src/components/ValueRenderer.vue +1 -0
  56. package/dist/spa/src/spa_types/core.ts +32 -0
  57. package/dist/spa/src/stores/filters.ts +16 -12
  58. package/dist/spa/src/types/Back.ts +91 -24
  59. package/dist/spa/src/types/Common.ts +24 -5
  60. package/dist/spa/src/types/adapters/CompletionAdapter.ts +27 -5
  61. package/dist/spa/src/types/adapters/index.ts +2 -2
  62. package/dist/spa/src/utils/createEditUtils.ts +65 -0
  63. package/dist/spa/src/utils/index.ts +2 -1
  64. package/dist/spa/src/utils/utils.ts +42 -7
  65. package/dist/spa/src/utils.ts +2 -1
  66. package/dist/spa/src/views/CreateEditSkeleton.vue +78 -0
  67. package/dist/spa/src/views/CreateView.vue +24 -50
  68. package/dist/spa/src/views/EditView.vue +23 -40
  69. package/dist/spa/src/views/ListView.vue +22 -32
  70. package/dist/spa/src/views/ShowView.vue +66 -24
  71. package/dist/types/Back.d.ts +100 -30
  72. package/dist/types/Back.d.ts.map +1 -1
  73. package/dist/types/Back.js.map +1 -1
  74. package/dist/types/Common.d.ts +31 -5
  75. package/dist/types/Common.d.ts.map +1 -1
  76. package/dist/types/Common.js.map +1 -1
  77. package/dist/types/adapters/CompletionAdapter.d.ts +18 -3
  78. package/dist/types/adapters/CompletionAdapter.d.ts.map +1 -1
  79. package/dist/types/adapters/index.d.ts +1 -1
  80. package/dist/types/adapters/index.d.ts.map +1 -1
  81. package/package.json +9 -5
@@ -119,7 +119,7 @@ import { ref, computed, onMounted, onUnmounted, watch, nextTick,type PropType, t
119
119
  import { IconCaretDownSolid } from '@iconify-prerendered/vue-flowbite';
120
120
  import { useElementSize } from '@vueuse/core'
121
121
 
122
- type ISingleSelectModelValue = string | number;
122
+ type ISingleSelectModelValue = string | number | boolean;
123
123
 
124
124
  const props = defineProps({
125
125
  options: Array,
@@ -159,6 +159,10 @@ const props = defineProps({
159
159
  type: String,
160
160
  default: '',
161
161
  },
162
+ disableTogleOfSelectedItem: {
163
+ type: Boolean,
164
+ default: false,
165
+ }
162
166
  });
163
167
 
164
168
  const emit = defineEmits(['update:modelValue', 'scroll-near-end', 'search']);
@@ -283,6 +287,7 @@ onMounted(() => {
283
287
  // Add scroll listeners if teleportToBody is true
284
288
  if (props.teleportToBody) {
285
289
  window.addEventListener('scroll', handleScroll, true);
290
+ window.addEventListener('resize', handleScroll);
286
291
  }
287
292
  });
288
293
 
@@ -314,7 +319,7 @@ const removeClickListener = () => {
314
319
  };
315
320
 
316
321
  const toogleItem = (item: any) => {
317
- if (selectedItems.value.includes(item)) {
322
+ if (selectedItems.value.includes(item) && !props.disableTogleOfSelectedItem) {
318
323
  selectedItems.value = selectedItems.value.filter(i => i.value !== item.value);
319
324
  } else {
320
325
  if (!props.multiple) {
@@ -345,6 +350,7 @@ onUnmounted(() => {
345
350
  // Remove scroll listeners if teleportToBody is true
346
351
  if (props.teleportToBody) {
347
352
  window.removeEventListener('scroll', handleScroll, true);
353
+ window.removeEventListener('resize', handleScroll);
348
354
  }
349
355
  if (searchDebounceHandle) {
350
356
  clearTimeout(searchDebounceHandle);
@@ -14,6 +14,11 @@
14
14
  <svg v-else-if="type === 'avatar'" class="me-3 animate-pulse text-lightSkeletonIconColor dark:text-darkSkeletonBackgroundColor" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
15
15
  <path d="M10 0a10 10 0 1 0 10 10A10.011 10.011 0 0 0 10 0Zm0 5a3 3 0 1 1 0 6 3 3 0 0 1 0-6Zm0 13a8.949 8.949 0 0 1-4.951-1.488A3.987 3.987 0 0 1 9 13h2a3.987 3.987 0 0 1 3.951 3.512A8.949 8.949 0 0 1 10 18Z"/>
16
16
  </svg>
17
+ <div
18
+ v-else-if="type === 'input'"
19
+ role="input"
20
+ :class="['animate-pulse bg-gray-100 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded', $attrs.class]"
21
+ ></div>
17
22
  <div v-else role="status" class="flex items-center justify-center animate-pulse bg-lightSkeletonIconColor rounded-full dark:bg-darkSkeletonBackgroundColor">
18
23
  <span class="sr-only">Loading...</span>
19
24
  </div>
@@ -4,6 +4,6 @@
4
4
  <path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/>
5
5
  <path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/>
6
6
  </svg>
7
- <span class="sr-only">Loading...</span>
7
+ <span class="sr-only">{{ $t('Loading...') }}</span>
8
8
  </div>
9
9
  </template>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div @click="onClick" class="flex items-center justify-center">
2
+ <div @click="onClick" class="flex items-center cursor-pointer">
3
3
  <slot />
4
4
  </div>
5
5
  </template>
@@ -20,7 +20,7 @@
20
20
  ref="input"
21
21
  :key="`select-${column.name}-${source}-${column.foreignResource?.name || column.foreignResource?.table || ''}`"
22
22
  class="w-full min-w-24"
23
- :options="columnOptions[column.name] || []"
23
+ :options="formatSelectOptions(columnOptions[column.name] || [], column)"
24
24
  :searchDisabled="!column.foreignResource.searchableFields"
25
25
  @scroll-near-end="loadMoreOptions && loadMoreOptions(column.name)"
26
26
  @search="(searchTerm) => {
@@ -33,6 +33,7 @@
33
33
  :modelValue="value"
34
34
  :readonly="(column.editReadonly && source === 'edit') || readonly"
35
35
  @update:modelValue="$emit('update:modelValue', $event)"
36
+ disableTogleOfSelectedItem
36
37
  >
37
38
  <template #extra-item v-if="columnLoadingState && columnLoadingState[column.name]?.loading">
38
39
  <div class="text-center text-gray-400 dark:text-gray-300 py-2 flex items-center justify-center gap-2">
@@ -45,11 +46,12 @@
45
46
  v-else-if="column.enum"
46
47
  ref="input"
47
48
  class="w-full min-w-24"
48
- :options="column.enum"
49
+ :options="formatSelectOptions(column.enum, column)"
49
50
  teleportToBody
50
51
  :modelValue="value"
51
52
  :readonly="(column.editReadonly && source === 'edit') || readonly"
52
53
  @update:modelValue="$emit('update:modelValue', $event)"
54
+ disableTogleOfSelectedItem
53
55
  />
54
56
  <Select
55
57
  v-else-if="(type || column.type) === 'boolean'"
@@ -60,6 +62,7 @@
60
62
  :modelValue="value"
61
63
  :readonly="(column.editReadonly && source === 'edit') || readonly"
62
64
  @update:modelValue="$emit('update:modelValue', $event)"
65
+ disableTogleOfSelectedItem
63
66
  />
64
67
  <Input
65
68
  v-else-if="['integer'].includes(type || column.type)"
@@ -188,7 +191,7 @@
188
191
  type?: string,
189
192
  value: any,
190
193
  currentValues: any,
191
- mode: string,
194
+ mode: 'create' | 'edit',
192
195
  columnOptions: any,
193
196
  unmasked: any,
194
197
  deletable?: boolean,
@@ -218,6 +221,16 @@ const input = ref<HTMLInputElement | null>(null);
218
221
  return options;
219
222
  };
220
223
 
224
+ const formatSelectOptions = (options: any, column: any) => {
225
+ const optionsToReturn = options;
226
+ if (!column.required[props.mode] && !column.isArray?.enabled) {
227
+ if (!optionsToReturn.some((option: any) => option.value === null)) {
228
+ optionsToReturn.push({ label: t('Unset'), value: null });
229
+ }
230
+ }
231
+ return optionsToReturn;
232
+ };
233
+
221
234
  function onFocusHandler(event:FocusEvent, column:any, source:string, ) {
222
235
  const focusedInput = event.target as HTMLInputElement;
223
236
  if(!focusedInput) return;
@@ -53,23 +53,46 @@
53
53
  @update:inValidity="$emit('update:inValidity', { name: column.name, value: $event })"
54
54
  @update:emptiness="$emit('update:emptiness', { name: column.name, value: $event })"
55
55
  />
56
+ <div v-if="columnsWithErrors && columnsWithErrors[column.name] && validatingMode && !isValidating" class="af-invalid-field-message mt-1 text-xs text-lightInputErrorColor dark:text-darkInputErrorColor">{{ columnsWithErrors[column.name] }}</div>
57
+ <Spinner v-if="shouldWeShowSpinner" class="w-4 mt-1"/>
58
+ <div v-if="column.editingNote && column.editingNote[mode]" class="mt-1 text-xs text-lightFormFieldTextColor dark:text-darkFormFieldTextColor">{{ column.editingNote[mode] }}</div>
56
59
  </template>
57
60
 
58
61
  <script setup lang="ts">
59
62
  import { IconPlusOutline } from '@iconify-prerendered/vue-flowbite';
60
63
  import ColumnValueInput from "./ColumnValueInput.vue";
61
- import { ref, nextTick } from 'vue';
64
+ import { ref, watch, nextTick } from 'vue';
65
+ import { Spinner } from '@/afcl';
62
66
 
63
67
  const props = defineProps<{
64
68
  source: 'create' | 'edit',
65
69
  column: any,
66
70
  currentValues: any,
67
- mode: string,
71
+ mode: 'create' | 'edit',
68
72
  columnOptions: any,
69
73
  unmasked: any,
70
74
  setCurrentValue: Function,
71
75
  readonly?: boolean,
76
+ columnsWithErrors: Record<string, string>,
77
+ isValidating: boolean,
78
+ validatingMode: boolean,
72
79
  }>();
80
+
81
+ const shouldWeShowSpinner = ref(false);
82
+
83
+
84
+ watch(() => props.currentValues[props.column.name], async (newVal) => {
85
+ await nextTick();
86
+ if (props.isValidating) {
87
+ shouldWeShowSpinner.value = true;
88
+ }
89
+ });
90
+
91
+ watch(() => [props.isValidating], () => {
92
+ if (!props.isValidating) {
93
+ shouldWeShowSpinner.value = false;
94
+ }
95
+ });
73
96
 
74
97
  const emit = defineEmits(['update:unmasked', 'update:inValidity', 'update:emptiness', 'focus-last-input']);
75
98
 
@@ -35,16 +35,12 @@ import {computed, onMounted, ref, watch} from "vue";
35
35
  import debounce from 'debounce'
36
36
  import RangePicker from './RangePicker.vue';
37
37
 
38
- const props = defineProps({
39
- valueStart: {
40
- default: '',
41
- },
42
- valueEnd: {
43
- default: '',
44
- },
45
- min: {},
46
- max: {},
47
- });
38
+ const props = defineProps<{
39
+ valueStart: number | null,
40
+ valueEnd: number | null,
41
+ min: number,
42
+ max: number,
43
+ }>()
48
44
 
49
45
  const emit = defineEmits(['update:valueStart', 'update:valueEnd']);
50
46
 
@@ -52,8 +48,8 @@ const minFormatted = computed(() => Math.floor(<number>props.min));
52
48
  const maxFormatted = computed(() => Math.ceil(<number>props.max));
53
49
 
54
50
 
55
- const start = ref<string | number>(props.valueStart);
56
- const end = ref<string | number>(props.valueEnd);
51
+ const start = ref<number | null>(props.valueStart);
52
+ const end = ref<number | null>(props.valueEnd);
57
53
 
58
54
  const sliderValue = ref<[number, number]>([minFormatted.value, maxFormatted.value]);
59
55
 
@@ -71,8 +67,8 @@ watch([start, end], () => {
71
67
 
72
68
  const updateFromSlider =
73
69
  debounce((value: [number, number]) => {
74
- start.value = value[0] === minFormatted.value ? '': value[0];
75
- end.value = value[1] === maxFormatted.value ? '': value[1];
70
+ start.value = value[0] === minFormatted.value ? null : value[0];
71
+ end.value = value[1] === maxFormatted.value ? null : value[1];
76
72
  }, 500);
77
73
 
78
74
  onMounted(() => {
@@ -89,11 +85,17 @@ onMounted(() => {
89
85
  })
90
86
 
91
87
  function updateStartFromProps() {
88
+ if (props.valueStart == start.value) {
89
+ return;
90
+ }
92
91
  start.value = props.valueStart;
93
92
  setSliderValues(start.value, end.value)
94
93
  }
95
94
 
96
95
  function updateEndFromProps() {
96
+ if (props.valueEnd == end.value) {
97
+ return;
98
+ }
97
99
  end.value = props.valueEnd;
98
100
  setSliderValues(start.value, end.value)
99
101
  }
@@ -19,7 +19,7 @@
19
19
 
20
20
  <div class="py-4 ">
21
21
  <ul class="space-y-3 font-medium text-sm">
22
- <li v-for="c in columnsWithFilter" :key="c">
22
+ <li v-for="c in columnsWithFilter" :key="c.name">
23
23
  <div class="flex flex-col">
24
24
  <div class="flex justify-between items-center">
25
25
  <p class="dark:text-gray-400 my-1">{{ c.label }}</p>
@@ -38,15 +38,15 @@
38
38
  </div>
39
39
  <component
40
40
  v-if="c.components?.filter"
41
- :is="getCustomComponent(c.components.filter)"
42
- :meta="c?.components?.list?.meta"
41
+ :is="getCustomComponent(formatComponent(c.components.filter))"
42
+ :meta="formatComponent(c.components.filter)?.meta"
43
43
  :column="c"
44
44
  class="w-full"
45
- @update:modelValue="(filtersArray) => {
45
+ @update:modelValue="(filtersArray:FilterParams[] ) => {
46
46
  filtersStore.filters = filtersStore.filters.filter(f => f.field !== c.name);
47
47
 
48
48
  for (const f of filtersArray) {
49
- filtersStore.filters.push({ field: c.name, ...f });
49
+ filtersStore.filters.push({ ...f, field: c.name });
50
50
  }
51
51
  console.log('filtersStore.filters', filtersStore.filters);
52
52
  emits('update:filters', [...filtersStore.filters]);
@@ -55,18 +55,18 @@
55
55
  />
56
56
  <Select
57
57
  v-else-if="c.foreignResource"
58
- :multiple="c.filterOptions.multiselect"
58
+ :multiple="c?.filterOptions?.multiselect"
59
59
  class="w-full"
60
60
  :options="columnOptions[c.name] || []"
61
61
  :searchDisabled="!c.foreignResource.searchableFields"
62
62
  @scroll-near-end="loadMoreOptions(c.name)"
63
63
  @search="(searchTerm) => {
64
- if (c.foreignResource.searchableFields && onSearchInput[c.name]) {
64
+ if (c.foreignResource?.searchableFields && onSearchInput[c.name]) {
65
65
  onSearchInput[c.name](searchTerm);
66
66
  }
67
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 ? [] : '')"
68
+ @update:modelValue="onFilterInput[c.name]({ column: c, operator: c.filterOptions?.multiselect ? AFFO.IN : AFFO.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 ? AFFO.IN : AFFO.EQ))?.value || (c.filterOptions?.multiselect ? [] : '')"
70
70
  >
71
71
  <template #extra-item v-if="columnLoadingState[c.name]?.loading">
72
72
  <div class="text-center text-gray-400 dark:text-gray-300 py-2 flex items-center justify-center gap-2">
@@ -76,7 +76,7 @@
76
76
  </template>
77
77
  </Select>
78
78
  <Select
79
- :multiple="c.filterOptions.multiselect"
79
+ :multiple="c.filterOptions?.multiselect"
80
80
  class="w-full"
81
81
  v-else-if="c.type === 'boolean'"
82
82
  :options="[
@@ -85,63 +85,63 @@
85
85
  // if field is not required, undefined might be there, and user might want to filter by it
86
86
  ...(c.required ? [] : [ { label: $t('Unset'), value: undefined } ])
87
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 ? [] : '')"
88
+ @update:modelValue="onFilterInput[c.name]({ column: c, operator: c.filterOptions?.multiselect ? AFFO.IN : AFFO.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 ? AFFO.IN : AFFO.EQ))?.value !== undefined
90
+ ? filtersStore.filters.find(f => f.field === c.name && f.operator === (c.filterOptions?.multiselect ? AFFO.IN : AFFO.EQ))?.value
91
+ : (c.filterOptions?.multiselect ? [] : '')"
92
92
  />
93
93
 
94
94
  <Select
95
- :multiple="c.filterOptions.multiselect"
95
+ :multiple="c.filterOptions?.multiselect"
96
96
  class="w-full"
97
97
  v-else-if="c.enum"
98
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 ? [] : '')"
99
+ @update:modelValue="onFilterInput[c.name]({ column: c, operator: c.filterOptions?.multiselect ? AFFO.IN : AFFO.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 ? AFFO.IN : AFFO.EQ))?.value || (c.filterOptions?.multiselect ? [] : '')"
101
101
  />
102
102
 
103
103
  <Input
104
- v-else-if="['string', 'text', 'json', 'richtext', 'unknown'].includes(c.type)"
104
+ v-else-if="['string', 'text', 'json', 'richtext', 'unknown'].includes(c.type ?? '')"
105
105
  type="text"
106
106
  full-width
107
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' })"
108
+ @update:modelValue="onFilterInput[c.name]({ column: c, operator: c.filterOptions?.substringSearch ? AFFO.ILIKE : AFFO.EQ, value: $event || undefined })"
109
+ :modelValue="(getFilterItem({ column: c, operator: c.filterOptions?.substringSearch ? AFFO.ILIKE : AFFO.EQ }) as string | number)"
110
110
  />
111
111
 
112
112
  <CustomDateRangePicker
113
- v-else-if="['datetime', 'date', 'time'].includes(c.type)"
113
+ v-else-if="['datetime', 'date', 'time'].includes(c.type ?? '')"
114
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 })"
115
+ :valueStart="filtersStore.filters.find(f => f.field === c.name && f.operator === AFFO.GTE)?.value || undefined"
116
+ @update:valueStart="onFilterInput[c.name]({ column: c, operator: AFFO.GTE, value: $event || undefined })"
117
+ :valueEnd="filtersStore.filters.find(f => f.field === c.name && f.operator === AFFO.LTE)?.value || undefined"
118
+ @update:valueEnd="onFilterInput[c.name]({ column: c, operator: AFFO.LTE, value: $event || undefined })"
119
119
  />
120
120
 
121
121
  <CustomRangePicker
122
- v-else-if="['integer', 'decimal', 'float'].includes(c.type) && c.allowMinMaxQuery"
122
+ v-else-if="['integer', 'decimal', 'float'].includes(c.type ?? '') && c.allowMinMaxQuery"
123
123
  :min="getFilterMinValue(c.name)"
124
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) ? (c.type === 'decimal' ? String($event) : $event) : undefined })"
127
- :valueEnd="getFilterItem({ column: c, operator: 'lte' })"
128
- @update:valueEnd="onFilterInput[c.name]({ column: c, operator: 'lte', value: ($event !== '' && $event !== null) ? (c.type === 'decimal' ? String($event) : $event) : undefined })"
125
+ :valueStart="(getFilterItem({ column: c, operator: AFFO.GTE }) as number)"
126
+ @update:valueStart="(val) => rangeChangeHandler((val !== '' && val !== null) ? (c.type === 'decimal' ? String(val) : val) : undefined, c, AFFO.GTE)"
127
+ :valueEnd="(getFilterItem({ column: c, operator: AFFO.LTE }) as number)"
128
+ @update:valueEnd="(val) => rangeChangeHandler((val !== '' && val !== null) ? (c.type === 'decimal' ? String(val) : val) : undefined, c, AFFO.LTE)"
129
129
  />
130
130
 
131
- <div v-else-if="['integer', 'decimal', 'float'].includes(c.type)" class="flex gap-2">
131
+ <div v-else-if="['integer', 'decimal', 'float'].includes(c.type ?? '')" class="flex gap-2">
132
132
  <Input
133
133
  type="number"
134
134
  aria-describedby="helper-text-explanation"
135
135
  :placeholder="$t('From')"
136
- @update:modelValue="onFilterInput[c.name]({ column: c, operator: 'gte', value: ($event !== '' && $event !== null) ? (c.type === 'decimal' ? String($event) : $event) : undefined })"
137
- :modelValue="getFilterItem({ column: c, operator: 'gte' })"
136
+ @update:modelValue="onFilterInput[c.name]({ column: c, operator: AFFO.GTE, value: ($event !== '' && $event !== null) ? (c.type === 'decimal' ? String($event) : $event) : undefined })"
137
+ :modelValue="(getFilterItem({ column: c, operator: AFFO.GTE }) as number)"
138
138
  />
139
139
  <Input
140
140
  type="number"
141
141
  aria-describedby="helper-text-explanation"
142
142
  :placeholder="$t('To')"
143
- @update:modelValue="onFilterInput[c.name]({ column: c, operator: 'lte', value: ($event !== '' && $event !== null) ? (c.type === 'decimal' ? String($event) : $event) : undefined })"
144
- :modelValue="getFilterItem({ column: c, operator: 'lte' })"
143
+ @update:modelValue="onFilterInput[c.name]({ column: c, operator: AFFO.LTE, value: ($event !== '' && $event !== null) ? (c.type === 'decimal' ? String($event) : $event) : undefined })"
144
+ :modelValue="(getFilterItem({ column: c, operator: AFFO.LTE }) as number)"
145
145
  />
146
146
  </div>
147
147
  </div>
@@ -164,14 +164,13 @@
164
164
  </div>
165
165
  </template>
166
166
 
167
- <script setup>
167
+ <script setup lang="ts">
168
168
  import { watch, computed, ref, reactive } from 'vue';
169
169
  import { useI18n } from 'vue-i18n';
170
170
  import CustomDateRangePicker from '@/components/CustomDateRangePicker.vue';
171
- import { callAdminForthApi, loadMoreForeignOptions, searchForeignOptions, createSearchInputHandlers } from '@/utils';
171
+ import { loadMoreForeignOptions, searchForeignOptions, createSearchInputHandlers, formatComponent } from '@/utils';
172
172
  import { useRouter } from 'vue-router';
173
173
  import CustomRangePicker from "@/components/CustomRangePicker.vue";
174
- import { useFiltersStore } from '@/stores/filters';
175
174
  import { getCustomComponent } from '@/utils';
176
175
  import Input from '@/afcl/Input.vue';
177
176
  import Select from '@/afcl/Select.vue';
@@ -179,27 +178,39 @@ import Spinner from '@/afcl/Spinner.vue';
179
178
  import debounce from 'debounce';
180
179
  import { Tooltip } from '@/afcl';
181
180
  import { IconCloseOutline } from '@iconify-prerendered/vue-flowbite';
181
+ import type { AdminforthFilterStore, AdminforthFilterStoreUnwrapped } from '@/spa_types/core';
182
+ import type { AdminForthResourceColumnCommon, FilterParams, ColumnMinMaxValue } from '@/types/Common';
183
+ import { AdminForthFilterOperators } from '@/types/Common';
184
+
182
185
 
183
- const filtersStore = useFiltersStore();
184
186
  const { t } = useI18n();
185
187
 
188
+ const AFFO = AdminForthFilterOperators;
186
189
 
187
190
  // props: columns
188
191
  // add support for v-model:filers
189
- const props = defineProps(['columns', 'filters', 'show', 'columnsMinMax']);
192
+ // const props = defineProps(['columns', 'filters', 'show', 'columnsMinMax']);
193
+ const props = defineProps<{
194
+ columns: AdminForthResourceColumnCommon[],
195
+ filters?: AdminforthFilterStore['filters'],
196
+ show: Boolean,
197
+ columnsMinMax: ColumnMinMaxValue,
198
+ filtersStore: AdminforthFilterStoreUnwrapped
199
+ }>();
200
+
190
201
  const emits = defineEmits(['update:filters', 'hide']);
191
202
 
192
203
  const router = useRouter();
193
204
 
194
205
 
195
206
  const columnsWithFilter = computed(
196
- () => props.columns?.filter(column => column.showIn.filter) || []
207
+ () => props.columns?.filter(column => column.showIn?.filter) || []
197
208
  );
198
209
 
199
- const columnOptions = ref({});
200
- const columnLoadingState = reactive({});
201
- const columnOffsets = reactive({});
202
- const columnEmptyResultsCount = reactive({});
210
+ const columnOptions = ref<{[key: string]: Record<string, any>[]}>({});
211
+ const columnLoadingState = reactive<Record<string, { loading: boolean; hasMore: boolean }>>({});
212
+ const columnOffsets = reactive<Record<string, number>>({});
213
+ const columnEmptyResultsCount = reactive<Record<string, number>>({});
203
214
 
204
215
  watch(() => props.columns, async (newColumns) => {
205
216
  if (!newColumns) return;
@@ -219,12 +230,12 @@ watch(() => props.columns, async (newColumns) => {
219
230
  }, { immediate: true });
220
231
 
221
232
  // Function to load more options for a specific column
222
- async function loadMoreOptions(columnName, searchTerm = '') {
233
+ async function loadMoreOptions(columnName: string, searchTerm = '') {
223
234
  return loadMoreForeignOptions({
224
235
  columnName,
225
236
  searchTerm,
226
237
  columns: props.columns,
227
- resourceId: router.currentRoute.value.params.resourceId,
238
+ resourceId: router.currentRoute.value.params.resourceId as string,
228
239
  columnOptions,
229
240
  columnLoadingState,
230
241
  columnOffsets,
@@ -232,12 +243,12 @@ async function loadMoreOptions(columnName, searchTerm = '') {
232
243
  });
233
244
  }
234
245
 
235
- async function searchOptions(columnName, searchTerm) {
246
+ async function searchOptions(columnName: string, searchTerm: string) {
236
247
  return searchForeignOptions({
237
248
  columnName,
238
249
  searchTerm,
239
250
  columns: props.columns,
240
- resourceId: router.currentRoute.value.params.resourceId,
251
+ resourceId: router.currentRoute.value.params.resourceId as string,
241
252
  columnOptions,
242
253
  columnLoadingState,
243
254
  columnOffsets,
@@ -263,9 +274,9 @@ watch(() => props.show, (show) => {
263
274
  // }
264
275
 
265
276
  const onFilterInput = computed(() => {
266
- if (!props.columns) return {};
277
+ if (!props.columns) return {} as Record<string, any>;
267
278
 
268
- return props.columns.reduce((acc, c) => {
279
+ return props.columns.reduce<Record<string, ReturnType<typeof debounce>>>((acc, c) => {
269
280
  return {
270
281
  ...acc,
271
282
  [c.name]: debounce(({ column, operator, value }) => {
@@ -275,6 +286,27 @@ const onFilterInput = computed(() => {
275
286
  }, {});
276
287
  });
277
288
 
289
+ // rangeState is used for cutom range picker, because if we change two values very quickly
290
+ // in filters writes only the last one, because of debounce
291
+ const rangeState = reactive<Record<string, { gte: number | null; lte: number | null }>>({});
292
+
293
+ const updateRange = (column: AdminForthResourceColumnCommon) => {
294
+ debounce(() => {
295
+ const { gte, lte } = rangeState[column.name];
296
+
297
+ setFilterItem({ column, operator: AFFO.GTE, value: gte });
298
+ setFilterItem({ column, operator: AFFO.LTE, value: lte });
299
+ }, column?.filterOptions?.debounceTimeMs || 10)();
300
+ }
301
+
302
+ function rangeChangeHandler(value: number | null, column: AdminForthResourceColumnCommon, operator: 'gte' | 'lte') {
303
+ if (!rangeState[column.name]) {
304
+ rangeState[column.name] = { gte: null, lte: null };
305
+ }
306
+ rangeState[column.name][operator] = value;
307
+ updateRange(column);
308
+ }
309
+
278
310
  const onSearchInput = computed(() => {
279
311
  return createSearchInputHandlers(
280
312
  props.columns,
@@ -283,40 +315,40 @@ const onSearchInput = computed(() => {
283
315
  );
284
316
  });
285
317
 
286
- function setFilterItem({ column, operator, value }) {
318
+ function setFilterItem({ column, operator, value }: { column: AdminForthResourceColumnCommon; operator: AdminForthFilterOperators; value: any }) {
287
319
 
288
- const index = filtersStore.filters.findIndex(f => f.field === column.name && f.operator === operator);
320
+ const index = props.filtersStore.filters.findIndex(f => f.field === column.name && f.operator === operator);
289
321
  if (value === undefined || value === '' || value === null) {
290
322
  if (index !== -1) {
291
- filtersStore.filters.splice(index, 1);
323
+ props.filtersStore.filters.splice(index, 1);
292
324
  }
293
325
  } else {
294
326
  if (index === -1) {
295
- filtersStore.setFilter({ field: column.name, value, operator });
327
+ props.filtersStore.setFilter({ field: column.name, value, operator });
296
328
  } else {
297
- filtersStore.setFilters([...filtersStore.filters.slice(0, index), { field: column.name, value, operator }, ...filtersStore.filters.slice(index + 1)])
329
+ props.filtersStore.setFilters([...props.filtersStore.filters.slice(0, index), { field: column.name, value, operator }, ...props.filtersStore.filters.slice(index + 1)])
298
330
  }
299
331
  }
300
- emits('update:filters', [...filtersStore.filters]);
332
+ emits('update:filters', [...props.filtersStore.filters]);
301
333
  }
302
334
 
303
- function getFilterItem({ column, operator }) {
304
- const filterValue = filtersStore.filters.find(f => f.field === column.name && f.operator === operator)?.value;
335
+ function getFilterItem({ column, operator }: { column: AdminForthResourceColumnCommon; operator: AdminForthFilterOperators }) {
336
+ const filterValue = props.filtersStore.filters.find(f => f.field === column.name && f.operator === operator)?.value;
305
337
  return filterValue !== undefined ? filterValue : '';
306
338
  }
307
339
 
308
340
  async function clear() {
309
- filtersStore.filters = [...filtersStore.filters.filter(f => filtersStore.shouldFilterBeHidden(f.field))];
310
- emits('update:filters', [...filtersStore.filters]);
341
+ props.filtersStore.filters = [...props.filtersStore.filters.filter(f => props.filtersStore.shouldFilterBeHidden(f.field))];
342
+ emits('update:filters', [...props.filtersStore.filters]);
311
343
  }
312
344
 
313
- function getFilterMinValue(columnName) {
345
+ function getFilterMinValue(columnName: string) {
314
346
  if(props.columnsMinMax && props.columnsMinMax[columnName]) {
315
347
  return props.columnsMinMax[columnName]?.min
316
348
  }
317
349
  }
318
350
 
319
- function getFilterMaxValue(columnName) {
351
+ function getFilterMaxValue(columnName: string) {
320
352
  if(props.columnsMinMax && props.columnsMinMax[columnName]) {
321
353
  return props.columnsMinMax[columnName]?.max
322
354
  }
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="rounded-lg shadow-resourseFormShadow dark:shadow-darkResourseFormShadow dark:shadow-2xl">
2
+ <div class="rounded-lg shadow-resourseFormShadow dark:shadow-darkResourseFormShadow dark:shadow-2xl border dark:border-darkFormBorder">
3
3
  <div v-if="group.groupName && !group.noTitle" class="text-md font-semibold px-6 py-3 flex flex-1 items-center dark:border-darkFormBorder text-lightListTableHeadingText bg-lightFormHeading dark:bg-darkFormHeading dark:text-darkListTableHeadingText rounded-t-lg">
4
4
  {{ group.groupName }}
5
5
  </div>
@@ -30,7 +30,7 @@
30
30
  <Tooltip v-if="column.required[mode]">
31
31
 
32
32
  <IconExclamationCircleSolid v-if="column.required[mode]" class="w-4 h-4"
33
- :class="(columnError(column) && validating) ? 'text-lightInputErrorColor dark:text-darkInputErrorColor' : 'text-lightRequiredIconColor dark:text-darkRequiredIconColor'"
33
+ :class="(columnsWithErrors[column.name] && validatingMode && !isValidating) ? 'text-lightInputErrorColor dark:text-darkInputErrorColor' : 'text-lightRequiredIconColor dark:text-darkRequiredIconColor'"
34
34
  />
35
35
 
36
36
  <template #tooltip>
@@ -56,9 +56,10 @@
56
56
  @update:inValidity="customComponentsInValidity[$event.name] = $event.value"
57
57
  @update:emptiness="customComponentsEmptiness[$event.name] = $event.value"
58
58
  :readonly="readonlyColumns?.includes(column.name)"
59
+ :columnsWithErrors="columnsWithErrors"
60
+ :isValidating="isValidating"
61
+ :validatingMode="validatingMode"
59
62
  />
60
- <div v-if="columnError(column) && validating" class="af-invalid-field-message mt-1 text-xs text-lightInputErrorColor dark:text-darkInputErrorColor">{{ columnError(column) }}</div>
61
- <div v-if="column.editingNote && column.editingNote[mode]" class="mt-1 text-xs text-lightFormFieldTextColor dark:text-darkFormFieldTextColor">{{ column.editingNote[mode] }}</div>
62
63
  </td>
63
64
  </tr>
64
65
  </tbody>
@@ -79,14 +80,16 @@
79
80
  const props = defineProps<{
80
81
  source: 'create' | 'edit',
81
82
  group: any,
82
- mode: string,
83
- validating: boolean,
83
+ mode: 'create' | 'edit',
84
+ validatingMode: boolean,
84
85
  currentValues: any,
85
86
  unmasked: any,
86
87
  columnError: (column: any) => string,
87
88
  setCurrentValue: (columnName: string, value: any) => void,
88
89
  columnOptions: any,
89
90
  readonlyColumns?: string[],
91
+ columnsWithErrors: Record<string, string>,
92
+ isValidating: boolean
90
93
  }>();
91
94
 
92
95
  const customComponentsInValidity: Ref<Record<string, boolean>> = ref({});