Skip to content

Commit a61faa4

Browse files
authored
refactor(datagrid): drop redundant resize cursor and consolidate sort dispatch (#1131)
* refactor(datagrid): collapse cell hierarchy to single DataGridCellView * refactor(datagrid): bound display cache via NSCache and add O(1) RowID lookup * refactor(datagrid): incremental row visual state via RowVisualIndex * refactor(datagrid): off-main JSON parse and cancellable Task.sleep cooldowns * fix(datagrid): use NSButton for cell accessory clicks and brighten chevron contrast * refactor(datagrid): add typeSelect, animated undo insert, defensive row-heights flag * fix(datagrid): row tint refresh on mark-delete and focus-follow on programmatic selection * fix(datagrid): force focus overlay refresh on every selection change * fix(datagrid): keep focus overlay on top via zPosition and defer key-change handler * fix(datagrid): defer focus overlay refresh through every reload path * refactor(datagrid): cell-owned focus border replaces FocusOverlayView synchronization * chore(datagrid): delete dead code surfaced by audit * refactor(datagrid): snapshot theme palette per render pass and weak-cache connection data * refactor(quickswitcher): replace .sheet with NSPanel for Spotlight pattern * fix(switcher): ESC clears search if non-empty otherwise bubbles to dismiss * chore(datagrid): inline single-caller TypePicker and drop TableViewCoordinating protocol * refactor(window): replace custom restoration with NSWindowRestoration end-to-end * refactor(hig): honor Reduce Transparency and Increase Contrast for material backgrounds * refactor(datagrid): drop redundant resize cursor handling and consolidate sort dispatch --------- Signed-off-by: Ngô Quốc Đạt <datlechin@gmail.com>
1 parent eaa10aa commit a61faa4

12 files changed

Lines changed: 110 additions & 482 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2525
- AI providers: Anthropic test connection uses the configured model, known model list updated through Claude 4.7, and Ollama detection now logs the actual error category instead of swallowing every failure as 'not running'
2626
- AI Chat views: replace custom pill buttons with native `.borderless` styles, switch hardcoded text colors to semantic system colors, use relative font sizing in Markdown rendering, align spacing to the 8-pt grid, and add accessibility labels to icon-only buttons
2727
- Translucent backgrounds (Welcome sidebar, settings banners, ER diagram toolbar, JSON editor controls, Pro feature scrim) honor the system Reduce Transparency and Increase Contrast accessibility settings, swapping the material for a solid surface color when either is on
28+
- Internal: result-grid sortable header drops the custom resize cursor handling that duplicated AppKit's built-in column-edge resize, and consolidates three sort delegate methods into one that carries the full sort state. No user-facing change; multi-column sort, shift-click cycle, and the column resize cursor still work the same.
2829

2930
### Fixed
3031

TablePro/Views/Main/Child/DataTabGridDelegate.swift

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@ final class DataTabGridDelegate: DataGridViewDelegate {
1515
var selectionState: GridSelectionState?
1616

1717
var onCellEdit: ((Int, Int, String?) -> Void)?
18-
var onSort: ((Int, Bool, Bool) -> Void)?
19-
var onClearSort: (() -> Void)?
20-
var onRemoveSortColumn: ((Int) -> Void)?
18+
var onSortStateChanged: ((SortState) -> Void)?
2119
var onAddRow: (() -> Void)?
2220
var onUndoInsert: ((Int) -> Void)?
2321
var onFilterColumn: ((String) -> Void)?
@@ -29,16 +27,8 @@ final class DataTabGridDelegate: DataGridViewDelegate {
2927
onCellEdit?(row, column, newValue)
3028
}
3129

32-
func dataGridSort(column: Int, ascending: Bool, isMultiSort: Bool) {
33-
onSort?(column, ascending, isMultiSort)
34-
}
35-
36-
func dataGridClearSort() {
37-
onClearSort?()
38-
}
39-
40-
func dataGridRemoveSortColumn(_ columnIndex: Int) {
41-
onRemoveSortColumn?(columnIndex)
30+
func dataGridSortStateChanged(_ state: SortState) {
31+
onSortStateChanged?(state)
4232
}
4333

4434
func dataGridAddRow() {

TablePro/Views/Main/Child/MainEditorContentView.swift

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,7 @@ struct MainEditorContentView: View {
3636
// MARK: - Callbacks
3737

3838
let onCellEdit: (Int, Int, String?) -> Void
39-
let onSort: (Int, Bool, Bool) -> Void
40-
let onClearSort: () -> Void
41-
let onRemoveSortColumn: (Int) -> Void
39+
let onSortStateChanged: (SortState) -> Void
4240
let onAddRow: () -> Void
4341
let onUndoInsert: (Int) -> Void
4442
let onSelectionChange: (Set<Int>) -> Void
@@ -182,9 +180,7 @@ struct MainEditorContentView: View {
182180
dataTabDelegate.coordinator = coordinator
183181
dataTabDelegate.selectionState = selectionState
184182
dataTabDelegate.onCellEdit = onCellEdit
185-
dataTabDelegate.onSort = onSort
186-
dataTabDelegate.onClearSort = onClearSort
187-
dataTabDelegate.onRemoveSortColumn = onRemoveSortColumn
183+
dataTabDelegate.onSortStateChanged = onSortStateChanged
188184
dataTabDelegate.onUndoInsert = onUndoInsert
189185
dataTabDelegate.onFilterColumn = onFilterColumn
190186
dataTabDelegate.onRefresh = onRefresh

TablePro/Views/Main/MainContentCoordinator.swift

Lines changed: 35 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1154,46 +1154,25 @@ final class MainContentCoordinator {
11541154

11551155
// MARK: - Sorting
11561156

1157-
func handleSort(columnIndex: Int, ascending: Bool, isMultiSort: Bool = false) {
1157+
func handleSortStateChanged(_ newState: SortState) {
11581158
guard let (tab, tabIndex) = tabManager.selectedTabAndIndex else { return }
1159+
guard newState != tab.sortState else { return }
11591160

11601161
let tableRows = tabSessionRegistry.tableRows(for: tab.id)
1161-
guard columnIndex >= 0 && columnIndex < tableRows.columns.count else { return }
11621162

1163-
var currentSort = tab.sortState
1164-
let newDirection: SortDirection = ascending ? .ascending : .descending
1165-
1166-
if isMultiSort {
1167-
// Multi-sort: toggle existing or append new column
1168-
if let existingIndex = currentSort.columns.firstIndex(where: { $0.columnIndex == columnIndex }) {
1169-
if currentSort.columns[existingIndex].direction == newDirection {
1170-
// Same direction clicked again — remove from sort
1171-
currentSort.columns.remove(at: existingIndex)
1172-
} else {
1173-
// Toggle direction
1174-
currentSort.columns[existingIndex].direction = newDirection
1175-
}
1176-
} else {
1177-
// Add new column to sort list
1178-
currentSort.columns.append(SortColumn(columnIndex: columnIndex, direction: newDirection))
1179-
}
1180-
} else {
1181-
// Single sort: replace all with single column
1182-
currentSort = SortState()
1183-
currentSort.columns = [SortColumn(columnIndex: columnIndex, direction: newDirection)]
1184-
}
11851163
if tab.tabType == .query {
1186-
// When more rows are available server-side, re-execute with ORDER BY
1187-
// instead of sorting locally (we only have a partial result set)
1188-
if tab.pagination.hasMoreRows {
1189-
let columnName = tableRows.columns[columnIndex]
1190-
let direction = currentSort.columns.first?.direction == .ascending ? "ASC" : "DESC"
1164+
if !newState.columns.isEmpty && tab.pagination.hasMoreRows {
11911165
let baseQuery = tab.pagination.baseQueryForMore ?? tab.content.query
11921166
let strippedQuery = Self.stripTrailingOrderBy(from: baseQuery)
1193-
let quotedColumn = queryBuilder.quoteIdentifier(columnName)
1194-
let orderQuery = "\(strippedQuery) ORDER BY \(quotedColumn) \(direction)"
1167+
let orderClause = newState.columns.compactMap { sortCol -> String? in
1168+
guard sortCol.columnIndex >= 0, sortCol.columnIndex < tableRows.columns.count else { return nil }
1169+
let columnName = tableRows.columns[sortCol.columnIndex]
1170+
let direction = sortCol.direction == .ascending ? "ASC" : "DESC"
1171+
return "\(queryBuilder.quoteIdentifier(columnName)) \(direction)"
1172+
}.joined(separator: ", ")
1173+
let orderQuery = orderClause.isEmpty ? strippedQuery : "\(strippedQuery) ORDER BY \(orderClause)"
11951174
tabManager.mutate(at: tabIndex) { tab in
1196-
tab.sortState = currentSort
1175+
tab.sortState = newState
11971176
tab.hasUserInteraction = true
11981177
tab.pagination.resetLoadMore()
11991178
tab.content.query = orderQuery
@@ -1202,14 +1181,24 @@ final class MainContentCoordinator {
12021181
return
12031182
}
12041183

1184+
if newState.columns.isEmpty {
1185+
tabManager.mutate(at: tabIndex) { tab in
1186+
tab.sortState = newState
1187+
tab.hasUserInteraction = true
1188+
}
1189+
querySortCache.removeValue(forKey: tab.id)
1190+
dataTabDelegate?.dataGridDidReplaceAllRows()
1191+
return
1192+
}
1193+
12051194
tabManager.mutate(at: tabIndex) { tab in
1206-
tab.sortState = currentSort
1195+
tab.sortState = newState
12071196
tab.hasUserInteraction = true
12081197
tab.pagination.reset()
12091198
}
12101199
let tabId = tab.id
12111200
let schemaVersion = tab.schemaVersion
1212-
let sortColumns = currentSort.columns
1201+
let sortColumns = newState.columns
12131202
let colTypes = tableRows.columnTypes
12141203
let storageRows = tableRows.rows
12151204
let snapshotRows: [(id: RowID, values: [String?])] = storageRows.map { ($0.id, Array($0.values)) }
@@ -1232,9 +1221,8 @@ final class MainContentCoordinator {
12321221

12331222
await MainActor.run { [weak self] in
12341223
guard let self else { return }
1235-
// Guard against stale completion: verify tab still expects this sort
12361224
guard let idx = self.tabManager.tabs.firstIndex(where: { $0.id == tabId }),
1237-
self.tabManager.tabs[idx].sortState == currentSort else {
1225+
self.tabManager.tabs[idx].sortState == newState else {
12381226
return
12391227
}
12401228
self.querySortCache[tabId] = QuerySortCacheEntry(
@@ -1261,16 +1249,21 @@ final class MainContentCoordinator {
12611249
}
12621250

12631251
let tabId = tab.id
1264-
let capturedSort = currentSort
1252+
let capturedSort = newState
12651253
let capturedQuery = tab.content.query
12661254
let capturedColumns = tableRows.columns
12671255
confirmDiscardChangesIfNeeded(action: .sort) { [weak self] confirmed in
12681256
guard let self, confirmed else { return }
1269-
let newQuery = self.queryBuilder.buildMultiSortQuery(
1270-
baseQuery: capturedQuery,
1271-
sortState: capturedSort,
1272-
columns: capturedColumns
1273-
)
1257+
let newQuery: String
1258+
if capturedSort.columns.isEmpty {
1259+
newQuery = Self.stripTrailingOrderBy(from: capturedQuery)
1260+
} else {
1261+
newQuery = self.queryBuilder.buildMultiSortQuery(
1262+
baseQuery: capturedQuery,
1263+
sortState: capturedSort,
1264+
columns: capturedColumns
1265+
)
1266+
}
12741267
guard self.tabManager.mutate(tabId: tabId, { tab in
12751268
tab.sortState = capturedSort
12761269
tab.hasUserInteraction = true
@@ -1281,43 +1274,6 @@ final class MainContentCoordinator {
12811274
}
12821275
}
12831276

1284-
func removeMultiSortColumn(columnIndex: Int) {
1285-
guard let tab = tabManager.selectedTab else { return }
1286-
guard let existing = tab.sortState.columns.first(where: { $0.columnIndex == columnIndex }) else { return }
1287-
let ascending = existing.direction == .ascending
1288-
handleSort(columnIndex: columnIndex, ascending: ascending, isMultiSort: true)
1289-
}
1290-
1291-
func clearSort() {
1292-
guard let (tab, tabIndex) = tabManager.selectedTabAndIndex else { return }
1293-
guard tab.sortState.isSorting else { return }
1294-
1295-
let emptySort = SortState()
1296-
1297-
if tab.tabType == .query {
1298-
tabManager.mutate(at: tabIndex) { tab in
1299-
tab.sortState = emptySort
1300-
tab.hasUserInteraction = true
1301-
}
1302-
querySortCache.removeValue(forKey: tab.id)
1303-
dataTabDelegate?.dataGridDidReplaceAllRows()
1304-
return
1305-
}
1306-
1307-
let tabId = tab.id
1308-
let capturedQuery = tab.content.query
1309-
confirmDiscardChangesIfNeeded(action: .sort) { [weak self] confirmed in
1310-
guard let self, confirmed else { return }
1311-
guard self.tabManager.mutate(tabId: tabId, { tab in
1312-
tab.sortState = emptySort
1313-
tab.hasUserInteraction = true
1314-
tab.pagination.reset()
1315-
tab.content.query = Self.stripTrailingOrderBy(from: capturedQuery)
1316-
}) else { return }
1317-
self.runQuery()
1318-
}
1319-
}
1320-
13211277
/// Multi-column sort returning a permutation of `RowID` (nonisolated for background thread).
13221278
nonisolated private static func multiColumnSortedIDs(
13231279
rows: [(id: RowID, values: [String?])],

TablePro/Views/Main/MainContentView.swift

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -356,16 +356,8 @@ struct MainContentView: View {
356356
rowIndex: rowIndex, columnIndex: colIndex, value: value)
357357
scheduleInspectorUpdate()
358358
},
359-
onSort: { columnIndex, ascending, isMultiSort in
360-
coordinator.handleSort(
361-
columnIndex: columnIndex, ascending: ascending,
362-
isMultiSort: isMultiSort)
363-
},
364-
onClearSort: {
365-
coordinator.clearSort()
366-
},
367-
onRemoveSortColumn: { columnIndex in
368-
coordinator.removeMultiSortColumn(columnIndex: columnIndex)
359+
onSortStateChanged: { newState in
360+
coordinator.handleSortStateChanged(newState)
369361
},
370362
onAddRow: {
371363
coordinator.addNewRow()

TablePro/Views/Results/DataGridViewDelegate.swift

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,7 @@ protocol DataGridViewDelegate: AnyObject {
1818
func dataGridAddRow()
1919
func dataGridUndoInsert(at index: Int)
2020
func dataGridMoveRow(from source: Int, to destination: Int)
21-
func dataGridSort(column: Int, ascending: Bool, isMultiSort: Bool)
22-
func dataGridRemoveSortColumn(_ columnIndex: Int)
23-
func dataGridClearSort()
21+
func dataGridSortStateChanged(_ state: SortState)
2422
func dataGridFilterColumn(_ columnName: String)
2523
func dataGridNavigateFK(value: String, fkInfo: ForeignKeyInfo)
2624
func dataGridDuplicateRow()
@@ -47,9 +45,7 @@ extension DataGridViewDelegate {
4745
func dataGridAddRow() {}
4846
func dataGridUndoInsert(at index: Int) {}
4947
func dataGridMoveRow(from source: Int, to destination: Int) {}
50-
func dataGridSort(column: Int, ascending: Bool, isMultiSort: Bool) {}
51-
func dataGridRemoveSortColumn(_ columnIndex: Int) {}
52-
func dataGridClearSort() {}
48+
func dataGridSortStateChanged(_ state: SortState) {}
5349
func dataGridFilterColumn(_ columnName: String) {}
5450
func dataGridNavigateFK(value: String, fkInfo: ForeignKeyInfo) {}
5551
func dataGridDuplicateRow() {}

TablePro/Views/Results/Extensions/DataGridView+Sort.swift

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,20 +159,35 @@ extension TableViewCoordinator {
159159

160160
@objc func sortAscending(_ sender: NSMenuItem) {
161161
guard let columnIndex = sender.representedObject as? Int else { return }
162-
delegate?.dataGridSort(column: columnIndex, ascending: true, isMultiSort: false)
162+
var state = SortState()
163+
state.columns = [SortColumn(columnIndex: columnIndex, direction: .ascending)]
164+
currentSortState = state
165+
updateSortIndicatorsFromCurrentState()
166+
delegate?.dataGridSortStateChanged(state)
163167
}
164168

165169
@objc func sortDescending(_ sender: NSMenuItem) {
166170
guard let columnIndex = sender.representedObject as? Int else { return }
167-
delegate?.dataGridSort(column: columnIndex, ascending: false, isMultiSort: false)
171+
var state = SortState()
172+
state.columns = [SortColumn(columnIndex: columnIndex, direction: .descending)]
173+
currentSortState = state
174+
updateSortIndicatorsFromCurrentState()
175+
delegate?.dataGridSortStateChanged(state)
168176
}
169177

170178
@objc func showAllColumns() {
171179
delegate?.dataGridShowAllColumns()
172180
}
173181

174182
@objc func clearSortAction() {
175-
delegate?.dataGridClearSort()
183+
currentSortState = SortState()
184+
updateSortIndicatorsFromCurrentState()
185+
delegate?.dataGridSortStateChanged(SortState())
186+
}
187+
188+
private func updateSortIndicatorsFromCurrentState() {
189+
guard let header = tableView?.headerView as? SortableHeaderView else { return }
190+
header.updateSortIndicators(state: currentSortState, schema: identitySchema)
176191
}
177192

178193
@objc func copyColumnName(_ sender: NSMenuItem) {

0 commit comments

Comments
 (0)