Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
acce241
Update to latest FluxC commit hash
Aug 3, 2018
a2b2a10
Write fetching stats errors to WooLog
Aug 3, 2018
2344504
Update to use the commit hash for the FluxC branch that supports the …
Aug 6, 2018
45cc9aa
Create custom orders to fulfill dashboard card
Aug 6, 2018
fcfb137
Add orders dashboard card to dashboard view
Aug 6, 2018
20c5e0a
Add WCOrderStore to constructor to ensure it gets initialized for use
Aug 6, 2018
2b758e2
Remove unnecessary null definition
Aug 6, 2018
3c3f8fd
Add support for displaying and populating orders dashboard card
Aug 6, 2018
cdd7bcf
Update the filter listener to pass back the filter string
Aug 7, 2018
92611d9
Add hooks to open order list from MainActivity
Aug 7, 2018
20186d9
Add a way to defer initial loading of a top level fragment
Aug 7, 2018
12d2366
Add logic to open a filtered orders list
Aug 7, 2018
4b0a557
Fix the way dashboard determines visibility
Aug 7, 2018
a6f1536
Fix the way Orders List determines visibility
Aug 7, 2018
c76185c
Add refresh dashboard cards logic
Aug 7, 2018
4680abc
Add unit tests for orders dashboard card
Aug 7, 2018
ca11163
Use the much safer Kotlin `as?` operator to avoid runtime exceptions
Aug 7, 2018
07c6356
Refactor to remove need for the dangerous Kotlin `as` operator
Aug 7, 2018
44d8028
Refactor to remove need for the dangerous Kotlin `as` operator
Aug 7, 2018
f7f439a
Fix unit tests
Aug 7, 2018
e0d40ab
Remove pluralized strings and replaced with a utility method much lik…
Aug 8, 2018
2038fad
Ignore post order note events
Aug 8, 2018
78fc331
Add missing button style
Aug 8, 2018
5b1863a
Rename dashboard orders card and related assets, variables, and methods.
Aug 8, 2018
1217c0c
Consolidate and optimize refresh code
Aug 8, 2018
c72770a
Animate unfilled orders card visibility changes
Aug 8, 2018
260f56d
Fixed a minor animation flicker
Aug 10, 2018
c35dde7
Add support for displaying a "+" sign if more orders are available fo…
Aug 10, 2018
a050e18
Fix DashboardPresenter tests
Aug 11, 2018
9783776
Fix DashboardPresenter tests
Aug 11, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ abstract class TopLevelFragment : Fragment(), TopLevelFragmentView {
// fragment is currently hosting a child fragment (drilled in).
const val CHILD_FRAGMENT_ACTIVE = "child-fragment-active"
}

