adminforth 2.4.0-next.138 → 2.4.0-next.139

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.
@@ -1,130 +1,128 @@
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" ref="headerRefs"
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"
21
- :rows="pageSize"
22
- :columns="columns.length"
23
- :row-heights="rowHeights"
24
- :column-widths="columnWidths"
25
- />
26
- <tr v-else-if="!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"
36
- v-for="(item, index) in dataPage"
37
- ref="rowRefs"
38
- :class="{
39
- 'afcl-table-body odd:bg-lightTableOddBackground odd:dark:bg-darkTableOddBackground even:bg-lightTableEvenBackground even:dark:bg-darkTableEvenBackground': evenHighlights,
40
- 'border-b border-lightTableBorder dark:border-darkTableBorder': index !== dataPage.length - 1 || totalPages > 1,
41
- }"
42
- >
43
- <td class="px-6 py-4"
44
- v-for="column in props.columns"
45
- >
46
- <slot v-if="$slots[`cell:${column.fieldName}`]"
47
- :name="`cell:${column.fieldName}`"
48
- :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"
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"
21
+ :rows="pageSize"
22
+ :columns="columns.length"
23
+ :row-heights="rowHeights"
24
+ :column-widths="columnWidths"
25
+ />
26
+ <tr v-else-if="!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"
36
+ v-for="(item, index) in dataPage"
37
+ ref="rowRefs"
38
+ :class="{
39
+ 'afcl-table-body odd:bg-lightTableOddBackground odd:dark:bg-darkTableOddBackground even:bg-lightTableEvenBackground even:dark:bg-darkTableEvenBackground': evenHighlights,
40
+ 'border-b border-lightTableBorder dark:border-darkTableBorder': index !== dataPage.length - 1 || totalPages > 1,
41
+ }"
49
42
  >
