Skip to content

Commit ec3566f

Browse files
authored
Shipping Labels: UI improvements for splitting shipment screen (#15838)
2 parents f65a395 + 94e2a97 commit ec3566f

File tree

9 files changed

+338
-3
lines changed

9 files changed

+338
-3
lines changed

RELEASE-NOTES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
22.8
55
-----
6+
- [*] Shipping Labels: Improve UI of Split shipments screen. [https://github.com/woocommerce/woocommerce-ios/pull/15838]
67
- [*] POS: icon button with confirmation step used for clearing the cart [https://github.com/woocommerce/woocommerce-ios/pull/15829]
78

89
22.7

WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/CollapsibleShipmentItemCard.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ private extension CollapsibleShipmentItemCard {
6060
.foregroundColor(Color(.accent))
6161
}
6262
})
63-
.buttonStyle(PlainButtonStyle())
6463
}
6564
}
6665

WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/CollapsibleShipmentItemCardViewModel.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,9 @@ private extension CollapsibleShipmentItemCardViewModel {
7676
$0.onSelectedChange = { [weak self] row in
7777
guard let self else { return }
7878

79-
mainItemRow.setSelected(false)
79+
// Check if all child items are selected
80+
let allChildrenSelected = childItemRows.allSatisfy { $0.selected }
81+
mainItemRow.setSelected(allChildrenSelected)
8082
onSelectionChange?()
8183
}
8284
})

WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/SelectableShipmentItemRow.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import SwiftUI
44
/// Row for a selectable shipment item to ship with the Woo Shipping extension.
55
struct SelectableShipmentItemRow: View {
66
@ObservedObject private var viewModel: SelectableShipmentItemRowViewModel
7+
@Environment(\.isEnabled) private var isEnabled
78

89
init(viewModel: SelectableShipmentItemRowViewModel) {
910
self.viewModel = viewModel
@@ -50,6 +51,7 @@ struct SelectableShipmentItemRow: View {
5051
}
5152
}
5253
.frame(maxWidth: .infinity)
54+
.opacity(isEnabled ? 1 : Layout.disabledOpacity)
5355
}
5456
}
5557

@@ -81,6 +83,7 @@ private extension SelectableShipmentItemRow {
8183
static let imageSize: CGFloat = 56.0
8284
static let imageCornerRadius: CGFloat = 4.0
8385
static let badgeOffset: CGFloat = 8.0
86+
static let disabledOpacity: CGFloat = 0.5
8487
}
8588
}
8689

WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/WooShippingSplitShipmentsView.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ struct WooShippingSplitShipmentsView: View {
101101
Button(Localization.SaveShipmentError.retry) {
102102
saveShipmentInfoAndDismiss()
103103
}
104+
Button(Localization.SaveShipmentError.revertChanges) {
105+
viewModel.revertChanges()
106+
}
104107
}
105108
}
106109
}
@@ -495,6 +498,11 @@ fileprivate extension WooShippingSplitShipmentsView {
495498
value: "Cancel",
496499
comment: "Cancel button title on the error alert when saving split shipment changes fails"
497500
)
501+
static let revertChanges = NSLocalizedString(
502+
"wooShipping.createLabels.splitShipment.saveShipmentError.revertChanges",
503+
value: "Revert changes",
504+
comment: "Button on the error alert to revert changes when saving split shipment changes fails"
505+
)
498506
}
499507
}
500508
}

WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/WooShippingSplitShipmentsViewModel.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,9 @@ final class WooShippingSplitShipmentsViewModel: ObservableObject {
137137
}
138138

139139
func onAppear() {
140-
showInstructionsNotice()
140+
if shipments.count == 1 {
141+
showInstructionsNotice()
142+
}
141143
updateMoveToNotice()
142144
}
143145

@@ -152,6 +154,11 @@ final class WooShippingSplitShipmentsViewModel: ObservableObject {
152154
dismissedInstructions = true
153155
}
154156

