diff --git a/MongoView/Node.swift b/MongoView/Node.swift index ac87ab1..b222a72 100644 --- a/MongoView/Node.swift +++ b/MongoView/Node.swift @@ -38,6 +38,14 @@ class Node: NSObject { var hasChildren: Bool { numberOfChildren > 0 } + + var root: Node { + if let parent = parent { + return parent.root + } else { + return self + } + } init(key: Key? = nil, value: BSON, parent: Node? = nil) { self.value = value @@ -84,8 +92,7 @@ extension Node { extension Node { static let dateFormatter: DateFormatter = { let formatter = DateFormatter() - formatter.locale = .current - formatter.setLocalizedDateFormatFromTemplate("yyyy-MM-dd HH:mm:ss ZZ") + formatter.dateFormat = "yyyy-MM-dd HH:mm:ss ZZ" return formatter }() @@ -136,7 +143,7 @@ extension Node { case let .bool(value): return value.description case let .datetime(value): - return value.description + return Node.dateFormatter.string(from: value) case .null: return "null" case let .regex(value): @@ -152,6 +159,7 @@ extension Node { case let .int32(value): return value.description case let .timestamp(value): + // todo: this needs to include the timestamp increment let date = Date(timeIntervalSince1970: TimeInterval(value.timestamp)) return Node.dateFormatter.string(from: date) case let .int64(value): @@ -226,13 +234,4 @@ extension Node { return "MaxKey" } } - - var isValueCopyable: Bool { - switch value { - case .document(_), .array(_), .binary(_), .minKey, .maxKey: - return false - default: - return true - } - } } diff --git a/MongoView/View Controllers/QueryViewController.swift b/MongoView/View Controllers/QueryViewController.swift index 3f41dff..f1a81d7 100644 --- a/MongoView/View Controllers/QueryViewController.swift +++ b/MongoView/View Controllers/QueryViewController.swift @@ -155,12 +155,25 @@ class QueryViewController: NSViewController { } } + private func openEditWindow(_ document: BSONDocument) { + let wc = EditDocumentWindowController(mongoController: mongoController, collection: collection, document: document) + wc.documentEdited = { + self.refresh() + self.mongoController.statusManager.set("Updated document", for: .document) + } + wc.showWindow(nil) + } + @objc func outlineCellDoubleClicked() { - if let item = outlineView.item(atRow: outlineView.clickedRow) { - if outlineView.isItemExpanded(item) { - outlineView.collapseItem(item) - } else { - outlineView.expandItem(item) + if let node = outlineView.item(atRow: outlineView.clickedRow) as? Node { + if node.hasChildren { + if outlineView.isItemExpanded(node) { + outlineView.collapseItem(node) + } else { + outlineView.expandItem(node) + } + } else if node.isValueInlineEditable { + outlineView.editColumn(1, row: outlineView.clickedRow, with: nil, select: false) } } } @@ -181,12 +194,7 @@ class QueryViewController: NSViewController { return } - let wc = EditDocumentWindowController(mongoController: mongoController, collection: collection, document: document) - wc.documentEdited = { - self.refresh() - self.mongoController.statusManager.set("Updated document", for: .document) - } - wc.showWindow(nil) + openEditWindow(document) } @IBAction func copy(_ sender: Any) { @@ -205,6 +213,51 @@ class QueryViewController: NSViewController { NSPasteboard.general.clearContents() NSPasteboard.general.setString(str, forType: .string) } + + @objc func editedValue(_ textField: NSTextField) { + guard let node = outlineView.item(atRow: outlineView.selectedRow) as? Node, + case let .document(rootDoc) = node.root.value else { + return + } + + let proposedValue = textField.stringValue + + if let newValue = node.coerceBSONValue(proposedValue) { + let updateDoc: BSONDocument = [ + "$set": [ + node.buildUpdateKey(): newValue + ] + ] + mongoController.collection(collection).updateOne(filter: rootDoc, update: updateDoc).whenComplete { (result) in + switch result { + case .success(nil): + fatalError() + case .success(_): + self.mongoController.statusManager.set("Updated document", for: .document) + case let .failure(error): + DispatchQueue.main.async { + let alert = NSAlert(error: error) + alert.beginSheetModal(for: self.view.window!, completionHandler: nil) + } + } + } + } else { + textField.stringValue = node.valueString + + let alert = NSAlert() + alert.alertStyle = .critical + alert.messageText = "Invalid value format" + alert.informativeText = "The value '\(proposedValue)' is not valid for fields of type \(node.value.type).\nIf you want to change the value type, edit the JSON document." + alert.addButton(withTitle: "OK") + alert.addButton(withTitle: "Edit Document") + alert.beginSheetModal(for: self.view.window!) { (res) in + alert.window.close() + if res == .alertSecondButtonReturn { + self.openEditWindow(rootDoc) + } + } + } + } } extension QueryViewController: NSMenuItemValidation { @@ -273,7 +326,9 @@ extension QueryViewController: NSOutlineViewDelegate { } else if tableColumn.identifier == .fieldValueColumn { let cell = outlineView.makeView(withIdentifier: .fieldValueCell, owner: nil) as! NSTableCellView cell.textField!.stringValue = node.valueString - cell.textField!.isEditable = false + cell.textField!.isEditable = node.isValueInlineEditable + cell.textField!.target = self + cell.textField!.action = #selector(editedValue(_:)) return cell } else if tableColumn.identifier == .valueTypeColumn { let cell = outlineView.makeView(withIdentifier: .valueTypeCell, owner: nil) as! NSTableCellView @@ -295,3 +350,100 @@ extension NSUserInterfaceItemIdentifier { static let valueTypeColumn = NSUserInterfaceItemIdentifier(rawValue: "ValueTypeCol") static let valueTypeCell = NSUserInterfaceItemIdentifier(rawValue: "ValueTypeCell") } + +fileprivate extension Node { + var isValueCopyable: Bool { + switch value.type { + case .document, .array, .binary, .minKey, .maxKey: + return false + default: + return true + } + } + + var isValueInlineEditable: Bool { + switch value.type { + case .double, .string, .objectID, .bool, .datetime, .int32, .int64, .decimal128: + return true + default: + return false + } + } + + func coerceBSONValue(_ str: String) -> BSON? { + guard isValueInlineEditable else { return false } + switch value.type { + case .double: + if let d = Double(str) { + return .double(d) + } else { + return nil + } + case .string: + return .string(str) + case .objectID: + if let id = try? BSONObjectID(str) { + return .objectID(id) + } else { + return nil + } + case .bool: + let lower = str.lowercased() + if lower == "true" { + return .bool(true) + } else if lower == "false" { + return .bool(false) + } else { + return nil + } + case .datetime: + if let date = Node.dateFormatter.date(from: str) { + return .datetime(date) + } else { + return nil + } + case .int32: + if let i = Int32(str) { + return .int32(i) + } else { + return nil + } + case .int64: + if let i = Int64(str) { + return .int64(i) + } else { + return nil + } + case .decimal128: + if let dec = try? BSONDecimal128(str) { + return .decimal128(dec) + } else { + return nil + } + default: + return nil + } + } + + func buildUpdateKey() -> String { + let parentKey: String + if let parent = parent { + if case .objectID(_) = parent.key, parent.parent == nil { + parentKey = "" + } else { + parentKey = parent.buildUpdateKey() + "." + } + } else { + parentKey = "" + } + + switch key { + case let .index(index): + return parentKey + index.description + case let .name(name): + return parentKey + name + default: + fatalError() + } + } +}