Skip to content

Commit 8754f6d

Browse files
authored
Merge pull request #4350 from woocommerce/issue/4312-show-attributes-row
2 parents 54306b9 + 5565c8a commit 8754f6d

File tree

9 files changed

+143
-132
lines changed

9 files changed

+143
-132
lines changed

Networking/Networking/Model/Copiable/Models+Copiable.generated.swift

+30
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,36 @@ extension ProductAddOnOption {
391391
}
392392
}
393393

394+
extension ProductAttribute {
395+
public func copy(
396+
siteID: CopiableProp<Int64> = .copy,
397+
attributeID: CopiableProp<Int64> = .copy,
398+
name: CopiableProp<String> = .copy,
399+
position: CopiableProp<Int> = .copy,
400+
visible: CopiableProp<Bool> = .copy,
401+
variation: CopiableProp<Bool> = .copy,
402+
options: CopiableProp<[String]> = .copy
403+
) -> ProductAttribute {
404+
let siteID = siteID ?? self.siteID
405+
let attributeID = attributeID ?? self.attributeID
406+
let name = name ?? self.name
407+
let position = position ?? self.position
408+
let visible = visible ?? self.visible
409+
let variation = variation ?? self.variation
410+
let options = options ?? self.options
411+
412+
return ProductAttribute(
413+
siteID: siteID,
414+
attributeID: attributeID,
415+
name: name,
416+
position: position,
417+
visible: visible,
418+
variation: variation,
419+
options: options
420+
)
421+
}
422+
}
423+
394424
extension ProductImage {
395425
public func copy(
396426
imageID: CopiableProp<Int64> = .copy,

Networking/Networking/Model/Product/ProductAttribute.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Codegen
33

44
/// Represents a ProductAttribute entity.
55
///
6-
public struct ProductAttribute: Codable, GeneratedFakeable {
6+
public struct ProductAttribute: Codable, GeneratedFakeable, GeneratedCopiable {
77
public let siteID: Int64
88
public let attributeID: Int64
99
public let name: String

WooCommerce/Classes/ViewRelated/Products/Edit Product/DefaultProductFormTableViewModel.swift

+50-17
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ private extension DefaultProductFormTableViewModel {
9292
return .linkedProducts(viewModel: linkedProductsRow(product: product, isEditable: editable), isEditable: editable)
9393
case .noPriceWarning:
9494
return .noPriceWarning(viewModel: noPriceWarningRow())
95+
case .attributes(let editable):
96+
return .attributes(viewModel: productVariationsAttributesRow(product: product.product, isEditable: editable), isEditable: editable)
9597
default:
9698
assertionFailure("Unexpected action in the settings section: \(action)")
9799
return nil
@@ -382,23 +384,8 @@ private extension DefaultProductFormTableViewModel {
382384
func variationsRow(product: Product) -> ProductFormSection.SettingsRow.ViewModel {
383385
let icon = UIImage.variationsImage
384386
let title = product.variations.isEmpty ? Localization.addVariationsTitle : Localization.variationsTitle
385-
386-
let details: String
387-
let format = NSLocalizedString("%1$@ (%2$ld options)", comment: "Format for each Product attribute")
388-
389-
switch product.variations.count {
390-
case 1...:
391-
details = product.attributesForVariations
392-
.map({ String.localizedStringWithFormat(format, $0.name, $0.options.count) })
393-
.joined(separator: "\n")
394-
default:
395-
details = ""
396-
}
397-
398-
return ProductFormSection.SettingsRow.ViewModel(icon: icon,
399-
title: title,
400-
details: details,
401-
isActionable: true)
387+
let details = Localization.variationsDetail(count: product.variations.count)
388+
return ProductFormSection.SettingsRow.ViewModel(icon: icon, title: title, details: details, isActionable: true)
402389
}
403390

404391
// MARK: Product variation only
@@ -420,6 +407,20 @@ private extension DefaultProductFormTableViewModel {
420407
return ProductFormSection.SettingsRow.WarningViewModel(icon: icon, title: title)
421408
}
422409

410+
func productVariationsAttributesRow(product: Product, isEditable: Bool) -> ProductFormSection.SettingsRow.ViewModel {
411+
let icon = UIImage.customizeImage
412+
let title = Localization.productVariationAttributesTitle
413+
414+
let details = product.attributesForVariations
415+
.map {
416+
let format = Localization.variationAttributesDetailFormat(optionCount: $0.options.count)
417+
return String.localizedStringWithFormat(format, $0.name, $0.options.count)
418+
}
419+
.joined(separator: "\n")
420+
421+
return ProductFormSection.SettingsRow.ViewModel(icon: icon, title: title, details: details, isActionable: isEditable)
422+
}
423+
423424
func variationAttributesRow(productVariation: EditableProductVariationModel, isEditable: Bool) -> ProductFormSection.SettingsRow.ViewModel {
424425
let icon = UIImage.customizeImage
425426
let title = Localization.variationAttributesTitle
@@ -569,14 +570,46 @@ private extension DefaultProductFormTableViewModel {
569570
NSLocalizedString("Variations",
570571
comment: "Title of the Product Variations row on Product main screen for a variable product")
571572

573+
static func variationsDetail(count: Int) -> String {
574+
let format: String = {
575+
switch count {
576+
case 0:
577+
return ""
578+
case 1:
579+
return NSLocalizedString("%1$ld variation",
580+
comment: "Format for the variations detail row in singular form. Reads, `1 variation`")
581+
default:
582+
return NSLocalizedString("%1$ld variations",
583+
comment: "Format for the variations detail row in plural form. Reads, `2 variations`")
584+
}
585+
}()
586+
587+
return String.localizedStringWithFormat(format, count)
588+
}
589+
572590
// Variation status
573591
static let variationStatusTitle =
574592
NSLocalizedString("Enabled",
575593
comment: "Title of the status row on Product Variation main screen to enable/disable a variation")
576594

595+
// Product Variations Attributes
596+
static let productVariationAttributesTitle = NSLocalizedString("Variations Attributes",
597+
comment: "Title of the variations attributes row on Product screen")
598+
577599
// Variation attributes
578600
static let variationAttributesTitle = NSLocalizedString("Attributes", comment: "Title of the attributes row on Product Variation main screen")
579601

602+
static func variationAttributesDetailFormat(optionCount: Int) -> String {
603+
switch optionCount {
604+
case 0:
605+
return ""
606+
case 1:
607+
return NSLocalizedString("%1$@ (%2$ld option)", comment: "Format for each Product attribute in singular form")
608+
default:
609+
return NSLocalizedString("%1$@ (%2$ld options)", comment: "Format for each Product attribute in plural form")
610+
}
611+
}
612+
580613
// No price warning row
581614
static let noPriceWarningTitle =
582615
NSLocalizedString("Variations without price won’t be shown in your store",

WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormActionsFactory.swift

+5
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ private extension ProductFormActionsFactory {
168168
let shouldShowReviewsRow = product.reviewsAllowed
169169
let canEditProductType = formType != .add && editable
170170
let canEditInventorySettingsRow = editable && product.hasIntegerStockQuantity
171+
let shouldShowAttributesRow = product.product.attributesForVariations.isNotEmpty
171172
let shouldShowNoPriceWarningRow: Bool = {
172173
let variationsHaveNoPriceSet = variationsPrice == .notSet
173174
let productHasNoPriceSet = variationsPrice == .unknown && product.product.variations.isNotEmpty && product.product.price.isEmpty
@@ -177,6 +178,7 @@ private extension ProductFormActionsFactory {
177178
let actions: [ProductFormEditAction?] = [
178179
.variations,
179180
shouldShowNoPriceWarningRow ? .noPriceWarning : nil,
181+
shouldShowAttributesRow ? .attributes(editable: editable) : nil,
180182
shouldShowReviewsRow ? .reviews: nil,
181183
.shippingSettings(editable: editable),
182184
.inventorySettings(editable: canEditInventorySettingsRow),
@@ -265,6 +267,9 @@ private extension ProductFormActionsFactory {
265267
case .noPriceWarning:
266268
// Always visible when available
267269
return true
270+
case .attributes:
271+
// Always visible when available
272+
return true
268273
default:
269274
return false
270275
}

WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewController.swift

+41-2
Original file line numberDiff line numberDiff line change
@@ -370,8 +370,7 @@ final class ProductFormViewController<ViewModel: ProductFormViewModelProtocol>:
370370
guard isEditable else {
371371
return
372372
}
373-
editVariationAttributes()
374-
trackEditVariationAttributesRowTapped()
373+
editAttributes()
375374
case .status, .noPriceWarning:
376375
break
377376
}
@@ -781,6 +780,10 @@ private extension ProductFormViewController {
781780
ServiceLocator.analytics.track(event: WooAnalyticsEvent.Variations.editVariationAttributeOptionsRowTapped(productID: variation.productID,
782781
variationID: variation.productVariationID))
783782
}
783+
784+
func trackEditProductAttributeRowTapped() {
785+
ServiceLocator.analytics.track(event: WooAnalyticsEvent.Variations.editAttributesButtonTapped(productID: product.productID))
786+
}
784787
}
785788

786789
// MARK: Navigation Bar Items
@@ -1323,6 +1326,35 @@ private extension ProductFormViewController {
13231326
// MARK: Action - Edit Product Variation Attributes
13241327
//
13251328
private extension ProductFormViewController {
1329+
/// Edit the product attributes or the variation attributes depending on the product model type.
1330+
///
1331+
func editAttributes() {
1332+
switch product {
1333+
case is EditableProductModel:
1334+
editProductAttributes()
1335+
trackEditProductAttributeRowTapped()
1336+
case is EditableProductVariationModel:
1337+
editVariationAttributes()
1338+
trackEditVariationAttributesRowTapped()
1339+
default:
1340+
break
1341+
}
1342+
}
1343+
1344+
/// Navigate to edit product attributes
1345+
///
1346+
func editProductAttributes() {
1347+
guard let productModel = product as? EditableProductModel else {
1348+
return
1349+
}
1350+
let attributesViewModel = EditAttributesViewModel(product: productModel.product, allowVariationCreation: false)
1351+
let attributesViewController = EditAttributesViewController(viewModel: attributesViewModel)
1352+
attributesViewController.onAttributesUpdate = { [weak self] updatedProduct in
1353+
self?.onAttributeUpdated(attributesViewController: attributesViewController, updatedProduct: updatedProduct)
1354+
}
1355+
show(attributesViewController, sender: self)
1356+
}
1357+
13261358
func editVariationAttributes() {
13271359
guard let productVariationModel = product as? EditableProductVariationModel else {
13281360
return
@@ -1349,6 +1381,13 @@ private extension ProductFormViewController {
13491381
}
13501382
viewModel.updateVariationAttributes(attributes)
13511383
}
1384+
1385+
/// Perform necessary actions when an attribute is created or updated.
1386+
///
1387+
func onAttributeUpdated(attributesViewController: UIViewController, updatedProduct: Product) {
1388+
viewModel.updateProductVariations(from: updatedProduct)
1389+
navigationController?.popToViewController(attributesViewController, animated: true)
1390+
}
13521391
}
13531392

13541393
// MARK: Constants

WooCommerce/Classes/ViewRelated/Products/Variations/ProductVariationsViewController.swift

+1-54
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ final class ProductVariationsViewController: UIViewController {
8282
didSet {
8383
viewModel.updatedFormTypeIfNeeded(newProduct: product)
8484

85-
configureRightButtonItem()
8685
resetResultsController(oldProduct: oldValue)
8786
updateEmptyState()
8887
onProductUpdate?(product)
@@ -166,22 +165,6 @@ private extension ProductVariationsViewController {
166165
"Variations",
167166
comment: "Title that appears on top of the Product Variation List screen."
168167
)
169-
configureRightButtonItem()
170-
}
171-
172-
/// Configure right button item.
173-
///
174-
func configureRightButtonItem() {
175-
guard viewModel.shouldShowMoreButton(for: product) else {
176-
return navigationItem.rightBarButtonItem = nil
177-
}
178-
179-
let moreButton = UIBarButtonItem(image: .moreImage,
180-
style: .plain,
181-
target: self,
182-
action: #selector(presentMoreOptionsActionSheet(_:)))
183-
moreButton.accessibilityLabel = Localization.moreButtonLabel
184-
navigationItem.setRightBarButton(moreButton, animated: false)
185168
}
186169

187170
/// Apply Woo styles.
@@ -283,13 +266,8 @@ private extension ProductVariationsViewController {
283266

284267
addTopButton(title: Localization.generateVariationAction,
285268
insets: .init(top: 16, left: 16, bottom: 8, right: 16),
286-
actionSelector: #selector(addButtonTapped),
287-
stylingHandler: { $0.applyPrimaryButtonStyle() })
288-
289-
addTopButton(title: Localization.editAttributesAction,
290-
insets: .init(top: 8, left: 16, bottom: 16, right: 16),
291269
hasBottomBorder: true,
292-
actionSelector: #selector(editAttributesTapped),
270+
actionSelector: #selector(addButtonTapped),
293271
stylingHandler: { $0.applySecondaryButtonStyle() })
294272

295273
topStackView.addArrangedSubview(topBannerView)
@@ -563,37 +541,6 @@ private extension ProductVariationsViewController {
563541
analytics.track(event: WooAnalyticsEvent.Variations.addMoreVariationsButtonTapped(productID: product.productID))
564542
createVariation()
565543
}
566-
567-
@objc func editAttributesTapped() {
568-
navigateToEditAttributeViewController(allowVariationCreation: false)
569-
trackEditAttributesButtonPressed()
570-
}
571-
572-
func trackEditAttributesButtonPressed() {
573-
analytics.track(event: WooAnalyticsEvent.Variations.editAttributesButtonTapped(productID: product.productID))
574-
}
575-
}
576-
577-
// MARK: - Action sheet
578-
//
579-
private extension ProductVariationsViewController {
580-
@objc private func presentMoreOptionsActionSheet(_ sender: UIBarButtonItem) {
581-
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
582-
actionSheet.view.tintColor = .text
583-
584-
let editAttributesAction = UIAlertAction(title: Localization.editAttributesAction, style: .default) { [weak self] _ in
585-
self?.editAttributesTapped()
586-
}
587-
actionSheet.addAction(editAttributesAction)
588-
589-
let cancelAction = UIAlertAction(title: Localization.cancelAction, style: .cancel)
590-
actionSheet.addAction(cancelAction)
591-
592-
let popoverController = actionSheet.popoverPresentationController
593-
popoverController?.barButtonItem = sender
594-
595-
present(actionSheet, animated: true)
596-
}
597544
}
598545

599546
// MARK: - Placeholders

WooCommerce/Classes/ViewRelated/Products/Variations/ProductVariationsViewModel.swift

-6
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,6 @@ extension ProductVariationsViewModel {
4343
product.variations.isEmpty || product.attributesForVariations.isEmpty
4444
}
4545

46-
/// Defines if the More Options button should be shown
47-
///
48-
func shouldShowMoreButton(for product: Product) -> Bool {
49-
product.variations.isEmpty && product.attributesForVariations.isNotEmpty
50-
}
51-
5246
/// Defines if empty state screen should show guide for creating attributes
5347
///
5448
func shouldShowAttributeGuide(for product: Product) -> Bool {

WooCommerce/WooCommerceTests/ViewRelated/Products/Actions Factory/ProductFormActionsFactoryTests.swift

+15
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,21 @@ final class ProductFormActionsFactoryTests: XCTestCase {
452452
let containsWarningAction = factory.settingsSectionActions().contains(ProductFormEditAction.noPriceWarning)
453453
XCTAssertFalse(containsWarningAction)
454454
}
455+
456+
func test_actions_for_variable_product_with_attributes_contains_attributes_action() {
457+
// Given
458+
let product = Fixtures.variableProductWithVariations.copy(attributes: [
459+
ProductAttribute.fake().copy(variation: true)
460+
])
461+
let model = EditableProductModel(product: product)
462+
463+
// When
464+
let factory = ProductFormActionsFactory(product: model, formType: .edit, variationsPrice: .unknown)
465+
466+
// Then
467+
let containsAttributeAction = factory.settingsSectionActions().contains(ProductFormEditAction.attributes(editable: true))
468+
XCTAssertTrue(containsAttributeAction)
469+
}
455470
}
456471

457472
private extension ProductFormActionsFactoryTests {

0 commit comments

Comments
 (0)