diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/packages/FetchSavedPackagesFromStore.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/packages/FetchSavedPackagesFromStore.kt new file mode 100644 index 00000000000..454cf74a63c --- /dev/null +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/packages/FetchSavedPackagesFromStore.kt @@ -0,0 +1,41 @@ +package com.woocommerce.android.ui.orders.wooshippinglabels.packages + +import com.woocommerce.android.ui.orders.wooshippinglabels.packages.WooShippingLabelPackageCreationViewModel.PackageData +import com.woocommerce.android.ui.orders.wooshippinglabels.packages.WooShippingLabelPackageCreationViewModel.PackageType +import javax.inject.Inject + +class FetchSavedPackagesFromStore @Inject constructor() { + operator fun invoke(): List { + // This is a mocked response. + // When fully implemented, this will be sorted from the Shipping plugin API. + return listOf( + PackageData( + type = PackageType.ENVELOPE, + name = "Small Flat Rate Box", + description = "USPS Priority Mail Flat Rate Boxes", + length = "10", + width = "10", + height = "10", + isSelected = true + ), + PackageData( + type = PackageType.BOX, + name = "Small Flat Rate Box", + description = "Custom package", + length = "20", + width = "20", + height = "20", + isSelected = false + ), + PackageData( + type = PackageType.BOX, + name = "Small Flat Rate Box", + description = "DHL Express", + length = "30", + width = "30", + height = "30", + isSelected = false + ) + ) + } +} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/packages/WooShippingLabelPackageCreationScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/packages/WooShippingLabelPackageCreationScreen.kt index a2e2d91c9b4..b130a7ccd9a 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/packages/WooShippingLabelPackageCreationScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/packages/WooShippingLabelPackageCreationScreen.kt @@ -6,8 +6,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.material.Tab @@ -23,6 +21,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.woocommerce.android.ui.compose.theme.WooThemeWithBackground +import com.woocommerce.android.ui.orders.wooshippinglabels.packages.WooShippingLabelPackageCreationViewModel.PackageData +import com.woocommerce.android.ui.orders.wooshippinglabels.packages.WooShippingLabelPackageCreationViewModel.PackageType import com.woocommerce.android.ui.orders.wooshippinglabels.packages.WooShippingLabelPackageCreationViewModel.PageTab import com.woocommerce.android.ui.orders.wooshippinglabels.packages.WooShippingLabelPackageCreationViewModel.PageType.CARRIER import com.woocommerce.android.ui.orders.wooshippinglabels.packages.WooShippingLabelPackageCreationViewModel.PageType.CUSTOM @@ -40,7 +40,7 @@ fun WooShippingLabelPackageCreationScreen( tabs = viewState.value?.pageTabs.orEmpty(), createCustomPackageScreen = { WooShippingCustomPackageCreationScreen(viewModel) }, createCarrierPackageScreen = { WooShippingCarrierPackageScreen() }, - createSavedPackageScreen = { WooShippingSavedPackageScreen() } + createSavedPackageScreen = { WooShippingSavedPackageScreen(viewModel) } ) } @@ -66,9 +66,13 @@ fun WooShippingLabelPackageCreationScreen( Scaffold( topBar = { - TabRow(selectedTabIndex = tabIndex) { + TabRow( + selectedTabIndex = tabIndex, + backgroundColor = MaterialTheme.colors.surface, + ) { tabs.forEachIndexed { index, pageTab -> Tab( + selectedContentColor = MaterialTheme.colors.onSurface, text = { Text(text = pageTab.title) }, selected = tabIndex == index, onClick = { tabIndex = index } @@ -83,7 +87,6 @@ fun WooShippingLabelPackageCreationScreen( .background(MaterialTheme.colors.surface) .padding(paddingValues) .fillMaxSize() - .verticalScroll(rememberScrollState()) ) { currentPageIndex -> when (tabs[currentPageIndex].type) { CUSTOM -> createCustomPackageScreen() @@ -120,8 +123,43 @@ fun WooShippingLabelsPackageCreationScreenPreview() { onSavePackageChanged = { } ) }, - createCarrierPackageScreen = { }, - createSavedPackageScreen = { } + createSavedPackageScreen = { + WooShippingSavedPackageScreen( + savedPackages = listOf( + PackageData( + type = PackageType.ENVELOPE, + name = "Small Flat Rate Box", + description = "USPS Priority Mail Flat Rate Boxes", + length = "10", + width = "10", + height = "10", + isSelected = true + ), + PackageData( + type = PackageType.BOX, + name = "Small Flat Rate Box", + description = "Custom package", + length = "20", + width = "20", + height = "20", + isSelected = false + ), + PackageData( + type = PackageType.BOX, + name = "Small Flat Rate Box", + description = "DHL Express", + length = "30", + width = "30", + height = "30", + isSelected = false + ) + ), + isAddPackageEnabled = true, + onAddPackageClick = { }, + onSavedPackageSelected = { _, _ -> } + ) + }, + createCarrierPackageScreen = { } ) } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/packages/WooShippingLabelPackageCreationViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/packages/WooShippingLabelPackageCreationViewModel.kt index 836d69570c1..2f35f8cd020 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/packages/WooShippingLabelPackageCreationViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/packages/WooShippingLabelPackageCreationViewModel.kt @@ -17,12 +17,13 @@ import javax.inject.Inject @HiltViewModel class WooShippingLabelPackageCreationViewModel @Inject constructor( savedState: SavedStateHandle, - private val resourceProvider: ResourceProvider + private val resourceProvider: ResourceProvider, + private val fetchSavedPackages: FetchSavedPackagesFromStore ) : ScopedViewModel(savedState) { private val _viewState = savedState.getStateFlow( scope = viewModelScope, - initialValue = ViewState(pageTabs, CustomPackageCreationData.EMPTY) + initialValue = ViewState(pageTabs) ) val viewState = _viewState.asLiveData() @@ -42,8 +43,30 @@ class WooShippingLabelPackageCreationViewModel @Inject constructor( ) ) - fun onAddPackageClick() { - triggerEvent(PackageSelected(_viewState.value.customPackageCreationData)) + init { + _viewState.update { viewState -> + viewState.copy(savedPackageSelection = SavedPackageSelection(fetchSavedPackages())) + } + } + + fun onSavedPackageSelected(packageData: PackageData, isSelected: Boolean) { + _viewState.update { viewState -> + viewState.savedPackageSelection.packages + .map { it.copy(isSelected = false) } + .toMutableList() + .apply { set(indexOf(packageData), packageData.copy(isSelected = isSelected)) } + .let { SavedPackageSelection(it) } + .let { viewState.copy(savedPackageSelection = it) } + } + } + + fun onAddSavedPackageClick() { + _viewState.value.savedPackageSelection.packages.find { it.isSelected } + ?.let { triggerEvent(SavedPackageSelected(it)) } + } + + fun onAddCustomPackageClick() { + triggerEvent(CustomPackageCreated(_viewState.value.customPackageCreationData)) } fun onPackageTypeSpinnerClick() { @@ -88,7 +111,8 @@ class WooShippingLabelPackageCreationViewModel @Inject constructor( @Parcelize data class ViewState( val pageTabs: List = emptyList(), - val customPackageCreationData: CustomPackageCreationData + val customPackageCreationData: CustomPackageCreationData = CustomPackageCreationData.EMPTY, + val savedPackageSelection: SavedPackageSelection = SavedPackageSelection(emptyList()) ) : Parcelable @Parcelize @@ -97,6 +121,29 @@ class WooShippingLabelPackageCreationViewModel @Inject constructor( val type: PageType ) : Parcelable + @Parcelize + data class PackageData( + val type: PackageType, + val name: String, + val description: String, + val length: String, + val width: String, + val height: String, + val isSelected: Boolean, + val dimensionUnit: String = "cm" + ) : Parcelable { + val dimensionsForDisplay: String + get() = "$length x $width x $height $dimensionUnit" + } + + @Parcelize + data class SavedPackageSelection( + val packages: List + ) : Parcelable { + val hasSelection: Boolean + get() = packages.find { it.isSelected } != null + } + @Parcelize data class CustomPackageCreationData( val type: PackageType, @@ -130,6 +177,7 @@ class WooShippingLabelPackageCreationViewModel @Inject constructor( ENVELOPE(R.string.woo_shipping_labels_package_creation_envelope_type) } - data class PackageSelected(val packageData: CustomPackageCreationData) : MultiLiveEvent.Event() + data class SavedPackageSelected(val packageData: PackageData) : MultiLiveEvent.Event() + data class CustomPackageCreated(val packageData: CustomPackageCreationData) : MultiLiveEvent.Event() data class ShowPackageTypeDialog(val currentSelection: PackageType) : MultiLiveEvent.Event() } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/packages/forms/WooShippingCustomPackageScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/packages/forms/WooShippingCustomPackageScreen.kt index 8240ffada01..83bcb39f439 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/packages/forms/WooShippingCustomPackageScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/packages/forms/WooShippingCustomPackageScreen.kt @@ -6,7 +6,9 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll import androidx.compose.material.Button import androidx.compose.material.Checkbox import androidx.compose.material.Text @@ -37,7 +39,7 @@ fun WooShippingCustomPackageCreationScreen(viewModel: WooShippingLabelPackageCre packageLength = viewState?.customPackageCreationData?.length.orEmpty(), packageWidth = viewState?.customPackageCreationData?.width.orEmpty(), isAddPackageEnabled = viewState?.customPackageCreationData?.isValid ?: false, - onAddPackageClick = viewModel::onAddPackageClick, + onAddPackageClick = viewModel::onAddCustomPackageClick, onPackageTypeClick = viewModel::onPackageTypeSpinnerClick, onLengthChange = viewModel::onLengthChange, onWidthChange = viewModel::onWidthChange, @@ -65,6 +67,7 @@ fun WooShippingCustomPackageCreationScreen( modifier = modifier .fillMaxSize() .padding(16.dp) + .verticalScroll(rememberScrollState()) ) { Column( modifier = modifier.weight(1f), diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/packages/forms/WooShippingSavedPackageScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/packages/forms/WooShippingSavedPackageScreen.kt index 120209e5dfa..4204fa4109f 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/packages/forms/WooShippingSavedPackageScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/packages/forms/WooShippingSavedPackageScreen.kt @@ -1,9 +1,163 @@ package com.woocommerce.android.ui.orders.wooshippinglabels.packages.forms +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Button +import androidx.compose.material.Divider +import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.woocommerce.android.R +import com.woocommerce.android.ui.compose.component.SelectionCheck +import com.woocommerce.android.ui.compose.theme.WooThemeWithBackground +import com.woocommerce.android.ui.orders.wooshippinglabels.packages.WooShippingLabelPackageCreationViewModel +import com.woocommerce.android.ui.orders.wooshippinglabels.packages.WooShippingLabelPackageCreationViewModel.PackageData +import com.woocommerce.android.ui.orders.wooshippinglabels.packages.WooShippingLabelPackageCreationViewModel.PackageType @Composable -fun WooShippingSavedPackageScreen() { - Text("Saved Package Form") +fun WooShippingSavedPackageScreen(viewModel: WooShippingLabelPackageCreationViewModel) { + val viewState = viewModel.viewState.observeAsState() + WooShippingSavedPackageScreen( + savedPackages = viewState.value?.savedPackageSelection?.packages.orEmpty(), + isAddPackageEnabled = viewState.value?.savedPackageSelection?.hasSelection ?: false, + onAddPackageClick = viewModel::onAddSavedPackageClick, + onSavedPackageSelected = viewModel::onSavedPackageSelected + + ) +} + +@Composable +fun WooShippingSavedPackageScreen( + modifier: Modifier = Modifier, + savedPackages: List, + isAddPackageEnabled: Boolean, + onAddPackageClick: () -> Unit, + onSavedPackageSelected: (PackageData, Boolean) -> Unit +) { + Column( + modifier = modifier + .fillMaxSize() + ) { + Column( + modifier = modifier + .weight(1f) + .padding(16.dp) + .verticalScroll(rememberScrollState()) + ) { + savedPackages.forEach { packageData -> + WooShippingSavedPackageItem( + modifier, + packageData, + onSavedPackageSelected + ) + } + } + Column { + Divider() + Button( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + enabled = isAddPackageEnabled, + onClick = onAddPackageClick + ) { + Text(stringResource(id = R.string.woo_shipping_labels_package_creation_add_package)) + } + } + } +} + +@Composable +fun WooShippingSavedPackageItem( + modifier: Modifier, + packageData: PackageData, + onPackageSelected: (PackageData, Boolean) -> Unit +) { + Column( + modifier = modifier + .clickable { onPackageSelected(packageData, packageData.isSelected.not()) } + .padding(top = 8.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + SelectionCheck( + isSelected = packageData.isSelected, + onSelectionChange = { onPackageSelected(packageData, it) } + ) + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text( + text = packageData.description, + style = MaterialTheme.typography.caption, + color = colorResource(id = R.color.color_on_surface_disabled) + ) + Text( + text = packageData.name, + style = MaterialTheme.typography.body1 + ) + Text( + text = packageData.dimensionsForDisplay, + style = MaterialTheme.typography.body2 + ) + } + } + Divider() + } +} + +@Preview +@Composable +fun WooShippingSavedPackageScreenPreview() { + WooThemeWithBackground { + WooShippingSavedPackageScreen( + savedPackages = listOf( + PackageData( + type = PackageType.ENVELOPE, + name = "Small Flat Rate Box", + description = "USPS Priority Mail Flat Rate Boxes", + length = "10", + width = "10", + height = "10", + isSelected = true + ), + PackageData( + type = PackageType.BOX, + name = "Small Flat Rate Box", + description = "Custom package", + length = "20", + width = "20", + height = "20", + isSelected = false + ), + PackageData( + type = PackageType.BOX, + name = "Small Flat Rate Box", + description = "DHL Express", + length = "30", + width = "30", + height = "30", + isSelected = false + ) + ), + isAddPackageEnabled = true, + onAddPackageClick = {}, + onSavedPackageSelected = { _, _ -> } + ) + } } diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/packages/WooShippingLabelPackageCreationViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/packages/WooShippingLabelPackageCreationViewModelTest.kt index f5ab1b3e7fa..91aead53f3b 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/packages/WooShippingLabelPackageCreationViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/orders/wooshippinglabels/packages/WooShippingLabelPackageCreationViewModelTest.kt @@ -2,8 +2,9 @@ package com.woocommerce.android.ui.orders.wooshippinglabels.packages import androidx.lifecycle.SavedStateHandle import com.woocommerce.android.R +import com.woocommerce.android.ui.orders.wooshippinglabels.packages.WooShippingLabelPackageCreationViewModel.CustomPackageCreated import com.woocommerce.android.ui.orders.wooshippinglabels.packages.WooShippingLabelPackageCreationViewModel.CustomPackageCreationData -import com.woocommerce.android.ui.orders.wooshippinglabels.packages.WooShippingLabelPackageCreationViewModel.PackageSelected +import com.woocommerce.android.ui.orders.wooshippinglabels.packages.WooShippingLabelPackageCreationViewModel.PackageData import com.woocommerce.android.ui.orders.wooshippinglabels.packages.WooShippingLabelPackageCreationViewModel.PackageType import com.woocommerce.android.ui.orders.wooshippinglabels.packages.WooShippingLabelPackageCreationViewModel.ShowPackageTypeDialog import com.woocommerce.android.ui.orders.wooshippinglabels.packages.WooShippingLabelPackageCreationViewModel.ViewState @@ -21,8 +22,8 @@ import org.mockito.kotlin.whenever class WooShippingLabelPackageCreationViewModelTest : BaseUnitTest() { private lateinit var sut: WooShippingLabelPackageCreationViewModel - private val savedStateHandle: SavedStateHandle = SavedStateHandle() private val resourceProvider: ResourceProvider = mock() + private val fetchSavedPackages: FetchSavedPackagesFromStore = mock() @Before fun setUp() { @@ -35,7 +36,9 @@ class WooShippingLabelPackageCreationViewModelTest : BaseUnitTest() { whenever( resourceProvider.getString(R.string.woo_shipping_labels_package_creation_tab_saved) ).thenReturn("Saved") - sut = WooShippingLabelPackageCreationViewModel(savedStateHandle, resourceProvider) + + whenever(fetchSavedPackages()).thenReturn(emptyList()) + sut = WooShippingLabelPackageCreationViewModel(SavedStateHandle(), resourceProvider, fetchSavedPackages) } @Test @@ -57,9 +60,9 @@ class WooShippingLabelPackageCreationViewModelTest : BaseUnitTest() { sut.onSavePackageChanged(true) sut.onPackageTypeSelected(PackageType.ENVELOPE) - sut.onAddPackageClick() + sut.onAddCustomPackageClick() - assertThat(lastEvent).isEqualTo(PackageSelected(customPackageData)) + assertThat(lastEvent).isEqualTo(CustomPackageCreated(customPackageData)) } @Test @@ -124,4 +127,37 @@ class WooShippingLabelPackageCreationViewModelTest : BaseUnitTest() { assertThat(lastViewState?.customPackageCreationData?.saveAsTemplate).isEqualTo(newSaveAsTemplate) } + + @Test + fun `onSavedPackageSelected selects only one package at a time`() = testBlocking { + var lastViewState: ViewState? = null + val package1 = PackageData( + type = PackageType.BOX, + name = "Package 1", + description = "Description 1", + length = "10", + width = "10", + height = "10", + isSelected = false + ) + val package2 = PackageData( + type = PackageType.ENVELOPE, + name = "Package 2", + description = "Description 2", + length = "20", + width = "20", + height = "20", + isSelected = false + ) + whenever(fetchSavedPackages()).thenReturn(listOf(package1, package2)) + + sut = WooShippingLabelPackageCreationViewModel(SavedStateHandle(), resourceProvider, fetchSavedPackages) + sut.viewState.observeForever { lastViewState = it } + sut.onSavedPackageSelected(package1, true) + + val selectedPackages = lastViewState?.savedPackageSelection?.packages?.filter { it.isSelected } + assertThat(selectedPackages).isNotNull + assertThat(selectedPackages).size().isEqualTo(1) + assertThat(selectedPackages?.first()).isEqualTo(package1.copy(isSelected = true)) + } }