Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions Modules/Sources/Networking/Remote/BookingsRemote.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ public protocol BookingsRemoteProtocol {
pageNumber: Int,
pageSize: Int,
startDateBefore: String?,
startDateAfter: String?) async throws -> [Booking]
startDateAfter: String?,
searchQuery: String?) async throws -> [Booking]
}

/// Booking: Remote Endpoints
Expand All @@ -27,12 +28,14 @@ public final class BookingsRemote: Remote, BookingsRemoteProtocol {
/// - pageSize: Number of bookings to be retrieved per page.
/// - startDateBefore: Filter bookings with start date before this timestamp.
/// - startDateAfter: Filter bookings with start date after this timestamp.
/// - searchQuery: Search query to filter bookings.
///
public func loadAllBookings(for siteID: Int64,
pageNumber: Int = Default.pageNumber,
pageSize: Int = Default.pageSize,
startDateBefore: String? = nil,
startDateAfter: String? = nil) async throws -> [Booking] {
startDateAfter: String? = nil,
searchQuery: String? = nil) async throws -> [Booking] {
var parameters = [
ParameterKey.page: String(pageNumber),
ParameterKey.perPage: String(pageSize)
Expand All @@ -46,6 +49,10 @@ public final class BookingsRemote: Remote, BookingsRemoteProtocol {
parameters[ParameterKey.startDateAfter] = startDateAfter
}

if let searchQuery = searchQuery, !searchQuery.isEmpty {
parameters[ParameterKey.search] = searchQuery
}

let path = Path.bookings
let request = JetpackRequest(wooApiVersion: .wcBookings, method: .get, siteID: siteID, path: path, parameters: parameters, availableAsRESTRequest: true)
let mapper = ListMapper<Booking>(siteID: siteID)
Expand All @@ -71,5 +78,6 @@ public extension BookingsRemote {
static let perPage: String = "per_page"
static let startDateBefore: String = "start_date_before"
static let startDateAfter: String = "start_date_after"
static let search: String = "search"
}
}
12 changes: 12 additions & 0 deletions Modules/Sources/Yosemite/Actions/BookingAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,16 @@ public enum BookingAction: Action {
///
case checkIfStoreHasBookings(siteID: Int64,
onCompletion: (Result<Bool, Error>) -> Void)

/// Searches for bookings matching the specified criteria and search query.
///
/// - Parameter onCompletion: called when search completes, returns an error or an array of bookings.
///
case searchBookings(siteID: Int64,
searchQuery: String,
pageNumber: Int,
pageSize: Int = BookingsRemote.Default.pageSize,
startDateBefore: String? = nil,
startDateAfter: String? = nil,
onCompletion: (Result<[Booking], Error>) -> Void)
}
39 changes: 37 additions & 2 deletions Modules/Sources/Yosemite/Stores/BookingStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ public class BookingStore: Store {
onCompletion: onCompletion)
case let .checkIfStoreHasBookings(siteID, onCompletion):
checkIfStoreHasBookings(siteID: siteID, onCompletion: onCompletion)
case let .searchBookings(siteID, searchQuery, pageNumber, pageSize, startDateBefore, startDateAfter, onCompletion):
searchBookings(siteID: siteID,
searchQuery: searchQuery,
pageNumber: pageNumber,
pageSize: pageSize,
startDateBefore: startDateBefore,
startDateAfter: startDateAfter,
onCompletion: onCompletion)
}
}
}
Expand All @@ -69,7 +77,8 @@ private extension BookingStore {
pageNumber: pageNumber,
pageSize: pageSize,
startDateBefore: startDateBefore,
startDateAfter: startDateAfter)
startDateAfter: startDateAfter,
searchQuery: nil)
await upsertStoredBookingsInBackground(
readOnlyBookings: bookings,
siteID: siteID,
Expand Down Expand Up @@ -101,14 +110,40 @@ private extension BookingStore {
pageNumber: 1,
pageSize: 1,
startDateBefore: nil,
startDateAfter: nil)
startDateAfter: nil,
searchQuery: nil)
let hasRemoteBookings = !bookings.isEmpty
onCompletion(.success(hasRemoteBookings))
} catch {
onCompletion(.failure(error))
}
}
}

/// Searches for bookings matching the specified criteria and search query.
/// Returns results immediately without saving to storage.
///
func searchBookings(siteID: Int64,
searchQuery: String,
pageNumber: Int,
pageSize: Int,
startDateBefore: String?,
startDateAfter: String?,
onCompletion: @escaping (Result<[Booking], Error>) -> Void) {
Task { @MainActor in
do {
let bookings = try await remote.loadAllBookings(for: siteID,
pageNumber: pageNumber,
pageSize: pageSize,
startDateBefore: startDateBefore,
startDateAfter: startDateAfter,
searchQuery: searchQuery)
onCompletion(.success(bookings))
} catch {
onCompletion(.failure(error))
}
}
}
}


Expand Down
11 changes: 8 additions & 3 deletions Modules/Tests/NetworkingTests/Remote/BookingsRemoteTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,16 @@ struct BookingsRemoteTests {
let remote = BookingsRemote(network: network)
let startDateBefore = "2024-12-31T23:59:59"
let startDateAfter = "2024-01-01T00:00:00"
let searchQuery = "test search"
network.simulateResponse(requestUrlSuffix: "bookings", filename: "booking-list")

// When
_ = try await remote.loadAllBookings(for: sampleSiteID,
pageNumber: 2,
pageSize: 50,
startDateBefore: startDateBefore,
startDateAfter: startDateAfter)
startDateAfter: startDateAfter,
searchQuery: searchQuery)

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

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

// When
_ = try await remote.loadAllBookings(for: sampleSiteID,
startDateBefore: nil,
startDateAfter: nil)
startDateAfter: nil,
searchQuery: nil)

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

#expect(parameters["start_date_before"] == nil)
#expect(parameters["start_date_after"] == nil)
#expect(parameters["s"] == nil)
#expect(parameters["page"] != nil)
#expect(parameters["per_page"] != nil)
}
Expand Down
3 changes: 2 additions & 1 deletion Modules/Tests/YosemiteTests/Mocks/MockBookingsRemote.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ final class MockBookingsRemote: BookingsRemoteProtocol {
pageNumber: Int,
pageSize: Int,
startDateBefore: String?,
startDateAfter: String?) async throws -> [Booking] {
startDateAfter: String?,
searchQuery: String?) async throws -> [Booking] {
guard let result = loadAllBookingsResult else {
throw NetworkError.timeout()
}
Expand Down
81 changes: 81 additions & 0 deletions Modules/Tests/YosemiteTests/Stores/BookingStoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,87 @@ struct BookingStoreTests {
let error = result.failure as? NetworkError
#expect(error == .timeout())
}

// MARK: - searchBookings

@Test func searchBookings_returns_bookings_on_success() async throws {
// Given
let booking1 = Booking.fake().copy(siteID: sampleSiteID, bookingID: 123)
let booking2 = Booking.fake().copy(siteID: sampleSiteID, bookingID: 456)
remote.whenLoadingAllBookings(thenReturn: .success([booking1, booking2]))
let store = BookingStore(dispatcher: Dispatcher(),
storageManager: storageManager,
network: network,
remote: remote)

// When
let result = await withCheckedContinuation { continuation in
store.onAction(BookingAction.searchBookings(siteID: sampleSiteID,
searchQuery: "test",
pageNumber: defaultPageNumber,
pageSize: defaultPageSize,
onCompletion: { result in
continuation.resume(returning: result)
}))
}

// Then
let bookings = try result.get()
#expect(bookings.count == 2)
#expect(bookings[0].bookingID == 123)
#expect(bookings[1].bookingID == 456)
}

@Test func searchBookings_returns_error_on_failure() async throws {
// Given
remote.whenLoadingAllBookings(thenReturn: .failure(NetworkError.timeout()))
let store = BookingStore(dispatcher: Dispatcher(),
storageManager: storageManager,
network: network,
remote: remote)

// When
let result = await withCheckedContinuation { continuation in
store.onAction(BookingAction.searchBookings(siteID: sampleSiteID,
searchQuery: "test",
pageNumber: defaultPageNumber,
pageSize: defaultPageSize,
onCompletion: { result in
continuation.resume(returning: result)
}))
}

// Then
#expect(result.isFailure)
let error = result.failure as? NetworkError
#expect(error == .timeout())
}

@Test func searchBookings_does_not_save_results_to_storage() async throws {
// Given
let booking = Booking.fake().copy(siteID: sampleSiteID, bookingID: 123)
remote.whenLoadingAllBookings(thenReturn: .success([booking]))
let store = BookingStore(dispatcher: Dispatcher(),
storageManager: storageManager,
network: network,
remote: remote)
#expect(storedBookingCount == 0)

// When
let result = await withCheckedContinuation { continuation in
store.onAction(BookingAction.searchBookings(siteID: sampleSiteID,
searchQuery: "test",
pageNumber: defaultPageNumber,
pageSize: defaultPageSize,
onCompletion: { result in
continuation.resume(returning: result)
}))
}

// Then
#expect(result.isSuccess)
#expect(storedBookingCount == 0)
}
}

private extension BookingStoreTests {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import struct Yosemite.Booking

struct BookingListContainerView: View {
@ObservedObject private var viewModel: BookingListContainerViewModel
@State private var isSearching = false
@ScaledMetric private var scale: CGFloat = 1.0
@Binding var selectedBooking: Booking?

Expand All @@ -18,6 +19,7 @@ struct BookingListContainerView: View {
ForEach(BookingListTab.allCases, id: \.rawValue) { tab in
BookingListView(
viewModel: viewModel.listViewModel(for: tab),
searchViewModel: viewModel.searchViewModel(for: tab),
selectedBooking: $selectedBooking
)
.tag(tab)
Expand All @@ -26,11 +28,21 @@ struct BookingListContainerView: View {
.tabViewStyle(.page(indexDisplayMode: .never))
}
.navigationTitle(Localization.viewTitle)
.if(isSearching, transform: { view in
view.searchable(text: $viewModel.searchQuery,
isPresented: $isSearching,
prompt: Localization.searchPrompt)
})
.toolbar(removing: .sidebarToggle)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button {
// TODO
withAnimation {
isSearching.toggle()
if !isSearching {
viewModel.searchQuery = ""
}
}
} label: {
Image(systemName: "magnifyingglass")
}
Expand Down Expand Up @@ -136,5 +148,10 @@ private extension BookingListContainerView {
value: "Filter",
comment: "Button to filter the booking list"
)
static let searchPrompt = NSLocalizedString(
"bookingListView.search.prompt",
value: "Search bookings",
comment: "Prompt in the search bar on top of the booking list"
)
}
}
Loading