From 65e786ba1311084d48d15922652070c4df71e550 Mon Sep 17 00:00:00 2001 From: Pamela Chia Date: Thu, 20 Aug 2026 19:18:47 +0800 Subject: [PATCH 01/12] feat(docs): manifest-gated markdown alternate helper (#48389) --- .../guides/troubleshooting/[slug]/page.tsx | 5 +- apps/docs/app/guides/troubleshooting/page.tsx | 2 + apps/docs/app/page.tsx | 5 +- apps/docs/features/docs/GuidesMdx.utils.tsx | 3 +- .../docs/TroubleshootingSection.page.tsx | 9 +- apps/docs/lib/md-alternates.test.ts | 117 ++++++++++++++++++ apps/docs/lib/md-alternates.ts | 9 ++ apps/docs/turbo.jsonc | 11 ++ 8 files changed, 150 insertions(+), 11 deletions(-) create mode 100644 apps/docs/lib/md-alternates.test.ts create mode 100644 apps/docs/lib/md-alternates.ts diff --git a/apps/docs/app/guides/troubleshooting/[slug]/page.tsx b/apps/docs/app/guides/troubleshooting/[slug]/page.tsx index 9f15cac7d53fc..40d22deedb31a 100644 --- a/apps/docs/app/guides/troubleshooting/[slug]/page.tsx +++ b/apps/docs/app/guides/troubleshooting/[slug]/page.tsx @@ -1,9 +1,9 @@ -import { notFound } from 'next/navigation' - import TroubleshootingPage from '~/features/docs/Troubleshooting.page' import { getAllTroubleshootingEntries, getArticleSlug } from '~/features/docs/Troubleshooting.utils' import { PROD_URL } from '~/lib/constants' import { getCustomContent } from '~/lib/custom-content/getCustomContent' +import { mdAlternate } from '~/lib/md-alternates' +import { notFound } from 'next/navigation' export const dynamicParams = false @@ -38,6 +38,7 @@ export const generateMetadata = async (props: { params: Promise<{ slug: string } title: `${metadataTitle || 'Supabase'} | Troubleshooting${entry ? ` | ${entry.data.title}` : ''}`, alternates: { canonical: `${PROD_URL}/guides/troubleshooting/${slug}`, + types: mdAlternate(`troubleshooting/${slug}`), }, } } diff --git a/apps/docs/app/guides/troubleshooting/page.tsx b/apps/docs/app/guides/troubleshooting/page.tsx index 19e92851bb5dd..66d34249eb9d5 100644 --- a/apps/docs/app/guides/troubleshooting/page.tsx +++ b/apps/docs/app/guides/troubleshooting/page.tsx @@ -14,6 +14,7 @@ import { TROUBLESHOOTING_CONTAINER_ID } from '~/features/docs/Troubleshooting.ut import { SidebarSkeleton } from '~/layouts/MainSkeleton' import { PROD_URL } from '~/lib/constants' import { getCustomContent } from '~/lib/custom-content/getCustomContent' +import { mdAlternate } from '~/lib/md-alternates' import { type Metadata } from 'next' const { metadataTitle } = getCustomContent(['metadata:title']) @@ -62,5 +63,6 @@ export const metadata: Metadata = { title: `${metadataTitle || 'Supabase'} | Troubleshooting`, alternates: { canonical: `${PROD_URL}/guides/troubleshooting`, + types: mdAlternate('troubleshooting'), }, } diff --git a/apps/docs/app/page.tsx b/apps/docs/app/page.tsx index 452dad1311080..27e6a28281c9d 100644 --- a/apps/docs/app/page.tsx +++ b/apps/docs/app/page.tsx @@ -28,10 +28,7 @@ const generateMetadata = async (_, parent: ResolvingMetadata): Promise ...(parentAlternates && { languages: parentAlternates.languages || undefined, media: parentAlternates.media || undefined, - types: { - ...(parentAlternates.types ?? {}), - 'text/markdown': 'https://supabase.com/llms-full.txt', - }, + types: parentAlternates.types || undefined, }), }, } diff --git a/apps/docs/features/docs/GuidesMdx.utils.tsx b/apps/docs/features/docs/GuidesMdx.utils.tsx index 70a23ed0961b4..ab1d34c649453 100644 --- a/apps/docs/features/docs/GuidesMdx.utils.tsx +++ b/apps/docs/features/docs/GuidesMdx.utils.tsx @@ -9,6 +9,7 @@ import { generateOpenGraphImageMeta } from '~/features/seo/openGraph' import { BASE_PATH } from '~/lib/constants' import { getCustomContent } from '~/lib/custom-content/getCustomContent' import { GUIDES_DIRECTORY, isValidGuideFrontmatter, type GuideFrontmatter } from '~/lib/docs' +import { mdAlternate } from '~/lib/md-alternates' import { GuideModelLoader } from '~/resources/guide/guideModelLoader' import { fromMarkdown } from 'mdast-util-from-markdown' import { gfmFromMarkdown } from 'mdast-util-gfm' @@ -182,7 +183,7 @@ const genGuideMeta = canonical: meta.canonical || `${BASE_PATH}${pathname}`, types: { ...(parentAlternates?.types ?? {}), - 'text/markdown': `${BASE_PATH}${pathname}.md`, + ...mdAlternate(pathname.replace(/^\/guides\//, '')), }, }, openGraph: { diff --git a/apps/docs/features/docs/TroubleshootingSection.page.tsx b/apps/docs/features/docs/TroubleshootingSection.page.tsx index 9fdeb8b61738d..3cb49e2ca3799 100644 --- a/apps/docs/features/docs/TroubleshootingSection.page.tsx +++ b/apps/docs/features/docs/TroubleshootingSection.page.tsx @@ -1,18 +1,18 @@ -import { type Metadata } from 'next' - -import { TroubleshootingHeader, TroubleshootingEntries } from '~/features/docs/Troubleshooting.ui' +import { TroubleshootingEntries, TroubleshootingHeader } from '~/features/docs/Troubleshooting.ui' import { TroubleshootingFilterEmptyState, TroubleshootingListController, } from '~/features/docs/Troubleshooting.ui.client' import { - type ITroubleshootingMetadata, getTroubleshootingEntriesByTopic, getTroubleshootingErrorsByTopic, getTroubleshootingKeywordsByTopic, + type ITroubleshootingMetadata, } from '~/features/docs/Troubleshooting.utils' import { PROD_URL } from '~/lib/constants' import { getCustomContent } from '~/lib/custom-content/getCustomContent' +import { mdAlternate } from '~/lib/md-alternates' +import { type Metadata } from 'next' const { metadataTitle } = getCustomContent(['metadata:title']) @@ -66,6 +66,7 @@ export function generateSectionTroubleshootingMetadata( title: `${metadataTitle ?? 'Supabase'} | ${sectionName} Troubleshooting`, alternates: { canonical: `${PROD_URL}/guides/${topic}/troubleshooting`, + types: mdAlternate(`${topic}/troubleshooting`), }, } } diff --git a/apps/docs/lib/md-alternates.test.ts b/apps/docs/lib/md-alternates.test.ts new file mode 100644 index 0000000000000..9aa9296f111a3 --- /dev/null +++ b/apps/docs/lib/md-alternates.test.ts @@ -0,0 +1,117 @@ +import { promises as fs } from 'node:fs' +import path from 'node:path' +import { PROD_URL } from '~/lib/constants' +import { describe, expect, it, vi } from 'vitest' + +import { mdAlternate } from './md-alternates' + +vi.mock('~/public/markdown/manifest.json', () => ({ + default: [ + 'getting-started/quickstarts/react', + 'troubleshooting/all-about-supabase-egress-a_Sg_e', + 'troubleshooting', + ], +})) + +describe('mdAlternate', () => { + it('returns the absolute .md sibling for a manifest-listed guide slug', () => { + expect(mdAlternate('getting-started/quickstarts/react')).toEqual({ + 'text/markdown': `${PROD_URL}/guides/getting-started/quickstarts/react.md`, + }) + }) + + it('returns the sibling for a troubleshooting entry', () => { + expect(mdAlternate('troubleshooting/all-about-supabase-egress-a_Sg_e')).toEqual({ + 'text/markdown': `${PROD_URL}/guides/troubleshooting/all-about-supabase-egress-a_Sg_e.md`, + }) + }) + + it('returns the sibling for the troubleshooting index', () => { + expect(mdAlternate('troubleshooting')).toEqual({ + 'text/markdown': `${PROD_URL}/guides/troubleshooting.md`, + }) + }) + + it('returns undefined for slugs without generated markdown', () => { + expect(mdAlternate('database/extensions/wrappers/s3')).toBeUndefined() + expect(mdAlternate('local-development/cli/config')).toBeUndefined() + }) +}) + +const WIRING: [string, string][] = [ + ['features/docs/GuidesMdx.utils.tsx', 'mdAlternate('], + ['app/guides/troubleshooting/[slug]/page.tsx', 'mdAlternate(`troubleshooting/${slug}`)'], + ['app/guides/troubleshooting/page.tsx', "mdAlternate('troubleshooting')"], + ['features/docs/TroubleshootingSection.page.tsx', 'mdAlternate(`${topic}/troubleshooting`)'], +] + +describe('markdown alternate wiring', () => { + it('every tag emitter routes through mdAlternate', async () => { + for (const [file, wiring] of WIRING) { + const source = await fs.readFile(path.join(process.cwd(), file), 'utf-8') + expect(source.includes(wiring), `${file} must contain "${wiring}"`).toBe(true) + } + }) + + it('the generator registers the troubleshooting index in the manifest', async () => { + const source = await fs.readFile( + path.join(process.cwd(), 'internals/generate-guides-markdown.ts'), + 'utf-8' + ) + expect(source.includes("renderManifest(sources, ['troubleshooting'])")).toBe(true) + }) +}) + +const SCAN_ROOTS = ['app', 'components', 'features', 'lib', 'internals'] +const ALLOWED_MD_LITERAL_FILES = new Set([ + 'lib/md-alternates.ts', + 'app/api/guides-md/[...slug]/route.ts', +]) + +function isScannableSource(fileName: string): boolean { + return ( + /\.(ts|tsx)$/.test(fileName) && + !/\.test\.(ts|tsx)$/.test(fileName) && + !fileName.endsWith('.d.ts') + ) +} + +async function collectSourceFiles(dir: string): Promise { + const dirents = await fs.readdir(dir, { withFileTypes: true }) + const files: string[] = [] + for (const dirent of dirents) { + const full = path.join(dir, dirent.name) + if (dirent.isDirectory()) { + if (dirent.name === 'node_modules') continue + files.push(...(await collectSourceFiles(full))) + } else if (isScannableSource(dirent.name)) { + files.push(full) + } + } + return files +} + +describe('no hardcoded text/markdown outside the helper', () => { + it('every text/markdown occurrence lives in an allowed file', async () => { + const rootFiles = (await fs.readdir(process.cwd(), { withFileTypes: true })) + .filter((dirent) => dirent.isFile() && isScannableSource(dirent.name)) + .map((dirent) => path.join(process.cwd(), dirent.name)) + const nestedFiles = ( + await Promise.all( + SCAN_ROOTS.map((root) => collectSourceFiles(path.join(process.cwd(), root))) + ) + ).flat() + + const offenders = ( + await Promise.all( + [...rootFiles, ...nestedFiles].map(async (file) => { + const rel = path.relative(process.cwd(), file) + if (ALLOWED_MD_LITERAL_FILES.has(rel)) return null + const source = await fs.readFile(file, 'utf-8') + return source.includes('text/markdown') ? rel : null + }) + ) + ).filter((rel): rel is string => rel !== null) + expect(offenders, `hardcoded text/markdown in: ${offenders.join(', ')}`).toEqual([]) + }) +}) diff --git a/apps/docs/lib/md-alternates.ts b/apps/docs/lib/md-alternates.ts new file mode 100644 index 0000000000000..6e2294acbc091 --- /dev/null +++ b/apps/docs/lib/md-alternates.ts @@ -0,0 +1,9 @@ +import { PROD_URL } from '~/lib/constants' +import MARKDOWN_SLUGS from '~/public/markdown/manifest.json' + +const SLUGS = new Set(MARKDOWN_SLUGS) + +export function mdAlternate(slug: string): { 'text/markdown': string } | undefined { + if (!SLUGS.has(slug)) return undefined + return { 'text/markdown': `${PROD_URL}/guides/${slug}.md` } +} diff --git a/apps/docs/turbo.jsonc b/apps/docs/turbo.jsonc index 9d663a7ff71bc..3bd522d323562 100644 --- a/apps/docs/turbo.jsonc +++ b/apps/docs/turbo.jsonc @@ -10,10 +10,21 @@ "inputs": ["spec/**"], "outputs": ["features/docs/generated/**"], }, + // Fetches remote (GitHub) content and the JSON artifacts the markdown generator reads. + // Not cached: the inputs are remote, so a cache restore could resurrect stale content. + "build:federated-content": { + "cache": false, + "env": [ + "DOCS_GITHUB_APP_ID", + "DOCS_GITHUB_APP_INSTALLATION_ID", + "DOCS_GITHUB_APP_PRIVATE_KEY", + ], + }, // Generates the guide/reference .md files and manifest.json under public/markdown/. // Declaring outputs lets Turbo restore these artifacts on a cache hit; without it a // cached run would skip the script and leave the generated markdown missing for `next build`. "build:markdown": { + "dependsOn": ["build:federated-content"], "outputs": ["public/markdown/**", "public/markdown/manifest.json"], }, "test": { From 1a483ab25506109bc07f781495ecc2b496757351 Mon Sep 17 00:00:00 2001 From: David Camacho Cateura <47002519+dcamachoc@users.noreply.github.com> Date: Thu, 20 Aug 2026 14:39:38 +0200 Subject: [PATCH 02/12] feat: Show all partner audit logs fields (#49305) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Changes to the audit logs UI ## What is the current behavior? Partner related fields in the audit logs are not shown ## What is the new behavior? - Shows all partner related fields in the audit logs - Also uses the new fields to compute the user name ## Summary by CodeRabbit * **New Features** * Audit log entries now display partner names, installation IDs, user emails, and user IDs when available. * Partner identity and email are shown when standard actor details are unavailable. * Partner names are consistently formatted for clearer display. * Entries without partner information continue to display cleanly without blank or confusing actor details. --- .../interfaces/AuditLogs/LogDetailsPanel.tsx | 24 +++++++++++++++++ .../Organization/AuditLogs/AuditLogs.tsx | 17 +++++++++--- .../Organization/AuditLogs/AuditLogs.utils.ts | 10 +++++++ .../organization-audit-logs-query.ts | 4 +++ .../AuditLogs/OrgAuditLogs.utils.test.ts | 27 ++++++++++++++++++- 5 files changed, 77 insertions(+), 5 deletions(-) diff --git a/apps/studio/components/interfaces/AuditLogs/LogDetailsPanel.tsx b/apps/studio/components/interfaces/AuditLogs/LogDetailsPanel.tsx index 488e5b7386df3..4dbe508ee0131 100644 --- a/apps/studio/components/interfaces/AuditLogs/LogDetailsPanel.tsx +++ b/apps/studio/components/interfaces/AuditLogs/LogDetailsPanel.tsx @@ -94,6 +94,30 @@ export const LogDetailsPanel = ({ selectedLog, onClose }: LogDetailsPanelProps) )} + {selectedLog?.actor.partner && ( + + + + )} + {selectedLog?.actor.partner_installation_id && ( + + + + )} + {selectedLog?.actor.partner_user_email && ( + + + + )} + {selectedLog?.actor.partner_user_id && ( + + + + )} diff --git a/apps/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx b/apps/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx index f104b5ed19106..04af2231f0fc0 100644 --- a/apps/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx +++ b/apps/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx @@ -9,7 +9,12 @@ import { useEffect, useMemo, useState } from 'react' import { Alert, AlertDescription, AlertTitle, Button, WarningIcon } from 'ui' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' -import { filterByProjects, filterByUsers, sortAuditLogs } from './AuditLogs.utils' +import { + filterByProjects, + filterByUsers, + formatPartnerIfExists, + sortAuditLogs, +} from './AuditLogs.utils' import { LogDetailsPanel } from '@/components/interfaces/AuditLogs/LogDetailsPanel' import { LogsDatePicker } from '@/components/interfaces/Settings/Logs/Logs.DatePickers' import { ScaffoldContainer, ScaffoldSection } from '@/components/layouts/Scaffold' @@ -351,6 +356,12 @@ export const AuditLogs = () => { /> ) + const actorDisplayName = + user?.username || + log.actor.email || + formatPartnerIfExists(log.actor.partner, log.actor.partner_user_email) || + '-' + return ( {
{userIcon}
-

- {user?.username ?? log.actor.email ?? '-'} -

+

{actorDisplayName}

{role && (

{role?.name} diff --git a/apps/studio/components/interfaces/Organization/AuditLogs/AuditLogs.utils.ts b/apps/studio/components/interfaces/Organization/AuditLogs/AuditLogs.utils.ts index 92a61306543b7..d69f65a85d31b 100644 --- a/apps/studio/components/interfaces/Organization/AuditLogs/AuditLogs.utils.ts +++ b/apps/studio/components/interfaces/Organization/AuditLogs/AuditLogs.utils.ts @@ -50,3 +50,13 @@ export const formatSelectedDateRange = (value: DatePickerToFrom) => { return { from: from.utc().toISOString(), to: to.utc().toISOString() } } } + +export function formatPartnerIfExists( + partner: string | undefined, + partnerEmail: string | undefined +) { + if (!partner) return undefined + + const capitalized = `${partner[0].toUpperCase()}${partner.slice(1).toLowerCase()}` + return partnerEmail ? `${partnerEmail} (${capitalized})` : capitalized +} diff --git a/apps/studio/data/organizations/organization-audit-logs-query.ts b/apps/studio/data/organizations/organization-audit-logs-query.ts index 77870c3c7b102..e7b2ff688d415 100644 --- a/apps/studio/data/organizations/organization-audit-logs-query.ts +++ b/apps/studio/data/organizations/organization-audit-logs-query.ts @@ -29,6 +29,10 @@ export type AuditLog = { app_id?: string app_name?: string ip?: string + partner?: string + partner_installation_id?: string + partner_user_email?: string + partner_user_id?: string } timestamp: number } diff --git a/apps/studio/tests/components/AuditLogs/OrgAuditLogs.utils.test.ts b/apps/studio/tests/components/AuditLogs/OrgAuditLogs.utils.test.ts index 86b1a30f50158..ca3820398e298 100644 --- a/apps/studio/tests/components/AuditLogs/OrgAuditLogs.utils.test.ts +++ b/apps/studio/tests/components/AuditLogs/OrgAuditLogs.utils.test.ts @@ -1,7 +1,10 @@ import dayjs from 'dayjs' import { afterEach, describe, expect, test, vi } from 'vitest' -import { formatSelectedDateRange } from '@/components/interfaces/Organization/AuditLogs/AuditLogs.utils' +import { + formatPartnerIfExists, + formatSelectedDateRange, +} from '@/components/interfaces/Organization/AuditLogs/AuditLogs.utils' // Pin "now" to a fixed point so date comparisons are deterministic const NOW = dayjs('2024-06-15T14:30:00') @@ -81,3 +84,25 @@ describe('formatSelectedDateRange', () => { expect(result.to).toMatch(/Z$/) }) }) + +describe('formatPartnerIfExists', () => { + test('capitalizes the partner name', () => { + expect(formatPartnerIfExists('github', undefined)).toBe('Github') + }) + + test('prefixes with the partner email when provided', () => { + expect(formatPartnerIfExists('github', 'a@b.com')).toBe('a@b.com (Github)') + }) + + test('normalizes casing regardless of input case', () => { + expect(formatPartnerIfExists('GITHUB', undefined)).toBe('Github') + }) + + test('returns undefined when partner is undefined', () => { + expect(formatPartnerIfExists(undefined, 'a@b.com')).toBeUndefined() + }) + + test('returns undefined when partner is an empty string', () => { + expect(formatPartnerIfExists('', 'a@b.com')).toBeUndefined() + }) +}) From 474bf5da4a67a35528ba087e61a3e00098b2baf7 Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Thu, 20 Aug 2026 09:09:45 -0400 Subject: [PATCH 03/12] fix(studio): reset rename form between same-named SQL snippets (#49275) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Bug fix ## What is the current behavior? In the SQL Editor, when two snippets are both named "Untitled query" and one is renamed, the rename modal's state is not reset afterwards. Opening the rename modal for the second snippet prefills the input with the first snippet's new name, and the second snippet can't be renamed at all because the "Rename query" button stays disabled. `RenameQueryModal` fed the snippet to react-hook-form through the `values` option, which only re-runs its reset when the values object deep-changes. Two snippets with the same name (and no description) produce a deep-equal object, so switching between them never resets the form — it keeps the previously renamed name and stays non-dirty. ## What is the new behavior? The form is mounted per snippet (`key={snippet.id}`) with plain `defaultValues`, so no form state can carry over between snippets regardless of name collisions. `SQLEditorNav` derives modal visibility from the selected snippet and clears it on cancel/complete, matching `SearchList`. Covered by a new component test in `RenameQueryModal.test.tsx` that renames one "Untitled query", reopens the modal for a second one, and asserts the field resets and the second rename submits. ## Additional context ## Summary by CodeRabbit * **Bug Fixes** * Fixed the rename dialog retaining input from a previously renamed snippet. * Ensured the rename form resets correctly after successful submission and when switching between snippets. * **Tests** * Added regression coverage for renaming multiple untitled snippets with the same original name. --- .../SQLEditor/RenameQueryModal.test.tsx | 43 ++++ .../interfaces/SQLEditor/RenameQueryModal.tsx | 184 +++++++++--------- .../SQLEditorNavV2/SQLEditorNav.tsx | 11 +- 3 files changed, 142 insertions(+), 96 deletions(-) diff --git a/apps/studio/components/interfaces/SQLEditor/RenameQueryModal.test.tsx b/apps/studio/components/interfaces/SQLEditor/RenameQueryModal.test.tsx index 1113bd5bf0704..477a92297957d 100644 --- a/apps/studio/components/interfaces/SQLEditor/RenameQueryModal.test.tsx +++ b/apps/studio/components/interfaces/SQLEditor/RenameQueryModal.test.tsx @@ -34,6 +34,8 @@ const createSnippet = (id: string, name: string): SnippetWithContent => ({ const SNIPPET_A = createSnippet('snippet-a', 'First query') const SNIPPET_B = createSnippet('snippet-b', 'Second query') +const UNTITLED_A = createSnippet('untitled-a', 'Untitled query') +const UNTITLED_B = createSnippet('untitled-b', 'Untitled query') /** The modal renders the AI title generator, which checks for an OpenAI key when self-hosted. */ const mockOpenAIKeyCheck = () => @@ -108,6 +110,47 @@ describe('RenameQueryModal', () => { expect(screen.getByRole('button', { name: 'Rename query' })).toBeDisabled() }) + test('resets for the next snippet when both snippets share a name', async () => { + mockOpenAIKeyCheck() + const requests = mockUpsert() + const onComplete = vi.fn() + + const { rerender } = customRender( + + ) + + await userEvent.clear(getNameInput()) + await userEvent.type(getNameInput(), 'Renamed query') + fireEvent.click(screen.getByRole('button', { name: 'Rename query' })) + await waitFor(() => expect(onComplete).toHaveBeenCalledOnce()) + + // The parent keeps the renamed snippet selected while closing the modal, then reopens it for + // a second snippet that still carries the same original name + rerender( + + ) + rerender( + + ) + + await waitFor(() => expect(getNameInput()).toHaveValue('Untitled query')) + + await userEvent.clear(getNameInput()) + await userEvent.type(getNameInput(), 'Second renamed query') + fireEvent.click(screen.getByRole('button', { name: 'Rename query' })) + + await waitFor(() => expect(onComplete).toHaveBeenCalledTimes(2)) + expect(requests).toEqual([ + { id: 'untitled-a', name: 'Renamed query' }, + { id: 'untitled-b', name: 'Second renamed query' }, + ]) + }) + test('discards an abandoned edit when cancelled', async () => { mockOpenAIKeyCheck() const onCancel = vi.fn() diff --git a/apps/studio/components/interfaces/SQLEditor/RenameQueryModal.tsx b/apps/studio/components/interfaces/SQLEditor/RenameQueryModal.tsx index 1ae91dc20c121..8041ebe9b7d3b 100644 --- a/apps/studio/components/interfaces/SQLEditor/RenameQueryModal.tsx +++ b/apps/studio/components/interfaces/SQLEditor/RenameQueryModal.tsx @@ -49,12 +49,13 @@ const formSchema = z.object({ description: z.string().optional(), }) -export const RenameQueryModal = ({ - snippet = {} as any, - visible, - onCancel, - onComplete, -}: RenameQueryModalProps) => { +interface RenameQueryFormProps { + snippet: SqlSnippet | Snippet + onCancel: () => void + onComplete: () => void +} + +const RenameQueryForm = ({ snippet, onCancel, onComplete }: RenameQueryFormProps) => { const { ref } = useParams() const router = useRouter() @@ -145,7 +146,7 @@ export const RenameQueryModal = ({ } toast.success('Successfully renamed snippet!') - reset({ name, description }, { keepDirtyValues: false }) + reset({ name, description }) if (onComplete) onComplete() } catch (error: any) { // [Joshen] We probably need some rollback cause all the saving is async @@ -156,98 +157,105 @@ export const RenameQueryModal = ({ const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { name: name ?? '', description: description ?? '' }, - values: { name: name ?? '', description: description ?? '' }, - resetOptions: { keepDirtyValues: true }, }) const { reset, formState } = form const { isDirty, isSubmitting } = formState - const handleCancel = () => { - onCancel() - reset(undefined, { keepDirtyValues: false }) - } + return ( +

+ + + ( + + + + + + )} + /> +
+ generateTitle()} + size="tiny" + disabled={ + isTitleGenerationLoading || !isApiKeySet || isHipaaProjectDisallowed || isAiOptedOut + } + tooltip={{ + content: { + side: 'bottom', + text: isHipaaProjectDisallowed + ? 'This feature is not available for HIPAA projects.' + : isAiOptedOut + ? 'Your organization has opted out of AI features.' + : isApiKeySet + ? undefined + : 'Add your "OPENAI_API_KEY" to your environment variables to use this feature.', + }, + }} + > +
+
+ +
+ Rename with Supabase AI +
+
+
+ ( + + +