adminforth 2.4.0-next.7 → 2.4.0-next.71

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 (114) 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 +1 -1
  5. package/commands/createApp/templates/package.json.hbs +1 -1
  6. package/commands/createApp/utils.js +39 -13
  7. package/commands/createCustomComponent/configUpdater.js +25 -21
  8. package/commands/createCustomComponent/fileGenerator.js +1 -1
  9. package/commands/createCustomComponent/main.js +2 -1
  10. package/commands/createCustomComponent/templates/login/beforeLogin.vue.hbs +18 -0
  11. package/dist/dataConnectors/baseConnector.d.ts.map +1 -1
  12. package/dist/dataConnectors/baseConnector.js +16 -3
  13. package/dist/dataConnectors/baseConnector.js.map +1 -1
  14. package/dist/dataConnectors/mongo.d.ts.map +1 -1
  15. package/dist/dataConnectors/mongo.js +14 -14
  16. package/dist/dataConnectors/mongo.js.map +1 -1
  17. package/dist/index.d.ts +2 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +20 -9
  20. package/dist/index.js.map +1 -1
  21. package/dist/modules/codeInjector.d.ts.map +1 -1
  22. package/dist/modules/codeInjector.js +25 -9
  23. package/dist/modules/codeInjector.js.map +1 -1
  24. package/dist/modules/configValidator.d.ts.map +1 -1
  25. package/dist/modules/configValidator.js +50 -1
  26. package/dist/modules/configValidator.js.map +1 -1
  27. package/dist/modules/restApi.d.ts.map +1 -1
  28. package/dist/modules/restApi.js +45 -2
  29. package/dist/modules/restApi.js.map +1 -1
  30. package/dist/modules/styles.d.ts +42 -0
  31. package/dist/modules/styles.d.ts.map +1 -1
  32. package/dist/modules/styles.js +44 -2
  33. package/dist/modules/styles.js.map +1 -1
  34. package/dist/spa/index.html +1 -1
  35. package/dist/spa/src/App.vue +14 -5
  36. package/dist/spa/src/afcl/Button.vue +4 -4
  37. package/dist/spa/src/afcl/Checkbox.vue +21 -13
  38. package/dist/spa/src/afcl/CountryFlag.vue +3 -1
  39. package/dist/spa/src/afcl/Dropzone.vue +4 -2
  40. package/dist/spa/src/afcl/Input.vue +5 -3
  41. package/dist/spa/src/afcl/JsonViewer.vue +25 -0
  42. package/dist/spa/src/afcl/Link.vue +1 -1
  43. package/dist/spa/src/afcl/LinkButton.vue +1 -1
  44. package/dist/spa/src/afcl/Select.vue +44 -15
  45. package/dist/spa/src/afcl/Table.vue +8 -8
  46. package/dist/spa/src/afcl/Toggle.vue +32 -0
  47. package/dist/spa/src/afcl/Tooltip.vue +1 -1
  48. package/dist/spa/src/afcl/index.ts +2 -2
  49. package/dist/spa/src/components/AcceptModal.vue +4 -4
  50. package/dist/spa/src/components/ColumnValueInput.vue +21 -2
  51. package/dist/spa/src/components/ColumnValueInputWrapper.vue +1 -0
  52. package/dist/spa/src/components/CustomDatePicker.vue +2 -2
  53. package/dist/spa/src/components/CustomDateRangePicker.vue +1 -0
  54. package/dist/spa/src/components/Filters.vue +73 -28
  55. package/dist/spa/src/components/GroupsTable.vue +3 -3
  56. package/dist/spa/src/components/ResourceForm.vue +61 -26
  57. package/dist/spa/src/components/ResourceListTable.vue +34 -36
  58. package/dist/spa/src/components/ResourceListTableVirtual.vue +23 -25
  59. package/dist/spa/src/components/ShowTable.vue +11 -6
  60. package/dist/spa/src/components/ValueRenderer.vue +4 -4
  61. package/dist/spa/src/controls/BoolToggle.vue +34 -0
  62. package/dist/spa/src/spa_types/core.ts +7 -0
  63. package/dist/spa/src/stores/core.ts +1 -1
  64. package/dist/spa/src/types/Back.ts +46 -12
  65. package/dist/spa/src/types/Common.ts +10 -1
  66. package/dist/spa/src/types/adapters/CompletionAdapter.ts +25 -0
  67. package/dist/spa/src/types/adapters/EmailAdapter.ts +29 -0
  68. package/dist/spa/src/types/adapters/ImageGenerationAdapter.ts +50 -0
  69. package/dist/spa/src/types/adapters/OAuth2Adapter.ts +34 -0
  70. package/dist/spa/src/types/adapters/StorageAdapter.ts +73 -0
  71. package/dist/spa/src/types/adapters/index.ts +5 -0
  72. package/dist/spa/src/utils.ts +209 -0
  73. package/dist/spa/src/views/CreateView.vue +3 -3
  74. package/dist/spa/src/views/EditView.vue +1 -1
  75. package/dist/spa/src/views/ListView.vue +2 -2
  76. package/dist/spa/src/views/LoginView.vue +39 -37
  77. package/dist/spa/src/views/ResourceParent.vue +1 -1
  78. package/dist/spa/src/views/ShowView.vue +3 -3
  79. package/dist/types/Back.d.ts +40 -9
  80. package/dist/types/Back.d.ts.map +1 -1
  81. package/dist/types/Back.js.map +1 -1
  82. package/dist/types/Common.d.ts +9 -0
  83. package/dist/types/Common.d.ts.map +1 -1
  84. package/dist/types/Common.js.map +1 -1
  85. package/dist/types/adapters/CompletionAdapter.d.ts +20 -0
  86. package/dist/types/adapters/CompletionAdapter.d.ts.map +1 -0
  87. package/dist/types/adapters/CompletionAdapter.js +2 -0
  88. package/dist/types/adapters/CompletionAdapter.js.map +1 -0
  89. package/dist/types/adapters/EmailAdapter.d.ts +21 -0
  90. package/dist/types/adapters/EmailAdapter.d.ts.map +1 -0
  91. package/dist/types/adapters/EmailAdapter.js +2 -0
  92. package/dist/types/adapters/EmailAdapter.js.map +1 -0
  93. package/dist/types/adapters/ImageGenerationAdapter.d.ts +37 -0
  94. package/dist/types/adapters/ImageGenerationAdapter.d.ts.map +1 -0
  95. package/dist/types/adapters/ImageGenerationAdapter.js +2 -0
  96. package/dist/types/adapters/ImageGenerationAdapter.js.map +1 -0
  97. package/dist/types/adapters/OAuth2Adapter.d.ts +32 -0
  98. package/dist/types/adapters/OAuth2Adapter.d.ts.map +1 -0
  99. package/dist/types/adapters/OAuth2Adapter.js +2 -0
  100. package/dist/types/adapters/OAuth2Adapter.js.map +1 -0
  101. package/dist/types/adapters/StorageAdapter.d.ts +63 -0
  102. package/dist/types/adapters/StorageAdapter.d.ts.map +1 -0
  103. package/dist/types/adapters/StorageAdapter.js +2 -0
  104. package/dist/types/adapters/StorageAdapter.js.map +1 -0
  105. package/dist/types/adapters/index.d.ts +6 -0
  106. package/dist/types/adapters/index.d.ts.map +1 -0
  107. package/dist/types/adapters/index.js +2 -0
  108. package/dist/types/adapters/index.js.map +1 -0
  109. package/package.json +2 -2
  110. package/dist/spa/src/types/Adapters.ts +0 -213
  111. package/dist/types/Adapters.d.ts +0 -168
  112. package/dist/types/Adapters.d.ts.map +0 -1
  113. package/dist/types/Adapters.js +0 -2
  114. package/dist/types/Adapters.js.map +0 -1
