Skip to content
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

[Woo POS] [Variable Products] Show variation name based on variation and variable product attributes #14807

Merged
merged 6 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions Networking/Networking/Model/Product/ProductAttribute.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,14 @@ extension ProductAttribute: Comparable {
}
}

extension ProductAttribute: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(siteID)
hasher.combine(name)
hasher.combine(attributeID)
}
}

// MARK: - Decoding Errors
//
enum ProductAttributeDecodingError: Error {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import Foundation
import Combine
import enum Yosemite.POSItem
import protocol Yosemite.PointOfSaleItemServiceProtocol
import enum Yosemite.PointOfSaleProductServiceError
import struct Yosemite.POSParentProduct
import class Yosemite.Store

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ private extension ChildItemList {
name: "Variable latte",
productImageSource: nil,
productID: 1,
type: .variable
type: .variable(.init())
)
let parentItem = POSItem.parentProduct(parentProduct)
let itemsController = PointOfSalePreviewItemsController()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ private struct ItemListRow: View {
name: "Variable mocha",
productImageSource: "https://pd.w.org/2024/12/986762d0d4d4cf17.82435881-scaled.jpeg",
productID: 16,
type: .variable
type: .variable(.init())
)
)
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,13 @@ private extension ParentProductCardView {

#if DEBUG
#Preview {
let parentProduct = POSParentProduct(id: UUID(), name: "Parent variable product", productImageSource: nil, productID: 42, type: .variable)
let parentProduct = POSParentProduct(
id: UUID(),
name: "Parent variable product",
productImageSource: nil,
productID: 42,
type: .variable(.init())
)
ParentProductCardView(parentProduct: parentProduct)
}
#endif
2 changes: 1 addition & 1 deletion WooCommerce/Classes/POS/Utils/PreviewHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ private var mockItems: [POSItem] {
name: "Variable product 1",
productImageSource: nil,
productID: 5,
type: .variable
type: .variable(.init())
)
),
.simpleProduct(POSSimpleProduct(id: UUID(), name: "Product 4", formattedPrice: "$4.00", productID: 4, price: "4.00"))
Expand Down
42 changes: 0 additions & 42 deletions WooCommerce/Classes/ViewModels/ProductDetailsCellViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,6 @@ import Foundation
import Yosemite
import WooFoundation

// MARK: - View Model for a Variation Attribute
//
struct VariationAttributeViewModel: Equatable {

/// Attribute name
///
let name: String

/// Attribute value
///
let value: String?

/// Returns the attribute value, or "Any \(name)" if the attribute value is nil or empty
///
var nameOrValue: String {
guard let value = value, value.isNotEmpty else {
return String(format: Localization.anyAttributeFormat, name)
}
return value
}

init(name: String, value: String? = nil) {
self.name = name
self.value = value
}

init(orderItemAttribute: OrderItemAttribute) {
self.init(name: orderItemAttribute.name, value: orderItemAttribute.value)
}

init(productVariationAttribute: ProductVariationAttribute) {
self.init(name: productVariationAttribute.name, value: productVariationAttribute.option)
}
}

extension VariationAttributeViewModel {
enum Localization {
static let anyAttributeFormat =
NSLocalizedString("Any %1$@", comment: "Format of a product variation attribute description where the attribute is set to any value.")
}
}


