Skip to content

Commit 4ca6cef

Browse files
authored
Merge pull request #4961 from woocommerce/issue/4599-new-package-details-items
Shipping Labels: Render package item for new package details screen (supporting multiple packages)
2 parents a55f7af + a0887f2 commit 4ca6cef

File tree

4 files changed

+269
-5
lines changed

4 files changed

+269
-5
lines changed

WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/Create Shipping Label Form/Package Details/Multi-package/ShippingLabelPackageItem.swift

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,73 @@ struct ShippingLabelPackageItem: View {
2323

2424
var body: some View {
2525
CollapsibleView(isCollapsible: isCollapsible, isCollapsed: $isCollapsed, safeAreaInsets: safeAreaInsets) {
26-
// TODO-4599 - Update view
27-
ShippingLabelPackageNumberRow(packageNumber: packageNumber, numberOfItems: 1)
26+
ShippingLabelPackageNumberRow(packageNumber: packageNumber, numberOfItems: viewModel.itemsRows.count)
2827
} content: {
29-
// TODO-4599 - Update view
30-
EmptyView()
28+
ListHeaderView(text: Localization.itemsToFulfillHeader, alignment: .left)
29+
.padding(.horizontal, insets: safeAreaInsets)
30+
31+
Divider()
32+
33+
ForEach(viewModel.itemsRows) { productItemRow in
34+
productItemRow
35+
.padding(.horizontal, insets: safeAreaInsets)
36+
.background(Color(.systemBackground))
37+
Divider()
38+
.padding(.horizontal, insets: safeAreaInsets)
39+
.padding(.leading, Constants.dividerPadding)
40+
}
41+
42+
ListHeaderView(text: Localization.packageDetailsHeader, alignment: .left)
43+
.padding(.horizontal, insets: safeAreaInsets)
44+
45+
VStack(spacing: 0) {
46+
Divider()
47+
48+
TitleAndValueRow(title: Localization.packageSelected, value: viewModel.selectedPackageName, selectable: true) {
49+
isShowingPackageSelection.toggle()
50+
}
51+
.padding(.horizontal, insets: safeAreaInsets)
52+
.sheet(isPresented: $isShowingPackageSelection, content: {
53+
// TODO-4599: Update package selection with new view model
54+
// ShippingLabelPackageSelection(viewModel: viewModel)
55+
})
56+
57+
Divider()
58+
59+
TitleAndTextFieldRow(title: Localization.totalPackageWeight,
60+
placeholder: "0",
61+
text: $viewModel.totalWeight,
62+
symbol: viewModel.weightUnit,
63+
keyboardType: .decimalPad)
64+
.padding(.horizontal, insets: safeAreaInsets)
65+
66+
Divider()
67+
}
68+
.background(Color(.systemBackground))
69+
70+
ListHeaderView(text: Localization.footer, alignment: .left)
71+
.padding(.horizontal, insets: safeAreaInsets)
3172
}
3273
}
3374
}
3475

