Skip to content

Commit c34f2a4

Browse files
authored
feat: beautified JSON display in Details sidebar with syntax highlighting (#539)
* feat: beautified JSON display in Details sidebar with syntax highlighting * refactor: decompose Details sidebar into modular field editor architecture
1 parent d001677 commit c34f2a4

27 files changed

+1102
-544
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
- Auto-fit column width: double-click column divider or right-click → "Size to Fit"
1717
- Collapsible results panel (`Cmd+Opt+R`), multiple result tabs for multi-statement queries, result pinning
1818
- Inline error banner for query errors
19+
- JSON syntax highlighting and brace matching in Details sidebar and JSON editor popover
20+
- Database-aware SQL functions in field menu (MySQL, PostgreSQL, SQLite, SQL Server, ClickHouse)
1921

2022
### Changed
2123

2224
- Replace GCD dispatch patterns with Swift structured concurrency
25+
- Refactor Details sidebar into modular field editor architecture with extracted editor components
2326

2427
### Fixed
2528

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
//
2+
// SQLFunctionProvider.swift
3+
// TablePro
4+
5+
internal enum SQLFunctionProvider {
6+
internal struct SQLFunction {
7+
let label: String
8+
let expression: String
9+
}
10+
11+
static func functions(for databaseType: DatabaseType) -> [SQLFunction] {
12+
if databaseType == .mysql || databaseType == .mariadb {
13+
return [
14+
SQLFunction(label: "NOW()", expression: "NOW()"),
15+
SQLFunction(label: "CURRENT_TIMESTAMP()", expression: "CURRENT_TIMESTAMP()"),
16+
SQLFunction(label: "CURDATE()", expression: "CURDATE()"),
17+
SQLFunction(label: "CURTIME()", expression: "CURTIME()"),
18+
SQLFunction(label: "UTC_TIMESTAMP()", expression: "UTC_TIMESTAMP()"),
19+
SQLFunction(label: "UUID()", expression: "UUID()")
20+
]
21+
} else if databaseType == .postgresql || databaseType == .redshift {
22+
return [
23+
SQLFunction(label: "now()", expression: "now()"),
24+
SQLFunction(label: "CURRENT_TIMESTAMP", expression: "CURRENT_TIMESTAMP"),
25+
SQLFunction(label: "CURRENT_DATE", expression: "CURRENT_DATE"),
26+
SQLFunction(label: "CURRENT_TIME", expression: "CURRENT_TIME"),
27+
SQLFunction(label: "gen_random_uuid()", expression: "gen_random_uuid()")
28+
]
29+
} else if databaseType == .sqlite || databaseType == .duckdb || databaseType == .cloudflareD1 {
30+
return [
31+
SQLFunction(label: "datetime('now')", expression: "datetime('now')"),
32+
SQLFunction(label: "date('now')", expression: "date('now')"),
33+
SQLFunction(label: "time('now')", expression: "time('now')"),
34+
SQLFunction(label: "datetime('now','localtime')", expression: "datetime('now','localtime')")
35+
]
36+
} else if databaseType == .mssql {
37+
return [
38+
SQLFunction(label: "GETDATE()", expression: "GETDATE()"),
39+
SQLFunction(label: "GETUTCDATE()", expression: "GETUTCDATE()"),
40+
SQLFunction(label: "SYSDATETIME()", expression: "SYSDATETIME()"),
41+
SQLFunction(label: "NEWID()", expression: "NEWID()")
42+
]
43+
} else if databaseType == .clickhouse {
44+
return [
45+
SQLFunction(label: "now()", expression: "now()"),
46+
SQLFunction(label: "today()", expression: "today()"),
47+
SQLFunction(label: "yesterday()", expression: "yesterday()"),
48+
SQLFunction(label: "generateUUIDv4()", expression: "generateUUIDv4()")
49+
]
50+
} else {
51+
return [
52+
SQLFunction(label: "CURRENT_TIMESTAMP", expression: "CURRENT_TIMESTAMP"),
53+
SQLFunction(label: "CURRENT_DATE", expression: "CURRENT_DATE"),
54+
SQLFunction(label: "CURRENT_TIME", expression: "CURRENT_TIME")
55+
]
56+
}
57+
}
58+
}

TablePro/Extensions/String+JSON.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@
88
import Foundation
99

1010
extension String {
11+
/// Returns true if this string looks like a JSON object or array (starts with `{`/`[` and parses successfully).
12+
/// Only checks objects and arrays to avoid false positives with bare primitives like `"hello"`, `123`, `true`.
13+
var looksLikeJson: Bool {
14+
let trimmed = unicodeScalars.first
15+
guard trimmed == "{" || trimmed == "[" else { return false }
16+
guard let data = data(using: .utf8) else { return false }
17+
return (try? JSONSerialization.jsonObject(with: data)) != nil
18+
}
19+
1120
/// Returns a pretty-printed version of this string if it contains valid JSON, or nil otherwise.
1221
func prettyPrintedAsJson() -> String? {
1322
guard let data = data(using: .utf8),

TablePro/Models/UI/MultiRowEditState.swift

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import Foundation
1010
import Observation
1111

1212
/// Represents the edit state for a single field across multiple rows
13-
struct FieldEditState {
13+
struct FieldEditState: Identifiable {
14+
var id = UUID()
1415
let columnIndex: Int
1516
let columnName: String
1617
let columnTypeEnum: ColumnType
@@ -101,8 +102,8 @@ final class MultiRowEditState {
101102
}
102103

103104
// Check if all values are the same
104-
let uniqueValues = Set(values.map { $0 ?? "__NULL__" })
105-
let hasMultipleValues = uniqueValues.count > 1
105+
let allSame = values.dropFirst().allSatisfy { $0 == values.first }
106+
let hasMultipleValues = !allSame
106107

107108
let originalValue: String?
108109
if hasMultipleValues {
@@ -113,6 +114,7 @@ final class MultiRowEditState {
113114
}
114115

115116
// Preserve pending edits if data hasn't changed
117+
var preservedId: UUID?
116118
var pendingValue: String?
117119
var isPendingNull = false
118120
var isPendingDefault = false
@@ -126,6 +128,7 @@ final class MultiRowEditState {
126128
let oldField = fields[colIndex]
127129
// Preserve pending edits when original data matches
128130
if oldField.originalValue == originalValue && oldField.hasMultipleValues == hasMultipleValues {
131+
preservedId = oldField.id
129132
pendingValue = oldField.pendingValue
130133
isPendingNull = oldField.isPendingNull
131134
isPendingDefault = oldField.isPendingDefault
@@ -143,7 +146,7 @@ final class MultiRowEditState {
143146
pendingValue = originalValue ?? ""
144147
}
145148

146-
newFields.append(FieldEditState(
149+
var newField = FieldEditState(
147150
columnIndex: colIndex,
148151
columnName: columnName,
149152
columnTypeEnum: columnTypeEnum,
@@ -155,7 +158,11 @@ final class MultiRowEditState {
155158
isPendingDefault: isPendingDefault,
156159
isTruncated: preservedIsTruncated,
157160
isLoadingFullValue: preservedIsLoadingFullValue
158-
))
161+
)
162+
if let preservedId {
163+
newField.id = preservedId
164+
}
165+
newFields.append(newField)
159166
}
160167

161168
self.fields = newFields

0 commit comments

Comments
 (0)