adminforth 2.7.19 → 2.8.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 (107) hide show
  1. package/commands/createApp/templates/index.ts.hbs +2 -1
  2. package/commands/createCustomComponent/main.js +1 -0
  3. package/commands/createCustomComponent/templates/customCrud/beforeActionButtons.vue.hbs +38 -0
  4. package/commands/createPlugin/templates/custom/tsconfig.json.hbs +2 -5
  5. package/dist/dataConnectors/baseConnector.d.ts.map +1 -1
  6. package/dist/dataConnectors/baseConnector.js +33 -15
  7. package/dist/dataConnectors/baseConnector.js.map +1 -1
  8. package/dist/dataConnectors/clickhouse.d.ts.map +1 -1
  9. package/dist/dataConnectors/clickhouse.js +15 -0
  10. package/dist/dataConnectors/clickhouse.js.map +1 -1
  11. package/dist/dataConnectors/mongo.d.ts.map +1 -1
  12. package/dist/dataConnectors/mongo.js +30 -1
  13. package/dist/dataConnectors/mongo.js.map +1 -1
  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.map +1 -1
  18. package/dist/dataConnectors/postgres.js +11 -0
  19. package/dist/dataConnectors/postgres.js.map +1 -1
  20. package/dist/dataConnectors/sqlite.d.ts.map +1 -1
  21. package/dist/dataConnectors/sqlite.js +11 -0
  22. package/dist/dataConnectors/sqlite.js.map +1 -1
  23. package/dist/modules/codeInjector.d.ts +1 -0
  24. package/dist/modules/codeInjector.d.ts.map +1 -1
  25. package/dist/modules/codeInjector.js +4 -0
  26. package/dist/modules/codeInjector.js.map +1 -1
  27. package/dist/modules/configValidator.d.ts.map +1 -1
  28. package/dist/modules/configValidator.js +6 -2
  29. package/dist/modules/configValidator.js.map +1 -1
  30. package/dist/modules/restApi.d.ts.map +1 -1
  31. package/dist/modules/restApi.js +2 -0
  32. package/dist/modules/restApi.js.map +1 -1
  33. package/dist/modules/styles.d.ts +8 -1
  34. package/dist/modules/styles.d.ts.map +1 -1
  35. package/dist/modules/styles.js +9 -2
  36. package/dist/modules/styles.js.map +1 -1
  37. package/dist/spa/src/App.vue +12 -4
  38. package/dist/spa/src/adminforth.ts +31 -11
  39. package/dist/spa/src/afcl/BarChart.vue +2 -2
  40. package/dist/spa/src/afcl/Checkbox.vue +2 -2
  41. package/dist/spa/src/afcl/Dialog.vue +38 -21
  42. package/dist/spa/src/afcl/Dropzone.vue +2 -2
  43. package/dist/spa/src/afcl/Input.vue +1 -1
  44. package/dist/spa/src/afcl/LinkButton.vue +1 -1
  45. package/dist/spa/src/afcl/PieChart.vue +5 -5
  46. package/dist/spa/src/afcl/Select.vue +17 -12
  47. package/dist/spa/src/afcl/Table.vue +202 -72
  48. package/dist/spa/src/afcl/Textarea.vue +31 -0
  49. package/dist/spa/src/afcl/Toggle.vue +2 -2
  50. package/dist/spa/src/afcl/Tooltip.vue +0 -1
  51. package/dist/spa/src/afcl/index.ts +1 -1
  52. package/dist/spa/src/components/AcceptModal.vue +1 -1
  53. package/dist/spa/src/components/ColumnValueInput.vue +11 -11
  54. package/dist/spa/src/components/ColumnValueInputWrapper.vue +2 -2
  55. package/dist/spa/src/components/CustomRangePicker.vue +5 -5
  56. package/dist/spa/src/components/ErrorMessage.vue +21 -0
  57. package/dist/spa/src/components/Filters.vue +7 -6
  58. package/dist/spa/src/components/GroupsTable.vue +2 -1
  59. package/dist/spa/src/components/MenuLink.vue +3 -3
  60. package/dist/spa/src/components/ResourceForm.vue +35 -27
  61. package/dist/spa/src/components/ResourceListTable.vue +42 -42
  62. package/dist/spa/src/components/ResourceListTableVirtual.vue +41 -41
  63. package/dist/spa/src/components/ShowTable.vue +3 -3
  64. package/dist/spa/src/components/SkeleteLoader.vue +2 -2
  65. package/dist/spa/src/components/ThreeDotsMenu.vue +69 -10
  66. package/dist/spa/src/components/Toast.vue +25 -2
  67. package/dist/spa/src/components/ValueRenderer.vue +39 -12
  68. package/dist/spa/src/i18n.ts +1 -1
  69. package/dist/spa/src/shims-vue.d.ts +5 -0
  70. package/dist/spa/src/spa_types/core.ts +1 -1
  71. package/dist/spa/src/stores/modal.ts +6 -1
  72. package/dist/spa/src/stores/toast.ts +22 -3
  73. package/dist/spa/src/types/Back.ts +31 -6
  74. package/dist/spa/src/types/Common.ts +33 -24
  75. package/dist/spa/src/types/FrontendAPI.ts +21 -5
  76. package/dist/spa/src/types/adapters/EmailAdapter.ts +2 -4
  77. package/dist/spa/src/types/adapters/ImageVisionAdapter.ts +30 -0
  78. package/dist/spa/src/types/adapters/index.ts +1 -0
  79. package/dist/spa/src/utils.ts +8 -7
  80. package/dist/spa/src/views/CreateView.vue +15 -16
  81. package/dist/spa/src/views/EditView.vue +23 -17
  82. package/dist/spa/src/views/ListView.vue +116 -66
  83. package/dist/spa/src/views/LoginView.vue +2 -9
  84. package/dist/spa/src/views/ResourceParent.vue +1 -1
  85. package/dist/spa/src/views/ShowView.vue +59 -39
  86. package/dist/spa/src/websocket.ts +6 -1
  87. package/dist/spa/tsconfig.app.json +1 -1
  88. package/dist/spa/vite.config.ts +45 -2
  89. package/dist/types/Back.d.ts +16 -1
  90. package/dist/types/Back.d.ts.map +1 -1
  91. package/dist/types/Back.js +15 -0
  92. package/dist/types/Back.js.map +1 -1
  93. package/dist/types/Common.d.ts +27 -22
  94. package/dist/types/Common.d.ts.map +1 -1
  95. package/dist/types/Common.js.map +1 -1
  96. package/dist/types/FrontendAPI.d.ts +21 -3
  97. package/dist/types/FrontendAPI.d.ts.map +1 -1
  98. package/dist/types/FrontendAPI.js.map +1 -1
  99. package/dist/types/adapters/EmailAdapter.d.ts +2 -3
  100. package/dist/types/adapters/EmailAdapter.d.ts.map +1 -1
  101. package/dist/types/adapters/ImageVisionAdapter.d.ts +25 -0
  102. package/dist/types/adapters/ImageVisionAdapter.d.ts.map +1 -0
  103. package/dist/types/adapters/ImageVisionAdapter.js +2 -0
  104. package/dist/types/adapters/ImageVisionAdapter.js.map +1 -0
  105. package/dist/types/adapters/index.d.ts +1 -0
  106. package/dist/types/adapters/index.d.ts.map +1 -1
  107. package/package.json +2 -1
