Skip to content

Commit 086b94f

Browse files
committed
Merge branch 'trunk' into WOOMOB-1508-add-yosemite-models-for-bookings-info
2 parents 2f0855b + 17d34bf commit 086b94f

File tree

11 files changed

+453
-38
lines changed

11 files changed

+453
-38
lines changed

Modules/Sources/Networking/Remote/BookingsRemote.swift

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ public protocol BookingsRemoteProtocol {
1111
pageSize: Int,
1212
startDateBefore: String?,
1313
startDateAfter: String?,
14-
searchQuery: String?) async throws -> [Booking]
14+
searchQuery: String?,
15+
order: BookingsRemote.Order) async throws -> [Booking]
1516

1617
func loadBooking(bookingID: Int64,
1718
siteID: Int64) async throws -> Booking?
@@ -32,16 +33,19 @@ public final class BookingsRemote: Remote, BookingsRemoteProtocol {
3233
/// - startDateBefore: Filter bookings with start date before this timestamp.
3334
/// - startDateAfter: Filter bookings with start date after this timestamp.
3435
/// - searchQuery: Search query to filter bookings.
36+
/// - order: Sort order for bookings (ascending or descending).
3537
///
3638
public func loadAllBookings(for siteID: Int64,
3739
pageNumber: Int = Default.pageNumber,
3840
pageSize: Int = Default.pageSize,
3941
startDateBefore: String? = nil,
4042
startDateAfter: String? = nil,
41-
searchQuery: String? = nil) async throws -> [Booking] {
43+
searchQuery: String? = nil,
44+
order: Order) async throws -> [Booking] {
4245
var parameters = [
4346
ParameterKey.page: String(pageNumber),
44-
ParameterKey.perPage: String(pageSize)
47+
ParameterKey.perPage: String(pageSize),
48+
ParameterKey.order: order.rawValue
4549
]
4650

4751
if let startDateBefore = startDateBefore {
@@ -90,6 +94,11 @@ public extension BookingsRemote {
9094
public static let pageNumber: Int = Remote.Default.firstPageNumber
9195
}
9296

97+
enum Order: String {
98+
case ascending = "asc"
99+
case descending = "desc"
100+
}
101+
93102
private enum Path {
94103
static let bookings = "bookings"
95104
}
@@ -100,5 +109,6 @@ public extension BookingsRemote {
100109
static let startDateBefore: String = "start_date_before"
101110
static let startDateAfter: String = "start_date_after"
102111
static let search: String = "search"
112+
static let order: String = "order"
103113
}
104114
}

Modules/Sources/Yosemite/Actions/BookingAction.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public enum BookingAction: Action {
1515
pageSize: Int = BookingsRemote.Default.pageSize,
1616
startDateBefore: String? = nil,
1717
startDateAfter: String? = nil,
18+
order: BookingsRemote.Order = .descending,
1819
shouldClearCache: Bool = false,
1920
onCompletion: (Result<Bool, Error>) -> Void)
2021
/// Synchronizes the Booking matching the specified criteria.
@@ -41,5 +42,6 @@ public enum BookingAction: Action {
4142
pageSize: Int = BookingsRemote.Default.pageSize,
4243
startDateBefore: String? = nil,
4344
startDateAfter: String? = nil,
45+
order: BookingsRemote.Order = .descending,
4446
onCompletion: (Result<[Booking], Error>) -> Void)
4547
}

Modules/Sources/Yosemite/Stores/BookingStore.swift

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,25 +39,27 @@ public class BookingStore: Store {
3939
}
4040

4141
switch action {
42-
case let .synchronizeBookings(siteID, pageNumber, pageSize, startDateBefore, startDateAfter, shouldClearCache, onCompletion):
42+
case let .synchronizeBookings(siteID, pageNumber, pageSize, startDateBefore, startDateAfter, order, shouldClearCache, onCompletion):
4343
synchronizeBookings(siteID: siteID,
4444
pageNumber: pageNumber,
4545
pageSize: pageSize,
4646
startDateBefore: startDateBefore,
4747
startDateAfter: startDateAfter,
48+
order: order,
4849
shouldClearCache: shouldClearCache,
4950
onCompletion: onCompletion)
5051
case .synchronizeBooking(siteID: let siteID, bookingID: let bookingID, onCompletion: let onCompletion):
5152
synchronizeBooking(siteID: siteID, bookingID: bookingID, onCompletion: onCompletion)
5253
case let .checkIfStoreHasBookings(siteID, onCompletion):
5354
checkIfStoreHasBookings(siteID: siteID, onCompletion: onCompletion)
54-
case let .searchBookings(siteID, searchQuery, pageNumber, pageSize, startDateBefore, startDateAfter, onCompletion):
55+
case let .searchBookings(siteID, searchQuery, pageNumber, pageSize, startDateBefore, startDateAfter, order, onCompletion):
5556
searchBookings(siteID: siteID,
5657
searchQuery: searchQuery,
5758
pageNumber: pageNumber,
5859
pageSize: pageSize,
5960
startDateBefore: startDateBefore,
6061
startDateAfter: startDateAfter,
62+
order: order,
6163
onCompletion: onCompletion)
6264
}
6365
}
@@ -75,6 +77,7 @@ private extension BookingStore {
7577
pageSize: Int,
7678
startDateBefore: String?,
7779
startDateAfter: String?,
80+
order: BookingsRemote.Order,
7881
shouldClearCache: Bool,
7982
onCompletion: @escaping (Result<Bool, Error>) -> Void) {
8083
Task { @MainActor in
@@ -84,7 +87,8 @@ private extension BookingStore {
8487
pageSize: pageSize,
8588
startDateBefore: startDateBefore,
8689
startDateAfter: startDateAfter,
87-
searchQuery: nil)
90+
searchQuery: nil,
91+
order: order)
8892

8993
let orders = try await ordersRemote.loadOrders(
9094
for: siteID,
@@ -163,7 +167,8 @@ private extension BookingStore {
163167
pageSize: 1,
164168
startDateBefore: nil,
165169
startDateAfter: nil,
166-
searchQuery: nil)
170+
searchQuery: nil,
171+
order: .descending)
167172
let hasRemoteBookings = !bookings.isEmpty
168173
onCompletion(.success(hasRemoteBookings))
169174
} catch {
@@ -181,6 +186,7 @@ private extension BookingStore {
181186
pageSize: Int,
182187
startDateBefore: String?,
183188
startDateAfter: String?,
189+
order: BookingsRemote.Order,
184190
onCompletion: @escaping (Result<[Booking], Error>) -> Void) {
185191
Task { @MainActor in
186192
do {
@@ -189,7 +195,8 @@ private extension BookingStore {
189195
pageSize: pageSize,
190196
startDateBefore: startDateBefore,
191197
startDateAfter: startDateAfter,
192-
searchQuery: searchQuery)
198+
searchQuery: searchQuery,
199+
order: order)
193200
onCompletion(.success(bookings))
194201
} catch {
195202
onCompletion(.failure(error))

Modules/Tests/NetworkingTests/Remote/BookingsRemoteTests.swift

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ struct BookingsRemoteTests {
1313
network.simulateResponse(requestUrlSuffix: "bookings", filename: "booking-list")
1414

1515
// When
16-
let bookings = try await remote.loadAllBookings(for: sampleSiteID)
16+
let bookings = try await remote.loadAllBookings(for: sampleSiteID, order: .descending)
1717

1818
// Then
1919
#expect(bookings.count == 2)
@@ -34,7 +34,7 @@ struct BookingsRemoteTests {
3434

3535
// Then
3636
await #expect(throws: NetworkError.notFound()) {
37-
_ = try await remote.loadAllBookings(for: sampleSiteID)
37+
_ = try await remote.loadAllBookings(for: sampleSiteID, order: .descending)
3838
}
3939
}
4040

@@ -52,7 +52,8 @@ struct BookingsRemoteTests {
5252
pageSize: 50,
5353
startDateBefore: startDateBefore,
5454
startDateAfter: startDateAfter,
55-
searchQuery: searchQuery)
55+
searchQuery: searchQuery,
56+
order: .ascending)
5657

5758
// Then
5859
let request = try #require(network.requestsForResponseData.first as? JetpackRequest)
@@ -63,6 +64,7 @@ struct BookingsRemoteTests {
6364
#expect((parameters["start_date_before"] as? String) == startDateBefore)
6465
#expect((parameters["start_date_after"] as? String) == startDateAfter)
6566
#expect((parameters["search"] as? String) == searchQuery)
67+
#expect((parameters["order"] as? String) == "asc")
6668
}
6769

6870
@Test func test_loadAllBookings_omits_nil_parameters() async throws {
@@ -74,7 +76,8 @@ struct BookingsRemoteTests {
7476
_ = try await remote.loadAllBookings(for: sampleSiteID,
7577
startDateBefore: nil,
7678
startDateAfter: nil,
77-
searchQuery: nil)
79+
searchQuery: nil,
80+
order: .descending)
7881

7982
// Then
8083
let request = try #require(network.requestsForResponseData.first as? JetpackRequest)
@@ -85,5 +88,6 @@ struct BookingsRemoteTests {
8588
#expect(parameters["s"] == nil)
8689
#expect(parameters["page"] != nil)
8790
#expect(parameters["per_page"] != nil)
91+
#expect(parameters["order"] != nil)
8892
}
8993
}

Modules/Tests/YosemiteTests/Mocks/MockBookingsRemote.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ final class MockBookingsRemote: BookingsRemoteProtocol {
2020
pageSize: Int,
2121
startDateBefore: String?,
2222
startDateAfter: String?,
23-
searchQuery: String?) async throws -> [Booking] {
23+
searchQuery: String?,
24+
order: BookingsRemote.Order) async throws -> [Booking] {
2425
guard let result = loadAllBookingsResult else {
2526
throw NetworkError.timeout()
2627
}

WooCommerce/Classes/Bookings/BookingList/BookingListContainerView.swift

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import struct Yosemite.Booking
44
struct BookingListContainerView: View {
55
@ObservedObject private var viewModel: BookingListContainerViewModel
66
@State private var isSearching = false
7+
@State private var showingSortOptions = false
8+
79
@ScaledMetric private var scale: CGFloat = 1.0
810
@Binding var selectedBooking: Booking?
911

@@ -48,6 +50,10 @@ struct BookingListContainerView: View {
4850
}
4951
}
5052
}
53+
.sheet(isPresented: $showingSortOptions) {
54+
sortingOptions
55+
.presentationDetents([.fraction(0.25), .medium, .large])
56+
}
5157
}
5258
}
5359

