Skip to content

Commit 02cf922

Browse files
authored
Merge pull request #3598 from woocommerce/issue/3529-fetch-options-for-existing-attributes
Variations: New Attribute -> Sync global attribute options(terms)
2 parents 472504b + a7a0a41 commit 02cf922

File tree

5 files changed

+276
-56
lines changed

5 files changed

+276
-56
lines changed

Networking/Networking/Model/Product/ProductAttribute.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,19 @@ public struct ProductAttribute: Decodable {
7070
}
7171
}
7272

73+
public extension ProductAttribute {
74+
/// Returns weather an attribute belongs to a product(local) or to the store(global)
75+
///
76+
var isLocal: Bool {
77+
attributeID == 0 // Currently the only way to know if an attribute is local is if it has a zero ID
78+
}
79+
80+
/// Returns weather an attribute belongs to a product(local) or to the store(global)
81+
///
82+
var isGlobal: Bool {
83+
!isLocal
84+
}
85+
}
7386

7487
/// Defines all the ProductAttribute CodingKeys.
7588
///

WooCommerce/Classes/ViewRelated/Products/Variations/Add Attributes/AddAttributeOptionsViewController.swift

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import UIKit
22
import Yosemite
3+
import WordPressUI
34

45
final class AddAttributeOptionsViewController: UIViewController {
56

67
@IBOutlet weak private var tableView: UITableView!
8+
private let ghostTableView = UITableView()
79

810
private let viewModel: AddAttributeOptionsViewModel
911

@@ -29,6 +31,7 @@ final class AddAttributeOptionsViewController: UIViewController {
2931
configureNavigationBar()
3032
configureMainView()
3133
configureTableView()
34+
configureGhostTableView()
3235
registerTableViewHeaderSections()
3336
registerTableViewCells()
3437
startListeningToNotifications()
@@ -63,6 +66,15 @@ private extension AddAttributeOptionsViewController {
6366
tableView.isEditing = true
6467
}
6568

69+
func configureGhostTableView() {
70+
view.addSubview(ghostTableView)
71+
ghostTableView.isHidden = true
72+
ghostTableView.translatesAutoresizingMaskIntoConstraints = false
73+
ghostTableView.pinSubviewToAllEdges(view)
74+
ghostTableView.backgroundColor = .listBackground
75+
ghostTableView.removeLastCellSeparator()
76+
}
77+
6678
func registerTableViewHeaderSections() {
6779
let headerNib = UINib(nibName: TwoColumnSectionHeaderView.reuseIdentifier, bundle: nil)
6880
tableView.register(headerNib, forHeaderFooterViewReuseIdentifier: TwoColumnSectionHeaderView.reuseIdentifier)
@@ -71,6 +83,7 @@ private extension AddAttributeOptionsViewController {
7183
func registerTableViewCells() {
7284
tableView.registerNib(for: BasicTableViewCell.self)
7385
tableView.registerNib(for: TextFieldTableViewCell.self)
86+
ghostTableView.registerNib(for: WooBasicTableViewCell.self)
7487
}
7588

7689
func observeViewModel() {
@@ -84,6 +97,12 @@ private extension AddAttributeOptionsViewController {
8497
title = viewModel.titleView
8598
navigationItem.rightBarButtonItem?.isEnabled = viewModel.isNextButtonEnabled
8699
tableView.reloadData()
100+
101+
if viewModel.showGhostTableView {
102+
displayGhostTableView()
103+
} else {
104+
removeGhostTableView()
105+
}
87106
}
88107
}
89108

@@ -181,12 +200,12 @@ private extension AddAttributeOptionsViewController {
181200
///
182201
func configure(_ cell: UITableViewCell, for row: Row, at indexPath: IndexPath) {
183202
switch (row, cell) {
184-
case (.termTextField, let cell as TextFieldTableViewCell):
203+
case (.optionTextField, let cell as TextFieldTableViewCell):
185204
configureTextField(cell: cell)
186-
case (let .selectedTerms(name), let cell as BasicTableViewCell):
205+
case (let .selectedOptions(name), let cell as BasicTableViewCell):
187206
configureOptionOffered(cell: cell, text: name, index: indexPath.row)
188-
case (.existingTerms, let cell as BasicTableViewCell):
189-
configureOption(cell: cell, text: "Work in Progress")
207+
case (let .existingOptions(name), let cell as BasicTableViewCell):
208+
configureOptionAdded(cell: cell, text: name)
190209
default:
191210
fatalError("Unsupported Cell")
192211
break
@@ -222,11 +241,32 @@ private extension AddAttributeOptionsViewController {
222241
cell.imageView?.isUserInteractionEnabled = true
223242
}
224243

225-
func configureOption(cell: BasicTableViewCell, text: String) {
244+
func configureOptionAdded(cell: BasicTableViewCell, text: String) {
226245
cell.textLabel?.text = text
227246
}
228247
}
229248

249+
// MARK: - Placeholders
250+
//
251+
private extension AddAttributeOptionsViewController {
252+
/// Renders ghost placeholder while options are being synched.
253+
///
254+
func displayGhostTableView() {
255+
let options = GhostOptions(displaysSectionHeader: false,
256+
reuseIdentifier: WooBasicTableViewCell.reuseIdentifier,
257+
rowsPerSection: [3])
258+
ghostTableView.displayGhostContent(options: options, style: .wooDefaultGhostStyle)
259+
ghostTableView.isHidden = false
260+
}
261+
262+
/// Removes ghost placeholder
263+
///
264+
func removeGhostTableView() {
265+
ghostTableView.removeGhostContent()
266+
ghostTableView.isHidden = true
267+
}
268+
}
269+
230270
// MARK: - Keyboard management
231271
//
232272
private extension AddAttributeOptionsViewController {
@@ -263,15 +303,15 @@ extension AddAttributeOptionsViewController {
263303
}
264304

265305
enum Row: Equatable {
266-
case termTextField
267-
case selectedTerms(name: String)
268-
case existingTerms
306+
case optionTextField
307+
case selectedOptions(name: String)
308+
case existingOptions(name: String)
269309

270310
fileprivate var type: UITableViewCell.Type {
271311
switch self {
272-
case .termTextField:
312+
case .optionTextField:
273313
return TextFieldTableViewCell.self
274-
case .selectedTerms, .existingTerms:
314+
case .selectedOptions, .existingOptions:
275315
return BasicTableViewCell.self
276316
}
277317
}

WooCommerce/Classes/ViewRelated/Products/Variations/Add Attributes/AddAttributeOptionsViewModel.swift

Lines changed: 107 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,46 @@
11
import Foundation
22
import Yosemite
33

4+
import protocol Storage.StorageManagerType
5+
46
/// Provides view data for Add Attributes, and handles init/UI/navigation actions needed.
57
///
68
final class AddAttributeOptionsViewModel {
79
typealias Section = AddAttributeOptionsViewController.Section
810
typealias Row = AddAttributeOptionsViewController.Row
911

12+
/// Enum to represents the original invocation source of this view model
13+
///
14+
enum Source {
15+
case new(name: String)
16+
case existing(attribute: ProductAttribute)
17+
}
18+
1019
/// Defines the necessary state to produce the ViewModel's outputs.
1120
///
1221
private struct State {
1322
/// Stores the options to be offered
1423
///
1524
var optionsOffered: [String] = []
25+
26+
/// Stores options previously added
27+
///
28+
var optionsAdded: [ProductAttributeTerm] = []
29+
30+
/// Indicates if the view model is syncing a global attribute options
31+
///
32+
var isSyncing: Bool = false
1633
}
1734

1835
/// Title of the navigation bar
1936
///
2037
var titleView: String? {
21-
newAttributeName ?? attribute?.name
38+
switch source {
39+
case .new(let name):
40+
return name
41+
case .existing(let attribute):
42+
return attribute.name
43+
}
2244
}
2345

2446
/// Defines next button visibility
@@ -27,12 +49,38 @@ final class AddAttributeOptionsViewModel {
2749
state.optionsOffered.isNotEmpty
2850
}
2951

52+
var showGhostTableView: Bool {
53+
state.isSyncing
54+
}
55+
3056
/// Closure to notify the `ViewController` when the view model properties change.
3157
///
3258
var onChange: (() -> (Void))?
3359

34-
private(set) var newAttributeName: String?
35-
private(set) var attribute: ProductAttribute?
60+
/// Main attribute dependency.
61+
///
62+
private let source: Source
63+
64+
/// When an attribute exists, returns an already configured `ResultsController`
65+
/// When there isn't an existing attribute, returns a dummy/un-initialized `ResultsController`
66+
///
67+
private lazy var optionsOfferedResultsController: ResultsController<StorageProductAttributeTerm> = {
68+
guard case let .existing(attribute) = source, attribute.isGlobal else {
69+
// Return a dummy ResultsController if there isn't an existing attribute. It's a workaround to not deal with an optional ResultsController.
70+
return ResultsController<StorageProductAttributeTerm>(storageManager: viewStorage, matching: nil, sortedBy: [])
71+
}
72+
73+
let predicate = NSPredicate(format: "siteID == %lld && attribute.attributeID == %lld", attribute.siteID, attribute.attributeID)
74+
let descriptor = NSSortDescriptor(key: "name", ascending: true)
75+
let controller = ResultsController<StorageProductAttributeTerm>(storageManager: viewStorage, matching: predicate, sortedBy: [descriptor])
76+
77+
controller.onDidChangeContent = { [weak self] in
78+
self?.state.optionsAdded = controller.fetchedObjects
79+
}
80+
81+
try? controller.performFetch()
82+
return controller
83+
}()
3684

3785
/// Current `ViewModel` state.
3886
///
@@ -45,14 +93,25 @@ final class AddAttributeOptionsViewModel {
4593

4694
private(set) var sections: [Section] = []
4795

48-
init(newAttribute: String?) {
49-
self.newAttributeName = newAttribute
50-
updateSections()
51-
}
96+
/// Stores manager to dispatch sync global options action.
97+
///
98+
private let stores: StoresManager
5299

53-
init(existingAttribute: ProductAttribute) {
54-
self.attribute = existingAttribute
55-
updateSections()
100+
/// Storage manager to read fetched global options.
101+
///
102+
private let viewStorage: StorageManagerType
103+
104+
init(source: Source, stores: StoresManager = ServiceLocator.stores, viewStorage: StorageManagerType = ServiceLocator.storageManager) {
105+
self.source = source
106+
self.stores = stores
107+
self.viewStorage = viewStorage
108+
109+
switch source {
110+
case .new:
111+
updateSections()
112+
case .existing:
113+
synchronizeGlobalOptions()
114+
}
56115
}
57116
}
58117

@@ -84,17 +143,16 @@ extension AddAttributeOptionsViewModel {
84143
}
85144
}
86145

87-
// MARK: - Synchronize Product Attribute terms
146+
// MARK: - Synchronize Product Attribute Options
88147
//
89148
private extension AddAttributeOptionsViewModel {
90-
// TODO: to be implemented - fetch of terms
91-
92149
/// Updates data in sections
93150
///
94151
func updateSections() {
95-
let textFieldSection = Section(header: nil, footer: Localization.footerTextField, rows: [.termTextField], allowsReorder: false)
152+
let textFieldSection = Section(header: nil, footer: Localization.footerTextField, rows: [.optionTextField], allowsReorder: false)
96153
let offeredSection = createOfferedSection()
97-
sections = [textFieldSection, offeredSection].compactMap { $0 }
154+
let addedSection = createAddedSection()
155+
sections = [textFieldSection, offeredSection, addedSection].compactMap { $0 }
98156
}
99157

100158
func createOfferedSection() -> Section? {
@@ -103,20 +161,50 @@ private extension AddAttributeOptionsViewModel {
103161
}
104162

105163
let rows = state.optionsOffered.map { option in
106-
AddAttributeOptionsViewModel.Row.selectedTerms(name: option)
164+
AddAttributeOptionsViewModel.Row.selectedOptions(name: option)
165+
}
166+
167+
return Section(header: Localization.headerSelectedOptions, footer: nil, rows: rows, allowsReorder: true)
168+
}
169+
170+
func createAddedSection() -> Section? {
171+
// TODO: Handle attribute local options
172+
guard state.optionsAdded.isNotEmpty else {
173+
return nil
174+
}
175+
176+
let rows = state.optionsAdded.map { option in
177+
AddAttributeOptionsViewModel.Row.existingOptions(name: option.name)
178+
}
179+
180+
return Section(header: Localization.headerExistingOptions, footer: nil, rows: rows, allowsReorder: false)
181+
}
182+
183+
/// Synchronizes options for global attributes
184+
///
185+
func synchronizeGlobalOptions() {
186+
guard case let .existing(attribute) = source, attribute.isGlobal else {
187+
return
107188
}
108189

109-
return Section(header: Localization.headerSelectedTerms, footer: nil, rows: rows, allowsReorder: true)
190+
let fetchOptions = ProductAttributeTermAction.synchronizeProductAttributeTerms(siteID: attribute.siteID,
191+
attributeID: attribute.attributeID) { [weak self] _ in
192+
guard let self = self else { return }
193+
self.state.optionsAdded = self.optionsOfferedResultsController.fetchedObjects
194+
self.state.isSyncing = false
195+
}
196+
state.isSyncing = true
197+
stores.dispatch(fetchOptions)
110198
}
111199
}
112200

113201
private extension AddAttributeOptionsViewModel {
114202
enum Localization {
115203
static let footerTextField = NSLocalizedString("Add each option and press enter",
116204
comment: "Footer of text field section in Add Attribute Options screen")
117-
static let headerSelectedTerms = NSLocalizedString("OPTIONS OFFERED",
205+
static let headerSelectedOptions = NSLocalizedString("OPTIONS OFFERED",
118206
comment: "Header of selected attribute options section in Add Attribute Options screen")
119-
static let headerExistingTerms = NSLocalizedString("ADD OPTIONS",
207+
static let headerExistingOptions = NSLocalizedString("ADD OPTIONS",
120208
comment: "Header of existing attribute options section in Add Attribute Options screen")
121209
}
122210
}

WooCommerce/Classes/ViewRelated/Products/Variations/Add Attributes/AddAttributeViewController.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -266,17 +266,20 @@ extension AddAttributeViewController: KeyboardScrollable {
266266
extension AddAttributeViewController {
267267

268268
@objc private func doneButtonPressed() {
269-
presentAddAttributeOptions(for: viewModel.newAttributeName)
269+
guard let name = viewModel.newAttributeName else {
270+
return
271+
}
272+
presentAddAttributeOptions(for: name)
270273
}
271274

272-
private func presentAddAttributeOptions(for newAttribute: String?) {
273-
let viewModel = AddAttributeOptionsViewModel(newAttribute: newAttribute)
275+
private func presentAddAttributeOptions(for newAttribute: String) {
276+
let viewModel = AddAttributeOptionsViewModel(source: .new(name: newAttribute))
274277
let addAttributeOptionsVC = AddAttributeOptionsViewController(viewModel: viewModel)
275278
navigationController?.pushViewController(addAttributeOptionsVC, animated: true)
276279
}
277280

278281
private func presentAddAttributeOptions(for existingAttribute: ProductAttribute) {
279-
let viewModel = AddAttributeOptionsViewModel(existingAttribute: existingAttribute)
282+
let viewModel = AddAttributeOptionsViewModel(source: .existing(attribute: existingAttribute))
280283
let addAttributeOptionsVC = AddAttributeOptionsViewController(viewModel: viewModel)
281284
navigationController?.pushViewController(addAttributeOptionsVC, animated: true)
282285
}

0 commit comments

Comments
 (0)