Skip to content

Commit de058f9

Browse files
FIX: Connection Name and Description Losing Last Character When Editing (#57)
* FIX: Use Ant Design Form pattern for name and description fields - Replace controlled inputs (value prop) with Form-managed inputs (name prop) - Use Form.useForm() hook and form.setFieldsValue() to set values when editing existing connections - Remove getValueFromEvent which conflicted with controlled input pattern - Move collapseSpaces transformation to onChange handler This fixes the issue where the form fields wouldn't properly display values when editing existing connections. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * FIX: Debounce name/description state updates to prevent stale closure - Add debounced state update for name and description fields (300ms) - Prevents the handleCreateOrUpdate callback from capturing stale dbSelectionInfo values during rapid typing - Ensures the last character is not lost when updating a connection This fixes the bug where editing connection name from "test_pg" to "test_pg_edited" would save as "test_pg_edite" (missing last character). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * FIX: Remove unnecessary debounce from name/description state updates The debounce was introduced to fix stale closure issues, but it actually creates a worse bug - a 300ms window where clicking Update could submit pre-edit values. The root cause is already fixed in ConnectionDetailsSection.jsx by using Ant Design's Form pattern (Form.useForm + form.setFieldsValue). With uncontrolled inputs, the direct state update works correctly. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * FIX: Add debounce to parent state update in ConnectionDetailsSection Debounce the handleConnectionNameDesc callback (300ms) to ensure the parent's handleCreateOrUpdate closure captures the latest state value. The form input remains responsive since Ant Design manages the input internally, while the parent state update is debounced. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * FIX: Apply collapseSpaces synchronously to form value for validation Update form value synchronously with collapsed spaces so the validator always sees the processed value, preventing spurious validation errors when consecutive spaces are typed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * FIX: Only populate form values when connectionId changes Change useEffect dependency from dbSelectionInfo fields to connectionId to prevent form values from being overwritten during typing due to debounced state updates. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * FIX: Add isUserEditingRef to prevent form overwrites during typing - Restore debounce for parent state updates - Add isUserEditingRef to track when user is actively editing - Only populate form values when not in editing mode - Reset editing flag when connectionId changes (new connection loaded) This ensures: 1. Form populates correctly when async connection data arrives 2. User edits are not overwritten by the useEffect 3. Parent state updates are debounced to prevent stale closure issues 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * FIX: Use normalize prop for collapseSpaces transformation Replace manual form.setFieldValue with normalize prop on Form.Item. This ensures collapseSpaces is applied before storage and validation, preventing false validation errors when consecutive spaces are typed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * FIX: Apply collapseSpaces when populating form from server data setFieldsValue bypasses the normalize prop, so apply collapseSpaces manually when loading connection data to ensure consistent formatting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * FIX: Move form to parent and use Form.useWatch for hasDetailsChanged - Move Form.useForm() to CreateConnection.jsx and pass form as prop - Remove debounce logic from ConnectionDetailsSection (no longer needed) - Read name/description directly from form in handleCreateOrUpdate - Use Form.useWatch to reactively track form values for hasDetailsChanged - This eliminates the race condition and fixes Update button not enabling 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * chore: fix eslint formatting 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: add hasDetailsChanged to deps and normalize original name - Add hasDetailsChanged to handleCreateOrUpdate dependency array to fix stale closure issue - Apply collapseSpaces when storing originalDbSelectionInfo.name to ensure consistent comparison with form values 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4bdec24 commit de058f9

2 files changed

Lines changed: 45 additions & 32 deletions

File tree

frontend/src/base/components/environment/ConnectionDetailsSection.jsx

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { memo } from "react";
1+
import { memo, useEffect } from "react";
22
import PropTypes from "prop-types";
33
import { Typography, Input, Select, Form } from "antd";
44

@@ -13,49 +13,49 @@ const ConnectionDetailsSection = memo(
1313
({
1414
connectionId,
1515
dbSelectionInfo,
16-
handleConnectionNameDesc,
16+
connectionDetailsForm,
1717
handleCardClick,
1818
mappedDataSources,
1919
dbUsage,
2020
}) => {
21+
// Populate form values when connection data is loaded
22+
// Apply collapseSpaces since setFieldsValue bypasses the normalize prop
23+
useEffect(() => {
24+
connectionDetailsForm.setFieldsValue({
25+
name: collapseSpaces(dbSelectionInfo.name || ""),
26+
description: dbSelectionInfo.description,
27+
});
28+
}, [
29+
connectionDetailsForm,
30+
connectionId,
31+
dbSelectionInfo.name,
32+
dbSelectionInfo.description,
33+
]);
34+
2135
return (
2236
<div className="createConnectionSection flex-1 createConnectionSectionDivider overflow-y-auto">
2337
<Typography className="sectionTitle">Connection Details</Typography>
2438
<div className="formFieldsWrapper">
25-
<Form layout="vertical">
39+
<Form form={connectionDetailsForm} layout="vertical">
2640
<Form.Item
2741
label="Name"
28-
getValueFromEvent={({ target: { value } }) =>
29-
// collapse any run of 2+ spaces only when followed by non-space
30-
collapseSpaces(value)
31-
}
42+
name="name"
43+
normalize={collapseSpaces}
3244
rules={[
3345
{ required: true, message: "Please enter the connection name" },
3446
{ validator: validateFormFieldName },
3547
]}
3648
required
3749
>
38-
<Input
39-
className="field"
40-
value={dbSelectionInfo.name}
41-
onChange={(e) =>
42-
handleConnectionNameDesc("name", e.target.value)
43-
}
44-
/>
50+
<Input className="field" />
4551
</Form.Item>
4652

4753
<Form.Item
4854
label="Description"
55+
name="description"
4956
rules={[{ validator: validateFormFieldDescription }]}
5057
>
51-
<Input.TextArea
52-
className="field"
53-
rows={2}
54-
value={dbSelectionInfo.description}
55-
onChange={(e) =>
56-
handleConnectionNameDesc("description", e.target.value)
57-
}
58-
/>
58+
<Input.TextArea className="field" rows={2} />
5959
</Form.Item>
6060

6161
<Form.Item label="Database" required>
@@ -90,7 +90,7 @@ ConnectionDetailsSection.propTypes = {
9090
description: PropTypes.string,
9191
icon: PropTypes.string,
9292
}).isRequired,
93-
handleConnectionNameDesc: PropTypes.func.isRequired,
93+
connectionDetailsForm: PropTypes.object.isRequired,
9494
handleCardClick: PropTypes.func.isRequired,
9595
mappedDataSources: PropTypes.array.isRequired,
9696
dbUsage: PropTypes.shape({

frontend/src/base/components/environment/CreateConnection.jsx

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useEffect, useState, useCallback, useMemo } from "react";
22
import Cookies from "js-cookie";
33
import PropTypes from "prop-types";
4+
import { Form } from "antd";
45

56
import { useAxiosPrivate } from "../../../service/axios-service.js";
67
import { orgStore } from "../../../store/org-store.js";
@@ -19,6 +20,7 @@ import {
1920
} from "./environment-api-service.js";
2021
import "./environment.css";
2122
import { useNotificationService } from "../../../service/notification-service.js";
23+
import { collapseSpaces } from "./helper";
2224

2325
const CreateConnection = ({
2426
setIsModalOpen,
@@ -54,6 +56,11 @@ const CreateConnection = ({
5456
const [isCredentialsRevealed, setIsCredentialsRevealed] = useState(false);
5557
const [isRevealLoading, setIsRevealLoading] = useState(false);
5658
const { notify } = useNotificationService();
59+
const [connectionDetailsForm] = Form.useForm();
60+
61+
// Watch form fields reactively for hasDetailsChanged comparison
62+
const formName = Form.useWatch("name", connectionDetailsForm);
63+
const formDescription = Form.useWatch("description", connectionDetailsForm);
5764

5865
// Initialize encryption service
5966
useEffect(() => {
@@ -118,16 +125,17 @@ const CreateConnection = ({
118125
getConnectionFields();
119126
}, [getConnectionFields]);
120127

121-
const handleConnectionNameDesc = useCallback((name, value) => {
122-
setDbSelectionInfo((prev) => ({ ...prev, [name]: value }));
123-
}, []);
124-
125128
const handleCreateOrUpdate = useCallback(async () => {
126129
setIsCreateOrUpdateLoading(true);
127130
try {
131+
// Get name and description directly from the form (source of truth)
132+
const { name, description } = connectionDetailsForm.getFieldsValue();
133+
128134
// Prepare connection data
129135
const connectionData = {
130136
...dbSelectionInfo,
137+
name,
138+
description,
131139
connection_details: {
132140
...inputFields,
133141
...(["postgres", "snowflake"].includes(
@@ -199,6 +207,8 @@ const CreateConnection = ({
199207
}
200208
}, [
201209
connectionId,
210+
connectionDetailsForm,
211+
hasDetailsChanged,
202212
dbSelectionInfo,
203213
inputFields,
204214
connType,
@@ -232,7 +242,10 @@ const CreateConnection = ({
232242
icon: db_icon,
233243
};
234244
setDbSelectionInfo(selectionInfo);
235-
setOriginalDbSelectionInfo({ ...selectionInfo });
245+
setOriginalDbSelectionInfo({
246+
...selectionInfo,
247+
name: collapseSpaces(selectionInfo.name || ""),
248+
});
236249
// Process connection details to handle JSON objects for textarea fields
237250
const processedConnectionDetails = { ...connection_details };
238251

@@ -398,10 +411,10 @@ const CreateConnection = ({
398411
const hasDetailsChanged = useMemo(() => {
399412
if (!connectionId || !originalDbSelectionInfo) return false;
400413
return (
401-
dbSelectionInfo.name !== originalDbSelectionInfo.name ||
402-
dbSelectionInfo.description !== originalDbSelectionInfo.description
414+
formName !== originalDbSelectionInfo.name ||
415+
formDescription !== originalDbSelectionInfo.description
403416
);
404-
}, [connectionId, dbSelectionInfo, originalDbSelectionInfo]);
417+
}, [connectionId, formName, formDescription, originalDbSelectionInfo]);
405418

406419
const mappedDataSources = useMemo(
407420
() =>
@@ -428,7 +441,7 @@ const CreateConnection = ({
428441
<ConnectionDetailsSection
429442
connectionId={connectionId}
430443
dbSelectionInfo={dbSelectionInfo}
431-
handleConnectionNameDesc={handleConnectionNameDesc}
444+
connectionDetailsForm={connectionDetailsForm}
432445
handleCardClick={(value) => {
433446
// Clear input fields when datasource changes to prevent old fields from being sent
434447
setInputFields({});

0 commit comments

Comments
 (0)