Skip to content

Commit

Permalink
Custom Fields: Local editing logic (#14029)
Browse files Browse the repository at this point in the history
  • Loading branch information
hafizrahman authored Sep 30, 2024
2 parents 39c6935 + f280d61 commit 1168651
Show file tree
Hide file tree
Showing 5 changed files with 299 additions and 53 deletions.
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 @@ -1714,6 +1714,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 @@ -4747,6 +4749,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 @@ -9792,6 +9796,7 @@
isa = PBXGroup;
children = (
CC5BA5F4287EDC900072F307 /* CustomFieldViewModelTests.swift */,
86F5FFE32CA30D9200C767C4 /* CustomFieldsListViewModelTests.swift */,
);
path = "Custom Fields";
sourceTree = "<group>";
Expand Down Expand Up @@ -10596,6 +10601,7 @@
B626C71A287659D60083820C /* CustomFieldsListView.swift */,
B6C838DD28793B3A003AB786 /* CustomFieldViewModel.swift */,
86F9D3632C897FFE00B1835B /* CustomFieldEditorView.swift */,
86F5FFE12CA302B300C767C4 /* CustomFieldsListViewModel.swift */,
);
path = "Custom Fields";
sourceTree = "<group>";
Expand Down Expand Up @@ -16152,6 +16158,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 @@ -16475,6 +16482,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

0 comments on commit 1168651

Please sign in to comment.