Skip to content

[Min/Max Quantities Edit Support] UI + Sync Logic #12758

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Networking/Networking.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,7 @@
B918DDF12A3C960000E74DE5 /* products-sku-search-variation.json in Resources */ = {isa = PBXBuildFile; fileRef = B918DDF02A3C960000E74DE5 /* products-sku-search-variation.json */; };
B93E032C2A96112A009CA9C1 /* setting-tax-based-on-shipping-success.json in Resources */ = {isa = PBXBuildFile; fileRef = B93E032B2A96112A009CA9C1 /* setting-tax-based-on-shipping-success.json */; };
B93E032E2A9613CB009CA9C1 /* setting-tax-based-on-parse-error.json in Resources */ = {isa = PBXBuildFile; fileRef = B93E032D2A9613CA009CA9C1 /* setting-tax-based-on-parse-error.json */; };
B96158FC2BF63B4F0080E52A /* String+MinMaxQuantities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B96158FB2BF63B4F0080E52A /* String+MinMaxQuantities.swift */; };
B963A5CC2853870000EFADA0 /* OrderItemRefundMetaData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B963A5CB2853870000EFADA0 /* OrderItemRefundMetaData.swift */; };
B990FA922AA1EBC600496375 /* TaxRate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B990FA912AA1EBC600496375 /* TaxRate.swift */; };
B9CA4F332AB213DF00285AB9 /* tax.json in Resources */ = {isa = PBXBuildFile; fileRef = B9CA4F322AB213DF00285AB9 /* tax.json */; };
Expand Down Expand Up @@ -1695,6 +1696,7 @@
B918DDF02A3C960000E74DE5 /* products-sku-search-variation.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "products-sku-search-variation.json"; sourceTree = "<group>"; };
B93E032B2A96112A009CA9C1 /* setting-tax-based-on-shipping-success.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "setting-tax-based-on-shipping-success.json"; sourceTree = "<group>"; };
B93E032D2A9613CA009CA9C1 /* setting-tax-based-on-parse-error.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "setting-tax-based-on-parse-error.json"; sourceTree = "<group>"; };
B96158FB2BF63B4F0080E52A /* String+MinMaxQuantities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+MinMaxQuantities.swift"; sourceTree = "<group>"; };
B963A5CB2853870000EFADA0 /* OrderItemRefundMetaData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderItemRefundMetaData.swift; sourceTree = "<group>"; };
B990FA912AA1EBC600496375 /* TaxRate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaxRate.swift; sourceTree = "<group>"; };
B9CA4F322AB213DF00285AB9 /* tax.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tax.json; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2368,6 +2370,7 @@
7452387021124B7700A973CD /* AnyCodable.swift */,
7452387121124B7700A973CD /* AnyDecodable.swift */,
7452386F21124B7700A973CD /* AnyEncodable.swift */,
B96158FB2BF63B4F0080E52A /* String+MinMaxQuantities.swift */,
);
name = Tools;
path = Network/Tools;
Expand Down Expand Up @@ -4669,6 +4672,7 @@
CE43066A23465F340073CBFF /* Refund.swift in Sources */,
68CB800C28D87BC800E169F8 /* Customer.swift in Sources */,
EE1CB90A2B4BC8C500AD24D5 /* CreateBlazeCampaignMapper.swift in Sources */,
B96158FC2BF63B4F0080E52A /* String+MinMaxQuantities.swift in Sources */,
DE50295D28C6068B00551736 /* JetpackUserMapper.swift in Sources */,
B524194121AC60A700D6FC0A /* DotcomDevice.swift in Sources */,
D8EDFE2225EE88C9003D2213 /* ReaderConnectionToken.swift in Sources */,
Expand Down
11 changes: 8 additions & 3 deletions Networking/Networking/Model/Product/Product.swift
Original file line number Diff line number Diff line change
Expand Up @@ -328,9 +328,9 @@ public struct Product: Codable, GeneratedCopiable, Equatable, GeneratedFakeable
self.bundledItems = bundledItems
self.compositeComponents = compositeComponents
self.subscription = subscription
self.minAllowedQuantity = minAllowedQuantity
self.minAllowedQuantity = minAllowedQuantity.refinedMinMaxQuantityEmptyValue
self.groupOfQuantity = groupOfQuantity.refinedMinMaxQuantityEmptyValue
self.maxAllowedQuantity = maxAllowedQuantity
self.groupOfQuantity = groupOfQuantity
self.combineVariationQuantities = combineVariationQuantities
}

Expand Down Expand Up @@ -722,6 +722,12 @@ public struct Product: Codable, GeneratedCopiable, Equatable, GeneratedFakeable
try container.encode(upsellIDs, forKey: .upsellIDs)
try container.encode(crossSellIDs, forKey: .crossSellIDs)

