@@ -30,6 +30,9 @@ struct DataBrowserView: View {
3030 @State private var showOperationError = false
3131 @State private var showGoToPage = false
3232 @State private var goToPageInput = " "
33+ @State private var searchText = " "
34+ @State private var activeSearchText = " "
35+ @State private var searchTask : Task < Void , Never > ?
3336 @State private var filters : [ TableFilter ] = [ ]
3437 @State private var filterLogicMode : FilterLogicMode = . and
3538 @State private var showFilterSheet = false
@@ -57,6 +60,14 @@ struct DataBrowserView: View {
5760 return " \( start) – \( end) "
5861 }
5962
63+ private var hasActiveSearch : Bool {
64+ !activeSearchText. isEmpty
65+ }
66+
67+ private var isRedis : Bool {
68+ connection. type == . redis
69+ }
70+
6071 private var hasActiveFilters : Bool {
6172 filters. contains { $0. isEnabled && $0. isValid }
6273 }
@@ -88,11 +99,9 @@ struct DataBrowserView: View {
8899 }
89100
90101 var body : some View {
91- content
92- . navigationTitle ( table. name)
93- . navigationBarTitleDisplayMode ( . inline)
102+ searchableContent
94103 . toolbar { topToolbar }
95- . toolbar ( rows. isEmpty ? . hidden : . visible, for: . bottomBar)
104+ . toolbar ( rows. isEmpty && !hasActiveSearch && !hasActiveFilters ? . hidden : . visible, for: . bottomBar)
96105 . toolbar { paginationToolbar }
97106 . task { await loadData ( isInitial: true ) }
98107 . sheet ( isPresented: $showInsertSheet) { insertSheet }
@@ -163,6 +172,26 @@ struct DataBrowserView: View {
163172 }
164173 }
165174
175+ @ViewBuilder
176+ private var searchableContent : some View {
177+ if isRedis {
178+ content
179+ . navigationTitle ( table. name)
180+ . navigationBarTitleDisplayMode ( . inline)
181+ } else {
182+ content
183+ . navigationTitle ( table. name)
184+ . navigationBarTitleDisplayMode ( . inline)
185+ . searchable ( text: $searchText, prompt: " Search all columns " )
186+ . onSubmit ( of: . search) { applySearch ( ) }
187+ . onChange ( of: searchText) { oldValue, newValue in
188+ if newValue. isEmpty, !oldValue. isEmpty, hasActiveSearch {
189+ clearSearch ( )
190+ }
191+ }
192+ }
193+ }
194+
166195 // MARK: - Content
167196
168197 @ViewBuilder
@@ -172,6 +201,8 @@ struct DataBrowserView: View {
172201 . frame ( maxWidth: . infinity, maxHeight: . infinity)
173202 } else if let appError {
174203 ErrorView ( error: appError) { await loadData ( ) }
204+ } else if rows. isEmpty, hasActiveSearch {
205+ ContentUnavailableView . search ( text: activeSearchText)
175206 } else if rows. isEmpty {
176207 ContentUnavailableView {
177208 Label ( " No Data " , systemImage: " tray " )
@@ -220,7 +251,7 @@ struct DataBrowserView: View {
220251 ForEach ( ExportFormat . allCases) { format in
221252 Button ( format. rawValue) {
222253 let text = ClipboardExporter . exportRow (
223- columns: columns, row: rows [ index ] ,
254+ columns: columns, row: row ,
224255 format: format, tableName: table. name
225256 )
226257 ClipboardExporter . copyToClipboard ( text)
@@ -230,17 +261,17 @@ struct DataBrowserView: View {
230261 if !foreignKeys. isEmpty {
231262 let rowFKs = foreignKeys. filter { fk in
232263 guard let colIndex = columns. firstIndex ( where: { $0. name == fk. column } ) ,
233- colIndex < rows [ index ] . count,
234- rows [ index ] [ colIndex] != nil else { return false }
264+ colIndex < row . count,
265+ row [ colIndex] != nil else { return false }
235266 return true
236267 }
237268 if !rowFKs. isEmpty {
238269 Divider ( )
239270 ForEach ( rowFKs, id: \. name) { fk in
240271 Button {
241272 if let colIndex = columns. firstIndex ( where: { $0. name == fk. column } ) ,
242- colIndex < rows [ index ] . count,
243- let value = rows [ index ] [ colIndex] {
273+ colIndex < row . count,
274+ let value = row [ colIndex] {
244275 fkPreviewItem = FKPreviewItem ( fk: fk, value: value)
245276 }
246277 } label: {
@@ -253,7 +284,7 @@ struct DataBrowserView: View {
253284 . swipeActions ( edge: . trailing, allowsFullSwipe: false ) {
254285 if !isView && hasPrimaryKeys && !connection. safeModeLevel. blocksWrites {
255286 Button ( role: . destructive) {
256- deleteTarget = primaryKeyValues ( for: rows [ index ] )
287+ deleteTarget = primaryKeyValues ( for: row )
257288 showDeleteConfirmation = true
258289 } label: {
259290 Label ( " Delete " , systemImage: " trash " )
@@ -290,6 +321,7 @@ struct DataBrowserView: View {
290321 }
291322 } label: {
292323 Image ( systemName: " square.and.arrow.up " )
324+ . accessibilityLabel ( Text ( " Export " ) )
293325 }
294326 . disabled ( rows. isEmpty)
295327 }
@@ -314,6 +346,7 @@ struct DataBrowserView: View {
314346 Image ( systemName: sortState. isSorting
315347 ? " arrow.up.arrow.down.circle.fill "
316348 : " arrow.up.arrow.down.circle " )
349+ . accessibilityLabel ( Text ( " Sort " ) )
317350 }
318351 . disabled ( columns. isEmpty)
319352 }
@@ -322,6 +355,7 @@ struct DataBrowserView: View {
322355 Image ( systemName: hasActiveFilters
323356 ? " line.3.horizontal.decrease.circle.fill "
324357 : " line.3.horizontal.decrease.circle " )
358+ . accessibilityLabel ( Text ( " Filter " ) )
325359 }
326360 . badge ( activeFilterCount)
327361 }
@@ -330,12 +364,14 @@ struct DataBrowserView: View {
330364 StructureView ( table: table, session: session, databaseType: connection. type)
331365 } label: {
332366 Image ( systemName: " info.circle " )
367+ . accessibilityLabel ( Text ( " Table Structure " ) )
333368 }
334369 }
335370 if !isView && !connection. safeModeLevel. blocksWrites {
336371 ToolbarItem ( placement: . primaryAction) {
337372 Button { showInsertSheet = true } label: {
338373 Image ( systemName: " plus " )
374+ . accessibilityLabel ( Text ( " Insert Row " ) )
339375 }
340376 }
341377 }
@@ -421,7 +457,20 @@ struct DataBrowserView: View {
421457
422458 do {
423459 let query : String
424- if hasActiveFilters {
460+ if hasActiveSearch {
461+ let searchableColumns = columns. filter { col in
462+ let upper = col. typeName. uppercased ( )
463+ return !upper. contains ( " BLOB " ) && !upper. contains ( " BYTEA " ) && !upper. contains ( " BINARY " )
464+ && !upper. contains ( " VARBINARY " ) && !upper. contains ( " IMAGE " )
465+ }
466+ query = SQLBuilder . buildSearchSelect (
467+ table: table. name, type: connection. type,
468+ searchText: activeSearchText, searchColumns: searchableColumns,
469+ filters: filters, logicMode: filterLogicMode,
470+ sortState: sortState,
471+ limit: pagination. pageSize, offset: pagination. currentOffset
472+ )
473+ } else if hasActiveFilters {
425474 query = SQLBuilder . buildFilteredSelect (
426475 table: table. name, type: connection. type,
427476 filters: filters, logicMode: filterLogicMode,
@@ -472,7 +521,18 @@ struct DataBrowserView: View {
472521 private func fetchTotalRows( session: ConnectionSession ) async {
473522 do {
474523 let countQuery : String
475- if hasActiveFilters {
524+ if hasActiveSearch {
525+ let searchableColumns = columns. filter { col in
526+ let upper = col. typeName. uppercased ( )
527+ return !upper. contains ( " BLOB " ) && !upper. contains ( " BYTEA " ) && !upper. contains ( " BINARY " )
528+ && !upper. contains ( " VARBINARY " ) && !upper. contains ( " IMAGE " )
529+ }
530+ countQuery = SQLBuilder . buildSearchCount (
531+ table: table. name, type: connection. type,
532+ searchText: activeSearchText, searchColumns: searchableColumns,
533+ filters: filters, logicMode: filterLogicMode
534+ )
535+ } else if hasActiveFilters {
476536 countQuery = SQLBuilder . buildFilteredCount (
477537 table: table. name, type: connection. type,
478538 filters: filters, logicMode: filterLogicMode
@@ -560,6 +620,24 @@ struct DataBrowserView: View {
560620 Task { await loadData ( ) }
561621 }
562622
623+ private func applySearch( ) {
624+ activeSearchText = searchText. trimmingCharacters ( in: . whitespacesAndNewlines)
625+ guard hasActiveSearch, !columns. isEmpty else { return }
626+ pagination. currentPage = 0
627+ pagination. totalRows = nil
628+ searchTask? . cancel ( )
629+ searchTask = Task { await loadData ( ) }
630+ }
631+
632+ private func clearSearch( ) {
633+ searchText = " "
634+ activeSearchText = " "
635+ pagination. currentPage = 0
636+ pagination. totalRows = nil
637+ searchTask? . cancel ( )
638+ searchTask = Task { await loadData ( ) }
639+ }
640+
563641 private func applyFilters( ) {
564642 pagination. currentPage = 0
565643 pagination. totalRows = nil
@@ -781,5 +859,6 @@ private struct RowCard: View {
781859 }
782860 }
783861 . padding ( . vertical, 2 )
862+ . accessibilityElement ( children: . combine)
784863 }
785864}
0 commit comments