Skip to content

Commit ce570a9

Browse files
authored
refactor(coordinator): extract paginateIfPossible helper, add selectedTabAndIndex (#939)
* refactor(coordinator): extract paginateIfPossible helper, add selectedTabAndIndex Pagination had seven methods (goToNextPage, goToPreviousPage, goToFirstPage, goToLastPage, updatePageSize, updateOffset, applyPaginationSettings) where six followed the same shape: guard let tabIndex = tabManager.selectedTabIndex, tabIndex < tabManager.tabs.count, tabManager.tabs[tabIndex].pagination.<condition> else { return } paginateAfterConfirmation(tabIndex: tabIndex) { pagination in pagination.<action>() } Six guard prologues and six tail calls collapse into one-line bodies via: - New QueryTabManager.selectedTabAndIndex helper that returns (tab, index) atomically with the bounds check, replacing the two-step (selectedTabIndex + bounds-check + tabs[index]) pattern. - New private paginateIfPossible(where:mutate:) that captures the selected-tab + condition + paginateAfterConfirmation chain. Each public pagination method becomes a one-liner using a KeyPath or short closure for the precondition. Behavior unchanged. -30 LOC in Pagination, +5 in QueryTabManager. Smoke-tested all six pagination actions plus offset Go. selectedTabAndIndex is also a foothold for further coordinator dedup — there are ~14 other selectedTabIndex + bounds-check sites in MainContentCoordinator extensions that could migrate, but each has slightly different surrounding logic so they're left for separate focused PRs. * test(coordinator): cover QueryTabManager.selectedTabAndIndex contract Four cases: - nilWhenNoSelection: empty manager returns nil - returnsSelectedTabAfterAdd: addTableTab autoselects the new tab; helper returns it with index 0 - nilWhenSelectionIsStale: if selectedTabId points to a removed tab, the bounds check kicks in and returns nil rather than crashing or returning stale data - returnsCorrectPairAfterSwitch: explicit selectedTabId assignment resolves to the matching (tab, index) pair The staleness test is the load-bearing one — it locks the contract that future migrations of the other ~14 selectedTabIndex + bounds-check sites can rely on. Without this, a refactor that loosens the bounds check could silently regress.
1 parent b87be8a commit ce570a9

3 files changed

Lines changed: 82 additions & 52 deletions

File tree

TablePro/Models/Query/QueryTabManager.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ final class QueryTabManager {
4545
return _tabIndexMap[id]
4646
}
4747

48+
var selectedTabAndIndex: (tab: QueryTab, index: Int)? {
49+
guard let index = selectedTabIndex, index < tabs.count else { return nil }
50+
return (tabs[index], index)
51+
}
52+
4853
init() {
4954
tabs = []
5055
selectedTabId = nil

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

Lines changed: 17 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -10,82 +10,47 @@ import Foundation
1010
extension MainContentCoordinator {
1111
// MARK: - Pagination
1212

13-
/// Navigate to next page
1413
func goToNextPage() {
15-
guard let tabIndex = tabManager.selectedTabIndex,
16-
tabIndex < tabManager.tabs.count,
17-
tabManager.tabs[tabIndex].pagination.hasNextPage else { return }
18-
19-
paginateAfterConfirmation(tabIndex: tabIndex) { pagination in
20-
pagination.goToNextPage()
21-
}
14+
paginateIfPossible(where: \.hasNextPage) { $0.goToNextPage() }
2215
}
2316

24-
/// Navigate to previous page
2517
func goToPreviousPage() {
26-
guard let tabIndex = tabManager.selectedTabIndex,
27-
tabIndex < tabManager.tabs.count,
28-
tabManager.tabs[tabIndex].pagination.hasPreviousPage else { return }
29-
30-
paginateAfterConfirmation(tabIndex: tabIndex) { pagination in
31-
pagination.goToPreviousPage()
32-
}
18+
paginateIfPossible(where: \.hasPreviousPage) { $0.goToPreviousPage() }
3319
}
3420

35-
/// Navigate to first page
3621
func goToFirstPage() {
37-
guard let tabIndex = tabManager.selectedTabIndex,
38-
tabIndex < tabManager.tabs.count,
39-
tabManager.tabs[tabIndex].pagination.hasPreviousPage else { return }
40-
41-
paginateAfterConfirmation(tabIndex: tabIndex) { pagination in
42-
pagination.goToFirstPage()
43-
}
22+
paginateIfPossible(where: \.hasPreviousPage) { $0.goToFirstPage() }
4423
}
4524

46-
/// Navigate to last page
4725
func goToLastPage() {
48-
guard let tabIndex = tabManager.selectedTabIndex,
49-
tabIndex < tabManager.tabs.count else { return }
50-
51-
let tab = tabManager.tabs[tabIndex]
52-
guard tab.pagination.currentPage != tab.pagination.totalPages else { return }
53-
54-
paginateAfterConfirmation(tabIndex: tabIndex) { pagination in
55-
pagination.goToLastPage()
56-
}
26+
paginateIfPossible(where: { $0.currentPage != $0.totalPages }) { $0.goToLastPage() }
5727
}
5828

59-
/// Update page size (limit) and reload
6029
func updatePageSize(_ newSize: Int) {
61-
guard let tabIndex = tabManager.selectedTabIndex,
62-
tabIndex < tabManager.tabs.count,
63-
newSize > 0 else { return }
64-
65-
paginateAfterConfirmation(tabIndex: tabIndex) { pagination in
66-
pagination.updatePageSize(newSize)
67-
}
30+
guard newSize > 0 else { return }
31+
paginateIfPossible { $0.updatePageSize(newSize) }
6832
}
6933

70-
/// Update offset and reload
7134
func updateOffset(_ newOffset: Int) {
72-
guard let tabIndex = tabManager.selectedTabIndex,
73-
tabIndex < tabManager.tabs.count,
74-
newOffset >= 0 else { return }
75-
76-
paginateAfterConfirmation(tabIndex: tabIndex) { pagination in
77-
pagination.updateOffset(newOffset)
78-
}
35+
guard newOffset >= 0 else { return }
36+
paginateIfPossible { $0.updateOffset(newOffset) }
7937
}
8038

81-
/// Apply both limit and offset changes and reload
8239
func applyPaginationSettings() {
8340
reloadCurrentPage()
8441
}
8542

8643
// MARK: - Private
8744

88-
/// Confirm discard if needed, then mutate pagination state and reload.
45+
private func paginateIfPossible(
46+
where condition: (PaginationState) -> Bool = { _ in true },
47+
mutate: @escaping (inout PaginationState) -> Void
48+
) {
49+
guard let (tab, tabIndex) = tabManager.selectedTabAndIndex,
50+
condition(tab.pagination) else { return }
51+
paginateAfterConfirmation(tabIndex: tabIndex, mutate: mutate)
52+
}
53+
8954
private func paginateAfterConfirmation(
9055
tabIndex: Int,
9156
mutate: @escaping (inout PaginationState) -> Void
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
//
2+
// QueryTabManagerTests.swift
3+
// TableProTests
4+
//
5+
// Locks the contract for selectedTabAndIndex — the helper that
6+
// MainContentCoordinator+Pagination (and future coordinator extensions)
7+
// use in place of the selectedTabIndex + bounds-check + tabs[index]
8+
// pattern. The tests guard against silent staleness if selectedTabId
9+
// ever points to a removed tab.
10+
//
11+
12+
import Foundation
13+
import Testing
14+
@testable import TablePro
15+
16+
@Suite("QueryTabManager.selectedTabAndIndex")
17+
@MainActor
18+
struct QueryTabManagerSelectedTabAndIndexTests {
19+
@Test("returns nil when no tab is selected")
20+
func nilWhenNoSelection() {
21+
let manager = QueryTabManager()
22+
#expect(manager.selectedTabAndIndex == nil)
23+
}
24+
25+
@Test("returns the selected tab and its index after addTableTab")
26+
func returnsSelectedTabAfterAdd() {
27+
let manager = QueryTabManager()
28+
manager.addTableTab(tableName: "users")
29+
30+
let result = manager.selectedTabAndIndex
31+
#expect(result?.index == 0)
32+
#expect(result?.tab.tableContext.tableName == "users")
33+
}
34+
35+
@Test("returns nil when selectedTabId points to a removed tab")
36+
func nilWhenSelectionIsStale() {
37+
let manager = QueryTabManager()
38+
manager.addTableTab(tableName: "users")
39+
let staleId = manager.tabs[0].id
40+
41+
manager.tabs.removeAll()
42+
manager.selectedTabId = staleId
43+
44+
#expect(manager.selectedTabAndIndex == nil)
45+
}
46+
47+
@Test("returns the correct (tab, index) pair after switching tabs")
48+
func returnsCorrectPairAfterSwitch() {
49+
let manager = QueryTabManager()
50+
manager.addTableTab(tableName: "users")
51+
manager.addTableTab(tableName: "orders")
52+
let firstId = manager.tabs[0].id
53+
54+
manager.selectedTabId = firstId
55+
56+
let result = manager.selectedTabAndIndex
57+
#expect(result?.index == 0)
58+
#expect(result?.tab.tableContext.tableName == "users")
59+
}
60+
}

0 commit comments

Comments
 (0)