Skip to content

Add Airtable-style row sorting to GridView component#18

Merged
hotlong merged 4 commits intomainfrom
copilot/add-airtable-row-sorting
Jan 9, 2026
Merged

Add Airtable-style row sorting to GridView component#18
hotlong merged 4 commits intomainfrom
copilot/add-airtable-row-sorting

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Jan 9, 2026

Implements single and multi-column sorting for GridView with type-aware comparison and visual indicators.

Changes

  • GridView sorting logic

    • Click column headers to cycle: asc → desc → clear
    • Shift+click for multi-column sorting with priority indicators
    • Type-aware comparison: text (case-insensitive), number, date, boolean
    • Null/undefined values always sort to end
  • Visual indicators

    • ↑/↓ icons show sort direction
    • Priority numbers (1, 2, 3...) for multi-column sorts
    • Hover state shows ⇅ on sortable columns
  • API surface

    • enableSorting?: boolean - global toggle (default: true)
    • sortable?: boolean - per-column control
    • onSortChange?: (sorts: SortConfig[]) => void - state tracking
    • Export SortConfig type for external use

Usage

import { GridView, SortConfig } from '@objectql/ui'

const columns = [
  { id: 'name', label: 'Name', type: 'text' },
  { id: 'budget', label: 'Budget', type: 'number' },
  { id: 'actions', label: 'Actions', sortable: false }, // opt-out
]

<GridView
  columns={columns}
  data={data}
  onSortChange={(sorts) => console.log(sorts)}
  // sorts: [{ columnId: 'name', direction: 'asc' }]
/>

No breaking changes. Sorting enabled by default, fully backward compatible.

Original prompt

帮我开发airtable行排序


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 3 commits January 9, 2026 05:11
…dicators

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…ncies

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot AI changed the title [WIP] Add row sorting feature for Airtable Add Airtable-style row sorting to GridView component Jan 9, 2026
Copilot AI requested a review from hotlong January 9, 2026 05:20
@hotlong hotlong requested review from Copilot and removed request for hotlong January 9, 2026 05:28
Copy link
Copy Markdown
Contributor

Copilot AI left a 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 Airtable-style row sorting functionality to the GridView component, enabling both single and multi-column sorting with type-aware comparison logic and visual indicators.

Key Changes

  • Implements interactive column header sorting with click/shift+click support for single and multi-column sorting
  • Adds type-aware comparison for text, number, date, and boolean fields with proper null handling
  • Introduces visual indicators (arrows and priority numbers) to show active sort state

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.

File Description
packages/ui/src/components/grid/GridView.tsx Core sorting implementation including state management, sorting logic, type-aware comparison, and UI updates for sortable column headers
packages/ui/examples/airtable-example.tsx Updated example to demonstrate sorting functionality with state tracking and visual feedback
packages/ui/AIRTABLE_GUIDE.md Documentation additions explaining sorting API, behavior, and usage examples
packages/ui/ADVANCED_TABLE_FEATURES.md Added comprehensive sorting feature documentation with Chinese/English bilingual descriptions

