adminforth 2.26.0-next.3 → 2.26.0-next.30

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 (88) hide show
  1. package/commands/createApp/templates/package.json.hbs +1 -1
  2. package/dist/dataConnectors/baseConnector.d.ts.map +1 -1
  3. package/dist/dataConnectors/baseConnector.js +3 -0
  4. package/dist/dataConnectors/baseConnector.js.map +1 -1
  5. package/dist/dataConnectors/clickhouse.d.ts +4 -0
  6. package/dist/dataConnectors/clickhouse.d.ts.map +1 -1
  7. package/dist/dataConnectors/clickhouse.js +14 -0
  8. package/dist/dataConnectors/clickhouse.js.map +1 -1
  9. package/dist/dataConnectors/mongo.d.ts +4 -0
  10. package/dist/dataConnectors/mongo.d.ts.map +1 -1
  11. package/dist/dataConnectors/mongo.js +9 -0
  12. package/dist/dataConnectors/mongo.js.map +1 -1
  13. package/dist/dataConnectors/mysql.d.ts +4 -0
  14. package/dist/dataConnectors/mysql.d.ts.map +1 -1
  15. package/dist/dataConnectors/mysql.js +11 -0
  16. package/dist/dataConnectors/mysql.js.map +1 -1
  17. package/dist/dataConnectors/postgres.d.ts +4 -0
  18. package/dist/dataConnectors/postgres.d.ts.map +1 -1
  19. package/dist/dataConnectors/postgres.js +11 -0
  20. package/dist/dataConnectors/postgres.js.map +1 -1
  21. package/dist/dataConnectors/qdrant.d.ts +57 -0
  22. package/dist/dataConnectors/qdrant.d.ts.map +1 -0
  23. package/dist/dataConnectors/qdrant.js +469 -0
  24. package/dist/dataConnectors/qdrant.js.map +1 -0
  25. package/dist/dataConnectors/sqlite.d.ts +4 -0
  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.map +1 -1
  30. package/dist/index.js +22 -33
  31. package/dist/index.js.map +1 -1
  32. package/dist/modules/codeInjector.d.ts.map +1 -1
  33. package/dist/modules/codeInjector.js +59 -50
  34. package/dist/modules/codeInjector.js.map +1 -1
  35. package/dist/modules/configValidator.d.ts.map +1 -1
  36. package/dist/modules/configValidator.js +3 -2
  37. package/dist/modules/configValidator.js.map +1 -1
  38. package/dist/modules/restApi.d.ts +1 -0
  39. package/dist/modules/restApi.d.ts.map +1 -1
  40. package/dist/modules/restApi.js +33 -16
  41. package/dist/modules/restApi.js.map +1 -1
  42. package/dist/modules/utils.d.ts +6 -0
  43. package/dist/modules/utils.d.ts.map +1 -1
  44. package/dist/modules/utils.js +13 -0
  45. package/dist/modules/utils.js.map +1 -1
  46. package/dist/servers/express.d.ts.map +1 -1
  47. package/dist/servers/express.js +7 -1
  48. package/dist/servers/express.js.map +1 -1
  49. package/dist/spa/package-lock.json +57 -0
  50. package/dist/spa/package.json +2 -0
  51. package/dist/spa/pnpm-lock.yaml +32 -0
  52. package/dist/spa/src/adminforth.ts +17 -29
  53. package/dist/spa/src/afcl/Input.vue +1 -1
  54. package/dist/spa/src/afcl/Modal.vue +18 -3
  55. package/dist/spa/src/afcl/Select.vue +4 -2
  56. package/dist/spa/src/afcl/Table.vue +27 -13
  57. package/dist/spa/src/components/AcceptModal.vue +33 -53
  58. package/dist/spa/src/components/BreadcrumbsWithButtons.vue +4 -5
  59. package/dist/spa/src/components/ColumnValueInputWrapper.vue +11 -3
  60. package/dist/spa/src/components/ListActionsThreeDots.vue +10 -9
  61. package/dist/spa/src/components/ResourceListTable.vue +291 -144
  62. package/dist/spa/src/components/Sidebar.vue +6 -2
  63. package/dist/spa/src/components/ThreeDotsMenu.vue +10 -9
  64. package/dist/spa/src/i18n.ts +1 -1
  65. package/dist/spa/src/renderers/CountryFlag.vue +2 -2
  66. package/dist/spa/src/stores/core.ts +4 -2
  67. package/dist/spa/src/types/Back.ts +24 -5
  68. package/dist/spa/src/types/Common.ts +45 -5
  69. package/dist/spa/src/types/FrontendAPI.ts +6 -1
  70. package/dist/spa/src/utils/listUtils.ts +8 -2
  71. package/dist/spa/src/utils/utils.ts +29 -10
  72. package/dist/spa/src/views/CreateView.vue +8 -8
  73. package/dist/spa/src/views/EditView.vue +8 -7
  74. package/dist/spa/src/views/ListView.vue +14 -48
  75. package/dist/spa/src/views/LoginView.vue +13 -13
  76. package/dist/spa/src/views/PageNotFound.vue +5 -1
  77. package/dist/spa/src/views/ShowView.vue +6 -6
  78. package/dist/types/Back.d.ts +23 -6
  79. package/dist/types/Back.d.ts.map +1 -1
  80. package/dist/types/Back.js.map +1 -1
  81. package/dist/types/Common.d.ts +25 -5
  82. package/dist/types/Common.d.ts.map +1 -1
  83. package/dist/types/Common.js.map +1 -1
  84. package/dist/types/FrontendAPI.d.ts +13 -1
  85. package/dist/types/FrontendAPI.d.ts.map +1 -1
  86. package/dist/types/FrontendAPI.js.map +1 -1
  87. package/package.json +2 -1
  88. package/dist/spa/src/components/ResourceListTableVirtual.vue +0 -794
