@@ -7,6 +7,7 @@ import os
77import SwiftUI
88import TableProDatabase
99import TableProModels
10+ import TableProQuery
1011
1112struct DataBrowserView : View {
1213 let connection : DatabaseConnection
@@ -29,6 +30,9 @@ struct DataBrowserView: View {
2930 @State private var showOperationError = false
3031 @State private var showGoToPage = false
3132 @State private var goToPageInput = " "
33+ @State private var filters : [ TableFilter ] = [ ]
34+ @State private var filterLogicMode : FilterLogicMode = . and
35+ @State private var showFilterBar = false
3236
3337 private var isView : Bool {
3438 table. type == . view || table. type == . materializedView
@@ -48,6 +52,10 @@ struct DataBrowserView: View {
4852 return " \( start) – \( end) "
4953 }
5054
55+ private var hasActiveFilters : Bool {
56+ filters. contains { $0. isEnabled && $0. isValid }
57+ }
58+
5159 var body : some View {
5260 content
5361 . navigationTitle ( table. name)
@@ -113,6 +121,48 @@ struct DataBrowserView: View {
113121
114122 private var rowList : some View {
115123 List {
124+ if showFilterBar {
125+ Section {
126+ if filters. count > 1 {
127+ Picker ( " Logic " , selection: $filterLogicMode) {
128+ Text ( " AND " ) . tag ( FilterLogicMode . and)
129+ Text ( " OR " ) . tag ( FilterLogicMode . or)
130+ }
131+ . pickerStyle ( . segmented)
132+ }
133+
134+ ForEach ( $filters) { $filter in
135+ FilterRowView (
136+ filter: $filter,
137+ columns: columns,
138+ onDelete: { filters. removeAll { $0. id == filter. id } }
139+ )
140+ }
141+
142+ HStack {
143+ Button {
144+ filters. append ( TableFilter ( columnName: columns. first? . name ?? " " ) )
145+ } label: {
146+ Label ( " Add Filter " , systemImage: " plus.circle " )
147+ }
148+ Spacer ( )
149+ Button ( " Apply " ) {
150+ applyFilters ( )
151+ }
152+ . buttonStyle ( . borderedProminent)
153+ . disabled ( !hasActiveFilters)
154+ }
155+
156+ if hasActiveFilters {
157+ Button ( " Clear Filters " , role: . destructive) {
158+ clearFilters ( )
159+ }
160+ }
161+ } header: {
162+ Text ( " Filters " )
163+ }
164+ }
165+
116166 ForEach ( Array ( rows. enumerated ( ) ) , id: \. offset) { index, row in
117167 NavigationLink {
118168 RowDetailView (
@@ -156,6 +206,13 @@ struct DataBrowserView: View {
156206
157207 @ToolbarContentBuilder
158208 private var topToolbar : some ToolbarContent {
209+ ToolbarItem ( placement: . topBarTrailing) {
210+ Button { withAnimation { showFilterBar. toggle ( ) } } label: {
211+ Image ( systemName: hasActiveFilters
212+ ? " line.3.horizontal.decrease.circle.fill "
213+ : " line.3.horizontal.decrease.circle " )
214+ }
215+ }
159216 ToolbarItem ( placement: . topBarTrailing) {
160217 NavigationLink {
161218 StructureView ( table: table, session: session, databaseType: connection. type)
@@ -251,10 +308,19 @@ struct DataBrowserView: View {
251308 appError = nil
252309
253310 do {
254- let query = SQLBuilder . buildSelect (
255- table: table. name, type: connection. type,
256- limit: pagination. pageSize, offset: pagination. currentOffset
257- )
311+ let query : String
312+ if hasActiveFilters {
313+ query = SQLBuilder . buildFilteredSelect (
314+ table: table. name, type: connection. type,
315+ filters: filters, logicMode: filterLogicMode,
316+ limit: pagination. pageSize, offset: pagination. currentOffset
317+ )
318+ } else {
319+ query = SQLBuilder . buildSelect (
320+ table: table. name, type: connection. type,
321+ limit: pagination. pageSize, offset: pagination. currentOffset
322+ )
323+ }
258324 let result = try await session. driver. execute ( query: query)
259325 columns = result. columns
260326 rows = result. rows
@@ -276,7 +342,15 @@ struct DataBrowserView: View {
276342
277343 private func fetchTotalRows( session: ConnectionSession ) async {
278344 do {
279- let countQuery = SQLBuilder . buildCount ( table: table. name, type: connection. type)
345+ let countQuery : String
346+ if hasActiveFilters {
347+ countQuery = SQLBuilder . buildFilteredCount (
348+ table: table. name, type: connection. type,
349+ filters: filters, logicMode: filterLogicMode
350+ )
351+ } else {
352+ countQuery = SQLBuilder . buildCount ( table: table. name, type: connection. type)
353+ }
280354 let countResult = try await session. driver. execute ( query: countQuery)
281355 if let firstRow = countResult. rows. first, let firstCol = firstRow. first {
282356 pagination. totalRows = Int ( firstCol ?? " 0 " )
@@ -349,6 +423,97 @@ struct DataBrowserView: View {
349423 return ( column: col. name, value: value)
350424 }
351425 }
426+
427+ private func applyFilters( ) {
428+ pagination. currentPage = 0
429+ pagination. totalRows = nil
430+ Task { await loadData ( ) }
431+ }
432+
433+ private func clearFilters( ) {
434+ filters. removeAll ( )
435+ pagination. currentPage = 0
436+ pagination. totalRows = nil
437+ Task { await loadData ( ) }
438+ }
439+ }
440+
441+ // MARK: - Filter Row
442+
443+ private struct FilterRowView : View {
444+ @Binding var filter : TableFilter
445+ let columns : [ ColumnInfo ]
446+ let onDelete : ( ) -> Void
447+
448+ var body : some View {
449+ VStack ( spacing: 8 ) {
450+ HStack {
451+ Picker ( " Column " , selection: $filter. columnName) {
452+ ForEach ( columns, id: \. name) { col in
453+ Text ( col. name) . tag ( col. name)
454+ }
455+ }
456+ . pickerStyle ( . menu)
457+
458+ Button ( role: . destructive) { onDelete ( ) } label: {
459+ Image ( systemName: " minus.circle.fill " )
460+ . foregroundStyle ( . red)
461+ }
462+ . buttonStyle ( . plain)
463+ }
464+
465+ Picker ( " Operator " , selection: $filter. filterOperator) {
466+ ForEach ( FilterOperator . allCases, id: \. self) { op in
467+ Text ( op. displayName) . tag ( op)
468+ }
469+ }
470+ . pickerStyle ( . menu)
471+
472+ if filter. filterOperator. needsValue {
473+ TextField ( " Value " , text: $filter. value)
474+ . textInputAutocapitalization ( . never)
475+ . autocorrectionDisabled ( )
476+ }
477+
478+ if filter. filterOperator == . between {
479+ TextField ( " Second value " , text: $filter. secondValue)
480+ . textInputAutocapitalization ( . never)
481+ . autocorrectionDisabled ( )
482+ }
483+ }
484+ }
485+ }
486+
487+ // MARK: - Filter Operator Display
488+
489+ extension FilterOperator {
490+ var displayName : String {
491+ switch self {
492+ case . equal: return " equals "
493+ case . notEqual: return " not equals "
494+ case . greaterThan: return " greater than "
495+ case . greaterThanOrEqual: return " ≥ "
496+ case . lessThan: return " less than "
497+ case . lessThanOrEqual: return " ≤ "
498+ case . like: return " like "
499+ case . notLike: return " not like "
500+ case . isNull: return " is null "
501+ case . isNotNull: return " is not null "
502+ case . in: return " in "
503+ case . notIn: return " not in "
504+ case . between: return " between "
505+ case . contains: return " contains "
506+ case . startsWith: return " starts with "
507+ case . endsWith: return " ends with "
508+ }
509+ }
510+
511+ var needsValue : Bool {
512+ switch self {
513+ case . isNull, . isNotNull: return false
514+ default : return true
515+ }
516+ }
352517}
353518
354519// MARK: - Row Card
0 commit comments