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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Interface, Sidebar and Toolbar color groups from the theme editor and the theme file.
- Fonts from the theme file.
- Clipboard URL banner in the New Connection form. Import from URL fills the form from a pasted link.
- Share > Copy as JSON in the connection list. Copy as JSON in the data grid stays.
- Export as SQL in the ER diagram. File > Export > Export Tables… to SQL with Structure writes the DDL.
- Enum label editing and New Type… for PostgreSQL types. Run `ALTER TYPE` or `CREATE TYPE` in a query tab.
- Server Dashboard for SQLite and DuckDB.
Expand Down
11 changes: 0 additions & 11 deletions TablePro/Core/Services/Export/ConnectionShareLink.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,17 +89,6 @@ internal enum ConnectionShareLink {
return link
}

static func compactJSON(for connection: DatabaseConnection, exporter: ConnectionBundleExporter = .init()) -> String {
let encoder = JSONEncoder()
encoder.outputFormatting = .sortedKeys
guard let data = try? encoder.encode(exporter.portableSettings(for: connection)),
let json = String(data: data, encoding: .utf8) else {
logger.warning("Could not encode '\(connection.name, privacy: .private)' as JSON")
return "{}"
}
return json
}

private static func sshQueryItems(_ ssh: ExportableSSHConfig) -> [URLQueryItem] {
var items = [
URLQueryItem(name: "ssh", value: "1"),
Expand Down

This file was deleted.

2 changes: 2 additions & 0 deletions TablePro/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -202479,6 +202479,7 @@
}
},
"Use" : {
"extractionState" : "stale",
"localizations" : {
"fr" : {
"stringUnit" : {
Expand Down Expand Up @@ -202781,6 +202782,7 @@
}
},
"Use clipboard URL" : {
"extractionState" : "stale",
"localizations" : {
"fr" : {
"stringUnit" : {
Expand Down
3 changes: 0 additions & 3 deletions TablePro/ViewModels/WelcomeViewModel+Library.swift
Original file line number Diff line number Diff line change
Expand Up @@ -441,9 +441,6 @@ extension WelcomeViewModel {
guard let connection = connectionsById[id],
let link = ConnectionShareLink.deeplink(for: connection) else { return }
ClipboardService.shared.writeText(link)
case .copyJSON(let id):
guard let connection = connectionsById[id] else { return }
ClipboardService.shared.writeText(ConnectionShareLink.compactJSON(for: connection))
case .exportToFile(let ids):
exportConnections(ids.compactMap { connectionsById[$0] })
case .publishToTeamCatalog(let ids):
Expand Down

This file was deleted.

42 changes: 0 additions & 42 deletions TablePro/Views/ConnectionForm/ConnectionFormCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,6 @@ final class ConnectionFormCoordinator: ObservableObject {

@Published var saveError: String?

@Published var clipboardCandidate: ClipboardConnectionCandidate?
@Published var clipboardBannerDismissed: Bool = false

@Published var isChoosingType: Bool = false


Expand Down Expand Up @@ -1044,43 +1041,4 @@ final class ConnectionFormCoordinator: ObservableObject {
advanced.additionalFieldValues[fieldId] = value
}
}

// MARK: - Clipboard

func detectClipboardConnectionStringIfNeeded(
connectionStorage: ConnectionStorage = .shared,
pasteboard: NSPasteboard = .general
) {
guard isNew, !clipboardBannerDismissed, clipboardCandidate == nil else { return }
guard let raw = pasteboard.string(forType: .string),
let candidate = ClipboardConnectionCandidate(clipboardText: raw),
!matchesExistingConnection(candidate.parsed, connectionStorage: connectionStorage)
else { return }
clipboardCandidate = candidate
}

func applyClipboardCandidate(_ candidate: ClipboardConnectionCandidate) {
applyParsed(candidate.parsed)
auth.promptForPassword = false
normalizeTransport()
clipboardCandidate = nil
}

func dismissClipboardCandidate() {
clipboardCandidate = nil
clipboardBannerDismissed = true
}

private func matchesExistingConnection(
_ parsed: ParsedConnectionURL,
connectionStorage: ConnectionStorage
) -> Bool {
connectionStorage.loadConnections().contains { saved in
guard saved.username == parsed.username, saved.localSocketPath == parsed.localSocketPath else {
return false
}
return parsed.localSocketPath != nil
|| (saved.host == parsed.host && saved.port == parsed.resolvedPort)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ internal final class ConnectionFormWindowController: NSWindowController, NSWindo
/// form's save, cancel and delete paths need an explicit way to close the window.
coordinator.dismissAction = { ConnectionFormWindowController.close(request) }
coordinator.start()
coordinator.detectClipboardConnectionStringIfNeeded()

let split = ConnectionFormSplitViewController(coordinator: coordinator)

Expand Down
11 changes: 0 additions & 11 deletions TablePro/Views/ConnectionForm/Panes/GeneralPaneView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,6 @@ struct GeneralPaneView: View {

var body: some View {
Form {
if let candidate = coordinator.clipboardCandidate {
Section {
ClipboardConnectionBanner(
candidate: candidate,
onUse: { coordinator.applyClipboardCandidate(candidate) },
onDismiss: { coordinator.dismissClipboardCandidate() }
)
.listRowInsets(EdgeInsets())
}
}

identitySection
connectionSection
authenticationSection
Expand Down
3 changes: 0 additions & 3 deletions TablePro/Views/Welcome/WelcomeMenuSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ internal enum WelcomeMenuCommand: Equatable {
case setFavorite([UUID], Bool)
case copyConnectionString(UUID)
case copyTableProLink(UUID)
case copyJSON(UUID)
case exportToFile([UUID])
case publishToTeamCatalog([UUID])
case publishToTeamLibrary([UUID])
Expand Down Expand Up @@ -234,7 +233,6 @@ internal enum WelcomeMenuSpec {
WelcomeMenuSection([
.command(Titles.copyConnectionString, .copyConnectionString(id)),
.command(Titles.copyTableProLink, .copyTableProLink(id)),
.command(Titles.copyJSON, .copyJSON(id)),
]),
WelcomeMenuSection(publish),
])
Expand Down Expand Up @@ -378,7 +376,6 @@ internal extension WelcomeMenuSpec {
static var share: String { String(localized: "Share") }
static var copyConnectionString: String { String(localized: "Copy Connection String") }
static var copyTableProLink: String { String(localized: "Copy TablePro Link") }
static var copyJSON: String { String(localized: "Copy as JSON") }
static var moveToGroup: String { String(localized: "Move to Group") }
static var removeFromGroup: String { String(localized: "Remove from Group") }
static var includeInSync: String { String(localized: "Include in iCloud Sync") }
Expand Down
5 changes: 1 addition & 4 deletions TableProTests/Core/Services/ConnectionSharingIconTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,16 @@ struct ConnectionSharingIconTests {

// MARK: - Connection icon

@Test("A file export and Copy as JSON carry the connection's icon")
@Test("A file export carries the connection's icon")
func exportCarriesIcon() throws {
let library = try ImportLibraryFixture()
defer { library.cleanUp() }
var connection = DatabaseConnection(name: "Prod", type: .postgresql)
connection.iconName = "server.rack"

let bundle = try library.exporter.connectionsOnlyBundle(for: [connection])
let json = ConnectionShareLink.compactJSON(for: connection, exporter: library.exporter)
let fromJSON = try JSONDecoder().decode(ExportableConnection.self, from: Data(json.utf8))

#expect(bundle.connections.first?.settings.iconName == "server.rack")
#expect(fromJSON.iconName == "server.rack")
}

@Test("Copy TablePro Link carries the icon and the import keeps it")
Expand Down
66 changes: 3 additions & 63 deletions TableProTests/Core/Services/ConnectionSharingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -355,70 +355,10 @@ struct ConnectionSharingTests {
}
}

// MARK: - Copy as JSON

@Suite("Copy as JSON")
struct BuildCompactJSONTests {
@Test("Returns valid JSON")
@MainActor
func testReturnsValidJSON() throws {
let conn = DatabaseConnection(
name: "Dev", host: "localhost", port: 3_306,
database: "mydb", username: "root", type: .mysql
)
let json = ConnectionShareLink.compactJSON(for: conn)
let data = try #require(json.data(using: .utf8))
let decoded = try? JSONDecoder().decode(ExportableConnection.self, from: data)
#expect(decoded != nil)
#expect(decoded?.name == "Dev")
#expect(decoded?.host == "localhost")
#expect(decoded?.type == "MySQL")
}

@Test("Excludes SSH profile ID")
@MainActor
func testExcludesSSHProfileId() throws {
let conn = DatabaseConnection(
name: "Dev", host: "localhost", port: 3_306,
database: "", username: "", type: .mysql
)
let json = ConnectionShareLink.compactJSON(for: conn)
let data = try #require(json.data(using: .utf8))
let decoded = try JSONDecoder().decode(ExportableConnection.self, from: data)
#expect(decoded.sshProfileId == nil)
}

@Test("Is compact (not pretty printed)")
@MainActor
func testIsCompact() {
let conn = DatabaseConnection(
name: "Dev", host: "localhost", port: 3_306,
database: "", username: "", type: .mysql
)
let json = ConnectionShareLink.compactJSON(for: conn)
#expect(!json.contains("\n "))
}

@Test("Includes SSH config when enabled")
@MainActor
func testIncludesSSHInJSON() throws {
var ssh = SSHConfiguration()
ssh.enabled = true
ssh.host = "bastion.com"
ssh.port = 22
ssh.username = "admin"
let conn = DatabaseConnection(
name: "SSH", host: "db.com", port: 5_432,
database: "", username: "", type: .postgresql,
sshConfig: ssh
)
let json = ConnectionShareLink.compactJSON(for: conn)
let data = try #require(json.data(using: .utf8))
let decoded = try JSONDecoder().decode(ExportableConnection.self, from: data)
#expect(decoded.sshConfig != nil)
#expect(decoded.sshConfig?.host == "bastion.com")
}
// MARK: - Export envelope

@Suite("Export envelope")
struct ExportEnvelopeTests {
@Test("Export files and the Team Library leave out the fields every importer drops")
@MainActor
func testEnvelopeLeavesOutImportBlockedFields() throws {
Expand Down
21 changes: 0 additions & 21 deletions TableProTests/Core/Services/Export/ConnectionShareLinkTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,25 +88,4 @@ struct ConnectionShareLinkTests {

#expect(URLComponents(string: link)?.queryItems?.first { $0.name == "sslMode" }?.value == "Verify Identity")
}

@Test("Copy as JSON is the settings object alone")
func compactJSONIsSettingsOnly() throws {
let library = try ImportLibraryFixture()
defer { library.cleanUp() }
let group = ConnectionGroup(name: "Production")
try library.groups.addGroup(group)
var connection = DatabaseConnection(name: "Orders", host: "db.example.com", port: 5_432, type: .postgresql)
connection.groupId = group.id

let json = ConnectionShareLink.compactJSON(for: connection, exporter: library.exporter)

let object = try #require(
try JSONSerialization.jsonObject(with: Data(json.utf8)) as? [String: Any]
)
#expect(object["name"] as? String == "Orders")
#expect(object["groupName"] == nil)
#expect(object["groupRef"] == nil)
#expect(object["ref"] == nil)
#expect(!json.contains("\n"))
}
}
Loading
Loading