SaveSnippetDialog.tsx124 lines · main
1import { useParams } from 'common'
2import { useEffect, useState } from 'react'
3import { toast } from 'sonner'
4import {
5 AiIconAnimation,
6 Button,
7 Dialog,
8 DialogContent,
9 DialogFooter,
10 DialogHeader,
11 DialogSection,
12 DialogSectionSeparator,
13 DialogTitle,
14 Input,
15 Label,
16} from 'ui'
17
18import { subscriptionHasHipaaAddon } from '@/components/interfaces/Billing/Subscription/Subscription.utils'
19import { generateSnippetTitle } from '@/components/interfaces/SQLEditor/SQLEditor.constants'
20import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
21import { useCheckOpenAIKeyQuery } from '@/data/ai/check-api-key-query'
22import { useSqlTitleGenerateMutation } from '@/data/ai/sql-title-mutation'
23import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query'
24import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query'
25import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
26
27interface SaveSnippetDialogProps {
28 open: boolean
29 sql: string
30 onOpenChange: (open: boolean) => void
31 onSave: (name: string) => void
32}
33
34export const SaveSnippetDialog = ({ open, sql, onOpenChange, onSave }: SaveSnippetDialogProps) => {
35 const { ref } = useParams()
36 const { data: organization } = useSelectedOrganizationQuery()
37 const { data: subscription } = useOrgSubscriptionQuery(
38 { orgSlug: organization?.slug },
39 { enabled: open }
40 )
41 const { data: projectSettings } = useProjectSettingsV2Query({ projectRef: ref })
42 const { data: check } = useCheckOpenAIKeyQuery()
43
44 const [name, setName] = useState(generateSnippetTitle())
45
46 const isApiKeySet = !!check?.hasKey
47 const hasHipaaAddon = subscriptionHasHipaaAddon(subscription) && projectSettings?.is_sensitive
48
49 const { mutate: generateTitle, isPending: isGenerating } = useSqlTitleGenerateMutation({
50 onSuccess: ({ title }) => setName(title),
51 onError: (error) => toast.error(`Failed to generate title: ${error.message}`),
52 })
53
54 // Reset the name each time the dialog opens
55 useEffect(() => {
56 if (open) setName(generateSnippetTitle())
57 }, [open])
58
59 const handleSave = () => {
60 const trimmed = name.trim()
61 if (!trimmed) return
62 onSave(trimmed)
63 onOpenChange(false)
64 }
65
66 return (
67 <Dialog open={open} onOpenChange={onOpenChange}>
68 <DialogContent size="small">
69 <DialogHeader>
70 <DialogTitle>Save snippet</DialogTitle>
71 </DialogHeader>
72 <DialogSectionSeparator />
73 <DialogSection className="flex flex-col gap-y-4 py-5">
74 <div className="flex flex-col gap-y-2">
75 <Label htmlFor="snippet-name">Name</Label>
76 <Input
77 id="snippet-name"
78 autoFocus
79 value={name}
80 onChange={(e) => setName(e.target.value)}
81 onKeyDown={(e) => {
82 if (e.key === 'Enter') handleSave()
83 }}
84 />
85 </div>
86 {!hasHipaaAddon && (
87 <div className="flex justify-end">
88 <ButtonTooltip
89 type="default"
90 size="tiny"
91 disabled={isGenerating || !isApiKeySet}
92 onClick={() => generateTitle({ sql })}
93 tooltip={{
94 content: {
95 side: 'bottom',
96 text: isApiKeySet
97 ? undefined
98 : 'Add your "OPENAI_API_KEY" to your environment variables to use this feature.',
99 },
100 }}
101 >
102 <div className="flex items-center gap-1">
103 <div className="scale-75">
104 <AiIconAnimation loading={isGenerating} />
105 </div>
106 <span>Generate with AI</span>
107 </div>
108 </ButtonTooltip>
109 </div>
110 )}
111 </DialogSection>
112 <DialogSectionSeparator />
113 <DialogFooter className="px-5 py-4">
114 <Button type="default" onClick={() => onOpenChange(false)}>
115 Cancel
116 </Button>
117 <Button disabled={!name.trim()} onClick={handleSave}>
118 Save snippet
119 </Button>
120 </DialogFooter>
121 </DialogContent>
122 </Dialog>
123 )
124}