Add inline editing for some value types
This commit is contained in:
parent
ba45bc6f1b
commit
24ce95c764
|
@ -38,6 +38,14 @@ class Node: NSObject {
|
||||||
var hasChildren: Bool {
|
var hasChildren: Bool {
|
||||||
numberOfChildren > 0
|
numberOfChildren > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var root: Node {
|
||||||
|
if let parent = parent {
|
||||||
|
return parent.root
|
||||||
|
} else {
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init(key: Key? = nil, value: BSON, parent: Node? = nil) {
|
init(key: Key? = nil, value: BSON, parent: Node? = nil) {
|
||||||
self.value = value
|
self.value = value
|
||||||
|
@ -84,8 +92,7 @@ extension Node {
|
||||||
extension Node {
|
extension Node {
|
||||||
static let dateFormatter: DateFormatter = {
|
static let dateFormatter: DateFormatter = {
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
formatter.locale = .current
|
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss ZZ"
|
||||||
formatter.setLocalizedDateFormatFromTemplate("yyyy-MM-dd HH:mm:ss ZZ")
|
|
||||||
return formatter
|
return formatter
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -136,7 +143,7 @@ extension Node {
|
||||||
case let .bool(value):
|
case let .bool(value):
|
||||||
return value.description
|
return value.description
|
||||||
case let .datetime(value):
|
case let .datetime(value):
|
||||||
return value.description
|
return Node.dateFormatter.string(from: value)
|
||||||
case .null:
|
case .null:
|
||||||
return "null"
|
return "null"
|
||||||
case let .regex(value):
|
case let .regex(value):
|
||||||
|
@ -152,6 +159,7 @@ extension Node {
|
||||||
case let .int32(value):
|
case let .int32(value):
|
||||||
return value.description
|
return value.description
|
||||||
case let .timestamp(value):
|
case let .timestamp(value):
|
||||||
|
// todo: this needs to include the timestamp increment
|
||||||
let date = Date(timeIntervalSince1970: TimeInterval(value.timestamp))
|
let date = Date(timeIntervalSince1970: TimeInterval(value.timestamp))
|
||||||
return Node.dateFormatter.string(from: date)
|
return Node.dateFormatter.string(from: date)
|
||||||
case let .int64(value):
|
case let .int64(value):
|
||||||
|
@ -226,13 +234,4 @@ extension Node {
|
||||||
return "MaxKey"
|
return "MaxKey"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var isValueCopyable: Bool {
|
|
||||||
switch value {
|
|
||||||
case .document(_), .array(_), .binary(_), .minKey, .maxKey:
|
|
||||||
return false
|
|
||||||
default:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
@objc func outlineCellDoubleClicked() {
|
||||||
if let item = outlineView.item(atRow: outlineView.clickedRow) {
|
if let node = outlineView.item(atRow: outlineView.clickedRow) as? Node {
|
||||||
if outlineView.isItemExpanded(item) {
|
if node.hasChildren {
|
||||||
outlineView.collapseItem(item)
|
if outlineView.isItemExpanded(node) {
|
||||||
} else {
|
outlineView.collapseItem(node)
|
||||||
outlineView.expandItem(item)
|
} 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let wc = EditDocumentWindowController(mongoController: mongoController, collection: collection, document: document)
|
openEditWindow(document)
|
||||||
wc.documentEdited = {
|
|
||||||
self.refresh()
|
|
||||||
self.mongoController.statusManager.set("Updated document", for: .document)
|
|
||||||
}
|
|
||||||
wc.showWindow(nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func copy(_ sender: Any) {
|
@IBAction func copy(_ sender: Any) {
|
||||||
|
@ -205,6 +213,51 @@ class QueryViewController: NSViewController {
|
||||||
NSPasteboard.general.clearContents()
|
NSPasteboard.general.clearContents()
|
||||||
NSPasteboard.general.setString(str, forType: .string)
|
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 {
|
extension QueryViewController: NSMenuItemValidation {
|
||||||
|
@ -273,7 +326,9 @@ extension QueryViewController: NSOutlineViewDelegate {
|
||||||
} else if tableColumn.identifier == .fieldValueColumn {
|
} else if tableColumn.identifier == .fieldValueColumn {
|
||||||
let cell = outlineView.makeView(withIdentifier: .fieldValueCell, owner: nil) as! NSTableCellView
|
let cell = outlineView.makeView(withIdentifier: .fieldValueCell, owner: nil) as! NSTableCellView
|
||||||
cell.textField!.stringValue = node.valueString
|
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
|
return cell
|
||||||
} else if tableColumn.identifier == .valueTypeColumn {
|
} else if tableColumn.identifier == .valueTypeColumn {
|
||||||
let cell = outlineView.makeView(withIdentifier: .valueTypeCell, owner: nil) as! NSTableCellView
|
let cell = outlineView.makeView(withIdentifier: .valueTypeCell, owner: nil) as! NSTableCellView
|
||||||
|
@ -295,3 +350,100 @@ extension NSUserInterfaceItemIdentifier {
|
||||||
static let valueTypeColumn = NSUserInterfaceItemIdentifier(rawValue: "ValueTypeCol")
|
static let valueTypeColumn = NSUserInterfaceItemIdentifier(rawValue: "ValueTypeCol")
|
||||||
static let valueTypeCell = NSUserInterfaceItemIdentifier(rawValue: "ValueTypeCell")
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue