Skip to content
This repository was archived by the owner on Feb 4, 2025. It is now read-only.

Commit ed42a8a

Browse files
authored
Merge pull request #1592 from wordpress-mobile/woo/add-product-categories
Woo/add product categories
2 parents 0ad6465 + 49e3fff commit ed42a8a

File tree

8 files changed

+193
-2
lines changed

8 files changed

+193
-2
lines changed

example/src/androidTest/java/org/wordpress/android/fluxc/mocked/MockedStack_WCProductsTest.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import org.wordpress.android.fluxc.TestUtils
1313
import org.wordpress.android.fluxc.action.WCProductAction
1414
import org.wordpress.android.fluxc.annotations.action.Action
1515
import org.wordpress.android.fluxc.model.SiteModel
16+
import org.wordpress.android.fluxc.model.WCProductCategoryModel
1617
import org.wordpress.android.fluxc.model.WCProductImageModel
1718
import org.wordpress.android.fluxc.model.WCProductModel
1819
import org.wordpress.android.fluxc.module.ResponseMockingInterceptor
@@ -21,6 +22,7 @@ import org.wordpress.android.fluxc.persistence.ProductSqlUtils
2122
import org.wordpress.android.fluxc.persistence.SiteSqlUtils
2223
import org.wordpress.android.fluxc.store.WCProductStore.FetchProductReviewsResponsePayload
2324
import org.wordpress.android.fluxc.store.WCProductStore.ProductErrorType
25+
import org.wordpress.android.fluxc.store.WCProductStore.RemoteAddProductCategoryResponsePayload
2426
import org.wordpress.android.fluxc.store.WCProductStore.RemoteProductCategoriesPayload
2527
import org.wordpress.android.fluxc.store.WCProductStore.RemoteProductListPayload
2628
import org.wordpress.android.fluxc.store.WCProductStore.RemoteProductPayload
@@ -665,6 +667,27 @@ class MockedStack_WCProductsTest : MockedStack_Base() {
665667
assertEquals(productAfter!!.getImages().size, 1)
666668
}
667669

