Skip to content

Commit a62c55a

Browse files
authored
Merge pull request #14912 from woocommerce/issue/WOOMOB-1472-close-filters-dialog
[Bookings] Unsaved changes dialog to Filters screen
2 parents a4e9094 + a1d98fa commit a62c55a

File tree

4 files changed

+100
-4
lines changed

4 files changed

+100
-4
lines changed

WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/filter/BookingFilterListScreen.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import androidx.navigation.compose.composable
2929
import androidx.navigation.compose.rememberNavController
3030
import com.woocommerce.android.R
3131
import com.woocommerce.android.ui.bookings.filter.type.BookingTypeFilterRoute
32+
import com.woocommerce.android.ui.compose.Render
3233
import com.woocommerce.android.ui.compose.component.Toolbar
3334
import com.woocommerce.android.ui.compose.component.WCColoredButton
3435
import com.woocommerce.android.ui.compose.preview.LightDarkThemePreviews
@@ -73,6 +74,8 @@ fun BookingFilterListScreen(state: BookingFilterListUiState) {
7374
.padding(innerPadding)
7475
)
7576

77+
state.dialogState?.Render()
78+
7679
// The navigation is driven by the state, so we handle back navigation by calling onClose
7780
// We need to ensure that this called after NavHost to make sure we receive back events
7881
BackHandler {

WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/filter/BookingFilterListUiState.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import androidx.annotation.StringRes
55
import com.woocommerce.android.R
66
import com.woocommerce.android.model.UiString
77
import com.woocommerce.android.ui.bookings.filter.type.titleRes
8+
import com.woocommerce.android.ui.compose.DialogState
89
import org.wordpress.android.fluxc.network.rest.wpcom.wc.bookings.BookingFilters
910
import org.wordpress.android.fluxc.network.rest.wpcom.wc.bookings.BookingsFilterOption
1011

@@ -24,6 +25,7 @@ data class BookingFilterListUiState(
2425
val initialBookingFilters: BookingFilters? = null,
2526
val newBookingFilters: Set<BookingsFilterOption> = emptySet(),
2627
val currentPage: BookingFilterPage = BookingFilterPage.List,
28+
val dialogState: DialogState? = null,
2729
val onClose: () -> Unit = {},
2830
val onShowBookings: () -> Unit = {},
2931
val openPage: (BookingFilterPage) -> Unit = {},

WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/filter/BookingFilterListViewModel.kt

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ package com.woocommerce.android.ui.bookings.filter
22

33
import androidx.lifecycle.SavedStateHandle
44
import androidx.lifecycle.asLiveData
5+
import com.woocommerce.android.R
6+
import com.woocommerce.android.model.UiString
57
import com.woocommerce.android.ui.bookings.filter.data.BookingFilterRepository
8+
import com.woocommerce.android.ui.compose.DialogState
69
import com.woocommerce.android.viewmodel.MultiLiveEvent
710
import com.woocommerce.android.viewmodel.ScopedViewModel
811
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -25,7 +28,7 @@ class BookingFilterListViewModel @Inject constructor(
2528
onClose = ::onClose,
2629
onShowBookings = ::onShowBookings,
2730
openPage = ::onOpenPage,
28-
onUpdateFilterOption = ::onUpdateFilterOption,
31+
onUpdateFilterOption = ::onUpdateFilterOption
2932
)
3033
)
3134
val uiState = _uiState.asLiveData()
@@ -66,8 +69,25 @@ class BookingFilterListViewModel @Inject constructor(
6669
current.copy(currentPage = BookingFilterPage.List)
6770
}
6871
} else {
69-
// TODO Verify unsaved changes and close
70-
triggerEvent(MultiLiveEvent.Event.Exit)
72+
if (hasUnsavedChanges()) {
73+
_uiState.update { current ->
74+
current.copy(
75+
dialogState = DialogState(
76+
message = R.string.discard_message,
77+
positiveButton = DialogState.DialogButton(
78+
text = UiString.UiStringRes(R.string.discard),
79+
onClick = ::onDiscardChanges
80+
),
81+
negativeButton = DialogState.DialogButton(
82+
text = UiString.UiStringRes(R.string.keep_changes),
83+
onClick = ::onDismissUnsavedChangesDialog
84+
),
85+
)
86+
)
87+
}
88+
} else {
89+
triggerEvent(MultiLiveEvent.Event.Exit)
90+
}
7191
}
7292
}
7393