// Quantity Rules
// https://woocommerce.com/document/minmax-quantities/#section-6
try container.encode(maxAllowedQuantity, forKey: .maxAllowedQuantity)
try container.encode(minAllowedQuantity, forKey: .minAllowedQuantity)
try container.encode(groupOfQuantity, forKey: .groupOfQuantity)

// Attributes
try container.encode(attributes, forKey: .attributes)

Expand All @@ -740,7 +746,6 @@ public struct Product: Codable, GeneratedCopiable, Equatable, GeneratedFakeable
}
}


/// Defines all of the Product CodingKeys
///
private extension Product {
Expand Down
10 changes: 8 additions & 2 deletions Networking/Networking/Model/Product/ProductVariation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,9 @@ public struct ProductVariation: Codable, GeneratedCopiable, Equatable, Generated
self.shippingClassID = shippingClassID
self.menuOrder = menuOrder
self.subscription = subscription
self.minAllowedQuantity = minAllowedQuantity
self.minAllowedQuantity = minAllowedQuantity.refinedMinMaxQuantityEmptyValue
self.groupOfQuantity = groupOfQuantity.refinedMinMaxQuantityEmptyValue
self.maxAllowedQuantity = maxAllowedQuantity
self.groupOfQuantity = groupOfQuantity
self.overrideProductQuantities = overrideProductQuantities
}

Expand Down Expand Up @@ -408,6 +408,12 @@ public struct ProductVariation: Codable, GeneratedCopiable, Equatable, Generated
// Variation (Local) Attributes
try container.encode(attributes, forKey: .attributes)

// Quantity Rules
// https://woocommerce.com/document/minmax-quantities/#section-6
try container.encode(maxAllowedQuantity, forKey: .maxAllowedQuantity)
try container.encode(minAllowedQuantity, forKey: .minAllowedQuantity)
try container.encode(groupOfQuantity, forKey: .groupOfQuantity)