@@ -1,16 +1,23 @@
1
1
  <template>
2
2
  <!-- table -->
3
- <div class="relative shadow-listTableShadow dark:shadow-darkListTableShadow overflow-auto "
3
+ <div
4
+ class="relative shadow-listTableShadow dark:shadow-darkListTableShadow overflow-auto "
4
5
  :class="{'rounded-default': !noRoundings}"
6
+ :style="isVirtualScrollEnabled ? { maxHeight: `${containerHeight}px` } : {}"
7
+ @scroll="handleScroll"
8
+ ref="containerRef"
5
9
  >
6
10
  <!-- skelet loader -->
7
- <div role="status" v-if="!resource || !resource.columns"
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
- <div role="status" class="max-w-sm animate-pulse">
10
- <div class="h-2 bg-lightListSkeletLoader rounded-full dark:bg-darkListSkeletLoader max-w-[360px]"></div>
11
- </div>
11
+ <div
12
+ role="status" v-if="!resource || !resource.columns"
13
+ 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"
14
+ >
15
+ <div role="status" class="max-w-sm animate-pulse">
16
+ <div class="h-2 bg-lightListSkeletLoader rounded-full dark:bg-darkListSkeletLoader max-w-[360px]"></div>
17
+ </div>
12
18
  </div>
13
- <table v-else class=" w-full text-sm text-left rtl:text-right text-lightListTableText dark:text-darkListTableText rounded-default">
19
+
20
+ <table v-else class="w-full text-sm text-left rtl:text-right text-lightListTableText dark:text-darkListTableText rounded-default">
14
21
 
15
22
  <tbody>
16
23
  <!-- table header -->
@@ -27,27 +34,33 @@
27
34
 
28
35
  <td v-for="c in columnsListed" ref="headerRefs" scope="col" class="list-table-header-cell px-2 md:px-3 lg:px-6 py-3" :class="{'sticky-column bg-lightListTableHeading dark:bg-darkListTableHeading': c.listSticky}">
29
36
 
30
- <div @click="(evt) => c.sortable && onSortButtonClick(evt, c.name)"
31
- class="flex items-center " :class="{'cursor-pointer':c.sortable}">
37
+ <div
38
+ @click="(evt) => c.sortable && onSortButtonClick(evt, c.name)"
39
+ class="flex items-center " :class="{'cursor-pointer':c.sortable}"
40
+ >
32
41
  {{ c.label }}
33
42
 
34
43
  <div v-if="c.sortable">
35
- <svg v-if="ascArr.includes(c.name) || descArr.includes(c.name)" class="w-3 h-3 ms-1.5"
36
- fill='currentColor'
37
- :class="{'rotate-180':descArr.includes(c.name)}" viewBox="0 0 24 24">
38
- <path
39
- d="M8.574 11.024h6.852a2.075 2.075 0 0 0 1.847-1.086 1.9 1.9 0 0 0-.11-1.986L13.736 2.9a2.122 2.122 0 0 0-3.472 0L6.837 7.952a1.9 1.9 0 0 0-.11 1.986 2.074 2.074 0 0 0 1.847 0z"/>
44
+ <svg
45
+ v-if="ascArr.includes(c.name) || descArr.includes(c.name)" class="w-3 h-3 ms-1.5"
46
+ fill='currentColor'
47
+ :class="{'rotate-180':descArr.includes(c.name)}" viewBox="0 0 24 24"
48
+ >
49
+ <path d="M8.574 11.024h6.852a2.075 2.075 0 0 0 1.847-1.086 1.9 1.9 0 0 0-.11-1.986L13.736 2.9a2.122 2.122 0 0 0-3.472 0L6.837 7.952a1.9 1.9 0 0 0-.11 1.986 2.074 2.074 0 0 0 1.847 0z"/>
40
50
  </svg>
