diff --git a/CHANGELOGS/2026-08-25-add-server-form.md b/CHANGELOGS/2026-08-25-add-server-form.md new file mode 100644 index 0000000000..4990fdac5b --- /dev/null +++ b/CHANGELOGS/2026-08-25-add-server-form.md @@ -0,0 +1,24 @@ +# 2026-08-25 — Fix Add Server form field entry and save + +## Summary + +Fixed the Settings Add/Edit Server form so name, username, and password can be typed and persisted on save. + +## Changes + +### `packages/app/src/components/settings-v2/dialog-server-v2.tsx` + +- Initialize add/edit form state during setup (before first paint) so uncontrolled defaults apply correctly. +- Keep URL controlled for validation/preview; make name, username, and password write-once initial values (plain strings, not reactive) so typed characters are not rolled back on Windows/Electron. +- Stop disabling text fields while the health check runs; only the submit button stays disabled. +- Allow Cancel during a pending check. + +### `packages/app/src/components/dialog-select-server.tsx` + +- Persist username independently of password on add (aligned with edit), so credentials are not dropped when only one is set. +- Remove `isPending` early-returns from field change handlers so input continues updating the store during health checks. +- Keep optional legacy `ServerForm` fields enabled while checking; only the submit button is disabled. + +## Verification + +- `bun typecheck` in `packages/app` diff --git a/packages/app/src/components/dialog-select-server.tsx b/packages/app/src/components/dialog-select-server.tsx index 102c1a7cd1..a20b78d378 100644 --- a/packages/app/src/components/dialog-select-server.tsx +++ b/packages/app/src/components/dialog-select-server.tsx @@ -29,7 +29,6 @@ interface ServerFormProps { username: string password: string placeholder: string - busy: boolean error: string status: boolean | undefined onChange: (value: string) => void @@ -136,7 +135,6 @@ function ServerForm(props: ServerFormProps) { autofocus validationState={props.error ? "invalid" : "valid"} error={props.error} - disabled={props.busy} onChange={props.onChange} onKeyDown={keyDown} /> @@ -146,7 +144,6 @@ function ServerForm(props: ServerFormProps) { label={language.t("dialog.server.add.name")} placeholder={language.t("dialog.server.add.namePlaceholder")} defaultValue={props.name} - disabled={props.busy} onChange={props.onNameChange} onKeyDown={keyDown} /> @@ -156,7 +153,6 @@ function ServerForm(props: ServerFormProps) { label={language.t("dialog.server.add.username")} placeholder={language.t("dialog.server.add.usernamePlaceholder")} defaultValue={props.username} - disabled={props.busy} onChange={props.onUsernameChange} onKeyDown={keyDown} /> @@ -165,7 +161,6 @@ function ServerForm(props: ServerFormProps) { label={language.t("dialog.server.add.password")} placeholder={language.t("dialog.server.add.passwordPlaceholder")} defaultValue={props.password} - disabled={props.busy} onChange={props.onPasswordChange} onKeyDown={keyDown} /> @@ -252,13 +247,14 @@ export function useServerManagementController(options: { onSelect?: () => void; return } + const name = store.addServer.name.trim() || undefined + const username = store.addServer.username || undefined + const password = store.addServer.password || undefined const conn: ServerConnection.Http = { type: "http", - http: { url: normalized }, + displayName: name, + http: { url: normalized, username, password }, } - if (store.addServer.name.trim()) conn.displayName = store.addServer.name.trim() - if (store.addServer.password) conn.http.password = store.addServer.password - if (store.addServer.password && store.addServer.username) conn.http.username = store.addServer.username const result = await checkServerHealth(conn.http) if (!result.healthy) { setStore("addServer", { error: language.t("dialog.server.add.error") }) @@ -394,7 +390,6 @@ export function useServerManagementController(options: { onSelect?: () => void; } const handleAddChange = (value: string) => { - if (addMutation.isPending) return setStore("addServer", { url: value, error: "" }) void previewStatus(value, store.addServer.username, store.addServer.password, (next) => setStore("addServer", { status: next }), @@ -402,12 +397,10 @@ export function useServerManagementController(options: { onSelect?: () => void; } const handleAddNameChange = (value: string) => { - if (addMutation.isPending) return setStore("addServer", { name: value, error: "" }) } const handleAddUsernameChange = (value: string) => { - if (addMutation.isPending) return setStore("addServer", { username: value, error: "" }) void previewStatus(store.addServer.url, value, store.addServer.password, (next) => setStore("addServer", { status: next }), @@ -415,7 +408,6 @@ export function useServerManagementController(options: { onSelect?: () => void; } const handleAddPasswordChange = (value: string) => { - if (addMutation.isPending) return setStore("addServer", { password: value, error: "" }) void previewStatus(store.addServer.url, store.addServer.username, value, (next) => setStore("addServer", { status: next }), @@ -423,7 +415,6 @@ export function useServerManagementController(options: { onSelect?: () => void; } const handleEditChange = (value: string) => { - if (editMutation.isPending) return setStore("editServer", { value, error: "" }) void previewStatus(value, store.editServer.username, store.editServer.password, (next) => setStore("editServer", { status: next }), @@ -431,12 +422,10 @@ export function useServerManagementController(options: { onSelect?: () => void; } const handleEditNameChange = (value: string) => { - if (editMutation.isPending) return setStore("editServer", { name: value, error: "" }) } const handleEditUsernameChange = (value: string) => { - if (editMutation.isPending) return setStore("editServer", { username: value, error: "" }) void previewStatus(store.editServer.value, value, store.editServer.password, (next) => setStore("editServer", { status: next }), @@ -444,7 +433,6 @@ export function useServerManagementController(options: { onSelect?: () => void; } const handleEditPasswordChange = (value: string) => { - if (editMutation.isPending) return setStore("editServer", { password: value, error: "" }) void previewStatus(store.editServer.value, store.editServer.username, value, (next) => setStore("editServer", { status: next }), @@ -692,7 +680,6 @@ export function ServerConnectionForm(props: { controller: ReturnType dialog.close(), navigateOnAdd: false, }) - const [opened, setOpened] = createSignal(false) - onMount(() => { - if (props.mode === "add") controller.startAdd() - if (props.mode === "edit" && props.server) controller.startEdit(props.server) - setOpened(true) - }) + // Initialize before first paint so optional-field initial values stick. + if (props.mode === "add") controller.startAdd() + if (props.mode === "edit" && props.server) controller.startEdit(props.server) + + // Plain strings (not accessors) so Solid treats these as initial values, not controlled. + const initialName = controller.formName() + const initialUsername = controller.formUsername() + const initialPassword = controller.formPassword() onCleanup(() => { controller.resetForm() }) createEffect(() => { - if (!opened()) return if (controller.isFormMode()) return dialog.close() }) @@ -69,7 +70,6 @@ export const DialogServerV2: Component<{ value={controller.formValue()} placeholder={language.t("dialog.server.add.placeholder")} invalid={!!controller.formError()} - disabled={controller.formBusy()} autofocus onInput={(event) => controller.handleFormChange()(event.currentTarget.value)} onKeyDown={keyDown} @@ -84,9 +84,8 @@ export const DialogServerV2: Component<{ type="text" appearance="large" class="!w-full self-stretch" - value={controller.formName()} + value={initialName} placeholder={language.t("dialog.server.add.namePlaceholder")} - disabled={controller.formBusy()} onInput={(event) => controller.handleFormNameChange()(event.currentTarget.value)} onKeyDown={keyDown} /> @@ -98,9 +97,8 @@ export const DialogServerV2: Component<{ type="text" appearance="large" class="!w-full self-stretch" - value={controller.formUsername()} + value={initialUsername} placeholder={language.t("dialog.server.add.usernamePlaceholder")} - disabled={controller.formBusy()} onInput={(event) => controller.handleFormUsernameChange()(event.currentTarget.value)} onKeyDown={keyDown} /> @@ -111,9 +109,8 @@ export const DialogServerV2: Component<{ type="password" appearance="large" class="!w-full self-stretch" - value={controller.formPassword()} + value={initialPassword} placeholder={language.t("dialog.server.add.passwordPlaceholder")} - disabled={controller.formBusy()} onInput={(event) => controller.handleFormPasswordChange()(event.currentTarget.value)} onKeyDown={keyDown} /> @@ -122,7 +119,7 @@ export const DialogServerV2: Component<{ - dialog.close()}> + dialog.close()}> {language.t("common.cancel")}