adminforth 2.4.0-next.17 → 2.4.0-next.171

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 (177) hide show
  1. package/commands/callTsProxy.js +14 -4
  2. package/commands/cli.js +12 -4
  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/dist/dataConnectors/baseConnector.d.ts.map +1 -1
  12. package/dist/dataConnectors/baseConnector.js +46 -15
  13. package/dist/dataConnectors/baseConnector.js.map +1 -1
  14. package/dist/dataConnectors/clickhouse.d.ts.map +1 -1
  15. package/dist/dataConnectors/clickhouse.js +15 -0
  16. package/dist/dataConnectors/clickhouse.js.map +1 -1
  17. package/dist/dataConnectors/mongo.d.ts.map +1 -1
  18. package/dist/dataConnectors/mongo.js +44 -15
  19. package/dist/dataConnectors/mongo.js.map +1 -1
  20. package/dist/dataConnectors/mysql.d.ts.map +1 -1
  21. package/dist/dataConnectors/mysql.js +11 -0
  22. package/dist/dataConnectors/mysql.js.map +1 -1
  23. package/dist/dataConnectors/postgres.d.ts.map +1 -1
  24. package/dist/dataConnectors/postgres.js +11 -0
  25. package/dist/dataConnectors/postgres.js.map +1 -1
  26. package/dist/dataConnectors/sqlite.d.ts.map +1 -1
  27. package/dist/dataConnectors/sqlite.js +11 -0
  28. package/dist/dataConnectors/sqlite.js.map +1 -1
  29. package/dist/index.d.ts +2 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +20 -9
  32. package/dist/index.js.map +1 -1
  33. package/dist/modules/codeInjector.d.ts +1 -0
  34. package/dist/modules/codeInjector.d.ts.map +1 -1
  35. package/dist/modules/codeInjector.js +42 -5
  36. package/dist/modules/codeInjector.js.map +1 -1
  37. package/dist/modules/configValidator.d.ts.map +1 -1
  38. package/dist/modules/configValidator.js +62 -3
  39. package/dist/modules/configValidator.js.map +1 -1
  40. package/dist/modules/restApi.d.ts.map +1 -1
  41. package/dist/modules/restApi.js +149 -25
  42. package/dist/modules/restApi.js.map +1 -1
  43. package/dist/modules/styles.d.ts +457 -13
  44. package/dist/modules/styles.d.ts.map +1 -1
  45. package/dist/modules/styles.js +513 -31
  46. package/dist/modules/styles.js.map +1 -1
  47. package/dist/modules/utils.d.ts +1 -0
  48. package/dist/modules/utils.d.ts.map +1 -1
  49. package/dist/modules/utils.js +9 -0
  50. package/dist/modules/utils.js.map +1 -1
  51. package/dist/spa/index.html +1 -1
  52. package/dist/spa/package-lock.json +5 -4
  53. package/dist/spa/package.json +1 -1
  54. package/dist/spa/src/App.vue +48 -167
  55. package/dist/spa/src/adminforth.ts +42 -18
  56. package/dist/spa/src/afcl/BarChart.vue +2 -2
  57. package/dist/spa/src/afcl/Button.vue +6 -6
  58. package/dist/spa/src/afcl/Checkbox.vue +21 -13
  59. package/dist/spa/src/afcl/CountryFlag.vue +4 -1
  60. package/dist/spa/src/{components/CustomDatePicker.vue → afcl/DatePicker.vue} +95 -9
  61. package/dist/spa/src/afcl/Dialog.vue +44 -27
  62. package/dist/spa/src/afcl/Dropzone.vue +12 -12
  63. package/dist/spa/src/afcl/Input.vue +6 -6
  64. package/dist/spa/src/afcl/JsonViewer.vue +25 -0
  65. package/dist/spa/src/afcl/LinkButton.vue +1 -1
  66. package/dist/spa/src/afcl/PieChart.vue +5 -5
  67. package/dist/spa/src/afcl/ProgressBar.vue +7 -7
  68. package/dist/spa/src/afcl/Select.vue +68 -34
  69. package/dist/spa/src/afcl/Skeleton.vue +6 -6
  70. package/dist/spa/src/afcl/Table.vue +202 -72
  71. package/dist/spa/src/afcl/Textarea.vue +31 -0
  72. package/dist/spa/src/afcl/Toggle.vue +32 -0
  73. package/dist/spa/src/afcl/Tooltip.vue +1 -2
  74. package/dist/spa/src/afcl/VerticalTabs.vue +16 -7
  75. package/dist/spa/src/afcl/index.ts +4 -3
  76. package/dist/spa/src/components/AcceptModal.vue +7 -7
  77. package/dist/spa/src/components/Breadcrumbs.vue +5 -5
  78. package/dist/spa/src/components/ColumnValueInput.vue +38 -18
  79. package/dist/spa/src/components/ColumnValueInputWrapper.vue +4 -3
  80. package/dist/spa/src/components/CustomDateRangePicker.vue +9 -8
  81. package/dist/spa/src/components/CustomRangePicker.vue +37 -8
  82. package/dist/spa/src/components/ErrorMessage.vue +21 -0
  83. package/dist/spa/src/components/Filters.vue +85 -39
  84. package/dist/spa/src/components/GroupsTable.vue +9 -8
  85. package/dist/spa/src/components/MenuLink.vue +47 -23
  86. package/dist/spa/src/components/ResourceForm.vue +94 -51
  87. package/dist/spa/src/components/ResourceListTable.vue +78 -80
  88. package/dist/spa/src/components/ResourceListTableVirtual.vue +70 -72
  89. package/dist/spa/src/components/ShowTable.vue +17 -12
  90. package/dist/spa/src/components/Sidebar.vue +419 -0
  91. package/dist/spa/src/components/SingleSkeletLoader.vue +6 -6
  92. package/dist/spa/src/components/SkeleteLoader.vue +3 -3
  93. package/dist/spa/src/components/ThreeDotsMenu.vue +73 -14
  94. package/dist/spa/src/components/Toast.vue +27 -9
  95. package/dist/spa/src/components/UserMenuSettingsButton.vue +77 -0
  96. package/dist/spa/src/components/ValueRenderer.vue +43 -16
  97. package/dist/spa/src/controls/BoolToggle.vue +34 -0
  98. package/dist/spa/src/i18n.ts +1 -1
  99. package/dist/spa/src/renderers/CompactField.vue +1 -1
  100. package/dist/spa/src/renderers/CompactUUID.vue +1 -1
  101. package/dist/spa/src/router/index.ts +8 -0
  102. package/dist/spa/src/shims-vue.d.ts +5 -0
  103. package/dist/spa/src/spa_types/core.ts +12 -1
  104. package/dist/spa/src/stores/core.ts +1 -1
  105. package/dist/spa/src/stores/filters.ts +29 -2
  106. package/dist/spa/src/stores/modal.ts +6 -1
  107. package/dist/spa/src/stores/toast.ts +22 -3
  108. package/dist/spa/src/types/Back.ts +131 -22
  109. package/dist/spa/src/types/Common.ts +55 -31
  110. package/dist/spa/src/types/FrontendAPI.ts +31 -5
  111. package/dist/spa/src/types/adapters/CompletionAdapter.ts +25 -0
  112. package/dist/spa/src/types/adapters/EmailAdapter.ts +27 -0
  113. package/dist/spa/src/types/adapters/ImageGenerationAdapter.ts +50 -0
  114. package/dist/spa/src/types/adapters/ImageVisionAdapter.ts +30 -0
  115. package/dist/spa/src/types/adapters/KeyValueAdapter.ts +16 -0
  116. package/dist/spa/src/types/adapters/OAuth2Adapter.ts +34 -0
  117. package/dist/spa/src/types/adapters/StorageAdapter.ts +73 -0
  118. package/dist/spa/src/types/adapters/index.ts +7 -0
  119. package/dist/spa/src/utils.ts +217 -7
  120. package/dist/spa/src/views/CreateView.vue +18 -19
  121. package/dist/spa/src/views/EditView.vue +25 -19
  122. package/dist/spa/src/views/ListView.vue +126 -81
  123. package/dist/spa/src/views/LoginView.vue +24 -33
  124. package/dist/spa/src/views/ResourceParent.vue +2 -2
  125. package/dist/spa/src/views/SettingsView.vue +139 -0
  126. package/dist/spa/src/views/ShowView.vue +59 -39
  127. package/dist/spa/src/websocket.ts +6 -1
  128. package/dist/spa/tsconfig.app.json +1 -1
  129. package/dist/spa/vite.config.ts +45 -2
  130. package/dist/types/Back.d.ts +96 -14
  131. package/dist/types/Back.d.ts.map +1 -1
  132. package/dist/types/Back.js +15 -0
  133. package/dist/types/Back.js.map +1 -1
  134. package/dist/types/Common.d.ts +48 -28
  135. package/dist/types/Common.d.ts.map +1 -1
  136. package/dist/types/Common.js.map +1 -1
  137. package/dist/types/FrontendAPI.d.ts +31 -3
  138. package/dist/types/FrontendAPI.d.ts.map +1 -1
  139. package/dist/types/FrontendAPI.js.map +1 -1
  140. package/dist/types/adapters/CompletionAdapter.d.ts +20 -0
  141. package/dist/types/adapters/CompletionAdapter.d.ts.map +1 -0
  142. package/dist/types/adapters/CompletionAdapter.js +2 -0
  143. package/dist/types/adapters/CompletionAdapter.js.map +1 -0
  144. package/dist/types/adapters/EmailAdapter.d.ts +20 -0
  145. package/dist/types/adapters/EmailAdapter.d.ts.map +1 -0
  146. package/dist/types/adapters/EmailAdapter.js +2 -0
  147. package/dist/types/adapters/EmailAdapter.js.map +1 -0
  148. package/dist/types/adapters/ImageGenerationAdapter.d.ts +37 -0
  149. package/dist/types/adapters/ImageGenerationAdapter.d.ts.map +1 -0
  150. package/dist/types/adapters/ImageGenerationAdapter.js +2 -0
  151. package/dist/types/adapters/ImageGenerationAdapter.js.map +1 -0
  152. package/dist/types/adapters/ImageVisionAdapter.d.ts +25 -0
  153. package/dist/types/adapters/ImageVisionAdapter.d.ts.map +1 -0
  154. package/dist/types/adapters/ImageVisionAdapter.js +2 -0
  155. package/dist/types/adapters/ImageVisionAdapter.js.map +1 -0
  156. package/dist/types/adapters/KeyValueAdapter.d.ts +10 -0
  157. package/dist/types/adapters/KeyValueAdapter.d.ts.map +1 -0
  158. package/dist/types/adapters/KeyValueAdapter.js +2 -0
  159. package/dist/types/adapters/KeyValueAdapter.js.map +1 -0
  160. package/dist/types/adapters/OAuth2Adapter.d.ts +32 -0
  161. package/dist/types/adapters/OAuth2Adapter.d.ts.map +1 -0
  162. package/dist/types/adapters/OAuth2Adapter.js +2 -0
  163. package/dist/types/adapters/OAuth2Adapter.js.map +1 -0
  164. package/dist/types/adapters/StorageAdapter.d.ts +63 -0
  165. package/dist/types/adapters/StorageAdapter.d.ts.map +1 -0
  166. package/dist/types/adapters/StorageAdapter.js +2 -0
  167. package/dist/types/adapters/StorageAdapter.js.map +1 -0
  168. package/dist/types/adapters/index.d.ts +8 -0
  169. package/dist/types/adapters/index.d.ts.map +1 -0
  170. package/dist/types/adapters/index.js +2 -0
  171. package/dist/types/adapters/index.js.map +1 -0
  172. package/package.json +3 -2
  173. package/dist/spa/src/types/Adapters.ts +0 -213
  174. package/dist/types/Adapters.d.ts +0 -168
  175. package/dist/types/Adapters.d.ts.map +0 -1
  176. package/dist/types/Adapters.js +0 -2
  177. package/dist/types/Adapters.js.map +0 -1
