Skip to content

Commit 41ea606

Browse files
authored
Merge pull request #2208 from woocommerce/issue/2171-order-search-total
Orders → Search: Show Status Totals
2 parents 3e12f8d + 264757e commit 41ea606

File tree

8 files changed

+174
-54
lines changed

8 files changed

+174
-54
lines changed

RELEASE-NOTES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- [internal]: the "send magic link" screen has navigation changes that can cause regressions. See https://git.io/Jfqio for testing details.
88
- The Orders list is now automatically refreshed when reopening the app.
99
- The Orders list is automatically refreshed if a new order (push notification) comes in.
10+
- Orders -> Search: The statuses now shows the total number of orders with that status.
1011

1112

1213
4.1
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
2+
import Foundation
3+
4+
extension NumberFormatter {
5+
/// Returns `number` as a localized string or “99+” if it is greater than `99`.
6+
///
7+
static func localizedOrNinetyNinePlus(_ number: Int) -> String {
8+
if number > 99 {
9+
return Constants.ninetyNinePlus
10+
} else {
11+
return localizedString(from: NSNumber(value: number), number: .none)
12+
}
13+
}
14+
15+
private enum Constants {
16+
static let ninetyNinePlus = NSLocalizedString(
17+
"99+",
18+
comment: "Shown when there are more than 99 items of something (e.g. Processing Orders)."
19+
)
20+
}
21+
}