// MARK: - View Model for a product details cell
//
Expand Down
4 changes: 0 additions & 4 deletions WooCommerce/WooCommerce.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -2178,7 +2178,6 @@
CCE73D2529EDAB5C0064E797 /* SubscriptionPeriod+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCE73D2429EDAB5C0064E797 /* SubscriptionPeriod+UI.swift */; };
CCE785C829C1E8280003977F /* BundledProductsListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCE785C729C1E8280003977F /* BundledProductsListViewModelTests.swift */; };
CCE785CA29C1F9170003977F /* ProductBundleItemStockStatus+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCE785C929C1F9170003977F /* ProductBundleItemStockStatus+UI.swift */; };
CCEC256A27B581E800EF9FA3 /* ProductVariationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCEC256927B581E800EF9FA3 /* ProductVariationFormatter.swift */; };
CCF27B35280EF69700B755E1 /* orders_3337_add_product.json in Resources */ = {isa = PBXBuildFile; fileRef = CCF27B33280EF69600B755E1 /* orders_3337_add_product.json */; };
CCF27B3A280EF98F00B755E1 /* orders_3337.json in Resources */ = {isa = PBXBuildFile; fileRef = CCF27B39280EF98F00B755E1 /* orders_3337.json */; };
CCF87BBE279047BC00461C43 /* InfiniteScrollList.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCF87BBD279047BC00461C43 /* InfiniteScrollList.swift */; };
Expand Down Expand Up @@ -5314,7 +5313,6 @@
CCE73D2429EDAB5C0064E797 /* SubscriptionPeriod+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SubscriptionPeriod+UI.swift"; sourceTree = "<group>"; };
CCE785C729C1E8280003977F /* BundledProductsListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundledProductsListViewModelTests.swift; sourceTree = "<group>"; };
CCE785C929C1F9170003977F /* ProductBundleItemStockStatus+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProductBundleItemStockStatus+UI.swift"; sourceTree = "<group>"; };
CCEC256927B581E800EF9FA3 /* ProductVariationFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductVariationFormatter.swift; sourceTree = "<group>"; };
CCF27B33280EF69600B755E1 /* orders_3337_add_product.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = orders_3337_add_product.json; sourceTree = "<group>"; };
CCF27B39280EF98F00B755E1 /* orders_3337.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = orders_3337.json; sourceTree = "<group>"; };
CCF87BBD279047BC00461C43 /* InfiniteScrollList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfiniteScrollList.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -12080,7 +12078,6 @@
D817585C22BB5E6900289CFE /* Order Details */,
0371C36C2876E91500277E2C /* Feature Announcement Cards */,
D843D5D82248EE90001BFA55 /* ManualTrackingViewModel.swift */,
CCEC256927B581E800EF9FA3 /* ProductVariationFormatter.swift */,
CECC758B23D2227000486676 /* ProductDetailsCellViewModel.swift */,
D843D5D622485B19001BFA55 /* ShippingProvidersViewModel.swift */,
D8736B5922F07D7100A14A29 /* MainTabViewModel.swift */,
Expand Down Expand Up @@ -15818,7 +15815,6 @@
02E8B17C23E2C78A00A43403 /* ProductImageStatus.swift in Sources */,
03F5CB832A0C3A1A0026877A /* AnimatedPlaceholder.swift in Sources */,
0259D5FF2581F3FA003B1CD6 /* ShippingLabelPaperSizeOptionsViewController.swift in Sources */,
CCEC256A27B581E800EF9FA3 /* ProductVariationFormatter.swift in Sources */,
02EA6BFA2435E92600FFF90A /* KingfisherImageDownloader+ImageDownloadable.swift in Sources */,
7E7C5F8F2719BA7300315B61 /* ProductCategoryCellViewModel.swift in Sources */,
DE8AA0B32BBE55E40084D2CC /* DashboardViewHostingController.swift in Sources */,
Expand Down
8 changes: 8 additions & 0 deletions Yosemite/Yosemite.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@
02C254FA2563B66600A04423 /* ShippingLabelRefund+ReadOnlyConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C254F92563B66600A04423 /* ShippingLabelRefund+ReadOnlyConvertible.swift */; };
02C254FE2563C6E500A04423 /* ShippingLabelSettings+ReadOnlyConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C254FD2563C6E500A04423 /* ShippingLabelSettings+ReadOnlyConvertible.swift */; };
02C255022563C76A00A04423 /* ShippingLabel+ReadOnlyConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C255012563C76A00A04423 /* ShippingLabel+ReadOnlyConvertible.swift */; };
02CC7C2C2D2CE5CB00907B83 /* ProductVariationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02CC7C2B2D2CE5CB00907B83 /* ProductVariationFormatter.swift */; };
02CC7C2E2D2CE5F600907B83 /* VariationAttributeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02CC7C2D2D2CE5F600907B83 /* VariationAttributeViewModel.swift */; };
02DAE7F8291A9F11009342B7 /* DomainStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DAE7F7291A9F11009342B7 /* DomainStoreTests.swift */; };
02DAE7FA291A9F36009342B7 /* MockDomainRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DAE7F9291A9F36009342B7 /* MockDomainRemote.swift */; };
02DF98092A136BFB0009E2EA /* MockSitePluginsRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DF98082A136BFB0009E2EA /* MockSitePluginsRemote.swift */; };
Expand Down Expand Up @@ -618,6 +620,8 @@
02C254F92563B66600A04423 /* ShippingLabelRefund+ReadOnlyConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ShippingLabelRefund+ReadOnlyConvertible.swift"; sourceTree = "<group>"; };
02C254FD2563C6E500A04423 /* ShippingLabelSettings+ReadOnlyConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ShippingLabelSettings+ReadOnlyConvertible.swift"; sourceTree = "<group>"; };
02C255012563C76A00A04423 /* ShippingLabel+ReadOnlyConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ShippingLabel+ReadOnlyConvertible.swift"; sourceTree = "<group>"; };
02CC7C2B2D2CE5CB00907B83 /* ProductVariationFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductVariationFormatter.swift; sourceTree = "<group>"; };
02CC7C2D2D2CE5F600907B83 /* VariationAttributeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VariationAttributeViewModel.swift; sourceTree = "<group>"; };
02DAE7F7291A9F11009342B7 /* DomainStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainStoreTests.swift; sourceTree = "<group>"; };
02DAE7F9291A9F36009342B7 /* MockDomainRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDomainRemote.swift; sourceTree = "<group>"; };
02DF98082A136BFB0009E2EA /* MockSitePluginsRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSitePluginsRemote.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2065,6 +2069,8 @@
isa = PBXGroup;
children = (
B9C0C1072A3C666A00DF84EA /* ProductVariationStorageManager.swift */,
02CC7C2B2D2CE5CB00907B83 /* ProductVariationFormatter.swift */,
02CC7C2D2D2CE5F600907B83 /* VariationAttributeViewModel.swift */,
);
path = ProductVariations;
sourceTree = "<group>";
Expand Down Expand Up @@ -2383,6 +2389,7 @@
CE0FBB252D0C65EB008B7789 /* WooShippingCustomPackage+ReadOnlyConvertible.swift in Sources */,
CE3B7AD72225ECA90050FE4B /* OrderStatusStore.swift in Sources */,
B5631ECD2114DF8C008D3535 /* EntityListener.swift in Sources */,
02CC7C2E2D2CE5F600907B83 /* VariationAttributeViewModel.swift in Sources */,
D80F758A223F72AA002F4A3B /* ShipmentTrackingProviderGroup+ReadOnlyConvertible.swift in Sources */,
B9AECD442851D95600E78584 /* TotalRefundedCalculationUseCase.swift in Sources */,
86DAA7982CD9E31E002AE55E /* MockPaymentActionHandler.swift in Sources */,
Expand Down Expand Up @@ -2600,6 +2607,7 @@
0232372922F7DA6E00715FAB /* StatsTimeRangeV4.swift in Sources */,
247CE85C25832A5000F9D9D1 /* MockProductVariationActionHandler.swift in Sources */,
CEC7D5992CDE360E00111B79 /* WooShippingStore.swift in Sources */,
02CC7C2C2D2CE5CB00907B83 /* ProductVariationFormatter.swift in Sources */,
B5F2AE9720EBB54A00FEDC59 /* FetchedResultsControllerDelegateWrapper.swift in Sources */,
247CE7C02582DC7200F9D9D1 /* ProductImage+Mocks.swift in Sources */,
7492FADF217FB11D00ED2C69 /* SettingAction.swift in Sources */,
Expand Down
19 changes: 17 additions & 2 deletions Yosemite/Yosemite/PointOfSale/POSParentProduct.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
import Foundation

