11import Foundation
22import Yosemite
33
4+ import protocol Storage. StorageManagerType
5+
46/// Provides view data for Add Attributes, and handles init/UI/navigation actions needed.
57///
68final 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//
89148private 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
113201private 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}
0 commit comments