@@ -19,12 +19,26 @@
19
19
  ref="input"
20
20
  class="w-full min-w-24"
21
21
  :options="columnOptions[column.name] || []"
22
+ :searchDisabled="!column.foreignResource.searchableFields"
23
+ @scroll-near-end="loadMoreOptions && loadMoreOptions(column.name)"
24
+ @search="(searchTerm) => {
25
+ if (column.foreignResource.searchableFields && onSearchInput && onSearchInput[column.name]) {
26
+ onSearchInput[column.name](searchTerm);
27
+ }
28
+ }"
22
29
  teleportToBody
23
30
  :placeholder = "columnOptions[column.name]?.length ?$t('Select...'): $t('There are no options available')"
24
31
  :modelValue="value"
25
32
  :readonly="(column.editReadonly && source === 'edit') || readonly"
26
33
  @update:modelValue="$emit('update:modelValue', $event)"
27
- />
34
+ >
35
+ <template #extra-item v-if="columnLoadingState && columnLoadingState[column.name]?.loading">
36
+ <div class="text-center text-gray-400 dark:text-gray-300 py-2 flex items-center justify-center gap-2">
37
+ <Spinner class="w-4 h-4" />
38
+ {{ $t('Loading...') }}
39
+ </div>
40
+ </template>
41
+ </Select>
28
42
  <Select
