Add inline timer form validation
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s
This commit is contained in:
parent
3e16ea278d
commit
d54a93e1f4
6 changed files with 80 additions and 4 deletions
|
|
@ -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 :
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue