From 8f304b5e94cc145349f5d64eb35c633a308f4d23 Mon Sep 17 00:00:00 2001 From: Hafiz Rahman Date: Fri, 13 Dec 2024 20:17:05 +0700 Subject: [PATCH 01/14] First pass at adding selection tracker to OrderListFragment. --- .../orders/OrderSelectionItemkeyProvider.kt | 26 +++++++++ .../orders/SelectableOrderListItemLookup.kt | 58 +++++++++++++++++++ .../ui/orders/list/OrderListAdapter.kt | 11 ++++ .../ui/orders/list/OrderListFragment.kt | 38 ++++++++++++ .../android/ui/orders/list/OrderListView.kt | 3 + 5 files changed, 136 insertions(+) create mode 100644 WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/OrderSelectionItemkeyProvider.kt create mode 100644 WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/SelectableOrderListItemLookup.kt diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/OrderSelectionItemkeyProvider.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/OrderSelectionItemkeyProvider.kt new file mode 100644 index 00000000000..b776f59bedb --- /dev/null +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/OrderSelectionItemkeyProvider.kt @@ -0,0 +1,26 @@ +package com.woocommerce.android.ui.orders + +import androidx.recyclerview.selection.ItemKeyProvider +import androidx.recyclerview.widget.RecyclerView +import com.woocommerce.android.ui.orders.list.OrderListAdapter +import com.woocommerce.android.ui.orders.list.OrderListItemUIType + +/** + * Class provides the selection library access to stable selection keys and identifying items + * presented by a [RecyclerView] instance. + */ +class OrderSelectionItemKeyProvider(private val recyclerView: RecyclerView) : + ItemKeyProvider(SCOPE_MAPPED) { + override fun getKey(position: Int): Long? { + return (recyclerView.adapter as? OrderListAdapter)?.currentList?.get(position)?.let { item -> + if (item is OrderListItemUIType.OrderListItemUI) item.orderId else null + } + } + + override fun getPosition(key: Long): Int { + return (recyclerView.adapter as? OrderListAdapter)?.currentList + ?.indexOfFirst { item -> + item is OrderListItemUIType.OrderListItemUI && item.orderId == key + } ?: RecyclerView.NO_POSITION + } +} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/SelectableOrderListItemLookup.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/SelectableOrderListItemLookup.kt new file mode 100644 index 00000000000..8f61f347719 --- /dev/null +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/SelectableOrderListItemLookup.kt @@ -0,0 +1,58 @@ +package com.woocommerce.android.ui.orders + +import android.view.MotionEvent +import androidx.recyclerview.selection.ItemDetailsLookup +import androidx.recyclerview.widget.RecyclerView +import com.woocommerce.android.ui.orders.list.OrderListAdapter +import com.woocommerce.android.ui.orders.list.OrderListItemUIType.OrderListItemUI + +class SelectableOrderListItemLookup(private val recyclerView: RecyclerView) : ItemDetailsLookup() { + override fun getItemDetails(event: MotionEvent): ItemDetails? = + recyclerView + .findChildViewUnder(event.x, event.y) + ?.let { view -> + recyclerView.getChildViewHolder(view)?.let { viewHolder -> + val position = viewHolder.bindingAdapterPosition + val item = (recyclerView.adapter as? OrderListAdapter)?.currentList?.get(position) + if (item is OrderListItemUI) { + SelectableOrderItemDetailsLookup(position, item.orderId) + } else { + null + } + } + } +} + +class DefaultOrderListItemLookup(private val recyclerView: RecyclerView) : ItemDetailsLookup() { + override fun getItemDetails(event: MotionEvent): ItemDetails? = + recyclerView + .findChildViewUnder(event.x, event.y) + ?.let { view -> + recyclerView.getChildViewHolder(view)?.let { viewHolder -> + val position = viewHolder.bindingAdapterPosition + val item = (recyclerView.adapter as? OrderListAdapter)?.currentList?.get(position) + if (item is OrderListItemUI) { + DefaultOrderItemDetailsLookup(position, item.orderId) + } else { + null + } + } + } +} + +class DefaultOrderItemDetailsLookup( + private val position: Int, + private val orderId: Long +) : ItemDetailsLookup.ItemDetails() { + override fun getPosition() = position + override fun getSelectionKey() = orderId +} + +class SelectableOrderItemDetailsLookup( + private val position: Int, + private val orderId: Long +) : ItemDetailsLookup.ItemDetails() { + override fun getPosition() = position + override fun getSelectionKey() = orderId + override fun inSelectionHotspot(e: MotionEvent) = true +} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListAdapter.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListAdapter.kt index 8208a26daa8..4d3e523025b 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListAdapter.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListAdapter.kt @@ -7,6 +7,7 @@ import android.view.ViewGroup import androidx.core.view.ViewCompat import androidx.paging.PagedList import androidx.paging.PagedListAdapter +import androidx.recyclerview.selection.SelectionTracker import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.ViewHolder @@ -36,6 +37,7 @@ class OrderListAdapter( var activeOrderStatusMap: Map = emptyMap() var allOrderIds: List = listOf() + var tracker: SelectionTracker? = null override fun getItemViewType(position: Int): Int { return when (getItem(position)) { @@ -165,6 +167,15 @@ class OrderListAdapter( ) viewBinding.divider.visibility = if (orderItemUI.isLastItemInSection) View.GONE else View.VISIBLE + val isSelected = tracker?.isSelected(orderItemUI.orderId) ?: orderItemUI.isSelected + viewBinding.orderItemLayout.setBackgroundColor( + if (isSelected) { + viewBinding.root.context.getColor(R.color.color_item_selected) + } else { + Color.TRANSPARENT + } + ) + when { orderItemUI.isSelected -> { viewBinding.orderItemLayout.setBackgroundColor( diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListFragment.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListFragment.kt index 463ecd55b49..48ed41c2df7 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListFragment.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListFragment.kt @@ -21,6 +21,8 @@ import androidx.navigation.findNavController import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.findNavController import androidx.paging.PagedList +import androidx.recyclerview.selection.SelectionTracker +import androidx.recyclerview.selection.StorageStrategy import androidx.recyclerview.widget.ItemTouchHelper import androidx.transition.TransitionManager import com.google.android.material.floatingactionbutton.FloatingActionButton @@ -58,6 +60,8 @@ import com.woocommerce.android.ui.jitm.JitmMessagePathsProvider import com.woocommerce.android.ui.main.AppBarStatus import com.woocommerce.android.ui.main.MainActivity import com.woocommerce.android.ui.main.MainNavigationRouter +import com.woocommerce.android.ui.orders.DefaultOrderListItemLookup +import com.woocommerce.android.ui.orders.OrderSelectionItemKeyProvider import com.woocommerce.android.ui.orders.OrderStatusUpdateSource import com.woocommerce.android.ui.orders.OrdersCommunicationViewModel import com.woocommerce.android.ui.orders.creation.CodeScannerStatus @@ -65,8 +69,10 @@ import com.woocommerce.android.ui.orders.creation.GoogleBarcodeFormatMapper.Barc import com.woocommerce.android.ui.orders.creation.OrderCreateEditViewModel import com.woocommerce.android.ui.orders.list.OrderListViewModel.OrderListEvent.ShowErrorSnack import com.woocommerce.android.ui.orders.list.OrderListViewModel.OrderListEvent.ShowOrderFilters +import com.woocommerce.android.ui.products.MutableMultipleSelectionPredicate import com.woocommerce.android.util.ChromeCustomTabUtils import com.woocommerce.android.util.CurrencyFormatter +import com.woocommerce.android.util.FeatureFlag import com.woocommerce.android.viewmodel.MultiLiveEvent import com.woocommerce.android.widgets.WCEmptyView.EmptyViewType import dagger.hilt.android.AndroidEntryPoint @@ -108,6 +114,8 @@ class OrderListFragment : @Inject lateinit var feedbackPrefs: FeedbackPrefs + private var tracker: SelectionTracker? = null + private val selectionPredicate = MutableMultipleSelectionPredicate() private val viewModel: OrderListViewModel by viewModels() private val communicationViewModel: OrdersCommunicationViewModel by activityViewModels() private var snackBar: Snackbar? = null @@ -239,6 +247,36 @@ class OrderListFragment : binding.orderFiltersCard.setClickListener { viewModel.onFiltersButtonTapped() } initCreateOrderFAB(binding.createOrderButton) initSwipeBehaviour() + + if (FeatureFlag.BULK_UPDATE_ORDERS_STATUS.isEnabled()) { + addSelectionTracker() + } + } + + private fun addSelectionTracker() { + tracker = SelectionTracker.Builder( + "orderSelection", // a string to identify our selection in the context of this fragment + binding.orderListView.ordersList, // the RecyclerView where we will apply the tracker + OrderSelectionItemKeyProvider(binding.orderListView.ordersList), // the source of selection keys + DefaultOrderListItemLookup( + binding.orderListView.ordersList + ), // the source of information about recycler items + StorageStrategy.createLongStorage() // strategy for type-safe storage of the selection state + ).withSelectionPredicate(selectionPredicate) + .build() + + binding.orderListView.adapter.tracker = tracker // Use the new property + + tracker?.addObserver( + object : SelectionTracker.SelectionObserver() { + override fun onSelectionChanged() { + val selectionCount = tracker?.selection?.size() ?: 0 + // TODO: add onSelectionChanged handling to OrderListViewModel + // Temporarily showing toast instead to debug size selection + ToastUtils.showToast(context, "Current selection count: $selectionCount") + } + } + ) } private fun setupToolbar() { diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListView.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListView.kt index 75f7f6272ed..7b424fcce03 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListView.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListView.kt @@ -29,6 +29,9 @@ class OrderListView @JvmOverloads constructor( val ordersList get() = binding.ordersList + val adapter: OrderListAdapter + get() = ordersAdapter + fun init( currencyFormatter: CurrencyFormatter, orderListListener: OrderListListener From 49fd143d49ce804056736535acbd8d6e36e7556b Mon Sep 17 00:00:00 2001 From: Hafiz Rahman Date: Mon, 16 Dec 2024 14:59:48 +0700 Subject: [PATCH 02/14] Add initial onSelectionChanged handling. --- .../ui/orders/list/OrderListFragment.kt | 6 +++- .../ui/orders/list/OrderListViewModel.kt | 28 ++++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListFragment.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListFragment.kt index 48ed41c2df7..9636300a386 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListFragment.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListFragment.kt @@ -271,9 +271,13 @@ class OrderListFragment : object : SelectionTracker.SelectionObserver() { override fun onSelectionChanged() { val selectionCount = tracker?.selection?.size() ?: 0 - // TODO: add onSelectionChanged handling to OrderListViewModel + // Temporarily showing toast instead to debug size selection ToastUtils.showToast(context, "Current selection count: $selectionCount") + + viewModel.onSelectionChanged(selectionCount) + + } } ) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListViewModel.kt index f36f24e9279..e625067f463 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListViewModel.kt @@ -211,6 +211,8 @@ class OrderListViewModel @Inject constructor( ) }.asLiveData() + fun isSelecting() = viewState.orderListState == ViewState.OrderListState.Selecting + init { lifecycleRegistry.currentState = Lifecycle.State.CREATED lifecycleRegistry.currentState = Lifecycle.State.STARTED @@ -887,6 +889,26 @@ class OrderListViewModel @Inject constructor( ) } + fun onSelectionChanged(count: Int) { + when { + count == 0 -> exitSelectionMode() + count > 0 && !isSelecting() -> enterSelectionMode(count) + count > 0 -> viewState = viewState.copy(selectionCount = count) + } + } + + private fun enterSelectionMode(count: Int) { + viewState = viewState.copy( + selectionCount = count + ) + } + + private fun exitSelectionMode() { + viewState = viewState.copy( + selectionCount = null + ) + } + sealed class OrderListEvent : Event() { data class ShowErrorSnack(@StringRes val messageRes: Int) : OrderListEvent() object ShowOrderFilters : OrderListEvent() @@ -931,10 +953,14 @@ class OrderListViewModel @Inject constructor( val isSimplePaymentsAndOrderCreationFeedbackVisible: Boolean = false, val jitmEnabled: Boolean = false, val isErrorFetchingDataBannerVisible: Boolean = false, - val shouldDisplayTroubleshootingBanner: Boolean = false + val shouldDisplayTroubleshootingBanner: Boolean = false, + val orderListState: OrderListState? = null, + val selectionCount: Int? = null ) : Parcelable { @IgnoredOnParcel val isFilteringActive = filterCount > 0 + + enum class OrderListState { Selecting, Browsing } } enum class Mode { From 5cdbf54d3f66a1d046b7112e6d5d48fa0fe74d2c Mon Sep 17 00:00:00 2001 From: Hafiz Rahman Date: Mon, 16 Dec 2024 15:21:40 +0700 Subject: [PATCH 03/14] Add initial state handling for the order list state (selecting or browsing) --- .../ui/orders/list/OrderListFragment.kt | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListFragment.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListFragment.kt index 9636300a386..266bf7690f8 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListFragment.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListFragment.kt @@ -10,6 +10,8 @@ import android.view.MenuItem.OnActionExpandListener import android.view.View import android.view.ViewGroup import androidx.activity.OnBackPressedCallback +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.view.ActionMode import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView.OnQueryTextListener import androidx.core.view.ViewGroupCompat @@ -88,7 +90,8 @@ class OrderListFragment : OnQueryTextListener, OnActionExpandListener, OrderListListener, - SwipeToComplete.OnSwipeListener { + SwipeToComplete.OnSwipeListener, + ActionMode.Callback { companion object { const val TAG: String = "OrderListFragment" const val STATE_KEY_SEARCH_QUERY = "search-query" @@ -115,6 +118,7 @@ class OrderListFragment : lateinit var feedbackPrefs: FeedbackPrefs private var tracker: SelectionTracker? = null + private var actionMode: ActionMode? = null private val selectionPredicate = MutableMultipleSelectionPredicate() private val viewModel: OrderListViewModel by viewModels() private val communicationViewModel: OrdersCommunicationViewModel by activityViewModels() @@ -689,12 +693,36 @@ class OrderListFragment : new.shouldDisplayTroubleshootingBanner.takeIfNotEqualTo(old?.shouldDisplayTroubleshootingBanner) { displayTimeoutErrorCard(it) } + new.orderListState?.takeIfNotEqualTo(old?.orderListState) { + handleListState(it) + } } viewModel.lastUpdateOrdersList.observe(viewLifecycleOwner) { lastUpdate -> binding.orderFiltersCard.updateLastUpdate(lastUpdate) } } + private fun handleListState(orderListState: OrderListViewModel.ViewState.OrderListState) { + when (orderListState) { + OrderListViewModel.ViewState.OrderListState.Selecting -> { + actionMode = (requireActivity() as AppCompatActivity) + .startSupportActionMode(this@OrderListFragment) + delayMultiSelection() + } + + OrderListViewModel.ViewState.OrderListState.Browsing -> { + actionMode?.finish() + } + } + } + + private fun delayMultiSelection() { + selectionPredicate.selectMultiple = false + binding.orderListView.ordersList.post { + selectionPredicate.selectMultiple = true + } + } + private fun openFirstOrder() { binding.orderListView.openFirstOrder() } @@ -1088,4 +1116,19 @@ class OrderListFragment : extraTags = ArrayList() ).let { activity?.startActivity(it) } } + + override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { + TODO("Not yet implemented") + } + + override fun onPrepareActionMode(mode: ActionMode, menu: Menu) = false + + override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean { + TODO("Not yet implemented") + } + + override fun onDestroyActionMode(mode: ActionMode) { + tracker?.clearSelection() + actionMode = null + } } From 763308ad9b5aefbd07bf3daf74df22ecb5f7ea84 Mon Sep 17 00:00:00 2001 From: Hafiz Rahman Date: Mon, 16 Dec 2024 15:58:30 +0700 Subject: [PATCH 04/14] Show menu and toolbar selection text when some orders are selected. --- .../ui/orders/list/OrderListFragment.kt | 31 +++++++++++++------ .../ui/orders/list/OrderListViewModel.kt | 2 ++ .../res/menu/menu_action_mode_orders_list.xml | 8 +++++ WooCommerce/src/main/res/values/strings.xml | 3 ++ 4 files changed, 34 insertions(+), 10 deletions(-) create mode 100644 WooCommerce/src/main/res/menu/menu_action_mode_orders_list.xml diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListFragment.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListFragment.kt index 266bf7690f8..a18677a9164 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListFragment.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListFragment.kt @@ -75,6 +75,7 @@ import com.woocommerce.android.ui.products.MutableMultipleSelectionPredicate import com.woocommerce.android.util.ChromeCustomTabUtils import com.woocommerce.android.util.CurrencyFormatter import com.woocommerce.android.util.FeatureFlag +import com.woocommerce.android.util.StringUtils import com.woocommerce.android.viewmodel.MultiLiveEvent import com.woocommerce.android.widgets.WCEmptyView.EmptyViewType import dagger.hilt.android.AndroidEntryPoint @@ -275,13 +276,7 @@ class OrderListFragment : object : SelectionTracker.SelectionObserver() { override fun onSelectionChanged() { val selectionCount = tracker?.selection?.size() ?: 0 - - // Temporarily showing toast instead to debug size selection - ToastUtils.showToast(context, "Current selection count: $selectionCount") - viewModel.onSelectionChanged(selectionCount) - - } } ) @@ -696,6 +691,14 @@ class OrderListFragment : new.orderListState?.takeIfNotEqualTo(old?.orderListState) { handleListState(it) } + new.selectionCount?.takeIfNotEqualTo(old?.selectionCount) { count -> + actionMode?.title = StringUtils.getQuantityString( + context = requireContext(), + quantity = count, + default = R.string.orderlist_selection_count, + one = R.string.orderlist_selection_count_single + ) + } } viewModel.lastUpdateOrdersList.observe(viewLifecycleOwner) { lastUpdate -> binding.orderFiltersCard.updateLastUpdate(lastUpdate) @@ -1117,14 +1120,22 @@ class OrderListFragment : ).let { activity?.startActivity(it) } } - override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { - TODO("Not yet implemented") + override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { + mode.menuInflater.inflate(R.menu.menu_action_mode_orders_list, menu) + return true } override fun onPrepareActionMode(mode: ActionMode, menu: Menu) = false - override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean { - TODO("Not yet implemented") + override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { + return when (item.itemId) { + R.id.menu_orderlist_update_status -> { + // todo: implement bulk update status + true + } + + else -> false + } } override fun onDestroyActionMode(mode: ActionMode) { diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListViewModel.kt index e625067f463..56d3406ccea 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/orders/list/OrderListViewModel.kt @@ -899,12 +899,14 @@ class OrderListViewModel @Inject constructor( private fun enterSelectionMode(count: Int) { viewState = viewState.copy( + orderListState = ViewState.OrderListState.Selecting, selectionCount = count ) } private fun exitSelectionMode() { viewState = viewState.copy( + orderListState = ViewState.OrderListState.Browsing, selectionCount = null ) } diff --git a/WooCommerce/src/main/res/menu/menu_action_mode_orders_list.xml b/WooCommerce/src/main/res/menu/menu_action_mode_orders_list.xml new file mode 100644 index 00000000000..8e7ed21acc4 --- /dev/null +++ b/WooCommerce/src/main/res/menu/menu_action_mode_orders_list.xml @@ -0,0 +1,8 @@ + + + + diff --git a/WooCommerce/src/main/res/values/strings.xml b/WooCommerce/src/main/res/values/strings.xml index 467a5f99742..f4648c0bf3f 100644 --- a/WooCommerce/src/main/res/values/strings.xml +++ b/WooCommerce/src/main/res/values/strings.xml @@ -602,6 +602,9 @@ If your data still isn\'t loading, contact our support team for assistance. Order trashed Error trashing order + %d orders selected + %d order selected + Update status