Skip to content

Commit 5bddf6b

Browse files
authored
Merge pull request #2209 from woocommerce/issue/2037-filter-products-yosemite-networking
Filter Products: Yosemite & Networking layers
2 parents 97ba482 + 535a85e commit 5bddf6b

File tree

12 files changed

+505
-28
lines changed

12 files changed

+505
-28
lines changed

Networking/Networking/Remote/ProductsRemote.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ public class ProductsRemote: Remote {
1515
/// determines fields present in response. Default is view.
1616
/// - pageNumber: Number of page that should be retrieved.
1717
/// - pageSize: Number of products to be retrieved per page.
18+
/// - stockStatus: Optional stock status filtering. Default to nil (no filtering).
19+
/// - productStatus: Optional product status filtering. Default to nil (no filtering).
20+
/// - productType: Optional product type filtering. Default to nil (no filtering).
1821
/// - orderBy: the key to order the remote products. Default to product name.
1922
/// - order: ascending or descending order. Default to ascending.
2023
/// - completion: Closure to be executed upon completion.
@@ -23,16 +26,25 @@ public class ProductsRemote: Remote {
2326
context: String? = nil,
2427
pageNumber: Int = Default.pageNumber,
2528
pageSize: Int = Default.pageSize,
29+
stockStatus: ProductStockStatus? = nil,
30+
productStatus: ProductStatus? = nil,
31+
productType: ProductType? = nil,
2632
orderBy: OrderKey = .name,
2733
order: Order = .ascending,
2834
completion: @escaping ([Product]?, Error?) -> Void) {
35+
let filterParameters = [
36+
ParameterKey.stockStatus: stockStatus?.rawValue ?? "",
37+
ParameterKey.productStatus: productStatus?.rawValue ?? "",
38+
ParameterKey.productType: productType?.rawValue ?? ""
39+
].filter({ $0.value.isEmpty == false })
40+
2941
let parameters = [
3042
ParameterKey.page: String(pageNumber),
3143
ParameterKey.perPage: String(pageSize),
3244
ParameterKey.contextKey: context ?? Default.context,
3345
ParameterKey.orderBy: orderBy.value,
3446
ParameterKey.order: order.value
35-
]
47+
].merging(filterParameters, uniquingKeysWith: { (first, _) in first })
3648

3749
let path = Path.products
3850
let request = JetpackRequest(wooApiVersion: .mark3, method: .get, siteID: siteID, path: path, parameters: parameters)
@@ -183,6 +195,9 @@ public extension ProductsRemote {
183195
static let orderBy: String = "orderby"
184196
static let order: String = "order"
185197
static let sku: String = "sku"
198+
static let productStatus: String = "status"
199+
static let productType: String = "type"
200+
static let stockStatus: String = "stock_status"
186201
static let fields: String = "_fields"
187202
}
188203

WooCommerce/Classes/ViewRelated/Products/ProductsViewController.swift

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,21 @@ final class ProductsViewController: UIViewController {
9999

100100
private let imageService: ImageService = ServiceLocator.imageService
101101

102-
private lazy var filters: FilterProductListViewModel.Filters = FilterProductListViewModel.Filters(stockStatus: nil, productStatus: nil, productType: nil)
102+
private var filters: FilterProductListViewModel.Filters = FilterProductListViewModel.Filters(stockStatus: nil, productStatus: nil, productType: nil) {
103+
didSet {
104+
if filters != oldValue {
105+
guard let siteID = ServiceLocator.stores.sessionManager.defaultStoreID else {
106+
assertionFailure("No valid site ID for Products tab")
107+
return
108+
}
109+
resultsController.updatePredicate(siteID: siteID,
110+
stockStatus: filters.stockStatus,
111+
productStatus: filters.productStatus,
112+
productType: filters.productType)
113+
syncingCoordinator.resynchronize {}
114+
}
115+
}
116+
}
103117

104118
// MARK: - View Lifecycle
105119

@@ -325,7 +339,10 @@ private extension ProductsViewController {
325339

326340
func createResultsController(siteID: Int64) -> ResultsController<StorageProduct> {
327341
let storageManager = ServiceLocator.storageManager
328-
let predicate = NSPredicate(format: "siteID == %lld", siteID)
342+
let predicate = NSPredicate.createProductPredicate(siteID: siteID,
343+
stockStatus: filters.stockStatus,
344+
productStatus: filters.productStatus,
345+
productType: filters.productType)
329346

330347
return ResultsController<StorageProduct>(storageManager: storageManager,
331348
matching: predicate,
@@ -461,7 +478,6 @@ private extension ProductsViewController {
461478
let viewModel = FilterProductListViewModel(filters: filters)
462479
let filterProductListViewController = FilterListViewController(viewModel: viewModel) { [weak self] filters in
463480
self?.filters = filters
464-
// TODO-2037: filter products based on the filters
465481
}
466482
present(filterProductListViewController, animated: true, completion: nil)
467483
}
@@ -548,6 +564,9 @@ extension ProductsViewController: SyncingCoordinatorDelegate {
548564
.synchronizeProducts(siteID: siteID,
549565
pageNumber: pageNumber,
550566
pageSize: pageSize,
567+
stockStatus: filters.stockStatus,
568+
productStatus: filters.productStatus,
569+
productType: filters.productType,
551570
sortOrder: sortOrder) { [weak self] error in
552571
guard let self = self else {
553572
return

Yosemite/Yosemite.xcodeproj/project.pbxproj

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@
2727
0225512122FC2F3000D98613 /* OrderStatsV4Interval+Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0225512022FC2F3000D98613 /* OrderStatsV4Interval+Date.swift */; };
2828
0225512522FC312400D98613 /* OrderStatsV4Interval+DateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0225512422FC312400D98613 /* OrderStatsV4Interval+DateTests.swift */; };
2929
0232372922F7DA6E00715FAB /* StatsTimeRangeV4.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0232372822F7DA6E00715FAB /* StatsTimeRangeV4.swift */; };
30+
0248B3652459018100A271A4 /* ResultsController+FilterProducts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0248B3642459018100A271A4 /* ResultsController+FilterProducts.swift */; };
31+
0248B3672459020500A271A4 /* ResultsController+FilterProductTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0248B3662459020500A271A4 /* ResultsController+FilterProductTests.swift */; };
32+
0248B36924590FC300A271A4 /* ProductStore+FilterProductsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0248B36824590FC300A271A4 /* ProductStore+FilterProductsTests.swift */; };
33+
0248B36B2459127200A271A4 /* MockupNetwork+Path.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0248B36A2459127200A271A4 /* MockupNetwork+Path.swift */; };
3034
025CA2CA238F515600B05C81 /* ProductShippingClassStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025CA2C9238F515600B05C81 /* ProductShippingClassStore.swift */; };
3135
025CA2CC238F518600B05C81 /* ProductShippingClassAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025CA2CB238F518600B05C81 /* ProductShippingClassAction.swift */; };
3236
025CA2CE238F53CB00B05C81 /* ProductShippingClass+ReadOnlyConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025CA2CD238F53CB00B05C81 /* ProductShippingClass+ReadOnlyConvertible.swift */; };
@@ -243,6 +247,10 @@
243247
0225512022FC2F3000D98613 /* OrderStatsV4Interval+Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrderStatsV4Interval+Date.swift"; sourceTree = "<group>"; };
244248
0225512422FC312400D98613 /* OrderStatsV4Interval+DateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrderStatsV4Interval+DateTests.swift"; sourceTree = "<group>"; };
245249
0232372822F7DA6E00715FAB /* StatsTimeRangeV4.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsTimeRangeV4.swift; sourceTree = "<group>"; };
250+
0248B3642459018100A271A4 /* ResultsController+FilterProducts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ResultsController+FilterProducts.swift"; sourceTree = "<group>"; };
251+
0248B3662459020500A271A4 /* ResultsController+FilterProductTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ResultsController+FilterProductTests.swift"; sourceTree = "<group>"; };
252+
0248B36824590FC300A271A4 /* ProductStore+FilterProductsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProductStore+FilterProductsTests.swift"; sourceTree = "<group>"; };
253+
0248B36A2459127200A271A4 /* MockupNetwork+Path.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MockupNetwork+Path.swift"; sourceTree = "<group>"; };
246254
025CA2C9238F515600B05C81 /* ProductShippingClassStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductShippingClassStore.swift; sourceTree = "<group>"; };
247255
025CA2CB238F518600B05C81 /* ProductShippingClassAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductShippingClassAction.swift; sourceTree = "<group>"; };
248256
025CA2CD238F53CB00B05C81 /* ProductShippingClass+ReadOnlyConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProductShippingClass+ReadOnlyConvertible.swift"; sourceTree = "<group>"; };
@@ -480,6 +488,7 @@
480488
isa = PBXGroup;
481489
children = (
482490
0212AC61242C68B600C51F6C /* ResultsController+SortProducts.swift */,
491+
0248B3642459018100A271A4 /* ResultsController+FilterProducts.swift */,
483492
);
484493
path = Products;
485494
sourceTree = "<group>";
@@ -488,6 +497,7 @@
488497
isa = PBXGroup;
489498
children = (
490499
0212AC66242C799B00C51F6C /* ResultsController+StorageProductTests.swift */,
500+
0248B3662459020500A271A4 /* ResultsController+FilterProductTests.swift */,
491501
);
492502
path = Products;
493503
sourceTree = "<group>";
@@ -780,6 +790,7 @@
780790
020B2F9523BDE4DD00BD79AD /* ProductStoreTests+Validation.swift */,
781791
02FF056623DEB2180058E6E7 /* MediaStoreTests.swift */,
782792
0212AC63242C6FC300C51F6C /* ProductStore+ProductsSortOrderTests.swift */,
793+
0248B36824590FC300A271A4 /* ProductStore+FilterProductsTests.swift */,
783794
);
784795
path = Stores;
785796
sourceTree = "<group>";
@@ -901,6 +912,7 @@
901912
B53A569F211245E0000776C9 /* MockupStorage+Sample.swift */,
902913
0202B6962387AFBF00F3EBE0 /* MockInMemoryStorage.swift */,
903914
020220E32396969E00290165 /* MockProduct.swift */,
915+
0248B36A2459127200A271A4 /* MockupNetwork+Path.swift */,
904916
);
905917
path = Mockups;
906918
sourceTree = "<group>";
@@ -1217,6 +1229,7 @@
12171229
02FF055323D983F30058E6E7 /* URL+Media.swift in Sources */,
12181230
7499936620EFBC7200CF01CD /* OrderNoteStore.swift in Sources */,
12191231
74858DB421C02B5A00754F3E /* OrderNote+ReadOnlyType.swift in Sources */,
1232+
0248B3652459018100A271A4 /* ResultsController+FilterProducts.swift in Sources */,
12201233
02E262C2238CF74D00B79588 /* StorageShippingSettingsService.swift in Sources */,
12211234
749375042249691D007D85D1 /* Product+ReadOnlyConvertible.swift in Sources */,
12221235
CE43A90222A072D800A4FF29 /* ProductDownload+ReadOnlyConvertible.swift in Sources */,
@@ -1282,6 +1295,7 @@
12821295
0202B6992387B01500F3EBE0 /* AppSettingsStoreTests+ProductsVisibility.swift in Sources */,
12831296
02FF055623D984310058E6E7 /* MockupFileManager.swift in Sources */,
12841297
029B00A7230D64E800B0AE66 /* StatsTimeRangeTests.swift in Sources */,
1298+
0248B36924590FC300A271A4 /* ProductStore+FilterProductsTests.swift in Sources */,
12851299
02FF055F23D985710058E6E7 /* URL+MediaTests.swift in Sources */,
12861300
02124DAC24318D6B00980D74 /* Media+MediaTypeTests.swift in Sources */,
12871301
025CA2D0238F54E800B05C81 /* ProductShippingClassStoreTests.swift in Sources */,
@@ -1306,9 +1320,11 @@
13061320
7455263022305F88003F8932 /* OrderStatusStoreTests.swift in Sources */,
13071321
B5C9DE242087FF20006B910A /* StoreTests.swift in Sources */,
13081322
02FF055C23D9846A0058E6E7 /* MediaFileManagerTests.swift in Sources */,
1323+
0248B36B2459127200A271A4 /* MockupNetwork+Path.swift in Sources */,
13091324
B5C9DE252087FF20006B910A /* MockupProcessor.swift in Sources */,
13101325
D8C11A5A22DFC21600D4A88D /* StatsStoreV4Tests.swift in Sources */,
13111326
02124DB02431C18700980D74 /* Media+ProductImageTests.swift in Sources */,
1327+
0248B3672459020500A271A4 /* ResultsController+FilterProductTests.swift in Sources */,
13121328
B54EAF2121188C470029C35E /* EntityListenerTests.swift in Sources */,
13131329
021C7BF52386362A00A3BCBD /* Product+UpdaterTestCases.swift in Sources */,
13141330
02FF056F23E04F320058E6E7 /* MockMediaExportService.swift in Sources */,

