Skip to content

Commit 5ca4c93

Browse files
author
Sharma Elanthiriayan
committed
Merge branch 'trunk' into issue/5829-ReviewsViewController-tests
2 parents e4dd149 + 0d4ac9a commit 5ca4c93

File tree

7 files changed

+132
-30
lines changed

7 files changed

+132
-30
lines changed

WooCommerce/Classes/ViewRelated/Coupons/CouponListViewController.swift

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ final class CouponListViewController: UIViewController {
1010
///
1111
private var emptyStateViewController: UIViewController?
1212

13+
/// Pull To Refresh Support.
14+
///
15+
private lazy var refreshControl: UIRefreshControl = {
16+
let refreshControl = UIRefreshControl()
17+
refreshControl.addTarget(self, action: #selector(refreshCouponList), for: .valueChanged)
18+
return refreshControl
19+
}()
20+
1321
private var subscriptions: Set<AnyCancellable> = []
1422

1523
init(siteID: Int64) {
@@ -33,15 +41,16 @@ final class CouponListViewController: UIViewController {
3341
.removeDuplicates()
3442
.sink { [weak self] state in
3543
guard let self = self else { return }
36-
self.removeNoResultsOverlay()
37-
self.removePlaceholderCoupons()
44+
self.resetViews()
3845
switch state {
3946
case .empty:
4047
self.displayNoResultsOverlay()
4148
case .loading:
4249
self.displayPlaceholderCoupons()
4350
case .coupons:
4451
self.tableView.reloadData()
52+
case .refreshing:
53+
self.refreshControl.beginRefreshing()
4554
case .initialized:
4655
break
4756
}
@@ -53,6 +62,25 @@ final class CouponListViewController: UIViewController {
5362
}
5463
}
5564

65+
// MARK: - Actions
66+
private extension CouponListViewController {
67+
/// Triggers a refresh for the coupon list
68+
///
69+
@objc func refreshCouponList() {
70+
viewModel.refreshCoupons()
71+
}
72+
73+
/// Removes overlays and loading indicators if present.
74+
///
75+
func resetViews() {
76+
removeNoResultsOverlay()
77+
removePlaceholderCoupons()
78+
if refreshControl.isRefreshing {
79+
refreshControl.endRefreshing()
80+
}
81+
}
82+
}
83+
5684

5785
// MARK: - View Configuration
5886
//
@@ -66,6 +94,7 @@ private extension CouponListViewController {
6694
tableView.dataSource = self
6795
tableView.estimatedRowHeight = Constants.estimatedRowHeight
6896
tableView.rowHeight = UITableView.automaticDimension
97+
tableView.addSubview(refreshControl)
6998
}
7099

71100
func registerTableViewCells() {

WooCommerce/Classes/ViewRelated/Coupons/CouponManagementListViewModel.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ enum CouponListState {
1515
case loading // View should show ghost cells
1616
case empty // View should display the empty state
1717
case coupons // View should display the contents of `couponViewModels`
18+
case refreshing // View should display the refresh control
1819
}
1920

2021
final class CouponListViewModel {
@@ -116,6 +117,12 @@ final class CouponListViewModel {
116117
func coupon(at indexPath: IndexPath) -> Coupon? {
117118
return resultsController.safeObject(at: indexPath)
118119
}
120+
121+
/// Triggers a refresh of loaded coupons
122+
///
123+
func refreshCoupons() {
124+
syncingCoordinator.resynchronize(reason: nil, onCompletion: nil)
125+
}
119126
}
120127

121128

@@ -133,7 +140,7 @@ extension CouponListViewModel: SyncingCoordinatorDelegate {
133140
pageSize: Int,
134141
reason: String?,
135142
onCompletion: ((Bool) -> Void)?) {
136-
transitionToSyncingState(pageNumber: pageNumber)
143+
transitionToSyncingState(pageNumber: pageNumber, hasData: couponViewModels.isNotEmpty)
137144
let action = CouponAction
138145
.synchronizeCoupons(siteID: siteID,
139146
pageNumber: pageNumber,
@@ -162,9 +169,9 @@ extension CouponListViewModel: SyncingCoordinatorDelegate {
162169
// MARK: - Pagination
163170
//
164171
private extension CouponListViewModel {
165-
func transitionToSyncingState(pageNumber: Int) {
172+
func transitionToSyncingState(pageNumber: Int, hasData: Bool) {
166173
if pageNumber == 1 {
167-
state = .loading
174+
state = hasData ? .refreshing : .loading
168175
}
169176
}
170177

WooCommerce/Classes/ViewRelated/Dashboard/Stats v4/StoreStatsAndTopPerformersPeriodViewController.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,10 +189,15 @@ extension StoreStatsAndTopPerformersPeriodViewController {
189189
topPerformersPeriodViewController.displayGhostContent()
190190
}
191191

192-
/// Unlocks the and removes the Placeholder Content
192+
/// Removes the placeholder content for store stats.
193193
///
194-
func removeGhostContent() {
194+
func removeStoreStatsGhostContent() {
195195
storeStatsPeriodViewController.removeGhostContent()
196+
}
197+
198+
/// Removes the placeholder content for top performers.
199+
///
200+
func removeTopPerformersGhostContent() {
196201
topPerformersPeriodViewController.removeGhostContent()
197202
}
198203

WooCommerce/Classes/ViewRelated/Dashboard/Stats v4/StoreStatsAndTopPerformersViewController.swift

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,6 @@ private extension StoreStatsAndTopPerformersViewController {
110110
defer {
111111
group.notify(queue: .main) { [weak self] in
112112
self?.isSyncing = false
113-
self?.removeGhostContent()
114113
self?.showSpinner(shouldShowSpinner: false)
115114
if let error = syncError {
116115
DDLogError("⛔️ Error loading dashboard: \(error)")
@@ -151,8 +150,12 @@ private extension StoreStatsAndTopPerformersViewController {
151150
// For tasks dispatched for each time period.
152151
let periodGroup = DispatchGroup()
153152

153+
// For tasks dispatched for store stats (order and visitor stats) for each time period.
154+
let periodStoreStatsGroup = DispatchGroup()
155+
154156
group.enter()
155157
periodGroup.enter()
158+
periodStoreStatsGroup.enter()
156159
self.syncStats(for: siteID,
157160
siteTimezone: timezoneForSync,
158161
timeRange: vc.timeRange,
@@ -166,10 +169,12 @@ private extension StoreStatsAndTopPerformersViewController {
166169
}
167170
group.leave()
168171
periodGroup.leave()
172+
periodStoreStatsGroup.leave()
169173
}
170174

171175
group.enter()
172176
periodGroup.enter()
177+
periodStoreStatsGroup.enter()
173178
self.syncSiteVisitStats(for: siteID,
174179
siteTimezone: timezoneForSync,
175180
timeRange: vc.timeRange,
@@ -180,6 +185,7 @@ private extension StoreStatsAndTopPerformersViewController {
180185
}
181186
group.leave()
182187
periodGroup.leave()
188+
periodStoreStatsGroup.leave()
183189
}
184190

185191
group.enter()
@@ -194,6 +200,8 @@ private extension StoreStatsAndTopPerformersViewController {
194200
}
195201
group.leave()
196202
periodGroup.leave()
203+
204+
vc.removeTopPerformersGhostContent()
197205
}
198206

199207
periodGroup.notify(queue: .main) {
@@ -204,6 +212,10 @@ private extension StoreStatsAndTopPerformersViewController {
204212
syncError = periodSyncError
205213
}
206214
}
215+
216+
periodStoreStatsGroup.notify(queue: .main) {
217+
vc.removeStoreStatsGhostContent()
218+
}
207219
}
208220
}
209221

@@ -225,27 +237,12 @@ private extension StoreStatsAndTopPerformersViewController {
225237
/// Displays the Ghost Placeholder whenever there is no visible data.
226238
///
227239
func ensureGhostContentIsDisplayed() {
228-
guard visibleChildViewController.shouldDisplayStoreStatsGhostContent else {
229-
return
240+
periodVCs.forEach { periodVC in
241+
guard periodVC.shouldDisplayStoreStatsGhostContent else {
242+
return
243+
}
244+
periodVC.displayGhostContent()
230245
}
231-
232-
displayGhostContent()
233-
}
234-
235-
/// Locks UI Interaction and displays Ghost Placeholder animations.
236-
///
237-
func displayGhostContent() {
238-
view.isUserInteractionEnabled = false
239-
buttonBarView.startGhostAnimation(style: .wooDefaultGhostStyle)
240-
visibleChildViewController.displayGhostContent()
241-
}
242-
243-
/// Unlocks the and removes the Placeholder Content
244-
///
245-
func removeGhostContent() {
246-
view.isUserInteractionEnabled = true
247-
buttonBarView.stopGhostAnimation()
248-
visibleChildViewController.removeGhostContent()
249246
}
250247

251248
/// If the Ghost Content was previously onscreen, this method will restart the animations.

WooCommerce/Classes/ViewRelated/Dashboard/Stats v4/StoreStatsV4PeriodViewController.swift

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Charts
22
import Combine
33
import UIKit
4+
import struct WordPressUI.GhostStyle
45
import Yosemite
56

67
/// Different display modes of site visit stats
@@ -160,6 +161,13 @@ final class StoreStatsV4PeriodViewController: UIViewController {
160161
observeReloadChartAnimated()
161162
}
162163

164+
override func viewWillAppear(_ animated: Bool) {
165+
super.viewWillAppear(animated)
166+
167+
// After returning to the My Store tab, `restartGhostAnimation` is required to resume ghost animation.
168+
restartGhostAnimationIfNeeded()
169+
}
170+
163171
override func viewDidAppear(_ animated: Bool) {
164172
super.viewDidAppear(animated)
165173
reloadAllFields()
@@ -243,7 +251,7 @@ extension StoreStatsV4PeriodViewController {
243251
///
244252
func displayGhostContent() {
245253
ensurePlaceholderIsVisible()
246-
placeholderChartsView.startGhostAnimation(style: .wooDefaultGhostStyle)
254+
placeholderChartsView.startGhostAnimation(style: Constants.ghostStyle)
247255
}
248256

249257
/// Removes the Placeholder Content.
@@ -267,6 +275,12 @@ extension StoreStatsV4PeriodViewController {
267275
view.pinSubviewToAllEdges(placeholderChartsView)
268276
}
269277

278+
private func restartGhostAnimationIfNeeded() {
279+
guard placeholderChartsView.superview != nil else {
280+
return
281+
}
282+
placeholderChartsView.restartGhostAnimation(style: Constants.ghostStyle)
283+
}
270284
}
271285

272286
// MARK: - Configuration
@@ -748,5 +762,7 @@ private extension StoreStatsV4PeriodViewController {
748762

749763
static let containerBackgroundColor: UIColor = .systemBackground
750764
static let headerComponentBackgroundColor: UIColor = .clear
765+
766+
static let ghostStyle: GhostStyle = .wooDefaultGhostStyle
751767
}
752768
}

WooCommerce/Classes/ViewRelated/Orders/OrderListViewController.swift

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ final class OrderListViewController: UIViewController {
158158
//
159159
// We can remove this once we've replaced XLPagerTabStrip.
160160
tableView.reloadData()
161+
162+
restartPlaceholderAnimation()
161163
}
162164

163165
/// Returns a function that creates cells for `dataSource`.
@@ -410,7 +412,7 @@ private extension OrderListViewController {
410412
// let's reset the state before using it again
411413
ghostableTableView.removeGhostContent()
412414
ghostableTableView.displayGhostContent(options: options,
413-
style: .wooDefaultGhostStyle)
415+
style: Constants.ghostStyle)
414416
ghostableTableView.startGhostAnimation()
415417
ghostableTableView.isHidden = false
416418
}
@@ -423,6 +425,14 @@ private extension OrderListViewController {
423425
ghostableTableView.removeGhostContent()
424426
}
425427

428+
/// After returning to the screen, `restartGhostAnimation` is required to resume ghost animation.
429+
func restartPlaceholderAnimation() {
430+
guard ghostableTableView.isHidden == false else {
431+
return
432+
}
433+
ghostableTableView.restartGhostAnimation(style: Constants.ghostStyle)
434+
}
435+
426436
/// Shows the EmptyStateViewController
427437
///
428438
func displayEmptyViewController() {
@@ -675,4 +685,8 @@ private extension OrderListViewController {
675685
case results
676686
case empty
677687
}
688+
689+
enum Constants {
690+
static let ghostStyle: GhostStyle = .wooDefaultGhostStyle
691+
}
678692
}

WooCommerce/WooCommerceTests/ViewRelated/Coupons/CouponListViewModelTests.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,38 @@ final class CouponListViewModelTests: XCTestCase {
111111
// Then
112112
XCTAssertEqual(sut.state, .empty)
113113
}
114+
115+
func test_refreshCoupon_updates_state_to_refreshing() {
116+
// Given
117+
setUpWithCouponFetched() // we need to have existing data to enter refreshing state
118+
119+
// When
120+
sut.refreshCoupons()
121+
122+
// Then
123+
XCTAssertEqual(sut.state, .refreshing)
124+
}
125+
126+
func test_refreshCoupons_calls_resynchronize_on_syncCoordinator() {
127+
// Given
128+
sut = CouponListViewModel(siteID: 123, syncingCoordinator: mockSyncingCoordinator)
129+
130+
// When
131+
sut.refreshCoupons()
132+
133+
// Then
134+
XCTAssert(mockSyncingCoordinator.spyDidCallResynchronize)
135+
}
136+
137+
func test_handleCouponSyncResult_removes_refreshing_when_refresh_completes() {
138+
// Given
139+
setUpWithCouponFetched()
140+
sut.refreshCoupons()
141+
142+
// When
143+
sut.handleCouponSyncResult(result: .success(false))
144+
145+
// Then
146+
XCTAssertEqual(sut.state, .coupons)
147+
}
114148
}

0 commit comments

Comments
 (0)