adminforth 2.4.0-next.21 → 2.4.0-next.210

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 (192) hide show
  1. package/commands/callTsProxy.js +14 -4
  2. package/commands/cli.js +10 -3
  3. package/commands/createApp/templates/custom/tsconfig.json.hbs +2 -3
  4. package/commands/createApp/templates/index.ts.hbs +10 -2
  5. package/commands/createApp/templates/package.json.hbs +1 -1
  6. package/commands/createApp/utils.js +27 -2
  7. package/commands/createCustomComponent/configLoader.js +3 -0
  8. package/commands/createCustomComponent/main.js +1 -0
  9. package/commands/createCustomComponent/templates/customCrud/beforeActionButtons.vue.hbs +38 -0
  10. package/commands/createPlugin/templates/custom/tsconfig.json.hbs +2 -5
  11. package/commands/createPlugin/templates/package.json.hbs +1 -1
  12. package/dist/auth.d.ts +9 -1
  13. package/dist/auth.d.ts.map +1 -1
  14. package/dist/auth.js +15 -2
  15. package/dist/auth.js.map +1 -1
  16. package/dist/dataConnectors/baseConnector.d.ts.map +1 -1
  17. package/dist/dataConnectors/baseConnector.js +46 -15
  18. package/dist/dataConnectors/baseConnector.js.map +1 -1
  19. package/dist/dataConnectors/clickhouse.d.ts.map +1 -1
  20. package/dist/dataConnectors/clickhouse.js +15 -0
  21. package/dist/dataConnectors/clickhouse.js.map +1 -1
  22. package/dist/dataConnectors/mongo.d.ts.map +1 -1
  23. package/dist/dataConnectors/mongo.js +44 -15
  24. package/dist/dataConnectors/mongo.js.map +1 -1
  25. package/dist/dataConnectors/mysql.d.ts.map +1 -1
  26. package/dist/dataConnectors/mysql.js +11 -0
  27. package/dist/dataConnectors/mysql.js.map +1 -1
  28. package/dist/dataConnectors/postgres.d.ts.map +1 -1
  29. package/dist/dataConnectors/postgres.js +11 -0
  30. package/dist/dataConnectors/postgres.js.map +1 -1
  31. package/dist/dataConnectors/sqlite.d.ts.map +1 -1
  32. package/dist/dataConnectors/sqlite.js +11 -0
  33. package/dist/dataConnectors/sqlite.js.map +1 -1
  34. package/dist/index.d.ts +2 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +20 -9
  37. package/dist/index.js.map +1 -1
  38. package/dist/modules/codeInjector.d.ts +2 -0
  39. package/dist/modules/codeInjector.d.ts.map +1 -1
  40. package/dist/modules/codeInjector.js +52 -8
  41. package/dist/modules/codeInjector.js.map +1 -1
  42. package/dist/modules/configValidator.d.ts.map +1 -1
  43. package/dist/modules/configValidator.js +74 -7
  44. package/dist/modules/configValidator.js.map +1 -1
  45. package/dist/modules/restApi.d.ts.map +1 -1
  46. package/dist/modules/restApi.js +154 -26
  47. package/dist/modules/restApi.js.map +1 -1
  48. package/dist/modules/styles.d.ts +503 -13
  49. package/dist/modules/styles.d.ts.map +1 -1
  50. package/dist/modules/styles.js +559 -31
  51. package/dist/modules/styles.js.map +1 -1
  52. package/dist/modules/utils.d.ts +2 -0
  53. package/dist/modules/utils.d.ts.map +1 -1
  54. package/dist/modules/utils.js +16 -0
  55. package/dist/modules/utils.js.map +1 -1
  56. package/dist/servers/express.d.ts.map +1 -1
  57. package/dist/servers/express.js +14 -0
  58. package/dist/servers/express.js.map +1 -1
  59. package/dist/spa/index.html +1 -1
  60. package/dist/spa/package-lock.json +5 -4
  61. package/dist/spa/package.json +1 -1
  62. package/dist/spa/src/App.vue +54 -169
  63. package/dist/spa/src/adminforth.ts +42 -18
  64. package/dist/spa/src/afcl/BarChart.vue +2 -2
  65. package/dist/spa/src/afcl/Button.vue +6 -6
  66. package/dist/spa/src/afcl/ButtonGroup.vue +91 -0
  67. package/dist/spa/src/afcl/Card.vue +25 -0
  68. package/dist/spa/src/afcl/Checkbox.vue +21 -13
  69. package/dist/spa/src/afcl/CountryFlag.vue +4 -1
  70. package/dist/spa/src/{components/CustomDatePicker.vue → afcl/DatePicker.vue} +95 -9
  71. package/dist/spa/src/afcl/Dialog.vue +44 -27
  72. package/dist/spa/src/afcl/Dropzone.vue +12 -12
  73. package/dist/spa/src/afcl/Input.vue +6 -6
  74. package/dist/spa/src/afcl/JsonViewer.vue +25 -0
  75. package/dist/spa/src/afcl/LinkButton.vue +3 -3
  76. package/dist/spa/src/afcl/PieChart.vue +5 -5
  77. package/dist/spa/src/afcl/ProgressBar.vue +7 -7
  78. package/dist/spa/src/afcl/Select.vue +68 -34
  79. package/dist/spa/src/afcl/Skeleton.vue +6 -6
  80. package/dist/spa/src/afcl/Table.vue +213 -74
  81. package/dist/spa/src/afcl/Textarea.vue +31 -0
  82. package/dist/spa/src/afcl/Toggle.vue +32 -0
  83. package/dist/spa/src/afcl/Tooltip.vue +1 -2
  84. package/dist/spa/src/afcl/VerticalTabs.vue +16 -7
  85. package/dist/spa/src/afcl/index.ts +6 -3
  86. package/dist/spa/src/components/AcceptModal.vue +7 -7
  87. package/dist/spa/src/components/Breadcrumbs.vue +5 -5
  88. package/dist/spa/src/components/ColumnValueInput.vue +38 -18
  89. package/dist/spa/src/components/ColumnValueInputWrapper.vue +4 -3
  90. package/dist/spa/src/components/CustomDateRangePicker.vue +9 -8
  91. package/dist/spa/src/components/CustomRangePicker.vue +37 -8
  92. package/dist/spa/src/components/ErrorMessage.vue +21 -0
  93. package/dist/spa/src/components/Filters.vue +85 -39
  94. package/dist/spa/src/components/GroupsTable.vue +9 -8
  95. package/dist/spa/src/components/MenuLink.vue +90 -23
  96. package/dist/spa/src/components/ResourceForm.vue +94 -51
  97. package/dist/spa/src/components/ResourceListTable.vue +78 -80
  98. package/dist/spa/src/components/ResourceListTableVirtual.vue +71 -73
  99. package/dist/spa/src/components/ShowTable.vue +17 -12
  100. package/dist/spa/src/components/Sidebar.vue +448 -0
  101. package/dist/spa/src/components/SingleSkeletLoader.vue +6 -6
  102. package/dist/spa/src/components/SkeleteLoader.vue +3 -3
  103. package/dist/spa/src/components/ThreeDotsMenu.vue +73 -14
  104. package/dist/spa/src/components/Toast.vue +27 -9
  105. package/dist/spa/src/components/UserMenuSettingsButton.vue +70 -0
  106. package/dist/spa/src/components/ValueRenderer.vue +43 -16
  107. package/dist/spa/src/controls/BoolToggle.vue +34 -0
  108. package/dist/spa/src/i18n.ts +1 -1
  109. package/dist/spa/src/renderers/CompactField.vue +1 -1
  110. package/dist/spa/src/renderers/CompactUUID.vue +1 -1
  111. package/dist/spa/src/router/index.ts +8 -0
  112. package/dist/spa/src/shims-vue.d.ts +5 -0
  113. package/dist/spa/src/spa_types/core.ts +13 -1
  114. package/dist/spa/src/stores/core.ts +1 -1
  115. package/dist/spa/src/stores/filters.ts +29 -2
  116. package/dist/spa/src/stores/modal.ts +6 -1
  117. package/dist/spa/src/stores/toast.ts +22 -3
  118. package/dist/spa/src/types/Back.ts +137 -22
  119. package/dist/spa/src/types/Common.ts +67 -32
  120. package/dist/spa/src/types/FrontendAPI.ts +31 -5
  121. package/dist/spa/src/types/adapters/CaptchaAdapter.ts +34 -0
  122. package/dist/spa/src/types/adapters/CompletionAdapter.ts +25 -0
  123. package/dist/spa/src/types/adapters/EmailAdapter.ts +27 -0
  124. package/dist/spa/src/types/adapters/ImageGenerationAdapter.ts +50 -0
  125. package/dist/spa/src/types/adapters/ImageVisionAdapter.ts +30 -0
  126. package/dist/spa/src/types/adapters/KeyValueAdapter.ts +16 -0
  127. package/dist/spa/src/types/adapters/OAuth2Adapter.ts +34 -0
  128. package/dist/spa/src/types/adapters/StorageAdapter.ts +73 -0
  129. package/dist/spa/src/types/adapters/index.ts +8 -0
  130. package/dist/spa/src/utils.ts +219 -8
  131. package/dist/spa/src/views/CreateView.vue +18 -19
  132. package/dist/spa/src/views/EditView.vue +25 -19
  133. package/dist/spa/src/views/ListView.vue +139 -86
  134. package/dist/spa/src/views/LoginView.vue +31 -37
  135. package/dist/spa/src/views/ResourceParent.vue +2 -2
  136. package/dist/spa/src/views/SettingsView.vue +121 -0
  137. package/dist/spa/src/views/ShowView.vue +59 -39
  138. package/dist/spa/src/websocket.ts +6 -1
  139. package/dist/spa/tsconfig.app.json +1 -1
  140. package/dist/spa/vite.config.ts +45 -2
  141. package/dist/types/Back.d.ts +115 -14
  142. package/dist/types/Back.d.ts.map +1 -1
  143. package/dist/types/Back.js +15 -0
  144. package/dist/types/Back.js.map +1 -1
  145. package/dist/types/Common.d.ts +59 -29
  146. package/dist/types/Common.d.ts.map +1 -1
  147. package/dist/types/Common.js.map +1 -1
  148. package/dist/types/FrontendAPI.d.ts +31 -3
  149. package/dist/types/FrontendAPI.d.ts.map +1 -1
  150. package/dist/types/FrontendAPI.js.map +1 -1
  151. package/dist/types/adapters/CaptchaAdapter.d.ts +30 -0
  152. package/dist/types/adapters/CaptchaAdapter.d.ts.map +1 -0
  153. package/dist/types/adapters/CaptchaAdapter.js +5 -0
  154. package/dist/types/adapters/CaptchaAdapter.js.map +1 -0
  155. package/dist/types/adapters/CompletionAdapter.d.ts +20 -0
  156. package/dist/types/adapters/CompletionAdapter.d.ts.map +1 -0
  157. package/dist/types/adapters/CompletionAdapter.js +2 -0
  158. package/dist/types/adapters/CompletionAdapter.js.map +1 -0
  159. package/dist/types/adapters/EmailAdapter.d.ts +20 -0
  160. package/dist/types/adapters/EmailAdapter.d.ts.map +1 -0
  161. package/dist/types/adapters/EmailAdapter.js +2 -0
  162. package/dist/types/adapters/EmailAdapter.js.map +1 -0
  163. package/dist/types/adapters/ImageGenerationAdapter.d.ts +37 -0
  164. package/dist/types/adapters/ImageGenerationAdapter.d.ts.map +1 -0
  165. package/dist/types/adapters/ImageGenerationAdapter.js +2 -0
  166. package/dist/types/adapters/ImageGenerationAdapter.js.map +1 -0
  167. package/dist/types/adapters/ImageVisionAdapter.d.ts +25 -0
  168. package/dist/types/adapters/ImageVisionAdapter.d.ts.map +1 -0
  169. package/dist/types/adapters/ImageVisionAdapter.js +2 -0
  170. package/dist/types/adapters/ImageVisionAdapter.js.map +1 -0
  171. package/dist/types/adapters/KeyValueAdapter.d.ts +10 -0
  172. package/dist/types/adapters/KeyValueAdapter.d.ts.map +1 -0
  173. package/dist/types/adapters/KeyValueAdapter.js +2 -0
  174. package/dist/types/adapters/KeyValueAdapter.js.map +1 -0
  175. package/dist/types/adapters/OAuth2Adapter.d.ts +32 -0
  176. package/dist/types/adapters/OAuth2Adapter.d.ts.map +1 -0
  177. package/dist/types/adapters/OAuth2Adapter.js +2 -0
  178. package/dist/types/adapters/OAuth2Adapter.js.map +1 -0
  179. package/dist/types/adapters/StorageAdapter.d.ts +63 -0
  180. package/dist/types/adapters/StorageAdapter.d.ts.map +1 -0
  181. package/dist/types/adapters/StorageAdapter.js +2 -0
  182. package/dist/types/adapters/StorageAdapter.js.map +1 -0
  183. package/dist/types/adapters/index.d.ts +9 -0
  184. package/dist/types/adapters/index.d.ts.map +1 -0
  185. package/dist/types/adapters/index.js +2 -0
  186. package/dist/types/adapters/index.js.map +1 -0
  187. package/package.json +3 -2
  188. package/dist/spa/src/types/Adapters.ts +0 -213
  189. package/dist/types/Adapters.d.ts +0 -168
  190. package/dist/types/Adapters.d.ts.map +0 -1
  191. package/dist/types/Adapters.js +0 -2
  192. package/dist/types/Adapters.js.map +0 -1
