Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ class BookingNoteFragment : BaseFragment() {
return composeView {
BookingNoteScreen(
viewModel = viewModel,
onBack = { findNavController().popBackStack() },
)
}
}
Expand All @@ -45,6 +44,7 @@ class BookingNoteFragment : BaseFragment() {
is MultiLiveEvent.Event.ShowSnackbar -> {
uiMessageResolver.showSnack(event.message)
}

is MultiLiveEvent.Event.Exit -> {
findNavController().navigateUp()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,27 +29,25 @@ import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import com.woocommerce.android.R
import com.woocommerce.android.ui.compose.Render
import com.woocommerce.android.ui.compose.component.Toolbar
import com.woocommerce.android.ui.compose.component.WCTextButton

@Composable
fun BookingNoteScreen(
viewModel: BookingNoteViewModel,
onBack: () -> Unit,
) {
val viewState by viewModel.state.observeAsState()
viewState?.let {
BookingNoteScreen(
viewState = it,
onBack = onBack,
)
}
}

@Composable
fun BookingNoteScreen(
viewState: BookingNoteViewState,
onBack: () -> Unit,
) {
val focusRequester = remember { FocusRequester() }

Expand All @@ -62,7 +60,7 @@ fun BookingNoteScreen(
topBar = {
Toolbar(
title = stringResource(R.string.booking_note_screen_title),
onNavigationButtonClick = onBack,
onNavigationButtonClick = viewState.onBackPressed,
actions = {
if (viewState.isSaveVisible) {
WCTextButton(
Expand Down Expand Up @@ -105,6 +103,8 @@ fun BookingNoteScreen(
}
}
}

viewState.dialogState?.Render()
}

@Composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.asLiveData
import com.woocommerce.android.R
import com.woocommerce.android.ui.bookings.BookingsRepository
import com.woocommerce.android.ui.compose.DialogState
import com.woocommerce.android.viewmodel.MultiLiveEvent
import com.woocommerce.android.viewmodel.ScopedViewModel
import com.woocommerce.android.viewmodel.navArgs
Expand All @@ -25,18 +26,22 @@ class BookingNoteViewModel @Inject constructor(
private val initialNoteState = MutableStateFlow("")
private val editedNoteState = MutableStateFlow<String?>(null)
private val noteSaveStatusFlow = MutableStateFlow<NoteSaveStatus>(NoteSaveStatus.Idle)
private val dialogStateFlow = MutableStateFlow<DialogState?>(null)

val state: LiveData<BookingNoteViewState> = combine(
initialNoteState,
editedNoteState,
noteSaveStatusFlow
) { initialNote, editedNote, noteSaveStatus ->
noteSaveStatusFlow,
dialogStateFlow,
) { initialNote, editedNote, noteSaveStatus, dialogState ->
BookingNoteViewState(
initialNote = initialNote,
editedNote = editedNote,
noteSaveStatus = noteSaveStatus,
onNoteChange = ::onNoteChange,
onSaveClicked = ::saveNote,
onBackPressed = ::onBackPressed,
dialogState = dialogState,
)
}.asLiveData()

Expand All @@ -57,6 +62,40 @@ class BookingNoteViewModel @Inject constructor(
editedNoteState.value = value
}

private fun onBackPressed() {
if (noteSaveStatusFlow.value == NoteSaveStatus.InProgress) {
return
}
val initial = initialNoteState.value.trim()
val current = editedNoteState.value.orEmpty().trim()
val noteHasChanged = initial != current
if (noteHasChanged) {
dialogStateFlow.value = DialogState(
message = R.string.booking_note_discard_changes_message,
positiveButton = DialogState.DialogButton(
text = R.string.booking_note_discard_changes_discard,
onClick = ::onDiscardChanges
),
negativeButton = DialogState.DialogButton(
text = R.string.cancel,
onClick = ::onDismissDialog
),
onDismiss = ::onDismissDialog,
)
} else {
triggerEvent(MultiLiveEvent.Event.Exit)
}
}

private fun onDiscardChanges() {
dialogStateFlow.value = null
triggerEvent(MultiLiveEvent.Event.Exit)
}

private fun onDismissDialog() {
dialogStateFlow.value = null
}

private fun saveNote() {
launch {
noteSaveStatusFlow.value = NoteSaveStatus.InProgress
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package com.woocommerce.android.ui.bookings.note

import com.woocommerce.android.ui.compose.DialogState

data class BookingNoteViewState(
val initialNote: String = "",
val editedNote: String? = null,
val noteSaveStatus: NoteSaveStatus = NoteSaveStatus.Idle,
val onNoteChange: (String) -> Unit = {},
val onSaveClicked: () -> Unit = {},
val onBackPressed: () -> Unit = {},
val dialogState: DialogState? = null,
) {

val isSaveVisible: Boolean
Expand Down
3 changes: 3 additions & 0 deletions WooCommerce/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@
<string name="this_month">This Month</string>
<string name="this_year">This Year</string>
<string name="discard">Discard</string>
<!-- Booking note discard changes dialog -->
<string name="booking_note_discard_changes_message">Are you sure you want to discard these changes?</string>
<string name="booking_note_discard_changes_discard">Discard changes</string>
<string name="products">Products</string>
<string name="point_of_sale">Point of Sale</string>
<string name="custom_amounts">Custom Amounts</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.woocommerce.android.ui.bookings.note

import androidx.lifecycle.SavedStateHandle
import com.woocommerce.android.R
import com.woocommerce.android.model.UiString
import com.woocommerce.android.ui.bookings.BookingsRepository
import com.woocommerce.android.util.getOrAwaitValue
import com.woocommerce.android.viewmodel.BaseUnitTest
Expand Down Expand Up @@ -180,6 +181,103 @@ class BookingNoteViewModelTest : BaseUnitTest() {
assertThat(finalState.noteSaveStatus).isEqualTo(NoteSaveStatus.Idle)
}

@Test
fun `given no changes, when back pressed, then exit is triggered and no dialog`() = testBlocking {
// Given
val viewModel = createViewModel()
val state = viewModel.state.getOrAwaitValue()

// When
state.onBackPressed()

// Then
val event = viewModel.event.getOrAwaitValue()
assertThat(event).isEqualTo(MultiLiveEvent.Event.Exit)
val latestState = viewModel.state.getOrAwaitValue()
assertThat(latestState.dialogState).isNull()
}

@Test
fun `given changed note, when back pressed, then show discard dialog with correct copy and buttons`() = testBlocking {
// Given
val viewModel = createViewModel()
val state = viewModel.state.getOrAwaitValue()
state.onNoteChange("Changed note")
viewModel.state.getOrAwaitValue()

// When
state.onBackPressed()

// Then
val withDialog = viewModel.state.getOrAwaitValue()
val dialog = withDialog.dialogState
check(dialog != null)
// Verify message and buttons types/ids
assertThat(dialog.message).isInstanceOf(UiString.UiStringRes::class.java)
assertThat((dialog.message as UiString.UiStringRes).stringRes)
.isEqualTo(R.string.booking_note_discard_changes_message)
val positive = dialog.positiveButton
val negative = dialog.negativeButton
check(positive != null && negative != null)
assertThat((positive.text as UiString.UiStringRes).stringRes)
.isEqualTo(R.string.booking_note_discard_changes_discard)
assertThat((negative.text as UiString.UiStringRes).stringRes)
.isEqualTo(R.string.cancel)

// Clicking negative should dismiss dialog and not exit
negative.onClick()
val afterDismiss = viewModel.state.getOrAwaitValue()
assertThat(afterDismiss.dialogState).isNull()
}

@Test
fun `when confirm discard in dialog, then exit is triggered`() = testBlocking {
// Given
val viewModel = createViewModel()
val state = viewModel.state.getOrAwaitValue()
state.onNoteChange("Changed note")
viewModel.state.getOrAwaitValue()
state.onBackPressed()
val withDialog = viewModel.state.getOrAwaitValue()
val positive = withDialog.dialogState?.positiveButton
check(positive != null)

// When
positive.onClick()

// Then
val event = viewModel.event.getOrAwaitValue()
assertThat(event).isEqualTo(MultiLiveEvent.Event.Exit)
}

@Test
fun `when save in progress and back pressed, then do nothing (no dialog, no exit)`() = testBlocking {
// Given a slow repository update to keep InProgress state
whenever(bookingsRepository.updateNote(any(), any())).doSuspendableAnswer {
delay(200)
Result.success(Unit)
}
val viewModel = createViewModel()
val state = viewModel.state.getOrAwaitValue()
state.onNoteChange("Changed")
viewModel.state.getOrAwaitValue()

// Start save
state.onSaveClicked()
val inProgress = viewModel.state.getOrAwaitValue()
assertThat(inProgress.noteSaveStatus).isEqualTo(NoteSaveStatus.InProgress)

// When
inProgress.onBackPressed()

// Then: dialog remains null while in progress
val stillInProgress = viewModel.state.getOrAwaitValue()
assertThat(stillInProgress.dialogState).isNull()

// Clean up
advanceUntilIdle()
}

private fun createViewModel(
savedState: SavedStateHandle = savedStateHandle,
): BookingNoteViewModel {
Expand Down