@@ -1,79 +1,135 @@
1
1
  <template>
2
-
3
- <div class="afcl-table-container relative overflow-x-auto shadow-md sm:rounded-lg">
4
- <table class="afcl-table w-full text-sm text-left rtl:text-right text-lightTableText dark:text-darkTableText">
5
- <thead class="afcl-table-thread text-xs text-lightTableHeadingText uppercase bg-lightTableHeadingBackground dark:bg-darkTableHeadingBackground dark:text-darkTableHeadingText">
6
- <tr>
7
- <th scope="col" class="px-6 py-3"
8
- v-for="column in columns"
9
- >
10
- <slot v-if="$slots[`header:${column.fieldName}`]" :name="`header:${column.fieldName}`" :column="column" />
11
-
12
- <span v-else>
13
- {{ column.label }}
14
- </span>
15
- </th>
16
- </tr>
17
- </thead>
18
- <tbody>
19
- <tr
20
- v-for="(item, index) in dataPage"
21
- :class="{
22
- 'afcl-table-body odd:bg-lightTableOddBackground odd:dark:bg-darkTableOddBackground even:bg-lightTableEvenBackground even:dark:bg-darkTableEvenBackground': evenHighlights,
23
- 'border-b border-lightTableBorder dark:border-darkTableBorder': index !== dataPage.length - 1 || totalPages > 1,
24
- }"
25
- >
26
- <td class="px-6 py-4"
27
- v-for="column in props.columns"
28
- >
29
- <slot v-if="$slots[`cell:${column.fieldName}`]"
30
- :name="`cell:${column.fieldName}`"
31
- :item="item" :column="column"
2
+ <div class="afcl-table-container relative overflow-x-auto shadow-md sm:rounded-lg">
3
+ <div class="overflow-x-auto w-full">
4
+ <table class="afcl-table w-full text-sm text-left rtl:text-right text-lightTableText dark:text-darkTableText overflow-x-auto">
5
+ <thead class="afcl-table-thread text-xs text-lightTableHeadingText uppercase bg-lightTableHeadingBackground dark:bg-darkTableHeadingBackground dark:text-darkTableHeadingText">
6
+ <tr>
7
+ <th scope="col" class="px-6 py-3" ref="headerRefs" :key="`header-${column.fieldName}`"
8
+ v-for="column in columns"
9
+ >
10
+ <slot v-if="$slots[`header:${column.fieldName}`]" :name="`header:${column.fieldName}`" :column="column" />
11
+
12
+ <span v-else>
13
+ {{ column.label }}
14
+ </span>
15
+ </th>
16
+ </tr>
17
+ </thead>
18
+ <tbody>
19
+ <SkeleteLoader
20
+ v-if="isLoading || props.isLoading"
21
+ :rows="pageSize"
22
+ :columns="columns.length"
23
+ :row-heights="rowHeights"
24
+ :column-widths="columnWidths"
25
+ />
26
+ <tr v-else-if="!isLoading && !props.isLoading && dataPage.length === 0" class="afcl-table-empty-body">
27
+ <td :colspan="columns.length" class="px-6 py-12 text-center">
28
+ <div class="flex flex-col items-center justify-center text-lightTableText dark:text-darkTableText">
29
+ <IconTableRowOutline class="w-10 h-10 mb-4 text-gray-400 dark:text-gray-500" />
30
+ <p class="text-md">{{ $t('No data available') }}</p>
31
+ </div>
32
+ </td>
33
+ </tr>
34
+ <tr
35
+ v-else="!isLoading && !props.isLoading"
36
+ v-for="(item, index) in dataPage"
37
+ :key="`row-${index}`"
38
+ ref="rowRefs"
39
+ :class="{
40
+ 'afcl-table-body odd:bg-lightTableOddBackground odd:dark:bg-darkTableOddBackground even:bg-lightTableEvenBackground even:dark:bg-darkTableEvenBackground': evenHighlights,
41
+ 'border-b border-lightTableBorder dark:border-darkTableBorder': index !== dataPage.length - 1 || totalPages > 1,
42
+ }"
32
43
  >
