Skip to content

Commit 5bb61c8

Browse files
committed
Merge branch 'trunk' into issue/6568-product-reviews-list-empty-crash
# Conflicts: # WooCommerce/Classes/ViewRelated/Products/Edit Product/Reviews/ProductReviewsDataSource.swift
2 parents 6146424 + eeb1b13 commit 5bb61c8

File tree

12 files changed

+269
-30
lines changed

12 files changed

+269
-30
lines changed

RELEASE-NOTES.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
*** PLEASE FOLLOW THIS FORMAT: [<priority indicator, more stars = higher priority>] <description> [<PR URL>]
22

3+
9.0
4+
-----
5+
- [internal] Reviews lists on Products and Menu tabs are refactored to avoid duplicated code. Please quickly smoke test them to make sure that everything still works as before. [https://github.com/woocommerce/woocommerce-ios/pull/6553]
6+
37
8.9
48
-----
59
- [*] Coupons: Fixed issue loading the coupon list from the local storage on initial load. [https://github.com/woocommerce/woocommerce-ios/pull/6463]

WooCommerce/Classes/ViewRelated/Coupons/Add and Edit Coupons/AddEditCoupon.swift

Lines changed: 179 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,29 +15,196 @@ struct AddEditCoupon: View {
1515

1616
var body: some View {
1717
NavigationView {
18+
GeometryReader { geometry in
19+
ScrollView {
20+
VStack (alignment: .leading) {
21+
Group {
22+
ListHeaderView(text: Localization.headerCouponDetails.uppercased(), alignment: .left)
1823

19-
//TODO: implement the content of the view
20-
Text("Hello, World!")
21-
.toolbar {
22-
ToolbarItem(placement: .cancellationAction) {
23-
Button("Cancel", action: {
24-
presentation.wrappedValue.dismiss()
25-
})
26-
}
27-
}
24+
Group {
25+
TitleAndTextFieldRow(title: Localization.couponAmountPercentage,
26+
placeholder: Localization.couponAmountPercentage,
27+
text: $viewModel.amountField,
28+
editable: false,
29+
fieldAlignment: .leading,
30+
keyboardType: .asciiCapableNumberPad)
31+
Divider()
32+
.padding(.leading, Constants.margin)
33+
}
34+
35+
Text(Localization.footerCouponAmountPercentage)
36+
.subheadlineStyle()
37+
.padding(.horizontal, Constants.margin)
38+
39+
Group {
40+
TitleAndTextFieldRow(title: Localization.couponCode,
41+
placeholder: Localization.couponCode,
42+
text: $viewModel.codeField,
43+
editable: false,
44+
fieldAlignment: .leading,
45+
keyboardType: .asciiCapableNumberPad)
46+
Divider()
47+
.padding(.leading, Constants.margin)
48+
}
49+
50+
Text(Localization.footerCouponCode)
51+
.subheadlineStyle()
52+
.padding(.horizontal, Constants.margin)
53+
54+
//TODO: leading aligning for this button
55+
Button {
56+
//TODO: handle action
57+
} label: {
58+
Text(Localization.regenerateCouponCodeButton)
59+
}
60+
.buttonStyle(LinkButtonStyle())
61+
.padding(.horizontal, Constants.margin)
62+
63+
Button {
64+
//TODO: handle action
65+
} label: {
66+
Text(Localization.addDescriptionButton)
67+
.bodyStyle()
68+
}
69+
.buttonStyle(SecondaryButtonStyle())
70+
.padding(.horizontal, Constants.margin)
71+
.padding(.bottom, Constants.verticalSpacing)
72+
73+
Group {
74+
TitleAndValueRow(title: Localization.couponExpiryDate,
75+
value: .placeholder(Localization.couponExpiryDatePlaceholder),
76+
selectionStyle: .disclosure, action: { })
77+
Divider()
78+
.padding(.leading, Constants.margin)
79+
}
80+
81+
Group {
82+
TitleAndToggleRow(title: Localization.includeFreeShipping, isOn: .constant(false))
83+
.padding(.horizontal, Constants.margin)
84+
Divider()
85+
.padding(.leading, Constants.margin)
86+
}
87+
}
88+
89+
Group {
90+
ListHeaderView(text: Localization.headerApplyCouponTo.uppercased(), alignment: .left)
91+
92+
// TODO: add a new style with the icon on the left side
93+
Button {
94+
//TODO: handle action
95+
} label: {
96+
Text(Localization.editProductsButton)
97+
.bodyStyle()
98+
}
99+
.buttonStyle(SecondaryButtonStyle())
100+
.padding(.horizontal, Constants.margin)
101+
102+
// TODO: add a new style with the icon on the left side
103+
Button {
104+
//TODO: handle action
105+
} label: {
106+
Text(Localization.editProductCategoriesButton)
107+
.bodyStyle()
108+
}
109+
.buttonStyle(SecondaryButtonStyle())
110+
.padding(.horizontal, Constants.margin)
111+
}
112+
113+
Group {
114+
ListHeaderView(text: Localization.headerUsageDetails.uppercased(), alignment: .left)
115+
116+
TitleAndValueRow(title: Localization.usageRestrictions,
117+
value: .placeholder(""),
118+
selectionStyle: .disclosure, action: { })
119+
Divider()
120+
.padding(.leading, Constants.margin)
121+
}
122+
123+
Button {
124+
//TODO: handle action
125+
} label: {
126+
Text(Localization.saveButton)
127+
}
128+
.buttonStyle(PrimaryButtonStyle())
129+
.padding(.horizontal, Constants.margin)
130+
.padding(.top, Constants.verticalSpacing)
131+
}
132+
}
133+
.ignoresSafeArea(.container, edges: [.horizontal, .bottom])
134+
}
135+
.toolbar {
136+
ToolbarItem(placement: .cancellationAction) {
137+
Button(Localization.cancelButton, action: {
138+
presentation.wrappedValue.dismiss()
139+
})
140+
}
141+
}
142+
.navigationTitle(viewModel.title)
143+
.navigationBarTitleDisplayMode(.large)
144+
.wooNavigationBarStyle()
28145
}
29-
.navigationTitle(viewModel.title)
30-
.wooNavigationBarStyle()
31146
}
32147
}
33148

34149
// MARK: - Constants
35150
//
36151
private extension AddEditCoupon {
152+
153+
enum Constants {
154+
static let margin: CGFloat = 16
155+
static let verticalSpacing: CGFloat = 8
156+
}
157+
37158
enum Localization {
38-
static let titleEditPercentageDiscount = NSLocalizedString(
159+
static let cancelButton = NSLocalizedString(
39160
"Cancel",
40161
comment: "Cancel button in the navigation bar of the view for adding or editing a coupon.")
162+
static let headerCouponDetails = NSLocalizedString(
163+
"Coupon details",
164+
comment: "Header of the coupon details in the view for adding or editing a coupon.")
165+
static let couponAmountPercentage = NSLocalizedString(
166+
"Amount (%)",
167+
comment: "Text field Amount in percentage in the view for adding or editing a coupon.")
168+
static let footerCouponAmountPercentage = NSLocalizedString(
169+
"Set the percentage of the discount you want to offer.",
170+
comment: "The footer of the text field Amount in percentage in the view for adding or editing a coupon.")
171+
static let couponCode = NSLocalizedString(
172+
"Coupon Code",
173+
comment: "Text field coupon code in the view for adding or editing a coupon.")
174+
static let footerCouponCode = NSLocalizedString(
175+
"Customers need to enter this code to use the coupon.",
176+
comment: "The footer of the text field coupon code in the view for adding or editing a coupon.")
177+
static let regenerateCouponCodeButton = NSLocalizedString(
178+
"Regenerate Coupon Code",
179+
comment: "Button in the view for adding or editing a coupon.")
180+
static let addDescriptionButton = NSLocalizedString(
181+
"+ Add Description (Optional)",
182+
comment: "Button for adding a description to a coupon in the view for adding or editing a coupon.")
183+
static let couponExpiryDate = NSLocalizedString(
184+
"Coupon Expiry Date",
185+
comment: "Field in the view for adding or editing a coupon.")
186+
static let couponExpiryDatePlaceholder = NSLocalizedString(
187+
"None",
188+
comment: "Coupon expiry date placeholder in the view for adding or editing a coupon")
189+
static let includeFreeShipping = NSLocalizedString(
190+
"Include Free Shipping?",
191+
comment: "Toggle field in the view for adding or editing a coupon.")
192+
static let headerApplyCouponTo = NSLocalizedString(
193+
"Apply this coupon to",
194+
comment: "Header of the section for applying a coupon to specific products or categories in the view for adding or editing a coupon.")
195+
static let editProductsButton = NSLocalizedString(
196+
"Edit Products",
197+
comment: "Button for specify the products where a coupon can be applied in the view for adding or editing a coupon.")
198+
static let editProductCategoriesButton = NSLocalizedString(
199+
"Edit Product Categories",
200+
comment: "Button for specify the product categories where a coupon can be applied in the view for adding or editing a coupon.")
201+
static let headerUsageDetails = NSLocalizedString(
202+
"Usage Details",
203+
comment: "Header of the section usage details in the view for adding or editing a coupon.")
204+
static let usageRestrictions = NSLocalizedString(
205+
"Usage Restrictions",
206+
comment: "Field in the view for adding or editing a coupon.")
207+
static let saveButton = NSLocalizedString("Save", comment: "Action for saving a Coupon remotely")
41208
}
42209
}
43210

WooCommerce/Classes/ViewRelated/Coupons/Add and Edit Coupons/AddEditCouponViewModel.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,16 @@ final class AddEditCouponViewModel: ObservableObject {
2222
}
2323
}
2424

25-
@Published private(set) var coupon: Coupon?
25+
private var coupon: Coupon? {
26+
didSet {
27+
amountField = coupon?.amount ?? ""
28+
codeField = coupon?.code ?? ""
29+
}
30+
}
31+
32+
// Fields
33+
@Published var amountField = String()
34+
@Published var codeField = String()
2635

2736
/// Init method for coupon creation
2837
///

WooCommerce/Classes/ViewRelated/Products/Edit Product/Reviews/ProductReviewsViewController.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ final class ProductReviewsViewController: UIViewController, GhostableViewControl
99
private let viewModel: ProductReviewsViewModel
1010

1111
lazy var ghostTableViewController = GhostTableViewController(options: GhostTableViewOptions(cellClass: ProductReviewTableViewCell.self,
12-
estimatedRowHeight: ProductReviewsDataSource
12+
estimatedRowHeight: DefaultReviewsDataSource
1313
.Settings
1414
.estimatedRowHeight))
1515

@@ -60,7 +60,9 @@ final class ProductReviewsViewController: UIViewController, GhostableViewControl
6060
// MARK: - View Lifecycle
6161
init(product: Product) {
6262
self.product = product
63-
viewModel = ProductReviewsViewModel(siteID: product.siteID, data: ProductReviewsDataSource(product: product))
63+
viewModel = ProductReviewsViewModel(siteID: product.siteID,
64+
data: DefaultReviewsDataSource(siteID: product.siteID,
65+
customizer: ProductReviewsDataSourceCustomizer(product: product)))
6466
super.init(nibName: type(of: self).nibName, bundle: nil)
6567
}
6668

WooCommerce/Classes/ViewRelated/Products/Edit Product/Reviews/ProductReviewsViewModel.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,21 @@ private extension ProductReviewsViewModel {
9090
static let pageSize = 25
9191
}
9292
}
93+
94+
/// Customizes the `ReviewsDataSource` for a product related reviews screen (only the reviews of the passed product)
95+
final class ProductReviewsDataSourceCustomizer: ReviewsDataSourceCustomizing {
96+
let shouldShowProductTitleOnCells = false
97+
private let product: Product
98+
99+
init(product: Product) {
100+
self.product = product
101+
}
102+
103+
func reviewsFilterPredicate(with sitePredicate: NSPredicate) -> NSPredicate {
104+
let statusPredicate = NSPredicate(format: "statusKey ==[c] %@ AND productID == %lld",
105+
ProductReviewStatus.approved.rawValue,
106+
product.productID)
107+
108+
return NSCompoundPredicate(andPredicateWithSubpredicates: [sitePredicate, statusPredicate])
109+
}
110+
}

WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/TitleAndTextFieldRow.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ struct TitleAndTextFieldRow: View {
99
private let keyboardType: UIKeyboardType
1010
private let onEditingChanged: ((Bool) -> Void)?
1111
private let editable: Bool
12+
private let fieldAlignment: TextAlignment
1213

1314
@Binding private var text: String
1415

@@ -17,13 +18,15 @@ struct TitleAndTextFieldRow: View {
1718
text: Binding<String>,
1819
symbol: String? = nil,
1920
editable: Bool = true,
21+
fieldAlignment: TextAlignment = .trailing,
2022
keyboardType: UIKeyboardType = .default,
2123
onEditingChanged: ((Bool) -> Void)? = nil) {
2224
self.title = title
2325
self.placeholder = placeholder
2426
self._text = text
2527
self.symbol = symbol
2628
self.editable = editable
29+
self.fieldAlignment = fieldAlignment
2730
self.keyboardType = keyboardType
2831
self.onEditingChanged = onEditingChanged
2932
}
@@ -36,7 +39,7 @@ struct TitleAndTextFieldRow: View {
3639
.fixedSize()
3740
HStack {
3841
TextField(placeholder, text: $text, onEditingChanged: onEditingChanged ?? { _ in })
39-
.multilineTextAlignment(.trailing)
42+
.multilineTextAlignment(fieldAlignment)
4043
.font(.body)
4144
.keyboardType(keyboardType)
4245
.disabled(!editable)
@@ -101,5 +104,14 @@ struct TitleAndTextFieldRow_Previews: PreviewProvider {
101104
.environment(\.sizeCategory, .accessibilityExtraLarge)
102105
.previewLayout(.fixed(width: 375, height: 150))
103106
.previewDisplayName("Dynamic Type: Large Font Size with symbol")
107+
108+
TitleAndTextFieldRow(title: "Total package weight",
109+
placeholder: "Value",
110+
text: .constant(""),
111+
symbol: "oz",
112+
fieldAlignment: .leading,
113+
keyboardType: .default)
114+
.previewLayout(.fixed(width: 375, height: 150))
115+
.previewDisplayName("With leading alignment")
104116
}
105117
}

WooCommerce/Classes/ViewRelated/Reviews/DefaultReviewsDataSource.swift

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ final class DefaultReviewsDataSource: NSObject, ReviewsDataSource {
1212

1313
private let siteID: Int64
1414

15+
/// Adds an extra layer of logic customization depending on the case (e.g only reviews of a specific product, global site reviews...)
16+
private let customizer: ReviewsDataSourceCustomizing
17+
1518
/// Product Reviews
1619
///
1720
private lazy var reviewsResultsController: ResultsController<StorageProductReview> = {
@@ -20,7 +23,7 @@ final class DefaultReviewsDataSource: NSObject, ReviewsDataSource {
2023

2124
return ResultsController<StorageProductReview>(storageManager: storageManager,
2225
sectionNameKeyPath: "normalizedAgeAsString",
23-
matching: filterPredicate(),
26+
matching: customizer.reviewsFilterPredicate(with: sitePredicate()),
2427
sortedBy: [descriptor])
2528
}()
2629

@@ -78,9 +81,9 @@ final class DefaultReviewsDataSource: NSObject, ReviewsDataSource {
7881
return reviewsResultsController.numberOfObjects
7982
}
8083

81-
82-
init(siteID: Int64) {
84+
init(siteID: Int64, customizer: ReviewsDataSourceCustomizing) {
8385
self.siteID = siteID
86+
self.customizer = customizer
8487
super.init()
8588
observeResults()
8689
}
@@ -131,8 +134,9 @@ final class DefaultReviewsDataSource: NSObject, ReviewsDataSource {
131134
}
132135

133136
func refreshDataObservers() {
134-
reviewsResultsController.predicate = filterPredicate()
135-
productsResultsController.predicate = sitePredicate()
137+
let sitePredicate = sitePredicate()
138+
reviewsResultsController.predicate = customizer.reviewsFilterPredicate(with: sitePredicate)
139+
productsResultsController.predicate = sitePredicate
136140
}
137141
}
138142

@@ -179,7 +183,7 @@ private extension DefaultReviewsDataSource {
179183
let reviewProduct = product(id: review.productID)
180184
let note = notification(id: review.reviewID)
181185

182-
return ReviewViewModel(review: review, product: reviewProduct, notification: note)
186+
return ReviewViewModel(showProductTitle: customizer.shouldShowProductTitleOnCells, review: review, product: reviewProduct, notification: note)
183187
}
184188

185189
private func product(id productID: Int64) -> Product? {
@@ -237,7 +241,7 @@ extension DefaultReviewsDataSource: ReviewsInteractionDelegate {
237241
}
238242

239243

240-
private extension DefaultReviewsDataSource {
244+
extension DefaultReviewsDataSource {
241245
enum Settings {
242246
static let estimatedRowHeight = CGFloat(88)
243247
}

0 commit comments

Comments
 (0)