Skip to content

Commit a75994f

Browse files
authored
Merge pull request #7739 from woocommerce/feat/3157-search-sku
Search products by SKU: add a segmented control to products search
2 parents 66f8e13 + 2b830da commit a75994f

File tree

24 files changed

+556
-45
lines changed

24 files changed

+556
-45
lines changed

Experiments/Experiments/DefaultFeatureFlagService.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ public struct DefaultFeatureFlagService: FeatureFlagService {
3333
return true
3434
case .promptToEnableCodInIppOnboarding:
3535
return true
36+
case .searchProductsBySKU:
37+
return buildConfig == .localDeveloper || buildConfig == .alpha
3638
case .orderCreationSearchCustomers:
3739
return buildConfig == .localDeveloper || buildConfig == .alpha
3840
default:

Experiments/Experiments/FeatureFlag.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ public enum FeatureFlag: Int {
7070
///
7171
case promptToEnableCodInIppOnboarding
7272

73+
/// Enables searching products by partial SKU for WC version 6.6+.
74+
///
75+
case searchProductsBySKU
76+
7377
/// Enables the Search Customers functionality in the Order Creation screen
7478
///
7579
case orderCreationSearchCustomers

Networking/Networking.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
025CA2C4238EBC4300B05C81 /* ProductShippingClassRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025CA2C3238EBC4300B05C81 /* ProductShippingClassRemote.swift */; };
3131
025CA2C6238F4F3500B05C81 /* ProductShippingClassRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025CA2C5238F4F3500B05C81 /* ProductShippingClassRemoteTests.swift */; };
3232
025CA2C8238F4FF400B05C81 /* product-shipping-classes-load-all.json in Resources */ = {isa = PBXBuildFile; fileRef = 025CA2C7238F4FF400B05C81 /* product-shipping-classes-load-all.json */; };
33+
0261F5A928D4641500B7AC72 /* products-sku-search.json in Resources */ = {isa = PBXBuildFile; fileRef = 0261F5A828D4641500B7AC72 /* products-sku-search.json */; };
3334
02698CF624C17FC1005337C4 /* product-alternative-types.json in Resources */ = {isa = PBXBuildFile; fileRef = 02698CF524C17FC1005337C4 /* product-alternative-types.json */; };
3435
02698CF824C183A5005337C4 /* ProductVariationListMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02698CF724C183A5005337C4 /* ProductVariationListMapperTests.swift */; };
3536
02698CFA24C188E9005337C4 /* product-variations-load-all-alternative-types.json in Resources */ = {isa = PBXBuildFile; fileRef = 02698CF924C188E8005337C4 /* product-variations-load-all-alternative-types.json */; };
@@ -727,6 +728,7 @@
727728
025CA2C3238EBC4300B05C81 /* ProductShippingClassRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductShippingClassRemote.swift; sourceTree = "<group>"; };
728729
025CA2C5238F4F3500B05C81 /* ProductShippingClassRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductShippingClassRemoteTests.swift; sourceTree = "<group>"; };
729730
025CA2C7238F4FF400B05C81 /* product-shipping-classes-load-all.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "product-shipping-classes-load-all.json"; sourceTree = "<group>"; };
731+
0261F5A828D4641500B7AC72 /* products-sku-search.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "products-sku-search.json"; sourceTree = "<group>"; };
730732
02698CF524C17FC1005337C4 /* product-alternative-types.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "product-alternative-types.json"; sourceTree = "<group>"; };
731733
02698CF724C183A5005337C4 /* ProductVariationListMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductVariationListMapperTests.swift; sourceTree = "<group>"; };
732734
02698CF924C188E8005337C4 /* product-variations-load-all-alternative-types.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "product-variations-load-all-alternative-types.json"; sourceTree = "<group>"; };
@@ -1999,6 +2001,7 @@
19992001
451274A525276C82009911FF /* product-variation.json */,
20002002
CE0A0F1E223998A00075ED8D /* products-load-all.json */,
20012003
0282DD90233A120A006A5FDB /* products-search-photo.json */,
2004+
0261F5A828D4641500B7AC72 /* products-sku-search.json */,
20022005
45D685F923D0C3CF005F87D0 /* product-search-sku.json */,
20032006
4599FC5B24A6276F0056157A /* product-tags-all.json */,
20042007
45AF57A824AB42CD0088E2F7 /* product-tags-extra.json */,
@@ -2518,6 +2521,7 @@
25182521
D865CE5F278CA183002C8520 /* stripe-payment-intent-requires-action.json in Resources */,
25192522
CCF48B2C2628AE160034EA83 /* shipping-label-account-settings.json in Resources */,
25202523
31A451D927863A2E00FE81AA /* stripe-account-live-test.json in Resources */,
2524+
0261F5A928D4641500B7AC72 /* products-sku-search.json in Resources */,
25212525
09885C8027C3FFD200910A62 /* product-variations-bulk-update.json in Resources */,
25222526
31054734262E36AB00C5C02B /* wcpay-payment-intent-error.json in Resources */,
25232527
02AF07EE27493AE700B2D81E /* media-upload-to-wordpress-site.json in Resources */,

