fix(ui): clear pending config updates

This commit is contained in:
Val Alexander
2026-04-17 10:34:59 -05:00
parent e92079be6b
commit 24556b3cdc
5 changed files with 53 additions and 2 deletions

View File

@@ -42,6 +42,7 @@ import {
findAgentConfigEntryIndex,
loadConfig,
openConfigFile,
resetConfigPendingChanges,
runUpdate,
saveConfig,
updateConfigFormValue,
@@ -856,6 +857,7 @@ export function renderApp(state: AppViewState) {
onFormPatch: (path: Array<string | number>, value: unknown) =>
updateConfigFormValue(state, path, value),
onReload: () => loadConfig(state),
onReset: () => resetConfigPendingChanges(state),
onSave: () => saveConfig(state),
onApply: () => applyConfig(state),
onUpdate: () => runUpdate(state),

View File

@@ -4,6 +4,7 @@ import {
applyConfig,
ensureAgentConfigEntry,
findAgentConfigEntryIndex,
resetConfigPendingChanges,
runUpdate,
saveConfig,
updateConfigFormValue,
@@ -163,6 +164,29 @@ describe("updateConfigFormValue", () => {
});
});
describe("resetConfigPendingChanges", () => {
it("restores the original form and raw config snapshot", () => {
const state = createState();
state.configSnapshot = {
config: { gateway: { mode: "local" } },
valid: true,
issues: [],
raw: '{\n "gateway": { "mode": "local" }\n}\n',
};
state.configFormOriginal = { gateway: { mode: "local" } };
state.configRawOriginal = '{\n "gateway": { "mode": "local" }\n}\n';
state.configForm = { gateway: { mode: "remote", port: 3000 } };
state.configRaw = '{\n "gateway": { "mode": "remote", "port": 3000 }\n}\n';
state.configFormDirty = true;
resetConfigPendingChanges(state);
expect(state.configFormDirty).toBe(false);
expect(state.configForm).toEqual({ gateway: { mode: "local" } });
expect(state.configRaw).toBe('{\n "gateway": { "mode": "local" }\n}\n');
});
});
describe("agent config helpers", () => {
it("finds explicit agent entries", () => {
expect(

View File

@@ -218,6 +218,16 @@ export function updateConfigFormValue(
mutateConfigForm(state, (draft) => setPathValue(draft, path, value));
}
export function resetConfigPendingChanges(state: ConfigState) {
state.configForm = cloneConfigObject(
state.configFormOriginal ?? state.configSnapshot?.config ?? {},
);
state.configRaw =
state.configRawOriginal ||
serializeConfigForm(state.configFormOriginal ?? state.configSnapshot?.config ?? {});
state.configFormDirty = false;
}
export function removeConfigFormValue(state: ConfigState, path: Array<string | number>) {
mutateConfigForm(state, (draft) => removePathValue(draft, path));
}

View File

@@ -33,6 +33,7 @@ describe("config view", () => {
onSearchChange: vi.fn(),
onSectionChange: vi.fn(),
onReload: vi.fn(),
onReset: vi.fn(),
onSave: vi.fn(),
onApply: vi.fn(),
onUpdate: vi.fn(),
@@ -49,11 +50,13 @@ describe("config view", () => {
});
function findActionButtons(container: HTMLElement): {
clearButton?: HTMLButtonElement;
saveButton?: HTMLButtonElement;
applyButton?: HTMLButtonElement;
} {
const buttons = Array.from(container.querySelectorAll("button"));
return {
clearButton: buttons.find((btn) => btn.textContent?.trim() === "Clear pending updates"),
saveButton: buttons.find((btn) => btn.textContent?.trim() === "Save"),
applyButton: buttons.find((btn) => btn.textContent?.trim() === "Apply"),
};
@@ -129,22 +132,30 @@ describe("config view", () => {
raw: "{\n}\n",
originalRaw: "{\n}\n",
});
({ saveButton, applyButton } = findActionButtons(container));
let { clearButton, saveButton, applyButton } = findActionButtons(container);
expect(clearButton).not.toBeUndefined();
expect(saveButton).not.toBeUndefined();
expect(applyButton).not.toBeUndefined();
expect(clearButton?.disabled).toBe(true);
expect(saveButton?.disabled).toBe(true);
expect(applyButton?.disabled).toBe(true);
const onReset = vi.fn();
renderCase({
formMode: "raw",
raw: '{\n gateway: { mode: "local" }\n}\n',
originalRaw: "{\n}\n",
onReset,
});
({ saveButton, applyButton } = findActionButtons(container));
({ clearButton, saveButton, applyButton } = findActionButtons(container));
expect(saveButton).not.toBeUndefined();
expect(applyButton).not.toBeUndefined();
expect(clearButton?.disabled).toBe(false);
expect(saveButton?.disabled).toBe(false);
expect(applyButton?.disabled).toBe(false);
clearButton?.click();
expect(onReset).toHaveBeenCalledTimes(1);
});
it("switches mode via the sidebar toggle", () => {

View File

@@ -52,6 +52,7 @@ export type ConfigProps = {
onSectionChange: (section: string | null) => void;
onSubsectionChange: (section: string | null) => void;
onReload: () => void;
onReset: () => void;
onSave: () => void;
onApply: () => void;
onUpdate: () => void;
@@ -959,6 +960,9 @@ export function renderConfig(props: ConfigProps) {
<button class="btn btn--sm" ?disabled=${props.loading} @click=${props.onReload}>
${props.loading ? t("common.loading") : t("common.reload")}
</button>
<button class="btn btn--sm" ?disabled=${!hasChanges} @click=${props.onReset}>
Clear pending updates
</button>
<button class="btn btn--sm primary" ?disabled=${!canSave} @click=${props.onSave}>
${props.saving ? "Saving…" : "Save"}
</button>