41
- <svg v-else class="w-3 h-3 ms-1.5 opacity-30" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
42
- fill='currentColor'
43
- viewBox="0 0 24 24">
44
- <path
45
- d="M8.574 11.024h6.852a2.075 2.075 0 0 0 1.847-1.086 1.9 1.9 0 0 0-.11-1.986L13.736 2.9a2.122 2.122 0 0 0-3.472 0L6.837 7.952a1.9 1.9 0 0 0-.11 1.986 2.074 2.074 0 0 0 1.847 1.086Zm6.852 1.952H8.574a2.072 2.072 0 0 0-1.847 1.087 1.9 1.9 0 0 0 .11 1.985l3.426 5.05a2.123 2.123 0 0 0 3.472 0l3.427-5.05a1.9 1.9 0 0 0 .11-1.985 2.074 2.074 0 0 0-1.846-1.087Z"/>
51
+ <svg
52
+ v-else class="w-3 h-3 ms-1.5 opacity-30" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
53
+ fill='currentColor'
54
+ viewBox="0 0 24 24"
55
+ >
56
+ <path d="M8.574 11.024h6.852a2.075 2.075 0 0 0 1.847-1.086 1.9 1.9 0 0 0-.11-1.986L13.736 2.9a2.122 2.122 0 0 0-3.472 0L6.837 7.952a1.9 1.9 0 0 0-.11 1.986 2.074 2.074 0 0 0 1.847 1.086Zm6.852 1.952H8.574a2.072 2.072 0 0 0-1.847 1.087 1.9 1.9 0 0 0 .11 1.985l3.426 5.05a2.123 2.123 0 0 0 3.472 0l3.427-5.05a1.9 1.9 0 0 0 .11-1.985 2.074 2.074 0 0 0-1.846-1.087Z"/>
46
57
  </svg>
47
58
  </div>
59
+
48
60
  <span
49
61
  class="bg-red-100 text-red-800 text-xs font-medium me-1 px-1 py-0.5 rounded dark:bg-gray-700 dark:text-red-400 border border-red-400"
50
- v-if="sort.findIndex((s: any) => s.field === c.name) !== -1 && sort?.length > 1">
62
+ v-if="sort.findIndex((s: any) => s.field === c.name) !== -1 && sort?.length > 1"
63
+ >
51
64
  {{ sort.findIndex((s: any) => s.field === c.name) + 1 }}
52
65
  </span>
53
66
 
@@ -64,7 +77,7 @@
64
77
  <!-- table header end -->
65
78
  <SkeleteLoader
66
79
  v-if="!rows"
67
- :columns="resource?.columns.filter((c: AdminForthResourceColumnInputCommon) => c.showIn?.list).length + 2"
80
+ :columns="resource?.columns.filter((c: AdminForthResourceColumnCommon) => c.showIn?.list).length + 2"
68
81
  :rows="rowHeights.length || 3"
69
82
  :row-heights="rowHeights"
70
83
  :column-widths="columnWidths"
@@ -72,40 +85,45 @@
72
85
 
73
86
  <tr v-else-if="rows.length === 0" class="bg-lightListTable dark:bg-darkListTable dark:border-darkListTableBorder">
74
87
  <td :colspan="resource?.columns.length + 2">
75
-
76
- <div id="toast-simple"
77
- class=" mx-auto my-5 flex items-center w-full max-w-xs p-4 space-x-4 rtl:space-x-reverse text-gray-500 divide-x rtl:divide-x-reverse divide-gray-200 dark:text-gray-400 dark:divide-gray-700 space-x dark:bg-gray-800"
78
- role="alert">
88
+ <div
89
+ id="toast-simple"
90
+ class=" mx-auto my-5 flex items-center w-full max-w-xs p-4 space-x-4 rtl:space-x-reverse text-gray-500 divide-x rtl:divide-x-reverse divide-gray-200 dark:text-gray-400 dark:divide-gray-700 space-x dark:bg-gray-800"
91
+ role="alert"
92
+ >
79
93
  <IconInboxOutline class="w-6 h-6 text-gray-500 dark:text-gray-400"/>
80
94
  <div class="ps-4 text-sm font-normal">{{ $t('No items here yet') }}</div>
81
95
  </div>
82
-
83
96
  </td>
84
97
  </tr>
85
98
 
