wrap the document, record and send-mail editors in real forms
typing into a field and pressing enter did nothing in the document editor, the customer/item/payment/expense dialogs and the send-document dialog — inputs sat in a div, so the button was the only way to submit. now each wraps in a <form onSubmit>, with every non-submit button (cancel, close, add/remove line, "same as billing") marked type="button" so it doesn't submit instead of doing its own thing. the record editors share one dialog shell (EditorShell), so a single form there covers customer, item, payment and expense at once — kept the dialog's grid gap intact with display:contents rather than nesting a box. left the list-view filter boxes and the reports date-range picker alone: none of them has a submit action, so there's nothing for enter to do. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -197,7 +197,8 @@ export const DocumentEditor = ({ resource, id, onClose }: Props) => {
|
|||||||
template !== '' &&
|
template !== '' &&
|
||||||
lines.some((l) => l.name.trim());
|
lines.some((l) => l.name.trim());
|
||||||
|
|
||||||
const save = () => {
|
const save = (ev?: React.FormEvent) => {
|
||||||
|
ev?.preventDefault();
|
||||||
// The three resources agree on everything below the header, and disagree on nothing but it.
|
// The three resources agree on everything below the header, and disagree on nothing but it.
|
||||||
const header = isRecurring
|
const header = isRecurring
|
||||||
? {
|
? {
|
||||||
@@ -260,18 +261,18 @@ export const DocumentEditor = ({ resource, id, onClose }: Props) => {
|
|||||||
const busy = create.isPending || update.isPending;
|
const busy = create.isPending || update.isPending;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full min-h-0 flex-col bg-background">
|
<form onSubmit={save} className="flex h-full min-h-0 flex-col bg-background">
|
||||||
<div className="flex shrink-0 items-center gap-2 border-b border-border px-3 py-2">
|
<div className="flex shrink-0 items-center gap-2 border-b border-border px-3 py-2">
|
||||||
<div className="min-w-0 flex-1 text-xs font-semibold">
|
<div className="min-w-0 flex-1 text-xs font-semibold">
|
||||||
{isNew ? `New ${KIND_NOUN[resource]}` : `Edit ${number || KIND_NOUN[resource]}`}
|
{isNew ? `New ${KIND_NOUN[resource]}` : `Edit ${number || KIND_NOUN[resource]}`}
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={onClose} disabled={busy}>
|
<Button variant="ghost" size="sm" type="button" className="h-7 text-xs" onClick={onClose} disabled={busy}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" className="h-7 text-xs" onClick={save} disabled={!canSave || busy}>
|
<Button type="submit" size="sm" className="h-7 text-xs" disabled={!canSave || busy}>
|
||||||
{busy ? 'Saving…' : 'Save'}
|
{busy ? 'Saving…' : 'Save'}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="sm" className="h-7 px-2" onClick={onClose} title="Close">
|
<Button variant="ghost" size="sm" type="button" className="h-7 px-2" onClick={onClose} title="Close">
|
||||||
<X className="h-3.5 w-3.5" />
|
<X className="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -517,6 +518,7 @@ export const DocumentEditor = ({ resource, id, onClose }: Props) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
type="button"
|
||||||
className="h-8 w-8 p-0 text-muted-foreground"
|
className="h-8 w-8 p-0 text-muted-foreground"
|
||||||
disabled={lines.length === 1}
|
disabled={lines.length === 1}
|
||||||
onClick={() => setLines((prev) => prev.filter((l) => l.key !== line.key))}
|
onClick={() => setLines((prev) => prev.filter((l) => l.key !== line.key))}
|
||||||
@@ -532,6 +534,7 @@ export const DocumentEditor = ({ resource, id, onClose }: Props) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
type="button"
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs"
|
||||||
onClick={() => setLines((prev) => [...prev, newLine(prev.length)])}
|
onClick={() => setLines((prev) => [...prev, newLine(prev.length)])}
|
||||||
>
|
>
|
||||||
@@ -592,7 +595,7 @@ export const DocumentEditor = ({ resource, id, onClose }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+24
-14
@@ -87,21 +87,31 @@ const EditorShell = ({
|
|||||||
}) => (
|
}) => (
|
||||||
<Dialog open onOpenChange={(open) => !open && onClose()}>
|
<Dialog open onOpenChange={(open) => !open && onClose()}>
|
||||||
<DialogContent className={wide ? 'sm:max-w-2xl' : 'sm:max-w-lg'}>
|
<DialogContent className={wide ? 'sm:max-w-2xl' : 'sm:max-w-lg'}>
|
||||||
<DialogHeader>
|
{/* `contents` keeps the form invisible to DialogContent's grid layout — it still owns the gap between
|
||||||
<DialogTitle>{title}</DialogTitle>
|
header, body and footer, the form just carries Enter-to-submit across all three. */}
|
||||||
{description && <DialogDescription>{description}</DialogDescription>}
|
<form
|
||||||
</DialogHeader>
|
onSubmit={(ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
onSave();
|
||||||
|
}}
|
||||||
|
className="contents"
|
||||||
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
{description && <DialogDescription>{description}</DialogDescription>}
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="max-h-[65vh] space-y-3 overflow-y-auto pr-1">{children}</div>
|
<div className="max-h-[65vh] space-y-3 overflow-y-auto pr-1">{children}</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="ghost" size="sm" onClick={onClose} disabled={busy}>
|
<Button variant="ghost" size="sm" type="button" onClick={onClose} disabled={busy}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" onClick={onSave} disabled={!canSave || busy}>
|
<Button type="submit" size="sm" disabled={!canSave || busy}>
|
||||||
{busy ? 'Saving…' : 'Save'}
|
{busy ? 'Saving…' : 'Save'}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
@@ -289,7 +299,7 @@ const AddressFields = ({
|
|||||||
<div className="mb-2 flex items-center justify-between">
|
<div className="mb-2 flex items-center justify-between">
|
||||||
<div className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">{label}</div>
|
<div className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">{label}</div>
|
||||||
{onCopy && (
|
{onCopy && (
|
||||||
<Button variant="ghost" size="sm" className="h-6 text-[10px]" onClick={onCopy}>
|
<Button variant="ghost" size="sm" type="button" className="h-6 text-[10px]" onClick={onCopy}>
|
||||||
Same as billing
|
Same as billing
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+52
-45
@@ -85,62 +85,69 @@ export const SendDocumentDialog = ({
|
|||||||
|
|
||||||
const missingRecipient = !to.trim();
|
const missingRecipient = !to.trim();
|
||||||
|
|
||||||
const submit = () => {
|
const submit = (ev?: React.FormEvent) => {
|
||||||
|
ev?.preventDefault();
|
||||||
action.mutate({ id: target.id, action: 'send', payload: { subject, body, from, to } }, { onSuccess: onClose });
|
action.mutate({ id: target.id, action: 'send', payload: { subject, body, from, to } }, { onSuccess: onClose });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open onOpenChange={(open) => !open && onClose()}>
|
<Dialog open onOpenChange={(open) => !open && onClose()}>
|
||||||
<DialogContent className="sm:max-w-lg">
|
<DialogContent className="sm:max-w-lg">
|
||||||
<DialogHeader>
|
{/* `contents` keeps the form invisible to DialogContent's grid layout — it still owns the gap between
|
||||||
<DialogTitle>
|
header, body and footer, the form just carries Enter-to-submit across all three. */}
|
||||||
{target.resend ? 'Resend' : 'Send'} {kind} {target.number}
|
<form onSubmit={submit} className="contents">
|
||||||
</DialogTitle>
|
<DialogHeader>
|
||||||
<DialogDescription>
|
<DialogTitle>
|
||||||
This emails {target.customerName} through InvoiceShelf. It cannot be undone.
|
{target.resend ? 'Resend' : 'Send'} {kind} {target.number}
|
||||||
</DialogDescription>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
<DialogDescription>
|
||||||
|
This emails {target.customerName} through InvoiceShelf. It cannot be undone.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs">To</Label>
|
<Label className="text-xs">To</Label>
|
||||||
<Input value={to} onChange={(e) => setTo(e.target.value)} className="h-8 text-xs" type="email" />
|
<Input value={to} onChange={(e) => setTo(e.target.value)} className="h-8 text-xs" type="email" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs">From</Label>
|
||||||
|
<Input value={from} onChange={(e) => setFrom(e.target.value)} className="h-8 text-xs" type="email" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs">From</Label>
|
<Label className="text-xs">Subject</Label>
|
||||||
<Input value={from} onChange={(e) => setFrom(e.target.value)} className="h-8 text-xs" type="email" />
|
<Input value={subject} onChange={(e) => setSubject(e.target.value)} className="h-8 text-xs" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="space-y-1">
|
||||||
<div className="space-y-1">
|
<Label className="text-xs">Message</Label>
|
||||||
<Label className="text-xs">Subject</Label>
|
<Textarea value={body} onChange={(e) => setBody(e.target.value)} rows={7} className="text-xs" />
|
||||||
<Input value={subject} onChange={(e) => setSubject(e.target.value)} className="h-8 text-xs" />
|
<p className="text-[10px] text-muted-foreground">
|
||||||
</div>
|
HTML. InvoiceShelf attaches the PDF itself — do not link it here.
|
||||||
<div className="space-y-1">
|
</p>
|
||||||
<Label className="text-xs">Message</Label>
|
</div>
|
||||||
<Textarea value={body} onChange={(e) => setBody(e.target.value)} rows={7} className="text-xs" />
|
{missingRecipient && (
|
||||||
<p className="text-[10px] text-muted-foreground">
|
<p className="text-[10px] text-warning">
|
||||||
HTML. InvoiceShelf attaches the PDF itself — do not link it here.
|
This customer has no email address on file. Enter one to send.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
{missingRecipient && (
|
|
||||||
<p className="text-[10px] text-warning">This customer has no email address on file. Enter one to send.</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="ghost" size="sm" onClick={onClose} disabled={action.isPending}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" onClick={submit} disabled={action.isPending || missingRecipient}>
|
|
||||||
{action.isPending ? (
|
|
||||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Send className="mr-1.5 h-3.5 w-3.5" />
|
|
||||||
)}
|
)}
|
||||||
Send to {to || '—'}
|
</div>
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
<DialogFooter>
|
||||||
|
<Button variant="ghost" size="sm" type="button" onClick={onClose} disabled={action.isPending}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" size="sm" disabled={action.isPending || missingRecipient}>
|
||||||
|
{action.isPending ? (
|
||||||
|
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Send className="mr-1.5 h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
|
Send to {to || '—'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user