feat: Improve body chart days configuration in DashboardLabPage
All checks were successful
Deploy Development / deploy (push) Successful in 46s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 15s

- Introduced a draft state for body chart days input to allow for temporary edits without immediate clamping.
- Updated input handling to support numeric input mode and improved accessibility with aria-label.
- Enhanced save and reset functionalities to manage the new draft state effectively.
- Refactored layout normalization to accommodate changes in body chart days configuration.
This commit is contained in:
Lars 2026-04-07 12:05:05 +02:00
parent 87c4cbc4b4
commit 4493b140bd

View File

@ -28,15 +28,28 @@ export default function DashboardLabPage() {
const [err, setErr] = useState(null)
const [busy, setBusy] = useState(false)
const [msg, setMsg] = useState(null)
/** Während der Fokus im Körper-Chart-Feld: Rohstring, damit Tippen (z. B. „14“) nicht sofort geclamped wird */
const [bodyChartDaysDraft, setBodyChartDaysDraft] = useState(null)
const metaById = catalogMetaById(catalog)
const commitBodyChartDraftToLayout = useCallback((draftStr, baseLayout) => {
const clamped = normalizeBodyChartDays(draftStr === '' || draftStr == null ? BODY_CHART_DAYS_DEFAULT : draftStr)
return {
...baseLayout,
widgets: baseLayout.widgets.map((x) =>
x.id !== 'body_overview' ? x : { ...x, config: { ...x.config, chart_days: clamped } }
),
}
}, [])
const load = useCallback(async () => {
setErr(null)
try {
const [cat, b] = await Promise.all([api.getAppWidgetsCatalog(), api.getAppDashboardLayout()])
setCatalog(cat)
setBundle(b)
setBodyChartDaysDraft(null)
setLayout(normalizeLayoutForEditor(b.layout))
} catch (e) {
setErr(formatFastApiDetail(null, e.message))
@ -49,11 +62,17 @@ export default function DashboardLabPage() {
const save = async () => {
if (!layout) return
let toSave = layout
if (bodyChartDaysDraft !== null) {
toSave = normalizeLayoutForEditor(commitBodyChartDraftToLayout(bodyChartDaysDraft, layout))
setLayout(toSave)
setBodyChartDaysDraft(null)
}
setBusy(true)
setMsg(null)
setErr(null)
try {
await api.putAppDashboardLayout(layout)
await api.putAppDashboardLayout(toSave)
setMsg('Layout gespeichert.')
await load()
} catch (e) {
@ -70,6 +89,7 @@ export default function DashboardLabPage() {
setErr(null)
try {
const r = await api.resetAppDashboardLayout()
setBodyChartDaysDraft(null)
setLayout(normalizeLayoutForEditor(r.layout))
setMsg('Auf Standard zurückgesetzt.')
await load()
@ -82,6 +102,7 @@ export default function DashboardLabPage() {
const applyDefaultLocal = () => {
if (bundle?.default_layout) {
setBodyChartDaysDraft(null)
setLayout(normalizeLayoutForEditor(structuredClone(bundle.default_layout)))
setMsg('Standard geladen (noch nicht gespeichert).')
}
@ -206,37 +227,29 @@ export default function DashboardLabPage() {
Körper-Chart Zeitraum (Tage): {BODY_CHART_DAYS_MIN}{BODY_CHART_DAYS_MAX}
</label>
<input
type="number"
type="text"
inputMode="numeric"
autoComplete="off"
className="form-input"
style={{ maxWidth: 120 }}
min={BODY_CHART_DAYS_MIN}
max={BODY_CHART_DAYS_MAX}
value={chartDaysVal}
onChange={(e) => {
const v = parseInt(e.target.value, 10)
setLayout((L) => ({
...L,
widgets: L.widgets.map((x, j) =>
j !== i
? x
: {
...x,
config: {
...x.config,
chart_days: Number.isFinite(v) ? v : BODY_CHART_DAYS_DEFAULT,
},
}
),
}))
aria-label="Körper-Chart Zeitraum in Tagen"
value={
bodyChartDaysDraft !== null
? bodyChartDaysDraft
: String(chartDaysVal)
}
onFocus={() => setBodyChartDaysDraft(String(chartDaysVal))}
onChange={(e) => setBodyChartDaysDraft(e.target.value)}
onBlur={() => {
setLayout((L) =>
normalizeLayoutForEditor(
commitBodyChartDraftToLayout(bodyChartDaysDraft ?? String(chartDaysVal), L)
)
)
setBodyChartDaysDraft(null)
}}
onBlur={(e) => {
const clamped = normalizeBodyChartDays(e.target.value)
setLayout((L) => ({
...L,
widgets: L.widgets.map((x, j) =>
j !== i ? x : { ...x, config: { ...x.config, chart_days: clamped } }
),
}))
onKeyDown={(e) => {
if (e.key === 'Enter') e.currentTarget.blur()
}}
/>
</div>