86
- <component
87
- v-else
88
- v-for="(row, rowI) in rows"
89
- :is="tableRowReplaceInjection ? getCustomComponent(tableRowReplaceInjection) : 'tr'"
90
- :key="`row_${row._primaryKeyValue}`"
91
- :record="row"
92
- :resource="resource"
93
- :adminUser="coreStore.adminUser"
94
- :meta="tableRowReplaceInjection ? tableRowReplaceInjection.meta : undefined"
95
- @click="onClick($event, row)"
96
- ref="rowRefs"
97
- class="list-table-body-row bg-lightListTable dark:bg-darkListTable border-lightListBorder dark:border-gray-700 hover:bg-lightListTableRowHover dark:hover:bg-darkListTableRowHover"
98
- :class="{'border-b': rowI !== rows.length - 1, 'cursor-pointer': row._clickUrl !== null}"
99
- >
100
- <td class="w-4 p-4 cursor-default sticky-column bg-lightListTable dark:bg-darkListTable" @click="(e)=>e.stopPropagation()">
101
- <Checkbox
102
- :model-value="checkboxesInternal.includes(row._primaryKeyValue)"
103
- @change="(e: any)=>{addToCheckedValues(row._primaryKeyValue)}"
104
- @click="(e: any)=>e.stopPropagation()"
105
- >
106
- <span class="sr-only">{{ $t('checkbox') }}</span>
107
- </Checkbox>
108
- </td>
99
+ <!-- Top spacer(virtual scroll) -->
100
+ <tr v-if="isVirtualScrollEnabled && spacerHeight > 0">
101
+ <td :colspan="resource?.columns.length + 2" :style="{ height: `${spacerHeight}px` }"></td>
102
+ </tr>
103
+
104
+ <component
105
+ v-for="(row, rowI) in rowsToRender"
106
+ :is="tableRowReplaceInjection ? getCustomComponent(formatComponent(tableRowReplaceInjection)) : 'tr'"
107
+ :key="`row_${row._primaryKeyValue}`"
108
+ :record="row"
109
+ :resource="resource"
110
+ :adminUser="coreStore.adminUser"
111
+ :meta="tableRowReplaceInjection ? formatComponent(tableRowReplaceInjection).meta : undefined"
112
+ @click="onClick($event, row)"
113
+ ref="rowRefs"
114
+ class="list-table-body-row bg-lightListTable dark:bg-darkListTable border-lightListBorder dark:border-gray-700 hover:bg-lightListTableRowHover dark:hover:bg-darkListTableRowHover"
115
+ :class="{'border-b': rowI !== rowsToRender.length - 1, 'cursor-pointer': row._clickUrl !== null}"
116
+ @mounted="(el: any) => updateRowHeight(`row_${row._primaryKeyValue}`, el.offsetHeight)"
117
+ >
118
+ <td class="w-4 p-4 cursor-default sticky-column bg-lightListTable dark:bg-darkListTable" @click="(e)=>e.stopPropagation()">
119
+ <Checkbox
120
+ :model-value="checkboxesInternal.includes(row._primaryKeyValue)"
121
+ @change="(e: any)=>{addToCheckedValues(row._primaryKeyValue)}"
122
+ @click="(e: any)=>e.stopPropagation()"
123
+ >
124
+ <span class="sr-only">{{ $t('checkbox') }}</span>
125
+ </Checkbox>
126
+ </td>
109
127
 
110
128
  <td v-for="c in columnsListed" class="px-2 md:px-3 lg:px-6 py-4" :class="{'sticky-column bg-lightListTable dark:bg-darkListTable': c.listSticky}">
111
129
  <!-- if c.name in listComponentsPerColumn, render it. If not, render ValueRenderer -->
@@ -130,7 +148,6 @@
130
148
  primaryKey: row._primaryKeyValue,
131
149
  }
132
150
  }"
133
-
134
151
  >
135
152
  <IconEyeSolid class="af-show-icon w-5 h-5 me-2"/>
136
153
  </RouterLink>
@@ -182,21 +199,21 @@
182
199
 
183
200
  <template v-if="resource.options?.actions">
184
201
  <Tooltip
185
- v-for="action in resource.options.actions.filter(a => a.showIn?.list || a.showIn?.listQuickIcon)"
202
+ v-for="action in resource.options.actions.filter(a => a.showIn?.list)"
186
203
  :key="action.id"
187
204
  >
188
205
  <component
189
- :is="action.customComponent ? getCustomComponent(action.customComponent) : CallActionWrapper"
190
- :meta="action.customComponent?.meta"
206
+ v-if="action.customComponent"
207
+ :is="action.customComponent ? getCustomComponent(formatComponent(action.customComponent)) : CallActionWrapper"
208
+ :meta="formatComponent(action.customComponent).meta"
191
209
  :row="row"
192
210
  :resource="resource"
193
- :adminUser="adminUser"
194
- @callAction="(payload? : Object) => startCustomAction(action.id, row, payload)"
211
+ :adminUser="coreStore.adminUser"
212
+ @callAction="(payload? : Object) => startCustomAction(action.id as string | number, row, payload)"
195
213
  >
196
214
  <button
197
215
  type="button"
198
216
  class="border border-gray-300 dark:border-gray-700 dark:border-opacity-0 border-opacity-0 hover:border-opacity-100 dark:hover:border-opacity-100 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer"
199
- :disabled="rowActionLoadingStates?.[action.id]"
200
217
  >
201
218
  <component
202
219
  v-if="action.icon"
@@ -219,12 +236,18 @@
219
236
  :deleteRecord="deleteRecord"
220
237
  :resourceId="resource.resourceId"
221
238
  :startCustomAction="startCustomAction"
222
- :customActionIconsThreeDotsMenuItems="customActionIconsThreeDotsMenuItems"
239
+ :customActionIconsThreeDotsMenuItems="customActionIconsThreeDotsMenuItems ?? []"
223
240
  />
