Skip to content

Commit 97ec53c

Browse files
authored
[Local Catalog] Show items from GRDB in POS (#16235)
2 parents ad04076 + 9ef581f commit 97ec53c

File tree

12 files changed

+493
-23
lines changed

12 files changed

+493
-23
lines changed

Modules/Sources/PointOfSale/Controllers/PointOfSaleItemsController.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ protocol PointOfSaleSearchingItemsControllerProtocol: PointOfSaleItemsController
4141
init(itemProvider: PointOfSaleItemServiceProtocol,
4242
itemFetchStrategyFactory: PointOfSaleItemFetchStrategyFactoryProtocol,
4343
initialState: ItemsViewState = ItemsViewState(containerState: .loading,
44-
itemsStack: ItemsStackState(root: .loading([]),
44+
itemsStack: ItemsStackState(root: .initial,
4545
itemStates: [:])),
4646
analyticsProvider: POSAnalyticsProviding) {
4747
self.itemProvider = itemProvider
@@ -213,8 +213,12 @@ private extension PointOfSaleItemsController {
213213
func setRootLoadingState() {
214214
let items = itemsViewState.itemsStack.root.items
215215

216-
let isInitialState = itemsViewState.containerState == .loading
217-
if !isInitialState {
216+
let isInitialState = itemsViewState.containerState == .loading && itemsViewState.itemsStack.root == .initial
217+
if isInitialState {
218+
// Transition from initial to loading on first load
219+
itemsViewState.itemsStack.root = .loading([])
220+
} else {
221+
// Preserve items during refresh
218222
itemsViewState.itemsStack.root = .loading(items)
219223
}
220224
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import Foundation
2+
import Observation
3+
import class WooFoundation.CurrencySettings
4+
import enum Yosemite.POSItem
5+
import protocol Yosemite.POSObservableDataSourceProtocol
6+
import struct Yosemite.POSVariableParentProduct
7+
import class Yosemite.GRDBObservableDataSource
8+
import protocol Storage.GRDBManagerProtocol
9+
10+
/// Controller that wraps an observable data source for POS items
11+
/// Uses computed state based on data source observations for automatic UI updates
12+
@Observable
13+
final class PointOfSaleObservableItemsController: PointOfSaleItemsControllerProtocol {
14+
private let dataSource: POSObservableDataSourceProtocol
15+
16+
// Track which items have been loaded at least once
17+
private var hasLoadedProducts = false
18+
private var hasLoadedVariationsForCurrentParent = false
19+
20+
// Track current parent for variation state mapping
21+
private var currentParentItem: POSItem?
22+
23+
var itemsViewState: ItemsViewState {
24+
ItemsViewState(
25+
containerState: containerState,
26+
itemsStack: ItemsStackState(
27+
root: rootState,
28+
itemStates: variationStates
29+
)
30+
)
31+
}
32+
33+
init(siteID: Int64,
34+
grdbManager: GRDBManagerProtocol,
35+
currencySettings: CurrencySettings) {
36+
self.dataSource = GRDBObservableDataSource(
37+
siteID: siteID,
38+
grdbManager: grdbManager,
39+
currencySettings: currencySettings
40+
)
41+
}
42+
43+
// periphery:ignore - used by tests
44+
init(dataSource: POSObservableDataSourceProtocol) {
45+
self.dataSource = dataSource
46+
}
47+
48+
func loadItems(base: ItemListBaseItem) async {
49+
switch base {
50+
case .root:
51+
dataSource.loadProducts()
52+
hasLoadedProducts = true
53+
case .parent(let parent):
54+
guard case .variableParentProduct(let parentProduct) = parent else {
55+
assertionFailure("Unsupported parent type for loading items: \(parent)")
56+
return
57+
}
58+
59+
// If switching to a different parent, reset the loaded flag
60+
if currentParentItem != parent {
61+
currentParentItem = parent
62+
hasLoadedVariationsForCurrentParent = false
63+
}
64+
65+
dataSource.loadVariations(for: parentProduct)
66+
hasLoadedVariationsForCurrentParent = true
67+
}
68+
}
69+
70+
func refreshItems(base: ItemListBaseItem) async {
71+
switch base {
72+
case .root:
73+
dataSource.refresh()
74+
case .parent(let parent):
75+
guard case .variableParentProduct(let parentProduct) = parent else {
76+
assertionFailure("Unsupported parent type for refreshing items: \(parent)")
77+
return
78+
}
79+
dataSource.loadVariations(for: parentProduct)
80+
}
81+
}
82+
83+
func loadNextItems(base: ItemListBaseItem) async {
84+
switch base {
85+
case .root:
86+
dataSource.loadMoreProducts()
87+
case .parent:
88+
dataSource.loadMoreVariations()
89+
}
90+
}
91+
}
92+
93+
// MARK: - State Computation
94+
private extension PointOfSaleObservableItemsController {
95+
var containerState: ItemsContainerState {
96+
// Use .loading during initial load, .content otherwise
97+
if !hasLoadedProducts && dataSource.isLoadingProducts {
98+
return .loading
99+
}
100+
return .content
101+
}
102+
103+
var rootState: ItemListState {
104+
let items = dataSource.productItems
105+
106+
// Initial state - not yet loaded
107+
if !hasLoadedProducts {
108+
return .initial
109+
}
110+
111+
// Loading state - preserve existing items
112+
if dataSource.isLoadingProducts {
113+
return .loading(items)
114+
}
115+
116+
// Error state
117+
if let error = dataSource.productError, items.isEmpty {
118+
return .error(.errorOnLoadingProducts(error: error))
119+
}
120+
121+
// Empty state
122+
if items.isEmpty {
123+
return .empty
124+
}
125+
126+
// Loaded state
127+
return .loaded(items, hasMoreItems: dataSource.hasMoreProducts)
128+
}
129+
130+
var variationStates: [POSItem: ItemListState] {
131+
guard let parentItem = currentParentItem else {
132+
return [:]
133+
}
134+
135+
let items = dataSource.variationItems
136+
137+
// Initial state - not yet loaded
138+
if !hasLoadedVariationsForCurrentParent {
139+
return [parentItem: .initial]
140+
}
141+
142+
// Loading state - preserve existing items
143+
if dataSource.isLoadingVariations {
144+
return [parentItem: .loading(items)]
145+
}
146+
147+
// Error state
148+
if let error = dataSource.variationError, items.isEmpty {
149+
return [parentItem: .error(.errorOnLoadingVariations(error: error))]
150+
}
151+
152+
// Empty state
153+
if items.isEmpty {
154+
return [parentItem: .empty]
155+
}
156+
157+
// Loaded state
158+
return [parentItem: .loaded(items, hasMoreItems: dataSource.hasMoreVariations)]
159+
}
160+
}

Modules/Sources/PointOfSale/Models/ItemListState.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import enum Yosemite.POSItem
22

33
enum ItemListState {
4+
case initial
45
case loading(_ currentItems: [POSItem])
56
case loaded(_ items: [POSItem], hasMoreItems: Bool)
67
case inlineError(_ items: [POSItem], error: PointOfSaleErrorState, context: InlineErrorContext)
@@ -38,7 +39,7 @@ extension ItemListState {
3839
.loaded(let items, _),
3940
.inlineError(let items, _, _):
4041
return items
41-
case .error, .empty:
42+
case .initial, .error, .empty:
4243
return []
4344
}
4445
}

Modules/Sources/PointOfSale/Presentation/Item Selector/ChildItemList.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ struct ChildItemList: View {
3434
var body: some View {
3535
VStack {
3636
switch state {
37-
case .loaded([], _):
37+
case .initial, .loaded([], _):
3838
emptyView
3939
case .loading, .loaded, .inlineError:
4040
listView

Modules/Sources/PointOfSale/Presentation/Item Selector/ItemList.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ struct ItemList<HeaderView: View>: View {
122122
await itemsController.loadNextItems(base: node)
123123
}
124124
})
125-
case .loaded, .error, .empty, .none, .inlineError(_, _, .refresh):
125+
case .initial, .loaded, .error, .empty, .none, .inlineError(_, _, .refresh):
126126
EmptyView()
127127
}
128128
}
@@ -136,7 +136,7 @@ struct ItemList<HeaderView: View>: View {
136136
await itemsController.loadItems(base: .root)
137137
}
138138
})
139-
case .loaded, .error, .empty, .none, .loading, .inlineError(_, _, .pagination):
139+
case .initial, .loaded, .error, .empty, .none, .loading, .inlineError(_, _, .pagination):
140140
EmptyView()
141141
}
142142
}

Modules/Sources/PointOfSale/Presentation/ItemListView.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,8 @@ struct ItemListView: View {
147147
@ViewBuilder
148148
private func itemListContent(_ itemListType: ItemListType) -> some View {
149149
switch itemListState(itemListType) {
150-
case .loading,
150+
case .initial,
151+
.loading,
151152
.loaded,
152153
.inlineError:
153154
listView(itemListType: itemListType)

Modules/Sources/PointOfSale/Presentation/PointOfSaleEntryPointView.swift

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,23 @@ public struct PointOfSaleEntryPointView: View {
6666
services: POSDependencyProviding) {
6767
self.onPointOfSaleModeActiveStateChange = onPointOfSaleModeActiveStateChange
6868

69-
self.itemsController = PointOfSaleItemsController(
70-
itemProvider: PointOfSaleItemService(currencySettings: services.currency.currencySettings),
71-
itemFetchStrategyFactory: itemFetchStrategyFactory,
72-
analyticsProvider: services.analytics
73-
)
69+
// Use observable controller with GRDB if available and feature flag is enabled, otherwise fall back to standard controller
70+
// Note: We check feature flag here for eligibility. Once eligibility checking is
71+
// refactored to be more centralized, this check can be simplified.
72+
let isGRDBEnabled = services.featureFlags.isFeatureFlagEnabled(.pointOfSaleLocalCatalogi1)
73+
if let grdbManager = grdbManager, catalogSyncCoordinator != nil, isGRDBEnabled {
74+
self.itemsController = PointOfSaleObservableItemsController(
75+
siteID: siteID,
76+
grdbManager: grdbManager,
77+
currencySettings: services.currency.currencySettings
78+
)
79+
} else {
80+
self.itemsController = PointOfSaleItemsController(
81+
itemProvider: PointOfSaleItemService(currencySettings: services.currency.currencySettings),
82+
itemFetchStrategyFactory: itemFetchStrategyFactory,
83+
analyticsProvider: services.analytics
84+
)
85+
}
7486
self.purchasableItemsSearchController = PointOfSaleItemsController(
7587
itemProvider: PointOfSaleItemService(currencySettings: services.currency.currencySettings),
7688
itemFetchStrategyFactory: itemFetchStrategyFactory,

Modules/Sources/Yosemite/PointOfSale/Items/GRDBObservableDataSource.swift

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ public final class GRDBObservableDataSource: POSObservableDataSourceProtocol {
1616
public private(set) var variationItems: [POSItem] = []
1717
public private(set) var isLoadingProducts: Bool = false
1818
public private(set) var isLoadingVariations: Bool = false
19-
public private(set) var error: Error? = nil
19+
public private(set) var productError: Error? = nil
20+
public private(set) var variationError: Error? = nil
2021

2122
public var hasMoreProducts: Bool {
2223
productItems.count >= (pageSize * currentProductPage) && totalProductCount > productItems.count
@@ -92,6 +93,7 @@ public final class GRDBObservableDataSource: POSObservableDataSourceProtocol {
9293
currentVariationPage = 1
9394
isLoadingVariations = true
9495
variationItems = []
96+
variationError = nil
9597

9698
setupVariationObservation(parentProduct: parentProduct)
9799
setupVariationStatisticsObservation(parentProduct: parentProduct)
@@ -146,15 +148,15 @@ public final class GRDBObservableDataSource: POSObservableDataSourceProtocol {
146148
.sink(
147149
receiveCompletion: { [weak self] completion in
148150
if case .failure(let error) = completion {
149-
self?.error = error
151+
self?.productError = error
150152
self?.isLoadingProducts = false
151153
}
152154
},
153155
receiveValue: { [weak self] observedProducts in
154156
guard let self else { return }
155157
let posItems = itemMapper.mapProductsToPOSItems(products: observedProducts)
156158
productItems = posItems
157-
error = nil
159+
productError = nil
158160
isLoadingProducts = false
159161
}
160162
)
@@ -194,7 +196,7 @@ public final class GRDBObservableDataSource: POSObservableDataSourceProtocol {
194196
.sink(
195197
receiveCompletion: { [weak self] completion in
196198
if case .failure(let error) = completion {
197-
self?.error = error
199+
self?.variationError = error
198200
self?.isLoadingVariations = false
199201
}
200202
},
@@ -205,7 +207,7 @@ public final class GRDBObservableDataSource: POSObservableDataSourceProtocol {
205207
parentProduct: parentProduct
206208
)
207209
variationItems = posItems
208-
error = nil
210+
variationError = nil
209211
isLoadingVariations = false
210212
}
211213
)

Modules/Sources/Yosemite/PointOfSale/Items/POSObservableDataSource.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ public protocol POSObservableDataSourceProtocol {
2121
/// Whether more variations are available for current parent
2222
var hasMoreVariations: Bool { get }
2323

24-
/// Current error, if any
25-
var error: Error? { get }
24+
/// Error from product loading, if any
25+
var productError: Error? { get }
26+
27+
/// Error from variation loading, if any
28+
var variationError: Error? { get }
2629

2730
/// Loads the first page of products
2831
func loadProducts()

0 commit comments

Comments
 (0)