Skip to content

Commit 1c9488f

Browse files
committed
feat: page-based pagination controls for iOS data browser
1 parent 156ad3f commit 1c9488f

2 files changed

Lines changed: 58 additions & 45 deletions

File tree

TableProMobile/TableProMobile/Helpers/SQLBuilder.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ enum SQLBuilder {
2424
.replacingOccurrences(of: "'", with: "''")
2525
}
2626

27+
static func buildCount(table: String, type: DatabaseType) -> String {
28+
let quoted = quoteIdentifier(table, for: type)
29+
return "SELECT COUNT(*) FROM \(quoted)"
30+
}
31+
2732
static func buildSelect(table: String, type: DatabaseType, limit: Int, offset: Int) -> String {
2833
let quoted = quoteIdentifier(table, for: type)
2934
return "SELECT * FROM \(quoted) LIMIT \(limit) OFFSET \(offset)"

TableProMobile/TableProMobile/Views/DataBrowserView.swift

Lines changed: 53 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,10 @@ struct DataBrowserView: View {
1919
@State private var columnDetails: [ColumnInfo] = []
2020
@State private var rows: [[String?]] = []
2121
@State private var isLoading = true
22-
@State private var isLoadingMore = false
2322
@State private var appError: AppError?
2423
@State private var toastMessage: String?
2524
@State private var toastTask: Task<Void, Never>?
2625
@State private var pagination = PaginationState(pageSize: 100, currentPage: 0)
27-
@State private var hasMore = true
2826
@State private var showInsertSheet = false
2927
@State private var deleteTarget: [(column: String, value: String)]?
3028
@State private var showDeleteConfirmation = false
@@ -41,6 +39,16 @@ struct DataBrowserView: View {
4139
columnDetails.contains { $0.isPrimaryKey }
4240
}
4341

42+
private var paginationLabel: String {
43+
guard !rows.isEmpty else { return "" }
44+
let start = pagination.currentOffset + 1
45+
let end = pagination.currentOffset + rows.count
46+
if let total = pagination.totalRows {
47+
return "\(start)\(end) of \(total)"
48+
}
49+
return "\(start)\(end)"
50+
}
51+
4452
var body: some View {
4553
Group {
4654
if isLoading {
@@ -69,7 +77,7 @@ struct DataBrowserView: View {
6977
.navigationBarTitleDisplayMode(.inline)
7078
.toolbar {
7179
ToolbarItem(placement: .status) {
72-
Text(verbatim: "\(rows.count) rows")
80+
Text(verbatim: paginationLabel)
7381
.font(.caption)
7482
.foregroundStyle(.secondary)
7583
}
@@ -93,6 +101,29 @@ struct DataBrowserView: View {
93101
}
94102
}
95103
}
104+
ToolbarItemGroup(placement: .bottomBar) {
105+
Button {
106+
Task { await goToPreviousPage() }
107+
} label: {
108+
Image(systemName: "chevron.left")
109+
}
110+
.disabled(pagination.currentPage == 0 || isLoading)
111+
112+
Spacer()
113+
114+
Text(paginationLabel)
115+
.font(.caption)
116+
.foregroundStyle(.secondary)
117+
118+
Spacer()
119+
120+
Button {
121+
Task { await goToNextPage() }
122+
} label: {
123+
Image(systemName: "chevron.right")
124+
}
125+
.disabled(!pagination.hasNextPage || isLoading)
126+
}
96127
}
97128
.task { await loadData(isInitial: true) }
98129
.sheet(isPresented: $showInsertSheet) {
@@ -147,7 +178,6 @@ struct DataBrowserView: View {
147178

148179
private var cardList: some View {
149180
List {
150-
// Offset-based identity is acceptable here: rows don't animate/reorder
151181
ForEach(Array(rows.enumerated()), id: \.offset) { index, row in
152182
NavigationLink {
153183
RowDetailView(
@@ -180,28 +210,6 @@ struct DataBrowserView: View {
180210
}
181211
}
182212
}
183-
184-
if hasMore {
185-
Section {
186-
Button {
187-
Task { await loadNextPage() }
188-
} label: {
189-
HStack {
190-
Spacer()
191-
if isLoadingMore {
192-
ProgressView()
193-
.controlSize(.small)
194-
Text("Loading...")
195-
} else {
196-
Label("Load More", systemImage: "arrow.down.circle")
197-
}
198-
Spacer()
199-
}
200-
.foregroundStyle(.blue)
201-
}
202-
.disabled(isLoadingMore)
203-
}
204-
}
205213
}
206214
.listStyle(.plain)
207215
.refreshable { await loadData() }
@@ -224,7 +232,6 @@ struct DataBrowserView: View {
224232
isLoading = true
225233
}
226234
appError = nil
227-
pagination.reset()
228235

229236
do {
230237
let query = SQLBuilder.buildSelect(
@@ -234,12 +241,13 @@ struct DataBrowserView: View {
234241
let result = try await session.driver.execute(query: query)
235242
self.columns = result.columns
236243
self.rows = result.rows
237-
self.hasMore = result.rows.count >= pagination.pageSize
238244

239245
// columnDetails (from fetchColumns) provides PK info for edit/delete.
240246
// columns (from query result) only have name/type, no PK metadata.
241247
self.columnDetails = try await session.driver.fetchColumns(table: table.name, schema: nil)
242248

249+
await fetchTotalRows(session: session)
250+
243251
isLoading = false
244252
} catch {
245253
let context = ErrorContext(
@@ -252,27 +260,27 @@ struct DataBrowserView: View {
252260
}
253261
}
254262

255-
private func loadNextPage() async {
256-
guard let session else { return }
257-
258-
isLoadingMore = true
259-
pagination.currentPage += 1
260-
263+
private func fetchTotalRows(session: ConnectionSession) async {
261264
do {
262-
let query = SQLBuilder.buildSelect(
263-
table: table.name, type: connection.type,
264-
limit: pagination.pageSize, offset: pagination.currentOffset
265-
)
266-
let result = try await session.driver.execute(query: query)
267-
rows.append(contentsOf: result.rows)
268-
hasMore = result.rows.count >= pagination.pageSize
265+
let countQuery = SQLBuilder.buildCount(table: table.name, type: connection.type)
266+
let countResult = try await session.driver.execute(query: countQuery)
267+
if let firstRow = countResult.rows.first, let firstCol = firstRow.first {
268+
pagination.totalRows = Int(firstCol ?? "0")
269+
}
269270
} catch {
270-
pagination.currentPage -= 1
271-
Self.logger.warning("Failed to load next page: \(error.localizedDescription, privacy: .public)")
272-
withAnimation { toastMessage = String(localized: "Failed to load more rows") }
271+
Self.logger.warning("Failed to fetch row count: \(error.localizedDescription, privacy: .public)")
273272
}
273+
}
274+
275+
private func goToNextPage() async {
276+
pagination.currentPage += 1
277+
await loadData()
278+
}
274279

275-
isLoadingMore = false
280+
private func goToPreviousPage() async {
281+
guard pagination.currentPage > 0 else { return }
282+
pagination.currentPage -= 1
283+
await loadData()
276284
}
277285

278286
private func deleteRow(withPKs pkValues: [(column: String, value: String)]) async {

0 commit comments

Comments
 (0)