224
241
  </div>
225
242
 
226
243
  </td>
227
- </component>
244
+ </component>
245
+ <!-- Bottom spacer(virtual scroll) -->
246
+ <tr v-if="isVirtualScrollEnabled && totalHeight > 0">
247
+ <td :colspan="resource?.columns.length + 2"
248
+ :style="{ height: `${Math.max(0, totalHeight - (endIndex + 1) * (props.itemHeight || 52.5))}px` }">
249
+ </td>
250
+ </tr>
228
251
  </tbody>
229
252
  </table>
230
253
  </div>
@@ -236,85 +259,83 @@
236
259
  <div class="af-pagination-buttons-container inline-flex "
237
260
  v-if="(rows || totalRows) && totalRows >= pageSize && totalRows > 0"
238
261
  >
239
- <!-- Buttons -->
240
- <button
241
- 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"
242
- @click="page--; pageInput = page.toString();" :disabled="page <= 1">
243
- <svg class="w-3.5 h-3.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
244
- viewBox="0 0 14 10">
245
- <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
246
- d="M13 5H1m0 0 4 4M1 5l4-4"/>
247
- </svg>
248
- <span class="hidden sm:inline">
249
- {{ $t('Prev') }}
250
- </span>
251
- </button>
252
- <button
253
- 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"
254
- @click="page = 1; pageInput = page.toString();" :disabled="page <= 1">
255
- <!-- <IconChevronDoubleLeftOutline class="w-4 h-4" /> -->
256
- 1
257
- </button>
258
- <input
259
- type="text"
260
- v-model="pageInput"
261
- :style="{ width: `${Math.max(1, pageInput.length+4)}ch` }"
262
- class="af-pagination-input min-w-10 outline-none inline-block 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"
263
- @keydown="onPageKeydown($event)"
264
- @blur="validatePageInput()"
265
- />
266
-
267
- <button
268
- 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"
269
- @click="page = totalPages; pageInput = page.toString();" :disabled="page >= totalPages">
270
- {{ totalPages }}
271
-
272
- <!-- <IconChevronDoubleRightOutline class="w-4 h-4" /> -->
273
- </button>
274
- <button
275
- 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"
276
- @click="page++; pageInput = page.toString();" :disabled="page >= totalPages">
277
- <span class="hidden sm:inline">{{ $t('Next') }}</span>
278
- <svg class="w-3.5 h-3.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
279
- viewBox="0 0 14 10">
280
- <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
281
- d="M1 5h12m0 0L9 1m4 4L9 9"/>
282
- </svg>
283
- </button>
262
+ <!-- Buttons -->
263
+ <button
264
+ 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"
265
+ @click="page--; pageInput = page.toString();"
266
+ :disabled="page <= 1"
267
+ >
268
+ <svg class="w-3.5 h-3.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 10">
269
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5H1m0 0 4 4M1 5l4-4"/>
270
+ </svg>
271
+ <span class="hidden sm:inline">
272
+ {{ $t('Prev') }}
273
+ </span>
274
+ </button>
275
+ <button
276
+ 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"
277
+ @click="page = 1;
278
+ pageInput = page.toString();"
279
+ :disabled="page <= 1"
280
+ >
281
+ 1
282
+ </button>
283
+ <input
284
+ type="text"
285
+ v-model="pageInput"
286
+ :style="{ width: `${Math.max(1, pageInput.length+4)}ch` }"
287
+ class="af-pagination-input min-w-10 outline-none inline-block 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"
288
+ @keydown="onPageKeydown($event)"
289
+ @blur="validatePageInput()"
290
+ />
291
+
292
+ <button
293
+ 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"
294
+ @click="page = totalPages; pageInput = page.toString();" :disabled="page >= totalPages">
295
+ {{ totalPages }}
296
+ </button>
297
+ <button
298
+ 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"
299
+ @click="page++; pageInput = page.toString();" :disabled="page >= totalPages">
300
+ <span class="hidden sm:inline">{{ $t('Next') }}</span>
301
+ <svg class="w-3.5 h-3.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 10">
302
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 5h12m0 0L9 1m4 4L9 9"/>
303
+ </svg>
304
+ </button>
284
305
  </div>
285
306
 
286
307
  <!-- Help text -->
287
308
  <span class="ml-4 text-sm text-lightListTablePaginationHelpText dark:text-darkListTablePaginationHelpText">
