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.
@@ -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,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({ offset: (currentLoadingIndex - 1) * props.pageSize, limit: props.pageSize });
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
- dataResult.value = { data: props.data.slice(start, end), total: props.data.length };
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>
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.296",
4
4
  "description": "OpenSource Vue3 powered forth-generation admin panel",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",