@@ -17,14 +17,29 @@
17
17
  <Select
18
18
  v-else-if="column.foreignResource"
19
19
  ref="input"
20
+ :key="`${column.name}-${(columnOptions[column.name] || []).length}`"
20
21
  class="w-full min-w-24"
21
22
  :options="columnOptions[column.name] || []"
23
+ :searchDisabled="!column.foreignResource.searchableFields"
24
+ @scroll-near-end="loadMoreOptions && loadMoreOptions(column.name)"
25
+ @search="(searchTerm) => {
26
+ if (column.foreignResource.searchableFields && onSearchInput && onSearchInput[column.name]) {
27
+ onSearchInput[column.name](searchTerm);
28
+ }
29
+ }"
22
30
  teleportToBody
23
31
  :placeholder = "columnOptions[column.name]?.length ?$t('Select...'): $t('There are no options available')"
24
32
  :modelValue="value"
25
33
  :readonly="(column.editReadonly && source === 'edit') || readonly"
26
34
  @update:modelValue="$emit('update:modelValue', $event)"
27
- />
35
+ >
36
+ <template #extra-item v-if="columnLoadingState && columnLoadingState[column.name]?.loading">
37
+ <div class="text-center text-gray-400 dark:text-gray-300 py-2 flex items-center justify-center gap-2">
38
+ <Spinner class="w-4 h-4" />
39
+ {{ $t('Loading...') }}
40
+ </div>
41
+ </template>
42
+ </Select>
28
43
  <Select