50
- </slot>
51
- <span v-else-if="!isLoading" >
52
- {{ item[column.fieldName] }}
53
- </span>
54
- <div v-else>
55
- <div class=" w-full">
56
- <Skeleton class="h-4" />
57
- </div>
58
- </div>
59
- </td>
60
- </tr>
61
- </tbody>
62
- </table>
63
- <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"
64
- v-if="totalPages > 1"
65
- :aria-label="$t('Table navigation')">
66
- <i18n-t
67
- 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"
68
- >
69
- <template #from><span class="font-semibold text-lightTablePaginationNumeration dark:text-darkTablePaginationNumeration">{{ Math.min((currentPage - 1) * props.pageSize + 1, dataResult.total) }}</span></template>
70
- <template #to><span class="font-semibold text-lightTablePaginationNumeration dark:text-darkTablePaginationNumeration">{{ Math.min(currentPage * props.pageSize, dataResult.total) }}</span></template>
71
- <template #total><span class="font-semibold text-lightTablePaginationNumeration dark:text-darkTablePaginationNumeration">{{ dataResult.total }}</span></template>
72
- </i18n-t>
73
- <div class="af-pagination-container flex flex-row items-center xs:flex-row xs:justify-between xs:items-center gap-3">
74
- <div class="inline-flex">
75
- <!-- Buttons -->
76
- <button
77
- 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"
78
- @click="currentPage--; pageInput = currentPage.toString();" :disabled="currentPage <= 1">
79
- <svg class="w-3.5 h-3.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
80
- viewBox="0 0 14 10">
81
- <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
82
- d="M13 5H1m0 0 4 4M1 5l4-4"/>
83
- </svg>
84
- </button>
85
- <button
86
- 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"
87
- @click="switchPage(1); pageInput = currentPage.toString();" :disabled="currentPage <= 1">
88
- <!-- <IconChevronDoubleLeftOutline class="w-4 h-4" /> -->
89
- 1
90
- </button>
91
- <div
92
- contenteditable="true"
93
- 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"
94
- @keydown="onPageKeydown($event)"
95
- @input="onPageInput($event)"
96
- @blur="validatePageInput()"
97
- >
98
- {{ pageInput }}
43
+ <td class="px-6 py-4"
44
+ v-for="column in props.columns"
45
+ >
46
+ <slot v-if="$slots[`cell:${column.fieldName}`]"
47
+ :name="`cell:${column.fieldName}`"
48
+ :item="item" :column="column"
49
+ >
50
+ </slot>
51
+ <span v-else-if="!isLoading" >
52
+ {{ item[column.fieldName] }}
53
+ </span>
54
+ <div v-else>
55
+ <div class=" w-full">
56
+ <Skeleton class="h-4" />
57
+ </div>
58
+ </div>
59
+ </td>
60
+ </tr>
61
+ </tbody>
62
+ </table>
63
+ </div>
64
+ <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"
65
+ v-if="totalPages > 1"
66
+ :aria-label="$t('Table navigation')">
67
+ <i18n-t
68
+ 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"
69
+ >
70
+ <template #from><span class="font-semibold text-lightTablePaginationNumeration dark:text-darkTablePaginationNumeration">{{ Math.min((currentPage - 1) * props.pageSize + 1, dataResult.total) }}</span></template>
71
+ <template #to><span class="font-semibold text-lightTablePaginationNumeration dark:text-darkTablePaginationNumeration">{{ Math.min(currentPage * props.pageSize, dataResult.total) }}</span></template>
72
+ <template #total><span class="font-semibold text-lightTablePaginationNumeration dark:text-darkTablePaginationNumeration">{{ dataResult.total }}</span></template>
73
+ </i18n-t>
74
+ <div class="af-pagination-container flex flex-row items-center xs:flex-row xs:justify-between xs:items-center gap-3">
75
+ <div class="inline-flex">
76
+ <!-- Buttons -->
77
+ <button
78
+ 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"
79
+ @click="currentPage--; pageInput = currentPage.toString();" :disabled="currentPage <= 1">
80
+ <svg class="w-3.5 h-3.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
81
+ viewBox="0 0 14 10">
82
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
83
+ d="M13 5H1m0 0 4 4M1 5l4-4"/>
84
+ </svg>
85
+ </button>
86
+ <button
87
+ 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"
88
+ @click="switchPage(1); pageInput = currentPage.toString();" :disabled="currentPage <= 1">
89
+ <!-- <IconChevronDoubleLeftOutline class="w-4 h-4" /> -->
90
+ 1
91
+ </button>
92
+ <div
93
+ contenteditable="true"
94
+ 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"
95
+ @keydown="onPageKeydown($event)"
96
+ @input="onPageInput($event)"
97
+ @blur="validatePageInput()"
98
+ >
99
+ {{ pageInput }}
100
+ </div>
101
+
102
+ <button
103
+ 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"
104
+ @click="currentPage = totalPages; pageInput = currentPage.toString();" :disabled="currentPage >= totalPages">
105
+ {{ totalPages }}
106
+
107
+ <!-- <IconChevronDoubleRightOutline class="w-4 h-4" /> -->
108
+ </button>
109
+ <button
110
+ 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"
111
+ @click="currentPage++; pageInput = currentPage.toString();" :disabled="currentPage >= totalPages">
112
+ <svg class="w-3.5 h-3.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
113
+ viewBox="0 0 14 10">
114
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
115
+ d="M1 5h12m0 0L9 1m4 4L9 9"/>
116
+ </svg>
117
+ </button>
99
118
  </div>
100
-
101
- <button
102
- 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"
103
- @click="currentPage = totalPages; pageInput = currentPage.toString();" :disabled="currentPage >= totalPages">
104
- {{ totalPages }}
105
-
106
- <!-- <IconChevronDoubleRightOutline class="w-4 h-4" /> -->
107
- </button>
108
- <button
109
- 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"
110
- @click="currentPage++; pageInput = currentPage.toString();" :disabled="currentPage >= totalPages">
111
- <svg class="w-3.5 h-3.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
112
- viewBox="0 0 14 10">
113
- <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
114
- d="M1 5h12m0 0L9 1m4 4L9 9"/>
115
- </svg>
116
- </button>
117
119
  </div>
118
- </div>
119
- </nav>
120
- </div>
121
-
122
-
123
-
120
+ </nav>
121
+ </div>
124
122
  </template>
125
123
 
126
124
  <script setup lang="ts">
127
- import { ref, computed, useTemplateRef, watch, shallowRef, toRef } from 'vue';
125
+ import { ref, computed, useTemplateRef, watch, onMounted } from 'vue';
128
126
  import SkeleteLoader from '@/components/SkeleteLoader.vue';
129
127
  import { IconTableRowOutline } from '@iconify-prerendered/vue-flowbite';
130
128
 
@@ -132,53 +130,6 @@
132
130
  refreshTable
133
131
  })
134
132
 
