Openstatus www.openstatus.dev

feat: delete action resend ux (#1636)

authored by

Maximilian Kaske and committed by
GitHub
5003141b c5feeba3

+91 -69
+1 -2
apps/dashboard/src/app/(dashboard)/monitors/[id]/nav-actions.tsx
··· 195 195 <QuickActions 196 196 actions={actions} 197 197 deleteAction={{ 198 - title: monitor.name, 199 - confirmationValue: "delete monitor", 198 + confirmationValue: monitor.name ?? "monitor", 200 199 submitAction: async () => { 201 200 await deleteMonitorMutation.mutateAsync({ 202 201 id: Number.parseInt(id),
+4 -3
apps/dashboard/src/app/(dashboard)/status-pages/[id]/nav-actions.tsx
··· 58 58 <TooltipTrigger asChild> 59 59 <Button variant="ghost" size="sm" className="group h-7 w-7" asChild> 60 60 <a 61 - href={`https://${statusPage.customDomain || `${statusPage.slug}.openstatus.dev`}`} 61 + href={`https://${ 62 + statusPage.customDomain || `${statusPage.slug}.openstatus.dev` 63 + }`} 62 64 target="_blank" 63 65 rel="noreferrer" 64 66 > ··· 72 74 <QuickActions 73 75 actions={actions} 74 76 deleteAction={{ 75 - title: statusPage.title, 76 - confirmationValue: "delete status page", 77 + confirmationValue: statusPage.title ?? "status page", 77 78 submitAction: async () => { 78 79 await deleteStatusPageMutation.mutateAsync({ 79 80 id: Number.parseInt(id),
+1 -2
apps/dashboard/src/components/data-table/incidents/data-table-row-actions.tsx
··· 109 109 <QuickActions 110 110 actions={actions} 111 111 deleteAction={{ 112 - title: row.original.title, 113 - confirmationValue: "delete incident", 112 + confirmationValue: row.original.title ?? "incident", 114 113 submitAction: async () => { 115 114 await deleteIncidentMutation.mutateAsync({ 116 115 id: row.original.id,
+1 -2
apps/dashboard/src/components/data-table/maintenances/data-table-row-actions.tsx
··· 55 55 <QuickActions 56 56 actions={actions} 57 57 deleteAction={{ 58 - title: row.original.title, 59 - confirmationValue: "delete", 58 + confirmationValue: row.original.title ?? "maintenance", 60 59 submitAction: async () => { 61 60 await deleteMaintenanceMutation.mutateAsync({ 62 61 id: row.original.id,
+20 -8
apps/dashboard/src/components/data-table/monitors/data-table-action-bar.tsx
··· 2 2 3 3 import { SelectTrigger } from "@radix-ui/react-select"; 4 4 import type { Table } from "@tanstack/react-table"; 5 - import { CheckCircle2, Trash2 } from "lucide-react"; 5 + import { Check, CheckCircle2, Copy, Trash2 } from "lucide-react"; 6 6 import * as React from "react"; 7 7 8 8 import { ··· 34 34 AlertDialogTitle, 35 35 AlertDialogTrigger, 36 36 } from "@/components/ui/alert-dialog"; 37 + import { Button } from "@/components/ui/button"; 37 38 import { Input } from "@/components/ui/input"; 39 + import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"; 38 40 39 41 type Monitor = RouterOutputs["monitor"]["list"][number]; 40 42 ··· 53 55 const [open, setOpen] = React.useState(false); 54 56 const [isPending, startTransition] = React.useTransition(); 55 57 const [value, setValue] = React.useState(""); 58 + const { copy, isCopied } = useCopyToClipboard(); 56 59 const rows = table.getFilteredSelectedRowModel().rows; 57 60 const trpc = useTRPC(); 58 61 const queryClient = useQueryClient(); ··· 82 85 ); 83 86 84 87 const confirmationValue = React.useMemo( 85 - () => `delete monitor${rows.length === 1 ? "" : "s"}`, 86 - [rows.length], 88 + () => rows.map((row) => row.original.name).join(", "), 89 + [rows], 87 90 ); 88 91 89 92 const handleDelete = async () => { ··· 183 186 selected monitor(s) from the database. 184 187 </AlertDialogDescription> 185 188 </AlertDialogHeader> 186 - <form id="form-alert-dialog" className="space-y-0.5"> 187 - <p className="text-muted-foreground text-xs"> 188 - Please write &apos; 189 - <span className="font-semibold">{confirmationValue}</span> 190 - &apos; to confirm 189 + <form id="form-alert-dialog" className="space-y-1.5"> 190 + <p className="text-muted-foreground text-sm"> 191 + Type{" "} 192 + <Button 193 + variant="secondary" 194 + size="sm" 195 + type="button" 196 + className="font-normal [&_svg]:size-3" 197 + onClick={() => copy(confirmationValue, { withToast: false })} 198 + > 199 + {confirmationValue} 200 + {isCopied ? <Check /> : <Copy />} 201 + </Button>{" "} 202 + to confirm 191 203 </p> 192 204 <Input value={value} onChange={(e) => setValue(e.target.value)} /> 193 205 </form>
+1 -2
apps/dashboard/src/components/data-table/monitors/data-table-row-actions.tsx
··· 42 42 <QuickActions 43 43 actions={actions} 44 44 deleteAction={{ 45 - title: row.original.name, 46 - confirmationValue: "delete monitor", 45 + confirmationValue: row.original.name ?? "monitor", 47 46 submitAction: async () => { 48 47 await deleteMonitorMutation.mutateAsync({ 49 48 id: row.original.id,
+1 -2
apps/dashboard/src/components/data-table/notifications/data-table-row-actions.tsx
··· 47 47 <QuickActions 48 48 actions={actions} 49 49 deleteAction={{ 50 - title: props.row.original.name, 51 - confirmationValue: "delete notifier", 50 + confirmationValue: props.row.original.name ?? "notifier", 52 51 submitAction: async () => { 53 52 await deleteNotifierMutation.mutateAsync({ 54 53 id: props.row.original.id,
+1 -2
apps/dashboard/src/components/data-table/private-locations/data-table-row-actions.tsx
··· 47 47 <QuickActions 48 48 actions={actions} 49 49 deleteAction={{ 50 - title: props.row.original.name, 51 - confirmationValue: "delete private location", 50 + confirmationValue: props.row.original.name ?? "private location", 52 51 submitAction: async () => { 53 52 await deletePrivateLocationMutation.mutateAsync({ 54 53 id: props.row.original.id,
+1 -1
apps/dashboard/src/components/data-table/settings/invitations/data-table.tsx
··· 70 70 <div className="flex justify-end"> 71 71 <QuickActions 72 72 deleteAction={{ 73 - title: item.email, 73 + confirmationValue: item.email ?? "invitation", 74 74 submitAction: async () => 75 75 deleteInvitationMutation.mutateAsync({ id: item.id }), 76 76 }}
+1 -2
apps/dashboard/src/components/data-table/settings/members/data-table.tsx
··· 52 52 <div className="flex justify-end"> 53 53 <QuickActions 54 54 deleteAction={{ 55 - title: item.user?.email ?? "", 56 - confirmationValue: "delete member", 55 + confirmationValue: item.user.email ?? "user", 57 56 // FIXME: when deleting myself, throws an error, should have been caught by the toast.error 58 57 submitAction: async () => 59 58 await deleteMemberMutation.mutateAsync({
+1 -2
apps/dashboard/src/components/data-table/status-pages/data-table-row-actions.tsx
··· 40 40 <QuickActions 41 41 actions={actions} 42 42 deleteAction={{ 43 - title: row.original.title, 44 - confirmationValue: "delete status page", 43 + confirmationValue: row.original.title ?? "status page", 45 44 submitAction: async () => { 46 45 await deleteStatusPageMutation.mutateAsync({ 47 46 id: row.original.id,
+1 -1
apps/dashboard/src/components/data-table/status-report-updates/data-table-row-actions.tsx
··· 55 55 <QuickActions 56 56 actions={actions} 57 57 deleteAction={{ 58 - title: String(row.id), 58 + confirmationValue: row.status ?? "status report update", 59 59 submitAction: async () => { 60 60 await deleteStatusReportUpdateMutation.mutateAsync({ 61 61 id: row.id,
+1 -1
apps/dashboard/src/components/data-table/status-reports/data-table-row-actions.tsx
··· 99 99 <QuickActions 100 100 actions={actions} 101 101 deleteAction={{ 102 - title: row.original.title, 102 + confirmationValue: row.original.title ?? "status report", 103 103 submitAction: async () => { 104 104 await deleteStatusReportMutation.mutateAsync({ 105 105 id: row.original.id,
+1 -1
apps/dashboard/src/components/data-table/subscribers/data-table-row-actions.tsx
··· 30 30 <QuickActions 31 31 actions={[]} 32 32 deleteAction={{ 33 - title: row.original.email, 33 + confirmationValue: row.original.email ?? "subscriber", 34 34 submitAction: async () => { 35 35 await deleteAction.mutateAsync({ 36 36 id: row.original.id,
+32 -14
apps/dashboard/src/components/dropdowns/quick-actions.tsx
··· 3 3 import type * as React from "react"; 4 4 import { useState, useTransition } from "react"; 5 5 6 - import { type LucideIcon, MoreHorizontal, Trash2 } from "lucide-react"; 6 + import { 7 + Check, 8 + Copy, 9 + type LucideIcon, 10 + MoreHorizontal, 11 + Trash2, 12 + } from "lucide-react"; 7 13 8 14 import { 9 15 AlertDialog, ··· 27 33 DropdownMenuTrigger, 28 34 } from "@/components/ui/dropdown-menu"; 29 35 import { Input } from "@/components/ui/input"; 36 + import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"; 30 37 import type { DropdownMenuContentProps } from "@radix-ui/react-dropdown-menu"; 31 38 import { isTRPCClientError } from "@trpc/client"; 32 39 import { toast } from "sonner"; ··· 42 49 onClick?: () => Promise<void> | void; 43 50 }[]; 44 51 deleteAction?: { 45 - title: string; 46 52 /** 47 - * If set, an input field will require the user input to validate deletion 53 + * The value that must be typed to confirm deletion. Also used in the dialog title. 48 54 */ 49 - confirmationValue?: string; 55 + confirmationValue: string; 50 56 submitAction?: () => Promise<void>; 51 57 }; 52 58 } ··· 63 69 const [value, setValue] = useState(""); 64 70 const [isPending, startTransition] = useTransition(); 65 71 const [open, setOpen] = useState(false); 72 + const { copy, isCopied } = useCopyToClipboard(); 66 73 67 74 const handleDelete = async () => { 68 75 startTransition(async () => { ··· 147 154 > 148 155 <AlertDialogHeader> 149 156 <AlertDialogTitle> 150 - Are you sure about deleting `{deleteAction?.title}`? 157 + Are you sure about deleting `{deleteAction?.confirmationValue}`? 151 158 </AlertDialogTitle> 152 159 <AlertDialogDescription> 153 160 This action cannot be undone. This will permanently remove the entry 154 161 from the database. 155 162 </AlertDialogDescription> 156 163 </AlertDialogHeader> 157 - {deleteAction?.confirmationValue ? ( 158 - <form id="form-alert-dialog" className="space-y-0.5"> 159 - <p className="text-muted-foreground text-xs"> 160 - Please write &apos; 161 - <span className="font-semibold"> 162 - {deleteAction?.confirmationValue} 163 - </span> 164 - &apos; to confirm 164 + {deleteAction?.confirmationValue && ( 165 + <form id="form-alert-dialog" className="space-y-1.5"> 166 + <p className="text-muted-foreground text-sm"> 167 + Type{" "} 168 + <Button 169 + variant="secondary" 170 + size="sm" 171 + type="button" 172 + className="font-normal [&_svg]:size-3" 173 + onClick={() => 174 + copy(deleteAction.confirmationValue || "", { 175 + withToast: false, 176 + }) 177 + } 178 + > 179 + {deleteAction.confirmationValue} 180 + {isCopied ? <Check /> : <Copy />} 181 + </Button>{" "} 182 + to confirm 165 183 </p> 166 184 <Input value={value} onChange={(e) => setValue(e.target.value)} /> 167 185 </form> 168 - ) : null} 186 + )} 169 187 <AlertDialogFooter> 170 188 <AlertDialogCancel onClick={(e) => e.stopPropagation()}> 171 189 Cancel
+18 -8
apps/dashboard/src/components/forms/form-alert-dialog.tsx
··· 13 13 } from "@/components/ui/alert-dialog"; 14 14 import { Button } from "@/components/ui/button"; 15 15 import { Input } from "@/components/ui/input"; 16 + import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"; 16 17 import { isTRPCClientError } from "@trpc/client"; 18 + import { Check, Copy } from "lucide-react"; 17 19 import { useState, useTransition } from "react"; 18 20 import { toast } from "sonner"; 19 21 20 22 interface FormAlertDialogProps { 21 - title: string; 22 23 confirmationValue: string; 23 24 submitAction: () => Promise<void>; 24 25 children?: React.ReactNode; 25 26 } 26 27 27 28 export function FormAlertDialog({ 28 - title, 29 29 confirmationValue, 30 30 submitAction, 31 31 children, 32 32 }: FormAlertDialogProps) { 33 33 const [value, setValue] = useState(""); 34 34 const [isPending, startTransition] = useTransition(); 35 + const { copy, isCopied } = useCopyToClipboard(); 35 36 const [open, setOpen] = useState(false); 36 37 37 38 const handleDelete = async () => { ··· 68 69 <AlertDialogContent> 69 70 <AlertDialogHeader> 70 71 <AlertDialogTitle> 71 - Are you sure about delete `{title}`? 72 + Are you sure about delete `{confirmationValue}`? 72 73 </AlertDialogTitle> 73 74 <AlertDialogDescription> 74 75 This action cannot be undone. This will permanently delete the item. 75 76 </AlertDialogDescription> 76 77 </AlertDialogHeader> 77 - <form id="form-alert-dialog" className="space-y-0.5"> 78 - <p className="text-muted-foreground text-xs"> 79 - Please write &apos; 80 - <span className="font-semibold">{confirmationValue}</span> 81 - &apos; to confirm 78 + <form id="form-alert-dialog" className="space-y-1.5"> 79 + <p className="text-muted-foreground text-sm"> 80 + Type{" "} 81 + <Button 82 + variant="secondary" 83 + size="sm" 84 + type="button" 85 + className="font-normal [&_svg]:size-3" 86 + onClick={() => copy(confirmationValue, { withToast: false })} 87 + > 88 + {confirmationValue} 89 + {isCopied ? <Check /> : <Copy />} 90 + </Button>{" "} 91 + to confirm 82 92 </p> 83 93 <Input value={value} onChange={(e) => setValue(e.target.value)} /> 84 94 </form>
+1 -5
apps/dashboard/src/components/forms/monitor/form-danger-zone.tsx
··· 23 23 <FormCardDescription>This action cannot be undone.</FormCardDescription> 24 24 </FormCardHeader> 25 25 <FormCardFooter variant="destructive" className="justify-end"> 26 - <FormAlertDialog 27 - title={title} 28 - confirmationValue="delete monitor" 29 - submitAction={onSubmit} 30 - /> 26 + <FormAlertDialog confirmationValue={title} submitAction={onSubmit} /> 31 27 </FormCardFooter> 32 28 </FormCard> 33 29 );
+1 -2
apps/dashboard/src/components/forms/settings/form-api-key.tsx
··· 128 128 </Button> 129 129 ) : ( 130 130 <FormAlertDialog 131 - title="API Key" 132 - confirmationValue="delete api key" 131 + confirmationValue="API Key" 133 132 submitAction={async () => { 134 133 await revokeApiKeyMutation.mutateAsync({ 135 134 keyId: apiKey.id,
+1 -5
apps/dashboard/src/components/forms/status-page/form-danger-zone.tsx
··· 23 23 <FormCardDescription>This action cannot be undone.</FormCardDescription> 24 24 </FormCardHeader> 25 25 <FormCardFooter variant="destructive" className="justify-end"> 26 - <FormAlertDialog 27 - title={title} 28 - confirmationValue="delete status page" 29 - submitAction={onSubmit} 30 - /> 26 + <FormAlertDialog confirmationValue={title} submitAction={onSubmit} /> 31 27 </FormCardFooter> 32 28 </FormCard> 33 29 );
+1 -2
apps/dashboard/src/components/nav/nav-monitors.tsx
··· 188 188 <QuickActions 189 189 actions={actions} 190 190 deleteAction={{ 191 - title: item.name, 192 - confirmationValue: "delete monitor", 191 + confirmationValue: item.name ?? "monitor", 193 192 submitAction: async () => { 194 193 await deleteMonitorMutation.mutateAsync({ 195 194 id: item.id,
+1 -2
apps/dashboard/src/components/nav/nav-status-pages.tsx
··· 160 160 <QuickActions 161 161 actions={actions} 162 162 deleteAction={{ 163 - title: item.title, 164 - confirmationValue: "delete status page", 163 + confirmationValue: item.title ?? "status page", 165 164 submitAction: async () => { 166 165 await deleteStatusPage.mutateAsync({ id: item.id }); 167 166 if (pathname.includes(`/status-pages/${item.id}`)) {