Skip to content

Commit b69e8ae

Browse files
authored
[Shipping Labels] Show options to move selected items (#15418)
2 parents 0366ca7 + e0f41f4 commit b69e8ae

File tree

5 files changed

+250
-31
lines changed

5 files changed

+250
-31
lines changed

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,18 @@ final class CollapsibleShipmentCardViewModel: ObservableObject, Identifiable {
1414

1515
var onSelectionChange: (() -> Void)?
1616

17-
var hasSelectedAnItem: Bool {
17+
var selectedShipmentIds: [String] {
1818
if mainShipmentRow.selected {
19-
return true
19+
if childShipmentRows.isNotEmpty {
20+
return childShipmentRows.map { $0.shipmentId }
21+
} else {
22+
return [mainShipmentRow.shipmentId]
23+
}
2024
}
2125

2226
return childShipmentRows
2327
.filter { $0.selected }
24-
.isNotEmpty
28+
.map(\.shipmentId)
2529
}
2630

2731
init(parentShipmentId: String,
@@ -50,6 +54,7 @@ final class CollapsibleShipmentCardViewModel: ObservableObject, Identifiable {
5054
func selectAll() {
5155
mainShipmentRow.setSelected(true)
5256
childShipmentRows.forEach({ $0.setSelected(true) })
57+
onSelectionChange?()
5358
}
5459
}
5560

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import SwiftUI
2+
3+
struct MoveToShipmentNoticeViewModel {
4+
enum MoveTo {
5+
case existingShipment(index: Int)
6+
case newShipment
7+
}
8+
9+
let selectedItemsCount: Int
10+
let existingShipmentsCount: Int
11+
let currentShipmentIndex: Int?
12+
let actionHandler: ((MoveTo) -> Void)
13+
}
14+
15+
struct MoveToShipmentNotice: View {
16+
let viewModel: MoveToShipmentNoticeViewModel
17+
18+
var body: some View {
19+
HStack {
20+
Text(String.localizedStringWithFormat(Localization.message, viewModel.selectedItemsCount))
21+
.font(.subheadline)
22+
.fontWeight(.semibold)
23+
.foregroundColor(Color(uiColor: .text))
24+
25+
Spacer()
26+
27+
if viewModel.existingShipmentsCount == 0 {
28+
moveToNewShipment
29+
} else {
30+
menuWithExistingShipments
31+
}
32+
}
33+
.padding(.horizontal, Layout.horizontalPadding)
34+
.padding(.vertical, Layout.verticalPadding)
35+
.background(.thickMaterial)
36+
.cornerRadius(Layout.cornerRadius)
37+
.shadow(color: .black.opacity(Layout.shadowColorOpacity),
38+
radius: Layout.cornerRadius,
39+
y: Layout.shadowYOffset)
40+
}
41+
}
42+
43+
private extension MoveToShipmentNotice {
44+
var moveToNewShipment: some View {
45+
Button {
46+
viewModel.actionHandler(.newShipment)
47+
} label: {
48+
HStack(spacing: Layout.horizontalSpacing) {
49+
Text(Localization.moveToNewShipment)
50+
.font(.subheadline)
51+
.fontWeight(.semibold)
52+
}
53+
.foregroundColor(Color(.accent))
54+
}
55+
}
56+
57+
var menuWithExistingShipments: some View {
58+
Menu {
59+
ForEach(0..<viewModel.existingShipmentsCount, id: \.self) { index in
60+
if viewModel.currentShipmentIndex != index {
61+
Button(String.localizedStringWithFormat(Localization.shipment, index + 1), action: {
62+
viewModel.actionHandler(.existingShipment(index: index))
63+
})
64+
}
65+
}
66+
67+
Button(Localization.newShipment, action: {
68+
viewModel.actionHandler(.newShipment)
69+
})
70+
} label: {
71+
HStack(spacing: Layout.horizontalSpacing) {
72+
Text(Localization.moveTo)
73+
.font(.subheadline)
74+
.fontWeight(.semibold)
75+
76+
Image(systemName: "chevron.up.chevron.down")
77+
}
78+
.foregroundColor(Color(.accent))
79+
}
80+
.environment(\.menuOrder, .fixed)
81+
}
82+
}
83+
84+
private extension MoveToShipmentNotice {
85+
enum Layout {
86+
static let horizontalPadding: CGFloat = 16
87+
static let verticalPadding: CGFloat = 22
88+
static let horizontalSpacing: CGFloat = 8
89+
static let cornerRadius: CGFloat = 8
90+
static let shadowYOffset: CGFloat = 2
91+
static let shadowColorOpacity: CGFloat = 0.16
92+
}
93+
94+
enum Localization {
95+
static let message = NSLocalizedString(
96+
"wooShippingSplitShipments.MoveToShipmentNotice.title",
97+
value: "%1$d selected",
98+
comment: "The number of selected items in split shipments flow. %1$d is the number of selected items. Reads like: 2 selected"
99+
)
100+
static let shipment = NSLocalizedString(
101+
"wooShippingSplitShipments.MoveToShipmentNotice.shipment",
102+
value: "Shipment %1$d",
103+
comment: "Label used in the button to select the shipment in split shipments flow. %1$d is the shipment number. Reads like: Shipment 1"
104+
)
105+
static let moveTo = NSLocalizedString(
106+
"wooShippingSplitShipments.MoveToShipmentNotice.moveTo",
107+
value: "Move to",
108+
comment: "Button to move selected items to a shipment in split shipments flow"
109+
)
110+
static let moveToNewShipment = NSLocalizedString(
111+
"wooShippingSplitShipments.MoveToShipmentNotice.moveToNewShipment",
112+
value: "Move to new shipment",
113+
comment: "Button to move selected items to a new shipment in split shipments flow"
114+
)
115+
static let newShipment = NSLocalizedString(
116+
"wooShippingSplitShipments.MoveToShipmentNotice.newShipment",
117+
value: "New shipment",
118+
comment: "Title of the button to move selected items to a new shipment in split shipments flow"
119+
)
120+
}
121+
}
122+
123+
#Preview {
124+
MoveToShipmentNotice(viewModel: MoveToShipmentNoticeViewModel(selectedItemsCount: 4,
125+
existingShipmentsCount: 3,
126+
currentShipmentIndex: 1,
127+
actionHandler: { _ in }))
128+
}

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

Lines changed: 72 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,28 @@ struct WooShippingSplitShipmentsDetailView: View {
88

99
var body: some View {
1010
NavigationView {
11-
ScrollView {
12-
VStack(alignment: .leading, spacing: Layout.contentPadding) {
13-
AdaptiveStack(horizontalAlignment: .leading) {
14-
Text(viewModel.itemsCountLabel)
15-
.headlineStyle()
16-
Spacer()
17-
Text(viewModel.itemsDetailLabel)
18-
.foregroundStyle(Color(.textSubtle))
19-
}
11+
ZStack(alignment: .bottom) {
12+
ScrollView {
13+
VStack(alignment: .leading, spacing: Layout.contentPadding) {
14+
AdaptiveStack(horizontalAlignment: .leading) {
15+
Text(viewModel.itemsCountLabel)
16+
.headlineStyle()
17+
Spacer()
18+
Text(viewModel.itemsDetailLabel)
19+
.foregroundStyle(Color(.textSubtle))
20+
}
2021

21-
VStack(spacing: Layout.verticalSpacing) {
22-
ForEach(viewModel.shipmentCardViewModels) { item in
23-
CollapsibleShipmentCard(viewModel: item)
22+
VStack(spacing: Layout.verticalSpacing) {
23+
ForEach(viewModel.shipmentCardViewModels) { item in
24+
CollapsibleShipmentCard(viewModel: item)
25+
}
2426
}
2527
}
28+
.padding(Layout.contentPadding)
2629
}
27-
.padding(Layout.contentPadding)
30+
31+
noticeStack
32+
.padding(Layout.contentPadding)
2833
}
2934
.navigationBarTitleDisplayMode(.inline)
3035
.navigationTitle(Localization.title)
@@ -41,20 +46,73 @@ struct WooShippingSplitShipmentsDetailView: View {
4146
}
4247
}
4348
}
44-
.notice($viewModel.instructionsNotice, autoDismiss: false)
4549
.onAppear {
4650
viewModel.onAppear()
4751
}
4852
}
4953
}
5054

5155
private extension WooShippingSplitShipmentsDetailView {
56+
var noticeStack: some View {
57+
VStack(spacing: Layout.contentPadding) {
58+
if let message = viewModel.instructions {
59+
InstructionsSnackbar(message: message) {
60+
viewModel.dismissInstructions()
61+
}
62+
}
63+
64+
if let moveTo = viewModel.moveToNoticeViewModel {
65+
MoveToShipmentNotice(viewModel: moveTo)
66+
}
67+
}
68+
}
69+
}
70+
71+
private struct InstructionsSnackbar: View {
72+
let message: String
73+
let actionHandler: () -> Void
74+
75+
var body: some View {
76+
HStack(alignment: .top, spacing: Layout.hSpacing) {
77+
BoldableTextView(message)
78+
.foregroundStyle(Color(.textInverted))
79+
80+
Spacer()
81+
82+
Button {
83+
actionHandler()
84+
} label: {
85+
Image(systemName: "xmark")
86+
.foregroundStyle(Color(.withColorStudio(.gray)))
87+
}
88+
}
89+
.padding(WooShippingSplitShipmentsDetailView.Layout.contentPadding)
90+
.background {
91+
RoundedRectangle(cornerRadius: WooShippingSplitShipmentsDetailView.Layout.cornerRadius)
92+
.fill(Color(.text))
93+
.shadow(color: Color(.text).opacity(Layout.shadowColorOpacity),
94+
radius: WooShippingSplitShipmentsDetailView.Layout.shadowRadius,
95+
y: WooShippingSplitShipmentsDetailView.Layout.shadowYOffset)
96+
}
97+
}
98+
99+
private enum Layout {
100+
static let hSpacing: CGFloat = 8
101+
static let shadowColorOpacity: CGFloat = 0.16
102+
}
103+
}
104+
105+
fileprivate extension WooShippingSplitShipmentsDetailView {
52106
enum Layout {
53107
static let contentPadding: CGFloat = 16
54108
static let borderCornerRadius: CGFloat = 8
109+
static let shadowRadius: CGFloat = 8
110+
static let shadowYOffset: CGFloat = 2
55111
static let borderWidth: CGFloat = 0.5
56112
static let verticalSpacing: CGFloat = 8
113+
static let cornerRadius: CGFloat = 8
57114
}
115+
58116
enum Localization {
59117
static let title = NSLocalizedString(
60118
"wooShippingSplitShipmentsDetailView.title",

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

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ final class WooShippingSplitShipmentsViewModel: ObservableObject {
2828

2929
let shipmentCardViewModels: [CollapsibleShipmentCardViewModel]
3030

31-
@Published var instructionsNotice: Notice?
31+
@Published private(set) var moveToNoticeViewModel: MoveToShipmentNoticeViewModel?
32+
33+
@Published private(set) var instructions: String?
34+
private var dismissedInstructions: Bool = false
35+
3236

3337
init(order: Order,
3438
config: WooShippingConfig,
@@ -75,42 +79,62 @@ final class WooShippingSplitShipmentsViewModel: ObservableObject {
7579

7680
func onAppear() {
7781
showInstructionsNotice()
82+
showMoveToNotice()
7883
}
7984

8085
func selectAll() {
8186
shipmentCardViewModels.forEach {
8287
$0.selectAll()
8388
}
8489
}
90+
91+
func dismissInstructions() {
92+
instructions = nil
93+
dismissedInstructions = true
94+
}
8595
}
8696

8797
private extension WooShippingSplitShipmentsViewModel {
8898
func configureSelectionCallback() {
8999
shipmentCardViewModels.forEach { viewModel in
90100
viewModel.onSelectionChange = { [weak self] in
91-
self?.checkSelectionAndHideInstructions()
101+
self?.showMoveToNotice()
92102
}
93103
}
94104
}
95105

96106
func showInstructionsNotice() {
97-
if hasSelectedAnItem() == false {
98-
instructionsNotice = Notice(message: Localization.SelectionInstructionsNotice.message,
99-
feedbackType: .success,
100-
actionTitle: Localization.SelectionInstructionsNotice.dismiss) { [weak self] in
101-
self?.instructionsNotice = nil
102-
}
107+
if !dismissedInstructions {
108+
instructions = Localization.SelectionInstructionsNotice.message
103109
}
104110
}
105111

106-
func checkSelectionAndHideInstructions() {
107-
if hasSelectedAnItem() {
108-
instructionsNotice = nil
112+
func showMoveToNotice() {
113+
let selectedItemsCount = {
114+
shipmentCardViewModels.map { $0.selectedShipmentIds.count }.reduce(0, +)
115+
}()
116+
117+
guard selectedItemsCount > 0 else {
118+
return self.moveToNoticeViewModel = nil
109119
}
110-
}
111120

112-
func hasSelectedAnItem() -> Bool {
113-
shipmentCardViewModels.contains(where: { $0.hasSelectedAnItem })
121+
// TODO: Use count and index values from shipment tabs
122+
moveToNoticeViewModel = MoveToShipmentNoticeViewModel(selectedItemsCount: selectedItemsCount,
123+
existingShipmentsCount: 3,
124+
currentShipmentIndex: 2,
125+
actionHandler: { [weak self] moveTo in
126+
guard let self else { return }
127+
128+
self.moveToNoticeViewModel = nil
129+
self.instructions = nil
130+
131+
switch moveTo {
132+
case .existingShipment:
133+
break
134+
case .newShipment:
135+
break
136+
}
137+
})
114138
}
115139

116140
/// Configures the labels in the section header.

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3040,6 +3040,7 @@
30403040
EEBB9B3B2D8E5071008D6CE5 /* SelectableShipmentRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEBB9B3A2D8E5058008D6CE5 /* SelectableShipmentRow.swift */; };
30413041
EEBB9B3D2D8E5099008D6CE5 /* SelectableShipmentRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEBB9B3C2D8E508E008D6CE5 /* SelectableShipmentRowViewModel.swift */; };
30423042
EEBB9B402D8FE5B6008D6CE5 /* WooShippingSplitShipmentsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEBB9B3F2D8FE5B4008D6CE5 /* WooShippingSplitShipmentsViewModelTests.swift */; };
3043+
EEBBC3BC2D92A1E0008D6CE5 /* MoveToShipmentNotice.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEBBC3BB2D92A1C6008D6CE5 /* MoveToShipmentNotice.swift */; };
30433044
EEBDF7DA2A2EF69B00EFEF47 /* ShareProductCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEBDF7D92A2EF69B00EFEF47 /* ShareProductCoordinator.swift */; };
30443045
EEBDF7DF2A2F674100EFEF47 /* ShareProductAIEligibilityChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEBDF7DE2A2F674100EFEF47 /* ShareProductAIEligibilityChecker.swift */; };
30453046
EEBDF7E22A2F685C00EFEF47 /* DefaultShareProductAIEligibilityCheckerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEBDF7E12A2F685C00EFEF47 /* DefaultShareProductAIEligibilityCheckerTests.swift */; };
@@ -6257,6 +6258,7 @@
62576258
EEBB9B3A2D8E5058008D6CE5 /* SelectableShipmentRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableShipmentRow.swift; sourceTree = "<group>"; };
62586259
EEBB9B3C2D8E508E008D6CE5 /* SelectableShipmentRowViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableShipmentRowViewModel.swift; sourceTree = "<group>"; };
62596260
EEBB9B3F2D8FE5B4008D6CE5 /* WooShippingSplitShipmentsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooShippingSplitShipmentsViewModelTests.swift; sourceTree = "<group>"; };
6261+
EEBBC3BB2D92A1C6008D6CE5 /* MoveToShipmentNotice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoveToShipmentNotice.swift; sourceTree = "<group>"; };
62606262
EEBDF7D92A2EF69B00EFEF47 /* ShareProductCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareProductCoordinator.swift; sourceTree = "<group>"; };
62616263
EEBDF7DE2A2F674100EFEF47 /* ShareProductAIEligibilityChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareProductAIEligibilityChecker.swift; sourceTree = "<group>"; };
62626264
EEBDF7E12A2F685C00EFEF47 /* DefaultShareProductAIEligibilityCheckerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultShareProductAIEligibilityCheckerTests.swift; sourceTree = "<group>"; };
@@ -14024,6 +14026,7 @@
1402414026
EE7E75A62D83EAD200E6FF5B /* WooShipping Split Shipments */ = {
1402514027
isa = PBXGroup;
1402614028
children = (
14029+
EEBBC3BB2D92A1C6008D6CE5 /* MoveToShipmentNotice.swift */,
1402714030
EEBB9B3C2D8E508E008D6CE5 /* SelectableShipmentRowViewModel.swift */,
1402814031
EEBB9B3A2D8E5058008D6CE5 /* SelectableShipmentRow.swift */,
1402914032
EEBB81702D8C0834008D6CE5 /* CollapsibleShipmentCard.swift */,
@@ -16044,6 +16047,7 @@
1604416047
CEE113952CFA2F7700F53E30 /* WooShippingSelectedPackageView.swift in Sources */,
1604516048
454B28BE23BF63C600CD2091 /* DateIntervalFormatter+Helpers.swift in Sources */,
1604616049
AE6DBE3B2732CAAD00957E7A /* AdaptiveStack.swift in Sources */,
16050+
EEBBC3BC2D92A1E0008D6CE5 /* MoveToShipmentNotice.swift in Sources */,
1604716051
03A6C18628B8CC7F00AADF23 /* InPersonPaymentsOnboardingErrorButtonViewModel.swift in Sources */,
1604816052
024DF3052372ADCD006658FE /* KeyboardScrollable.swift in Sources */,
1604916053
BAE4F8432734325C00871344 /* SettingsViewModel.swift in Sources */,

0 commit comments

Comments
 (0)