33
- </slot>
34
- <span v-else>
35
- {{ item[column.fieldName] }}
36
- </span>
37
- </td>
38
- </tr>
39
- </tbody>
40
- </table>
41
- <nav class="afcl-table-pagination-container bg-lightTableBackground dark:bg-darkTableBackground flex items-center flex-column flex-wrap md:flex-row justify-between p-4"
42
- v-if="totalPages > 1"
43
- :aria-label="$t('Table navigation')">
44
+ <td class="px-6 py-4" :key="`cell-${index}-${column.fieldName}`"
45
+ v-for="column in props.columns"
46
+ >
47
+ <slot v-if="$slots[`cell:${column.fieldName}`]"
48
+ :name="`cell:${column.fieldName}`"
49
+ :item="item" :column="column"
50
+ >
51
+ </slot>
52
+ <span v-else-if="!isLoading || props.isLoading" >
53
+ {{ item[column.fieldName] }}
54
+ </span>
55
+ <div v-else>
56
+ <div class=" w-full">
57
+ <Skeleton class="h-4" />
58
+ </div>
59
+ </div>
60
+ </td>
61
+ </tr>
62
+ </tbody>
63
+ </table>
64
+ </div>
65
+ <nav class="afcl-table-pagination-container bg-lightTableBackground dark:bg-darkTableBackground mt-2 flex flex-col gap-2 items-center sm:flex-row justify-center sm:justify-between px-4 pb-4"
66
+ v-if="totalPages > 1"
67
+ :aria-label="$t('Table navigation')">
44
68
  <i18n-t
45
- keypath="Showing {from} to {to} of {total}" tag="span" class="afcl-table-pagination-text text-sm font-normal text-lightTablePaginationText dark:text-darkTablePaginationText mb-4 md:mb-0 block w-full md:inline md:w-auto"
69
+ keypath="Showing {from} to {to} of {total}" tag="span" class="afcl-table-pagination-text text-sm font-normal text-center text-lightTablePaginationText dark:text-darkTablePaginationText sm:mb-4 md:mb-0 block w-full md:inline md:w-auto"
46
70
  >
47
- <template #from><span class="font-semibold text-lightTablePaginationNumeration dark:text-darkTablePaginationNumeration">{{ Math.min((currentPage - 1) * props.pageSize + 1, props.data.length) }}</span></template>
48
- <template #to><span class="font-semibold text-lightTablePaginationNumeration dark:text-darkTablePaginationNumeration">{{ Math.min(currentPage * props.pageSize, props.data.length) }}</span></template>
49
- <template #total><span class="font-semibold text-lightTablePaginationNumeration dark:text-darkTablePaginationNumeration">{{ props.data.length }}</span></template>
71
+ <template #from><span class="font-semibold text-lightTablePaginationNumeration dark:text-darkTablePaginationNumeration">{{ Math.min((currentPage - 1) * props.pageSize + 1, dataResult.total) }}</span></template>
72
+ <template #to><span class="font-semibold text-lightTablePaginationNumeration dark:text-darkTablePaginationNumeration">{{ Math.min(currentPage * props.pageSize, dataResult.total) }}</span></template>
73
+ <template #total><span class="font-semibold text-lightTablePaginationNumeration dark:text-darkTablePaginationNumeration">{{ dataResult.total }}</span></template>
50
74
  </i18n-t>
75
+ <div class="af-pagination-container flex flex-row items-center xs:flex-row xs:justify-between xs:items-center gap-3">
76
+ <div class="inline-flex">
77
+ <!-- Buttons -->
78
+ <button
79
+ class="flex items-center py-1 px-3 gap-1 text-sm font-medium text-lightActivePaginationButtonText bg-lightActivePaginationButtonBackground border-r-0 rounded-s hover:opacity-90 dark:bg-darkActivePaginationButtonBackground dark:text-darkActivePaginationButtonText disabled:opacity-50"
80
+ @click="currentPage--; pageInput = currentPage.toString();" :disabled="currentPage <= 1">
81
+ <svg class="w-3.5 h-3.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
82
+ viewBox="0 0 14 10">
83
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
84
+ d="M13 5H1m0 0 4 4M1 5l4-4"/>
85
+ </svg>
86
+ </button>
87
+ <button
88
+ class="flex items-center py-1 px-3 text-sm font-medium text-lightUnactivePaginationButtonText focus:outline-none bg-lightUnactivePaginationButtonBackground border-r-0 border border-lightUnactivePaginationButtonBorder hover:bg-lightUnactivePaginationButtonHoverBackground hover:text-lightUnactivePaginationButtonHoverText dark:bg-darkUnactivePaginationButtonBackground dark:text-darkUnactivePaginationButtonText dark:border-darkUnactivePaginationButtonBorder dark:hover:text-darkUnactivePaginationButtonHoverText dark:hover:bg-darkUnactivePaginationButtonHoverBackground disabled:opacity-50"
89
+ @click="switchPage(1); pageInput = currentPage.toString();" :disabled="currentPage <= 1">
90
+ <!-- <IconChevronDoubleLeftOutline class="w-4 h-4" /> -->
91
+ 1
92
+ </button>
93
+ <div
94
+ contenteditable="true"
95
+ class="min-w-10 outline-none inline-block w-auto py-1.5 px-3 text-sm text-center text-lightTablePaginationInputText border border-lightTablePaginationInputBorder bg-lightTablePaginationInputBackground dark:border-darkTablePaginationInputBorder dark:text-darkTablePaginationInputText dark:bg-darkTablePaginationInputBackground z-10"
96
+ @keydown="onPageKeydown($event)"
97
+ @input="onPageInput($event)"
98
+ @blur="validatePageInput()"
99
+ >
100
+ {{ pageInput }}
101
+ </div>
51
102
 