670+
@Test
671+
fun testAddProductCategorySuccess() {
672+
interceptor.respondWith("wc-add-product-category-response-success.json")
673+
674+
val productCategoryModel = WCProductCategoryModel().apply { name = "test12" }
675+
productRestClient.addProductCategory(siteModel, productCategoryModel)
676+
677+
countDownLatch = CountDownLatch(1)
678+
assertTrue(countDownLatch.await(TestUtils.DEFAULT_TIMEOUT_MS.toLong(), TimeUnit.MILLISECONDS))
679+
680+
assertEquals(WCProductAction.ADDED_PRODUCT_CATEGORY, lastAction!!.type)
681+
val payload = lastAction!!.payload as RemoteAddProductCategoryResponsePayload
682+
assertFalse(payload.isError)
683+
assertEquals(siteModel.id, payload.site.id)
684+
assertEquals(productCategoryModel.name, payload.category?.name)
685+
686+
// Save product categories to the database
687+
assertEquals(1, ProductSqlUtils.insertOrUpdateProductCategory(payload.category!!))
688+
assertEquals(1, ProductSqlUtils.getProductCategoriesForSite(siteModel).size)
689+
}
690+
668691
@Test
669692
fun testFetchProductCategoriesSuccess() {
670693
interceptor.respondWith("wc-fetch-all-product-categories-response-success.json")

example/src/androidTest/java/org/wordpress/android/fluxc/release/ReleaseStack_WCProductTest.kt

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import org.wordpress.android.fluxc.action.WCProductAction
1313
import org.wordpress.android.fluxc.example.BuildConfig
1414
import org.wordpress.android.fluxc.generated.MediaActionBuilder
1515
import org.wordpress.android.fluxc.generated.WCProductActionBuilder
16+
import org.wordpress.android.fluxc.model.WCProductCategoryModel
1617
import org.wordpress.android.fluxc.model.WCProductImageModel
1718
import org.wordpress.android.fluxc.model.WCProductModel
1819
import org.wordpress.android.fluxc.network.rest.wpcom.wc.product.CoreProductStatus
@@ -22,6 +23,7 @@ import org.wordpress.android.fluxc.persistence.ProductSqlUtils
2223
import org.wordpress.android.fluxc.store.MediaStore
2324
import org.wordpress.android.fluxc.store.MediaStore.OnMediaListFetched
2425
import org.wordpress.android.fluxc.store.WCProductStore
26+
import org.wordpress.android.fluxc.store.WCProductStore.AddProductCategoryPayload
2527
import org.wordpress.android.fluxc.store.WCProductStore.FetchProductCategoriesPayload
2628
import org.wordpress.android.fluxc.store.WCProductStore.FetchProductPasswordPayload
2729
import org.wordpress.android.fluxc.store.WCProductStore.FetchProductReviewsPayload
@@ -45,6 +47,7 @@ import org.wordpress.android.fluxc.store.WCProductStore.UpdateProductReviewStatu
4547
import java.util.concurrent.CountDownLatch
4648
import java.util.concurrent.TimeUnit.MILLISECONDS
4749
import javax.inject.Inject
50+
import kotlin.random.Random
4851

4952
class ReleaseStack_WCProductTest : ReleaseStack_WCBase() {
5053
internal enum class TestEvent {
@@ -61,7 +64,8 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() {
6164
UPDATED_PRODUCT_REVIEW_STATUS,
6265
UPDATED_PRODUCT_IMAGES,
6366
UPDATED_PRODUCT_PASSWORD,
64-
FETCH_PRODUCT_CATEGORIES
67+
FETCH_PRODUCT_CATEGORIES,
68+
ADDED_PRODUCT_CATEGORY
6569
}
6670

6771
@Inject internal lateinit var productStore: WCProductStore
@@ -232,6 +236,31 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() {
232236
assertTrue(fetchAllCategories.isNotEmpty())
233237
}
234238

239+
@Throws(InterruptedException::class)
240+
@Test
241+
fun testAddProductCategory() {
242+
// Remove all product categories from the database
243+
ProductSqlUtils.deleteAllProductCategories()
244+
assertEquals(0, ProductSqlUtils.getProductCategoriesForSite(sSite).size)
245+
246+
nextEvent = TestEvent.ADDED_PRODUCT_CATEGORY
247+
mCountDownLatch = CountDownLatch(1)
248+
249+
val productCategoryModel = WCProductCategoryModel().apply {
250+
// duplicate category names fail in the API level so added a random number next to the "Test"
251+
name = "Test" + Random.nextInt(0, 10000)
252+
}
253+
mDispatcher.dispatch(WCProductActionBuilder.newAddProductCategoryAction(
254+
AddProductCategoryPayload(sSite, productCategoryModel))
255+
)
256+
assertTrue(mCountDownLatch.await(TestUtils.DEFAULT_TIMEOUT_MS.toLong(), MILLISECONDS))
257+
258+
// Verify results
259+
val fetchAllCategories = productStore.getProductCategoriesForSite(sSite)
260+
assertTrue(fetchAllCategories.isNotEmpty())
261+
assertTrue(fetchAllCategories.size == 1)
262+
}
263+
235264
@Throws(InterruptedException::class)
236265
@Test
237266
fun testFetchProductReviews() {
@@ -657,6 +686,10 @@ class ReleaseStack_WCProductTest : ReleaseStack_WCBase() {
657686
assertEquals(TestEvent.FETCH_PRODUCT_CATEGORIES, nextEvent)
658687
mCountDownLatch.countDown()
659688
}
689+
WCProductAction.ADDED_PRODUCT_CATEGORY -> {
690+
assertEquals(TestEvent.ADDED_PRODUCT_CATEGORY, nextEvent)
691+
mCountDownLatch.countDown()
692+
}
660693
else -> throw AssertionError("Unexpected cause of change: " + event.causeOfChange)
661694
}
662695
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"data": {
3+
"id": 105,
4+
"name": "test12",
5+
"slug": "test12",
6+
"parent": 0,
7+
"description": "",
8+
"display": "default",
9+
"image": null,
10+
"menu_order": 0,
11+
"count": 0,
12+
"_links": {
13+
"self": [{
14+
"href": "https:\/\/awootestshop.mystagingwebsite.com\/wp-json\/wc\/v3\/products\/categories\/105"
15+
}],
16+
"collection": [{
17+
"href": "https:\/\/awootestshop.mystagingwebsite.com\/wp-json\/wc\/v3\/products\/categories"
18+
}]
19+
}
20+
}
21+
}

example/src/main/java/org/wordpress/android/fluxc/example/ui/products/WooProductsFragment.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import kotlinx.android.synthetic.main.fragment_woo_products.*
1212
import org.greenrobot.eventbus.Subscribe
1313
import org.greenrobot.eventbus.ThreadMode
1414
import org.wordpress.android.fluxc.Dispatcher
15+
import org.wordpress.android.fluxc.action.WCProductAction.ADDED_PRODUCT_CATEGORY
1516
import org.wordpress.android.fluxc.action.WCProductAction.FETCH_PRODUCTS
1617
import org.wordpress.android.fluxc.action.WCProductAction.FETCH_PRODUCT_CATEGORIES
1718
import org.wordpress.android.fluxc.action.WCProductAction.FETCH_PRODUCT_REVIEWS
@@ -27,9 +28,11 @@ import org.wordpress.android.fluxc.example.ui.StoreSelectorDialog
2728
import org.wordpress.android.fluxc.example.utils.showSingleLineDialog
2829
import org.wordpress.android.fluxc.generated.WCProductActionBuilder
2930
import org.wordpress.android.fluxc.model.SiteModel
31+
import org.wordpress.android.fluxc.model.WCProductCategoryModel
3032
import org.wordpress.android.fluxc.model.WCProductImageModel
3133
import org.wordpress.android.fluxc.store.MediaStore
3234
import org.wordpress.android.fluxc.store.WCProductStore
35+
import org.wordpress.android.fluxc.store.WCProductStore.AddProductCategoryPayload
3336
import org.wordpress.android.fluxc.store.WCProductStore.FetchProductCategoriesPayload
3437
import org.wordpress.android.fluxc.store.WCProductStore.FetchProductReviewsPayload
3538
import org.wordpress.android.fluxc.store.WCProductStore.FetchProductShippingClassListPayload
@@ -257,6 +260,23 @@ class WooProductsFragment : Fragment() {
257260
}
258261
}
259262

263+
add_product_category.setOnClickListener {
264+
selectedSite?.let { site ->
265+
showSingleLineDialog(
266+
activity,
267+
"Enter a catrgory name:"
268+
) { editText ->
269+
val categoryName = editText.text.toString()
270+
if (categoryName.isNotEmpty()) {
271+
prependToLog("Submitting request to add product category")
272+
val wcProductCategoryModel = WCProductCategoryModel().apply { name = categoryName }
273+
val payload = AddProductCategoryPayload(site, wcProductCategoryModel)
274+
dispatcher.dispatch(WCProductActionBuilder.newAddProductCategoryAction(payload))
275+
} else prependToLog("No category name entered...doing nothing")
276+
}
277+
}
278+
}
279+
260280
update_product_images.setOnClickListener {
261281
showSingleLineDialog(
262282
activity,
@@ -414,6 +434,9 @@ class WooProductsFragment : Fragment() {
414434
load_more_product_categories.isEnabled = false
415435
}
416436
}
437+
ADDED_PRODUCT_CATEGORY -> {
438+
prependToLog("${event.rowsAffected} product category added")
439+
} else -> { }
417440
}
418441
}
419442
}