29
43
  v-else-if="column.enum"
30
44
  ref="input"
@@ -142,7 +156,8 @@
142
156
  import CustomDatePicker from "@/components/CustomDatePicker.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 { ref, inject } from 'vue';
146
161
  import { getCustomComponent } from '@/utils';
147
162
  import { useI18n } from 'vue-i18n';
148
163
  import { useCoreStore } from '@/stores/core';
@@ -171,6 +186,10 @@
171
186
  }
172
187
  );
173
188
 
189
+ const columnLoadingState = inject('columnLoadingState', {} as any);
190
+ const onSearchInput = inject('onSearchInput', {} as any);
191
+ const loadMoreOptions = inject('loadMoreOptions', (() => {}) as any);
192
+
174
193
  const input = ref(null);
175
194
 
176
195
  const getBooleanOptions = (column: any) => {
@@ -13,6 +13,7 @@
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)"
@@ -10,7 +10,7 @@
10
10
  </div>
11
11
 
12
12
  <input ref="datepickerStartEl" type="text"
13
- 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"
13
+ class="af-pick-date-button 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"
14
14
  :placeholder="$t('Select date')" :disabled="readonly" />
15
15
 
16
16
  </div>
@@ -26,7 +26,7 @@
26
26
  </div>
27
27
 
28
28
  <input v-model="startTime" type="time" id="start-time" onfocus="this.showPicker()" onclick="this.showPicker()" step="1"
29
- 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"
29
+ class="af-pick-time-button 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"
30
30
  value="00:00" :disabled="readonly" required/>
31
31
  </div>
32
32
  </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 {
@@ -2,7 +2,7 @@
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-white w-80 dark:bg-gray-800 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"
@@ -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"
@@ -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-gray-700 bg-lightFormHeading dark:bg-darkFormHeading dark:text-gray-400 rounded-t-lg">
4
4
  {{ group.groupName }}
5
5
  </div>
6
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 ">
7
+ <thead v-if="!allColumnsHaveCustomComponent" class="text-xs text-gray-700 uppercase dark:text-gray-400 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"
@@ -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
  });
@@ -6,7 +6,6 @@
6
6
  <!-- skelet loader -->
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
-
10
9
  <div role="status" class="max-w-sm animate-pulse">
11
10
  <div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[360px]"></div>
12
11
  </div>
@@ -17,13 +16,13 @@
17
16
  <!-- table header -->
18
17
  <tr class="t-header sticky z-10 top-0 text-xs bg-lightListTableHeading dark:bg-darkListTableHeading dark:text-gray-400">