WooCommerce/Classes/ViewModels/MainTabViewModel.swift

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,6 @@ final class MainTabViewModel {
2929

3030

3131
private extension MainTabViewModel {
32-
enum Constants {
33-
static let ninetyNinePlus = NSLocalizedString(
34-
"99+",
35-
comment: "Content of the badge presented over the Orders icon when there are more than 99 orders processing"
36-
)
37-
}
38-
3932
@objc func requestBadgeCount() {
4033
guard let siteID = ServiceLocator.stores.sessionManager.defaultStoreID else {
4134
DDLogError("# Error: Cannot fetch order count")
@@ -62,14 +55,7 @@ private extension MainTabViewModel {
6255
return
6356
}
6457

65-
let returnValue = readableCount(processingCount)
66-
67-
onBadgeReload?(returnValue)
68-
}
69-
70-
private func readableCount(_ count: Int) -> String {
71-
let localizedCount = NumberFormatter.localizedString(from: NSNumber(value: count), number: .none)
72-
return count > 99 ? Constants.ninetyNinePlus : localizedCount
58+
onBadgeReload?(NumberFormatter.localizedOrNinetyNinePlus(processingCount))
7359
}
7460

7561
private func observeBadgeRefreshNotifications() {

WooCommerce/Classes/ViewRelated/Search/Order/OrderSearchStarterViewController.swift

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ final class OrderSearchStarterViewController: UIViewController, KeyboardFrameAdj
4040
}
4141

4242
private func configureTableView() {
43-
tableView.register(BasicTableViewCell.loadNib(),
44-
forCellReuseIdentifier: BasicTableViewCell.reuseIdentifier)
43+
tableView.register(SettingTitleAndValueTableViewCell.loadNib(),
44+
forCellReuseIdentifier: SettingTitleAndValueTableViewCell.reuseIdentifier)
4545

4646
tableView.backgroundColor = .listBackground
4747
tableView.delegate = self
@@ -59,13 +59,16 @@ extension OrderSearchStarterViewController: UITableViewDataSource {
5959
}
6060

6161
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
62-
let cell = tableView.dequeueReusableCell(withIdentifier: BasicTableViewCell.reuseIdentifier,
63-
for: indexPath)
64-
let orderStatus = viewModel.orderStatus(at: indexPath)
62+
guard let cell =
63+
tableView.dequeueReusableCell(withIdentifier: SettingTitleAndValueTableViewCell.reuseIdentifier,
64+
for: indexPath) as? SettingTitleAndValueTableViewCell else {
65+
fatalError("Unexpected or missing cell")
66+
}
67+
68+
let cellViewModel = viewModel.cellViewModel(at: indexPath)
6569

6670
cell.accessoryType = .disclosureIndicator
67-
cell.selectionStyle = .default
68-
cell.textLabel?.text = orderStatus.name
71+
cell.updateUI(title: cellViewModel.name ?? "", value: cellViewModel.total)
6972

7073
return cell
7174
}
@@ -80,13 +83,13 @@ extension OrderSearchStarterViewController: UITableViewDataSource {
8083
extension OrderSearchStarterViewController: UITableViewDelegate {
8184

8285
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
83-
let orderStatus = viewModel.orderStatus(at: indexPath)
86+
let cellViewModel = viewModel.cellViewModel(at: indexPath)
8487

85-
analytics.trackSelectionOf(orderStatus: orderStatus)
88+
analytics.trackSelectionOf(orderStatusSlug: cellViewModel.slug)
8689

8790
let ordersViewController = OrdersViewController(
88-
title: orderStatus.name ?? NSLocalizedString("Orders", comment: "Default title for Orders List shown when tapping on the Search filter."),
89-
statusFilter: orderStatus
91+
title: cellViewModel.name ?? NSLocalizedString("Orders", comment: "Default title for Orders List shown when tapping on the Search filter."),
92+
statusFilter: cellViewModel.orderStatus
9093
)
9194

9295
navigationController?.pushViewController(ordersViewController, animated: true)
@@ -108,8 +111,8 @@ extension OrderSearchStarterViewController: KeyboardScrollable {
108111
private extension Analytics {
109112
/// Submit events depicting selection of an `OrderStatus` in the UI.
110113
///
111-
func trackSelectionOf(orderStatus: OrderStatus) {
112-
track(.filterOrdersOptionSelected, withProperties: ["status": orderStatus.slug])
113-
track(.ordersListFilterOrSearch, withProperties: ["filter": orderStatus.slug, "search": ""])
114+
func trackSelectionOf(orderStatusSlug: String) {
115+
track(.filterOrdersOptionSelected, withProperties: ["status": orderStatusSlug])
116+
track(.ordersListFilterOrSearch, withProperties: ["filter": orderStatusSlug, "search": ""])
114117
}
115118
}

WooCommerce/Classes/ViewRelated/Search/Order/OrderSearchStarterViewModel.swift

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,24 @@ final class OrderSearchStarterViewModel {
1414
private let siteID: Int64
1515
private let storageManager: StorageManagerType
1616

17+
/// The `ViewModel` containing only the data used by the displayed cell.
18+
///
19+
struct CellViewModel {
20+
let name: String?
21+
let slug: String
22+
23+
/// The total displayed on the right side.
24+
///
25+
/// If this is above 99, this will be “99+”.
26+
let total: String
27+
28+
/// The source `OrderStatus` used to create this `ViewModel`.
29+
///
30+
/// This should only be used for initializing `OrdersViewController`.
31+
///
32+
let orderStatus: OrderStatus
33+
}
34+
1735
private lazy var resultsController: ResultsController<StorageOrderStatus> = {
1836
let descriptor = NSSortDescriptor(key: "slug", ascending: true)
1937
let predicate = NSPredicate(format: "siteID == %lld", siteID)
@@ -61,9 +79,16 @@ extension OrderSearchStarterViewModel {
6179
resultsController.numberOfObjects
6280
}
6381

64-
/// The `OrderStatus` located at `indexPath`.
82+
/// The `CellViewModel` located at `indexPath`.
6583
///
66-
func orderStatus(at indexPath: IndexPath) -> OrderStatus {
67-
resultsController.object(at: indexPath)
84+
func cellViewModel(at indexPath: IndexPath) -> CellViewModel {
85+
let orderStatus = resultsController.object(at: indexPath)
86+
87+
let total = NumberFormatter.localizedOrNinetyNinePlus(orderStatus.total)
88+
89+
return CellViewModel(name: orderStatus.name,
90+
slug: orderStatus.slug,
91+
total: total,
92+
orderStatus: orderStatus)
6893
}
6994
}

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,8 @@
314314
5754727D2451F1D800A94C3C /* PublishSubject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5754727C2451F1D800A94C3C /* PublishSubject.swift */; };
315315
5754727F24520B2A00A94C3C /* Observer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5754727E24520B2A00A94C3C /* Observer.swift */; };
316316
575472812452185300A94C3C /* ForegroundNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575472802452185300A94C3C /* ForegroundNotification.swift */; };
317+
57612989245888E2007BB2D9 /* NumberFormatter+LocalizedOrNinetyNinePlus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57612988245888E2007BB2D9 /* NumberFormatter+LocalizedOrNinetyNinePlus.swift */; };
318+
5761298B24589B84007BB2D9 /* NumberFormatter+LocalizedOrNinetyNinePlusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5761298A24589B84007BB2D9 /* NumberFormatter+LocalizedOrNinetyNinePlusTests.swift */; };
317319
576F92222423C3C0003E5FEF /* OrdersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 576F92212423C3C0003E5FEF /* OrdersViewModel.swift */; };
318320
5795F22C23E26A8D00F6707C /* OrderSearchStarterViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5795F22B23E26A8D00F6707C /* OrderSearchStarterViewController.xib */; };
319321
5795F22E23E26A9E00F6707C /* OrderSearchStarterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5795F22D23E26A9E00F6707C /* OrderSearchStarterViewController.swift */; };
@@ -1138,6 +1140,8 @@
11381140
5754727C2451F1D800A94C3C /* PublishSubject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublishSubject.swift; sourceTree = "<group>"; };
11391141
5754727E24520B2A00A94C3C /* Observer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observer.swift; sourceTree = "<group>"; };
11401142
575472802452185300A94C3C /* ForegroundNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForegroundNotification.swift; sourceTree = "<group>"; };
1143+
57612988245888E2007BB2D9 /* NumberFormatter+LocalizedOrNinetyNinePlus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NumberFormatter+LocalizedOrNinetyNinePlus.swift"; sourceTree = "<group>"; };
1144+
5761298A24589B84007BB2D9 /* NumberFormatter+LocalizedOrNinetyNinePlusTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NumberFormatter+LocalizedOrNinetyNinePlusTests.swift"; sourceTree = "<group>"; };
11411145
576F92212423C3C0003E5FEF /* OrdersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrdersViewModel.swift; sourceTree = "<group>"; };
11421146
5795F22B23E26A8D00F6707C /* OrderSearchStarterViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = OrderSearchStarterViewController.xib; sourceTree = "<group>"; };
11431147
5795F22D23E26A9E00F6707C /* OrderSearchStarterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderSearchStarterViewController.swift; sourceTree = "<group>"; };
@@ -2865,6 +2869,7 @@
28652869
F997174623DC070C00592D8E /* XLPagerStrip+AccessibilityIdentifierTests.swift */,
28662870
0215320C2423309B003F2BBD /* UIStackView+SubviewsTests.swift */,
28672871
6856D7981E11F85D5E4EFED7 /* NSMutableAttributedStringHelperTests.swift */,
2872+
5761298A24589B84007BB2D9 /* NumberFormatter+LocalizedOrNinetyNinePlusTests.swift */,
28682873
);
28692874
path = Extensions;
28702875
sourceTree = "<group>";
@@ -3306,6 +3311,7 @@
33063311
02396250239948470096F34C /* UIImage+TintColor.swift */,
33073312
F997174323DC065900592D8E /* XLPagerStrip+AccessibilityIdentifier.swift */,
33083313
0215320A24231D5A003F2BBD /* UIStackView+Subviews.swift */,
3314+
57612988245888E2007BB2D9 /* NumberFormatter+LocalizedOrNinetyNinePlus.swift */,
33093315
);
33103316
path = Extensions;
33113317
sourceTree = "<group>";
@@ -4584,6 +4590,7 @@
45844590
024EFA6923FCC10B00F36918 /* Product+Media.swift in Sources */,
45854591
5795F23023E26B5300F6707C /* OrderSearchStarterViewModel.swift in Sources */,
45864592
0202B68D23876BC100F3EBE0 /* ProductsTabProductViewModel+ProductVariation.swift in Sources */,
4593+
57612989245888E2007BB2D9 /* NumberFormatter+LocalizedOrNinetyNinePlus.swift in Sources */,
45874594
B5A8532220BDBFAF00FAAB4D /* CircularImageView.swift in Sources */,
45884595
CE1F51252064179A00C6C810 /* UILabel+Helpers.swift in Sources */,
45894596
0235595324496A93004BE2B8 /* BottomSheetListSelectorViewProperties.swift in Sources */,
@@ -4778,6 +4785,7 @@
47784785
B57C5C9F21B80E8300FF82B2 /* SampleError.swift in Sources */,
47794786
02404EE42315151400FF1170 /* MockupStatsVersionStoresManager.swift in Sources */,
47804787
B53B898920D450AF00EDB467 /* SessionManagerTests.swift in Sources */,
4788+
5761298B24589B84007BB2D9 /* NumberFormatter+LocalizedOrNinetyNinePlusTests.swift in Sources */,
47814789
7435E59021C0162C00216F0F /* OrderNoteWooTests.swift in Sources */,
47824790
45F5A3C323DF31D2007D40E5 /* ShippingInputFormatterTests.swift in Sources */,
47834791
02153211242376B5003F2BBD /* ProductPriceSettingsViewModelTests.swift in Sources */,
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
2+
import XCTest
3+
4+
@testable import WooCommerce
5+
6+
final class NumberFormatterLocalizedOrNinetyNinePlusTests: XCTestCase {
7+
8+
func testItReturnsNinetyNinePlusIfTheNumberIsGreaterThanNinetyNine() {
9+
let localized = NumberFormatter.localizedOrNinetyNinePlus(100)
10+
11+
XCTAssertEqual(localized, NSLocalizedString("99+", comment: ""))
12+
}
13+
14+
func testItReturnsTheLocalizedNumberIfTheNumberIsLessThanNinetyNine() {
15+
let localized = NumberFormatter.localizedOrNinetyNinePlus(98)
16+
17+
XCTAssertEqual(localized, NumberFormatter.localizedString(from: NSNumber(value: 98), number: .none))
18+
}
19+
20+
func testItReturnsTheLocalizedNumberIfTheNumberIsNinetyNine() {
21+
let localized = NumberFormatter.localizedOrNinetyNinePlus(99)
22+
23+
XCTAssertEqual(localized, NumberFormatter.localizedString(from: NSNumber(value: 99), number: .none))
24+
}
25+
}

