Skip to content

Commit 45c5181

Browse files
committed
refactor(datagrid): undo replay returns Delta and supports non-tail insert
1 parent b4c1643 commit 45c5181

4 files changed

Lines changed: 150 additions & 20 deletions

File tree

TablePro/Core/ChangeTracking/DataChangeManager.swift

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,21 @@ struct UndoResult {
1717
let needsRowRemoval: Bool
1818
let needsRowRestore: Bool
1919
let restoreRow: [String?]?
20+
let delta: Delta
21+
22+
init(
23+
action: UndoAction,
24+
needsRowRemoval: Bool,
25+
needsRowRestore: Bool,
26+
restoreRow: [String?]?,
27+
delta: Delta = .none
28+
) {
29+
self.action = action
30+
self.needsRowRemoval = needsRowRemoval
31+
self.needsRowRestore = needsRowRestore
32+
self.restoreRow = restoreRow
33+
self.delta = delta
34+
}
2035
}
2136

2237
/// Manager for tracking and applying data changes
@@ -222,7 +237,6 @@ final class DataChangeManager: ChangeManaging {
222237
}
223238

224239
hasChanges = !pending.isEmpty
225-
reloadVersion += 1
226240

227241
if let result = lastUndoResult {
228242
onUndoApplied?(result)
@@ -259,7 +273,10 @@ final class DataChangeManager: ChangeManaging {
259273
originalDBValue: newValue, newValue: previousValue, originalRow: originalRow
260274
)
261275
}
262-
lastUndoResult = UndoResult(action: action, needsRowRemoval: false, needsRowRestore: false, restoreRow: nil)
276+
lastUndoResult = UndoResult(
277+
action: action, needsRowRemoval: false, needsRowRestore: false, restoreRow: nil,
278+
delta: .cellChanged(row: rowIndex, column: columnIndex)
279+
)
263280
}
264281

265282
private func applyRowInsertionUndo(rowIndex: Int, action: UndoAction) {
@@ -274,12 +291,14 @@ final class DataChangeManager: ChangeManaging {
274291
if pending.isRowInserted(rowIndex) {
275292
_ = pending.undoRowInsertion(rowIndex: rowIndex)
276293
lastUndoResult = UndoResult(
277-
action: action, needsRowRemoval: true, needsRowRestore: false, restoreRow: nil
294+
action: action, needsRowRemoval: true, needsRowRestore: false, restoreRow: nil,
295+
delta: .rowsRemoved(IndexSet(integer: rowIndex))
278296
)
279297
} else {
280298
pending.reinsertRow(rowIndex: rowIndex, columns: columns, savedValues: savedValues)
281299
lastUndoResult = UndoResult(
282-
action: action, needsRowRemoval: false, needsRowRestore: true, restoreRow: savedValues
300+
action: action, needsRowRemoval: false, needsRowRestore: true, restoreRow: savedValues,
301+
delta: .rowsInserted(IndexSet(integer: rowIndex))
283302
)
284303
}
285304
}
@@ -292,12 +311,14 @@ final class DataChangeManager: ChangeManaging {
292311
if pending.isRowDeleted(rowIndex) {
293312
_ = pending.undoRowDeletion(rowIndex: rowIndex)
294313
lastUndoResult = UndoResult(
295-
action: action, needsRowRemoval: false, needsRowRestore: true, restoreRow: originalRow
314+
action: action, needsRowRemoval: false, needsRowRestore: true, restoreRow: originalRow,
315+
delta: .fullReplace
296316
)
297317
} else {
298318
pending.reapplyRowDeletion(rowIndex: rowIndex, originalRow: originalRow)
299319
lastUndoResult = UndoResult(
300-
action: action, needsRowRemoval: true, needsRowRestore: false, restoreRow: nil
320+
action: action, needsRowRemoval: true, needsRowRestore: false, restoreRow: nil,
321+
delta: .fullReplace
301322
)
302323
}
303324
}
@@ -315,14 +336,16 @@ final class DataChangeManager: ChangeManaging {
315336
_ = pending.undoRowDeletion(rowIndex: rowIndex)
316337
}
317338
lastUndoResult = UndoResult(
318-
action: action, needsRowRemoval: false, needsRowRestore: true, restoreRow: nil
339+
action: action, needsRowRemoval: false, needsRowRestore: true, restoreRow: nil,
340+
delta: .fullReplace
319341
)
320342
} else {
321343
for (rowIndex, originalRow) in rows {
322344
pending.reapplyRowDeletion(rowIndex: rowIndex, originalRow: originalRow)
323345
}
324346
lastUndoResult = UndoResult(
325-
action: action, needsRowRemoval: true, needsRowRestore: false, restoreRow: nil
347+
action: action, needsRowRemoval: true, needsRowRestore: false, restoreRow: nil,
348+
delta: .fullReplace
326349
)
327350
}
328351
}
@@ -335,15 +358,18 @@ final class DataChangeManager: ChangeManaging {
335358
}
336359

337360
let firstInserted = rowIndices.first.map { pending.isRowInserted($0) } ?? false
361+
let indices = IndexSet(rowIndices)
338362
if firstInserted {
339363
_ = pending.undoBatchRowInsertion(rowIndices: rowIndices, columnCount: columns.count)
340364
lastUndoResult = UndoResult(
341-
action: action, needsRowRemoval: true, needsRowRestore: false, restoreRow: nil
365+
action: action, needsRowRemoval: true, needsRowRestore: false, restoreRow: nil,
366+
delta: .rowsRemoved(indices)
342367
)
343368
} else {
344369
pending.reinsertBatch(rowIndices: rowIndices, rowValues: rowValues, columns: columns)
345370
lastUndoResult = UndoResult(
346-
action: action, needsRowRemoval: false, needsRowRestore: true, restoreRow: nil
371+
action: action, needsRowRemoval: false, needsRowRestore: true, restoreRow: nil,
372+
delta: .rowsInserted(indices)
347373
)
348374
}
349375
}

TablePro/Core/Services/Query/RowOperationsManager.swift

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -176,19 +176,16 @@ final class RowOperationsManager {
176176
} else if result.needsRowRestore {
177177
let columnCount = tableRows.columns.count
178178
let values = result.restoreRow ?? [String?](repeating: nil, count: columnCount)
179-
guard rowIndex >= 0, rowIndex == tableRows.count else {
180-
return UndoApplicationResult(adjustedSelection: nil, delta: .none)
181-
}
182-
let delta = tableRows.appendInsertedRow(values: values)
179+
let delta = tableRows.insertInsertedRow(at: rowIndex, values: values)
183180
return UndoApplicationResult(adjustedSelection: nil, delta: delta)
184181
}
185182
return UndoApplicationResult(adjustedSelection: nil, delta: .none)
186183

187184
case .rowDeletion:
188-
return UndoApplicationResult(adjustedSelection: nil, delta: .none)
185+
return UndoApplicationResult(adjustedSelection: nil, delta: result.delta)
189186

190187
case .batchRowDeletion:
191-
return UndoApplicationResult(adjustedSelection: nil, delta: .none)
188+
return UndoApplicationResult(adjustedSelection: nil, delta: result.delta)
192189