29
44
  v-else-if="column.enum"
30
45
  ref="input"
@@ -52,6 +67,7 @@
52
67
  step="1"
53
68
  class="w-40"
54
69
  placeholder="0"
70
+ :fullWidth="true"
55
71
  :min="![undefined, null].includes(column.minValue) ? column.minValue : ''"
56
72
  :max="![undefined, null].includes(column.maxValue) ? column.maxValue : ''"
57
73
  :prefix="column.inputPrefix"
@@ -60,7 +76,7 @@
60
76
  :modelValue="value"
61
77
  @update:modelValue="$emit('update:modelValue', $event)"
62
78
  />
63
- <CustomDatePicker
79
+ <DatePicker
64
80
  v-else-if="['datetime', 'date', 'time'].includes(type || column.type)"
65
81
  ref="input"
66
82
  :column="column"
@@ -76,6 +92,7 @@
76
92
  step="0.1"
77
93
  class="w-40"
78
94
  placeholder="0.0"
95
+ :fullWidth="true"
79
96
  :min="![undefined, null].includes(column.minValue) ? column.minValue : ''"
80
97
  :max="![undefined, null].includes(column.maxValue) ? column.maxValue : ''"
81
98
  :prefix="column.inputPrefix"
@@ -84,28 +101,25 @@
84
101
  @update:modelValue="$emit('update:modelValue', $event)"
85
102
  :readonly="(column.editReadonly && source === 'edit') || readonly"
86
103
  />
87
- <textarea
104
+ <Textarea
88
105
  v-else-if="['text', 'richtext'].includes(type || column.type)"
89
- ref="input"
90
- class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white focus:ring-lightPrimary focus:border-lightPrimary dark:focus:ring-darkPrimary dark:focus:border-darkPrimary"
91
106
  :placeholder="$t('Text')"
92
- :value="value"
93
- @input="$emit('update:modelValue', $event.target.value)"
107
+ :modelValue="value"
108
+ @update:modelValue="$emit('update:modelValue', $event)"
94
109
  :readonly="(column.editReadonly && source === 'edit') || readonly"
95
110
  />
96
- <textarea
111
+ <Textarea
97
112
  v-else-if="['json'].includes(type || column.type)"
98
- ref="input"
99
- class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white focus:ring-lightPrimary focus:border-lightPrimary dark:focus:ring-darkPrimary dark:focus:border-darkPrimary"
100
113
  :placeholder="$t('Text')"
101
- :value="value"
102
- @input="$emit('update:modelValue', $event.target.value)"
114
+ :modelValue="value"
115
+ @update:modelValue="$emit('update:modelValue', $event)"
103
116
  />
104
117
  <Input
105
118
  v-else
106
119
  ref="input"
107
120
  :type="!column.masked || unmasked[column.name] ? 'text' : 'password'"
108
121
  class="w-full"
122
+ :fullWidth="true"
109
123
  :placeholder="$t('Text')"
110
124
  :prefix="column.inputPrefix"
111
125
  :suffix="column.inputSuffix"
@@ -123,7 +137,7 @@
123
137
  class="h-6 inset-y-2 right-0 flex items-center px-2 pt-4 z-index-100 focus:outline-none"
124
138
  @click="$emit('delete')"
125
139
  >
126
- <IconTrashBinSolid class="w-6 h-6 text-gray-400"/>
140
+ <IconTrashBinSolid class="w-6 h-6 text-lightInputIcons dark:text-darkInputIcons"/>
127
141
  </button>
