diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/mediapicker/MediaPickerUtil.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/mediapicker/MediaPickerUtil.kt index 105104befc5..750f9c4d8a7 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/mediapicker/MediaPickerUtil.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/mediapicker/MediaPickerUtil.kt @@ -63,7 +63,7 @@ object MediaPickerUtil { private fun handleMediaLibraryPickerResult(data: Bundle): List { return data.parcelableArrayList(MediaPickerConstants.EXTRA_REMOTE_MEDIA) - ?.map { Product.Image(it.id, it.name, it.url, DateTimeUtils.dateFromIso8601(it.date)) } + ?.map { Product.Image(it.id, it.name, it.url, DateTimeUtils.dateFromIso8601(it.date), false) } ?: emptyList() } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/model/Product.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/model/Product.kt index 208d61926bd..492095cd26b 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/model/Product.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/model/Product.kt @@ -104,7 +104,8 @@ data class Product( val id: Long, val name: String?, val source: String, - val dateCreated: Date? + val dateCreated: Date?, + val isCoverImage: Boolean ) : Parcelable fun isSameProduct(product: Product): Boolean { @@ -539,10 +540,11 @@ fun WCProductModel.toAppModel(): Product { numVariations = this.getNumVariations(), images = this.getImageListOrEmpty().map { Product.Image( - it.id, - it.name, - it.src, - DateTimeUtils.dateFromIso8601(this.dateCreated) ?: Date() + id = it.id, + name = it.name, + source = it.src, + dateCreated = DateTimeUtils.dateFromIso8601(this.dateCreated) ?: Date(), + isCoverImage = it.src == this.getFirstImageUrl() ) }, attributes = this.getAttributeList().map { it.toAppModel() }, @@ -597,7 +599,8 @@ fun MediaModel.toAppModel(): Product.Image { id = this.mediaId, name = this.fileName.orEmpty(), source = this.url, - dateCreated = DateTimeUtils.dateFromIso8601(this.uploadDate) + dateCreated = DateTimeUtils.dateFromIso8601(this.uploadDate), + isCoverImage = false ) } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/model/ProductVariation.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/model/ProductVariation.kt index b8ba8bce789..252b250d098 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/model/ProductVariation.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/model/ProductVariation.kt @@ -289,10 +289,11 @@ fun WCProductVariationModel.toAppModel(): ProductVariation { globalUniqueId = this.globalUniqueId, image = this.getImageModel()?.let { Product.Image( - it.id, - it.name, - it.src, - DateTimeUtils.dateFromIso8601(this.dateCreated) ?: Date() + id = it.id, + name = it.name, + source = it.src, + dateCreated = DateTimeUtils.dateFromIso8601(this.dateCreated) ?: Date(), + isCoverImage = false ) }, price = this.price.toBigDecimalOrNull(), diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/model/SubscriptionProductVariation.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/model/SubscriptionProductVariation.kt index f6886f796a9..cb48bb1646c 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/model/SubscriptionProductVariation.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/model/SubscriptionProductVariation.kt @@ -94,10 +94,11 @@ class SubscriptionProductVariation( globalUniqueId = model.globalUniqueId, image = model.getImageModel()?.let { Product.Image( - it.id, - it.name, - it.src, - DateTimeUtils.dateFromIso8601(model.dateCreated) ?: Date() + id = it.id, + name = it.name, + source = it.src, + dateCreated = DateTimeUtils.dateFromIso8601(model.dateCreated) ?: Date(), + isCoverImage = false ) }, price = model.price.toBigDecimalOrNull(), diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/images/ProductImagesViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/images/ProductImagesViewModel.kt index 3841f4e2e43..fc1ad4bc2c1 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/images/ProductImagesViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/images/ProductImagesViewModel.kt @@ -148,7 +148,10 @@ class ProductImagesViewModel @Inject constructor( } fun onValidateButtonClicked() { - viewState = viewState.copy(productImagesState = Browsing) + viewState = viewState.copy( + images = images.updateProductCoverImageToFirstItem(), + productImagesState = Browsing + ) } fun onNavigateBackButtonClicked() { @@ -159,6 +162,7 @@ class ProductImagesViewModel @Inject constructor( images = productImagesState.initialState ) } + Browsing -> { val hasChange = !images.areSameImagesAs(originalImages) analyticsTracker.track( @@ -229,7 +233,11 @@ class ProductImagesViewModel @Inject constructor( fun onGalleryImageDragStarted() { when (viewState.productImagesState) { is Dragging -> { /* no-op*/ } - Browsing -> viewState = viewState.copy(productImagesState = Dragging(images)) + + Browsing -> viewState = viewState.copy( + images = images.uncheckProductCoverImage(), + productImagesState = Dragging(images) + ) } } @@ -249,6 +257,11 @@ class ProductImagesViewModel @Inject constructor( } } + private fun List.updateProductCoverImageToFirstItem() = + this.mapIndexed { index, image -> image.copy(isCoverImage = index == 0) } + + private fun List.uncheckProductCoverImage() = this.map { it.copy(isCoverImage = false) } + @Parcelize data class ViewState( val showSourceChooser: Boolean? = null, diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/widgets/WCProductImageGalleryView.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/widgets/WCProductImageGalleryView.kt index 1e8974febf8..bdb339e9750 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/widgets/WCProductImageGalleryView.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/widgets/WCProductImageGalleryView.kt @@ -202,7 +202,16 @@ class WCProductImageGalleryView @JvmOverloads constructor( // use a negative id so we can check it in isPlaceholder() below val id = (-index - 1).toLong() // set the image src to this uri so we can preview it while uploading - placeholders.add(0, Product.Image(id, "", imageUriList[index].toString(), Date())) + placeholders.add( + 0, + Product.Image( + id = id, + name = "", + source = imageUriList[index].toString(), + dateCreated = Date(), + isCoverImage = false + ) + ) } adapter.setPlaceholderImages(placeholders) @@ -243,13 +252,14 @@ class WCProductImageGalleryView @JvmOverloads constructor( imageList.addAll(images) // restore the "Add image" icon (never shown when list is empty) - if (showAddImageIcon && imageList.size > 0) { + if (showAddImageIcon && imageList.isNotEmpty()) { imageList.add( Product.Image( id = ADD_IMAGE_ITEM_ID, name = "", source = "", - dateCreated = Date() + dateCreated = Date(), + isCoverImage = false ) ) } @@ -290,7 +300,7 @@ class WCProductImageGalleryView @JvmOverloads constructor( } for (index in images.indices) { - if (images[index].id != actualImages[index].id) { + if (images[index] != actualImages[index]) { return false } } @@ -423,11 +433,13 @@ class WCProductImageGalleryView @JvmOverloads constructor( viewBinding.uploadProgess.visibility = View.VISIBLE viewBinding.addImageContainer.visibility = View.GONE } + VIEW_TYPE_ADD_IMAGE -> { viewBinding.productImage.visibility = View.GONE viewBinding.uploadProgess.visibility = View.GONE viewBinding.addImageContainer.visibility = View.VISIBLE } + else -> { viewBinding.productImage.visibility = View.VISIBLE viewBinding.productImage.alpha = 1.0F @@ -439,6 +451,10 @@ class WCProductImageGalleryView @JvmOverloads constructor( viewBinding.deleteImageButton.setOnClickListener { listener.onGalleryImageDeleteIconClicked(image) } + viewBinding.coverTag.visibility = when { + image.isCoverImage -> View.VISIBLE + else -> View.GONE + } } private fun setMargins() { diff --git a/WooCommerce/src/main/res/drawable/bg_rounded_box.xml b/WooCommerce/src/main/res/drawable/bg_rounded_box.xml index 8f9ebc4e578..4d90c97cbbc 100644 --- a/WooCommerce/src/main/res/drawable/bg_rounded_box.xml +++ b/WooCommerce/src/main/res/drawable/bg_rounded_box.xml @@ -1,5 +1,8 @@ - - + + diff --git a/WooCommerce/src/main/res/layout/fragment_product_images.xml b/WooCommerce/src/main/res/layout/fragment_product_images.xml index 8b3a427659d..0435dcb5b8e 100644 --- a/WooCommerce/src/main/res/layout/fragment_product_images.xml +++ b/WooCommerce/src/main/res/layout/fragment_product_images.xml @@ -60,7 +60,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" - android:text="@string/product_images_drag_and_drop_description" /> + android:text="@string/product_images_drag_and_drop_to_reorder" /> + + diff --git a/WooCommerce/src/main/res/values/strings.xml b/WooCommerce/src/main/res/values/strings.xml index 4f5debfb9f0..30c98f2d8c9 100644 --- a/WooCommerce/src/main/res/values/strings.xml +++ b/WooCommerce/src/main/res/values/strings.xml @@ -2397,6 +2397,7 @@ Add photo Replace photo Remove photo + Cover Add a product image Error removing product image Error uploading product image @@ -2416,7 +2417,7 @@ Select Media Source WordPress media library Only one photo can be displayed per product variation - Drag and drop to re-order photos + Drag and drop to re-order photos. The first photo will be set as the cover. Validate Media could not be found %d file couldn\'t be uploaded diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/products/ProductTestUtils.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/products/ProductTestUtils.kt index fde8ee82abe..96ccbaf9ceb 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/products/ProductTestUtils.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/products/ProductTestUtils.kt @@ -172,7 +172,8 @@ object ProductTestUtils { id = imageId, name = "Image $imageId", source = "Image $imageId source", - dateCreated = Date.from(Instant.EPOCH) + dateCreated = Date.from(Instant.EPOCH), + isCoverImage = false ) fun generateProductImagesList() = diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/products/ai/preview/AiProductPreviewViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/products/ai/preview/AiProductPreviewViewModelTest.kt index 1e4b9c965c4..390230d5edc 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/products/ai/preview/AiProductPreviewViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/products/ai/preview/AiProductPreviewViewModelTest.kt @@ -35,7 +35,7 @@ class AiProductPreviewViewModelTest : BaseUnitTest() { companion object { private const val PRODUCT_FEATURES = "product_features" private val SAMPLE_PRODUCT = AIProductModel.buildDefault("default name", "default description") - private val SAMPLE_UPLOADED_IMAGE = Product.Image(0, "image", "url", Date()) + private val SAMPLE_UPLOADED_IMAGE = Product.Image(0, "image", "url", Date(), false) } private val buildProductPreviewProperties: BuildProductPreviewProperties = mock() diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/products/images/ProductImagesViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/products/images/ProductImagesViewModelTest.kt index 3c8178c616e..839806aeaf9 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/products/images/ProductImagesViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/products/images/ProductImagesViewModelTest.kt @@ -19,6 +19,13 @@ import org.mockito.kotlin.verify @ExperimentalCoroutinesApi class ProductImagesViewModelTest : BaseUnitTest() { + companion object { + private val DEFAULT_PRODUCT_IMAGES = ProductTestUtils.generateProductImagesList() + .mapIndexed { index, image -> + image.copy(isCoverImage = index == 0) + } + } + lateinit var viewModel: ProductImagesViewModel private val networkStatus: NetworkStatus = mock() @@ -34,7 +41,7 @@ class ProductImagesViewModelTest : BaseUnitTest() { requestCode = 123 ).toSavedStateHandle() - private fun initialize(productImages: List = ProductTestUtils.generateProductImagesList()) { + private fun initialize(productImages: List = DEFAULT_PRODUCT_IMAGES) { viewModel = ProductImagesViewModel( networkStatus, mediaFileUploadHandler, @@ -73,7 +80,7 @@ class ProductImagesViewModelTest : BaseUnitTest() { fun `Trigger exitWithResult event on back button clicked when in browsing state`() { initialize() - val images = ProductTestUtils.generateProductImagesList() + val images = DEFAULT_PRODUCT_IMAGES viewModel.onDeleteImageConfirmed(images[0]) viewModel.onNavigateBackButtonClicked() @@ -157,6 +164,17 @@ class ProductImagesViewModelTest : BaseUnitTest() { } } + @Test + fun `When switching to drag mode, clear product image cover`() { + initialize() + + viewModel.onGalleryImageDragStarted() + + observeState { state -> + assertThat(state.images?.none { it.isCoverImage }).isTrue() + } + } + @Test fun `Validate drag and drop process on validation button clicked`() { val images = ProductTestUtils.generateProductImagesList() @@ -175,6 +193,7 @@ class ProductImagesViewModelTest : BaseUnitTest() { observeState { state -> assertThat(state.images).doesNotContain(imageToRemove) assertThat(state.images).contains(imageToReorder, Index.atIndex(2)) + assertThat(state.images?.first()?.isCoverImage).isTrue() } }