M5-T01B: render ConfigPage sections as Mantine Accordion

This commit is contained in:
2026-06-22 12:34:04 +02:00
parent da7d0f9d62
commit eac7860b51
3 changed files with 92 additions and 31 deletions
+47 -29
View File
@@ -1,8 +1,8 @@
/**
* ConfigPage — config editor (M2-T08).
* ConfigPage — config editor (M2-T08, M5-T01B).
*
* Behaviours:
* 1. Load config: GET /api/config → render sections (grouped) with Mantine inputs.
* 1. Load config: GET /api/config → render sections as Accordion items (M5-T01B).
* - Non-secret fields show their value.
* - Secret fields render as empty PasswordInput (never show a masked value).
* 2. Save config: PUT /api/config with full-field submission semantics.
@@ -13,11 +13,16 @@
* 3. SMTP test button: POST /api/config/smtp/test.
* - Tri-state: success / config-error / failed.
* - Errors read `err.body.result` from ApiError.
*
* Layout (M5-T01B): each config section = one Accordion.Item. The first section
* is open by default. Future panels (e.g. T12 Home Assistant Expose) should be
* appended as additional Accordion.Items after the config sections.
*/
import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import {
Accordion,
Container,
Title,
Text,
@@ -30,7 +35,6 @@ import {
Divider,
Loader,
Center,
Paper,
Badge,
} from '@mantine/core'
import apiClient, { ApiError } from '../api/client'
@@ -153,32 +157,27 @@ function ConfigFieldInput({ field, value, onChange }: ConfigFieldInputProps) {
}
// ---------------------------------------------------------------------------
// ConfigSectionPanel — one section
// ConfigSectionFields — the fields inside one accordion panel
// ---------------------------------------------------------------------------
interface ConfigSectionPanelProps {
interface ConfigSectionFieldsProps {
section: ConfigSection
localValues: Record<string, string>
onChange: (envName: string, value: string) => void
}
function ConfigSectionPanel({ section, localValues, onChange }: ConfigSectionPanelProps) {
function ConfigSectionFields({ section, localValues, onChange }: ConfigSectionFieldsProps) {
return (
<Paper withBorder p="md" radius="md">
<Title order={4} mb="md">
{section.name}
</Title>
<Stack gap="sm">
{section.fields.map((field) => (
<ConfigFieldInput
key={field.env_name}
field={field}
value={localValues[field.env_name] ?? ''}
onChange={onChange}
/>
))}
</Stack>
</Paper>
<Stack gap="sm">
{section.fields.map((field) => (
<ConfigFieldInput
key={field.env_name}
field={field}
value={localValues[field.env_name] ?? ''}
onChange={onChange}
/>
))}
</Stack>
)
}
@@ -338,6 +337,9 @@ export function ConfigPage() {
s.name.toLowerCase().includes('smtp') || s.name.toLowerCase().includes('email'),
)
// Default: open the first section so users immediately see content.
const defaultAccordionValue = data.sections[0]?.name ?? null
return (
<Container size="md" pt="xl" pb="xl" data-testid="config-page">
<Group justify="space-between" mb="lg" wrap="nowrap">
@@ -349,14 +351,30 @@ export function ConfigPage() {
<form onSubmit={handleSave} data-testid="config-form">
<Stack gap="lg">
{data.sections.map((section) => (
<ConfigSectionPanel
key={section.name}
section={section}
localValues={localValues}
onChange={handleChange}
/>
))}
{/* M5-T01B: each config section is one Accordion.Item.
Future panels (e.g. T12 Home Assistant Expose) should be appended
as additional Accordion.Items here, after the config sections. */}
<Accordion
defaultValue={defaultAccordionValue}
variant="separated"
radius="md"
data-testid="config-accordion"
>
{data.sections.map((section) => (
<Accordion.Item key={section.name} value={section.name}>
<Accordion.Control data-testid={`accordion-control-${section.name}`}>
<Text fw={500}>{section.name}</Text>
</Accordion.Control>
<Accordion.Panel>
<ConfigSectionFields
section={section}
localValues={localValues}
onChange={handleChange}
/>
</Accordion.Panel>
</Accordion.Item>
))}
</Accordion>
<Divider />