Skip to content

Commit 0af2e8f

Browse files
committed
fix(datagrid): scope pendingScrollToTopAfterReplace per-tabId, add dispatch tests
The original Bool flag could strand if pagination's runQuery failed before setActiveTableRows fired — the flag would stay set, and the next setActiveTableRows from any other code path (undo, FK navigation, multi-statement query) would unexpectedly scroll to top. Changing to Set<UUID> scopes the intent per-tab. Pagination inserts the tabId; notifyFullReplaceIfActive checks via remove() and only dispatches scrollToTop on a matching hit. A stranded entry stays scoped to that one tab and only fires when that tab next receives a full replace — narrow blast radius. Adds three regression tests: - scrollToTopFiresOnPendingFlag: setActiveTableRows on the active tab WITH the flag set dispatches scrollToTop and clears the flag - scrollToTopFlagIsScopedPerTab: a flag set for tab A doesn't fire when tab B is replaced; tab A's entry stays - scrollToTopSkippedWhenFlagAbsent: setActiveTableRows without the flag doesn't scroll
1 parent 2e59834 commit 0af2e8f

4 files changed

Lines changed: 42 additions & 4 deletions

File tree

TablePro/Views/Main/Extensions/MainContentCoordinator+Pagination.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ extension MainContentCoordinator {
9797

9898
mutate(&self.tabManager.tabs[idx].pagination)
9999
self.tabManager.tabs[idx].paginationVersion += 1
100-
self.pendingScrollToTopAfterReplace = true
100+
self.pendingScrollToTopAfterReplace.insert(tabId)
101101
self.reloadCurrentPage()
102102
}
103103
}

TablePro/Views/Main/Extensions/MainContentCoordinator+TableRowsMutation.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,7 @@ extension MainContentCoordinator {
4545
idx < tabManager.tabs.count,
4646
tabManager.tabs[idx].id == tabId else { return }
4747
dataTabDelegate?.tableViewCoordinator?.applyFullReplace()
48-
if pendingScrollToTopAfterReplace {
49-
pendingScrollToTopAfterReplace = false
48+
if pendingScrollToTopAfterReplace.remove(tabId) != nil {
5049
dataTabDelegate?.tableViewCoordinator?.scrollToTop()
5150
}
5251
}

TablePro/Views/Main/MainContentCoordinator.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ final class MainContentCoordinator {
147147
/// Cache for async-sorted query tab rows (large datasets sorted on background thread)
148148
@ObservationIgnored var querySortCache: [UUID: QuerySortCacheEntry] = [:]
149149

150-
@ObservationIgnored var pendingScrollToTopAfterReplace: Bool = false
150+
@ObservationIgnored var pendingScrollToTopAfterReplace: Set<UUID> = []
151151

152152
// MARK: - Internal State
153153

TableProTests/Views/Main/TableRowsMutationTests.swift

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,45 @@ struct TableRowsMutationTests {
111111
#expect(f.fake.fullReplaceCount == 2)
112112
}
113113

114+
@Test("setActiveTableRows dispatches scrollToTop when pendingScrollToTopAfterReplace contains tabId")
115+
func scrollToTopFiresOnPendingFlag() {
116+
let f = makeFixture()
117+
f.tabManager.addTableTab(tableName: "users")
118+
let activeTabId = f.tabManager.tabs[0].id
119+
120+
f.coordinator.pendingScrollToTopAfterReplace.insert(activeTabId)
121+
f.coordinator.setActiveTableRows(makeTableRows(rowCount: 3), for: activeTabId)
122+
123+
#expect(f.fake.scrollToTopCount == 1)
124+
#expect(f.coordinator.pendingScrollToTopAfterReplace.contains(activeTabId) == false)
125+
}
126+
127+
@Test("scrollToTop pending flag for tab A does not fire when tab B is replaced")
128+
func scrollToTopFlagIsScopedPerTab() {
129+
let f = makeFixture()
130+
f.tabManager.addTableTab(tableName: "users")
131+
let firstTabId = f.tabManager.tabs[0].id
132+
f.tabManager.addTableTab(tableName: "orders")
133+
let secondTabId = f.tabManager.tabs[1].id
134+
135+
f.coordinator.pendingScrollToTopAfterReplace.insert(firstTabId)
136+
f.coordinator.setActiveTableRows(makeTableRows(rowCount: 3), for: secondTabId)
137+
138+
#expect(f.fake.scrollToTopCount == 0)
139+
#expect(f.coordinator.pendingScrollToTopAfterReplace.contains(firstTabId) == true)
140+
}
141+
142+
@Test("setActiveTableRows without pending flag does not scroll to top")
143+
func scrollToTopSkippedWhenFlagAbsent() {
144+
let f = makeFixture()
145+
f.tabManager.addTableTab(tableName: "users")
146+
let activeTabId = f.tabManager.tabs[0].id
147+
148+
f.coordinator.setActiveTableRows(makeTableRows(rowCount: 3), for: activeTabId)
149+
150+
#expect(f.fake.scrollToTopCount == 0)
151+
}
152+
114153
@Test("setActiveTableRows is a no-op when delegate is unwired")
115154
func unwiredDelegateIsNoOp() {
116155
let tabManager = QueryTabManager()

0 commit comments

Comments
 (0)