52
- <ul class="afcl-table-pagination-list inline-flex -space-x-px rtl:space-x-reverse text-sm h-8">
53
- <li v-for="page in totalPages" :key="page">
54
- <a href="#"
55
- @click.prevent="switchPage(page)"
56
- :aria-current="page === page ? 'page' : undefined"
57
- :class='{
58
- "afcl-table-pagination-button text-blue-600 bg-lightActivePaginationButtonBackground text-lightActivePaginationButtonText dark:bg-darkActivePaginationButtonBackground dark:text-darkActivePaginationButtonText hover:opacity-90": page === currentPage,
59
- "text-lightUnactivePaginationButtonText border bg-lightUnactivePaginationButtonBackground border-lightUnactivePaginationButtonBorder hover:bg-lightUnactivePaginationButtonHoverBackground hover:text-lightUnactivePaginationButtonHoverText dark:bg-darkUnactivePaginationButtonBackground dark:border-darkUnactivePaginationButtonBorder dark:text-darkUnactivePaginationButtonText dark:hover:bg-darkUnactivePaginationButtonHoverBackground dark:hover:text-darkUnactivePaginationButtonHoverText": page !== currentPage,
60
- "rounded-s-lg ms-0": page === 1,
61
- "rounded-e-lg": page === totalPages,
62
- }'
63
- class="flex items-center justify-center px-3 h-8 leading-tight ">
64
- {{ page }}
65
- </a>
66
- </li>
67
- </ul>
68
- </nav>
69
- </div>
70
-
103
+ <button
104
+ class="flex items-center py-1 px-3 text-sm font-medium text-lightUnactivePaginationButtonText focus:outline-none bg-lightUnactivePaginationButtonBackground border-l-0 border border-lightUnactivePaginationButtonBorder hover:bg-lightUnactivePaginationButtonHoverBackground hover:text-lightUnactivePaginationButtonHoverText dark:bg-darkUnactivePaginationButtonBackground dark:text-darkUnactivePaginationButtonText dark:border-darkUnactivePaginationButtonBorder dark:hover:text-darkUnactivePaginationButtonHoverText dark:hover:bg-darkUnactivePaginationButtonHoverBackground disabled:opacity-50"
105
+ @click="currentPage = totalPages; pageInput = currentPage.toString();" :disabled="currentPage >= totalPages">
106
+ {{ totalPages }}
71
107
 
72
-
108
+ <!-- <IconChevronDoubleRightOutline class="w-4 h-4" /> -->
109
+ </button>
110
+ <button
111
+ class="flex items-center py-1 px-3 gap-1 text-sm font-medium text-lightActivePaginationButtonText focus:outline-none bg-lightActivePaginationButtonBackground border-l-0 rounded-e hover:opacity-90 dark:bg-darkActivePaginationButtonBackground dark:text-darkActivePaginationButtonText disabled:opacity-50"
112
+ @click="currentPage++; pageInput = currentPage.toString();" :disabled="currentPage >= totalPages">
113
+ <svg class="w-3.5 h-3.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
114
+ viewBox="0 0 14 10">
115
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
116
+ d="M1 5h12m0 0L9 1m4 4L9 9"/>
117
+ </svg>
118
+ </button>
119
+ </div>
120
+ </div>
121
+ </nav>
122
+ </div>
73
123
  </template>
74
124
 
75
125
  <script setup lang="ts">
76
- import { ref, type Ref, computed } from 'vue';
126
+ import { ref, computed, useTemplateRef, watch, onMounted } from 'vue';
127
+ import SkeleteLoader from '@/components/SkeleteLoader.vue';
128
+ import { IconTableRowOutline } from '@iconify-prerendered/vue-flowbite';
129
+
130
+ defineExpose({
131
+ refreshTable
132
+ })
77
133
 
78
134
  const props = withDefaults(
79
135
  defineProps<{
@@ -83,34 +139,108 @@
83
139
  }[],
84
140
  data: {
85
141
  [key: string]: any,
86
- }[],
142
+ }[] | ((params: { offset: number, limit: number }) => Promise<{data: {[key: string]: any}[], total: number}>),
87
143
  evenHighlights?: boolean,
88
144
  pageSize?: number,
145
+ isLoading?: boolean,
89
146
  }>(), {
90
147
  evenHighlights: true,
91
- pageSize: 10,
148
+ pageSize: 5,
92
149
  }
93
150
  );
94
151
 
