fix(ui): refine config save guardrails (#1707)

* fix: refine config save guardrails

* docs: add changelog for config save guardrails (#1707) (thanks @Glucksberg)
This commit is contained in:
Peter Steinberger
2026-01-25 05:52:32 +00:00
committed by GitHub
parent bac80f0886
commit 495616d13e
4 changed files with 28 additions and 4 deletions

View File

@@ -26,6 +26,7 @@ Docs: https://docs.clawd.bot
- BlueBubbles: keep part-index GUIDs in reply tags when short IDs are missing. - BlueBubbles: keep part-index GUIDs in reply tags when short IDs are missing.
- Web UI: hide internal `message_id` hints in chat bubbles. - Web UI: hide internal `message_id` hints in chat bubbles.
- Web UI: show Stop button during active runs, swap back to New session when idle. (#1664) Thanks @ndbroadbent. - Web UI: show Stop button during active runs, swap back to New session when idle. (#1664) Thanks @ndbroadbent.
- Web UI: clear stale disconnect banners on reconnect; allow form saves with unsupported schema paths but block missing schema. (#1707) Thanks @Glucksberg.
- Heartbeat: normalize target identifiers for consistent routing. - Heartbeat: normalize target identifiers for consistent routing.
- TUI: reload history after gateway reconnect to restore session state. (#1663) - TUI: reload history after gateway reconnect to restore session state. (#1663)
- Telegram: use wrapped fetch for long-polling on Node to normalize AbortSignal handling. (#1639) - Telegram: use wrapped fetch for long-polling on Node to normalize AbortSignal handling. (#1639)

View File

@@ -124,6 +124,7 @@ export function connectGateway(host: GatewayHost) {
mode: "webchat", mode: "webchat",
onHello: (hello) => { onHello: (hello) => {
host.connected = true; host.connected = true;
host.lastError = null;
host.hello = hello; host.hello = hello;
applySnapshot(host, hello); applySnapshot(host, hello);
void loadAssistantIdentity(host as unknown as ClawdbotApp); void loadAssistantIdentity(host as unknown as ClawdbotApp);

View File

@@ -38,7 +38,7 @@ describe("config view", () => {
onSubsectionChange: vi.fn(), onSubsectionChange: vi.fn(),
}); });
it("disables save when form is unsafe", () => { it("allows save when form is unsafe", () => {
const container = document.createElement("div"); const container = document.createElement("div");
render( render(
renderConfig({ renderConfig({
@@ -59,6 +59,28 @@ describe("config view", () => {
container, container,
); );
const saveButton = Array.from(
container.querySelectorAll("button"),
).find((btn) => btn.textContent?.trim() === "Save") as
| HTMLButtonElement
| undefined;
expect(saveButton).not.toBeUndefined();
expect(saveButton?.disabled).toBe(false);
});
it("disables save when schema is missing", () => {
const container = document.createElement("div");
render(
renderConfig({
...baseProps(),
schema: null,
formMode: "form",
formValue: { gateway: { mode: "local" } },
originalValue: {},
}),
container,
);
const saveButton = Array.from( const saveButton = Array.from(
container.querySelectorAll("button"), container.querySelectorAll("button"),
).find((btn) => btn.textContent?.trim() === "Save") as ).find((btn) => btn.textContent?.trim() === "Save") as

View File

@@ -233,10 +233,10 @@ export function renderConfig(props: ConfigProps) {
const hasRawChanges = props.formMode === "raw" && props.raw !== props.originalRaw; const hasRawChanges = props.formMode === "raw" && props.raw !== props.originalRaw;
const hasChanges = props.formMode === "form" ? diff.length > 0 : hasRawChanges; const hasChanges = props.formMode === "form" ? diff.length > 0 : hasRawChanges;
// Save/apply buttons require actual changes to be enabled // Save/apply buttons require actual changes to be enabled.
// Note: formUnsafe warns about unsupported schema paths but shouldn't block saving // Note: formUnsafe warns about unsupported schema paths but shouldn't block saving.
const canSaveForm = const canSaveForm =
Boolean(props.formValue) && !props.loading; Boolean(props.formValue) && !props.loading && Boolean(analysis.schema);
const canSave = const canSave =
props.connected && props.connected &&
!props.saving && !props.saving &&