Skip to content

Commit 3aea5f1

Browse files
authored
Custom Fields: Update navigation behavior for list and editor screens. (#14175)
2 parents 7b58997 + 5b0bb45 commit 3aea5f1

File tree

5 files changed

+177
-90
lines changed

5 files changed

+177
-90
lines changed

WooCommerce/Classes/ViewModels/Order Details/OrderDetailsViewModel.swift

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -482,22 +482,19 @@ extension OrderDetailsViewModel {
482482
viewController.navigationController?.pushViewController(billingInformationViewController, animated: true)
483483
case .customFields:
484484
ServiceLocator.analytics.track(.orderViewCustomFieldsTapped)
485+
485486
let customFields = order.customFields.map {
486487
CustomFieldViewModel(metadata: $0)
487488
}
488-
let customFieldsView = UIHostingController(
489-
rootView: CustomFieldsListView(
490-
isEditable: featureFlagService.isFeatureFlagEnabled(.viewEditCustomFieldsInProductsAndOrders),
491-
viewModel: CustomFieldsListViewModel(customFields: customFields),
492-
onBackButtonTapped: {
493-
// Restore the hidden navigation bar
494-
viewController.navigationController?.setNavigationBarHidden(false, animated: false)
495-
})
496-
)
497489

498-
// Hide the navigation bar as `CustomFieldsListView` will create its own toolbar.
499-
viewController.navigationController?.setNavigationBarHidden(true, animated: false)
500-
viewController.navigationController?.pushViewController(customFieldsView, animated: true)
490+
let isEditable = featureFlagService.isFeatureFlagEnabled(.viewEditCustomFieldsInProductsAndOrders)
491+
let viewModel = CustomFieldsListViewModel(customFields: customFields)
492+
493+
let customFieldsListViewController = CustomFieldsListHostingController(isEditable: isEditable,
494+
viewModel: viewModel)
495+
496+
viewController.navigationController?.pushViewController(customFieldsListViewController, animated: true)
497+
501498
case .seeReceipt:
502499
let countryCode = configurationLoader.configuration.countryCode
503500
ServiceLocator.analytics.track(event: .InPersonPayments.receiptViewTapped(countryCode: countryCode, source: .backend))

WooCommerce/Classes/ViewRelated/Custom Fields/CustomFieldEditorView.swift

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import SwiftUI
22

33
struct CustomFieldEditorView: View {
4-
@Environment(\.presentationMode) var presentationMode
4+
@Environment(\.dismiss) private var dismiss
5+
56
@State private var key: String
67
@State private var value: String
78
@State private var showRichTextEditor = false
@@ -98,13 +99,20 @@ struct CustomFieldEditorView: View {
9899
}
99100
.background(Color(.listBackground))
100101
.toolbar {
102+
ToolbarItem(placement: .cancellationAction) {
103+
Button {
104+
dismiss()
105+
} label: {
106+
Text(Localization.cancelButton)
107+
}
108+
}
101109
ToolbarItem(placement: .navigationBarTrailing) {
102110
HStack {
103111
Button {
104112
saveChanges()
105-
presentationMode.wrappedValue.dismiss()
113+
dismiss()
106114
} label: {
107-
Text("Save") // todo-13493: set String to be translatable
115+
Text(Localization.saveButton)
108116
}
109117
.disabled(!hasUnsavedChanges)
110118

@@ -155,6 +163,18 @@ private extension CustomFieldEditorView {
155163
}
156164

157165
enum Localization {
166+
static let cancelButton = NSLocalizedString(
167+
"customFieldEditorView.cancel",
168+
value: "Cancel",
169+
comment: "Label for the Cancel button to close the editor"
170+
)
171+
172+
static let saveButton = NSLocalizedString(
173+
"customFieldEditorView.save",
174+
value: "Save",
175+
comment: "Label for the Save button to save changes"
176+
)
177+
158178
static let keyLabel = NSLocalizedString(
159179
"customFieldEditorView.keyLabel",
160180
value: "Key",

WooCommerce/Classes/ViewRelated/Custom Fields/CustomFieldsListView.swift

Lines changed: 128 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,103 @@
1+
import Combine
12
import SwiftUI
23

4+
final class CustomFieldsListHostingController: UIHostingController<CustomFieldsListView> {
5+
private let viewModel: CustomFieldsListViewModel
6+
private var subscriptions: Set<AnyCancellable> = []
7+
8+
init(isEditable: Bool, viewModel: CustomFieldsListViewModel) {
9+
self.viewModel = viewModel
10+
super.init(rootView: CustomFieldsListView(isEditable: isEditable,
11+
viewModel: viewModel)
12+
)
13+
}
14+
15+
override func viewDidLoad() {
16+
super.viewDidLoad()
17+
18+
configureNavigation()
19+
observeStateChange()
20+
}
21+
22+
/// Create a `UIBarButtonItem` to be used as the add custom field button on the top-right.
23+
///
24+
private lazy var addCustomFieldButtonItem: UIBarButtonItem = {
25+
let button = UIBarButtonItem(image: .plusImage,
26+
style: .plain,
27+
target: self,
28+
action: #selector(openAddCustomFieldScreen))
29+
button.accessibilityTraits = .button
30+
button.accessibilityLabel = Localization.accessibilityLabelAddCustomField
31+
button.accessibilityHint = Localization.accessibilityHintAddCustomField
32+
button.accessibilityIdentifier = "add-custom-field-button"
33+
34+
return button
35+
}()
36+
37+
/// Create a `UIBarButtonItem` to be used as the save custom field button on the top-right.
38+
///
39+
private lazy var saveCustomFieldButtonItem =
40+
UIBarButtonItem(title: Localization.save,
41+
style: .plain,
42+
target: self,
43+
action: #selector(saveCustomField))
44+
45+
required dynamic init?(coder aDecoder: NSCoder) {
46+
fatalError("init(coder:) has not been implemented")
47+
}
48+
}
49+
50+
private extension CustomFieldsListHostingController {
51+
func configureNavigation() {
52+
title = Localization.title
53+
navigationItem.rightBarButtonItems = [saveCustomFieldButtonItem, addCustomFieldButtonItem]
54+
}
55+
56+
@objc func openAddCustomFieldScreen() {
57+
viewModel.isAddingNewField = true
58+
}
59+
60+
@objc func saveCustomField() {
61+
// todo: call viewmodel's save function
62+
navigationController?.popViewController(animated: true)
63+
}
64+
65+
func observeStateChange() {
66+
viewModel.$hasChanges
67+
.sink { [weak self] hasChanges in
68+
self?.saveCustomFieldButtonItem.isEnabled = hasChanges
69+
}
70+
.store(in: &subscriptions)
71+
}
72+
}
73+
374
struct CustomFieldsListView: View {
475
@Environment(\.presentationMode) var presentationMode
576
@ObservedObject private var viewModel: CustomFieldsListViewModel
677

778
let isEditable: Bool
8-
let onBackButtonTapped: () -> Void
979

1080
init(isEditable: Bool,
11-
viewModel: CustomFieldsListViewModel,
12-
onBackButtonTapped: @escaping () -> Void) {
81+
viewModel: CustomFieldsListViewModel) {
1382
self.isEditable = isEditable
1483
self.viewModel = viewModel
15-
self.onBackButtonTapped = onBackButtonTapped
1684
}
1785

1886
var body: some View {
19-
NavigationStack {
20-
List(viewModel.combinedList) { customField in
21-
if isEditable {
22-
NavigationLink(destination: CustomFieldEditorView(key: customField.key, value: customField.value, onSave: { updatedKey, updatedValue in
23-
viewModel.saveField(key: updatedKey, value: updatedValue, fieldId: customField.fieldId)
24-
})) {
25-
CustomFieldRow(isEditable: true,
26-
title: customField.key,
27-
content: customField.value.removedHTMLTags,
28-
contentURL: nil)
29-
}
30-
} else {
31-
CustomFieldRow(isEditable: false,
32-
title: customField.key,
33-
content: customField.value.removedHTMLTags,
34-
contentURL: nil)
35-
}
36-
}
37-
.listStyle(.plain)
38-
.navigationTitle(Localization.title)
39-
.navigationBarTitleDisplayMode(.inline)
40-
.toolbar {
41-
ToolbarItem(placement: .cancellationAction) {
42-
Button(action: {
43-
onBackButtonTapped()
44-
presentationMode.wrappedValue.dismiss()
45-
}, label: {
46-
Image(systemName: "chevron.backward")
47-
.headlineLinkStyle()
48-
})
49-
}
50-
51-
ToolbarItem(placement: .navigationBarTrailing) {
52-
if isEditable {
53-
HStack {
54-
Button {
55-
// todo-13493: add save handling
56-
} label: {
57-
Text("Save") // todo-13493: set String to be translatable
58-
}
59-
.disabled(!viewModel.hasChanges)
60-
Button(action: {
61-
// todo-13493: add addition handling
62-
}, label: {
63-
Image(systemName: "plus")
64-
.renderingMode(.template)
65-
})
66-
}
67-
}
68-
}
87+
List(viewModel.combinedList) { customField in
88+
Button(action: { viewModel.selectedCustomField = customField }) {
89+
CustomFieldRow(isEditable: isEditable,
90+
title: customField.key,
91+
content: customField.value.removedHTMLTags,
92+
contentURL: nil)
6993
}
70-
.wooNavigationBarStyle()
94+
}
95+
.listStyle(.plain)
96+
.sheet(item: $viewModel.selectedCustomField) { customField in
97+
buildCustomFieldEditorView(customField: customField)
98+
}
99+
.sheet(isPresented: $viewModel.isAddingNewField) {
100+
buildCustomFieldEditorView(customField: nil)
71101
}
72102
}
73103
}
@@ -128,12 +158,52 @@ private struct CustomFieldRow: View {
128158
}
129159
}
130160

161+
// MARK: - Helpers
162+
//
163+
private extension CustomFieldsListView {
164+
/// Builds the Custom Field Editor View.
165+
/// - When `customField` is provided, it configures the editor for editing an existing field
166+
/// - When `customField` is nil, it configures the editor for creating a new field
167+
func buildCustomFieldEditorView(customField: CustomFieldsListViewModel.CustomFieldUI?) -> some View {
168+
NavigationView {
169+
CustomFieldEditorView(
170+
key: customField?.key ?? "",
171+
value: customField?.value ?? "",
172+
onSave: { updatedKey, updatedValue in
173+
viewModel.saveField(
174+
key: updatedKey,
175+
value: updatedValue,
176+
fieldId: customField?.fieldId
177+
)
178+
}
179+
)
180+
}
181+
}
182+
}
131183

132184
// MARK: - Constants
133185
//
134-
extension CustomFieldsListView {
186+
private extension CustomFieldsListHostingController {
135187
enum Localization {
136-
static let title = NSLocalizedString("Custom Fields", comment: "Title for the order custom fields list")
188+
static let title = NSLocalizedString(
189+
"customFieldsListHostingController.title",
190+
value: "Custom Fields",
191+
comment: "Title for the order custom fields list")
192+
193+
static let accessibilityLabelAddCustomField = NSLocalizedString(
194+
"customFieldsListHostingController.accessibilityLabelAddCustomField",
195+
value: "Add custom field",
196+
comment: "Accessibility label for the Add Custom Field button")
197+
198+
static let accessibilityHintAddCustomField = NSLocalizedString(
199+
"customFieldsListHostingController.accessibilityHintAddCustomField",
200+
value: "Add a new custom field to the list",
201+
comment: "VoiceOver accessibility hint, informing the user the button can be used to add custom field.")
202+
203+
static let save = NSLocalizedString(
204+
"customFieldsListHostingController.save",
205+
value: "Save",
206+
comment: "Button to save the changes on Custom Fields list")
137207
}
138208
}
139209

@@ -155,9 +225,7 @@ struct OrderCustomFieldsDetails_Previews: PreviewProvider {
155225
customFields: [
156226
CustomFieldViewModel(id: 0, title: "First Title", content: "First Content"),
157227
CustomFieldViewModel(id: 1, title: "Second Title", content: "Second Content", contentURL: URL(string: "https://woocommerce.com/"))
158-
]),
159-
onBackButtonTapped: { }
160-
)
228+
]))
161229
}
162230
}
163231

WooCommerce/Classes/ViewRelated/Custom Fields/CustomFieldsListViewModel.swift

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Combine
12
import Foundation
23

34
final class CustomFieldsListViewModel: ObservableObject {
@@ -7,18 +8,20 @@ final class CustomFieldsListViewModel: ObservableObject {
78
savingError != nil
89
}
910

11+
@Published var selectedCustomField: CustomFieldUI? = nil
12+
@Published var isAddingNewField: Bool = false
13+
1014
@Published private(set) var savingError: Error?
1115
@Published private(set) var combinedList: [CustomFieldUI] = []
1216

1317
@Published private var editedFields: [CustomFieldUI] = []
1418
@Published private var addedFields: [CustomFieldUI] = []
15-
var hasChanges: Bool {
16-
!editedFields.isEmpty || !addedFields.isEmpty
17-
}
19+
@Published private(set) var hasChanges: Bool = false
1820

1921
init(customFields: [CustomFieldViewModel]) {
2022
self.originalCustomFields = customFields
2123
updateCombinedList()
24+
configureHasChanges()
2225
}
2326
}
2427

@@ -97,6 +100,12 @@ private extension CustomFieldsListViewModel {
97100
}
98101
combinedList = editedList + addedFields
99102
}
103+
104+
func configureHasChanges() {
105+
$editedFields.combineLatest($addedFields)
106+
.map { !$0.isEmpty || !$1.isEmpty }
107+
.assign(to: &$hasChanges)
108+
}
100109
}
101110

102111
extension CustomFieldsListViewModel {

WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewController.swift

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1494,19 +1494,12 @@ private extension ProductFormViewController {
14941494
CustomFieldViewModel(metadata: $0)
14951495
}
14961496

1497-
let customFieldsView = UIHostingController(
1498-
rootView: CustomFieldsListView(
1499-
isEditable: true,
1500-
viewModel: CustomFieldsListViewModel(customFields: customFields),
1501-
onBackButtonTapped: { [weak self] in
1502-
// Restore the hidden navigation bar
1503-
self?.navigationController?.setNavigationBarHidden(false, animated: false)
1504-
})
1505-
)
1497+
let viewModel = CustomFieldsListViewModel(customFields: customFields)
1498+
1499+
let customFieldsListViewController = CustomFieldsListHostingController(isEditable: true,
1500+
viewModel: viewModel)
15061501

1507-
// Hide the navigation bar as `CustomFieldsListView` will create its own toolbar.
1508-
navigationController?.setNavigationBarHidden(true, animated: false)
1509-
navigationController?.pushViewController(customFieldsView, animated: true)
1502+
navigationController?.pushViewController(customFieldsListViewController, animated: true)
15101503
}
15111504
}
15121505

0 commit comments

Comments
 (0)