19
18
  <td scope="col" class="p-4">
20
- <div class="flex items-center">
21
- <input id="checkbox-all-search" type="checkbox" :checked="allFromThisPageChecked" @change="selectAll()"
22
- :disabled="!rows || !rows.length"
23
- class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded
24
- 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">
25
- <label for="checkbox-all-search" class="sr-only">{{ $t('checkbox') }}</label>
26
- </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>
27
26
  </td>
28
27
 
29
28
  <td v-for="c in columnsListed" ref="headerRefs" scope="col" class="px-2 md:px-3 lg:px-6 py-3">
@@ -91,18 +90,16 @@
91
90
 
92
91
  :class="{'border-b': rowI !== rows.length - 1, 'cursor-pointer': row._clickUrl !== null}"
93
92
  >
94
- <td class="w-4 p-4 cursor-default" @click="(e)=>{e.stopPropagation()}">
95
- <div class="flex items center ">
96
- <input
97
- @click="(e)=>{e.stopPropagation()}"
98
- id="checkbox-table-search-1"
99
- type="checkbox"
100
- :checked="checkboxesInternal.includes(row._primaryKeyValue)"
101
- @change="(e)=>{addToCheckedValues(row._primaryKeyValue)}"
102
- 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">
103
- <label for="checkbox-table-search-1" class="sr-only">{{ $t('checkbox') }}</label>
104
- </div>
105
- </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
+
106
103
  <td v-for="c in columnsListed" class="px-2 md:px-3 lg:px-6 py-4">
107
104
  <!-- if c.name in listComponentsPerColumn, render it. If not, render ValueRenderer -->
108
105
  <component
@@ -128,7 +125,7 @@
128
125
  }"
129
126
 
130
127
  >
131
- <IconEyeSolid class="w-5 h-5 me-2"/>
128
+ <IconEyeSolid class="af-show-icon w-5 h-5 me-2"/>
132
129
  </RouterLink>
133
130
 
134
131
  <template v-slot:tooltip>
@@ -147,7 +144,7 @@
147
144
  }
148
145
  }"
149
146
  >
150
- <IconPenSolid class="w-5 h-5 me-2"/>
147
+ <IconPenSolid class="af-edit-icon w-5 h-5 me-2"/>
151
148
  </RouterLink>
152
149
  <template v-slot:tooltip>
153
150
  {{ $t('Edit item') }}
@@ -159,7 +156,7 @@
159
156
  v-if="resource.options?.allowedActions.delete"
160
157
  @click="deleteRecord(row)"
161
158
  >
162
- <IconTrashBinSolid class="w-5 h-5 me-2"/>
159
+ <IconTrashBinSolid class="af-delete-icon w-5 h-5 me-2"/>
163
160
  </button>
164
161
 
165
162
  <template v-slot:tooltip>
@@ -199,14 +196,14 @@
199
196
  <!-- pagination
200
197
  totalRows in v-if is used to not hide page input during loading when user puts cursor into it and edit directly (rows gets null there during edit)
201
198
  -->
202
- <div class="flex flex-row items-center mt-4 xs:flex-row xs:justify-between xs:items-center gap-3"
203
- v-if="(rows || totalRows) && totalRows >= pageSize && totalRows > 0"
204
- >
199
+ <div class="af-pagination-container flex flex-row items-center mt-4 xs:flex-row xs:justify-between xs:items-center gap-3">
205
200
 
206
- <div class="inline-flex ">
201
+ <div class="af-pagination-buttons-container inline-flex "
202
+ v-if="(rows || totalRows) && totalRows >= pageSize && totalRows > 0"
203
+ >
207
204
  <!-- Buttons -->
208
205
  <button
209
- class="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-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"
210
207
  @click="page--; pageInput = page.toString();" :disabled="page <= 1">
211
208
  <svg class="w-3.5 h-3.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
212
209
  viewBox="0 0 14 10">
@@ -218,14 +215,14 @@
218
215
  </span>
