Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ final class WooShippingItemsViewModel: ObservableObject {
private let currencySettings: CurrencySettings

/// Data source for items to be shipped.
private var dataSource: WooShippingItemsDataSource
private(set) var dataSource: WooShippingItemsDataSource

/// Label with the total number of items to ship.
@Published private(set) var itemsCountLabel: String = ""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import Yosemite
import SwiftUI

/// Displays a single collapsible shipment item row or grouped parent and child shipment item rows
struct CollapsibleShipmentCard: View {
@State private var isCollapsed: Bool = true

private let viewModel: CollapsibleShipmentCardViewModel

init(viewModel: CollapsibleShipmentCardViewModel) {
self.viewModel = viewModel
}

var body: some View {
VStack(alignment: .leading, spacing: 0) {
mainShipmentRow
.padding(.horizontal, Layout.horizontalPadding)
.padding(.vertical, Layout.verticalPadding)
.background(
mainShipmentRowBackground
)

if !isCollapsed {
VStack(spacing: 0) {
ForEach(Array(viewModel.childShipmentRows.enumerated()), id: \.element.id) { index, item in
VStack(spacing: 0) {
Divider()

SelectableShipmentRow(viewModel: item)
.padding(.leading, Layout.horizontalPadding * 2)
.padding(.trailing, Layout.horizontalPadding)
.padding(.vertical, Layout.verticalPadding)
.background(backgroundForChildShipmentRow(isFinalRow: index == viewModel.childShipmentRows.count - 1))
}
}
}
}
}
.frame(maxWidth: .infinity, alignment: .center)
.roundedBorder(cornerRadius: Layout.borderCornerRadius, lineColor: Color(.separator), lineWidth: Layout.borderWidth)
}
}

private extension CollapsibleShipmentCard {
@ViewBuilder
var mainShipmentRow: some View {
if viewModel.childShipmentRows.isEmpty {
SelectableShipmentRow(viewModel: viewModel.mainShipmentRow)
} else {
Button(action: {
withAnimation {
isCollapsed.toggle()
}
}, label: {
ZStack(alignment: .topTrailing) {
SelectableShipmentRow(viewModel: viewModel.mainShipmentRow)
.contentShape(Rectangle())

Image(uiImage: isCollapsed ? .chevronDownImage : .chevronUpImage)
.foregroundColor(Color(.accent))
}
})
.buttonStyle(PlainButtonStyle())
}
}

@ViewBuilder
var mainShipmentRowBackground: some View {
if isCollapsed {
RoundedRectangle(cornerRadius: Layout.borderCornerRadius)
.fill(Color(.listForeground(modal: false)))
} else {
UnevenRoundedRectangle(cornerRadii: .init(topLeading: Layout.borderCornerRadius, topTrailing: Layout.borderCornerRadius))
.fill(Color(.listForeground(modal: false)))
}
}

@ViewBuilder
func backgroundForChildShipmentRow(isFinalRow: Bool) -> some View {
if isFinalRow {
UnevenRoundedRectangle(cornerRadii: .init(bottomLeading: Layout.borderCornerRadius, bottomTrailing: Layout.borderCornerRadius))
.fill(Color(.listForeground(modal: false)))
} else {
Color(.listForeground(modal: false))
}
}
}

private extension CollapsibleShipmentCard {
enum Layout {
static let borderCornerRadius: CGFloat = 8
static let borderWidth: CGFloat = 0.5
static let borderLineWidth: CGFloat = 1
static let horizontalPadding: CGFloat = 16
static let verticalPadding: CGFloat = 8
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import SwiftUI
import WooFoundation
import Yosemite

/// View model for `CollapsibleShipmentCard`.
final class CollapsibleShipmentCardViewModel: ObservableObject, Identifiable {
let id = UUID()

/// The main shipment row.
let mainShipmentRow: SelectableShipmentRowViewModel

/// Child shipment rows, if the shipment has more than one quantity
let childShipmentRows: [SelectableShipmentRowViewModel]

init(parentShipmentId: String,
childShipmentIds: [String],
item: ShippingLabelPackageItem,
currency: String) {
let mainShippingItem = WooShippingItemRowViewModel(item: ShippingLabelPackageItem(copy: item, quantity: max(1.0, Decimal(childShipmentIds.count))),
currency: currency)
let childShippingItem = WooShippingItemRowViewModel(item: ShippingLabelPackageItem(copy: item, quantity: 1.0),
currency: currency)

self.mainShipmentRow = SelectableShipmentRowViewModel(shipmentId: parentShipmentId,
isSelectable: true,
item: mainShippingItem,
showQuantity: true)
self.childShipmentRows = childShipmentIds.map({
SelectableShipmentRowViewModel(shipmentId: $0,
isSelectable: true,
item: childShippingItem,
showQuantity: false)
})

observeSelection()
}

func selectAll() {
mainShipmentRow.setSelected(true)
childShipmentRows.forEach({ $0.setSelected(true) })
}
}

private extension CollapsibleShipmentCardViewModel {
func observeSelection() {
mainShipmentRow.onSelectedChange = { [weak self] row in
guard let self else { return }

childShipmentRows.forEach({ $0.setSelected(row.selected) })
}

childShipmentRows.forEach({
$0.onSelectedChange = { [weak self] row in
guard let self else { return }

mainShipmentRow.setSelected(false)
}
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import Yosemite
import SwiftUI

/// Row for a selectable shipment item to ship with the Woo Shipping extension.
struct SelectableShipmentRow: View {
@ObservedObject private var viewModel: SelectableShipmentRowViewModel

init(viewModel: SelectableShipmentRowViewModel) {
self.viewModel = viewModel
}

@ScaledMetric private var scale: CGFloat = 1

var body: some View {
AdaptiveStack(spacing: Layout.horizontalSpacing) {
if viewModel.isSelectable {
selectionCircle(selected: viewModel.selected)
.contentShape(Rectangle())
.onTapGesture {
viewModel.handleTap()
}
}

ProductImageThumbnail(productImageURL: viewModel.item.imageUrl,
productImageSize: Layout.imageSize,
scale: scale,
productImageCornerRadius: Layout.imageCornerRadius,
foregroundColor: Color(UIColor.listSmallIcon))
.overlay(alignment: .topTrailing) {
if viewModel.showQuantity {
BadgeView(text: viewModel.item.quantityLabel,
customizations: .init(textColor: .white, backgroundColor: .black),
backgroundShape: badgeStyle)
.offset(x: Layout.badgeOffset, y: -Layout.badgeOffset)
Comment on lines +31 to +34
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: It looks to me that the badge is missing a white border around it when the label is longer.

image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I see that this applies to the order creation screen as well. I logged an issue to keep track. #15402

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be quickly fixed by adding stroke to the background view of the BadgeView with style roundedRectangle. But fixing it in a separate PR sounds good to me too.

}
}
VStack(alignment: .leading) {
Text(viewModel.item.name)
.bodyStyle()
Text(viewModel.item.detailsLabel)
.subheadlineStyle()
AdaptiveStack(verticalAlignment: .lastTextBaseline) {
Text(viewModel.item.weightLabel)
.subheadlineStyle()
Spacer()
Text(viewModel.item.priceLabel)
.font(.subheadline)
.foregroundStyle(Color(.text))
}
}
}
.frame(maxWidth: .infinity)
}
}

private extension SelectableShipmentRow {
@ViewBuilder
func selectionCircle(selected: Bool) -> some View {
if selected {
Image(uiImage: .checkCircleImage.withRenderingMode(.alwaysTemplate))
.foregroundStyle(Color(.primary))
} else {
Image(uiImage: .checkEmptyCircleImage)
}
}

/// Displays a different badge background shape based on the item quantity
/// Circular for 2-character quantities, rounded for 3-character quantities or more
var badgeStyle: BadgeView.BackgroundShape {
if viewModel.item.quantityLabel.count < 3 {
return .circle
} else {
return .roundedRectangle(cornerRadius: Layout.badgeOffset)
}
}
}

private extension SelectableShipmentRow {
enum Layout {
static let horizontalSpacing: CGFloat = 16
static let imageSize: CGFloat = 56.0
static let imageCornerRadius: CGFloat = 4.0
static let badgeOffset: CGFloat = 8.0
}
}

#Preview {
SelectableShipmentRow(viewModel: SelectableShipmentRowViewModel(shipmentId: "123",
isSelectable: false,
item: WooShippingItemRowViewModel(imageUrl: nil,
quantityLabel: "3",
name: "Little Nap Brazil 250g",
detailsLabel: "15×10×8cm • Espresso",
weightLabel: "275g",
priceLabel: "$60.00")))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import SwiftUI
import WooFoundation
import Yosemite

/// View model for `SelectableShipmentRow`.
final class SelectableShipmentRowViewModel: ObservableObject, Identifiable {
let id = UUID()

let item: WooShippingItemRowViewModel

@Published private(set) var selected: Bool = false

let isSelectable: Bool

let shipmentId: String

let showQuantity: Bool

var onSelectedChange: ((SelectableShipmentRowViewModel) -> Void)?

init(shipmentId: String,
isSelectable: Bool,
item: WooShippingItemRowViewModel,
showQuantity: Bool = true) {
self.shipmentId = shipmentId
self.isSelectable = isSelectable
self.item = item
self.showQuantity = showQuantity
}

func handleTap() {
selected = !selected
if let onSelectedChange {
onSelectedChange(self)
}
}

func setSelected(_ selected: Bool) {
self.selected = selected
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import SwiftUI
import Yosemite

struct WooShippingSplitShipmentsDetailView: View {
@Environment(\.dismiss) private var dismiss
Expand All @@ -17,11 +18,9 @@ struct WooShippingSplitShipmentsDetailView: View {
.foregroundStyle(Color(.textSubtle))
}

VStack {
ForEach(viewModel.items) { item in
WooShippingItemRow(viewModel: item)
.padding()
.roundedBorder(cornerRadius: Layout.borderCornerRadius, lineColor: Color(.separator), lineWidth: Layout.borderWidth)
VStack(spacing: Layout.verticalSpacing) {
ForEach(viewModel.shipmentCardViewModels) { item in
CollapsibleShipmentCard(viewModel: item)
}
}
}
Expand All @@ -32,7 +31,7 @@ struct WooShippingSplitShipmentsDetailView: View {
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button(Localization.selectAll) {

viewModel.selectAll()
}
}
ToolbarItem(placement: .confirmationAction) {
Expand All @@ -50,6 +49,7 @@ private extension WooShippingSplitShipmentsDetailView {
static let contentPadding: CGFloat = 16
static let borderCornerRadius: CGFloat = 8
static let borderWidth: CGFloat = 0.5
static let verticalSpacing: CGFloat = 8
}
enum Localization {
static let title = NSLocalizedString(
Expand All @@ -73,6 +73,16 @@ private extension WooShippingSplitShipmentsDetailView {
#if DEBUG
#Preview {
WooShippingSplitShipmentsDetailView(viewModel: WooShippingSplitShipmentsViewModel(order: ShippingLabelSampleData.sampleOrder(),
config: ShippingLabelSampleData.sampleWooShippingConfig()))
config: ShippingLabelSampleData.sampleWooShippingConfig(),
items: [ShippingLabelPackageItem(productOrVariationID: 1,
name: "Shirt",
weight: 0.5,
quantity: 2,
value: 9.99,
dimensions: ProductDimensions(length: "",
width: "",
height: ""),
attributes: [],
imageURL: nil)]))
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,17 @@ private extension WooShippingSplitShipmentsRow {
#if DEBUG
#Preview {
WooShippingSplitShipmentsRow(viewModel: WooShippingSplitShipmentsViewModel(order: ShippingLabelSampleData.sampleOrder(),
config: ShippingLabelSampleData.sampleWooShippingConfig()))
config: ShippingLabelSampleData.sampleWooShippingConfig(),
items: [ShippingLabelPackageItem(productOrVariationID: 1,
name: "Shirt",
weight: 0.5,
quantity: 2,
value: 9.99,
dimensions: ProductDimensions(length: "",
width: "",
height: ""),
attributes: [],
imageURL: nil)]))
.padding()
}
#endif
Loading