@@ -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 showFilterSheet = 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)
@@ -57,6 +65,15 @@ struct DataBrowserView: View {
5765 . toolbar { paginationToolbar }
5866 . task { await loadData ( isInitial: true ) }
5967 . sheet ( isPresented: $showInsertSheet) { insertSheet }
68+ . sheet ( isPresented: $showFilterSheet) {
69+ FilterSheetView (
70+ filters: $filters,
71+ logicMode: $filterLogicMode,
72+ columns: columns,
73+ onApply: { applyFilters ( ) } ,
74+ onClear: { clearFilters ( ) }
75+ )
76+ }
6077 . confirmationDialog ( " Delete Row " , isPresented: $showDeleteConfirmation, titleVisibility: . visible) {
6178 Button ( " Delete " , role: . destructive) {
6279 if let pkValues = deleteTarget {
@@ -111,6 +128,10 @@ struct DataBrowserView: View {
111128 }
112129 }
113130
131+ private var activeFilterCount : Int {
132+ filters. filter { $0. isEnabled && $0. isValid } . count
133+ }
134+
114135 private var rowList : some View {
115136 List {
116137 ForEach ( Array ( rows. enumerated ( ) ) , id: \. offset) { index, row in
@@ -156,6 +177,14 @@ struct DataBrowserView: View {
156177
157178 @ToolbarContentBuilder
158179 private var topToolbar : some ToolbarContent {
180+ ToolbarItem ( placement: . topBarTrailing) {
181+ Button { showFilterSheet = true } label: {
182+ Image ( systemName: hasActiveFilters
183+ ? " line.3.horizontal.decrease.circle.fill "
184+ : " line.3.horizontal.decrease.circle " )
185+ }
186+ . badge ( activeFilterCount)
187+ }
159188 ToolbarItem ( placement: . topBarTrailing) {
160189 NavigationLink {
161190 StructureView ( table: table, session: session, databaseType: connection. type)
@@ -251,10 +280,19 @@ struct DataBrowserView: View {
251280 appError = nil
252281
253282 do {
254- let query = SQLBuilder . buildSelect (
255- table: table. name, type: connection. type,
256- limit: pagination. pageSize, offset: pagination. currentOffset
257- )
283+ let query : String
284+ if hasActiveFilters {
285+ query = SQLBuilder . buildFilteredSelect (
286+ table: table. name, type: connection. type,
287+ filters: filters, logicMode: filterLogicMode,
288+ limit: pagination. pageSize, offset: pagination. currentOffset
289+ )
290+ } else {
291+ query = SQLBuilder . buildSelect (
292+ table: table. name, type: connection. type,
293+ limit: pagination. pageSize, offset: pagination. currentOffset
294+ )
295+ }
258296 let result = try await session. driver. execute ( query: query)
259297 columns = result. columns
260298 rows = result. rows
@@ -276,7 +314,15 @@ struct DataBrowserView: View {
276314
277315 private func fetchTotalRows( session: ConnectionSession ) async {
278316 do {
279- let countQuery = SQLBuilder . buildCount ( table: table. name, type: connection. type)
317+ let countQuery : String
318+ if hasActiveFilters {
319+ countQuery = SQLBuilder . buildFilteredCount (
320+ table: table. name, type: connection. type,
321+ filters: filters, logicMode: filterLogicMode
322+ )
323+ } else {
324+ countQuery = SQLBuilder . buildCount ( table: table. name, type: connection. type)
325+ }
280326 let countResult = try await session. driver. execute ( query: countQuery)
281327 if let firstRow = countResult. rows. first, let firstCol = firstRow. first {
282328 pagination. totalRows = Int ( firstCol ?? " 0 " )
@@ -349,6 +395,162 @@ struct DataBrowserView: View {
349395 return ( column: col. name, value: value)
350396 }
351397 }
398+
399+ private func applyFilters( ) {
400+ pagination. currentPage = 0
401+ pagination. totalRows = nil
402+ Task { await loadData ( ) }
403+ }
404+
405+ private func clearFilters( ) {
406+ filters. removeAll ( )
407+ pagination. currentPage = 0
408+ pagination. totalRows = nil
409+ Task { await loadData ( ) }
410+ }
411+ }
412+
413+ // MARK: - Filter Sheet
414+
415+ private struct FilterSheetView : View {
416+ @Environment ( \. dismiss) private var dismiss
417+ @Binding var filters : [ TableFilter ]
418+ @Binding var logicMode : FilterLogicMode
419+ let columns : [ ColumnInfo ]
420+ let onApply : ( ) -> Void
421+ let onClear : ( ) -> Void
422+
423+ @State private var draft : [ TableFilter ] = [ ]
424+ @State private var draftLogicMode : FilterLogicMode = . and
425+
426+ private var hasValidFilters : Bool {
427+ draft. contains { $0. isEnabled && $0. isValid }
428+ }
429+
430+ private func bindingForFilter( _ id: UUID ) -> Binding < TableFilter > ? {
431+ guard let index = draft. firstIndex ( where: { $0. id == id } ) else { return nil }
432+ return $draft [ index]
433+ }
434+
435+ var body : some View {
436+ NavigationStack {
437+ Form {
438+ if draft. count > 1 {
439+ Section {
440+ Picker ( " Logic " , selection: $draftLogicMode) {
441+ Text ( " AND " ) . tag ( FilterLogicMode . and)
442+ Text ( " OR " ) . tag ( FilterLogicMode . or)
443+ }
444+ . pickerStyle ( . segmented)
445+ }
446+ }
447+
448+ ForEach ( draft) { filter in
449+ if let binding = bindingForFilter ( filter. id) {
450+ Section {
451+ Picker ( " Column " , selection: binding. columnName) {
452+ ForEach ( columns, id: \. name) { col in
453+ Text ( col. name) . tag ( col. name)
454+ }
455+ }
456+
457+ Picker ( " Operator " , selection: binding. filterOperator) {
458+ ForEach ( FilterOperator . allCases, id: \. self) { op in
459+ Text ( op. displayName) . tag ( op)
460+ }
461+ }
462+
463+ if filter. filterOperator. needsValue {
464+ TextField ( " Value " , text: binding. value)
465+ . textInputAutocapitalization ( . never)
466+ . autocorrectionDisabled ( )
467+ }
468+
469+ if filter. filterOperator == . between {
470+ TextField ( " Second value " , text: binding. secondValue)
471+ . textInputAutocapitalization ( . never)
472+ . autocorrectionDisabled ( )
473+ }
474+ }
475+ }
476+ }
477+ . onDelete { indexSet in
478+ draft. remove ( atOffsets: indexSet)
479+ }
480+
481+ Section {
482+ Button {
483+ draft. append ( TableFilter ( columnName: columns. first? . name ?? " " ) )
484+ } label: {
485+ Label ( " Add Filter " , systemImage: " plus.circle " )
486+ }
487+ }
488+
489+ if !draft. isEmpty {
490+ Section {
491+ Button ( " Clear All Filters " , role: . destructive) {
492+ filters. removeAll ( )
493+ logicMode = . and
494+ onClear ( )
495+ dismiss ( )
496+ }
497+ }
498+ }
499+ }
500+ . navigationTitle ( " Filters " )
501+ . navigationBarTitleDisplayMode ( . inline)
502+ . toolbar {
503+ ToolbarItem ( placement: . cancellationAction) {
504+ Button ( " Cancel " ) { dismiss ( ) }
505+ }
506+ ToolbarItem ( placement: . confirmationAction) {
507+ Button ( " Apply " ) {
508+ filters = draft
509+ logicMode = draftLogicMode
510+ onApply ( )
511+ dismiss ( )
512+ }
513+ . disabled ( !hasValidFilters)
514+ }
515+ }
516+ . onAppear {
517+ draft = filters
518+ draftLogicMode = logicMode
519+ }
520+ }
521+ }
522+ }
523+
524+ // MARK: - Filter Operator Display
525+
526+ extension FilterOperator {
527+ var displayName : String {
528+ switch self {
529+ case . equal: return " equals "
530+ case . notEqual: return " not equals "
531+ case . greaterThan: return " greater than "
532+ case . greaterThanOrEqual: return " ≥ "
533+ case . lessThan: return " less than "
534+ case . lessThanOrEqual: return " ≤ "
535+ case . like: return " like "
536+ case . notLike: return " not like "
537+ case . isNull: return " is null "
538+ case . isNotNull: return " is not null "
539+ case . in: return " in "
540+ case . notIn: return " not in "
541+ case . between: return " between "
542+ case . contains: return " contains "
543+ case . startsWith: return " starts with "
544+ case . endsWith: return " ends with "
545+ }
546+ }
547+
548+ var needsValue : Bool {
549+ switch self {
550+ case . isNull, . isNotNull: return false
551+ default : return true
552+ }
553+ }
352554}
353555
354556// MARK: - Row Card
0 commit comments