Skip to content

Commit e769dc5

Browse files
authored
Bookings: Add search functionality for booking list (#16215)
2 parents 4fc86e3 + a721ac5 commit e769dc5

File tree

14 files changed

+850
-56
lines changed

14 files changed

+850
-56
lines changed

Modules/Sources/Networking/Remote/BookingsRemote.swift

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ public protocol BookingsRemoteProtocol {
1010
pageNumber: Int,
1111
pageSize: Int,
1212
startDateBefore: String?,
13-
startDateAfter: String?) async throws -> [Booking]
13+
startDateAfter: String?,
14+
searchQuery: String?) async throws -> [Booking]
1415
}
1516

1617
/// Booking: Remote Endpoints
@@ -27,12 +28,14 @@ public final class BookingsRemote: Remote, BookingsRemoteProtocol {
2728
/// - pageSize: Number of bookings to be retrieved per page.
2829
/// - startDateBefore: Filter bookings with start date before this timestamp.
2930
/// - startDateAfter: Filter bookings with start date after this timestamp.
31+
/// - searchQuery: Search query to filter bookings.
3032
///
3133
public func loadAllBookings(for siteID: Int64,
3234
pageNumber: Int = Default.pageNumber,
3335
pageSize: Int = Default.pageSize,
3436
startDateBefore: String? = nil,
35-
startDateAfter: String? = nil) async throws -> [Booking] {
37+
startDateAfter: String? = nil,
38+
searchQuery: String? = nil) async throws -> [Booking] {
3639
var parameters = [
3740
ParameterKey.page: String(pageNumber),
3841
ParameterKey.perPage: String(pageSize)
@@ -46,6 +49,10 @@ public final class BookingsRemote: Remote, BookingsRemoteProtocol {
4649
parameters[ParameterKey.startDateAfter] = startDateAfter
4750
}
4851

52+
if let searchQuery = searchQuery, !searchQuery.isEmpty {
53+
parameters[ParameterKey.search] = searchQuery
54+
}
55+
4956
let path = Path.bookings
5057
let request = JetpackRequest(wooApiVersion: .wcBookings, method: .get, siteID: siteID, path: path, parameters: parameters, availableAsRESTRequest: true)
5158
let mapper = ListMapper<Booking>(siteID: siteID)
@@ -71,5 +78,6 @@ public extension BookingsRemote {
7178
static let perPage: String = "per_page"
7279
static let startDateBefore: String = "start_date_before"
7380
static let startDateAfter: String = "start_date_after"
81+
static let search: String = "search"
7482
}
7583
}

Modules/Sources/Yosemite/Actions/BookingAction.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,16 @@ public enum BookingAction: Action {
2323
///
2424
case checkIfStoreHasBookings(siteID: Int64,
2525
onCompletion: (Result<Bool, Error>) -> Void)
26+
27+
/// Searches for bookings matching the specified criteria and search query.
28+
///
29+
/// - Parameter onCompletion: called when search completes, returns an error or an array of bookings.
30+
///
31+
case searchBookings(siteID: Int64,
32+
searchQuery: String,
33+
pageNumber: Int,
34+
pageSize: Int = BookingsRemote.Default.pageSize,
35+
startDateBefore: String? = nil,
36+
startDateAfter: String? = nil,
37+
onCompletion: (Result<[Booking], Error>) -> Void)
2638
}

Modules/Sources/Yosemite/Stores/BookingStore.swift

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ public class BookingStore: Store {
4545
onCompletion: onCompletion)
4646
case let .checkIfStoreHasBookings(siteID, onCompletion):
4747
checkIfStoreHasBookings(siteID: siteID, onCompletion: onCompletion)
48+
case let .searchBookings(siteID, searchQuery, pageNumber, pageSize, startDateBefore, startDateAfter, onCompletion):
49+
searchBookings(siteID: siteID,
50+
searchQuery: searchQuery,
51+
pageNumber: pageNumber,
52+
pageSize: pageSize,
53+
startDateBefore: startDateBefore,
54+
startDateAfter: startDateAfter,
55+
onCompletion: onCompletion)
4856
}
4957
}
5058
}
@@ -69,7 +77,8 @@ private extension BookingStore {
6977
pageNumber: pageNumber,
7078
pageSize: pageSize,
7179
startDateBefore: startDateBefore,
72-
startDateAfter: startDateAfter)
80+
startDateAfter: startDateAfter,
81+
searchQuery: nil)
7382
await upsertStoredBookingsInBackground(
7483
readOnlyBookings: bookings,
7584
siteID: siteID,
@@ -101,14 +110,40 @@ private extension BookingStore {
101110
pageNumber: 1,
102111
pageSize: 1,
103112
startDateBefore: nil,
104-
startDateAfter: nil)
113+
startDateAfter: nil,
114+
searchQuery: nil)
105115
let hasRemoteBookings = !bookings.isEmpty
106116
onCompletion(.success(hasRemoteBookings))
107117
} catch {
108118
onCompletion(.failure(error))
109119
}
110120
}
111121
}
122+
123+
/// Searches for bookings matching the specified criteria and search query.
124+
/// Returns results immediately without saving to storage.
125+
///
126+
func searchBookings(siteID: Int64,
127+
searchQuery: String,
128+
pageNumber: Int,
129+
pageSize: Int,
130+
startDateBefore: String?,
131+
startDateAfter: String?,
132+
onCompletion: @escaping (Result<[Booking], Error>) -> Void) {
133+
Task { @MainActor in
134+
do {
135+
let bookings = try await remote.loadAllBookings(for: siteID,
136+
pageNumber: pageNumber,
137+
pageSize: pageSize,
138+
startDateBefore: startDateBefore,
139+
startDateAfter: startDateAfter,
140+
searchQuery: searchQuery)
141+
onCompletion(.success(bookings))
142+
} catch {
143+
onCompletion(.failure(error))
144+
}
145+
}
146+
}
112147
}
113148