288
- <span v-if="((((page || 1) - 1) * pageSize + 1 > totalRows) && totalRows > 0)">{{ $t('Wrong Page') }} </span>
289
- <template v-else-if="resource && totalRows > 0">
290
-
291
- <span class="af-pagination-info hidden sm:inline">
292
- <i18n-t keypath="Showing {from} to {to} of {total} Entries" tag="p" >
293
- <template v-slot:from>
294
- <strong>{{ from }}</strong>
295
- </template>
296
- <template v-slot:to>
297
- <strong>{{ to }}</strong>
298
- </template>
299
- <template v-slot:total>
300
- <strong>{{ totalRows }}</strong>
301
- </template>
302
- </i18n-t>
303
- </span>
304
- <span class="sm:hidden">
305
- <i18n-t keypath="{from} - {to} of {total}" tag="p" >
306
- <template v-slot:from>
307
- <strong>{{ from }}</strong>
308
- </template>
309
- <template v-slot:to>
310
- <strong>{{ to }}</strong>
311
- </template>
312
- <template v-slot:total>
313
- <strong>{{ totalRows }}</strong>
314
- </template>
315
- </i18n-t>
316
- </span>
317
- </template>
309
+ <span v-if="((((page || 1) - 1) * pageSize + 1 > totalRows) && totalRows > 0)">{{ $t('Wrong Page') }} </span>
310
+ <template v-else-if="resource && totalRows > 0">
311
+
312
+ <span class="af-pagination-info hidden sm:inline">
313
+ <i18n-t keypath="Showing {from} to {to} of {total} Entries" tag="p" >
314
+ <template v-slot:from>
315
+ <strong>{{ from }}</strong>
316
+ </template>
317
+ <template v-slot:to>
318
+ <strong>{{ to }}</strong>
319
+ </template>
320
+ <template v-slot:total>
321
+ <strong>{{ totalRows }}</strong>
322
+ </template>
323
+ </i18n-t>
324
+ </span>
325
+ <span class="sm:hidden">
326
+ <i18n-t keypath="{from} - {to} of {total}" tag="p" >
327
+ <template v-slot:from>
328
+ <strong>{{ from }}</strong>
329
+ </template>
330
+ <template v-slot:to>
331
+ <strong>{{ to }}</strong>
332
+ </template>
333
+ <template v-slot:total>
334
+ <strong>{{ totalRows }}</strong>
335
+ </template>
336
+ </i18n-t>
337
+ </span>
338
+ </template>
318
339
  </span>
319
340
  </div>
320
341
  </template>
@@ -326,7 +347,7 @@ import { computed, onMounted, ref, watch, useTemplateRef, nextTick, type Ref } f
326
347
  import { callAdminForthApi } from '@/utils';
327
348
  import { useI18n } from 'vue-i18n';
328
349
  import ValueRenderer from '@/components/ValueRenderer.vue';
329
- import { getCustomComponent } from '@/utils';
350
+ import { getCustomComponent, formatComponent } from '@/utils';
330
351
  import { useCoreStore } from '@/stores/core';
331
352
  import { showSuccesTost, showErrorTost } from '@/composables/useFrontendApi';
332
353
  import SkeleteLoader from '@/components/SkeleteLoader.vue';
@@ -339,7 +360,7 @@ import {
339
360
  } from '@iconify-prerendered/vue-flowbite';
340
361
  import router from '@/router';
341
362
  import { Tooltip } from '@/afcl';
342
- import type { AdminForthResourceCommon, AdminForthResourceColumnInputCommon, AdminForthResourceColumnCommon, AdminForthComponentDeclaration } from '@/types/Common';
363
+ import type { AdminForthResourceCommon, AdminForthResourceColumnCommon, AdminForthComponentDeclarationFull, AdminForthComponentDeclaration } from '@/types/Common';
343
364
  import { useAdminforth } from '@/adminforth';
344
365
  import Checkbox from '@/afcl/Checkbox.vue';
345
366
  import ListActionsThreeDots from '@/components/ListActionsThreeDots.vue';
@@ -359,10 +380,23 @@ const props = defineProps<{
359
380
  noRoundings?: boolean,
360
381
  customActionsInjection?: any[],
361
382
  tableBodyStartInjection?: any[],
362
- customActionIconsThreeDotsMenuItems?: any[]
383
+ containerHeight?: number,
384
+ itemHeight?: number,
385
+ bufferSize?: number,
386
+ customActionIconsThreeDotsMenuItems?: AdminForthComponentDeclaration[]
363
387
  tableRowReplaceInjection?: AdminForthComponentDeclaration,
388
+ isVirtualScrollEnabled: boolean
364
389
  }>();
365
390
 
391
+ //select between all rows or rows, that should be rendered in virtual scroll
392
+ const rowsToRender = computed(() => {
393
+ if (!props.isVirtualScrollEnabled) {
394
+ return props.rows || [];
395
+ } else {
396
+ return visibleRows.value;
397
+ }
398
+ });
399
+
366
400
  // emits, update page