WooCommerce/WooCommerceTests/ViewRelated/Search/Order/OrderSearchStarterViewModelTests.swift

Lines changed: 73 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22
import XCTest
33
import Foundation
44
import Yosemite
5-
import Storage
5+
6+
import protocol Storage.StorageManagerType
7+
import protocol Storage.StorageType
68

79
@testable import WooCommerce
810

11+
private typealias CellViewModel = OrderSearchStarterViewModel.CellViewModel
12+
913
/// Tests for `OrderSearchStarterViewModel`
1014
///
1115
final class OrderSearchStarterViewModelTests: XCTestCase {
@@ -44,56 +48,103 @@ final class OrderSearchStarterViewModelTests: XCTestCase {
4448

4549
// Then
4650
XCTAssertEqual(viewModel.numberOfObjects, expectedItems.count)
47-
XCTAssertEqual(viewModel.fetchedOrderStatuses, expectedItems)
48-
XCTAssertFalse(viewModel.fetchedOrderStatuses.contains(where: { $0.siteID != siteID }))
49-
XCTAssertFalse(viewModel.fetchedOrderStatuses.contains(unexpectedItem))
51+
XCTAssertEqual(viewModel.cellViewModels.slugs, expectedItems.slugs)
52+
XCTAssertFalse(viewModel.cellViewModels.contains(where: { $0.slug == unexpectedItem.slug }))
5053
}
5154

5255
func testItSortsTheOrderStatusesBySlug() {
5356
// Given
5457
let viewModel = OrderSearchStarterViewModel(siteID: siteID, storageManager: storageManager)
5558

56-
insertOrderStatus(siteID: siteID, status: .completed, slug: "delta")
57-
insertOrderStatus(siteID: siteID, status: .processing, slug: "charlie")
58-
insertOrderStatus(siteID: siteID, status: .failed, slug: "echo")
59-
insertOrderStatus(siteID: siteID, status: .cancelled, slug: "alpha")
60-
insertOrderStatus(siteID: siteID, status: .cancelled, slug: "beta")
59+
insert(OrderStatus(name: "autem", siteID: siteID, slug: "delta", total: 0))
60+
insert(OrderStatus(name: "dolores", siteID: siteID, slug: "charlie", total: 0))
61+
insert(OrderStatus(name: "fugit", siteID: siteID, slug: "echo", total: 0))
62+
insert(OrderStatus(name: "itaque", siteID: siteID, slug: "alpha", total: 0))
63+
insert(OrderStatus(name: "eos", siteID: siteID, slug: "beta", total: 0))
6164

6265
// When
6366
viewModel.activateAndForwardUpdates(to: UITableView())
6467

6568
// Then
6669
let expectedSlugs = ["alpha", "beta", "charlie", "delta", "echo"]
67-
let actualSlugs = viewModel.fetchedOrderStatuses.map(\.slug)
68-
XCTAssertEqual(actualSlugs, expectedSlugs)
70+
XCTAssertEqual(viewModel.cellViewModels.slugs, expectedSlugs)
71+
}
72+
73+
func testItReturnsTheNameSlugAndTotalInTheCellViewModel() {
74+
// Given
75+
let viewModel = OrderSearchStarterViewModel(siteID: siteID, storageManager: storageManager)
76+
77+
insert(OrderStatus(name: "autem", siteID: siteID, slug: "delta", total: 18))
78+
insert(OrderStatus(name: "dolores", siteID: siteID, slug: "charlie", total: 73))
79+
80+
viewModel.activateAndForwardUpdates(to: UITableView())
81+
82+
// When
83+
// Retrieve "delta" which is the second row
84+
let cellViewModel = viewModel.cellViewModel(at: IndexPath(row: 1, section: 0))
85+
86+
// Then
87+
XCTAssertEqual(cellViewModel.name, "autem")
88+
XCTAssertEqual(cellViewModel.slug, "delta")
89+
XCTAssertEqual(cellViewModel.total,
90+
NumberFormatter.localizedString(from: NSNumber(value: 18), number: .none))
91+
}
92+
93+
func testGivenAnOrderStatusTotalOfMoreThanNinetyNineItUsesNinetyNinePlus() {
94+
// Given
95+
let viewModel = OrderSearchStarterViewModel(siteID: siteID, storageManager: storageManager)
96+
97+
insert(OrderStatus(name: "Processing", siteID: siteID, slug: "slug", total: 200))
98+
99+
viewModel.activateAndForwardUpdates(to: UITableView())
100+
101+
// When
102+
let cellViewModel = viewModel.cellViewModel(at: IndexPath(row: 0, section: 0))
103+
104+
// Then
105+
XCTAssertEqual(cellViewModel.total, NSLocalizedString("99+", comment: ""))
69106
}
70107
}
71108