152
+ const pageInput = ref('1');
153
+ const rowRefs = useTemplateRef<HTMLElement[]>('rowRefs');
154
+ const headerRefs = useTemplateRef<HTMLElement[]>('headerRefs');
155
+ const rowHeights = ref<number[]>([]);
156
+ const columnWidths = ref<number[]>([]);
95
157
  const currentPage = ref(1);
158
+ const isLoading = ref(false);
159
+ const dataResult = ref<{data: {[key: string]: any}[], total: number}>({data: [], total: 0});
160
+ const isAtLeastOneLoading = ref<boolean[]>([false]);
161
+
162
+ onMounted(() => {
163
+ refresh();
164
+ });
165
+
166
+ watch([currentPage, () => props.data], async () => {
167
+ refresh();
168
+ });
169
+
170
+ watch(() => currentPage.value, () => {
171
+ rowHeights.value = !rowRefs.value ? [] : rowRefs.value.map((el: HTMLElement) => el.offsetHeight);
172
+ columnWidths.value = !headerRefs.value ? [] : headerRefs.value.map((el: HTMLElement) => el.offsetWidth);
173
+ });
96
174
 
97
175
  const totalPages = computed(() => {
98
- return Math.ceil(props.data.length / props.pageSize);
176
+ return dataResult.value?.total ? Math.ceil(dataResult.value.total / props.pageSize) : 1;
99
177
  });
100
178
 
101
179
  const dataPage = computed(() => {
102
- const start = (currentPage.value - 1) * props.pageSize;
103
- const end = start + props.pageSize;
104
- return props.data.slice(start, end);
180
+ return dataResult.value?.data;
105
181
  });
106
182
 
107
183
  function switchPage(p: number) {
108
184
  currentPage.value = p;
185
+ pageInput.value = p.toString();
109
186
  }
110
187
 
111
188
  const emites = defineEmits([
112
189
  'update:activeTab',
113
190
  ]);
114
191
 
192
+ function onPageInput(event: any) {
193
+ pageInput.value = event.target.innerText;
194
+ }
195
+
196
+ function validatePageInput() {
197
+ const newPage = parseInt(pageInput.value) || 1;
198
+ const validPage = Math.max(1, Math.min(newPage, totalPages.value));
199
+ currentPage.value = validPage;
200
+ pageInput.value = validPage.toString();
201
+ }
202
+
203
+ watch(() => currentPage.value, (newPage) => {
204
+ pageInput.value = newPage.toString();
205
+ });
206
+
207
+ async function onPageKeydown(event: any) {
208
+ // page input should accept only numbers, arrow keys and backspace
209
+ if (['Enter', 'Space'].includes(event.code) ||
210
+ (!['Backspace', 'ArrowRight', 'ArrowLeft'].includes(event.code)
211
+ && isNaN(Number(String.fromCharCode(event.keyCode || 0))))) {
212
+ event.preventDefault();
213
+ if (event.code === 'Enter') {
214
+ validatePageInput();
215
+ event.target.blur();
216
+ }
217
+ }
218
+ }
219
+
220
+ async function refresh() {
221
+ if (typeof props.data === 'function') {
222
+ isLoading.value = true;
223
+ const currentLoadingIndex = currentPage.value;
224
+ isAtLeastOneLoading.value[currentLoadingIndex] = true;
225
+ const result = await props.data({ offset: (currentLoadingIndex - 1) * props.pageSize, limit: props.pageSize });
226
+ isAtLeastOneLoading.value[currentLoadingIndex] = false;
227
+ if (isAtLeastOneLoading.value.every(v => v === false)) {
228
+ isLoading.value = false;
229
+ }
230
+ dataResult.value = result;
231
+ } else {
232
+ const start = (currentPage.value - 1) * props.pageSize;
233
+ const end = start + props.pageSize;
234
+ dataResult.value = { data: props.data.slice(start, end), total: props.data.length };
235
+ }
236
+ }
237
+
238
+ function refreshTable() {
239
+ if ( currentPage.value !== 1 ) {
240
+ currentPage.value = 1;
241
+ } else {
242
+ refresh();
243
+ }
244
+ }
115
245
 
116
246
  </script>
@@ -0,0 +1,31 @@
1
+ <template>
2
+
3
+ <textarea
4
+ ref="input"
5
+ class="bg-lightInputBackground border border-lightInputBorder text-lightInputText placeholder-lightInputPlaceholderText text-sm rounded-lg block w-full p-2.5 dark:bg-darkInputBackground dark:border-darkInputBorder dark:placeholder-darkInputPlaceholderText dark:text-darkInputText dark:border-darkInputBorder focus:ring-lightInputFocusRing focus:border-lightInputFocusBorder dark:focus:ring-darkInputFocusRing dark:focus:border-darkInputFocusBorder"
6
+ :placeholder="placeholder"
7
+ :value="modelValue"
8
+ @input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
9
+ :readonly="readonly"
10
+ />
11
+
12
+ </template>
13
+
14
+ <script setup lang="ts">
15
+
16
+ import { ref } from 'vue';
17
+
18
+ const props = defineProps<{
19
+ modelValue: string,
20
+ readonly?: boolean,
21
+ placeholder?: string,
22
+ }>()
23
+
24
+ const input = ref<HTMLInputElement | null>(null)
25
+
26
+ defineExpose({
27
+ focus: () => input.value?.focus(),
28
+ });
29
+
30
+ </script>
31
+
@@ -5,9 +5,9 @@
5
5
  value="" class="sr-only peer"
