Skip to content

Commit 55df609

Browse files
authored
Merge branch 'trunk' into woomob-244-woo-pos-coupons-list-disabled-state-designer-ui-and-copy
2 parents 4c128b1 + 104f8c9 commit 55df609

File tree

4 files changed

+42
-134
lines changed

4 files changed

+42
-134
lines changed

WooCommerce/Classes/POS/Controllers/PointOfSaleItemsController.swift

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,21 @@ protocol PointOfSaleSearchingItemsControllerProtocol: PointOfSaleItemsController
3535

3636
@available(iOS 17.0, *)
3737
@Observable final class PointOfSaleItemsController: PointOfSaleSearchingItemsControllerProtocol {
38-
var itemsViewState: ItemsViewState = ItemsViewState(containerState: .loading,
39-
itemsStack: ItemsStackState(root: .loading([]),
40-
itemStates: [:]))
38+
var itemsViewState: ItemsViewState
4139
private let paginationTracker: AsyncPaginationTracker
4240
private var childPaginationTrackers: [POSItem: AsyncPaginationTracker] = [:]
4341
private var itemProvider: PointOfSaleItemServiceProtocol
4442
private let itemFetchStrategyFactory: PointOfSaleItemFetchStrategyFactory
4543
private var fetchStrategy: PointOfSalePurchasableItemFetchStrategy
4644

47-
init(itemProvider: PointOfSaleItemServiceProtocol, itemFetchStrategyFactory: PointOfSaleItemFetchStrategyFactory) {
45+
init(itemProvider: PointOfSaleItemServiceProtocol,
46+
itemFetchStrategyFactory: PointOfSaleItemFetchStrategyFactory,
47+
initialState: ItemsViewState = ItemsViewState(containerState: .loading,
48+
itemsStack: ItemsStackState(root: .loading([]),
49+
itemStates: [:]))) {
4850
self.itemProvider = itemProvider
4951
self.itemFetchStrategyFactory = itemFetchStrategyFactory
52+
self.itemsViewState = initialState
5053
self.paginationTracker = .init()
5154
self.fetchStrategy = itemFetchStrategyFactory.defaultStrategy
5255
}
@@ -65,7 +68,7 @@ protocol PointOfSaleSearchingItemsControllerProtocol: PointOfSaleItemsController
6568
@MainActor
6669
func searchItems(searchTerm: String, baseItem: ItemListBaseItem) async {
6770
fetchStrategy = itemFetchStrategyFactory.searchStrategy(searchTerm: searchTerm)
68-
setLoadingState(base: baseItem)
71+
setSearchingState(base: baseItem)
6972
await loadFirstPage(base: baseItem)
7073
}
7174

@@ -259,8 +262,6 @@ private extension PointOfSaleItemsController {
259262
}
260263
return pagedItems.hasMorePages
261264
} catch PointOfSaleItemServiceError.requestCancelled {
262-
itemsViewState.containerState = .content
263-
itemsViewState.itemsStack.root = .loaded(itemsViewState.itemsStack.root.items, hasMoreItems: true)
264265
// Assume that we have more pages since we'd made a request, and it was cancelled
265266
return true
266267
}
@@ -291,9 +292,6 @@ private extension PointOfSaleItemsController {
291292
updateState(for: parentItem, to: .loaded(allItems, hasMoreItems: pagedItems.hasMorePages))
292293
return pagedItems.hasMorePages
293294
} catch PointOfSaleItemServiceError.requestCancelled {
294-
itemsViewState.containerState = .content
295-
updateState(for: parentItem, to: .loaded(itemsViewState.itemsStack.itemStates[parentItem]?.items ?? [],
296-
hasMoreItems: true))
297295
// Assume that we have more pages since we'd made a request, and it was cancelled
298296
return true
299297
}

WooCommerce/Classes/POS/Presentation/ItemListView.swift

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ struct ItemListView: View {
1414

1515
@Binding var selectedItemType: ItemType
1616

17+
@State private var searchTask: Task<Void, Never>?
18+
@State private var didFinishSearch = true
19+
1720
var itemsController: PointOfSaleItemsControllerProtocol {
1821
switch selectedItemType {
1922
case .products(search: false):
@@ -101,9 +104,35 @@ private extension ItemListView {
101104
Text("Search")
102105
}
103106
.onChange(of: searchTerm) { oldValue, newValue in
104-
Task {
105-
selectedItemType = .products(search: newValue.isNotEmpty)
107+
selectedItemType = .products(search: newValue.isNotEmpty)
108+
109+
// The debouncing logic is a little tricky, because the loading state is held in the controller.
110+
// Arguably, we should use view state `isSearching` for this, so the UI is independent of the request timing.
111+
112+
// As the user types, we don't want to send every keystroke to the remote, so we debounce the requests.
113+
// However, we don't want to debounce the first keystroke of a new search, so that the loading
114+
// state shows immediately and the UI feels responsive.
115+
116+
// So, if the last search was finished, we don't debounce the first character. If it didn't
117+
// finish i.e. it is still ongoing, we debounce the next keystrokes by 300ms. In either case,
118+
// the ongoing search is redundant now there's a new search term, so we cancel it.
119+
let shouldDebounceNextSearchRequest = !didFinishSearch
120+
searchTask?.cancel()
121+
122+
searchTask = Task {
123+
if shouldDebounceNextSearchRequest {
124+
try? await Task.sleep(nanoseconds: 300 * NSEC_PER_MSEC)
125+
}
126+
127+
guard !Task.isCancelled else { return }
128+
129+
didFinishSearch = false
130+
106131
await posModel.purchasableItemsSearchController.searchItems(searchTerm: newValue, baseItem: .root)
132+
133+
if !Task.isCancelled {
134+
didFinishSearch = true
135+
}
107136
}
108137
}
109138
}

WooCommerce/Classes/ViewRelated/Hub Menu/HubMenu.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ struct HubMenu: View {
4646
purchasableItemsSearchController: PointOfSaleItemsController(
4747
itemProvider: PointOfSaleItemService(
4848
currencySettings: ServiceLocator.currencySettings),
49-
itemFetchStrategyFactory: viewModel.posItemFetchStrategyFactory),
49+
itemFetchStrategyFactory: viewModel.posItemFetchStrategyFactory,
50+
initialState: .init(containerState: .content,
51+
itemsStack: .init(root: .loaded([], hasMoreItems: true), itemStates: [:]))),
5052
couponsController: PointOfSaleCouponsController(itemProvider: viewModel.posCouponProvider),
5153
onPointOfSaleModeActiveStateChange: { isEnabled in
5254
viewModel.updateDefaultConfigurationForPointOfSale(isEnabled)

WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleItemsControllerTests.swift

Lines changed: 0 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -366,63 +366,6 @@ final class PointOfSaleItemsControllerTests {
366366
#expect(errorState == PointOfSaleErrorState.errorOnLoadingProductsNextPage)
367367
}
368368

369-
@available(iOS 17.0, *)
370-
@Test func loadItems_when_request_is_cancelled_then_state_is_loaded() async throws {
371-
// Given
372-
let itemProvider = MockPointOfSaleItemService()
373-
let sut = PointOfSaleItemsController(
374-
itemProvider: itemProvider,
375-
itemFetchStrategyFactory: PointOfSaleItemFetchStrategyFactory(siteID: 1, credentials: nil)
376-
)
377-
378-
itemProvider.errorToThrow = PointOfSaleItemServiceError.requestCancelled
379-
try #require(sut.itemsViewState.containerState == .loading)
380-
381-
// When
382-
await sut.loadItems(base: .root)
383-
384-
// Then
385-
guard case .loaded(let items, let hasMoreItems) = sut.itemsViewState.itemsStack.root else {
386-
Issue.record("Expected loaded ItemList state, but got \(sut.itemsViewState.itemsStack.root)")
387-
return
388-
}
389-
#expect(items.count == 0)
390-
#expect(hasMoreItems)
391-
}
392-
393-
@available(iOS 17.0, *)
394-
@Test func loadNextItems_when_request_is_cancelled_then_state_is_loaded() async throws {
395-
// Given
396-
let itemProvider = MockPointOfSaleItemService()
397-
let sut = PointOfSaleItemsController(
398-
itemProvider: itemProvider,
399-
itemFetchStrategyFactory: PointOfSaleItemFetchStrategyFactory(siteID: 1, credentials: nil)
400-
)
401-
402-
itemProvider.shouldSimulateTwoPages = true
403-
await sut.loadItems(base: .root)
404-
405-
guard case .loaded = sut.itemsViewState.itemsStack.root else {
406-
Issue.record("Expected loaded ItemList state, but got \(sut.itemsViewState.itemsStack.root)")
407-
return
408-
}
409-
410-
itemProvider.errorToThrow = PointOfSaleItemServiceError.requestCancelled
411-
412-
// When
413-
await sut.loadNextItems(base: .root)
414-
415-
// Then
416-
#expect(sut.itemsViewState.containerState == .content)
417-
418-
guard case .loaded(let items, let hasMoreItems) = sut.itemsViewState.itemsStack.root else {
419-
Issue.record("Expected loaded ItemList state, but got \(sut.itemsViewState.itemsStack.root)")
420-
return
421-
}
422-
#expect(items.count == 2)
423-
#expect(hasMoreItems)
424-
}
425-
426369
@available(iOS 17.0, *)
427370
@Test func loadNextItems_after_itemProvider_throws_error_then_the_same_page_is_requested_next() async throws {
428371
// Given
@@ -638,70 +581,6 @@ final class PointOfSaleItemsControllerTests {
638581
}
639582
}
640583

641-
@available(iOS 17.0, *)
642-
@Test func loadChildItems_when_request_is_cancelled_then_state_is_loaded() async throws {
643-
// Given
644-
let itemProvider = MockPointOfSaleItemService()
645-
let sut = PointOfSaleItemsController(
646-
itemProvider: itemProvider,
647-
itemFetchStrategyFactory: PointOfSaleItemFetchStrategyFactory(siteID: 1, credentials: nil)
648-
)
649-
650-
let parentItem = POSItem.variableParentProduct(POSVariableParentProduct(id: UUID(),
651-
name: "Parent product",
652-
productImageSource: nil,
653-
productID: 125))
654-
let baseItem = ItemListBaseItem.parent(parentItem)
655-
656-
itemProvider.errorToThrow = PointOfSaleItemServiceError.requestCancelled
657-
try #require(sut.itemsViewState.containerState == .loading)
658-
659-
// When
660-
await sut.loadItems(base: baseItem)
661-
662-
// Then
663-
guard case .loaded(let items, let hasMoreItems) = sut.itemsViewState.itemsStack.itemStates[parentItem] else {
664-
Issue.record("Expected loaded ItemList state, but got \(String(describing: sut.itemsViewState.itemsStack.itemStates[parentItem]))")
665-
return
666-
}
667-
#expect(items.count == 0)
668-
#expect(hasMoreItems)
669-
}
670-
671-
@available(iOS 17.0, *)
672-
@Test func loadNextChildItems_when_request_is_cancelled_then_state_is_loaded() async throws {
673-
// Given
674-
let itemProvider = MockPointOfSaleItemService()
675-
let sut = PointOfSaleItemsController(
676-
itemProvider: itemProvider,
677-
itemFetchStrategyFactory: PointOfSaleItemFetchStrategyFactory(siteID: 1, credentials: nil)
678-
)
679-
680-
let parentItem = POSItem.variableParentProduct(POSVariableParentProduct(id: UUID(),
681-
name: "Parent product",
682-
productImageSource: nil,
683-
productID: 125))
684-
let baseItem = ItemListBaseItem.parent(parentItem)
685-
686-
itemProvider.shouldSimulateTwoPagesOfVariations = true
687-
await sut.loadItems(base: baseItem)
688-
689-
itemProvider.errorToThrow = PointOfSaleItemServiceError.requestCancelled
690-
691-
// When
692-
await sut.loadNextItems(base: baseItem)
693-
694-
// Then
695-
#expect(sut.itemsViewState.containerState == .content)
696-
697-
guard case .loaded(let items, let hasMoreItems) = sut.itemsViewState.itemsStack.itemStates[parentItem] else {
698-
Issue.record("Expected loaded ItemList state, but got \(String(describing: sut.itemsViewState.itemsStack.itemStates[parentItem]))")
699-
return
700-
}
701-
#expect(items.count == 2)
702-
#expect(hasMoreItems)
703-
}
704-
705584
@available(iOS 17.0, *)
706585
@Test func search_sets_a_fetch_strategy_with_search_term_on_the_service() async throws {
707586
// Given

0 commit comments

Comments
 (0)