@@ -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
  });
@@ -1,42 +1,66 @@
1
1
  <template>
2
2
  <RouterLink
3
3
  :to="{name: item.resourceId ? 'resource-list' : item.path, params: item.resourceId ? { resourceId: item.resourceId }: {}}"
4
- class="flex group items-center py-2 text-lightSidebarText dark:text-darkSidebarText rounded-default hover:bg-lightSidebarItemHover hover:text-lightSidebarTextHover dark:hover:bg-darkSidebarItemHover dark:hover:text-darkSidebarTextHover active:bg-lightSidebarActive dark:active:bg-darkSidebarHover" role="menuitem"
4
+ class="af-menu-link flex group relative items-center w-full py-2 text-lightSidebarText dark:text-darkSidebarText rounded-default transition-all duration-200 ease-in-out"
5
5
  :class="{
6
- 'px-4': isChild,
7
- 'px-2': !isChild,
6
+ 'ml-1': isSidebarIconOnly && !isSidebarHovering && isChild,
7
+ 'hover:bg-lightSidebarItemHover hover:text-lightSidebarTextHover dark:hover:bg-darkSidebarItemHover dark:hover:text-darkSidebarTextHover active:bg-lightSidebarActive dark:active:bg-darkSidebarHover': !['divider', 'gap', 'heading'].includes(item.type),
8
+ 'px-6': (isChild && !isSidebarIconOnly && !isSidebarHovering) || (isChild && isSidebarIconOnly && isSidebarHovering),
9
+ 'px-3.5': !isChild || (isSidebarIconOnly && !isSidebarHovering),
10
+ 'max-w-12': isSidebarIconOnly && !isSidebarHovering,
8
11
  'bg-lightSidebarItemActive dark:bg-darkSidebarItemActive': item.resourceId ?