6
6
  :disabled="props.disabled"
7
7
  :checked="props.modelValue"
8
- @change="$emit('update:modelValue', $event.target.checked)"
8
+ @change="$emit('update:modelValue', ($event.target as HTMLInputElement).checked)"
9
9
  >
10
- <div class="border border-lightToggleBorderUnactive relative min-w-11 min-h-6 bg-lightToggleBgUnactive peer-focus:outline-none peer-focus:ring-4
10
+ <div class="afcl-toggle border border-lightToggleBorderUnactive relative min-w-11 min-h-6 bg-lightToggleBgUnactive peer-focus:outline-none peer-focus:ring-4
11
11
  peer-focus:ring-lightToggleRing dark:peer-focus:ring-darkToggleRing rounded-full peer dark:bg-darkToggleBgUnactive
12
12
  peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:top-[2px] peer-checked:border-none
13
13
  peer-checked:after:border-lightToggleBorderActive after:content-[''] after:absolute after:top-[1px]
@@ -10,7 +10,6 @@
10
10
  <slot name="tooltip"></slot>
11
11
  <div class="tooltip-arrow" data-popper-arrow></div>
12
12
  </div>
13
-
14
13
  </template>
15
14
 
16
15
  <script setup lang="ts">
@@ -22,4 +22,4 @@ export { default as CountryFlag } from './CountryFlag.vue';
22
22
  export { default as JsonViewer } from './JsonViewer.vue';
23
23
  export { default as Toggle } from './Toggle.vue';
24
24
  export { default as DatePicker } from './DatePicker.vue';
25
-
25
+ export { default as Textarea } from './Textarea.vue';
@@ -6,7 +6,7 @@ const modalStore = useModalStore();
6
6
 
7
7
  <template>
8
8
  <Teleport to="body">
9
- <div v-if="modalStore.isOpened" class="bg-gray-900/50 dark:bg-gray-900/80 overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-full max-h-full">
9
+ <div v-if="modalStore.isOpened" class="bg-gray-900/50 dark:bg-gray-900/80 overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-[100] justify-center items-center w-full md:inset-0 h-full max-h-full">
10
10
  <div class="relative p-4 w-full max-w-md max-h-full top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 " >
11
11
  <div class="afcl-confirmation-container relative bg-lightAcceptModalBackground rounded-lg shadow dark:bg-darkAcceptModalBackground dark:shadow-black">
12
12
  <button type="button"@click="modalStore.togleModal" class="absolute top-3 end-2.5 text-lightAcceptModalCloseIcon bg-transparent hover:bg-lightAcceptModalCloseIconHoverBackground hover:text-lightAcceptModalCloseIconHover rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:text-darkAcceptModalCloseIcon dark:hover:bg-darkAcceptModalCloseIconHoverBackground dark:hover:text-darkAcceptModalCloseIconHover" >
@@ -67,6 +67,7 @@
67
67
  step="1"
68
68
  class="w-40"
69
69
  placeholder="0"
70
+ :fullWidth="true"
70
71
  :min="![undefined, null].includes(column.minValue) ? column.minValue : ''"
71
72
  :max="![undefined, null].includes(column.maxValue) ? column.maxValue : ''"
72
73
  :prefix="column.inputPrefix"
@@ -91,6 +92,7 @@
91
92
  step="0.1"
92
93
  class="w-40"
93
94
  placeholder="0.0"
95
+ :fullWidth="true"
94
96
  :min="![undefined, null].includes(column.minValue) ? column.minValue : ''"
95
97
  :max="![undefined, null].includes(column.maxValue) ? column.maxValue : ''"
96
98
  :prefix="column.inputPrefix"
@@ -99,28 +101,25 @@
99
101
  @update:modelValue="$emit('update:modelValue', $event)"
100
102
  :readonly="(column.editReadonly && source === 'edit') || readonly"
101
103
  />
102
- <textarea
104
+ <Textarea
103
105
  v-else-if="['text', 'richtext'].includes(type || column.type)"
104
- ref="input"
105
- class="bg-lightInputBackground border border-lightInputBorder text-lightInputText placeholder-lightInputPlaceholderText text-sm rounded-lg block w-full p-2.5 dark:bg-darkInputBackground dark:border-darkInputBorder dark:placeholder-darkInputPlaceholderText dark:text-darkInputText dark:border-darkInputBorder focus:ring-lightInputFocusRing focus:border-lightInputFocusBorder dark:focus:ring-darkInputFocusRing dark:focus:border-darkInputFocusBorder"
106
106
  :placeholder="$t('Text')"
107
- :value="value"
108
- @input="$emit('update:modelValue', $event.target.value)"
107
+ :modelValue="value"
108
+ @update:modelValue="$emit('update:modelValue', $event)"
109
109
  :readonly="(column.editReadonly && source === 'edit') || readonly"
110
110
  />
111
- <textarea
111
+ <Textarea
112
112
  v-else-if="['json'].includes(type || column.type)"
113
- ref="input"
114
- class="bg-lightInputBackground border border-lightInputBorder text-lightInputText placeholder-lightInputPlaceholderText text-sm rounded-lg block w-full p-2.5 dark:bg-darkInputBackground dark:border-darkInputBorder dark:placeholder-darkInputPlaceholderText dark:text-darkInputText dark:border-darkInputBorder focus:ring-lightInputFocusRing focus:border-lightInputFocusBorder dark:focus:ring-darkInputFocusRing dark:focus:border-darkInputFocusBorder"
115
113
  :placeholder="$t('Text')"