367
401
  const emits = defineEmits([
368
402
  'update:page',
@@ -380,7 +414,7 @@ const sort: Ref<Array<{field: string, direction: string}>> = ref([]);
380
414
  const showListActionsThreeDots = computed(() => {
381
415
  return props.resource?.options?.actions?.some(a => a.showIn?.listThreeDotsMenu) // show if any action is set to show in three dots menu
382
416
  || (props.customActionIconsThreeDotsMenuItems && props.customActionIconsThreeDotsMenuItems.length > 0) // or if there are custom action icons for three dots menu
383
- || !props.resource?.options.baseActionsAsQuickIcons // or if there is no baseActionsAsQuickIcons
417
+ || !props.resource?.options?.baseActionsAsQuickIcons // or if there is no baseActionsAsQuickIcons
384
418
  || (props.resource?.options.baseActionsAsQuickIcons && props.resource?.options.baseActionsAsQuickIcons.length < 3) // if there all 3 base actions are shown as quick icons - hide three dots icon
385
419
  })
386
420
 
@@ -436,7 +470,7 @@ watch(() => props.rows, (newRows) => {
436
470
  columnWidths.value = newRows || !headerRefs.value ? [] : [48, ...headerRefs.value.map((el: HTMLElement) => el.offsetWidth)];
437
471
  });
438
472
 
439
- function addToCheckedValues(id: string) {
473
+ function addToCheckedValues(id: string | number) {
440
474
  if (checkboxesInternal.value.includes(id)) {
441
475
  checkboxesInternal.value = checkboxesInternal.value.filter((item) => item !== id);
442
476
  } else {
@@ -468,7 +502,7 @@ const allFromThisPageChecked = computed(() => {
468
502
  if (!props.rows || !props.rows.length) return false;
469
503
  return props.rows.every((r) => checkboxesInternal.value.includes(r._primaryKeyValue));
470
504
  });
471
- const ascArr = computed(() => sort.value.filter((s:any) => s.direction === 'asc').map((s: any) => s.field));
505
+ const ascArr = computed(() => sort.value.filter((s: any) => s.direction === 'asc').map((s: any) => s.field));
472
506
  const descArr = computed(() => sort.value.filter((s: any) => s.direction === 'desc').map((s: any) => s.field));
473
507
 
474
508
 
@@ -487,9 +521,9 @@ function onSortButtonClick(event: any, field: string) {
487
521
  } else {
488
522
  const sortField = sort.value[sortIndex];
489
523
  if (sortField.direction === 'asc') {
490
- sort.value = sort.value.map((s: any) => s.field === field ? {field, direction: 'desc'} : s);
524
+ sort.value = sort.value.map((s) => s.field === field ? {field, direction: 'desc'} : s);
491
525
  } else {
492
- sort.value = sort.value.filter((s: any) => s.field !== field);
526
+ sort.value = sort.value.filter((s) => s.field !== field);
493
527
  }
494
528
  }
495
529
  }
@@ -575,8 +609,8 @@ async function deleteRecord(row: any) {
575
609
 
576
610
  const actionLoadingStates = ref<Record<string | number, boolean>>({});
577
611
 
578
- async function startCustomAction(actionId: string, row: any, extraData: Record<string, any> = {}) {
579
- console.log('Starting custom action', actionId, row);
612
+ async function startCustomAction(actionId: string | number, row: any, extraData: Record<string, any> = {}) {
613
+
580
614
  actionLoadingStates.value[actionId] = true;
581
615
 
582
616
  const data = await callAdminForthApi({
@@ -586,7 +620,7 @@ async function startCustomAction(actionId: string, row: any, extraData: Record<s
586
620
  resourceId: props.resource?.resourceId,
587
621
  actionId: actionId,
588
622
  recordId: row._primaryKeyValue,
589
- extra: extraData,
623
+ extra: extraData
590
624
  }
591
625
  });
592
626
 
@@ -628,6 +662,119 @@ function validatePageInput() {
628
662
  page.value = validPage;
629
663
  pageInput.value = validPage.toString();
630
664
  }
665
+ /*
666
+ *___________________________________________________________________
667
+ * |
668
+ * Virtual Scroll Implementation |
669
+ *___________________________________________________________________|
670
+ */
671
+ // Add throttle utility
672
+ const throttle = (fn: Function, delay: number) => {
673
+ let lastCall = 0;
674
+ return (...args: any[]) => {
675
+ const now = Date.now();
676
+ if (now - lastCall >= delay) {
677
+ lastCall = now;
678
+ fn(...args);
679
+ }
680
+ };
681
+ };
682
+ // Virtual scroll state
683
+ const containerRef = ref<HTMLElement | null>(null);
684
+ const scrollTop = ref(0);
685
+ const visibleRows = ref<any[]>([]);
686
+ const startIndex = ref(0);
687
+ const endIndex = ref(0);
688
+ const totalHeight = ref(0);
689
+ const spacerHeight = ref(0);
690
+ const rowHeightsMap = ref<{[key: string]: number}>({});
691
+ const rowPositions = ref<number[]>([]);
692
+ // Calculate row positions based on heights
693
+ const calculateRowPositions = () => {
694
+ if (!props.rows) return;
695
+
696
+ let currentPosition = 0;
697
+ rowPositions.value = props.rows.map((row) => {
698
+ const height = rowHeightsMap.value[`row_${row._primaryKeyValue}`] || props.itemHeight || 52.5;
699
+ const position = currentPosition;
700
+ currentPosition += height;
701
+ return position;
702
+ });
703
+ totalHeight.value = currentPosition;
704
+ };
705
+ // Calculate visible rows based on scroll position
706
+ const calculateVisibleRows = () => {
707
+ if (!props.rows?.length) {
708
+ visibleRows.value = props.rows || [];
709
+ return;
710
+ }
711
+ const buffer = props.bufferSize || 5;
712
+ const containerHeight = props.containerHeight || 900;
713
+
714
+ // For single item or small datasets, show all rows
715
+ if (props.rows.length <= buffer * 2 + 1) {
716
+ startIndex.value = 0;
717
+ endIndex.value = props.rows.length - 1;
718
+ visibleRows.value = props.rows;
719
+ spacerHeight.value = 0;
720
+ return;
721
+ }
722
+
723
+ // Binary search for start index
724
+ let low = 0;
725
+ let high = rowPositions.value.length - 1;
726
+ const targetPosition = scrollTop.value;
727
+
728
+ while (low <= high) {
729
+ const mid = Math.floor((low + high) / 2);
730
+ if (rowPositions.value[mid] <= targetPosition) {
731
+ low = mid + 1;
732
+ } else {
733
+ high = mid - 1;
734
+ }
735
+ }
736
+
737
+ const newStartIndex = Math.max(0, low - 1 - buffer);
738
+ const newEndIndex = Math.min(
739
+ props.rows.length - 1,
740
+ newStartIndex + Math.ceil(containerHeight / (props.itemHeight || 52.5)) + buffer * 2
741
+ );
742
+ // Ensure at least one row is visible
743
+ if (newEndIndex < newStartIndex) {
744
+ startIndex.value = 0;
745
+ endIndex.value = Math.min(props.rows.length - 1, Math.ceil(containerHeight / (props.itemHeight || 52.5)));
746
+ } else {
747
+ startIndex.value = newStartIndex;
748
+ endIndex.value = newEndIndex;
749
+ }
750
+
751
+ visibleRows.value = props.rows.slice(startIndex.value, endIndex.value + 1);
752
+ spacerHeight.value = startIndex.value > 0 ? rowPositions.value[startIndex.value - 1] : 0;
753
+ };
754
+ // Throttled scroll handler
755
+ const handleScroll = throttle((e: Event) => {
756
+ if (!props.isVirtualScrollEnabled) return;
757
+ const target = e.target as HTMLElement;
758
+ scrollTop.value = target.scrollTop;
759
+ calculateVisibleRows();
760
+ }, 16);
761
+ // Update row height when it changes
762
+ const updateRowHeight = (rowId: string, height: number) => {
763
+ if (!props.isVirtualScrollEnabled) return;
764
+ if (rowHeightsMap.value[rowId] !== height) {
765
+ rowHeightsMap.value[rowId] = height;
766
+ calculateRowPositions();
767
+ calculateVisibleRows();
768
+ }
769
+ };
770
+ // Watch for changes in rows
771
+ watch(() => props.rows, () => {
772
+ if (props.rows) {
773
+ calculateRowPositions();
774
+ calculateVisibleRows();
775
+ }
776
+ }, { immediate: true });
777
+
631
778
 
632
779
  </script>
633
780
 
@@ -9,7 +9,8 @@
9
9
  '-translate-x-full': !sideBarOpen,
10
10
  'transform-none': sideBarOpen,
11
11
  'sidebar-collapsed': iconOnlySidebarEnabled && isSidebarIconOnly && !isSidebarHovering,
12
- 'sidebar-expanded': !iconOnlySidebarEnabled || !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering)
12
+ 'sidebar-expanded': !iconOnlySidebarEnabled || !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering),
13
+ 'sidebar-floating': isSidebarIconOnly && isSidebarHovering
13
14
  }"
14
15
  aria-label="Sidebar"
15
16
  >
@@ -51,7 +52,7 @@
51
52
  </div>
52
53
  </div>
53
54
 
54
- <div v-if="coreStore.config.defaultUserExists && !isLocalhost" class="p-4 mb-4 text-white rounded-lg bg-red-700/80 fill-white text-sm">
55
+ <div v-if="coreStore?.config?.defaultUserExists && !isLocalhost" class="p-4 mb-4 text-white rounded-lg bg-red-700/80 fill-white text-sm">
55
56
  <IconExclamationCircleOutline class="inline-block align-text-bottom mr-0,5 w-5 h-5" />
56
57
  Default user <strong>"adminforth"</strong> detected. Delete it and create your own account.
57
58
  </div>
@@ -217,6 +218,9 @@
217
218
 
218
219
  .sidebar-expanded {
219
220
  width: v-bind(expandedWidth); /* Expanded width (w-64) */
221
+ }
222
+
223
+ .sidebar-floating {
220
224
  box-shadow: 3px 0px 12px -2px rgba(0, 0, 0, 0.15);
221
225
  }
222
226