Networking/Networking/Remote/ProductsRemote.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ public protocol ProductsRemoteProtocol {
3131
productCategory: ProductCategory?,
3232
excludedProductIDs: [Int64],
3333
completion: @escaping (Result<[Product], Error>) -> Void)
34+
func searchProductsBySKU(for siteID: Int64,
35+
keyword: String,
36+
pageNumber: Int,
37+
pageSize: Int,
38+
completion: @escaping (Result<[Product], Error>) -> Void)
3439
func searchSku(for siteID: Int64,
3540
sku: String,
3641
completion: @escaping (Result<String, Error>) -> Void)
@@ -238,6 +243,30 @@ public final class ProductsRemote: Remote, ProductsRemoteProtocol {
238243
enqueue(request, mapper: mapper, completion: completion)
239244
}
240245

246+
/// Retrieves all of the `Product`s that match the SKU. Partial SKU search is supported for WooCommerce version 6.6+, otherwise full SKU match is performed.
247+
/// - Parameters:
248+
/// - siteID: Site for which we'll fetch remote products
249+
/// - keyword: Search string that should be matched by the SKU (partial or full depending on the WC version).
250+
/// - pageNumber: Number of page that should be retrieved.
251+
/// - pageSize: Number of products to be retrieved per page.
252+
/// - completion: Closure to be executed upon completion.
253+
public func searchProductsBySKU(for siteID: Int64,
254+
keyword: String,
255+
pageNumber: Int,
256+
pageSize: Int,
257+
completion: @escaping (Result<[Product], Error>) -> Void) {
258+
let parameters = [
259+
ParameterKey.sku: keyword,
260+
ParameterKey.partialSKUSearch: keyword,
261+
ParameterKey.page: String(pageNumber),
262+
ParameterKey.perPage: String(pageSize)
263+
]
264+
let path = Path.products
265+
let request = JetpackRequest(wooApiVersion: .mark3, method: .get, siteID: siteID, path: path, parameters: parameters)
266+
let mapper = ProductListMapper(siteID: siteID)
267+
enqueue(request, mapper: mapper, completion: completion)
268+
}
269+
241270
/// Retrieves a product SKU if available.
242271
///
243272
/// - Parameters:
@@ -329,6 +358,7 @@ public extension ProductsRemote {
329358
static let orderBy: String = "orderby"
330359
static let order: String = "order"
331360
static let sku: String = "sku"
361+
static let partialSKUSearch: String = "search_sku"
332362
static let productStatus: String = "status"
333363
static let productType: String = "type"
334364
static let stockStatus: String = "stock_status"

Networking/NetworkingTests/Remote/ProductsRemoteTests.swift

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ final class ProductsRemoteTests: XCTestCase {
395395

396396
/// Verifies that searchProducts properly relays Networking Layer errors.
397397
///
398-
func test_searchProducts_properly_relays_netwoking_errors() {
398+
func test_searchProducts_properly_relays_networking_errors() {
399399
// Given
400400
let remote = ProductsRemote(network: network)
401401

@@ -413,6 +413,47 @@ final class ProductsRemoteTests: XCTestCase {
413413
XCTAssertTrue(result.isFailure)
414414
}
415415

416+
// MARK: - Search Products by SKU
417+
418+
func test_searchProductsBySKU_properly_returns_parsed_products() throws {
419+
// Given
420+
let remote = ProductsRemote(network: network)
421+
network.simulateResponse(requestUrlSuffix: "products", filename: "products-sku-search")
422+
423+
// When
424+
let result: Result<[Product], Error> = waitFor { promise in
425+
remote.searchProductsBySKU(for: self.sampleSiteID,
426+
keyword: "choco",
427+
pageNumber: 0,
428+
pageSize: 100) { result in
429+
promise(result)
430+
}
431+
}
432+
433+
// Then
434+
XCTAssertTrue(result.isSuccess)
435+
let products = try result.get()
436+
XCTAssertEqual(products.count, 1)
437+
}
438+
439+
func test_searchProductsBySKU_properly_relays_networking_errors() {
440+
// Given
441+
let remote = ProductsRemote(network: network)
442+
443+
// When
444+
let result: Result<[Product], Error> = waitFor { promise in
445+
remote.searchProductsBySKU(for: self.sampleSiteID,
446+
keyword: String(),
447+
pageNumber: 0,
448+
pageSize: 100) { result in
449+
promise(result)
450+
}
451+
}
452+
453+
// Then
454+
XCTAssertTrue(result.isFailure)
455+
}
456+
416457

417458
// MARK: - Search Product SKU tests
418459

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
{
2+
"data": [
3+
{
4+
"id": 2783,
5+
"name": "Chocolate bars",
6+
"slug": "variable-chocolate-bar",
7+
"permalink": "",
8+
"date_created": "2020-06-12T22:36:02",
9+
"date_created_gmt": "2020-06-12T14:36:02",
10+
"date_modified": "2021-02-23T10:46:54",
11+
"date_modified_gmt": "2021-02-23T02:46:54",
12+
"type": "variation",
13+
"status": "publish",
14+
"featured": false,
15+
"catalog_visibility": "visible",
16+
"description": "<p>Tasty!!</p>\n",
17+
"short_description": "",
18+
"sku": "chocobars",
19+
"price": "22",
20+
"regular_price": "999",
21+
"sale_price": "22",
22+
"date_on_sale_from": null,
23+
"date_on_sale_from_gmt": null,
24+
"date_on_sale_to": null,
25+
"date_on_sale_to_gmt": null,
26+
"on_sale": true,
27+
"purchasable": true,
28+
"total_sales": "0",
29+
"virtual": false,
30+
"downloadable": false,
31+
"downloads": [],
32+
"download_limit": -1,
33+
"download_expiry": -1,
34+
"external_url": "",
35+
"button_text": "",
36+
"tax_status": "taxable",
37+
"tax_class": "reduced-rate",
38+
"manage_stock": "parent",
39+
"stock_quantity": 7774,
40+
"backorders": "no",
41+
"backorders_allowed": false,
42+
"backordered": false,
43+
"low_stock_amount": null,
44+
"sold_individually": false,
45+
"weight": "67",
46+
"dimensions": {
47+
"length": "6",
48+
"width": "",
49+
"height": "1"
50+
},
51+
"shipping_required": true,
52+
"shipping_taxable": true,
53+
"shipping_class": "10-day-shipping",
54+
"shipping_class_id": 96987521,
55+
"reviews_allowed": false,
56+
"average_rating": "0.00",
57+
"rating_count": 0,
58+
"upsell_ids": [],
59+
"cross_sell_ids": [],
60+
"parent_id": 846,
61+
"purchase_note": "",
62+
"categories": [],
63+
"tags": [],
64+
"images": [],
65+
"attributes": [
66+
{
67+
"id": 0,
68+
"name": "Darkness",
69+
"option": "87%"
70+
}
71+
],
72+
"default_attributes": [],
73+
"variations": [],
74+
"grouped_products": [],
75+
"menu_order": 6,
76+
"related_ids": [],
77+
"meta_data": [],
78+
"stock_status": "instock",
79+
"has_options": false,
80+
}
81+
]
82+
}

Storage/Storage/Tools/StorageType+Extensions.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,8 +345,8 @@ public extension StorageType {
345345

346346
/// Retrieves the Stored ProductSearchResults Lookup.
347347
///
348-
func loadProductSearchResults(keyword: String) -> ProductSearchResults? {
349-
let predicate = \ProductSearchResults.keyword == keyword
348+
func loadProductSearchResults(keyword: String, filterKey: String) -> ProductSearchResults? {
349+
let predicate = \ProductSearchResults.keyword == keyword && \ProductSearchResults.filterKey == filterKey
350350
return firstObject(ofType: ProductSearchResults.self, matching: predicate)
351351
}
352352

Storage/StorageTests/Tools/StorageTypeExtensionsTests.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -692,11 +692,13 @@ final class StorageTypeExtensionsTests: XCTestCase {
692692
func test_loadProductSearchResult_by_keyboard() throws {
693693
// Given
694694
let keyword = "Keyword"
695+
let filterKey = "all"
695696
let searchResult = storage.insertNewObject(ofType: ProductSearchResults.self)
696697
searchResult.keyword = keyword
698+
searchResult.filterKey = filterKey
697699

698700
// When
699-
let storedSearchResult = try XCTUnwrap(storage.loadProductSearchResults(keyword: keyword))
701+
let storedSearchResult = try XCTUnwrap(storage.loadProductSearchResults(keyword: keyword, filterKey: filterKey))
700702

701703
// Then
702704
XCTAssertEqual(searchResult, storedSearchResult)

WooCommerce/Classes/ViewRelated/Coupons/CouponSearchUICommand.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ final class CouponSearchUICommand: SearchUICommand {
1515

1616
let cancelButtonAccessibilityIdentifier = "coupon-search-screen-cancel-button"
1717

18+
var resynchronizeModels: (() -> Void) = {}
19+
1820
private let siteID: Int64
1921

2022
init(siteID: Int64) {
@@ -80,6 +82,10 @@ final class CouponSearchUICommand: SearchUICommand {
8082

8183
viewController.configure(.simple(message: message, image: .emptySearchResultsImage))
8284
}
85+
86+
func searchResultsPredicate(keyword: String) -> NSPredicate? {
87+
NSPredicate(format: "ANY searchResults.keyword = %@", keyword)
88+
}
8389
}
8490

8591
// MARK: - Subtypes

WooCommerce/Classes/ViewRelated/Products/Edit Product/Linked Products List Selector/ProductListSelector/ProductListMultiSelectorSearchUICommand.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ final class ProductListMultiSelectorSearchUICommand: NSObject, SearchUICommand {
1212

1313
let cancelButtonAccessibilityIdentifier = "product-search-screen-cancel-button"
1414

15+
var resynchronizeModels: (() -> Void) = {}
16+
1517
private let siteID: Int64
1618

1719
private let excludedProductIDs: [Int64]
@@ -90,6 +92,10 @@ final class ProductListMultiSelectorSearchUICommand: NSObject, SearchUICommand {
9092
reloadData()
9193
updateActionButton()
9294
}
95+
96+
func searchResultsPredicate(keyword: String) -> NSPredicate? {
97+
NSPredicate(format: "ANY searchResults.keyword = %@", keyword)
98+
}
9399
}
94100

95101
extension ProductListMultiSelectorSearchUICommand {

0 commit comments

Comments
 (0)