diff --git a/WooCommerce/Classes/ViewModels/Order Details/OrderDetailsViewModel.swift b/WooCommerce/Classes/ViewModels/Order Details/OrderDetailsViewModel.swift index 34d86561150..a926f8a8aae 100644 --- a/WooCommerce/Classes/ViewModels/Order Details/OrderDetailsViewModel.swift +++ b/WooCommerce/Classes/ViewModels/Order Details/OrderDetailsViewModel.swift @@ -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)) diff --git a/WooCommerce/Classes/ViewRelated/Custom Fields/CustomFieldEditorView.swift b/WooCommerce/Classes/ViewRelated/Custom Fields/CustomFieldEditorView.swift index 8f4b8245808..0370282695a 100644 --- a/WooCommerce/Classes/ViewRelated/Custom Fields/CustomFieldEditorView.swift +++ b/WooCommerce/Classes/ViewRelated/Custom Fields/CustomFieldEditorView.swift @@ -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 @@ -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) @@ -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", diff --git a/WooCommerce/Classes/ViewRelated/Custom Fields/CustomFieldsListView.swift b/WooCommerce/Classes/ViewRelated/Custom Fields/CustomFieldsListView.swift index 5d70860c6f5..5f5cd3fca64 100644 --- a/WooCommerce/Classes/ViewRelated/Custom Fields/CustomFieldsListView.swift +++ b/WooCommerce/Classes/ViewRelated/Custom Fields/CustomFieldsListView.swift @@ -1,73 +1,103 @@ +import Combine import SwiftUI +final class CustomFieldsListHostingController: UIHostingController { + private let viewModel: CustomFieldsListViewModel + private var subscriptions: Set = [] + + 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) } } } @@ -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") } } @@ -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: { } - ) + ])) } } diff --git a/WooCommerce/Classes/ViewRelated/Custom Fields/CustomFieldsListViewModel.swift b/WooCommerce/Classes/ViewRelated/Custom Fields/CustomFieldsListViewModel.swift index bb5f9b4083a..4dcdbc973ba 100644 --- a/WooCommerce/Classes/ViewRelated/Custom Fields/CustomFieldsListViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Custom Fields/CustomFieldsListViewModel.swift @@ -1,3 +1,4 @@ +import Combine import Foundation final class CustomFieldsListViewModel: ObservableObject { @@ -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() } } @@ -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 { diff --git a/WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewController.swift b/WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewController.swift index 77cca31a6d7..044914812b0 100644 --- a/WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewController.swift @@ -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) } }