diff --git a/.claude/skills/studio-e2e-tests/SKILL.md b/.claude/skills/studio-e2e-tests/SKILL.md
index ecd48bc8376..1006981e42d 100644
--- a/.claude/skills/studio-e2e-tests/SKILL.md
+++ b/.claude/skills/studio-e2e-tests/SKILL.md
@@ -88,7 +88,7 @@ test.describe.configure({ mode: 'serial' })
3. **`getByText` with exact match** - Good for unique text
```typescript
- page.getByText('Data API access', { exact: true })
+ page.getByText('Data API Access', { exact: true })
```
4. **`locator` with CSS** - Use sparingly, more fragile
diff --git a/.github/instructions/studio-e2e-tests.instructions.md b/.github/instructions/studio-e2e-tests.instructions.md
index 590512b176b..58488df8d18 100644
--- a/.github/instructions/studio-e2e-tests.instructions.md
+++ b/.github/instructions/studio-e2e-tests.instructions.md
@@ -23,7 +23,7 @@ All comments are **advisory**.
3. **`getByText` with exact match** — good for unique text
```typescript
- page.getByText('Data API access', { exact: true })
+ page.getByText('Data API Access', { exact: true })
```
4. **`locator` with CSS** — use sparingly, more fragile
diff --git a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/TableReplicationRow.tsx b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/TableReplicationRow.tsx
index 17f61d935c7..51bbe46529b 100644
--- a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/TableReplicationRow.tsx
+++ b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/TableReplicationRow.tsx
@@ -47,7 +47,7 @@ export const TableReplicationRow = ({
className="px-1.5"
icon={}
tooltip={{
- content: { side: 'bottom', text: 'Table Editor' },
+ content: { side: 'bottom', text: 'Open in Table Editor' },
}}
>
}
- variant="default"
disabled={!canUpdateColumns}
onClick={() => onAddColumn()}
tooltip={{
diff --git a/apps/studio/components/interfaces/ProjectHome/ConnectSection.tsx b/apps/studio/components/interfaces/ProjectHome/ConnectSection.tsx
index 4064f5800a7..1a0e8c03607 100644
--- a/apps/studio/components/interfaces/ProjectHome/ConnectSection.tsx
+++ b/apps/studio/components/interfaces/ProjectHome/ConnectSection.tsx
@@ -95,9 +95,7 @@ export const ConnectSection = () => {
{action.heading}
-
- {action.subheading}!
-
+
{action.subheading}
{
- const { ref } = useParams()
- const { isIndexAdvisorAvailable, isIndexAdvisorEnabled } = useIndexAdvisorStatus()
- const [isDismissed, setIsDismissed] = useLocalStorageQuery(
- LOCAL_STORAGE_KEYS.INDEX_ADVISOR_NOTICE_DISMISSED(ref ?? ''),
- false
- )
-
- if (!isIndexAdvisorAvailable || isIndexAdvisorEnabled || isDismissed) return null
-
- return (
-
-
-
-

-

-
-
-
-
-
-
- Recommends indexes to improve query performance.
-
-
-
-
-
-
-
-
-
- )
-}
diff --git a/apps/studio/components/interfaces/SQLEditor/MonacoEditor.tsx b/apps/studio/components/interfaces/SQLEditor/MonacoEditor.tsx
index e9b1a5184ec..7080fcd8a9f 100644
--- a/apps/studio/components/interfaces/SQLEditor/MonacoEditor.tsx
+++ b/apps/studio/components/interfaces/SQLEditor/MonacoEditor.tsx
@@ -1,16 +1,16 @@
-import Editor, { Monaco, OnMount } from '@monaco-editor/react'
+import { Monaco, OnMount } from '@monaco-editor/react'
import { useDebounce } from '@uidotdev/usehooks'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
+import { noop } from 'lodash'
import { useRouter } from 'next/router'
-import { MutableRefObject, useEffect, useRef, useState } from 'react'
-import { cn } from 'ui'
+import { RefObject, useEffect, useRef, useState } from 'react'
import { Admonition } from 'ui-patterns'
-import { useSetCommandMenuOpen } from 'ui-patterns/CommandMenu'
import type { IStandaloneCodeEditor } from './SQLEditor.types'
import { createSqlSnippetSkeletonV2 } from './SQLEditor.utils'
import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
import { getEditorSelectionParts } from '@/components/ui/AIEditor/utils'
+import { CodeEditor } from '@/components/ui/CodeEditor/CodeEditor'
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { useProfile } from '@/lib/profile'
@@ -19,14 +19,17 @@ import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useIsShortcutEnabled } from '@/state/shortcuts/useIsShortcutEnabled'
import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor-v2'
+import { wasNeverPersisted } from '@/state/sql-editor/sql-editor-lifecycle'
+import { canEditSnippet } from '@/state/sql-editor/sql-editor-rules'
+import { useSqlEditorSaveCoordinator } from '@/state/sql-editor/sql-editor-save-coordinator'
import { useTabsStateSnapshot } from '@/state/tabs'
export type MonacoEditorProps = {
id: string
snippetName: string
className?: string
- editorRef: MutableRefObject
- monacoRef: MutableRefObject
+ editorRef: RefObject
+ monacoRef: RefObject
autoFocus?: boolean
executeQuery: () => void
executeExplainQuery: () => void
@@ -44,7 +47,7 @@ export type MonacoEditorProps = {
placeholder?: string
}
-const MonacoEditor = ({
+export const MonacoEditor = ({
id,
snippetName,
editorRef,
@@ -74,20 +77,12 @@ const MonacoEditor = ({
LOCAL_STORAGE_KEYS.SQL_EDITOR_INTELLISENSE,
true
)
- const isAIAssistantHotkeyEnabled = useIsShortcutEnabled(SHORTCUT_IDS.AI_ASSISTANT_TOGGLE)
- const isCommandMenuHotkeyEnabled = useIsShortcutEnabled(SHORTCUT_IDS.COMMAND_MENU_OPEN)
- const setCommandMenuOpen = useSetCommandMenuOpen()
- // [Joshen] Lodash debounce doesn't seem to be working here, so opting to use useDebounce
const [value, setValue] = useState('')
const debouncedValue = useDebounce(value, 1000)
const snippet = snapV2.snippets[id]
- const disableEdit =
- snippet?.snippet.visibility === 'project' && snippet?.snippet.owner_id !== profile?.id
-
- const executeQueryRef = useRef(executeQuery)
- executeQueryRef.current = executeQuery
+ const disableEdit = !!snippet && !canEditSnippet(snippet.snippet, profile?.id)
const executeExplainQueryRef = useRef(executeExplainQuery)
executeExplainQueryRef.current = executeExplainQuery
@@ -95,22 +90,18 @@ const MonacoEditor = ({
const prettifyQueryRef = useRef(prettifyQuery)
prettifyQueryRef.current = prettifyQuery
+ const isAIAssistantHotkeyEnabled = useIsShortcutEnabled(SHORTCUT_IDS.AI_ASSISTANT_TOGGLE)
const aiHotkeyEnabledRef = useRef(isAIAssistantHotkeyEnabled)
aiHotkeyEnabledRef.current = isAIAssistantHotkeyEnabled
- const commandMenuHotkeyEnabledRef = useRef(isCommandMenuHotkeyEnabled)
- commandMenuHotkeyEnabledRef.current = isCommandMenuHotkeyEnabled
+ const { requestSave } = useSqlEditorSaveCoordinator()
+ const requestSaveRef = useRef(requestSave)
+ requestSaveRef.current = requestSave
- const setCommandMenuOpenRef = useRef(setCommandMenuOpen)
- setCommandMenuOpenRef.current = setCommandMenuOpen
-
- const handleEditorOnMount: OnMount = async (editor, monaco) => {
- editorRef.current = editor
- monacoRef.current = monaco
-
- const model = editorRef.current.getModel()
+ const handleEditorOnMount: OnMount = (editor, monaco) => {
+ const model = editor.getModel()
if (model !== null) {
- monacoRef.current.editor.setModelMarkers(model, 'owner', [])
+ monaco.editor.setModelMarkers(model, 'owner', [])
}
// Blur the editor on Escape so users can hop out to the rest of the UI.
@@ -136,17 +127,6 @@ const MonacoEditor = ({
].join(' && ')
)
- editor.addAction({
- id: 'run-query',
- label: 'Run Query',
- keybindings: [monaco.KeyMod.CtrlCmd + monaco.KeyCode.Enter],
- contextMenuGroupId: 'operation',
- contextMenuOrder: 0,
- run: () => {
- executeQueryRef.current()
- },
- })
-
if (showExplainAction) {
editor.addAction({
id: 'run-explain-query',
@@ -167,7 +147,7 @@ const MonacoEditor = ({
contextMenuGroupId: 'operation',
contextMenuOrder: 0,
run: () => {
- if (snippet) snapV2.addNeedsSaving(snippet.snippet.id)
+ if (snippet) requestSaveRef.current(snippet.snippet.id)
},
})
@@ -188,13 +168,15 @@ const MonacoEditor = ({
contextMenuGroupId: 'operation',
contextMenuOrder: 1,
run: () => {
- const selectedValue = (editorRef?.current as any)
- .getModel()
- .getValueInRange((editorRef?.current as any)?.getSelection())
+ const selection = editorRef?.current?.getSelection()
+ if (!selection) return
+
+ const selectedValue = editorRef?.current?.getModel()?.getValueInRange(selection)
+
openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
aiSnap.newChat({
name: 'Explain code section',
- sqlSnippets: [selectedValue],
+ sqlSnippets: [selectedValue ?? ''],
initialInput: 'Can you explain this section to me in more detail?',
})
},
@@ -223,15 +205,6 @@ const MonacoEditor = ({
})
}
- // Monaco claims Cmd+K as a chord prefix, which swallows the global command
- // menu shortcut while the editor is focused. Intercept it here and open the
- // command menu directly so it works the same inside and outside the editor.
- editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyK, () => {
- if (commandMenuHotkeyEnabledRef.current) {
- setCommandMenuOpenRef.current(true)
- }
- })
-
editor.onDidChangeCursorSelection(({ selection }) => {
const noSelection =
selection.startLineNumber === selection.endLineNumber &&
@@ -239,11 +212,6 @@ const MonacoEditor = ({
onHasSelection(!noSelection)
})
- if (autoFocus) {
- if (editor.getValue().length === 1) editor.setPosition({ lineNumber: 1, column: 2 })
- editor.focus()
- }
-
onMount?.(editor)
}
@@ -263,7 +231,7 @@ const MonacoEditor = ({
// than push. The caller navigated to `/sql/new?content=...` (a long,
// one-shot URL); replacing collapses it out of history so Back returns to
// the originating page instead of a wasted step that re-seeds the snippet.
- if (router.query.content) {
+ if (router.query.content !== undefined) {
router.replace(`/project/${ref}/sql/${snippet.id}`, undefined, { shallow: true })
} else {
router.push(`/project/${ref}/sql/${snippet.id}`, undefined, { shallow: true })
@@ -275,7 +243,7 @@ const MonacoEditor = ({
useEffect(() => {
if (debouncedValue.length > 0 && snippet) {
- const shouldInvalidate = snippet.snippet.isNotSavedInDatabaseYet
+ const shouldInvalidate = wasNeverPersisted(snippet.snippet.status)
snapV2.setSql({ id, sql: value, shouldInvalidate })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -297,29 +265,26 @@ const MonacoEditor = ({
description="This snippet has been shared to the project and is only editable by the owner who created this snippet. You may duplicate this snippet into a personal copy by right clicking on the snippet and selecting “Duplicate query”."
/>
)}
-
>
)
}
-
-export default MonacoEditor
diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ApiAccessToggle.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ApiAccessToggle.tsx
index 60b2eb8fb0c..1d1d590a112 100644
--- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ApiAccessToggle.tsx
+++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ApiAccessToggle.tsx
@@ -322,7 +322,7 @@ export const ApiAccessToggle = ({
- {!hideHeading &&
Data API access
}
+ {!hideHeading &&
Data API Access
}
Allow this table to be queried via Supabase client libraries or the API directly
diff --git a/apps/studio/components/ui/AutoEnableRLSNotice.tsx b/apps/studio/components/ui/AutoEnableRLSNotice.tsx
index 94c40f0a947..bb3b9e7e559 100644
--- a/apps/studio/components/ui/AutoEnableRLSNotice.tsx
+++ b/apps/studio/components/ui/AutoEnableRLSNotice.tsx
@@ -5,6 +5,8 @@ import { useMemo, useState } from 'react'
import { toast } from 'sonner'
import {
Button,
+ Card,
+ CardContent,
Dialog,
DialogContent,
DialogDescription,
@@ -15,7 +17,6 @@ import {
DialogTitle,
DialogTrigger,
} from 'ui'
-import { Admonition } from 'ui-patterns/admonition'
import { CodeBlock } from 'ui-patterns/CodeBlock'
import { AUTO_ENABLE_RLS_EVENT_TRIGGER_SQL } from '@/components/interfaces/Database/Triggers/EventTriggersList/EventTriggers.constants'
@@ -59,13 +60,21 @@ export const AutoEnableRLSNotice = ({ iconOnly }: { iconOnly?: boolean }) => {
}
return (
-
+
+
+
+
+
+
+
+
Auto-enable RLS for new tables
+
+ Create an event trigger that enables Row Level Security on all new tables
+
+
+
+
+
}
@@ -74,9 +83,9 @@ export const AutoEnableRLSNotice = ({ iconOnly }: { iconOnly?: boolean }) => {
tooltip={{ content: { side: 'bottom', text: 'Minimize' } }}
onClick={() => setIsMinimized(true)}
/>
- >
- }
- />
+
+
+
)
}
@@ -122,7 +131,7 @@ const CreateEnsureRLSTriggerDialog = ({ iconOnly }: { iconOnly?: boolean }) => {
tooltip={{ content: { side: 'bottom', text: 'Auto-enable RLS for new tables' } }}
/>
) : (
-
+
)}
diff --git a/e2e/studio/features/api-access-toggle.spec.ts b/e2e/studio/features/api-access-toggle.spec.ts
index 6a8e047d074..46bb1afc080 100644
--- a/e2e/studio/features/api-access-toggle.spec.ts
+++ b/e2e/studio/features/api-access-toggle.spec.ts
@@ -57,14 +57,14 @@ async function verifyTablePrivileges(
}
/**
- * Locates the API access toggle switch for Data API access.
+ * Locates the API access toggle switch for Data API Access.
* Only present when creating or duplicating a table (not when editing).
*/
function getApiAccessSwitch(page: Page) {
const sidePanel = page.getByTestId('table-editor-side-panel')
const dataApiSection = sidePanel
.locator('div')
- .filter({ hasText: 'Data API access' })
+ .filter({ hasText: 'Data API Access' })
.filter({ has: page.getByRole('switch') })
return dataApiSection.getByRole('switch')
}
@@ -239,10 +239,10 @@ test.describe('API Access Toggle', () => {
await page.getByRole('menuitem', { name: 'Edit table' }).click()
await expect(page.getByTestId('table-editor-side-panel')).toBeVisible()
- // Data API access section is visible
+ // Data API Access section is visible
await expect(
- page.getByText('Data API access'),
- 'Data API access label should be visible in edit mode'
+ page.getByText('Data API Access'),
+ 'Data API Access label should be visible in edit mode'
).toBeVisible()
// In edit mode the panel shows a "Manage access" link instead of a toggle switch
diff --git a/packages/ui/src/components/shadcn/ui/dropdown-menu.tsx b/packages/ui/src/components/shadcn/ui/dropdown-menu.tsx
index 8b3b97f54e7..45e630d8075 100644
--- a/packages/ui/src/components/shadcn/ui/dropdown-menu.tsx
+++ b/packages/ui/src/components/shadcn/ui/dropdown-menu.tsx
@@ -32,14 +32,14 @@ const DropdownMenuSubTrigger = React.forwardRef<
{children}
-
+
))
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName