Files
Joshen Lim c37e756983 Trigger update snippet when toggling favorite (#50121)
## Context

Currently in the SQL Editor, toggling "favourite" for a snippet doesn't
persist unless you manually save the snippet (which expects a change in
the snippet's content before allowing so) - which is a bit of an odd UX

This used to work before we introduced manual saving which is currently
the default behaviour for the SQL Editor - `addFavorite` and
`removeFavorite` would add to the `needsSaving` queue which the editor's
save scheduler will subscribe and trigger the save. However the save
scheduler doesn't subscribe to the queue for manual saving mode
([ref](https://github.com/supabase/supabase/blob/master/apps/studio/state/sql-editor/sql-editor-save-scheduler.ts#L90))
- hence toggling favourite on a snippet never triggers a PATCH request.

Am opting to immediately trigger a PATCH request when toggling
favourites which is a bit more of an expected UX imo

One thing to note is that favoriting a snippet essentially does a save
on the snippet - which means the contents will be persisted as well,
although i think this is alright

## To test
- [ ] Verify that toggling favourite for a SQL snippet persists
immediately
- Can verify by checking the context menu CTA to see if it's changed
from "Add to favourites" to "Remove from favourites" or vice versa

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Improvements**
  - Favoriting or unfavoriting SQL snippets now saves immediately.
- Favorite changes are handled consistently across the SQL Editor,
including the utility panel and snippet navigation.
- Pending content saves are coordinated to prevent favorite changes from
being overwritten.
- If saving a favorite fails, the previous favorite state is restored
and an error notification is shown.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-09 15:50:27 +08:00

126 lines
3.9 KiB
TypeScript

import { untrustedSql } from '@supabase/pg-meta'
import { beforeEach, describe, expect, it } from 'vitest'
import { sqlEditorState } from './sql-editor-state'
import type { SnippetWithContent } from '@/data/content/sql-folders-query'
import { untrustedLogSql } from '@/data/logs/safe-analytics-sql'
function makeLogSnippet(id: string): SnippetWithContent {
return {
id,
name: 'My Logs Query',
description: '',
visibility: 'user',
project_id: 42,
owner_id: 7,
folder_id: null,
favorite: false,
status: 'saved',
inserted_at: '2024-01-01T00:00:00.000Z',
updated_at: '2024-01-01T00:00:00.000Z',
type: 'log_sql',
content: {
content_id: id,
schema_version: '1',
unchecked_sql: untrustedLogSql('select * from logs'),
},
}
}
function makeSnippet(
id: string,
overrides: Omit<Partial<SnippetWithContent>, 'content' | 'type'> = {}
): SnippetWithContent {
return {
id,
name: 'My Query',
description: 'A description',
visibility: 'user',
project_id: 42,
owner_id: 7,
folder_id: null,
favorite: false,
status: 'saved',
inserted_at: '2024-01-01T00:00:00.000Z',
updated_at: '2024-01-01T00:00:00.000Z',
...overrides,
type: 'sql',
content: {
content_id: id,
schema_version: '1',
unchecked_sql: untrustedSql('SELECT * FROM users;'),
},
}
}
describe('addFavorite / removeFavorite', () => {
beforeEach(() => {
// sqlEditorState is a module-level singleton, so reset the state these tests touch
for (const id of Object.keys(sqlEditorState.snippets)) {
delete sqlEditorState.snippets[id]
}
sqlEditorState.needsSaving.clear()
})
it('marks a loaded snippet as favorite without queueing it for saving', () => {
sqlEditorState.addSnippet({ projectRef: 'ref', snippet: makeSnippet('snippet-1') })
sqlEditorState.addFavorite('snippet-1')
expect(sqlEditorState.snippets['snippet-1'].snippet.favorite).toBe(true)
// Favorites persist immediately via the save coordinator, not the queue.
expect(sqlEditorState.needsSaving.has('snippet-1')).toBe(false)
})
it('unmarks a favorited snippet without queueing it for saving', () => {
sqlEditorState.addSnippet({
projectRef: 'ref',
snippet: makeSnippet('snippet-1', { favorite: true }),
})
sqlEditorState.removeFavorite('snippet-1')
expect(sqlEditorState.snippets['snippet-1'].snippet.favorite).toBe(false)
expect(sqlEditorState.needsSaving.has('snippet-1')).toBe(false)
})
it('ignores addFavorite for a snippet that is not in the store', () => {
expect(() => sqlEditorState.addFavorite('missing')).not.toThrow()
expect(sqlEditorState.needsSaving.has('missing')).toBe(false)
})
it('ignores removeFavorite for a snippet that is not in the store', () => {
expect(() => sqlEditorState.removeFavorite('missing')).not.toThrow()
expect(sqlEditorState.needsSaving.has('missing')).toBe(false)
})
})
describe('setSql — source-aware branding', () => {
beforeEach(() => {
for (const id of Object.keys(sqlEditorState.snippets)) {
delete sqlEditorState.snippets[id]
}
sqlEditorState.needsSaving.clear()
})
it('updates the SQL of a database snippet and marks it for saving', () => {
sqlEditorState.addSnippet({ projectRef: 'ref', snippet: makeSnippet('db-1') })
sqlEditorState.setSql({ id: 'db-1', sql: 'select 2' })
expect(sqlEditorState.snippets['db-1'].snippet.content?.unchecked_sql).toBe('select 2')
expect(sqlEditorState.needsSaving.has('db-1')).toBe(true)
})
it('updates the SQL of a logs snippet and marks it for saving', () => {
sqlEditorState.addSnippet({ projectRef: 'ref', snippet: makeLogSnippet('logs-1') })
sqlEditorState.setSql({ id: 'logs-1', sql: 'select count(*) from logs' })
expect(sqlEditorState.snippets['logs-1'].snippet.content?.unchecked_sql).toBe(
'select count(*) from logs'
)
expect(sqlEditorState.needsSaving.has('logs-1')).toBe(true)
})
})