Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@ import Foundation
extension MainContentCoordinator {
func addNewRow() {
guard !safeModeLevel.blocksAllWrites,
let tabIndex = tabManager.selectedTabIndex,
tabIndex < tabManager.tabs.count else { return }

let tab = tabManager.tabs[tabIndex]
guard tab.tableContext.isEditable, tab.tableContext.tableName != nil else { return }
let (tab, tabIndex) = tabManager.selectedTabAndIndex,
tab.tableContext.isEditable,
tab.tableContext.tableName != nil else { return }

let tabId = tab.id
let columnDefaults = tableRowsStore.tableRows(for: tabId).columnDefaults
Expand Down Expand Up @@ -37,12 +35,11 @@ extension MainContentCoordinator {

func deleteSelectedRows(indices: Set<Int>) {
guard !safeModeLevel.blocksAllWrites,
let tabIndex = tabManager.selectedTabIndex,
tabIndex < tabManager.tabs.count,
tabManager.tabs[tabIndex].tableContext.isEditable,
let (tab, tabIndex) = tabManager.selectedTabAndIndex,
tab.tableContext.isEditable,
!indices.isEmpty else { return }

let tabId = tabManager.tabs[tabIndex].id
let tabId = tab.id

var deleteResult = RowOperationsManager.DeleteRowsResult(
nextRowToSelect: -1,
Expand Down Expand Up @@ -77,11 +74,9 @@ extension MainContentCoordinator {

func duplicateSelectedRow(index: Int) {
guard !safeModeLevel.blocksAllWrites,
let tabIndex = tabManager.selectedTabIndex,
tabIndex < tabManager.tabs.count else { return }

let tab = tabManager.tabs[tabIndex]
guard tab.tableContext.isEditable, tab.tableContext.tableName != nil else { return }
let (tab, tabIndex) = tabManager.selectedTabAndIndex,
tab.tableContext.isEditable,
tab.tableContext.tableName != nil else { return }

let tabId = tab.id
let columns = tableRowsStore.tableRows(for: tabId).columns
Expand Down Expand Up @@ -110,10 +105,8 @@ extension MainContentCoordinator {
}

func undoInsertRow(at rowIndex: Int) {
guard let tabIndex = tabManager.selectedTabIndex,
tabIndex < tabManager.tabs.count else { return }

let tabId = tabManager.tabs[tabIndex].id
guard let (tab, _) = tabManager.selectedTabAndIndex else { return }
let tabId = tab.id

var undoResult = RowOperationsManager.UndoInsertRowResult(
adjustedSelection: selectionState.indices,
Expand All @@ -135,10 +128,8 @@ extension MainContentCoordinator {
}

func handleUndoResult(_ result: UndoResult) {
guard let tabIndex = tabManager.selectedTabIndex,
tabIndex < tabManager.tabs.count else { return }
guard let (tab, tabIndex) = tabManager.selectedTabAndIndex else { return }

let tab = tabManager.tabs[tabIndex]
let tabId = tab.id

var application = RowOperationsManager.UndoApplicationResult(adjustedSelection: nil, delta: .none)
Expand All @@ -159,10 +150,7 @@ extension MainContentCoordinator {
}

func copySelectedRowsToClipboard(indices: Set<Int>) {
guard let index = tabManager.selectedTabIndex,
!indices.isEmpty else { return }

let tab = tabManager.tabs[index]
guard let (tab, _) = tabManager.selectedTabAndIndex, !indices.isEmpty else { return }
let tableRows = tableRowsStore.tableRows(for: tab.id)
rowOperationsManager.copySelectedRowsToClipboard(
selectedIndices: indices,
Expand All @@ -171,10 +159,7 @@ extension MainContentCoordinator {
}

func copySelectedRowsWithHeaders(indices: Set<Int>) {
guard let index = tabManager.selectedTabIndex,
!indices.isEmpty else { return }

let tab = tabManager.tabs[index]
guard let (tab, _) = tabManager.selectedTabAndIndex, !indices.isEmpty else { return }
let tableRows = tableRowsStore.tableRows(for: tab.id)
rowOperationsManager.copySelectedRowsToClipboard(
selectedIndices: indices,
Expand All @@ -184,9 +169,7 @@ extension MainContentCoordinator {
}

func copySelectedRowsAsJson(indices: Set<Int>) {
guard let index = tabManager.selectedTabIndex,
!indices.isEmpty else { return }
let tab = tabManager.tabs[index]
guard let (tab, _) = tabManager.selectedTabAndIndex, !indices.isEmpty else { return }
let tableRows = tableRowsStore.tableRows(for: tab.id)
let rows = indices.sorted().compactMap { idx -> [String?]? in
guard idx >= 0, idx < tableRows.count else { return nil }
Expand All @@ -202,10 +185,8 @@ extension MainContentCoordinator {

func pasteRows() {
guard !safeModeLevel.blocksAllWrites,
let index = tabManager.selectedTabIndex else { return }

let tab = tabManager.tabs[index]
guard tab.tabType == .table else { return }
let (tab, tabIndex) = tabManager.selectedTabAndIndex,
tab.tabType == .table else { return }

let tabId = tab.id
let columns = tableRowsStore.tableRows(for: tabId).columns
Expand All @@ -226,19 +207,19 @@ extension MainContentCoordinator {
let newIndices = Set(pasteResult.pastedRows.map { $0.rowIndex })
selectionState.indices = newIndices

tabManager.tabs[index].selectedRowIndices = newIndices
tabManager.tabs[index].hasUserInteraction = true
tabManager.tabs[tabIndex].selectedRowIndices = newIndices
tabManager.tabs[tabIndex].hasUserInteraction = true
querySortCache.removeValue(forKey: tabId)
dataTabDelegate?.tableViewCoordinator?.applyDelta(pasteResult.delta)
}

func updateCellInTab(rowIndex: Int, columnIndex: Int, value: String?) {
guard let index = tabManager.selectedTabIndex else { return }
let tabId = tabManager.tabs[index].id
guard let (tab, tabIndex) = tabManager.selectedTabAndIndex else { return }
let tabId = tab.id
let delta = mutateActiveTableRows(for: tabId) { rows in
rows.edit(row: rowIndex, column: columnIndex, value: value)
}
tabManager.tabs[index].hasUserInteraction = true
tabManager.tabs[tabIndex].hasUserInteraction = true
dataTabDelegate?.tableViewCoordinator?.applyDelta(delta)
}
}
7 changes: 7 additions & 0 deletions TablePro/Views/Results/DataGridCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,13 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData
displayCache.removeAll()
rebuildVisualStateCache()
updateCache()
guard let tableView else { return }
let visibleRange = tableView.rows(in: tableView.visibleRect)
guard visibleRange.length > 0 else { return }
tableView.reloadData(
forRowIndexes: IndexSet(integersIn: visibleRange.location..<(visibleRange.location + visibleRange.length)),
columnIndexes: IndexSet(integersIn: 0..<tableView.numberOfColumns)
)
}

func commitActiveCellEdit() {
Expand Down
113 changes: 113 additions & 0 deletions TableProTests/Views/Main/RowOperationsDispatchTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
//
// RowOperationsDispatchTests.swift
// TableProTests
//
// Locks the dispatch wiring from RowOperations into TableViewCoordinating.
// These tests guard the path that PR #938 (Phase D-b) accidentally severed:
// invalidateCachesForUndoRedo must fire on soft-delete (existing rows) so the
// red row background and yellow modified marker propagate to NSTableView's
// visible cell views without requiring a tab switch or scroll-recycle.
//

import Foundation
import Testing
@testable import TablePro

@MainActor
private final class FakeTableViewCoordinator: TableViewCoordinating {
var fullReplaceCount = 0
var insertedCount = 0
var removedCount = 0
var deltaCount = 0
var invalidateCount = 0
var commitEditCount = 0
var refreshFKCount = 0
var scrollToTopCount = 0
var beginEditingCalls: [(row: Int, column: Int)] = []

func applyInsertedRows(_ indices: IndexSet) { insertedCount += 1 }
func applyRemovedRows(_ indices: IndexSet) { removedCount += 1 }
func applyFullReplace() { fullReplaceCount += 1 }
func applyDelta(_ delta: Delta) { deltaCount += 1 }
func invalidateCachesForUndoRedo() { invalidateCount += 1 }
func commitActiveCellEdit() { commitEditCount += 1 }
func beginEditing(displayRow: Int, column: Int) {
beginEditingCalls.append((row: displayRow, column: column))
}
func refreshForeignKeyColumns() { refreshFKCount += 1 }
func scrollToTop() { scrollToTopCount += 1 }
}

@Suite("RowOperations dispatch")
@MainActor
struct RowOperationsDispatchTests {
private struct Fixture {
let coordinator: MainContentCoordinator
let tabManager: QueryTabManager
let delegate: DataTabGridDelegate
let fake: FakeTableViewCoordinator
let tabId: UUID
}

private func makeFixture(rowCount: Int = 5) -> Fixture {
let tabManager = QueryTabManager()
let coordinator = MainContentCoordinator(
connection: TestFixtures.makeConnection(),
tabManager: tabManager,
changeManager: DataChangeManager(),
filterStateManager: FilterStateManager(),
columnVisibilityManager: ColumnVisibilityManager(),
toolbarState: ConnectionToolbarState()
)
let delegate = DataTabGridDelegate()
let fake = FakeTableViewCoordinator()
delegate.tableViewCoordinator = fake
coordinator.dataTabDelegate = delegate

tabManager.addTableTab(tableName: "users")
let tabIndex = tabManager.selectedTabIndex ?? 0
tabManager.tabs[tabIndex].tableContext.isEditable = true
let tabId = tabManager.tabs[tabIndex].id

let columns = ["id", "name"]
let rows = (0..<rowCount).map { i in ["\(i)", "name\(i)"] }
let columnTypes: [ColumnType] = Array(repeating: .text(rawType: nil), count: columns.count)
coordinator.setActiveTableRows(
TableRows.from(queryRows: rows, columns: columns, columnTypes: columnTypes),
for: tabId
)

return Fixture(
coordinator: coordinator,
tabManager: tabManager,
delegate: delegate,
fake: fake,
tabId: tabId
)
}

@Test("Soft-delete of existing rows dispatches invalidateCachesForUndoRedo")
func softDeleteDispatchesInvalidate() {
let f = makeFixture(rowCount: 5)
let beforeInvalidate = f.fake.invalidateCount

f.coordinator.deleteSelectedRows(indices: [0, 1])

#expect(f.fake.invalidateCount == beforeInvalidate + 1)
#expect(f.fake.deltaCount == 0)
}

@Test("Physical delete of inserted rows dispatches applyDelta, not invalidate")
func physicalDeleteDispatchesDelta() {
let f = makeFixture(rowCount: 3)
f.coordinator.addNewRow()
let insertedIndex = f.coordinator.tableRowsStore.tableRows(for: f.tabId).count - 1
let beforeInvalidate = f.fake.invalidateCount
let beforeDelta = f.fake.deltaCount

f.coordinator.deleteSelectedRows(indices: [insertedIndex])

#expect(f.fake.invalidateCount == beforeInvalidate)
#expect(f.fake.deltaCount == beforeDelta + 1)
}
}
Loading