Files
supabase/apps/studio/data/content/content-upsert-mutation.ts
Charis d5653f1f92 refactor(studio): unify snippet save + persistence into SnippetStatus (3/9) (#47251)
## 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 -->
2026-06-24 08:56:39 -04:00

78 lines
2.5 KiB
TypeScript

import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import type { Content } from './content-query'
import { unmapSqlContentField } from './content-remap'
import { contentKeys } from './keys'
import type { Snippet, SnippetWithContent } from './sql-folders-query'
import type { components } from '@/data/api'
import { handleError, put } from '@/data/fetchers'
import type { ResponseError, UseCustomMutationOptions } from '@/types'
export type UpsertContentPayload = Omit<components['schemas']['UpsertContentBody'], 'content'> & {
id: string
content: Partial<Content['content']>
favorite?: boolean
}
export type UpsertContentVariables = {
projectRef: string
payload: UpsertContentPayload
}
export async function upsertContent(
{ projectRef, payload }: UpsertContentVariables,
signal?: AbortSignal
): Promise<SnippetWithContent | null> {
const { data, error } = await put('/platform/projects/{ref}/content', {
params: { path: { ref: projectRef } },
body: unmapSqlContentField(payload),
headers: { Version: '2' },
signal,
})
if (error) handleError(error)
const snippet = data as Snippet | null
// The upsert response is a snippet freshly persisted to the database, so it
// carries status 'saved' as it crosses into the app — same as the queries.
return snippet === null ? null : { ...snippet, status: 'saved' }
}
export type UpsertContentData = Awaited<ReturnType<typeof upsertContent>>
export const useContentUpsertMutation = ({
onError,
onSuccess,
invalidateQueriesOnSuccess = true,
...options
}: Omit<
UseCustomMutationOptions<UpsertContentData, ResponseError, UpsertContentVariables>,
'mutationFn'
> & {
invalidateQueriesOnSuccess?: boolean
} = {}) => {
const queryClient = useQueryClient()
return useMutation<UpsertContentData, ResponseError, UpsertContentVariables>({
mutationFn: (args) => upsertContent(args),
async onSuccess(data, variables, context) {
const { projectRef } = variables
if (invalidateQueriesOnSuccess) {
await Promise.all([
queryClient.invalidateQueries({ queryKey: contentKeys.allContentLists(projectRef) }),
queryClient.invalidateQueries({ queryKey: contentKeys.infiniteList(projectRef) }),
])
}
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
if (onError === undefined) {
toast.error(`Failed to insert content: ${data.message}`)
} else {
onError(data, variables, context)
}
},
...options,
})
}