@@ -58,7 +64,7 @@ private extension BookingListContainerView {
5864
Divider()
5965
HStack {
6066
Button {
61-
// TODO
67+
showingSortOptions = true
6268
} label: {
6369
Text(Localization.sortBy)
6470
.font(.body)
@@ -125,11 +131,54 @@ private extension BookingListContainerView {
125131

126132
return distanceFromLeftEdge - adjustmentForCenterOrigin + centerWithinTab
127133
}
134+
135+
var sortingOptions: some View {
136+
ScrollView {
137+
VStack(alignment: .leading, spacing: Layout.SortingOptions.contentSpacing) {
138+
Text(Localization.sortBy)
139+
.font(.subheadline.weight(.medium))
140+
.foregroundStyle(.secondary)
141+
142+
ForEach(BookingListViewModel.SortBy.allCases, id: \.rawValue) { sortBy in
143+
Button {
144+
viewModel.sortBy = sortBy
145+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
146+
showingSortOptions = false
147+
}
148+
} label: {
149+
HStack(alignment: .firstTextBaseline) {
150+
Text(sortBy.title)
151+
.font(.body.weight(.medium))
152+
.foregroundStyle(Color.primary)
153+
Spacer()
154+
Image(systemName: "checkmark")
155+
.font(.title3.weight(.medium))
156+
.foregroundStyle(Color.accentColor)
157+
.renderedIf(viewModel.sortBy == sortBy)
158+
}
159+
.frame(maxWidth: .infinity, alignment: .leading)
160+
.multilineTextAlignment(.leading)
161+
}
162+
}
163+
164+
Spacer()
165+
}
166+
.padding(.horizontal, Layout.SortingOptions.margin)
167+
.padding(.vertical, Layout.SortingOptions.topPadding)
168+
}
169+
}
128170
}
171+
172+
129173
private extension BookingListContainerView {
130174
enum Layout {
131175
static let topTabBarHeight: CGFloat = 44
132176
static let selectedTabIndicatorHeight: CGFloat = 3.0
177+
enum SortingOptions {
178+
static let contentSpacing: CGFloat = 24
179+
static let margin: CGFloat = 16
180+
static let topPadding: CGFloat = 52
181+
}
133182
}
134183

135184
enum Localization {

WooCommerce/Classes/Bookings/BookingList/BookingListContainerViewModel.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ final class BookingListContainerViewModel: ObservableObject {
1313

1414
@Published var selectedTab: BookingListTab = .today
1515
@Published var searchQuery: String = ""
16+
@Published var sortBy: BookingListViewModel.SortBy = .newestToOldest
1617

1718
private let searchQuerySubject = PassthroughSubject<String, Never>()
1819
private var searchQuerySubscription: AnyCancellable?
20+
private var sortBySubscription: AnyCancellable?
1921

2022
init(siteID: Int64) {
2123
let searchQueryPublisher = searchQuerySubject.eraseToAnyPublisher()
@@ -52,6 +54,18 @@ final class BookingListContainerViewModel: ObservableObject {
5254
.sink { [weak self] query in
5355
self?.searchQuerySubject.send(query)
5456
}
57+
58+
sortBySubscription = $sortBy
59+
.removeDuplicates()
60+
.sink { [weak self] sortBy in
61+
guard let self else { return }
62+
todayListViewModel.updateSortOrder(sortBy)
63+
upcomingListViewModel.updateSortOrder(sortBy)
64+
allListViewModel.updateSortOrder(sortBy)
65+
todaySearchViewModel.updateSortOrder(sortBy)
66+
upcomingSearchViewModel.updateSortOrder(sortBy)
67+
allSearchViewModel.updateSortOrder(sortBy)
68+
}
5569
}
5670

5771
func listViewModel(for tab: BookingListTab) -> BookingListViewModel {

WooCommerce/Classes/Bookings/BookingList/BookingListViewModel.swift

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import SwiftUI
33
import Yosemite
44
import Combine
55
import protocol Storage.StorageManagerType
6+
import class Networking.BookingsRemote
67

78
/// View model for `BookingListView`
89
final class BookingListViewModel: ObservableObject {
@@ -29,8 +30,10 @@ final class BookingListViewModel: ObservableObject {
2930
private let stores: StoresManager
3031
private let storage: StorageManagerType
3132
private let currentDate: Date
33+
private var currentOrder: SortBy = .newestToOldest
3234

3335
private static let refreshCacheReason = "refresh-cache"
36+
private static let reorderReason = "reorder"
3437

3538
/// Keeps track of the current state of the syncing
3639
@Published private(set) var syncState: SyncState = .empty
@@ -99,6 +102,20 @@ final class BookingListViewModel: ObservableObject {
99102
}
100103
}
101104
}
105+
106+
/// Updates the sort order and reloads the results controller.
107+
func updateSortOrder(_ sortBy: SortBy) {
108+
currentOrder = sortBy
109+
let ascending = sortBy == .oldestToNewest
110+
let sortDescriptorByDate = NSSortDescriptor(key: "startDate", ascending: ascending)
111+
resultsController.sortDescriptors = [sortDescriptorByDate]
112+
paginationTracker.resync(reason: Self.reorderReason) {}
113+
}
114+
115+
/// Converts SortBy to BookingsRemote.Order
116+
private func remoteOrder(from sortBy: SortBy) -> BookingsRemote.Order {
117+
sortBy == .oldestToNewest ? .ascending : .descending
118+
}
102119
}
103120

104121
// MARK: Configuration
@@ -144,6 +161,7 @@ extension BookingListViewModel: PaginationTrackerDelegate {
144161
pageSize: pageSize,
145162
startDateBefore: type.startDateBefore(currentDate: currentDate)?.ISO8601Format(),
146163
startDateAfter: type.startDateAfter(currentDate: currentDate)?.ISO8601Format(),
164+
order: remoteOrder(from: currentOrder),
147165
shouldClearCache: shouldClearCache
148166
) { [weak self] result in
149167
switch result {
@@ -188,3 +206,30 @@ extension BookingListViewModel {
188206
syncState = bookings.isNotEmpty ? .results : .empty
189207
}
190208
}
209+
210+
extension BookingListViewModel {
211+
enum SortBy: Int, CaseIterable {
212+
case newestToOldest
213+
case oldestToNewest
214+
215+
var title: String {
216+
switch self {
217+
case .newestToOldest: Localization.newestToOldest
218+
case .oldestToNewest: Localization.oldestToNewest
219+
}
220+
}
221+
222+
enum Localization {
223+
static let newestToOldest = NSLocalizedString(
224+
"bookingList.sort.newestToOldest",
225+
value: "Date: Newest to Oldest",
226+
comment: "Option to sort bookings from newest to oldest"
227+
)
228+
static let oldestToNewest = NSLocalizedString(
229+
"bookingList.sort.oldestToNewest",
230+
value: "Date: Oldest to Newest",
231+
comment: "Option to sort bookings from oldest to newest"
232+
)
233+
}
234+
}
235+
}

0 commit comments

Comments
 (0)