Skip to content

Commit 55929e6

Browse files
authored
Merge pull request #6131 from woocommerce/issue/6031-display_selected_variation_attributes
Order Creation: Display selected variation attributes in product list
2 parents 4050960 + 8e90601 commit 55929e6

File tree

11 files changed

+199
-66
lines changed

11 files changed

+199
-66
lines changed

RELEASE-NOTES.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
8.6
44
-----
5+
- [*] Orders: In the experimental Order Creation feature, product variations added to a new order now show a list of their attributes. [https://github.com/woocommerce/woocommerce-ios/pull/6131]
56
- [*] Enlarged the tap area for the action button on the notice view. [https://github.com/woocommerce/woocommerce-ios/pull/6146]
67

7-
88
8.5
99
-----
1010
- [*] In-Person Payments: Inform the user when a card reader battery is so low that it needs to be charged before the reader can be connected. [https://github.com/woocommerce/woocommerce-ios/pull/5998]
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import Foundation
2+
import Yosemite
3+
4+
/// Helper to format product variation details, such as variation name or attributes.
5+
///
6+
struct ProductVariationFormatter {
7+
8+
/// Generates a name for the product variation, given a list of the parent product attributes, e.g. "Blue - Any Size"
9+
/// - Parameters:
10+
/// - variation: The product variation whose name is being generated
11+
/// - allAttributes: A list of attributes from the parent `Product`
12+
///
13+
func generateName(for variation: ProductVariation, from allAttributes: [ProductAttribute]) -> String {
14+
let variationAttributes = generateAttributes(for: variation, from: allAttributes)
15+
return variationAttributes.map { $0.nameOrValue }.joined(separator: " - ")
16+
}
17+
18+
/// Generates the variation attributes, given a list of the parent product attributes.
19+
/// - Parameters:
20+
/// - variation: The product variation whose attributes are being generated
21+
/// - allAttributes: A list of attributes from the parent `Product`
22+
///
23+
func generateAttributes(for variation: ProductVariation, from allAttributes: [ProductAttribute]) -> [VariationAttributeViewModel] {
24+
return allAttributes
25+
.sorted(by: { (lhs, rhs) -> Bool in
26+
lhs.position < rhs.position
27+
})
28+
.map { attribute -> VariationAttributeViewModel in
29+
guard let variationAttribute = variation.attributes.first(where: { $0.id == attribute.attributeID && $0.name == attribute.name }) else {
30+
return VariationAttributeViewModel(name: attribute.name)
31+
}
32+
return VariationAttributeViewModel(productVariationAttribute: variationAttribute)
33+
}
34+
}
35+
}

WooCommerce/Classes/ViewRelated/Orders/Order Creation/NewOrderViewModel.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,11 +183,13 @@ final class NewOrderViewModel: ObservableObject {
183183
}
184184

185185
if item.variationID != 0, let variation = allProductVariations.first(where: { $0.productVariationID == item.variationID }) {
186+
let attributes = ProductVariationFormatter().generateAttributes(for: variation, from: product.attributes)
186187
return ProductRowViewModel(id: item.id,
187188
productVariation: variation,
188189
name: product.name,
189190
quantity: item.quantity,
190-
canChangeQuantity: canChangeQuantity)
191+
canChangeQuantity: canChangeQuantity,
192+
displayMode: .attributes(attributes))
191193
} else {
192194
return ProductRowViewModel(id: item.id, product: product, quantity: item.quantity, canChangeQuantity: canChangeQuantity)
193195
}

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

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,12 @@ final class AddProductVariationToOrderViewModel: ObservableObject {
4444
/// View models for each product variation row
4545
///
4646
var productVariationRows: [ProductRowViewModel] {
47-
productVariations.map { .init(productVariation: $0, name: createVariationName(for: $0), canChangeQuantity: false) }
47+
return productVariations.map {
48+
.init(productVariation: $0,
49+
name: ProductVariationFormatter().generateName(for: $0, from: productAttributes),
50+
canChangeQuantity: false,
51+
displayMode: .stock)
52+
}
4853
}
4954

5055
/// Closure to be invoked when a product variation is selected
@@ -124,18 +129,6 @@ final class AddProductVariationToOrderViewModel: ObservableObject {
124129
}
125130
onVariationSelected?(selectedVariation)
126131
}
127-
128-
/// Creates a name for a provided variation using all available product attributes, e.g. "Blue - Any Size"
129-
///
130-
func createVariationName(for variation: ProductVariation) -> String {
131-
let variationAttributes = productAttributes.map { attribute -> VariationAttributeViewModel in
132-
guard let variationAttribute = variation.attributes.first(where: { $0.id == attribute.attributeID && $0.name == attribute.name }) else {
133-
return VariationAttributeViewModel(name: attribute.name)
134-
}
135-
return VariationAttributeViewModel(productVariationAttribute: variationAttribute)
136-
}
137-
return variationAttributes.map { $0.nameOrValue }.joined(separator: " - ")
138-
}
139132
}
140133

141134
// MARK: - SyncingCoordinatorDelegate & Sync Methods

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

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,19 @@ final class ProductRowViewModel: ObservableObject, Identifiable {
4949
///
5050
private let manageStock: Bool
5151

52-
/// Label showing product details: stock status, price, and variations (if any).
52+
/// Display mode for a product variation.
53+
/// Determines which details to display in the product details label.
54+
///
55+
private let variationDisplayMode: VariationDisplayMode?
56+
57+
/// Label showing product details. Can include stock status or attributes, price, and variations (if any).
5358
///
5459
var productDetailsLabel: String {
55-
let stockLabel = createStockText()
60+
let stockOrAttributesLabel = createStockOrAttributesText()
5661
let priceLabel = createPriceText()
5762
let variationsLabel = createVariationsText()
5863

59-
return [stockLabel, priceLabel, variationsLabel]
64+
return [stockOrAttributesLabel, priceLabel, variationsLabel]
6065
.compactMap({ $0 })
6166
.joined(separator: "")
6267
}
@@ -100,6 +105,7 @@ final class ProductRowViewModel: ObservableObject, Identifiable {
100105
canChangeQuantity: Bool,
101106
imageURL: URL?,
102107
numberOfVariations: Int = 0,
108+
variationDisplayMode: VariationDisplayMode? = nil,
103109
currencyFormatter: CurrencyFormatter = CurrencyFormatter(currencySettings: ServiceLocator.currencySettings)) {
104110
self.id = id ?? productOrVariationID.description
105111
self.productOrVariationID = productOrVariationID
@@ -114,6 +120,7 @@ final class ProductRowViewModel: ObservableObject, Identifiable {
114120
self.imageURL = imageURL
115121
self.currencyFormatter = currencyFormatter
116122
self.numberOfVariations = numberOfVariations
123+
self.variationDisplayMode = variationDisplayMode
117124
}
118125

119126
/// Initialize `ProductRowViewModel` with a `Product`
@@ -153,6 +160,7 @@ final class ProductRowViewModel: ObservableObject, Identifiable {
153160
name: String,
154161
quantity: Decimal = 1,
155162
canChangeQuantity: Bool,
163+
displayMode: VariationDisplayMode,
156164
currencyFormatter: CurrencyFormatter = CurrencyFormatter(currencySettings: ServiceLocator.currencySettings)) {
157165
let imageURL: URL?
158166
if let encodedImageURLString = productVariation.image?.src.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) {
@@ -172,9 +180,32 @@ final class ProductRowViewModel: ObservableObject, Identifiable {
172180
quantity: quantity,
173181
canChangeQuantity: canChangeQuantity,
174182
imageURL: imageURL,
183+
variationDisplayMode: displayMode,
175184
currencyFormatter: currencyFormatter)
176185
}
177186

187+
/// Determines which product variation details to display.
188+
///
189+
enum VariationDisplayMode {
190+
/// Displays the variation's stock status
191+
case stock
192+
193+
/// Displays the provided list of variation attributes
194+
case attributes([VariationAttributeViewModel])
195+
}
196+
197+
/// Creates the stock or variation attributes text.
198+
/// Returns stock text for non-variations; uses variation display mode to determine the text for variations.
199+
///
200+
private func createStockOrAttributesText() -> String {
201+
switch variationDisplayMode {
202+
case .attributes(let attributes):
203+
return createAttributesText(from: attributes)
204+
default:
205+
return createStockText()
206+
}
207+
}
208+
178209
/// Create the stock text based on a product's stock status/quantity.
179210
///
180211
private func createStockText() -> String {
@@ -191,6 +222,12 @@ final class ProductRowViewModel: ObservableObject, Identifiable {
191222
}
192223
}
193224

225+
/// Create the attributes text based on the provided product variation attributes.
226+
///
227+
private func createAttributesText(from attributes: [VariationAttributeViewModel]) -> String {
228+
return attributes.map { $0.nameOrValue }.joined(separator: ", ")
229+
}
230+
194231
/// Create the price text based on a product's price and quantity.
195232
///
196233
private func createPriceText() -> String? {

WooCommerce/Classes/ViewRelated/Products/Edit Product/EditableProductVariationModel.swift

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@ final class EditableProductVariationModel {
66

77
let allAttributes: [ProductAttribute]
88

9-
private lazy var variationAttributes: [VariationAttributeViewModel] = {
10-
return generateVariationAttributes(productVariationAttributes: productVariation.attributes, allAttributes: allAttributes)
11-
}()
12-
139
init(productVariation: ProductVariation, allAttributes: [ProductAttribute], parentProductSKU: String?) {
1410
self.allAttributes = allAttributes
1511

@@ -21,23 +17,6 @@ final class EditableProductVariationModel {
2117
}
2218
}
2319

24-
private extension EditableProductVariationModel {
25-
26-
func generateVariationAttributes(productVariationAttributes: [ProductVariationAttribute],
27-
allAttributes: [ProductAttribute]) -> [VariationAttributeViewModel] {
28-
return allAttributes
29-
.sorted(by: { (lhs, rhs) -> Bool in
30-
lhs.position < rhs.position
31-
})
32-
.map { attribute -> VariationAttributeViewModel in
33-
guard let variationAttribute = productVariation.attributes.first(where: { $0.id == attribute.attributeID && $0.name == attribute.name }) else {
34-
return VariationAttributeViewModel(name: attribute.name)
35-
}
36-
return VariationAttributeViewModel(productVariationAttribute: variationAttribute)
37-
}
38-
}
39-
}
40-
4120
extension EditableProductVariationModel: ProductFormDataModel, TaxClassRequestable {
4221
var siteID: Int64 {
4322
productVariation.siteID
@@ -48,7 +27,7 @@ extension EditableProductVariationModel: ProductFormDataModel, TaxClassRequestab
4827
}
4928

5029
var name: String {
51-
variationAttributes.map { $0.nameOrValue }.joined(separator: " - ")
30+
ProductVariationFormatter().generateName(for: productVariation, from: allAttributes)
5231
}
5332

5433
var description: String? {

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1166,6 +1166,7 @@
11661166
CC254F3426C4113B005F3C82 /* ShippingLabelPackageSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC254F3326C4113B005F3C82 /* ShippingLabelPackageSelection.swift */; };
11671167
CC254F3626C437AB005F3C82 /* ShippingLabelCustomPackageForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC254F3526C437AB005F3C82 /* ShippingLabelCustomPackageForm.swift */; };
11681168
CC254F3826C43B52005F3C82 /* ShippingLabelCustomPackageFormViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC254F3726C43B52005F3C82 /* ShippingLabelCustomPackageFormViewModel.swift */; };
1169+
CC2E72F727B6BFB800A62872 /* ProductVariationFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC2E72F627B6BFB800A62872 /* ProductVariationFormatterTests.swift */; };
11691170
CC440E1E2770C6AF0074C264 /* ProductInOrderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC440E1D2770C6AF0074C264 /* ProductInOrderViewModel.swift */; };
11701171
CC4A4E962655273D00B75DCD /* ShippingLabelPaymentMethods.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC4A4E952655273D00B75DCD /* ShippingLabelPaymentMethods.swift */; };
11711172
CC4A4ED82655478D00B75DCD /* ShippingLabelPaymentMethodsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC4A4ED72655478D00B75DCD /* ShippingLabelPaymentMethodsViewModel.swift */; };
@@ -1199,6 +1200,7 @@
11991200
CCDC49ED24000533003166BA /* TestCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCDC49EC24000533003166BA /* TestCredentials.swift */; };
12001201
CCE4CD172667EBB100E09FD4 /* ShippingLabelPaymentMethodsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCE4CD162667EBB100E09FD4 /* ShippingLabelPaymentMethodsViewModelTests.swift */; };
12011202
CCE4CD282669324300E09FD4 /* ShippingLabelPaymentMethodsTopBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCE4CD272669324300E09FD4 /* ShippingLabelPaymentMethodsTopBanner.swift */; };
1203+
CCEC256A27B581E800EF9FA3 /* ProductVariationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCEC256927B581E800EF9FA3 /* ProductVariationFormatter.swift */; };
12021204
CCF87BBE279047BC00461C43 /* InfiniteScrollList.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCF87BBD279047BC00461C43 /* InfiniteScrollList.swift */; };
12031205
CCF87BC02790582500461C43 /* AddProductVariationToOrder.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCF87BBF2790582400461C43 /* AddProductVariationToOrder.swift */; };
12041206
CCFC00B523E9BD1500157A78 /* ScreenshotCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCFC00B423E9BD1500157A78 /* ScreenshotCredentials.swift */; };
@@ -2789,6 +2791,7 @@
27892791
CC254F3326C4113B005F3C82 /* ShippingLabelPackageSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelPackageSelection.swift; sourceTree = "<group>"; };
27902792
CC254F3526C437AB005F3C82 /* ShippingLabelCustomPackageForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelCustomPackageForm.swift; sourceTree = "<group>"; };
27912793
CC254F3726C43B52005F3C82 /* ShippingLabelCustomPackageFormViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelCustomPackageFormViewModel.swift; sourceTree = "<group>"; };
2794+
CC2E72F627B6BFB800A62872 /* ProductVariationFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductVariationFormatterTests.swift; sourceTree = "<group>"; };
27922795
CC440E1D2770C6AF0074C264 /* ProductInOrderViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductInOrderViewModel.swift; sourceTree = "<group>"; };
27932796
CC4A4E952655273D00B75DCD /* ShippingLabelPaymentMethods.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelPaymentMethods.swift; sourceTree = "<group>"; };
27942797
CC4A4ED72655478D00B75DCD /* ShippingLabelPaymentMethodsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelPaymentMethodsViewModel.swift; sourceTree = "<group>"; };
@@ -2824,6 +2827,7 @@
28242827
CCDC49F224006130003166BA /* UITests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = UITests.xctestplan; path = WooCommerceUITests/UITests.xctestplan; sourceTree = SOURCE_ROOT; };
28252828
CCE4CD162667EBB100E09FD4 /* ShippingLabelPaymentMethodsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelPaymentMethodsViewModelTests.swift; sourceTree = "<group>"; };
28262829
CCE4CD272669324300E09FD4 /* ShippingLabelPaymentMethodsTopBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelPaymentMethodsTopBanner.swift; sourceTree = "<group>"; };
2830+
CCEC256927B581E800EF9FA3 /* ProductVariationFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductVariationFormatter.swift; sourceTree = "<group>"; };
28272831
CCF87BBD279047BC00461C43 /* InfiniteScrollList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfiniteScrollList.swift; sourceTree = "<group>"; };
28282832
CCF87BBF2790582400461C43 /* AddProductVariationToOrder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProductVariationToOrder.swift; sourceTree = "<group>"; };
28292833
CCFC00B423E9BD1500157A78 /* ScreenshotCredentials.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScreenshotCredentials.swift; sourceTree = "<group>"; };
@@ -5181,6 +5185,7 @@
51815185
D41C9F2F26D9A41F00993558 /* WhatsNew */,
51825186
D8025469265517F9001B2CC1 /* CardPresentPayments */,
51835187
5791FB4124EC834300117FD6 /* MainTabViewModelTests.swift */,
5188+
CC2E72F627B6BFB800A62872 /* ProductVariationFormatterTests.swift */,
51845189
);
51855190
path = ViewModels;
51865191
sourceTree = "<group>";
@@ -6821,6 +6826,7 @@
68216826
D8815AE526383FC200EDAD62 /* CardPresentPayments */,
68226827
D817585C22BB5E6900289CFE /* Order Details */,
68236828
D843D5D82248EE90001BFA55 /* ManualTrackingViewModel.swift */,
6829+
CCEC256927B581E800EF9FA3 /* ProductVariationFormatter.swift */,
68246830
CECC758B23D2227000486676 /* ProductDetailsCellViewModel.swift */,
68256831
D843D5D622485B19001BFA55 /* ShippingProvidersViewModel.swift */,
68266832
D8736B5922F07D7100A14A29 /* MainTabViewModel.swift */,
@@ -8612,6 +8618,7 @@
86128618
45DB70662614CE3F0064A6CF /* Decimal+Helpers.swift in Sources */,
86138619
02E8B17C23E2C78A00A43403 /* ProductImageStatus.swift in Sources */,
86148620
0259D5FF2581F3FA003B1CD6 /* ShippingLabelPaperSizeOptionsViewController.swift in Sources */,
8621+
CCEC256A27B581E800EF9FA3 /* ProductVariationFormatter.swift in Sources */,
86158622
02EA6BFA2435E92600FFF90A /* KingfisherImageDownloader+ImageDownloadable.swift in Sources */,
86168623
7E7C5F8F2719BA7300315B61 /* ProductCategoryCellViewModel.swift in Sources */,
86178624
DEC2962326BD4E6E005A056B /* ShippingLabelCustomsFormInput.swift in Sources */,
@@ -9301,6 +9308,7 @@
93019308
9379E1A6225537D0006A6BE4 /* TestingAppDelegate.swift in Sources */,
93029309
453904F323BB88B5007C4956 /* ProductTaxStatusListSelectorCommandTests.swift in Sources */,
93039310
02BAB02124D0235F00F8B06E /* ProductPriceSettingsViewModel+ProductVariationTests.swift in Sources */,
9311+
CC2E72F727B6BFB800A62872 /* ProductVariationFormatterTests.swift in Sources */,
93049312
D802547826551DB8001B2CC1 /* CardPresentModalDisplayMessageTests.swift in Sources */,
93059313
02503C632538301400FD235D /* ProductVariationFormActionsFactory+ReadonlyVariationTests.swift in Sources */,
93069314
DE67D46926BAA82600EFE8DB /* Publisher+WithLatestFromTests.swift in Sources */,

0 commit comments

Comments
 (0)