-
Notifications
You must be signed in to change notification settings - Fork 132
[Dynamic Dashboard] Orders card status filtering #11533
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
2e62bd4
f157228
c01391a
2010826
5081c69
d825c9e
aeffb9c
65542d4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,24 +9,25 @@ import com.woocommerce.android.R | |
import com.woocommerce.android.analytics.AnalyticsEvent | ||
import com.woocommerce.android.analytics.AnalyticsTracker | ||
import com.woocommerce.android.analytics.AnalyticsTrackerWrapper | ||
import com.woocommerce.android.extensions.capitalize | ||
import com.woocommerce.android.extensions.formatToMMMdd | ||
import com.woocommerce.android.model.DashboardWidget | ||
import com.woocommerce.android.model.DashboardWidget.Type.ORDERS | ||
import com.woocommerce.android.model.Order | ||
import com.woocommerce.android.model.toOrderStatus | ||
import com.woocommerce.android.ui.dashboard.DashboardViewModel | ||
import com.woocommerce.android.ui.dashboard.DashboardViewModel.DashboardWidgetAction | ||
import com.woocommerce.android.ui.dashboard.DashboardViewModel.DashboardWidgetMenu | ||
import com.woocommerce.android.ui.dashboard.DashboardViewModel.RefreshEvent | ||
import com.woocommerce.android.ui.dashboard.defaultHideMenuEntry | ||
import com.woocommerce.android.ui.dashboard.orders.DashboardOrdersViewModel.Factory | ||
import com.woocommerce.android.ui.dashboard.orders.DashboardOrdersViewModel.ViewState.Content | ||
import com.woocommerce.android.ui.orders.filters.data.OrderStatusOption | ||
import com.woocommerce.android.ui.orders.filters.domain.GetOrderStatusFilterOptions | ||
import com.woocommerce.android.ui.orders.list.OrderListRepository | ||
import com.woocommerce.android.util.CurrencyFormatter | ||
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 dagger.assisted.Assisted | ||
import dagger.assisted.AssistedFactory | ||
import dagger.assisted.AssistedInject | ||
|
@@ -35,27 +36,34 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi | |
import kotlinx.coroutines.flow.MutableSharedFlow | ||
import kotlinx.coroutines.flow.combine | ||
import kotlinx.coroutines.flow.emitAll | ||
import kotlinx.coroutines.flow.flatMapLatest | ||
import kotlinx.coroutines.flow.map | ||
import kotlinx.coroutines.flow.merge | ||
import kotlinx.coroutines.flow.onStart | ||
import kotlinx.coroutines.flow.transformLatest | ||
import kotlinx.coroutines.launch | ||
import java.util.Locale | ||
|
||
@Suppress("LongParameterList") | ||
@HiltViewModel(assistedFactory = Factory::class) | ||
class DashboardOrdersViewModel @AssistedInject constructor( | ||
savedStateHandle: SavedStateHandle, | ||
@Assisted private val parentViewModel: DashboardViewModel, | ||
private val orderListRepository: OrderListRepository, | ||
private val analyticsTrackerWrapper: AnalyticsTrackerWrapper, | ||
private val currencyFormatter: CurrencyFormatter, | ||
private val resourceProvider: ResourceProvider | ||
private val resourceProvider: ResourceProvider, | ||
private val getOrderStatusFilterOptions: GetOrderStatusFilterOptions | ||
) : ScopedViewModel(savedStateHandle) { | ||
companion object { | ||
const val MAX_NUMBER_OF_ORDERS_TO_DISPLAY_IN_CARD = 3 | ||
const val DEFAULT_FILTER_OPTION = "all" | ||
} | ||
|
||
private val orderStatusMap = MutableSharedFlow<Map<String, Order.OrderStatus>>(replay = 1) | ||
private val refreshTrigger = MutableSharedFlow<RefreshEvent>(extraBufferCapacity = 1) | ||
private val statusOptions = MutableSharedFlow<List<OrderStatusOption>>(replay = 1) | ||
private val _refreshTrigger = MutableSharedFlow<RefreshEvent>(extraBufferCapacity = 1) | ||
private val refreshTrigger = merge(parentViewModel.refreshTrigger, _refreshTrigger) | ||
.onStart { emit(RefreshEvent()) } | ||
private val selectedFilter = savedStateHandle.getStateFlow(viewModelScope, DEFAULT_FILTER_OPTION) | ||
|
||
val menu = DashboardWidgetMenu( | ||
items = listOf( | ||
|
@@ -71,49 +79,69 @@ class DashboardOrdersViewModel @AssistedInject constructor( | |
) | ||
|
||
@OptIn(ExperimentalCoroutinesApi::class) | ||
val viewState = merge(parentViewModel.refreshTrigger, refreshTrigger) | ||
.onStart { emit(RefreshEvent()) } | ||
.transformLatest { | ||
val viewState = selectedFilter.flatMapLatest { status -> | ||
refreshTrigger.map { Pair(status, it) } | ||
}.transformLatest { (filterStatus, refresh) -> | ||
if (refresh.isForced) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. np, wouldn't the check for forced here cause issues on initial load if we don't have any cached orders? I think that as we won't emit anything, we won't have any UI until the orders fetch is done, or I am missing something 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm, I added it to avoid the ugly flashing when changing the status filter. I added a check for this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice |
||
emit(ViewState.Loading) | ||
emitAll( | ||
} | ||
emitAll( | ||
combine( | ||
orderListRepository.observeTopOrders( | ||
count = MAX_NUMBER_OF_ORDERS_TO_DISPLAY_IN_CARD, | ||
isForced = it.isForced | ||
).combine(orderStatusMap) { result, statusMap -> | ||
result.fold( | ||
onSuccess = { orders -> | ||
Content( | ||
orders.map { order -> | ||
val status = statusMap[order.status.value]?.label | ||
?: order.status.value.capitalize(Locale.getDefault()) | ||
|
||
ViewState.OrderItem( | ||
number = "#${order.number}", | ||
date = order.dateCreated.formatToMMMdd(), | ||
customerName = order.billingName.ifEmpty { | ||
resourceProvider.getString(R.string.orderdetail_customer_name_default) | ||
}, | ||
status = status, | ||
statusColor = order.status.color, | ||
totalPrice = currencyFormatter.formatCurrency(order.total, order.currency) | ||
) | ||
} | ||
) | ||
}, | ||
onFailure = { error -> | ||
ViewState.Error(error.message ?: "") | ||
} | ||
) | ||
} | ||
) | ||
}.asLiveData() | ||
isForced = refresh.isForced, | ||
statusFilter = filterStatus | ||
.takeIf { it != DEFAULT_FILTER_OPTION } | ||
?.let { Order.Status.fromValue(it) } | ||
), | ||
statusOptions | ||
) { result, statusOptions -> | ||
result.fold( | ||
onSuccess = { orders -> | ||
Content( | ||
orders = orders.map { order -> | ||
val status = statusOptions | ||
.first { option -> option.key == order.status.value }.label | ||
|
||
ViewState.OrderItem( | ||
number = "#${order.number}", | ||
date = order.dateCreated.formatToMMMdd(), | ||
customerName = order.billingName.ifEmpty { | ||
resourceProvider.getString(R.string.orderdetail_customer_name_default) | ||
}, | ||
status = status, | ||
statusColor = order.status.color, | ||
totalPrice = currencyFormatter.formatCurrency(order.total, order.currency) | ||
) | ||
}, | ||
filterOptions = statusOptions, | ||
selectedFilter = statusOptions.first { it.key == filterStatus } | ||
) | ||
}, | ||
onFailure = { error -> | ||
ViewState.Error(error.message ?: "") | ||
} | ||
) | ||
} | ||
) | ||
}.asLiveData() | ||
|
||
init { | ||
viewModelScope.launch { | ||
orderStatusMap.tryEmit( | ||
orderListRepository.getCachedOrderStatusOptions().mapValues { (_, value) -> | ||
value.toOrderStatus() | ||
} | ||
statusOptions.tryEmit( | ||
getOrderStatusFilterOptions() | ||
.toMutableList() | ||
.apply { | ||
this.add( | ||
index = 0, | ||
element = OrderStatusOption( | ||
key = DEFAULT_FILTER_OPTION, | ||
label = resourceProvider.getString(R.string.orderfilters_default_filter_value), | ||
statusCount = 0, | ||
isSelected = true | ||
) | ||
) | ||
} | ||
) | ||
} | ||
} | ||
|
@@ -140,13 +168,21 @@ class DashboardOrdersViewModel @AssistedInject constructor( | |
AnalyticsTracker.KEY_TYPE to DashboardWidget.Type.ORDERS.trackingIdentifier | ||
) | ||
) | ||
refreshTrigger.tryEmit(RefreshEvent(isForced = true)) | ||
_refreshTrigger.tryEmit(RefreshEvent(isForced = true)) | ||
} | ||
|
||
fun onFilterSelected(filter: OrderStatusOption) { | ||
selectedFilter.value = filter.key | ||
} | ||
|
||
sealed class ViewState { | ||
data object Loading : ViewState() | ||
data class Error(val message: String) : ViewState() | ||
data class Content(val orders: List<OrderItem>) : ViewState() | ||
data class Content( | ||
val orders: List<OrderItem>, | ||
val filterOptions: List<OrderStatusOption>, | ||
val selectedFilter: OrderStatusOption | ||
) : ViewState() | ||
|
||
@StringRes val title: Int = ORDERS.titleResource | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
np, given the usage of a SharedFlow here, if we don't have any cached values for the order status options, we won't show emit the UI state until the API call to fetch them is done.
WDYT about replacing the
MutableSharedFlow
with aMutableStateFlow
that starts withlistOf(DEFAULT_FILTER_OPTION...)
? This way we'll start withall
as a single entry at the beginning.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a clever idea 👍