Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Toolbar briefly showing "MySQL" and missing version (e.g., "MongoDB" instead of "MongoDB 8.2.5") when opening a new tab
- Keyboard shortcuts not working (beep sound) after connecting from welcome screen until a second tab is opened

### Changed

- Redesigned right sidebar detail pane with compact field layout and type-aware editors

## [0.10.0] - 2026-03-01

### Added
Expand Down
18 changes: 17 additions & 1 deletion TablePro/Core/ChangeTracking/DataChangeManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,23 @@ final class DataChangeManager: ObservableObject {
newValue: String?,
originalRow: [String?]? = nil
) {
guard oldValue != newValue else { return }
if oldValue == newValue {
let updateKey = RowChangeKey(rowIndex: rowIndex, type: .update)
if let existingIndex = changeIndex[updateKey],
let cellIndex = changes[existingIndex].cellChanges.firstIndex(where: { $0.columnIndex == columnIndex }) {
let originalOldValue = changes[existingIndex].cellChanges[cellIndex].oldValue
if originalOldValue == newValue {
changes[existingIndex].cellChanges.remove(at: cellIndex)
modifiedCells[rowIndex]?.remove(columnIndex)
if modifiedCells[rowIndex]?.isEmpty == true { modifiedCells.removeValue(forKey: rowIndex) }
if changes[existingIndex].cellChanges.isEmpty { removeChangeAt(existingIndex) }
changedRowIndices.insert(rowIndex)
hasChanges = !changes.isEmpty
reloadVersion += 1
}
}
return
}

// New changes invalidate redo history (standard undo/redo behavior)
if !isRedoing { undoManager.clearRedo() }
Expand Down
21 changes: 21 additions & 0 deletions TablePro/Core/Services/ColumnType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,27 @@ enum ColumnType: Equatable {
}
}

var isBooleanType: Bool {
switch self {
case .boolean: return true
default: return false
}
}

/// Compact lowercase badge label for sidebar
var badgeLabel: String {
switch self {
case .boolean: return "bool"
case .json: return "json"
case .date, .timestamp, .datetime: return "date"
case .enumType: return "enum"
case .set: return "set"
case .integer, .decimal: return "number"
case .blob: return "binary"
case .text: return "string"
}
}