116
- :value="value"
117
- @input="$emit('update:modelValue', $event.target.value)"
114
+ :modelValue="value"
115
+ @update:modelValue="$emit('update:modelValue', $event)"
118
116
  />
119
117
  <Input
120
118
  v-else
121
119
  ref="input"
122
120
  :type="!column.masked || unmasked[column.name] ? 'text' : 'password'"
123
121
  class="w-full"
122
+ :fullWidth="true"
124
123
  :placeholder="$t('Text')"
125
124
  :prefix="column.inputPrefix"
126
125
  :suffix="column.inputSuffix"
@@ -158,6 +157,7 @@
158
157
  import Select from '@/afcl/Select.vue';
159
158
  import Input from '@/afcl/Input.vue';
160
159
  import Spinner from '@/afcl/Spinner.vue';
160
+ import Textarea from '@/afcl/Textarea.vue';
161
161
  import { ref, inject } from 'vue';
162
162
  import { getCustomComponent } from '@/utils';
163
163
  import { useI18n } from 'vue-i18n';
@@ -191,7 +191,7 @@
191
191
  const onSearchInput = inject('onSearchInput', {} as any);
192
192
  const loadMoreOptions = inject('loadMoreOptions', (() => {}) as any);
193
193
 
194
- const input = ref(null);
194
+ const input = ref<HTMLInputElement | null>(null);
195
195
 
