Skip to content
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

Issue/13268 implement clear and apply actions #13483

Merged
merged 10 commits into from
Dec 1, 2020
Original file line number Diff line number Diff line change
Expand Up @@ -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?) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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<UiState>()
val uiState: LiveData<UiState> = _uiState

fun start(remoteSiteId: RemoteId) {
private val _dismissDialog = MutableLiveData<Event<Unit>>()
val dismissDialog: LiveData<Event<Unit>> = _dismissDialog

fun start(remoteSiteId: RemoteId, parentViewModel: ActivityLogViewModel) {
if (isStarted) return
isStarted = true
this.remoteSiteId = remoteSiteId
this.parentViewModel = parentViewModel

fetchAvailableActivityTypes()
}
Expand All @@ -60,8 +68,11 @@ class ActivityLogTypeFilterViewModel @Inject constructor(
// TODO malinjir replace "it.toString()" with activity type name
val activityTypeListItems: List<ListItemUiState.ActivityType> = 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,
Expand All @@ -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
}
Expand All @@ -87,6 +98,8 @@ class ActivityLogTypeFilterViewModel @Inject constructor(
}

private fun onApplyClicked() {
parentViewModel.onActivityTypesSelected(getSelectedActivityTypeIds())
_dismissDialog.value = Event(Unit)
}

private fun onRetryClicked() {
Expand All @@ -108,6 +121,12 @@ class ActivityLogTypeFilterViewModel @Inject constructor(
}
}

private fun getSelectedActivityTypeIds(): List<Int> =
(_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
Expand All @@ -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)
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Int>) {
// TODO malinjir store the ids
// TODO malinjir: refetch/load data
}

fun onRewindConfirmed(rewindId: String) {
rewindStatusService.rewind(rewindId, site)
showRewindStartedMessage()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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<UiState>()
val dismissDialogEvents = mutableListOf<Unit>()
viewModel.uiState.observeForever {
uiStates.add(it)
}
viewModel.dismissDialog.observeForever {
dismissDialogEvents.add(it.peekContent())
}

whenever(dummyActivityTypesProvider.fetchAvailableActivityTypes(anyOrNull()))
.thenReturn(
Expand All @@ -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<DummyActivityType> {
return (1..count).asSequence().map { DummyActivityType(it, it.toString()) }.toList()
}

private data class Observers(val uiStates: List<UiState>)
private data class Observers(
val uiStates: List<UiState>,
val dismissDialogEvents: List<Unit>
)
}