Skip to content
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

Custom Fields: Local editing logic #14029

Merged
merged 10 commits into from
Sep 30, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,7 @@ extension OrderDetailsViewModel {
let customFieldsView = UIHostingController(
rootView: CustomFieldsListView(
isEditable: featureFlagService.isFeatureFlagEnabled(.viewEditCustomFieldsInProductsAndOrders),
customFields: customFields))
viewModel: CustomFieldsListViewModel(customFields: customFields)))
viewController.present(customFieldsView, animated: true)
case .seeReceipt:
let countryCode = configurationLoader.configuration.countryCode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,53 +2,62 @@ import SwiftUI

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

let isEditable: Bool
let customFields: [CustomFieldViewModel]

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

var body: some View {
NavigationView {
GeometryReader { geometry in
ScrollView {
VStack(alignment: .leading) {
ForEach(customFields) { customField in
if isEditable {
NavigationLink(destination: CustomFieldEditorView(key: customField.title,
value: customField.content)
) {
CustomFieldRow(isEditable: true,
title: customField.title,
content: customField.content.removedHTMLTags,
contentURL: customField.contentURL)
}
}
else {
CustomFieldRow(isEditable: false,
title: customField.title,
content: customField.content.removedHTMLTags,
contentURL: customField.contentURL)
}

Divider()
.padding(.leading)
}
List(viewModel.combinedList) { customField in
if isEditable {
NavigationLink(destination: CustomFieldEditorView(key: customField.key, value: customField.value)) {
CustomFieldRow(isEditable: true,
title: customField.key,
content: customField.value.removedHTMLTags,
contentURL: nil)
}
.padding(.horizontal, insets: geometry.safeAreaInsets)
.background(Color(.listForeground(modal: false)))
} else {
CustomFieldRow(isEditable: false,
title: customField.key,
content: customField.value.removedHTMLTags,
contentURL: nil)
}
.background(Color(.listBackground))
.ignoresSafeArea(edges: .horizontal)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Image(uiImage: .closeButton)
})
}
.navigationTitle(Localization.title)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Image(uiImage: .closeButton)
})
}

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)
})
}
}
}
.navigationTitle(Localization.title)
.navigationBarTitleDisplayMode(.inline)
}
}
.wooNavigationBarStyle()
Expand Down Expand Up @@ -105,15 +114,6 @@ private struct CustomFieldRow: View {
.footnoteStyle()
.lineLimit(isEditable ? 2 : nil)
}
}.padding([.leading, .trailing], Constants.vStackPadding)

Spacer()

if isEditable {
// Chevron icon
Image(uiImage: .chevronImage)
.flipsForRightToLeftLayoutDirection(true)
.foregroundStyle(Color(.textTertiary))
}
}
.padding(Constants.hStackPadding)
Expand Down Expand Up @@ -144,11 +144,13 @@ private extension CustomFieldRow {
struct OrderCustomFieldsDetails_Previews: PreviewProvider {
static var previews: some View {
CustomFieldsListView(
isEditable: false,
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/"))
])
isEditable: true,
viewModel: CustomFieldsListViewModel(
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/"))
])
)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import Foundation

final class CustomFieldsListViewModel: ObservableObject {
private let originalCustomFields: [CustomFieldViewModel]

var shouldShowErrorState: Bool {
savingError != nil
}

@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
}

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

// MARK: - Items actions
extension CustomFieldsListViewModel {
/// Params:
/// - index: The index of field to be edited, taken from the `combinedList` array
/// - newField: The new content for the custom field in question
func editField(at index: Int, newField: CustomFieldUI) {
guard index >= 0 && index < combinedList.count else {
DDLogError("⛔️ Error: Invalid index for editing a custom field")
return
}

let oldField = combinedList[index]
if newField.fieldId == nil {
// When editing a field that has no id yet, it means the field has only been added locally.
editLocallyAddedField(oldField: oldField, newField: newField)
} else {
if let existingId = oldField.fieldId {
editExistingField(idToEdit: existingId, newField: newField)
} else {
DDLogError("⛔️ Error: Trying to edit an existing field but it has no id. It might be the wrong field to edit.")
}
}

updateCombinedList()
}

func addField(_ field: CustomFieldUI) {
addedFields.append(field)
updateCombinedList()
}
}

private extension CustomFieldsListViewModel {
func editLocallyAddedField(oldField: CustomFieldUI, newField: CustomFieldUI) {
if let index = addedFields.firstIndex(where: { $0.key == oldField.key }) {
addedFields[index] = newField
} else {
// This shouldn't happen in normal flow, but logging just in case
DDLogError("⛔️ Error: Trying to edit a locally added field that doesn't exist in addedFields")
}
}

/// Checking by id when editing an existing field since existing fields will always have them.
func editExistingField(idToEdit: Int64, newField: CustomFieldUI) {
guard idToEdit == newField.fieldId else {
DDLogError("⛔️ Error: Trying to edit existing field but supplied new id is different.")
return
}

if let index = editedFields.firstIndex(where: { $0.fieldId == idToEdit }) {
// Existing field has been locally edited, let's update it again
editedFields[index] = newField
} else {
// First time the field is locally edited
editedFields.append(newField)
}
}

func updateCombinedList() {
let editedList = originalCustomFields.map { field in
editedFields.first { $0.fieldId == field.id } ?? CustomFieldUI(customField: field)
}
combinedList = editedList + addedFields
}
}

