Skip to content

Commit 7aa229b

Browse files
authored
Merge pull request #5870 from woocommerce/issue/5847-reusable-addproducttoorder
Order Creation: Add protocol for AddProductToOrder view models to make it reusable
2 parents 2576e3f + 675552e commit 7aa229b

File tree

5 files changed

+141
-110
lines changed

5 files changed

+141
-110
lines changed

WooCommerce/Classes/ViewRelated/Orders/Order Creation/ProductsSection/AddProductToOrder.swift

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import SwiftUI
22

3-
/// View showing a list of products to add to an order.
3+
/// View showing a list of products or product variations to add to an order.
44
///
5-
struct AddProductToOrder: View {
5+
struct AddProductToOrder<ViewModel: AddProductToOrderViewModelProtocol>: View {
66
/// Defines whether the view is presented.
77
///
88
@Binding var isPresented: Bool
99

1010
/// View model to drive the view.
1111
///
12-
@ObservedObject var viewModel: AddProductToOrderViewModel
12+
@ObservedObject var viewModel: ViewModel
1313

1414
var body: some View {
1515
NavigationView {
@@ -20,7 +20,7 @@ struct AddProductToOrder: View {
2020
ForEach(viewModel.productRows) { rowViewModel in
2121
ProductRow(viewModel: rowViewModel)
2222
.onTapGesture {
23-
viewModel.selectProduct(rowViewModel.productOrVariationID)
23+
viewModel.selectProductOrVariation(rowViewModel.productOrVariationID)
2424
isPresented.toggle()
2525
}
2626
}
@@ -88,13 +88,11 @@ private struct InfiniteScrollIndicator: View {
8888
}
8989
}
9090

91-
private extension AddProductToOrder {
92-
enum Localization {
93-
static let title = NSLocalizedString("Add Product", comment: "Title for the screen to add a product to an order")
94-
static let close = NSLocalizedString("Close", comment: "Text for the close button in the Add Product screen")
95-
static let emptyStateMessage = NSLocalizedString("No products found",
96-
comment: "Message displayed if there are no products to display in the Add Product screen")
97-
}
91+
private enum Localization {
92+
static let title = NSLocalizedString("Add Product", comment: "Title for the screen to add a product to an order")
93+
static let close = NSLocalizedString("Close", comment: "Text for the close button in the Add Product screen")
94+
static let emptyStateMessage = NSLocalizedString("No products found",
95+
comment: "Message displayed if there are no products to display in the Add Product screen")
9896
}
9997

10098
struct AddProduct_Previews: PreviewProvider {

WooCommerce/Classes/ViewRelated/Orders/Order Creation/ProductsSection/AddProductToOrderViewModel.swift

Lines changed: 4 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import Yosemite
22
import protocol Storage.StorageManagerType
33

4-
/// View model for `AddProductToOrder`.
4+
/// View model for `AddProductToOrder` with a list of products.
55
///
6-
final class AddProductToOrderViewModel: ObservableObject {
6+
final class AddProductToOrderViewModel: AddProductToOrderViewModelProtocol {
77
private let siteID: Int64
88

99
/// Storage to fetch product list
@@ -48,7 +48,7 @@ final class AddProductToOrderViewModel: ObservableObject {
4848

4949
/// Current sync status; used to determine what list view to display.
5050
///
51-
@Published private(set) var syncStatus: SyncStatus?
51+
@Published private(set) var syncStatus: AddProductToOrderSyncStatus?
5252

5353
/// SyncCoordinator: Keeps tracks of which pages have been refreshed, and encapsulates the "What should we sync now" logic.
5454
///
@@ -58,14 +58,6 @@ final class AddProductToOrderViewModel: ObservableObject {
5858
///
5959
@Published private(set) var shouldShowScrollIndicator = false
6060

61-
/// View models of the ghost rows used during the loading process.
62-
///
63-
var ghostRows: [ProductRowViewModel] {
64-
return Array(0..<6).map { index in
65-
ProductRowViewModel(product: sampleGhostProduct(id: index), canChangeQuantity: false)
66-
}
67-
}
68-
6961
/// Products Results Controller.
7062
///
7163
private lazy var productsResultsController: ResultsController<StorageProduct> = {
@@ -90,7 +82,7 @@ final class AddProductToOrderViewModel: ObservableObject {
9082

9183
/// Select a product to add to the order
9284
///
93-
func selectProduct(_ productID: Int64) {
85+
func selectProductOrVariation(_ productID: Int64) {
9486
guard let selectedProduct = products.first(where: { $0.productID == productID }) else {
9587
return
9688
}
@@ -186,82 +178,3 @@ private extension AddProductToOrderViewModel {
186178
syncingCoordinator.delegate = self
187179
}
188180
}
189-
190-
// MARK: - Utils
191-
extension AddProductToOrderViewModel {
192-
/// Represents possible statuses for syncing products
193-
///
194-
enum SyncStatus {
195-
case firstPageSync
196-
case results
197-
case empty
198-
}
199-
200-
/// Used for ghost list view while syncing
201-
///
202-
private func sampleGhostProduct(id: Int64) -> Product {
203-
Product(siteID: 1,
204-
productID: id,
205-
name: "Love Ficus",
206-
slug: "",
207-
permalink: "",
208-
date: Date(),
209-
dateCreated: Date(),
210-
dateModified: nil,
211-
dateOnSaleStart: nil,
212-
dateOnSaleEnd: nil,
213-
productTypeKey: ProductType.simple.rawValue,
214-
statusKey: ProductStatus.draft.rawValue,
215-
featured: false,
216-
catalogVisibilityKey: ProductCatalogVisibility.hidden.rawValue,
217-
fullDescription: nil,
218-
shortDescription: nil,
219-
sku: "123456",
220-
price: "20",
221-
regularPrice: nil,
222-
salePrice: nil,
223-
onSale: false,
224-
purchasable: true,
225-
totalSales: 0,
226-
virtual: false,
227-
downloadable: false,
228-
downloads: [],
229-
downloadLimit: -1,
230-
downloadExpiry: -1,
231-
buttonText: "",
232-
externalURL: nil,
233-
taxStatusKey: ProductTaxStatus.taxable.rawValue,
234-
taxClass: nil,
235-
manageStock: false,
236-
stockQuantity: 7,
237-
stockStatusKey: ProductStockStatus.inStock.rawValue,
238-
backordersKey: ProductBackordersSetting.notAllowed.rawValue,
239-
backordersAllowed: false,
240-
backordered: false,
241-
soldIndividually: true,
242-
weight: nil,
243-
dimensions: ProductDimensions(length: "1", width: "1", height: "1"),
244-
shippingRequired: false,
245-
shippingTaxable: false,
246-
shippingClass: nil,
247-
shippingClassID: 0,
248-
productShippingClass: nil,
249-
reviewsAllowed: false,
250-
averageRating: "5",
251-
ratingCount: 0,
252-
relatedIDs: [],
253-
upsellIDs: [],
254-
crossSellIDs: [],
255-
parentID: 0,
256-
purchaseNote: nil,
257-
categories: [],
258-
tags: [],
259-
images: [],
260-
attributes: [],
261-
defaultAttributes: [],
262-
variations: [],
263-
groupedProducts: [],
264-
menuOrder: 0,
265-
addOns: [])
266-
}
267-
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import Yosemite
2+
3+
/// Represents possible statuses for syncing a list of products or product variations
4+
///
5+
enum AddProductToOrderSyncStatus {
6+
case firstPageSync
7+
case results
8+
case empty
9+
}
10+
11+
/// Protocol for view models for `AddProductToOrder`, to add a product or product variation to an order.
12+
///
13+
protocol AddProductToOrderViewModelProtocol: ObservableObject {
14+
/// View models for each product row
15+
///
16+
var productRows: [ProductRowViewModel] { get }
17+
18+
/// Current sync status; used to determine what list view to display.
19+
///
20+
var syncStatus: AddProductToOrderSyncStatus? { get }
21+
22+
/// Tracks if the infinite scroll indicator should be displayed
23+
///
24+
var shouldShowScrollIndicator: Bool { get }
25+
26+
/// Select a product or product variation to add to the order
27+
///
28+
func selectProductOrVariation(_ productID: Int64)
29+
30+
/// Sync first page of products from remote if needed.
31+
///
32+
func syncFirstPage()
33+
34+
/// Sync next page of products from remote.
35+
///
36+
func syncNextPage()
37+
}
38+
39+
// MARK: - Utils
40+
extension AddProductToOrderViewModelProtocol {
41+
/// View models of the ghost rows used during the loading process.
42+
///
43+
var ghostRows: [ProductRowViewModel] {
44+
return Array(0..<6).map { index in
45+
ProductRowViewModel(product: sampleGhostProduct(id: index), canChangeQuantity: false)
46+
}
47+
}
48+
49+
/// Used for ghost list view while syncing
50+
///
51+
private func sampleGhostProduct(id: Int64) -> Product {
52+
Product(siteID: 1,
53+
productID: id,
54+
name: "Love Ficus",
55+
slug: "",
56+
permalink: "",
57+
date: Date(),
58+
dateCreated: Date(),
59+
dateModified: nil,
60+
dateOnSaleStart: nil,
61+
dateOnSaleEnd: nil,
62+
productTypeKey: ProductType.simple.rawValue,
63+
statusKey: ProductStatus.draft.rawValue,
64+
featured: false,
65+
catalogVisibilityKey: ProductCatalogVisibility.hidden.rawValue,
66+
fullDescription: nil,
67+
shortDescription: nil,
68+
sku: "123456",
69+
price: "20",
70+
regularPrice: nil,
71+
salePrice: nil,
72+
onSale: false,
73+
purchasable: true,
74+
totalSales: 0,
75+
virtual: false,
76+
downloadable: false,
77+
downloads: [],
78+
downloadLimit: -1,
79+
downloadExpiry: -1,
80+
buttonText: "",
81+
externalURL: nil,
82+
taxStatusKey: ProductTaxStatus.taxable.rawValue,
83+
taxClass: nil,
84+
manageStock: false,
85+
stockQuantity: 7,
86+
stockStatusKey: ProductStockStatus.inStock.rawValue,
87+
backordersKey: ProductBackordersSetting.notAllowed.rawValue,
88+
backordersAllowed: false,
89+
backordered: false,
90+
soldIndividually: true,
91+
weight: nil,
92+
dimensions: ProductDimensions(length: "1", width: "1", height: "1"),
93+
shippingRequired: false,
94+
shippingTaxable: false,
95+
shippingClass: nil,
96+
shippingClassID: 0,
97+
productShippingClass: nil,
98+
reviewsAllowed: false,
99+
averageRating: "5",
100+
ratingCount: 0,
101+
relatedIDs: [],
102+
upsellIDs: [],
103+
crossSellIDs: [],
104+
parentID: 0,
105+
purchaseNote: nil,
106+
categories: [],
107+
tags: [],
108+
images: [],
109+
attributes: [],
110+
defaultAttributes: [],
111+
variations: [],
112+
groupedProducts: [],
113+
menuOrder: 0,
114+
addOns: [])
115+
}
116+
}

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1139,6 +1139,7 @@
11391139
CC0324A3263AD9F40056C6B7 /* MockShippingLabelAccountSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0324A2263AD9F40056C6B7 /* MockShippingLabelAccountSettings.swift */; };
11401140
CC078531266E706300BA9AC1 /* ErrorTopBannerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC078530266E706300BA9AC1 /* ErrorTopBannerFactory.swift */; };
11411141
CC07860526736B6500BA9AC1 /* ErrorTopBannerFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC07860426736B6500BA9AC1 /* ErrorTopBannerFactoryTests.swift */; };
1142+
CC13C0C9278DE76A00C0B5B5 /* AddProductToOrderViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC13C0C8278DE76A00C0B5B5 /* AddProductToOrderViewModelProtocol.swift */; };
11421143
CC200BB127847DE300EC5884 /* OrderPaymentSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC200BB027847DE300EC5884 /* OrderPaymentSection.swift */; };
11431144
CC254F2D26C17AB5005F3C82 /* BottomButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC254F2C26C17AB5005F3C82 /* BottomButtonView.swift */; };
11441145
CC254F3026C2A53D005F3C82 /* ShippingLabelAddNewPackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC254F2F26C2A53D005F3C82 /* ShippingLabelAddNewPackage.swift */; };
@@ -2728,6 +2729,7 @@
27282729
CC0324A2263AD9F40056C6B7 /* MockShippingLabelAccountSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockShippingLabelAccountSettings.swift; sourceTree = "<group>"; };
27292730
CC078530266E706300BA9AC1 /* ErrorTopBannerFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorTopBannerFactory.swift; sourceTree = "<group>"; };
27302731
CC07860426736B6500BA9AC1 /* ErrorTopBannerFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorTopBannerFactoryTests.swift; sourceTree = "<group>"; };
2732+
CC13C0C8278DE76A00C0B5B5 /* AddProductToOrderViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProductToOrderViewModelProtocol.swift; sourceTree = "<group>"; };
27312733
CC200BB027847DE300EC5884 /* OrderPaymentSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderPaymentSection.swift; sourceTree = "<group>"; };
27322734
CC254F2C26C17AB5005F3C82 /* BottomButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomButtonView.swift; sourceTree = "<group>"; };
27332735
CC254F2F26C2A53D005F3C82 /* ShippingLabelAddNewPackage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelAddNewPackage.swift; sourceTree = "<group>"; };
@@ -6135,6 +6137,7 @@
61356137
children = (
61366138
CC53FB372755213900C4CA4F /* AddProductToOrder.swift */,
61376139
CC53FB3B2757EC7200C4CA4F /* AddProductToOrderViewModel.swift */,
6140+
CC13C0C8278DE76A00C0B5B5 /* AddProductToOrderViewModelProtocol.swift */,
61386141
CC53FB3427551A6E00C4CA4F /* ProductRow.swift */,
61396142
CC53FB39275697B000C4CA4F /* ProductRowViewModel.swift */,
61406143
CCC284102768C18500F6CC8B /* ProductInOrder.swift */,
@@ -8173,6 +8176,7 @@
81738176
029BFD4F24597D4B00FDDEEC /* UIButton+TitleAndImage.swift in Sources */,
81748177
B5A8F8AD20B88D9900D211DE /* LoginPrologueViewController.swift in Sources */,
81758178
B5D1AFC620BC7B7300DB0E8C /* StorePickerViewController.swift in Sources */,
8179+
CC13C0C9278DE76A00C0B5B5 /* AddProductToOrderViewModelProtocol.swift in Sources */,
81768180
02DD81FB242CAA400060E50B /* WordPressMediaLibraryPickerDataSource.swift in Sources */,
81778181
0240B3AC230A910C000A866C /* StoreStatsV4ChartAxisHelper.swift in Sources */,
81788182
31579028273EE2B1008CA3AF /* VersionHelpers.swift in Sources */,

WooCommerce/WooCommerceTests/ViewRelated/Orders/Order Creation/NewOrderViewModelTests.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ class NewOrderViewModelTests: XCTestCase {
136136
let viewModel = NewOrderViewModel(siteID: sampleSiteID, storageManager: storageManager)
137137

138138
// When
139-
viewModel.addProductViewModel.selectProduct(product.productID)
139+
viewModel.addProductViewModel.selectProductOrVariation(product.productID)
140140

141141
// Then
142142
let expectedOrderItem = product.toOrderItem(quantity: 1)
@@ -152,11 +152,11 @@ class NewOrderViewModelTests: XCTestCase {
152152
let viewModel = NewOrderViewModel(siteID: sampleSiteID, storageManager: storageManager)
153153

154154
// When
155-
viewModel.addProductViewModel.selectProduct(product.productID)
155+
viewModel.addProductViewModel.selectProductOrVariation(product.productID)
156156
viewModel.productRows[0].incrementQuantity()
157157

158158
// And when another product is added to the order (to confirm the first product's quantity change is retained)
159-
viewModel.addProductViewModel.selectProduct(product.productID)
159+
viewModel.addProductViewModel.selectProductOrVariation(product.productID)
160160

161161
// Then
162162
let expectedOrderItem = product.toOrderItem(quantity: 2)
@@ -170,7 +170,7 @@ class NewOrderViewModelTests: XCTestCase {
170170
let storageManager = MockStorageManager()
171171
storageManager.insertSampleProduct(readOnlyProduct: product)
172172
let viewModel = NewOrderViewModel(siteID: sampleSiteID, storageManager: storageManager)
173-
viewModel.addProductViewModel.selectProduct(product.productID)
173+
viewModel.addProductViewModel.selectProductOrVariation(product.productID)
174174

175175
// When
176176
let expectedOrderItem = viewModel.orderDetails.items[0]
@@ -189,8 +189,8 @@ class NewOrderViewModelTests: XCTestCase {
189189
let viewModel = NewOrderViewModel(siteID: sampleSiteID, storageManager: storageManager)
190190

191191
// Given products are added to order
192-
viewModel.addProductViewModel.selectProduct(product0.productID)
193-
viewModel.addProductViewModel.selectProduct(product1.productID)
192+
viewModel.addProductViewModel.selectProductOrVariation(product0.productID)
193+
viewModel.addProductViewModel.selectProductOrVariation(product1.productID)
194194

195195
// When
196196
let expectedRemainingItem = viewModel.orderDetails.items[1]
@@ -255,7 +255,7 @@ class NewOrderViewModelTests: XCTestCase {
255255
let viewModel = NewOrderViewModel(siteID: sampleSiteID, storageManager: storageManager)
256256

257257
// When & Then
258-
viewModel.addProductViewModel.selectProduct(product.productID)
258+
viewModel.addProductViewModel.selectProductOrVariation(product.productID)
259259
XCTAssertTrue(viewModel.shouldShowPaymentSection)
260260

261261
// When & Then
@@ -272,7 +272,7 @@ class NewOrderViewModelTests: XCTestCase {
272272
let viewModel = NewOrderViewModel(siteID: sampleSiteID, storageManager: storageManager, currencySettings: currencySettings)
273273

274274
// When & Then
275-
viewModel.addProductViewModel.selectProduct(product.productID)
275+
viewModel.addProductViewModel.selectProductOrVariation(product.productID)
276276
XCTAssertEqual(viewModel.paymentDataViewModel.itemsTotal, "£8.50")
277277
XCTAssertEqual(viewModel.paymentDataViewModel.orderTotal, "£8.50")
278278

0 commit comments

Comments
 (0)