114149

Modules/Tests/NetworkingTests/Remote/BookingsRemoteTests.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,16 @@ struct BookingsRemoteTests {
4343
let remote = BookingsRemote(network: network)
4444
let startDateBefore = "2024-12-31T23:59:59"
4545
let startDateAfter = "2024-01-01T00:00:00"
46+
let searchQuery = "test search"
4647
network.simulateResponse(requestUrlSuffix: "bookings", filename: "booking-list")
4748

4849
// When
4950
_ = try await remote.loadAllBookings(for: sampleSiteID,
5051
pageNumber: 2,
5152
pageSize: 50,
5253
startDateBefore: startDateBefore,
53-
startDateAfter: startDateAfter)
54+
startDateAfter: startDateAfter,
55+
searchQuery: searchQuery)
5456

5557
// Then
5658
let request = try #require(network.requestsForResponseData.first as? JetpackRequest)
@@ -60,24 +62,27 @@ struct BookingsRemoteTests {
6062
#expect((parameters["per_page"] as? String) == "50")
6163
#expect((parameters["start_date_before"] as? String) == startDateBefore)
6264
#expect((parameters["start_date_after"] as? String) == startDateAfter)
65+
#expect((parameters["search"] as? String) == searchQuery)
6366
}
6467

65-
@Test func test_loadAllBookings_omits_nil_date_parameters() async throws {
68+
@Test func test_loadAllBookings_omits_nil_parameters() async throws {
6669
// Given
6770
let remote = BookingsRemote(network: network)
6871
network.simulateResponse(requestUrlSuffix: "bookings", filename: "booking-list")
6972

7073
// When
7174
_ = try await remote.loadAllBookings(for: sampleSiteID,
7275
startDateBefore: nil,
73-
startDateAfter: nil)
76+
startDateAfter: nil,
77+
searchQuery: nil)
7478

7579
// Then
7680
let request = try #require(network.requestsForResponseData.first as? JetpackRequest)
7781
let parameters = request.parameters
7882

7983
#expect(parameters["start_date_before"] == nil)
8084
#expect(parameters["start_date_after"] == nil)
85+
#expect(parameters["s"] == nil)
8186
#expect(parameters["page"] != nil)
8287
#expect(parameters["per_page"] != nil)
8388
}

