mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
## What Final step (**6 of 6**) of the `SQLEditor.tsx` decomposition. Splits the two `ResizablePanel` bodies out of the `SQLEditorContent` monolith into presentational sub-components: - **`SQLEditorPane`** — the editor panel: loading state, `DiffEditor` + diff ask-AI widget, `MonacoEditor` + ask-AI widget. Reads the Monaco refs (`editorRef`/`monacoRef`/`diffEditorRef`) from `SQLEditorContext`, and receives the `diff`/`prompt`/`ai` controllers plus reactive state as props. - **`SQLEditorResults`** — the results panel: loading state + `UtilityPanel`. `SQLEditorContent` is now a composition root: shared-ref provider, hook composition, and the run-query warning modal. ## Result `SQLEditor.tsx` goes from the original **1056-line** monolith down to **259 lines** (≈75% reduction). The remaining size over a bare ~140-line root is the run-query warning modal, kept inline **deliberately**: its handlers hold the `acceptUntrustedSql` promotion, which must stay at the explicit user-action boundary in the root rather than moving into a presentational pane. ## Behavior-preserving The moved JSX is byte-identical aside from prop threading. No logic, effects, dependency arrays, or `eslint-disable`s changed. The two **render-time ref reads** — the editor placeholder (`!promptState.isOpen && !editorRef.current?.getValue()`) and the ask-AI widget gate (`editorRef.current && promptState.isOpen && !isDiffOpen`) — are preserved verbatim in `SQLEditorPane`, which re-renders whenever the `prompt`/`diff` props change, keeping those reads fresh (the guardrail from the plan). Verification (all green): - `SQLEditor.test.tsx` characterization suite — 11/11 pass - `tsc --noEmit` — no new errors - eslint — clean (no new ratchet entries) - prettier — clean ## Stack Builds on decompose 5 (#47935). This is the last PR in the series — the decomposition is complete after this merges. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a resizable SQL editor layout with run warnings, query results, and an Explain view. * Introduced centralized SQL editor controllers and expanded AI-assisted prompt/diff workflows. * Added keyboard support for running and Explain analysis. * **Bug Fixes** * Restored editor focus after accepting or discarding AI changes. * Improved handling and validation of SQL Explain actions. * Preserved editor scroll position when switching snippets. * **Refactor** * Streamlined the SQLEditor into a composed layout and improved memoization to reduce unnecessary re-renders. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
163 lines
4.4 KiB
TypeScript
163 lines
4.4 KiB
TypeScript
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
|
import { useParams } from 'common'
|
|
import { useRouter } from 'next/router'
|
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
import { toast } from 'sonner'
|
|
|
|
import { ContentDiff, DiffType } from './SQLEditor.types'
|
|
import {
|
|
compareAsAddition,
|
|
compareAsModification,
|
|
compareAsNewSnippet,
|
|
createSqlSnippetSkeletonV2,
|
|
} from './SQLEditor.utils'
|
|
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
|
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
|
import { useProfile } from '@/lib/profile'
|
|
import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor/sql-editor-state'
|
|
|
|
export const useNewQuery = () => {
|
|
const router = useRouter()
|
|
const { ref } = useParams()
|
|
const { profile } = useProfile()
|
|
const { data: project } = useSelectedProjectQuery()
|
|
const snapV2 = useSqlEditorV2StateSnapshot()
|
|
|
|
const { can: canCreateSQLSnippet } = useAsyncCheckPermissions(
|
|
PermissionAction.CREATE,
|
|
'user_content',
|
|
{
|
|
resource: { type: 'sql', owner_id: profile?.id },
|
|
subject: { id: profile?.id },
|
|
}
|
|
)
|
|
|
|
const newQuery = async (sql: string, name: string, shouldRedirect: boolean = true) => {
|
|
if (!ref) 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) {
|
|
toast('Your queries will not be saved as you do not have sufficient permissions')
|
|
return undefined
|
|
}
|
|
|
|
try {
|
|
const snippet = createSqlSnippetSkeletonV2({
|
|
name,
|
|
sql,
|
|
owner_id: profile?.id,
|
|
project_id: project?.id,
|
|
})
|
|
snapV2.addSnippet({ projectRef: ref, snippet })
|
|
snapV2.addNeedsSaving(snippet.id)
|
|
if (shouldRedirect) {
|
|
router.push(`/project/${ref}/sql/${snippet.id}`)
|
|
return undefined
|
|
} else {
|
|
return snippet.id
|
|
}
|
|
} catch (error: any) {
|
|
toast.error(`Failed to create new query: ${error.message}`)
|
|
return undefined
|
|
}
|
|
}
|
|
|
|
return { newQuery }
|
|
}
|
|
|
|
export function useSqlEditorDiff() {
|
|
const [sourceSqlDiff, setSourceSqlDiff] = useState<ContentDiff>()
|
|
const [selectedDiffType, setSelectedDiffType] = useState<DiffType>()
|
|
const [isAcceptDiffLoading, setIsAcceptDiffLoading] = useState(false)
|
|
|
|
const isDiffOpen = !!sourceSqlDiff
|
|
|
|
const defaultSqlDiff = useMemo(() => {
|
|
if (!sourceSqlDiff) {
|
|
return { original: '', modified: '' }
|
|
}
|
|
|
|
switch (selectedDiffType) {
|
|
case DiffType.Modification:
|
|
return compareAsModification(sourceSqlDiff)
|
|
case DiffType.Addition:
|
|
return compareAsAddition(sourceSqlDiff)
|
|
case DiffType.NewSnippet:
|
|
return compareAsNewSnippet(sourceSqlDiff)
|
|
default:
|
|
return { original: '', modified: '' }
|
|
}
|
|
}, [selectedDiffType, sourceSqlDiff])
|
|
|
|
const closeDiff = useCallback(() => {
|
|
setSourceSqlDiff(undefined)
|
|
setSelectedDiffType(undefined)
|
|
}, [])
|
|
|
|
return useMemo(
|
|
() => ({
|
|
sourceSqlDiff,
|
|
setSourceSqlDiff,
|
|
selectedDiffType,
|
|
setSelectedDiffType,
|
|
isAcceptDiffLoading,
|
|
setIsAcceptDiffLoading,
|
|
isDiffOpen,
|
|
defaultSqlDiff,
|
|
closeDiff,
|
|
}),
|
|
[sourceSqlDiff, selectedDiffType, isAcceptDiffLoading, isDiffOpen, defaultSqlDiff, closeDiff]
|
|
)
|
|
}
|
|
|
|
interface PromptState {
|
|
isOpen: boolean
|
|
selection: string
|
|
beforeSelection: string
|
|
afterSelection: string
|
|
startLineNumber: number
|
|
endLineNumber: number
|
|
}
|
|
|
|
const initialPromptState: PromptState = {
|
|
isOpen: false,
|
|
selection: '',
|
|
beforeSelection: '',
|
|
afterSelection: '',
|
|
startLineNumber: 0,
|
|
endLineNumber: 0,
|
|
}
|
|
|
|
export function useSqlEditorPrompt() {
|
|
const [promptState, setPromptState] = useState<PromptState>(initialPromptState)
|
|
const [promptInput, setPromptInput] = useState('')
|
|
|
|
useEffect(() => {
|
|
if (!promptState.isOpen) {
|
|
setPromptInput('')
|
|
}
|
|
}, [promptState.isOpen])
|
|
|
|
const resetPrompt = useCallback(() => {
|
|
setPromptState(initialPromptState)
|
|
setPromptInput('')
|
|
}, [])
|
|
|
|
const openPrompt = useCallback((context: Omit<PromptState, 'isOpen'>) => {
|
|
setPromptState((prev) => ({ ...prev, isOpen: true, ...context }))
|
|
}, [])
|
|
|
|
return useMemo(
|
|
() => ({
|
|
promptState,
|
|
setPromptState,
|
|
promptInput,
|
|
setPromptInput,
|
|
resetPrompt,
|
|
openPrompt,
|
|
}),
|
|
[promptState, promptInput, resetPrompt, openPrompt]
|
|
)
|
|
}
|