/**
* The extending class may use this variable to defer a part of its
* normal initialization until manually requested.
*/
var deferInit: Boolean = false

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
childFragmentManager.addOnBackStackChangedListener(this)
Expand Down Expand Up @@ -106,7 +113,7 @@ abstract class TopLevelFragment : Fragment(), TopLevelFragmentView {
* and set the title to the active child fragments title.
*/
private fun updateParentViewState(childActive: Boolean) {
val mainActivity: AppCompatActivity? = activity as AppCompatActivity
val mainActivity: AppCompatActivity? = activity as? AppCompatActivity
if (childActive) {
container.getChildAt(0).visibility = View.GONE
mainActivity?.supportActionBar?.setDisplayHomeAsUpEnabled(true)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.woocommerce.android.ui.base

interface TopLevelFragmentRouter {
fun showOrderList(orderStatusFilter: String? = null)
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ interface DashboardContract {
interface Presenter : BasePresenter<View> {
fun loadStats(granularity: StatsGranularity, forced: Boolean = false)
fun getStatsCurrency(): String?
fun fetchUnfilledOrderCount()
}

interface View : BaseView<Presenter> {
var isActive: Boolean

fun refreshDashboard()
fun setLoadingIndicator(active: Boolean)
fun showStats(revenueStats: Map<String, Double>, salesStats: Map<String, Int>, granularity: StatsGranularity)
fun hideUnfilledOrdersCard()
fun showUnfilledOrdersCard(count: Int, canLoadMore: Boolean)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import android.view.ViewGroup
import com.woocommerce.android.R
import com.woocommerce.android.tools.SelectedSite
import com.woocommerce.android.ui.base.TopLevelFragment
import com.woocommerce.android.ui.base.TopLevelFragmentRouter
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_dashboard.*
import kotlinx.android.synthetic.main.fragment_dashboard.view.*
import org.wordpress.android.fluxc.network.rest.wpcom.wc.order.CoreOrderStatus
import org.wordpress.android.fluxc.store.WCStatsStore.StatsGranularity
import javax.inject.Inject

Expand All @@ -27,7 +29,7 @@ class DashboardFragment : TopLevelFragment(), DashboardContract.View, DashboardS
private var loadDataPending = false // If true, the fragment will refresh its data when it's visible

override var isActive: Boolean = false
get() = childFragmentManager.backStackEntryCount == 0
get() = childFragmentManager.backStackEntryCount == 0 && !isHidden

override fun onAttach(context: Context?) {
AndroidSupportInjection.inject(this)
Expand All @@ -50,8 +52,7 @@ class DashboardFragment : TopLevelFragment(), DashboardContract.View, DashboardS
)
}
setOnRefreshListener {
setLoadingIndicator(true)
presenter.loadStats(dashboard_stats.activeGranularity, forced = true)
refreshDashboard()
}
}
}
Expand All @@ -66,20 +67,26 @@ class DashboardFragment : TopLevelFragment(), DashboardContract.View, DashboardS
if (isActive) {
setLoadingIndicator(true)
dashboard_stats.initView(listener = this, selectedSite = selectedSite)
dashboard_unfilled_orders.initView(object : DashboardUnfilledOrdersCard.Listener {
override fun onViewOrdersClicked() {
(activity as? TopLevelFragmentRouter)?.showOrderList(CoreOrderStatus.PROCESSING.value)
}
})
presenter.fetchUnfilledOrderCount()
presenter.loadStats(dashboard_stats.activeGranularity)
} else {
loadDataPending = true
}
}

override fun onBackStackChanged() {
super.onBackStackChanged()
override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)

// If this fragment is now visible and we've deferred loading data due to it not
// being visible - go ahead and load the data.
if (isActive && loadDataPending) {
loadDataPending = false
setLoadingIndicator(true)
presenter.loadStats(dashboard_stats.activeGranularity)
hideUnfilledOrdersCard()
refreshDashboard()
}
}

Expand Down Expand Up @@ -112,15 +119,39 @@ class DashboardFragment : TopLevelFragment(), DashboardContract.View, DashboardS
}

