Skip to content

Commit f4b1eff

Browse files
authored
Merge pull request #2260 from woocommerce/issue/2120-product-setting-visibility-UI
Product Settings: show, edit and update Product Visibility
2 parents b41960e + d5e149f commit f4b1eff

26 files changed

+883
-58
lines changed

Networking/Networking/Model/Product/ProductStatus.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ extension ProductStatus: RawRepresentable {
5656
case .pending:
5757
return NSLocalizedString("Pending review", comment: "Display label for the product's pending status")
5858
case .privateStatus:
59-
return NSLocalizedString("Private", comment: "Display label for the product's private status")
59+
return NSLocalizedString("Privately published", comment: "Display label for the product's private status")
6060
case .custom(let payload):
6161
return payload // unable to localize at runtime.
6262
}

Networking/Networking/Remote/SitePostsRemote.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public class SitePostsRemote: Remote {
1616
/// - completion: Closure to be executed upon completion.
1717
///
1818
public func loadSitePost(for siteID: Int64, postID: Int64, completion: @escaping (Post?, Error?) -> Void) {
19-
let path = String(format: "/sites/%d/posts/%d", siteID, postID)
19+
let path = String(format: "sites/%d/posts/%d", siteID, postID)
2020
let parameters = ["fields": "site_ID,password"]
2121
let request = DotcomRequest(wordpressApiVersion: .mark1_1, method: .get, path: path, parameters: parameters)
2222
let mapper = PostMapper()
@@ -38,7 +38,7 @@ public class SitePostsRemote: Remote {
3838
var parameters = try post.toDictionary()
3939
let parametersFields = ["fields": "site_ID,password"]
4040
parameters.merge(parametersFields) { (current, _) in current }
41-
let path = String(format: "/sites/%d/posts/%d", siteID, postID)
41+
let path = String(format: "sites/%d/posts/%d", siteID, postID)
4242
let request = DotcomRequest(wordpressApiVersion: .mark1_2, method: .post, path: path, parameters: parameters)
4343
let mapper = PostMapper()
4444

Networking/NetworkingTests/Remote/SitePostsRemoteTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class SitePostsRemoteTests: XCTestCase {
3535

3636
let postID: Int64 = 7
3737

38-
network.simulateResponse(requestUrlSuffix: "/sites/\(sampleSiteID)/posts/\(postID)", filename: "site-post")
38+
network.simulateResponse(requestUrlSuffix: "sites/\(sampleSiteID)/posts/\(postID)", filename: "site-post")
3939
remote.loadSitePost(for: sampleSiteID, postID: postID) {[weak self] (sitePost, error) in
4040
XCTAssertNil(error)
4141
XCTAssertNotNil(sitePost)
@@ -70,7 +70,7 @@ class SitePostsRemoteTests: XCTestCase {
7070
let remote = SitePostsRemote(network: network)
7171
let expectation = self.expectation(description: "Wait for site post update")
7272

73-
network.simulateResponse(requestUrlSuffix: "/sites/\(sampleSiteID)/posts/\(postID)", filename: "site-post-update")
73+
network.simulateResponse(requestUrlSuffix: "sites/\(sampleSiteID)/posts/\(postID)", filename: "site-post-update")
7474

7575
let newPassword = "new-password"
7676
let post = Post(siteID: sampleSiteID, password: newPassword)

WooCommerce/Classes/Extensions/UIImage+Woo.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,12 @@ extension UIImage {
397397
return im2.imageWithTintColor(tintColor)
398398
}
399399

400+
/// Password Field Image
401+
///
402+
static var passwordFieldImage: UIImage {
403+
return UIImage.gridicon(.visible)
404+
}
405+
400406
/// Waiting for Customers Image
401407
///
402408
static var waitingForCustomersImage: UIImage {

WooCommerce/Classes/Styles/UIColor+SemanticColors.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,13 @@ extension UIColor {
149149
return UIColor(light: .accent,
150150
dark: .systemColor(.label))
151151
}
152+
153+
/// Text. WooCommercePurple-60 (< iOS 13 and Light Mode) and WooCommercePurple-30 (Dark Mode)
154+
///
155+
static var textBrand: UIColor {
156+
return UIColor(light: .withColorStudio(.wooCommercePurple, shade: .shade60),
157+
dark: .withColorStudio(.wooCommercePurple, shade: .shade30))
158+
}
152159
}
153160

154161

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@ final class ProductStatusSettingListSelectorCommand: ListSelectorCommand {
1212
let data: [ProductStatus] = [
1313
.publish,
1414
.draft,
15-
.pending,
16-
.privateStatus
15+
.pending
1716
]
1817

1918
private(set) var selected: ProductStatus?

WooCommerce/Classes/ViewRelated/Products/Edit Product/Product Settings/ProductSettingsRows.swift

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,28 @@ enum ProductSettingsRows {
3939
return
4040
}
4141

42+
/// If the status is private, the status cell becomes not editable.
43+
if isStatusPrivate() {
44+
cell.accessoryType = .none
45+
cell.selectionStyle = .none
46+
cell.applyNonSelectableLabelsStyle()
47+
}
48+
else {
49+
cell.accessoryType = .disclosureIndicator
50+
cell.selectionStyle = .default
51+
cell.applyDefaultLabelsStyle()
52+
}
53+
4254
cell.updateUI(title: NSLocalizedString("Status", comment: "Status label in Product Settings"), value: settings.status.description)
43-
cell.accessoryType = .disclosureIndicator
4455
}
4556

4657
func handleTap(sourceViewController: UIViewController, onCompletion: @escaping (ProductSettings) -> Void) {
58+
59+
/// If the status is private, the cell doesn't trigger any action
60+
guard !isStatusPrivate() else {
61+
return
62+
}
63+
4764
let command = ProductStatusSettingListSelectorCommand(selected: settings.status)
4865

4966
let listSelectorViewController = ListSelectorViewController(command: command) { selected in
@@ -58,6 +75,48 @@ enum ProductSettingsRows {
5875
let reuseIdentifier: String = SettingTitleAndValueTableViewCell.reuseIdentifier
5976

6077
let cellTypes: [UITableViewCell.Type] = [SettingTitleAndValueTableViewCell.self]
78+
79+
/// Utils
80+
func isStatusPrivate() -> Bool {
81+
return settings.status == .privateStatus
82+
}
83+
}
84+
85+
struct Visibility: ProductSettingsRowMediator {
86+
87+
private let settings: ProductSettings
88+
89+
init(_ settings: ProductSettings) {
90+
self.settings = settings
91+
}
92+
93+
func configure(cell: UITableViewCell) {
94+
guard let cell = cell as? SettingTitleAndValueTableViewCell else {
95+
return
96+
}
97+
98+
let title = NSLocalizedString("Visibility", comment: "Visibility label in Product Settings")
99+
cell.updateUI(title: title, value: ProductVisibility(status: settings.status, password: settings.password).description)
100+
cell.accessoryType = .disclosureIndicator
101+
}
102+
103+
func handleTap(sourceViewController: UIViewController, onCompletion: @escaping (ProductSettings) -> Void) {
104+
// If the password was not fetched, the cell is not selectable
105+
guard settings.password != nil else {
106+
return
107+
}
108+
109+
let viewController = ProductVisibilityViewController(settings: settings) { (productSettings) in
110+
self.settings.password = productSettings.password
111+
self.settings.status = productSettings.status
112+
onCompletion(self.settings)
113+
}
114+
sourceViewController.navigationController?.pushViewController(viewController, animated: true)
115+
}
116+
117+
let reuseIdentifier: String = SettingTitleAndValueTableViewCell.reuseIdentifier
118+
119+
let cellTypes: [UITableViewCell.Type] = [SettingTitleAndValueTableViewCell.self]
61120
}
62121

63122
struct CatalogVisibility: ProductSettingsRowMediator {

WooCommerce/Classes/ViewRelated/Products/Edit Product/Product Settings/ProductSettingsSections.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ enum ProductSettingsSections {
2121
let rows: [ProductSettingsRowMediator]
2222

2323
init(_ settings: ProductSettings) {
24-
rows = [ProductSettingsRows.Status(settings), ProductSettingsRows.CatalogVisibility(settings)]
24+
rows = [ProductSettingsRows.Status(settings), ProductSettingsRows.Visibility(settings), ProductSettingsRows.CatalogVisibility(settings)]
2525
}
2626
}
2727

WooCommerce/Classes/ViewRelated/Products/Edit Product/Product Settings/ProductSettingsViewController.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,17 @@ final class ProductSettingsViewController: UIViewController {
1414
typealias Completion = (_ productSettings: ProductSettings) -> Void
1515
private let onCompletion: Completion
1616

17+
// Password Completion callback called when the password is fetched
18+
//
19+
typealias PasswordRetrievedCompletion = (_ password: String) -> Void
20+
private let onPasswordCompletion: PasswordRetrievedCompletion
21+
1722
/// Init
1823
///
19-
init(product: Product, completion: @escaping Completion) {
20-
viewModel = ProductSettingsViewModel(product: product)
24+
init(product: Product, password: String?, completion: @escaping Completion, onPasswordRetrieved: @escaping PasswordRetrievedCompletion) {
25+
viewModel = ProductSettingsViewModel(product: product, password: password)
2126
onCompletion = completion
27+
onPasswordCompletion = onPasswordRetrieved
2228
super.init(nibName: nil, bundle: nil)
2329
}
2430

@@ -35,6 +41,9 @@ final class ProductSettingsViewController: UIViewController {
3541
viewModel.onReload = { [weak self] in
3642
self?.tableView.reloadData()
3743
}
44+
viewModel.onPasswordRetrieved = { [weak self] (passwordRetrieved) in
45+
self?.onPasswordCompletion(passwordRetrieved)
46+
}
3847
}
3948

4049
}
@@ -127,7 +136,6 @@ extension ProductSettingsViewController: UITableViewDelegate {
127136
viewModel.handleCellTap(at: indexPath, sourceViewController: self)
128137
}
129138

130-
131139
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
132140
return UITableView.automaticDimension
133141
}

WooCommerce/Classes/ViewRelated/Products/Edit Product/Product Settings/ProductSettingsViewModel.swift

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,50 @@ import Yosemite
44
/// The Product Settings contains 2 sections: Publish Settings and More Options
55
final class ProductSettingsViewModel {
66

7-
private(set) var sections: [ProductSettingsSectionMediator] {
8-
didSet {
9-
self.onReload?()
10-
}
11-
}
7+
private let product: Product
8+
9+
/// The original password, the one fetched from site post API
10+
private var password: String?
1211

1312
var productSettings: ProductSettings {
1413
didSet {
1514
sections = Self.configureSections(productSettings)
1615
}
1716
}
1817

19-
private let product: Product
18+
private(set) var sections: [ProductSettingsSectionMediator] {
19+
didSet {
20+
self.onReload?()
21+
}
22+
}
2023

2124
/// Closures
2225
/// - `onReload` called when sections data are reloaded/refreshed
26+
/// - `onPasswordRetrieved` called when the password is fetched
2327
var onReload: (() -> Void)?
28+
var onPasswordRetrieved: ((_ password: String) -> Void)?
2429

25-
init(product: Product) {
30+
init(product: Product, password: String?) {
2631
self.product = product
27-
productSettings = ProductSettings(from: product)
32+
self.password = password
33+
productSettings = ProductSettings(from: product, password: password)
2834
sections = Self.configureSections(productSettings)
35+
36+
/// If nil, we fetch the password from site post API because it was never fetched
37+
if password == nil {
38+
retrieveProductPassword(siteID: product.siteID, productID: product.productID) { [weak self] (password, error) in
39+
guard let self = self else {
40+
return
41+
}
42+
guard error == nil, let password = password else {
43+
return
44+
}
45+
self.onPasswordRetrieved?(password)
46+
self.password = password
47+
self.productSettings.password = password
48+
self.sections = Self.configureSections(self.productSettings)
49+
}
50+
}
2951
}
3052

3153
func handleCellTap(at indexPath: IndexPath, sourceViewController: UIViewController) {
@@ -40,13 +62,30 @@ final class ProductSettingsViewModel {
4062
}
4163

4264
func hasUnsavedChanges() -> Bool {
43-
guard ProductSettings(from: product) != productSettings else {
65+
guard ProductSettings(from: product, password: password) != productSettings else {
4466
return false
4567
}
4668
return true
4769
}
4870
}
4971

72+
// MARK: Syncing data. Yosemite related stuff
73+
private extension ProductSettingsViewModel {
74+
func retrieveProductPassword(siteID: Int64, productID: Int64, onCompletion: ((String?, Error?) -> ())? = nil) {
75+
let action = SitePostAction.retrieveSitePostPassword(siteID: siteID, postID: productID) { (password, error) in
76+
guard let _ = password else {
77+
DDLogError("⛔️ Error fetching product password: \(error.debugDescription)")
78+
onCompletion?(nil, error)
79+
return
80+
}
81+
82+
onCompletion?(password, nil)
83+
}
84+
85+
ServiceLocator.stores.dispatch(action)
86+
}
87+
}
88+
5089
// MARK: Configure sections and rows in Product Settings
5190
//
5291
private extension ProductSettingsViewModel {

0 commit comments

Comments
 (0)