-
Notifications
You must be signed in to change notification settings - Fork 121
[Shipping Labels] Split shipment initial UI #15397
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
Changes from all commits
f3d899b
b7869be
8c7a83d
7502bf9
e381c35
34ee101
44a0ec8
50919d7
c0d7cba
f3a3f7e
0cdc5a3
36a64c6
2c64d09
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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) | ||
itsmeichigo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| .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, | ||
itsmeichigo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this can be quickly fixed by adding |
||
| } | ||
| } | ||
| 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 | ||
| } | ||
| } |

Uh oh!
There was an error while loading. Please reload this page.