193190
case .batchRowInsertion(let rowIndices, let rowValues):
194191
if result.needsRowRemoval {
@@ -200,10 +197,10 @@ final class RowOperationsManager {
200197
return UndoApplicationResult(adjustedSelection: nil, delta: delta)
201198
} else if result.needsRowRestore {
202199
var insertedIndices = IndexSet()
203-
for (index, rowIndex) in rowIndices.enumerated() {
204-
guard index < rowValues.count else { continue }
205-
guard rowIndex == tableRows.count else { continue }
206-
_ = tableRows.appendInsertedRow(values: rowValues[index])
200+
let pairs = zip(rowIndices, rowValues).sorted { $0.0 < $1.0 }
201+
for (rowIndex, values) in pairs {
202+
guard rowIndex >= 0, rowIndex <= tableRows.count else { continue }
203+
_ = tableRows.insertInsertedRow(at: rowIndex, values: values)
207204
insertedIndices.insert(rowIndex)
208205
}
209206
guard !insertedIndices.isEmpty else {

TablePro/Models/Query/TableRows.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,15 @@ struct TableRows: Sendable {
7272
return .rowsInserted(IndexSet(integer: rows.count - 1))
7373
}
7474

75+
@discardableResult
76+
mutating func insertInsertedRow(at index: Int, values: [String?]) -> Delta {
77+
guard index >= 0, index <= rows.count else { return .none }
78+
let normalized = Self.normalize(values: values, toCount: columns.count)
79+
let row = Row(id: .inserted(UUID()), values: normalized)
80+
rows.insert(row, at: index)
81+
return .rowsInserted(IndexSet(integer: index))
82+
}
83+
7584
@discardableResult
7685
mutating func appendPage(_ pageRows: [[String?]], startingAt offset: Int) -> Delta {
7786
guard !pageRows.isEmpty else { return .none }

TableProTests/Models/Query/TableRowsTests.swift

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,104 @@ struct TableRowsInsertTests {
207207
#expect(table.rows[0].values == ["only-one", nil, nil])
208208
#expect(table.rows[1].values == ["a", "b", "c"])
209209
}
210+
211+
@Test("insertInsertedRow at the head shifts existing rows down")
212+
func insertInsertedRowAtHead() {
213+
var table = TableRows.from(
214+
queryRows: [["a"], ["b"]],
215+
columns: ["c1"],
216+
columnTypes: [.text(rawType: nil)]
217+
)
218+
let delta = table.insertInsertedRow(at: 0, values: ["z"])
219+
#expect(delta == .rowsInserted(IndexSet(integer: 0)))
220+
#expect(table.count == 3)
221+
#expect(table.rows[0].values == ["z"])
222+
#expect(table.rows[0].id.isInserted)
223+
#expect(table.rows[1].values == ["a"])
224+
#expect(table.rows[2].values == ["b"])
225+
}
226+
227+
@Test("insertInsertedRow in the middle preserves surrounding rows")
228+
func insertInsertedRowInMiddle() {
229+
var table = TableRows.from(
230+
queryRows: [["a"], ["b"], ["c"]],
231+
columns: ["c1"],
232+
columnTypes: [.text(rawType: nil)]
233+
)
234+
let delta = table.insertInsertedRow(at: 1, values: ["z"])
235+
#expect(delta == .rowsInserted(IndexSet(integer: 1)))
236+
#expect(table.count == 4)
237+
#expect(table.rows[0].values == ["a"])
238+
#expect(table.rows[1].values == ["z"])
239+
#expect(table.rows[1].id.isInserted)
240+
#expect(table.rows[2].values == ["b"])
241+
#expect(table.rows[3].values == ["c"])
242+
}
243+
244+
@Test("insertInsertedRow at the tail (index == count) appends")
245+
func insertInsertedRowAtTail() {
246+
var table = TableRows.from(
247+
queryRows: [["a"]],
248+
columns: ["c1"],
249+
columnTypes: [.text(rawType: nil)]
250+
)
251+
let delta = table.insertInsertedRow(at: table.count, values: ["z"])
252+
#expect(delta == .rowsInserted(IndexSet(integer: 1)))
253+
#expect(table.count == 2)
254+
#expect(table.rows[1].values == ["z"])
255+
#expect(table.rows[1].id.isInserted)
256+
}
257+
258+
@Test("insertInsertedRow pads short values and truncates long values")
259+
func insertInsertedRowPadsAndTruncates() {
260+
var table = TableRows.from(
261+
queryRows: [],
262+
columns: ["c1", "c2", "c3"],
263+
columnTypes: [.text(rawType: nil), .text(rawType: nil), .text(rawType: nil)]
264+
)
265+
_ = table.insertInsertedRow(at: 0, values: ["only-one"])
266+
_ = table.insertInsertedRow(at: 1, values: ["a", "b", "c", "d"])
267+
#expect(table.rows[0].values == ["only-one", nil, nil])
268+
#expect(table.rows[1].values == ["a", "b", "c"])
269+
}
270+
271+
@Test("insertInsertedRow with negative index returns Delta.none and does not mutate")
272+
func insertInsertedRowNegativeIndexIsNoOp() {
273+
var table = TableRows.from(
274+
queryRows: [["a"]],
275+
columns: ["c1"],
276+
columnTypes: [.text(rawType: nil)]
277+
)
278+
let delta = table.insertInsertedRow(at: -1, values: ["z"])
279+
#expect(delta == .none)
280+
#expect(table.count == 1)
281+
#expect(table.rows[0].values == ["a"])
282+
}
283+
284+
@Test("insertInsertedRow past the end returns Delta.none and does not mutate")
285+
func insertInsertedRowPastEndIsNoOp() {
286+
var table = TableRows.from(
287+
queryRows: [["a"]],
288+
columns: ["c1"],
289+
columnTypes: [.text(rawType: nil)]
290+
)
291+
let delta = table.insertInsertedRow(at: 2, values: ["z"])
292+
#expect(delta == .none)
293+
#expect(table.count == 1)
294+
#expect(table.rows[0].values == ["a"])
295+
}
296+
297+
@Test("Two insertInsertedRow calls produce different RowID UUIDs")
298+
func insertInsertedRowProducesDistinctUUIDs() {
299+
var table = TableRows.from(
300+
queryRows: [],
301+
columns: ["c1"],
302+
columnTypes: [.text(rawType: nil)]
303+
)
304+
_ = table.insertInsertedRow(at: 0, values: ["x"])
305+
_ = table.insertInsertedRow(at: 0, values: ["y"])
306+
#expect(table.rows[0].id != table.rows[1].id)
307+
}
210308
}
211309

212310
@Suite("TableRows - appendPage")

0 commit comments

Comments
 (0)