From d54a93e1f4ffda2cfb3ea2e51b4e1b1087f8b6a0 Mon Sep 17 00:00:00 2001 From: Shinuwa Date: Mon, 3 Aug 2026 16:20:05 +0200 Subject: [PATCH] Add inline timer form validation --- DESIGN_SYSTEM.md | 1 + tests/helpers/data-validation.mjs | 2 ++ website/public/data/site.json | 2 ++ .../toolboxes/modules/TimerModule.jsx | 34 +++++++++++++++++-- website/src/hooks/useDraftForm.js | 17 ++++++++-- website/src/styles/_toolboxes.scss | 28 +++++++++++++++ 6 files changed, 80 insertions(+), 4 deletions(-) diff --git a/DESIGN_SYSTEM.md b/DESIGN_SYSTEM.md index 8278c30..cdcaf58 100644 --- a/DESIGN_SYSTEM.md +++ b/DESIGN_SYSTEM.md @@ -173,6 +173,7 @@ Regles UI : - les boutons de controle utilisent les icones `play-circle`, `pause-circle`, `record-circle`, `stop-circle`, `refresh`, `sort-time`, `scrollable`. - pour `Heure precise` et `Intervalle`, le refresh inline est un toggle compact de repetition automatique ; il est bloque si la frequence est inferieure a 5 minutes. - le formulaire `Intervalle` affiche deux champs `hh:mm:ss` compacts : duree de repetition et depart optionnel ; un depart vide signifie `Maintenant`. +- les invalidations du formulaire countdown s'affichent en notice inline rouge compacte, portée par `useDraftForm`. Alertes timer : diff --git a/tests/helpers/data-validation.mjs b/tests/helpers/data-validation.mjs index 4eea9fe..03053ad 100644 --- a/tests/helpers/data-validation.mjs +++ b/tests/helpers/data-validation.mjs @@ -265,6 +265,8 @@ export function validateSiteContent(site) { "toolboxes.modules.timer.intervalStartTimeOption", "toolboxes.modules.timer.intervalStartTimeLabel", "toolboxes.modules.timer.addCountdownButton", + "toolboxes.modules.timer.requiredError", + "toolboxes.modules.timer.invalidError", "toolboxes.modules.timer.defaultCountdownLabel", "toolboxes.modules.timer.finishedLabel", "toolboxes.modules.timer.targetLabel", diff --git a/website/public/data/site.json b/website/public/data/site.json index 363bca8..4752927 100644 --- a/website/public/data/site.json +++ b/website/public/data/site.json @@ -430,6 +430,8 @@ "intervalStartTimeOption": "Heure précise", "intervalStartTimeLabel": "Départ", "addCountdownButton": "Ajouter", + "requiredError": "Valeur requise.", + "invalidError": "Valeur invalide.", "defaultCountdownLabel": "Timer", "finishedLabel": "Terminé", "targetLabel": "Suivant", diff --git a/website/src/features/toolboxes/modules/TimerModule.jsx b/website/src/features/toolboxes/modules/TimerModule.jsx index 6166b43..10bcb1c 100644 --- a/website/src/features/toolboxes/modules/TimerModule.jsx +++ b/website/src/features/toolboxes/modules/TimerModule.jsx @@ -94,6 +94,27 @@ function hasTimePartValue(parts) { return Boolean(String(parts?.hours || "").trim() || String(parts?.minutes || "").trim() || String(parts?.seconds || "").trim()); } +function getCountdownValidationError(type, draft, textContent) { + const requiredError = textContent.requiredError || "Valeur requise."; + const invalidError = textContent.invalidError || "Valeur invalide."; + if (type === "duration" && timePartsToDurationMs(draft.duration) <= 0) { + return requiredError; + } + if (type === "daily_time" && !timePartsToString(draft.time)) { + return invalidError; + } + if (type === "time_pattern") { + if (!hasTimePartValue(draft.pattern)) return requiredError; + if (!timePartsToString(draft.pattern, { allowWildcard: true })) return invalidError; + } + if (type === "interval") { + const startTimeHasValue = hasTimePartValue(draft.intervalStartTime); + if (timePartsToDurationMs(draft.interval) <= 0) return requiredError; + if (startTimeHasValue && !timePartsToString(draft.intervalStartTime)) return invalidError; + } + return ""; +} + export function TimerModule({ toolboxId, moduleId, context }) { const data = context.normalizeTimerData(context.getModuleData(toolboxId, moduleId, { activeTab: "stopwatch" })); const textContent = context.moduleText?.timer || {}; @@ -194,6 +215,8 @@ export function TimerModule({ toolboxId, moduleId, context }) { function createCountdown(event) { countdownDraft.handleSubmit(event, (draft, { updateValues }) => { const type = COUNTDOWN_TYPES.includes(draft.type) ? draft.type : "duration"; + const validationError = getCountdownValidationError(type, draft, textContent); + if (validationError) return validationError; const label = draft.label.trim() || textContent.defaultCountdownLabel || "Timer"; const now = getNowMs(); let countdown = null; @@ -335,6 +358,7 @@ export function TimerModule({ toolboxId, moduleId, context }) { @@ -441,9 +465,9 @@ function StopwatchControls({ textContent, elapsedMs, running, onStart, onPause, ); } -function CountdownControls({ textContent, form, onChange, onSubmit }) { +function CountdownControls({ textContent, form, error, onChange, onSubmit }) { return ( -
+ )} + {error && ( + + )}
); diff --git a/website/src/hooks/useDraftForm.js b/website/src/hooks/useDraftForm.js index 93f3aa3..ac1361a 100644 --- a/website/src/hooks/useDraftForm.js +++ b/website/src/hooks/useDraftForm.js @@ -1,23 +1,31 @@ // Rôle : centralise les petits formulaires contrôlés avec brouillon local. -// Fournit helpers de champs, submit preventDefault et reset, sans porter de validation métier. +// Fournit helpers de champs, submit preventDefault, reset et erreur inline retournée par le submit métier. // À utiliser quand plusieurs champs sont simplement saisis puis nettoyés au submit. import { useState } from "react"; export function useDraftForm(initialValues) { const [values, setValues] = useState(initialValues); + const [error, setError] = useState(""); function setField(name, value) { + setError(""); setValues((current) => ({ ...current, [name]: value })); } function updateValues(patch) { + setError(""); setValues((current) => ({ ...current, ...patch })); } function reset(nextValues = initialValues) { + setError(""); setValues(nextValues); } + function clearError() { + setError(""); + } + function getFieldProps(name, props = {}) { return { ...props, @@ -32,13 +40,18 @@ export function useDraftForm(initialValues) { function handleSubmit(event, onSubmit) { event.preventDefault(); - onSubmit(values, { reset, setField, setValues, updateValues }); + const result = onSubmit(values, { reset, setField, setValues, updateValues, setError, clearError }); + if (typeof result === "string") setError(result); + else setError(""); } return { values, + error, setField, setValues, + setError, + clearError, updateValues, reset, getFieldProps, diff --git a/website/src/styles/_toolboxes.scss b/website/src/styles/_toolboxes.scss index fecdb8e..97ef861 100644 --- a/website/src/styles/_toolboxes.scss +++ b/website/src/styles/_toolboxes.scss @@ -3609,6 +3609,34 @@ button.combo-input-token.combo-value-grab .ui-icon { font-size: var(--font-size-xs); } +.timer-form-error { + display: inline-flex; + width: 100%; + min-width: 0; + align-items: center; + gap: 7px; + padding: 7px 9px; + border: 1px solid rgba(251, 113, 133, 0.26); + border-radius: var(--radius-sm); + background: rgba(80, 25, 42, 0.28); + color: var(--color-danger); + font-size: var(--font-size-xs); + font-weight: 800; +} + +.timer-form-error .ui-icon { + width: 13px; + height: 13px; + flex: 0 0 auto; +} + +.timer-form-error span { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .timer-control-panel > span, .timer-control-panel label span { color: var(--color-text-secondary);