219
216
  </button>
220
217
  <button
221
- class="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-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"
222
219
  @click="page = 1; pageInput = page.toString();" :disabled="page <= 1">
223
220
  <!-- <IconChevronDoubleLeftOutline class="w-4 h-4" /> -->
224
221
  1
225
222
  </button>
226
223
  <div
227
224
  contenteditable="true"
228
- class="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 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"
229
226
  @keydown="onPageKeydown($event)"
230
227
  @input="onPageInput($event)"
231
228
  @blur="validatePageInput()"
@@ -234,14 +231,14 @@
234
231
  </div>
235
232
 
236
233
  <button
237
- class="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-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"
238
235
  @click="page = totalPages; pageInput = page.toString();" :disabled="page >= totalPages">
239
236
  {{ totalPages }}
240
237
 
241
238
  <!-- <IconChevronDoubleRightOutline class="w-4 h-4" /> -->
242
239
  </button>
243
240
  <button
244
- class="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-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"
245
242
  @click="page++; pageInput = page.toString();" :disabled="page >= totalPages">
246
243
  <span class="hidden sm:inline">{{ $t('Next') }}</span>
247
244
  <svg class="w-3.5 h-3.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
@@ -254,10 +251,10 @@
254
251
 
255
252
  <!-- Help text -->
256
253
  <span class="text-sm text-gray-700 dark:text-gray-400">
257
- <span v-if="((page || 1) - 1) * pageSize + 1 > totalRows">{{ $t('Wrong Page') }} </span>
258
- <template v-else>
254
+ <span v-if="((((page || 1) - 1) * pageSize + 1 > totalRows) && totalRows > 0)">{{ $t('Wrong Page') }} </span>
255
+ <template v-else-if="resource && totalRows > 0">
259
256
 
260
- <span class="hidden sm:inline">
257
+ <span class="af-pagination-info hidden sm:inline">
261
258
  <i18n-t keypath="Showing {from} to {to} of {total} Entries" tag="p" >
262
259
  <template v-slot:from>
263
260
  <strong>{{ from }}</strong>
