mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 13:00:44 +00:00
fix(ui): add clear pending config updates action (#68178)
Merged via squash.
Prepared head SHA: 1a3cb66fcb
Co-authored-by: BunsDev <68980965+BunsDev@users.noreply.github.com>
Co-authored-by: BunsDev <68980965+BunsDev@users.noreply.github.com>
Reviewed-by: @BunsDev
This commit is contained in:
@@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Discord: keep slash command follow-up chunks ephemeral when the command is configured for ephemeral replies, so long `/status` output no longer leaks fallback model or runtime details into the public channel. (#69869) thanks @gumadeiras.
|
||||
- Plugins/discovery: reject package plugin source entries that escape the package directory before explicit runtime entries or inferred built JavaScript peers can be used. (#69868) thanks @gumadeiras.
|
||||
- CLI/channels: resolve channel presence through a shared policy that keeps ambient env vars and stale persisted auth from surfacing disabled bundled plugins in status, doctor, security audit, and cron delivery validation unless the channel or plugin is effectively enabled or explicitly configured. (#69862) Thanks @gumadeiras.
|
||||
- Control UI/config: preserve intentionally empty raw config snapshots when clearing pending updates so reset restores the original bytes instead of synthesizing JSON for blank config files. (#68178) Thanks @BunsDev.
|
||||
|
||||
### Fixes
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
applyConfig,
|
||||
ensureAgentConfigEntry,
|
||||
findAgentConfigEntryIndex,
|
||||
resetConfigPendingChanges,
|
||||
runUpdate,
|
||||
saveConfig,
|
||||
updateConfigFormValue,
|
||||
@@ -163,6 +164,50 @@ 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');
|
||||
});
|
||||
|
||||
it("preserves an intentionally empty original raw config", () => {
|
||||
const state = createState();
|
||||
state.configSnapshot = {
|
||||
config: {},
|
||||
valid: true,
|
||||
issues: [],
|
||||
raw: "",
|
||||
};
|
||||
state.configFormOriginal = {};
|
||||
state.configRawOriginal = "";
|
||||
state.configForm = { gateway: { mode: "remote" } };
|
||||
state.configRaw = '{\n "gateway": { "mode": "remote" }\n}\n';
|
||||
state.configFormDirty = true;
|
||||
|
||||
resetConfigPendingChanges(state);
|
||||
|
||||
expect(state.configFormDirty).toBe(false);
|
||||
expect(state.configForm).toEqual({});
|
||||
expect(state.configRaw).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("agent config helpers", () => {
|
||||
it("finds explicit agent entries", () => {
|
||||
expect(
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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,31 @@ describe("config view", () => {
|
||||
raw: "{\n}\n",
|
||||
originalRaw: "{\n}\n",
|
||||
});
|
||||
({ saveButton, applyButton } = findActionButtons(container));
|
||||
let clearButton: HTMLButtonElement | undefined;
|
||||
({ 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", () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user