196
196
  const getBooleanOptions = (column: any) => {
197
197
  const options: Array<{ label: string; value: boolean | null }> = [
@@ -19,7 +19,7 @@
19
19
  @update:unmasked="$emit('update:unmasked', column.name)"
20
20
  @update:inValidity="$emit('update:inValidity', { name: column.name, value: $event })"
21
21
  @update:emptiness="$emit('update:emptiness', { name: column.name, value: $event })"
22
- @delete="setCurrentValue(column.name, currentValues[column.name].filter((_, index) => index !== arrayItemIndex))"
22
+ @delete="setCurrentValue(column.name, currentValues[column.name].filter((_: any, index: any) => index !== arrayItemIndex))"
23
23
  />
24
24
  </div>
25
25
  <div class="flex items-center">
@@ -71,7 +71,7 @@
71
71
 
72
72
  const emit = defineEmits(['update:unmasked', 'update:inValidity', 'update:emptiness', 'focus-last-input']);
73
73
 
74
- const arrayItemRefs = ref([]);
74
+ const arrayItemRefs = ref<HTMLInputElement[]>([]);
75
75
 
76
76
  async function addArrayItem() {
77
77
  props.setCurrentValue(props.column.name, props.currentValues[props.column.name], props.currentValues[props.column.name].length);
@@ -57,15 +57,15 @@ const props = defineProps({
57
57
 
58
58
  const emit = defineEmits(['update:valueStart', 'update:valueEnd']);
59
59
 
60
- const minFormatted = computed(() => Math.floor(props.min));
61
- const maxFormatted = computed(() => Math.ceil(props.max));
60
+ const minFormatted = computed(() => Math.floor(<number>props.min));
61
+ const maxFormatted = computed(() => Math.ceil(<number>props.max));
62
62
 
63
63
  const isChanged = computed(() => {
64
64
  return start.value && start.value !== minFormatted.value || end.value && end.value !== maxFormatted.value;
65
65
  });
66
66
 
67
- const start = ref(props.valueStart);
68
- const end = ref(props.valueEnd);
67
+ const start = ref<string | number>(props.valueStart);
68
+ const end = ref<string | number>(props.valueEnd);
69
69
 
70
70
  const sliderValue = ref([minFormatted.value, maxFormatted.value]);
71
71
 
@@ -118,7 +118,7 @@ const clear = () => {
118
118
  setSliderValues('', '')
119
119
  }
120
120
 
121
- function setSliderValues(start, end) {
121
+ function setSliderValues(start: any, end: any) {
122
122
  sliderValue.value = [start || minFormatted.value, end || maxFormatted.value];
123
123
  }
124
124
  </script>
@@ -0,0 +1,21 @@
1
+ <template>
2
+ <div v-if="error" class="af-login-modal-error flex items-center p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400" role="alert">
3
+ <svg class="flex-shrink-0 inline w-4 h-4 me-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
4
+ <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z"/>
5
+ </svg>
6
+ <span class="sr-only">{{ $t('Info') }}</span>
7
+ <div>
8
+ {{ error }}
9
+ </div>
10
+ </div>
11
+ </template>
12
+
13
+ <script setup>
14
+
15
+ defineProps({
16
+ error: {
17
+ type: String,
18
+ default: null
19
+ }
20
+ });
21
+ </script>
@@ -108,9 +108,9 @@
108
108
  :min="getFilterMinValue(c.name)"
109
109
  :max="getFilterMaxValue(c.name)"
110
110
  :valueStart="getFilterItem({ column: c, operator: 'gte' })"
111
- @update:valueStart="onFilterInput[c.name]({ column: c, operator: 'gte', value: $event || undefined })"
111
+ @update:valueStart="onFilterInput[c.name]({ column: c, operator: 'gte', value: ($event !== '' && $event !== null) ? $event : undefined })"
112
112
  :valueEnd="getFilterItem({ column: c, operator: 'lte' })"
113
- @update:valueEnd="onFilterInput[c.name]({ column: c, operator: 'lte', value: $event || undefined })"
113
+ @update:valueEnd="onFilterInput[c.name]({ column: c, operator: 'lte', value: ($event !== '' && $event !== null) ? $event : undefined })"
114
114
  />
115
115
 
116
116
  <div v-else-if="['integer', 'decimal', 'float'].includes(c.type)" class="flex gap-2">
@@ -118,14 +118,14 @@
118
118
  type="number"
119
119
  aria-describedby="helper-text-explanation"
120
120
  :placeholder="$t('From')"
121
- @update:modelValue="onFilterInput[c.name]({ column: c, operator: 'gte', value: $event || undefined })"
121
+ @update:modelValue="onFilterInput[c.name]({ column: c, operator: 'gte', value: ($event !== '' && $event !== null) ? $event : undefined })"
122
122
  :modelValue="getFilterItem({ column: c, operator: 'gte' })"
123
123
  />
124
124
  <Input
125
125
  type="number"
126
126
  aria-describedby="helper-text-explanation"
127
127
  :placeholder="$t('To')"
128
- @update:modelValue="onFilterInput[c.name]({ column: c, operator: 'lte', value: $event|| undefined })"
128
+ @update:modelValue="onFilterInput[c.name]({ column: c, operator: 'lte', value: ($event !== '' && $event !== null) ? $event : undefined })"
129
129
  :modelValue="getFilterItem({ column: c, operator: 'lte' })"
130
130
  />
131
131
  </div>
@@ -269,7 +269,7 @@ const onSearchInput = computed(() => {
269
269
  function setFilterItem({ column, operator, value }) {
270
270
 
271
271
  const index = filtersStore.filters.findIndex(f => f.field === column.name && f.operator === operator);
272
- if (value === undefined) {
272
+ if (value === undefined || value === '' || value === null) {
273
273
  if (index !== -1) {
274
274
  filtersStore.filters.splice(index, 1);
275
275
  }
@@ -284,7 +284,8 @@ function setFilterItem({ column, operator, value }) {
284
284
  }
285
285
 
286
286
  function getFilterItem({ column, operator }) {
287
- return filtersStore.filters.find(f => f.field === column.name && f.operator === operator)?.value || '';
287
+ const filterValue = filtersStore.filters.find(f => f.field === column.name && f.operator === operator)?.value;
288
+ return filterValue !== undefined ? filterValue : '';
288
289
  }
289
290
 
290
291
  async function clear() {
@@ -70,6 +70,7 @@
70
70
  import { ref, computed, watch, nextTick, type Ref } from 'vue';
71
71
  import { useI18n } from 'vue-i18n';
72
72
  import ColumnValueInputWrapper from "@/components/ColumnValueInputWrapper.vue";
73
+ import type { AdminForthResourceColumnInputCommon } from '@/types/Common';
73
74
 
74
75
  const { t } = useI18n();
75
76
 
@@ -89,7 +90,7 @@
89
90
  const customComponentsInValidity: Ref<Record<string, boolean>> = ref({});
90
91
  const customComponentsEmptiness: Ref<Record<string, boolean>> = ref({});
91
92
  const allColumnsHaveCustomComponent = computed(() => {
92
- return props.group.columns.every(column => {
93
+ return props.group.columns.every((column: AdminForthResourceColumnInputCommon) => {
93
94
  const componentKey = `${props.source}Row` as keyof typeof column.components;
94
95
  return column.components?.[componentKey];
95
96
  });
@@ -10,13 +10,13 @@
10
10
  ($route.name === item.path)
11
11
  }"
12
12
  >
13
- <component v-if="item.icon" :is="getIcon(item.icon)" class="w-5 h-5 text-lightSidebarIcons dark:text-darkSidebarIcons transition duration-75 group-hover:text-lightSidebarIconsHover dark:group-hover:text-darkSidebarIconsHover" ></component>
13
+ <component v-if="item.icon" :is="getIcon(item.icon)" class="min-w-5 min-h-5 text-lightSidebarIcons dark:text-darkSidebarIcons transition duration-75 group-hover:text-lightSidebarIconsHover dark:group-hover:text-darkSidebarIconsHover" ></component>
14
14
  <span class="text-ellipsis overflow-hidden ms-3">{{ item.label }}</span>
15
15
  <span v-if="item.badge"
16
16
  >
17
17
 
18
18
  <Tooltip v-if="item.badgeTooltip">
19
- <div class="inline-flex items-center justify-center h-3 py-3 px-1 ms-3 text-sm font-medium rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
19
+ <div class="af-badge inline-flex items-center justify-center h-3 py-3 px-1 ms-3 text-sm font-medium rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
20
20
  fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent min-w-[1.5rem] max-w-[3rem]">{{ item.badge }}</div>
21
21
 
22
22
  <template #tooltip>
@@ -24,7 +24,7 @@
24
24
  </template>
25
25
  </Tooltip>
26
26
  <template v-else>
27
- <div class="inline-flex items-center justify-center h-3 py-3 px-1 ms-3 text-sm font-medium rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
27
+ <div class="af-badge inline-flex items-center justify-center h-3 py-3 px-1 ms-3 text-sm font-medium rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
28
28
  fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent min-w-[1.5rem] max-w-[3rem]">{{ item.badge }}</div>
29
29
  </template>
30
30