9
12
  ($route.params.resourceId === item.resourceId && $route.name === 'resource-list') :
10
13
  ($route.name === item.path)
11
14
  }"
12
15
  >
13
- <component v-if="item.icon" :is="getIcon(item.icon)" class="w-5 h-5 text-lightSidebarIcons dark:text-darkSidebarIcons transition duration-75 group-hover:text-lightSidebarIconsHover dark:group-hover:text-darkSidebarIconsHover" ></component>
14
- <span class="text-ellipsis overflow-hidden ms-3">{{ item.label }}</span>
15
- <span v-if="item.badge"
16
+ <component v-if="item.icon" :is="getIcon(item.icon)"
17
+ class="text-lightSidebarIcons dark:text-darkSidebarIcons group-hover:text-lightSidebarIconsHover dark:group-hover:text-darkSidebarIconsHover transition-all duration-200 ease-in-out"
18
+ :class="{
19
+ 'min-w-4 min-h-4': isSidebarIconOnly && !isSidebarHovering && isChild,
20
+ 'min-w-5 min-h-5': !(isSidebarIconOnly && !isSidebarHovering && isChild)
21
+ }" >
22
+ </component>
23
+ <span
24
+ class="overflow-hidden block ms-3 pr-5 text-left rtl:text-right transition-all duration-200 ease-in-out"
25
+ :class="{
26
+ 'opacity-0 ms-0 translate-x-4 flex-none': isSidebarIconOnly && !isSidebarHovering,
27
+ 'opacity-100 ms-3 translate-x-0 flex-none': isSidebarIconOnly && isSidebarHovering,
28
+ 'opacity-100 ms-3 translate-x-0 flex-1': !isSidebarIconOnly
29
+ }"
30
+ :style="isSidebarIconOnly ? {
31
+ minWidth: isChild
32
+ ? 'calc(16.5rem - 0.75rem*2 - 1.5rem*2 - 1.25rem - 0.75rem)'
33
+ : 'calc(16.5rem - 0.75rem*2 - 0.875rem*2 - 1.25rem - 0.75rem)',
34
+ width: isChild
35
+ ? 'calc(16.5rem - 0.75rem*2 - 1.5rem*2 - 1.25rem - 0.75rem)'
36
+ : 'calc(16.5rem - 0.75rem*2 - 0.875rem*2 - 1.25rem - 0.75rem)'
37
+ } : {}"
16
38
  >
