diff --git a/apps/studio/components/layouts/Tabs.tsx b/apps/studio/components/layouts/Tabs.tsx
new file mode 100644
index 00000000000..760ede2f516
--- /dev/null
+++ b/apps/studio/components/layouts/Tabs.tsx
@@ -0,0 +1,324 @@
+import {
+ DndContext,
+ DragEndEvent,
+ DragOverlay,
+ PointerSensor,
+ useSensor,
+ useSensors,
+} from '@dnd-kit/core'
+import { horizontalListSortingStrategy, SortableContext, useSortable } from '@dnd-kit/sortable'
+import { CSS } from '@dnd-kit/utilities'
+import { AnimatePresence, motion } from 'framer-motion'
+import { Plus, X } from 'lucide-react'
+import { useRouter } from 'next/router'
+import { useMemo } from 'react'
+import { useSnapshot } from 'valtio'
+
+import { useParams } from 'common'
+import { EntityTypeIcon } from 'components/ui/EntityTypeIcon'
+import { useAppStateSnapshot } from 'state/app-state'
+import {
+ editorEntityTypes,
+ getTabsStore,
+ handleTabClose,
+ handleTabDragEnd,
+ handleTabNavigation,
+ makeTabPermanent,
+ type Tab,
+} from 'state/tabs'
+import { cn, Tabs_Shadcn_, TabsList_Shadcn_, TabsTrigger_Shadcn_ } from 'ui'
+import { useEditorType } from './editors/EditorsLayout.hooks'
+import { CollapseButton } from './Tabs/CollapseButton'
+
+/**
+ * Individual draggable tab component that handles:
+ * - Drag
+ * - Drop functionality
+ * - Dynamic schema name display
+ * - Tab label animations
+ * - Close button interactions
+ */
+const SortableTab = ({
+ tab,
+ index,
+ openTabs,
+ onClose,
+}: {
+ tab: Tab
+ index: number
+ openTabs: Tab[]
+ onClose: (id: string) => void
+}) => {
+ const router = useRouter()
+ const { ref } = useParams()
+ const currentSchema = (router.query.schema as string) || 'public'
+
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
+ id: tab.id,
+ })
+
+ const style = {
+ transform: CSS.Transform.toString(transform),
+ transition,
+ zIndex: isDragging ? 1 : 0,
+ }
+
+ // Update schema visibility check to include URL param comparison
+ const shouldShowSchema = useMemo(() => {
+ // For both table and schema tabs, show schema if:
+ // Any tab has a different schema than the current schema parameter
+ if (tab.type === 'r') {
+ const anyTabHasDifferentSchema = openTabs
+ .filter((t) => t.type === 'r')
+ .some((t) => t.metadata?.schema !== currentSchema)
+
+ return anyTabHasDifferentSchema
+ }
+
+ return false
+ }, [openTabs, currentSchema, tab.type])
+
+ // Create a motion version of TabsTrigger while preserving all functionality
+ // const MotionTabsTrigger = motion(TabsTrigger_Shadcn_)
+
+ return (
+
+ makeTabPermanent(ref, tab.id)}
+ className={cn(
+ 'flex items-center gap-2 px-3 text-xs',
+ 'bg-dash-sidebar/50 dark:bg-surface-100/50',
+ 'data-[state=active]:bg-dash-sidebar dark:data-[state=active]:bg-surface-100',
+ 'border-b border-default',
+ // bottom border active rule
+ 'data-[state=active]:border-b-background-dash-sidebar dark:data-[state=active]:border-b-background-surface-100',
+ 'relative group h-full',
+ 'hover:bg-surface-300 dark:hover:bg-surface-100',
+ tab.isPreview && 'italic font-light' // Optional: style preview tabs differently
+ )}
+ {...listeners}
+ >
+
+
+
+ {shouldShowSchema && (
+
+ {tab?.metadata?.schema}.
+
+ )}
+
+
{tab.label || 'Untitled'}
+
+ {
+ e.preventDefault()
+ e.stopPropagation()
+ }}
+ className="ml-1 opacity-0 group-hover:opacity-100 hover:bg-200 rounded-sm cursor-pointer"
+ onMouseDown={(e) => {
+ e.preventDefault()
+ e.stopPropagation()
+ }}
+ onPointerDown={(e) => {
+ e.preventDefault()
+ e.stopPropagation()
+ onClose(tab.id)
+ }}
+ >
+
+
+
+
+ {index < openTabs.length && (
+
+ )}
+
+ )
+}
+
+const TabPreview = ({ tab }: { tab: string }) => {
+ const { ref } = useParams()
+ const store = getTabsStore(ref)
+ const tabs = useSnapshot(store)
+
+ const tabData = tabs.tabsMap[tab]
+
+ if (!tabData) return null
+
+ return (
+
+
+ {tabData.label || 'Untitled'}
+
+
+ )
+}
+
+export function Tabs() {
+ const { ref } = useParams()
+ const router = useRouter()
+ const appSnap = useAppStateSnapshot()
+
+ const editor = useEditorType()
+ const store = getTabsStore(ref)
+ const tabs = useSnapshot(store)
+ const sensors = useSensors(
+ useSensor(PointerSensor, {
+ activationConstraint: {
+ distance: 1, // Start with a very small distance
+ },
+ })
+ )
+
+ const openTabs = tabs.openTabs
+ .map((id) => tabs.tabsMap[id])
+ .filter((tab) => tab !== undefined) as Tab[]
+
+ const hasNewTab = router.asPath.includes('/new')
+
+ // Filter by editor type - only show SQL tabs for SQL editor and table tabs for table editor
+ const editorTabs = !!editor
+ ? openTabs.filter((tab) => editorEntityTypes[editor]?.includes(tab.type))
+ : []
+
+ const handleDragEnd = (event: DragEndEvent) => {
+ const { active, over } = event
+ if (!over || active.id === over.id) return
+
+ const oldIndex = tabs.openTabs.indexOf(active.id.toString())
+ const newIndex = tabs.openTabs.indexOf(over.id.toString())
+
+ if (oldIndex !== newIndex) {
+ handleTabDragEnd(ref, oldIndex, newIndex, active.id.toString(), router)
+ }
+ }
+
+ const handleClose = (id: string) => {
+ const onClearDashboardHistory = () => {
+ if (ref && editor) {
+ appSnap.setDashboardHistory(ref, editor === 'table' ? 'editor' : editor, undefined)
+ }
+ }
+ handleTabClose({ ref, id, router, editor, onClearDashboardHistory })
+ }
+
+ const handleTabChange = (id: string) => {
+ handleTabNavigation(ref, id, router)
+ }
+
+ return (
+
+
+
+
+ tab.id)}
+ strategy={horizontalListSortingStrategy}
+ >
+ {editorTabs.map((tab, index) => (
+ handleClose(tab.id)}
+ />
+ ))}
+
+
+ {/* Non-draggable new tab */}
+ {hasNewTab && (
+ <>
+
+
+
+ New
+
+ {
+ e.preventDefault()
+ e.stopPropagation()
+ }}
+ className="ml-1 opacity-0 group-hover:opacity-100 hover:bg-200 rounded-sm cursor-pointer"
+ onMouseDown={(e) => {
+ e.preventDefault()
+ e.stopPropagation()
+ }}
+ onPointerDown={(e) => {
+ e.preventDefault()
+ e.stopPropagation()
+ handleClose('new')
+ }}
+ >
+
+ {' '}
+
+
+ >
+ )}
+
+
+ {!hasNewTab && (
+
+ router.push(
+ `/project/${router.query.ref}/${editor === 'table' ? 'editor' : 'sql'}/new?skip=true`
+ )
+ }
+ initial={{ opacity: 0, scale: 0.8, x: -10 }}
+ animate={{ opacity: 1, scale: 1, x: 0 }}
+ transition={{ duration: 0.2 }}
+ >
+
+
+ )}
+
+
+
+
+
+ {tabs.activeTab ? : null}
+
+
+ )
+}
diff --git a/apps/studio/components/layouts/Tabs/ActionCard.tsx b/apps/studio/components/layouts/Tabs/ActionCard.tsx
new file mode 100644
index 00000000000..14f5472b2df
--- /dev/null
+++ b/apps/studio/components/layouts/Tabs/ActionCard.tsx
@@ -0,0 +1,34 @@
+import { Badge, Card } from 'ui'
+
+export const ActionCard = (card: {
+ icon: JSX.Element
+ title: string
+ description: string
+ bgColor: string
+ isBeta?: boolean
+ onClick?: () => void
+}) => {
+ return (
+
+
+ {card.isBeta && (
+
+ Coming soon
+
+ )}
+
+ {card.icon}
+
+
+
{card.title}
+
{card.description}
+
+
+
+ )
+}
diff --git a/apps/studio/components/layouts/Tabs/CollapseButton.tsx b/apps/studio/components/layouts/Tabs/CollapseButton.tsx
new file mode 100644
index 00000000000..90d985e2422
--- /dev/null
+++ b/apps/studio/components/layouts/Tabs/CollapseButton.tsx
@@ -0,0 +1,37 @@
+import { PanelLeftClose, PanelLeftOpen } from 'lucide-react'
+
+import { useAppStateSnapshot } from 'state/app-state'
+import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
+
+export function CollapseButton({ hideTabs }: { hideTabs: boolean }) {
+ const { showSidebar, setShowSidebar } = useAppStateSnapshot()
+
+ return (
+
+
+ setShowSidebar(!showSidebar)}
+ >
+ {showSidebar ? (
+
+ ) : (
+
+ )}
+
+
+ {true ? 'Collapse' : 'Expand'} sidebar
+
+ )
+}
diff --git a/apps/studio/components/layouts/Tabs/MigrateToPostgres.tsx b/apps/studio/components/layouts/Tabs/MigrateToPostgres.tsx
new file mode 100644
index 00000000000..e2629ecc6bf
--- /dev/null
+++ b/apps/studio/components/layouts/Tabs/MigrateToPostgres.tsx
@@ -0,0 +1,46 @@
+import { motion } from 'framer-motion'
+import { BaggageClaim, BookOpen } from 'lucide-react'
+import { Button } from 'ui'
+
+// not in use yet
+export const MigrateToPostgres = () => (
+
+
+
+
+
+
+
+
+
+
Migrate your Database to Postgres
+
+ Already have a database? Migrate any database to Supabase to access backups, RESTful
+ auto APIs, Authentication and more.
+
+
+
+
+
}>
+ Migrate to Postgres
+
+
+
+)
diff --git a/apps/studio/components/layouts/Tabs/NewTab.tsx b/apps/studio/components/layouts/Tabs/NewTab.tsx
new file mode 100644
index 00000000000..25718d60ba1
--- /dev/null
+++ b/apps/studio/components/layouts/Tabs/NewTab.tsx
@@ -0,0 +1,188 @@
+import { PermissionAction } from '@supabase/shared-types/out/constants'
+import { partition } from 'lodash'
+import { Table2 } from 'lucide-react'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import { toast } from 'sonner'
+
+import { useParams } from 'common'
+import { SQL_TEMPLATES } from 'components/interfaces/SQLEditor/SQLEditor.queries'
+import { createSqlSnippetSkeletonV2 } from 'components/interfaces/SQLEditor/SQLEditor.utils'
+import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
+import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
+import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
+import { uuidv4 } from 'lib/helpers'
+import { useProfile } from 'lib/profile'
+import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2'
+import { useTableEditorStateSnapshot } from 'state/table-editor'
+import { getTabsStore } from 'state/tabs'
+import {
+ Button,
+ cn,
+ SQL_ICON,
+ Tabs_Shadcn_,
+ TabsContent_Shadcn_,
+ TabsList_Shadcn_,
+ TabsTrigger_Shadcn_,
+} from 'ui'
+import { useEditorType } from '../editors/EditorsLayout.hooks'
+import { useProjectContext } from '../ProjectLayout/ProjectContext'
+import { ActionCard } from './ActionCard'
+import { RecentItems } from './RecentItems'
+
+export function NewTab() {
+ const router = useRouter()
+ const { ref } = useParams()
+ const editor = useEditorType()
+ const snap = useTableEditorStateSnapshot()
+ const { profile } = useProfile()
+ const [templates] = partition(SQL_TEMPLATES, { type: 'template' })
+ const [quickstarts] = partition(SQL_TEMPLATES, { type: 'quickstart' })
+ const { mutate: sendEvent } = useSendEventMutation()
+ const snapV2 = useSqlEditorV2StateSnapshot()
+ const { project } = useProjectContext()
+ const org = useSelectedOrganization()
+
+ const canCreateSQLSnippet = useCheckPermissions(PermissionAction.CREATE, 'user_content', {
+ resource: { type: 'sql', owner_id: profile?.id },
+ subject: { id: profile?.id },
+ })
+
+ const tableEditorActions = [
+ {
+ icon:
,
+ title: 'Create a table',
+ description: 'Design and create a new database table',
+ bgColor: 'bg-blue-500',
+ isBeta: false,
+ onClick: snap.onAddTable,
+ },
+ ]
+
+ const sqlEditorActions = [
+ {
+ icon:
,
+ title: 'New SQL Snippet',
+ description: 'Execute SQL queries',
+ bgColor: 'bg-green-500',
+ isBeta: false,
+ onClick: () => router.push(`/project/${ref}/sql/new`),
+ },
+ ]
+
+ const actions = editor === 'sql' ? sqlEditorActions : tableEditorActions
+
+ const handleNewQuery = async (sql: string, name: string) => {
+ 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) {
+ return toast('Your queries will not be saved as you do not have sufficient permissions')
+ }
+
+ try {
+ const snippet = createSqlSnippetSkeletonV2({
+ id: uuidv4(),
+ name,
+ sql,
+ owner_id: profile?.id,
+ project_id: project?.id,
+ })
+ snapV2.addSnippet({ projectRef: ref, snippet })
+ snapV2.addNeedsSaving(snippet.id)
+
+ const store = getTabsStore(ref)
+ const tabId = `sql-${snippet.id}`
+ store.openTabs = [...store.openTabs, tabId]
+ store.tabsMap[tabId] = {
+ id: tabId,
+ type: 'sql',
+ label: name,
+ metadata: { sqlId: snippet.id },
+ }
+ store.activeTab = tabId
+
+ router.push(`/project/${ref}/sql/${snippet.id}`)
+ } catch (error: any) {
+ toast.error(`Failed to create new query: ${error.message}`)
+ }
+ }
+
+ return (
+
+
+
+ {actions.map((item, i) => (
+
+ ))}
+
+
+
+ {editor === 'sql' && (
+
+
+
+ Templates
+ Quickstarts
+
+
+
+ {templates.slice(0, 9).map((item, i) => (
+
{
+ handleNewQuery(item.sql, item.title)
+ sendEvent({
+ action: 'sql_editor_template_clicked',
+ properties: { templateName: item.title },
+ groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' },
+ })
+ }}
+ bgColor="bg-alternative border"
+ key={`action-card-${i}`}
+ {...item}
+ icon={
+
+ }
+ />
+ ))}
+
+
+
+ View more templates
+
+
+
+
+
+ {quickstarts.map((item, i) => (
+
{
+ handleNewQuery(item.sql, item.title)
+ sendEvent({
+ action: 'sql_editor_quickstart_clicked',
+ properties: { quickstartName: item.title },
+ groups: { project: ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' },
+ })
+ }}
+ bgColor="bg-alternative border"
+ key={`action-card-${i}`}
+ {...item}
+ icon={
+
+ }
+ />
+ ))}
+
+
+
+ View more templates
+
+
+
+
+
+ )}
+
+ )
+}
diff --git a/apps/studio/components/layouts/Tabs/RecentItems.tsx b/apps/studio/components/layouts/Tabs/RecentItems.tsx
new file mode 100644
index 00000000000..4fc55e3c4f4
--- /dev/null
+++ b/apps/studio/components/layouts/Tabs/RecentItems.tsx
@@ -0,0 +1,98 @@
+import dayjs from 'dayjs'
+import relativeTime from 'dayjs/plugin/relativeTime'
+import { AnimatePresence, motion } from 'framer-motion'
+import Link from 'next/link'
+import { useSnapshot } from 'valtio'
+
+import { useParams } from 'common'
+import { EntityTypeIcon } from 'components/ui/EntityTypeIcon'
+import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
+import type { RecentItem } from 'state/recent-items'
+import { getRecentItemsStore } from 'state/recent-items'
+import { editorEntityTypes } from 'state/tabs'
+import { useEditorType } from '../editors/EditorsLayout.hooks'
+
+dayjs.extend(relativeTime)
+
+export function RecentItems() {
+ const { ref } = useParams()
+ const store = getRecentItemsStore(ref)
+ const recentItemsSnap = useSnapshot(store)
+ const editor = useEditorType()
+
+ if (!recentItemsSnap?.items || !ref) return null
+
+ // Filter items based on editor type
+ const filteredItems = !editor
+ ? [...recentItemsSnap.items]
+ : recentItemsSnap.items.filter((item) => editorEntityTypes[editor].includes(item.type))
+
+ const sortedItems = filteredItems.sort((a, b) => b.timestamp - a.timestamp)
+
+ return (
+
+
Recent items
+
+ {sortedItems.length === 0 ? (
+
+
+
+
No recent items yet
+
+ Items will appear here as you browse through your project
+
+
+
+ ) : (
+
+
+ {sortedItems.map((item: RecentItem, index: number) => (
+
+
+
+
+
+
+
+ {item.metadata?.schema}
+ {item.metadata?.schema && . }
+ {item.label || 'Untitled'}
+
+
+
+ {dayjs(item.timestamp).fromNow()}
+
+
+
+
+ ))}
+
+
+ )}
+
+
+ )
+}
diff --git a/apps/studio/components/layouts/Tabs/Tabs.utils.ts b/apps/studio/components/layouts/Tabs/Tabs.utils.ts
new file mode 100644
index 00000000000..e9636ab5177
--- /dev/null
+++ b/apps/studio/components/layouts/Tabs/Tabs.utils.ts
@@ -0,0 +1,98 @@
+import { Entity } from 'data/entity-types/entity-types-infinite-query'
+import {
+ getRecentItemsByType,
+ getRecentItemsStore,
+ RecentItem,
+ removeRecentItems,
+} from 'state/recent-items'
+import { createTabId, getTabsStore, removeTabs } from 'state/tabs'
+
+export const tableEditorTabsCleanUp = ({
+ ref,
+ schemas,
+ entities,
+}: {
+ ref: string
+ schemas: string[]
+ entities: Entity[]
+}) => {
+ // Identity all entities by tab ID
+ const entitiesById: string[] = entities.map((x: Entity) => createTabId(x.type, { id: x.id }))
+
+ // Remove any recent items that no longer exists in the database
+ const recentItemsStore = getRecentItemsStore(ref)
+
+ const recentItemsFilteredToSchemas: string[] = []
+ for (const schema of schemas) {
+ recentItemsFilteredToSchemas.push(
+ ...recentItemsStore?.items
+ ?.filter((x: RecentItem) => x.metadata?.schema === schema)
+ .map((x) => x.id)
+ )
+ }
+
+ const recentItemsToRemove = [
+ ...recentItemsFilteredToSchemas.filter((entityId) => !entitiesById.includes(entityId)),
+ ]
+
+ removeRecentItems(ref, recentItemsToRemove)
+
+ // Close any tabs which might have been opened but no longer exists in the database
+ const tabsStore = getTabsStore(ref)
+
+ const tabsFilteredToSchemas: string[] = []
+ for (const schema of schemas) {
+ tabsFilteredToSchemas.push(
+ ...tabsStore?.openTabs?.filter((tabId) => {
+ const tab = tabsStore.tabsMap[tabId]
+ return tab.metadata?.schema === schema
+ })
+ )
+ }
+
+ const tableEditorTabsToBeCleaned = [
+ ...tabsFilteredToSchemas.filter(
+ (id) => !id.startsWith('sql') && !id.startsWith('schema') && !entitiesById.includes(id)
+ ),
+ ]
+
+ // perform tabs cleanup
+ removeTabs(ref, tableEditorTabsToBeCleaned)
+}
+
+export const sqlEditorTabsCleanup = ({
+ ref,
+ snippets,
+}: {
+ ref: string
+ snippets: { id: string; type: string }[]
+}) => {
+ // these are tabs that are static content
+ // these canot be removed from localstorage based on this query request
+ const IGNORED_TAB_IDS = ['sql-templates', 'sql-quickstarts']
+
+ // Identify all SQL snippets / content by their tab ids
+ const currentContentIds = [
+ ...snippets
+ .filter((content) => content.type === 'sql')
+ .map((content) => createTabId('sql', { id: content.id })),
+ // append ignored tab IDs
+ ...IGNORED_TAB_IDS,
+ ]
+
+ // Remove any snippet tabs that might no longer be existing (removed outside of the dashboard session)
+ const tabsStore = getTabsStore(ref)
+ const snippetTabsToBeCleaned = tabsStore.openTabs.filter(
+ (id: string) => id.startsWith('sql') && !currentContentIds.includes(id)
+ )
+ removeTabs(ref, snippetTabsToBeCleaned)
+
+ // Remove any recent items that might no longer be existing (removed outside of the dashboard session)
+ const recentItems = getRecentItemsByType(ref, 'sql')
+ removeRecentItems(
+ ref,
+ recentItems
+ ? recentItems.filter((item) => !currentContentIds.includes(item.id)).map((item) => item.id)
+ : []
+ )
+}
diff --git a/apps/studio/components/layouts/editors/EditorBaseLayout.tsx b/apps/studio/components/layouts/editors/EditorBaseLayout.tsx
new file mode 100644
index 00000000000..7bbb23035b0
--- /dev/null
+++ b/apps/studio/components/layouts/editors/EditorBaseLayout.tsx
@@ -0,0 +1,52 @@
+import { usePathname } from 'next/navigation'
+import { ComponentProps, ReactNode } from 'react'
+
+import { useParams } from 'common'
+import {
+ useIsSQLEditorTabsEnabled,
+ useIsTableEditorTabsEnabled,
+} from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
+import { cn } from 'ui'
+import { ProjectLayoutWithAuth } from '../ProjectLayout/ProjectLayout'
+import { Tabs } from '../Tabs'
+import { CollapseButton } from '../Tabs/CollapseButton'
+import { useEditorType } from './EditorsLayout.hooks'
+
+export interface ExplorerLayoutProps extends ComponentProps
{
+ children: ReactNode
+ hideTabs?: boolean
+ title?: string
+ product?: string
+}
+
+export const EditorBaseLayout = ({ children, title, product, ...props }: ExplorerLayoutProps) => {
+ const { ref } = useParams()
+ const pathname = usePathname()
+ const editor = useEditorType()
+
+ const isTableEditorTabsEnabled = useIsTableEditorTabsEnabled()
+ const isSQLEditorTabsEnabled = useIsSQLEditorTabsEnabled()
+
+ const tableEditorTabsEnabled = editor === 'table' && isTableEditorTabsEnabled
+ const sqlEditorTabsEnabled = editor === 'sql' && isSQLEditorTabsEnabled
+ const hideTabs = pathname === `/project/${ref}/editor` || pathname === `/project/${ref}/sql`
+
+ return (
+
+
+ {tableEditorTabsEnabled || sqlEditorTabsEnabled ? (
+
+ {hideTabs && }
+ {!hideTabs && }
+
+ ) : null}
+
{children}
+
+
+ )
+}
diff --git a/apps/studio/components/layouts/editors/EditorsLayout.hooks.ts b/apps/studio/components/layouts/editors/EditorsLayout.hooks.ts
new file mode 100644
index 00000000000..1495cc2b374
--- /dev/null
+++ b/apps/studio/components/layouts/editors/EditorsLayout.hooks.ts
@@ -0,0 +1,13 @@
+import { useParams } from 'common'
+import { usePathname } from 'next/navigation'
+
+export function useEditorType(): 'table' | 'sql' | undefined {
+ const pathname = usePathname()
+ const { ref } = useParams()
+
+ return pathname?.includes(`/project/${ref}/editor`)
+ ? 'table'
+ : pathname?.includes(`/project/${ref}/sql`)
+ ? 'sql'
+ : undefined
+}
diff --git a/apps/studio/data/entity-types/entity-types-infinite-query.ts b/apps/studio/data/entity-types/entity-types-infinite-query.ts
index ab8e7cf0bcc..6afdb4f9a0f 100644
--- a/apps/studio/data/entity-types/entity-types-infinite-query.ts
+++ b/apps/studio/data/entity-types/entity-types-infinite-query.ts
@@ -1,5 +1,6 @@
import { QueryClient, useInfiniteQuery, UseInfiniteQueryOptions } from '@tanstack/react-query'
import { executeSql, ExecuteSqlVariables } from 'data/sql/execute-sql-query'
+import { ResponseError } from 'types'
import { ENTITY_TYPE } from './entity-type-constants'
import { entityTypeKeys } from './keys'
@@ -114,7 +115,7 @@ export async function getEntityTypes(
}
export type EntityTypesData = Awaited>
-export type EntityTypesError = unknown
+export type EntityTypesError = ResponseError
export const useEntityTypesQuery = (
{
@@ -130,8 +131,8 @@ export const useEntityTypesQuery = (
enabled = true,
...options
}: UseInfiniteQueryOptions = {}
-) =>
- useInfiniteQuery(
+) => {
+ return useInfiniteQuery(
entityTypeKeys.list(projectRef, { schemas, search, sort, limit, filterTypes }),
({ signal, pageParam }) =>
getEntityTypes(
@@ -163,6 +164,7 @@ export const useEntityTypesQuery = (
...options,
}
)
+}
export function prefetchEntityTypes(
client: QueryClient,
diff --git a/apps/studio/data/tables/tables-query.ts b/apps/studio/data/tables/tables-query.ts
index b6187f0a76d..76463bc224e 100644
--- a/apps/studio/data/tables/tables-query.ts
+++ b/apps/studio/data/tables/tables-query.ts
@@ -59,6 +59,7 @@ export async function getTables(
if (Array.isArray(data) && sortByProperty) {
return sortBy(data, (t) => t[sortByProperty]) as PostgresTable[]
}
+
return data as Omit[]
}
diff --git a/apps/studio/lib/constants/index.ts b/apps/studio/lib/constants/index.ts
index 1e9fe8b0845..d84cc68ed39 100644
--- a/apps/studio/lib/constants/index.ts
+++ b/apps/studio/lib/constants/index.ts
@@ -50,6 +50,9 @@ export const LOCAL_STORAGE_KEYS = {
UI_PREVIEW_INLINE_EDITOR: 'supabase-ui-preview-inline-editor',
UI_ONBOARDING_NEW_PAGE_SHOWN: 'supabase-ui-onboarding-new-page-shown',
+ UI_TABLE_EDITOR_TABS: 'supabase-ui-table-editor-tabs',
+ UI_SQL_EDITOR_TABS: 'supabase-ui-sql-editor-tabs',
+
SQL_SCRATCH_PAD_BANNER_ACKNOWLEDGED: 'supabase-sql-scratch-pad-banner-acknowledged',
DASHBOARD_HISTORY: (ref: string) => `dashboard-history-${ref}`,
diff --git a/apps/studio/next.config.js b/apps/studio/next.config.js
index 55fc4c9417e..c2e729d23fa 100644
--- a/apps/studio/next.config.js
+++ b/apps/studio/next.config.js
@@ -380,11 +380,6 @@ const nextConfig = {
destination: '/org/_/billing',
permanent: true,
},
- {
- source: '/project/:ref/sql',
- destination: '/project/:ref/sql/new',
- permanent: true,
- },
{
permanent: true,
source: '/project/:ref/reports/linter',
diff --git a/apps/studio/package.json b/apps/studio/package.json
index 801cd1fcbc4..23c2c6aecd4 100644
--- a/apps/studio/package.json
+++ b/apps/studio/package.json
@@ -25,6 +25,9 @@
"@ai-sdk/openai": "^0.0.72",
"@dagrejs/dagre": "^1.0.4",
"@deno/eszip": "0.83.0",
+ "@dnd-kit/core": "^6.1.0",
+ "@dnd-kit/sortable": "^8.0.0",
+ "@dnd-kit/utilities": "^3.2.2",
"@electric-sql/pglite": "^0.2.15",
"@graphiql/react": "^0.19.4",
"@graphiql/toolkit": "^0.9.1",
diff --git a/apps/studio/pages/api/platform/projects/[ref]/content/count.ts b/apps/studio/pages/api/platform/projects/[ref]/content/count.ts
index 10c30fe73e5..875d4c1f8ab 100644
--- a/apps/studio/pages/api/platform/projects/[ref]/content/count.ts
+++ b/apps/studio/pages/api/platform/projects/[ref]/content/count.ts
@@ -20,5 +20,5 @@ type ResponseData =
paths['/platform/projects/{ref}/content/count']['get']['responses']['200']['content']['application/json']
const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => {
- return res.status(200).json({ shared: 1, favorites: 1, private: 1 })
+ return res.status(200).json({ shared: 0, favorites: 0, private: 1 })
}
diff --git a/apps/studio/pages/api/platform/projects/[ref]/content/index.ts b/apps/studio/pages/api/platform/projects/[ref]/content/index.ts
index b3325c69333..9bc53912644 100644
--- a/apps/studio/pages/api/platform/projects/[ref]/content/index.ts
+++ b/apps/studio/pages/api/platform/projects/[ref]/content/index.ts
@@ -28,6 +28,11 @@ type GetResponseData =
const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => {
// Platform specific endpoint
+ const { favorite, visibility } = req.query
+ if (favorite || visibility === 'project') {
+ return res.status(200).json({ data: [] })
+ }
+
const snippets = [
{
id: '1',
diff --git a/apps/studio/pages/project/[ref]/database/column-privileges.tsx b/apps/studio/pages/project/[ref]/database/column-privileges.tsx
index 4111c0b4076..0409e6a391a 100644
--- a/apps/studio/pages/project/[ref]/database/column-privileges.tsx
+++ b/apps/studio/pages/project/[ref]/database/column-privileges.tsx
@@ -4,7 +4,7 @@ import Link from 'next/link'
import { useCallback, useMemo, useState } from 'react'
import { toast } from 'sonner'
-import { useFeaturePreviewContext } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
+import { useIsColumnLevelPrivilegesEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import {
getDefaultColumnCheckedStates,
getDefaultTableCheckedStates,
@@ -15,6 +15,7 @@ import PrivilegesHead from 'components/interfaces/Database/Privileges/Privileges
import PrivilegesTable from 'components/interfaces/Database/Privileges/PrivilegesTable'
import ProtectedSchemaWarning from 'components/interfaces/Database/ProtectedSchemaWarning'
import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout'
+import DefaultLayout from 'components/layouts/DefaultLayout'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
import AlertError from 'components/ui/AlertError'
@@ -31,7 +32,6 @@ import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
import { useAppStateSnapshot } from 'state/app-state'
import type { NextPageWithLayout } from 'types'
import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui'
-import DefaultLayout from 'components/layouts/DefaultLayout'
const EDITABLE_ROLES = ['authenticated', 'anon', 'service_role']
@@ -39,10 +39,7 @@ const PrivilegesPage: NextPageWithLayout = () => {
const { ref, table: paramTable } = useParams()
const { project } = useProjectContext()
const snap = useAppStateSnapshot()
-
- const featurePreviewContext = useFeaturePreviewContext()
- const { flags } = featurePreviewContext
- const isEnabled = flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_CLS]
+ const isEnabled = useIsColumnLevelPrivilegesEnabled()
const { selectedSchema, setSelectedSchema } = useQuerySchemaState()
const [selectedTable, setSelectedTable] = useState(paramTable)
diff --git a/apps/studio/pages/project/[ref]/editor/[id].tsx b/apps/studio/pages/project/[ref]/editor/[id].tsx
index 36438f75894..ea6977367ca 100644
--- a/apps/studio/pages/project/[ref]/editor/[id].tsx
+++ b/apps/studio/pages/project/[ref]/editor/[id].tsx
@@ -1,19 +1,24 @@
import { useRouter } from 'next/router'
-import { useCallback } from 'react'
+import { useCallback, useEffect } from 'react'
-import { useParams } from 'common/hooks'
+import { useParams } from 'common'
+import { useIsTableEditorTabsEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { TableGridEditor } from 'components/interfaces/TableGridEditor'
import DeleteConfirmationDialogs from 'components/interfaces/TableGridEditor/DeleteConfirmationDialogs'
import DefaultLayout from 'components/layouts/DefaultLayout'
+import { EditorBaseLayout } from 'components/layouts/editors/EditorBaseLayout'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import TableEditorLayout from 'components/layouts/TableEditorLayout/TableEditorLayout'
+import TableEditorMenu from 'components/layouts/TableEditorLayout/TableEditorMenu'
import { useTableEditorQuery } from 'data/table-editor/table-editor-query'
import { TablesData } from 'data/tables/tables-query'
+import { addTab, createTabId, getTabsStore } from 'state/tabs'
import type { NextPageWithLayout } from 'types'
const TableEditorPage: NextPageWithLayout = () => {
const { id: _id, ref: projectRef } = useParams()
const id = _id ? Number(_id) : undefined
+ const store = getTabsStore(projectRef)
const { project } = useProjectContext()
const { data: selectedTable, isLoading } = useTableEditorQuery({
@@ -35,6 +40,36 @@ const TableEditorPage: NextPageWithLayout = () => {
[router, projectRef]
)
+ /**
+ * Effect: Creates or updates tab when table is loaded
+ * Runs when:
+ * - selectedTable changes (when a new table is loaded)
+ * - id changes (when URL parameter changes)
+ */
+ const isTableEditorTabsEnabled = useIsTableEditorTabsEnabled()
+
+ useEffect(() => {
+ // tabs preview flag logic
+ if (isTableEditorTabsEnabled && selectedTable && projectRef) {
+ const tabId = createTabId(selectedTable.entity_type, { id: selectedTable.id })
+ if (!store.tabsMap[tabId]) {
+ addTab(projectRef, {
+ id: tabId,
+ type: selectedTable.entity_type,
+ label: selectedTable.name,
+ metadata: {
+ schema: selectedTable.schema,
+ name: selectedTable.name,
+ tableId: id,
+ },
+ })
+ } else {
+ // If tab already exists, just make it active
+ store.activeTab = tabId
+ }
+ }
+ }, [selectedTable, id, projectRef, isTableEditorTabsEnabled])
+
return (
<>
@@ -48,7 +83,9 @@ const TableEditorPage: NextPageWithLayout = () => {
TableEditorPage.getLayout = (page) => (
- {page}
+ } product="Table Editor">
+ {page}
+
)
diff --git a/apps/studio/pages/project/[ref]/editor/index.tsx b/apps/studio/pages/project/[ref]/editor/index.tsx
index ca9392c8485..2f3bebfa86f 100644
--- a/apps/studio/pages/project/[ref]/editor/index.tsx
+++ b/apps/studio/pages/project/[ref]/editor/index.tsx
@@ -1,24 +1,48 @@
-import { useRouter } from 'next/router'
-
import { useParams } from 'common'
-
+import { useIsTableEditorTabsEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import EmptyState from 'components/interfaces/TableGridEditor/EmptyState'
import SidePanelEditor from 'components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor'
import DefaultLayout from 'components/layouts/DefaultLayout'
+import { EditorBaseLayout } from 'components/layouts/editors/EditorBaseLayout'
import TableEditorLayout from 'components/layouts/TableEditorLayout/TableEditorLayout'
+import TableEditorMenu from 'components/layouts/TableEditorLayout/TableEditorMenu'
+import { NewTab } from 'components/layouts/Tabs/NewTab'
+import { useRouter } from 'next/router'
+import { useEffect } from 'react'
+import { useAppStateSnapshot } from 'state/app-state'
+import { editorEntityTypes, getTabsStore } from 'state/tabs'
import type { NextPageWithLayout } from 'types'
const TableEditorPage: NextPageWithLayout = () => {
- const { ref: projectRef } = useParams()
const router = useRouter()
+ const { ref: projectRef } = useParams()
+ const tabStore = getTabsStore(projectRef)
+ const appSnap = useAppStateSnapshot()
+ const isTableEditorTabsEnabled = useIsTableEditorTabsEnabled()
const onTableCreated = (table: { id: number }) => {
router.push(`/project/${projectRef}/editor/${table.id}`)
}
+ useEffect(() => {
+ if (isTableEditorTabsEnabled) {
+ const lastOpenedTab = appSnap.dashboardHistory.editor
+ const lastTabId = tabStore.openTabs.find((id) =>
+ editorEntityTypes.table.includes(tabStore.tabsMap[id]?.type)
+ )
+ if (lastOpenedTab !== undefined) {
+ router.push(`/project/${projectRef}/editor/${appSnap.dashboardHistory.editor}`)
+ } else if (lastTabId) {
+ // Handle redirect to last opened table tab, or last table tab
+ const lastTab = tabStore.tabsMap[lastTabId]
+ if (lastTab) router.push(`/project/${projectRef}/editor/${lastTab.metadata?.tableId}`)
+ }
+ }
+ }, [isTableEditorTabsEnabled])
+
return (
<>
-
+ {isTableEditorTabsEnabled ? : }
>
)
@@ -26,7 +50,9 @@ const TableEditorPage: NextPageWithLayout = () => {
TableEditorPage.getLayout = (page) => (
- {page}
+ } product="Table Editor">
+ {page}
+
)
diff --git a/apps/studio/pages/project/[ref]/editor/new.tsx b/apps/studio/pages/project/[ref]/editor/new.tsx
new file mode 100644
index 00000000000..3a2e12a9e3b
--- /dev/null
+++ b/apps/studio/pages/project/[ref]/editor/new.tsx
@@ -0,0 +1,36 @@
+import { useRouter } from 'next/router'
+
+import { useParams } from 'common'
+import { SidePanelEditor } from 'components/interfaces/TableGridEditor'
+import DefaultLayout from 'components/layouts/DefaultLayout'
+import { EditorBaseLayout } from 'components/layouts/editors/EditorBaseLayout'
+import TableEditorLayout from 'components/layouts/TableEditorLayout/TableEditorLayout'
+import TableEditorMenu from 'components/layouts/TableEditorLayout/TableEditorMenu'
+import { NewTab } from 'components/layouts/Tabs/NewTab'
+import type { NextPageWithLayout } from 'types'
+
+const EditorNewPage: NextPageWithLayout = () => {
+ const router = useRouter()
+ const { ref: projectRef } = useParams()
+
+ const onTableCreated = (table: { id: number }) => {
+ router.push(`/project/${projectRef}/editor/${table.id}`)
+ }
+
+ return (
+ <>
+
+
+ >
+ )
+}
+
+EditorNewPage.getLayout = (page) => (
+
+ } product="Table Editor">
+ {page}
+
+
+)
+
+export default EditorNewPage
diff --git a/apps/studio/pages/project/[ref]/sql/[id].tsx b/apps/studio/pages/project/[ref]/sql/[id].tsx
index 38f310f2078..8b37d86603f 100644
--- a/apps/studio/pages/project/[ref]/sql/[id].tsx
+++ b/apps/studio/pages/project/[ref]/sql/[id].tsx
@@ -2,13 +2,17 @@ import { useRouter } from 'next/router'
import { useEffect } from 'react'
import { useParams } from 'common/hooks/useParams'
+import { useIsSQLEditorTabsEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { SQLEditor } from 'components/interfaces/SQLEditor/SQLEditor'
+import DefaultLayout from 'components/layouts/DefaultLayout'
+import { EditorBaseLayout } from 'components/layouts/editors/EditorBaseLayout'
import SQLEditorLayout from 'components/layouts/SQLEditorLayout/SQLEditorLayout'
+import { SQLEditorMenu } from 'components/layouts/SQLEditorLayout/SQLEditorMenu'
import { useContentIdQuery } from 'data/content/content-id-query'
import { useAppStateSnapshot } from 'state/app-state'
import { SnippetWithContent, useSnippets, useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2'
+import { addTab, createTabId } from 'state/tabs'
import type { NextPageWithLayout } from 'types'
-import DefaultLayout from 'components/layouts/DefaultLayout'
const SqlEditor: NextPageWithLayout = () => {
const router = useRouter()
@@ -51,16 +55,36 @@ const SqlEditor: NextPageWithLayout = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, allSnippets, content])
- return (
-
-
-
- )
+ const isSqlEditorTabsEnabled = useIsSQLEditorTabsEnabled()
+
+ // Watch for route changes
+ useEffect(() => {
+ if (isSqlEditorTabsEnabled) {
+ if (!router.isReady || !id || id === 'new') return
+
+ const tabId = createTabId('sql', { id })
+ const snippet = allSnippets.find((x) => x.id === id)
+
+ addTab(ref, {
+ id: tabId,
+ type: 'sql',
+ label: snippet?.name || 'Untitled Query',
+ metadata: {
+ sqlId: id,
+ name: snippet?.name,
+ },
+ })
+ }
+ }, [router.isReady, id, isSqlEditorTabsEnabled])
+
+ return
}
SqlEditor.getLayout = (page) => (
- {page}
+ } product="SQL Editor">
+ {page}
+
)
diff --git a/apps/studio/pages/project/[ref]/sql/index.tsx b/apps/studio/pages/project/[ref]/sql/index.tsx
new file mode 100644
index 00000000000..44e2019cf0a
--- /dev/null
+++ b/apps/studio/pages/project/[ref]/sql/index.tsx
@@ -0,0 +1,49 @@
+import { useParams } from 'common'
+import { useIsSQLEditorTabsEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
+import DefaultLayout from 'components/layouts/DefaultLayout'
+import { EditorBaseLayout } from 'components/layouts/editors/EditorBaseLayout'
+import SQLEditorLayout from 'components/layouts/SQLEditorLayout/SQLEditorLayout'
+import { SQLEditorMenu } from 'components/layouts/SQLEditorLayout/SQLEditorMenu'
+import { NewTab } from 'components/layouts/Tabs/NewTab'
+import { useRouter } from 'next/router'
+import { useEffect } from 'react'
+import { useAppStateSnapshot } from 'state/app-state'
+import { getTabsStore } from 'state/tabs'
+import type { NextPageWithLayout } from 'types'
+
+const TableEditorPage: NextPageWithLayout = () => {
+ const router = useRouter()
+ const { ref: projectRef } = useParams()
+ const store = getTabsStore(projectRef)
+ const appSnap = useAppStateSnapshot()
+ const isSqlEditorTabsEnabled = useIsSQLEditorTabsEnabled()
+
+ useEffect(() => {
+ if (!isSqlEditorTabsEnabled) {
+ // Redirect to /new if not using tabs
+ router.push(`/project/${projectRef}/sql/new`)
+ } else {
+ // Handle redirect to last opened snippet tab, or last snippet tab
+ const lastOpenedTab = appSnap.dashboardHistory.sql
+ const lastTabId = store.openTabs.find((id) => store.tabsMap[id]?.type === 'sql')
+ if (lastOpenedTab !== undefined) {
+ router.push(`/project/${projectRef}/sql/${appSnap.dashboardHistory.sql}`)
+ } else if (lastTabId) {
+ const lastTab = store.tabsMap[lastTabId]
+ if (lastTab) router.push(`/project/${projectRef}/sql/${lastTab.id.replace('sql-', '')}`)
+ }
+ }
+ }, [isSqlEditorTabsEnabled])
+
+ return isSqlEditorTabsEnabled ? : null
+}
+
+TableEditorPage.getLayout = (page) => (
+
+ } product="SQL Editor">
+ {page}
+
+
+)
+
+export default TableEditorPage
diff --git a/apps/studio/pages/project/[ref]/sql/quickstarts.tsx b/apps/studio/pages/project/[ref]/sql/quickstarts.tsx
index 7d53fea7325..71e9e91bf1d 100644
--- a/apps/studio/pages/project/[ref]/sql/quickstarts.tsx
+++ b/apps/studio/pages/project/[ref]/sql/quickstarts.tsx
@@ -1,16 +1,49 @@
+import { useParams } from 'next/navigation'
+import { useRouter } from 'next/router'
+import { useEffect } from 'react'
+
+import { useIsSQLEditorTabsEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import SQLQuickstarts from 'components/interfaces/SQLEditor/SQLTemplates/SQLQuickstarts'
import DefaultLayout from 'components/layouts/DefaultLayout'
+import { EditorBaseLayout } from 'components/layouts/editors/EditorBaseLayout'
import SQLEditorLayout from 'components/layouts/SQLEditorLayout/SQLEditorLayout'
+import { SQLEditorMenu } from 'components/layouts/SQLEditorLayout/SQLEditorMenu'
+import { addTab, createTabId } from 'state/tabs'
import type { NextPageWithLayout } from 'types'
-const SqlEditorWelcome: NextPageWithLayout = () => {
+const SqlQuickstarts: NextPageWithLayout = () => {
+ const router = useRouter()
+ const { ref } = useParams<{ ref: string }>()
+
+ const isSqlEditorTabsEnabled = useIsSQLEditorTabsEnabled()
+
+ useEffect(() => {
+ if (isSqlEditorTabsEnabled) {
+ if (!router.isReady) return
+
+ const tabId = createTabId('sql', { id: 'quickstarts' })
+
+ addTab(ref, {
+ id: tabId,
+ type: 'sql',
+ label: 'Quickstarts',
+ metadata: {
+ sqlId: 'quickstarts',
+ name: 'quickstarts',
+ },
+ })
+ }
+ }, [router.isReady, isSqlEditorTabsEnabled, ref])
+
return
}
-SqlEditorWelcome.getLayout = (page) => (
+SqlQuickstarts.getLayout = (page) => (
- {page}
+ } product="SQL Editor">
+ {page}
+
)
-export default SqlEditorWelcome
+export default SqlQuickstarts
diff --git a/apps/studio/pages/project/[ref]/sql/templates.tsx b/apps/studio/pages/project/[ref]/sql/templates.tsx
index de06e63259a..e481286527e 100644
--- a/apps/studio/pages/project/[ref]/sql/templates.tsx
+++ b/apps/studio/pages/project/[ref]/sql/templates.tsx
@@ -1,16 +1,49 @@
+import { useParams } from 'next/navigation'
+import { useRouter } from 'next/router'
+import { useEffect } from 'react'
+
+import { useIsSQLEditorTabsEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import SQLTemplates from 'components/interfaces/SQLEditor/SQLTemplates/SQLTemplates'
import DefaultLayout from 'components/layouts/DefaultLayout'
+import { EditorBaseLayout } from 'components/layouts/editors/EditorBaseLayout'
import SQLEditorLayout from 'components/layouts/SQLEditorLayout/SQLEditorLayout'
+import { SQLEditorMenu } from 'components/layouts/SQLEditorLayout/SQLEditorMenu'
+import { addTab, createTabId } from 'state/tabs'
import type { NextPageWithLayout } from 'types'
-const SqlEditorWelcome: NextPageWithLayout = () => {
+const SqlTemplates: NextPageWithLayout = () => {
+ const router = useRouter()
+ const { ref } = useParams<{ ref: string }>()
+
+ const isSqlEditorTabsEnabled = useIsSQLEditorTabsEnabled()
+
+ useEffect(() => {
+ if (isSqlEditorTabsEnabled) {
+ if (!router.isReady) return
+
+ const tabId = createTabId('sql', { id: 'templates' })
+
+ addTab(ref, {
+ id: tabId,
+ type: 'sql',
+ label: 'Templates',
+ metadata: {
+ sqlId: 'templates',
+ name: 'templates',
+ },
+ })
+ }
+ }, [router.isReady, isSqlEditorTabsEnabled, ref])
+
return
}
-SqlEditorWelcome.getLayout = (page) => (
+SqlTemplates.getLayout = (page) => (
- {page}
+ } product="SQL Editor">
+ {page}
+
)
-export default SqlEditorWelcome
+export default SqlTemplates
diff --git a/apps/studio/public/img/previews/api-docs-preview.png b/apps/studio/public/img/previews/api-docs-preview.png
index 64a9ce7a06c..d7c715a6733 100644
Binary files a/apps/studio/public/img/previews/api-docs-preview.png and b/apps/studio/public/img/previews/api-docs-preview.png differ
diff --git a/apps/studio/public/img/previews/assistant-v2.png b/apps/studio/public/img/previews/assistant-v2.png
index 02d697fd934..f30a43eeb30 100644
Binary files a/apps/studio/public/img/previews/assistant-v2.png and b/apps/studio/public/img/previews/assistant-v2.png differ
diff --git a/apps/studio/public/img/previews/cls-preview.png b/apps/studio/public/img/previews/cls-preview.png
index 85c3d2c8196..daacc2793bc 100644
Binary files a/apps/studio/public/img/previews/cls-preview.png and b/apps/studio/public/img/previews/cls-preview.png differ
diff --git a/apps/studio/public/img/previews/rls-ai-assistant-preview.png b/apps/studio/public/img/previews/rls-ai-assistant-preview.png
index de1875f1fa7..4c646ff445b 100644
Binary files a/apps/studio/public/img/previews/rls-ai-assistant-preview.png and b/apps/studio/public/img/previews/rls-ai-assistant-preview.png differ
diff --git a/apps/studio/public/img/previews/tabs-editor.png b/apps/studio/public/img/previews/tabs-editor.png
new file mode 100644
index 00000000000..e040742c864
Binary files /dev/null and b/apps/studio/public/img/previews/tabs-editor.png differ
diff --git a/apps/studio/public/img/previews/tabs-sql.png b/apps/studio/public/img/previews/tabs-sql.png
new file mode 100644
index 00000000000..c20bf073e14
Binary files /dev/null and b/apps/studio/public/img/previews/tabs-sql.png differ
diff --git a/apps/studio/state/app-state.ts b/apps/studio/state/app-state.ts
index d3e1bdba865..21ff3556be6 100644
--- a/apps/studio/state/app-state.ts
+++ b/apps/studio/state/app-state.ts
@@ -1,9 +1,9 @@
import type { Message as MessageType } from 'ai/react'
import { LOCAL_STORAGE_KEYS as COMMON_LOCAL_STORAGE_KEYS } from 'common'
-import { LOCAL_STORAGE_KEYS } from 'lib/constants'
-import { SupportedAssistantEntities } from 'components/ui/AIAssistantPanel/AIAssistant.types'
-import { proxy, snapshot, subscribe, useSnapshot } from 'valtio'
import { SQL_TEMPLATES } from 'components/interfaces/SQLEditor/SQLEditor.queries'
+import { SupportedAssistantEntities } from 'components/ui/AIAssistantPanel/AIAssistant.types'
+import { LOCAL_STORAGE_KEYS } from 'lib/constants'
+import { proxy, snapshot, subscribe, useSnapshot } from 'valtio'
export type CommonDatabaseEntity = {
id: number
@@ -100,6 +100,7 @@ const getInitialState = () => {
showGenerateSqlModal: false,
ongoingQueriesPanelOpen: false,
mobileMenuOpen: false,
+ showSidebar: true,
}
}
@@ -149,13 +150,14 @@ const getInitialState = () => {
showGenerateSqlModal: false,
ongoingQueriesPanelOpen: false,
mobileMenuOpen: false,
+ showSidebar: true,
}
}
export const appState = proxy({
...getInitialState(),
- setDashboardHistory: (ref: string, key: 'sql' | 'editor', id: string) => {
+ setDashboardHistory: (ref: string, key: 'sql' | 'editor', id: string | undefined) => {
if (appState.dashboardHistory[key] !== id) {
appState.dashboardHistory[key] = id
localStorage.setItem(
@@ -211,6 +213,11 @@ export const appState = proxy({
appState.showGenerateSqlModal = value
},
+ showSidebar: true,
+ setShowSidebar: (value: boolean) => {
+ appState.showSidebar = value
+ },
+
resetAiAssistantPanel: () => {
appState.aiAssistantPanel = {
...INITIAL_AI_ASSISTANT,
@@ -232,6 +239,11 @@ export const appState = proxy({
}
},
+ showOngoingQueriesPanelOpen: false,
+ setOnGoingQueriesPanelOpen: (value: boolean) => {
+ appState.ongoingQueriesPanelOpen = value
+ },
+
saveLatestMessage: (message: any) => {
appState.aiAssistantPanel = {
...appState.aiAssistantPanel,
@@ -239,11 +251,6 @@ export const appState = proxy({
}
},
- showOngoingQueriesPanelOpen: false,
- setOnGoingQueriesPanelOpen: (value: boolean) => {
- appState.ongoingQueriesPanelOpen = value
- },
-
setEditorPanel: (value: Partial) => {
// Close AI Assistant panel if editor panel is being opened
if (value.open && appState.aiAssistantPanel.open) {
diff --git a/apps/studio/state/recent-items.ts b/apps/studio/state/recent-items.ts
new file mode 100644
index 00000000000..a6272e3d3b4
--- /dev/null
+++ b/apps/studio/state/recent-items.ts
@@ -0,0 +1,160 @@
+import { partition } from 'lodash'
+import { proxy, subscribe } from 'valtio'
+import { editorEntityTypes, Tab, TabType } from './tabs'
+
+/**
+ * [TODO REFACTOR] Joshen: JFYI this differs from the way that we usually write our stores
+ * where we have contexts and such - ideally we need to refactor this for consistency
+ * and the main benefit from our usual way of writing stores is that we do not need to pass
+ * in the project ref to all the functions, as the stores are written to be scoped to the project
+ *
+ * So do not use this as reference for writing new valtio stores - refer to database-selector instead
+ */
+
+// Define the key for storing recent items in localStorage
+const RECENT_ITEMS_KEY = 'supabase_recent_items'
+// Define the maximum number of recent items to store
+const MAX_RECENT_ITEMS = 8
+
+const getStorageKey = (ref: string) => `${RECENT_ITEMS_KEY}_${ref}`
+
+// Define the structure of a recent item
+export interface RecentItem {
+ id: string // Unique identifier for the item
+ type: TabType // Type of the tab (e.g., sql, table)
+ label: string // Label for the item
+ timestamp: number // Timestamp of when the item was added
+ metadata?: {
+ // Optional metadata associated with the item
+ schema?: string // Schema name (if applicable)
+ name?: string // Name of the item (if applicable)
+ tableId?: number // Table ID (if applicable)
+ sqlId?: string // SQL ID (if applicable)
+ }
+}
+
+// Define the structure of the recent items state
+interface RecentItems {
+ items: RecentItem[] // Each project just has an array of items
+}
+
+interface RecentItemsStore {
+ [key: string]: RecentItems // Removed | undefined since we always initialize it
+}
+
+const defaultState: RecentItems = {
+ items: [],
+}
+
+export const recentItemsStore = proxy({})
+
+export const getRecentItemsStore = (ref: string | undefined): RecentItems => {
+ if (!ref) return proxy(defaultState)
+ if (!recentItemsStore[ref]) {
+ const stored = localStorage.getItem(getStorageKey(ref))
+ console.debug('[Recent Items] Loading stored items:', stored)
+
+ try {
+ const parsed = stored ? JSON.parse(stored) : defaultState
+ // Validate the shape of the data
+ if (!parsed.items || !Array.isArray(parsed.items)) {
+ console.warn('[Recent Items] Invalid stored data, using default')
+ recentItemsStore[ref] = proxy({ ...defaultState })
+ } else {
+ recentItemsStore[ref] = proxy(parsed)
+ }
+ } catch (error) {
+ console.error('[Recent Items] Failed to parse stored items:', error)
+ recentItemsStore[ref] = proxy({ ...defaultState })
+ }
+ }
+
+ return recentItemsStore[ref]
+}
+
+// Subscribe to changes for each ref and save to localStorage
+if (typeof window !== 'undefined') {
+ subscribe(recentItemsStore, () => {
+ Object.entries(recentItemsStore).forEach(([ref, state]) => {
+ try {
+ localStorage.setItem(getStorageKey(ref), JSON.stringify(state))
+ } catch (error) {
+ console.error('Failed to save recent items to localStorage:', error)
+ }
+ })
+ })
+}
+
+// Function to add a recent item
+export const addRecentItem = (ref: string | undefined, tab: Tab) => {
+ if (!ref) return
+ const store = getRecentItemsStore(ref)
+ // Check if an item with the same ID already exists
+ const existingItem = store.items.find((item) => item.id === tab.id)
+
+ if (existingItem) {
+ // If it exists, update its timestamp
+ existingItem.timestamp = Date.now()
+ return // Exit the function
+ }
+
+ // If it doesn't exist, create and add a new item
+ const recentItem: RecentItem = {
+ id: tab.id, // Set the ID
+ type: tab.type, // Set the type
+ label: tab.label || 'Untitled', // Set the label or default to 'Untitled'
+ timestamp: Date.now(), // Set the current timestamp
+ metadata: tab.metadata, // Set the metadata
+ }
+
+ // Add the new recent item to the beginning of the list
+ store.items.unshift(recentItem)
+
+ // Ensure that there's only up to max of MAX_RECENT_ITEMS items per tab type
+ const [itemsOfSameType, itemsOfDifferentType] = partition(store.items, (item) => {
+ if (editorEntityTypes.table.includes(item.type)) return item
+ })
+ store.items = [...itemsOfSameType.slice(0, MAX_RECENT_ITEMS), ...itemsOfDifferentType]
+}
+
+// Function to clear all recent items
+export const clearRecentItems = (ref: string | undefined) => {
+ if (!ref) return
+ const store = getRecentItemsStore(ref)
+ store.items = [] // Set items to an empty array
+}
+
+// Function to remove a specific recent item by its ID
+export const removeRecentItem = (ref: string | undefined, itemId: string) => {
+ if (!ref) return
+ const store = getRecentItemsStore(ref)
+ store.items = store.items.filter((item) => item.id !== itemId)
+}
+
+// Function to remove multiple recent items by their IDs
+export const removeRecentItems = (ref: string | undefined, itemIds: string[]) => {
+ if (!ref) return
+ const store = getRecentItemsStore(ref)
+ store.items = store.items.filter((item) => !itemIds.includes(item.id))
+}
+
+// Function to remove recent items by their type
+export const removeRecentItemsByType = (ref: string | undefined, type: TabType) => {
+ if (!ref) return
+ const store = getRecentItemsStore(ref)
+ store.items = store.items.filter((item) => item.type !== type)
+}
+
+// Function to get all recent items
+export const getRecentItems = (ref: string | undefined) => {
+ if (!ref) return
+ const store = getRecentItemsStore(ref)
+ return store.items
+}
+
+// Function to get recent items by their type
+export const getRecentItemsByType = (ref: string | undefined, type: TabType) => {
+ if (!ref) return
+ const store = getRecentItemsStore(ref)
+ return store.items.filter((item) => item.type === type)
+}
diff --git a/apps/studio/state/tabs.ts b/apps/studio/state/tabs.ts
new file mode 100644
index 00000000000..7b3b50755f0
--- /dev/null
+++ b/apps/studio/state/tabs.ts
@@ -0,0 +1,392 @@
+import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
+import { NextRouter } from 'next/router'
+import { ReactNode } from 'react'
+import { proxy, subscribe } from 'valtio'
+import { addRecentItem } from './recent-items'
+
+/**
+ * [TODO REFACTOR] Joshen: JFYI this differs from the way that we usually write our stores
+ * where we have contexts and such - ideally we need to refactor this for consistency
+ * and the main benefit from our usual way of writing stores is that we do not need to pass
+ * in the project ref to all the functions, as the stores are written to be scoped to the project
+ *
+ * So do not use this as reference for writing new valtio stores - refer to database-selector instead
+ */
+
+// Define the type of tabs available in the application
+export type TabType = ENTITY_TYPE | 'sql'
+
+export interface Tab {
+ id: string // Unique identifier for the tab
+ type: TabType // Type of the tab, which can be an entity type or specific string values
+ label?: string // Optional label for the tab, displayed to the user
+ icon?: ReactNode // Optional icon associated with the tab
+ metadata?: {
+ // Optional metadata related to the tab
+ schema?: string // Optional schema name associated with the tab
+ name?: string // Optional name of the entity represented by the tab
+ tableId?: number // Optional ID of the table associated with the tab
+ sqlId?: string // Optional ID of the SQL query associated with the tab
+ }
+ isPreview?: boolean // Optional flag indicating if the tab is in preview mode
+ createdAt?: Date // Optional timestamp for when the tab was created
+ updatedAt?: Date // Optional timestamp for when the tab was last updated
+}
+
+// Interface representing the state of tabs
+interface TabsState {
+ activeTab: string | null
+ openTabs: string[]
+ tabsMap: { [key: string]: Tab }
+ previewTabId?: string
+}
+
+// Map of tab states for different references
+interface TabsStateMap {
+ [ref: string]: TabsState
+}
+
+const STORAGE_KEY = 'supabase_studio_tabs'
+const getStorageKey = (ref: string) => `${STORAGE_KEY}_${ref}`
+
+const defaultState: TabsState = {
+ openTabs: [],
+ activeTab: null,
+ tabsMap: {},
+}
+
+// Store now holds states for multiple refs
+export const tabsStore = proxy({})
+
+// Helper to get/create state for a specific ref
+export const getTabsStore = (ref: string | undefined): TabsState => {
+ if (!ref) return proxy(defaultState)
+ if (!tabsStore[ref]) {
+ const stored = localStorage.getItem(getStorageKey(ref))
+ console.debug('[Tabs] Loading stored tabs:', stored)
+ try {
+ const parsed = stored ? JSON.parse(stored) : defaultState
+ if (
+ !parsed.openTabs ||
+ !Array.isArray(parsed.openTabs) ||
+ !parsed.tabsMap ||
+ typeof parsed.tabsMap !== 'object'
+ ) {
+ console.warn('[Tabs] Invalid stored data, using default')
+ tabsStore[ref] = proxy({ ...defaultState })
+ } else {
+ tabsStore[ref] = proxy(parsed)
+ }
+ } catch (error) {
+ console.error('[Tabs] Failed to parse stored tabs:', error)
+ tabsStore[ref] = proxy({ ...defaultState })
+ }
+ }
+
+ return tabsStore[ref]
+}
+
+// Subscribe to changes for each ref and save to localStorage
+if (typeof window !== 'undefined') {
+ subscribe(tabsStore, () => {
+ Object.entries(tabsStore).forEach(([ref, state]) => {
+ try {
+ localStorage.setItem(getStorageKey(ref), JSON.stringify(state))
+ } catch (error) {
+ console.error('Failed to save tabs to localStorage:', error)
+ }
+ })
+ })
+}
+
+// Function to add a new tab to the store
+export const addTab = (ref: string | undefined, tab: Tab) => {
+ if (!ref) return
+ const store = getTabsStore(ref)
+
+ // If tab exists and is active, don't do anything
+ if (store.tabsMap[tab.id] && store.activeTab === tab.id) {
+ return
+ }
+
+ // If tab exists but isn't active, just make it active
+ if (store.tabsMap[tab.id]) {
+ store.activeTab = tab.id
+ if (!tab.isPreview) addRecentItem(ref, tab)
+ return
+ }
+
+ // If this tab should be permanent, add it normally
+ if (tab.isPreview === false) {
+ store.openTabs = [...store.openTabs, tab.id]
+ store.tabsMap[tab.id] = tab
+ store.activeTab = tab.id
+ // Add to recent items when creating permanent tab
+ addRecentItem(ref, tab)
+ return
+ }
+
+ // Remove any existing preview tab
+ if (store.previewTabId) {
+ store.openTabs = store.openTabs.filter((id) => id !== store.previewTabId)
+ delete store.tabsMap[store.previewTabId]
+ }
+
+ // Add new preview tab
+ store.tabsMap[tab.id] = { ...tab, isPreview: true }
+ store.openTabs = [...store.openTabs, tab.id]
+ store.previewTabId = tab.id
+ store.activeTab = tab.id
+}
+
+// Function to remove a tab from the store
+// this is used for removing tabs from the localstorage state
+// for handling a manual tab removal with a close action, use handleTabClose()
+export const removeTab = (ref: string | undefined, id: string) => {
+ console.log('removeTab')
+ const store = getTabsStore(ref)
+ const idx = store.openTabs.indexOf(id)
+ store.openTabs = store.openTabs.filter((tabId) => tabId !== id)
+ delete store.tabsMap[id]
+
+ // Update active tab if the removed tab was active
+ if (id === store.activeTab) {
+ store.activeTab = store.openTabs[idx - 1] || store.openTabs[idx + 1] || null
+ }
+}
+
+// Function to remove multiple tabs from the store
+// this is used for removing tabs from the localstorage state
+// for handling a manual tab removal with a close action, use handleTabClose()
+export const removeTabs = (ref: string | undefined, ids: string[]) => {
+ if (!ref) return
+ if (!ids.length) return
+
+ ids.forEach((id) => removeTab(ref, id))
+}
+
+export const renameTab = (ref: string, id: string, name: string) => {
+ const store = getTabsStore(ref)
+ if (!!store.tabsMap[id]) {
+ store.tabsMap[id].label = name
+ }
+}
+
+// Function to reorder tabs in the store
+export const reorderTabs = (ref: string | undefined, oldIndex: number, newIndex: number) => {
+ if (!ref) return
+ const store = getTabsStore(ref)
+ const newOpenTabs = [...store.openTabs]
+ const [removedTab] = newOpenTabs.splice(oldIndex, 1)
+ newOpenTabs.splice(newIndex, 0, removedTab)
+ store.openTabs = newOpenTabs
+}
+
+// Function to make a tab permanent
+export const makeTabPermanent = (ref: string | undefined, tabId: string) => {
+ if (!ref) return
+ const store = getTabsStore(ref)
+ const tab = store.tabsMap[tabId]
+
+ if (tab?.isPreview) {
+ tab.isPreview = false
+ store.previewTabId = undefined
+ // Add to recent items when preview tab becomes permanent
+ addRecentItem(ref, tab)
+ }
+}
+
+// make the active tab permanent if it is a preview
+export const makeActiveTabPermanent = (ref?: string) => {
+ if (!ref) return false
+ const store = getTabsStore(ref)
+
+ if (store.activeTab && store.tabsMap[store.activeTab]?.isPreview) {
+ makeTabPermanent(ref, store.activeTab)
+ return true
+ }
+ return false
+}
+
+// handle navigation to a specific tab
+export const handleTabNavigation = (ref: string | undefined, id: string, router: NextRouter) => {
+ if (!ref) return
+
+ const store = getTabsStore(ref)
+ const tab = store.tabsMap[id]
+ if (!tab) return
+
+ store.activeTab = id
+
+ // Add to recent items when navigating to a non-preview, non-new tab
+ if (!tab.isPreview) addRecentItem(ref, tab)
+
+ switch (tab.type) {
+ case 'sql':
+ const schema = (router.query.schema as string) || 'public'
+ router.push(`/project/${router.query.ref}/sql/${tab.metadata?.sqlId}?schema=${schema}`)
+ break
+ case 'r':
+ case 'v':
+ case 'm':
+ case 'f':
+ case 'p':
+ router.push(
+ `/project/${router.query.ref}/editor/${tab.metadata?.tableId}?schema=${tab.metadata?.schema}`
+ )
+ break
+ }
+}
+
+// Function to handle closing a tab
+export const handleTabClose = ({
+ ref,
+ id,
+ router,
+ editor,
+ onClose,
+ onClearDashboardHistory,
+}: {
+ ref: string | undefined
+ id: string
+ router: NextRouter
+ editor?: 'sql' | 'table'
+ onClose?: (id: string) => void
+ onClearDashboardHistory: () => void
+}) => {
+ if (!ref) return
+
+ const store = getTabsStore(ref)
+ // tabs without the one we're closing
+ const currentTab = store.tabsMap[id]
+ const currentTabs = Object.values(store.tabsMap).filter((tab) => tab.id !== id)
+
+ const nextTabId = !editor
+ ? undefined
+ : currentTabs.filter((tab) => {
+ return editorEntityTypes[editor]?.includes(tab.type)
+ })[0]?.id
+ delete store.tabsMap[id]
+
+ if (currentTab) {
+ // Update store
+ // If the tab being removed is logged in the store, update the open tabs
+ store.openTabs = [...currentTabs.map((tab) => tab.id).filter((id) => id !== currentTab.id)]
+ }
+
+ // Check if there is a preview tab and if it matches the tab being closed
+ if (store.previewTabId) {
+ if (store.previewTabId === id) {
+ // remove the preview tab if it matches the tab being closed
+ store.previewTabId = undefined
+ }
+ }
+
+ // [Joshen] Only navigate away if we're closing the tab that's currently in focus
+ if (store.activeTab === id || id === 'new') {
+ if (nextTabId) {
+ store.activeTab = nextTabId
+ handleTabNavigation(ref, nextTabId, router)
+ } else {
+ onClearDashboardHistory()
+
+ // If no tabs of same type, go to the home of the current section
+ switch (currentTab?.type) {
+ case 'sql':
+ router.push(`/project/${router.query.ref}/sql`)
+ break
+ case 'r':
+ case 'v':
+ case 'm':
+ case 'f':
+ case 'p':
+ router.push(`/project/${router.query.ref}/editor`)
+ break
+ default:
+ router.push(`/project/${router.query.ref}/${editor}`)
+ }
+ }
+ }
+
+ onClose?.(id)
+}
+
+// Function to handle the end of a tab drag event
+export const handleTabDragEnd = (
+ ref: string | undefined,
+ oldIndex: number,
+ newIndex: number,
+ tabId: string,
+ router: any
+) => {
+ if (!ref) return
+ const store = getTabsStore(ref)
+ // Make permanent if needed
+ const draggedTab = store.tabsMap[tabId]
+ if (draggedTab?.isPreview) {
+ makeTabPermanent(ref, tabId)
+ }
+
+ // Reorder tabs
+ const newOpenTabs = [...store.openTabs]
+ newOpenTabs.splice(oldIndex, 1)
+ newOpenTabs.splice(newIndex, 0, tabId)
+
+ store.openTabs = newOpenTabs
+ store.activeTab = tabId
+
+ // Handle navigation
+ handleTabNavigation(ref, tabId, router)
+}
+
+type CreateTabIdParams = {
+ r: { id: number }
+ v: { id: number }
+ m: { id: number }
+ f: { id: number }
+ p: { id: number }
+ sql: { id: string }
+ schema: { schema: string }
+ view: never
+ function: never
+ new: never
+}
+
+// Function to create a unique tab ID based on type and parameters
+export function createTabId(type: T, params: CreateTabIdParams[T]): string {
+ switch (type) {
+ case 'r':
+ return `r-${(params as CreateTabIdParams['r']).id}`
+ case 'v':
+ return `v-${(params as CreateTabIdParams['v']).id}`
+ case 'm':
+ return `m-${(params as CreateTabIdParams['m']).id}`
+ case 'f':
+ return `f-${(params as CreateTabIdParams['f']).id}`
+ case 'p':
+ return `p-${(params as CreateTabIdParams['p']).id}`
+ case 'sql':
+ return `sql-${(params as CreateTabIdParams['sql']).id}`
+ default:
+ return ''
+ }
+}
+
+// Object mapping editor types to their corresponding tab types
+export const editorEntityTypes = {
+ table: ['r', 'v', 'm', 'f', 'p'],
+ sql: ['sql'],
+}
+
+// Function to remove tabs based on their editor type
+export function removeTabsByEditor(ref: string | undefined, type: 'table' | 'sql') {
+ if (!ref) return
+ const store = getTabsStore(ref)
+
+ // Get tab IDs to remove
+ const tabIdsToRemove = store.openTabs.filter((id) => {
+ return editorEntityTypes[type].some((entityType) => id.startsWith(`${entityType}-`))
+ })
+
+ removeTabs(ref, tabIdsToRemove)
+}
diff --git a/apps/studio/styles/monaco.scss b/apps/studio/styles/monaco.scss
index 7ebacfe0974..47a90618bea 100644
--- a/apps/studio/styles/monaco.scss
+++ b/apps/studio/styles/monaco.scss
@@ -13,10 +13,16 @@
border: 0 !important;
}
+[data-theme='dark'] .monaco-editor,
+[data-theme='dark'] .monaco-diff-editor {
+ --vscode-editor-background: hsl(var(--background-surface-100)) !important;
+ --vscode-editorGutter-background: hsl(var(--background-surface-100)) !important;
+ color-scheme: dark;
+}
.monaco-editor,
.monaco-diff-editor {
- --vscode-editor-background: hsl(var(--background-surface-200)) !important;
- --vscode-editorGutter-background: hsl(var(--background-surface-200)) !important;
+ --vscode-editor-background: hsl(var(--background-dash-sidebar)) !important;
+ --vscode-editorGutter-background: hsl(var(--background-dash-sidebar)) !important;
}
.gutter {
diff --git a/packages/ui/src/components/TreeView/TreeView.tsx b/packages/ui/src/components/TreeView/TreeView.tsx
index 0c7d72fa7f2..8fa03898fcb 100644
--- a/packages/ui/src/components/TreeView/TreeView.tsx
+++ b/packages/ui/src/components/TreeView/TreeView.tsx
@@ -133,23 +133,35 @@ const TreeViewItem = forwardRef<
}
}, [isLoading])
+ const handleBlur = () => {
+ onEditSubmit?.(localValueState)
+ }
+
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
onEditSubmit?.(localValueState)
}
- const handleBlur = () => {
- onEditSubmit?.(localValueState)
- }
+ // [Joshen] These properties were causing console errors as they were getting passed as props to the parent div
+ const {
+ isDisabled,
+ isHalfSelected,
+ handleSelect,
+ handleExpand,
+ treeState,
+ dispatch,
+ ...divProps
+ } = props as any
return (
=10.0.0'}
+ '@dnd-kit/accessibility@3.1.1':
+ resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==}
+ peerDependencies:
+ react: '>=16.8.0'
+
+ '@dnd-kit/core@6.3.1':
+ resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==}
+ peerDependencies:
+ react: '>=16.8.0'
+ react-dom: '>=16.8.0'
+
+ '@dnd-kit/sortable@8.0.0':
+ resolution: {integrity: sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==}
+ peerDependencies:
+ '@dnd-kit/core': ^6.1.0
+ react: '>=16.8.0'
+
+ '@dnd-kit/utilities@3.2.2':
+ resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==}
+ peerDependencies:
+ react: '>=16.8.0'
+
'@effect-ts/core@0.60.5':
resolution: {integrity: sha512-qi1WrtJA90XLMnj2hnUszW9Sx4dXP03ZJtCc5DiUBIOhF4Vw7plfb65/bdBySPoC9s7zy995TdUX1XBSxUkl5w==}
@@ -16370,6 +16401,31 @@ snapshots:
'@discoveryjs/json-ext@0.5.7': {}
+ '@dnd-kit/accessibility@3.1.1(react@18.2.0)':
+ dependencies:
+ react: 18.2.0
+ tslib: 2.6.2
+
+ '@dnd-kit/core@6.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
+ dependencies:
+ '@dnd-kit/accessibility': 3.1.1(react@18.2.0)
+ '@dnd-kit/utilities': 3.2.2(react@18.2.0)
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ tslib: 2.6.2
+
+ '@dnd-kit/sortable@8.0.0(@dnd-kit/core@6.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0)':
+ dependencies:
+ '@dnd-kit/core': 6.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
+ '@dnd-kit/utilities': 3.2.2(react@18.2.0)
+ react: 18.2.0
+ tslib: 2.6.2
+
+ '@dnd-kit/utilities@3.2.2(react@18.2.0)':
+ dependencies:
+ react: 18.2.0
+ tslib: 2.6.2
+
'@effect-ts/core@0.60.5':
dependencies:
'@effect-ts/system': 0.57.5
@@ -17364,7 +17420,7 @@ snapshots:
'@motionone/easing@10.16.3':
dependencies:
'@motionone/utils': 10.16.3
- tslib: 2.6.2
+ tslib: 2.8.1
'@motionone/generators@10.16.4':
dependencies:
@@ -26452,7 +26508,7 @@ snapshots:
lower-case@2.0.2:
dependencies:
- tslib: 2.6.2
+ tslib: 2.8.1
lowlight@1.20.0:
dependencies:
@@ -29334,7 +29390,7 @@ snapshots:
dependencies:
react: 18.2.0
react-style-singleton: 2.2.3(@types/react@18.3.3)(react@18.2.0)
- tslib: 2.6.2
+ tslib: 2.8.1
optionalDependencies:
'@types/react': 18.3.3
@@ -29415,7 +29471,7 @@ snapshots:
dependencies:
get-nonce: 1.0.1
react: 18.2.0
- tslib: 2.6.2
+ tslib: 2.8.1
optionalDependencies:
'@types/react': 18.3.3
@@ -31577,11 +31633,11 @@ snapshots:
upper-case-first@2.0.2:
dependencies:
- tslib: 2.6.2
+ tslib: 2.8.1
upper-case@2.0.2:
dependencies:
- tslib: 2.6.2
+ tslib: 2.8.1
uqr@0.1.2: {}
@@ -31608,7 +31664,7 @@ snapshots:
use-callback-ref@1.3.3(@types/react@18.3.3)(react@18.2.0):
dependencies:
react: 18.2.0
- tslib: 2.6.2
+ tslib: 2.8.1
optionalDependencies:
'@types/react': 18.3.3
@@ -31632,7 +31688,7 @@ snapshots:
dependencies:
detect-node-es: 1.1.0
react: 18.2.0
- tslib: 2.6.2
+ tslib: 2.8.1
optionalDependencies:
'@types/react': 18.3.3
diff --git a/tests/studio-tests/tests/common-functionality/table-editor.spec.ts b/tests/studio-tests/tests/common-functionality/table-editor.spec.ts
index 7d4322545c3..d05fe4462f7 100644
--- a/tests/studio-tests/tests/common-functionality/table-editor.spec.ts
+++ b/tests/studio-tests/tests/common-functionality/table-editor.spec.ts
@@ -30,6 +30,7 @@ test.describe('Table Editor', () => {
// The page has been loaded with the table data, we can now interact with the page
await page.getByRole('button', { name: 'New table', exact: true }).click()
+
await page.getByTestId('table-name-input').waitFor({ state: 'visible' })
await page.getByTestId('table-name-input').click()
await page.getByTestId('table-name-input').fill(tableName)