mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 12:30:44 +00:00
fix(ui): remove duplicate config section headers
Fix duplicate section title and description rendering in single-section Control UI config pages.\n\nKeeps root multi-section card headers intact, keeps single-section hero copy as the only visible section title, and adds browser coverage for both single-section and root views.\n\nFixes #68003.\n\nThanks @d1rshan.
This commit is contained in:
@@ -438,20 +438,25 @@ export function renderConfigForm(props: ConfigFormProps) {
|
||||
sectionKey: string;
|
||||
label: string;
|
||||
description: string;
|
||||
showHeader: boolean;
|
||||
node: JsonSchema;
|
||||
nodeValue: unknown;
|
||||
path: Array<string | number>;
|
||||
}) => html`
|
||||
<section class="config-section-card" id=${params.id}>
|
||||
<div class="config-section-card__header">
|
||||
<span class="config-section-card__icon">${getSectionIcon(params.sectionKey)}</span>
|
||||
<div class="config-section-card__titles">
|
||||
<h3 class="config-section-card__title">${params.label}</h3>
|
||||
${params.description
|
||||
? html`<p class="config-section-card__desc">${params.description}</p>`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
${params.showHeader
|
||||
? html`
|
||||
<div class="config-section-card__header">
|
||||
<span class="config-section-card__icon">${getSectionIcon(params.sectionKey)}</span>
|
||||
<div class="config-section-card__titles">
|
||||
<h3 class="config-section-card__title">${params.label}</h3>
|
||||
${params.description
|
||||
? html`<p class="config-section-card__desc">${params.description}</p>`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<div class="config-section-card__content">
|
||||
${renderNode({
|
||||
schema: params.node,
|
||||
@@ -490,6 +495,7 @@ export function renderConfigForm(props: ConfigFormProps) {
|
||||
sectionKey,
|
||||
label,
|
||||
description,
|
||||
showHeader: false,
|
||||
node,
|
||||
nodeValue: scopedValue,
|
||||
path: [sectionKey, subsectionKey],
|
||||
@@ -506,6 +512,7 @@ export function renderConfigForm(props: ConfigFormProps) {
|
||||
sectionKey: key,
|
||||
label: meta.label,
|
||||
description: meta.description,
|
||||
showHeader: activeSection == null,
|
||||
node,
|
||||
nodeValue: value[key],
|
||||
path: [key],
|
||||
|
||||
@@ -349,7 +349,80 @@ describe("config view", () => {
|
||||
(input as HTMLInputElement).value = "gateway";
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
expect(onSearchChange).toHaveBeenCalledWith("gateway");
|
||||
});
|
||||
|
||||
it("shows section hero and hides nested card header in single-section form view", () => {
|
||||
const { container } = renderConfigView({
|
||||
activeSection: "auth",
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
auth: {
|
||||
type: "object",
|
||||
properties: {
|
||||
authPermanentBackoffMinutes: {
|
||||
type: "number",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
formValue: {
|
||||
auth: {
|
||||
authPermanentBackoffMinutes: 10,
|
||||
},
|
||||
},
|
||||
originalValue: {
|
||||
auth: {
|
||||
authPermanentBackoffMinutes: 10,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const heroTitle = container.querySelector(".config-section-hero__title");
|
||||
expect(heroTitle?.textContent?.trim()).toBe("Authentication");
|
||||
expect(container.querySelector(".config-section-card__header")).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps card headers in multi-section root view", () => {
|
||||
const { container } = renderConfigView({
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
auth: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
gateway: {
|
||||
type: "object",
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
formValue: {
|
||||
auth: {},
|
||||
gateway: {},
|
||||
},
|
||||
originalValue: {
|
||||
auth: {},
|
||||
gateway: {},
|
||||
},
|
||||
});
|
||||
|
||||
expect(container.querySelectorAll(".config-section-card__header").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("clears the active search query", () => {
|
||||
const container = document.createElement("div");
|
||||
const onSearchChange = vi.fn();
|
||||
render(
|
||||
renderConfig({
|
||||
...baseProps(),
|
||||
searchQuery: "gateway",
|
||||
onSearchChange,
|
||||
}),
|
||||
container,
|
||||
);
|
||||
const clearButton = container.querySelector<HTMLButtonElement>(".config-search__clear");
|
||||
expect(clearButton).toBeTruthy();
|
||||
clearButton?.click();
|
||||
|
||||
Reference in New Issue
Block a user