M5-T01B: render ConfigPage sections as Mantine Accordion
This commit is contained in:
@@ -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 />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user