adminforth 2.5.2 → 2.6.0

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 (82) hide show
  1. package/commands/createApp/templates/index.ts.hbs +7 -0
  2. package/commands/createCustomComponent/configLoader.js +3 -0
  3. package/dist/dataConnectors/baseConnector.d.ts.map +1 -1
  4. package/dist/dataConnectors/baseConnector.js +16 -3
  5. package/dist/dataConnectors/baseConnector.js.map +1 -1
  6. package/dist/dataConnectors/mongo.d.ts.map +1 -1
  7. package/dist/dataConnectors/mongo.js +13 -6
  8. package/dist/dataConnectors/mongo.js.map +1 -1
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +19 -8
  12. package/dist/index.js.map +1 -1
  13. package/dist/modules/codeInjector.d.ts.map +1 -1
  14. package/dist/modules/codeInjector.js +20 -3
  15. package/dist/modules/codeInjector.js.map +1 -1
  16. package/dist/modules/configValidator.d.ts.map +1 -1
  17. package/dist/modules/configValidator.js +48 -1
  18. package/dist/modules/configValidator.js.map +1 -1
  19. package/dist/modules/restApi.d.ts.map +1 -1
  20. package/dist/modules/restApi.js +145 -25
  21. package/dist/modules/restApi.js.map +1 -1
  22. package/dist/modules/styles.d.ts +450 -13
  23. package/dist/modules/styles.d.ts.map +1 -1
  24. package/dist/modules/styles.js +506 -31
  25. package/dist/modules/styles.js.map +1 -1
  26. package/dist/modules/utils.d.ts +1 -0
  27. package/dist/modules/utils.d.ts.map +1 -1
  28. package/dist/modules/utils.js +9 -0
  29. package/dist/modules/utils.js.map +1 -1
  30. package/dist/spa/index.html +1 -1
  31. package/dist/spa/src/App.vue +21 -11
  32. package/dist/spa/src/afcl/Button.vue +3 -3
  33. package/dist/spa/src/afcl/Checkbox.vue +21 -13
  34. package/dist/spa/src/afcl/CountryFlag.vue +4 -1
  35. package/dist/spa/src/{components/CustomDatePicker.vue → afcl/DatePicker.vue} +95 -9
  36. package/dist/spa/src/afcl/Dialog.vue +6 -6
  37. package/dist/spa/src/afcl/Dropzone.vue +10 -10
  38. package/dist/spa/src/afcl/Input.vue +4 -4
  39. package/dist/spa/src/afcl/ProgressBar.vue +7 -7
  40. package/dist/spa/src/afcl/Select.vue +51 -21
  41. package/dist/spa/src/afcl/Skeleton.vue +6 -6
  42. package/dist/spa/src/afcl/Table.vue +11 -11
  43. package/dist/spa/src/afcl/Toggle.vue +32 -0
  44. package/dist/spa/src/afcl/Tooltip.vue +1 -1
  45. package/dist/spa/src/afcl/VerticalTabs.vue +3 -3
  46. package/dist/spa/src/afcl/index.ts +2 -1
  47. package/dist/spa/src/components/AcceptModal.vue +6 -6
  48. package/dist/spa/src/components/Breadcrumbs.vue +5 -5
  49. package/dist/spa/src/components/ColumnValueInput.vue +28 -9
  50. package/dist/spa/src/components/ColumnValueInputWrapper.vue +2 -1
  51. package/dist/spa/src/components/CustomDateRangePicker.vue +9 -8
  52. package/dist/spa/src/components/CustomRangePicker.vue +32 -3
  53. package/dist/spa/src/components/Filters.vue +76 -31
  54. package/dist/spa/src/components/GroupsTable.vue +7 -7
  55. package/dist/spa/src/components/ResourceForm.vue +61 -26
  56. package/dist/spa/src/components/ResourceListTable.vue +28 -29
  57. package/dist/spa/src/components/ResourceListTableVirtual.vue +25 -27
  58. package/dist/spa/src/components/ShowTable.vue +8 -6
  59. package/dist/spa/src/components/SingleSkeletLoader.vue +6 -6
  60. package/dist/spa/src/components/SkeleteLoader.vue +1 -1
  61. package/dist/spa/src/components/ThreeDotsMenu.vue +5 -5
  62. package/dist/spa/src/components/Toast.vue +2 -7
  63. package/dist/spa/src/components/ValueRenderer.vue +4 -4
  64. package/dist/spa/src/controls/BoolToggle.vue +34 -0
  65. package/dist/spa/src/spa_types/core.ts +7 -0
  66. package/dist/spa/src/stores/core.ts +1 -1
  67. package/dist/spa/src/types/Back.ts +71 -10
  68. package/dist/spa/src/types/Common.ts +12 -7
  69. package/dist/spa/src/utils.ts +209 -0
  70. package/dist/spa/src/views/CreateView.vue +4 -4
  71. package/dist/spa/src/views/EditView.vue +3 -3
  72. package/dist/spa/src/views/ListView.vue +13 -18
  73. package/dist/spa/src/views/LoginView.vue +22 -24
  74. package/dist/spa/src/views/ResourceParent.vue +1 -1
  75. package/dist/spa/src/views/ShowView.vue +3 -3
  76. package/dist/types/Back.d.ts +55 -8
  77. package/dist/types/Back.d.ts.map +1 -1
  78. package/dist/types/Back.js.map +1 -1
  79. package/dist/types/Common.d.ts +11 -6
  80. package/dist/types/Common.d.ts.map +1 -1
  81. package/dist/types/Common.js.map +1 -1
  82. package/package.json +1 -1
