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.
@@ -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 scope="col" class="px-6 py-3" ref="headerRefs" :key="`header-${column.fieldName}`"
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({ offset: (currentLoadingIndex - 1) * props.pageSize, limit: props.pageSize });
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
- dataResult.value = { data: props.data.slice(start, end), total: props.data.length };
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>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adminforth",
3
- "version": "2.4.0-next.295",
3
+ "version": "2.4.0-next.297",
4
4
  "description": "OpenSource Vue3 powered forth-generation admin panel",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",