Skip to content

Commit a696bb2

Browse files
authored
Bookings: Multiple selection support (#16285)
2 parents fbfb087 + a0ed11e commit a696bb2

File tree

8 files changed

+255
-98
lines changed

8 files changed

+255
-98
lines changed

WooCommerce/Classes/Bookings/BookingFilters/BookableProductListSyncable.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Yosemite
55
struct BookableProductListSyncable: ListSyncable {
66
typealias StorageType = StorageProduct
77
typealias ModelType = Product
8+
typealias ListFilterType = BookingProductFilter
89

910
let siteID: Int64
1011

@@ -50,6 +51,10 @@ struct BookableProductListSyncable: ListSyncable {
5051
func displayName(for item: Product) -> String {
5152
item.name
5253
}
54+
55+
func filterItem(for item: Product) -> BookingProductFilter {
56+
BookingProductFilter(productID: item.productID, name: item.name)
57+
}
5358
}
5459

5560
private extension BookableProductListSyncable {

WooCommerce/Classes/Bookings/BookingFilters/BookingFiltersViewModel.swift

Lines changed: 37 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,18 @@ final class BookingFiltersViewModel: FilterListViewModel {
3535
}
3636

3737
var criteria: Filters {
38-
let teamMember = teamMemberFilterViewModel.selectedValue as? BookingResource
39-
let product = productFilterViewModel.selectedValue as? BookingProductFilter
38+
let teamMembers = (teamMemberFilterViewModel.selectedValue as? MultipleFilterSelection)?.items as? [BookingResource] ?? []
39+
let products = (productFilterViewModel.selectedValue as? MultipleFilterSelection)?.items as? [BookingProductFilter] ?? []
4040
let customer = customerFilterViewModel.selectedValue as? CustomerFilter
41-
let attendanceStatus = attendanceStatusFilterViewModel.selectedValue as? BookingAttendanceStatus
42-
let paymentStatus = paymentStatusFilterViewModel.selectedValue as? BookingStatus
41+
let attendanceStatuses = (attendanceStatusFilterViewModel.selectedValue as? MultipleFilterSelection)?.items as? [BookingAttendanceStatus] ?? []
42+
let paymentStatuses = (paymentStatusFilterViewModel.selectedValue as? MultipleFilterSelection)?.items as? [BookingStatus] ?? []
4343
let dateRange = dateTimeFilterViewModel.selectedValue as? BookingDateRangeFilter
4444
let numberOfActiveFilters = filterTypeViewModels.numberOfActiveFilters
4545

46-
return Filters(teamMember: teamMember,
47-
product: product,
48-
attendanceStatus: attendanceStatus,
49-
paymentStatus: paymentStatus,
46+
return Filters(teamMembers: teamMembers,
47+
products: products,
48+
attendanceStatuses: attendanceStatuses,
49+
paymentStatuses: paymentStatuses,
5050
customer: customer,
5151
dateRange: dateRange,
5252
numberOfActiveFilters: numberOfActiveFilters)
@@ -86,55 +86,47 @@ final class BookingFiltersViewModel: FilterListViewModel {
8686

8787
struct Filters: Equatable, HumanReadable {
8888

89-
let teamMember: BookingResource?
90-
let product: BookingProductFilter?
91-
let attendanceStatus: BookingAttendanceStatus?
92-
let paymentStatus: BookingStatus?
89+
let teamMembers: [BookingResource]
90+
let products: [BookingProductFilter]
91+
let attendanceStatuses: [BookingAttendanceStatus]
92+
let paymentStatuses: [BookingStatus]
9393
let customer: CustomerFilter?
9494
let dateRange: BookingDateRangeFilter?
9595

9696
let numberOfActiveFilters: Int
9797

9898
init() {
99-
teamMember = nil
100-
product = nil
101-
attendanceStatus = nil
102-
paymentStatus = nil
99+
teamMembers = []
100+
products = []
101+
attendanceStatuses = []
102+
paymentStatuses = []
103103
customer = nil
104104
dateRange = nil
105105
numberOfActiveFilters = 0
106106
}
107107

108-
init(teamMember: BookingResource?,
109-
product: BookingProductFilter?,
110-
attendanceStatus: BookingAttendanceStatus?,
111-
paymentStatus: BookingStatus?,
108+
init(teamMembers: [BookingResource],
109+
products: [BookingProductFilter],
110+
attendanceStatuses: [BookingAttendanceStatus],
111+
paymentStatuses: [BookingStatus],
112112
customer: CustomerFilter?,
113113
dateRange: BookingDateRangeFilter?,
114114
numberOfActiveFilters: Int) {
115-
self.teamMember = teamMember
116-
self.product = product
117-
self.attendanceStatus = attendanceStatus
118-
self.paymentStatus = paymentStatus
115+
self.teamMembers = teamMembers
116+
self.products = products
117+
self.attendanceStatuses = attendanceStatuses
118+
self.paymentStatuses = paymentStatuses
119119
self.customer = customer
120120
self.dateRange = dateRange
121121
self.numberOfActiveFilters = numberOfActiveFilters
122122
}
123123

124124
var readableString: String {
125-
var readable: [String] = []
126-
if let teamMember {
127-
readable.append(teamMember.name)
128-
}
129-
if let product {
130-
readable.append(product.name)
131-
}
132-
if let attendanceStatus {
133-
readable.append(attendanceStatus.localizedTitle)
134-
}
135-
if let paymentStatus {
136-
readable.append(paymentStatus.localizedTitle)
137-
}
125+
var readable: [String] = teamMembers.map { $0.name } +
126+
products.map { $0.name } +
127+
attendanceStatuses.map { $0.localizedTitle } +
128+
paymentStatuses.map { $0.localizedTitle }
129+
138130
if let customer {
139131
readable.append(customer.description)
140132
}
@@ -185,25 +177,25 @@ extension BookingFiltersViewModel.BookingListFilter {
185177
case .teamMember(let siteID):
186178
return FilterTypeViewModel(title: title,
187179
listSelectorConfig: .bookingResource(siteID: siteID),
188-
selectedValue: filters.teamMember)
180+
selectedValue: MultipleFilterSelection(items: filters.teamMembers))
189181
case .product(let siteID):
190182
return FilterTypeViewModel(title: title,
191183
listSelectorConfig: .bookableProduct(siteID: siteID),
192-
selectedValue: filters.product)
184+
selectedValue: MultipleFilterSelection(items: filters.products))
193185
case .customer(let siteID):
194186
return FilterTypeViewModel(title: title,
195187
listSelectorConfig: .customer(siteID: siteID, source: .booking),
196188
selectedValue: filters.customer)
197189
case .attendanceStatus:
198-
let options: [BookingAttendanceStatus?] = [nil, .booked, .checkedIn, .cancelled, .noShow]
190+
let options: [BookingAttendanceStatus?] = [.booked, .checkedIn, .cancelled, .noShow]
199191
return FilterTypeViewModel(title: title,
200-
listSelectorConfig: .staticOptions(options: options),
201-
selectedValue: filters.attendanceStatus)
192+
listSelectorConfig: .multiSelectStaticOptions(options: options),
193+
selectedValue: MultipleFilterSelection(items: filters.attendanceStatuses))
202194
case .paymentStatus:
203-
let options: [BookingStatus?] = [nil, .complete, .paid, .unpaid, .cancelled, .pendingConfirmation, .confirmed]
195+
let options: [BookingStatus?] = [.complete, .paid, .unpaid, .cancelled, .pendingConfirmation, .confirmed]
204196
return FilterTypeViewModel(title: title,
205-
listSelectorConfig: .staticOptions(options: options),
206-
selectedValue: filters.paymentStatus)
197+
listSelectorConfig: .multiSelectStaticOptions(options: options),
198+
selectedValue: MultipleFilterSelection(items: filters.paymentStatuses))
207199
case .dateTime:
208200
return FilterTypeViewModel(title: title,
209201
listSelectorConfig: .bookingDateTime,

WooCommerce/Classes/Bookings/BookingFilters/ListSyncable.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Yosemite
66
protocol ListSyncable {
77
associatedtype StorageType: ResultsControllerMutableType
88
associatedtype ModelType: Equatable & Hashable where ModelType == StorageType.ReadOnlyType
9+
associatedtype ListFilterType: FilterType & Equatable
910

1011
var title: String { get }
1112
var emptyStateMessage: String { get }
@@ -27,4 +28,7 @@ protocol ListSyncable {
2728

2829
/// Returns the display name for an item
2930
func displayName(for item: ModelType) -> String
31+
32+
/// Returns the filter type for an item
33+
func filterItem(for item: ModelType) -> ListFilterType
3034
}

WooCommerce/Classes/Bookings/BookingFilters/SyncableListSelectorView.swift

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,20 @@ import SwiftUI
22

33
struct SyncableListSelectorView<Syncable: ListSyncable>: View {
44
@ObservedObject private var viewModel: SyncableListSelectorViewModel<Syncable>
5-
@State var selectedItem: Syncable.ModelType?
5+
@State private var selectedItems: [Syncable.ListFilterType]
66

77
private let syncable: Syncable
8-
private let initialSelection: (Syncable.ModelType?) -> Bool
9-
private let onSelection: (Syncable.ModelType?) -> Void
8+
private let onSelection: ([Syncable.ListFilterType]) -> Void
109

1110
private let viewPadding: CGFloat = 16
1211

1312
init(viewModel: SyncableListSelectorViewModel<Syncable>,
1413
syncable: Syncable,
15-
initialSelection: @escaping (Syncable.ModelType?) -> Bool,
16-
onSelection: @escaping (Syncable.ModelType?) -> Void) {
14+
initialSelections: [Syncable.ListFilterType],
15+
onSelection: @escaping ([Syncable.ListFilterType]) -> Void) {
1716
self.viewModel = viewModel
1817
self.syncable = syncable
19-
self.initialSelection = initialSelection
18+
self.selectedItems = initialSelections
2019
self.onSelection = onSelection
2120
}
2221

@@ -37,9 +36,6 @@ struct SyncableListSelectorView<Syncable: ListSyncable>: View {
3736
}
3837
.navigationTitle(syncable.title)
3938
.navigationBarTitleDisplayMode(.inline)
40-
.onChange(of: selectedItem) { _, newValue in
41-
onSelection(newValue)
42-
}
4339
}
4440
}
4541

@@ -63,14 +59,17 @@ private extension SyncableListSelectorView {
6359
value: "Any",
6460
comment: "Option to select no filter on a list selector view"
6561
),
66-
isSelected: selectedItem == nil,
67-
onSelection: { selectedItem = nil }
62+
isSelected: selectedItems.isEmpty,
63+
onSelection: {
64+
selectedItems.removeAll()
65+
onSelection([])
66+
}
6867
)
6968

7069
ForEach(items, id: \.self) { item in
7170
optionRow(text: syncable.displayName(for: item),
72-
isSelected: isItemSelected(item),
73-
onSelection: { selectedItem = item })
71+
isSelected: selectedItems.contains(where: { $0 == syncable.filterItem(for: item) }),
72+
onSelection: { toggleSelection(for: item) })
7473
}
7574

7675
InfiniteScrollIndicator(showContent: viewModel.shouldShowBottomActivityIndicator)
@@ -83,11 +82,14 @@ private extension SyncableListSelectorView {
8382
.background(Color(.listBackground))
8483
}
8584

86-
func isItemSelected(_ item: Syncable.ModelType?) -> Bool {
87-
if let selectedItem {
88-
return item == selectedItem
85+
func toggleSelection(for item: Syncable.ModelType) {
86+
let filterItem = syncable.filterItem(for: item)
87+
if let index = selectedItems.firstIndex(of: filterItem) {
88+
selectedItems.remove(at: index)
89+
} else {
90+
selectedItems.append(filterItem)
8991
}
90-
return initialSelection(item)
92+
onSelection(selectedItems)
9193
}
9294

9395
func optionRow(text: String, isSelected: Bool, onSelection: @escaping () -> Void) -> some View {

WooCommerce/Classes/Bookings/BookingFilters/TeamMemberListSyncable.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Yosemite
55
struct TeamMemberListSyncable: ListSyncable {
66
typealias StorageType = StorageBookingResource
77
typealias ModelType = BookingResource
8+
typealias ListFilterType = BookingResource
89

910
let siteID: Int64
1011

@@ -42,6 +43,10 @@ struct TeamMemberListSyncable: ListSyncable {
4243
func displayName(for item: BookingResource) -> String {
4344
item.name
4445
}
46+
47+
func filterItem(for item: BookingResource) -> BookingResource {
48+
item
49+
}
4550
}
4651

4752
private extension TeamMemberListSyncable {

0 commit comments

Comments
 (0)