17
-
18
- <Tooltip v-if="item.badgeTooltip">
19
- <div class="inline-flex items-center justify-center h-3 py-3 px-1 ms-3 text-sm font-medium rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
20
- fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent min-w-[1.5rem] max-w-[3rem]">{{ item.badge }}</div>
21
-
22
- <template #tooltip>
23
- {{ item.badgeTooltip }}
24
- </template>
25
- </Tooltip>
26
- <template v-else>
27
- <div class="inline-flex items-center justify-center h-3 py-3 px-1 ms-3 text-sm font-medium rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
28
- fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent min-w-[1.5rem] max-w-[3rem]">{{ item.badge }}</div>
29
- </template>
30
-
39
+ {{ item.label }}
31
40
  </span>
32
-
41
+ <span class="absolute right-1 top-1/2 -translate-y-1/2" v-if="item.badge && (!isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering))">
42
+ <Tooltip v-if="item.badgeTooltip">
43
+ <div class="af-badge inline-flex items-center justify-center h-3 py-2.5 px-1 ms-3 text-xs font-medium rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
44
+ fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent min-w-[1.5rem] max-w-[3rem]">{{ item.badge }}</div>
45
+ <template #tooltip>
46
+ {{ item.badgeTooltip }}
47
+ </template>
48
+ </Tooltip>
49
+ <template v-else>
50
+ <div class="af-badge inline-flex items-center justify-center h-3 py-2.5 px-1 ms-3 text-xs font-medium rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
51
+ fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent min-w-[1.5rem] max-w-[3rem]">{{ item.badge }}</div>
52
+ </template>
53
+ </span>
54
+ <div v-if="item.badge && isSidebarIconOnly && !isSidebarHovering" class="af-badge absolute right-1 top-1/2 -translate-y-1/2 inline-flex items-center justify-center h-2 w-2 text-sm font-medium rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
55
+ fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent">
56
+ </div>
33
57
  </RouterLink>
34
58
  </template>
35
59
 
36
- <script setup lang="ts">
60
+ <script setup lang="ts">
37
61
  import { getIcon } from '@/utils';
38
62
  import { Tooltip } from '@/afcl';
39
- const props = defineProps(['item', 'isChild']);
40
63
 
64
+ defineProps(['item', 'isChild', 'isSidebarIconOnly', 'isSidebarHovering']);
41
65
 
42
66
  </script>
@@ -63,14 +63,14 @@
63
63
 
64
64
  <script setup lang="ts">
65
65
 
66
- import { applyRegexValidation, callAdminForthApi} from '@/utils';
66
+ import { applyRegexValidation, callAdminForthApi, loadMoreForeignOptions, searchForeignOptions, createSearchInputHandlers} from '@/utils';
67
67
  import { computedAsync } from '@vueuse/core';
68
- import { computed, onMounted, ref, watch } from 'vue';
68
+ import { computed, onMounted, reactive, ref, watch, provide, type Ref } from 'vue';
69
69
  import { useRouter, useRoute } from 'vue-router';
70
70
  import { useCoreStore } from "@/stores/core";
71
71
  import GroupsTable from '@/components/GroupsTable.vue';
72
72
  import { useI18n } from 'vue-i18n';
73
- import { type AdminForthResourceCommon } from '@/types/Common';
73
+ import { type AdminForthResourceColumnCommon, type AdminForthResourceCommon } from '@/types/Common';
74
74
 
