Skip to content

Commit 3de6cea

Browse files
committed
feat: Include an option to cancel a search request that's inprogress
1 parent e6d961a commit 3de6cea

File tree

1 file changed

+50
-45
lines changed

1 file changed

+50
-45
lines changed

FileBrowserClient/FileBrowserClient/FileListView.swift

Lines changed: 50 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ struct FileListView: View {
9393
@State private var searchClicked = false
9494
@State private var searchInProgress = false
9595
@State private var searchText = ""
96+
@State private var searchTask: Task<Void, Never>?
9697
@State private var searchType: SearchType?
9798
@State private var previousPathStack: [String] = []
9899

@@ -142,9 +143,10 @@ struct FileListView: View {
142143
.submitLabel(.go)
143144
.onSubmit {
144145
guard !searchText.isEmpty else { return }
145-
DispatchQueue.main.async {
146+
searchTask = Task {
146147
searchInProgress = true
147-
searchFiles(query: searchText)
148+
await searchFiles(query: searchText)
149+
searchTask = nil
148150
}
149151
}
150152

@@ -154,20 +156,30 @@ struct FileListView: View {
154156
// TODO: Instead of disabling cancel button, change the logic to cancel search API call
155157
// TODO: Searches can be very expensive, so having a cancel option would be nice
156158
Button(action: {
157-
guard !searchInProgress else { return }
158-
Log.debug("🔙 Search cancelled")
159+
if let task = searchTask {
160+
Log.debug("🔙 Search cancelled mid request")
161+
task.cancel()
162+
statusMessage = StatusPayload(
163+
text: "✖️ Search cancelled",
164+
color: .yellow,
165+
duration: 3.5
166+
)
167+
searchTask = nil
168+
} else {
169+
Log.debug("🔙 Search cancelled - no task in flight")
170+
}
171+
// TODO: Reset searchType and add error handling back
159172
searchClicked = false
160173
searchInProgress = false
161174
viewModel.searchResults.removeAll()
162175
pathStack = previousPathStack
163176
viewModel.fetchFiles(at: pathStack.last ?? "/")
164177
}) {
165178
Image(systemName: "xmark.circle.fill")
166-
.foregroundColor(searchInProgress ? .gray : .red)
179+
.foregroundColor(.red)
167180
.font(.system(size: 24))
168181
}
169182
.padding(.trailing, 0)
170-
.disabled(searchInProgress)
171183
}
172184
.padding(.horizontal)
173185

@@ -770,25 +782,28 @@ struct FileListView: View {
770782
return URL(string: "\(serverURL)/api/search/\(removePrefix(urlPath: searchLocation))?query=\(encodedQuery)")
771783
}
772784

773-
func searchFiles(query: String) {
785+
func searchFiles(query: String) async {
774786
Log.debug("🔍 Searching for: \(query)")
775787
guard let serverURL = auth.serverURL, let token = auth.token else {
776-
DispatchQueue.main.async {
788+
await MainActor.run {
777789
errorMessage = "Invalid authorization. Please log out and log back in."
778790
searchInProgress = false
779791
}
780792
return
781793
}
782794

783795
guard let url = getSearchURL(serverURL: serverURL, query: query) else {
784-
errorMessage = "Invalid search: \(query)"
785-
searchInProgress = false
796+
await MainActor.run {
797+
errorMessage = "Invalid search: \(query)"
798+
searchInProgress = false
799+
}
786800
Log.error("❌ Failed to generate search URL")
787801
return
788802
}
803+
789804
Log.debug("🔍 Search URL: \(url.relativePath)")
790805

791-
DispatchQueue.main.async {
806+
await MainActor.run {
792807
viewModel.isLoading = true
793808
errorMessage = nil
794809
}
@@ -797,46 +812,36 @@ struct FileListView: View {
797812
request.httpMethod = "GET"
798813
request.setValue(token, forHTTPHeaderField: "X-Auth")
799814

800-
URLSession.shared.dataTask(with: request) { data, _, error in
801-
DispatchQueue.main.async {
815+
do {
816+
let (data, _) = try await URLSession.shared.data(for: request)
817+
818+
guard !Task.isCancelled else { return }
819+
820+
let results = try JSONDecoder().decode([FileItemSearch].self, from: data)
821+
822+
await MainActor.run {
802823
viewModel.isLoading = false
803-
searchInProgress = false // ✅ re-enable cancel when done
804-
}
824+
searchInProgress = false
805825

806-
if let error = error {
807-
DispatchQueue.main.async {
808-
errorMessage = error.localizedDescription
809-
}
810-
return
811-
}
826+
let typeHead = searchType.map { String(describing: $0) } ?? "files"
812827

813-
guard let data = data else {
814-
DispatchQueue.main.async {
815-
errorMessage = "No data received"
828+
if results.isEmpty {
829+
viewModel.errorMessage = "No \(typeHead) found for: \(query)"
830+
} else {
831+
viewModel.searchResults = results
832+
let typeDescription = typeHead == "files" ? typeHead : "\(typeHead) files"
833+
Log.info("🔍 Search returned \(results.count) \(typeDescription)")
816834
}
817-
return
818835
}
819-
820-
do {
821-
let results = try JSONDecoder().decode([FileItemSearch].self, from: data)
822-
DispatchQueue.main.async {
823-
// TODO: Too much logic for something simple - just add a new case "files" in the parent enum
824-
let typeHead: String = searchType != nil ? String(describing: searchType!) : "files"
825-
if results.isEmpty {
826-
viewModel.errorMessage = "No \(typeHead) found for: \(query)"
827-
} else {
828-
viewModel.searchResults = results
829-
let typeDescription = typeHead == "files" ? typeHead : "\(typeHead) files"
830-
Log.info("🔍 Search returned \(results.count) \(typeDescription)")
831-
}
832-
}
833-
} catch {
834-
DispatchQueue.main.async {
835-
errorMessage = "Failed to parse search results"
836-
}
837-
Log.error("Search decode error: \(error.localizedDescription)")
836+
} catch {
837+
guard !Task.isCancelled else { return }
838+
await MainActor.run {
839+
viewModel.isLoading = false
840+
searchInProgress = false
841+
errorMessage = "Failed to perform search: \(error.localizedDescription)"
838842
}
839-
}.resume()
843+
Log.error("Search error: \(error.localizedDescription)")
844+
}
840845
}
841846

842847
func deleteSession() {

0 commit comments

Comments
 (0)