Files
supabase/apps/studio/components/interfaces/Explorer/hooks.ts
Saxon Fletcher 78bd0e066b chore(studio): align query results grid typography with table editor (#49752)
## Summary
- Match Explorer/SQL query results table typography to Table Editor:
sans cells at `text-grid`, headers at `text-xs` / `text-foreground`
- Reuse Table Editor `NullValue` for null cells so empty results read
the same way
- When the Explorer feature preview is on, the inline editor "expand"
action opens a new Explorer query tab (same pattern as the assistant)
instead of the SQL Editor

## Test plan
- [ ] Run a query in Explorer and confirm header/cell font, size, and
color match Table Editor
- [ ] Confirm `NULL` cells use the same faded treatment as Table Editor
- [ ] Check the SQL Editor results pane (shared `DataGridResults`) still
looks correct
- [ ] With Explorer feature preview on, expand the inline editor and
confirm it creates an Explorer query tab with the current SQL, then
closes the panel
- [ ] With Explorer feature preview off, expand the inline editor and
confirm it still opens a SQL Editor snippet

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

## Summary by CodeRabbit

## New Features
- Added the ability to open SQL directly in Explorer from the editor
panel.
- Run SQL actions now create a query draft and navigate to the query
tab.

## Style
- Improved data grid readability with clearer text, left-aligned
headers, truncated labels, and selectable content.
- Added dedicated styling for null values.
- Updated empty-results messaging with standard sans-serif text.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-31 18:38:30 +10:00

164 lines
5.0 KiB
TypeScript

import { useRouter } from 'next/router'
import { useEffect, useEffectEvent, useState } from 'react'
import { useNotebookQuery } from '@/data/content/notebooks/notebook-query'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { generateUuid } from '@/lib/api/snippets.browser'
import { useProfile } from '@/lib/profile'
import type { AssistantModel } from '@/state/ai-assistant-state'
import { useAiAssistantState, whenAiAssistantInitialized } from '@/state/ai-assistant-state'
import { useExplorerQueryStateSnapshot } from '@/state/explorer-query'
import { useNotebooksStateSnapshot } from '@/state/notebooks/notebooks-state'
import { type Notebook } from '@/state/notebooks/types'
import { Notebooks } from '@/types'
/**
* Fetches a notebook's content by id and merges it into the valtio store, so landing on
* a notebook any way other than creating it in this session (direct link, hard refresh,
* clicking it from the nav list) still hydrates `notebooksState`.
*/
export const useLoadNotebook = ({ id, projectRef }: { id?: string; projectRef?: string }) => {
const notebooksSnap = useNotebooksStateSnapshot()
const currentNotebook = id ? notebooksSnap.notebooks[id] : undefined
const isCurrentProjectNotebook = currentNotebook?.projectRef === projectRef
const isNewLocalNotebook = isCurrentProjectNotebook && currentNotebook?.status === 'new'
const hasLoadedNotebook =
isCurrentProjectNotebook && currentNotebook?.notebook.content !== undefined
const { data, error, isError } = useNotebookQuery(
{ projectRef, id },
{
retry: false,
enabled: !isNewLocalNotebook && !hasLoadedNotebook,
}
)
const mergeNotebook = useEffectEvent(() => {
if (projectRef && data) notebooksSnap.setNotebook({ projectRef, notebook: data })
})
useEffect(() => {
mergeNotebook()
}, [projectRef, data])
return { isNotFound: isError && error.code === 404 }
}
export const useCreateNotebook = () => {
const router = useRouter()
const { profile } = useProfile()
const { data: project } = useSelectedProjectQuery()
const notebooksSnap = useNotebooksStateSnapshot()
const createNotebook = ({
id: idOverride,
name,
cells,
}: { id?: string; name?: string; cells?: Notebooks.Content['cells'] } = {}) => {
if (!profile) return console.error('Profile is required')
if (!project) return console.error('Project is required')
const id = idOverride ?? generateUuid()
const notebook: Notebook = {
id,
type: 'notebook',
name: name ?? 'New Notebook',
description: '',
visibility: 'project',
favorite: false,
content: {
schema_version: 1,
cells: cells ?? [],
},
owner_id: profile.id,
project_id: project.id,
}
notebooksSnap.addNotebook({ projectRef: project.ref, notebook })
notebooksSnap.addNeedsSaving(notebook.id)
router.push(`/project/${project.ref}/explorer/notebook/${notebook.id}`)
}
return { createNotebook }
}
export const useCreateChat = () => {
const router = useRouter()
const { data: project } = useSelectedProjectQuery()
const { data: organization } = useSelectedOrganizationQuery()
const aiAssistantState = useAiAssistantState()
const [isCreating, setIsCreating] = useState(false)
const openChat = (id: string) => {
if (!project) {
console.error('Project is required')
return undefined
}
router.push(`/project/${project.ref}/explorer/chat/${id}`)
}
const createChat = async ({
name,
initialMessage,
model,
}: {
name?: string
initialMessage?: string
model?: AssistantModel
} = {}) => {
if (!project) {
console.error('Project is required')
return undefined
}
setIsCreating(true)
try {
// Hydration replaces the chat map and the selected model wholesale, so wait it out before
// creating anything — otherwise the new chat is dropped as soon as the persisted state lands.
await whenAiAssistantInitialized(aiAssistantState)
aiAssistantState.setContext({
projectRef: project.ref,
orgSlug: organization?.slug,
connectionString: project.connectionString ?? '',
})
if (model) aiAssistantState.setModel(model)
const id = aiAssistantState.createChat({ name, initialMessage })
router.push(`/project/${project.ref}/explorer/chat/${id}`)
return id
} finally {
setIsCreating(false)
}
}
return { createChat, openChat, isCreating }
}
export const useCreateQuery = () => {
const router = useRouter()
const { data: project } = useSelectedProjectQuery()
const querySnap = useExplorerQueryStateSnapshot()
const createQuery = ({ sql, name }: { sql?: string; name?: string } = {}) => {
if (!project) return console.error('Project is required')
const id = generateUuid()
querySnap.createDraft({ id, projectRef: project.ref, sql, name })
router.push(`/project/${project.ref}/explorer/query/${id}`)
return id
}
return { createQuery }
}