72109
// MARK: - Helpers
73110

74111
private extension OrderSearchStarterViewModel {
75-
/// Returns the `OrderStatus` for all the rows
112+
/// Returns all the `CellViewModel` based on the fetched `OrderStatus`.
76113
///
77-
var fetchedOrderStatuses: [Yosemite.OrderStatus] {
78-
(0..<numberOfObjects).map { orderStatus(at: IndexPath(row: $0, section: 0)) }
114+
var cellViewModels: [CellViewModel] {
115+
(0..<numberOfObjects).map { cellViewModel(at: IndexPath(row: $0, section: 0)) }
116+
}
117+
}
118+
119+
private extension Array where Element == OrderStatus {
120+
var slugs: [String] {
121+
map(\.slug)
122+
}
123+
}
124+
125+
private extension Array where Element == CellViewModel {
126+
var slugs: [String] {
127+
map(\.slug)
79128
}
80129
}
81130

82131
// MARK: - Fixtures
83132

84133
private extension OrderSearchStarterViewModelTests {
85134
@discardableResult
86-
func insertOrderStatus(siteID: Int64,
87-
status: OrderStatusEnum,
88-
slug: String? = nil) -> Yosemite.OrderStatus {
135+
func insert(_ readOnlyOrderStatus: OrderStatus) -> OrderStatus {
136+
let storageOrderStatus = storage.insertNewObject(ofType: StorageOrderStatus.self)
137+
storageOrderStatus.update(with: readOnlyOrderStatus)
138+
return readOnlyOrderStatus
139+
}
140+
141+
@discardableResult
142+
func insertOrderStatus(siteID: Int64, status: OrderStatusEnum) -> OrderStatus {
89143
let readOnlyOrderStatus = OrderStatus(name: status.rawValue,
90144
siteID: siteID,
91-
slug: slug ?? status.rawValue,
145+
slug: status.rawValue,
92146
total: 0)
93147

94-
let storageOrderStatus = storage.insertNewObject(ofType: StorageOrderStatus.self)
95-
storageOrderStatus.update(with: readOnlyOrderStatus)
96-
97-
return readOnlyOrderStatus
148+
return insert(readOnlyOrderStatus)
98149
}
99150
}

0 commit comments

Comments
 (0)