128
142
  <button
129
143
  v-else-if="column.masked"
@@ -131,18 +145,20 @@
131
145
  @click="$emit('update:unmasked')"
132
146
  class="h-6 inset-y-2 right-0 flex items-center px-2 pt-4 z-index-100 focus:outline-none"
133
147
  >
134
- <IconEyeSolid class="w-6 h-6 text-gray-400" v-if="!unmasked[column.name]"/>
135
- <IconEyeSlashSolid class="w-6 h-6 text-gray-400" v-else />
148
+ <IconEyeSolid class="w-6 h-6 text-lightInputIcons dark:text-darkInputIcons" v-if="!unmasked[column.name]"/>
149
+ <IconEyeSlashSolid class="w-6 h-6 text-lightInputIcons dark:text-darkInputIcons" v-else />
136
150
  </button>
137
151
  </div>
138
152
  </template>
139
153
 
140
154
  <script setup lang="ts">
141
155
  import { IconEyeSlashSolid, IconEyeSolid, IconTrashBinSolid } from '@iconify-prerendered/vue-flowbite';
142
- import CustomDatePicker from "@/components/CustomDatePicker.vue";
156
+ import DatePicker from "@/afcl/DatePicker.vue";
143
157
  import Select from '@/afcl/Select.vue';
144
158
  import Input from '@/afcl/Input.vue';
145
- import { ref } from 'vue';
159
+ import Spinner from '@/afcl/Spinner.vue';
160
+ import Textarea from '@/afcl/Textarea.vue';
161
+ import { ref, inject } from 'vue';
146
162
  import { getCustomComponent } from '@/utils';
147
163
  import { useI18n } from 'vue-i18n';
148
164
  import { useCoreStore } from '@/stores/core';
@@ -171,7 +187,11 @@
171
187
  }
172
188
  );
173
189
 
174
- const input = ref(null);
190
+ const columnLoadingState = inject('columnLoadingState', {} as any);
191
+ const onSearchInput = inject('onSearchInput', {} as any);
192
+ const loadMoreOptions = inject('loadMoreOptions', (() => {}) as any);
193
+
194
+ const input = ref<HTMLInputElement | null>(null);
175
195
 
