Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
89348f1
Add missing properties
itsmeichigo Jul 31, 2025
25ccab4
Make some entities conform to ListItemConvertible with default implem…
itsmeichigo Jul 31, 2025
5825c4c
Replace full product objects in product list
itsmeichigo Jul 31, 2025
07f9d23
Fix swiftlint issue
itsmeichigo Jul 31, 2025
df8c1a5
Merge branch 'woomob-619-xcode-warnings-performing-io-on-the-main-thr…
itsmeichigo Aug 1, 2025
177a232
Move conversion from full object to separate file
itsmeichigo Aug 1, 2025
2e10b2f
Update tests
itsmeichigo Aug 1, 2025
8fefa69
Merge branch 'woomob-619-xcode-warnings-performing-io-on-the-main-thr…
itsmeichigo Aug 1, 2025
58de948
Unify logic for createStockText
itsmeichigo Aug 1, 2025
7962628
Revert changes made to PaginatedListSelectorViewController
itsmeichigo Aug 1, 2025
1385645
Revert changes made to SearchUICommand and related files
itsmeichigo Aug 1, 2025
3cdd596
Revert redundant changes
itsmeichigo Aug 1, 2025
c508d42
Remove redundant conformance
itsmeichigo Aug 1, 2025
ca151c9
Update release notes
itsmeichigo Aug 1, 2025
2eeae10
Merge branch 'woomob-619-xcode-warnings-performing-io-on-the-main-thr…
itsmeichigo Aug 5, 2025
cc94596
Add ProductListItem on the view layer
itsmeichigo Aug 5, 2025
6051068
Merge branch 'woomob-619-xcode-warnings-performing-io-on-the-main-thr…
itsmeichigo Aug 5, 2025
c612ea0
Remove added tests for ResultsController
itsmeichigo Aug 5, 2025
1c90859
Merge branch 'woomob-619-xcode-warnings-performing-io-on-the-main-thr…
itsmeichigo Aug 5, 2025
ce5913b
Add workaround for using simplified objects for display in list items
itsmeichigo Aug 5, 2025
42ff794
Fix swiftlint
itsmeichigo Aug 5, 2025
3c03c14
Merge branch 'trunk' into woomob-619-product-list-update
itsmeichigo Aug 7, 2025
30e5dec
Remove unused properties
itsmeichigo Aug 7, 2025
47c60a6
Merge branch 'trunk' into woomob-619-product-list-update
itsmeichigo Aug 7, 2025
54706cd
Ignore periphery for OrderDetailsProduct initializer
itsmeichigo Aug 7, 2025
21bb2e8
Restore initializer for ProductDetailsCellViewModel
itsmeichigo Aug 7, 2025
39a54d1
Remove unused property
itsmeichigo Aug 7, 2025
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
6 changes: 1 addition & 5 deletions Modules/Sources/Yosemite/Tools/ResultsController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public class GenericResultsController<T: ResultsControllerMutableType, Output> {

/// Internal NSFetchedResultsController Instance.
///
private lazy var controller: NSFetchedResultsController<T> = {
public private(set) lazy var controller: NSFetchedResultsController<T> = {
viewStorage.createFetchedResultsController(
fetchRequest: fetchRequest,
sectionNameKeyPath: sectionNameKeyPath,
Expand All @@ -65,10 +65,6 @@ public class GenericResultsController<T: ResultsControllerMutableType, Output> {
// swiftlint:disable:next weak_delegate
private let internalDelegate = FetchedResultsControllerDelegateWrapper()

/// NotificationCenter ObserverBlock Token
///
private var notificationCenterToken: Any?

/// Closure to be executed before the results are changed.
///
public var onWillChangeContent: (() -> Void)?
Expand Down
36 changes: 11 additions & 25 deletions Modules/Tests/YosemiteTests/Tools/ResultsControllerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ final class ResultsControllerTests: XCTestCase {
///
func testResultsControllerPicksUpEntitiesAvailablePriorToInstantiation() {
storageManager.insertSampleAccount()
viewStorage.saveIfNeeded()

let resultsController = ResultsController<StorageAccount>(viewStorage: viewStorage, sortedBy: [sampleSortDescriptor])
try? resultsController.performFetch()
Expand All @@ -70,12 +69,11 @@ final class ResultsControllerTests: XCTestCase {
/// Verifies that ResultsController does pick up entities inserted after being instantiated.
///
func testResultsControllerPicksUpEntitiesInsertedAfterInstantiation() {
storageManager.insertSampleAccount()

let resultsController = ResultsController<StorageAccount>(viewStorage: viewStorage, sortedBy: [sampleSortDescriptor])
try? resultsController.performFetch()

storageManager.insertSampleAccount()
viewStorage.saveIfNeeded()

XCTAssertEqual(resultsController.sections.count, 1)
XCTAssertEqual(resultsController.sections.first?.objects.count, 1)
XCTAssertEqual(resultsController.sections.first?.numberOfObjects, 1)
Expand All @@ -85,18 +83,16 @@ final class ResultsControllerTests: XCTestCase {
/// Verifies that `sectionNameKeyPath` effectively causes the ResultsController to produce multiple sections, based on the grouping parameter.
///
func testResultsControllerGroupSectionsBySectionNameKeypath() {
let sectionNameKeyPath = "userID"
let resultsController = ResultsController<StorageAccount>(viewStorage: viewStorage,
sectionNameKeyPath: sectionNameKeyPath,
sortedBy: [sampleSortDescriptor])
try? resultsController.performFetch()

let numberOfAccounts = 100
for _ in 0 ..< numberOfAccounts {
storageManager.insertSampleAccount()
}

viewStorage.saveIfNeeded()
let sectionNameKeyPath = "userID"
let resultsController = ResultsController<StorageAccount>(viewStorage: viewStorage,
sectionNameKeyPath: sectionNameKeyPath,
sortedBy: [sampleSortDescriptor])
try? resultsController.performFetch()

XCTAssertEqual(resultsController.sections.count, numberOfAccounts)

Expand All @@ -109,15 +105,13 @@ final class ResultsControllerTests: XCTestCase {
/// Verifies that `object(at indexPath:)` effectively returns the expected (ReadOnly) Entity.
///
func testObjectAtIndexPathReturnsExpectedEntity() {
let mutableAccount = storageManager.insertSampleAccount()
let sectionNameKeyPath = "userID"
let resultsController = ResultsController<StorageAccount>(viewStorage: viewStorage,
sectionNameKeyPath: sectionNameKeyPath,
sortedBy: [sampleSortDescriptor])
try? resultsController.performFetch()

let mutableAccount = storageManager.insertSampleAccount()
viewStorage.saveIfNeeded()

let indexPath = IndexPath(row: 0, section: 0)
let readOnlyAccount = resultsController.object(at: indexPath)

Expand All @@ -144,7 +138,6 @@ final class ResultsControllerTests: XCTestCase {
}

storageManager.insertSampleAccount()
viewStorage.saveIfNeeded()

waitForExpectations(timeout: Constants.expectationTimeout, handler: nil)
}
Expand All @@ -167,7 +160,6 @@ final class ResultsControllerTests: XCTestCase {
}

storageManager.insertSampleAccount()
viewStorage.saveIfNeeded()

waitForExpectations(timeout: Constants.expectationTimeout, handler: nil)
}
Expand All @@ -189,7 +181,6 @@ final class ResultsControllerTests: XCTestCase {
}

storageManager.insertSampleAccount()
viewStorage.saveIfNeeded()

waitForExpectations(timeout: Constants.expectationTimeout, handler: nil)
}
Expand All @@ -211,7 +202,6 @@ final class ResultsControllerTests: XCTestCase {
}

storageManager.insertSampleAccount()
viewStorage.saveIfNeeded()

waitForExpectations(timeout: Constants.expectationTimeout, handler: nil)
}
Expand All @@ -228,7 +218,6 @@ final class ResultsControllerTests: XCTestCase {
let second = storageManager.insertSampleAccount().toReadOnly()
let expected = [first.userID: first, second.userID: second]

viewStorage.saveIfNeeded()

for retrieved in resultsController.fetchedObjects {
XCTAssertEqual(retrieved.username, expected[retrieved.userID]?.username)
Expand All @@ -239,14 +228,13 @@ final class ResultsControllerTests: XCTestCase {
/// Verifies that `fetchedObjects` effectively returns all of the (readOnly) objects that are expected to be available.
///
func testResettingStorageIsMappedIntoOnResetClosure() {
storageManager.insertSampleAccount()
storageManager.insertSampleAccount()

let sortDescriptor = NSSortDescriptor(key: #selector(getter: StorageAccount.userID).description, ascending: true)
let resultsController = ResultsController<StorageAccount>(viewStorage: viewStorage, sortedBy: [sortDescriptor])
try? resultsController.performFetch()

storageManager.insertSampleAccount()
storageManager.insertSampleAccount()

viewStorage.saveIfNeeded()
XCTAssertEqual(resultsController.fetchedObjects.count, 2)

let expectation = self.expectation(description: "OnDidReset")
Expand Down Expand Up @@ -296,8 +284,6 @@ final class ResultsControllerTests: XCTestCase {
}
}

viewStorage.saveIfNeeded()

for (sectionNumber, sectionObject) in resultsController.sections.enumerated() {
for (row, object) in sectionObject.objects.enumerated() {
let indexPath = IndexPath(row: row, section: sectionNumber)
Expand Down
1 change: 1 addition & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- [*] Fix initialization of authenticator to avoid crashes during login [https://github.com/woocommerce/woocommerce-ios/pull/15953]
- [*] Shipping Labels: Made HS tariff number field required in customs form for EU destinations [https://github.com/woocommerce/woocommerce-ios/pull/15946]
- [*] Order Details: Attempt to improve performance by using a simplified version of product objects. [https://github.com/woocommerce/woocommerce-ios/pull/15959]
- [*] Product List: Load list with simplified product objects to improve performance. [https://teamkiwip2.wordpress.com/2025/08/01/hack-week-improving-performance-when-loading-cached-products/]
- [*] Order Details > Edit Shipping/Billing Address: Added map-based address lookup support for iOS 17+. [https://github.com/woocommerce/woocommerce-ios/pull/15964]
- [*] Order Creation: Prevent subscription products to be added to an order [https://github.com/woocommerce/woocommerce-ios/pull/15960]
- [internal] Replace COTS_DEVICE reader model name with TAP_TO_PAY_DEVICE. [https://github.com/woocommerce/woocommerce-ios/pull/15961]
Expand Down
48 changes: 48 additions & 0 deletions WooCommerce/Classes/Copiable/Models+Copiable.generated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,54 @@ extension WooCommerce.AggregateOrderItem {
}
}

extension WooCommerce.ProductListItem {
func copy(
siteID: CopiableProp<Int64> = .copy,
productID: CopiableProp<Int64> = .copy,
name: CopiableProp<String> = .copy,
productTypeKey: CopiableProp<String> = .copy,
statusKey: CopiableProp<String> = .copy,
sku: NullableCopiableProp<String> = .copy,
manageStock: CopiableProp<Bool> = .copy,
stockQuantity: NullableCopiableProp<Decimal> = .copy,
stockStatusKey: CopiableProp<String> = .copy,
imageURL: NullableCopiableProp<URL> = .copy,
variations: CopiableProp<[Int64]> = .copy,
bundleStockStatus: NullableCopiableProp<ProductStockStatus> = .copy,
bundleStockQuantity: NullableCopiableProp<Int64> = .copy
) -> WooCommerce.ProductListItem {
let siteID = siteID ?? self.siteID
let productID = productID ?? self.productID
let name = name ?? self.name
let productTypeKey = productTypeKey ?? self.productTypeKey
let statusKey = statusKey ?? self.statusKey
let sku = sku ?? self.sku
let manageStock = manageStock ?? self.manageStock
let stockQuantity = stockQuantity ?? self.stockQuantity
let stockStatusKey = stockStatusKey ?? self.stockStatusKey
let imageURL = imageURL ?? self.imageURL
let variations = variations ?? self.variations
let bundleStockStatus = bundleStockStatus ?? self.bundleStockStatus
let bundleStockQuantity = bundleStockQuantity ?? self.bundleStockQuantity

return WooCommerce.ProductListItem(
siteID: siteID,
productID: productID,
name: name,
productTypeKey: productTypeKey,
statusKey: statusKey,
sku: sku,
manageStock: manageStock,
stockQuantity: stockQuantity,
stockStatusKey: stockStatusKey,
imageURL: imageURL,
variations: variations,
bundleStockStatus: bundleStockStatus,
bundleStockQuantity: bundleStockQuantity
)
}
}

extension WooCommerce.ShippingLabelSelectedRate {
func copy(
packageID: CopiableProp<String> = .copy,
Expand Down
20 changes: 20 additions & 0 deletions WooCommerce/Classes/Extensions/Product+ListItem.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Foundation
import Yosemite

extension Product {
func toListItem() -> ProductListItem {
ProductListItem(siteID: siteID,
productID: productID,
name: name,
productTypeKey: productTypeKey,
statusKey: statusKey,
sku: sku,
manageStock: manageStock,
stockQuantity: stockQuantity,
stockStatusKey: stockStatusKey,
imageURL: imageURL,
variations: variations,
bundleStockStatus: bundleStockStatus,
bundleStockQuantity: bundleStockQuantity)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,65 +4,45 @@ import Yosemite
/// Represents a Product Entity with basic details to display in the product section of order details screen.
///
struct OrderDetailsProduct: Equatable {
let siteID: Int64
let productID: Int64
let name: String

let productTypeKey: String
let sku: String?

let price: String
let virtual: Bool

let stockQuantity: Decimal? // Core API reports Int or null; some extensions allow decimal values as well

let imageURL: URL?

let addOns: [Yosemite.ProductAddOn] //TODO: migrate AddOns to MetaData
let addOns: [Yosemite.ProductAddOn]

var productType: ProductType {
return ProductType(rawValue: productTypeKey)
}

init(siteID: Int64,
productID: Int64,
name: String,
/// periphery: ignore - used in test module
init(productID: Int64,
productTypeKey: String,
sku: String?,
price: String,
virtual: Bool,
stockQuantity: Decimal?,
imageURL: URL?,
addOns: [Yosemite.ProductAddOn]) {
self.siteID = siteID
self.productID = productID
self.name = name
self.productTypeKey = productTypeKey
self.sku = sku
self.price = price
self.virtual = virtual
self.stockQuantity = stockQuantity
self.imageURL = imageURL
self.addOns = addOns
}

init(storageProduct: StorageProduct) {
self.siteID = storageProduct.siteID
self.productID = storageProduct.productID
self.name = storageProduct.name
self.productTypeKey = storageProduct.productTypeKey
self.sku = storageProduct.sku
self.price = storageProduct.price
self.virtual = storageProduct.virtual

self.stockQuantity = {
var quantity: Decimal?
if let stockQuantity = storageProduct.stockQuantity {
quantity = Decimal(string: stockQuantity)
}
return quantity
}()

self.imageURL = storageProduct.imagesArray.first?.toReadOnly().imageURL

let addOnsArray: [StorageProductAddOn] = storageProduct.addOns?.toArray() ?? []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,6 @@ final class RefundDetailsViewModel {
self.refund = refund
}

/// Products from a Refund
///
var products: [OrderDetailsProduct] {
return dataSource.products
}

/// Subtotal from all refunded products
///
var itemSubtotal: String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ struct ProductDetailsCellViewModel {
self.isChildProduct = isChildProduct
}

/// periphery: ignore - used in test module
/// Order Item initializer
///
init(item: OrderItem,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,9 +305,16 @@ private struct ProductsTabProductTableViewCellRepresentable: UIViewRepresentable
}

struct ProductsTabProductTableViewCell_Previews: PreviewProvider {
private static var nonSelectedViewModel = ProductsTabProductViewModel(product: Product.swiftUIPreviewSample(), isSelected: false)
private static var selectedViewModel = ProductsTabProductViewModel(product: Product.swiftUIPreviewSample().copy(statusKey: ProductStatus.pending.rawValue),
isSelected: true)
private static var nonSelectedViewModel = ProductsTabProductViewModel(
product: Product.swiftUIPreviewSample().toListItem(),
isSelected: false
)
private static var selectedViewModel = ProductsTabProductViewModel(
product: Product.swiftUIPreviewSample()
.toListItem()
.copy(statusKey: ProductStatus.pending.rawValue),
isSelected: true
)

private static func makeStack() -> some View {
VStack {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,14 +164,6 @@ extension EditableProductModel: ProductFormDataModel, TaxClassRequestable {
product.bundledItems
}

var bundleStockStatus: ProductStockStatus? {
product.bundleStockStatus
}

var bundleStockQuantity: Int64? {
product.bundleStockQuantity
}

var password: String? {
product.password
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ final class LinkedProductListSelectorDataSource: PaginatedListSelectorDataSource
func configureCell(cell: ProductsTabProductTableViewCell, model: Product) {
cell.selectionStyle = .none

let viewModel = ProductsTabProductViewModel(product: model, isDraggable: true)
let viewModel = ProductsTabProductViewModel(product: model.toListItem(), isDraggable: true)
cell.update(viewModel: viewModel, imageService: imageService)

cell.configureAccessoryDeleteButton { [weak self] in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ final class ProductListMultiSelectorDataSource: PaginatedListSelectorDataSource
func configureCell(cell: ProductsTabProductTableViewCell, model: Product) {
cell.selectionStyle = .default

let viewModel = ProductsTabProductViewModel(product: model, isSelected: isSelected(model: model))
let viewModel = ProductsTabProductViewModel(product: model.toListItem(), isSelected: isSelected(model: model))
cell.update(viewModel: viewModel, imageService: imageService)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ final class ProductListMultiSelectorSearchUICommand: NSObject, SearchUICommand {
}

func createCellViewModel(model: Product) -> ProductsTabProductViewModel {
return ProductsTabProductViewModel(product: model, isSelected: isProductSelected(model))
return ProductsTabProductViewModel(product: model.toListItem(), isSelected: isProductSelected(model))
}

/// Synchronizes the Products matching a given Keyword
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,6 @@ protocol ProductFormDataModel {

// Product Bundles
var bundledItems: [ProductBundleItem] { get }
var bundleStockStatus: ProductStockStatus? { get }
var bundleStockQuantity: Int64? { get }

// Password
var password: String? { get }
Expand Down
Loading