Skip to content

Commit f524b0d

Browse files
cevianclaude
andcommitted
refactor(dev-ui): redesign postgres connection form UX
Connection string is now always visible and primary, with individual fields collapsed behind a toggle. Mutual disabling prevents filling both modes. Nickname moved to top. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 832ebd7 commit f524b0d

File tree

1 file changed

+72
-64
lines changed

1 file changed

+72
-64
lines changed

packages/core/dev-ui/src/components/CustomConnectionForm.tsx

Lines changed: 72 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ export function CustomConnectionForm({
1414
onSuccess,
1515
onCancel,
1616
}: CustomConnectionFormProps) {
17-
const [mode, setMode] = useState<"fields" | "url">("fields");
17+
const hasConnectionString = !!config.parseConnectionString;
1818
const [connectionString, setConnectionString] = useState("");
19+
const [fieldsExpanded, setFieldsExpanded] = useState(!hasConnectionString);
1920
const [values, setValues] = useState<Record<string, string>>(() => {
2021
const defaults: Record<string, string> = {};
2122
for (const field of config.fields) {
@@ -26,16 +27,19 @@ export function CustomConnectionForm({
2627
const [error, setError] = useState<string | null>(null);
2728
const [submitting, setSubmitting] = useState(false);
2829

30+
// Detect which mode is active based on what the user has filled in
31+
const usingConnectionString = hasConnectionString && connectionString.trim().length > 0;
32+
2933
const setValue = useCallback((name: string, value: string) => {
3034
setValues((prev) => ({ ...prev, [name]: value }));
3135
}, []);
3236

3337
const handleSubmit = useCallback(async () => {
3438
setError(null);
3539

36-
// If in URL mode, parse first
3740
let finalValues = { ...values };
38-
if (mode === "url" && connectionString.trim()) {
41+
42+
if (usingConnectionString) {
3943
if (!config.parseConnectionString) return;
4044
try {
4145
const parsed = config.parseConnectionString(connectionString.trim());
@@ -44,13 +48,13 @@ export function CustomConnectionForm({
4448
setError("Invalid connection string format");
4549
return;
4650
}
47-
}
48-
49-
// Validate required fields (skip nickname which is optional)
50-
for (const field of config.fields) {
51-
if (field.required && !finalValues[field.name]?.trim()) {
52-
setError(`${field.label} is required`);
53-
return;
51+
} else {
52+
// Validate required fields (skip nickname which is optional)
53+
for (const field of config.fields) {
54+
if (field.required && !finalValues[field.name]?.trim()) {
55+
setError(`${field.label} is required`);
56+
return;
57+
}
5458
}
5559
}
5660

@@ -93,79 +97,82 @@ export function CustomConnectionForm({
9397
} finally {
9498
setSubmitting(false);
9599
}
96-
}, [values, mode, connectionString, config, integrationId, onSuccess]);
100+
}, [values, usingConnectionString, connectionString, config, integrationId, onSuccess]);
101+
102+
// Fields that aren't nickname (nickname is shown outside the collapsible section)
103+
const connectionFields = config.fields.filter((f) => f.name !== "nickname");
104+
const usingFields = connectionFields.some((f) => {
105+
const val = (values[f.name] ?? "").trim();
106+
return val.length > 0 && val !== (f.defaultValue ?? "");
107+
});
97108

98109
const inputClass =
99110
"w-full text-[12px] px-2.5 py-1.5 rounded-md border border-[#e8e4df] bg-white text-[#1a1a1a] placeholder:text-[#c4bfb8] focus:outline-none focus:border-[#a8a099] transition-colors";
111+
const disabledInputClass =
112+
"w-full text-[12px] px-2.5 py-1.5 rounded-md border border-[#e8e4df] bg-[#f5f3f0] text-[#a8a099] placeholder:text-[#d4d0cb] cursor-not-allowed";
100113

101114
return (
102115
<div className="flex flex-col gap-3 pt-1">
103-
{/* Mode toggle */}
104-
<div className="flex gap-1">
116+
{/* Nickname (always on top) */}
117+
<div>
118+
<label className="text-[10px] text-[#a8a099] block mb-0.5">Nickname (optional)</label>
119+
<input
120+
type="text"
121+
value={values.nickname ?? ""}
122+
onChange={(e) => setValue("nickname", e.target.value)}
123+
placeholder="e.g. Production DB"
124+
className={inputClass}
125+
/>
126+
</div>
127+
128+
{/* Connection string input (always visible when available) */}
129+
{hasConnectionString && (
130+
<div>
131+
<label className="text-[10px] text-[#a8a099] block mb-0.5">
132+
Connection String{!usingFields && <span className="text-red-400 ml-0.5">*</span>}
133+
</label>
134+
<input
135+
type="password"
136+
value={connectionString}
137+
onChange={(e) => setConnectionString(e.target.value)}
138+
placeholder="postgresql://user:pass@host:5432/db?sslmode=require"
139+
disabled={usingFields}
140+
className={usingFields ? disabledInputClass : inputClass}
141+
/>
142+
</div>
143+
)}
144+
145+
{/* Expandable fields section */}
146+
{hasConnectionString && (
105147
<button
106148
type="button"
107-
onClick={() => setMode("fields")}
108-
className={`text-[10px] px-2 py-0.5 rounded-full transition-colors cursor-pointer ${
109-
mode === "fields"
110-
? "bg-[#1a1a1a] text-white"
111-
: "bg-[#f0ece7] text-[#787068] hover:bg-[#e8e4df]"
112-
}`}
149+
onClick={() => setFieldsExpanded((prev) => !prev)}
150+
className="flex items-center gap-2 w-full cursor-pointer group"
113151
>
114-
Fields
152+
<div className="flex-1 h-px bg-[#e8e4df]" />
153+
<span className="text-[10px] text-[#a8a099] group-hover:text-[#787068] underline decoration-dotted underline-offset-2 transition-colors whitespace-nowrap">
154+
{fieldsExpanded ? "hide fields" : "or enter fields individually"}
155+
</span>
156+
<div className="flex-1 h-px bg-[#e8e4df]" />
115157
</button>
116-
{config.parseConnectionString && (
117-
<button
118-
type="button"
119-
onClick={() => setMode("url")}
120-
className={`text-[10px] px-2 py-0.5 rounded-full transition-colors cursor-pointer ${
121-
mode === "url"
122-
? "bg-[#1a1a1a] text-white"
123-
: "bg-[#f0ece7] text-[#787068] hover:bg-[#e8e4df]"
124-
}`}
125-
>
126-
Connection String
127-
</button>
128-
)}
129-
</div>
158+
)}
130159

131-
{mode === "url" ? (
132-
/* Connection string mode */
133-
<div className="flex flex-col gap-2">
134-
<div>
135-
<label className="text-[10px] text-[#a8a099] block mb-0.5">Connection String<span className="text-red-400 ml-0.5">*</span></label>
136-
<input
137-
type="password"
138-
value={connectionString}
139-
onChange={(e) => setConnectionString(e.target.value)}
140-
placeholder="postgresql://user:pass@host:5432/db?sslmode=require"
141-
className={inputClass}
142-
/>
143-
</div>
144-
<div>
145-
<label className="text-[10px] text-[#a8a099] block mb-0.5">Nickname</label>
146-
<input
147-
type="text"
148-
value={values.nickname ?? ""}
149-
onChange={(e) => setValue("nickname", e.target.value)}
150-
placeholder="e.g. Production DB"
151-
className={inputClass}
152-
/>
153-
</div>
154-
</div>
155-
) : (
156-
/* Individual fields mode */
160+
{fieldsExpanded && (
157161
<div className="flex flex-col gap-2">
158-
{config.fields.map((field) => (
162+
{connectionFields.map((field) => (
159163
<div key={field.name}>
160164
<label className="text-[10px] text-[#a8a099] block mb-0.5">
161165
{field.label}
162-
{field.required && <span className="text-red-400 ml-0.5">*</span>}
166+
{field.required && !usingConnectionString && (
167+
<span className="text-red-400 ml-0.5">*</span>
168+
)}
163169
</label>
164170
{field.type === "select" ? (
165171
<select
166172
value={values[field.name] ?? field.defaultValue ?? ""}
167173
onChange={(e) => setValue(field.name, e.target.value)}
168-
className={inputClass}
174+
disabled={usingConnectionString}
175+
className={usingConnectionString ? disabledInputClass : inputClass}
169176
>
170177
{field.options?.map((opt) => (
171178
<option key={opt} value={opt}>
@@ -179,7 +186,8 @@ export function CustomConnectionForm({
179186
value={values[field.name] ?? ""}
180187
onChange={(e) => setValue(field.name, e.target.value)}
181188
placeholder={field.placeholder}
182-
className={inputClass}
189+
disabled={usingConnectionString}
190+
className={usingConnectionString ? disabledInputClass : inputClass}
183191
/>
184192
)}
185193
</div>

0 commit comments

Comments
 (0)