157+
func revertChanges() {
158+
shipments = shipmentsSavedInRemote
159+
selectedShipmentIndex = 0
160+
}
161+
155162
func didPurchaseLabel(for shipmentIndex: Int, purchasedLabelID: Int64) {
156163
let currentShipment = shipments[shipmentIndex]
157164
let updatedContents = currentShipment.contents.map {

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2538,6 +2538,7 @@
25382538
DE02ABBE2B578D0E008E0AC4 /* CreditCardType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE02ABBD2B578D0E008E0AC4 /* CreditCardType.swift */; };
25392539
DE02ABC02B57D333008E0AC4 /* BlazeConfirmPaymentViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE02ABBF2B57D333008E0AC4 /* BlazeConfirmPaymentViewModelTests.swift */; };
25402540
DE02ABC22B5903AB008E0AC4 /* BlazeCampaignCreationErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE02ABC12B5903AB008E0AC4 /* BlazeCampaignCreationErrorView.swift */; };
2541+
DE02B64F2E12766B00B79E0D /* CollapsibleShipmentItemCardViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE02B64E2E12766B00B79E0D /* CollapsibleShipmentItemCardViewModelTests.swift */; };
25412542
DE02C65C2D5A0B9F0089850D /* FailedProductImageCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE02C65B2D5A0B9F0089850D /* FailedProductImageCollectionViewCell.swift */; };
25422543
DE02C65E2D5A0C5D0089850D /* FailedProductImageCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = DE02C65D2D5A0C5D0089850D /* FailedProductImageCollectionViewCell.xib */; };
25432544
DE06D6602D64699D00419FFA /* AuthenticatedWebViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE06D65F2D64699D00419FFA /* AuthenticatedWebViewModelTests.swift */; };
@@ -5697,6 +5698,7 @@
56975698
DE02ABBD2B578D0E008E0AC4 /* CreditCardType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditCardType.swift; sourceTree = "<group>"; };
56985699
DE02ABBF2B57D333008E0AC4 /* BlazeConfirmPaymentViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeConfirmPaymentViewModelTests.swift; sourceTree = "<group>"; };
56995700
DE02ABC12B5903AB008E0AC4 /* BlazeCampaignCreationErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignCreationErrorView.swift; sourceTree = "<group>"; };
5701+
DE02B64E2E12766B00B79E0D /* CollapsibleShipmentItemCardViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = CollapsibleShipmentItemCardViewModelTests.swift; path = "WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/Split shipments/CollapsibleShipmentItemCardViewModelTests.swift"; sourceTree = "<group>"; };
57005702
DE02C65B2D5A0B9F0089850D /* FailedProductImageCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailedProductImageCollectionViewCell.swift; sourceTree = "<group>"; };
57015703
DE02C65D2D5A0C5D0089850D /* FailedProductImageCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FailedProductImageCollectionViewCell.xib; sourceTree = "<group>"; };
57025704
DE06D65F2D64699D00419FFA /* AuthenticatedWebViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticatedWebViewModelTests.swift; sourceTree = "<group>"; };
@@ -10541,6 +10543,7 @@
1054110543
B56DB3BD2049BFAA00D4AA8E = {
1054210544
isa = PBXGroup;
1054310545
children = (
10546+
DE02B64E2E12766B00B79E0D /* CollapsibleShipmentItemCardViewModelTests.swift */,
1054410547
3F3689E22DCB1B470065B48F /* Modules */,
1054510548
D8FBFF1622D4CC2F006E3336 /* docs */,
1054610549
8CA4F6DC220B24EB00A47B5D /* config */,
@@ -16807,6 +16810,7 @@
1680716810
isa = PBXSourcesBuildPhase;
1680816811
buildActionMask = 2147483647;
1680916812
files = (
16813+
DE02B64F2E12766B00B79E0D /* CollapsibleShipmentItemCardViewModelTests.swift in Sources */,
1681016814
45C8B2692316B2440002FA77 /* BillingAddressTableViewCellTests.swift in Sources */,
1681116815
B95A45EC2A77D7A60073A91F /* CustomerSelectorViewModelTests.swift in Sources */,
1681216816
3178C1FD26409360000D771A /* BluetoothCardReaderSettingsConnectedViewModelTests.swift in Sources */,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import XCTest
2+
@testable import WooCommerce
3+
import WooFoundation
4+
import Yosemite
5+
6+
final class CollapsibleShipmentItemCardViewModelTests: XCTestCase {
7+
8+
// MARK: - observeSelection Tests
9+
10+
func test_observeSelection_when_main_item_selected_then_all_children_are_selected() {
11+
// Given
12+
let packageItem = samplePackageItem(quantity: 3)
13+
let viewModel = CollapsibleShipmentItemCardViewModel(item: packageItem, currency: "USD")
14+
15+
var selectionChangeCallCount = 0
16+
viewModel.onSelectionChange = {
17+
selectionChangeCallCount += 1
18+
}
19+
20+
// When
21+
viewModel.mainItemRow.handleTap() // Select main item
22+
23+
// Then
24+
XCTAssertTrue(viewModel.mainItemRow.selected)
25+
XCTAssertTrue(viewModel.childItemRows.allSatisfy { $0.selected })
26+
XCTAssertEqual(selectionChangeCallCount, 1)
27+
}
28+
29+
func test_observeSelection_when_main_item_deselected_then_all_children_are_deselected() {
30+
// Given
31+
let packageItem = samplePackageItem(quantity: 3)
32+
let viewModel = CollapsibleShipmentItemCardViewModel(item: packageItem, currency: "USD")
33+
34+
// First select the main item
35+
viewModel.mainItemRow.handleTap()
36+
37+
var selectionChangeCallCount = 0
38+
viewModel.onSelectionChange = {
39+
selectionChangeCallCount += 1
40+
}
41+
42+
// When
43+
viewModel.mainItemRow.handleTap() // Deselect main item
44+
45+
// Then
46+
XCTAssertFalse(viewModel.mainItemRow.selected)
47+
XCTAssertTrue(viewModel.childItemRows.allSatisfy { !$0.selected })
48+
XCTAssertEqual(selectionChangeCallCount, 1)
49+
}
50+
51+
func test_observeSelection_when_all_children_selected_then_main_item_is_selected() {
52+
// Given
53+
let packageItem = samplePackageItem(quantity: 3)
54+
let viewModel = CollapsibleShipmentItemCardViewModel(item: packageItem, currency: "USD")
55+
56+
var selectionChangeCallCount = 0
57+
viewModel.onSelectionChange = {
58+
selectionChangeCallCount += 1
59+
}
60+
61+
// When - Select all child items one by one
62+
viewModel.childItemRows[0].handleTap()
63+
viewModel.childItemRows[1].handleTap()
64+
viewModel.childItemRows[2].handleTap()
65+
66+
// Then
67+
XCTAssertTrue(viewModel.mainItemRow.selected)
68+
XCTAssertTrue(viewModel.childItemRows.allSatisfy { $0.selected })
69+
XCTAssertEqual(selectionChangeCallCount, 3)
70+
}
71+
72+
func test_observeSelection_when_some_children_selected_then_main_item_is_not_selected() {
73+
// Given
74+
let packageItem = samplePackageItem(quantity: 3)
75+
let viewModel = CollapsibleShipmentItemCardViewModel(item: packageItem, currency: "USD")
76+
77+
var selectionChangeCallCount = 0
78+
viewModel.onSelectionChange = {
79+
selectionChangeCallCount += 1
80+
}
81+
82+
// When - Select only some child items
83+
viewModel.childItemRows[0].handleTap()
84+
viewModel.childItemRows[1].handleTap()
85+
// Note: childItemRows[2] is not selected
86+
87+
// Then
88+
XCTAssertFalse(viewModel.mainItemRow.selected)
89+
XCTAssertTrue(viewModel.childItemRows[0].selected)
90+
XCTAssertTrue(viewModel.childItemRows[1].selected)
91+
XCTAssertFalse(viewModel.childItemRows[2].selected)
92+
XCTAssertEqual(selectionChangeCallCount, 2)
93+
}
94+
95+
func test_observeSelection_when_all_children_selected_then_one_deselected_then_main_item_is_deselected() {
96+
// Given
97+
let packageItem = samplePackageItem(quantity: 3)
98+
let viewModel = CollapsibleShipmentItemCardViewModel(item: packageItem, currency: "USD")
99+
100+
// First select all children
101+
viewModel.childItemRows.forEach { $0.handleTap() }
102+
103+
// Verify all are selected initially
104+
XCTAssertTrue(viewModel.mainItemRow.selected)
105+
XCTAssertTrue(viewModel.childItemRows.allSatisfy { $0.selected })
106+
107+
var selectionChangeCallCount = 0
108+
viewModel.onSelectionChange = {
109+
selectionChangeCallCount += 1
110+
}
111+
112+
// When - Deselect one child item
113+
viewModel.childItemRows[1].handleTap()
114+
115+
// Then
116+
XCTAssertFalse(viewModel.mainItemRow.selected)
117+
XCTAssertTrue(viewModel.childItemRows[0].selected)
118+
XCTAssertFalse(viewModel.childItemRows[1].selected)
119+
XCTAssertTrue(viewModel.childItemRows[2].selected)
120+
XCTAssertEqual(selectionChangeCallCount, 1)
121+
}
122+
123+
func test_observeSelection_with_single_quantity_item_has_no_children() {
124+
// Given
125+
let packageItem = samplePackageItem(quantity: 1)
126+
let viewModel = CollapsibleShipmentItemCardViewModel(item: packageItem, currency: "USD")
127+
128+
// Then
129+
XCTAssertTrue(viewModel.childItemRows.isEmpty)
130+
XCTAssertFalse(viewModel.mainItemRow.selected)
131+
}
132+
133+
// MARK: - numberOfSelectedItems Tests
134+
135+
func test_numberOfSelectedItems_with_single_quantity_item() {
136+
// Given
137+
let packageItem = samplePackageItem(quantity: 1)
138+
let viewModel = CollapsibleShipmentItemCardViewModel(item: packageItem, currency: "USD")
139+
140+
// When not selected
141+
XCTAssertEqual(viewModel.numberOfSelectedItems, 0)
142+
143+
// When selected
144+
viewModel.mainItemRow.handleTap()
145+
XCTAssertEqual(viewModel.numberOfSelectedItems, 1)
146+
}
147+
148+
func test_numberOfSelectedItems_with_multiple_quantity_item() {
149+
// Given
150+
let packageItem = samplePackageItem(quantity: 3)
151+
let viewModel = CollapsibleShipmentItemCardViewModel(item: packageItem, currency: "USD")
152+
153+
// When no children selected
154+
XCTAssertEqual(viewModel.numberOfSelectedItems, 0)
155+
156+
// When some children selected
157+
viewModel.childItemRows[0].handleTap()
158+
viewModel.childItemRows[1].handleTap()
159+
XCTAssertEqual(viewModel.numberOfSelectedItems, 2)
160+
161+
// When all children selected
162+
viewModel.childItemRows[2].handleTap()
163+
XCTAssertEqual(viewModel.numberOfSelectedItems, 3)
164+
}
165+
166+
// MARK: - selectAll Tests
167+
168+
func test_selectAll_selects_main_and_all_children() {
169+
// Given
170+
let packageItem = samplePackageItem(quantity: 3)
171+
let viewModel = CollapsibleShipmentItemCardViewModel(item: packageItem, currency: "USD")
172+
173+
var selectionChangeCallCount = 0
174+
viewModel.onSelectionChange = {
175+
selectionChangeCallCount += 1
176+
}
177+
178+
// When
179+
viewModel.selectAll()
180+
181+
// Then
182+
XCTAssertTrue(viewModel.mainItemRow.selected)
183+
XCTAssertTrue(viewModel.childItemRows.allSatisfy { $0.selected })
184+
XCTAssertEqual(selectionChangeCallCount, 1)
185+
}
186+
187+
func test_selectAll_with_single_quantity_item() {
188+
// Given
189+
let packageItem = samplePackageItem(quantity: 1)
190+
let viewModel = CollapsibleShipmentItemCardViewModel(item: packageItem, currency: "USD")
191+
192+
var selectionChangeCallCount = 0
193+
viewModel.onSelectionChange = {
194+
selectionChangeCallCount += 1
195+
}
196+
197+
// When
198+
viewModel.selectAll()
199+
200+
// Then
201+
XCTAssertTrue(viewModel.mainItemRow.selected)
202+
XCTAssertTrue(viewModel.childItemRows.isEmpty)
203+
XCTAssertEqual(selectionChangeCallCount, 1)
204+
}
205+
}
206+
207+
// MARK: - Helper Methods
208+
209+
private extension CollapsibleShipmentItemCardViewModelTests {
210+
func samplePackageItem(quantity: Decimal) -> ShippingLabelPackageItem {
211+
ShippingLabelPackageItem(
212+
productOrVariationID: 123,
213+
orderItemID: 456,
214+
name: "Test Item",
215+
weight: 1.5,
216+
quantity: quantity,
217+
value: 10.0,
218+
dimensions: ProductDimensions(length: "10", width: "10", height: "10"),
219+
attributes: [],
220+
imageURL: nil
221+
)
222+
}
223+
}

0 commit comments

Comments
 (0)