Openstatus www.openstatus.dev

fix(dashboard): markdown and form sheet discard (#1472)

* fix: markdown

* fix: discard changes on close

authored by

Maximilian Kaske and committed by
GitHub
3eee0dc7 c62def93

+268 -43
+1
apps/dashboard/package.json
··· 100 100 }, 101 101 "devDependencies": { 102 102 "@tailwindcss/postcss": "4.1.11", 103 + "@tailwindcss/typography": "0.5.10", 103 104 "@types/dom-speech-recognition": "0.0.6", 104 105 "@types/node": "24.0.8", 105 106 "@types/react": "19.2.2",
+1
apps/dashboard/src/app/globals.css
··· 1 1 @import "tailwindcss"; 2 2 @import "tw-animate-css"; 3 + @plugin "@tailwindcss/typography"; 3 4 4 5 /* safelist */ 5 6 @source inline("has-data-[slot=slider-range]:bg-red-500");
+1 -1
apps/dashboard/src/components/data-table/maintenances/columns.tsx
··· 25 25 cell: ({ row }) => { 26 26 const value = String(row.getValue("message")); 27 27 return ( 28 - <div className="max-w-[200px] truncate text-muted-foreground"> 28 + <div className="max-w-[200px] truncate text-muted-foreground prose prose-sm line-clamp-3"> 29 29 <ProcessMessage value={value} /> 30 30 </div> 31 31 );
+1 -1
apps/dashboard/src/components/data-table/status-report-updates/data-table.tsx
··· 111 111 </div> 112 112 </TableCell> 113 113 <TableCell> 114 - <div className="text-wrap"> 114 + <div className="text-wrap prose prose-sm line-clamp-3"> 115 115 <ProcessMessage value={update.message} /> 116 116 </div> 117 117 </TableCell>
+144
apps/dashboard/src/components/forms/form-sheet.tsx
··· 1 1 "use client"; 2 2 3 3 import { 4 + AlertDialog, 5 + AlertDialogAction, 6 + AlertDialogCancel, 7 + AlertDialogContent, 8 + AlertDialogDescription, 9 + AlertDialogFooter, 10 + AlertDialogHeader, 11 + AlertDialogTitle, 12 + } from "@/components/ui/alert-dialog"; 13 + import { 4 14 Sheet, 5 15 SheetContent, 6 16 SheetDescription, ··· 10 20 SheetTrigger, 11 21 } from "@/components/ui/sheet"; 12 22 import { cn } from "@/lib/utils"; 23 + import React, { 24 + createContext, 25 + useContext, 26 + useEffect, 27 + useRef, 28 + useState, 29 + } from "react"; 13 30 14 31 export function FormSheetContent({ 15 32 children, ··· 83 100 > 84 101 {children} 85 102 </SheetTrigger> 103 + ); 104 + } 105 + 106 + export function FormSheetAlertDialog({ 107 + onConfirm, 108 + ...props 109 + }: React.ComponentProps<typeof AlertDialog> & { 110 + onConfirm: () => void; 111 + }) { 112 + return ( 113 + <AlertDialog {...props}> 114 + <AlertDialogContent> 115 + <AlertDialogHeader> 116 + <AlertDialogTitle>Discard changes?</AlertDialogTitle> 117 + <AlertDialogDescription> 118 + You have unsaved changes. Are you sure you want to discard them? 119 + </AlertDialogDescription> 120 + </AlertDialogHeader> 121 + <AlertDialogFooter> 122 + <AlertDialogCancel>Continue editing</AlertDialogCancel> 123 + <AlertDialogAction onClick={onConfirm}> 124 + Discard changes 125 + </AlertDialogAction> 126 + </AlertDialogFooter> 127 + </AlertDialogContent> 128 + </AlertDialog> 129 + ); 130 + } 131 + 132 + const FormSheetDirtyContext = createContext<{ 133 + isDirty: boolean; 134 + setIsDirty: (dirty: boolean) => void; 135 + } | null>(null); 136 + 137 + export function useFormSheetDirty() { 138 + const context = useContext(FormSheetDirtyContext); 139 + if (!context) { 140 + throw new Error( 141 + "useFormSheetDirty must be used within FormSheetWithDirtyProtection", 142 + ); 143 + } 144 + return context; 145 + } 146 + 147 + export function FormSheetWithDirtyProtection({ 148 + children, 149 + open: controlledOpen, 150 + onOpenChange: controlledOnOpenChange, 151 + }: { 152 + children: React.ReactNode; 153 + open?: boolean; 154 + onOpenChange?: (open: boolean) => void; 155 + }) { 156 + const [internalOpen, setInternalOpen] = useState(false); 157 + const [isDirty, setIsDirty] = useState(false); 158 + const [showAlert, setShowAlert] = useState(false); 159 + const shouldBypassAlert = useRef(false); 160 + 161 + const open = controlledOpen ?? internalOpen; 162 + const setOpen = controlledOnOpenChange ?? setInternalOpen; 163 + 164 + // Reset states when sheet closes 165 + useEffect(() => { 166 + if (!open) { 167 + setIsDirty(false); 168 + shouldBypassAlert.current = false; 169 + } 170 + }, [open]); 171 + 172 + const handleOpenChange = (newOpen: boolean) => { 173 + if (!newOpen && isDirty && !shouldBypassAlert.current) { 174 + // User is trying to close with unsaved changes 175 + setShowAlert(true); 176 + } else { 177 + setOpen(newOpen); 178 + } 179 + }; 180 + 181 + const handleDiscardChanges = () => { 182 + shouldBypassAlert.current = true; 183 + setShowAlert(false); 184 + setOpen(false); 185 + }; 186 + 187 + const handleInteractOutside = (e: Event) => { 188 + if (isDirty) { 189 + e.preventDefault(); 190 + setShowAlert(true); 191 + } 192 + }; 193 + 194 + const handleEscapeKeyDown = (e: KeyboardEvent) => { 195 + if (isDirty) { 196 + e.preventDefault(); 197 + setShowAlert(true); 198 + } 199 + }; 200 + 201 + return ( 202 + <FormSheetDirtyContext.Provider value={{ isDirty, setIsDirty }}> 203 + <Sheet open={open} onOpenChange={handleOpenChange}> 204 + {/* Clone children and inject event handlers if it's SheetContent */} 205 + {React.Children.map(children, (child) => { 206 + if ( 207 + React.isValidElement(child) && 208 + (child.type === FormSheetContent || child.type === SheetContent) 209 + ) { 210 + return React.cloneElement( 211 + child as React.ReactElement<{ 212 + onInteractOutside?: (e: Event) => void; 213 + onEscapeKeyDown?: (e: KeyboardEvent) => void; 214 + }>, 215 + { 216 + onInteractOutside: handleInteractOutside, 217 + onEscapeKeyDown: handleEscapeKeyDown, 218 + }, 219 + ); 220 + } 221 + return child; 222 + })} 223 + </Sheet> 224 + <FormSheetAlertDialog 225 + open={showAlert} 226 + onOpenChange={setShowAlert} 227 + onConfirm={handleDiscardChanges} 228 + /> 229 + </FormSheetDirtyContext.Provider> 86 230 ); 87 231 } 88 232
+9 -3
apps/dashboard/src/components/forms/maintenance/form.tsx
··· 9 9 FormCardContent, 10 10 FormCardSeparator, 11 11 } from "@/components/forms/form-card"; 12 + import { useFormSheetDirty } from "@/components/forms/form-sheet"; 12 13 import { Button } from "@/components/ui/button"; 13 14 import { Calendar } from "@/components/ui/calendar"; 14 15 import { Checkbox } from "@/components/ui/checkbox"; ··· 37 38 import { isTRPCClientError } from "@trpc/client"; 38 39 import { addDays, format } from "date-fns"; 39 40 import { CalendarIcon, ClockIcon } from "lucide-react"; 40 - import type React from "react"; 41 - import { useTransition } from "react"; 41 + import React, { useTransition } from "react"; 42 42 import { useForm } from "react-hook-form"; 43 43 import { toast } from "sonner"; 44 44 import { z } from "zod"; ··· 81 81 }); 82 82 const watchMessage = form.watch("message"); 83 83 const [isPending, startTransition] = useTransition(); 84 + const { setIsDirty } = useFormSheetDirty(); 85 + 86 + const formIsDirty = form.formState.isDirty; 87 + React.useEffect(() => { 88 + setIsDirty(formIsDirty); 89 + }, [formIsDirty, setIsDirty]); 84 90 85 91 function submitAction(values: FormValues) { 86 92 if (isPending) return; ··· 362 368 <TabsContent value="tab-2"> 363 369 <div className="grid gap-2"> 364 370 <Label>Preview</Label> 365 - <div className="rounded-md border px-3 py-2 text-foreground text-sm"> 371 + <div className="rounded-md border px-3 py-2 text-foreground text-sm prose prose-sm"> 366 372 <ProcessMessage value={watchMessage} /> 367 373 </div> 368 374 </div>
+3 -3
apps/dashboard/src/components/forms/maintenance/sheet.tsx
··· 2 2 3 3 import { FormCard, FormCardGroup } from "@/components/forms/form-card"; 4 4 import { 5 - FormSheet, 6 5 FormSheetContent, 7 6 FormSheetDescription, 8 7 FormSheetFooter, ··· 10 9 FormSheetHeader, 11 10 FormSheetTitle, 12 11 FormSheetTrigger, 12 + FormSheetWithDirtyProtection, 13 13 } from "@/components/forms/form-sheet"; 14 14 import { 15 15 FormMaintenance, ··· 32 32 const [open, setOpen] = useState(false); 33 33 34 34 return ( 35 - <FormSheet open={open} onOpenChange={setOpen}> 35 + <FormSheetWithDirtyProtection open={open} onOpenChange={setOpen}> 36 36 <FormSheetTrigger {...props} asChild> 37 37 {children} 38 38 </FormSheetTrigger> ··· 69 69 </Button> 70 70 </FormSheetFooter> 71 71 </FormSheetContent> 72 - </FormSheet> 72 + </FormSheetWithDirtyProtection> 73 73 ); 74 74 }
+8 -1
apps/dashboard/src/components/forms/monitor-tag/form-monitor-tag.tsx
··· 1 1 "use client"; 2 2 3 + import { useFormSheetDirty } from "@/components/forms/form-sheet"; 3 4 import { Button } from "@/components/ui/button"; 4 5 import { 5 6 Form, ··· 15 16 import { zodResolver } from "@hookform/resolvers/zod"; 16 17 import { useQuery } from "@tanstack/react-query"; 17 18 import { Plus, Trash2 } from "lucide-react"; 18 - import { useTransition } from "react"; 19 + import React, { useTransition } from "react"; 19 20 import { useFieldArray, useForm } from "react-hook-form"; 20 21 import { toast } from "sonner"; 21 22 import { z } from "zod"; ··· 58 59 }); 59 60 60 61 const [isPending, startTransition] = useTransition(); 62 + const { setIsDirty } = useFormSheetDirty(); 63 + 64 + const formIsDirty = form.formState.isDirty; 65 + React.useEffect(() => { 66 + setIsDirty(formIsDirty); 67 + }, [formIsDirty, setIsDirty]); 61 68 62 69 function submitAction(values: FormValues) { 63 70 if (isPending) return;
+3 -3
apps/dashboard/src/components/forms/monitor-tag/sheet.tsx
··· 6 6 FormCardGroup, 7 7 } from "@/components/forms/form-card"; 8 8 import { 9 - FormSheet, 10 9 FormSheetContent, 11 10 FormSheetDescription, 12 11 FormSheetFooter, 13 12 FormSheetHeader, 14 13 FormSheetTitle, 15 14 FormSheetTrigger, 15 + FormSheetWithDirtyProtection, 16 16 } from "@/components/forms/form-sheet"; 17 17 import { 18 18 FormMonitorTag, ··· 33 33 const [open, setOpen] = useState(false); 34 34 35 35 return ( 36 - <FormSheet open={open} onOpenChange={setOpen}> 36 + <FormSheetWithDirtyProtection open={open} onOpenChange={setOpen}> 37 37 <FormSheetTrigger {...props} asChild> 38 38 {children} 39 39 </FormSheetTrigger> ··· 62 62 </Button> 63 63 </FormSheetFooter> 64 64 </FormSheetContent> 65 - </FormSheet> 65 + </FormSheetWithDirtyProtection> 66 66 ); 67 67 }
+8 -1
apps/dashboard/src/components/forms/notifications/form-discord.tsx
··· 15 15 FormCardContent, 16 16 FormCardSeparator, 17 17 } from "@/components/forms/form-card"; 18 + import { useFormSheetDirty } from "@/components/forms/form-sheet"; 18 19 import { Button } from "@/components/ui/button"; 19 20 import { Form } from "@/components/ui/form"; 20 21 import { Input } from "@/components/ui/input"; ··· 23 24 import { cn } from "@/lib/utils"; 24 25 import { zodResolver } from "@hookform/resolvers/zod"; 25 26 import { isTRPCClientError } from "@trpc/client"; 26 - import { useTransition } from "react"; 27 + import React, { useTransition } from "react"; 27 28 import { useForm } from "react-hook-form"; 28 29 import { toast } from "sonner"; 29 30 import { z } from "zod"; ··· 58 59 }, 59 60 }); 60 61 const [isPending, startTransition] = useTransition(); 62 + const { setIsDirty } = useFormSheetDirty(); 63 + 64 + const formIsDirty = form.formState.isDirty; 65 + React.useEffect(() => { 66 + setIsDirty(formIsDirty); 67 + }, [formIsDirty, setIsDirty]); 61 68 62 69 function submitAction(values: FormValues) { 63 70 if (isPending) return;
+8 -1
apps/dashboard/src/components/forms/notifications/form-email.tsx
··· 14 14 FormCardContent, 15 15 FormCardSeparator, 16 16 } from "@/components/forms/form-card"; 17 + import { useFormSheetDirty } from "@/components/forms/form-sheet"; 17 18 import { Form } from "@/components/ui/form"; 18 19 import { Input } from "@/components/ui/input"; 19 20 import { Label } from "@/components/ui/label"; 20 21 import { cn } from "@/lib/utils"; 21 22 import { zodResolver } from "@hookform/resolvers/zod"; 22 23 import { isTRPCClientError } from "@trpc/client"; 23 - import { useTransition } from "react"; 24 + import React, { useTransition } from "react"; 24 25 import { useForm } from "react-hook-form"; 25 26 import { toast } from "sonner"; 26 27 import { z } from "zod"; ··· 55 56 }, 56 57 }); 57 58 const [isPending, startTransition] = useTransition(); 59 + const { setIsDirty } = useFormSheetDirty(); 60 + 61 + const formIsDirty = form.formState.isDirty; 62 + React.useEffect(() => { 63 + setIsDirty(formIsDirty); 64 + }, [formIsDirty, setIsDirty]); 58 65 59 66 function submitAction(values: FormValues) { 60 67 if (isPending) return;
+8 -1
apps/dashboard/src/components/forms/notifications/form-ntfy.tsx
··· 14 14 FormCardContent, 15 15 FormCardSeparator, 16 16 } from "@/components/forms/form-card"; 17 + import { useFormSheetDirty } from "@/components/forms/form-sheet"; 17 18 import { Button } from "@/components/ui/button"; 18 19 import { Form } from "@/components/ui/form"; 19 20 import { Input } from "@/components/ui/input"; ··· 22 23 import { cn } from "@/lib/utils"; 23 24 import { zodResolver } from "@hookform/resolvers/zod"; 24 25 import { isTRPCClientError } from "@trpc/client"; 25 - import { useTransition } from "react"; 26 + import React, { useTransition } from "react"; 26 27 import { useForm } from "react-hook-form"; 27 28 import { toast } from "sonner"; 28 29 import { z } from "zod"; ··· 61 62 }, 62 63 }); 63 64 const [isPending, startTransition] = useTransition(); 65 + const { setIsDirty } = useFormSheetDirty(); 66 + 67 + const formIsDirty = form.formState.isDirty; 68 + React.useEffect(() => { 69 + setIsDirty(formIsDirty); 70 + }, [formIsDirty, setIsDirty]); 64 71 65 72 function submitAction(values: FormValues) { 66 73 if (isPending) return;
+8 -1
apps/dashboard/src/components/forms/notifications/form-pagerduty.tsx
··· 14 14 FormCardContent, 15 15 FormCardSeparator, 16 16 } from "@/components/forms/form-card"; 17 + import { useFormSheetDirty } from "@/components/forms/form-sheet"; 17 18 import { Button } from "@/components/ui/button"; 18 19 import { Form } from "@/components/ui/form"; 19 20 import { Input } from "@/components/ui/input"; ··· 24 25 import { PagerDutySchema } from "@openstatus/notification-pagerduty"; 25 26 import { isTRPCClientError } from "@trpc/client"; 26 27 import { parseAsString, useQueryState } from "nuqs"; 27 - import { useEffect, useTransition } from "react"; 28 + import React, { useEffect, useTransition } from "react"; 28 29 import { useForm } from "react-hook-form"; 29 30 import { toast } from "sonner"; 30 31 import { z } from "zod"; ··· 61 62 }, 62 63 }); 63 64 const [isPending, startTransition] = useTransition(); 65 + const { setIsDirty } = useFormSheetDirty(); 66 + 67 + const formIsDirty = form.formState.isDirty; 68 + React.useEffect(() => { 69 + setIsDirty(formIsDirty); 70 + }, [formIsDirty, setIsDirty]); 64 71 65 72 useEffect(() => { 66 73 if (searchConfig) {
+8 -1
apps/dashboard/src/components/forms/notifications/form-slack.tsx
··· 15 15 FormCardContent, 16 16 FormCardSeparator, 17 17 } from "@/components/forms/form-card"; 18 + import { useFormSheetDirty } from "@/components/forms/form-sheet"; 18 19 import { Button } from "@/components/ui/button"; 19 20 import { Form } from "@/components/ui/form"; 20 21 import { Input } from "@/components/ui/input"; ··· 23 24 import { cn } from "@/lib/utils"; 24 25 import { zodResolver } from "@hookform/resolvers/zod"; 25 26 import { isTRPCClientError } from "@trpc/client"; 26 - import { useTransition } from "react"; 27 + import React, { useTransition } from "react"; 27 28 import { useForm } from "react-hook-form"; 28 29 import { toast } from "sonner"; 29 30 import { z } from "zod"; ··· 58 59 }, 59 60 }); 60 61 const [isPending, startTransition] = useTransition(); 62 + const { setIsDirty } = useFormSheetDirty(); 63 + 64 + const formIsDirty = form.formState.isDirty; 65 + React.useEffect(() => { 66 + setIsDirty(formIsDirty); 67 + }, [formIsDirty, setIsDirty]); 61 68 62 69 function submitAction(values: FormValues) { 63 70 if (isPending) return;
+8 -1
apps/dashboard/src/components/forms/notifications/form-sms.tsx
··· 14 14 FormCardContent, 15 15 FormCardSeparator, 16 16 } from "@/components/forms/form-card"; 17 + import { useFormSheetDirty } from "@/components/forms/form-sheet"; 17 18 import { Form } from "@/components/ui/form"; 18 19 import { Input } from "@/components/ui/input"; 19 20 import { Label } from "@/components/ui/label"; 20 21 import { cn } from "@/lib/utils"; 21 22 import { zodResolver } from "@hookform/resolvers/zod"; 22 - import { useTransition } from "react"; 23 + import React, { useTransition } from "react"; 23 24 import { useForm } from "react-hook-form"; 24 25 import { toast } from "sonner"; 25 26 import { z } from "zod"; ··· 54 55 }, 55 56 }); 56 57 const [isPending, startTransition] = useTransition(); 58 + const { setIsDirty } = useFormSheetDirty(); 59 + 60 + const formIsDirty = form.formState.isDirty; 61 + React.useEffect(() => { 62 + setIsDirty(formIsDirty); 63 + }, [formIsDirty, setIsDirty]); 57 64 58 65 function submitAction(values: FormValues) { 59 66 if (isPending) return;
+8 -1
apps/dashboard/src/components/forms/notifications/form-webhook.tsx
··· 15 15 FormCardContent, 16 16 FormCardSeparator, 17 17 } from "@/components/forms/form-card"; 18 + import { useFormSheetDirty } from "@/components/forms/form-sheet"; 18 19 import { Button } from "@/components/ui/button"; 19 20 import { Form } from "@/components/ui/form"; 20 21 import { Input } from "@/components/ui/input"; ··· 23 24 import { cn } from "@/lib/utils"; 24 25 import { zodResolver } from "@hookform/resolvers/zod"; 25 26 import { isTRPCClientError } from "@trpc/client"; 26 - import { useTransition } from "react"; 27 + import React, { useTransition } from "react"; 27 28 import { useForm } from "react-hook-form"; 28 29 import { toast } from "sonner"; 29 30 import { z } from "zod"; ··· 61 62 }, 62 63 }); 63 64 const [isPending, startTransition] = useTransition(); 65 + const { setIsDirty } = useFormSheetDirty(); 66 + 67 + const formIsDirty = form.formState.isDirty; 68 + React.useEffect(() => { 69 + setIsDirty(formIsDirty); 70 + }, [formIsDirty, setIsDirty]); 64 71 65 72 function submitAction(values: FormValues) { 66 73 if (isPending) return;
+3 -3
apps/dashboard/src/components/forms/notifications/sheet.tsx
··· 2 2 3 3 import { FormCard, FormCardGroup } from "@/components/forms/form-card"; 4 4 import { 5 - FormSheet, 6 5 FormSheetContent, 7 6 FormSheetDescription, 8 7 FormSheetFooter, 9 8 FormSheetHeader, 10 9 FormSheetTitle, 11 10 FormSheetTrigger, 11 + FormSheetWithDirtyProtection, 12 12 } from "@/components/forms/form-sheet"; 13 13 import { Button } from "@/components/ui/button"; 14 14 import { config } from "@/data/notifications.client"; ··· 34 34 const Form = provider ? config[provider].form : undefined; 35 35 36 36 return ( 37 - <FormSheet open={open} onOpenChange={setOpen}> 37 + <FormSheetWithDirtyProtection open={open} onOpenChange={setOpen}> 38 38 <FormSheetTrigger {...props} asChild> 39 39 {children} 40 40 </FormSheetTrigger> ··· 78 78 </Button> 79 79 </FormSheetFooter> 80 80 </FormSheetContent> 81 - </FormSheet> 81 + </FormSheetWithDirtyProtection> 82 82 ); 83 83 }
+8 -2
apps/dashboard/src/components/forms/private-location/form.tsx
··· 8 8 FormCardContent, 9 9 FormCardSeparator, 10 10 } from "@/components/forms/form-card"; 11 + import { useFormSheetDirty } from "@/components/forms/form-sheet"; 11 12 import { Checkbox } from "@/components/ui/checkbox"; 12 13 import { 13 14 Form, ··· 31 32 import { zodResolver } from "@hookform/resolvers/zod"; 32 33 import { isTRPCClientError } from "@trpc/client"; 33 34 import { Check, Copy } from "lucide-react"; 34 - import type React from "react"; 35 - import { useTransition } from "react"; 35 + import React, { useTransition } from "react"; 36 36 import { useForm } from "react-hook-form"; 37 37 import { toast } from "sonner"; 38 38 import { z } from "zod"; ··· 66 66 }); 67 67 const [isPending, startTransition] = useTransition(); 68 68 const { copy, isCopied } = useCopyToClipboard(); 69 + const { setIsDirty } = useFormSheetDirty(); 70 + 71 + const formIsDirty = form.formState.isDirty; 72 + React.useEffect(() => { 73 + setIsDirty(formIsDirty); 74 + }, [formIsDirty, setIsDirty]); 69 75 70 76 function submitAction(values: FormValues) { 71 77 if (isPending) return;
+3 -3
apps/dashboard/src/components/forms/private-location/sheet.tsx
··· 2 2 3 3 import { FormCard, FormCardGroup } from "@/components/forms/form-card"; 4 4 import { 5 - FormSheet, 6 5 FormSheetContent, 7 6 FormSheetDescription, 8 7 FormSheetFooter, 9 8 FormSheetHeader, 10 9 FormSheetTitle, 11 10 FormSheetTrigger, 11 + FormSheetWithDirtyProtection, 12 12 } from "@/components/forms/form-sheet"; 13 13 import { 14 14 FormPrivateLocation, ··· 31 31 const [open, setOpen] = useState(false); 32 32 33 33 return ( 34 - <FormSheet open={open} onOpenChange={setOpen}> 34 + <FormSheetWithDirtyProtection open={open} onOpenChange={setOpen}> 35 35 <FormSheetTrigger {...props} asChild> 36 36 {children} 37 37 </FormSheetTrigger> ··· 62 62 </Button> 63 63 </FormSheetFooter> 64 64 </FormSheetContent> 65 - </FormSheet> 65 + </FormSheetWithDirtyProtection> 66 66 ); 67 67 }
+9 -2
apps/dashboard/src/components/forms/status-report-update/form.tsx
··· 5 5 FormCardContent, 6 6 FormCardSeparator, 7 7 } from "@/components/forms/form-card"; 8 + import { useFormSheetDirty } from "@/components/forms/form-sheet"; 8 9 import { Button } from "@/components/ui/button"; 9 10 import { Calendar } from "@/components/ui/calendar"; 10 11 import { ··· 38 39 import { isTRPCClientError } from "@trpc/client"; 39 40 import { format } from "date-fns"; 40 41 import { CalendarIcon, ClockIcon } from "lucide-react"; 41 - import { useTransition } from "react"; 42 + import React, { useTransition } from "react"; 42 43 import { useForm } from "react-hook-form"; 43 44 import { toast } from "sonner"; 44 45 import { z } from "zod"; ··· 70 71 }); 71 72 const watchMessage = form.watch("message"); 72 73 const [isPending, startTransition] = useTransition(); 74 + const { setIsDirty } = useFormSheetDirty(); 75 + 76 + const formIsDirty = form.formState.isDirty; 77 + React.useEffect(() => { 78 + setIsDirty(formIsDirty); 79 + }, [formIsDirty, setIsDirty]); 73 80 74 81 function submitAction(values: FormValues) { 75 82 if (isPending) return; ··· 269 276 <TabsContent value="tab-2"> 270 277 <div className="grid gap-2"> 271 278 <Label>Preview</Label> 272 - <div className="rounded-md border px-3 py-2 text-foreground text-sm"> 279 + <div className="rounded-md border px-3 py-2 text-foreground text-sm prose prose-sm"> 273 280 <ProcessMessage value={watchMessage} /> 274 281 </div> 275 282 </div>
+3 -3
apps/dashboard/src/components/forms/status-report-update/sheet.tsx
··· 2 2 3 3 import { FormCard, FormCardGroup } from "@/components/forms/form-card"; 4 4 import { 5 - FormSheet, 6 5 FormSheetContent, 7 6 FormSheetDescription, 8 7 FormSheetFooter, 9 8 FormSheetHeader, 10 9 FormSheetTitle, 11 10 FormSheetTrigger, 11 + FormSheetWithDirtyProtection, 12 12 } from "@/components/forms/form-sheet"; 13 13 import { 14 14 FormStatusReportUpdate, ··· 27 27 }) { 28 28 const [open, setOpen] = useState(false); 29 29 return ( 30 - <FormSheet open={open} onOpenChange={setOpen}> 30 + <FormSheetWithDirtyProtection open={open} onOpenChange={setOpen}> 31 31 <FormSheetTrigger asChild>{children}</FormSheetTrigger> 32 32 <FormSheetContent className="sm:max-w-lg"> 33 33 <FormSheetHeader> ··· 55 55 </Button> 56 56 </FormSheetFooter> 57 57 </FormSheetContent> 58 - </FormSheet> 58 + </FormSheetWithDirtyProtection> 59 59 ); 60 60 }
+9 -2
apps/dashboard/src/components/forms/status-report/form.tsx
··· 9 9 FormCardContent, 10 10 FormCardSeparator, 11 11 } from "@/components/forms/form-card"; 12 + import { useFormSheetDirty } from "@/components/forms/form-sheet"; 12 13 import { Button } from "@/components/ui/button"; 13 14 import { Calendar } from "@/components/ui/calendar"; 14 15 import { Checkbox } from "@/components/ui/checkbox"; ··· 46 47 import { isTRPCClientError } from "@trpc/client"; 47 48 import { format } from "date-fns"; 48 49 import { CalendarIcon, ClockIcon } from "lucide-react"; 49 - import { useTransition } from "react"; 50 + import React, { useTransition } from "react"; 50 51 import { useForm } from "react-hook-form"; 51 52 import { toast } from "sonner"; 52 53 import { z } from "zod"; ··· 89 90 }); 90 91 const watchMessage = form.watch("message"); 91 92 const [isPending, startTransition] = useTransition(); 93 + const { setIsDirty } = useFormSheetDirty(); 94 + 95 + const formIsDirty = form.formState.isDirty; 96 + React.useEffect(() => { 97 + setIsDirty(formIsDirty); 98 + }, [formIsDirty, setIsDirty]); 92 99 93 100 function submitAction(values: FormValues) { 94 101 if (isPending) return; ··· 308 315 <TabsContent value="tab-2"> 309 316 <div className="grid gap-2"> 310 317 <Label>Preview</Label> 311 - <div className="rounded-md border px-3 py-2 text-foreground text-sm"> 318 + <div className="rounded-md border px-3 py-2 text-foreground text-sm prose prose-sm"> 312 319 <ProcessMessage value={watchMessage} /> 313 320 </div> 314 321 </div>
+3 -3
apps/dashboard/src/components/forms/status-report/sheet.tsx
··· 2 2 3 3 import { FormCard, FormCardGroup } from "@/components/forms/form-card"; 4 4 import { 5 - FormSheet, 6 5 FormSheetContent, 7 6 FormSheetDescription, 8 7 FormSheetFooter, 9 8 FormSheetHeader, 10 9 FormSheetTitle, 11 10 FormSheetTrigger, 11 + FormSheetWithDirtyProtection, 12 12 } from "@/components/forms/form-sheet"; 13 13 import { 14 14 FormStatusReport, ··· 33 33 const [open, setOpen] = useState(false); 34 34 35 35 return ( 36 - <FormSheet open={open} onOpenChange={setOpen}> 36 + <FormSheetWithDirtyProtection open={open} onOpenChange={setOpen}> 37 37 <FormSheetTrigger asChild>{children}</FormSheetTrigger> 38 38 <FormSheetContent className="sm:max-w-lg"> 39 39 <FormSheetHeader> ··· 68 68 </Button> 69 69 </FormSheetFooter> 70 70 </FormSheetContent> 71 - </FormSheet> 71 + </FormSheetWithDirtyProtection> 72 72 ); 73 73 }
+3 -6
pnpm-lock.yaml
··· 27 27 specifier: 5.7.2 28 28 version: 5.7.2 29 29 30 - apps/build-docker: 31 - dependencies: 32 - '@libsql/client': 33 - specifier: 0.15.15 34 - version: 0.15.15(bufferutil@4.0.8)(utf-8-validate@6.0.5) 35 - 36 30 apps/dashboard: 37 31 dependencies: 38 32 '@auth/core': ··· 300 294 '@tailwindcss/postcss': 301 295 specifier: 4.1.11 302 296 version: 4.1.11 297 + '@tailwindcss/typography': 298 + specifier: 0.5.10 299 + version: 0.5.10(tailwindcss@4.1.11) 303 300 '@types/dom-speech-recognition': 304 301 specifier: 0.0.6 305 302 version: 0.0.6