diff --git a/Networking/Networking.xcodeproj/project.pbxproj b/Networking/Networking.xcodeproj/project.pbxproj index 281ba185ea8..1dbbfe83535 100644 --- a/Networking/Networking.xcodeproj/project.pbxproj +++ b/Networking/Networking.xcodeproj/project.pbxproj @@ -61,6 +61,8 @@ B5C6FCD420A373BB00A4F8E4 /* OrderMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C6FCD320A373BA00A4F8E4 /* OrderMapper.swift */; }; B5C6FCD620A3768900A4F8E4 /* order.json in Resources */ = {isa = PBXBuildFile; fileRef = B5C6FCD520A3768900A4F8E4 /* order.json */; }; CE20179320E3EFA7005B4C18 /* broken-orders.json in Resources */ = {isa = PBXBuildFile; fileRef = CE20179220E3EFA7005B4C18 /* broken-orders.json */; }; + CE21B3E72106811000A259D5 /* new-order-note.json in Resources */ = {isa = PBXBuildFile; fileRef = CE21B3E62106811000A259D5 /* new-order-note.json */; }; + CE583A0E2109154500D73C1C /* OrderNoteMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE583A0D2109154500D73C1C /* OrderNoteMapper.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -133,6 +135,8 @@ BD9439D9B8F2C1ED2EADAA51 /* Pods-NetworkingTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NetworkingTests.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-NetworkingTests/Pods-NetworkingTests.debug.xcconfig"; sourceTree = ""; }; C8F9A8CC6F90A8C9B5EF2EE2 /* Pods-Networking.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Networking.release.xcconfig"; path = "../Pods/Target Support Files/Pods-Networking/Pods-Networking.release.xcconfig"; sourceTree = ""; }; CE20179220E3EFA7005B4C18 /* broken-orders.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "broken-orders.json"; sourceTree = ""; }; + CE21B3E62106811000A259D5 /* new-order-note.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "new-order-note.json"; sourceTree = ""; }; + CE583A0D2109154500D73C1C /* OrderNoteMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderNoteMapper.swift; sourceTree = ""; }; F3F25DC15EC1D7C631169CB5 /* Pods_Networking.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Networking.framework; sourceTree = BUILT_PRODUCTS_DIR; }; F6CEE1CA2AD376C0C28AE9F6 /* Pods-NetworkingTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NetworkingTests.release.xcconfig"; path = "../Pods/Target Support Files/Pods-NetworkingTests/Pods-NetworkingTests.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -314,6 +318,7 @@ B559EBA820A0B5B100836CD4 /* Responses */ = { isa = PBXGroup; children = ( + CE21B3E62106811000A259D5 /* new-order-note.json */, B505F6D420BEE4E600BB1B69 /* me.json */, B559EBA920A0B5CD00836CD4 /* orders-load-all.json */, B5C6FCD520A3768900A4F8E4 /* order.json */, @@ -335,6 +340,7 @@ B567AF2A20A0FA4200AB6C62 /* OrderListMapper.swift */, B56C1EB520EA757B00D749F9 /* SiteListMapper.swift */, 74C8F06720EEB7BC00B6EDC9 /* OrderNotesMapper.swift */, + CE583A0D2109154500D73C1C /* OrderNoteMapper.swift */, ); path = Mapper; sourceTree = ""; @@ -482,6 +488,7 @@ files = ( 74C8F06620EEB76400B6EDC9 /* order-notes.json in Resources */, 74C8F06C20EEBD5D00B6EDC9 /* broken-order.json in Resources */, + CE21B3E72106811000A259D5 /* new-order-note.json in Resources */, B505F6D520BEE4E700BB1B69 /* me.json in Resources */, B5C6FCD620A3768900A4F8E4 /* order.json in Resources */, B559EBAA20A0B5CD00836CD4 /* orders-load-all.json in Resources */, @@ -575,6 +582,7 @@ B518662220A097C200037A38 /* Network.swift in Sources */, B518662420A099BF00037A38 /* AlamofireNetwork.swift in Sources */, B557DA1820979D51005962F4 /* Credentials.swift in Sources */, + CE583A0E2109154500D73C1C /* OrderNoteMapper.swift in Sources */, B5C6FCCF20A3592900A4F8E4 /* OrderItem.swift in Sources */, B505F6EC20BEFDC200BB1B69 /* Loader.swift in Sources */, B5BB1D1220A255EC00112D92 /* OrderStatus.swift in Sources */, diff --git a/Networking/Networking/Mapper/OrderNoteMapper.swift b/Networking/Networking/Mapper/OrderNoteMapper.swift new file mode 100644 index 00000000000..c6735c265c7 --- /dev/null +++ b/Networking/Networking/Mapper/OrderNoteMapper.swift @@ -0,0 +1,29 @@ +import Foundation + + +/// Mapper: OrderNote (Singular) +/// +class OrderNoteMapper: Mapper { + + /// (Attempts) to convert a dictionary into a single OrderNote + /// + func map(response: Data) throws -> OrderNote { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .formatted(DateFormatter.Defaults.dateTimeFormatter) + + return try decoder.decode(OrderNoteEnvelope.self, from: response).orderNote + } +} + + +/// OrderNote Disposable Entity: +/// `Add Order Note` endpoint the single added note within the `data` key. This entity +/// allows us to parse all the things with JSONDecoder. +/// +private struct OrderNoteEnvelope: Decodable { + let orderNote: OrderNote + + private enum CodingKeys: String, CodingKey { + case orderNote = "data" + } +} diff --git a/Networking/Networking/Remote/OrdersRemote.swift b/Networking/Networking/Remote/OrdersRemote.swift index b2762f62783..1a7940e1c79 100644 --- a/Networking/Networking/Remote/OrdersRemote.swift +++ b/Networking/Networking/Remote/OrdersRemote.swift @@ -68,6 +68,26 @@ public class OrdersRemote: Remote { let request = JetpackRequest(wooApiVersion: .mark2, method: .post, siteID: siteID, path: path, parameters: parameters) enqueue(request, mapper: mapper, completion: completion) } + + /// Adds an order note to a specific Order. + /// + /// - Parameters: + /// - siteID: Site which hosts the Order. + /// - orderID: Identifier of the Order to be updated. + /// - isCustomerNote: if true, the note will be shown to customers and they will be notified. + /// if false, the note will be for admin reference only. Default is false. + /// - note: The note to be posted. + /// - completion: Closure to be executed upon completion. + /// + public func addOrderNote(for siteID: Int, orderID: Int, isCustomerNote: Bool, with note: String, completion: @escaping (OrderNote?, Error?) -> Void) { + let path = "\(Constants.ordersPath)/" + String(orderID) + "/" + "\(Constants.notesPath)" + let parameters = [ParameterKeys.note: note, + ParameterKeys.customerNote: String(isCustomerNote)] + let mapper = OrderNoteMapper() + + let request = JetpackRequest(wooApiVersion: .mark2, method: .post, siteID: siteID, path: path, parameters: parameters) + enqueue(request, mapper: mapper, completion: completion) + } } @@ -81,8 +101,10 @@ private extension OrdersRemote { } enum ParameterKeys { - static let status: String = "status" - static let page: String = "page" - static let perPage: String = "per_page" + static let customerNote: String = "customer_note" + static let note: String = "note" + static let page: String = "page" + static let perPage: String = "per_page" + static let status: String = "status" } } diff --git a/Networking/NetworkingTests/Remote/OrdersRemoteTests.swift b/Networking/NetworkingTests/Remote/OrdersRemoteTests.swift index b77c64094a4..afba740082a 100644 --- a/Networking/NetworkingTests/Remote/OrdersRemoteTests.swift +++ b/Networking/NetworkingTests/Remote/OrdersRemoteTests.swift @@ -166,4 +166,21 @@ class OrdersRemoteTests: XCTestCase { wait(for: [expectation], timeout: Constants.expectationTimeout) } + + /// Verifies that addOrderNote properly parses the `new-order-note` sample response. + /// + func testLoadAddOrderNoteProperlyReturnsParsedOrderNote() { + let remote = OrdersRemote(network: network) + let expectation = self.expectation(description: "Add Order Note") + let noteData = "This order would be so much better with ketchup." + + network.simulateResponse(requestUrlSuffix: "orders/\(sampleOrderID)/notes", filename: "new-order-note") + + remote.addOrderNote(for: sampleSiteID, orderID: sampleOrderID, isCustomerNote: true, with: noteData) { (orderNote, error) in + XCTAssertNil(error) + XCTAssertNotNil(orderNote) + expectation.fulfill() + } + wait(for: [expectation], timeout: Constants.expectationTimeout) + } } diff --git a/Networking/NetworkingTests/Responses/new-order-note.json b/Networking/NetworkingTests/Responses/new-order-note.json new file mode 100644 index 00000000000..1e6a8140c17 --- /dev/null +++ b/Networking/NetworkingTests/Responses/new-order-note.json @@ -0,0 +1,26 @@ +{ + "data": { + "id": 2235, + "date_created": "2018-06-22T11:36:20", + "date_created_gmt": "2018-06-22T15:36:20", + "note": "This order would be so much better with ketchup.", + "customer_note": true, + "_links": { + "self": [ + { + "href": "https://jamosova3.mystagingwebsite.com/wp-json/wc/v2/orders/1179/notes/2235" + } + ], + "collection": [ + { + "href": "https://jamosova3.mystagingwebsite.com/wp-json/wc/v2/orders/1179/notes" + } + ], + "up": [ + { + "href": "https://jamosova3.mystagingwebsite.com/wp-json/wc/v2/orders/1179" + } + ] + } + } +} diff --git a/WooCommerce/Classes/Extensions/UIView+Helpers.swift b/WooCommerce/Classes/Extensions/UIView+Helpers.swift index 5ad7dec60d4..12c72e9821a 100644 --- a/WooCommerce/Classes/Extensions/UIView+Helpers.swift +++ b/WooCommerce/Classes/Extensions/UIView+Helpers.swift @@ -2,7 +2,7 @@ import Foundation import UIKit -/// UIView Helper Methods +/// UIView Class Methods /// extension UIView { @@ -19,3 +19,23 @@ extension UIView { return loadNib().instantiate(withOwner: nil, options: nil).first as! T } } + + +/// UIView Extension Methods +/// +extension UIView { + + /// Returns the first Subview of the specified Type (if any). + /// + func firstSubview(ofType type: T.Type) -> T? { + for subview in subviews { + guard let target = (subview as? T) ?? subview.firstSubview(ofType: type) else { + continue + } + + return target + } + + return nil + } +} diff --git a/WooCommerce/Classes/ViewRelated/Orders/OrderDetails/AddNote/AddANoteViewController.swift b/WooCommerce/Classes/ViewRelated/Orders/OrderDetails/AddNote/AddANoteViewController.swift new file mode 100644 index 00000000000..773e052f714 --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/Orders/OrderDetails/AddNote/AddANoteViewController.swift @@ -0,0 +1,231 @@ +import UIKit +import Yosemite +import Gridicons +import CocoaLumberjack + +class AddANoteViewController: UIViewController { + + // MARK: - Properties + + @IBOutlet var tableView: UITableView! + + var viewModel: OrderDetailsViewModel! + + private var sections = [Section]() + + private var isCustomerNote = false + + private var noteText: String = "" + + // MARK: - View Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + configureNavigation() + configureTableView() + registerTableViewCells() + loadSections() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + tableView.firstSubview(ofType: UITextView.self)?.becomeFirstResponder() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + view.endEditing(true) + } + + func configureNavigation() { + title = NSLocalizedString("Order #\(viewModel.order.number)", comment: "Add a note screen - title. Example: Order #15") + + let dismissButtonTitle = NSLocalizedString("Dismiss", comment: "Add a note screen - button title for closing the view") + let leftBarButton = UIBarButtonItem(title: dismissButtonTitle, + style: .plain, + target: self, + action: #selector(dismissButtonTapped)) + leftBarButton.tintColor = .white + navigationItem.setLeftBarButton(leftBarButton, animated: false) + + let addButtonTitle = NSLocalizedString("Add", comment: "Add a note screen - button title to send the note") + let rightBarButton = UIBarButtonItem(title: addButtonTitle, + style: .done, + target: self, + action: #selector(addButtonTapped)) + rightBarButton.tintColor = .white + navigationItem.setRightBarButton(rightBarButton, animated: false) + navigationItem.rightBarButtonItem?.isEnabled = false + } + + @objc func dismissButtonTapped() { + dismiss(animated: true, completion: nil) + } + + @objc func addButtonTapped() { + let action = OrderNoteAction.addOrderNote(siteID: viewModel.order.siteID, orderID: viewModel.order.orderID, isCustomerNote: isCustomerNote, note: noteText) { [weak self] (orderNote, error) in + if let error = error { + DDLogError("⛔️ Error adding a note: \(error.localizedDescription)") + // TODO: should this alert the user that there was an error? + return + } + self?.dismiss(animated: true, completion: nil) + } + + StoresManager.shared.dispatch(action) + } +} + +// MARK: - TableView Configuration +// +private extension AddANoteViewController { + /// Setup: TableView + /// + private func configureTableView() { + tableView.estimatedRowHeight = Constants.rowHeight + tableView.rowHeight = UITableViewAutomaticDimension + } + + /// Registers all of the available TableViewCells + /// + private func registerTableViewCells() { + let cells = [ + TextViewTableViewCell.self, + SwitchTableViewCell.self + ] + + for cell in cells { + tableView.register(cell.loadNib(), forCellReuseIdentifier: cell.reuseIdentifier) + } + } + + /// Setup: Sections + /// + private func loadSections() { + let writeNoteSectionTitle = NSLocalizedString("WRITE NOTE", comment: "Add a note screen - Write Note section title") + let writeNoteSection = Section(title: writeNoteSectionTitle, rows: [.writeNote]) + let emailCustomerSection = Section(title: nil, rows: [.emailCustomer]) + + sections = [writeNoteSection, emailCustomerSection] + } + + /// Switch between a private note and a customer note + /// + func toggleNoteType() { + isCustomerNote = !isCustomerNote + } + + /// Cell Configuration + /// + private func setup(cell: UITableViewCell, for row: Row) { + switch row { + case .writeNote: + setupWriteNoteCell(cell) + case .emailCustomer: + setupEmailCustomerCell(cell) + } + } + + private func setupWriteNoteCell(_ cell: UITableViewCell) { + guard let cell = cell as? TextViewTableViewCell else { + fatalError() + } + + cell.iconImage = Gridicon.iconOfType(.aside) + cell.iconTint = isCustomerNote ? StyleManager.statusPrimaryBoldColor : StyleManager.wooGreyMid + + cell.onTextChange = { [weak self] (text) in + self?.navigationItem.rightBarButtonItem?.isEnabled = !text.isEmpty + self?.noteText = text + } + } + + private func setupEmailCustomerCell(_ cell: UITableViewCell) { + guard let cell = cell as? SwitchTableViewCell else { + fatalError() + } + + cell.topText = NSLocalizedString("Email note to customer", comment: "Label for yes/no switch - emailing the note to customer.") + cell.bottomText = NSLocalizedString("If disabled will add the note as private.", comment: "Detail label for yes/no switch.") + + cell.onToggleSwitchTouchUp = { [weak self] in + guard let `self` = self else { + return + } + + self.toggleNoteType() + self.refreshTextViewCell() + } + } + + private func refreshTextViewCell() { + guard let cell = tableView.firstSubview(ofType: TextViewTableViewCell.self) else { + return + } + + setupWriteNoteCell(cell) + } +} + +// MARK: - UITableViewDataSource Conformance +// +extension AddANoteViewController: UITableViewDataSource { + func numberOfSections(in tableView: UITableView) -> Int { + return sections.count + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return sections[section].rows.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let row = sections[indexPath.section].rows[indexPath.row] + let cell = tableView.dequeueReusableCell(withIdentifier: row.reuseIdentifier, for: indexPath) + setup(cell: cell, for: row) + return cell + } + + func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return sections[section].title + } + + func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { + // iOS 11 table bug. Must return a tiny value to collapse `nil` or `empty` section footers. + return CGFloat.leastNonzeroMagnitude + } +} + +// MARK: - UITableViewDelegate Conformance +// +extension AddANoteViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectSelectedRowWithAnimation(true) + } +} + +// MARK: - Constants +// +private extension AddANoteViewController { + struct Constants { + static let rowHeight = CGFloat(44) + } + + private struct Section { + let title: String? + let rows: [Row] + } + + private enum Row { + case writeNote + case emailCustomer + + var reuseIdentifier: String { + switch self { + case .writeNote: + return TextViewTableViewCell.reuseIdentifier + case .emailCustomer: + return SwitchTableViewCell.reuseIdentifier + } + } + } +} diff --git a/WooCommerce/Classes/ViewRelated/Orders/OrderDetails/CustomerNoteTableViewCell.swift b/WooCommerce/Classes/ViewRelated/Orders/OrderDetails/CustomerNoteTableViewCell.swift index 44b82cb10e0..977a7e46883 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/OrderDetails/CustomerNoteTableViewCell.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/OrderDetails/CustomerNoteTableViewCell.swift @@ -7,6 +7,7 @@ class CustomerNoteTableViewCell: UITableViewCell { noteLabel.applyBodyStyle() } } + @IBOutlet private weak var iconImageView: UIImageView! { didSet { iconImageView.image = Gridicon.iconOfType(.quote) @@ -14,7 +15,6 @@ class CustomerNoteTableViewCell: UITableViewCell { } } - var quote: String? { get { return noteLabel.text diff --git a/WooCommerce/Classes/ViewRelated/Orders/OrderDetails/OrderDetailsViewController.swift b/WooCommerce/Classes/ViewRelated/Orders/OrderDetails/OrderDetailsViewController.swift index 1a9dd68196a..1323327a55a 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/OrderDetails/OrderDetailsViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/OrderDetails/OrderDetailsViewController.swift @@ -405,7 +405,10 @@ extension OrderDetailsViewController: UITableViewDelegate { tableView.deselectRow(at: indexPath, animated: true) if sections[indexPath.section].rows[indexPath.row] == .addOrderNote { - // TODO: present modal for Add Note screen + let addANoteViewController = self.storyboard!.instantiateViewController(withIdentifier: Constants.noteViewController) as! AddANoteViewController + addANoteViewController.viewModel = viewModel + let navController = UINavigationController(rootViewController: addANoteViewController) + present(navController, animated: true, completion: nil) } else if sections[indexPath.section].rows[indexPath.row] == .productDetails { performSegue(withIdentifier: Constants.productDetailsSegue, sender: nil) } @@ -478,6 +481,7 @@ private extension OrderDetailsViewController { static let rowHeight = CGFloat(38) static let sectionHeight = CGFloat(44) static let productDetailsSegue = "ShowProductListViewController" + static let noteViewController = "AddANoteViewController" } private struct Section { diff --git a/WooCommerce/Classes/ViewRelated/Orders/OrderDetails/OrderNoteTableViewCell.xib b/WooCommerce/Classes/ViewRelated/Orders/OrderDetails/OrderNoteTableViewCell.xib index 5b774b048d8..69f2dc0fd8c 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/OrderDetails/OrderNoteTableViewCell.xib +++ b/WooCommerce/Classes/ViewRelated/Orders/OrderDetails/OrderNoteTableViewCell.xib @@ -1,5 +1,5 @@ - + diff --git a/WooCommerce/Classes/ViewRelated/Orders/Orders.storyboard b/WooCommerce/Classes/ViewRelated/Orders/Orders.storyboard index 75347030883..76b1e7fb18a 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Orders.storyboard +++ b/WooCommerce/Classes/ViewRelated/Orders/Orders.storyboard @@ -145,7 +145,6 @@ - @@ -173,8 +172,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/SwitchTableViewCell.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/SwitchTableViewCell.swift new file mode 100644 index 00000000000..a4c204d94e8 --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/SwitchTableViewCell.swift @@ -0,0 +1,61 @@ +import UIKit + +class SwitchTableViewCell: UITableViewCell { + + // MARK: - Properties + // + @IBOutlet private var topLabel: UILabel! + @IBOutlet private var bottomLabel: UILabel! + @IBOutlet private var toggleSwitch: UISwitch! + + var onToggleSwitchTouchUp: (() -> Void)? + + var topText: String? { + get { + return topLabel.text + } + set { + topLabel.text = newValue + } + } + + var bottomText: String? { + get { + return bottomLabel.text + } + set { + bottomLabel.text = newValue + } + } + + override func awakeFromNib() { + super.awakeFromNib() + topLabel.applyBodyStyle() + bottomLabel.applyFootnoteStyle() + setupGestureRecognizers() + } + + @IBAction func toggleSwitchWasPressed() { + onToggleSwitchTouchUp?() + } +} + + +// MARK: - Private Methods +// +private extension SwitchTableViewCell { + + func setupGestureRecognizers() { + let gestureRecognizer = UITapGestureRecognizer() + gestureRecognizer.on { [weak self] gesture in + self?.contentViewWasPressed() + } + + addGestureRecognizer(gestureRecognizer) + } + + func contentViewWasPressed() { + toggleSwitch.isOn = !toggleSwitch.isOn + onToggleSwitchTouchUp?() + } +} diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/SwitchTableViewCell.xib b/WooCommerce/Classes/ViewRelated/ReusableViews/SwitchTableViewCell.xib new file mode 100644 index 00000000000..e0bbfec762d --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/SwitchTableViewCell.xib @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/TextViewTableViewCell.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/TextViewTableViewCell.swift new file mode 100644 index 00000000000..0f5ca8446b4 --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/TextViewTableViewCell.swift @@ -0,0 +1,42 @@ +import UIKit +import Gridicons + +class TextViewTableViewCell: UITableViewCell { + + @IBOutlet var noteIconButton: UIButton! + + @IBOutlet var noteTextView: UITextView! + + var iconImage: UIImage? { + get { + return noteIconButton.image(for: .normal) + } + set { + noteIconButton.setImage(newValue, for: .normal) + noteIconButton.tintColor = .white + noteIconButton.layer.cornerRadius = noteIconButton.frame.width / 2 + } + } + + var iconTint: UIColor? { + get { + return noteIconButton.backgroundColor + } + set { + noteIconButton.backgroundColor = newValue + } + } + + var onTextChange: ((String) -> Void)? + + override func awakeFromNib() { + super.awakeFromNib() + noteTextView.delegate = self + } +} + +extension TextViewTableViewCell: UITextViewDelegate { + func textViewDidChange(_ textView: UITextView) { + onTextChange?(noteTextView.text) + } +} diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/TextViewTableViewCell.xib b/WooCommerce/Classes/ViewRelated/ReusableViews/TextViewTableViewCell.xib new file mode 100644 index 00000000000..b803174208e --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/TextViewTableViewCell.xib @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 84f5094cc24..f915b2fc840 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -124,6 +124,11 @@ CE34DA2520A1ECD6005D8523 /* ContactViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE34DA2420A1ECD6005D8523 /* ContactViewModel.swift */; }; CE4296B920A5E9E400B2AFBD /* CNContact+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE4296B820A5E9E400B2AFBD /* CNContact+Helpers.swift */; }; CE4DDB7B20DD312400D32EC8 /* Date+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE4DDB7A20DD312400D32EC8 /* Date+Helpers.swift */; }; + CE583A0421076C0100D73C1C /* AddANoteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE583A0321076C0100D73C1C /* AddANoteViewController.swift */; }; + CE583A072107849F00D73C1C /* SwitchTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE583A052107849F00D73C1C /* SwitchTableViewCell.swift */; }; + CE583A082107849F00D73C1C /* SwitchTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE583A062107849F00D73C1C /* SwitchTableViewCell.xib */; }; + CE583A0B2107937F00D73C1C /* TextViewTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE583A092107937F00D73C1C /* TextViewTableViewCell.swift */; }; + CE583A0C2107937F00D73C1C /* TextViewTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE583A0A2107937F00D73C1C /* TextViewTableViewCell.xib */; }; CE85535D209B5BB700938BDC /* OrderDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE85535C209B5BB700938BDC /* OrderDetailsViewModel.swift */; }; CE855364209BA6A700938BDC /* ShowHideSectionFooter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE855360209BA6A700938BDC /* ShowHideSectionFooter.swift */; }; CE855365209BA6A700938BDC /* CustomerInfoTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE855361209BA6A700938BDC /* CustomerInfoTableViewCell.xib */; }; @@ -288,6 +293,11 @@ CE34DA2420A1ECD6005D8523 /* ContactViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactViewModel.swift; sourceTree = ""; }; CE4296B820A5E9E400B2AFBD /* CNContact+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CNContact+Helpers.swift"; sourceTree = ""; }; CE4DDB7A20DD312400D32EC8 /* Date+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Helpers.swift"; sourceTree = ""; }; + CE583A0321076C0100D73C1C /* AddANoteViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddANoteViewController.swift; sourceTree = ""; }; + CE583A052107849F00D73C1C /* SwitchTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchTableViewCell.swift; sourceTree = ""; }; + CE583A062107849F00D73C1C /* SwitchTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SwitchTableViewCell.xib; sourceTree = ""; }; + CE583A092107937F00D73C1C /* TextViewTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextViewTableViewCell.swift; sourceTree = ""; }; + CE583A0A2107937F00D73C1C /* TextViewTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TextViewTableViewCell.xib; sourceTree = ""; }; CE85535C209B5BB700938BDC /* OrderDetailsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderDetailsViewModel.swift; sourceTree = ""; }; CE855360209BA6A700938BDC /* ShowHideSectionFooter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShowHideSectionFooter.swift; sourceTree = ""; }; CE855361209BA6A700938BDC /* CustomerInfoTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = CustomerInfoTableViewCell.xib; sourceTree = ""; }; @@ -615,6 +625,14 @@ path = Extensions; sourceTree = ""; }; + CE583A0221076BBE00D73C1C /* AddNote */ = { + isa = PBXGroup; + children = ( + CE583A0321076C0100D73C1C /* AddANoteViewController.swift */, + ); + path = AddNote; + sourceTree = ""; + }; CE85535B209B5B6A00938BDC /* ViewModels */ = { isa = PBXGroup; children = ( @@ -665,6 +683,10 @@ CE32B10A20BEDE05006FBCF4 /* TwoColumnSectionHeaderView.xib */, CE1EC8C920B479F1009762BF /* TwoColumnLabelView.swift */, CE1EC8C720B478B6009762BF /* TwoColumnLabelView.xib */, + CE583A052107849F00D73C1C /* SwitchTableViewCell.swift */, + CE583A062107849F00D73C1C /* SwitchTableViewCell.xib */, + CE583A092107937F00D73C1C /* TextViewTableViewCell.swift */, + CE583A0A2107937F00D73C1C /* TextViewTableViewCell.xib */, ); path = ReusableViews; sourceTree = ""; @@ -672,6 +694,7 @@ CEE006022077D0F80079161F /* OrderDetails */ = { isa = PBXGroup; children = ( + CE583A0221076BBE00D73C1C /* AddNote */, CE17C2E020ACA06800AFBD20 /* BillingDetailsTableViewCell.swift */, CE17C2E120ACA06800AFBD20 /* BillingDetailsTableViewCell.xib */, CE855362209BA6A700938BDC /* CustomerInfoTableViewCell.swift */, @@ -807,6 +830,7 @@ files = ( B56DB3D72049BFAA00D4AA8E /* LaunchScreen.storyboard in Resources */, CE85FD6020F7BE8D0080B73E /* LogOutTableViewCell.xib in Resources */, + CE583A0C2107937F00D73C1C /* TextViewTableViewCell.xib in Resources */, B5A8F8AF20B88DCC00D211DE /* LoginPrologueViewController.xib in Resources */, B5D1AFC820BC7B9600DB0E8C /* StorePickerViewController.xib in Resources */, B57C744320F54F1C00EEFC87 /* AccountHeaderView.xib in Resources */, @@ -815,6 +839,7 @@ 93BCF01F20DC2CE200EBF7A1 /* bash_secrets.tpl in Resources */, B56DB3D42049BFAA00D4AA8E /* Assets.xcassets in Resources */, B559EBAF20A0BF8F00836CD4 /* README.md in Resources */, + CE583A082107849F00D73C1C /* SwitchTableViewCell.xib in Resources */, CE22571B20E16FBC0037F478 /* LeftImageTableViewCell.xib in Resources */, B5D1AFB420BC445A00DB0E8C /* Images.xcassets in Resources */, CE1EC8CF20B6FD53009762BF /* FootnoteView.xib in Resources */, @@ -989,6 +1014,7 @@ CE85FD5F20F7BE8D0080B73E /* LogOutTableViewCell.swift in Sources */, CE32B11A20BF8E32006FBCF4 /* UIButton+Helpers.swift in Sources */, CE263DE6206ACD220015A693 /* NotificationsViewController.swift in Sources */, + CE583A0B2107937F00D73C1C /* TextViewTableViewCell.swift in Sources */, B557652B20F681E800185843 /* StoreTableViewCell.swift in Sources */, B57C743D20F5493300EEFC87 /* AccountHeaderView.swift in Sources */, B50911312049E27A007D25DC /* OrdersViewController.swift in Sources */, @@ -1009,6 +1035,7 @@ B5DBF3CB20E149CC00B53AED /* AuthenticatedState.swift in Sources */, 747AA08B2107CF8D0047A89B /* TracksProvider.swift in Sources */, CE1EC8F120B8A408009762BF /* OrderNoteTableViewCell.swift in Sources */, + CE583A0421076C0100D73C1C /* AddANoteViewController.swift in Sources */, B58B4AC02108FF6100076FDD /* Array+Helpers.swift in Sources */, CE4DDB7B20DD312400D32EC8 /* Date+Helpers.swift in Sources */, B50911322049E27A007D25DC /* SettingsViewController.swift in Sources */, @@ -1031,6 +1058,7 @@ B50911302049E27A007D25DC /* DashboardViewController.swift in Sources */, B5D1AFB820BC510200DB0E8C /* UIImage+Woo.swift in Sources */, CE1EC8E920B8A3F5009762BF /* OrderNoteViewModel.swift in Sources */, + CE583A072107849F00D73C1C /* SwitchTableViewCell.swift in Sources */, CEE006082077D14C0079161F /* OrderDetailsViewController.swift in Sources */, CE34DA2520A1ECD6005D8523 /* ContactViewModel.swift in Sources */, B58B4AB62108F11C00076FDD /* Notice.swift in Sources */, diff --git a/Yosemite/Yosemite.xcodeproj/project.pbxproj b/Yosemite/Yosemite.xcodeproj/project.pbxproj index 1936c9b783b..bdd277ae75c 100644 --- a/Yosemite/Yosemite.xcodeproj/project.pbxproj +++ b/Yosemite/Yosemite.xcodeproj/project.pbxproj @@ -184,12 +184,12 @@ B5BC736920D1AAE900B5B6FA /* Resources */ = { isa = PBXGroup; children = ( + 745D21C120D8043A00BBE7C3 /* generic_error.json */, B5BC736A20D1AAE900B5B6FA /* me.json */, - 74A7689120D47F9E00F9D437 /* orders.json */, 7424B49320EAD37C00CC62F6 /* order.json */, - B5B914C120EFE03200F2F832 /* sites.json */, 7499936B20EFC20100CF01CD /* order-notes.json */, - 745D21C120D8043A00BBE7C3 /* generic_error.json */, + 74A7689120D47F9E00F9D437 /* orders.json */, + B5B914C120EFE03200F2F832 /* sites.json */, ); path = Resources; sourceTree = ""; diff --git a/Yosemite/Yosemite/Actions/OrderNoteAction.swift b/Yosemite/Yosemite/Actions/OrderNoteAction.swift index 857099d2df5..fb8ddf2b4ee 100644 --- a/Yosemite/Yosemite/Actions/OrderNoteAction.swift +++ b/Yosemite/Yosemite/Actions/OrderNoteAction.swift @@ -6,4 +6,5 @@ import Networking // public enum OrderNoteAction: Action { case retrieveOrderNotes(siteID: Int, orderID: Int, onCompletion: ([OrderNote]?, Error?) -> Void) + case addOrderNote(siteID: Int, orderID: Int, isCustomerNote: Bool, note: String, onCompletion: (OrderNote?, Error?) -> Void) } diff --git a/Yosemite/Yosemite/Stores/OrderNoteStore.swift b/Yosemite/Yosemite/Stores/OrderNoteStore.swift index a978517c679..78f978fa654 100644 --- a/Yosemite/Yosemite/Stores/OrderNoteStore.swift +++ b/Yosemite/Yosemite/Stores/OrderNoteStore.swift @@ -24,6 +24,8 @@ public class OrderNoteStore: Store { switch action { case .retrieveOrderNotes(let siteId, let orderId, let onCompletion): retrieveOrderNotes(siteID: siteId, orderID: orderId, onCompletion: onCompletion) + case .addOrderNote(let siteId, let orderId, let isCustomerNote, let note, let onCompletion): + addOrderNote(siteID: siteId, orderID: orderId, isCustomerNote: isCustomerNote, note: note, onCompletion: onCompletion) } } } @@ -47,6 +49,21 @@ private extension OrderNoteStore { onCompletion(orderNotes, nil) } } + + /// Adds a single order note and associates it with the provided siteID and orderID. + /// + func addOrderNote(siteID: Int, orderID: Int, isCustomerNote: Bool, note: String, onCompletion: @escaping (OrderNote?, Error?) -> Void) { + let remote = OrdersRemote(network: network) + remote.addOrderNote(for: siteID, orderID: orderID, isCustomerNote: isCustomerNote, with: note) { [weak self] (orderNote, error) in + guard let note = orderNote else { + onCompletion(nil, error) + return + } + + self?.upsertStoredOrderNotes(readOnlyOrderNotes: [note], orderID: orderID) + onCompletion(note, nil) + } + } }