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 @@ -482,22 +482,19 @@ extension OrderDetailsViewModel {
viewController.navigationController?.pushViewController(billingInformationViewController, animated: true)
case .customFields:
ServiceLocator.analytics.track(.orderViewCustomFieldsTapped)

let customFields = order.customFields.map {
CustomFieldViewModel(metadata: $0)
}
let customFieldsView = UIHostingController(
rootView: CustomFieldsListView(
isEditable: featureFlagService.isFeatureFlagEnabled(.viewEditCustomFieldsInProductsAndOrders),
viewModel: CustomFieldsListViewModel(customFields: customFields),
onBackButtonTapped: {
// Restore the hidden navigation bar
viewController.navigationController?.setNavigationBarHidden(false, animated: false)
})
)

// Hide the navigation bar as `CustomFieldsListView` will create its own toolbar.
viewController.navigationController?.setNavigationBarHidden(true, animated: false)
viewController.navigationController?.pushViewController(customFieldsView, animated: true)
let isEditable = featureFlagService.isFeatureFlagEnabled(.viewEditCustomFieldsInProductsAndOrders)
let viewModel = CustomFieldsListViewModel(customFields: customFields)

let customFieldsListViewController = CustomFieldsListHostingController(isEditable: isEditable,
viewModel: viewModel)

viewController.navigationController?.pushViewController(customFieldsListViewController, animated: true)

case .seeReceipt:
let countryCode = configurationLoader.configuration.countryCode
ServiceLocator.analytics.track(event: .InPersonPayments.receiptViewTapped(countryCode: countryCode, source: .backend))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import SwiftUI

struct CustomFieldEditorView: View {
@Environment(\.presentationMode) var presentationMode
@Environment(\.dismiss) private var dismiss

@State private var key: String
@State private var value: String
@State private var showRichTextEditor = false
Expand Down Expand Up @@ -98,13 +99,20 @@ struct CustomFieldEditorView: View {
}
.background(Color(.listBackground))
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button {
dismiss()
} label: {
Text(Localization.cancelButton)
}
}
ToolbarItem(placement: .navigationBarTrailing) {
HStack {
Button {
saveChanges()
presentationMode.wrappedValue.dismiss()
dismiss()
} label: {
Text("Save") // todo-13493: set String to be translatable
Text(Localization.saveButton)
}
.disabled(!hasUnsavedChanges)

Expand Down Expand Up @@ -155,6 +163,18 @@ private extension CustomFieldEditorView {
}

enum Localization {
static let cancelButton = NSLocalizedString(
"customFieldEditorView.cancel",
value: "Cancel",
comment: "Label for the Cancel button to close the editor"
)

static let saveButton = NSLocalizedString(
"customFieldEditorView.save",
value: "Save",
comment: "Label for the Save button to save changes"
)

static let keyLabel = NSLocalizedString(
"customFieldEditorView.keyLabel",
value: "Key",
Expand Down
188 changes: 128 additions & 60 deletions WooCommerce/Classes/ViewRelated/Custom Fields/CustomFieldsListView.swift
Original file line number Diff line number Diff line change
@@ -1,73 +1,103 @@
import Combine
import SwiftUI

final class CustomFieldsListHostingController: UIHostingController<CustomFieldsListView> {
private let viewModel: CustomFieldsListViewModel
private var subscriptions: Set<AnyCancellable> = []

init(isEditable: Bool, viewModel: CustomFieldsListViewModel) {
self.viewModel = viewModel
super.init(rootView: CustomFieldsListView(isEditable: isEditable,
viewModel: viewModel)
)
}

override func viewDidLoad() {
super.viewDidLoad()

configureNavigation()
observeStateChange()
}

/// Create a `UIBarButtonItem` to be used as the add custom field button on the top-right.
///
private lazy var addCustomFieldButtonItem: UIBarButtonItem = {
let button = UIBarButtonItem(image: .plusImage,
style: .plain,
target: self,
action: #selector(openAddCustomFieldScreen))
button.accessibilityTraits = .button
button.accessibilityLabel = Localization.accessibilityLabelAddCustomField
button.accessibilityHint = Localization.accessibilityHintAddCustomField
button.accessibilityIdentifier = "add-custom-field-button"

return button
}()

/// Create a `UIBarButtonItem` to be used as the save custom field button on the top-right.
///
private lazy var saveCustomFieldButtonItem =
UIBarButtonItem(title: Localization.save,
style: .plain,
target: self,
action: #selector(saveCustomField))

required dynamic init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

private extension CustomFieldsListHostingController {
func configureNavigation() {
title = Localization.title
navigationItem.rightBarButtonItems = [saveCustomFieldButtonItem, addCustomFieldButtonItem]
}

@objc func openAddCustomFieldScreen() {
viewModel.isAddingNewField = true
}

@objc func saveCustomField() {
// todo: call viewmodel's save function
navigationController?.popViewController(animated: true)
}

func observeStateChange() {
viewModel.$hasChanges
.sink { [weak self] hasChanges in
self?.saveCustomFieldButtonItem.isEnabled = hasChanges
}
.store(in: &subscriptions)
}
}

struct CustomFieldsListView: View {
@Environment(\.presentationMode) var presentationMode
@ObservedObject private var viewModel: CustomFieldsListViewModel

let isEditable: Bool
let onBackButtonTapped: () -> Void

init(isEditable: Bool,
viewModel: CustomFieldsListViewModel,
onBackButtonTapped: @escaping () -> Void) {
viewModel: CustomFieldsListViewModel) {
self.isEditable = isEditable
self.viewModel = viewModel
self.onBackButtonTapped = onBackButtonTapped
}

var body: some View {
NavigationStack {
List(viewModel.combinedList) { customField in
if isEditable {
NavigationLink(destination: CustomFieldEditorView(key: customField.key, value: customField.value, onSave: { updatedKey, updatedValue in
viewModel.saveField(key: updatedKey, value: updatedValue, fieldId: customField.fieldId)
})) {
CustomFieldRow(isEditable: true,
title: customField.key,
content: customField.value.removedHTMLTags,
contentURL: nil)
}
} else {
CustomFieldRow(isEditable: false,
title: customField.key,
content: customField.value.removedHTMLTags,
contentURL: nil)
}
}
.listStyle(.plain)
.navigationTitle(Localization.title)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button(action: {
onBackButtonTapped()
presentationMode.wrappedValue.dismiss()
}, label: {
Image(systemName: "chevron.backward")
.headlineLinkStyle()
})
}

ToolbarItem(placement: .navigationBarTrailing) {
if isEditable {
HStack {
Button {
// todo-13493: add save handling
} label: {
Text("Save") // todo-13493: set String to be translatable
}
.disabled(!viewModel.hasChanges)
Button(action: {
// todo-13493: add addition handling
}, label: {
Image(systemName: "plus")
.renderingMode(.template)
})
}
}
}
List(viewModel.combinedList) { customField in
Button(action: { viewModel.selectedCustomField = customField }) {
CustomFieldRow(isEditable: isEditable,
title: customField.key,
content: customField.value.removedHTMLTags,
contentURL: nil)
}
.wooNavigationBarStyle()
}
.listStyle(.plain)
.sheet(item: $viewModel.selectedCustomField) { customField in
buildCustomFieldEditorView(customField: customField)
}
.sheet(isPresented: $viewModel.isAddingNewField) {
buildCustomFieldEditorView(customField: nil)
}
}
}
Expand Down Expand Up @@ -128,12 +158,52 @@ private struct CustomFieldRow: View {
}
}

// MARK: - Helpers
//
private extension CustomFieldsListView {
/// Builds the Custom Field Editor View.
/// - When `customField` is provided, it configures the editor for editing an existing field
/// - When `customField` is nil, it configures the editor for creating a new field
func buildCustomFieldEditorView(customField: CustomFieldsListViewModel.CustomFieldUI?) -> some View {
NavigationView {
CustomFieldEditorView(
key: customField?.key ?? "",
value: customField?.value ?? "",
onSave: { updatedKey, updatedValue in
viewModel.saveField(
key: updatedKey,
value: updatedValue,
fieldId: customField?.fieldId
)
}
)
}
}
}

// MARK: - Constants
//
extension CustomFieldsListView {
private extension CustomFieldsListHostingController {
enum Localization {
static let title = NSLocalizedString("Custom Fields", comment: "Title for the order custom fields list")
static let title = NSLocalizedString(
"customFieldsListHostingController.title",
value: "Custom Fields",
comment: "Title for the order custom fields list")

static let accessibilityLabelAddCustomField = NSLocalizedString(
"customFieldsListHostingController.accessibilityLabelAddCustomField",
value: "Add custom field",
comment: "Accessibility label for the Add Custom Field button")

static let accessibilityHintAddCustomField = NSLocalizedString(
"customFieldsListHostingController.accessibilityHintAddCustomField",
value: "Add a new custom field to the list",
comment: "VoiceOver accessibility hint, informing the user the button can be used to add custom field.")

static let save = NSLocalizedString(
"customFieldsListHostingController.save",
value: "Save",
comment: "Button to save the changes on Custom Fields list")
}
}

Expand All @@ -155,9 +225,7 @@ struct OrderCustomFieldsDetails_Previews: PreviewProvider {
customFields: [
CustomFieldViewModel(id: 0, title: "First Title", content: "First Content"),
CustomFieldViewModel(id: 1, title: "Second Title", content: "Second Content", contentURL: URL(string: "https://woocommerce.com/"))
]),
onBackButtonTapped: { }
)
]))
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Combine
import Foundation

final class CustomFieldsListViewModel: ObservableObject {
Expand All @@ -7,18 +8,20 @@ final class CustomFieldsListViewModel: ObservableObject {
savingError != nil
}

@Published var selectedCustomField: CustomFieldUI? = nil
@Published var isAddingNewField: Bool = false

@Published private(set) var savingError: Error?
@Published private(set) var combinedList: [CustomFieldUI] = []

@Published private var editedFields: [CustomFieldUI] = []
@Published private var addedFields: [CustomFieldUI] = []
var hasChanges: Bool {
!editedFields.isEmpty || !addedFields.isEmpty
}
@Published private(set) var hasChanges: Bool = false

init(customFields: [CustomFieldViewModel]) {
self.originalCustomFields = customFields
updateCombinedList()
configureHasChanges()
}
}

Expand Down Expand Up @@ -97,6 +100,12 @@ private extension CustomFieldsListViewModel {
}
combinedList = editedList + addedFields
}

func configureHasChanges() {
$editedFields.combineLatest($addedFields)
.map { !$0.isEmpty || !$1.isEmpty }
.assign(to: &$hasChanges)
}
}

extension CustomFieldsListViewModel {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1494,19 +1494,12 @@ private extension ProductFormViewController {
CustomFieldViewModel(metadata: $0)
}

let customFieldsView = UIHostingController(
rootView: CustomFieldsListView(
isEditable: true,
viewModel: CustomFieldsListViewModel(customFields: customFields),
onBackButtonTapped: { [weak self] in
// Restore the hidden navigation bar
self?.navigationController?.setNavigationBarHidden(false, animated: false)
})
)
let viewModel = CustomFieldsListViewModel(customFields: customFields)

let customFieldsListViewController = CustomFieldsListHostingController(isEditable: true,
viewModel: viewModel)

// Hide the navigation bar as `CustomFieldsListView` will create its own toolbar.
navigationController?.setNavigationBarHidden(true, animated: false)
navigationController?.pushViewController(customFieldsView, animated: true)
navigationController?.pushViewController(customFieldsListViewController, animated: true)
}
}

Expand Down