Skip to content

Commit ec19228

Browse files
committed
refactor: split ConnectionFormView and MainContentView into extensions
ConnectionFormView.swift (1321 → 284 lines): - +GeneralTab (218), +Footer (135), +Helpers (726) MainContentView.swift (1137 → 453 lines): - +EventHandlers (299), +Setup (234), +Helpers (129), +Modifiers (71) - +Bindings (128) already existed 7 new extension files. No API changes.
1 parent 8e4658f commit ec19228

9 files changed

Lines changed: 1905 additions & 1814 deletions
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
//
2+
// ConnectionFormView+Footer.swift
3+
// TablePro
4+
//
5+
// Created by Ngo Quoc Dat on 16/12/25.
6+
//
7+
8+
import SwiftUI
9+
import TableProPluginKit
10+
11+
// MARK: - Footer
12+
13+
extension ConnectionFormView {
14+
var footer: some View {
15+
VStack(alignment: .leading, spacing: 8) {
16+
HStack {
17+
// Test connection
18+
Button(action: testConnection) {
19+
HStack(spacing: 6) {
20+
if isTesting {
21+
ProgressView()
22+
.controlSize(.small)
23+
} else if testSucceeded {
24+
Image(systemName: "checkmark.circle.fill")
25+
.foregroundStyle(.green)
26+
} else {
27+
Image(systemName: "bolt.horizontal")
28+
.foregroundStyle(.secondary)
29+
}
30+
Text(testSucceeded ? String(localized: "Connected") : String(localized: "Test Connection"))
31+
}
32+
}
33+
.disabled(isTesting || isInstallingPlugin || !isValid)
34+
35+
Spacer()
36+
37+
// Delete button (edit mode only)
38+
if !isNew {
39+
Button("Delete", role: .destructive) {
40+
Task {
41+
let confirmed = await AlertHelper.confirmDestructive(
42+
title: String(localized: "Delete Connection"),
43+
message: String(localized: "Are you sure you want to delete this connection? This cannot be undone."),
44+
confirmButton: String(localized: "Delete"),
45+
window: NSApp.keyWindow
46+
)
47+
if confirmed {
48+
deleteConnection()
49+
}
50+
}
51+
}
52+
}
53+
54+
// Cancel
55+
Button("Cancel") {
56+
NSApplication.shared.closeWindows(withId: "connection-form")
57+
}
58+
59+
// Save
60+
Button(isNew ? String(localized: "Create") : String(localized: "Save")) {
61+
saveConnection()
62+
}
63+
.keyboardShortcut(.return)
64+
.buttonStyle(.borderedProminent)
65+
.disabled(isInstallingPlugin || !isValid)
66+
}
67+
.padding(.horizontal, 16)
68+
.padding(.vertical, 12)
69+
}
70+
.background(Color(nsColor: .windowBackgroundColor))
71+
.onExitCommand {
72+
NSApplication.shared.closeWindows(withId: "connection-form")
73+
}
74+
.onChange(of: host) { _, _ in testSucceeded = false }
75+
.onChange(of: port) { _, _ in testSucceeded = false }
76+
.onChange(of: username) { _, _ in testSucceeded = false }
77+
.onChange(of: password) { _, _ in testSucceeded = false }
78+
.onChange(of: database) { _, _ in testSucceeded = false }
79+
.onChange(of: type) { _, _ in testSucceeded = false }
80+
.onChange(of: sshEnabled) { _, _ in testSucceeded = false }
81+
.onChange(of: sshHost) { _, _ in testSucceeded = false }
82+
.onChange(of: sshPort) { _, _ in testSucceeded = false }
83+
.onChange(of: sshUsername) { _, _ in testSucceeded = false }
84+
.onChange(of: sshAuthMethod) { _, _ in testSucceeded = false }
85+
.onChange(of: sslMode) { _, _ in testSucceeded = false }
86+
}
87+
88+
// MARK: - Import from URL Sheet
89+
90+
var connectionURLImportSheet: some View {
91+
VStack(spacing: 16) {
92+
Text(String(localized: "Import from URL"))
93+
.font(.headline)
94+
95+
Text(String(localized: "Paste a connection URL to auto-fill the form fields."))
96+
.font(.subheadline)
97+
.foregroundStyle(.secondary)
98+
99+
TextField(
100+
String(localized: "Connection URL"),
101+
text: $connectionURL,
102+
prompt: Text("postgresql://user:password@host:5432/database")
103+
)
104+
.textFieldStyle(.roundedBorder)
105+
106+
if let urlParseError {
107+
Text(urlParseError)
108+
.font(.caption)
109+
.foregroundStyle(.red)
110+
}
111+
112+
HStack {
113+
Button(String(localized: "Cancel")) {
114+
showURLImport = false
115+
}
116+
.keyboardShortcut(.cancelAction)
117+
118+
Spacer()
119+
120+
Button(String(localized: "Import")) {
121+
parseConnectionURL()
122+
if urlParseError == nil && !connectionURL.isEmpty {
123+
connectionURL = ""
124+
urlParseError = nil
125+
showURLImport = false
126+
}
127+
}
128+
.keyboardShortcut(.defaultAction)
129+
.disabled(connectionURL.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
130+
}
131+
}
132+
.padding(20)
133+
.frame(width: 420)
134+
}
135+
}
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
//
2+
// ConnectionFormView+GeneralTab.swift
3+
// TablePro
4+
//
5+
// Created by Ngo Quoc Dat on 16/12/25.
6+
//
7+
8+
import SwiftUI
9+
import TableProPluginKit
10+
11+
// MARK: - General Tab
12+
13+
extension ConnectionFormView {
14+
var generalForm: some View {
15+
Form {
16+
Section {
17+
Picker(String(localized: "Type"), selection: $type) {
18+
ForEach(availableDatabaseTypes) { t in
19+
Label {
20+
HStack {
21+
Text(t.rawValue)
22+
if t.isDownloadablePlugin && !PluginManager.shared.isDriverLoaded(for: t) {
23+
Text("Not Installed")
24+
.font(.caption)
25+
.foregroundStyle(.secondary)
26+
.padding(.horizontal, 4)
27+
.padding(.vertical, 1)
28+
.background(.quaternary, in: RoundedRectangle(cornerRadius: 3))
29+
}
30+
}
31+
} icon: {
32+
t.iconImage
33+
.resizable()
34+
.aspectRatio(contentMode: .fit)
35+
.frame(width: 20, height: 20)
36+
}
37+
.tag(t)
38+
}
39+
}
40+
.disabled(isInstallingPlugin)
41+
TextField(
42+
String(localized: "Name"),
43+
text: $name,
44+
prompt: Text("Connection name")
45+
)
46+
Button {
47+
showURLImport = true
48+
} label: {
49+
Label(String(localized: "Import from URL"), systemImage: "link")
50+
}
51+
}
52+
53+
if type.isDownloadablePlugin && !PluginManager.shared.isDriverLoaded(for: type) {
54+
Section {
55+
LabeledContent(String(localized: "Plugin")) {
56+
if isInstallingPlugin {
57+
HStack(spacing: 6) {
58+
ProgressView()
59+
.controlSize(.small)
60+
Text("Installing...")
61+
.foregroundStyle(.secondary)
62+
}
63+
} else if let error = pluginInstallError {
64+
HStack(spacing: 6) {
65+
Text(error)
66+
.foregroundStyle(.red)
67+
.font(.caption)
68+
.lineLimit(2)
69+
Button("Retry") {
70+
pluginInstallError = nil
71+
installPlugin(for: type)
72+
}
73+
.controlSize(.small)
74+
}
75+
} else {
76+
HStack(spacing: 6) {
77+
Text("Not Installed")
78+
.foregroundStyle(.secondary)
79+
Button("Install") {
80+
installPlugin(for: type)
81+
}
82+
.controlSize(.small)
83+
}
84+
}
85+
}
86+
}
87+
} else if PluginManager.shared.connectionMode(for: type) == .fileBased {
88+
Section(String(localized: "Database File")) {
89+
HStack {
90+
TextField(
91+
String(localized: "File Path"),
92+
text: $database,
93+
prompt: Text(filePathPrompt)
94+
)
95+
Button(String(localized: "Browse...")) { browseForFile() }
96+
.controlSize(.small)
97+
}
98+
}
99+
} else if PluginManager.shared.connectionMode(for: type) == .apiOnly {
100+
if PluginManager.shared.supportsDatabaseSwitching(for: type) {
101+
Section(String(localized: "Connection")) {
102+
TextField(
103+
String(localized: "Database"),
104+
text: $database,
105+
prompt: Text("database_name")
106+
)
107+
}
108+
}
109+
} else {
110+
Section(String(localized: "Connection")) {
111+
TextField(
112+
String(localized: "Host"),
113+
text: $host,
114+
prompt: Text("localhost")
115+
)
116+
TextField(
117+
String(localized: "Port"),
118+
text: $port,
119+
prompt: Text(defaultPort)
120+
)
121+
if PluginManager.shared.requiresAuthentication(for: type) {
122+
TextField(
123+
String(localized: "Database"),
124+
text: $database,
125+
prompt: Text("database_name")
126+
)
127+
}
128+
}
129+
}
130+
131+
if PluginManager.shared.connectionMode(for: type) != .fileBased {
132+
Section(String(localized: "Authentication")) {
133+
if PluginManager.shared.requiresAuthentication(for: type)
134+
&& PluginManager.shared.connectionMode(for: type) != .apiOnly
135+
{
136+
TextField(
137+
String(localized: "Username"),
138+
text: $username,
139+
prompt: Text("root")
140+
)
141+
}
142+
ForEach(authSectionFields, id: \.id) { field in
143+
if isFieldVisible(field) {
144+
ConnectionFieldRow(
145+
field: field,
146+
value: Binding(
147+
get: {
148+
additionalFieldValues[field.id]
149+
?? field.defaultValue ?? ""
150+
},
151+
set: { additionalFieldValues[field.id] = $0 }
152+
)
153+
)
154+
}
155+
}
156+
if !hidePasswordField {
157+
PasswordPromptToggle(
158+
type: type,
159+
promptForPassword: $promptForPassword,
160+
password: $password,
161+
additionalFieldValues: $additionalFieldValues
162+
)
163+
}
164+
if additionalFieldValues["usePgpass"] == "true" {
165+
pgpassStatusView
166+
}
167+
}
168+
}
169+
170+
Section(String(localized: "Appearance")) {
171+
LabeledContent(String(localized: "Color")) {
172+
ConnectionColorPicker(selectedColor: $connectionColor)
173+
}
174+
LabeledContent(String(localized: "Tag")) {
175+
ConnectionTagEditor(selectedTagId: $selectedTagId)
176+
}
177+
LabeledContent(String(localized: "Group")) {
178+
ConnectionGroupPicker(selectedGroupId: $selectedGroupId)
179+
}
180+
let isProUnlocked = LicenseManager.shared.isFeatureAvailable(.safeMode)
181+
Picker(String(localized: "Safe Mode"), selection: $safeModeLevel) {
182+
ForEach(SafeModeLevel.allCases) { level in
183+
if level.requiresPro && !isProUnlocked {
184+
Text("\(level.displayName) (Pro)").tag(level)
185+
} else {
186+
Text(level.displayName).tag(level)
187+
}
188+
}
189+
}
190+
.onChange(of: safeModeLevel) { oldValue, newValue in
191+
if newValue.requiresPro && !isProUnlocked {
192+
safeModeLevel = oldValue
193+
showSafeModeProAlert = true
194+
}
195+
}
196+
.alert(
197+
String(localized: "Pro License Required"),
198+
isPresented: $showSafeModeProAlert
199+
) {
200+
Button(String(localized: "Activate License...")) {
201+
showActivationSheet = true
202+
}
203+
Button(String(localized: "OK"), role: .cancel) {}
204+
} message: {
205+
Text(String(localized: "Safe Mode, Safe Mode (Full), and Read-Only require a Pro license."))
206+
}
207+
}
208+
}
209+
.formStyle(.grouped)
210+
.scrollContentBackground(.hidden)
211+
.sheet(isPresented: $showURLImport) {
212+
connectionURLImportSheet
213+
}
214+
.sheet(isPresented: $showActivationSheet) {
215+
LicenseActivationSheet()
216+
}
217+
}
218+
}

0 commit comments

Comments
 (0)