Skip to content

Commit cc17e34

Browse files
authored
fix(datagrid): keep a column order whose columns have not changed (#2461)
* fix(datagrid): keep a column order whose columns have not changed * fix(datagrid): drop a saved column order when two columns share a name --------- Signed-off-by: Ngô Quốc Đạt <datlechin@gmail.com>
1 parent 0a1ae5c commit cc17e34

3 files changed

Lines changed: 104 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5050
- Connections strip not scrolling to the entry you switch to.
5151
- Grid cells left at the old column positions until the next click, after a resize, an auto-fit, a reorder, hiding a column, or a row-number width change. (#2449)
5252
- The row-number column draggable out of first place, which walked it to the far right on the next refresh.
53+
- A reordered column snapping back on the next refresh in the Structure tab and in query results.
5354
- Double-clicking a cell editing the wrong row, after deleting one of several rows added in the same session.
5455
- VoiceOver reading a cell's value under a different row's number after such a delete.
5556

TablePro/Views/Results/DataGridCoordinator.swift

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,11 +262,24 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
262262
tabType = configuration.tabType
263263
}
264264

265+
/// A grid with no table behind it keeps a saved column order only while its columns are still the
266+
/// ones that order was saved for.
267+
///
268+
/// A query result's columns are authored by the SELECT list and their order is meaningful, so an
269+
/// order saved for a different set must not be replayed over it: `computeTargetOrder` appends
270+
/// every column the saved order does not name, which landed a newly written column at the far
271+
/// right instead of where it was typed (#1565). Dropping the order outright answered that and
272+
/// also threw away orders the columns had never moved under, so a reorder was captured, persisted
273+
/// and then silently undone on the next update.
265274
func savedColumnLayout(binding: ColumnLayoutState) -> ColumnLayoutState? {
266275
guard tabType == .table else {
267-
guard !binding.columnWidths.isEmpty || binding.columnContentWidths?.isEmpty == false else { return nil }
268276
var layout = binding
269-
layout.columnOrder = nil
277+
if let order = layout.columnOrder, !canRestoreColumnOrder(order) {
278+
layout.columnOrder = nil
279+
}
280+
guard !layout.columnWidths.isEmpty
281+
|| layout.columnContentWidths?.isEmpty == false
282+
|| layout.columnOrder?.isEmpty == false else { return nil }
270283
return layout
271284
}
272285

@@ -281,6 +294,15 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
281294
return binding
282295
}
283296

297+
/// A saved order is a list of names, and a name identifies a column only while the names are
298+
/// unique. `SELECT a.id, b.id` gives two columns called `id`, and `ColumnIdentitySchema` resolves
299+
/// a duplicate name to its last slot, so replaying such an order swaps the two columns.
300+
private func canRestoreColumnOrder(_ order: [String]) -> Bool {
301+
let columns = identitySchema.columnNames
302+
let names = Set(columns)
303+
return names.count == columns.count && Set(order) == names
304+
}
305+
284306
/// A saved width is the width the column had, accessory or not. Nothing here re-derives it from
285307
/// the accessory, because the accessory no longer contributes to column width: a layout saved
286308
/// while the arrow was showing and restored before it is known has to come back the same size.

TableProTests/Views/Results/TableViewCoordinatorLayoutTests.swift

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,12 @@ struct TableViewCoordinatorLayoutTests {
128128
tableName: nil,
129129
persister: FakeColumnLayoutPersister()
130130
)
131+
let rows = TableRows.from(
132+
queryRows: [[.text("1"), .text("direct"), .text("EU")]],
133+
columns: ["id", "business_model", "region"],
134+
columnTypes: Array(repeating: ColumnType.text(rawType: "TEXT"), count: 3)
135+
)
136+
coordinator.rebuildColumnMetadataCache(from: rows)
131137
var binding = ColumnLayoutState()
132138
binding.columnWidths = ["id": 60, "business_model": 120]
133139
binding.columnOrder = ["id", "business_model"]
@@ -138,6 +144,79 @@ struct TableViewCoordinatorLayoutTests {
138144
#expect(coordinator.savedColumnLayout(binding: binding) == expected)
139145
}
140146

147+
/// The order is dropped because the columns moved under it, not because the grid has no table.
148+
/// A re-run of the same query, and every update of the Structure grid, arrives with the same
149+
/// column set, and the reorder the user made has to survive it.
150+
@Test("Query tab keeps a column order saved for the same columns")
151+
func queryTabKeepsMatchingColumnOrder() {
152+
let coordinator = makeCoordinator(
153+
tabType: .query,
154+
connectionId: nil,
155+
tableName: nil,
156+
persister: FakeColumnLayoutPersister()
157+
)
158+
let rows = TableRows.from(
159+
queryRows: [[.text("1"), .text("direct")]],
160+
columns: ["id", "business_model"],
161+
columnTypes: Array(repeating: ColumnType.text(rawType: "TEXT"), count: 2)
162+
)
163+
coordinator.rebuildColumnMetadataCache(from: rows)
164+
var binding = ColumnLayoutState()
165+
binding.columnWidths = ["id": 60, "business_model": 120]
166+
binding.columnOrder = ["business_model", "id"]
167+
168+
#expect(coordinator.savedColumnLayout(binding: binding) == binding)
169+
}
170+
171+
/// A saved order names its columns, and `SELECT a.id, b.id` gives two of them the same name.
172+
/// `ColumnIdentitySchema` resolves a duplicate to its last slot, so replaying such an order
173+
/// silently swaps the pair.
174+
@Test("Query tab drops a column order when two columns share a name")
175+
func queryTabDropsColumnOrderForDuplicateNames() {
176+
let coordinator = makeCoordinator(
177+
tabType: .query,
178+
connectionId: nil,
179+
tableName: nil,
180+
persister: FakeColumnLayoutPersister()
181+
)
182+
let rows = TableRows.from(
183+
queryRows: [[.text("1"), .text("2")]],
184+
columns: ["id", "id"],
185+
columnTypes: Array(repeating: ColumnType.text(rawType: "TEXT"), count: 2)
186+
)
187+
coordinator.rebuildColumnMetadataCache(from: rows)
188+
var binding = ColumnLayoutState()
189+
binding.columnWidths = ["id": 60]
190+
binding.columnOrder = ["id", "id"]
191+
192+
var expected = ColumnLayoutState()
193+
expected.columnWidths = ["id": 60]
194+
195+
#expect(coordinator.savedColumnLayout(binding: binding) == expected)
196+
}
197+
198+
/// A reorder with no width change is the whole layout, and it used to fall through the
199+
/// emptiness guard and come back as nil.
200+
@Test("Query tab keeps an order-only layout")
201+
func queryTabKeepsAnOrderOnlyLayout() {
202+
let coordinator = makeCoordinator(
203+
tabType: .query,
204+
connectionId: nil,
205+
tableName: nil,
206+
persister: FakeColumnLayoutPersister()
207+
)
208+
let rows = TableRows.from(
209+
queryRows: [[.text("1"), .text("direct")]],
210+
columns: ["id", "business_model"],
211+
columnTypes: Array(repeating: ColumnType.text(rawType: "TEXT"), count: 2)
212+
)
213+
coordinator.rebuildColumnMetadataCache(from: rows)
214+
var binding = ColumnLayoutState()
215+
binding.columnOrder = ["business_model", "id"]
216+
217+
#expect(coordinator.savedColumnLayout(binding: binding) == binding)
218+
}
219+
141220
@Test("Query tab keeps remembered widths when there is no saved order")
142221
func queryTabKeepsWidths() {
143222
let coordinator = makeCoordinator(

0 commit comments

Comments
 (0)