Skip to content

Commit bd681d4

Browse files
authored
Merge pull request #8652 from woocommerce/issue/8519-status-picker
Bulk Editing: Add status picker
2 parents 02ed2e2 + bbb58eb commit bd681d4

File tree

6 files changed

+314
-4
lines changed

6 files changed

+314
-4
lines changed

Networking/Networking/Model/Product/Product.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,8 @@ public struct Product: Codable, GeneratedCopiable, Equatable, GeneratedFakeable
465465
public func encode(to encoder: Encoder) throws {
466466
var container = encoder.container(keyedBy: CodingKeys.self)
467467

468+
try container.encode(productID, forKey: .productID)
469+
468470
try container.encode(images, forKey: .images)
469471

470472
try container.encode(name, forKey: .name)

WooCommerce/Classes/ViewRelated/Products/Edit Product/Product Settings/List Selector Data Source/ProductStatusSettingListSelectorCommand.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ final class ProductStatusSettingListSelectorCommand: ListSelectorCommand {
1515
.pending
1616
]
1717

18-
private(set) var selected: ProductStatus?
18+
@Published private(set) var selected: ProductStatus?
1919

2020
init(selected: ProductStatus?) {
2121
self.selected = selected

WooCommerce/Classes/ViewRelated/Products/ProductsListViewModel.swift

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,21 @@ import Yosemite
44
/// View model for `ProductsViewController`. Only stores logic related to Bulk Editing.
55
///
66
class ProductListViewModel {
7+
8+
enum BulkEditError: Error {
9+
case noProductsSelected
10+
}
11+
12+
let siteID: Int64
13+
private let stores: StoresManager
14+
715
private var selectedProducts: Set<Product> = .init()
816

17+
init(siteID: Int64, stores: StoresManager) {
18+
self.siteID = siteID
19+
self.stores = stores
20+
}
21+
922
var selectedProductsCount: Int {
1023
selectedProducts.count
1124
}
@@ -29,4 +42,35 @@ class ProductListViewModel {
2942
func deselectAll() {
3043
selectedProducts.removeAll()
3144
}
45+
46+
/// Check if selected products share the same common ProductStatus. Returns `nil` otherwise.
47+
///
48+
var commonStatusForSelectedProducts: ProductStatus? {
49+
let status = selectedProducts.first?.productStatus
50+
if selectedProducts.allSatisfy({ $0.productStatus == status }) {
51+
return status
52+
} else {
53+
return nil
54+
}
55+
}
56+
57+
/// Update selected products with new ProductStatus and trigger Network action to save the change remotely.
58+
///
59+
func updateSelectedProducts(with newStatus: ProductStatus, completion: @escaping (Result<Void, Error>) -> Void ) {
60+
guard selectedProductsCount > 0 else {
61+
completion(.failure(BulkEditError.noProductsSelected))
62+
return
63+
}
64+
65+
let updatedProducts = selectedProducts.map({ $0.copy(statusKey: newStatus.rawValue) })
66+
let batchAction = ProductAction.updateProducts(siteID: siteID, products: updatedProducts) { result in
67+
switch result {
68+
case .success:
69+
completion(.success(()))
70+
case .failure(let error):
71+
completion(.failure(error))
72+
}
73+
}
74+
stores.dispatch(batchAction)
75+
}
3276
}

WooCommerce/Classes/ViewRelated/Products/ProductsViewController.swift

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import UIKit
22
import WordPressUI
33
import Yosemite
4+
import Combine
45

56
import class AutomatticTracks.CrashLogging
67

@@ -9,7 +10,7 @@ import class AutomatticTracks.CrashLogging
910
///
1011
final class ProductsViewController: UIViewController, GhostableViewController {
1112

12-
let viewModel: ProductListViewModel = .init()
13+
let viewModel: ProductListViewModel
1314

1415
/// Main TableView
1516
///
@@ -194,6 +195,8 @@ final class ProductsViewController: UIViewController, GhostableViewController {
194195
///
195196
private var hasErrorLoadingData: Bool = false
196197

198+
private var subscriptions: Set<AnyCancellable> = []
199+
197200
deinit {
198201
NotificationCenter.default.removeObserver(self)
199202
}
@@ -202,6 +205,7 @@ final class ProductsViewController: UIViewController, GhostableViewController {
202205

203206
init(siteID: Int64) {
204207
self.siteID = siteID
208+
self.viewModel = .init(siteID: siteID, stores: ServiceLocator.stores)
205209
super.init(nibName: type(of: self).nibName, bundle: nil)
206210

207211
configureTabBarItem()
@@ -296,7 +300,11 @@ private extension ProductsViewController {
296300
}
297301
coordinatingController.start()
298302
}
303+
}
299304

305+
// MARK: - Bulk Editing flows
306+
//
307+
private extension ProductsViewController {
300308
@objc func startBulkEditing() {
301309
tableView.setEditing(true, animated: true)
302310

@@ -328,8 +336,8 @@ private extension ProductsViewController {
328336
@objc func openBulkEditingOptions(sender: UIButton) {
329337
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
330338

331-
let updateStatus = UIAlertAction(title: Localization.bulkEditingStatusOption, style: .default) { _ in
332-
// TODO-8519: show UI for status update
339+
let updateStatus = UIAlertAction(title: Localization.bulkEditingStatusOption, style: .default) { [weak self] _ in
340+
self?.showStatusBulkEditingModal()
333341
}
334342
let updatePrice = UIAlertAction(title: Localization.bulkEditingPriceOption, style: .default) { _ in
335343
// TODO-8520: show UI for price update
@@ -347,6 +355,71 @@ private extension ProductsViewController {
347355

348356
present(actionSheet, animated: true)
349357
}
358+
359+
func showStatusBulkEditingModal() {
360+
let initialStatus = viewModel.commonStatusForSelectedProducts
361+
let command = ProductStatusSettingListSelectorCommand(selected: initialStatus)
362+
let listSelectorViewController = ListSelectorViewController(command: command) { _ in
363+
// view dismiss callback - no-op
364+
}
365+
listSelectorViewController.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel,
366+
target: self,
367+
action: #selector(dismissModal))
368+
369+
let applyButton = UIBarButtonItem(title: Localization.bulkEditingApply)
370+
applyButton.on(call: { [weak self] _ in
371+
self?.applyBulkEditingStatus(newStatus: command.selected, modalVC: listSelectorViewController)
372+
})
373+
command.$selected.sink { newStatus in
374+
if let newStatus, newStatus != initialStatus {
375+
applyButton.isEnabled = true
376+
} else {
377+
applyButton.isEnabled = false
378+
}
379+
}.store(in: &subscriptions)
380+
listSelectorViewController.navigationItem.rightBarButtonItem = applyButton
381+
382+
self.present(WooNavigationController(rootViewController: listSelectorViewController), animated: true)
383+
}
384+
385+
@objc func dismissModal() {
386+
dismiss(animated: true)
387+
}
388+
389+
func applyBulkEditingStatus(newStatus: ProductStatus?, modalVC: UIViewController) {
390+
guard let newStatus else { return }
391+
392+
displayProductsSavingInProgressView(on: modalVC)
393+
viewModel.updateSelectedProducts(with: newStatus) { [weak self] result in
394+
guard let self else { return }
395+
396+
self.dismiss(animated: true, completion: nil)
397+
switch result {
398+
case .success:
399+
self.finishBulkEditing()
400+
self.presentNotice(title: Localization.statusUpdatedNotice)
401+
case .failure:
402+
self.presentNotice(title: Localization.updateErrorNotice)
403+
}
404+
}
405+
}
406+
407+
func displayProductsSavingInProgressView(on vc: UIViewController) {
408+
let viewProperties = InProgressViewProperties(title: Localization.productsSavingTitle, message: Localization.productsSavingMessage)
409+
let inProgressViewController = InProgressViewController(viewProperties: viewProperties)
410+
inProgressViewController.modalPresentationStyle = .fullScreen
411+
412+
vc.present(inProgressViewController, animated: true, completion: nil)
413+
}
414+
415+
func presentNotice(title: String) {
416+
let contextNoticePresenter: NoticePresenter = {
417+
let noticePresenter = DefaultNoticePresenter()
418+
noticePresenter.presentingViewController = tabBarController
419+
return noticePresenter
420+
}()
421+
contextNoticePresenter.enqueue(notice: .init(title: title))
422+
}
350423
}
351424

352425
// MARK: - View Configuration
@@ -1218,5 +1291,17 @@ private extension ProductsViewController {
12181291
"%1$@ selected",
12191292
comment: "Title that appears on top of the Product List screen during bulk editing. Reads like: 2 selected"
12201293
)
1294+
1295+
static let bulkEditingApply = NSLocalizedString("Apply", comment: "Title for the button to apply bulk editing changes to selected products.")
1296+
1297+
static let productsSavingTitle = NSLocalizedString("Updating your products...",
1298+
comment: "Title of the in-progress UI while bulk updating selected products remotely")
1299+
static let productsSavingMessage = NSLocalizedString("Please wait while we update these products on your store",
1300+
comment: "Message of the in-progress UI while bulk updating selected products remotely")
1301+
1302+
static let statusUpdatedNotice = NSLocalizedString("Status updated",
1303+
comment: "Title of the notice when a user updated status for selected products")
1304+
static let updateErrorNotice = NSLocalizedString("Cannot update products",
1305+
comment: "Title of the notice when there is an error updating selected products")
12211306
}
12221307
}

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1257,6 +1257,7 @@
12571257
AEA3F91527BEC96B00B9F555 /* PriceFieldFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEA3F91427BEC96B00B9F555 /* PriceFieldFormatterTests.swift */; };
12581258
AEACCB6D2785FF4A000D01F0 /* NavigationRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEACCB6C2785FF4A000D01F0 /* NavigationRow.swift */; };
12591259
AEB4DB99290AE8F300AE4340 /* MockCookieJar.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEB4DB98290AE8F300AE4340 /* MockCookieJar.swift */; };
1260+
AEB6903729770B1D00872FE0 /* ProductListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEB6903629770B1D00872FE0 /* ProductListViewModelTests.swift */; };
12601261
AEB73C0C25CD734200A8454A /* AttributePickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEB73C0B25CD734200A8454A /* AttributePickerViewModel.swift */; };
12611262
AEB73C1725CD8E5800A8454A /* AttributePickerViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEB73C1625CD8E5800A8454A /* AttributePickerViewModelTests.swift */; };
12621263
AEBFD13F28E7655F00F598C6 /* StoreInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEBFD13E28E7655F00F598C6 /* StoreInfoView.swift */; };
@@ -3310,6 +3311,7 @@
33103311
AEA3F91427BEC96B00B9F555 /* PriceFieldFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceFieldFormatterTests.swift; sourceTree = "<group>"; };
33113312
AEACCB6C2785FF4A000D01F0 /* NavigationRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRow.swift; sourceTree = "<group>"; };
33123313
AEB4DB98290AE8F300AE4340 /* MockCookieJar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCookieJar.swift; sourceTree = "<group>"; };
3314+
AEB6903629770B1D00872FE0 /* ProductListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductListViewModelTests.swift; sourceTree = "<group>"; };
33133315
AEB73C0B25CD734200A8454A /* AttributePickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributePickerViewModel.swift; sourceTree = "<group>"; };
33143316
AEB73C1625CD8E5800A8454A /* AttributePickerViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributePickerViewModelTests.swift; sourceTree = "<group>"; };
33153317
AEBFD13E28E7655F00F598C6 /* StoreInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreInfoView.swift; sourceTree = "<group>"; };
@@ -4773,6 +4775,7 @@
47734775
0271139924DD15D800574A07 /* ProductsTabProductViewModel+VariationTests.swift */,
47744776
573A960224F433DD0091F3A5 /* ProductsTopBannerFactoryTests.swift */,
47754777
0279F0DB252DBF1F0098D7DE /* ProductVariationDetailsFactoryTests.swift */,
4778+
AEB6903629770B1D00872FE0 /* ProductListViewModelTests.swift */,
47764779
);
47774780
path = Products;
47784781
sourceTree = "<group>";
@@ -11579,6 +11582,7 @@
1157911582
02A9A496244D84AB00757B99 /* ProductsSortOrderBottomSheetListSelectorCommandTests.swift in Sources */,
1158011583
B9B6DEF1283F8EB100901FB7 /* SitePluginsURLTests.swift in Sources */,
1158111584
D83F5935225B3CDD00626E75 /* DatePickerTableViewCellTests.swift in Sources */,
11585+
AEB6903729770B1D00872FE0 /* ProductListViewModelTests.swift in Sources */,
1158211586
314DC4C1268D28B100444C9E /* CardReaderSettingsKnownReadersStorageTests.swift in Sources */,
1158311587
262AF38A2713B67600E39AFF /* SimplePaymentsAmountViewModelTests.swift in Sources */,
1158411588
93FA787221CD2A1A00B663E5 /* CurrencySettingsTests.swift in Sources */,

0 commit comments

Comments
 (0)