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] 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 | 186 +++++++++---------
.../SQLEditorNavV2/SQLEditorNav.tsx | 11 +-
3 files changed, 143 insertions(+), 97 deletions(-)
diff --git a/apps/studio/components/interfaces/SQLEditor/RenameQueryModal.test.tsx b/apps/studio/components/interfaces/SQLEditor/RenameQueryModal.test.tsx
index 1113bd5bf07..477a9229795 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 1ae91dc20c1..8041ebe9b7d 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 (
-