Modules/Tests/YosemiteTests/Mocks/MockBookingsRemote.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ final class MockBookingsRemote: BookingsRemoteProtocol {
1414
pageNumber: Int,
1515
pageSize: Int,
1616
startDateBefore: String?,
17-
startDateAfter: String?) async throws -> [Booking] {
17+
startDateAfter: String?,
18+
searchQuery: String?) async throws -> [Booking] {
1819
guard let result = loadAllBookingsResult else {
1920
throw NetworkError.timeout()
2021
}

Modules/Tests/YosemiteTests/Stores/BookingStoreTests.swift

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,87 @@ struct BookingStoreTests {
374374
let error = result.failure as? NetworkError
375375
#expect(error == .timeout())
376376
}
377+
378+
// MARK: - searchBookings
379+
380+
@Test func searchBookings_returns_bookings_on_success() async throws {
381+
// Given
382+
let booking1 = Booking.fake().copy(siteID: sampleSiteID, bookingID: 123)
383+
let booking2 = Booking.fake().copy(siteID: sampleSiteID, bookingID: 456)
384+
remote.whenLoadingAllBookings(thenReturn: .success([booking1, booking2]))
385+
let store = BookingStore(dispatcher: Dispatcher(),
386+
storageManager: storageManager,
387+
network: network,
388+
remote: remote)
389+
390+
// When
391+
let result = await withCheckedContinuation { continuation in
392+
store.onAction(BookingAction.searchBookings(siteID: sampleSiteID,
393+
searchQuery: "test",
394+
pageNumber: defaultPageNumber,
395+
pageSize: defaultPageSize,
396+
onCompletion: { result in
397+
continuation.resume(returning: result)
398+
}))
399+
}
400+
401+
// Then
402+
let bookings = try result.get()
403+
#expect(bookings.count == 2)
404+
#expect(bookings[0].bookingID == 123)
405+
#expect(bookings[1].bookingID == 456)
406+
}
407+
408+
@Test func searchBookings_returns_error_on_failure() async throws {
409+
// Given
410+
remote.whenLoadingAllBookings(thenReturn: .failure(NetworkError.timeout()))
411+
let store = BookingStore(dispatcher: Dispatcher(),
412+
storageManager: storageManager,
413+
network: network,
414+
remote: remote)
415+
416+
// When
417+
let result = await withCheckedContinuation { continuation in
418+
store.onAction(BookingAction.searchBookings(siteID: sampleSiteID,
419+
searchQuery: "test",
420+
pageNumber: defaultPageNumber,
421+
pageSize: defaultPageSize,
422+
onCompletion: { result in
423+
continuation.resume(returning: result)
424+
}))
425+
}
426+
427+
// Then
428+
#expect(result.isFailure)
429+
let error = result.failure as? NetworkError
430+
#expect(error == .timeout())
431+
}
432+
433+
@Test func searchBookings_does_not_save_results_to_storage() async throws {
434+
// Given
435+
let booking = Booking.fake().copy(siteID: sampleSiteID, bookingID: 123)
436+
remote.whenLoadingAllBookings(thenReturn: .success([booking]))
437+
let store = BookingStore(dispatcher: Dispatcher(),
438+
storageManager: storageManager,
439+
network: network,
440+
remote: remote)
441+
#expect(storedBookingCount == 0)
442+
443+
// When
444+
let result = await withCheckedContinuation { continuation in
445+
store.onAction(BookingAction.searchBookings(siteID: sampleSiteID,
446+
searchQuery: "test",
447+
pageNumber: defaultPageNumber,
448+
pageSize: defaultPageSize,
449+
onCompletion: { result in
450+
continuation.resume(returning: result)
451+
}))
452+
}
453+
454+
// Then
455+
#expect(result.isSuccess)
456+
#expect(storedBookingCount == 0)
457+
}
377458
}
378459

379460
private extension BookingStoreTests {

WooCommerce/Classes/Bookings/BookingList/BookingListContainerView.swift

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import struct Yosemite.Booking
33

44
struct BookingListContainerView: View {
55
@ObservedObject private var viewModel: BookingListContainerViewModel
6+
@State private var isSearching = false
67
@ScaledMetric private var scale: CGFloat = 1.0
78
@Binding var selectedBooking: Booking?
89

@@ -18,6 +19,7 @@ struct BookingListContainerView: View {
1819
ForEach(BookingListTab.allCases, id: \.rawValue) { tab in
1920
BookingListView(
2021
viewModel: viewModel.listViewModel(for: tab),
22+
searchViewModel: viewModel.searchViewModel(for: tab),
2123
selectedBooking: $selectedBooking
2224
)
2325
.tag(tab)
@@ -26,11 +28,21 @@ struct BookingListContainerView: View {
2628
.tabViewStyle(.page(indexDisplayMode: .never))
2729
}
2830
.navigationTitle(Localization.viewTitle)
31+
.if(isSearching, transform: { view in
32+
view.searchable(text: $viewModel.searchQuery,
33+
isPresented: $isSearching,
34+
prompt: Localization.searchPrompt)
35+
})
2936
.toolbar(removing: .sidebarToggle)
3037
.toolbar {
3138
ToolbarItem(placement: .confirmationAction) {
3239
Button {
33-
// TODO
40+
withAnimation {
41+
isSearching.toggle()
42+
if !isSearching {
43+
viewModel.searchQuery = ""
44+
}
45+
}
3446
} label: {
3547
Image(systemName: "magnifyingglass")
3648
}
@@ -136,5 +148,10 @@ private extension BookingListContainerView {
136148
value: "Filter",
137149
comment: "Button to filter the booking list"
138150
)
151+
static let searchPrompt = NSLocalizedString(
152+
"bookingListView.search.prompt",
153+
value: "Search bookings",
154+
comment: "Prompt in the search bar on top of the booking list"
155+
)
139156
}
140157
}

0 commit comments

Comments
 (0)