public enum POSParentProductType {
case variable
public enum POSParentProductType: Equatable, Hashable {
case variable(POSVariableParentProduct)
}

public struct POSVariableParentProduct: Equatable, Hashable {
let allAttributes: [ProductAttribute]

init(allAttributes: [ProductAttribute]) {
self.allAttributes = allAttributes
}

#if DEBUG
/// Initializer for SwiftUI previews.
public init() {
allAttributes = []
}
#endif
}

public struct POSParentProduct: Equatable, Hashable, Identifiable {
Expand Down
20 changes: 15 additions & 5 deletions Yosemite/Yosemite/PointOfSale/PointOfSaleItemService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import class Networking.AlamofireNetwork
import class WooFoundation.CurrencyFormatter
import class WooFoundation.CurrencySettings

public enum PointOfSaleProductServiceError: Error {
public enum PointOfSaleItemServiceError: Error, Equatable {
case requestFailed
case invalidParentProduct(POSParentProduct)
case unknown
}

Expand Down Expand Up @@ -65,16 +66,25 @@ public final class PointOfSaleItemService: PointOfSaleItemServiceProtocol {
}

public func providePointOfSaleVariationItems(for parentProduct: POSParentProduct, pageNumber: Int) async throws -> PagedItems<POSItem> {
guard case let .variable(variableProduct) = parentProduct.type else {
assertionFailure(
"Unexpected parent product when loading variations: \(parentProduct)"
)
throw PointOfSaleItemServiceError.invalidParentProduct(parentProduct)
}
let variations = try await variationRemote
.loadVariationsForPointOfSale(for: siteID,
parentProductID: parentProduct.productID,
pageNumber: pageNumber)
return .init(
items: variations.compactMap({ variation in
POSItem
let variationName = ProductVariationFormatter().generateName(
for: variation,
from: variableProduct.allAttributes
)
return POSItem
.variation(.init(id: UUID(),
// TODO-14702: variation name with ProductVariationFormatter
name: "Variation \(variation.productVariationID)",
name: variationName,
formattedPrice: currencyFormatter.formatAmount(variation.price) ?? "-",
productImageSource: variation.image?.src))
}),
Expand Down Expand Up @@ -104,7 +114,7 @@ public final class PointOfSaleItemService: PointOfSaleItemServiceProtocol {
name: product.name,
productImageSource: thumbnailSource,
productID: product.productID,
type: .variable))
type: .variable(.init(allAttributes: product.attributesForVariations))))
default:
return nil
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import Foundation
import Yosemite

/// Helper to format product variation details, such as variation name or attributes.
///
struct ProductVariationFormatter {
public struct ProductVariationFormatter {
public init() {}

/// Generates a name for the product variation, given a list of the parent product attributes, e.g. "Blue - Any Size"
/// - Parameters:
/// - variation: The product variation whose name is being generated
/// - allAttributes: A list of attributes from the parent `Product`
///
func generateName(for variation: ProductVariation, from allAttributes: [ProductAttribute]) -> String {
public func generateName(for variation: ProductVariation, from allAttributes: [ProductAttribute]) -> String {
let variationAttributes = generateAttributes(for: variation, from: allAttributes)
return variationAttributes.map { $0.nameOrValue }.joined(separator: " - ")
}
Expand All @@ -20,7 +20,7 @@ struct ProductVariationFormatter {
/// - variation: The product variation whose attributes are being generated
/// - allAttributes: A list of attributes from the parent `Product`
///
func generateAttributes(for variation: ProductVariation, from allAttributes: [ProductAttribute]) -> [VariationAttributeViewModel] {
public func generateAttributes(for variation: ProductVariation, from allAttributes: [ProductAttribute]) -> [VariationAttributeViewModel] {
return allAttributes
.sorted(by: { (lhs, rhs) -> Bool in
lhs.position < rhs.position
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Foundation

/// View Model for a Variation Attribute.
public struct VariationAttributeViewModel: Equatable {

/// Attribute name
///
public let name: String

/// Attribute value
///
public let value: String?

/// Returns the attribute value, or "Any \(name)" if the attribute value is nil or empty
///
public var nameOrValue: String {
guard let value = value, !value.isEmpty else {
return String(format: Localization.anyAttributeFormat, name)
}
return value
}

public init(name: String, value: String? = nil) {
self.name = name
self.value = value
}

public init(orderItemAttribute: OrderItemAttribute) {
self.init(name: orderItemAttribute.name, value: orderItemAttribute.value)
}

init(productVariationAttribute: ProductVariationAttribute) {
self.init(name: productVariationAttribute.name, value: productVariationAttribute.option)
}
}

extension VariationAttributeViewModel {
enum Localization {
static let anyAttributeFormat =
NSLocalizedString("Any %1$@", comment: "Format of a product variation attribute description where the attribute is set to any value.")
}
}
Loading