adminforth 2.4.0-next.295 → 2.4.0-next.297
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 +98 -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,15 @@
|
|
|
181
210
|
emit('update:tableLoading', isLoading.value || props.isLoading);
|
|
182
211
|
});
|
|
183
212
|
|
|
213
|
+
watch([() => currentSortField.value, () => currentSortDirection.value], () => {
|
|
214
|
+
refresh();
|
|
215
|
+
emit('update:sortField', currentSortField.value);
|
|
216
|
+
emit('update:sortDirection', currentSortField.value ? currentSortDirection.value : undefined);
|
|
217
|
+
const field = currentSortField.value ?? null;
|
|
218
|
+
const direction = currentSortField.value ? currentSortDirection.value : null;
|
|
219
|
+
emit('sort-change', { field, direction });
|
|
220
|
+
}, { immediate: false });
|
|
221
|
+
|
|
184
222
|
const totalPages = computed(() => {
|
|
185
223
|
return dataResult.value?.total ? Math.ceil(dataResult.value.total / props.pageSize) : 1;
|
|
186
224
|
});
|
|
@@ -196,6 +234,9 @@
|
|
|
196
234
|
|
|
197
235
|
const emit = defineEmits([
|
|
198
236
|
'update:tableLoading',
|
|
237
|
+
'update:sortField',
|
|
238
|
+
'update:sortDirection',
|
|
239
|
+
'sort-change',
|
|
199
240
|
]);
|
|
200
241
|
|
|
201
242
|
function onPageInput(event: any) {
|
|
@@ -231,7 +272,12 @@
|
|
|
231
272
|
isLoading.value = true;
|
|
232
273
|
const currentLoadingIndex = currentPage.value;
|
|
233
274
|
isAtLeastOneLoading.value[currentLoadingIndex] = true;
|
|
234
|
-
const result = await props.data({
|
|
275
|
+
const result = await props.data({
|
|
276
|
+
offset: (currentLoadingIndex - 1) * props.pageSize,
|
|
277
|
+
limit: props.pageSize,
|
|
278
|
+
sortField: currentSortField.value,
|
|
279
|
+
...(currentSortField.value ? { sortDirection: currentSortDirection.value } : {}),
|
|
280
|
+
});
|
|
235
281
|
isAtLeastOneLoading.value[currentLoadingIndex] = false;
|
|
236
282
|
if (isAtLeastOneLoading.value.every(v => v === false)) {
|
|
237
283
|
isLoading.value = false;
|
|
@@ -240,7 +286,9 @@
|
|
|
240
286
|
} else if (typeof props.data === 'object' && Array.isArray(props.data)) {
|
|
241
287
|
const start = (currentPage.value - 1) * props.pageSize;
|
|
242
288
|
const end = start + props.pageSize;
|
|
243
|
-
|
|
289
|
+
const total = props.data.length;
|
|
290
|
+
const sorted = sortArrayData(props.data, currentSortField.value, currentSortDirection.value);
|
|
291
|
+
dataResult.value = { data: sorted.slice(start, end), total };
|
|
244
292
|
}
|
|
245
293
|
}
|
|
246
294
|
|
|
@@ -252,4 +300,48 @@
|
|
|
252
300
|
}
|
|
253
301
|
}
|
|
254
302
|
|
|
303
|
+
function isColumnSortable(col:{fieldName:string; sortable?:boolean}) {
|
|
304
|
+
// Sorting is controlled per column; default is NOT sortable. Enable with `sortable: true`.
|
|
305
|
+
return col.sortable === true;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function onHeaderClick(col:{fieldName:string; sortable?:boolean}) {
|
|
309
|
+
if (!isColumnSortable(col)) return;
|
|
310
|
+
if (currentSortField.value !== col.fieldName) {
|
|
311
|
+
currentSortField.value = col.fieldName;
|
|
312
|
+
currentSortDirection.value = props.defaultSortDirection ?? 'asc';
|
|
313
|
+
} else {
|
|
314
|
+
currentSortDirection.value =
|
|
315
|
+
currentSortDirection.value === 'asc' ? 'desc' :
|
|
316
|
+
currentSortField.value ? (currentSortField.value = undefined, props.defaultSortDirection ?? 'asc') :
|
|
317
|
+
'asc';
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function getAriaSort(col:{fieldName:string; sortable?:boolean}) {
|
|
322
|
+
if (!isColumnSortable(col)) return undefined;
|
|
323
|
+
if (currentSortField.value !== col.fieldName) return 'none';
|
|
324
|
+
return currentSortDirection.value === 'asc' ? 'ascending' : 'descending';
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
|
|
328
|
+
|
|
329
|
+
function sortArrayData(data:any[], sortField?:string, dir:'asc'|'desc'='asc') {
|
|
330
|
+
if (!sortField) return data;
|
|
331
|
+
// Helper function to get nested properties by path
|
|
332
|
+
const getByPath = (o:any, p:string) => p.split('.').reduce((a:any,k)=>a?.[k], o);
|
|
333
|
+
return [...data].sort((a,b) => {
|
|
334
|
+
const av = getByPath(a, sortField), bv = getByPath(b, sortField);
|
|
335
|
+
// Handle null/undefined values
|
|
336
|
+
if (av == null && bv == null) return 0;
|
|
337
|
+
// Handle null/undefined values
|
|
338
|
+
if (av == null) return 1; if (bv == null) return -1;
|
|
339
|
+
// Data types
|
|
340
|
+
if (av instanceof Date && bv instanceof Date) return dir === 'asc' ? av.getTime() - bv.getTime() : bv.getTime() - av.getTime();
|
|
341
|
+
// Strings and numbers
|
|
342
|
+
if (typeof av === 'number' && typeof bv === 'number') return dir === 'asc' ? av - bv : bv - av;
|
|
343
|
+
const cmp = collator.compare(String(av), String(bv));
|
|
344
|
+
return dir === 'asc' ? cmp : -cmp;
|
|
345
|
+
});
|
|
346
|
+
}
|
|
255
347
|
</script>
|