mirror of
https://github.com/supabase/supabase.git
synced 2026-10-09 19:35:06 +03:00
tab states
This commit is contained in:
1 parent
28bf02b248
commit
f86e37af23
23 files changed
+807
-210
No files matched your search
@@ -97,7 +97,7 @@ export const RouteValidationWrapper = ({ children }: PropsWithChildren<{}>) => {
|
||||
if (ref !== undefined && id !== undefined) {
|
||||
if (router.pathname.endsWith('/sql/[id]') && id !== 'new') {
|
||||
const snippet = getSqlEditorV2StateSnapshot().snippets[id]?.snippet
|
||||
if (!snippet?.isNotSavedInDatabaseYet) {
|
||||
if (!snippet?.isDraftTab && !snippet?.isNotSavedInDatabaseYet) {
|
||||
setLastVisitedSnippet(id)
|
||||
}
|
||||
} else if (router.pathname.endsWith('/editor/[id]')) {
|
||||
|
||||
@@ -214,8 +214,11 @@ const MonacoEditor = ({
|
||||
|
||||
function handleEditorChange(value: string | undefined) {
|
||||
tabsSnap.makeActiveTabPermanent()
|
||||
if (id && value) {
|
||||
if (id && value !== undefined) {
|
||||
setValue(value)
|
||||
if (snippet) {
|
||||
snapV2.setSql({ id, sql: value, skipSave: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ import {
|
||||
import { SqlEditorPanelHeader } from './SqlEditorPanelHeader'
|
||||
import { SqlEditorShowSqlToggle } from './SqlEditorShowSqlToggle'
|
||||
import { getSnippetSqlFromContent } from './sqlSnippet.utils'
|
||||
import { useCreateDraftSqlTab } from './useCreateDraftSqlTab'
|
||||
import { useSaveSqlSnippet } from './useSaveSqlSnippet'
|
||||
import { useSqlEditorCompletion } from './useSqlEditorCompletion'
|
||||
import { UtilityActions } from './UtilityPanel/UtilityActions'
|
||||
@@ -85,7 +86,6 @@ import { useExecuteSqlMutation } from '@/data/sql/execute-sql-mutation'
|
||||
import { isError } from '@/data/utils/error-check'
|
||||
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
import { generateUuid } from '@/lib/api/snippets.browser'
|
||||
import { BASE_PATH } from '@/lib/constants'
|
||||
import { formatSql } from '@/lib/formatSql'
|
||||
import { detectOS } from '@/lib/helpers'
|
||||
@@ -171,13 +171,11 @@ export const SQLEditor = () => {
|
||||
registerInCommandMenu: true,
|
||||
})
|
||||
|
||||
const { createDraftTab } = useCreateDraftSqlTab()
|
||||
|
||||
const openNewSnippet = useCallback(() => {
|
||||
if (!ref) return
|
||||
// skip=true bypasses the "load last visited snippet" redirect on /sql/new.
|
||||
// Without it, the effect in pages/project/[ref]/sql/[id].tsx bounces back
|
||||
// to the previous snippet.
|
||||
router.push(`/project/${ref}/sql/new?skip=true`)
|
||||
}, [ref, router])
|
||||
createDraftTab()
|
||||
}, [createDraftTab])
|
||||
|
||||
useShortcut(SHORTCUT_IDS.SQL_EDITOR_NEW_SNIPPET, openNewSnippet, {
|
||||
registerInCommandMenu: true,
|
||||
@@ -194,27 +192,22 @@ export const SQLEditor = () => {
|
||||
refocusEditor()
|
||||
}, [refocusEditor])
|
||||
|
||||
// generate a new snippet title and an id to be used for new snippets. The dependency on urlId is to avoid a bug which
|
||||
// shows up when clicking on the SQL Editor while being in the SQL editor on a random snippet.
|
||||
const [generatedNewSnippetName, generatedId] = useMemo(() => {
|
||||
const name = generateSnippetTitle()
|
||||
return [name, generateUuid([`${name}.sql`])]
|
||||
}, [urlId])
|
||||
|
||||
// the id is stable across renders - it depends either on the url or on the memoized generated id
|
||||
const effectiveUrlId = notebookEditorContext?.blockId ?? urlId
|
||||
const id = !effectiveUrlId || effectiveUrlId === 'new' ? generatedId : effectiveUrlId
|
||||
const id = effectiveUrlId === 'new' ? '' : (effectiveUrlId ?? '')
|
||||
|
||||
if (!id) {
|
||||
return null
|
||||
}
|
||||
|
||||
const limit = snapV2.limit
|
||||
const results = snapV2.results[id]?.[0]
|
||||
const results = id ? snapV2.results[id]?.[0] : undefined
|
||||
const snippetIsLoading = !(
|
||||
id in snapV2.snippets && snapV2.snippets[id].snippet.content !== undefined
|
||||
id &&
|
||||
id in snapV2.snippets &&
|
||||
snapV2.snippets[id].snippet.content !== undefined
|
||||
)
|
||||
const isLoading = effectiveUrlId === 'new' ? false : snippetIsLoading
|
||||
const snippetDisplayTitle =
|
||||
effectiveUrlId === 'new'
|
||||
? generatedNewSnippetName
|
||||
: (snapV2.snippets[id]?.snippet.name ?? generatedNewSnippetName)
|
||||
const isLoading = !id || snippetIsLoading
|
||||
const snippetDisplayTitle = snapV2.snippets[id]?.snippet.name ?? generateSnippetTitle()
|
||||
|
||||
useSqlEditorCompletion(id, monacoRef.current)
|
||||
|
||||
@@ -378,7 +371,6 @@ export const SQLEditor = () => {
|
||||
id,
|
||||
snippetName: snippetDisplayTitle,
|
||||
getEditorSql,
|
||||
isOnNewRoute: urlId === 'new',
|
||||
})
|
||||
|
||||
useShortcut(SHORTCUT_IDS.SQL_EDITOR_SAVE, () => void saveQuery(), {
|
||||
@@ -1103,11 +1095,7 @@ export const SQLEditor = () => {
|
||||
: ''
|
||||
}
|
||||
id={id}
|
||||
snippetName={
|
||||
urlId === 'new'
|
||||
? generatedNewSnippetName
|
||||
: (snapV2.snippets[id]?.snippet.name ?? generatedNewSnippetName)
|
||||
}
|
||||
snippetName={snippetDisplayTitle}
|
||||
className={cn(isDiffOpen && 'hidden')}
|
||||
editorRef={editorRef}
|
||||
monacoRef={monacoRef}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Popover, PopoverContent, PopoverTrigger, ToggleGroup, ToggleGroupItem }
|
||||
import { ChartConfig, ChartSettings } from './ChartConfig'
|
||||
import { UtilityTabLogsResults } from './UtilityTabLogsResults'
|
||||
import { UtilityTabResults } from './UtilityTabResults'
|
||||
import { isDraftSqlSnippet } from '@/components/interfaces/SQLEditor/createDraftSqlTab'
|
||||
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
|
||||
import { DownloadResultsButton } from '@/components/ui/DownloadResultsButton'
|
||||
import { useContentUpsertMutation } from '@/data/content/content-upsert-mutation'
|
||||
@@ -76,7 +77,11 @@ export const UtilityPanel = ({
|
||||
},
|
||||
}
|
||||
|
||||
snapV2.updateSnippet({ id, snippet: newSnippet as unknown as Snippet })
|
||||
snapV2.updateSnippet({
|
||||
id,
|
||||
snippet: newSnippet as unknown as Snippet,
|
||||
skipSave: isDraftSqlSnippet(snippet),
|
||||
})
|
||||
},
|
||||
onError: async (_err, _newContent, _context) => {
|
||||
toast.error(`Failed to update chart. Please try again.`)
|
||||
@@ -122,6 +127,19 @@ export const UtilityPanel = ({
|
||||
}
|
||||
|
||||
if (!ref || !snippet?.id) return
|
||||
if (isDraftSqlSnippet(snippet)) {
|
||||
snapV2.updateSnippet({
|
||||
id,
|
||||
snippet: {
|
||||
content: {
|
||||
...snippet.content,
|
||||
chart: config,
|
||||
},
|
||||
} as unknown as Snippet,
|
||||
skipSave: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
upsertContent({
|
||||
projectRef: ref,
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
import type { NextRouter } from 'next/router'
|
||||
|
||||
import {
|
||||
persistDraftSqlTab,
|
||||
readPersistedDraftSqlTab,
|
||||
removePersistedDraftSqlTab,
|
||||
} from './draftSqlTabStorage.utils'
|
||||
import { generateSnippetTitle } from './SQLEditor.constants'
|
||||
import { createSqlSnippetSkeletonV2 } from './SQLEditor.utils'
|
||||
import { generateUuid } from '@/lib/api/snippets.browser'
|
||||
import type { QueryExecutionSource } from '@/state/query-execution-source'
|
||||
import type { SnippetWithContent } from '@/state/sql-editor-v2'
|
||||
import { createTabId } from '@/state/tabs'
|
||||
|
||||
type SqlEditorV2Actions = {
|
||||
addSnippet: (args: { projectRef: string; snippet: SnippetWithContent }) => void
|
||||
}
|
||||
|
||||
type TabsActions = {
|
||||
addTab: (tab: {
|
||||
id: string
|
||||
type: 'sql'
|
||||
label?: string
|
||||
metadata?: {
|
||||
sqlId?: string
|
||||
name?: string
|
||||
isDraft?: boolean
|
||||
}
|
||||
isPreview?: boolean
|
||||
}) => void
|
||||
}
|
||||
|
||||
export type CreateDraftSqlTabParams = {
|
||||
projectRef: string
|
||||
projectId: number
|
||||
ownerId: number
|
||||
snapV2: SqlEditorV2Actions
|
||||
tabs: TabsActions
|
||||
router?: NextRouter
|
||||
initialSql?: string
|
||||
querySource?: QueryExecutionSource
|
||||
/** When true, only registers state without changing the route */
|
||||
skipNavigation?: boolean
|
||||
}
|
||||
|
||||
export function createDraftSqlTab({
|
||||
projectRef,
|
||||
projectId,
|
||||
ownerId,
|
||||
snapV2,
|
||||
tabs,
|
||||
router,
|
||||
initialSql = '',
|
||||
querySource = 'database',
|
||||
skipNavigation = false,
|
||||
}: CreateDraftSqlTabParams): string {
|
||||
const name = generateSnippetTitle()
|
||||
const draftId = generateUuid([`${name}.sql`, Date.now().toString()])
|
||||
|
||||
const snippet = createSqlSnippetSkeletonV2({
|
||||
idOverride: draftId,
|
||||
name,
|
||||
sql: initialSql,
|
||||
owner_id: ownerId,
|
||||
project_id: projectId,
|
||||
querySource,
|
||||
})
|
||||
|
||||
snippet.isDraftTab = true
|
||||
|
||||
snapV2.addSnippet({ projectRef, snippet })
|
||||
|
||||
persistDraftSqlTab(projectRef, draftId, {
|
||||
sql: initialSql,
|
||||
name,
|
||||
querySource,
|
||||
})
|
||||
|
||||
tabs.addTab({
|
||||
id: createTabId('sql', { id: draftId }),
|
||||
type: 'sql',
|
||||
label: name,
|
||||
metadata: {
|
||||
sqlId: draftId,
|
||||
name,
|
||||
isDraft: true,
|
||||
},
|
||||
})
|
||||
|
||||
if (!skipNavigation && router) {
|
||||
void router.push(`/project/${projectRef}/sql/${draftId}`)
|
||||
}
|
||||
|
||||
return draftId
|
||||
}
|
||||
|
||||
export function restoreDraftSqlTab({
|
||||
draftId,
|
||||
projectRef,
|
||||
projectId,
|
||||
ownerId,
|
||||
snapV2,
|
||||
name = generateSnippetTitle(),
|
||||
initialSql = '',
|
||||
querySource = 'database',
|
||||
}: Omit<CreateDraftSqlTabParams, 'tabs' | 'router' | 'skipNavigation'> & {
|
||||
draftId: string
|
||||
name?: string
|
||||
initialSql?: string
|
||||
}) {
|
||||
const persisted = readPersistedDraftSqlTab(projectRef, draftId)
|
||||
const resolvedName = persisted?.name ?? name
|
||||
const resolvedSql = persisted?.sql ?? initialSql
|
||||
const resolvedQuerySource = persisted?.querySource ?? querySource
|
||||
|
||||
const snippet = createSqlSnippetSkeletonV2({
|
||||
idOverride: draftId,
|
||||
name: resolvedName,
|
||||
sql: resolvedSql,
|
||||
owner_id: ownerId,
|
||||
project_id: projectId,
|
||||
querySource: resolvedQuerySource,
|
||||
})
|
||||
|
||||
snippet.isDraftTab = true
|
||||
snapV2.addSnippet({ projectRef, snippet })
|
||||
}
|
||||
|
||||
export function clearPersistedDraftSqlTab(projectRef: string, draftId: string) {
|
||||
removePersistedDraftSqlTab(projectRef, draftId)
|
||||
}
|
||||
|
||||
export function restoreOpenDraftSqlTabs({
|
||||
projectRef,
|
||||
projectId,
|
||||
ownerId,
|
||||
snapV2,
|
||||
openTabs,
|
||||
tabsMap,
|
||||
existingSnippetIds,
|
||||
}: {
|
||||
projectRef: string
|
||||
projectId: number
|
||||
ownerId: number
|
||||
snapV2: SqlEditorV2Actions
|
||||
openTabs: string[]
|
||||
tabsMap: Record<string, { type: string; metadata?: { sqlId?: string; isDraft?: boolean } }>
|
||||
existingSnippetIds: Set<string>
|
||||
}) {
|
||||
const openDraftIds: string[] = []
|
||||
|
||||
for (const tabId of openTabs) {
|
||||
const tab = tabsMap[tabId]
|
||||
if (tab?.type !== 'sql' || !tab.metadata?.isDraft || !tab.metadata.sqlId) {
|
||||
continue
|
||||
}
|
||||
|
||||
openDraftIds.push(tab.metadata.sqlId)
|
||||
|
||||
if (existingSnippetIds.has(tab.metadata.sqlId)) {
|
||||
continue
|
||||
}
|
||||
|
||||
restoreDraftSqlTab({
|
||||
draftId: tab.metadata.sqlId,
|
||||
projectRef,
|
||||
projectId,
|
||||
ownerId,
|
||||
snapV2,
|
||||
name: tab.metadata.name,
|
||||
})
|
||||
}
|
||||
|
||||
return openDraftIds
|
||||
}
|
||||
|
||||
export function isDraftSqlSnippet(
|
||||
snippet: Pick<SnippetWithContent, 'isDraftTab' | 'isNotSavedInDatabaseYet'> | undefined
|
||||
) {
|
||||
return snippet?.isDraftTab === true
|
||||
}
|
||||
|
||||
export function getOpenDraftSqlTabIds(
|
||||
openTabs: string[],
|
||||
tabsMap: Record<string, { type?: string; metadata?: { sqlId?: string; isDraft?: boolean } }>
|
||||
) {
|
||||
const ids = new Set<string>()
|
||||
|
||||
for (const tabId of openTabs) {
|
||||
const tab = tabsMap[tabId]
|
||||
if (tab?.type === 'sql' && tab.metadata?.isDraft && tab.metadata.sqlId) {
|
||||
ids.add(tab.metadata.sqlId)
|
||||
}
|
||||
}
|
||||
|
||||
return ids
|
||||
}
|
||||
|
||||
export function shouldHideDraftSqlTabFromNav(
|
||||
snippetId: string,
|
||||
openDraftIds: Set<string>,
|
||||
snippet?: Pick<SnippetWithContent, 'isDraftTab' | 'isNotSavedInDatabaseYet'>
|
||||
) {
|
||||
if (openDraftIds.has(snippetId)) return true
|
||||
if (isDraftSqlSnippet(snippet)) return true
|
||||
if (snippet?.isNotSavedInDatabaseYet) return true
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
getDraftSqlTabStorageKey,
|
||||
persistDraftSqlTab,
|
||||
prunePersistedDraftSqlTabs,
|
||||
readPersistedDraftSqlTab,
|
||||
removePersistedDraftSqlTab,
|
||||
} from './draftSqlTabStorage.utils'
|
||||
|
||||
const PROJECT_REF = 'test-project'
|
||||
|
||||
describe('draftSqlTabStorage', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
vi.stubGlobal('window', globalThis.window)
|
||||
})
|
||||
|
||||
it('persists and reads draft tab sql', () => {
|
||||
persistDraftSqlTab(PROJECT_REF, 'draft-1', {
|
||||
sql: 'select 1',
|
||||
name: 'Untitled query',
|
||||
})
|
||||
|
||||
expect(readPersistedDraftSqlTab(PROJECT_REF, 'draft-1')).toMatchObject({
|
||||
sql: 'select 1',
|
||||
name: 'Untitled query',
|
||||
querySource: 'database',
|
||||
})
|
||||
})
|
||||
|
||||
it('merges partial updates without clearing sql', () => {
|
||||
persistDraftSqlTab(PROJECT_REF, 'draft-1', {
|
||||
sql: 'select 1',
|
||||
name: 'Untitled query',
|
||||
})
|
||||
|
||||
persistDraftSqlTab(PROJECT_REF, 'draft-1', {
|
||||
name: 'Untitled query 2',
|
||||
})
|
||||
|
||||
expect(readPersistedDraftSqlTab(PROJECT_REF, 'draft-1')).toMatchObject({
|
||||
sql: 'select 1',
|
||||
name: 'Untitled query 2',
|
||||
})
|
||||
})
|
||||
|
||||
it('removes a persisted draft tab', () => {
|
||||
persistDraftSqlTab(PROJECT_REF, 'draft-1', {
|
||||
sql: 'select 1',
|
||||
name: 'Untitled query',
|
||||
})
|
||||
|
||||
removePersistedDraftSqlTab(PROJECT_REF, 'draft-1')
|
||||
|
||||
expect(readPersistedDraftSqlTab(PROJECT_REF, 'draft-1')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('prunes drafts that are no longer open', () => {
|
||||
persistDraftSqlTab(PROJECT_REF, 'draft-1', { sql: 'select 1', name: 'One' })
|
||||
persistDraftSqlTab(PROJECT_REF, 'draft-2', { sql: 'select 2', name: 'Two' })
|
||||
|
||||
prunePersistedDraftSqlTabs(PROJECT_REF, ['draft-2'])
|
||||
|
||||
expect(readPersistedDraftSqlTab(PROJECT_REF, 'draft-1')).toBeUndefined()
|
||||
expect(readPersistedDraftSqlTab(PROJECT_REF, 'draft-2')).toBeDefined()
|
||||
})
|
||||
|
||||
it('uses a project-scoped storage key', () => {
|
||||
expect(getDraftSqlTabStorageKey(PROJECT_REF)).toBe(`sql-editor-draft-tabs-${PROJECT_REF}`)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,94 @@
|
||||
import { LOCAL_STORAGE_KEYS } from 'common'
|
||||
|
||||
import type { QueryExecutionSource } from '@/state/query-execution-source'
|
||||
|
||||
export type PersistedDraftSqlTab = {
|
||||
sql: string
|
||||
name: string
|
||||
querySource: QueryExecutionSource
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
export type DraftSqlTabStorage = Record<string, PersistedDraftSqlTab>
|
||||
|
||||
export function getDraftSqlTabStorageKey(projectRef: string) {
|
||||
return LOCAL_STORAGE_KEYS.SQL_EDITOR_DRAFT_TABS(projectRef)
|
||||
}
|
||||
|
||||
export function readDraftSqlTabStorage(projectRef: string): DraftSqlTabStorage {
|
||||
if (typeof window === 'undefined' || !projectRef) return {}
|
||||
|
||||
try {
|
||||
const raw = localStorage.getItem(getDraftSqlTabStorageKey(projectRef))
|
||||
if (!raw) return {}
|
||||
|
||||
const parsed = JSON.parse(raw) as unknown
|
||||
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return parsed as DraftSqlTabStorage
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export function readPersistedDraftSqlTab(
|
||||
projectRef: string,
|
||||
draftId: string
|
||||
): PersistedDraftSqlTab | undefined {
|
||||
return readDraftSqlTabStorage(projectRef)[draftId]
|
||||
}
|
||||
|
||||
export function persistDraftSqlTab(
|
||||
projectRef: string,
|
||||
draftId: string,
|
||||
patch: {
|
||||
sql?: string
|
||||
name: string
|
||||
querySource?: QueryExecutionSource
|
||||
}
|
||||
) {
|
||||
if (typeof window === 'undefined' || !projectRef) return
|
||||
|
||||
const storage = readDraftSqlTabStorage(projectRef)
|
||||
const existing = storage[draftId]
|
||||
|
||||
storage[draftId] = {
|
||||
sql: patch.sql ?? existing?.sql ?? '',
|
||||
name: patch.name,
|
||||
querySource: patch.querySource ?? existing?.querySource ?? 'database',
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
|
||||
localStorage.setItem(getDraftSqlTabStorageKey(projectRef), JSON.stringify(storage))
|
||||
}
|
||||
|
||||
export function removePersistedDraftSqlTab(projectRef: string, draftId: string) {
|
||||
if (typeof window === 'undefined' || !projectRef) return
|
||||
|
||||
const storage = readDraftSqlTabStorage(projectRef)
|
||||
if (!(draftId in storage)) return
|
||||
|
||||
delete storage[draftId]
|
||||
localStorage.setItem(getDraftSqlTabStorageKey(projectRef), JSON.stringify(storage))
|
||||
}
|
||||
|
||||
export function prunePersistedDraftSqlTabs(projectRef: string, openDraftIds: string[]) {
|
||||
if (typeof window === 'undefined' || !projectRef) return
|
||||
|
||||
const storage = readDraftSqlTabStorage(projectRef)
|
||||
const openDraftIdSet = new Set(openDraftIds)
|
||||
let hasChanges = false
|
||||
|
||||
for (const draftId of Object.keys(storage)) {
|
||||
if (!openDraftIdSet.has(draftId)) {
|
||||
delete storage[draftId]
|
||||
hasChanges = true
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
localStorage.setItem(getDraftSqlTabStorageKey(projectRef), JSON.stringify(storage))
|
||||
}
|
||||
}
|
||||
@@ -99,6 +99,31 @@ describe('saveSqlSnippet', () => {
|
||||
expect(onTabLabelUpdate).toHaveBeenCalledWith('Active users')
|
||||
})
|
||||
|
||||
it('clears draft tab flag when saving a draft snippet', async () => {
|
||||
const updateSnippet = vi.fn()
|
||||
const onDraftSaved = vi.fn()
|
||||
|
||||
await saveSqlSnippet({
|
||||
...baseParams,
|
||||
updateSnippet,
|
||||
onDraftSaved,
|
||||
snippet: {
|
||||
id: 'snippet-id',
|
||||
name: untitledSnippetTitle,
|
||||
visibility: 'user',
|
||||
isNotSavedInDatabaseYet: true,
|
||||
isDraftTab: true,
|
||||
} as any,
|
||||
})
|
||||
|
||||
expect(updateSnippet).toHaveBeenCalledWith({
|
||||
id: 'snippet-id',
|
||||
snippet: { isDraftTab: false },
|
||||
skipSave: true,
|
||||
})
|
||||
expect(onDraftSaved).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('skips AI rename when snippet already has a custom name', async () => {
|
||||
const generateSqlTitle = vi.fn()
|
||||
|
||||
|
||||
@@ -23,8 +23,13 @@ export type SaveSqlSnippetParams = {
|
||||
}) => void
|
||||
addNeedsSaving: (id: string) => void
|
||||
generateSqlTitle: (args: { sql: string }) => Promise<{ title: string }>
|
||||
updateSnippet: (args: { id: string; snippet: { name: string } }) => void
|
||||
updateSnippet: (args: {
|
||||
id: string
|
||||
snippet: { name?: string; isDraftTab?: boolean }
|
||||
skipSave?: boolean
|
||||
}) => void
|
||||
onTabLabelUpdate?: (name: string) => void
|
||||
onDraftSaved?: () => void
|
||||
onNavigateToSnippet?: () => void
|
||||
}
|
||||
|
||||
@@ -44,6 +49,7 @@ export async function saveSqlSnippet({
|
||||
generateSqlTitle,
|
||||
updateSnippet,
|
||||
onTabLabelUpdate,
|
||||
onDraftSaved,
|
||||
onNavigateToSnippet,
|
||||
}: SaveSqlSnippetParams): Promise<{ saved: boolean; reason?: 'empty' }> {
|
||||
const trimmedSql = sql.trim()
|
||||
@@ -68,6 +74,13 @@ export async function saveSqlSnippet({
|
||||
}
|
||||
|
||||
const shouldInvalidate = currentSnippet.isNotSavedInDatabaseYet
|
||||
|
||||
if (currentSnippet.isDraftTab) {
|
||||
updateSnippet({ id, snippet: { isDraftTab: false }, skipSave: true })
|
||||
currentSnippet = { ...currentSnippet, isDraftTab: false }
|
||||
onDraftSaved?.()
|
||||
}
|
||||
|
||||
setSql({ id, sql: trimmedSql, shouldInvalidate })
|
||||
addNeedsSaving(id)
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { useParams } from 'common'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
import { createDraftSqlTab } from './createDraftSqlTab'
|
||||
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
import { useProfile } from '@/lib/profile'
|
||||
import type { QueryExecutionSource } from '@/state/query-execution-source'
|
||||
import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor-v2'
|
||||
import { useTabsStateSnapshot } from '@/state/tabs'
|
||||
|
||||
export function useCreateDraftSqlTab() {
|
||||
const router = useRouter()
|
||||
const { ref: projectRef } = useParams()
|
||||
const { profile } = useProfile()
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const snapV2 = useSqlEditorV2StateSnapshot()
|
||||
const tabs = useTabsStateSnapshot()
|
||||
|
||||
const { can: canCreateSQLSnippet } = useAsyncCheckPermissions(
|
||||
PermissionAction.CREATE,
|
||||
'user_content',
|
||||
{
|
||||
resource: { type: 'sql', owner_id: profile?.id },
|
||||
subject: { id: profile?.id },
|
||||
}
|
||||
)
|
||||
|
||||
const createDraftTab = useCallback(
|
||||
(options?: { initialSql?: string; querySource?: QueryExecutionSource }) => {
|
||||
if (!projectRef) return console.error('Project ref is required')
|
||||
if (!project) return console.error('Project is required')
|
||||
if (!profile) return console.error('Profile is required')
|
||||
if (!canCreateSQLSnippet) return
|
||||
|
||||
return createDraftSqlTab({
|
||||
projectRef,
|
||||
projectId: project.id,
|
||||
ownerId: profile.id,
|
||||
snapV2,
|
||||
tabs,
|
||||
router,
|
||||
initialSql: options?.initialSql,
|
||||
querySource: options?.querySource,
|
||||
})
|
||||
},
|
||||
[canCreateSQLSnippet, profile, project, projectRef, router, snapV2, tabs]
|
||||
)
|
||||
|
||||
return { createDraftTab, canCreateSQLSnippet }
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useParams } from 'common'
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
import { restoreOpenDraftSqlTabs } from './createDraftSqlTab'
|
||||
import { prunePersistedDraftSqlTabs } from './draftSqlTabStorage.utils'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
import { useProfile } from '@/lib/profile'
|
||||
import { getSqlEditorV2StateSnapshot, useSqlEditorV2StateSnapshot } from '@/state/sql-editor-v2'
|
||||
import { useTabsStateSnapshot } from '@/state/tabs'
|
||||
|
||||
export function useRestorePersistedDraftSqlTabs() {
|
||||
const { ref: projectRef } = useParams()
|
||||
const { profile } = useProfile()
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const snapV2 = useSqlEditorV2StateSnapshot()
|
||||
const tabs = useTabsStateSnapshot()
|
||||
const hasRestoredRef = useRef<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!projectRef || !project || !profile) return
|
||||
if (hasRestoredRef.current === projectRef) return
|
||||
|
||||
hasRestoredRef.current = projectRef
|
||||
|
||||
const openDraftIds = restoreOpenDraftSqlTabs({
|
||||
projectRef,
|
||||
projectId: project.id,
|
||||
ownerId: profile.id,
|
||||
snapV2,
|
||||
openTabs: tabs.openTabs,
|
||||
tabsMap: tabs.tabsMap,
|
||||
existingSnippetIds: new Set(Object.keys(getSqlEditorV2StateSnapshot().snippets)),
|
||||
})
|
||||
|
||||
prunePersistedDraftSqlTabs(projectRef, openDraftIds)
|
||||
}, [profile, project, projectRef, snapV2, tabs.openTabs, tabs.tabsMap])
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { useParams } from 'common'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useCallback } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { clearPersistedDraftSqlTab } from './createDraftSqlTab'
|
||||
import { saveSqlSnippet } from './saveSqlSnippet.utils'
|
||||
import { useSqlTitleGenerateMutation } from '@/data/ai/sql-title-mutation'
|
||||
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
||||
@@ -18,16 +18,9 @@ export type UseSaveSqlSnippetOptions = {
|
||||
id: string
|
||||
snippetName: string
|
||||
getEditorSql: () => string | undefined
|
||||
isOnNewRoute?: boolean
|
||||
}
|
||||
|
||||
export function useSaveSqlSnippet({
|
||||
id,
|
||||
snippetName,
|
||||
getEditorSql,
|
||||
isOnNewRoute = false,
|
||||
}: UseSaveSqlSnippetOptions) {
|
||||
const router = useRouter()
|
||||
export function useSaveSqlSnippet({ id, snippetName, getEditorSql }: UseSaveSqlSnippetOptions) {
|
||||
const { ref: projectRef } = useParams()
|
||||
const { profile } = useProfile()
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
@@ -52,9 +45,8 @@ export function useSaveSqlSnippet({
|
||||
if (!profile) return console.error('Profile is required')
|
||||
|
||||
const state = getSqlEditorV2StateSnapshot()
|
||||
const snippet = state.snippets[id]
|
||||
const isReadOnly =
|
||||
snippet?.snippet.visibility === 'project' && snippet?.snippet.owner_id !== profile.id
|
||||
const snippet = state.snippets[id]?.snippet
|
||||
const isReadOnly = snippet?.visibility === 'project' && snippet?.owner_id !== profile.id
|
||||
|
||||
if (isReadOnly) return
|
||||
|
||||
@@ -82,11 +74,13 @@ export function useSaveSqlSnippet({
|
||||
const tabId = createTabId('sql', { id })
|
||||
tabs.updateTab(tabId, { label: name })
|
||||
},
|
||||
onNavigateToSnippet: isOnNewRoute
|
||||
? () => {
|
||||
void router.push(`/project/${projectRef}/sql/${id}`, undefined, { shallow: true })
|
||||
}
|
||||
: undefined,
|
||||
onDraftSaved: () => {
|
||||
const tabId = createTabId('sql', { id })
|
||||
tabs.updateTab(tabId, { metadata: { isDraft: false } })
|
||||
if (projectRef) {
|
||||
clearPersistedDraftSqlTab(projectRef, id)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
if (result.reason === 'empty') {
|
||||
@@ -98,12 +92,10 @@ export function useSaveSqlSnippet({
|
||||
getEditorSql,
|
||||
id,
|
||||
isHipaaProjectDisallowed,
|
||||
isOnNewRoute,
|
||||
profile,
|
||||
project,
|
||||
projectRef,
|
||||
querySourceState.executionSource,
|
||||
router,
|
||||
snippetName,
|
||||
snapV2.addNeedsSaving,
|
||||
snapV2.addSnippet,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
import { OngoingQueriesPanel } from '@/components/interfaces/SQLEditor/OngoingQueriesPanel'
|
||||
import { useRestorePersistedDraftSqlTabs } from '@/components/interfaces/SQLEditor/useRestorePersistedDraftSqlTabs'
|
||||
import { withAuth } from '@/hooks/misc/withAuth'
|
||||
|
||||
export interface SQLEditorLayoutProps {
|
||||
@@ -8,6 +9,8 @@ export interface SQLEditorLayoutProps {
|
||||
}
|
||||
|
||||
const SQLEditorLayout = ({ children }: SQLEditorLayoutProps) => {
|
||||
useRestorePersistedDraftSqlTabs()
|
||||
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { keepPreviousData } from '@tanstack/react-query'
|
||||
import { IS_PLATFORM, LOCAL_STORAGE_KEYS, useParams } from 'common'
|
||||
import { Heart } from 'lucide-react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
InnerSideBarEmptyPanel,
|
||||
@@ -29,6 +29,10 @@ import { SqlEditorNavFolder } from './SqlEditorNavFolder'
|
||||
import { SQLEditorSectionActions } from './SQLEditorSectionActions'
|
||||
import { UnshareSnippetModal } from './UnshareSnippetModal'
|
||||
import { useSqlEditorCreateActions } from './useSqlEditorCreateActions'
|
||||
import {
|
||||
getOpenDraftSqlTabIds,
|
||||
shouldHideDraftSqlTabFromNav,
|
||||
} from '@/components/interfaces/SQLEditor/createDraftSqlTab'
|
||||
import { DownloadSnippetModal } from '@/components/interfaces/SQLEditor/DownloadSnippetModal'
|
||||
import { MoveQueryModal } from '@/components/interfaces/SQLEditor/MoveQueryModal'
|
||||
import { RenameQueryModal } from '@/components/interfaces/SQLEditor/RenameQueryModal'
|
||||
@@ -142,6 +146,17 @@ export const SQLEditorNav = ({ sort = 'inserted_at' }: SQLEditorNavProps) => {
|
||||
|
||||
const snippet = snapV2.snippets[id as string]?.snippet
|
||||
|
||||
const openDraftSqlTabIds = useMemo(
|
||||
() => getOpenDraftSqlTabIds(tabs.openTabs, tabs.tabsMap),
|
||||
[tabs.openTabs, tabs.tabsMap]
|
||||
)
|
||||
|
||||
const isHiddenDraftSqlTab = useCallback(
|
||||
(snippetId: string, snippetToCheck?: SnippetWithContent) =>
|
||||
shouldHideDraftSqlTabFromNav(snippetId, openDraftSqlTabIds, snippetToCheck),
|
||||
[openDraftSqlTabIds]
|
||||
)
|
||||
|
||||
// ==========================
|
||||
// Private snippets & folders
|
||||
// ==========================
|
||||
@@ -195,7 +210,7 @@ export const SQLEditorNav = ({ sort = 'inserted_at' }: SQLEditorNavProps) => {
|
||||
if (
|
||||
snippet &&
|
||||
snippet.visibility === 'user' &&
|
||||
!snippet.isNotSavedInDatabaseYet &&
|
||||
!isHiddenDraftSqlTab(snippet.id, snippet) &&
|
||||
!snippetInfo.snippetIds.has(snippet.id)
|
||||
) {
|
||||
snippetInfo.snippetIds.add(snippet.id)
|
||||
@@ -203,11 +218,21 @@ export const SQLEditorNav = ({ sort = 'inserted_at' }: SQLEditorNavProps) => {
|
||||
}
|
||||
|
||||
return snippetInfo
|
||||
}, [privateSnippetsPages?.pages, subResults, isLoading, isPlaceholderData, isFetching, snippet])
|
||||
}, [
|
||||
privateSnippetsPages?.pages,
|
||||
subResults,
|
||||
isLoading,
|
||||
isPlaceholderData,
|
||||
isFetching,
|
||||
snippet,
|
||||
isHiddenDraftSqlTab,
|
||||
])
|
||||
|
||||
const privateSnippets = useMemo(() => {
|
||||
const userSnippets =
|
||||
filteredSnippets.snippets?.filter((snippet) => snippet.visibility === 'user') ?? []
|
||||
filteredSnippets.snippets?.filter(
|
||||
(snippet) => snippet.visibility === 'user' && !isHiddenDraftSqlTab(snippet.id)
|
||||
) ?? []
|
||||
|
||||
return mergeSnippetsWithLogSql(
|
||||
userSnippets,
|
||||
@@ -216,7 +241,7 @@ export const SQLEditorNav = ({ sort = 'inserted_at' }: SQLEditorNavProps) => {
|
||||
if (sort === 'name') return a.name.localeCompare(b.name)
|
||||
else return new Date(b.inserted_at).valueOf() - new Date(a.inserted_at).valueOf()
|
||||
})
|
||||
}, [filteredSnippets.snippets, logSqlSnippets, sort])
|
||||
}, [filteredSnippets.snippets, logSqlSnippets, sort, isHiddenDraftSqlTab])
|
||||
const folders = useSnippetFolders(projectRef!)
|
||||
|
||||
const { data: snippetCountData, error: snippetCountError } = useContentCountQuery({
|
||||
@@ -264,7 +289,7 @@ export const SQLEditorNav = ({ sort = 'inserted_at' }: SQLEditorNavProps) => {
|
||||
if (
|
||||
snippet &&
|
||||
snippet.favorite &&
|
||||
!snippet.isNotSavedInDatabaseYet &&
|
||||
!isHiddenDraftSqlTab(snippet.id, snippet) &&
|
||||
!snippets.find((x) => x.id === snippet.id)
|
||||
) {
|
||||
snippets.push(snippet as SqlSnippet)
|
||||
@@ -277,13 +302,14 @@ export const SQLEditorNav = ({ sort = 'inserted_at' }: SQLEditorNavProps) => {
|
||||
|
||||
return (
|
||||
withLogSqlFavorites
|
||||
.filter((snippet) => !isHiddenDraftSqlTab(snippet.id))
|
||||
.map((snippet) => ({ ...snippet, folder_id: undefined }))
|
||||
.sort((a, b) => {
|
||||
if (sort === 'name') return a.name.localeCompare(b.name)
|
||||
else return new Date(b.inserted_at).valueOf() - new Date(a.inserted_at).valueOf()
|
||||
}) ?? []
|
||||
)
|
||||
}, [favoriteSqlSnippetsData?.pages, snippet, sort, logSqlSnippets])
|
||||
}, [favoriteSqlSnippetsData?.pages, snippet, sort, logSqlSnippets, isHiddenDraftSqlTab])
|
||||
|
||||
const numFavoriteSnippets = snippetCountData?.favorites ?? 0
|
||||
|
||||
@@ -315,19 +341,21 @@ export const SQLEditorNav = ({ sort = 'inserted_at' }: SQLEditorNavProps) => {
|
||||
if (
|
||||
snippet &&
|
||||
snippet.visibility === 'project' &&
|
||||
!snippet.isNotSavedInDatabaseYet &&
|
||||
!isHiddenDraftSqlTab(snippet.id, snippet) &&
|
||||
!snippets.find((x) => x.id === snippet.id)
|
||||
) {
|
||||
snippets.push(snippet as SqlSnippet)
|
||||
}
|
||||
|
||||
return (
|
||||
snippets.sort((a, b) => {
|
||||
if (sort === 'name') return a.name.localeCompare(b.name)
|
||||
else return new Date(b.inserted_at).valueOf() - new Date(a.inserted_at).valueOf()
|
||||
}) ?? []
|
||||
snippets
|
||||
.filter((snippet) => !isHiddenDraftSqlTab(snippet.id))
|
||||
.sort((a, b) => {
|
||||
if (sort === 'name') return a.name.localeCompare(b.name)
|
||||
else return new Date(b.inserted_at).valueOf() - new Date(a.inserted_at).valueOf()
|
||||
}) ?? []
|
||||
)
|
||||
}, [sharedSqlSnippetsData?.pages, snippet, sort, logSqlSnippets])
|
||||
}, [sharedSqlSnippetsData?.pages, snippet, sort, logSqlSnippets, isHiddenDraftSqlTab])
|
||||
|
||||
const numProjectSnippets = snippetCountData?.shared ?? 0
|
||||
const numTotalSnippets =
|
||||
|
||||
+12
-2
@@ -5,6 +5,7 @@ import { parseAsBoolean, useQueryState } from 'nuqs'
|
||||
import { useCallback } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { createDraftSqlTab } from '@/components/interfaces/SQLEditor/createDraftSqlTab'
|
||||
import { generateSnippetTitle } from '@/components/interfaces/SQLEditor/SQLEditor.constants'
|
||||
import { createSqlSnippetSkeletonV2 } from '@/components/interfaces/SQLEditor/SQLEditor.utils'
|
||||
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
||||
@@ -14,6 +15,7 @@ import { IS_PLATFORM } from '@/lib/constants'
|
||||
import { useProfile } from '@/lib/profile'
|
||||
import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state'
|
||||
import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor-v2'
|
||||
import { useTabsStateSnapshot } from '@/state/tabs'
|
||||
|
||||
export type SqlEditorSnippetCreateTarget = 'private' | 'shared' | 'favorite'
|
||||
|
||||
@@ -23,6 +25,7 @@ export function useSqlEditorCreateActions() {
|
||||
const { profile } = useProfile()
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const snapV2 = useSqlEditorV2StateSnapshot()
|
||||
const tabs = useTabsStateSnapshot()
|
||||
const assistant = useAiAssistantStateSnapshot()
|
||||
const [, setShowNewReportModal] = useQueryState('newReport', parseAsBoolean.withDefault(false))
|
||||
const { reportsAll } = useIsFeatureEnabled(['reports:all'])
|
||||
@@ -53,7 +56,14 @@ export function useSqlEditorCreateActions() {
|
||||
|
||||
try {
|
||||
if (target === 'private') {
|
||||
await router.push(`/project/${projectRef}/sql/new?skip=true`)
|
||||
createDraftSqlTab({
|
||||
projectRef,
|
||||
projectId: project.id,
|
||||
ownerId: profile.id,
|
||||
snapV2,
|
||||
tabs,
|
||||
router,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -74,7 +84,7 @@ export function useSqlEditorCreateActions() {
|
||||
toast.error(`Failed to create new query: ${error.message}`)
|
||||
}
|
||||
},
|
||||
[canCreateSQLSnippet, profile, project, projectRef, router, snapV2]
|
||||
[canCreateSQLSnippet, profile, project, projectRef, router, snapV2, tabs]
|
||||
)
|
||||
|
||||
const createNewChat = useCallback(() => {
|
||||
|
||||
@@ -131,7 +131,7 @@ export const SortableTab = ({
|
||||
className={cn(
|
||||
'flex items-center',
|
||||
isToggleGroupTab
|
||||
? 'min-w-0 max-w-[240px] shrink'
|
||||
? 'min-w-0 flex-1 max-w-fit overflow-hidden'
|
||||
: 'h-(--header-height) shrink-0 first-of-type:border-l'
|
||||
)}
|
||||
>
|
||||
@@ -140,7 +140,7 @@ export const SortableTab = ({
|
||||
value={tab.id}
|
||||
aria-label={tab.label || 'Untitled'}
|
||||
className={cn(
|
||||
'group relative flex w-auto min-w-0 max-w-full items-center gap-1.5 overflow-hidden px-2 text-xs',
|
||||
'group relative flex w-full min-w-0 flex-1 max-w-fit items-center gap-1.5 overflow-hidden px-2 text-xs',
|
||||
SQL_EDITOR_SIDEBAR_SEARCH_ROW_HEIGHT_CLASSNAME,
|
||||
tab.isPreview && 'italic font-light'
|
||||
)}
|
||||
|
||||
@@ -27,7 +27,7 @@ export const TabPreview = ({
|
||||
'relative flex items-center text-xs shadow-lg',
|
||||
variant === 'toggle-group'
|
||||
? cn(
|
||||
'max-w-[240px] gap-1.5 overflow-hidden rounded-md bg-surface-300 px-2',
|
||||
'max-w-fit gap-1.5 overflow-hidden rounded-md bg-surface-300 px-2',
|
||||
SQL_EDITOR_SIDEBAR_SEARCH_ROW_HEIGHT_CLASSNAME
|
||||
)
|
||||
: 'h-10 gap-2 rounded-xs bg-dash-sidebar px-3 dark:bg-surface-100'
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import { horizontalListSortingStrategy, SortableContext } from '@dnd-kit/sortable'
|
||||
import { useParams } from 'common'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { Plus, X } from 'lucide-react'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { useRouter } from 'next/router'
|
||||
import {
|
||||
cn,
|
||||
@@ -30,7 +30,10 @@ import { CollapseButton } from './CollapseButton'
|
||||
import { SortableTab } from './SortableTab'
|
||||
import { TabPreview } from './TabPreview'
|
||||
import { useTabsScroll } from './Tabs.utils'
|
||||
import { clearPersistedDraftSqlTab } from '@/components/interfaces/SQLEditor/createDraftSqlTab'
|
||||
import { useCreateDraftSqlTab } from '@/components/interfaces/SQLEditor/useCreateDraftSqlTab'
|
||||
import { useDashboardHistory } from '@/hooks/misc/useDashboardHistory'
|
||||
import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor-v2'
|
||||
import { editorEntityTypes, isSqlEditorTab, useTabsStateSnapshot, type Tab } from '@/state/tabs'
|
||||
|
||||
export const EditorTabs = () => {
|
||||
@@ -40,6 +43,8 @@ export const EditorTabs = () => {
|
||||
|
||||
const editor = useEditorType()
|
||||
const tabs = useTabsStateSnapshot()
|
||||
const snapV2 = useSqlEditorV2StateSnapshot()
|
||||
const { createDraftTab } = useCreateDraftSqlTab()
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
@@ -52,8 +57,6 @@ export const EditorTabs = () => {
|
||||
.map((id) => tabs.tabsMap[id])
|
||||
.filter((tab) => tab !== undefined) as Tab[]
|
||||
|
||||
const hasNewTab = router.asPath.includes('/new')
|
||||
|
||||
// Filter by editor type - only show SQL tabs for SQL editor and table tabs for table editor
|
||||
const editorTabs = !!editor
|
||||
? openTabs.filter((tab) => editorEntityTypes[editor]?.includes(tab.type))
|
||||
@@ -79,8 +82,25 @@ export const EditorTabs = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const cleanupDraftTabState = (tabId: string) => {
|
||||
const tab = tabs.tabsMap[tabId]
|
||||
const draftSqlId = tab?.metadata?.isDraft ? tab.metadata?.sqlId : undefined
|
||||
if (draftSqlId && ref) {
|
||||
snapV2.removeSnippet(draftSqlId, true)
|
||||
clearPersistedDraftSqlTab(ref, draftSqlId)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = (tabId: string) => {
|
||||
tabs.handleTabClose({ id: tabId, router, editor, onClearDashboardHistory })
|
||||
tabs.handleTabClose({
|
||||
id: tabId,
|
||||
router,
|
||||
editor,
|
||||
onClearDashboardHistory,
|
||||
onClose: () => {
|
||||
cleanupDraftTabState(tabId)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleCloseAll = () => {
|
||||
@@ -90,6 +110,7 @@ export const EditorTabs = () => {
|
||||
? tabs.openTabs.filter((x) => !isSqlEditorTab(x, tabs.tabsMap))
|
||||
: tabs.openTabs.filter((x) => isSqlEditorTab(x, tabs.tabsMap))
|
||||
|
||||
tabsToClose.forEach(cleanupDraftTabState)
|
||||
tabs.removeTabs(tabsToClose)
|
||||
onClearDashboardHistory()
|
||||
router.push(`/project/${ref}/${editor === 'table' ? 'editor' : 'sql'}`)
|
||||
@@ -103,6 +124,7 @@ export const EditorTabs = () => {
|
||||
? tabs.openTabs.filter((x) => !isSqlEditorTab(x, tabs.tabsMap) && x !== tabId)
|
||||
: tabs.openTabs.filter((x) => isSqlEditorTab(x, tabs.tabsMap) && x !== tabId)
|
||||
|
||||
tabsToClose.forEach(cleanupDraftTabState)
|
||||
tabs.removeTabs(tabsToClose)
|
||||
onClearDashboardHistory()
|
||||
|
||||
@@ -121,6 +143,7 @@ export const EditorTabs = () => {
|
||||
const tabIdx = openedTabs.indexOf(tabId)
|
||||
const activeTabIdx = openedTabs.indexOf(tabs.activeTab!)
|
||||
const tabsToClose = openedTabs.slice(tabIdx + 1)
|
||||
tabsToClose.forEach(cleanupDraftTabState)
|
||||
tabs.removeTabs(tabsToClose)
|
||||
|
||||
const isActiveTabClosed = tabIdx < activeTabIdx
|
||||
@@ -139,7 +162,7 @@ export const EditorTabs = () => {
|
||||
tabCount: editorTabs.length,
|
||||
enabled: editor !== 'sql',
|
||||
})
|
||||
const activeTabValue = hasNewTab ? 'new' : (tabs.activeTab ?? undefined)
|
||||
const activeTabValue = tabs.activeTab ?? undefined
|
||||
const isSqlEditor = editor === 'sql'
|
||||
|
||||
const sortableTabs = (
|
||||
@@ -174,54 +197,32 @@ export const EditorTabs = () => {
|
||||
</SortableContext>
|
||||
)
|
||||
|
||||
const newTabCloseButton = (
|
||||
<span
|
||||
role="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
className="ml-1 opacity-0 group-hover:opacity-100 hover:bg-200 rounded-xs cursor-pointer"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
onPointerDown={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
handleClose('new')
|
||||
}}
|
||||
>
|
||||
<X size={12} className="text-foreground-light" />
|
||||
</span>
|
||||
)
|
||||
|
||||
const addTabButton = (
|
||||
<AnimatePresence initial={false}>
|
||||
{!hasNewTab && (
|
||||
<motion.button
|
||||
className={cn(
|
||||
'flex shrink-0 items-center justify-center rounded-md hover:bg-surface-300',
|
||||
isSqlEditor
|
||||
? cn(SQL_EDITOR_SIDEBAR_SEARCH_ROW_HEIGHT_CLASSNAME, 'aspect-square shrink-0')
|
||||
: 'w-10 min-h-(--header-height) hover:bg-surface-100 border-b'
|
||||
)}
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/project/${router.query.ref}/${editor === 'table' ? 'editor' : 'sql'}/new?skip=true`
|
||||
)
|
||||
<motion.button
|
||||
className={cn(
|
||||
'flex shrink-0 items-center justify-center rounded-md hover:bg-surface-300',
|
||||
isSqlEditor
|
||||
? cn(SQL_EDITOR_SIDEBAR_SEARCH_ROW_HEIGHT_CLASSNAME, 'aspect-square shrink-0')
|
||||
: 'w-10 min-h-(--header-height) hover:bg-surface-100 border-b'
|
||||
)}
|
||||
onClick={() => {
|
||||
if (editor === 'sql') {
|
||||
createDraftTab()
|
||||
} else {
|
||||
void router.push(`/project/${router.query.ref}/editor/new?skip=true`)
|
||||
}
|
||||
initial={{ opacity: 0, scale: 0.8, x: -10 }}
|
||||
animate={{ opacity: 1, scale: 1, x: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<Plus
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
className="text-foreground-lighter hover:text-foreground-light"
|
||||
/>
|
||||
</motion.button>
|
||||
)}
|
||||
}}
|
||||
initial={{ opacity: 0, scale: 0.8, x: -10 }}
|
||||
animate={{ opacity: 1, scale: 1, x: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<Plus
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
className="text-foreground-lighter hover:text-foreground-light"
|
||||
/>
|
||||
</motion.button>
|
||||
</AnimatePresence>
|
||||
)
|
||||
|
||||
@@ -241,23 +242,9 @@ export const EditorTabs = () => {
|
||||
onValueChange={(value) => {
|
||||
if (value) handleTabChange(value)
|
||||
}}
|
||||
className="flex w-max min-w-0 max-w-full flex-nowrap justify-start overflow-hidden"
|
||||
className="flex min-w-0 flex-1 flex-nowrap justify-start overflow-hidden"
|
||||
>
|
||||
{sortableTabs}
|
||||
{hasNewTab && (
|
||||
<ToggleGroupItem
|
||||
value="new"
|
||||
aria-label="New tab"
|
||||
className={cn(
|
||||
'group flex shrink-0 items-center gap-1.5 px-2 text-xs',
|
||||
SQL_EDITOR_SIDEBAR_SEARCH_ROW_HEIGHT_CLASSNAME
|
||||
)}
|
||||
>
|
||||
<Plus size={14} strokeWidth={1.5} className="text-foreground-lighter" />
|
||||
<span>New</span>
|
||||
{newTabCloseButton}
|
||||
</ToggleGroupItem>
|
||||
)}
|
||||
</ToggleGroup>
|
||||
{addTabButton}
|
||||
</div>
|
||||
@@ -277,25 +264,6 @@ export const EditorTabs = () => {
|
||||
)}
|
||||
>
|
||||
{sortableTabs}
|
||||
{hasNewTab && (
|
||||
<TabsTrigger_Shadcn_
|
||||
value="new"
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-3 text-xs',
|
||||
'bg-dash-sidebar/50 dark:bg-surface-100/50',
|
||||
'data-[state=active]:bg-dash-sidebar dark:data-[state=active]:bg-surface-100',
|
||||
'relative group h-full border-t-2 border-b-0!',
|
||||
'hover:bg-surface-300 dark:hover:bg-surface-100'
|
||||
)}
|
||||
>
|
||||
<Plus size={16} strokeWidth={1.5} className={'text-foreground-lighter'} />
|
||||
<div className="flex items-center gap-0">
|
||||
<span>New</span>
|
||||
</div>
|
||||
{newTabCloseButton}
|
||||
<div className="absolute w-full -bottom-px left-0 right-0 h-px bg-dash-sidebar dark:bg-surface-100 opacity-0 group-data-[state=active]:opacity-100" />
|
||||
</TabsTrigger_Shadcn_>
|
||||
)}
|
||||
{addTabButton}
|
||||
<div className="grow h-full border-b pr-6" />
|
||||
</TabsList_Shadcn_>
|
||||
|
||||
@@ -2,10 +2,15 @@ import { usePrevious } from '@uidotdev/usehooks'
|
||||
import { useParams } from 'common/hooks/useParams'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect } from 'react'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { Button } from 'ui'
|
||||
import { Admonition } from 'ui-patterns'
|
||||
|
||||
import {
|
||||
createDraftSqlTab,
|
||||
isDraftSqlSnippet,
|
||||
restoreDraftSqlTab,
|
||||
} from '@/components/interfaces/SQLEditor/createDraftSqlTab'
|
||||
import { SQLEditor } from '@/components/interfaces/SQLEditor/SQLEditor'
|
||||
import { generateSnippetTitle } from '@/components/interfaces/SQLEditor/SQLEditor.constants'
|
||||
import { getSnippetQuerySource } from '@/components/interfaces/SQLEditor/sqlSnippet.utils'
|
||||
@@ -18,6 +23,7 @@ import { useContentIdQuery } from '@/data/content/content-id-query'
|
||||
import { useDashboardHistory } from '@/hooks/misc/useDashboardHistory'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
import { IS_PLATFORM } from '@/lib/constants'
|
||||
import { useProfile } from '@/lib/profile'
|
||||
import { useQueryExecutionSourceSnapshot } from '@/state/query-execution-source'
|
||||
import { SnippetWithContent, useSnippets, useSqlEditorV2StateSnapshot } from '@/state/sql-editor-v2'
|
||||
import { createTabId, useTabsStateSnapshot } from '@/state/tabs'
|
||||
@@ -25,26 +31,32 @@ import type { NextPageWithLayout } from '@/types'
|
||||
|
||||
const SqlEditor: NextPageWithLayout = () => {
|
||||
const router = useRouter()
|
||||
const { id, ref, content, skip } = useParams()
|
||||
const { id, ref, content, source } = useParams()
|
||||
const previousRoute = usePrevious(id)
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const { profile } = useProfile()
|
||||
const creatingDraftRef = useRef(false)
|
||||
|
||||
const editor = useEditorType()
|
||||
const tabs = useTabsStateSnapshot()
|
||||
const snapV2 = useSqlEditorV2StateSnapshot()
|
||||
const querySourceState = useQueryExecutionSourceSnapshot()
|
||||
const { history, setLastVisitedSnippet } = useDashboardHistory()
|
||||
const { setLastVisitedSnippet } = useDashboardHistory()
|
||||
|
||||
const allSnippets = useSnippets(ref!)
|
||||
const snippet = allSnippets.find((x) => x.id === id)
|
||||
const tabId = id ? tabs.openTabs.find((x) => x.endsWith(id)) : undefined
|
||||
const tab = tabId ? tabs.tabsMap[tabId] : undefined
|
||||
const isDraftTab = isDraftSqlSnippet(snippet) || tab?.metadata?.isDraft === true
|
||||
|
||||
const tabId = !!id ? tabs.openTabs.find((x) => x.endsWith(id)) : undefined
|
||||
|
||||
// [Joshen] May need to investigate separately, but occasionally addSnippet doesnt exist in
|
||||
// the snapV2 valtio store for some reason hence why the added typeof check here
|
||||
const canFetchContentBasedOnId = Boolean(
|
||||
id !== 'new' && typeof snapV2.addSnippet === 'function' && !snippet?.isNotSavedInDatabaseYet
|
||||
id &&
|
||||
id !== 'new' &&
|
||||
typeof snapV2.addSnippet === 'function' &&
|
||||
!isDraftTab &&
|
||||
!snippet?.isNotSavedInDatabaseYet
|
||||
)
|
||||
|
||||
const { data, error, isError } = useContentIdQuery(
|
||||
{ projectRef: ref, id },
|
||||
{
|
||||
@@ -57,42 +69,72 @@ const SqlEditor: NextPageWithLayout = () => {
|
||||
isError && error.code === 404 && error.message.includes('Content not found')
|
||||
const invalidId = isError && error.code === 400 && error.message.includes('Invalid uuid')
|
||||
|
||||
// [Joshen] Atm we suspect that replication lag is causing this to happen whereby a newly created snippet
|
||||
// shows the "Unable to find snippet" error which blocks the whole UI
|
||||
// Am opting to silently swallow this error, since the saves are still going through and we're scoping this behaviour
|
||||
// behaviour down to a very specific use case too with all these conditionals
|
||||
// More details: https://github.com/supabase/supabase/pull/39389
|
||||
const snippetMissingImmediatelyAfterCreating =
|
||||
!!snippet && snippetMissing && previousRoute === 'new' && 'isNotSavedInDatabaseYet' in snippet
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
id !== 'new' ||
|
||||
!router.isReady ||
|
||||
!ref ||
|
||||
!project ||
|
||||
!profile ||
|
||||
creatingDraftRef.current
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
creatingDraftRef.current = true
|
||||
|
||||
const initialSql = typeof content === 'string' ? content : ''
|
||||
const querySource = source === 'logs' ? ('logs' as const) : ('database' as const)
|
||||
|
||||
const draftId = createDraftSqlTab({
|
||||
projectRef: ref,
|
||||
projectId: project.id,
|
||||
ownerId: profile.id,
|
||||
snapV2,
|
||||
tabs,
|
||||
initialSql,
|
||||
querySource,
|
||||
skipNavigation: true,
|
||||
})
|
||||
|
||||
if (source === 'logs') {
|
||||
querySourceState.setExecutionSource('logs')
|
||||
}
|
||||
|
||||
void router.replace(`/project/${ref}/sql/${draftId}`)
|
||||
}, [content, id, profile, project, querySourceState, ref, router, snapV2, source, tabs])
|
||||
|
||||
useEffect(() => {
|
||||
if (!router.isReady || !id || id === 'new' || !ref || !project || !profile) return
|
||||
if (!tab?.metadata?.isDraft || snippet) return
|
||||
|
||||
restoreDraftSqlTab({
|
||||
draftId: id,
|
||||
projectRef: ref,
|
||||
projectId: project.id,
|
||||
ownerId: profile.id,
|
||||
snapV2,
|
||||
name: tab.metadata?.name ?? generateSnippetTitle(),
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [router.isReady, id, ref, project, profile, tab?.metadata?.isDraft, snippet])
|
||||
|
||||
useEffect(() => {
|
||||
if (ref && data && project) {
|
||||
// [Joshen] Check if snippet belongs to the current project
|
||||
if (!IS_PLATFORM || data.project_id === project.id) {
|
||||
snapV2.setSnippet(ref, data as unknown as SnippetWithContent)
|
||||
setLastVisitedSnippet(data.id)
|
||||
} else {
|
||||
setLastVisitedSnippet(undefined)
|
||||
router.replace(`/project/${ref}/sql/new`)
|
||||
void router.replace(`/project/${ref}/sql/new?skip=true`)
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ref, data, project])
|
||||
|
||||
useEffect(() => {
|
||||
if (!router.isReady || !ref || id === 'new') return
|
||||
|
||||
if (snippetMissing && !snippetMissingImmediatelyAfterCreating) {
|
||||
setLastVisitedSnippet(undefined)
|
||||
const tabId = createTabId('sql', { id })
|
||||
if (tabs.openTabs.includes(tabId)) {
|
||||
tabs.removeTab(tabId)
|
||||
}
|
||||
router.replace(`/project/${ref}/sql/new?skip=true`)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [router.isReady, ref, id, snippetMissing, snippetMissingImmediatelyAfterCreating])
|
||||
|
||||
useEffect(() => {
|
||||
if (router.query.source === 'logs') {
|
||||
querySourceState.setExecutionSource('logs')
|
||||
@@ -102,40 +144,44 @@ const SqlEditor: NextPageWithLayout = () => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [router.query.source, data])
|
||||
|
||||
// Load the last visited snippet when landing on /new
|
||||
useEffect(() => {
|
||||
if (
|
||||
id === 'new' &&
|
||||
skip !== 'true' && // [Joshen] Skip flag implies to skip loading the last visited snippet
|
||||
history.sql !== undefined &&
|
||||
content === undefined
|
||||
) {
|
||||
const snippet = allSnippets.find((snippet) => snippet.id === history.sql)
|
||||
if (snippet !== undefined) router.replace(`/project/${ref}/sql/${history.sql}`)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [id, allSnippets, content])
|
||||
|
||||
// Watch for route changes
|
||||
useEffect(() => {
|
||||
if (!router.isReady || !id || id === 'new') return
|
||||
|
||||
const tabId = createTabId('sql', { id })
|
||||
const snippet = allSnippets.find((x) => x.id === id)
|
||||
const nextTabId = createTabId('sql', { id })
|
||||
const routeSnippet = allSnippets.find((x) => x.id === id)
|
||||
|
||||
tabs.addTab({
|
||||
id: tabId,
|
||||
id: nextTabId,
|
||||
type: 'sql',
|
||||
label: snippet?.name || generateSnippetTitle(),
|
||||
label: routeSnippet?.name || tab?.metadata?.name || generateSnippetTitle(),
|
||||
metadata: {
|
||||
sqlId: id,
|
||||
name: snippet?.name,
|
||||
name: routeSnippet?.name ?? tab?.metadata?.name,
|
||||
isDraft: isDraftTab,
|
||||
},
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [router.isReady, id])
|
||||
|
||||
if (snippetMissing && !snippetMissingImmediatelyAfterCreating) {
|
||||
useEffect(() => {
|
||||
if (!router.isReady || !ref || !id || id === 'new') return
|
||||
|
||||
if (snippetMissing && !snippetMissingImmediatelyAfterCreating && !isDraftTab) {
|
||||
setLastVisitedSnippet(undefined)
|
||||
const staleTabId = createTabId('sql', { id })
|
||||
if (tabs.openTabs.includes(staleTabId)) {
|
||||
tabs.removeTab(staleTabId)
|
||||
}
|
||||
void router.replace(`/project/${ref}/sql/new?skip=true`)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [router.isReady, ref, id, snippetMissing, snippetMissingImmediatelyAfterCreating, isDraftTab])
|
||||
|
||||
if (id === 'new') {
|
||||
return null
|
||||
}
|
||||
|
||||
if (snippetMissing && !snippetMissingImmediatelyAfterCreating && !isDraftTab) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ const SQLEditorIndexPage: NextPageWithLayout = () => {
|
||||
router.replace(`/project/${projectRef}/sql/${lastTab.metadata.sqlId}`)
|
||||
}
|
||||
} else {
|
||||
router.replace(`/project/${projectRef}/sql/new`)
|
||||
router.replace(`/project/${projectRef}/sql/new?skip=true`)
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
@@ -6,10 +6,12 @@ import { proxy, ref, snapshot, subscribe, useSnapshot } from 'valtio'
|
||||
import { devtools, proxyMap } from 'valtio/utils'
|
||||
|
||||
import type { QueryPlanRow } from '@/components/interfaces/ExplainVisualizer/ExplainVisualizer.types'
|
||||
import { persistDraftSqlTab } from '@/components/interfaces/SQLEditor/draftSqlTabStorage.utils'
|
||||
import { DiffType } from '@/components/interfaces/SQLEditor/SQLEditor.types'
|
||||
import {
|
||||
buildSnippetUpsertContent,
|
||||
getSnippetContentType,
|
||||
getSnippetQuerySource,
|
||||
getSnippetSqlFromContent,
|
||||
} from '@/components/interfaces/SQLEditor/sqlSnippet.utils'
|
||||
import { upsertContent, UpsertContentPayload } from '@/data/content/content-upsert-mutation'
|
||||
@@ -33,10 +35,16 @@ type StateSnippet = {
|
||||
snippet: SnippetWithContent
|
||||
}
|
||||
|
||||
function isDraftSqlTabId(id: string) {
|
||||
return sqlEditorState.snippets[id]?.snippet?.isDraftTab === true
|
||||
}
|
||||
|
||||
// [Joshen] API codegen is somehow missing the content property
|
||||
export interface SnippetWithContent extends Snippet {
|
||||
content?: SqlSnippets.Content
|
||||
isNotSavedInDatabaseYet?: boolean
|
||||
/** Ephemeral SQL editor tab — not shown in the sidebar until saved */
|
||||
isDraftTab?: boolean
|
||||
}
|
||||
|
||||
const NEW_FOLDER_ID = 'new-folder'
|
||||
@@ -168,7 +176,7 @@ export const sqlEditorState = proxy({
|
||||
sql: sqlValue,
|
||||
label: snippet.name,
|
||||
})
|
||||
} else {
|
||||
} else if (!isDraftSqlTabId(id)) {
|
||||
sqlEditorState.needsSaving.set(id, true)
|
||||
}
|
||||
}
|
||||
@@ -220,6 +228,18 @@ export const sqlEditorState = proxy({
|
||||
return
|
||||
}
|
||||
|
||||
if (isDraftSqlTabId(id)) {
|
||||
const stateSnippet = sqlEditorState.snippets[id]
|
||||
if (stateSnippet) {
|
||||
persistDraftSqlTab(stateSnippet.projectRef, id, {
|
||||
sql,
|
||||
name: snippet.name,
|
||||
querySource: getSnippetQuerySource(snippet),
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (!skipSave) {
|
||||
sqlEditorState.needsSaving.set(id, shouldInvalidate)
|
||||
}
|
||||
@@ -345,6 +365,7 @@ export const sqlEditorState = proxy({
|
||||
}
|
||||
return
|
||||
}
|
||||
if (isDraftSqlTabId(id)) return
|
||||
sqlEditorState.needsSaving.set(id, true)
|
||||
},
|
||||
|
||||
@@ -549,6 +570,11 @@ if (typeof window !== 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
if (isDraftSqlTabId(id)) {
|
||||
sqlEditorState.needsSaving.delete(id)
|
||||
return
|
||||
}
|
||||
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
|
||||
@@ -21,9 +21,7 @@ export function isSqlEditorTab(tabOrId: Tab | string, tabsMap?: Record<string, T
|
||||
const tab = tabsMap?.[tabOrId]
|
||||
if (tab) return SQL_EDITOR_TAB_TYPES.includes(tab.type)
|
||||
return (
|
||||
tabOrId.startsWith('sql-') ||
|
||||
tabOrId.startsWith('notebook-') ||
|
||||
tabOrId.startsWith('chat-')
|
||||
tabOrId.startsWith('sql-') || tabOrId.startsWith('notebook-') || tabOrId.startsWith('chat-')
|
||||
)
|
||||
}
|
||||
|
||||
@@ -57,6 +55,7 @@ export interface Tab {
|
||||
notebookId?: string
|
||||
chatId?: string
|
||||
scrollTop?: number
|
||||
isDraft?: boolean
|
||||
}
|
||||
isPreview?: boolean
|
||||
createdAt?: Date
|
||||
@@ -244,7 +243,14 @@ export function createTabsState(projectRef: string) {
|
||||
store.previewTabId = tab.id
|
||||
store.activeTab = tab.id
|
||||
},
|
||||
updateTab: (id: string, updates: { label?: string; scrollTop?: number }) => {
|
||||
updateTab: (
|
||||
id: string,
|
||||
updates: {
|
||||
label?: string
|
||||
scrollTop?: number
|
||||
metadata?: Partial<NonNullable<Tab['metadata']>>
|
||||
}
|
||||
) => {
|
||||
if (!!store.tabsMap[id]) {
|
||||
if ('label' in updates) {
|
||||
store.tabsMap[id].label = updates.label
|
||||
@@ -260,6 +266,12 @@ export function createTabsState(projectRef: string) {
|
||||
if ('scrollTop' in updates && store.tabsMap[id].metadata) {
|
||||
store.tabsMap[id].metadata.scrollTop = updates.scrollTop
|
||||
}
|
||||
if (updates.metadata && store.tabsMap[id].metadata) {
|
||||
store.tabsMap[id].metadata = {
|
||||
...store.tabsMap[id].metadata,
|
||||
...updates.metadata,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// Function to remove a tab from the store
|
||||
|
||||
@@ -48,6 +48,7 @@ export const LOCAL_STORAGE_KEYS = {
|
||||
SQL_EDITOR_SQL_BLOCK_ACKNOWLEDGED: (ref: string) => `sql-editor-sql-block-acknowledged-${ref}`,
|
||||
SQL_EDITOR_SECTION_STATE: (ref: string) => `sql-editor-section-state-${ref}`,
|
||||
SQL_EDITOR_SORT: (ref: string) => `sql-editor-sort-${ref}`,
|
||||
SQL_EDITOR_DRAFT_TABS: (ref: string) => `sql-editor-draft-tabs-${ref}`,
|
||||
|
||||
LOG_EXPLORER_SPLIT_SIZE: 'supabase_log-explorer-split-size',
|
||||
GRAPHQL_INTROSPECTION_NOTICE_COLLAPSED: (ref: string) =>
|
||||
|
||||
Reference in new issue
Block a user