/// The allowed enum/set values, if known
var enumValues: [String]? {
switch self {
Expand Down
24 changes: 24 additions & 0 deletions TablePro/Extensions/String+JSON.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// String+JSON.swift
// TablePro
//
// JSON formatting utilities for string values.
//

import Foundation

extension String {
/// Returns a pretty-printed version of this string if it contains valid JSON, or nil otherwise.
func prettyPrintedAsJson() -> String? {
guard let data = data(using: .utf8),
let jsonObject = try? JSONSerialization.jsonObject(with: data),
let prettyData = try? JSONSerialization.data(
withJSONObject: jsonObject,
options: [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
),
let prettyString = String(data: prettyData, encoding: .utf8) else {
return nil
}
return prettyString
}
}
52 changes: 42 additions & 10 deletions TablePro/Models/MultiRowEditState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import Foundation
struct FieldEditState {
let columnIndex: Int
let columnName: String
let columnType: String
let isLongText: Bool // NEW: Whether to use multi-line editor
let columnTypeEnum: ColumnType
let isLongText: Bool

/// Original values from all selected rows (nil if multiple different values)
let originalValue: String?
Expand Down Expand Up @@ -51,6 +51,8 @@ struct FieldEditState {
class MultiRowEditState: ObservableObject {
@Published var fields: [FieldEditState] = []

var onFieldChanged: ((Int, String?) -> Void)?

private(set) var selectedRowIndices: Set<Int> = []
private(set) var allRows: [[String?]] = []
private(set) var columns: [String] = []
Expand All @@ -68,7 +70,8 @@ class MultiRowEditState: ObservableObject {
columnTypes: [ColumnType] // Changed from [String] to [ColumnType]
) {
// Check if the underlying data has changed (not just edits)
let dataChanged = self.allRows != allRows || self.columns != columns
let columnsChanged = self.columns != columns
let selectionChanged = self.selectedRowIndices != selectedRowIndices

self.selectedRowIndices = selectedRowIndices
self.allRows = allRows
Expand All @@ -80,7 +83,6 @@ class MultiRowEditState: ObservableObject {

for (colIndex, columnName) in columns.enumerated() {
let columnTypeEnum = colIndex < columnTypes.count ? columnTypes[colIndex] : ColumnType.text(rawType: nil)
let columnType = columnTypeEnum.displayName
let isLongText = columnTypeEnum.isLongText

// Gather values from all selected rows
Expand All @@ -107,17 +109,19 @@ class MultiRowEditState: ObservableObject {
var isPendingNull = false
var isPendingDefault = false

if !dataChanged, colIndex < fields.count {
if !columnsChanged, !selectionChanged, colIndex < fields.count {
let oldField = fields[colIndex]
pendingValue = oldField.pendingValue
isPendingNull = oldField.isPendingNull
isPendingDefault = oldField.isPendingDefault
if oldField.originalValue == originalValue && oldField.hasMultipleValues == hasMultipleValues {
pendingValue = oldField.pendingValue
isPendingNull = oldField.isPendingNull
isPendingDefault = oldField.isPendingDefault
}
}

newFields.append(FieldEditState(
columnIndex: colIndex,
columnName: columnName,
columnType: columnType,
columnTypeEnum: columnTypeEnum,
isLongText: isLongText,
originalValue: originalValue,
hasMultipleValues: hasMultipleValues,
Expand All @@ -133,9 +137,18 @@ class MultiRowEditState: ObservableObject {
/// Update a field's pending value
func updateField(at index: Int, value: String?) {
guard index < fields.count else { return }
fields[index].pendingValue = value
let hadPendingEdit = fields[index].hasEdit
let original = fields[index].originalValue
if value == original || (original == nil && value == "") {
fields[index].pendingValue = nil
} else {
fields[index].pendingValue = value
}
fields[index].isPendingNull = false
fields[index].isPendingDefault = false
if fields[index].pendingValue != nil || hadPendingEdit {
onFieldChanged?(index, value)
}
}

/// Set a field to NULL
Expand All @@ -144,6 +157,7 @@ class MultiRowEditState: ObservableObject {
fields[index].pendingValue = nil
fields[index].isPendingNull = true
fields[index].isPendingDefault = false
onFieldChanged?(index, nil)
}

/// Set a field to DEFAULT
Expand All @@ -152,6 +166,7 @@ class MultiRowEditState: ObservableObject {
fields[index].pendingValue = nil
fields[index].isPendingNull = false
fields[index].isPendingDefault = true
onFieldChanged?(index, "__DEFAULT__")
}

/// Set a field to a SQL function (e.g., NOW())
Expand All @@ -160,6 +175,23 @@ class MultiRowEditState: ObservableObject {
fields[index].pendingValue = function
fields[index].isPendingNull = false
fields[index].isPendingDefault = false
onFieldChanged?(index, function)
}

/// Set a field to empty string
func setFieldToEmpty(at index: Int) {
guard index < fields.count else { return }
let hadPendingEdit = fields[index].hasEdit
if fields[index].originalValue == "" {
fields[index].pendingValue = nil
} else {
fields[index].pendingValue = ""
}
fields[index].isPendingNull = false
fields[index].isPendingDefault = false
if fields[index].pendingValue != nil || hadPendingEdit {
onFieldChanged?(index, "")
}
}

/// Clear all pending edits
Expand Down
28 changes: 28 additions & 0 deletions TablePro/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -2310,6 +2310,9 @@
}
}
}
},
"Copy Value" : {

},
"Copy with Headers" : {
"localizations" : {
Expand Down Expand Up @@ -2795,6 +2798,9 @@
}
}
}
},
"DEFAULT" : {

},
"Default Column" : {
"localizations" : {
Expand Down Expand Up @@ -3922,6 +3928,9 @@
}
}
}
},
"false" : {

},
"FALSE" : {
"extractionState" : "stale",
Expand Down Expand Up @@ -5225,6 +5234,9 @@
},
"MQL Preview" : {

},
"Multiple values" : {

},
"Name" : {
"localizations" : {
Expand Down Expand Up @@ -5549,6 +5561,9 @@
}
}
}
},
"No matching fields" : {

},
"No Matching Queries" : {
"localizations" : {
Expand Down Expand Up @@ -6304,6 +6319,9 @@
}
}
}
},
"Pretty Print" : {

},
"Pretty print (formatted output)" : {
"localizations" : {
Expand Down Expand Up @@ -7234,6 +7252,9 @@
}
}
}
},
"Search for field..." : {

},
"Search or type..." : {
"localizations" : {
Expand Down Expand Up @@ -7420,6 +7441,9 @@
}
}
}
},
"Set EMPTY" : {

},
"Set NULL" : {
"localizations" : {
Expand All @@ -7432,6 +7456,7 @@
}
},
"Set special value" : {
"extractionState" : "stale",
"localizations" : {
"vi" : {
"stringUnit" : {
Expand Down Expand Up @@ -8569,6 +8594,9 @@
}
}
}
},
"true" : {

},
"TRUE" : {
"extractionState" : "stale",
Expand Down
24 changes: 24 additions & 0 deletions TablePro/Views/MainContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ struct MainContentView: View {
onCellEdit: { rowIndex, colIndex, value in
coordinator.updateCellInTab(
rowIndex: rowIndex, columnIndex: colIndex, value: value)
scheduleInspectorUpdate()
},
onSort: { columnIndex, ascending, isMultiSort in
coordinator.handleSort(
Expand Down Expand Up @@ -771,6 +772,7 @@ struct MainContentView: View {
!selectedRowIndices.isEmpty
else {
rightPanelState.editState.fields = []
rightPanelState.editState.onFieldChanged = nil
return
}

Expand All @@ -787,6 +789,28 @@ struct MainContentView: View {
columns: tab.resultColumns,
columnTypes: tab.columnTypes
)

let capturedCoordinator = coordinator
let capturedEditState = rightPanelState.editState
rightPanelState.editState.onFieldChanged = { columnIndex, newValue in
guard let tab = capturedCoordinator.tabManager.selectedTab else { return }
let columnName = columnIndex < tab.resultColumns.count ? tab.resultColumns[columnIndex] : ""

for rowIndex in capturedEditState.selectedRowIndices {
guard rowIndex < tab.resultRows.count else { continue }
let originalRow = tab.resultRows[rowIndex].values
let oldValue = columnIndex < originalRow.count ? originalRow[columnIndex] : nil

capturedCoordinator.changeManager.recordCellChange(
rowIndex: rowIndex,
columnIndex: columnIndex,
columnName: columnName,
oldValue: oldValue,
newValue: newValue,
originalRow: originalRow
)
}
}
}

// MARK: - Inspector Context
Expand Down
15 changes: 1 addition & 14 deletions TablePro/Views/Results/JSONEditorContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ struct JSONEditorContentView: View {
self.initialValue = initialValue
self.onCommit = onCommit
self.onDismiss = onDismiss
self._text = State(initialValue: Self.prettyPrint(initialValue) ?? initialValue ?? "")
self._text = State(initialValue: initialValue?.prettyPrintedAsJson() ?? initialValue ?? "")
}

var body: some View {
Expand Down Expand Up @@ -79,19 +79,6 @@ struct JSONEditorContentView: View {

// MARK: - JSON Helpers

private static func prettyPrint(_ jsonString: String?) -> String? {
guard let data = jsonString?.data(using: .utf8),
let jsonObject = try? JSONSerialization.jsonObject(with: data),
let prettyData = try? JSONSerialization.data(
withJSONObject: jsonObject,
options: [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
),
let prettyString = String(data: prettyData, encoding: .utf8) else {
return nil
}
return prettyString
}

private static func compact(_ jsonString: String?) -> String? {
guard let data = jsonString?.data(using: .utf8),
let jsonObject = try? JSONSerialization.jsonObject(with: data),
Expand Down
Loading