@@ -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
 
@@ -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>
@@ -2,16 +2,16 @@
2
2
  <!-- drawer component -->
3
3
  <div id="drawer-navigation"
4
4
 
5
- class="af-filters-sidebar 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"
@@ -124,7 +138,7 @@
124
138
  <button
125
139
  :disabled="!filtersStore.filters.length"
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,6 +258,14 @@ 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);
@@ -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>
@@ -63,9 +63,9 @@
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 } 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';
@@ -95,6 +95,11 @@ const currentValues = ref(null);
95
95
  const customComponentsInValidity = ref({});
96
96
  const customComponentsEmptiness = ref({});
97
97
 
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
+
98
103
  const columnError = (column) => {
99
104
  const val = computed(() => {
100
105
  if (!currentValues.value) {
@@ -186,7 +191,7 @@ const validateValue = (type, value, column) => {
186
191
  };
187
192
 
188
193
 
189
- const setCurrentValue = (key, value, index=null) => {
194
+ const setCurrentValue = (key, value, index = null) => {
190
195
  const col = props.resource.columns.find((column) => column.name === key);
191
196
  // if field is an array, we need to update the array or individual element
192
197
  if (col.type === 'json' && col.isArray?.enabled) {
@@ -239,6 +244,23 @@ const setCurrentValue = (key, value, index=null) => {
239
244
  emit('update:record', up);
240
245
  };
241
246
 
247
+ watch(() => props.resource.columns, async (newColumns) => {
248
+ if (!newColumns) return;
249
+
250
+ for (const column of newColumns) {
251
+ if (column.foreignResource) {
252
+ if (!columnOptions.value[column.name]) {
253
+ columnOptions.value[column.name] = [];
254
+ columnLoadingState[column.name] = { loading: false, hasMore: true };
255
+ columnOffsets[column.name] = 0;
256
+ columnEmptyResultsCount[column.name] = 0;
257
+
258
+ await loadMoreOptions(column.name);
259
+ }
260
+ }
261
+ }
262
+ }, { immediate: true });
263
+
242
264
  onMounted(() => {
243
265
  currentValues.value = Object.assign({}, props.record);
244
266
  // json values should transform to string
@@ -266,29 +288,31 @@ onMounted(() => {
266
288
  emit('update:isValid', isValid.value);
267
289
  });
268
290
 
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
- }, {});
291
+ async function loadMoreOptions(columnName: string, searchTerm = '') {
292
+ return loadMoreForeignOptions({
293
+ columnName,
294
+ searchTerm,
295
+ columns: props.resource.columns,
296
+ resourceId: router.currentRoute.value.params.resourceId as string,
297
+ columnOptions,
298
+ columnLoadingState,
299
+ columnOffsets,
300
+ columnEmptyResultsCount
301
+ });
302
+ }
303
+
304
+ async function searchOptions(columnName: string, searchTerm: string) {
305
+ return searchForeignOptions({
306
+ columnName,
307
+ searchTerm,
308
+ columns: props.resource.columns,
309
+ resourceId: router.currentRoute.value.params.resourceId as string,
310
+ columnOptions,
311
+ columnLoadingState,
312
+ columnOffsets,
313
+ columnEmptyResultsCount
314
+ });
315
+ }
292
316
 
293
317
 
294
318
  const editableColumns = computed(() => {
@@ -330,6 +354,17 @@ const getOtherColumns = () => {
330
354
 
331
355
  const otherColumns = getOtherColumns();
332
356
 
357
+ const onSearchInput = computed(() => {
358
+ return createSearchInputHandlers(
359
+ props.resource.columns,
360
+ searchOptions
361
+ );
362
+ });
363
+
364
+ provide('columnLoadingState', columnLoadingState);
365
+ provide('onSearchInput', onSearchInput);
366
+ provide('loadMoreOptions', loadMoreOptions);
367
+
333
368
  watch(() => isValid.value, (value) => {
334
369
  emit('update:isValid', value);
335
370
  });
@@ -7,22 +7,22 @@
7
7
  <div role="status" v-if="!resource || !resource.columns"
8
8
  class="max-w p-4 space-y-4 divide-y divide-gray-200 rounded shadow animate-pulse dark:divide-gray-700 md:p-6 dark:border-gray-700">
9
9
  <div role="status" class="max-w-sm animate-pulse">
10
- <div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[360px]"></div>
10
+ <div class="h-2 bg-lightListSkeletLoader rounded-full dark:bg-darkListSkeletLoader max-w-[360px]"></div>
11
11
  </div>
12
12
  </div>
13
- <table v-else class=" w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400 rounded-default">
13
+ <table v-else class=" w-full text-sm text-left rtl:text-right text-lightListTableText dark:text-darkListTableText rounded-default">
14
14
 
15
15
  <tbody>
16
16
  <!-- table header -->
17
- <tr class="t-header sticky z-10 top-0 text-xs bg-lightListTableHeading dark:bg-darkListTableHeading dark:text-gray-400">
17
+ <tr class="t-header sticky z-10 top-0 text-xs text-lightListTableHeadingText bg-lightListTableHeading dark:bg-darkListTableHeading dark:text-darkListTableHeadingText">
18
18
  <td scope="col" class="p-4">
19
- <div class="flex items-center">
20
- <input id="checkbox-all-search" type="checkbox" :checked="allFromThisPageChecked" @change="selectAll()"
21
- :disabled="!rows || !rows.length"
22
- class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded
23
- focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
24
- <label for="checkbox-all-search" class="sr-only">{{ $t('checkbox') }}</label>
25
- </div>
19
+ <Checkbox
20
+ :modelValue="allFromThisPageChecked"
21
+ :disabled="!rows || !rows.length"
22
+ @update:modelValue="selectAll"
23
+ >
24
+ <span class="sr-only">{{ $t('checkbox') }}</span>
25
+ </Checkbox>
26
26
  </td>
27
27
 
28
28
  <td v-for="c in columnsListed" ref="headerRefs" scope="col" class="px-2 md:px-3 lg:px-6 py-3">
@@ -90,18 +90,16 @@
90
90
 
91
91
  :class="{'border-b': rowI !== rows.length - 1, 'cursor-pointer': row._clickUrl !== null}"
92
92
  >
93
- <td class="w-4 p-4 cursor-default" @click="(e)=>{e.stopPropagation()}">
94
- <div class="flex items center ">
95
- <input
96
- @click="(e)=>{e.stopPropagation()}"
97
- id="checkbox-table-search-1"
98
- type="checkbox"
99
- :checked="checkboxesInternal.includes(row._primaryKeyValue)"
100
- @change="(e)=>{addToCheckedValues(row._primaryKeyValue)}"
101
- class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600 cursor-pointer">
102
- <label for="checkbox-table-search-1" class="sr-only">{{ $t('checkbox') }}</label>
103
- </div>
104
- </td>
93
+ <td class="w-4 p-4 cursor-default" @click="(e)=>e.stopPropagation()">
94
+ <Checkbox
95
+ :model-value="checkboxesInternal.includes(row._primaryKeyValue)"
96
+ @change="(e)=>{addToCheckedValues(row._primaryKeyValue)}"
97
+ @click="(e)=>e.stopPropagation()"
98
+ >
99
+ <span class="sr-only">{{ $t('checkbox') }}</span>
100
+ </Checkbox>
101
+ </td>
102
+
105
103
  <td v-for="c in columnsListed" class="px-2 md:px-3 lg:px-6 py-4">
106
104
  <!-- if c.name in listComponentsPerColumn, render it. If not, render ValueRenderer -->
107
105
  <component
@@ -205,7 +203,7 @@
205
203
  >
206
204
  <!-- Buttons -->
207
205
  <button
208
- class="af-pagination-prev-button flex items-center py-1 px-3 gap-1 text-sm font-medium text-gray-900 focus:outline-none bg-white border-r-0 rounded-s 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"
206
+ class="af-pagination-prev-button flex items-center py-1 px-3 gap-1 text-sm font-medium text-lightListTablePaginationText focus:outline-none bg-lightListTablePaginationBackgoround border-r-0 rounded-s border border-lightListTablePaginationBorder hover:bg-lightListTablePaginationBackgoroundHover hover:text-lightListTablePaginationTextHover focus:z-10 focus:ring-4 focus:ring-lightListTablePaginationFocusRing dark:focus:ring-darkListTablePaginationFocusRing dark:bg-darkListTablePaginationBackgoround dark:text-darkListTablePaginationText dark:border-darkListTablePaginationBorder dark:hover:text-darkListTablePaginationTextHover dark:hover:bg-darkListTablePaginationBackgoroundHover disabled:opacity-50"
209
207
  @click="page--; pageInput = page.toString();" :disabled="page <= 1">
210
208
  <svg class="w-3.5 h-3.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
211
209
  viewBox="0 0 14 10">
@@ -217,14 +215,14 @@
217
215
  </span>
218
216
  </button>
219
217
  <button
220
- class="af-pagination-first-page-button flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white border-r-0 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"
218
+ class="af-pagination-first-page-button flex items-center py-1 px-3 text-sm font-medium text-lightListTablePaginationText focus:outline-none bg-lightListTablePaginationBackgoround border-r-0 border border-lightListTablePaginationBorder hover:bg-lightListTablePaginationBackgoroundHover hover:text-lightListTablePaginationTextHover focus:z-10 focus:ring-4 focus:ring-lightListTablePaginationFocusRing dark:focus:ring-darkListTablePaginationFocusRing dark:bg-darkListTablePaginationBackgoround dark:text-darkListTablePaginationText dark:border-darkListTablePaginationBorder dark:hover:text-darkListTablePaginationTextHover dark:hover:bg-darkListTablePaginationBackgoroundHover disabled:opacity-50"
221
219
  @click="page = 1; pageInput = page.toString();" :disabled="page <= 1">
222
220
  <!-- <IconChevronDoubleLeftOutline class="w-4 h-4" /> -->
223
221
  1
224
222
  </button>
225
223
  <div
226
224
  contenteditable="true"
227
- class="af-pagination-input min-w-10 outline-none inline-block w-auto min-w-10 py-1.5 px-3 text-sm text-center text-gray-700 border border-gray-300 dark:border-gray-700 dark:text-gray-400 dark:bg-gray-800 z-10"
225
+ class="af-pagination-input min-w-10 outline-none inline-block w-auto py-1.5 px-3 text-sm text-center text-lightListTablePaginationCurrentPageText border border-lightListTablePaginationBorder dark:border-darkListTablePaginationBorder dark:text-darkListTablePaginationCurrentPageText dark:bg-darkListTablePaginationBackgoround z-10"
228
226
  @keydown="onPageKeydown($event)"
229
227
  @input="onPageInput($event)"
230
228
  @blur="validatePageInput()"
@@ -233,14 +231,14 @@
233
231
  </div>
234
232
 
235
233
  <button
236
- class="af-pagination-last-page-button flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white border-l-0 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"
234
+ class="af-pagination-last-page-button flex items-center py-1 px-3 text-sm font-medium text-lightListTablePaginationText focus:outline-none bg-lightListTablePaginationBackgoround border-l-0 border border-lightListTablePaginationBorder hover:bg-lightListTablePaginationBackgoroundHover hover:text-lightListTablePaginationTextHover focus:z-10 focus:ring-4 focus:ring-lightListTablePaginationFocusRing dark:focus:ring-darkListTablePaginationFocusRing dark:bg-darkListTablePaginationBackgoround dark:text-darkListTablePaginationText dark:border-darkListTablePaginationBorder dark:hover:text-white dark:hover:bg-darkListTablePaginationBackgoroundHover disabled:opacity-50"
237
235
  @click="page = totalPages; pageInput = page.toString();" :disabled="page >= totalPages">
238
236
  {{ totalPages }}
239
237
 
240
238
  <!-- <IconChevronDoubleRightOutline class="w-4 h-4" /> -->
241
239
  </button>
242
240
  <button
243
- class="af-pagination-next-button flex items-center py-1 px-3 gap-1 text-sm font-medium text-gray-900 focus:outline-none bg-white border-l-0 rounded-e 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"
241
+ class="af-pagination-next-button flex items-center py-1 px-3 gap-1 text-sm font-medium text-lightListTablePaginationText focus:outline-none bg-lightListTablePaginationBackgoround border-l-0 rounded-e border border-lightListTablePaginationBorder hover:bg-lightListTablePaginationBackgoroundHover hover:text-lightListTablePaginationTextHover focus:z-10 focus:ring-4 focus:ring-lightListTablePaginationFocusRing dark:focus:ring-darkListTablePaginationFocusRing dark:bg-darkListTablePaginationBackgoround dark:text-darkListTablePaginationText dark:border-darkListTablePaginationBorder dark:hover:text-white dark:hover:bg-darkListTablePaginationBackgoroundHover disabled:opacity-50"
244
242
  @click="page++; pageInput = page.toString();" :disabled="page >= totalPages">
245
243
  <span class="hidden sm:inline">{{ $t('Next') }}</span>
246
244
  <svg class="w-3.5 h-3.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
@@ -252,7 +250,7 @@
252
250
  </div>
253
251
 
254
252
  <!-- Help text -->
255
- <span class="text-sm text-gray-700 dark:text-gray-400">
253
+ <span class="text-sm text-lightListTablePaginationHelpText dark:text-darkListTablePaginationHelpText">
256
254
  <span v-if="((((page || 1) - 1) * pageSize + 1 > totalRows) && totalRows > 0)">{{ $t('Wrong Page') }} </span>
257
255
  <template v-else-if="resource && totalRows > 0">
258
256
 
@@ -306,12 +304,13 @@ import {
306
304
  import {
307
305
  IconEyeSolid,
308
306
  IconPenSolid,
309
- IconTrashBinSolid
307
+ IconTrashBinSolid,
310
308
  } from '@iconify-prerendered/vue-flowbite';
311
309
  import router from '@/router';
312
310
  import { Tooltip } from '@/afcl';
313
311
  import type { AdminForthResourceCommon } from '@/types/Common';
314
312
  import adminforth from '@/adminforth';
313
+ import Checkbox from '@/afcl/Checkbox.vue';
315
314
 
316
315
  const coreStore = useCoreStore();
317
316
  const { t } = useI18n();