diff --git a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/ActivityLogListFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/ActivityLogListFragment.kt index c216196610a9..5eb1ef43a399 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/ActivityLogListFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/ActivityLogListFragment.kt @@ -182,7 +182,7 @@ class ActivityLogListFragment : Fragment() { } private fun showActivityTypeFilterDialog(remoteSiteId: RemoteId) { - ActivityLogTypeFilterFragment.newInstance(remoteSiteId).show(parentFragmentManager, ACTIVITY_TYPE_FILTER_TAG) + ActivityLogTypeFilterFragment.newInstance(remoteSiteId).show(childFragmentManager, ACTIVITY_TYPE_FILTER_TAG) } private fun refreshProgressBars(eventListStatus: ActivityLogViewModel.ActivityLogListStatus?) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/filter/ActivityLogTypeFilterFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/filter/ActivityLogTypeFilterFragment.kt index c5ec635c7068..1e0abcc85c7a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/filter/ActivityLogTypeFilterFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/filter/ActivityLogTypeFilterFragment.kt @@ -3,6 +3,8 @@ package org.wordpress.android.ui.activitylog.list.filter import android.content.Context import android.os.Bundle import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.fragment.app.DialogFragment @@ -22,8 +24,22 @@ import org.wordpress.android.ui.activitylog.list.filter.ActivityLogTypeFilterVie import org.wordpress.android.ui.utils.UiHelpers import org.wordpress.android.util.ColorUtils import org.wordpress.android.util.getColorResIdFromAttribute +import org.wordpress.android.viewmodel.activitylog.ActivityLogViewModel import javax.inject.Inject +private const val ACTIONS_MENU_GROUP = 1 + +/** + * Show the primary action closer to user's finger. + */ +private const val PRIMARY_ACTION_ORDER = 2 +private const val SECONDARY_ACTION_ORDER = 1 +/** + * Always show the primary action no matter the screen size. + */ +private const val PRIMARY_ACTION_SHOW_ALWAYS = true +private const val SECONDARY_ACTION_SHOW_ALWAYS = false + class ActivityLogTypeFilterFragment : DialogFragment() { @Inject lateinit var viewModelFactory: ViewModelProvider.Factory @Inject lateinit var uiHelpers: UiHelpers @@ -66,17 +82,27 @@ class ActivityLogTypeFilterFragment : DialogFragment() { viewModel = ViewModelProviders.of(this, viewModelFactory) .get(ActivityLogTypeFilterViewModel::class.java) + val parentViewModel = ViewModelProviders.of(requireParentFragment(), viewModelFactory) + .get(ActivityLogViewModel::class.java) + viewModel.uiState.observe(viewLifecycleOwner, Observer { uiState -> uiHelpers.updateVisibility(actionable_empty_view, uiState.errorVisibility) uiHelpers.updateVisibility(recycler_view, uiState.contentVisibility) uiHelpers.updateVisibility(progress_layout, uiState.loadingVisibility) + refreshMenuItems(uiState) when (uiState) { is FullscreenLoading -> refreshLoadingScreen(uiState) is Error -> refreshErrorScreen(uiState) is Content -> refreshContentScreen(uiState) } }) - viewModel.start(remoteSiteId = RemoteId(requireNotNull(arguments).getLong(WordPress.REMOTE_SITE_ID))) + viewModel.dismissDialog.observe(viewLifecycleOwner, Observer { + it.applyIfNotHandled { dismiss() } + }) + viewModel.start( + remoteSiteId = RemoteId(requireNotNull(arguments).getLong(WordPress.REMOTE_SITE_ID)), + parentViewModel = parentViewModel + ) } private fun refreshLoadingScreen(uiState: FullscreenLoading) { @@ -92,7 +118,31 @@ class ActivityLogTypeFilterFragment : DialogFragment() { private fun refreshContentScreen(uiState: Content) { (recycler_view.adapter as ActivityLogTypeFilterAdapter).update(uiState.items) - // TODO malinjir implement primary and secondary actions + } + + private fun refreshMenuItems(uiState: ActivityLogTypeFilterViewModel.UiState) { + val menu = toolbar_main.menu + menu.removeGroup(ACTIONS_MENU_GROUP) + + if (uiState is Content) { + addMenuItem(uiState.primaryAction, PRIMARY_ACTION_ORDER, showAlways = PRIMARY_ACTION_SHOW_ALWAYS) + addMenuItem(uiState.secondaryAction, SECONDARY_ACTION_ORDER, showAlways = SECONDARY_ACTION_SHOW_ALWAYS) + } + } + + private fun addMenuItem(action: ActivityLogTypeFilterViewModel.Action, order: Int, showAlways: Boolean) { + val actionLabel = uiHelpers.getTextOfUiString(requireContext(), action.label) + toolbar_main.menu.add(ACTIONS_MENU_GROUP, Menu.NONE, order, actionLabel).let { + if (showAlways) { + it.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS) + } else { + it.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM) + } + it.setOnMenuItemClickListener { + action.action.invoke() + true + } + } } private fun initAdapter() { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/filter/ActivityLogTypeFilterViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/filter/ActivityLogTypeFilterViewModel.kt index 5e53214db7d3..146d43ff328d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/filter/ActivityLogTypeFilterViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/filter/ActivityLogTypeFilterViewModel.kt @@ -9,13 +9,16 @@ import org.wordpress.android.R import org.wordpress.android.fluxc.model.LocalOrRemoteId.RemoteId import org.wordpress.android.modules.BG_THREAD import org.wordpress.android.modules.UI_THREAD +import org.wordpress.android.ui.activitylog.list.filter.ActivityLogTypeFilterViewModel.ListItemUiState.ActivityType import org.wordpress.android.ui.activitylog.list.filter.ActivityLogTypeFilterViewModel.UiState.Content import org.wordpress.android.ui.activitylog.list.filter.ActivityLogTypeFilterViewModel.UiState.FullscreenLoading import org.wordpress.android.ui.activitylog.list.filter.DummyActivityTypesProvider.DummyActivityType import org.wordpress.android.ui.utils.UiString import org.wordpress.android.ui.utils.UiString.UiStringRes import org.wordpress.android.ui.utils.UiString.UiStringText +import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.ScopedViewModel +import org.wordpress.android.viewmodel.activitylog.ActivityLogViewModel import javax.inject.Inject import javax.inject.Named @@ -26,14 +29,19 @@ class ActivityLogTypeFilterViewModel @Inject constructor( ) : ScopedViewModel(mainDispatcher) { private var isStarted = false private lateinit var remoteSiteId: RemoteId + private lateinit var parentViewModel: ActivityLogViewModel private val _uiState = MutableLiveData() val uiState: LiveData = _uiState - fun start(remoteSiteId: RemoteId) { + private val _dismissDialog = MutableLiveData>() + val dismissDialog: LiveData> = _dismissDialog + + fun start(remoteSiteId: RemoteId, parentViewModel: ActivityLogViewModel) { if (isStarted) return isStarted = true this.remoteSiteId = remoteSiteId + this.parentViewModel = parentViewModel fetchAvailableActivityTypes() } @@ -60,8 +68,11 @@ class ActivityLogTypeFilterViewModel @Inject constructor( // TODO malinjir replace "it.toString()" with activity type name val activityTypeListItems: List = activityTypes .map { - ListItemUiState.ActivityType(id = it.id, title = UiStringText(it.toString())) - .apply { onClick = { onItemClicked(it.id) } } + ListItemUiState.ActivityType( + id = it.id, + title = UiStringText(it.toString()), + onClick = { onItemClicked(it.id) } + ) } Content( listOf(headerListItem) + activityTypeListItems, @@ -77,7 +88,7 @@ class ActivityLogTypeFilterViewModel @Inject constructor( (_uiState.value as? Content)?.let { content -> val updatedList = content.items.map { itemUiState -> if (itemUiState is ListItemUiState.ActivityType && itemUiState.id == itemId) { - itemUiState.copy(checked = !itemUiState.checked).apply { onClick = itemUiState.onClick } + itemUiState.copy(checked = !itemUiState.checked) } else { itemUiState } @@ -87,6 +98,8 @@ class ActivityLogTypeFilterViewModel @Inject constructor( } private fun onApplyClicked() { + parentViewModel.onActivityTypesSelected(getSelectedActivityTypeIds()) + _dismissDialog.value = Event(Unit) } private fun onRetryClicked() { @@ -108,6 +121,12 @@ class ActivityLogTypeFilterViewModel @Inject constructor( } } + private fun getSelectedActivityTypeIds(): List = + (_uiState.value as Content).items + .filterIsInstance(ActivityType::class.java) + .filter { it.checked } + .map { it.id } + sealed class UiState { open val contentVisibility = false open val loadingVisibility = false @@ -120,6 +139,7 @@ class ActivityLogTypeFilterViewModel @Inject constructor( data class Error(val retryAction: Action) : UiState() { override val errorVisibility = true + // TODO malinjir replace strings according to design val errorTitle: UiString = UiStringRes(R.string.error) val errorSubtitle: UiString = UiStringRes(R.string.hpp_retry_error) @@ -143,10 +163,9 @@ class ActivityLogTypeFilterViewModel @Inject constructor( data class ActivityType( val id: Int, val title: UiString, - val checked: Boolean = false - ) : ListItemUiState() { - lateinit var onClick: (() -> Unit) - } + val checked: Boolean = false, + val onClick: (() -> Unit) + ) : ListItemUiState() } data class Action(val label: UiString) { diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityLogViewModel.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityLogViewModel.kt index 5f54880fdf60..0c0e219dfbdd 100644 --- a/WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityLogViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityLogViewModel.kt @@ -174,9 +174,15 @@ class ActivityLogViewModel @Inject constructor( } fun onActivityTypeFilterClicked() { + // TODO malinjir pass initially selected activity types _showActivityTypeFilterDialog.value = RemoteId(site.siteId) } + fun onActivityTypesSelected(activityTypeIds: List) { + // TODO malinjir store the ids + // TODO malinjir: refetch/load data + } + fun onRewindConfirmed(rewindId: String) { rewindStatusService.rewind(rewindId, site) showRewindStartedMessage() diff --git a/WordPress/src/test/java/org/wordpress/android/ui/activitylog/list/filter/ActivityLogTypeFilterViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/activitylog/list/filter/ActivityLogTypeFilterViewModelTest.kt index 51551f682738..d36be5482b91 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/activitylog/list/filter/ActivityLogTypeFilterViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/activitylog/list/filter/ActivityLogTypeFilterViewModelTest.kt @@ -19,11 +19,13 @@ import org.wordpress.android.ui.activitylog.list.filter.ActivityLogTypeFilterVie import org.wordpress.android.ui.activitylog.list.filter.ActivityLogTypeFilterViewModel.UiState.Content import org.wordpress.android.ui.activitylog.list.filter.DummyActivityTypesProvider.DummyActivityType import org.wordpress.android.ui.activitylog.list.filter.DummyActivityTypesProvider.DummyAvailableActivityTypesResponse +import org.wordpress.android.viewmodel.activitylog.ActivityLogViewModel @InternalCoroutinesApi class ActivityLogTypeFilterViewModelTest : BaseUnitTest() { private lateinit var viewModel: ActivityLogTypeFilterViewModel @Mock private lateinit var dummyActivityTypesProvider: DummyActivityTypesProvider + @Mock private lateinit var parentViewModel: ActivityLogViewModel @Before fun setUp() { @@ -128,11 +130,54 @@ class ActivityLogTypeFilterViewModelTest : BaseUnitTest() { assertThat(((uiStates.last() as Content).items[1] as ActivityType).checked).isFalse() } + @Test + fun `dialog dismissed, when the user clicks on apply action`() = test { + val observers = init() + startVM() + + (observers.uiStates.last() as Content).primaryAction.action.invoke() + + assertThat(observers.dismissDialogEvents).isNotEmpty + } + + @Test + fun `selected items propagated to activity log, when the user clicks on apply action`() = test { + val observers = init() + startVM() + // select an item + val activityType = ((observers.uiStates.last() as Content).items[1] as ActivityType) + activityType.onClick.invoke() + + (observers.uiStates.last() as Content).primaryAction.action.invoke() + + verify(parentViewModel).onActivityTypesSelected(listOf(activityType.id)) + } + + @Test + fun `items unchecked, when the user clicks on clear action`() = test { + val uiStates = init().uiStates + startVM() + // select an item + val activityType = ((uiStates.last() as Content).items[1] as ActivityType) + activityType.onClick.invoke() + + (uiStates.last() as Content).secondaryAction.action.invoke() + + assertThat( + (uiStates.last() as Content).items.filterIsInstance(ActivityType::class.java) + .filter { it.checked } + ).isEmpty() + } + private suspend fun init(successResponse: Boolean = true, activityTypeCount: Int = 5): Observers { val uiStates = mutableListOf() + val dismissDialogEvents = mutableListOf() viewModel.uiState.observeForever { uiStates.add(it) } + viewModel.dismissDialog.observeForever { + dismissDialogEvents.add(it.peekContent()) + } whenever(dummyActivityTypesProvider.fetchAvailableActivityTypes(anyOrNull())) .thenReturn( @@ -142,16 +187,19 @@ class ActivityLogTypeFilterViewModelTest : BaseUnitTest() { DummyAvailableActivityTypesResponse(true, listOf()) } ) - return Observers((uiStates)) + return Observers(uiStates, dismissDialogEvents) } private fun startVM() { - viewModel.start(RemoteId(0L)) + viewModel.start(RemoteId(0L), parentViewModel) } private fun generateActivityTypes(count: Int): List { return (1..count).asSequence().map { DummyActivityType(it, it.toString()) }.toList() } - private data class Observers(val uiStates: List) + private data class Observers( + val uiStates: List, + val dismissDialogEvents: List + ) }