mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
## What
PR 7 of the SQL editor state re-layering stack. Extracts the snippet
editing lifecycle out of `MonacoEditor` into a co-located
`useSnippetEditor` hook, and consolidates the edit debounce.
`useSnippetEditor` owns:
- creating the snippet in the store on first edit and routing to its URL
(replace vs push for a `?content=` deep link)
- writing changes back to the store via `setSql` (with
`wasNeverPersisted` → `shouldInvalidate`)
- seeding the editor from the `content` param
- the read-only determination (`canEditSnippet`)
`MonacoEditor` now consumes `{ snippet, disableEdit, handleEditorChange
}` and keeps only the editor shell + Monaco action wiring. It sheds the
`router`/`profile`/`project`/`params`/store/tabs hooks.
`handleEditorChange` was also flattened with an early return.
## Debounce consolidation
Previously there were **two 1s debounces in series**: `useSnippetEditor`
debounced editor changes before writing to the store, and the save
mechanism (`createSaveMechanism`) already debounces persistence. That
added latency (up to ~2s to save) and split the "when to persist" timing
policy across two layers — at odds with PR 5's design where the
scheduler/mechanism owns *when* and dirty state is meant to be
immediate.
This PR removes the editor-side debounce: edits write to the store
synchronously on every change, and the save mechanism's 1s debounce is
the sole throttle. Net effects:
- the store — and the snippet's dirty status — reflects the latest edit
immediately (correct for the future manual-save mode's Save button / nav
guard)
- save fires ~1s after the *last* keystroke instead of up to ~2s
- only the active snippet's own reactive consumers (a lightweight
sidebar item) re-render per keystroke; Monaco is uncontrolled
(`defaultValue`) so it is unaffected
Note: the double-debounce was legacy (the pre-refactor god store had the
same `useDebounce(value, 1000)` in MonacoEditor plus a debounced
module-load subscribe).
## Notes
- Behavior-preserving in outcome — autosave still lands ~1s after typing
stops, just with lower latency and immediate store consistency.
## Validation
- `pnpm --filter studio typecheck` ✅
- `pnpm exec vitest --run state/sql-editor/` ✅ (110 passed)
- lint ✅ (0 errors; no ratcheted-rule regressions)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* SQL editor changes now apply immediately, with unsaved status
reflected as soon as you edit.
* The editor now keeps the latest snippet details available for saving,
improving reliability when using “Save Query.”
* **Bug Fixes**
* Improved handling for creating and opening snippets from shared links
or prefilled content.
* Fixed status updates so saved snippets correctly switch to unsaved
after edits.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
99 lines
4.0 KiB
TypeScript
99 lines
4.0 KiB
TypeScript
import type { SnippetStatus } from '@/data/content/snippet-status'
|
|
|
|
/**
|
|
* True when the snippet has never been successfully written to the database.
|
|
* Gates content fetching and list invalidation, and marks locally-created
|
|
* snippets for the replication-lag 404 swallow in the `[id]` page.
|
|
*/
|
|
export function wasNeverPersisted(status: SnippetStatus | undefined): boolean {
|
|
return status === 'new' || status === 'new_saving' || status === 'new_save_failed'
|
|
}
|
|
|
|
/** True while a save request is in flight (whether first save or re-save). */
|
|
export function isSaving(status: SnippetStatus | undefined): boolean {
|
|
return status === 'new_saving' || status === 'saving'
|
|
}
|
|
|
|
/**
|
|
* True when the most recent save attempt failed (whether first save or
|
|
* re-save).
|
|
*/
|
|
export function isSaveFailed(status: SnippetStatus | undefined): boolean {
|
|
return status === 'new_save_failed' || status === 'save_failed'
|
|
}
|
|
|
|
/**
|
|
* True when the snippet holds changes that are not safely persisted: never
|
|
* saved ('new' family), a save in flight, a failed save, or pending local edits
|
|
* ('unsaved'). Used to warn before the tab is closed. Only 'saved' is clean.
|
|
*/
|
|
export function hasUnsavedChanges(status: SnippetStatus | undefined): boolean {
|
|
return status !== undefined && status !== 'saved'
|
|
}
|
|
|
|
/**
|
|
* Transition when a save request begins, preserving the never-persisted axis.
|
|
*/
|
|
export function statusOnSaveStart(status: SnippetStatus | undefined): SnippetStatus {
|
|
return wasNeverPersisted(status) ? 'new_saving' : 'saving'
|
|
}
|
|
|
|
/** Transition when a save succeeds — the snippet is now persisted and clean. */
|
|
export function statusOnSaveSuccess(): SnippetStatus {
|
|
return 'saved'
|
|
}
|
|
|
|
/** Transition when a save fails, preserving the never-persisted axis. */
|
|
export function statusOnSaveError(status: SnippetStatus | undefined): SnippetStatus {
|
|
return wasNeverPersisted(status) ? 'new_save_failed' : 'save_failed'
|
|
}
|
|
|
|
/**
|
|
* Transition when the snippet is edited locally. A persisted-and-clean snippet
|
|
* becomes 'unsaved' so its dirty state is durable immediately (rather than only
|
|
* once the debounced save begins). Every other status is already dirty or
|
|
* in-flight — the never-persisted family, a save in progress, or a failed
|
|
* save — and is left unchanged so the persistence and progress axes are
|
|
* preserved.
|
|
*/
|
|
export function statusOnEdit(status: SnippetStatus): SnippetStatus {
|
|
return status === 'saved' ? 'unsaved' : status
|
|
}
|
|
|
|
/**
|
|
* The lifecycle of a folder in the SQL editor nav, as a single set of
|
|
* mutually-exclusive states. Like SnippetStatus, this collapses two orthogonal
|
|
* axes — persistence (a locally-created placeholder vs a persisted folder) and
|
|
* progress (inline-name editing / save in flight / settled) — into one enum, so
|
|
* a folder can never be in a nonsensical combination (e.g. "new" yet "idle").
|
|
* The predicates below recover each axis.
|
|
*/
|
|
export type FolderStatus =
|
|
// Never persisted to the database (a locally-created placeholder):
|
|
| 'new_editing' // its name is being entered inline
|
|
| 'new_saving' // its create is in flight
|
|
// Persisted to the database:
|
|
| 'idle' // settled
|
|
| 'editing' // its name is being edited inline (rename)
|
|
| 'saving' // its rename is in flight
|
|
|
|
/** True for a locally-created placeholder folder that has not been persisted. */
|
|
export function isNewFolder(status: FolderStatus | undefined): boolean {
|
|
return status === 'new_editing' || status === 'new_saving'
|
|
}
|
|
|
|
/** True while the folder's name is being edited inline. */
|
|
export function isFolderEditing(status: FolderStatus | undefined): boolean {
|
|
return status === 'new_editing' || status === 'editing'
|
|
}
|
|
|
|
/** True while a folder create/rename is in flight. */
|
|
export function isFolderSaving(status: FolderStatus | undefined): boolean {
|
|
return status === 'new_saving' || status === 'saving'
|
|
}
|
|
|
|
/** Transition when a folder save begins, preserving the never-persisted axis. */
|
|
export function folderStatusOnSaveStart(status: FolderStatus): FolderStatus {
|
|
return isNewFolder(status) ? 'new_saving' : 'saving'
|
|
}
|