import { useEffect, useState } from 'react' import { toast } from 'sonner' import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useCheckOpenAIKeyQuery } from 'data/ai/check-api-key-query' import { useSqlTitleGenerateMutation } from 'data/ai/sql-title-mutation' import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { getContentById } from 'data/content/content-id-query' import { UpsertContentPayload, useContentUpsertMutation, } from 'data/content/content-upsert-mutation' import { Snippet } from 'data/content/sql-folders-query' import type { SqlSnippet } from 'data/content/sql-snippets-query' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { IS_PLATFORM } from 'lib/constants' import { useRouter } from 'next/router' import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2' import { createTabId, useTabsStateSnapshot } from 'state/tabs' import { AiIconAnimation, Button, Form, Input, Modal } from 'ui' import { subscriptionHasHipaaAddon } from '../Billing/Subscription/Subscription.utils' export interface RenameQueryModalProps { snippet?: SqlSnippet | Snippet visible: boolean onCancel: () => void onComplete: () => void } const RenameQueryModal = ({ snippet = {} as any, visible, onCancel, onComplete, }: RenameQueryModalProps) => { const { ref } = useParams() const router = useRouter() const { data: organization } = useSelectedOrganizationQuery() const snapV2 = useSqlEditorV2StateSnapshot() const tabsSnap = useTabsStateSnapshot() const { data: subscription } = useOrgSubscriptionQuery( { orgSlug: organization?.slug }, { enabled: visible } ) const isSQLSnippet = snippet.type === 'sql' const { data: projectSettings } = useProjectSettingsV2Query({ projectRef: ref }) // Customers on HIPAA plans should not have access to Supabase AI const hasHipaaAddon = subscriptionHasHipaaAddon(subscription) && projectSettings?.is_sensitive const { id, name, description } = snippet const [nameInput, setNameInput] = useState(name) const [descriptionInput, setDescriptionInput] = useState(description) const { mutate: titleSql, isPending: isTitleGenerationLoading } = useSqlTitleGenerateMutation({ onSuccess: (data) => { const { title, description } = data setNameInput(title) if (!descriptionInput) setDescriptionInput(description) }, onError: (error) => { toast.error(`Failed to rename query: ${error.message}`) }, }) const { data: check } = useCheckOpenAIKeyQuery() const isApiKeySet = !!check?.hasKey const generateTitle = async () => { if ('content' in snippet && isSQLSnippet) { titleSql({ sql: snippet.content.sql }) } else { try { const { content } = await getContentById({ projectRef: ref, id: snippet.id }) if ('sql' in content) titleSql({ sql: content.sql }) } catch (error) { toast.error('Unable to generate title based on query contents') } } } const validate = () => { const errors: any = {} if (!nameInput) errors.name = 'Please enter a query name' return errors } const { mutateAsync: upsertContent } = useContentUpsertMutation() const onSubmit = async (values: any, { setSubmitting }: any) => { if (!ref) return console.error('Project ref is required') if (!id) return console.error('Snippet ID is required') setSubmitting(true) try { let localSnippet = snippet // [Joshen] For SQL V2 - content is loaded on demand so we need to fetch the data if its not already loaded in the valtio state if (!('content' in localSnippet)) { localSnippet = await getContentById({ projectRef: ref, id }) snapV2.addSnippet({ projectRef: ref, snippet: localSnippet }) } const changedSnippet = await upsertContent({ projectRef: ref, payload: { ...localSnippet, name: nameInput, description: descriptionInput, } as UpsertContentPayload, }) if (IS_PLATFORM) { snapV2.renameSnippet({ id, name: nameInput, description: descriptionInput }) const tabId = createTabId('sql', { id }) tabsSnap.updateTab(tabId, { label: nameInput }) } else if (changedSnippet) { // In self-hosted, the snippet also updates the id when renaming it. This code is to ensure the previous snippet // is removed, new one is added, tab state is updated and the router is updated. // remove the old snippet from the state without saving to API snapV2.removeSnippet(id, true) snapV2.addSnippet({ projectRef: ref, snippet: changedSnippet }) // remove the tab for the old snippet if the snippet was open. Renaming can also happen when the tab is not open. const tabId = createTabId('sql', { id }) if (tabsSnap.hasTab(tabId)) { tabsSnap.removeTab(tabId) await router.push(`/project/${ref}/sql/${changedSnippet.id}`) } } toast.success('Successfully renamed snippet!') if (onComplete) onComplete() } catch (error: any) { setSubmitting(false) // [Joshen] We probably need some rollback cause all the saving is async toast.error(`Failed to rename snippet: ${error.message}`) } } useEffect(() => { setNameInput(name) setDescriptionInput(description) }, [snippet.id]) return (
{({ isSubmitting }: { isSubmitting: boolean }) => ( <> setNameInput(e.target.value)} />
{!hasHipaaAddon && ( generateTitle()} size="tiny" disabled={isTitleGenerationLoading || !isApiKeySet} tooltip={{ content: { side: 'bottom', text: isApiKeySet ? undefined : 'Add your "OPENAI_API_KEY" to your environment variables to use this feature.', }, }} >
Rename with Supabase AI
)}
setDescriptionInput(e.target.value)} />
)}
) } export default RenameQueryModal