example/src/main/res/layout/fragment_woo_products.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,13 @@
144144
android:visibility="gone"
145145
android:text="Load More Product Categories" />
146146

147+
<Button
148+
android:id="@+id/add_product_category"
149+
android:layout_width="wrap_content"
150+
android:layout_height="wrap_content"
151+
android:enabled="false"
152+
android:text="Add Product Category" />
153+
147154
<Button
148155
android:id="@+id/update_product_images"
149156
android:layout_width="wrap_content"

plugins/woocommerce/src/main/java/org/wordpress/android/fluxc/action/WCProductAction.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import org.wordpress.android.fluxc.annotations.Action;
44
import org.wordpress.android.fluxc.annotations.ActionEnum;
55
import org.wordpress.android.fluxc.annotations.action.IAction;
6+
import org.wordpress.android.fluxc.store.WCProductStore.AddProductCategoryPayload;
67
import org.wordpress.android.fluxc.store.WCProductStore.FetchProductCategoriesPayload;
78
import org.wordpress.android.fluxc.store.WCProductStore.FetchProductPasswordPayload;
89
import org.wordpress.android.fluxc.store.WCProductStore.FetchProductReviewsPayload;
@@ -14,6 +15,7 @@
1415
import org.wordpress.android.fluxc.store.WCProductStore.FetchSingleProductPayload;
1516
import org.wordpress.android.fluxc.store.WCProductStore.FetchSingleProductReviewPayload;
1617
import org.wordpress.android.fluxc.store.WCProductStore.FetchSingleProductShippingClassPayload;
18+
import org.wordpress.android.fluxc.store.WCProductStore.RemoteAddProductCategoryResponsePayload;
1719
import org.wordpress.android.fluxc.store.WCProductStore.RemoteProductCategoriesPayload;
1820
import org.wordpress.android.fluxc.store.WCProductStore.RemoteProductListPayload;
1921
import org.wordpress.android.fluxc.store.WCProductStore.RemoteProductPasswordPayload;
@@ -66,6 +68,8 @@ public enum WCProductAction implements IAction {
6668
UPDATE_PRODUCT_PASSWORD,
6769
@Action(payloadType = FetchProductCategoriesPayload.class)
6870
FETCH_PRODUCT_CATEGORIES,
71+
@Action(payloadType = AddProductCategoryPayload.class)
72+
ADD_PRODUCT_CATEGORY,
6973

7074

7175
// Remote responses
@@ -98,5 +102,7 @@ public enum WCProductAction implements IAction {
98102
@Action(payloadType = RemoteUpdatedProductPasswordPayload.class)
99103
UPDATED_PRODUCT_PASSWORD,
100104
@Action(payloadType = RemoteProductCategoriesPayload.class)
101-
FETCHED_PRODUCT_CATEGORIES
105+
FETCHED_PRODUCT_CATEGORIES,
106+
@Action(payloadType = RemoteAddProductCategoryResponsePayload.class)
107+
ADDED_PRODUCT_CATEGORY
102108
}

plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/network/rest/wpcom/wc/product/ProductRestClient.kt

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import org.wordpress.android.fluxc.store.WCProductStore.ProductSorting.DATE_ASC
4444
import org.wordpress.android.fluxc.store.WCProductStore.ProductSorting.DATE_DESC
4545
import org.wordpress.android.fluxc.store.WCProductStore.ProductSorting.TITLE_ASC
4646
import org.wordpress.android.fluxc.store.WCProductStore.ProductSorting.TITLE_DESC
47+
import org.wordpress.android.fluxc.store.WCProductStore.RemoteAddProductCategoryResponsePayload
4748
import org.wordpress.android.fluxc.store.WCProductStore.RemoteProductCategoriesPayload
4849
import org.wordpress.android.fluxc.store.WCProductStore.RemoteProductListPayload
4950
import org.wordpress.android.fluxc.store.WCProductStore.RemoteProductPasswordPayload
@@ -550,6 +551,43 @@ class ProductRestClient(
550551
add(request)
551552
}
552553

554+
/**
555+
* Posts a new Add Category record to the API for a category.
556+
*
557+
* Makes a POST call `/wc/v3/products/categories/id` to save a Category record via the Jetpack tunnel.
558+
* Returns a [WCProductCategoryModel] on successful response.
559+
*
560+
* Dispatches [WCProductAction.ADDED_PRODUCT_CATEGORY] action with the results.
561+
*/
562+
fun addProductCategory(
563+
site: SiteModel,
564+
category: WCProductCategoryModel
565+
) {
566+
val url = WOOCOMMERCE.products.categories.pathV3
567+
568+
val responseType = object : TypeToken<ProductCategoryApiResponse>() {}.type
569+
val params = mutableMapOf(
570+
"name" to category.name,
571+
"parent" to category.parent.toString()
572+
)
573+
val request = JetpackTunnelGsonRequest.buildPostRequest(url, site.siteId, params, responseType,
574+
{ response: ProductCategoryApiResponse? ->
575+
val categoryResponse = response?.let {
576+
productCategoryResponseToProductCategoryModel(it).apply {
577+
localSiteId = site.id
578+
}
579+
}
580+
val payload = RemoteAddProductCategoryResponsePayload(site, categoryResponse)
581+
dispatcher.dispatch(WCProductActionBuilder.newAddedProductCategoryAction(payload))
582+
},
583+
WPComErrorListener { networkError ->
584+
val productCategorySaveError = networkErrorToProductError(networkError)
585+
val payload = RemoteAddProductCategoryResponsePayload(productCategorySaveError, site, category)
586+
dispatcher.dispatch(WCProductActionBuilder.newAddedProductCategoryAction(payload))
587+
})
588+
add(request)
589+
}
590+
553591
/**
554592
* Makes a GET call to `/wc/v3/products/reviews` via the Jetpack tunnel (see [JetpackTunnelGsonRequest]),
555593
* retrieving a list of product reviews for a given WooCommerce [SiteModel].
@@ -980,6 +1018,7 @@ class ProductRestClient(
9801018
"woocommerce_rest_review_invalid_id" -> ProductErrorType.INVALID_REVIEW_ID
9811019
"woocommerce_product_invalid_image_id" -> ProductErrorType.INVALID_IMAGE_ID
9821020
"product_invalid_sku" -> ProductErrorType.DUPLICATE_SKU
1021+
"term_exists" -> ProductErrorType.TERM_EXISTS
9831022
else -> ProductErrorType.fromString(wpComError.apiError)
9841023
}
9851024
return ProductError(productErrorType, wpComError.message)

plugins/woocommerce/src/main/kotlin/org/wordpress/android/fluxc/store/WCProductStore.kt

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,11 +139,17 @@ class WCProductStore @Inject constructor(dispatcher: Dispatcher, private val wcP
139139
var productCategorySorting: ProductCategorySorting = DEFAULT_CATEGORY_SORTING
140140
) : Payload<BaseNetworkError>()
141141

142+
class AddProductCategoryPayload(
143+
val site: SiteModel,
144+
val category: WCProductCategoryModel
145+
) : Payload<BaseNetworkError>()
146+
142147
enum class ProductErrorType {
143148
INVALID_PARAM,
144149
INVALID_REVIEW_ID,
145150
INVALID_IMAGE_ID,
146151
DUPLICATE_SKU,
152+
TERM_EXISTS, // indicates duplicate term name. Currently only used when adding product categories
147153
GENERIC_ERROR;
148154

149155
companion object {
@@ -361,6 +367,17 @@ class WCProductStore @Inject constructor(dispatcher: Dispatcher, private val wcP
361367
}
362368
}
363369

370+
class RemoteAddProductCategoryResponsePayload(
371+
val site: SiteModel,
372+
val category: WCProductCategoryModel?
373+
) : Payload<ProductError>() {
374+
constructor(
375+
error: ProductError,
376+
site: SiteModel,
377+
category: WCProductCategoryModel?
378+
) : this(site, category) { this.error = error }
379+
}
380+
364381
// OnChanged events
365382
class OnProductChanged(
366383
var rowsAffected: Int,
@@ -541,6 +558,8 @@ class WCProductStore @Inject constructor(dispatcher: Dispatcher, private val wcP
541558
updateProductPassword(action.payload as UpdateProductPasswordPayload)
542559
WCProductAction.FETCH_PRODUCT_CATEGORIES ->
543560
fetchProductCategories(action.payload as FetchProductCategoriesPayload)
561+
WCProductAction.ADD_PRODUCT_CATEGORY ->
562+
addProductCategory(action.payload as AddProductCategoryPayload)
544563

545564
// remote responses
546565
WCProductAction.FETCHED_SINGLE_PRODUCT ->
@@ -573,6 +592,8 @@ class WCProductStore @Inject constructor(dispatcher: Dispatcher, private val wcP
573592
handleUpdatedProductPasswordCompleted(action.payload as RemoteUpdatedProductPasswordPayload)
574593
WCProductAction.FETCHED_PRODUCT_CATEGORIES ->
575594
handleFetchProductCategories(action.payload as RemoteProductCategoriesPayload)
595+
WCProductAction.ADDED_PRODUCT_CATEGORY ->
596+
handleAddProductCategory(action.payload as RemoteAddProductCategoryResponsePayload)
576597
}
577598
}
578599

@@ -641,6 +662,10 @@ class WCProductStore @Inject constructor(dispatcher: Dispatcher, private val wcP
641662
site, pageSize, offset, productCategorySorting) }
642663
}
643664

665+
private fun addProductCategory(payload: AddProductCategoryPayload) {
666+
with(payload) { wcProductRestClient.addProductCategory(site, category) }
667+
}
668+
644669
private fun updateProduct(payload: UpdateProductPayload) {
645670
with(payload) {
646671
val storedProduct = getProductByRemoteId(site, product.remoteProductId)
@@ -880,4 +905,18 @@ class WCProductStore @Inject constructor(dispatcher: Dispatcher, private val wcP
880905
onProductCategoryChanged.causeOfChange = WCProductAction.FETCH_PRODUCT_CATEGORIES
881906
emitChange(onProductCategoryChanged)
882907
}
908+
909+
private fun handleAddProductCategory(payload: RemoteAddProductCategoryResponsePayload) {
910+
val onProductCategoryChanged: OnProductCategoryChanged
911+
912+
if (payload.isError) {
913+
onProductCategoryChanged = OnProductCategoryChanged(0).also { it.error = payload.error }
914+
} else {
915+
val rowsAffected = payload.category?.let { ProductSqlUtils.insertOrUpdateProductCategory(it) } ?: 0
916+
onProductCategoryChanged = OnProductCategoryChanged(rowsAffected)
917+
}
918+
919+
onProductCategoryChanged.causeOfChange = WCProductAction.ADDED_PRODUCT_CATEGORY
920+
emitChange(onProductCategoryChanged)
921+
}
883922
}

0 commit comments

Comments
 (0)