fix(capture): guard the bottom-nav Log sheet against silent discard

CapturePicker mounted the same measurement/mood/medication-intake forms
the dashboard quick-entry sheets do, but closed unconditionally on a
backdrop tap, Escape, or mobile swipe-down — the dashboard's
sheetBodyHasUnsavedInput() confirm guard never made it into this newer
picker. Reuse it here too, so a half-filled form from the primary
mobile capture path asks before dropping typed input instead of
losing it.
This commit is contained in:
Marc-André Bombeck committed 2026-07-18 03:52:32 +02:00
1 parent 20f0d13eba
commit a66a346d0d
1 file changed
+67 -3
+67 -3
View File
@@ -7,6 +7,17 @@ import { MeasurementForm } from "@/components/measurements/measurement-form";
import { MoodForm } from "@/components/mood/mood-form";
import { MedicationIntakeQuickAdd } from "@/components/dashboard/medication-intake-quick-add";
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { sheetBodyHasUnsavedInput } from "@/components/dashboard/quick-entry-sheets";
import { useTranslations } from "@/lib/i18n/context";
/**
@@ -26,6 +37,12 @@ import { useTranslations } from "@/lib/i18n/context";
* Nothing here orphans a route: the same surfaces remain reachable
* from their own pages (`/measurements`, `/mood`, dashboard) and from
* the More hub. The picker is an additional fast path, not a removal.
*
* v1.30.1 — the form sheet shares the dashboard quick-entry sheets'
* confirm-before-discard guard (`sheetBodyHasUnsavedInput()`), so a
* swipe-down / backdrop tap / Escape on a half-filled form from the
* primary mobile capture path asks before dropping typed input,
* instead of closing unconditionally.
*/
type CaptureKind = "measurement" | "medication" | "mood";
@@ -41,6 +58,10 @@ export function CapturePicker({ open, onOpenChange }: CapturePickerProps) {
const { t } = useTranslations();
const [kind, setKind] = useState<CaptureKind | null>(null);
const [footerEl, setFooterEl] = useState<HTMLDivElement | null>(null);
// v1.30.1 — mirrors `QuickEntrySheets`' `confirmDiscardOpen`: hold a
// dismiss attempt here instead of closing outright when the form
// body carries unsaved input.
const [confirmDiscardOpen, setConfirmDiscardOpen] = useState(false);
function chooseKind(next: CaptureKind) {
// Close the chooser first, then open the form sheet so only one
@@ -53,6 +74,18 @@ export function CapturePicker({ open, onOpenChange }: CapturePickerProps) {
setKind(null);
}
// Intercept a form-sheet dismiss: close immediately when the form is
// clean, otherwise keep the sheet open and ask before discarding —
// same contract as `QuickEntrySheets.handleQuickEntryOpenChange`.
function handleFormOpenChange(next: boolean) {
if (next) return;
if (sheetBodyHasUnsavedInput()) {
setConfirmDiscardOpen(true);
return;
}
closeForm();
}
const options: ReadonlyArray<{
kind: CaptureKind;
label: string;
@@ -127,9 +160,7 @@ export function CapturePicker({ open, onOpenChange }: CapturePickerProps) {
{/* The chosen capture surface, reusing the existing form. */}
<ResponsiveSheet
open={kind !== null}
onOpenChange={(next) => {
if (!next) closeForm();
}}
onOpenChange={handleFormOpenChange}
title={formTitle}
footer={<div ref={setFooterEl} className="flex w-full" />}
>
@@ -155,6 +186,39 @@ export function CapturePicker({ open, onOpenChange }: CapturePickerProps) {
/>
)}
</ResponsiveSheet>
{/* v1.30.1 — confirm before discarding a partly-filled capture
form when the sheet is dismissed by an overlay tap, Escape,
or a mobile swipe-down. Copy shared with the dashboard
quick-entry sheets' identical guard. */}
<AlertDialog
open={confirmDiscardOpen}
onOpenChange={setConfirmDiscardOpen}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{t("dashboard.quickEntryDiscard.title")}
</AlertDialogTitle>
<AlertDialogDescription>
{t("dashboard.quickEntryDiscard.description")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>
{t("dashboard.quickEntryDiscard.cancel")}
</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
setConfirmDiscardOpen(false);
closeForm();
}}
>
{t("dashboard.quickEntryDiscard.confirm")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
}