Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experiment/enum and concrete types for variable products #14648

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions Networking/Networking/Remote/ProductVariationsRemote.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,15 @@ public protocol ProductVariationsRemoteProtocol {
productVariations: [ProductVariation],
completion: @escaping (Result<[ProductVariation], Error>) -> Void)
func deleteProductVariation(siteID: Int64, productID: Int64, variationID: Int64, completion: @escaping (Result<ProductVariation, Error>) -> Void)
func fetchProductVariations(for siteID: Int64,
parentProductID: Int64,
pageNumber: Int,
pageSize: Int) async throws -> [ProductVariation]
}

/// ProductVariation: Remote Endpoints
///
public class ProductVariationsRemote: Remote, ProductVariationsRemoteProtocol {

/// Retrieves all of the `ProductVariation`s available.
///
/// - Parameters:
Expand All @@ -57,6 +60,36 @@ public class ProductVariationsRemote: Remote, ProductVariationsRemoteProtocol {
pageNumber: Int = Default.pageNumber,
pageSize: Int = Default.pageSize,
completion: @escaping ([ProductVariation]?, Error?) -> Void) {
let request = productVariationsRequest(for: siteID,
productID: productID,
variationIDs: variationIDs,
context: context,
pageNumber: pageNumber,
pageSize: pageSize)
let mapper = ProductVariationListMapper(siteID: siteID, productID: productID)
enqueue(request, mapper: mapper, completion: completion)
}

public func fetchProductVariations(for siteID: Int64,
parentProductID: Int64,
pageNumber: Int,
pageSize: Int = Default.pageSize) async throws -> [ProductVariation] {
let request = productVariationsRequest(for: siteID,
productID: parentProductID,
variationIDs: [],
context: nil,
pageNumber: pageNumber,
pageSize: pageSize)
let mapper = ProductVariationListMapper(siteID: siteID, productID: parentProductID)
return try await enqueue(request, mapper: mapper)
}

private func productVariationsRequest(for siteID: Int64,
productID: Int64,
variationIDs: [Int64],
context: String?,
pageNumber: Int,
pageSize: Int) -> JetpackRequest {
let stringOfVariationIDs = variationIDs.map { String($0) }
.joined(separator: ",")
let parameters = [
Expand All @@ -74,8 +107,7 @@ public class ProductVariationsRemote: Remote, ProductVariationsRemoteProtocol {
path: path,
parameters: parameters,
availableAsRESTRequest: true)
let mapper = ProductVariationListMapper(siteID: siteID, productID: productID)
enqueue(request, mapper: mapper, completion: completion)
return request
}

/// Retrieves a specific `ProductVariation`.
Expand Down
1 change: 0 additions & 1 deletion Networking/Networking/Remote/ProductsRemote.swift
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,6 @@ public final class ProductsRemote: Remote, ProductsRemoteProtocol {
let parameters = [
ParameterKey.page: String(pageNumber),
ParameterKey.perPage: POSConstants.productsPerPage,
ParameterKey.productType: POSConstants.productType,
ParameterKey.orderBy: OrderKey.name.value,
ParameterKey.order: Order.ascending.value,
ParameterKey.productStatus: POSConstants.productStatus,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,35 @@
import Foundation
import Combine
import protocol Yosemite.POSDisplayableItem
import enum Yosemite.POSItem
import struct Yosemite.POSParentProduct
import protocol Yosemite.PointOfSaleItemServiceProtocol
import enum Yosemite.PointOfSaleProductServiceError
import protocol Yosemite.PointOfSaleVariationServiceProtocol

protocol PointOfSaleItemsControllerProtocol {
var itemListStatePublisher: any Publisher<ItemListState, Never> { get }
func loadInitialItems() async
func loadNextItems() async
func reload() async
func loadChildItems(for parentItem: POSParentProduct) async
func goBack()
}

class PointOfSaleItemsController: PointOfSaleItemsControllerProtocol {
private(set) var itemListStatePublisher: any Publisher<ItemListState, Never>
private var itemListStateSubject: PassthroughSubject<ItemListState, Never> = .init()
private var allItems: [POSDisplayableItem] = []
private var allItems: [POSItem] = []
private var currentPage: Int = Constants.initialPage
private var mightHaveMorePages: Bool = true
private let itemProvider: PointOfSaleItemServiceProtocol
private let rootItemProvider: PointOfSaleItemServiceProtocol

init(itemProvider: PointOfSaleItemServiceProtocol) {
self.itemProvider = itemProvider
private var allChildItems: [UUID: [POSItem]] = [:]
private let variationProvider: PointOfSaleVariationServiceProtocol

init(rootItemProvider: PointOfSaleItemServiceProtocol,
variationProvider: PointOfSaleVariationServiceProtocol) {
self.rootItemProvider = rootItemProvider
self.variationProvider = variationProvider
itemListStatePublisher = itemListStateSubject.eraseToAnyPublisher()
}

Expand All @@ -37,7 +46,7 @@ class PointOfSaleItemsController: PointOfSaleItemsControllerProtocol {
guard mightHaveMorePages else {
return
}
itemListStateSubject.send(.loading(allItems))
itemListStateSubject.send(.loading(allItems, context: .root, pageInfo: PageInfo(currentPage: currentPage, hasMorePages: true)))

let nextPage = currentPage + 1
try await load(pageNumber: nextPage)
Expand All @@ -52,7 +61,7 @@ class PointOfSaleItemsController: PointOfSaleItemsControllerProtocol {
allItems.removeAll()
currentPage = Constants.initialPage
mightHaveMorePages = true
itemListStateSubject.send(.loading(allItems))
itemListStateSubject.send(.loading(allItems, context: .root, pageInfo: PageInfo(currentPage: currentPage, hasMorePages: true)))
try? await load(pageNumber: currentPage)
}

Expand All @@ -74,9 +83,9 @@ class PointOfSaleItemsController: PointOfSaleItemsControllerProtocol {

@MainActor
private func fetchItems(pageNumber: Int) async throws {
let newItems = try await itemProvider.providePointOfSaleItems(pageNumber: pageNumber)
let newItems = try await rootItemProvider.providePointOfSaleItems(pageNumber: pageNumber)
let uniqueNewItems = newItems.filter { newItem in
!allItems.contains(where: { $0.isEqual(to: newItem) })
!allItems.contains(newItem)
}
allItems.append(contentsOf: uniqueNewItems)
}
Expand All @@ -85,10 +94,30 @@ class PointOfSaleItemsController: PointOfSaleItemsControllerProtocol {
if allItems.isEmpty {
itemListStateSubject.send(.empty)
} else {
itemListStateSubject.send(.loaded(allItems))
itemListStateSubject.send(.loaded(allItems, context: .root, pageInfo: PageInfo(currentPage: currentPage, hasMorePages: true)))
}
}

@MainActor
func loadChildItems(for parentProduct: POSParentProduct) async {
do {
let existingItems = allChildItems[parentProduct.id] ?? []
itemListStateSubject.send(.loading(existingItems, context: .child(parent: parentProduct, parentItem: .parentProduct(parentProduct)), pageInfo: PageInfo(currentPage: Constants.initialPage, hasMorePages: true)))

let newItems = try await variationProvider.providePointOfSaleItems(for: parentProduct, pageNumber: Constants.initialPage)
let updatedItems = existingItems + newItems

allChildItems[parentProduct.id] = updatedItems
itemListStateSubject.send(.loaded(updatedItems, context: .child(parent: parentProduct, parentItem: .parentProduct(parentProduct)), pageInfo: PageInfo(currentPage: Constants.initialPage, hasMorePages: true)))
} catch {
DDLogError("Error loading child items for \(parentProduct): \(error)")
}
}

func goBack() {
updateItemListStateAfterLoadAttempt()
}

private enum Constants {
static let initialPage: Int = 1
}
Expand Down
31 changes: 13 additions & 18 deletions WooCommerce/Classes/POS/Models/ItemListState.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import protocol Yosemite.POSDisplayableItem
import protocol Yosemite.POSOrderableItem
import enum Yosemite.POSItem
import struct Yosemite.POSParentProduct

enum ItemListState: Equatable {
case empty
case initialLoading
case loading(_ currentItems: [POSDisplayableItem])
case loaded(_ items: [POSDisplayableItem])
case loading(_ currentItems: [POSItem], context: NavigationContext, pageInfo: PageInfo)
case loaded(_ items: [POSItem], context: NavigationContext, pageInfo: PageInfo)
case error(PointOfSaleErrorState)

var isLoadingAfterInitialLoad: Bool {
Expand All @@ -16,19 +16,14 @@ enum ItemListState: Equatable {
return false
}
}
}

static func == (lhs: ItemListState, rhs: ItemListState) -> Bool {
switch (lhs, rhs) {
case (.empty, .empty),
(.initialLoading, .initialLoading):
return true
case (.loading(let lhsItems), .loading(let rhsItems)),
(.loaded(let lhsItems), .loaded(let rhsItems)):
return lhsItems.isEqual(to: rhsItems)
case (.error(let lhsError), .error(let rhsError)):
return lhsError == rhsError
default:
return false
}
}
struct PageInfo: Equatable {
let currentPage: Int
let hasMorePages: Bool
}

enum NavigationContext: Equatable {
case root
case child(parent: POSParentProduct, parentItem: POSItem)
}
12 changes: 12 additions & 0 deletions WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import struct Yosemite.Order
import struct Yosemite.OrderItem
import struct Yosemite.POSCartItem
import enum Yosemite.SystemStatusAction
import struct Yosemite.POSParentProduct

protocol PointOfSaleAggregateModelProtocol {
var orderStage: PointOfSaleOrderStage { get }
Expand All @@ -25,6 +26,7 @@ protocol PointOfSaleAggregateModelProtocol {
func loadInitialItems() async
func loadNextItems() async
func reload() async
func showChildren(for: POSParentProduct)

var cart: [CartItem] { get }
func addToCart(_ item: POSOrderableItem)
Expand Down Expand Up @@ -108,6 +110,16 @@ extension PointOfSaleAggregateModel {
func reload() async {
await itemsController.reload()
}

func showChildren(for parentProduct: POSParentProduct) {
Task { @MainActor in
await itemsController.loadChildItems(for: parentProduct)
}
}

func goBack() {
itemsController.goBack()
}
}

// MARK: - Cart
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ struct PointOfSaleItemListFullscreenView<Content: View>: View {
var body: some View {
ZStack {
VStack(alignment: .center, spacing: PointOfSaleItemListErrorLayout.headerSpacing) {
POSHeaderTitleView(foregroundColor: .posSecondaryText)
POSHeaderTitleView(foregroundColor: .posSecondaryText, context: .root, backAction: {})
Spacer()
}

Expand Down
48 changes: 34 additions & 14 deletions WooCommerce/Classes/POS/Presentation/ItemListView.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import SwiftUI
import protocol Yosemite.POSDisplayableItem
import enum Yosemite.POSItem
import protocol Yosemite.POSOrderableItem
import struct Yosemite.POSParentProduct

struct ItemListView: View {
@Environment(\.floatingControlAreaSize) var floatingControlAreaSize: CGSize
Expand All @@ -16,14 +17,16 @@ struct ItemListView: View {

var body: some View {
VStack {
headerView
switch posModel.itemListState {
case .initialLoading, .empty, .error:
// These cases are handled directly in the dashboard, we do not render
// a specific view within the ItemListView to handle them
headerView()
EmptyView()
case .loading(let items), .loaded(let items):
listView(items)
case .loading(let items, let context, _),
.loaded(let items, let context, _):
headerView(context: context).transition(.slide)
listView(items).transition(.slide)
}
}
.refreshable {
Expand All @@ -41,10 +44,14 @@ struct ItemListView: View {
///
private extension ItemListView {
@ViewBuilder
var headerView: some View {
func headerView(context: NavigationContext = .root) -> some View {
VStack {
HStack {
POSHeaderTitleView()
POSHeaderTitleView(context: context) {
withAnimation {
posModel.goBack()
}
}
if !shouldShowHeaderBanner {
Spacer()
Button(action: {
Expand Down Expand Up @@ -124,13 +131,13 @@ private extension ItemListView {
}

@ViewBuilder
func listView(_ items: [POSDisplayableItem]) -> some View {
func listView(_ items: [POSItem]) -> some View {
ScrollView {
VStack {
if dynamicTypeSize.isAccessibilitySize, shouldShowHeaderBanner {
bannerCardView
}
ForEach(items, id: \.id) { item in
ForEach(items) { item in
listRow(item: item)
}
GhostItemCardView()
Expand Down Expand Up @@ -158,15 +165,28 @@ private extension ItemListView {
}

@ViewBuilder
func listRow(item: POSDisplayableItem) -> some View {
if let item = item as? POSOrderableItem {
func listRow(item: POSItem) -> some View {
switch item {
case .product(let product):
Button(action: {
posModel.addToCart(product)
}, label: {
ProductCardView(product: product)
})
case .parentProduct(let parentProduct):
Button(action: {
withAnimation {
posModel.showChildren(for: parentProduct)
}
}, label: {
ParentProductCardView(parentProduct: parentProduct)
})
case .variation(let variation):
Button(action: {
posModel.addToCart(item)
posModel.addToCart(variation)
}, label: {
ItemCardView(item: item)
VariationCardView(variation: variation)
})
} else {
ItemCardView(item: item)
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion WooCommerce/Classes/POS/Presentation/ItemRowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,11 @@ private extension ItemRowView {
}

#if DEBUG
import struct Yosemite.POSProduct
#Preview {
let orderableItem = POSProduct(id: UUID(), name: "Product 3", formattedPrice: "$3.00", productID: 3, price: "3.00")
ItemRowView(cartItem: CartItem(id: UUID(),
item: PointOfSalePreviewItemService().providePointOfSaleItem(),
item: orderableItem,
quantity: 2),
onItemRemoveTapped: { })
}
Expand Down
Loading