Skip to content

Commit 76a4ce2

Browse files
committed
Make list selector generic
1 parent 36ce537 commit 76a4ce2

File tree

6 files changed

+254
-178
lines changed

6 files changed

+254
-178
lines changed

WooCommerce/Classes/Bookings/BookingFilters/BookingTeamMemberSelectorView.swift

Lines changed: 0 additions & 143 deletions
This file was deleted.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import Foundation
2+
import Yosemite
3+
4+
/// Protocol for configuring a booking list selector with different entity types.
5+
/// Provides all necessary configuration for fetching, displaying, and syncing list items.
6+
protocol ListSyncable {
7+
associatedtype StorageType: ResultsControllerMutableType
8+
associatedtype ModelType: Equatable & Hashable where ModelType == StorageType.ReadOnlyType
9+
10+
var siteID: Int64 { get }
11+
var title: String { get }
12+
var emptyStateMessage: String { get }
13+
14+
// MARK: - ResultsController Configuration
15+
16+
/// Creates the predicate for filtering storage objects
17+
func createPredicate() -> NSPredicate
18+
19+
/// Creates sort descriptors for ordering results
20+
func createSortDescriptors() -> [NSSortDescriptor]
21+
22+
// MARK: - Sync Configuration
23+
24+
/// Creates the action to sync items from remote
25+
func createSyncAction(pageNumber: Int, pageSize: Int, completion: @escaping (Result<Bool, Error>) -> Void) -> Action
26+
27+
// MARK: - Model Conversion
28+
29+
/// Converts storage object to model object
30+
func convert(_ storage: StorageType) -> ModelType
31+
32+
// MARK: - Display Configuration
33+
34+
/// Returns the display name for an item
35+
func displayName(for item: ModelType) -> String
36+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import SwiftUI
2+
import Yosemite
3+
4+
struct SyncableListSelectorView<Syncable: ListSyncable>: View {
5+
@ObservedObject private var viewModel: SyncableListSelectorViewModel<Syncable>
6+
@State var selectedItem: Syncable.ModelType?
7+
8+
private let syncable: Syncable
9+
private let onSelection: (Syncable.ModelType?) -> Void
10+
11+
private let viewPadding: CGFloat = 16
12+
13+
init(viewModel: SyncableListSelectorViewModel<Syncable>,
14+
syncable: Syncable,
15+
selectedItem: Syncable.ModelType?,
16+
onSelection: @escaping (Syncable.ModelType?) -> Void) {
17+
self.viewModel = viewModel
18+
self.syncable = syncable
19+
self.selectedItem = selectedItem
20+
self.onSelection = onSelection
21+
}
22+
23+
var body: some View {
24+
VStack {
25+
switch viewModel.syncState {
26+
case .empty:
27+
emptyStateView
28+
case .syncingFirstPage:
29+
loadingView
30+
case .results:
31+
itemList(with: viewModel.items,
32+
onNextPage: { viewModel.onLoadNextPageAction() })
33+
}
34+
}
35+
.task {
36+
viewModel.loadResources()
37+
}
38+
.navigationTitle(syncable.title)
39+
.navigationBarTitleDisplayMode(.inline)
40+
.onChange(of: selectedItem) { _, newValue in
41+
onSelection(newValue)
42+
}
43+
}
44+
}
45+
46+
private extension SyncableListSelectorView {
47+
var loadingView: some View {
48+
VStack {
49+
Spacer()
50+
ProgressView().progressViewStyle(.circular)
51+
Spacer()
52+
}
53+
.frame(maxWidth: .infinity)
54+
.background(Color(.systemBackground))
55+
}
56+
57+
func itemList(with items: [Syncable.ModelType],
58+
onNextPage: @escaping () -> Void) -> some View {
59+
List {
60+
optionRow(
61+
text: NSLocalizedString(
62+
"bookingListSelectorView.any",
63+
value: "Any",
64+
comment: "Option to select no filter on the booking list selector view"
65+
),
66+
isSelected: selectedItem == nil,
67+
onSelection: { selectedItem = nil }
68+
)
69+
70+
ForEach(items, id: \.self) { item in
71+
optionRow(text: syncable.displayName(for: item),
72+
isSelected: item == selectedItem,
73+
onSelection: { selectedItem = item })
74+
}
75+
76+
InfiniteScrollIndicator(showContent: viewModel.shouldShowBottomActivityIndicator)
77+
.padding(.top, viewPadding)
78+
.onAppear {
79+
onNextPage()
80+
}
81+
}
82+
.listStyle(.plain)
83+
.background(Color(.listBackground))
84+
}
85+
86+
func optionRow(text: String, isSelected: Bool, onSelection: @escaping () -> Void) -> some View {
87+
HStack {
88+
Text(text)
89+
Spacer()
90+
Image(systemName: "checkmark")
91+
.font(.body.weight(.medium))
92+
.foregroundStyle(Color.accentColor)
93+
.renderedIf(isSelected)
94+
}
95+
.tappable {
96+
onSelection()
97+
}
98+
.listRowBackground(Color(.listForeground(modal: false)))
99+
}
100+
101+
var emptyStateView: some View {
102+
VStack {
103+
Spacer()
104+
Text(syncable.emptyStateMessage)
105+
.secondaryBodyStyle()
106+
Spacer()
107+
}
108+
.multilineTextAlignment(.center)
109+
.padding(.horizontal, viewPadding)
110+
.background(Color(.systemBackground))
111+
}
112+
}

0 commit comments

Comments
 (0)