extension CustomFieldsListViewModel {
struct CustomFieldUI: Identifiable {
let id = UUID()
let key: String
let value: String
let fieldId: Int64?

init(key: String, value: String, fieldId: Int64? = nil) {
self.key = key
self.value = value
self.fieldId = fieldId
}

init(customField: CustomFieldViewModel) {
self.key = customField.title
self.value = customField.content
self.fieldId = customField.id
}
}
}
8 changes: 8 additions & 0 deletions WooCommerce/WooCommerce.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1712,6 +1712,8 @@
86DE68822B4BA47A00B437A6 /* BlazeAdDestinationSettingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86DE68812B4BA47900B437A6 /* BlazeAdDestinationSettingViewModel.swift */; };
86E40AED2B597DEC00990365 /* BlazeCampaignCreationCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86E40AEC2B597DEC00990365 /* BlazeCampaignCreationCoordinatorTests.swift */; };
86F0896F2B307D7E00D668A1 /* ThemesPreviewViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86F0896E2B307D7E00D668A1 /* ThemesPreviewViewModelTests.swift */; };
86F5FFE22CA302B300C767C4 /* CustomFieldsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86F5FFE12CA302B300C767C4 /* CustomFieldsListViewModel.swift */; };
86F5FFE42CA30D9200C767C4 /* CustomFieldsListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86F5FFE32CA30D9200C767C4 /* CustomFieldsListViewModelTests.swift */; };
86F9D3642C897FFE00B1835B /* CustomFieldEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86F9D3632C897FFE00B1835B /* CustomFieldEditorView.swift */; };
8CD41D4A21F8A7E300CF3C2B /* RELEASE-NOTES.txt in Resources */ = {isa = PBXBuildFile; fileRef = 8CD41D4921F8A7E300CF3C2B /* RELEASE-NOTES.txt */; };
933A27372222354600C2143A /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 933A27362222354600C2143A /* Logging.swift */; };
Expand Down Expand Up @@ -4736,6 +4738,8 @@
86DE68812B4BA47900B437A6 /* BlazeAdDestinationSettingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeAdDestinationSettingViewModel.swift; sourceTree = "<group>"; };
86E40AEC2B597DEC00990365 /* BlazeCampaignCreationCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignCreationCoordinatorTests.swift; sourceTree = "<group>"; };
86F0896E2B307D7E00D668A1 /* ThemesPreviewViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemesPreviewViewModelTests.swift; sourceTree = "<group>"; };
86F5FFE12CA302B300C767C4 /* CustomFieldsListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomFieldsListViewModel.swift; sourceTree = "<group>"; };
86F5FFE32CA30D9200C767C4 /* CustomFieldsListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomFieldsListViewModelTests.swift; sourceTree = "<group>"; };
86F9D3632C897FFE00B1835B /* CustomFieldEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomFieldEditorView.swift; sourceTree = "<group>"; };
8A659E65308A3D9DD79A95F9 /* Pods-WooCommerceTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WooCommerceTests.release.xcconfig"; path = "../Pods/Target Support Files/Pods-WooCommerceTests/Pods-WooCommerceTests.release.xcconfig"; sourceTree = "<group>"; };
8CA4F6DD220B257000A47B5D /* WooCommerce.debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = WooCommerce.debug.xcconfig; path = ../config/WooCommerce.debug.xcconfig; sourceTree = "<group>"; };
Expand Down Expand Up @@ -9769,6 +9773,7 @@
isa = PBXGroup;
children = (
CC5BA5F4287EDC900072F307 /* CustomFieldViewModelTests.swift */,
86F5FFE32CA30D9200C767C4 /* CustomFieldsListViewModelTests.swift */,
);
path = "Custom Fields";
sourceTree = "<group>";
Expand Down Expand Up @@ -10572,6 +10577,7 @@
B626C71A287659D60083820C /* CustomFieldsListView.swift */,
B6C838DD28793B3A003AB786 /* CustomFieldViewModel.swift */,
86F9D3632C897FFE00B1835B /* CustomFieldEditorView.swift */,
86F5FFE12CA302B300C767C4 /* CustomFieldsListViewModel.swift */,
);
path = "Custom Fields";
sourceTree = "<group>";
Expand Down Expand Up @@ -16095,6 +16101,7 @@
B99B87A72AEFCF0A006B8AB1 /* Order+Empty.swift in Sources */,
CE6A8FB62B725A690063564D /* AnalyticsReportLinkViewModel.swift in Sources */,
684AB83C2873DF04003DFDD1 /* CardReaderManualsViewModel.swift in Sources */,
86F5FFE22CA302B300C767C4 /* CustomFieldsListViewModel.swift in Sources */,
575472812452185300A94C3C /* PushNotification.swift in Sources */,
0396CFAD2981476900E91436 /* CardPresentModalBuiltInConnectingFailed.swift in Sources */,
02C1853B27FF0D9C00ABD764 /* RefundSubmissionUseCase.swift in Sources */,
Expand Down Expand Up @@ -16417,6 +16424,7 @@
4552085B25829091001CF873 /* AddAttributeViewModelTests.swift in Sources */,
02F1E6BD2A39805C00C3E4C7 /* ProductDescriptionAICoordinatorTests.swift in Sources */,
CC33238C29CDF67D00CA9709 /* ComponentSettingsViewModelTests.swift in Sources */,
86F5FFE42CA30D9200C767C4 /* CustomFieldsListViewModelTests.swift in Sources */,
0261F5A728D454CF00B7AC72 /* ProductSearchUICommandTests.swift in Sources */,
098FFA1727AD7F5D002EBEE4 /* OrderStatusListDataSourceTests.swift in Sources */,
DE19BB1D26C6911900AB70D9 /* ShippingLabelCustomsFormListViewModelTests.swift in Sources */,
Expand Down
Loading