adminforth 2.4.0-next.295 → 2.4.0-next.296
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.
- package/dist/spa/src/afcl/Table.vue +97 -6
- package/package.json +1 -1
|
@@ -4,13 +4,30 @@
|
|
|
4
4
|
<table class="afcl-table w-full text-sm text-left rtl:text-right text-lightTableText dark:text-darkTableText overflow-x-auto">
|
|
5
5
|
<thead class="afcl-table-thread text-xs text-lightTableHeadingText uppercase bg-lightTableHeadingBackground dark:bg-darkTableHeadingBackground dark:text-darkTableHeadingText">
|
|
6
6
|
<tr>
|
|
7
|
-
<th
|
|
7
|
+
<th
|
|
8
|
+
scope="col"
|
|
9
|
+
class="px-6 py-3"
|
|
10
|
+
ref="headerRefs"
|
|
11
|
+
:key="`header-${column.fieldName}`"
|
|
8
12
|
v-for="column in columns"
|
|
13
|
+
:aria-sort="getAriaSort(column)"
|
|
14
|
+
:class="{ 'cursor-pointer select-none afcl-table-header-sortable': isColumnSortable(column) }"
|
|
15
|
+
@click="onHeaderClick(column)"
|
|
9
16
|
>
|
|
10
17
|
<slot v-if="$slots[`header:${column.fieldName}`]" :name="`header:${column.fieldName}`" :column="column" />
|
|
11
|
-
|
|
12
|
-
<span v-else>
|
|
18
|
+
|
|
19
|
+
<span v-else class="inline-flex items-center">
|
|
13
20
|
{{ column.label }}
|
|
21
|
+
<span v-if="isColumnSortable(column)" class="text-lightTableHeadingText dark:text-darkTableHeadingText">
|
|
22
|
+
<!-- Unsorted -->
|
|
23
|
+
<svg v-if="currentSortField !== column.fieldName" class="w-3 h-3 ms-1.5 opacity-30" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24"><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"/></svg>
|
|
24
|
+
|
|
25
|
+
<!-- Sorted ascending -->
|
|
26
|
+
<svg v-else-if="currentSortDirection === 'asc'" class="w-3 h-3 ms-1.5" fill="currentColor" viewBox="0 0 24 24"><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"/></svg>
|
|
27
|
+
|
|
28
|
+
<!-- Sorted descending -->
|
|
29
|
+
<svg v-else class="w-3 h-3 ms-1.5 rotate-180" fill="currentColor" viewBox="0 0 24 24"><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"/></svg>
|
|
30
|
+
</span>
|
|
14
31
|
</span>
|
|
15
32
|
</th>
|
|
16
33
|
</tr>
|
|
@@ -141,13 +158,16 @@
|
|
|
141
158
|
columns: {
|
|
142
159
|
label: string,
|
|
143
160
|
fieldName: string,
|
|
161
|
+
sortable?: boolean,
|
|
144
162
|
}[],
|
|
145
163
|
data: {
|
|
146
164
|
[key: string]: any,
|
|
147
|
-
}[] | ((params: { offset: number, limit: number }) => Promise<{data: {[key: string]: any}[], total: number}>),
|
|
165
|
+
}[] | ((params: { offset: number, limit: number, sortField?: string, sortDirection?: 'asc' | 'desc' }) => Promise<{data: {[key: string]: any}[], total: number}>),
|
|
148
166
|
evenHighlights?: boolean,
|
|
149
167
|
pageSize?: number,
|
|
150
168
|
isLoading?: boolean,
|
|
169
|
+
defaultSortField?: string,
|
|
170
|
+
defaultSortDirection?: 'asc' | 'desc',
|
|
151
171
|
}>(), {
|
|
152
172
|
evenHighlights: true,
|
|
153
173
|
pageSize: 5,
|
|
@@ -163,8 +183,17 @@
|
|
|
163
183
|
const isLoading = ref(false);
|
|
164
184
|
const dataResult = ref<{data: {[key: string]: any}[], total: number}>({data: [], total: 0});
|
|
165
185
|
const isAtLeastOneLoading = ref<boolean[]>([false]);
|
|
186
|
+
const currentSortField = ref<string | undefined>(props.defaultSortField);
|
|
187
|
+
const currentSortDirection = ref<'asc' | 'desc'>(props.defaultSortDirection ?? 'asc');
|
|
166
188
|
|
|
167
189
|
onMounted(() => {
|
|
190
|
+
// If defaultSortField points to a non-sortable column, ignore it
|
|
191
|
+
if (currentSortField.value) {
|
|
192
|
+
const col = props.columns?.find(c => c.fieldName === currentSortField.value);
|
|
193
|
+
if (!col || !isColumnSortable(col)) {
|
|
194
|
+
currentSortField.value = undefined;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
168
197
|
refresh();
|
|
169
198
|
});
|
|
170
199
|
|
|
@@ -181,6 +210,14 @@
|
|
|
181
210
|
emit('update:tableLoading', isLoading.value || props.isLoading);
|
|
182
211
|
});
|
|
183
212
|
|
|
213
|
+
watch([() => currentSortField.value, () => currentSortDirection.value], () => {
|
|
214
|
+
if (currentPage.value !== 1) currentPage.value = 1;
|
|
215
|
+
refresh();
|
|
216
|
+
emit('update:sortField', currentSortField.value);
|
|
217
|
+
emit('update:sortDirection', currentSortField.value ? currentSortDirection.value : undefined);
|
|
218
|
+
emit('sort-change', { field: currentSortField.value, direction: currentSortDirection.value });
|
|
219
|
+
}, { immediate: false });
|
|
220
|
+
|
|
184
221
|
const totalPages = computed(() => {
|
|
185
222
|
return dataResult.value?.total ? Math.ceil(dataResult.value.total / props.pageSize) : 1;
|
|
186
223
|
});
|
|
@@ -196,6 +233,9 @@
|
|
|
196
233
|
|
|
197
234
|
const emit = defineEmits([
|
|
198
235
|
'update:tableLoading',
|
|
236
|
+
'update:sortField',
|
|
237
|
+
'update:sortDirection',
|
|
238
|
+
'sort-change',
|
|
199
239
|
]);
|
|
200
240
|
|
|
201
241
|
function onPageInput(event: any) {
|
|
@@ -231,7 +271,12 @@
|
|
|
231
271
|
isLoading.value = true;
|
|
232
272
|
const currentLoadingIndex = currentPage.value;
|
|
233
273
|
isAtLeastOneLoading.value[currentLoadingIndex] = true;
|
|
234
|
-
const result = await props.data({
|
|
274
|
+
const result = await props.data({
|
|
275
|
+
offset: (currentLoadingIndex - 1) * props.pageSize,
|
|
276
|
+
limit: props.pageSize,
|
|
277
|
+
sortField: currentSortField.value,
|
|
278
|
+
...(currentSortField.value ? { sortDirection: currentSortDirection.value } : {}),
|
|
279
|
+
});
|
|
235
280
|
isAtLeastOneLoading.value[currentLoadingIndex] = false;
|
|
236
281
|
if (isAtLeastOneLoading.value.every(v => v === false)) {
|
|
237
282
|
isLoading.value = false;
|
|
@@ -240,7 +285,9 @@
|
|
|
240
285
|
} else if (typeof props.data === 'object' && Array.isArray(props.data)) {
|
|
241
286
|
const start = (currentPage.value - 1) * props.pageSize;
|
|
242
287
|
const end = start + props.pageSize;
|
|
243
|
-
|
|
288
|
+
const total = props.data.length;
|
|
289
|
+
const sorted = sortArrayData(props.data, currentSortField.value, currentSortDirection.value);
|
|
290
|
+
dataResult.value = { data: sorted.slice(start, end), total };
|
|
244
291
|
}
|
|
245
292
|
}
|
|
246
293
|
|
|
@@ -252,4 +299,48 @@
|
|
|
252
299
|
}
|
|
253
300
|
}
|
|
254
301
|
|
|
302
|
+
function isColumnSortable(col:{fieldName:string; sortable?:boolean}) {
|
|
303
|
+
// Sorting is controlled per column; default is NOT sortable. Enable with `sortable: true`.
|
|
304
|
+
return col.sortable === true;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function onHeaderClick(col:{fieldName:string; sortable?:boolean}) {
|
|
308
|
+
if (!isColumnSortable(col)) return;
|
|
309
|
+
if (currentSortField.value !== col.fieldName) {
|
|
310
|
+
currentSortField.value = col.fieldName;
|
|
311
|
+
currentSortDirection.value = props.defaultSortDirection ?? 'asc';
|
|
312
|
+
} else {
|
|
313
|
+
currentSortDirection.value =
|
|
314
|
+
currentSortDirection.value === 'asc' ? 'desc' :
|
|
315
|
+
currentSortField.value ? (currentSortField.value = undefined, props.defaultSortDirection ?? 'asc') :
|
|
316
|
+
'asc';
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function getAriaSort(col:{fieldName:string; sortable?:boolean}) {
|
|
321
|
+
if (!isColumnSortable(col)) return undefined;
|
|
322
|
+
if (currentSortField.value !== col.fieldName) return 'none';
|
|
323
|
+
return currentSortDirection.value === 'asc' ? 'ascending' : 'descending';
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
|
|
327
|
+
|
|
328
|
+
function sortArrayData(data:any[], sortField?:string, dir:'asc'|'desc'='asc') {
|
|
329
|
+
if (!sortField) return data;
|
|
330
|
+
// Helper function to get nested properties by path
|
|
331
|
+
const getByPath = (o:any, p:string) => p.split('.').reduce((a:any,k)=>a?.[k], o);
|
|
332
|
+
return [...data].sort((a,b) => {
|
|
333
|
+
const av = getByPath(a, sortField), bv = getByPath(b, sortField);
|
|
334
|
+
// Handle null/undefined values
|
|
335
|
+
if (av == null && bv == null) return 0;
|
|
336
|
+
// Handle null/undefined values
|
|
337
|
+
if (av == null) return 1; if (bv == null) return -1;
|
|
338
|
+
// Data types
|
|
339
|
+
if (av instanceof Date && bv instanceof Date) return dir === 'asc' ? av.getTime() - bv.getTime() : bv.getTime() - av.getTime();
|
|
340
|
+
// Strings and numbers
|
|
341
|
+
if (typeof av === 'number' && typeof bv === 'number') return dir === 'asc' ? av - bv : bv - av;
|
|
342
|
+
const cmp = collator.compare(String(av), String(bv));
|
|
343
|
+
return dir === 'asc' ? cmp : -cmp;
|
|
344
|
+
});
|
|
345
|
+
}
|
|
255
346
|
</script>
|