176
196
  const getBooleanOptions = (column: any) => {
177
197
  const options: Array<{ label: string; value: boolean | null }> = [
@@ -13,12 +13,13 @@
13
13
  :currentValues="currentValues"
14
14
  :mode="mode"
15
15
  :columnOptions="columnOptions"
16
+ :unmasked="unmasked"
16
17
  :deletable="!column.editReadonly"
17
18
  @update:modelValue="setCurrentValue(column.name, $event, arrayItemIndex)"
18
19
  @update:unmasked="$emit('update:unmasked', column.name)"
19
20
  @update:inValidity="$emit('update:inValidity', { name: column.name, value: $event })"
20
21
  @update:emptiness="$emit('update:emptiness', { name: column.name, value: $event })"
21
- @delete="setCurrentValue(column.name, currentValues[column.name].filter((_, index) => index !== arrayItemIndex))"
22
+ @delete="setCurrentValue(column.name, currentValues[column.name].filter((_: any, index: any) => index !== arrayItemIndex))"
22
23
  />
23
24
  </div>
24
25
  <div class="flex items-center">
@@ -26,7 +27,7 @@
26
27
  v-if="!column.editReadonly"
27
28
  type="button"
28
29
  @click="addArrayItem"
29
- class="flex items-center py-1 px-3 me-2 text-sm font-medium rounded-default text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
30
+ class="flex items-center py-1 px-3 me-2 text-sm font-medium rounded-default text-lightInputText focus:outline-none bg-lightInputBackground rounded border border-lightInputBorder hover:bg-lightInputBackgroundHover hover:text-lightInputTextHover hover:border-lightInputBorderHover focus:z-10 focus:ring-4 focus:ring-lightInputFocusRing dark:focus:ring-darkInputFocusRing dark:bg-darkInputBackground dark:text-darkInputText dark:border-darkInputBorder dark:hover:darkInputTextHover dark:hover:bg-darkInputHover"
30
31
  :class="{'mt-2': currentValues[column.name].length}"
31
32
  >
32
33
  <IconPlusOutline class="w-4 h-4 me-2"/>
@@ -70,7 +71,7 @@
70
71
 
71
72
  const emit = defineEmits(['update:unmasked', 'update:inValidity', 'update:emptiness', 'focus-last-input']);
72
73
 
73
- const arrayItemRefs = ref([]);
74
+ const arrayItemRefs = ref<HTMLInputElement[]>([]);
74
75
 
75
76
  async function addArrayItem() {
76
77
  props.setCurrentValue(props.column.name, props.currentValues[props.column.name], props.currentValues[props.column.name].length);
@@ -3,21 +3,21 @@
3
3
  <div class="mx-auto grid grid-cols-2 gap-4 mb-2" :class="{hidden: column.type === 'time'}">
4
4
  <div class="relative">
5
5
  <div class="absolute inset-y-0 end-0 top-0 flex items-center pe-3.5 pointer-events-none">
6
- <IconCalendar class="w-4 h-4 text-gray-500 dark:text-gray-400"/>
6
+ <IconCalendar class="w-4 h-4 text-lightDatePickerIcon dark:text-darkDatePickerIcon"/>
7
7
  </div>
8
8
 
9
9
  <input ref="datepickerStartEl" type="text"
10
- class="bg-gray-50 border leading-none border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
10
+ class="bg-lightDatePickerButtonBackground border leading-none border-lightDatePickerButtonBorder text-lightDatePickerButtonText placeholder-lightDatePickerPlaceHolder text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-darkDatePickerButtonBackground dark:border-darkDatePickerButtonBorder dark:placeholder-darkDatePickerPlaceHolder dark:text-darkDatePickerButtonText dark:focus:ring-blue-500 dark:focus:border-blue-500"
11
11
  :placeholder="$t('From')">
12
12
  </div>
13
13
 
14
14
  <div class="relative">
15
15
  <div class="absolute inset-y-0 end-0 top-0 flex items-center pe-3.5 pointer-events-none">
16
- <IconCalendar class="w-4 h-4 text-gray-500 dark:text-gray-400"/>
16
+ <IconCalendar class="w-4 h-4 text-lightDatePickerIcon dark:text-darkDatePickerIcon"/>
17
17
  </div>
18
18
 
19
19
  <input ref="datepickerEndEl" type="text"
20
- class="bg-gray-50 border leading-none border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
20
+ class="bg-lightDatePickerButtonBackground border leading-none border-lightDatePickerButtonBorder text-lightDatePickerButtonText placeholder-lightDatePickerPlaceHolder text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-darkDatePickerButtonBackground dark:border-darkDatePickerButtonBorder dark:placeholder-darkDatePickerPlaceHolder dark:text-darkDatePickerButtonText dark:focus:ring-blue-500 dark:focus:border-blue-500"
21
21
  :placeholder="$t('To')">
22
22
  </div>
23
23
  </div>
@@ -27,11 +27,11 @@
27
27
  <div>
28
28
  <div class="relative">
29
29
  <div class="absolute inset-y-0 end-0 top-0 flex items-center pe-3.5 pointer-events-none">
30
- <IconTime class="w-4 h-4 text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-700"/>
30
+ <IconTime class="w-4 h-4 text-lightDatePickerIcon dark:text-darkDatePickerIcon bg-lightDatePickerButtonBackground dark:bg-darkDatePickerButtonBackground"/>
31
31
  </div>
32
32
 
33
33
  <input v-model="startTime" type="time" id="start-time"
34
- class="bg-gray-50 border leading-none border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
34
+ class="bg-lightDatePickerButtonBackground border leading-none border-lightDatePickerButtonBorder text-lightDatePickerButtonText placeholder-lightDatePickerPlaceHolder text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-darkDatePickerButtonBackground dark:border-darkDatePickerButtonBorder dark:placeholder-darkDatePickerPlaceHolder dark:text-darkDatePickerButtonText dark:focus:ring-blue-500 dark:focus:border-blue-500"
35
35
  value="00:00" required/>
36
36
  </div>
37
37
  </div>
@@ -39,11 +39,11 @@
39
39
  <div>
40
40
  <div class="relative">
41
41
  <div class="absolute inset-y-0 end-0 top-0 flex items-center pe-3.5 pointer-events-none">
42
- <IconTime class="w-4 h-4 text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-700"/>
42
+ <IconTime class="w-4 h-4 text-lightDatePickerIcon dark:text-darkDatePickerIcon bg-lightDatePickerButtonBackground dark:bg-darkDatePickerButtonBackground"/>
43
43
  </div>
44
44
 
45
45
  <input v-model="endTime" type="time" id="end-time"
46
- class="bg-gray-50 border leading-none border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
46
+ class="bg-lightDatePickerButtonBackground border leading-none border-lightDatePickerButtonBorder text-lightDatePickerButtonText placeholder-lightDatePickerPlaceHolder text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-darkDatePickerButtonBackground dark:border-darkDatePickerButtonBorder dark:placeholder-darkDatePickerPlaceHolder dark:text-darkDatePickerButtonText dark:focus:ring-blue-500 dark:focus:border-blue-500"
47
47
  value="00:00" required/>
48
48
  </div>
49
49
  </div>
@@ -172,6 +172,7 @@ function updateFromProps() {
172
172
  if (!props.valueEnd) {
173
173
  datepickerEndEl.value.value = '';
174
174
  endTime.value = '';
175
+ endDate.value = '';
175
176
  } else if (props.column.type === 'time') {
176
177
  endTime.value = props.valueEnd;
177
178
  } else {
@@ -4,7 +4,7 @@
4
4
  :min="minFormatted"
5
5
  :max="maxFormatted"
6
6
  type="number" aria-describedby="helper-text-explanation"
7
- class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-20 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
7
+ class="bg-lightRangePickerInputBackground border border-lightRangePickerInputBorder text-lightRangePickerInputText placeholder-lightRangePickerInputPlaceholder text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-20 p-2.5 dark:bg-darkRangePickerInputBackground dark:border-darkRangePickerInputBorder dark:placeholder-darkRangePickerInputPlaceholder dark:text-darkRangePickerInputText dark:focus:ring-blue-500 dark:focus:border-blue-500"
8
8
  :placeholder="$t('From')"
9
9
  v-model="start"
10
10
  >
@@ -13,7 +13,7 @@
13
13
  :min="minFormatted"
14
14
  :max="maxFormatted"
15
15
  type="number" aria-describedby="helper-text-explanation"
16
- class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-20 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
16
+ class="bg-lightRangePickerInputBackground border border-lightRangePickerInputBorder text-lightRangePickerInputText placeholder-lightRangePickerInputPlaceholder text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-20 p-2.5 dark:bg-darkRangePickerInputBackground dark:border-darkRangePickerInputBorder dark:placeholder-darkRangePickerInputPlaceholder dark:text-darkRangePickerInputText dark:focus:ring-blue-500 dark:focus:border-blue-500"
17
17
  :placeholder="$t('To')"
18
18
  v-model="end"
19
19
  >
@@ -21,7 +21,7 @@
21
21
  <button
22
22
  v-if="isChanged"
23
23
  type="button"
24
- class="flex items-center p-0.5 ml-auto px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
24
+ class="flex items-center p-0.5 ml-auto px-3 text-sm font-medium text-lightRangePickerButtonText focus:outline-none bg-lightRangePickerButtonBackground rounded border border-lightRangePickerButtonBorder hover:bg-lightRangePickerButtonBackgroundHover hover:text-lightRangePickerButtonTextHover focus:z-10 focus:ring-4 focus:ring-lightRangePickerFocusRing dark:focus:ring-darkRangePickerFocusRing dark:bg-darkRangePickerButtonBackground dark:text-darkRangePickerButtonText dark:border-darkRangePickerButtonBorder dark:hover:text-darkRangePickerButtonTextHover dark:hover:bg-darkRangePickerButtonBackgroundHover disabled:opacity-50 disabled:cursor-not-allowed"
25
25
  @click="clear">Clear
26
26
  </button>
27
27
 
@@ -57,15 +57,15 @@ const props = defineProps({
57
57
 
58
58
  const emit = defineEmits(['update:valueStart', 'update:valueEnd']);
59
59
 
60
- const minFormatted = computed(() => Math.floor(props.min));
61
- const maxFormatted = computed(() => Math.ceil(props.max));
60
+ const minFormatted = computed(() => Math.floor(<number>props.min));
61
+ const maxFormatted = computed(() => Math.ceil(<number>props.max));
62
62
 
63
63
  const isChanged = computed(() => {
64
64
  return start.value && start.value !== minFormatted.value || end.value && end.value !== maxFormatted.value;
65
65
  });
66
66
 
67
- const start = ref(props.valueStart);
68
- const end = ref(props.valueEnd);
67
+ const start = ref<string | number>(props.valueStart);
68
+ const end = ref<string | number>(props.valueEnd);
69
69
 
70
70
  const sliderValue = ref([minFormatted.value, maxFormatted.value]);
71
71
 
@@ -118,7 +118,7 @@ const clear = () => {
118
118
  setSliderValues('', '')
119
119
  }
120
120
 
121
- function setSliderValues(start, end) {
121
+ function setSliderValues(start: any, end: any) {
122
122
  sliderValue.value = [start || minFormatted.value, end || maxFormatted.value];
123
123
  }
124
124
  </script>
@@ -153,4 +153,33 @@ function setSliderValues(start, end) {
153
153
  @apply bg-lightPrimaryOpacity;
154
154
  }
155
155
  }
156
+
157
+ .dark .custom-slider {
158
+ &:deep(.vue-slider-rail) {
159
+ background-color: rgb(55 65 81); // gray-700
160
+ }
161
+
162
+ &:deep(.vue-slider-dot-handle) {
163
+ @apply bg-darkPrimary;
164
+ border: none;
165
+ box-shadow: none;
166
+ }
167
+
168
+ &:deep(.vue-slider-dot-handle:hover) {
169
+ @apply bg-darkPrimary;
170
+ filter: brightness(1.1);
171
+ border: none;
172
+ box-shadow: none;
173
+ }
174
+
175
+ &:deep(.vue-slider-process) {
176
+ @apply bg-darkPrimaryOpacity;
177
+ }
178
+
179
+ &:deep(.vue-slider-process:hover) {
180
+ filter: brightness(1.1);
181
+ @apply bg-darkPrimaryOpacity;
182
+ }
183
+ }
184
+
156
185
  </style>
@@ -0,0 +1,21 @@
1
+ <template>
2
+ <div v-if="error" class="af-login-modal-error flex items-center p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400" role="alert">
3
+ <svg class="flex-shrink-0 inline w-4 h-4 me-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
4
+ <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z"/>
5
+ </svg>
6
+ <span class="sr-only">{{ $t('Info') }}</span>
7
+ <div>
8
+ {{ error }}
9
+ </div>
10
+ </div>
11
+ </template>
12
+
13
+ <script setup>
14
+
15
+ defineProps({
16
+ error: {
17
+ type: String,
18
+ default: null
19
+ }
20
+ });
21
+ </script>
@@ -2,16 +2,16 @@
2
2
  <!-- drawer component -->
3
3
  <div id="drawer-navigation"
4
4
 
5
- class="fixed right-0 z-50 p-4 overflow-y-auto transition-transform translate-x-full bg-white w-80 dark:bg-gray-800 shadow-xl dark:shadow-gray-900"
5
+ class="af-filters-sidebar fixed right-0 z-50 p-4 overflow-y-auto transition-transform translate-x-full bg-lightFiltersBackgroung w-80 dark:bg-darkFiltersBackgroung shadow-xl dark:shadow-gray-900"
6
6
 
7
7
  :class="show ? 'top-0 transform-none' : ''"
8
8
  tabindex="-1" aria-labelledby="drawer-navigation-label"
9
9
  :style="{ height: `calc(100dvh ` }"
10
10
  >
11
- <h5 id="drawer-navigation-label" class="text-base font-semibold text-gray-500 uppercase dark:text-gray-400">
11
+ <h5 id="drawer-navigation-label" class="text-base font-semibold text-lightFiltersHeaderText uppercase dark:text-darkFiltersHeaderText">
12
12
  {{ $t('Filters') }}
13
13
 
14
- <button type="button" @click="$emit('hide')" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 absolute end-2.5 inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white" >
14
+ <button type="button" @click="$emit('hide')" class="text-lightFiltersCloseIcon bg-transparent hover:bg-lightFiltersCloseIconHoverBackground hover:text-lightFiltersCloseIconHover rounded-lg text-sm p-1.5 absolute end-2.5 inline-flex items-center dark:text-darkFiltersCloseIcon dark:hover:bg-darkFiltersCloseIconHoverBackground dark:hover:text-darkFiltersCloseIconHover" >
15
15
  <svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
16
16
  <span class="sr-only">{{ $t('Close menu') }}</span>
17
17
  </button>
@@ -43,9 +43,23 @@
43
43
  :multiple="c.filterOptions.multiselect"
44
44
  class="w-full"
45
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
+ }"
46
53
  @update:modelValue="onFilterInput[c.name]({ column: c, operator: c.filterOptions.multiselect ? 'in' : 'eq', value: c.filterOptions.multiselect ? ($event.length ? $event : undefined) : $event || undefined })"
47
54
  :modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === (c.filterOptions.multiselect ? 'in' : 'eq'))?.value || (c.filterOptions.multiselect ? [] : '')"
48
- />
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>
49
63
  <Select
50
64
  :multiple="c.filterOptions.multiselect"
51
65
  class="w-full"
@@ -94,9 +108,9 @@
94
108
  :min="getFilterMinValue(c.name)"
95
109
  :max="getFilterMaxValue(c.name)"
96
110
  :valueStart="getFilterItem({ column: c, operator: 'gte' })"
97
- @update:valueStart="onFilterInput[c.name]({ column: c, operator: 'gte', value: $event || undefined })"
111
+ @update:valueStart="onFilterInput[c.name]({ column: c, operator: 'gte', value: ($event !== '' && $event !== null) ? $event : undefined })"
98
112
  :valueEnd="getFilterItem({ column: c, operator: 'lte' })"
99
- @update:valueEnd="onFilterInput[c.name]({ column: c, operator: 'lte', value: $event || undefined })"
113
+ @update:valueEnd="onFilterInput[c.name]({ column: c, operator: 'lte', value: ($event !== '' && $event !== null) ? $event : undefined })"
100
114
  />
101
115
 
102
116
  <div v-else-if="['integer', 'decimal', 'float'].includes(c.type)" class="flex gap-2">
@@ -104,14 +118,14 @@
104
118
  type="number"
105
119
  aria-describedby="helper-text-explanation"
106
120
  :placeholder="$t('From')"
107
- @update:modelValue="onFilterInput[c.name]({ column: c, operator: 'gte', value: $event || undefined })"
121
+ @update:modelValue="onFilterInput[c.name]({ column: c, operator: 'gte', value: ($event !== '' && $event !== null) ? $event : undefined })"
108
122
  :modelValue="getFilterItem({ column: c, operator: 'gte' })"
109
123
  />
110
124
  <Input
111
125
  type="number"
112
126
  aria-describedby="helper-text-explanation"
113
127
  :placeholder="$t('To')"
114
- @update:modelValue="onFilterInput[c.name]({ column: c, operator: 'lte', value: $event|| undefined })"
128
+ @update:modelValue="onFilterInput[c.name]({ column: c, operator: 'lte', value: ($event !== '' && $event !== null) ? $event : undefined })"
115
129
  :modelValue="getFilterItem({ column: c, operator: 'lte' })"
116
130
  />
117
131
  </div>
@@ -122,9 +136,9 @@
122
136
 
123
137
  <div class="flex justify-end gap-2">
124
138
  <button
125
- :disabled="!filtersStore.filters.length"
139
+ :disabled="!filtersStore.visibleFiltersCount"
126
140
  type="button"
127
- class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
141
+ class="flex items-center py-1 px-3 text-sm font-medium text-lightFiltersClearAllButtonText focus:outline-none bg-lightFiltersClearAllButtonBackground rounded border border-lightFiltersClearAllButtonBorder hover:bg-lightFiltersClearAllButtonBackgroundHover hover:text-lightFiltersClearAllButtonTextHover focus:z-10 focus:ring-4 focus:ring-lightFiltersClearAllButtonFocus dark:focus:ring-darkFiltersClearAllButtonFocus dark:bg-darkFiltersClearAllButtonBackground dark:text-darkFiltersClearAllButtonText dark:border-darkFiltersClearAllButtonBorder dark:hover:text-darkFiltersClearAllButtonTextHover dark:hover:bg-darkFiltersClearAllButtonBackgroundHover disabled:opacity-50 disabled:cursor-not-allowed"
128
142
  @click="clear">{{ $t('Clear all') }}</button>
129
143
 
130
144
  </div>
@@ -136,17 +150,17 @@
136
150
  </template>
137
151
 
138
152
  <script setup>
139
- import { watch, computed } from 'vue';
153
+ import { watch, computed, ref, reactive } from 'vue';
140
154
  import { useI18n } from 'vue-i18n';
141
155
  import CustomDateRangePicker from '@/components/CustomDateRangePicker.vue';
142
- import { callAdminForthApi } from '@/utils';
156
+ import { callAdminForthApi, loadMoreForeignOptions, searchForeignOptions, createSearchInputHandlers } from '@/utils';
143
157
  import { useRouter } from 'vue-router';
144
- import { computedAsync } from '@vueuse/core'
145
158
  import CustomRangePicker from "@/components/CustomRangePicker.vue";
146
159
  import { useFiltersStore } from '@/stores/filters';
147
160
  import { getCustomComponent } from '@/utils';
148
161
  import Input from '@/afcl/Input.vue';
149
162
  import Select from '@/afcl/Select.vue';
163
+ import Spinner from '@/afcl/Spinner.vue';
150
164
  import debounce from 'debounce';
151
165
 
152
166
  const filtersStore = useFiltersStore();
@@ -165,31 +179,54 @@ const columnsWithFilter = computed(
165
179
  () => props.columns?.filter(column => column.showIn.filter) || []
166
180
  );
167
181
 
168
- const columnOptions = computedAsync(async () => {
169
- const ret = {};
170
- if (!props.columns) {
171
- return ret;
172
- }
173
- await Promise.all(
174
- Object.values(props.columns).map(async (column) => {
175
- if (column.foreignResource) {
176
- const list = await callAdminForthApi({
177
- method: 'POST',
178
- path: `/get_resource_foreign_data`,
179
- body: {
180
- resourceId: router.currentRoute.value.params.resourceId,
181
- column: column.name,
182
- limit: 10000,
183
- offset: 0,
184
- },
185
- });
186
- ret[column.name] = list.items;
182
+ const columnOptions = ref({});
183
+ const columnLoadingState = reactive({});
184
+ const columnOffsets = reactive({});
185
+ const columnEmptyResultsCount = reactive({});
186
+
187
+ watch(() => props.columns, async (newColumns) => {
188
+ if (!newColumns) return;
189
+
190
+ for (const column of newColumns) {
191
+ if (column.foreignResource) {
192
+ if (!columnOptions.value[column.name]) {
193
+ columnOptions.value[column.name] = [];
194
+ columnLoadingState[column.name] = { loading: false, hasMore: true };
195
+ columnOffsets[column.name] = 0;
196
+ columnEmptyResultsCount[column.name] = 0;
197
+
198
+ await loadMoreOptions(column.name);
187
199
  }
188
- })
189
- );
200
+ }
201
+ }
202
+ }, { immediate: true });
203
+
204
+ // Function to load more options for a specific column
205
+ async function loadMoreOptions(columnName, searchTerm = '') {
206
+ return loadMoreForeignOptions({
207
+ columnName,
208
+ searchTerm,
209
+ columns: props.columns,
210
+ resourceId: router.currentRoute.value.params.resourceId,
211
+ columnOptions,
212
+ columnLoadingState,
213
+ columnOffsets,
214
+ columnEmptyResultsCount
215
+ });
216
+ }
190
217
 
191
- return ret;
192
- }, {});
218
+ async function searchOptions(columnName, searchTerm) {
219
+ return searchForeignOptions({
220
+ columnName,
221
+ searchTerm,
222
+ columns: props.columns,
223
+ resourceId: router.currentRoute.value.params.resourceId,
224
+ columnOptions,
225
+ columnLoadingState,
226
+ columnOffsets,
227
+ columnEmptyResultsCount
228
+ });
229
+ }
193
230
 
194
231
 
195
232
  // sync 'body' class 'overflow-hidden' with show prop show
@@ -221,10 +258,18 @@ const onFilterInput = computed(() => {
221
258
  }, {});
222
259
  });
223
260
 
261
+ const onSearchInput = computed(() => {
262
+ return createSearchInputHandlers(
263
+ props.columns,
264
+ searchOptions,
265
+ (column) => column.filterOptions?.debounceTimeMs || 300
266
+ );
267
+ });
268
+
224
269
  function setFilterItem({ column, operator, value }) {
225
270
 
226
271
  const index = filtersStore.filters.findIndex(f => f.field === column.name && f.operator === operator);
227
- if (value === undefined) {
272
+ if (value === undefined || value === '' || value === null) {
228
273
  if (index !== -1) {
229
274
  filtersStore.filters.splice(index, 1);
230
275
  }
@@ -239,11 +284,12 @@ function setFilterItem({ column, operator, value }) {
239
284
  }
240
285
 
241
286
  function getFilterItem({ column, operator }) {
242
- return filtersStore.filters.find(f => f.field === column.name && f.operator === operator)?.value || '';
287
+ const filterValue = filtersStore.filters.find(f => f.field === column.name && f.operator === operator)?.value;
288
+ return filterValue !== undefined ? filterValue : '';
243
289
  }
244
290
 
245
291
  async function clear() {
246
- filtersStore.clearFilters();
292
+ filtersStore.filters = [...filtersStore.filters.filter(f => filtersStore.shouldFilterBeHidden(f.field))];
247
293
  emits('update:filters', [...filtersStore.filters]);
248
294
  }
249
295
 
@@ -1,10 +1,10 @@
1
1
  <template>
2
2
  <div class="rounded-lg shadow-resourseFormShadow dark:shadow-darkResourseFormShadow dark:shadow-2xl">
3
- <div v-if="group.groupName && !group.noTitle" class="text-md font-semibold px-6 py-3 flex flex-1 items-center dark:border-gray-600 text-gray-700 bg-lightFormHeading dark:bg-gray-700 dark:text-gray-400 rounded-t-lg">
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>
6
- <table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
7
- <thead v-if="!allColumnsHaveCustomComponent" class="text-xs text-gray-700 uppercase dark:text-gray-400 bg-lightFormHeading dark:bg-gray-700 block md:table-row-group ">
6
+ <table class="w-full text-sm text-left rtl:text-right text-lightFormFieldTextColor dark:text-darkFormFieldTextColor">
7
+ <thead v-if="!allColumnsHaveCustomComponent" class="text-xs text-lightListTableHeadingText uppercase dark:text-darkListTableHeadingText bg-lightFormHeading dark:bg-darkFormHeading block md:table-row-group ">
8
8
  <tr>
9
9
  <th scope="col" :class="{'rounded-tl-lg': !group.groupName}" class="px-6 py-3 hidden md:w-52 md:table-cell">
10
10
  {{ $t('Field') }}
@@ -19,7 +19,7 @@
19
19
  v-for="(column, i) in group.columns"
20
20
  :key="column.name"
21
21
  v-if="currentValues !== null"
22
- class="bg-ligftForm dark:bg-gray-800 dark:border-gray-700 block md:table-row"
22
+ class="bg-lightForm dark:bg-darkForm dark:border-darkFormBorder block md:table-row"
23
23
  :class="{ 'border-b': i !== group.columns.length - 1}"
24
24
  >
25
25
  <td class="px-6 py-4 flex items-center block md:table-cell pb-0 md:pb-4"
@@ -29,7 +29,7 @@
29
29
  <Tooltip v-if="column.required[mode]">
30
30
 
31
31
  <IconExclamationCircleSolid v-if="column.required[mode]" class="w-4 h-4"
32
- :class="(columnError(column) && validating) ? 'text-red-500 dark:text-red-400' : 'text-gray-400 dark:text-gray-500'"
32
+ :class="(columnError(column) && validating) ? 'text-lightInputErrorColor dark:text-darkInputErrorColor' : 'text-lightRequiredIconColor dark:text-darkRequiredIconColor'"
33
33
  />
34
34
 
35
35
  <template #tooltip>
@@ -55,8 +55,8 @@
55
55
  @update:emptiness="customComponentsEmptiness[$event.name] = $event.value"
56
56
  :readonly="readonlyColumns?.includes(column.name)"
57
57
  />
58
- <div v-if="columnError(column) && validating" class="mt-1 text-xs text-red-500 dark:text-red-400">{{ columnError(column) }}</div>
59
- <div v-if="column.editingNote && column.editingNote[mode]" class="mt-1 text-xs text-gray-400 dark:text-gray-500">{{ column.editingNote[mode] }}</div>
58
+ <div v-if="columnError(column) && validating" class="mt-1 text-xs text-lightInputErrorColor dark:text-darkInputErrorColor">{{ columnError(column) }}</div>
59
+ <div v-if="column.editingNote && column.editingNote[mode]" class="mt-1 text-xs text-lightFormFieldTextColor dark:text-darkFormFieldTextColor">{{ column.editingNote[mode] }}</div>
60
60
  </td>
61
61
  </tr>
62
62
  </tbody>
@@ -70,6 +70,7 @@
70
70
  import { ref, computed, watch, nextTick, type Ref } from 'vue';
71
71
  import { useI18n } from 'vue-i18n';
72
72
  import ColumnValueInputWrapper from "@/components/ColumnValueInputWrapper.vue";
73
+ import type { AdminForthResourceColumnInputCommon } from '@/types/Common';
73
74
 
74
75
  const { t } = useI18n();
75
76
 
@@ -89,7 +90,7 @@
89
90
  const customComponentsInValidity: Ref<Record<string, boolean>> = ref({});
90
91
  const customComponentsEmptiness: Ref<Record<string, boolean>> = ref({});
91
92
  const allColumnsHaveCustomComponent = computed(() => {
92
- return props.group.columns.every(column => {
93
+ return props.group.columns.every((column: AdminForthResourceColumnInputCommon) => {
93
94
  const componentKey = `${props.source}Row` as keyof typeof column.components;
94
95
  return column.components?.[componentKey];
95
96
  });