Yosemite/Yosemite/Actions/ProductAction.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,14 @@ public enum ProductAction: Action {
1212

1313
/// Synchronizes the Products matching the specified criteria.
1414
///
15-
case synchronizeProducts(siteID: Int64, pageNumber: Int, pageSize: Int, sortOrder: ProductsSortOrder, onCompletion: (Error?) -> Void)
15+
case synchronizeProducts(siteID: Int64,
16+
pageNumber: Int,
17+
pageSize: Int,
18+
stockStatus: ProductStockStatus?,
19+
productStatus: ProductStatus?,
20+
productType: ProductType?,
21+
sortOrder: ProductsSortOrder,
22+
onCompletion: (Error?) -> Void)
1623

1724
/// Retrieves the specified Product.
1825
///

Yosemite/Yosemite/Stores/ProductStore.swift

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,22 @@ public class ProductStore: Store {
3434
retrieveProducts(siteID: siteID, productIDs: productIDs, onCompletion: onCompletion)
3535
case .searchProducts(let siteID, let keyword, let pageNumber, let pageSize, let onCompletion):
3636
searchProducts(siteID: siteID, keyword: keyword, pageNumber: pageNumber, pageSize: pageSize, onCompletion: onCompletion)
37-
case .synchronizeProducts(let siteID, let pageNumber, let pageSize, let sortOrder, let onCompletion):
38-
synchronizeProducts(siteID: siteID, pageNumber: pageNumber, pageSize: pageSize, sortOrder: sortOrder, onCompletion: onCompletion)
37+
case .synchronizeProducts(let siteID,
38+
let pageNumber,
39+
let pageSize,
40+
let stockStatus,
41+
let productStatus,
42+
let productType,
43+
let sortOrder,
44+
let onCompletion):
45+
synchronizeProducts(siteID: siteID,
46+
pageNumber: pageNumber,
47+
pageSize: pageSize,
48+
stockStatus: stockStatus,
49+
productStatus: productStatus,
50+
productType: productType,
51+
sortOrder: sortOrder,
52+
onCompletion: onCompletion)
3953
case .requestMissingProducts(let order, let onCompletion):
4054
requestMissingProducts(for: order, onCompletion: onCompletion)
4155
case .updateProduct(let product, let onCompletion):
@@ -85,12 +99,22 @@ private extension ProductStore {
8599

86100
/// Synchronizes the products associated with a given Site ID, sorted by ascending name.
87101
///
88-
func synchronizeProducts(siteID: Int64, pageNumber: Int, pageSize: Int, sortOrder: ProductsSortOrder, onCompletion: @escaping (Error?) -> Void) {
102+
func synchronizeProducts(siteID: Int64,
103+
pageNumber: Int,
104+
pageSize: Int,
105+
stockStatus: ProductStockStatus?,
106+
productStatus: ProductStatus?,
107+
productType: ProductType?,
108+
sortOrder: ProductsSortOrder,
109+
onCompletion: @escaping (Error?) -> Void) {
89110
let remote = ProductsRemote(network: network)
90111

91112
remote.loadAllProducts(for: siteID,
92113
pageNumber: pageNumber,
93114
pageSize: pageSize,
115+
stockStatus: stockStatus,
116+
productStatus: productStatus,
117+
productType: productType,
94118
orderBy: sortOrder.remoteOrderKey,
95119
order: sortOrder.remoteOrder) { [weak self] (products, error) in
96120
guard let products = products else {
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import CoreData
2+
import Storage
3+
4+
extension NSPredicate {
5+
public static func createProductPredicate(siteID: Int64,
6+
stockStatus: ProductStockStatus? = nil,
7+
productStatus: ProductStatus? = nil,
8+
productType: ProductType? = nil) -> NSPredicate {
9+
let siteIDPredicate = NSPredicate(format: "siteID == %lld", siteID)
10+
11+
let stockStatusPredicate = stockStatus.flatMap { stockStatus -> NSPredicate? in
12+
let key = #selector(getter: StorageProduct.stockStatusKey)
13+
return NSPredicate(format: "\(key) == %@", stockStatus.rawValue)
14+
}
15+
16+
let productStatusPredicate = productStatus.flatMap { productStatus -> NSPredicate? in
17+
let key = #selector(getter: StorageProduct.statusKey)
18+
return NSPredicate(format: "\(key) == %@", productStatus.rawValue)
19+
}
20+
21+
let productTypePredicate = productType.flatMap { productType -> NSPredicate? in
22+
let key = #selector(getter: StorageProduct.productTypeKey)
23+
return NSPredicate(format: "\(key) == %@", productType.rawValue)
24+
}
25+
26+
let subpredicates = [siteIDPredicate, stockStatusPredicate, productStatusPredicate, productTypePredicate].compactMap({ $0 })
27+
28+
return NSCompoundPredicate(andPredicateWithSubpredicates: subpredicates)
29+
}
30+
}
31+
32+
extension ResultsController where T: StorageProduct {
33+
public func updatePredicate(siteID: Int64, stockStatus: ProductStockStatus? = nil, productStatus: ProductStatus? = nil, productType: ProductType? = nil) {
34+
self.predicate = NSPredicate.createProductPredicate(siteID: siteID, stockStatus: stockStatus, productStatus: productStatus, productType: productType)
35+
}
36+
}

Yosemite/YosemiteTests/Mockups/MockProduct.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ final class MockProduct {
77
productID: Int64 = 2020,
88
dateCreated: Date = Date(),
99
name: String = "Hogsmeade",
10+
productStatus: ProductStatus = .publish,
11+
productType: ProductType = .simple,
1012
sku: String? = nil,
1113
stockQuantity: Int? = nil,
1214
stockStatus: ProductStockStatus = .inStock,
@@ -24,8 +26,8 @@ final class MockProduct {
2426
dateModified: Date(),
2527
dateOnSaleStart: date(with: "2019-10-15T21:30:00"),
2628
dateOnSaleEnd: date(with: "2019-10-27T21:29:59"),
27-
productTypeKey: "booking",
28-
statusKey: "publish",
29+
productTypeKey: productType.rawValue,
30+
statusKey: productStatus.rawValue,
2931
featured: false,
3032
catalogVisibilityKey: "visible",
3133
fullDescription: "<p>This is the party room!</p>\n",
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import XCTest
2+
@testable import Networking
3+
4+
extension MockupNetwork {
5+
/// Returns the parameters ("\(key)=\(value)") for the WC API path in the first network request URL.
6+
var pathComponents: [String]? {
7+
guard let request = requestsForResponseData.first,
8+
let urlRequest = try? request.asURLRequest(),
9+
let url = urlRequest.url,
10+
requestsForResponseData.count == 1 else {
11+
return nil
12+
}
13+
guard let urlComponents = URLComponents(string: url.absoluteString) else {
14+
return nil
15+
}
16+
let parameters = urlComponents.queryItems
17+
18+
guard let path = parameters?.first(where: { $0.name == "path" })?.value else {
19+
return nil
20+
}
21+
return path.components(separatedBy: "&")
22+
}
23+
}

0 commit comments

Comments
 (0)