Files
supabase/apps/studio/data/content/notebooks/notebook-cache.ts
Charis eabb87564b fix(studio): resolve dirty notebook save conflicts (#49540)
## Summary
- require an explicit choice before saving a notebook that diverged
while dirty
- let users save over assistant changes or discard their local edits,
with deleted notebooks recreating safely
- keep dismissals side-effect free and close deleted notebook tabs when
edits are discarded

## Testing
- pnpm --filter studio exec vitest run
components/interfaces/Explorer/__tests__/ExplorerNotebookTab.assistant-cache-invalidation.test.tsx
data/content/notebooks/notebook-cache.test.ts --reporter=dot
- pnpm --filter studio typecheck

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

- **New Features**
- Added conflict handling when server-side notebook changes overlap with
local edits.
- Users can overwrite, recreate, discard, or dismiss changes through a
confirmation dialog.
- Deleted notebooks can be recreated when saved, while discarded deleted
notebooks are automatically removed from open tabs.
  - Conflict dialogs remain open while an action is in progress.

- **Bug Fixes**
- Improved notebook cache cleanup to remove stale and unsaved notebook
data reliably.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-26 17:12:10 +08:00

49 lines
1.6 KiB
TypeScript

import type { QueryClient } from '@tanstack/react-query'
import type { Snapshot } from 'valtio'
import { contentKeys } from '@/data/content/keys'
import { notebooksState } from '@/state/notebooks/notebooks-state'
import type { StateNotebook } from '@/state/notebooks/types'
import { hasUnsavedChanges } from '@/state/sql-editor/sql-editor-lifecycle'
/**
* Whether a notebook has edits worth discarding on close: anything not yet
* saved, except a never-persisted notebook that's still empty (nothing to
* lose by closing it).
*/
export function hasDiscardableChanges(
stateNotebook: StateNotebook | Snapshot<StateNotebook> | undefined
): boolean {
if (!hasUnsavedChanges(stateNotebook?.status)) return false
const isEmptyNewNotebook =
stateNotebook?.status === 'new' && (stateNotebook.notebook.content?.cells.length ?? 0) === 0
return !isEmptyNewNotebook
}
/**
* Evicts a notebook from the React Query cache and the Valtio store together.
* Callers are responsible for confirming before discarding unsaved edits.
*
* Removes the query entry outright rather than invalidating it: a mounted
* `useNotebookQuery` observer would otherwise read the stale cached value
* synchronously, before its refetch lands, and `notebooksState.setNotebook`'s
* merge guard would treat that stale merge as already-loaded and drop the
* real update.
*
*/
export function evictNotebookFromCaches({
queryClient,
projectRef,
id,
}: {
queryClient: QueryClient
projectRef: string
id: string
}): boolean {
notebooksState.removeNotebook({ id })
queryClient.removeQueries({ queryKey: contentKeys.resource(projectRef, id) })
return true
}