135
- type Row = Record<string, unknown>
136
- type LoadFn = (params: { offset: number, limit: number }) => Promise<{ data: Row[]; total: number }>
137
-
138
- const isFunc = (v: unknown): v is LoadFn => typeof v === 'function'
139
-
140
- function usePagedData(props: {
141
- data: Row[] | LoadFn
142
- pageSize: number
143
- currentPage: number
144
- }) {
145
- const page = ref(props.currentPage)
146
- const pageSize = toRef(props, 'pageSize')
147
-
148
- const isLoading = ref(false)
149
- const error = shallowRef<unknown>(null)
150
- const result = shallowRef<{ data: Row[]; total: number }>({ data: [], total: 0 })
151
-
152
- let requestId = 0
153
-
154
- async function fetchData() {
155
- const id = ++requestId
156
- isLoading.value = true
157
- error.value = null
158
- try {
159
- if (isFunc(props.data)) {
160
- const res = await props.data({offset: ((page.value - 1) * pageSize.value), limit: pageSize.value})
161
- if (id !== requestId) return
162
- result.value = res
163
- } else {
164
- const start = (page.value - 1) * pageSize.value
165
- const end = start + pageSize.value
166
- result.value = { data: props.data.slice(start, end), total: props.data.length }
167
- }
168
- } catch (e) {
169
- if (id !== requestId) return
170
- error.value = e
171
- result.value = { data: [], total: 0 }
172
- } finally {
173
- if (id === requestId) isLoading.value = false
174
- }
175
- }
176
-
177
- watch([page, pageSize, () => props.data], fetchData, { immediate: true })
178
-
179
- return { page, pageSize, isLoading, error, result, refresh: fetchData }
180
- }
181
-
182
133
  const props = withDefaults(
183
134
  defineProps<{
184
135
  columns: {
@@ -196,17 +147,23 @@
196
147
  }
197
148
  );
198
149
 
199
- const { result: dataResult, isLoading, error, page: currentPage, pageSize, refresh } = usePagedData({
200
- data: props.data,
201
- pageSize: props.pageSize,
202
- currentPage: 1
203
- });
204
-
205
150
  const pageInput = ref('1');
206
151
  const rowRefs = useTemplateRef<HTMLElement[]>('rowRefs');
207
152
  const headerRefs = useTemplateRef<HTMLElement[]>('headerRefs');
208
153
  const rowHeights = ref<number[]>([]);
209
154
  const columnWidths = ref<number[]>([]);
155
+ const currentPage = ref(1);
156
+ const isLoading = ref(false);
157
+ const dataResult = ref<{data: {[key: string]: any}[], total: number}>({data: [], total: 0});
158
+ const isAtLeastOneLoading = ref<boolean[]>([false]);
159
+
160
+ onMounted(() => {
161
+ refresh();
162
+ });
163
+
164
+ watch( currentPage, async () => {
165
+ refresh();
166
+ });
210
167
 
211
168
  watch(() => currentPage.value, () => {
212
169
  rowHeights.value = !rowRefs.value ? [] : rowRefs.value.map((el: HTMLElement) => el.offsetHeight);
@@ -218,7 +175,7 @@
218
175
  });
219
176
 
220
177
  const dataPage = computed(() => {
221
- return dataResult.value.data;
178
+ return dataResult.value?.data;
222
179
  });
223
180
 
224
181
  function switchPage(p: number) {
@@ -258,9 +215,30 @@
258
215
  }
259
216
  }
260
217
 
218
+ async function refresh() {
219
+ if (typeof props.data === 'function') {
220
+ isLoading.value = true;
221
+ const currentLoadingIndex = currentPage.value;
222
+ isAtLeastOneLoading.value[currentLoadingIndex] = true;
223
+ const result = await props.data({ offset: (currentLoadingIndex - 1) * props.pageSize, limit: props.pageSize });
224
+ isAtLeastOneLoading.value[currentLoadingIndex] = false;
225
+ if (isAtLeastOneLoading.value.every(v => v === false)) {
226
+ isLoading.value = false;
227
+ }
228
+ dataResult.value = result;
229
+ } else {
230
+ const start = (currentPage.value - 1) * props.pageSize;
231
+ const end = start + props.pageSize;
232
+ dataResult.value = { data: props.data.slice(start, end), total: props.data.length };
233
+ }
234
+ }
235
+
261
236
  function refreshTable() {
262
- currentPage.value = 1;
263
- refresh();
237
+ if ( currentPage.value !== 1 ) {
238
+ currentPage.value = 1;
239
+ } else {
240
+ refresh();
241
+ }
264
242
  }
265
243
 
266
244
  </script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adminforth",
3
- "version": "2.4.0-next.138",
3
+ "version": "2.4.0-next.139",
4
4
  "description": "OpenSource Vue3 powered forth-generation admin panel",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",