override fun refreshFragmentState() {
if (isActive) {
setLoadingIndicator(true)
presenter.loadStats(dashboard_stats.activeGranularity, forced = true)
} else {
loadDataPending = true
refreshDashboard()
}

override fun refreshDashboard() {
// If this fragment is currently active, force a refresh of data. If not, set
// a flag to force a refresh when it becomes active
when {
isActive -> {
loadDataPending = false
setLoadingIndicator(true)
presenter.loadStats(dashboard_stats.activeGranularity, forced = true)
presenter.fetchUnfilledOrderCount()
}
else -> loadDataPending = true
}
}

override fun onRequestLoadStats(period: StatsGranularity) {
presenter.loadStats(period)
}

override fun hideUnfilledOrdersCard() {
with(dashboard_unfilled_orders) {
post { visibility = View.GONE }
}
}

override fun showUnfilledOrdersCard(count: Int, canLoadMore: Boolean) {
with(dashboard_unfilled_orders) {
post {
updateOrdersCount(count, canLoadMore)
visibility = View.VISIBLE
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
package com.woocommerce.android.ui.dashboard

import com.woocommerce.android.tools.SelectedSite
import com.woocommerce.android.util.WooLog
import com.woocommerce.android.util.WooLog.T
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.wordpress.android.fluxc.Dispatcher
import org.wordpress.android.fluxc.action.WCOrderAction
import org.wordpress.android.fluxc.generated.WCOrderActionBuilder
import org.wordpress.android.fluxc.generated.WCStatsActionBuilder
import org.wordpress.android.fluxc.network.rest.wpcom.wc.order.CoreOrderStatus
import org.wordpress.android.fluxc.store.WCOrderStore
import org.wordpress.android.fluxc.store.WCOrderStore.FetchOrdersCountPayload
import org.wordpress.android.fluxc.store.WCOrderStore.OnOrderChanged
import org.wordpress.android.fluxc.store.WCStatsStore
import org.wordpress.android.fluxc.store.WCStatsStore.FetchOrderStatsPayload
import org.wordpress.android.fluxc.store.WCStatsStore.OnWCStatsChanged
Expand All @@ -14,8 +22,13 @@ import javax.inject.Inject
class DashboardPresenter @Inject constructor(
private val dispatcher: Dispatcher,
private val wcStatsStore: WCStatsStore,
private val wcOrderStore: WCOrderStore, // Required to ensure the WCOrderStore is initialized!
private val selectedSite: SelectedSite
) : DashboardContract.Presenter {
companion object {
private val TAG = DashboardPresenter::class.java
}

private var dashboardView: DashboardContract.View? = null

override fun takeView(view: DashboardContract.View) {
Expand All @@ -35,10 +48,16 @@ class DashboardPresenter @Inject constructor(

override fun getStatsCurrency() = wcStatsStore.getStatsCurrencyForSite(selectedSite.get())

override fun fetchUnfilledOrderCount() {
val payload = FetchOrdersCountPayload(selectedSite.get(), CoreOrderStatus.PROCESSING.value)
dispatcher.dispatch(WCOrderActionBuilder.newFetchOrdersCountAction(payload))
}

@Suppress("unused")
@Subscribe(threadMode = ThreadMode.MAIN)
fun onWCStatsChanged(event: OnWCStatsChanged) {
if (event.isError) {
WooLog.e(T.DASHBOARD, "$TAG - Error fetching stats: ${event.error.message}")
// TODO: Notify the user of the problem
// For now, send empty data so views aren't stuck in loading mode
dashboardView?.showStats(emptyMap(), emptyMap(), event.granularity)
Expand All @@ -50,4 +69,31 @@ class DashboardPresenter @Inject constructor(

dashboardView?.showStats(revenueStats, orderStats, event.granularity)
}

@Suppress("unused")
@Subscribe(threadMode = ThreadMode.MAIN)
fun onOrderChanged(event: OnOrderChanged) {
event.causeOfChange?.takeIf { it == WCOrderAction.FETCH_ORDERS_COUNT }?.let { _ ->
if (event.isError) {
WooLog.e(T.DASHBOARD,
"$TAG - Error fetching a count of orders waiting to be fulfilled: ${event.error.message}")
dashboardView?.hideUnfilledOrdersCard()
return
}
event.rowsAffected.takeIf { it > 0 }?.let { count ->
dashboardView?.showUnfilledOrdersCard(count, event.canLoadMore)
} ?: dashboardView?.hideUnfilledOrdersCard()
} ?: if (!event.isError && !isIgnoredOrderEvent(event.causeOfChange)) {
dashboardView?.refreshDashboard()
}
}

/**
* Use this function to add [OnOrderChanged] events that should be ignored.
*/
private fun isIgnoredOrderEvent(actionType: WCOrderAction?): Boolean {
return actionType == null ||
actionType == WCOrderAction.FETCH_ORDER_NOTES ||
actionType == WCOrderAction.POST_ORDER_NOTE
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.woocommerce.android.ui.dashboard

import android.content.Context
import android.support.constraint.ConstraintLayout
import android.util.AttributeSet
import android.view.View
import com.woocommerce.android.R
import com.woocommerce.android.util.StringUtils
import kotlinx.android.synthetic.main.dashboard_unfilled_orders.view.*

/**
* Dashboard card that displays the total number of orders awaiting fulfillment and a button to display
* those orders.
*/
class DashboardUnfilledOrdersCard @JvmOverloads constructor(ctx: Context, attrs: AttributeSet? = null)
: ConstraintLayout(ctx, attrs) {
init {
View.inflate(context, R.layout.dashboard_unfilled_orders, this)
}

interface Listener {
fun onViewOrdersClicked()
}

fun initView(listener: Listener) {
alertAction_action.setOnClickListener {
listener.onViewOrdersClicked()
}
}

/**
* Updates the title of the unfilled orders dashboard card using strings that match the
* quantity of the order count.
*
* NOTE: The title text is not using the [StringUtils.getQuantityString] method because it
* temporarily requires a way of displaying a "+" sign if [canLoadMore] is true. Once we
* have an API endpoint that can deliver a total for all orders this can be removed.
*/
fun updateOrdersCount(count: Int, canLoadMore: Boolean) {
alertAction_title.text = when {
count == 1 -> resources.getString(R.string.dashboard_fulfill_order_title)
canLoadMore -> resources.getString(R.string.dashboard_fulfill_orders_title, count, "+")
else -> resources.getString(R.string.dashboard_fulfill_orders_title, count, "")
}

alertAction_action.text = StringUtils.getQuantityString(
context, count, R.string.dashboard_action_view_orders, one = R.string.dashboard_action_view_order)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import com.woocommerce.android.tools.SelectedSite
import com.woocommerce.android.ui.base.TopLevelFragment
import com.woocommerce.android.ui.login.LoginActivity
import com.woocommerce.android.ui.login.LoginEpilogueActivity
import com.woocommerce.android.ui.main.BottomNavigationPosition.ORDERS
import com.woocommerce.android.ui.orders.OrderListFragment
import com.woocommerce.android.ui.prefs.AppSettingsActivity
import com.woocommerce.android.util.ActivityUtils
import dagger.android.AndroidInjection
Expand Down Expand Up @@ -260,7 +262,7 @@ class MainActivity : AppCompatActivity(),
override fun onNavigationItemReselected(item: MenuItem) {
val activeFragment = supportFragmentManager.findFragmentByTag(activeNavPosition.getTag())
if (!clearFragmentBackStack(activeFragment)) {
(activeFragment as TopLevelFragment).refreshFragmentState()
(activeFragment as? TopLevelFragment)?.refreshFragmentState()
}

val stat = when (activeNavPosition) {
Expand All @@ -282,8 +284,8 @@ class MainActivity : AppCompatActivity(),
* Extension function for retrieving an existing fragment from the [FragmentManager]
* if one exists, if not, create a new instance of the requested fragment.
*/
private fun FragmentManager.findFragment(position: BottomNavigationPosition): Fragment {
return findFragmentByTag(position.getTag()) ?: position.createFragment()
private fun FragmentManager.findFragment(position: BottomNavigationPosition): TopLevelFragment? {
return (findFragmentByTag(position.getTag()) ?: position.createFragment()) as? TopLevelFragment
}

/**
Expand All @@ -294,24 +296,31 @@ class MainActivity : AppCompatActivity(),
* hide the current top-level fragment and add/show the destination top-level fragment.
*
* Immediately execute transactions with FragmentManager#executePendingTransactions.
*
* @param navPosition The [BottomNavigationPosition] to activate
* @param deferInit If true, the [TopLevelFragment] may use this variable to defer a part of its
* normal initialization until manually requested.
*/
private fun switchFragment(navPosition: BottomNavigationPosition): Boolean {
private fun switchFragment(navPosition: BottomNavigationPosition, deferInit: Boolean = false): Boolean {
val activeFragment = supportFragmentManager.findFragmentByTag(activeNavPosition.getTag())

// Remove any child fragments in the back stack
clearFragmentBackStack(activeFragment)

// Grab the requested top-level fragment and load if not already
// in the current view.
val fragment = supportFragmentManager.findFragment(navPosition)
if (fragment.isHidden || !fragment.isAdded) {
// Remove the active fragment and replace with this newly selected one
hideParentFragment(activeFragment)
showTopLevelFragment(fragment as TopLevelFragment, navPosition.getTag())
supportFragmentManager.executePendingTransactions()
activeNavPosition = navPosition
return true
supportFragmentManager.findFragment(navPosition)?.let { frag ->
frag.deferInit = deferInit
if (frag.isHidden || !frag.isAdded) {
// Remove the active fragment and replace with this newly selected one
hideParentFragment(activeFragment)
showTopLevelFragment(frag, navPosition.getTag())
supportFragmentManager.executePendingTransactions()
activeNavPosition = navPosition
return true
}
}

return false
}

Expand Down Expand Up @@ -351,4 +360,16 @@ class MainActivity : AppCompatActivity(),
return false
}
// endregion

override fun showOrderList(orderStatusFilter: String?) {
val navPos = BottomNavigationPosition.ORDERS.position

if (switchFragment(ORDERS, true)) {
// Set the active bottom bar selection without firing its changed event
bottom_nav.active(navPos)

val fragment = supportFragmentManager.findFragment(ORDERS)
(fragment as? OrderListFragment)?.onFilterSelected(orderStatusFilter)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ package com.woocommerce.android.ui.main

import com.woocommerce.android.ui.base.BasePresenter
import com.woocommerce.android.ui.base.BaseView
import com.woocommerce.android.ui.base.TopLevelFragmentRouter

interface MainContract {
interface Presenter : BasePresenter<View> {
fun userIsLoggedIn(): Boolean
fun storeMagicLinkToken(token: String)
}

interface View : BaseView<Presenter> {
interface View : BaseView<Presenter>, TopLevelFragmentRouter {
fun notifyTokenUpdated()
fun showLoginScreen()
fun showLoginEpilogueScreen()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ interface OrderListContract {
fun showNoOrders()
fun refreshFragmentState()
fun showLoadOrdersError()
fun onFilterSelected(orderStatus: String?)
}
}
Loading