diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/OverflowMenu.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/OverflowMenu.kt index 428dbc1a59a..e13f8ea6199 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/OverflowMenu.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/compose/component/OverflowMenu.kt @@ -7,6 +7,8 @@ import androidx.compose.material.DropdownMenu import androidx.compose.material.DropdownMenuItem import androidx.compose.material.Icon import androidx.compose.material.IconButton +import androidx.compose.material.LocalContentColor +import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.icons.Icons.Outlined import androidx.compose.material.icons.outlined.MoreVert @@ -30,7 +32,8 @@ fun WCOverflowMenu( onSelected: (T) -> Unit, modifier: Modifier = Modifier, mapper: @Composable (T) -> String = { it.toString() }, - tint: Color = Color.Black + itemColor: @Composable (T) -> Color = { LocalContentColor.current }, + tint: Color = MaterialTheme.colors.primary ) { var showMenu by remember { mutableStateOf(false) } Box(modifier = modifier) { @@ -57,7 +60,10 @@ fun WCOverflowMenu( onSelected(item) } ) { - Text(mapper(item)) + Text( + text = mapper(item), + color = itemColor(item) + ) } if (index < items.size - 1) { Spacer(modifier = Modifier.height(dimensionResource(id = dimen.minor_100))) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/editor/CustomFieldsEditorFragment.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/editor/CustomFieldsEditorFragment.kt index 3b2e3ddf69b..9e6f40d27e9 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/editor/CustomFieldsEditorFragment.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/editor/CustomFieldsEditorFragment.kt @@ -15,10 +15,6 @@ import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class CustomFieldsEditorFragment : BaseFragment() { - companion object { - const val RESULT_KEY = "custom_field_result" - } - override val activityAppBarStatus: AppBarStatus = AppBarStatus.Hidden private val viewModel: CustomFieldsEditorViewModel by viewModels() @@ -36,7 +32,7 @@ class CustomFieldsEditorFragment : BaseFragment() { private fun handleEvents() { viewModel.event.observe(viewLifecycleOwner) { event -> when (event) { - is MultiLiveEvent.Event.ExitWithResult<*> -> navigateBackWithResult(RESULT_KEY, event.data) + is MultiLiveEvent.Event.ExitWithResult<*> -> navigateBackWithResult(event.key!!, event.data) MultiLiveEvent.Event.Exit -> findNavController().navigateUp() } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/editor/CustomFieldsEditorScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/editor/CustomFieldsEditorScreen.kt index 532e8eb0f59..4c199f8dfc7 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/editor/CustomFieldsEditorScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/editor/CustomFieldsEditorScreen.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material.LocalContentColor import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.runtime.Composable @@ -18,6 +19,7 @@ import com.woocommerce.android.R import com.woocommerce.android.ui.compose.component.DiscardChangesDialog import com.woocommerce.android.ui.compose.component.Toolbar import com.woocommerce.android.ui.compose.component.WCOutlinedTextField +import com.woocommerce.android.ui.compose.component.WCOverflowMenu import com.woocommerce.android.ui.compose.component.WCTextButton import com.woocommerce.android.ui.compose.component.aztec.OutlinedAztecEditor import com.woocommerce.android.ui.compose.component.getText @@ -33,6 +35,7 @@ fun CustomFieldsEditorScreen(viewModel: CustomFieldsEditorViewModel) { onKeyChanged = viewModel::onKeyChanged, onValueChanged = viewModel::onValueChanged, onDoneClicked = viewModel::onDoneClicked, + onDeleteClicked = viewModel::onDeleteClicked, onBackButtonClick = viewModel::onBackClick, ) } @@ -44,6 +47,7 @@ private fun CustomFieldsEditorScreen( onKeyChanged: (String) -> Unit, onValueChanged: (String) -> Unit, onDoneClicked: () -> Unit, + onDeleteClicked: () -> Unit, onBackButtonClick: () -> Unit, ) { BackHandler { onBackButtonClick() } @@ -60,6 +64,24 @@ private fun CustomFieldsEditorScreen( text = stringResource(R.string.done) ) } + if (!state.isCreatingNewItem) { + WCOverflowMenu( + items = listOf(R.string.delete), + mapper = { stringResource(it) }, + itemColor = { + when (it) { + R.string.delete -> MaterialTheme.colors.error + else -> LocalContentColor.current + } + }, + onSelected = { resourceId -> + when (resourceId) { + R.string.delete -> onDeleteClicked() + else -> error("Unhandled menu item") + } + } + ) + } } ) }, @@ -117,6 +139,7 @@ private fun CustomFieldsEditorScreenPreview() { onKeyChanged = {}, onValueChanged = {}, onDoneClicked = {}, + onDeleteClicked = {}, onBackButtonClick = {} ) } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/editor/CustomFieldsEditorViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/editor/CustomFieldsEditorViewModel.kt index d8129657e52..ee4a4651cba 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/editor/CustomFieldsEditorViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/editor/CustomFieldsEditorViewModel.kt @@ -25,6 +25,11 @@ class CustomFieldsEditorViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val repository: CustomFieldsRepository ) : ScopedViewModel(savedStateHandle) { + companion object { + const val CUSTOM_FIELD_UPDATED_RESULT_KEY = "custom_field_updated" + const val CUSTOM_FIELD_DELETED_RESULT_KEY = "custom_field_deleted" + } + private val navArgs by savedStateHandle.navArgs() private val customFieldDraft = savedStateHandle.getStateFlow( @@ -57,7 +62,8 @@ class CustomFieldsEditorViewModel @Inject constructor( storedValue?.value.orEmpty() != customField.value, isHtml = isHtml, discardChangesDialogState = discardChangesDialogState, - keyErrorMessage = keyErrorMessage + keyErrorMessage = keyErrorMessage, + isCreatingNewItem = storedValue == null ) }.asLiveData() @@ -84,15 +90,25 @@ class CustomFieldsEditorViewModel @Inject constructor( if (existingFields.any { it.key == value.key }) { keyErrorMessage.value = UiString.UiStringRes(R.string.custom_fields_editor_key_error_duplicate) } else { - triggerEvent(MultiLiveEvent.Event.ExitWithResult(value)) + triggerEvent( + MultiLiveEvent.Event.ExitWithResult(data = value, key = CUSTOM_FIELD_UPDATED_RESULT_KEY) + ) } } } else { // When editing, we don't need to check for duplicate keys - triggerEvent(MultiLiveEvent.Event.ExitWithResult(value)) + triggerEvent( + MultiLiveEvent.Event.ExitWithResult(data = value, key = CUSTOM_FIELD_UPDATED_RESULT_KEY) + ) } } + fun onDeleteClicked() { + triggerEvent( + MultiLiveEvent.Event.ExitWithResult(data = navArgs.customField, key = CUSTOM_FIELD_DELETED_RESULT_KEY) + ) + } + fun onBackClick() { if (state.value?.hasChanges == true) { showDiscardChangesDialog.value = true @@ -118,6 +134,7 @@ class CustomFieldsEditorViewModel @Inject constructor( val isHtml: Boolean = false, val discardChangesDialogState: DiscardChangesDialogState? = null, val keyErrorMessage: UiString? = null, + val isCreatingNewItem: Boolean = false ) { val showDoneButton get() = customField.key.isNotEmpty() && hasChanges && keyErrorMessage == null diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/list/CustomFieldsFragment.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/list/CustomFieldsFragment.kt index db940c61c8e..f3a38d521f2 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/list/CustomFieldsFragment.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/list/CustomFieldsFragment.kt @@ -4,35 +4,37 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.material.SnackbarHostState +import androidx.compose.material.SnackbarResult import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import com.woocommerce.android.extensions.handleResult import com.woocommerce.android.ui.base.BaseFragment -import com.woocommerce.android.ui.base.UIMessageResolver import com.woocommerce.android.ui.compose.composeView import com.woocommerce.android.ui.customfields.CustomFieldContentType import com.woocommerce.android.ui.customfields.CustomFieldUiModel -import com.woocommerce.android.ui.customfields.editor.CustomFieldsEditorFragment +import com.woocommerce.android.ui.customfields.editor.CustomFieldsEditorViewModel import com.woocommerce.android.ui.main.AppBarStatus import com.woocommerce.android.util.ActivityUtils import com.woocommerce.android.util.ChromeCustomTabUtils import com.woocommerce.android.viewmodel.MultiLiveEvent import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject +import kotlinx.coroutines.launch @AndroidEntryPoint class CustomFieldsFragment : BaseFragment() { private val viewModel: CustomFieldsViewModel by viewModels() - @Inject - lateinit var uiMessageResolver: UIMessageResolver + private val snackbarHostState = SnackbarHostState() override val activityAppBarStatus = AppBarStatus.Hidden override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { return composeView { CustomFieldsScreen( - viewModel = viewModel + viewModel = viewModel, + snackbarHostState = snackbarHostState ) } } @@ -47,7 +49,13 @@ class CustomFieldsFragment : BaseFragment() { when (event) { is CustomFieldsViewModel.OpenCustomFieldEditor -> openEditor(event.field) is CustomFieldsViewModel.CustomFieldValueClicked -> handleValueClick(event.field) - is MultiLiveEvent.Event.ShowSnackbar -> uiMessageResolver.showSnack(event.message) + is MultiLiveEvent.Event.ShowSnackbar -> showSnackbar(getString(event.message)) + is MultiLiveEvent.Event.ShowActionSnackbar -> showSnackbar( + message = event.message, + actionText = event.actionText, + action = { event.action.onClick(null) } + ) + is MultiLiveEvent.Event.Exit -> { findNavController().navigateUp() } @@ -56,13 +64,16 @@ class CustomFieldsFragment : BaseFragment() { } private fun handleResults() { - handleResult(CustomFieldsEditorFragment.RESULT_KEY) { result -> + handleResult(CustomFieldsEditorViewModel.CUSTOM_FIELD_UPDATED_RESULT_KEY) { result -> if (result.id == null) { viewModel.onCustomFieldInserted(result) } else { viewModel.onCustomFieldUpdated(result) } } + handleResult(CustomFieldsEditorViewModel.CUSTOM_FIELD_DELETED_RESULT_KEY) { result -> + viewModel.onCustomFieldDeleted(result) + } } private fun openEditor(field: CustomFieldUiModel?) { @@ -82,4 +93,17 @@ class CustomFieldsFragment : BaseFragment() { CustomFieldContentType.TEXT -> error("Values of type TEXT should not be clickable") } } + + private fun showSnackbar( + message: String, + actionText: String? = null, + action: (() -> Unit)? = null + ) { + viewLifecycleOwner.lifecycleScope.launch { + val result = snackbarHostState.showSnackbar(message = message, actionLabel = actionText) + if (actionText != null && action != null && result == SnackbarResult.ActionPerformed) { + action() + } + } + } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/list/CustomFieldsScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/list/CustomFieldsScreen.kt index a84d7ea54f1..5271960f845 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/list/CustomFieldsScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/list/CustomFieldsScreen.kt @@ -20,6 +20,8 @@ import androidx.compose.material.FloatingActionButton import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold +import androidx.compose.material.SnackbarHost +import androidx.compose.material.SnackbarHostState import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowForwardIos @@ -29,6 +31,7 @@ import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.runtime.Composable import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -52,7 +55,10 @@ import com.woocommerce.android.ui.customfields.CustomFieldContentType import com.woocommerce.android.ui.customfields.CustomFieldUiModel @Composable -fun CustomFieldsScreen(viewModel: CustomFieldsViewModel) { +fun CustomFieldsScreen( + viewModel: CustomFieldsViewModel, + snackbarHostState: SnackbarHostState +) { viewModel.state.observeAsState().value?.let { state -> CustomFieldsScreen( state = state, @@ -61,7 +67,8 @@ fun CustomFieldsScreen(viewModel: CustomFieldsViewModel) { onCustomFieldClicked = viewModel::onCustomFieldClicked, onCustomFieldValueClicked = viewModel::onCustomFieldValueClicked, onAddCustomFieldClicked = viewModel::onAddCustomFieldClicked, - onBackClick = viewModel::onBackClick + onBackClick = viewModel::onBackClick, + snackbarHostState = snackbarHostState ) } } @@ -75,7 +82,8 @@ private fun CustomFieldsScreen( onCustomFieldClicked: (CustomFieldUiModel) -> Unit, onCustomFieldValueClicked: (CustomFieldUiModel) -> Unit, onAddCustomFieldClicked: () -> Unit, - onBackClick: () -> Unit + onBackClick: () -> Unit, + snackbarHostState: SnackbarHostState = remember { SnackbarHostState() } ) { BackHandler { onBackClick() } @@ -106,6 +114,7 @@ private fun CustomFieldsScreen( ) } }, + snackbarHost = { SnackbarHost(snackbarHostState) }, backgroundColor = MaterialTheme.colors.surface ) { paddingValues -> val pullToRefreshState = rememberPullRefreshState(state.isRefreshing, onPullToRefresh) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/list/CustomFieldsViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/list/CustomFieldsViewModel.kt index d60b1fd7356..716832cb687 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/list/CustomFieldsViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/customfields/list/CustomFieldsViewModel.kt @@ -8,6 +8,7 @@ import com.woocommerce.android.R import com.woocommerce.android.ui.customfields.CustomFieldUiModel import com.woocommerce.android.ui.customfields.CustomFieldsRepository import com.woocommerce.android.viewmodel.MultiLiveEvent +import com.woocommerce.android.viewmodel.ResourceProvider import com.woocommerce.android.viewmodel.ScopedViewModel import com.woocommerce.android.viewmodel.getStateFlow import com.woocommerce.android.viewmodel.navArgs @@ -23,7 +24,8 @@ import javax.inject.Inject @HiltViewModel class CustomFieldsViewModel @Inject constructor( savedStateHandle: SavedStateHandle, - private val repository: CustomFieldsRepository + private val repository: CustomFieldsRepository, + private val resourceProvider: ResourceProvider ) : ScopedViewModel(savedStateHandle) { private val args: CustomFieldsFragmentArgs by savedStateHandle.navArgs() val parentItemId: Long = args.parentItemId @@ -101,6 +103,33 @@ class CustomFieldsViewModel @Inject constructor( } } + fun onCustomFieldDeleted(field: CustomFieldUiModel) { + pendingChanges.update { + if (field.id == null) { + // This field was just added and hasn't been saved yet + it.copy(insertedFields = it.insertedFields - field) + } else { + it.copy(deletedFieldIds = it.deletedFieldIds + field.id) + } + } + + triggerEvent( + MultiLiveEvent.Event.ShowActionSnackbar( + message = resourceProvider.getString(R.string.custom_fields_list_field_deleted), + actionText = resourceProvider.getString(R.string.undo), + action = { + pendingChanges.update { + if (field.id == null) { + it.copy(insertedFields = it.insertedFields + field) + } else { + it.copy(deletedFieldIds = it.deletedFieldIds - field.id) + } + } + } + ) + ) + } + fun onSaveClicked() { launch { isSaving.value = true @@ -109,7 +138,8 @@ class CustomFieldsViewModel @Inject constructor( parentItemId = args.parentItemId, parentItemType = args.parentItemType, updatedMetadata = currentPendingChanges.editedFields.map { it.toDomainModel() }, - insertedMetadata = currentPendingChanges.insertedFields.map { it.toDomainModel() } + insertedMetadata = currentPendingChanges.insertedFields.map { it.toDomainModel() }, + deletedMetadataIds = currentPendingChanges.deletedFieldIds ) repository.updateCustomFields(request) @@ -126,9 +156,12 @@ class CustomFieldsViewModel @Inject constructor( } } - private fun List.combineWithChanges(pendingChanges: PendingChanges) = map { customField -> - pendingChanges.editedFields.find { it.id == customField.id } ?: customField - } + pendingChanges.insertedFields + private fun List.combineWithChanges(pendingChanges: PendingChanges) = + filterNot { it.id in pendingChanges.deletedFieldIds } + .map { customField -> + pendingChanges.editedFields.find { it.id == customField.id } ?: customField + } + .plus(pendingChanges.insertedFields) data class UiState( val customFields: List, @@ -146,10 +179,11 @@ class CustomFieldsViewModel @Inject constructor( @Parcelize private data class PendingChanges( val editedFields: List = emptyList(), - val insertedFields: List = emptyList() + val insertedFields: List = emptyList(), + val deletedFieldIds: List = emptyList() ) : Parcelable { val hasChanges: Boolean - get() = editedFields.isNotEmpty() || insertedFields.isNotEmpty() + get() = editedFields.isNotEmpty() || insertedFields.isNotEmpty() || deletedFieldIds.isNotEmpty() } data class OpenCustomFieldEditor(val field: CustomFieldUiModel?) : MultiLiveEvent.Event() diff --git a/WooCommerce/src/main/res/values/strings.xml b/WooCommerce/src/main/res/values/strings.xml index 38de67edcaa..258ccfa49dd 100644 --- a/WooCommerce/src/main/res/values/strings.xml +++ b/WooCommerce/src/main/res/values/strings.xml @@ -4290,6 +4290,7 @@ Saving changes Changes saved Saving changes failed, please try again + Custom Field deleted Add custom fields Key Value diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/customfields/editor/CustomFieldsEditorViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/customfields/editor/CustomFieldsEditorViewModelTest.kt index 9b2364c4764..5274c0dfdcd 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/customfields/editor/CustomFieldsEditorViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/customfields/editor/CustomFieldsEditorViewModelTest.kt @@ -182,7 +182,8 @@ class CustomFieldsEditorViewModelTest : BaseUnitTest() { assertThat(events).isEqualTo( MultiLiveEvent.Event.ExitWithResult( - CustomFieldUiModel(id = CUSTOM_FIELD_ID, key = "new key", value = "new value") + data = CustomFieldUiModel(id = CUSTOM_FIELD_ID, key = "new key", value = "new value"), + key = CustomFieldsEditorViewModel.CUSTOM_FIELD_UPDATED_RESULT_KEY ) ) } @@ -217,6 +218,22 @@ class CustomFieldsEditorViewModelTest : BaseUnitTest() { assertThat(state.keyErrorMessage).isNull() } + @Test + fun `given editing an existing field, when delete is clicked, then return result`() = testBlocking { + setup(editing = true) + + val event = viewModel.event.runAndCaptureValues { + viewModel.onDeleteClicked() + }.last() + + assertThat(event).isEqualTo( + MultiLiveEvent.Event.ExitWithResult( + data = CustomFieldUiModel(CUSTOM_FIELD), + key = CustomFieldsEditorViewModel.CUSTOM_FIELD_DELETED_RESULT_KEY + ) + ) + } + @Test fun `when key starts with underscore, then show error`() = testBlocking { setup(editing = true) diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/customfields/list/CustomFieldsViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/customfields/list/CustomFieldsViewModelTest.kt index c3e383fd694..1c3b80850d9 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/customfields/list/CustomFieldsViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/customfields/list/CustomFieldsViewModelTest.kt @@ -9,6 +9,7 @@ import com.woocommerce.android.util.getOrAwaitValue import com.woocommerce.android.util.runAndCaptureValues import com.woocommerce.android.viewmodel.BaseUnitTest import com.woocommerce.android.viewmodel.MultiLiveEvent +import com.woocommerce.android.viewmodel.ResourceProvider import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.flowOf @@ -17,6 +18,7 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.Test import org.mockito.kotlin.any import org.mockito.kotlin.argThat +import org.mockito.kotlin.doAnswer import org.mockito.kotlin.doSuspendableAnswer import org.mockito.kotlin.mock import org.mockito.kotlin.verify @@ -45,13 +47,17 @@ class CustomFieldsViewModelTest : BaseUnitTest() { private val repository: CustomFieldsRepository = mock { onBlocking { observeDisplayableCustomFields(PARENT_ITEM_ID) }.thenReturn(flowOf(CUSTOM_FIELDS)) } + private val resourceProvider: ResourceProvider = mock { + on { getString(any()) } doAnswer { it.getArgument(0).toString() } + } private lateinit var viewModel: CustomFieldsViewModel suspend fun setup(prepareMocks: suspend () -> Unit = {}) { prepareMocks() viewModel = CustomFieldsViewModel( savedStateHandle = CustomFieldsFragmentArgs(PARENT_ITEM_ID, PARENT_ITEM_TYPE).toSavedStateHandle(), - repository = repository + repository = repository, + resourceProvider = resourceProvider ) } @@ -250,6 +256,56 @@ class CustomFieldsViewModelTest : BaseUnitTest() { assertThat(state.customFields.last().value).isEqualTo(customField.value) } + @Test + fun `when deleting a custom field, then custom fields are refreshed`() = testBlocking { + val customField = CustomFieldUiModel(CUSTOM_FIELDS.first()) + setup() + + val state = viewModel.state.runAndCaptureValues { + viewModel.onCustomFieldDeleted(customField) + advanceUntilIdle() + }.last() + + assertThat(state.customFields).hasSize(CUSTOM_FIELDS.size - 1) + assertThat(state.customFields).doesNotContain(customField) + } + + @Test + fun `when deleting a custom field, then show undo action`() = testBlocking { + val customField = CustomFieldUiModel(CUSTOM_FIELDS.first()) + setup() + + val event = viewModel.event.runAndCaptureValues { + viewModel.onCustomFieldDeleted(customField) + advanceUntilIdle() + }.last() + + assertThat(event).matches { + it is MultiLiveEvent.Event.ShowActionSnackbar && + it.message == resourceProvider.getString(R.string.custom_fields_list_field_deleted) && + it.actionText == resourceProvider.getString(R.string.undo) + } + } + + @Test + fun `when undoing a delete, then custom fields are refreshed`() = testBlocking { + val customField = CustomFieldUiModel(CUSTOM_FIELDS.first()) + setup() + + val event = viewModel.event.runAndCaptureValues { + viewModel.onCustomFieldDeleted(customField) + advanceUntilIdle() + }.last() + + val state = viewModel.state.runAndCaptureValues { + (event as MultiLiveEvent.Event.ShowActionSnackbar).action.onClick(null) + advanceUntilIdle() + }.last() + + assertThat(state.customFields).hasSize(CUSTOM_FIELDS.size) + assertThat(state.customFields).contains(customField) + } + @Test fun `given pending changes, when back button is clicked, then discard changes dialog is shown`() = testBlocking { setup()