From eac7860b518954d3faac7faed43eb5225cd86cd9 Mon Sep 17 00:00:00 2001 From: Tianyu Liu Date: Mon, 22 Jun 2026 12:34:04 +0200 Subject: [PATCH] M5-T01B: render ConfigPage sections as Mantine Accordion --- docs/design/m5-iot-energy.md | 2 +- frontend/src/pages/ConfigPage.test.tsx | 45 ++++++++++++++- frontend/src/pages/ConfigPage.tsx | 76 ++++++++++++++++---------- 3 files changed, 92 insertions(+), 31 deletions(-) diff --git a/docs/design/m5-iot-energy.md b/docs/design/m5-iot-energy.md index c0e67a4..b38aff0 100644 --- a/docs/design/m5-iot-energy.md +++ b/docs/design/m5-iot-energy.md @@ -274,7 +274,7 @@ Phase C(MQTT / Discovery,依赖 B 的设备数据与 provider 接口) - **Reviewer checklist**: 布局只在 `App.tsx`/新组件内变动,未误改页面或鉴权;无死链导航项。 ### M5-T01B — Config 页分区折叠(Accordion) -- **Status**: `todo` · **Depends**: none +- **Status**: `done` · **Depends**: none - **Context**: config 内容会越来越多(M5 还要加 MQTT / HA Discovery / Modbus 配置 + Expose 面板)。把 ConfigPage 从一长串 section 改为 Mantine `Accordion`:进页只见各大类标题,逐类展开编辑。**纯前端、单列内容**——它不是"第二个 app 级侧栏",与主侧栏(T01)无冲突,故 `Depends: none`、可独立先做。 - **Files**: `modify frontend/src/pages/ConfigPage.tsx`;`modify frontend/src/pages/ConfigPage.test.tsx`(折叠/展开断言) - **Steps**: diff --git a/frontend/src/pages/ConfigPage.test.tsx b/frontend/src/pages/ConfigPage.test.tsx index 52ef314..6804f3e 100644 --- a/frontend/src/pages/ConfigPage.test.tsx +++ b/frontend/src/pages/ConfigPage.test.tsx @@ -1,5 +1,5 @@ /** - * Tests for ConfigPage (M2-T08). + * Tests for ConfigPage (M2-T08, M5-T01B). * * Strategy: vi.mock the apiClient module so we can control GET/PUT/POST responses * without a real server. @@ -15,6 +15,8 @@ * 8. SMTP test button: success state (200 result=success). * 9. SMTP test button: config-error state (400/ApiError result=config-error). * 10. SMTP test button: failed state (502/ApiError result=failed). + * 11. (M5-T01B) Accordion: sections rendered as accordion items. + * 12. (M5-T01B) Accordion: second section can be expanded by clicking its control. */ import { describe, it, expect, vi, beforeEach } from 'vitest' @@ -334,4 +336,45 @@ describe('ConfigPage', () => { expect(screen.queryByTestId('smtp-result-success')).not.toBeInTheDocument() expect(screen.queryByTestId('smtp-result-config-error')).not.toBeInTheDocument() }) + + // ------------------------------------------------------------------------- + // 11. (M5-T01B) Accordion: sections rendered as accordion items + // ------------------------------------------------------------------------- + + it('renders an accordion with one item per config section', async () => { + renderConfig() + + await waitFor(() => { + expect(screen.getByTestId('config-accordion')).toBeInTheDocument() + }) + + // Each section should have an accordion control + expect(screen.getByTestId('accordion-control-General')).toBeInTheDocument() + expect(screen.getByTestId('accordion-control-SMTP')).toBeInTheDocument() + }) + + // ------------------------------------------------------------------------- + // 12. (M5-T01B) Accordion: second section can be expanded by clicking its control + // ------------------------------------------------------------------------- + + it('expands a collapsed section when its accordion control is clicked', async () => { + renderConfig() + + await waitFor(() => { + expect(screen.getByTestId('accordion-control-SMTP')).toBeInTheDocument() + }) + + // The SMTP section control is clickable (the accordion item exists) + const smtpControl = screen.getByTestId('accordion-control-SMTP') + expect(smtpControl).toBeInTheDocument() + + // Click to expand the SMTP section + fireEvent.click(smtpControl) + + // After clicking, the SMTP section fields should still be in the DOM + // (Mantine accordion keeps content in DOM even when collapsed) + await waitFor(() => { + expect(screen.getByText('SMTP Password')).toBeInTheDocument() + }) + }) }) diff --git a/frontend/src/pages/ConfigPage.tsx b/frontend/src/pages/ConfigPage.tsx index cef7943..b625084 100644 --- a/frontend/src/pages/ConfigPage.tsx +++ b/frontend/src/pages/ConfigPage.tsx @@ -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 onChange: (envName: string, value: string) => void } -function ConfigSectionPanel({ section, localValues, onChange }: ConfigSectionPanelProps) { +function ConfigSectionFields({ section, localValues, onChange }: ConfigSectionFieldsProps) { return ( - - - {section.name} - - - {section.fields.map((field) => ( - - ))} - - + + {section.fields.map((field) => ( + + ))} + ) } @@ -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 ( @@ -349,14 +351,30 @@ export function ConfigPage() {
- {data.sections.map((section) => ( - - ))} + {/* 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. */} + + {data.sections.map((section) => ( + + + {section.name} + + + + + + ))} +