75
75
  const { t } = useI18n();
76
76
 
@@ -91,11 +91,16 @@ const mode = computed(() => route.name === 'resource-create' ? 'create' : 'edit'
91
91
 
92
92
  const emit = defineEmits(['update:record', 'update:isValid']);
93
93
 
94
- const currentValues = ref(null);
95
- const customComponentsInValidity = ref({});
96
- const customComponentsEmptiness = ref({});
94
+ const currentValues = ref<any | any[] | null>(null);
95
+ const customComponentsInValidity: Ref<Record<string, AdminForthResourceColumnCommon>> = ref({});
96
+ const customComponentsEmptiness: Ref<Record<string, AdminForthResourceColumnCommon>> = ref({});
97
97
 
98
- const columnError = (column) => {
98
+ const columnOptions = ref<Record<string, any[]>>({});
99
+ const columnLoadingState = reactive<Record<string, { loading: boolean; hasMore: boolean }>>({});
100
+ const columnOffsets = reactive<Record<string, number>>({});
101
+ const columnEmptyResultsCount = reactive<Record<string, number>>({});
102
+
103
+ const columnError = (column: AdminForthResourceColumnCommon) => {
99
104
  const val = computed(() => {
100
105
  if (!currentValues.value) {
101
106
  return null;
@@ -104,7 +109,7 @@ const columnError = (column) => {
104
109
  return customComponentsInValidity.value?.[column.name];
105
110
  }
106
111
 
107
- if ( column.required[mode.value] ) {
112
+ if ( column.required?.[mode.value] ) {
108
113
  const naturalEmptiness = currentValues.value[column.name] === undefined ||
109
114
  currentValues.value[column.name] === null ||
110
115
  currentValues.value[column.name] === '' ||
@@ -131,17 +136,23 @@ const columnError = (column) => {
131
136
  }
132
137
  } else if (column.isArray?.enabled) {
133
138
  if (!column.isArray.allowDuplicateItems) {
134
- if (currentValues.value[column.name].filter((value, index, self) => self.indexOf(value) !== index).length > 0) {
139
+ if (currentValues.value[column.name].filter((value: any, index: any, self: any) => self.indexOf(value) !== index).length > 0) {
135
140
  return t('Array cannot contain duplicate items');
136
141
  }
137
142
  }
138
143
 
139
- return currentValues.value[column.name] && currentValues.value[column.name].reduce((error, item) => {
140
- return error || validateValue(column.isArray.itemType, item, column) ||
141
- (item === null || !item.toString() ? t('Array cannot contain empty items') : null);
144
+ return currentValues.value[column.name] && currentValues.value[column.name].reduce((error: any, item: any) => {
145
+ if (column.isArray) {
146
+ return error || validateValue(column.isArray.itemType, item, column) ||
147
+ (item === null || !item.toString() ? t('Array cannot contain empty items') : null);
148
+ } else {
149
+ return error;
150
+ }
142
151
  }, null);
143
152
  } else {
144
- return validateValue(column.type, currentValues.value[column.name], column);
153
+ if (column.type) {
154
+ return validateValue(column.type, currentValues.value[column.name], column);
155
+ }
145
156
  }
146
157
 
147
158
  return null;
@@ -149,7 +160,7 @@ const columnError = (column) => {
149
160
  return val.value;
150
161
  };
151
162
 
152
- const validateValue = (type, value, column) => {
163
+ const validateValue = (type: string, value: any, column: AdminForthResourceColumnCommon) => {
153
164
  if (type === 'string' || type === 'text') {
154
165
  if (column.maxLength && value?.length > column.maxLength) {
155
166
  return t('This field must be shorter than {maxLength} characters', { maxLength: column.maxLength });
@@ -157,7 +168,7 @@ const validateValue = (type, value, column) => {
157
168
 
158
169
  if (column.minLength && value?.length < column.minLength) {
159
170
  // if column.required[mode.value] is false, then we check if the field is empty
160
- let needToCheckEmpty = column.required[mode.value] || value?.length > 0;
171
+ let needToCheckEmpty = column.required?.[mode.value] || value?.length > 0;
161
172
  if (!needToCheckEmpty) {
162
173
  return null;
163
174
  }
@@ -186,10 +197,10 @@ const validateValue = (type, value, column) => {
186
197
  };
187
198
 
188
199
 
189
- const setCurrentValue = (key, value, index=null) => {
200
+ const setCurrentValue = (key: any, value: any, index = null) => {
190
201
  const col = props.resource.columns.find((column) => column.name === key);
191
202
  // if field is an array, we need to update the array or individual element
192
- if (col.type === 'json' && col.isArray?.enabled) {
203
+ if (((col?.type && col.type === 'json') && (col?.type && col.isArray?.enabled))) {
193
204
  if (index === null) {
194
205
  currentValues.value[key] = value;
195
206
  } else if (index === currentValues.value[key].length) {
@@ -204,12 +215,12 @@ const setCurrentValue = (key, value, index=null) => {
204
215
  } else {
205
216
  currentValues.value[key][index] = value;
206
217
  }
207
- if (['text', 'richtext', 'string'].includes(col.isArray.itemType) && col.enforceLowerCase) {
218
+ if (col?.isArray && ['text', 'richtext', 'string'].includes(col.isArray.itemType) && col.enforceLowerCase) {
208
219
  currentValues.value[key][index] = currentValues.value[key][index].toLowerCase();
209
220
  }
210
221
  }
211
222
  } else {
212
- if (['integer', 'float', 'decimal'].includes(col.type)) {
223
+ if (col?.type && ['integer', 'float', 'decimal'].includes(col.type)) {
213
224
  if (value || value === 0) {
214
225
  currentValues.value[key] = +value;
215
226
  } else {
@@ -218,7 +229,7 @@ const setCurrentValue = (key, value, index=null) => {
218
229
  } else {
219
230
  currentValues.value[key] = value;
220
231
  }
221
- if (['text', 'richtext', 'string'].includes(col.type) && col.enforceLowerCase) {
232
+ if (col?.type && ['text', 'richtext', 'string'].includes(col?.type) && col.enforceLowerCase) {
222
233
  currentValues.value[key] = currentValues.value[key].toLowerCase();
223
234
  }
224
235
  }
@@ -239,11 +250,28 @@ const setCurrentValue = (key, value, index=null) => {
239
250
  emit('update:record', up);
240
251
  };
241
252
 
253
+ watch(() => props.resource.columns, async (newColumns) => {
254
+ if (!newColumns) return;
255
+
256
+ for (const column of newColumns) {
257
+ if (column.foreignResource) {
258
+ if (!columnOptions.value[column.name]) {
259
+ columnOptions.value[column.name] = [];
260
+ columnLoadingState[column.name] = { loading: false, hasMore: true };
261
+ columnOffsets[column.name] = 0;
262
+ columnEmptyResultsCount[column.name] = 0;
263
+
264
+ await loadMoreOptions(column.name);
265
+ }
266
+ }
267
+ }
268
+ }, { immediate: true });
269
+
242
270
  onMounted(() => {
243
271
  currentValues.value = Object.assign({}, props.record);
244
272
  // json values should transform to string
245
273
  props.resource.columns.forEach((column) => {
246
- if (column.type === 'json') {
274
+ if (column.type === 'json' && currentValues.value) {
247
275
  if (column.isArray?.enabled) {
248
276
  // if value is null or undefined, we should set it to empty array
249
277
  if (!currentValues.value[column.name]) {
@@ -266,33 +294,35 @@ onMounted(() => {
266
294
  emit('update:isValid', isValid.value);
267
295
  });
268
296
 
269
- const columnOptions = computedAsync(async () => {
270
- return (await Promise.all(
271
- Object.values(props.resource.columns).map(async (column) => {
272
- if (column.foreignResource) {
273
- const list = await callAdminForthApi({
274
- method: 'POST',
275
- path: `/get_resource_foreign_data`,
276
- body: {
277
- resourceId: router.currentRoute.value.params.resourceId,
278
- column: column.name,
279
- limit: 1000,
280
- offset: 0,
281
- },
282
- });
283
-
284
- if (!column.required[props.source] && !column.isArray?.enabled) list.items.push({ value: null, label: column.foreignResource.unsetLabel });
285
-
286
- return { [column.name]: list.items };
287
- }
288
- })
289
- )).reduce((acc, val) => Object.assign(acc, val), {})
290
-
291
- }, {});
297
+ async function loadMoreOptions(columnName: string, searchTerm = '') {
298
+ return loadMoreForeignOptions({
299
+ columnName,
300
+ searchTerm,
301
+ columns: props.resource.columns,
302
+ resourceId: router.currentRoute.value.params.resourceId as string,
303
+ columnOptions,
304
+ columnLoadingState,
305
+ columnOffsets,
306
+ columnEmptyResultsCount
307
+ });
308
+ }
309
+
310
+ async function searchOptions(columnName: string, searchTerm: string) {
311
+ return searchForeignOptions({
312
+ columnName,
313
+ searchTerm,
314
+ columns: props.resource.columns,
315
+ resourceId: router.currentRoute.value.params.resourceId as string,
316
+ columnOptions,
317
+ columnLoadingState,
318
+ columnOffsets,
319
+ columnEmptyResultsCount
320
+ });
321
+ }
292
322
 
293
323
 
294
324
  const editableColumns = computed(() => {
295
- return props.resource?.columns?.filter(column => column.showIn[mode.value]);
325
+ return props.resource?.columns?.filter(column => column.showIn?.[mode.value]);
296
326
  });
297
327
 
298
328
  const isValid = computed(() => {
@@ -302,12 +332,14 @@ const isValid = computed(() => {
302
332
 
303
333
  const groups = computed(() => {
304
334
  let fieldGroupType;
305
- if (mode.value === 'edit' && coreStore.resource.options?.editFieldGroups !== undefined) {
306
- fieldGroupType = coreStore.resource.options.editFieldGroups;
307
- } else if (mode.value === 'create' && coreStore.resource.options?.createFieldGroups !== undefined) {
308
- fieldGroupType = coreStore.resource.options.createFieldGroups;
309
- } else {
310
- fieldGroupType = coreStore.resource.options?.fieldGroups;
335
+ if(coreStore.resource){
336
+ if (mode.value === 'edit' && coreStore.resource.options?.editFieldGroups !== undefined) {
337
+ fieldGroupType = coreStore.resource.options.editFieldGroups;
338
+ } else if (mode.value === 'create' && coreStore.resource.options?.createFieldGroups !== undefined) {
339
+ fieldGroupType = coreStore.resource.options.createFieldGroups;
340
+ } else {
341
+ fieldGroupType = coreStore.resource.options?.fieldGroups;
342
+ }
311
343
  }
312
344
  return fieldGroupType ?? [];
313
345
  });
@@ -330,6 +362,17 @@ const getOtherColumns = () => {
330
362
 
331
363
  const otherColumns = getOtherColumns();
332
364
 
365
+ const onSearchInput = computed(() => {
366
+ return createSearchInputHandlers(
367
+ props.resource.columns,
368
+ searchOptions
369
+ );
370
+ });
371
+
372
+ provide('columnLoadingState', columnLoadingState);
373
+ provide('onSearchInput', onSearchInput);
374
+ provide('loadMoreOptions', loadMoreOptions);
375
+
333
376
  watch(() => isValid.value, (value) => {
334
377
  emit('update:isValid', value);
335
378
  });