@@ -77,6 +97,22 @@ class BookingFilterListViewModel @Inject constructor(
7797
}
7898
triggerEvent(MultiLiveEvent.Event.Exit)
7999
}
100+
101+
private fun onDismissUnsavedChangesDialog() {
102+
_uiState.update { current -> current.copy(dialogState = null) }
103+
}
104+
105+
private fun onDiscardChanges() {
106+
// Hide dialog and exit without saving
107+
_uiState.update { current -> current.copy(dialogState = null) }
108+
triggerEvent(MultiLiveEvent.Event.Exit)
109+
}
110+
111+
private fun hasUnsavedChanges(): Boolean {
112+
val initial = _uiState.value.initialBookingFilters ?: BookingFilters()
113+
val updated = _uiState.value.updatedBookingFilters
114+
return updated != initial
115+
}
80116
}
81117

82118
private val BookingFilterListUiState.updatedBookingFilters: BookingFilters

WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/filter/BookingFilterListViewModelTest.kt

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import org.junit.Test
1313
import org.mockito.kotlin.doReturn
1414
import org.mockito.kotlin.mock
1515
import org.wordpress.android.fluxc.network.rest.wpcom.wc.bookings.BookingFilters
16+
import org.wordpress.android.fluxc.network.rest.wpcom.wc.bookings.BookingsFilterOption
1617

1718
@OptIn(ExperimentalCoroutinesApi::class)
1819
class BookingFilterListViewModelTest : BaseUnitTest() {
@@ -70,7 +71,7 @@ class BookingFilterListViewModelTest : BaseUnitTest() {
7071
}
7172

7273
@Test
73-
fun `given current page is List, when onClose, then Exit event is emitted`() {
74+
fun `given current page is List, and no changes, when onClose, then Exit event is emitted`() {
7475
// GIVEN
7576
val events = mutableListOf<MultiLiveEvent.Event>()
7677
viewModel.event.observeForever { events.add(it) }
@@ -84,4 +85,58 @@ class BookingFilterListViewModelTest : BaseUnitTest() {
8485
assertThat(events).isNotEmpty
8586
assertThat(events.last()).isEqualTo(MultiLiveEvent.Event.Exit)
8687
}
88+
89+
@Test
90+
fun `given unsaved changes on Filters, when onClose, then dialog becomes visible`() {
91+
// GIVEN
92+
val initial = viewModel.uiState.getOrAwaitValue()
93+
// introduce a change different from initial filters (which are empty)
94+
initial.onUpdateFilterOption(BookingsFilterOption.BookingType(BookingsFilterOption.BookingType.Type.SERVICE))
95+
val changed = viewModel.uiState.getOrAwaitValue()
96+
97+
// WHEN
98+
changed.onClose()
99+
100+
// THEN
101+
val afterClose = viewModel.uiState.getOrAwaitValue()
102+
assertThat(afterClose.dialogState).isNotNull()
103+
}
104+
105+
@Test
106+
fun `given dialog visible, when Keep Changes tapped, then dialog hides`() {
107+
// GIVEN
108+
val events = mutableListOf<MultiLiveEvent.Event>()
109+
viewModel.event.observeForever { events.add(it) }
110+
val initial = viewModel.uiState.getOrAwaitValue()
111+
initial.onUpdateFilterOption(BookingsFilterOption.BookingType(BookingsFilterOption.BookingType.Type.SERVICE))
112+
viewModel.uiState.getOrAwaitValue().onClose() // shows dialog
113+
114+
// WHEN: tap Keep Changes
115+
val withDialog = viewModel.uiState.getOrAwaitValue()
116+
withDialog.dialogState?.negativeButton?.onClick()
117+
118+
// THEN: dialog hidden
119+
val afterDismiss = viewModel.uiState.getOrAwaitValue()
120+
assertThat(afterDismiss.dialogState).isNull()
121+
}
122+
123+
@Test
124+
fun `given dialog visible, when Discard pressed, then dialog hides, and exit`() {
125+
// GIVEN
126+
val events = mutableListOf<MultiLiveEvent.Event>()
127+
viewModel.event.observeForever { events.add(it) }
128+
val initial = viewModel.uiState.getOrAwaitValue()
129+
initial.onUpdateFilterOption(BookingsFilterOption.BookingType(BookingsFilterOption.BookingType.Type.SERVICE))
130+
viewModel.uiState.getOrAwaitValue().onClose() // shows dialog
131+
132+
// WHEN: tap Discard
133+
val withDialog = viewModel.uiState.getOrAwaitValue()
134+
withDialog.dialogState?.positiveButton?.onClick()
135+
136+
// THEN: dialog hidden and Exit event emitted
137+
val afterDiscard = viewModel.uiState.getOrAwaitValue()
138+
assertThat(afterDiscard.dialogState).isNull()
139+
assertThat(events).isNotEmpty
140+
assertThat(events.last()).isEqualTo(MultiLiveEvent.Event.Exit)
141+
}
87142
}

0 commit comments

Comments
 (0)