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 ( -
- -
- Index Advisor - Index Advisor -
-
-
-
-
-

Enable Index Advisor

-
-

- 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