// Metadata
let metaDataValuePairs = buildMetaDataValuePairs()
if metaDataValuePairs.isEmpty == false {
Expand Down
14 changes: 14 additions & 0 deletions Networking/Networking/Network/Tools/String+MinMaxQuantities.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Foundation

/// Min/Max Quantities. Sync values to API requirements
/// https://woocommerce.com/document/minmax-quantities/#section-6
///
extension Optional where Wrapped == String {
var refinedMinMaxQuantityEmptyValue: String? {
guard let self = self else {
return nil
}

return self.isEmpty ? "0" : self
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -658,7 +658,7 @@ private extension DefaultProductFormTableViewModel {
quantityDetails.append(groupOfDescription)
}

let details = quantityDetails.isEmpty ? nil : quantityDetails.joined(separator: "\n")
let details = quantityDetails.isEmpty ? Localization.emptyQuantityRules : quantityDetails.joined(separator: "\n")

return ProductFormSection.SettingsRow.ViewModel(icon: icon,
title: title,
Expand Down Expand Up @@ -990,4 +990,7 @@ private extension DefaultProductFormTableViewModel.Localization {
comment: "Format of the Maximum Quantity setting (with a numeric quantity) on the Quantity Rules row")
static let groupOfFormat = NSLocalizedString("Group of: %@",
comment: "Format of the Group Of setting (with a numeric quantity) on the Quantity Rules row")
static let emptyQuantityRules = NSLocalizedString("productForm.quantityRules.placeholder",
value: "No Quantity Rules",
comment: "Placeholder for empty product ratings")
}
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,8 @@ extension EditableProductModel: ProductFormDataModel, TaxClassRequestable {
product.subscription
}

var hasQuantityRules: Bool {
let hasNoRules = minAllowedQuantity.isNilOrEmptyOrZero && maxAllowedQuantity.isNilOrEmptyOrZero && groupOfQuantity.isNilOrEmptyOrZero
return !hasNoRules
var canEditQuantityRules: Bool {
minAllowedQuantity != nil || maxAllowedQuantity != nil || groupOfQuantity != nil
}

var minAllowedQuantity: String? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,10 +214,11 @@ extension EditableProductVariationModel: ProductFormDataModel, TaxClassRequestab
productVariation.subscription
}

var hasQuantityRules: Bool {
var canEditQuantityRules: Bool {
let quantityRulesAreSet = minAllowedQuantity != nil || maxAllowedQuantity != nil || groupOfQuantity != nil
let enabled = productVariation.overrideProductQuantities == true && parentProductDisablesQuantityRules == false
let hasNoRules = minAllowedQuantity.isNilOrEmptyOrZero && maxAllowedQuantity.isNilOrEmptyOrZero && groupOfQuantity.isNilOrEmptyOrZero
return enabled && !hasNoRules

return enabled && quantityRulesAreSet
}

var minAllowedQuantity: String? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ extension ProductUpdateError: LocalizedError {
case .invalidSKU:
return NSLocalizedString("This SKU is used on another product or is invalid.",
comment: "The message of the alert when there is an error updating the product SKU")
case .generic(let message):
return message
case .unknown:
return NSLocalizedString("Unexpected error", comment: "The message of the alert when there is an unexpected error updating the product")
case .variationInvalidImageId:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ private extension ProductFormActionsFactory {
let canEditProductType = editable
let shouldShowShippingSettingsRow = product.isShippingEnabled()
let canEditInventorySettingsRow = editable && product.hasIntegerStockQuantity
let shouldShowQuantityRulesRow = isMinMaxQuantitiesEnabled && product.hasQuantityRules
let shouldShowQuantityRulesRow = isMinMaxQuantitiesEnabled && product.canEditQuantityRules

let actions: [ProductFormEditAction?] = [
.priceSettings(editable: editable, hideSeparator: false),
Expand All @@ -186,7 +186,7 @@ private extension ProductFormActionsFactory {
let shouldShowExternalURLRow = editable || product.product.externalURL?.isNotEmpty == true
let shouldShowSKURow = editable || product.sku?.isNotEmpty == true
let canEditProductType = editable
let shouldShowQuantityRulesRow = isMinMaxQuantitiesEnabled && product.hasQuantityRules
let shouldShowQuantityRulesRow = isMinMaxQuantitiesEnabled && product.canEditQuantityRules

let actions: [ProductFormEditAction?] = [
.priceSettings(editable: editable, hideSeparator: false),
Expand All @@ -208,7 +208,7 @@ private extension ProductFormActionsFactory {
let shouldShowReviewsRow = product.reviewsAllowed
let shouldShowSKURow = editable || product.sku?.isNotEmpty == true
let canEditProductType = editable
let shouldShowQuantityRulesRow = isMinMaxQuantitiesEnabled && product.hasQuantityRules
let shouldShowQuantityRulesRow = isMinMaxQuantitiesEnabled && product.canEditQuantityRules

let actions: [ProductFormEditAction?] = [
.groupedProducts(editable: editable),
Expand All @@ -235,7 +235,7 @@ private extension ProductFormActionsFactory {
let productHasNoPriceSet = variationsPrice == .unknown && product.product.variations.isNotEmpty && product.product.price.isEmpty
return canEditProductType && (variationsHaveNoPriceSet || productHasNoPriceSet)
}()
let shouldShowQuantityRulesRow = isMinMaxQuantitiesEnabled && product.hasQuantityRules
let shouldShowQuantityRulesRow = isMinMaxQuantitiesEnabled && product.canEditQuantityRules

let actions: [ProductFormEditAction?] = [
.variations(hideSeparator: shouldShowNoPriceWarningRow),
Expand All @@ -260,7 +260,7 @@ private extension ProductFormActionsFactory {
let canOpenBundledProducts = product.bundledItems.isNotEmpty
let shouldShowPriceSettingsRow = product.regularPrice.isNilOrEmpty == false
let shouldShowReviewsRow = product.reviewsAllowed
let shouldShowQuantityRulesRow = isMinMaxQuantitiesEnabled && product.hasQuantityRules
let shouldShowQuantityRulesRow = isMinMaxQuantitiesEnabled && product.canEditQuantityRules

let actions: [ProductFormEditAction?] = [
shouldShowBundledProductsRow ? .bundledProducts(actionable: canOpenBundledProducts) : nil,
Expand All @@ -283,7 +283,7 @@ private extension ProductFormActionsFactory {
let canOpenComponents = product.compositeComponents.isNotEmpty
let shouldShowPriceSettingsRow = product.regularPrice.isNilOrEmpty == false
let shouldShowReviewsRow = product.reviewsAllowed
let shouldShowQuantityRulesRow = isMinMaxQuantitiesEnabled && product.hasQuantityRules
let shouldShowQuantityRulesRow = isMinMaxQuantitiesEnabled && product.canEditQuantityRules

let actions: [ProductFormEditAction?] = [
shouldShowComponentsRow ? .components(actionable: canOpenComponents) : nil,
Expand All @@ -303,7 +303,7 @@ private extension ProductFormActionsFactory {

func allSettingsSectionActionsForSubscriptionProduct() -> [ProductFormEditAction] {
let shouldShowReviewsRow = product.reviewsAllowed
let shouldShowQuantityRulesRow = isMinMaxQuantitiesEnabled && product.hasQuantityRules
let shouldShowQuantityRulesRow = isMinMaxQuantitiesEnabled && product.canEditQuantityRules
let canEditInventorySettingsRow = editable && product.hasIntegerStockQuantity
let canEditProductType = editable
let shouldShowShippingSettingsRow = product.isShippingEnabled()
Expand All @@ -330,7 +330,7 @@ private extension ProductFormActionsFactory {
func allSettingsSectionActionsForVariableSubscriptionProduct() -> [ProductFormEditAction] {
let shouldShowReviewsRow = product.reviewsAllowed
let shouldShowAttributesRow = product.product.attributesForVariations.isNotEmpty
let shouldShowQuantityRulesRow = isMinMaxQuantitiesEnabled && product.hasQuantityRules
let shouldShowQuantityRulesRow = isMinMaxQuantitiesEnabled && product.canEditQuantityRules

let actions: [ProductFormEditAction?] = {
let canEditProductType = editable
Expand Down Expand Up @@ -364,7 +364,7 @@ private extension ProductFormActionsFactory {
func allSettingsSectionActionsForNonCoreProduct() -> [ProductFormEditAction] {
let shouldShowPriceSettingsRow = product.regularPrice.isNilOrEmpty == false
let shouldShowReviewsRow = product.reviewsAllowed
let shouldShowQuantityRulesRow = isMinMaxQuantitiesEnabled && product.hasQuantityRules
let shouldShowQuantityRulesRow = isMinMaxQuantitiesEnabled && product.canEditQuantityRules

let actions: [ProductFormEditAction?] = [
shouldShowPriceSettingsRow ? .priceSettings(editable: false, hideSeparator: false): nil,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ protocol ProductFormDataModel {
var subscription: ProductSubscription? { get }

// Quantity Rules (Min/Max Quantities extension)
var hasQuantityRules: Bool { get }
var canEditQuantityRules: Bool { get }
var minAllowedQuantity: String? { get }
var maxAllowedQuantity: String? { get }
var groupOfQuantity: String? { get }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1996,8 +1996,11 @@ private extension ProductFormViewController {
//
private extension ProductFormViewController {
func showQuantityRules() {
let viewModel = QuantityRulesViewModel(product: product)
let viewController = QuantityRulesViewController(viewModel: viewModel)
let quantityRulesViewModel = QuantityRulesViewModel(product: product) { [weak self] minQuantity, maxQuantity, groupOf in
self?.navigationController?.popViewController(animated: true)
self?.viewModel.updateQuantityRules(minQuantity: minQuantity, maxQuantity: maxQuantity, groupOf: groupOf)
}
let viewController = QuantityRulesViewController(viewModel: quantityRulesViewModel)
show(viewController, sender: self)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,10 @@ extension ProductFormViewModel {
let subscription = product.subscription?.copy(length: length)
product = EditableProductModel(product: product.product.copy(subscription: subscription))
}

func updateQuantityRules(minQuantity: String, maxQuantity: String, groupOf: String) {
product = EditableProductModel(product: product.product.copy(minAllowedQuantity: minQuantity, maxAllowedQuantity: maxQuantity, groupOfQuantity: groupOf))
}
}

// MARK: Remote actions
Expand Down Expand Up @@ -673,7 +677,7 @@ extension ProductFormViewModel {
extension ProductFormViewModel {
func trackProductFormLoaded() {
let hasLinkedProducts = product.upsellIDs.isNotEmpty || product.crossSellIDs.isNotEmpty
let hasMinMaxQuantityRules = product.hasQuantityRules
let hasMinMaxQuantityRules = product.canEditQuantityRules
analytics.track(event: .ProductDetail.loaded(hasLinkedProducts: hasLinkedProducts,
hasMinMaxQuantityRules: hasMinMaxQuantityRules,
horizontalSizeClass: UITraitCollection.current.horizontalSizeClass))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ protocol ProductFormViewModelProtocol {

func updateSubscriptionExpirySettings(length: String)

func updateQuantityRules(minQuantity: String, maxQuantity: String, groupOf: String)

// Remote action

/// Creates/updates a product remotely given an optional product status to override.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ private extension ProductVariationFormActionsFactory {
return nil
}
}()
let shouldShowQuantityRulesRow = isMinMaxQuantitiesEnabled && productVariation.hasQuantityRules
let shouldShowQuantityRulesRow = isMinMaxQuantitiesEnabled && productVariation.canEditQuantityRules

let actions: [ProductFormEditAction?] = [
subscriptionOrPriceRow,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,16 @@ extension ProductVariationFormViewModel {
parentProductSKU: parentProductSKU,
parentProductDisablesQuantityRules: parentProductDisablesQuantityRules)
}

func updateQuantityRules(minQuantity: String, maxQuantity: String, groupOf: String) {
productVariation = EditableProductVariationModel(productVariation: productVariation.productVariation.copy(minAllowedQuantity: minQuantity,
maxAllowedQuantity: maxQuantity,
groupOfQuantity: groupOf),
parentProductType: productVariation.productType,
allAttributes: allAttributes,
parentProductSKU: parentProductSKU,
parentProductDisablesQuantityRules: parentProductDisablesQuantityRules)
}
}

// MARK: Remote actions
Expand Down
Loading