-
Notifications
You must be signed in to change notification settings - Fork 21
feat: implement sortable columns with visual indicators and sorting l… #414
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: implement sortable columns with visual indicators and sorting l… #414
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR adds column sorting functionality to the Table component, supporting both client-side and server-side sorting modes.
- Adds interactive column headers with visual sort indicators (ascending/descending/unsorted)
- Implements client-side sorting for array data sources with support for nested field paths, dates, and numbers
- Extends server-side data fetching to pass sort parameters to the data provider function
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
adminforth/spa/src/afcl/Table.vue
Outdated
| <!-- Unsorted indicator --> | ||
| <svg v-if="!isSorted(column)" 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"></path></svg> | ||
| <!-- Sorted ascending indicator --> | ||
| <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"></path></svg> |
Copilot
AI
Nov 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The SVG path for ascending sort indicator is incomplete. The path ends with '1.847 0z' but should end with '1.086Zm6.852...' to match the complete path from the unsorted indicator. This will result in an incorrect visual representation of the ascending arrow.
| <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"></path></svg> | |
| <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 1.086Z"></path></svg> |
adminforth/spa/src/afcl/Table.vue
Outdated
| <!-- Sorted ascending indicator --> | ||
| <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"></path></svg> | ||
| <!-- Sorted descending indicator (rotated) --> | ||
| <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"></path></svg> |
Copilot
AI
Nov 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The SVG path for descending sort indicator has the same incomplete path issue as the ascending indicator. The path ends with '1.847 0z' but should include the complete path definition to properly render the arrow icon.
| <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"></path></svg> | |
| <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 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"></path></svg> |
adminforth/spa/src/afcl/Table.vue
Outdated
| emit('update:sortField', currentSortField.value); | ||
| emit('update:sortDirection', currentSortField.value ? currentSortDirection.value : undefined as any); | ||
| emit('sort-change', { field: currentSortField.value, direction: currentSortDirection.value }); | ||
| }); |
Copilot
AI
Nov 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The watcher triggers on initial component mount when currentSortField is initialized with props.defaultSortField, causing an unnecessary refresh() call after the onMounted hook already calls refresh(). Consider using { immediate: false } option or checking if this is the initial load to avoid duplicate data fetching.
| }); | |
| }, { immediate: false }); |
adminforth/spa/src/afcl/Table.vue
Outdated
| if (currentPage.value !== 1) currentPage.value = 1; | ||
| refresh(); | ||
| emit('update:sortField', currentSortField.value); | ||
| emit('update:sortDirection', currentSortField.value ? currentSortDirection.value : undefined as any); |
Copilot
AI
Nov 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The as any type assertion bypasses TypeScript's type checking. Consider properly typing the emit parameter or using a union type like 'asc' | 'desc' | undefined in the emit definition instead of forcing the type with as any.
| emit('update:sortDirection', currentSortField.value ? currentSortDirection.value : undefined as any); | |
| emit('update:sortDirection', currentSortField.value ? currentSortDirection.value : undefined); |
adminforth/spa/src/afcl/Table.vue
Outdated
| offset: (currentLoadingIndex - 1) * props.pageSize, | ||
| limit: props.pageSize, | ||
| sortField: currentSortField.value, | ||
| sortDirection: currentSortDirection.value, |
Copilot
AI
Nov 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When sorting is cleared (sortField is undefined), the function still passes currentSortDirection.value which will be 'asc' by default. This could confuse server-side implementations. Consider only passing sortDirection when sortField has a value, e.g., ...(currentSortField.value ? { sortDirection: currentSortDirection.value } : {}).
| sortDirection: currentSortDirection.value, | |
| ...(currentSortField.value ? { sortDirection: currentSortDirection.value } : {}), |
| { name: 'Alice', age: 35, country: 'BR' }, | ||
| { name: 'Colin', age: 40, country: 'AU' }, | ||
| ]" | ||
| :sortable="true" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lets remove this and use col.sortable (default = false)
|
|
||
| const res = await fetch(url.toString(), { credentials: 'include' }); | ||
| const json = await res.json(); | ||
| return { data: json.data, total: json.total }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lets simplify
const { data, total } = callAdminForthApi('getProducts', {limit, offset, softField, sortDirection});
adminforth/spa/src/afcl/Table.vue
Outdated
| // Helper function to get nested properties by path | ||
| const getByPath = (o:any, p:string) => p.split('.').reduce((a:any,k)=>a?.[k], o); | ||
| return [...data].sort((a,b) => { | ||
| let av = getByPath(a, sortField), bv = getByPath(b, sortField); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@NoOne7135 should be const?
… default sort handling
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <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> | ||
|
|
||
| <!-- Sorted descending --> | ||
| <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> |
Copilot
AI
Nov 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The SVG path for the descending sort icon is incomplete. It ends with 1.847 0z but should have a complete path. This matches the same truncated path as line 26, resulting in an incorrect/incomplete icon rendering.
| <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> | |
| <svg v-else class="w-3 h-3 ms-1.5" fill="currentColor" viewBox="0 0 24 24"><path d="M15.426 12.976H8.574a2.075 2.075 0 0 1-1.847 1.086 1.9 1.9 0 0 1 .11 1.986l3.427 5.05a2.122 2.122 0 0 0 3.472 0l3.426-5.05a1.9 1.9 0 0 1 .11-1.986 2.075 2.075 0 0 1-1.846-1.086z"/></svg> |
adminforth/spa/src/afcl/Table.vue
Outdated
| watch([() => currentSortField.value, () => currentSortDirection.value], () => { | ||
| if (currentPage.value !== 1) currentPage.value = 1; | ||
| refresh(); | ||
| emit('update:sortField', currentSortField.value); | ||
| emit('update:sortDirection', currentSortField.value ? currentSortDirection.value : undefined); | ||
| emit('sort-change', { field: currentSortField.value, direction: currentSortDirection.value }); | ||
| }); |
Copilot
AI
Nov 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This watcher triggers on mount when refs are initialized (even if values don't change), causing refresh() to be called twice: once in onMounted (line 197) and once from this watcher. Consider using { immediate: false } or restructuring to avoid the duplicate data fetch on initial load.
| currentSortDirection.value = | ||
| currentSortDirection.value === 'asc' ? 'desc' : | ||
| currentSortField.value ? (currentSortField.value = undefined, props.defaultSortDirection ?? 'asc') : | ||
| 'asc'; |
Copilot
AI
Nov 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] This tri-state toggle logic uses the comma operator to both clear currentSortField.value and return defaultSortDirection, which is difficult to read and reason about. Consider using an if-else block or separate the side effect from the assignment for better clarity.
| currentSortDirection.value = | |
| currentSortDirection.value === 'asc' ? 'desc' : | |
| currentSortField.value ? (currentSortField.value = undefined, props.defaultSortDirection ?? 'asc') : | |
| 'asc'; | |
| if (currentSortDirection.value === 'asc') { | |
| currentSortDirection.value = 'desc'; | |
| } else if (currentSortField.value) { | |
| currentSortField.value = undefined; | |
| currentSortDirection.value = props.defaultSortDirection ?? 'asc'; | |
| } else { | |
| currentSortDirection.value = 'asc'; | |
| } |
| const av = getByPath(a, sortField), bv = getByPath(b, sortField); | ||
| // Handle null/undefined values | ||
| if (av == null && bv == null) return 0; | ||
| // Handle null/undefined values |
Copilot
AI
Nov 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Duplicate comment 'Handle null/undefined values' appears on consecutive lines. Remove the duplicate on line 336 to improve code clarity.
| // Handle null/undefined values |
| Table supports column sorting out of the box. | ||
|
|
||
| - By default, columns are NOT sortable. Enable sorting per column with `sortable: true`. | ||
| - Clicking a sortable header cycles sorting in a tri‑state order: |
Copilot
AI
Nov 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using a non-standard hyphen character (U+2011 non-breaking hyphen) instead of a standard hyphen. Use 'tri-state' with a regular hyphen for consistency.
| - Clicking a sortable header cycles sorting in a tri‑state order: | |
| - Clicking a sortable header cycles sorting in a tri-state order: |
…e sortDirection emission behavior
…ogic