fix(11): address code-review findings in catalog inline-edit
- WR-01: remove blur-commit on EditableCell toggle (onChange already saves+closes; prevents double updateServiceField) - WR-02: skip server action when cell value unchanged (commit + new commitOnBlur dirty-check) - WR-03: on blur, revert required-empty field via cancel() instead of leaving the cell stuck in error state - WR-04: normalize it-IT price input (1.234,50) before parse; Number() rejects trailing garbage - IN-01: render row error in its own <tr> instead of an invalid 7th <td> in a 6-column row Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -93,8 +93,16 @@ export async function updateServiceField(
|
||||
const s = String(value).trim();
|
||||
await db.update(services).set({ category: s || null }).where(eq(services.id, serviceId));
|
||||
} else if (fieldName === "unit_price") {
|
||||
const num = parseFloat(String(value));
|
||||
if (isNaN(num) || num < 0) throw new Error("Prezzo invalido");
|
||||
// Normalize locale-formatted input (WR-04): the cell displays it-IT (€1.234,50),
|
||||
// so an admin may type "1.234,50". When a comma is present, treat "." as thousands
|
||||
// separators and "," as the decimal mark; otherwise parse "." as the decimal mark.
|
||||
// Number() (not parseFloat) rejects trailing garbage like "12abc".
|
||||
const rawInput = String(value).trim();
|
||||
const normalized = rawInput.includes(",")
|
||||
? rawInput.replace(/\./g, "").replace(",", ".")
|
||||
: rawInput;
|
||||
const num = Number(normalized);
|
||||
if (!Number.isFinite(num) || num < 0) throw new Error("Prezzo invalido");
|
||||
await db.update(services).set({ unit_price: num.toFixed(2) }).where(eq(services.id, serviceId));
|
||||
} else if (fieldName === "active") {
|
||||
const boolValue = typeof value === "boolean" ? value : value === "true";
|
||||
|
||||
@@ -34,6 +34,7 @@ function ServiceRow({ service }: { service: ServiceWithTags }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr
|
||||
className={`border-b border-[#e5e7eb] hover:bg-[#f9f9f9] transition-colors duration-150 ${
|
||||
!service.active ? "opacity-50" : ""
|
||||
@@ -85,12 +86,15 @@ function ServiceRow({ service }: { service: ServiceWithTags }) {
|
||||
onSave={(v) => saveField("active", v)}
|
||||
/>
|
||||
</td>
|
||||
{error && (
|
||||
</tr>
|
||||
{error && (
|
||||
<tr>
|
||||
<td className="py-1 px-3 text-xs text-red-600" colSpan={6}>
|
||||
{error}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
</tr>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,12 @@ export function EditableCell({
|
||||
}
|
||||
|
||||
function commit() {
|
||||
// WR-02: skip redundant server action when nothing changed
|
||||
if (tempValue === String(value)) {
|
||||
setError(null);
|
||||
setIsEditing(false);
|
||||
return;
|
||||
}
|
||||
if (required && tempValue.trim().length === 0) {
|
||||
setError("Campo richiesto");
|
||||
return;
|
||||
@@ -55,6 +61,23 @@ export function EditableCell({
|
||||
setIsEditing(false);
|
||||
}
|
||||
|
||||
// Blur path: never leave the cell stuck. No-op if unchanged (WR-02);
|
||||
// revert to the last valid value instead of persisting/holding an
|
||||
// invalid required field while focus has already moved away (WR-03).
|
||||
function commitOnBlur() {
|
||||
if (tempValue === String(value)) {
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
if (required && tempValue.trim().length === 0) {
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
setError(null);
|
||||
onSave(tempValue);
|
||||
setIsEditing(false);
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
setTempValue(String(value));
|
||||
setError(null);
|
||||
@@ -103,7 +126,7 @@ export function EditableCell({
|
||||
ref={inputRef as React.Ref<HTMLTextAreaElement>}
|
||||
value={tempValue}
|
||||
onChange={(e) => setTempValue(e.target.value)}
|
||||
onBlur={commit}
|
||||
onBlur={commitOnBlur}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
className={cn("ring-1 ring-primary resize-none text-sm", error && "ring-2 ring-red-500")}
|
||||
@@ -120,8 +143,9 @@ export function EditableCell({
|
||||
onSave(next);
|
||||
setIsEditing(false);
|
||||
}}
|
||||
onBlur={commit}
|
||||
onKeyDown={handleKeyDown}
|
||||
// WR-01: onChange already saves + closes — a blur-commit here would
|
||||
// fire a second (possibly stale) updateServiceField for the same field.
|
||||
onKeyDown={(e) => { if (e.key === "Escape") cancel(); }}
|
||||
className="h-4 w-4 cursor-pointer accent-[#1A463C]"
|
||||
/>
|
||||
) : (
|
||||
@@ -130,7 +154,7 @@ export function EditableCell({
|
||||
type={type}
|
||||
value={tempValue}
|
||||
onChange={(e) => setTempValue(e.target.value)}
|
||||
onBlur={commit}
|
||||
onBlur={commitOnBlur}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
step={type === "number" ? "0.01" : undefined}
|
||||
|
||||
Reference in New Issue
Block a user