From 15c857711f444f814dcb765f43d1ce93034b2f12 Mon Sep 17 00:00:00 2001 From: Adam Borbas Date: Thu, 6 Nov 2025 14:20:18 +0100 Subject: [PATCH 01/10] Add note attribute to Booking entity --- .../Sources/Fakes/Networking.generated.swift | 3 +- .../Networking/Model/Bookings/Booking.swift | 10 +- .../Copiable/Models+Copiable.generated.swift | 7 +- .../Booking/Booking+CoreDataProperties.swift | 1 + Modules/Sources/Storage/Model/MIGRATIONS.md | 4 + .../.xccurrentversion | 2 +- .../Model 129.xcdatamodel/contents | 2 +- .../Model 130.xcdatamodel/contents | 1177 +++++++++++++++++ .../Booking/Booking+ReadOnlyConvertible.swift | 4 +- .../CoreData/MigrationTests.swift | 21 + .../Booking Details/BookingDetailsView.swift | 3 +- 11 files changed, 1225 insertions(+), 9 deletions(-) create mode 100644 Modules/Sources/Storage/Resources/WooCommerce.xcdatamodeld/Model 130.xcdatamodel/contents diff --git a/Modules/Sources/Fakes/Networking.generated.swift b/Modules/Sources/Fakes/Networking.generated.swift index 45f3a9b179b..b67a0f1102d 100644 --- a/Modules/Sources/Fakes/Networking.generated.swift +++ b/Modules/Sources/Fakes/Networking.generated.swift @@ -343,7 +343,8 @@ extension Networking.Booking { attendanceStatusKey: .fake(), localTimezone: .fake(), currency: .fake(), - orderInfo: .fake() + orderInfo: .fake(), + note: .fake() ) } } diff --git a/Modules/Sources/Networking/Model/Bookings/Booking.swift b/Modules/Sources/Networking/Model/Bookings/Booking.swift index c53bc714f8c..e7f647cd6a8 100644 --- a/Modules/Sources/Networking/Model/Bookings/Booking.swift +++ b/Modules/Sources/Networking/Model/Bookings/Booking.swift @@ -24,6 +24,7 @@ public struct Booking: Codable, GeneratedCopiable, Hashable, GeneratedFakeable { public let localTimezone: String public let currency: String public let orderInfo: BookingOrderInfo? + public let note: String public var bookingStatus: BookingStatus { return BookingStatus(rawValue: statusKey) ?? .unknown @@ -54,7 +55,8 @@ public struct Booking: Codable, GeneratedCopiable, Hashable, GeneratedFakeable { attendanceStatusKey: String, localTimezone: String, currency: String, - orderInfo: BookingOrderInfo?) { + orderInfo: BookingOrderInfo?, + note: String) { self.siteID = siteID self.bookingID = bookingID self.allDay = allDay @@ -75,6 +77,7 @@ public struct Booking: Codable, GeneratedCopiable, Hashable, GeneratedFakeable { self.localTimezone = localTimezone self.currency = currency self.orderInfo = orderInfo + self.note = note } /// The public initializer for Booking. @@ -129,6 +132,7 @@ public struct Booking: Codable, GeneratedCopiable, Hashable, GeneratedFakeable { let localTimezone = try container.decode(String.self, forKey: .localTimezone) let currency = try container.decode(String.self, forKey: .currency) let orderInfo: BookingOrderInfo? = nil // to be prefilled when synced + let note = try container.decode(String.self, forKey: .note) self.init(siteID: siteID, bookingID: bookingID, @@ -149,7 +153,8 @@ public struct Booking: Codable, GeneratedCopiable, Hashable, GeneratedFakeable { attendanceStatusKey: attendanceStatusKey, localTimezone: localTimezone, currency: currency, - orderInfo: orderInfo) + orderInfo: orderInfo, + note: note) } public func encode(to encoder: Encoder) throws { @@ -203,6 +208,7 @@ private extension Booking { case attendanceStatusKey = "attendance_status" case localTimezone = "local_timezone" case currency + case note } } diff --git a/Modules/Sources/Networking/Model/Copiable/Models+Copiable.generated.swift b/Modules/Sources/Networking/Model/Copiable/Models+Copiable.generated.swift index c20e19dc571..b02490bf551 100644 --- a/Modules/Sources/Networking/Model/Copiable/Models+Copiable.generated.swift +++ b/Modules/Sources/Networking/Model/Copiable/Models+Copiable.generated.swift @@ -450,7 +450,8 @@ extension Networking.Booking { attendanceStatusKey: CopiableProp = .copy, localTimezone: CopiableProp = .copy, currency: CopiableProp = .copy, - orderInfo: NullableCopiableProp = .copy + orderInfo: NullableCopiableProp = .copy, + note: CopiableProp = .copy ) -> Networking.Booking { let siteID = siteID ?? self.siteID let bookingID = bookingID ?? self.bookingID @@ -472,6 +473,7 @@ extension Networking.Booking { let localTimezone = localTimezone ?? self.localTimezone let currency = currency ?? self.currency let orderInfo = orderInfo ?? self.orderInfo + let note = note ?? self.note return Networking.Booking( siteID: siteID, @@ -493,7 +495,8 @@ extension Networking.Booking { attendanceStatusKey: attendanceStatusKey, localTimezone: localTimezone, currency: currency, - orderInfo: orderInfo + orderInfo: orderInfo, + note: note ) } } diff --git a/Modules/Sources/Storage/Model/Booking/Booking+CoreDataProperties.swift b/Modules/Sources/Storage/Model/Booking/Booking+CoreDataProperties.swift index 23e8e8eb9b7..5aa9cbe9aff 100644 --- a/Modules/Sources/Storage/Model/Booking/Booking+CoreDataProperties.swift +++ b/Modules/Sources/Storage/Model/Booking/Booking+CoreDataProperties.swift @@ -23,4 +23,5 @@ extension Booking { @NSManaged public var localTimezone: String? @NSManaged public var currency: String? @NSManaged public var orderInfo: BookingOrderInfo? + @NSManaged public var note: String? } diff --git a/Modules/Sources/Storage/Model/MIGRATIONS.md b/Modules/Sources/Storage/Model/MIGRATIONS.md index dd85daf3e41..798fbabb19e 100644 --- a/Modules/Sources/Storage/Model/MIGRATIONS.md +++ b/Modules/Sources/Storage/Model/MIGRATIONS.md @@ -2,6 +2,10 @@ This file documents changes in the WCiOS Storage data model. Please explain any changes to the data model as well as any custom migrations. +## Model 130 (Release 23.7) +- @adborbas 2025-11-06 + - Added `note` attribute to `Booking` entity. + ## Model 129 (Release X.X.X.X) - @rafaelkayumov 2025-10-17 - Added `attendanceStatusKey` attribute to `Booking` entity. diff --git a/Modules/Sources/Storage/Resources/WooCommerce.xcdatamodeld/.xccurrentversion b/Modules/Sources/Storage/Resources/WooCommerce.xcdatamodeld/.xccurrentversion index 051471ed320..f6128c44773 100644 --- a/Modules/Sources/Storage/Resources/WooCommerce.xcdatamodeld/.xccurrentversion +++ b/Modules/Sources/Storage/Resources/WooCommerce.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - Model 129.xcdatamodel + Model 130.xcdatamodel diff --git a/Modules/Sources/Storage/Resources/WooCommerce.xcdatamodeld/Model 129.xcdatamodel/contents b/Modules/Sources/Storage/Resources/WooCommerce.xcdatamodeld/Model 129.xcdatamodel/contents index d030970308a..30557bf6665 100644 --- a/Modules/Sources/Storage/Resources/WooCommerce.xcdatamodeld/Model 129.xcdatamodel/contents +++ b/Modules/Sources/Storage/Resources/WooCommerce.xcdatamodeld/Model 129.xcdatamodel/contents @@ -1,5 +1,5 @@ - + diff --git a/Modules/Sources/Storage/Resources/WooCommerce.xcdatamodeld/Model 130.xcdatamodel/contents b/Modules/Sources/Storage/Resources/WooCommerce.xcdatamodeld/Model 130.xcdatamodel/contents new file mode 100644 index 00000000000..a242f13b827 --- /dev/null +++ b/Modules/Sources/Storage/Resources/WooCommerce.xcdatamodeld/Model 130.xcdatamodel/contents @@ -0,0 +1,1177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Modules/Sources/Yosemite/Model/Booking/Booking+ReadOnlyConvertible.swift b/Modules/Sources/Yosemite/Model/Booking/Booking+ReadOnlyConvertible.swift index db92ed7c8de..a2b926fde39 100644 --- a/Modules/Sources/Yosemite/Model/Booking/Booking+ReadOnlyConvertible.swift +++ b/Modules/Sources/Yosemite/Model/Booking/Booking+ReadOnlyConvertible.swift @@ -32,6 +32,7 @@ extension Storage.Booking: ReadOnlyConvertible { attendanceStatusKey = booking.attendanceStatusKey localTimezone = booking.localTimezone currency = booking.currency + note = booking.note } /// Returns a ReadOnly version of the receiver. @@ -56,7 +57,8 @@ extension Storage.Booking: ReadOnlyConvertible { attendanceStatusKey: attendanceStatusKey ?? "", localTimezone: localTimezone ?? "", currency: currency ?? "USD", - orderInfo: orderInfo?.toReadOnly()) + orderInfo: orderInfo?.toReadOnly(), + note: note ?? "") } } diff --git a/Modules/Tests/StorageTests/CoreData/MigrationTests.swift b/Modules/Tests/StorageTests/CoreData/MigrationTests.swift index a634d620938..2ab77b12df3 100644 --- a/Modules/Tests/StorageTests/CoreData/MigrationTests.swift +++ b/Modules/Tests/StorageTests/CoreData/MigrationTests.swift @@ -2312,6 +2312,27 @@ final class MigrationTests: XCTestCase { let updatedValue = migratedBooking.value(forKey: "attendanceStatusKey") as? String XCTAssertEqual(updatedValue, "checked_in") } + + func test_migrating_from_129_to_130_adds_new_note_attribute_to_booking() throws { + // Given + let sourceContainer = try startPersistentContainer("Model 129") + let sourceContext = sourceContainer.viewContext + + let booking = insertBooking(to: sourceContext) + try sourceContext.save() + + XCTAssertNil(booking.entity.attributesByName["note"], "Precondition. Attribute does not exist.") + + // When + let targetContainer = try migrate(sourceContainer, to: "Model 130") + + // Then + let targetContext = targetContainer.viewContext + let migratedBooking = try XCTUnwrap(targetContext.first(entityName: "Booking")) + + // `note` should be present in `migratedBooking` + XCTAssertNotNil(migratedBooking.entity.attributesByName["note"]) + } } // MARK: - Persistent Store Setup and Migrations diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index 6191a146fda..3f61c971132 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -337,7 +337,8 @@ struct BookingDetailsView_Previews: PreviewProvider { attendanceStatusKey: "booked", localTimezone: "America/New_York", currency: "USD", - orderInfo: nil + orderInfo: nil, + note: "" ) let viewModel = BookingDetailsViewModel(booking: sampleBooking) return BookingDetailsView(viewModel) From 707fdb811306898171fed9f63635eadbd8644a3a Mon Sep 17 00:00:00 2001 From: Adam Borbas Date: Fri, 7 Nov 2025 09:58:43 +0100 Subject: [PATCH 02/10] initial showing note on the UI --- .../Classes/Bookings/BookingsTabView.swift | 4 +- .../BookingDetailsViewModel.swift | 8 ++-- .../Booking Details/BookingDetailsView.swift | 41 ++++++++++++------- 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/WooCommerce/Classes/Bookings/BookingsTabView.swift b/WooCommerce/Classes/Bookings/BookingsTabView.swift index f9a8101fb1c..0fe671944a5 100644 --- a/WooCommerce/Classes/Bookings/BookingsTabView.swift +++ b/WooCommerce/Classes/Bookings/BookingsTabView.swift @@ -50,7 +50,9 @@ struct BookingsTabView: View { } detail: { if let selectedBooking { let viewModel = BookingDetailsViewModel(booking: selectedBooking) - BookingDetailsView(viewModel) + NavigationStack { + BookingDetailsView(viewModel) + } } else { Text("Select a booking to see details.") } diff --git a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift index e26793b0ea5..2ebb6236a2c 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift @@ -33,6 +33,8 @@ final class BookingDetailsViewModel: ObservableObject { booking.attendanceStatus } + var note: String { booking.note } + init(booking: Booking, stores: StoresManager = ServiceLocator.stores, storage: StorageManagerType = ServiceLocator.storageManager) { @@ -379,9 +381,9 @@ private extension BookingDetailsViewModel { ) static let bookingNotesSectionHeaderTitle = NSLocalizedString( - "BookingDetailsView.bookingNotes.headerTitle", - value: "Booking notes", - comment: "Header title for the 'Booking notes' section in the booking details screen." + "BookingDetailsView.bookingNote.headerTitle", + value: "Booking note", + comment: "Header title for the 'Booking note' section in the booking details screen." ) static let cancelBookingAlertMessage = NSLocalizedString( diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index 3f61c971132..e00226eac38 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -239,17 +239,29 @@ private extension BookingDetailsView { } func bookingNotesView() -> some View { - HStack(spacing: Layout.contentSidePadding) { - Image(systemName: "plus") - .font(.title3.weight(.medium)) - Text(Localization.bookingNotesRowText) - .rowTextStyle() - Spacer() - } - .foregroundStyle(Color.accentColor) - .padding(.vertical, Layout.rowTextVerticalPadding) - .tappable { - print("On Add a note tap") + NavigationLink { + // TODO: push booking notes editor + Text(viewModel.note) + } label: { + HStack(alignment: .center, spacing: Layout.contentSidePadding) { + if viewModel.note.isEmpty { + Image(systemName: "plus") + .font(.title3.weight(.medium)) + + Text(Localization.bookingNotesRowText) + .rowTextStyle() + .foregroundColor(.accentColor) + } else { + Text(viewModel.note) + .rowTextStyle() + .multilineTextAlignment(.leading) + } + + Spacer() + + DisclosureIndicator() + } + .padding(.vertical, Layout.rowTextVerticalPadding) } } } @@ -305,9 +317,9 @@ extension BookingDetailsView { /// Booking notes static let bookingNotesRowText = NSLocalizedString( - "BookingDetailsView.bookingNotes.addANoteRow.title", + "BookingDetailsView.bookingNote.addANoteRow.title", value: "Add a note", - comment: "Add a note row title in booking notes section in booking details view." + comment: "Add a booking note section in booking details view." ) } } @@ -338,7 +350,8 @@ struct BookingDetailsView_Previews: PreviewProvider { localTimezone: "America/New_York", currency: "USD", orderInfo: nil, - note: "" +// note: "" + note: "note note 123 note note note note note note note note note note note note note n312 ote note note note note note note note 123 note note note note note note note note note note note note note note note note note note note note note note note note note note " ) let viewModel = BookingDetailsViewModel(booking: sampleBooking) return BookingDetailsView(viewModel) From 7adbc4048c1c79c58f9a816929a21108ad92aba5 Mon Sep 17 00:00:00 2001 From: Adam Borbas Date: Mon, 10 Nov 2025 13:53:32 +0100 Subject: [PATCH 03/10] Added MultilineEditableTextRow --- .../BookingDetailsViewModel.swift | 1 + .../Booking Details/BookingDetailsView.swift | 37 ++----- .../MultilineEditableTextDetailView.swift | 102 ++++++++++++++++++ .../MultilineEditableTextRow.swift | 55 ++++++++++ .../WooCommerce.xcodeproj/project.pbxproj | 3 + 5 files changed, 172 insertions(+), 26 deletions(-) create mode 100644 WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/MultilineEditableTextRow/MultilineEditableTextDetailView.swift create mode 100644 WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/MultilineEditableTextRow/MultilineEditableTextRow.swift diff --git a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift index 2ebb6236a2c..bc9495ad155 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift @@ -72,6 +72,7 @@ private extension BookingDetailsViewModel { let bookingNotes = Section( header: .title(Localization.bookingNotesSectionHeaderTitle.uppercased()), + footerText: "This is a private note. It'll not be shared with the customer.", content: .bookingNotes ) diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index e00226eac38..1798b6ffee8 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -239,30 +239,9 @@ private extension BookingDetailsView { } func bookingNotesView() -> some View { - NavigationLink { - // TODO: push booking notes editor - Text(viewModel.note) - } label: { - HStack(alignment: .center, spacing: Layout.contentSidePadding) { - if viewModel.note.isEmpty { - Image(systemName: "plus") - .font(.title3.weight(.medium)) - - Text(Localization.bookingNotesRowText) - .rowTextStyle() - .foregroundColor(.accentColor) - } else { - Text(viewModel.note) - .rowTextStyle() - .multilineTextAlignment(.leading) - } - - Spacer() - - DisclosureIndicator() - } - .padding(.vertical, Layout.rowTextVerticalPadding) - } + MultilineEditableTextRow(value: viewModel.note, + placeholder: Localization.bookingNotesRowText, + detailTitle: Localization.bookingNoteNavbarText) } } @@ -321,6 +300,12 @@ extension BookingDetailsView { value: "Add a note", comment: "Add a booking note section in booking details view." ) + + static let bookingNoteNavbarText = NSLocalizedString( + "BookingDetailsView.bookingNote.navbar.title", + value: "Booking note", + comment: "Title of navigation bar when editing a booking note." + ) } } @@ -350,8 +335,8 @@ struct BookingDetailsView_Previews: PreviewProvider { localTimezone: "America/New_York", currency: "USD", orderInfo: nil, -// note: "" - note: "note note 123 note note note note note note note note note note note note note n312 ote note note note note note note note 123 note note note note note note note note note note note note note note note note note note note note note note note note note note " + note: "" +// note: "note note 123 note note note note note note note note note note note note note n312 ote note note note note note note note 123 note note note note note note note note note note note note note note note note note note note note note note note note note note " ) let viewModel = BookingDetailsViewModel(booking: sampleBooking) return BookingDetailsView(viewModel) diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/MultilineEditableTextRow/MultilineEditableTextDetailView.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/MultilineEditableTextRow/MultilineEditableTextDetailView.swift new file mode 100644 index 00000000000..317c3811b52 --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/MultilineEditableTextRow/MultilineEditableTextDetailView.swift @@ -0,0 +1,102 @@ +import SwiftUI + +struct MultilineEditableTextDetailView: View { + @Environment(\.dismiss) private var dismiss + + @Binding var text: String + @State private var editedText: String + @State private var showDiscardChangesDialog = false + @FocusState private var isFocused: Bool + + let title: String? + + init(text: Binding, title: String? = nil) { + self._text = text + self._editedText = State(initialValue: text.wrappedValue) + self.title = title + } + + var body: some View { + VStack { + TextEditor(text: $editedText) + .focused($isFocused) + .padding(.horizontal, Layout.horizontalPadding) + .padding(.vertical, Layout.verticalPadding) + } + .if(title != nil) { + $0.navigationTitle(title ?? "") + } + .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button(action: handleBackButtonTap) { + Image(systemName: "chevron.backward") + .font(.body.weight(.semibold)) + } + } + + if editedText != text { + ToolbarItem(placement: .primaryAction) { + Button(Localization.doneButtonTitle) { + text = editedText + dismiss() + } + .fontWeight(.medium) + } + } + } + .confirmationDialog( + Localization.discardChangesAlertTitle, + isPresented: $showDiscardChangesDialog, + titleVisibility: .visible + ) { + Button(Localization.discardChangesActionTitle, role: .destructive) { + dismiss() + } + Button(Localization.cancelActionTitle, role: .cancel) {} + } + .wooNavigationBarStyle() + .onAppear { isFocused = true } + } + + private func handleBackButtonTap() { + if editedText != text { + showDiscardChangesDialog = true + } else { + dismiss() + } + } +} + +fileprivate extension MultilineEditableTextDetailView { + enum Layout { + static let horizontalPadding: CGFloat = 16 + static let verticalPadding: CGFloat = 12 + } +} + +extension MultilineEditableTextDetailView { + enum Localization { + static let discardChangesAlertTitle = NSLocalizedString( + "MultilineEditableTextDetailView.discardChanges.alert.title", + value: "Are you sure you want to discard these changes?", + comment: "Title for the confirmation dialog when the user attempts to discard changes in the multiline text editor." + ) + static let discardChangesActionTitle = NSLocalizedString( + "MultilineEditableTextDetailView.discardChanges.alert.discardAction", + value: "Discard changes", + comment: "Destructive action button title to discard changes in the multiline text editor." + ) + static let cancelActionTitle = NSLocalizedString( + "MultilineEditableTextDetailView.discardChanges.alert.cancelAction", + value: "Cancel", + comment: "Cancel button title for the discard changes confirmation dialog in the multiline text editor." + ) + static let doneButtonTitle = NSLocalizedString( + "MultilineEditableTextDetailView.doneButton.title", + value: "Done", + comment: "Navigation bar button title used to save changes and close the multiline text editor." + ) + } +} diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/MultilineEditableTextRow/MultilineEditableTextRow.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/MultilineEditableTextRow/MultilineEditableTextRow.swift new file mode 100644 index 00000000000..4852ab5dbdf --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/MultilineEditableTextRow/MultilineEditableTextRow.swift @@ -0,0 +1,55 @@ +import SwiftUI + +struct MultilineEditableTextRow: View { + @State var value: String + let placeholder: String + let detailTitle: String? + + init(value: String, placeholder: String, detailTitle: String? = nil) { + self.value = value + self.placeholder = placeholder + self.detailTitle = detailTitle + } + + var body: some View { + NavigationLink { + MultilineEditableTextDetailView(text: $value, title: detailTitle) + } label: { + HStack(alignment: .center, spacing: Layout.spacing) { + if value.isEmpty { + Text(placeholder) + .rowTextStyle() + .foregroundStyle(Color(.textSubtle)) + } else { + Text(value) + .multilineTextAlignment(.leading) + .foregroundStyle(Color.primary) + } + + Spacer() + + DisclosureIndicator() + } + .padding(.vertical, Layout.padding) + } + } +} + +fileprivate extension MultilineEditableTextRow { + enum Layout { + static let spacing: CGFloat = 10 + static let padding: CGFloat = 12 + } +} + +#if DEBUG +#Preview { + @Previewable @State var text: String = "" + + NavigationStack { + MultilineEditableTextRow(value: text, placeholder: "Add note") + .padding(.horizontal, 16) + } + .preferredColorScheme(.dark) +} +#endif diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index b88c6969faa..7da3197cf18 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -5785,6 +5785,7 @@ 3FD9BFBE2E0A2533004A8DC8 /* WooCommerceScreenshots */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (3FD9BFC42E0A2534004A8DC8 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = WooCommerceScreenshots; sourceTree = ""; }; 646A2C682E9FCD7E003A32A1 /* Routing */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Routing; sourceTree = ""; }; 6489D8522EA667AC00D96802 /* Routing */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Routing; sourceTree = ""; }; + 64EA08E42EC214FA00050202 /* MultilineEditableTextRow */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = MultilineEditableTextRow; sourceTree = ""; }; DEDB5D342E7A68950022E5A1 /* Bookings */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Bookings; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ @@ -8487,6 +8488,7 @@ 45D875D72611EA3D00226C3F /* SwiftUI Components */ = { isa = PBXGroup; children = ( + 64EA08E42EC214FA00050202 /* MultilineEditableTextRow */, 2D880B482DFB2F3D00A6FB2C /* OptionalBinding.swift */, EE3B17B52AA03837004D3E0C /* CelebrationView.swift */, DE7E5E8B2B4E9353002E28D2 /* ErrorStateView.swift */, @@ -13262,6 +13264,7 @@ fileSystemSynchronizedGroups = ( 2D33E6B02DD1453E000C7198 /* WooShippingPaymentMethod */, 646A2C682E9FCD7E003A32A1 /* Routing */, + 64EA08E42EC214FA00050202 /* MultilineEditableTextRow */, DEDB5D342E7A68950022E5A1 /* Bookings */, ); name = WooCommerce; From 3988873992d33ff68e18c9ad2fccaf7560e053c2 Mon Sep 17 00:00:00 2001 From: Adam Borbas Date: Mon, 10 Nov 2025 14:13:05 +0100 Subject: [PATCH 04/10] extarct toolbar --- .../MultilineEditableTextDetailView.swift | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/MultilineEditableTextRow/MultilineEditableTextDetailView.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/MultilineEditableTextRow/MultilineEditableTextDetailView.swift index 317c3811b52..2e14822ea6e 100644 --- a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/MultilineEditableTextRow/MultilineEditableTextDetailView.swift +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/MultilineEditableTextRow/MultilineEditableTextDetailView.swift @@ -28,12 +28,28 @@ struct MultilineEditableTextDetailView: View { } .navigationBarTitleDisplayMode(.inline) .navigationBarBackButtonHidden(true) - .toolbar { + .toolbar { toolbar } + .wooNavigationBarStyle() + .onAppear { isFocused = true } + } + + private var toolbar: some ToolbarContent { + Group { ToolbarItem(placement: .topBarLeading) { Button(action: handleBackButtonTap) { Image(systemName: "chevron.backward") .font(.body.weight(.semibold)) } + .confirmationDialog( + Localization.discardChangesAlertTitle, + isPresented: $showDiscardChangesDialog, + titleVisibility: .visible + ) { + Button(Localization.discardChangesActionTitle, role: .destructive) { + dismiss() + } + Button(Localization.cancelActionTitle, role: .cancel) {} + } } if editedText != text { @@ -46,18 +62,6 @@ struct MultilineEditableTextDetailView: View { } } } - .confirmationDialog( - Localization.discardChangesAlertTitle, - isPresented: $showDiscardChangesDialog, - titleVisibility: .visible - ) { - Button(Localization.discardChangesActionTitle, role: .destructive) { - dismiss() - } - Button(Localization.cancelActionTitle, role: .cancel) {} - } - .wooNavigationBarStyle() - .onAppear { isFocused = true } } private func handleBackButtonTap() { From e613fd0197bdad57d54e4f8c0fa3f755a7887835 Mon Sep 17 00:00:00 2001 From: Adam Borbas Date: Mon, 10 Nov 2025 14:25:49 +0100 Subject: [PATCH 05/10] extact content --- .../MultilineEditableTextRow.swift | 43 ++++++++++++------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/MultilineEditableTextRow/MultilineEditableTextRow.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/MultilineEditableTextRow/MultilineEditableTextRow.swift index 4852ab5dbdf..a42fe8f7f62 100644 --- a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/MultilineEditableTextRow/MultilineEditableTextRow.swift +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/MultilineEditableTextRow/MultilineEditableTextRow.swift @@ -15,22 +15,33 @@ struct MultilineEditableTextRow: View { NavigationLink { MultilineEditableTextDetailView(text: $value, title: detailTitle) } label: { - HStack(alignment: .center, spacing: Layout.spacing) { - if value.isEmpty { - Text(placeholder) - .rowTextStyle() - .foregroundStyle(Color(.textSubtle)) - } else { - Text(value) - .multilineTextAlignment(.leading) - .foregroundStyle(Color.primary) - } - - Spacer() - - DisclosureIndicator() - } - .padding(.vertical, Layout.padding) + content + } + } + + @ViewBuilder + private var content: some View { + HStack(alignment: .center, spacing: Layout.spacing) { + label + + Spacer() + + DisclosureIndicator() + } + .padding(.vertical, Layout.padding) + } + + @ViewBuilder + private var label: some View { + if value.isEmpty { + Text(placeholder) + .rowTextStyle() + .foregroundStyle(Color(.textSubtle)) + + } else { + Text(value) + .multilineTextAlignment(.leading) + .foregroundStyle(Color.primary) } } } From 5fd98d856a049530c12fa4077a5167b724298c94 Mon Sep 17 00:00:00 2001 From: Adam Borbas Date: Mon, 10 Nov 2025 14:39:28 +0100 Subject: [PATCH 06/10] fix lint --- .../Bookings/Booking Details/BookingDetailsView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index 1798b6ffee8..1d88081ce72 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -336,7 +336,6 @@ struct BookingDetailsView_Previews: PreviewProvider { currency: "USD", orderInfo: nil, note: "" -// note: "note note 123 note note note note note note note note note note note note note n312 ote note note note note note note note 123 note note note note note note note note note note note note note note note note note note note note note note note note note note " ) let viewModel = BookingDetailsViewModel(booking: sampleBooking) return BookingDetailsView(viewModel) From 6c1e5a3719cfe22d886d885d0d83aeef77fc4dc5 Mon Sep 17 00:00:00 2001 From: Adam Borbas Date: Mon, 10 Nov 2025 14:54:28 +0100 Subject: [PATCH 07/10] Update loc --- .../Bookings/Booking Details/BookingDetailsView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index 1d88081ce72..4516fa873da 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -296,8 +296,8 @@ extension BookingDetailsView { /// Booking notes static let bookingNotesRowText = NSLocalizedString( - "BookingDetailsView.bookingNote.addANoteRow.title", - value: "Add a note", + "BookingDetailsView.bookingNote.addNoteRow.title", + value: "Add note", comment: "Add a booking note section in booking details view." ) From 72fe0df8f93e6e37c33c37334f776bdb68134a58 Mon Sep 17 00:00:00 2001 From: Adam Borbas Date: Mon, 10 Nov 2025 15:26:42 +0100 Subject: [PATCH 08/10] Add property to test --- Modules/Tests/NetworkingTests/Responses/booking-list.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Modules/Tests/NetworkingTests/Responses/booking-list.json b/Modules/Tests/NetworkingTests/Responses/booking-list.json index 5532948f4ad..af8356812d1 100644 --- a/Modules/Tests/NetworkingTests/Responses/booking-list.json +++ b/Modules/Tests/NetworkingTests/Responses/booking-list.json @@ -19,6 +19,7 @@ "parent_id": 0, "person_counts": [], "local_timezone": "", + "note": "", }, { "id": 77, @@ -39,6 +40,7 @@ "parent_id": 0, "person_counts": [], "local_timezone": "", + "note": "", } ] } From dd9e05afef98f1a2ee11fb8999cd4894bc4604a7 Mon Sep 17 00:00:00 2001 From: Adam Borbas Date: Tue, 11 Nov 2025 08:23:49 +0100 Subject: [PATCH 09/10] Localize --- .../Booking Details/BookingDetailsViewModel.swift | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift index bc9495ad155..a20b9a3bce8 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift @@ -71,8 +71,8 @@ private extension BookingDetailsViewModel { ) let bookingNotes = Section( - header: .title(Localization.bookingNotesSectionHeaderTitle.uppercased()), - footerText: "This is a private note. It'll not be shared with the customer.", + header: .title(Localization.bookingNoteSectionHeaderTitle.uppercased()), + footerText: Localization.bookingNoteSectionFooterText, content: .bookingNotes ) @@ -381,12 +381,18 @@ private extension BookingDetailsViewModel { comment: "Header title for the 'Payment' section in the booking details screen." ) - static let bookingNotesSectionHeaderTitle = NSLocalizedString( + static let bookingNoteSectionHeaderTitle = NSLocalizedString( "BookingDetailsView.bookingNote.headerTitle", value: "Booking note", comment: "Header title for the 'Booking note' section in the booking details screen." ) + static let bookingNoteSectionFooterText = NSLocalizedString( + "BookingDetailsView.bookingNote.footerText", + value: "This is a private note. It'll not be shared with the customer.", + comment: "Footer text for the `Booking note` section in the booking details screen." + ) + static let cancelBookingAlertMessage = NSLocalizedString( "BookingDetailsView.cancelation.alert.message", value: "%1$@ will no longer be able to attend “%2$@” on %3$@.", From a7e4da2c64babd87df00533654c64f06681f6cc8 Mon Sep 17 00:00:00 2001 From: Adam Borbas Date: Tue, 11 Nov 2025 08:24:08 +0100 Subject: [PATCH 10/10] No need for .if --- .../MultilineEditableTextDetailView.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/MultilineEditableTextRow/MultilineEditableTextDetailView.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/MultilineEditableTextRow/MultilineEditableTextDetailView.swift index 2e14822ea6e..9bc920d5513 100644 --- a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/MultilineEditableTextRow/MultilineEditableTextDetailView.swift +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/MultilineEditableTextRow/MultilineEditableTextDetailView.swift @@ -23,9 +23,7 @@ struct MultilineEditableTextDetailView: View { .padding(.horizontal, Layout.horizontalPadding) .padding(.vertical, Layout.verticalPadding) } - .if(title != nil) { - $0.navigationTitle(title ?? "") - } + .navigationTitle(title ?? "") .navigationBarTitleDisplayMode(.inline) .navigationBarBackButtonHidden(true) .toolbar { toolbar }