if (column.type === 'number') {
comparison = Number(aVal) - Number(bVal)
} else if (column.type === 'date') {
comparison = new Date(aVal).getTime() - new Date(bVal).getTime()
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For date type comparisons, there's no validation that the values are actually valid dates before calling new Date(). Invalid date strings will result in NaN from getTime(), which can lead to incorrect sort order. Consider adding validation or a try-catch to handle invalid date values gracefully, treating them similar to null values.

Suggested change
comparison = new Date(aVal).getTime() - new Date(bVal).getTime()
const aTime = Date.parse(String(aVal))
const bTime = Date.parse(String(bVal))
const aInvalid = Number.isNaN(aTime)
const bInvalid = Number.isNaN(bTime)
// Treat invalid date values similar to null/undefined:
// - if both invalid, move to next sort criterion
// - if one invalid, place it after the valid one
if (aInvalid && bInvalid) {
continue
}
if (aInvalid) {
return 1
}
if (bInvalid) {
return -1
}
comparison = aTime - bTime

Copilot uses AI. Check for mistakes.
// Type-specific comparison
let comparison = 0
if (column.type === 'number') {
comparison = Number(aVal) - Number(bVal)
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The number type comparison uses Number() to coerce values, but doesn't handle cases where the value cannot be converted to a valid number (which would result in NaN). NaN comparisons always return false, leading to unstable sorts. Consider adding validation to treat non-numeric values similar to null values, or use parseFloat with isNaN checks.

Suggested change
comparison = Number(aVal) - Number(bVal)
const aNum = Number(aVal)
const bNum = Number(bVal)
const aIsNaN = Number.isNaN(aNum)
const bIsNaN = Number.isNaN(bNum)
// Treat non-numeric values similarly to nulls:
// - If both are non-numeric, skip this sort key
// - If only one is non-numeric, place it after valid numbers
if (aIsNaN && bIsNaN) {
continue
}
if (aIsNaN) {
return 1
}
if (bIsNaN) {
return -1
}
comparison = aNum - bNum

Copilot uses AI. Check for mistakes.
Comment on lines 184 to 187
const indexMap = new Map<any, number>()
data.forEach((row, index) => {
indexMap.set(row, index)
})
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rowIndexMap is being built from the original unsorted data, but it's used to look up indices for the sorted data. This causes incorrect row indices to be passed to callbacks like onCellEdit, onDelete, and row selection handlers. The indexMap should be built from sortedData instead of the original data array to maintain correct index references after sorting.

Copilot uses AI. Check for mistakes.
Comment on lines +139 to +176
const sortData = (dataToSort: any[]) => {
if (!enableSorting || sorts.length === 0) {
return dataToSort
}

return [...dataToSort].sort((a, b) => {
for (const sort of sorts) {
const column = columns.find(c => c.id === sort.columnId)
if (!column) continue

let aVal = a[sort.columnId]
let bVal = b[sort.columnId]

// Handle null/undefined
if (aVal == null && bVal == null) continue
if (aVal == null) return 1
if (bVal == null) return -1

// Type-specific comparison
let comparison = 0
if (column.type === 'number') {
comparison = Number(aVal) - Number(bVal)
} else if (column.type === 'date') {
comparison = new Date(aVal).getTime() - new Date(bVal).getTime()
} else if (column.type === 'boolean') {
comparison = (aVal ? 1 : 0) - (bVal ? 1 : 0)
} else {
// Text comparison (case-insensitive)
comparison = String(aVal).localeCompare(String(bVal), undefined, { sensitivity: 'base' })
}

if (comparison !== 0) {
return sort.direction === 'asc' ? comparison : -comparison
}
}
return 0
})
}
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sortData function is defined inside the component body but is not wrapped in useCallback or useMemo. This means it's recreated on every render, which can cause the useMemo for groupedData to recompute unnecessarily. Additionally, sortData depends on columns, enableSorting, and sorts state, but calling it inside a useMemo creates a dependency issue. Consider wrapping sortData in useCallback with proper dependencies.

Copilot uses AI. Check for mistakes.
Comment on lines +202 to +203
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data, enableGrouping, groupByColumn, sorts, enableSorting])
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The useMemo dependency array has been disabled with eslint-disable comment, but the actual dependencies are incomplete. The callback includes a call to sortData() which depends on columns, sorts, and enableSorting, but columns is missing from the dependency array. This could lead to stale closures where the memoized value doesn't update when columns change.

Suggested change
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data, enableGrouping, groupByColumn, sorts, enableSorting])
}, [data, enableGrouping, groupByColumn, sorts, enableSorting, columns])

Copilot uses AI. Check for mistakes.
Comment on lines +507 to +511
onClick={(e) => {
if (isSortable && !enableColumnDragDrop) {
handleSort(column.id, e.shiftKey)
}
}}
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When both sorting and column drag-drop are enabled, the sorting click handler is disabled but the visual hover styling and sort indicators are still shown. This creates a confusing user experience where columns appear sortable (with hover effects and sort icons) but clicking doesn't trigger sorting. Consider either hiding the sort indicators when enableColumnDragDrop is true, or implementing a way to support both features simultaneously.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Copilot AI left a 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 4 out of 4 changed files in this pull request and generated 1 comment.

Comment on lines 72 to +83

// Update columns when initialColumns change
React.useEffect(() => {
setColumns(initialColumns)
}, [initialColumns])

// Notify parent of sort changes
React.useEffect(() => {
if (onSortChange) {
onSortChange(sorts)
}
}, [sorts, onSortChange])
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The onSortChange callback in useEffect has onSortChange itself in the dependency array. If the parent component doesn't memoize this callback (which is common), it will trigger the effect on every render, even when sorts haven't changed. This could lead to unnecessary re-renders and potential infinite loops if the parent updates state in onSortChange. Consider wrapping the effect content in a check or documenting that onSortChange should be memoized.

Suggested change
// Update columns when initialColumns change
React.useEffect(() => {
setColumns(initialColumns)
}, [initialColumns])
// Notify parent of sort changes
React.useEffect(() => {
if (onSortChange) {
onSortChange(sorts)
}
}, [sorts, onSortChange])
const onSortChangeRef = React.useRef<GridViewProps["onSortChange"]>()
// Update columns when initialColumns change
React.useEffect(() => {
setColumns(initialColumns)
}, [initialColumns])
// Keep a ref to the latest onSortChange callback
React.useEffect(() => {
onSortChangeRef.current = onSortChange
}, [onSortChange])
// Notify parent of sort changes
React.useEffect(() => {
if (onSortChangeRef.current) {
onSortChangeRef.current(sorts)
}
}, [sorts])

Copilot uses AI. Check for mistakes.
@hotlong hotlong marked this pull request as ready for review January 9, 2026 06:52
@hotlong
Copy link
Copy Markdown
Contributor

hotlong commented Jan 9, 2026

能不能实现向Airtable那种拖拽的排序?

@hotlong
Copy link
Copy Markdown
Contributor

hotlong commented Jan 9, 2026

能不能实现向Airtable那种拖拽的排序? @copilot

@hotlong hotlong merged commit 90ae69e into main Jan 9, 2026
13 of 14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants