feat: add config subsections in control ui
This commit is contained in:
@@ -358,6 +358,98 @@
|
|||||||
color: var(--ok);
|
color: var(--ok);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Section Hero */
|
||||||
|
.config-section-hero {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 14px 20px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="light"] .config-section-hero {
|
||||||
|
background: rgba(0, 0, 0, 0.015);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section-hero__icon {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
color: var(--accent);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section-hero__icon svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
stroke: currentColor;
|
||||||
|
fill: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section-hero__text {
|
||||||
|
display: grid;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section-hero__title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section-hero__desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subsection Nav */
|
||||||
|
.config-subnav {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 20px 12px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: rgba(0, 0, 0, 0.03);
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="light"] .config-subnav {
|
||||||
|
background: rgba(0, 0, 0, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-subnav__item {
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--muted);
|
||||||
|
background: rgba(0, 0, 0, 0.12);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 150ms ease, color 150ms ease, border-color 150ms ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="light"] .config-subnav__item {
|
||||||
|
background: rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-subnav__item:hover {
|
||||||
|
color: var(--text);
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="light"] .config-subnav__item:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-subnav__item.active {
|
||||||
|
color: var(--accent);
|
||||||
|
border-color: rgba(245, 159, 74, 0.4);
|
||||||
|
background: rgba(245, 159, 74, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
/* Content Area */
|
/* Content Area */
|
||||||
.config-content {
|
.config-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -1243,6 +1335,14 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.config-section-hero {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-subnav {
|
||||||
|
padding: 8px 16px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.config-content {
|
.config-content {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -485,11 +485,16 @@ export function renderApp(state: AppViewState) {
|
|||||||
originalValue: state.configFormOriginal,
|
originalValue: state.configFormOriginal,
|
||||||
searchQuery: state.configSearchQuery,
|
searchQuery: state.configSearchQuery,
|
||||||
activeSection: state.configActiveSection,
|
activeSection: state.configActiveSection,
|
||||||
|
activeSubsection: state.configActiveSubsection,
|
||||||
onRawChange: (next) => (state.configRaw = next),
|
onRawChange: (next) => (state.configRaw = next),
|
||||||
onFormModeChange: (mode) => (state.configFormMode = mode),
|
onFormModeChange: (mode) => (state.configFormMode = mode),
|
||||||
onFormPatch: (path, value) => updateConfigFormValue(state, path, value),
|
onFormPatch: (path, value) => updateConfigFormValue(state, path, value),
|
||||||
onSearchChange: (query) => (state.configSearchQuery = query),
|
onSearchChange: (query) => (state.configSearchQuery = query),
|
||||||
onSectionChange: (section) => (state.configActiveSection = section),
|
onSectionChange: (section) => {
|
||||||
|
state.configActiveSection = section;
|
||||||
|
state.configActiveSubsection = null;
|
||||||
|
},
|
||||||
|
onSubsectionChange: (section) => (state.configActiveSubsection = section),
|
||||||
onReload: () => loadConfig(state),
|
onReload: () => loadConfig(state),
|
||||||
onSave: () => saveConfig(state),
|
onSave: () => saveConfig(state),
|
||||||
onApply: () => applyConfig(state),
|
onApply: () => applyConfig(state),
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ export class ClawdbotApp extends LitElement {
|
|||||||
@state() configFormMode: "form" | "raw" = "form";
|
@state() configFormMode: "form" | "raw" = "form";
|
||||||
@state() configSearchQuery = "";
|
@state() configSearchQuery = "";
|
||||||
@state() configActiveSection: string | null = null;
|
@state() configActiveSection: string | null = null;
|
||||||
|
@state() configActiveSubsection: string | null = null;
|
||||||
|
|
||||||
@state() channelsLoading = false;
|
@state() channelsLoading = false;
|
||||||
@state() channelsSnapshot: ChannelsStatusSnapshot | null = null;
|
@state() channelsSnapshot: ChannelsStatusSnapshot | null = null;
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export type ConfigState = {
|
|||||||
configFormMode: "form" | "raw";
|
configFormMode: "form" | "raw";
|
||||||
configSearchQuery: string;
|
configSearchQuery: string;
|
||||||
configActiveSection: string | null;
|
configActiveSection: string | null;
|
||||||
|
configActiveSubsection: string | null;
|
||||||
lastError: string | null;
|
lastError: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { html, nothing } from "lit";
|
import { html, nothing } from "lit";
|
||||||
import type { ConfigUiHints } from "../types";
|
import type { ConfigUiHints } from "../types";
|
||||||
import { hintForPath, schemaType, type JsonSchema } from "./config-form.shared";
|
import {
|
||||||
|
hintForPath,
|
||||||
|
humanize,
|
||||||
|
schemaType,
|
||||||
|
type JsonSchema,
|
||||||
|
} from "./config-form.shared";
|
||||||
import { renderNode } from "./config-form.node";
|
import { renderNode } from "./config-form.node";
|
||||||
|
|
||||||
export type ConfigFormProps = {
|
export type ConfigFormProps = {
|
||||||
@@ -11,6 +16,7 @@ export type ConfigFormProps = {
|
|||||||
unsupportedPaths?: string[];
|
unsupportedPaths?: string[];
|
||||||
searchQuery?: string;
|
searchQuery?: string;
|
||||||
activeSection?: string | null;
|
activeSection?: string | null;
|
||||||
|
activeSubsection?: string | null;
|
||||||
onPatch: (path: Array<string | number>, value: unknown) => void;
|
onPatch: (path: Array<string | number>, value: unknown) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -146,6 +152,7 @@ export function renderConfigForm(props: ConfigFormProps) {
|
|||||||
const properties = schema.properties;
|
const properties = schema.properties;
|
||||||
const searchQuery = props.searchQuery ?? "";
|
const searchQuery = props.searchQuery ?? "";
|
||||||
const activeSection = props.activeSection;
|
const activeSection = props.activeSection;
|
||||||
|
const activeSubsection = props.activeSubsection ?? null;
|
||||||
|
|
||||||
// Filter and sort entries
|
// Filter and sort entries
|
||||||
let entries = Object.entries(properties);
|
let entries = Object.entries(properties);
|
||||||
@@ -168,6 +175,25 @@ export function renderConfigForm(props: ConfigFormProps) {
|
|||||||
return a[0].localeCompare(b[0]);
|
return a[0].localeCompare(b[0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let subsectionContext:
|
||||||
|
| { sectionKey: string; subsectionKey: string; schema: JsonSchema }
|
||||||
|
| null = null;
|
||||||
|
if (activeSection && activeSubsection && entries.length === 1) {
|
||||||
|
const sectionSchema = entries[0]?.[1];
|
||||||
|
if (
|
||||||
|
sectionSchema &&
|
||||||
|
schemaType(sectionSchema) === "object" &&
|
||||||
|
sectionSchema.properties &&
|
||||||
|
sectionSchema.properties[activeSubsection]
|
||||||
|
) {
|
||||||
|
subsectionContext = {
|
||||||
|
sectionKey: activeSection,
|
||||||
|
subsectionKey: activeSubsection,
|
||||||
|
schema: sectionSchema.properties[activeSubsection],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (entries.length === 0) {
|
if (entries.length === 0) {
|
||||||
return html`
|
return html`
|
||||||
<div class="config-empty">
|
<div class="config-empty">
|
||||||
@@ -183,38 +209,76 @@ export function renderConfigForm(props: ConfigFormProps) {
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="config-form config-form--modern">
|
<div class="config-form config-form--modern">
|
||||||
${entries.map(([key, node]) => {
|
${subsectionContext
|
||||||
const meta = SECTION_META[key] ?? {
|
? (() => {
|
||||||
label: key.charAt(0).toUpperCase() + key.slice(1),
|
const { sectionKey, subsectionKey, schema: node } = subsectionContext;
|
||||||
description: node.description ?? ""
|
const hint = hintForPath([sectionKey, subsectionKey], props.uiHints);
|
||||||
};
|
const label = hint?.label ?? node.title ?? humanize(subsectionKey);
|
||||||
|
const description = hint?.help ?? node.description ?? "";
|
||||||
return html`
|
const sectionValue = (value as Record<string, unknown>)[sectionKey];
|
||||||
<section class="config-section-card" id="config-section-${key}">
|
const scopedValue =
|
||||||
<div class="config-section-card__header">
|
sectionValue && typeof sectionValue === "object"
|
||||||
<span class="config-section-card__icon">${getSectionIcon(key)}</span>
|
? (sectionValue as Record<string, unknown>)[subsectionKey]
|
||||||
<div class="config-section-card__titles">
|
: undefined;
|
||||||
<h3 class="config-section-card__title">${meta.label}</h3>
|
const id = `config-section-${sectionKey}-${subsectionKey}`;
|
||||||
${meta.description ? html`
|
return html`
|
||||||
<p class="config-section-card__desc">${meta.description}</p>
|
<section class="config-section-card" id=${id}>
|
||||||
` : nothing}
|
<div class="config-section-card__header">
|
||||||
</div>
|
<span class="config-section-card__icon">${getSectionIcon(sectionKey)}</span>
|
||||||
</div>
|
<div class="config-section-card__titles">
|
||||||
<div class="config-section-card__content">
|
<h3 class="config-section-card__title">${label}</h3>
|
||||||
${renderNode({
|
${description
|
||||||
schema: node,
|
? html`<p class="config-section-card__desc">${description}</p>`
|
||||||
value: (value as Record<string, unknown>)[key],
|
: nothing}
|
||||||
path: [key],
|
</div>
|
||||||
hints: props.uiHints,
|
</div>
|
||||||
unsupported,
|
<div class="config-section-card__content">
|
||||||
disabled: props.disabled ?? false,
|
${renderNode({
|
||||||
showLabel: false,
|
schema: node,
|
||||||
onPatch: props.onPatch,
|
value: scopedValue,
|
||||||
})}
|
path: [sectionKey, subsectionKey],
|
||||||
</div>
|
hints: props.uiHints,
|
||||||
</section>
|
unsupported,
|
||||||
`;
|
disabled: props.disabled ?? false,
|
||||||
})}
|
showLabel: false,
|
||||||
|
onPatch: props.onPatch,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
`;
|
||||||
|
})()
|
||||||
|
: entries.map(([key, node]) => {
|
||||||
|
const meta = SECTION_META[key] ?? {
|
||||||
|
label: key.charAt(0).toUpperCase() + key.slice(1),
|
||||||
|
description: node.description ?? "",
|
||||||
|
};
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<section class="config-section-card" id="config-section-${key}">
|
||||||
|
<div class="config-section-card__header">
|
||||||
|
<span class="config-section-card__icon">${getSectionIcon(key)}</span>
|
||||||
|
<div class="config-section-card__titles">
|
||||||
|
<h3 class="config-section-card__title">${meta.label}</h3>
|
||||||
|
${meta.description
|
||||||
|
? html`<p class="config-section-card__desc">${meta.description}</p>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="config-section-card__content">
|
||||||
|
${renderNode({
|
||||||
|
schema: node,
|
||||||
|
value: (value as Record<string, unknown>)[key],
|
||||||
|
path: [key],
|
||||||
|
hints: props.uiHints,
|
||||||
|
unsupported,
|
||||||
|
disabled: props.disabled ?? false,
|
||||||
|
showLabel: false,
|
||||||
|
onPatch: props.onPatch,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,13 +21,20 @@ describe("config view", () => {
|
|||||||
uiHints: {},
|
uiHints: {},
|
||||||
formMode: "form" as const,
|
formMode: "form" as const,
|
||||||
formValue: {},
|
formValue: {},
|
||||||
|
originalValue: {},
|
||||||
|
searchQuery: "",
|
||||||
|
activeSection: null,
|
||||||
|
activeSubsection: null,
|
||||||
onRawChange: vi.fn(),
|
onRawChange: vi.fn(),
|
||||||
onFormModeChange: vi.fn(),
|
onFormModeChange: vi.fn(),
|
||||||
onFormPatch: vi.fn(),
|
onFormPatch: vi.fn(),
|
||||||
|
onSearchChange: vi.fn(),
|
||||||
|
onSectionChange: vi.fn(),
|
||||||
onReload: vi.fn(),
|
onReload: vi.fn(),
|
||||||
onSave: vi.fn(),
|
onSave: vi.fn(),
|
||||||
onApply: vi.fn(),
|
onApply: vi.fn(),
|
||||||
onUpdate: vi.fn(),
|
onUpdate: vi.fn(),
|
||||||
|
onSubsectionChange: vi.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
it("disables save when form is unsafe", () => {
|
it("disables save when form is unsafe", () => {
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { html, nothing } from "lit";
|
import { html, nothing } from "lit";
|
||||||
import type { ConfigUiHints } from "../types";
|
import type { ConfigUiHints } from "../types";
|
||||||
import { analyzeConfigSchema, renderConfigForm } from "./config-form";
|
import { analyzeConfigSchema, renderConfigForm } from "./config-form";
|
||||||
|
import {
|
||||||
|
hintForPath,
|
||||||
|
humanize,
|
||||||
|
schemaType,
|
||||||
|
type JsonSchema,
|
||||||
|
} from "./config-form.shared";
|
||||||
|
|
||||||
export type ConfigProps = {
|
export type ConfigProps = {
|
||||||
raw: string;
|
raw: string;
|
||||||
@@ -19,11 +25,13 @@ export type ConfigProps = {
|
|||||||
originalValue: Record<string, unknown> | null;
|
originalValue: Record<string, unknown> | null;
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
activeSection: string | null;
|
activeSection: string | null;
|
||||||
|
activeSubsection: string | null;
|
||||||
onRawChange: (next: string) => void;
|
onRawChange: (next: string) => void;
|
||||||
onFormModeChange: (mode: "form" | "raw") => void;
|
onFormModeChange: (mode: "form" | "raw") => void;
|
||||||
onFormPatch: (path: Array<string | number>, value: unknown) => void;
|
onFormPatch: (path: Array<string | number>, value: unknown) => void;
|
||||||
onSearchChange: (query: string) => void;
|
onSearchChange: (query: string) => void;
|
||||||
onSectionChange: (section: string | null) => void;
|
onSectionChange: (section: string | null) => void;
|
||||||
|
onSubsectionChange: (section: string | null) => void;
|
||||||
onReload: () => void;
|
onReload: () => void;
|
||||||
onSave: () => void;
|
onSave: () => void;
|
||||||
onApply: () => void;
|
onApply: () => void;
|
||||||
@@ -80,10 +88,49 @@ const SECTIONS: Array<{ key: string; label: string }> = [
|
|||||||
{ key: "wizard", label: "Setup Wizard" },
|
{ key: "wizard", label: "Setup Wizard" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
type SubsectionEntry = {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
order: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ALL_SUBSECTION = "__all__";
|
||||||
|
|
||||||
function getSectionIcon(key: string) {
|
function getSectionIcon(key: string) {
|
||||||
return sidebarIcons[key as keyof typeof sidebarIcons] ?? sidebarIcons.default;
|
return sidebarIcons[key as keyof typeof sidebarIcons] ?? sidebarIcons.default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveSectionMeta(key: string, schema?: JsonSchema): {
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
} {
|
||||||
|
const meta = SECTION_META[key];
|
||||||
|
if (meta) return meta;
|
||||||
|
return {
|
||||||
|
label: schema?.title ?? humanize(key),
|
||||||
|
description: schema?.description ?? "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveSubsections(params: {
|
||||||
|
key: string;
|
||||||
|
schema: JsonSchema | undefined;
|
||||||
|
uiHints: ConfigUiHints;
|
||||||
|
}): SubsectionEntry[] {
|
||||||
|
const { key, schema, uiHints } = params;
|
||||||
|
if (!schema || schemaType(schema) !== "object" || !schema.properties) return [];
|
||||||
|
const entries = Object.entries(schema.properties).map(([subKey, node]) => {
|
||||||
|
const hint = hintForPath([key, subKey], uiHints);
|
||||||
|
const label = hint?.label ?? node.title ?? humanize(subKey);
|
||||||
|
const description = hint?.help ?? node.description ?? "";
|
||||||
|
const order = hint?.order ?? 50;
|
||||||
|
return { key: subKey, label, description, order };
|
||||||
|
});
|
||||||
|
entries.sort((a, b) => (a.order !== b.order ? a.order - b.order : a.key.localeCompare(b.key)));
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
function computeDiff(
|
function computeDiff(
|
||||||
original: Record<string, unknown> | null,
|
original: Record<string, unknown> | null,
|
||||||
current: Record<string, unknown> | null
|
current: Record<string, unknown> | null
|
||||||
@@ -164,6 +211,31 @@ export function renderConfig(props: ConfigProps) {
|
|||||||
.map(k => ({ key: k, label: k.charAt(0).toUpperCase() + k.slice(1) }));
|
.map(k => ({ key: k, label: k.charAt(0).toUpperCase() + k.slice(1) }));
|
||||||
|
|
||||||
const allSections = [...availableSections, ...extraSections];
|
const allSections = [...availableSections, ...extraSections];
|
||||||
|
|
||||||
|
const activeSectionSchema =
|
||||||
|
props.activeSection && analysis.schema && schemaType(analysis.schema) === "object"
|
||||||
|
? (analysis.schema.properties?.[props.activeSection] as JsonSchema | undefined)
|
||||||
|
: undefined;
|
||||||
|
const activeSectionMeta = props.activeSection
|
||||||
|
? resolveSectionMeta(props.activeSection, activeSectionSchema)
|
||||||
|
: null;
|
||||||
|
const subsections = props.activeSection
|
||||||
|
? resolveSubsections({
|
||||||
|
key: props.activeSection,
|
||||||
|
schema: activeSectionSchema,
|
||||||
|
uiHints: props.uiHints,
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
const allowSubnav =
|
||||||
|
props.formMode === "form" &&
|
||||||
|
Boolean(props.activeSection) &&
|
||||||
|
subsections.length > 0;
|
||||||
|
const isAllSubsection = props.activeSubsection === ALL_SUBSECTION;
|
||||||
|
const effectiveSubsection = props.searchQuery
|
||||||
|
? null
|
||||||
|
: isAllSubsection
|
||||||
|
? null
|
||||||
|
: props.activeSubsection ?? (subsections[0]?.key ?? null);
|
||||||
|
|
||||||
// Compute diff for showing changes
|
// Compute diff for showing changes
|
||||||
const diff = props.formMode === "form"
|
const diff = props.formMode === "form"
|
||||||
@@ -304,6 +376,46 @@ export function renderConfig(props: ConfigProps) {
|
|||||||
</details>
|
</details>
|
||||||
` : nothing}
|
` : nothing}
|
||||||
|
|
||||||
|
${activeSectionMeta && props.formMode === "form"
|
||||||
|
? html`
|
||||||
|
<div class="config-section-hero">
|
||||||
|
<div class="config-section-hero__icon">${getSectionIcon(props.activeSection ?? "")}</div>
|
||||||
|
<div class="config-section-hero__text">
|
||||||
|
<div class="config-section-hero__title">${activeSectionMeta.label}</div>
|
||||||
|
${activeSectionMeta.description
|
||||||
|
? html`<div class="config-section-hero__desc">${activeSectionMeta.description}</div>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
|
||||||
|
${allowSubnav
|
||||||
|
? html`
|
||||||
|
<div class="config-subnav">
|
||||||
|
<button
|
||||||
|
class="config-subnav__item ${effectiveSubsection === null ? "active" : ""}"
|
||||||
|
@click=${() => props.onSubsectionChange(ALL_SUBSECTION)}
|
||||||
|
>
|
||||||
|
All
|
||||||
|
</button>
|
||||||
|
${subsections.map(
|
||||||
|
(entry) => html`
|
||||||
|
<button
|
||||||
|
class="config-subnav__item ${
|
||||||
|
effectiveSubsection === entry.key ? "active" : ""
|
||||||
|
}"
|
||||||
|
title=${entry.description || entry.label}
|
||||||
|
@click=${() => props.onSubsectionChange(entry.key)}
|
||||||
|
>
|
||||||
|
${entry.label}
|
||||||
|
</button>
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
|
||||||
<!-- Form content -->
|
<!-- Form content -->
|
||||||
<div class="config-content">
|
<div class="config-content">
|
||||||
${props.formMode === "form"
|
${props.formMode === "form"
|
||||||
@@ -322,6 +434,7 @@ export function renderConfig(props: ConfigProps) {
|
|||||||
onPatch: props.onFormPatch,
|
onPatch: props.onFormPatch,
|
||||||
searchQuery: props.searchQuery,
|
searchQuery: props.searchQuery,
|
||||||
activeSection: props.activeSection,
|
activeSection: props.activeSection,
|
||||||
|
activeSubsection: effectiveSubsection,
|
||||||
})}
|
})}
|
||||||
${formUnsafe
|
${formUnsafe
|
||||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||||
|
|||||||
Reference in New Issue
Block a user