76+
private extension ShippingLabelPackageItem {
77+
enum Localization {
78+
static let itemsToFulfillHeader = NSLocalizedString("ITEMS TO FULFILL", comment: "Header section items to fulfill in Shipping Label Package Detail")
79+
static let packageDetailsHeader = NSLocalizedString("PACKAGE DETAILS", comment: "Header section package details in Shipping Label Package Detail")
80+
static let packageSelected = NSLocalizedString("Package Selected",
81+
comment: "Title of the row for selecting a package in Shipping Label Package Detail screen")
82+
static let totalPackageWeight = NSLocalizedString("Total package weight",
83+
comment: "Title of the row for adding the package weight in Shipping Label Package Detail screen")
84+
static let footer = NSLocalizedString("Sum of products and package weight",
85+
comment: "Title of the footer in Shipping Label Package Detail screen")
86+
}
87+
88+
enum Constants {
89+
static let dividerPadding: CGFloat = 16
90+
}
91+
}
92+
3593
struct ShippingLabelPackageItem_Previews: PreviewProvider {
3694
static var previews: some View {
3795
let order = ShippingLabelPackageDetailsViewModel.sampleOrder()

WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/Create Shipping Label Form/Package Details/Multi-package/ShippingLabelPackateItemViewModel.swift

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,19 @@ final class ShippingLabelPackageItemViewModel: ObservableObject {
99
///
1010
let selectedPackageID: String
1111

12+
@Published var totalWeight: String = ""
13+
14+
/// The items rows observed by the main view `ShippingLabelPackageItem`
15+
///
16+
@Published private(set) var itemsRows: [ItemToFulfillRow] = []
17+
18+
/// The title of the selected package, if any.
19+
///
20+
var selectedPackageName: String {
21+
// TODO-4599: Update package name
22+
return Localization.selectPackagePlaceholder
23+
}
24+
1225
private let order: Order
1326
private let orderItems: [OrderItem]
1427
private let currency: String
@@ -20,7 +33,7 @@ final class ShippingLabelPackageItemViewModel: ObservableObject {
2033

2134
/// The weight unit used in the Store
2235
///
23-
private let weightUnit: String?
36+
let weightUnit: String?
2437

2538
init(order: Order,
2639
orderItems: [OrderItem],
@@ -38,5 +51,77 @@ final class ShippingLabelPackageItemViewModel: ObservableObject {
3851
self.weightUnit = weightUnit
3952
self.packagesResponse = packagesResponse
4053
self.selectedPackageID = selectedPackageID
54+
55+
configureItemRows(products: products, productVariations: productVariations)
56+
}
57+
58+
private func configureItemRows(products: [Product], productVariations: [ProductVariation]) {
59+
itemsRows = generateItemsRows(products: products, productVariations: productVariations)
60+
}
61+
}
62+
63+
// MARK: - Helper methods
64+
private extension ShippingLabelPackageItemViewModel {
65+
/// Generate the items rows, creating an element in the array for every item (eg. if there is an item with quantity 3,
66+
/// we will generate 3 different items), and we will remove virtual products.
67+
///
68+
func generateItemsRows(products: [Product], productVariations: [ProductVariation]) -> [ItemToFulfillRow] {
69+
var itemsToFulfill: [ItemToFulfillRow] = []
70+
for item in orderItems {
71+
let isVariation = item.variationID > 0
72+
var product: Product?
73+
var productVariation: ProductVariation?
74+
75+
if isVariation {
76+
productVariation = productVariations.first { $0.productVariationID == item.variationID }
77+
}
78+
else {
79+
product = products.first { $0.productID == item.productID }
80+
}
81+
if product?.virtual == false || productVariation?.virtual == false {
82+
var tempItemQuantity = Double(truncating: item.quantity as NSDecimalNumber)
83+
84+
for _ in 0..<item.quantity.intValue {
85+
let attributes = item.attributes.map { VariationAttributeViewModel(orderItemAttribute: $0) }
86+
var weight = Double(productVariation?.weight ?? product?.weight ?? "0") ?? 0
87+
if tempItemQuantity < 1 {
88+
weight *= tempItemQuantity
89+
} else {
90+
tempItemQuantity -= 1
91+
}
92+
let unit: String = weightUnit ?? ""
93+
let subtitle = Localization.subtitle(weight: weight.description,
94+
weightUnit: unit,
95+
attributes: attributes)
96+
itemsToFulfill.append(ItemToFulfillRow(title: item.name, subtitle: subtitle))
97+
}
98+
}
99+
}
100+
return itemsToFulfill
101+
}
102+
}
103+
104+
private extension ShippingLabelPackageItemViewModel {
105+
enum Localization {
106+
static let subtitleFormat =
107+
NSLocalizedString("%1$@", comment: "In Shipping Labels Package Details,"
108+
+ " the pattern used to show the weight of a product. For example, “1lbs”.")
109+
static let subtitleWithAttributesFormat =
110+
NSLocalizedString("%1$@・%2$@", comment: "In Shipping Labels Package Details if the product has attributes,"
111+
+ " the pattern used to show the attributes and weight. For example, “purple, has logo・1lbs”."
112+
+ " The %1$@ is the list of attributes (e.g. from variation)."
113+
+ " The %2$@ is the weight with the unit.")
114+
static func subtitle(weight: String?, weightUnit: String, attributes: [VariationAttributeViewModel]) -> String {
115+
let attributesText = attributes.map { $0.nameOrValue }.joined(separator: ", ")
116+
let formatter = WeightFormatter(weightUnit: weightUnit)
117+
let weight = formatter.formatWeight(weight: weight)
118+
if attributes.isEmpty {
119+
return String.localizedStringWithFormat(subtitleFormat, weight, weightUnit)
120+
} else {
121+
return String.localizedStringWithFormat(subtitleWithAttributesFormat, attributesText, weight)
122+
}
123+
}
124+
static let selectPackagePlaceholder = NSLocalizedString("Select a package",
125+
comment: "Placeholder for the selected package in the Shipping Labels Package Details screen")
41126
}
42127
}

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1283,6 +1283,7 @@
12831283
DE279BA826E9C8E3002BA963 /* ShippingLabelPackageItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE279BA726E9C8E3002BA963 /* ShippingLabelPackageItem.swift */; };
12841284
DE279BAA26E9C91D002BA963 /* ShippingLabelPackateItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE279BA926E9C91D002BA963 /* ShippingLabelPackateItemViewModel.swift */; };
12851285
DE279BAD26E9CBEA002BA963 /* ShippingLabelPackagesFormViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE279BAC26E9CBEA002BA963 /* ShippingLabelPackagesFormViewModelTests.swift */; };
1286+
DE279BAF26EA03EA002BA963 /* ShippingLabelPackageItemViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE279BAE26EA03EA002BA963 /* ShippingLabelPackageItemViewModelTests.swift */; };
12861287
DE46133926B2BEB8001DE59C /* ShippingLabelCountryListSelectorCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE46133826B2BEB8001DE59C /* ShippingLabelCountryListSelectorCommand.swift */; };
12871288
DE4B3B2C2692DC2200EEF2D8 /* ReviewOrderViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4B3B2B2692DC2200EEF2D8 /* ReviewOrderViewModelTests.swift */; };
12881289
DE4B3B2E269455D400EEF2D8 /* MockShipmentActionStoresManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4B3B2D269455D400EEF2D8 /* MockShipmentActionStoresManager.swift */; };
@@ -2712,6 +2713,7 @@
27122713
DE279BA726E9C8E3002BA963 /* ShippingLabelPackageItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelPackageItem.swift; sourceTree = "<group>"; };
27132714
DE279BA926E9C91D002BA963 /* ShippingLabelPackateItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelPackateItemViewModel.swift; sourceTree = "<group>"; };
27142715
DE279BAC26E9CBEA002BA963 /* ShippingLabelPackagesFormViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelPackagesFormViewModelTests.swift; sourceTree = "<group>"; };
2716+
DE279BAE26EA03EA002BA963 /* ShippingLabelPackageItemViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelPackageItemViewModelTests.swift; sourceTree = "<group>"; };
27152717
DE46133826B2BEB8001DE59C /* ShippingLabelCountryListSelectorCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelCountryListSelectorCommand.swift; sourceTree = "<group>"; };
27162718
DE4B3B2B2692DC2200EEF2D8 /* ReviewOrderViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewOrderViewModelTests.swift; sourceTree = "<group>"; };
27172719
DE4B3B2D269455D400EEF2D8 /* MockShipmentActionStoresManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockShipmentActionStoresManager.swift; sourceTree = "<group>"; };
@@ -6346,6 +6348,7 @@
63466348
isa = PBXGroup;
63476349
children = (
63486350
DE279BAC26E9CBEA002BA963 /* ShippingLabelPackagesFormViewModelTests.swift */,
6351+
DE279BAE26EA03EA002BA963 /* ShippingLabelPackageItemViewModelTests.swift */,
63496352
);
63506353
path = "Multi-package";
63516354
sourceTree = "<group>";
@@ -7902,6 +7905,7 @@
79027905
020BE76723B49FE9007FE54C /* AztecBoldFormatBarCommandTests.swift in Sources */,
79037906
57C9A8FE24C23335001E1C2F /* MockNoticePresenter.swift in Sources */,
79047907
02BAB01F24D0232800F8B06E /* MockProductVariation.swift in Sources */,
7908+
DE279BAF26EA03EA002BA963 /* ShippingLabelPackageItemViewModelTests.swift in Sources */,
79057909
CE4DA5C821DD759400074607 /* CurrencyFormatterTests.swift in Sources */,
79067910
B57C745120F56EE900EEFC87 /* UITableViewCellHelpersTests.swift in Sources */,
79077911
0225C42824768A4C00C5B4F0 /* FilterProductListViewModelTests.swift in Sources */,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import XCTest
2+
@testable import WooCommerce
3+
import Yosemite
4+
5+
class ShippingLabelPackageItemViewModelTests: XCTestCase {
6+
7+
private let sampleSiteID: Int64 = 1234
8+
9+
func test_itemsRows_returns_zero_itemsRows_with_empty_items() {
10+
11+
// Given
12+
let order = MockOrders().empty().copy(siteID: sampleSiteID)
13+
let currencyFormatter = CurrencyFormatter(currencySettings: CurrencySettings())
14+
let viewModel = ShippingLabelPackageItemViewModel(order: order,
15+
orderItems: order.items,
16+
packagesResponse: mockPackageResponse(),
17+
selectedPackageID: "",
18+
totalWeight: "",
19+
products: [],
20+
productVariations: [],
21+
formatter: currencyFormatter)
22+
23+
// Then
24+
XCTAssertEqual(viewModel.itemsRows.count, 0)
25+
26+
}
27+
28+
func test_itemsRows_returns_expected_values() {
29+
30+
// Given
31+
let orderItemAttributes = [OrderItemAttribute(metaID: 170, name: "Packaging", value: "Box")]
32+
let items = [MockOrderItem.sampleItem(name: "Easter Egg", productID: 1, quantity: 1),
33+
MockOrderItem.sampleItem(name: "Jacket", productID: 33, quantity: 1),
34+
MockOrderItem.sampleItem(name: "Italian Jacket", productID: 23, quantity: 2),
35+
MockOrderItem.sampleItem(name: "Jeans",
36+
productID: 49,
37+
variationID: 49,
38+
quantity: 1,
39+
attributes: orderItemAttributes)]
40+
let expectedFirstItemRow = ItemToFulfillRow(title: "Easter Egg", subtitle: "123 kg")
41+
let expectedLastItemRow = ItemToFulfillRow(title: "Jeans", subtitle: "Box・0 kg")
42+
let order = MockOrders().makeOrder().copy(siteID: sampleSiteID, items: items)
43+
let currencyFormatter = CurrencyFormatter(currencySettings: CurrencySettings())
44+
45+
let product1 = Product.fake().copy(siteID: sampleSiteID, productID: 1, virtual: false, weight: "123")
46+
let product2 = Product.fake().copy(siteID: sampleSiteID, productID: 33, virtual: true, weight: "9")
47+
let product3 = Product.fake().copy(siteID: sampleSiteID, productID: 23, virtual: false, weight: "1")
48+
let variation = ProductVariation.fake().copy(siteID: sampleSiteID,
49+
productID: 49,
50+
productVariationID: 49,
51+
attributes: [ProductVariationAttribute(id: 1, name: "Color", option: "Blue")])
52+
53+
// When
54+
let viewModel = ShippingLabelPackageItemViewModel(order: order,
55+
orderItems: items,
56+
packagesResponse: mockPackageResponse(),
57+
selectedPackageID: "",
58+
totalWeight: "",
59+
products: [product1, product2, product3],
60+
productVariations: [variation],
61+
formatter: currencyFormatter,
62+
weightUnit: "kg")
63+
64+
// Then
65+
XCTAssertEqual(viewModel.itemsRows.count, 4)
66+
XCTAssertEqual(viewModel.itemsRows.first?.title, expectedFirstItemRow.title)
67+
XCTAssertEqual(viewModel.itemsRows.first?.subtitle, expectedFirstItemRow.subtitle)
68+
XCTAssertEqual(viewModel.itemsRows.last?.title, expectedLastItemRow.title)
69+
XCTAssertEqual(viewModel.itemsRows.last?.subtitle, expectedLastItemRow.subtitle)
70+
}
71+
}
72+
73+
// MARK: - Mocks
74+
private extension ShippingLabelPackageItemViewModelTests {
75+
func mockPackageResponse(withCustom: Bool = true, withPredefined: Bool = true) -> ShippingLabelPackagesResponse {
76+
let storeOptions = ShippingLabelStoreOptions(currencySymbol: "$",
77+
dimensionUnit: "in",
78+
weightUnit: "oz",
79+
originCountry: "US")
80+
81+
let customPackages = [
82+
ShippingLabelCustomPackage(isUserDefined: true,
83+
title: "Box",
84+
isLetter: true,
85+
dimensions: "3 x 10 x 4",
86+
boxWeight: 10,
87+
maxWeight: 11),
88+
ShippingLabelCustomPackage(isUserDefined: true,
89+
title: "Box n°2",
90+
isLetter: true,
91+
dimensions: "30 x 1 x 20",
92+
boxWeight: 2,
93+
maxWeight: 4),
94+
ShippingLabelCustomPackage(isUserDefined: true,
95+
title: "Box n°3",
96+
isLetter: true,
97+
dimensions: "10 x 40 x 3",
98+
boxWeight: 7,
99+
maxWeight: 10)]
100+
101+
let predefinedOptions = [ShippingLabelPredefinedOption(title: "USPS", predefinedPackages: [ShippingLabelPredefinedPackage(id: "package-1",
102+
title: "Small",
103+
isLetter: true,
104+
dimensions: "3 x 4 x 5"),
105+
ShippingLabelPredefinedPackage(id: "package-2",
106+
title: "Big",
107+
isLetter: true,
108+
dimensions: "5 x 7 x 9")])]
109+
110+
let packagesResponse = ShippingLabelPackagesResponse(storeOptions: storeOptions,
111+
customPackages: withCustom ? customPackages : [],
112+
predefinedOptions: withPredefined ? predefinedOptions : [],
113+
unactivatedPredefinedOptions: [])
114+
115+
return packagesResponse
116+
}
117+
}

0 commit comments

Comments
 (0)