Add inline timer form validation
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s

This commit is contained in:
Shinuwa 2026-08-03 16:20:05 +02:00
parent 3e16ea278d
commit d54a93e1f4
6 changed files with 80 additions and 4 deletions

View file

@ -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 }) {
<CountdownControls
textContent={textContent}
form={countdownDraft.values}
error={countdownDraft.error}
onChange={countdownDraft.updateValues}
onSubmit={createCountdown}
/>
@ -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 (
<form className="timer-control-panel timer-countdown-form" role="tabpanel" onSubmit={onSubmit}>
<form className="timer-control-panel timer-countdown-form" role="tabpanel" onSubmit={onSubmit} aria-describedby={error ? "timer-countdown-error" : undefined}>
<label>
<span>{textContent.countdownLabel || "Libellé"}</span>
<input value={form.label} onChange={(event) => onChange({ label: event.target.value })} placeholder={textContent.countdownLabelPlaceholder || "Boss, event, craft..."} />
@ -490,6 +514,12 @@ function CountdownControls({ textContent, form, onChange, onSubmit }) {
</label>
</div>
)}
{error && (
<p id="timer-countdown-error" className="timer-form-error" role="alert">
<Icon name="close" />
<span>{error}</span>
</p>
)}
<button className="primary" type="submit">{textContent.addCountdownButton || "Ajouter"}</button>
</form>
);

View file

@ -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,

View file

@ -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);