@@ -307,12 +304,13 @@ import {
307
304
  import {
308
305
  IconEyeSolid,
309
306
  IconPenSolid,
310
- IconTrashBinSolid
307
+ IconTrashBinSolid,
311
308
  } from '@iconify-prerendered/vue-flowbite';
312
309
  import router from '@/router';
313
310
  import { Tooltip } from '@/afcl';
314
311
  import type { AdminForthResourceCommon } from '@/types/Common';
315
312
  import adminforth from '@/adminforth';
313
+ import Checkbox from '@/afcl/Checkbox.vue';
316
314
 
317
315
  const coreStore = useCoreStore();
318
316
  const { t } = useI18n();
@@ -5,7 +5,7 @@
5
5
  :style="`height: ${containerHeight}px; will-change: transform;`"
6
6
  @scroll="handleScroll"
7
7
  ref="containerRef"
8
- >
8
+ >
9
9
  <!-- skelet loader -->
10
10
  <div role="status" v-if="!resource || !resource.columns"
11
11
  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">
@@ -20,13 +20,13 @@
20
20
  <!-- table header -->
21
21
  <tr class="t-header sticky z-10 top-0 text-xs bg-lightListTableHeading dark:bg-darkListTableHeading dark:text-gray-400">
22
22
  <td scope="col" class="p-4">
23
- <div class="flex items-center">
24
- <input id="checkbox-all-search" type="checkbox" :checked="allFromThisPageChecked" @change="selectAll()"
25
- :disabled="!rows || !rows.length"
26
- class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded
27
- 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">
28
- <label for="checkbox-all-search" class="sr-only">{{ $t('checkbox') }}</label>
29
- </div>
23
+ <Checkbox
24
+ :modelValue="allFromThisPageChecked"
25
+ :disabled="!rows || !rows.length"
26
+ @update:modelValue="selectAll"
27
+ >
28
+ <span class="sr-only">{{ $t('checkbox') }}</span>
29
+ </Checkbox>
30
30
  </td>
31
31
 
32
32
  <td v-for="c in columnsListed" ref="headerRefs" scope="col" class="px-2 md:px-3 lg:px-6 py-3">
@@ -101,17 +101,14 @@
101
101
  :class="{'border-b': rowI !== visibleRows.length - 1, 'cursor-pointer': row._clickUrl !== null}"
102
102
  @mounted="(el) => updateRowHeight(`row_${row._primaryKeyValue}`, el.offsetHeight)"
103
103
  >
104
- <td class="w-4 p-4 cursor-default" @click="(e)=>{e.stopPropagation()}">
105
- <div class="flex items center ">
106
- <input
107
- @click="(e)=>{e.stopPropagation()}"
108
- id="checkbox-table-search-1"
109
- type="checkbox"
110
- :checked="checkboxesInternal.includes(row._primaryKeyValue)"
111
- @change="(e)=>{addToCheckedValues(row._primaryKeyValue)}"
112
- 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">
113
- <label for="checkbox-table-search-1" class="sr-only">{{ $t('checkbox') }}</label>
114
- </div>
104
+ <td class="w-4 p-4 cursor-default" @click="(e)=>e.stopPropagation()">
105
+ <Checkbox
106
+ :model-value="checkboxesInternal.includes(row._primaryKeyValue)"
107
+ @change="(e)=>{addToCheckedValues(row._primaryKeyValue)}"
108
+ @click="(e)=>e.stopPropagation()"
109
+ >
110
+ <span class="sr-only">{{ $t('checkbox') }}</span>
111
+ </Checkbox>
115
112
  </td>
116
113
  <td v-for="c in columnsListed" class="px-2 md:px-3 lg:px-6 py-4">
117
114
  <!-- if c.name in listComponentsPerColumn, render it. If not, render ValueRenderer -->
@@ -216,11 +213,11 @@
216
213
  <!-- pagination
217
214
  totalRows in v-if is used to not hide page input during loading when user puts cursor into it and edit directly (rows gets null there during edit)
218
215
  -->
219
- <div class="flex flex-row items-center mt-4 xs:flex-row xs:justify-between xs:items-center gap-3"
220
- v-if="(rows || totalRows) && totalRows >= pageSize && totalRows > 0"
221
- >
216
+ <div class="flex flex-row items-center mt-4 xs:flex-row xs:justify-between xs:items-center gap-3">
222
217
 
223
- <div class="inline-flex ">
218
+ <div class="inline-flex "
219
+ v-if="(rows || totalRows) && totalRows >= pageSize && totalRows > 0"
220
+ >
224
221
  <!-- Buttons -->
225
222
  <button
226
223
  class="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"
@@ -271,8 +268,8 @@
271
268
 
272
269
  <!-- Help text -->
273
270
  <span class="text-sm text-gray-700 dark:text-gray-400">
274
- <span v-if="((page || 1) - 1) * pageSize + 1 > totalRows">{{ $t('Wrong Page') }} </span>
275
- <template v-else>
271
+ <span v-if="((((page || 1) - 1) * pageSize + 1 > totalRows) && totalRows > 0)">{{ $t('Wrong Page') }} </span>
272
+ <template v-else-if="resource && totalRows > 0">
276
273
 
277
274
  <span class="hidden sm:inline">
278
275
  <i18n-t keypath="Showing {from} to {to} of {total} Entries" tag="p" >
@@ -330,6 +327,7 @@ import router from '@/router';
330
327
  import { Tooltip } from '@/afcl';
331
328
  import type { AdminForthResourceCommon } from '@/types/Common';
332
329
  import adminforth from '@/adminforth';
330
+ import Checkbox from '@/afcl/Checkbox.vue';
333
331
 
334
332
  const coreStore = useCoreStore();
335
333
  const { t } = useI18n();