Files
supabase/apps/studio/components/interfaces/SQLEditor/useEditorMount.ts
Charis 9e80a159b5 refactor(sql-editor): decompose into controller contexts + presentational components (decompose 6/6) (#47938)
## 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 -->
2026-07-15 13:10:09 -04:00

43 lines
1.6 KiB
TypeScript

import { useCallback, useContext, useState } from 'react'
import { snapshot } from 'valtio'
import type { IStandaloneCodeEditor } from './SQLEditor.types'
import { useSQLEditorContext } from './SQLEditorContext'
import { createTabId, TabsStateContext } from '@/state/tabs'
/**
* Owns the editor `onMount` handler (scroll-position restore + tracking) and the
* mount counter that lets a diff request which arrived before the editor was
* ready re-run once it is.
*/
export function useEditorMount({ id }: { id: string }) {
const { scrollTopRef } = useSQLEditorContext()
// The proxy store (stable reference), read non-reactively in onMount so the
// callback identity doesn't churn on every tab-state change.
const tabsState = useContext(TabsStateContext)
// Bumped on every editor mount (including the keyed remount on snippet switch)
// so a diff request that arrived before the editor was ready gets re-processed.
const [editorMountCount, setEditorMountCount] = useState(0)
const onMount = useCallback(
(editor: IStandaloneCodeEditor) => {
setEditorMountCount((count) => count + 1)
const tabId = createTabId('sql', { id })
const tabData = snapshot(tabsState).tabsMap[tabId]
// [Joshen] Tiny timeout to give a bit of time for the content to load before scrolling
setTimeout(() => {
if (tabData?.metadata?.scrollTop) {
editor.setScrollTop(tabData.metadata.scrollTop)
}
}, 20)
editor.onDidScrollChange((e) => (scrollTopRef.current = e.scrollTop))
},
[id, scrollTopRef, tabsState]
)
return { onMount, editorMountCount }
}