mirror of
https://github.com/supabase/supabase.git
synced 2026-10-17 07:09:07 +03:00
tabs now in feature flags
This commit is contained in:
1 parent
df9fe8c150
commit
481fb13ef2
10 files changed
+180
-93
No files matched your search
@@ -7,9 +7,13 @@ import { useAppStateSnapshot } from 'state/app-state'
|
||||
import { Badge, Button, Modal, ScrollArea, cn } from 'ui'
|
||||
import { FEATURE_PREVIEWS, useFeaturePreviewContext } from './FeaturePreviewContext'
|
||||
import { useFlag } from 'hooks/ui/useFlag'
|
||||
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
|
||||
import { getTabsStore, removePreviewTab, removeTabsByEditor } from 'state/tabs'
|
||||
import { useParams } from 'common'
|
||||
|
||||
const FeaturePreviewModal = () => {
|
||||
const snap = useAppStateSnapshot()
|
||||
const { ref } = useParams()
|
||||
const featurePreviewContext = useFeaturePreviewContext()
|
||||
const { mutate: sendEvent } = useSendEventMutation()
|
||||
const enableFunctionsAssistant = useFlag('functionsAssistantV2')
|
||||
@@ -40,6 +44,15 @@ const FeaturePreviewModal = () => {
|
||||
action: isSelectedFeatureEnabled ? 'disabled' : 'enabled',
|
||||
label: selectedFeatureKey,
|
||||
})
|
||||
|
||||
// handle Tabs preview
|
||||
removePreviewTab(ref as string | undefined)
|
||||
if (selectedFeatureKey === LOCAL_STORAGE_KEYS.UI_SQL_EDITOR_TABS) {
|
||||
removeTabsByEditor(ref as string | undefined, 'table')
|
||||
}
|
||||
if (selectedFeatureKey === LOCAL_STORAGE_KEYS.UI_SQL_EDITOR_TABS) {
|
||||
removeTabsByEditor(ref as string | undefined, 'sql')
|
||||
}
|
||||
}
|
||||
|
||||
function handleCloseFeaturePreviewModal() {
|
||||
|
||||
@@ -172,7 +172,7 @@ export const SQLEditorNav = ({ searchText: _searchText }: SQLEditorNavProps) =>
|
||||
const existingSnippetIds = Object.keys(snapV2.snippets).filter((x) => !ids.includes(x))
|
||||
|
||||
if (existingSnippetIds.length === 0) {
|
||||
router.push(`/project/${projectRef}/sql/new`)
|
||||
router.push(`/project/${projectRef}/sql`)
|
||||
} else if (ids.includes(id as string)) {
|
||||
router.push(`/project/${projectRef}/sql/${existingSnippetIds[0]}`)
|
||||
}
|
||||
|
||||
@@ -185,9 +185,12 @@ export function ExplorerTabs({ storeKey, onClose }: TabsProps) {
|
||||
handleTabNavigation(ref, id, router)
|
||||
}
|
||||
|
||||
const hasNewTab = router.pathname.includes('/new') // Object.values(tabs.tabsMap).some((tab) => tab.type === 'new')
|
||||
const hasNewTab = router.asPath.includes('/new')
|
||||
const isOnNewPage = router.pathname.endsWith('/explorer/new')
|
||||
|
||||
console.log('hasNewTab', hasNewTab)
|
||||
console.log('router', router.pathname)
|
||||
|
||||
return (
|
||||
<Tabs_Shadcn_
|
||||
value={hasNewTab ? 'new' : tabs.activeTab ?? undefined}
|
||||
|
||||
@@ -207,52 +207,53 @@ export function NewTab() {
|
||||
// const showRightPanel = false
|
||||
|
||||
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')
|
||||
const id = uuidv4()
|
||||
const tabId = createTabId('sql', { id })
|
||||
router.push(`/project/${ref}/sql/new`)
|
||||
// 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')
|
||||
// const id = uuidv4()
|
||||
// const tabId = createTabId('sql', { id })
|
||||
|
||||
if (sql && name) {
|
||||
try {
|
||||
const snippet = createSqlSnippetSkeletonV2({
|
||||
id,
|
||||
name,
|
||||
sql,
|
||||
owner_id: profile?.id,
|
||||
project_id: project?.id,
|
||||
})
|
||||
snapV2.addSnippet({ projectRef: ref, snippet })
|
||||
snapV2.addNeedsSaving(snippet.id)
|
||||
store.openTabs = [...store.openTabs, tabId]
|
||||
store.tabsMap[tabId] = {
|
||||
id: tabId,
|
||||
type: 'sql',
|
||||
// Remove the label since we'll derive it dynamically
|
||||
metadata: { sqlId: snippet.id },
|
||||
}
|
||||
store.activeTab = tabId
|
||||
// if (sql && name) {
|
||||
// try {
|
||||
// const snippet = createSqlSnippetSkeletonV2({
|
||||
// id,
|
||||
// name,
|
||||
// sql,
|
||||
// owner_id: profile?.id,
|
||||
// project_id: project?.id,
|
||||
// })
|
||||
// snapV2.addSnippet({ projectRef: ref, snippet })
|
||||
// snapV2.addNeedsSaving(snippet.id)
|
||||
// store.openTabs = [...store.openTabs, tabId]
|
||||
// store.tabsMap[tabId] = {
|
||||
// id: tabId,
|
||||
// type: 'sql',
|
||||
// // Remove the label since we'll derive it dynamically
|
||||
// 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}`)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const snippet = createSqlSnippetSkeletonV2({
|
||||
id: uuidv4(),
|
||||
name: untitledSnippetTitle,
|
||||
owner_id: profile.id,
|
||||
project_id: project.id,
|
||||
sql: '',
|
||||
})
|
||||
snapV2.addSnippet({ projectRef: ref as string, snippet })
|
||||
removeNewTab(ref)
|
||||
router.push(`/project/${ref}/sql/${snippet.id}`)
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to create new query: ${error.message}`)
|
||||
}
|
||||
}
|
||||
// router.push(`/project/${ref}/sql/${snippet.id}`)
|
||||
// } catch (error: any) {
|
||||
// toast.error(`Failed to create new query: ${error.message}`)
|
||||
// }
|
||||
// } else {
|
||||
// try {
|
||||
// const snippet = createSqlSnippetSkeletonV2({
|
||||
// id: uuidv4(),
|
||||
// name: untitledSnippetTitle,
|
||||
// owner_id: profile.id,
|
||||
// project_id: project.id,
|
||||
// sql: '',
|
||||
// })
|
||||
// snapV2.addSnippet({ projectRef: ref as string, snippet })
|
||||
// removeNewTab(ref)
|
||||
// router.push(`/project/${ref}/sql/${snippet.id}`)
|
||||
// } catch (error: any) {
|
||||
// toast.error(`Failed to create new query: ${error.message}`)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -508,7 +509,7 @@ export function NewTab() {
|
||||
<TabsTrigger_Shadcn_ value="quickstarts">Quickstarts</TabsTrigger_Shadcn_>
|
||||
</TabsList_Shadcn_>
|
||||
<TabsContent_Shadcn_ value="templates" className="max-w-5xl mx-auto py-5">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-3 gap-4 px-8">
|
||||
{templates.map((item, i) => (
|
||||
<ActionCard
|
||||
onClick={() => {
|
||||
@@ -533,7 +534,7 @@ export function NewTab() {
|
||||
</div>
|
||||
</TabsContent_Shadcn_>
|
||||
<TabsContent_Shadcn_ value="quickstarts" className="max-w-5xl mx-auto py-5">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-3 gap-4 px-8">
|
||||
{quickstarts.map((item, i) => (
|
||||
<ActionCard
|
||||
onClick={() => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { API_URL } from 'lib/constants'
|
||||
import type { Dashboards, LogSqlSnippets, Owner, SqlSnippets } from 'types'
|
||||
import { contentKeys } from './keys'
|
||||
import { createTabId, getTabsStore, removeTabs } from 'state/tabs'
|
||||
import { getRecentItemsByType, removeRecentItems } from 'state/recent-items'
|
||||
|
||||
export type ContentBase = {
|
||||
id: string
|
||||
@@ -58,16 +59,26 @@ export async function getContent(
|
||||
throw new Error('Content not found')
|
||||
}
|
||||
|
||||
// handle local tabs
|
||||
// get current content ids
|
||||
const currentContentIds = response.data
|
||||
.filter((content: Content) => content.type === 'sql')
|
||||
.map((content: Content) => createTabId('sql', { id: content.id }))
|
||||
|
||||
// handle local tabs
|
||||
// checks IDs against localstorage state
|
||||
const tabsStore = getTabsStore(projectRef)
|
||||
const tabIds = tabsStore.openTabs.filter((id: string) => !currentContentIds.includes(id))
|
||||
// attempts to remove tabs that are no longer in the response
|
||||
removeTabs(projectRef, tabIds)
|
||||
|
||||
// handle recent items
|
||||
const recentItems = getRecentItemsByType('sql')
|
||||
// remove recent items that are no longer in the response
|
||||
removeRecentItems(
|
||||
// tabIds that are no longer in the response
|
||||
recentItems.filter((item) => !currentContentIds.includes(item.id)).map((item) => item.id)
|
||||
)
|
||||
|
||||
return {
|
||||
content: response.data,
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ import type { NextPageWithLayout } from 'types'
|
||||
const SqlEditor: NextPageWithLayout = () => {
|
||||
const router = useRouter()
|
||||
const monaco = useMonaco()
|
||||
const { id, ref } = useParams()
|
||||
const { id, ref, content } = useParams()
|
||||
|
||||
const { project } = useProjectContext()
|
||||
const appSnap = useAppStateSnapshot()
|
||||
@@ -124,12 +124,12 @@ const SqlEditor: NextPageWithLayout = () => {
|
||||
pgInfoRef.current.functions = functions
|
||||
}
|
||||
|
||||
// useEffect(() => {
|
||||
// if (id === 'new' && appSnap.dashboardHistory.sql !== undefined && content === undefined) {
|
||||
// const snippet = snippets.find((snippet) => snippet.id === appSnap.dashboardHistory.sql)
|
||||
// if (snippet !== undefined) router.push(`/project/${ref}/sql/${appSnap.dashboardHistory.sql}`)
|
||||
// }
|
||||
// }, [id, snippets, content])
|
||||
useEffect(() => {
|
||||
if (id === 'new' && appSnap.dashboardHistory.sql !== undefined && content === undefined) {
|
||||
const snippet = snippets.find((snippet) => snippet.id === appSnap.dashboardHistory.sql)
|
||||
if (snippet !== undefined) router.push(`/project/${ref}/sql/${appSnap.dashboardHistory.sql}`)
|
||||
}
|
||||
}, [id, snippets, content])
|
||||
|
||||
// Enable pgsql format
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useParams } from 'common'
|
||||
import { useFeaturePreviewContext } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
import { EditorBaseLayout } from 'components/layouts/editors/editor-base-layout'
|
||||
import { ProjectContextFromParamsProvider } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import SQLEditorLayout from 'components/layouts/SQLEditorLayout/SQLEditorLayout'
|
||||
import { SQLEditorMenu } from 'components/layouts/SQLEditorLayout/SQLEditorMenu'
|
||||
import { NewTab } from 'components/layouts/tabs/new-tab'
|
||||
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
|
||||
import { useRouter } from 'next/router'
|
||||
import { getTabsStore } from 'state/tabs'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
@@ -13,15 +15,21 @@ const TableEditorPage: NextPageWithLayout = () => {
|
||||
const store = getTabsStore(projectRef)
|
||||
const router = useRouter()
|
||||
|
||||
// handle Tabs preview logic
|
||||
// handle redirect to last table tab
|
||||
const lastTabId = store.openTabs.find((id) => store.tabsMap[id]?.type === 'sql')
|
||||
|
||||
if (lastTabId) {
|
||||
const lastTab = store.tabsMap[lastTabId]
|
||||
if (lastTab) {
|
||||
router.push(`/project/${projectRef}/sql/${lastTab.id.replace('sql-', '')}`)
|
||||
}
|
||||
}
|
||||
// redirect to /new if not using tabs
|
||||
const { flags } = useFeaturePreviewContext()
|
||||
const isSqlEditorTabsEnabled = flags[LOCAL_STORAGE_KEYS.UI_SQL_EDITOR_TABS]
|
||||
if (!isSqlEditorTabsEnabled) {
|
||||
router.push(`/project/${projectRef}/sql/new`)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
File renamed without changes.
@@ -1,124 +1,157 @@
|
||||
// Import necessary functions from valtio for state management
|
||||
import { proxy, subscribe } from 'valtio'
|
||||
// Import Tab and TabType types for defining recent items
|
||||
import { Tab, TabType } from './tabs'
|
||||
|
||||
// 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
|
||||
|
||||
// Define the structure of a recent item
|
||||
export interface RecentItem {
|
||||
id: string
|
||||
type: TabType
|
||||
label: string
|
||||
timestamp: number
|
||||
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?: {
|
||||
schema?: string
|
||||
name?: string
|
||||
tableId?: number
|
||||
sqlId?: string
|
||||
// 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 RecentItemsState {
|
||||
items: RecentItem[]
|
||||
items: RecentItem[] // Array of recent items
|
||||
}
|
||||
|
||||
// Default state for recent items
|
||||
const defaultState: RecentItemsState = {
|
||||
items: [],
|
||||
items: [], // Initialize with an empty array
|
||||
}
|
||||
|
||||
// Load initial state from localStorage
|
||||
const loadInitialState = (): RecentItemsState => {
|
||||
// Check if running in a browser environment
|
||||
if (typeof window === 'undefined') return defaultState
|
||||
|
||||
// Retrieve stored recent items from localStorage
|
||||
const stored = localStorage.getItem(RECENT_ITEMS_KEY)
|
||||
if (!stored) return defaultState
|
||||
if (!stored) return defaultState // Return default state if nothing is stored
|
||||
|
||||
try {
|
||||
// Parse and return the stored items
|
||||
return JSON.parse(stored)
|
||||
} catch (error) {
|
||||
// Log error if parsing fails
|
||||
console.error('Failed to parse recent items from localStorage:', error)
|
||||
return defaultState
|
||||
return defaultState // Return default state on error
|
||||
}
|
||||
}
|
||||
|
||||
// Create a proxy store for recent items state
|
||||
export const recentItemsStore = proxy<RecentItemsState>(loadInitialState())
|
||||
|
||||
// Subscribe to changes and save to localStorage
|
||||
// Subscribe to changes in the store and save to localStorage
|
||||
if (typeof window !== 'undefined') {
|
||||
// Function to save current state to localStorage
|
||||
const saveToStorage = () => {
|
||||
try {
|
||||
// Save the recent items as a JSON string
|
||||
localStorage.setItem(RECENT_ITEMS_KEY, JSON.stringify(recentItemsStore))
|
||||
} catch (error) {
|
||||
// Log error if saving fails
|
||||
console.error('Failed to save recent items to localStorage:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Use a debounced version to avoid too many writes
|
||||
// Use a debounced version to avoid too many writes to localStorage
|
||||
let timeout: NodeJS.Timeout
|
||||
const debouncedSave = () => {
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(saveToStorage, 1000)
|
||||
clearTimeout(timeout) // Clear previous timeout
|
||||
timeout = setTimeout(saveToStorage, 1000) // Set new timeout
|
||||
}
|
||||
|
||||
// Subscribe to changes using valtio's subscribe
|
||||
// Subscribe to changes in the recent items store
|
||||
subscribe(recentItemsStore, () => {
|
||||
debouncedSave()
|
||||
debouncedSave() // Call the debounced save function on changes
|
||||
})
|
||||
}
|
||||
|
||||
// Function to add a recent item
|
||||
export const addRecentItem = (tab: Tab) => {
|
||||
// First check if item with same ID already exists
|
||||
// Check if an item with the same ID already exists
|
||||
const existingItem = recentItemsStore.items.find((item) => item.id === tab.id)
|
||||
|
||||
if (existingItem) {
|
||||
// If it exists, just update its timestamp
|
||||
// If it exists, update its timestamp
|
||||
existingItem.timestamp = Date.now()
|
||||
return
|
||||
return // Exit the function
|
||||
}
|
||||
|
||||
// If it doesn't exist, create and add new item
|
||||
// If it doesn't exist, create and add a new item
|
||||
const recentItem: RecentItem = {
|
||||
id: tab.id,
|
||||
type: tab.type,
|
||||
label: tab.label || 'Untitled',
|
||||
timestamp: Date.now(),
|
||||
metadata: tab.metadata,
|
||||
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
|
||||
}
|
||||
|
||||
// Remove any existing instance of this item (based on type and metadata)
|
||||
// Remove any existing instance of this item based on type and metadata
|
||||
recentItemsStore.items = recentItemsStore.items.filter((existingItem) => {
|
||||
if (existingItem.type !== recentItem.type) return true
|
||||
if (existingItem.type !== recentItem.type) return true // Keep items of different types
|
||||
|
||||
// Check for duplicates based on type-specific metadata
|
||||
switch (recentItem.type) {
|
||||
case 'sql':
|
||||
return existingItem.metadata?.sqlId !== recentItem.metadata?.sqlId
|
||||
return existingItem.metadata?.sqlId !== recentItem.metadata?.sqlId // Check SQL ID
|
||||
case 'r':
|
||||
case 'v':
|
||||
case 'm':
|
||||
case 'f':
|
||||
case 'p':
|
||||
return existingItem.metadata?.tableId !== recentItem.metadata?.tableId
|
||||
return existingItem.metadata?.tableId !== recentItem.metadata?.tableId // Check Table ID
|
||||
case 'schema':
|
||||
return existingItem.metadata?.schema !== recentItem.metadata?.schema
|
||||
return existingItem.metadata?.schema !== recentItem.metadata?.schema // Check Schema
|
||||
default:
|
||||
return true
|
||||
return true // Keep items of unknown type
|
||||
}
|
||||
})
|
||||
|
||||
// Add the new recent item to the beginning of the list
|
||||
recentItemsStore.items.unshift(recentItem)
|
||||
}
|
||||
|
||||
// Function to clear all recent items
|
||||
export const clearRecentItems = () => {
|
||||
recentItemsStore.items = []
|
||||
recentItemsStore.items = [] // Set items to an empty array
|
||||
}
|
||||
|
||||
// Function to remove a specific recent item by its ID
|
||||
export const removeRecentItem = (itemId: string) => {
|
||||
recentItemsStore.items = recentItemsStore.items.filter((item) => item.id !== itemId)
|
||||
}
|
||||
|
||||
// Function to remove multiple recent items by their IDs
|
||||
export const removeRecentItems = (itemIds: string[]) => {
|
||||
recentItemsStore.items = recentItemsStore.items.filter((item) => !itemIds.includes(item.id))
|
||||
}
|
||||
|
||||
// Function to remove recent items by their type
|
||||
export const removeRecentItemsByType = (type: TabType) => {
|
||||
recentItemsStore.items = recentItemsStore.items.filter((item) => item.type !== type)
|
||||
}
|
||||
|
||||
// Function to get all recent items
|
||||
export const getRecentItems = () => {
|
||||
return recentItemsStore.items
|
||||
}
|
||||
|
||||
// Function to get recent items by their type
|
||||
export const getRecentItemsByType = (type: TabType) => {
|
||||
return recentItemsStore.items.filter((item) => item.type === type)
|
||||
}
|
||||
@@ -265,7 +265,7 @@ export const handleTabClose = (
|
||||
handleTabNavigation(ref, nextTabId, router)
|
||||
} else {
|
||||
// If no tabs of same type, go to the home of the current section
|
||||
switch (currentTab.type) {
|
||||
switch (currentTab?.type) {
|
||||
case 'sql':
|
||||
router.push(`/project/${router.query.ref}/sql`)
|
||||
break
|
||||
@@ -273,7 +273,7 @@ export const handleTabClose = (
|
||||
router.push(`/project/${router.query.ref}/editor`)
|
||||
break
|
||||
default:
|
||||
router.push(`/project/${router.query.ref}/editor`)
|
||||
router.push(`/project/${router.query.ref}/${editor}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,3 +364,21 @@ export const editorEntityTypes = {
|
||||
table: ['r', 'v', 'm', 'f', 'p', 'schema'],
|
||||
sql: ['sql'],
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
export function removePreviewTab(ref: string | undefined) {
|
||||
if (!ref) return
|
||||
const store = getTabsStore(ref)
|
||||
store.previewTabId = undefined
|
||||
}
|
||||
Reference in new issue
Block a user