mirror of
https://github.com/supabase/supabase.git
synced 2026-10-07 10:25:06 +03:00
## What PR 3 of a stacked refactor of the SQL editor snippet state. Replaces the two overlapping pieces of snippet lifecycle state — the `savingStates` map (`IDLE|UPDATING|UPDATING_FAILED`) and the `isNotSavedInDatabaseYet` boolean — with a single `SnippetStatus` enum. ## Status is attached at the data layer (never absent) - `SnippetStatus` + `SnippetWithContent` now live in `data/content`. The snippet queries attach `status: 'saved'` via a typed `withSavedStatus()` helper, and `upsertContent` returns `SnippetWithContent` so move/rename responses carry status too. - A SQL-typed `getSqlSnippetById`/`useSqlSnippetByIdQuery` returns `SnippetWithContent` (the generic `useContentIdQuery` stays for Reports, which use it). `[id].tsx` loads content with **no casting**. - `'new'` is attached on local creation (`createSqlSnippetSkeletonV2`). ## Behavior Behavior-preserving for the existing auto-save flow (faithful mapping of both old fields, including the replication-lag swallow). One incidental fix: the read-only/saving indicator now also covers a brand-new snippet's first save (previously only re-saves of persisted snippets had distinct saving/failed states in some paths). ## Tests New `sql-editor-lifecycle.test.ts` (29 tests) covering every predicate and transition; existing rules tests updated. `pnpm --filter studio typecheck` clean; 52 state/sql-editor unit tests pass. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **Refactor** * Restructured SQL snippet persistence tracking, replacing boolean flags with a comprehensive status system for clearer visibility into save progress. * Enhanced saving indicator UI to reflect accurate snippet save states. * **Tests** * Added test coverage for snippet persistence state transitions and lifecycle scenarios. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
119 lines
3.7 KiB
TypeScript
119 lines
3.7 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import {
|
|
isSaveFailed,
|
|
isSaving,
|
|
statusOnSaveError,
|
|
statusOnSaveStart,
|
|
statusOnSaveSuccess,
|
|
wasNeverPersisted,
|
|
} from './sql-editor-lifecycle'
|
|
import type { SnippetStatus } from '@/data/content/snippet-status'
|
|
|
|
const NEVER_PERSISTED: Array<SnippetStatus> = ['new', 'new_saving', 'new_save_failed']
|
|
const PERSISTED: Array<SnippetStatus> = ['saved', 'unsaved', 'saving', 'save_failed']
|
|
|
|
describe('wasNeverPersisted', () => {
|
|
it.each(NEVER_PERSISTED)('is true for never-persisted status %s', (status) => {
|
|
expect(wasNeverPersisted(status)).toBe(true)
|
|
})
|
|
|
|
it.each(PERSISTED)('is false for persisted status %s', (status) => {
|
|
expect(wasNeverPersisted(status)).toBe(false)
|
|
})
|
|
|
|
it('treats an absent status as persisted', () => {
|
|
expect(wasNeverPersisted(undefined)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('isSaving', () => {
|
|
it('is true only while a save is in flight (new or re-save)', () => {
|
|
expect(isSaving('new_saving')).toBe(true)
|
|
expect(isSaving('saving')).toBe(true)
|
|
})
|
|
|
|
it.each(['new', 'new_save_failed', 'saved', 'unsaved', 'save_failed', undefined] as const)(
|
|
'is false for %s',
|
|
(status) => {
|
|
expect(isSaving(status)).toBe(false)
|
|
}
|
|
)
|
|
})
|
|
|
|
describe('isSaveFailed', () => {
|
|
it('is true only after a failed save (new or re-save)', () => {
|
|
expect(isSaveFailed('new_save_failed')).toBe(true)
|
|
expect(isSaveFailed('save_failed')).toBe(true)
|
|
})
|
|
|
|
it.each(['new', 'new_saving', 'saved', 'unsaved', 'saving', undefined] as const)(
|
|
'is false for %s',
|
|
(status) => {
|
|
expect(isSaveFailed(status)).toBe(false)
|
|
}
|
|
)
|
|
})
|
|
|
|
describe('statusOnSaveStart', () => {
|
|
it('keeps never-persisted snippets in the new family', () => {
|
|
expect(statusOnSaveStart('new')).toBe('new_saving')
|
|
expect(statusOnSaveStart('new_save_failed')).toBe('new_saving')
|
|
})
|
|
|
|
it('moves persisted snippets to saving', () => {
|
|
expect(statusOnSaveStart('saved')).toBe('saving')
|
|
expect(statusOnSaveStart('unsaved')).toBe('saving')
|
|
expect(statusOnSaveStart('save_failed')).toBe('saving')
|
|
expect(statusOnSaveStart(undefined)).toBe('saving')
|
|
})
|
|
})
|
|
|
|
describe('statusOnSaveSuccess', () => {
|
|
it('always resolves to saved', () => {
|
|
expect(statusOnSaveSuccess()).toBe('saved')
|
|
})
|
|
})
|
|
|
|
describe('statusOnSaveError', () => {
|
|
it('keeps never-persisted snippets in the new family', () => {
|
|
expect(statusOnSaveError('new_saving')).toBe('new_save_failed')
|
|
expect(statusOnSaveError('new')).toBe('new_save_failed')
|
|
})
|
|
|
|
it('moves persisted snippets to save_failed', () => {
|
|
expect(statusOnSaveError('saving')).toBe('save_failed')
|
|
expect(statusOnSaveError('saved')).toBe('save_failed')
|
|
expect(statusOnSaveError(undefined)).toBe('save_failed')
|
|
})
|
|
})
|
|
|
|
describe('lifecycle round trips', () => {
|
|
it('new snippet: first save succeeds then a re-save succeeds', () => {
|
|
let status: SnippetStatus = 'new'
|
|
status = statusOnSaveStart(status)
|
|
expect(status).toBe('new_saving')
|
|
status = statusOnSaveSuccess()
|
|
expect(status).toBe('saved')
|
|
// re-save
|
|
status = statusOnSaveStart(status)
|
|
expect(status).toBe('saving')
|
|
status = statusOnSaveSuccess()
|
|
expect(status).toBe('saved')
|
|
})
|
|
|
|
it('new snippet: first save fails, retry succeeds (stays never-persisted until success)', () => {
|
|
let status: SnippetStatus = 'new'
|
|
status = statusOnSaveStart(status)
|
|
status = statusOnSaveError(status)
|
|
expect(status).toBe('new_save_failed')
|
|
expect(wasNeverPersisted(status)).toBe(true)
|
|
// retry
|
|
status = statusOnSaveStart(status)
|
|
expect(status).toBe('new_saving')
|
|
status = statusOnSaveSuccess()
|
|
expect(status).toBe('saved')
|
|
expect(wasNeverPersisted(status)).toBe(false)
|
|
})
|
|
})
|