From 481fb13ef2c8a9c0f4165dfe0364b644386526ce Mon Sep 17 00:00:00 2001 From: Jonathan Summers-Muir Date: Tue, 26 Nov 2024 16:24:46 +0800 Subject: [PATCH] tabs now in feature flags --- .../FeaturePreview/FeaturePreviewModal.tsx | 13 +++ .../explorer/SQLEditorNavV2/SQLEditorNav.tsx | 2 +- .../components/layouts/tabs/explorer-tabs.tsx | 5 +- .../components/layouts/tabs/new-tab.tsx | 93 ++++++++-------- apps/studio/data/content/content-query.ts | 13 ++- apps/studio/pages/project/[ref]/sql/[id].tsx | 14 +-- apps/studio/pages/project/[ref]/sql/index.tsx | 10 +- .../[ref]/sql/{new.tsx => new-upcoming.tsx} | 0 apps/studio/state/recent-items.ts | 101 ++++++++++++------ apps/studio/state/tabs.ts | 22 +++- 10 files changed, 180 insertions(+), 93 deletions(-) rename apps/studio/pages/project/[ref]/sql/{new.tsx => new-upcoming.tsx} (100%) diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx index 01cbee09063..12c92478c05 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx @@ -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() { diff --git a/apps/studio/components/layouts/explorer/SQLEditorNavV2/SQLEditorNav.tsx b/apps/studio/components/layouts/explorer/SQLEditorNavV2/SQLEditorNav.tsx index 8d5d1a1fa6e..d57c8e12415 100644 --- a/apps/studio/components/layouts/explorer/SQLEditorNavV2/SQLEditorNav.tsx +++ b/apps/studio/components/layouts/explorer/SQLEditorNavV2/SQLEditorNav.tsx @@ -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]}`) } diff --git a/apps/studio/components/layouts/tabs/explorer-tabs.tsx b/apps/studio/components/layouts/tabs/explorer-tabs.tsx index a2335c866b1..62f45808231 100644 --- a/apps/studio/components/layouts/tabs/explorer-tabs.tsx +++ b/apps/studio/components/layouts/tabs/explorer-tabs.tsx @@ -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 ( { - 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() { Quickstarts -
+
{templates.map((item, i) => ( { @@ -533,7 +534,7 @@ export function NewTab() {
-
+
{quickstarts.map((item, i) => ( { diff --git a/apps/studio/data/content/content-query.ts b/apps/studio/data/content/content-query.ts index e7cda81aea4..05692814223 100644 --- a/apps/studio/data/content/content-query.ts +++ b/apps/studio/data/content/content-query.ts @@ -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, } diff --git a/apps/studio/pages/project/[ref]/sql/[id].tsx b/apps/studio/pages/project/[ref]/sql/[id].tsx index 279b429f28a..0743d40d256 100644 --- a/apps/studio/pages/project/[ref]/sql/[id].tsx +++ b/apps/studio/pages/project/[ref]/sql/[id].tsx @@ -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(() => { diff --git a/apps/studio/pages/project/[ref]/sql/index.tsx b/apps/studio/pages/project/[ref]/sql/index.tsx index a80bc09e975..8bc345963a4 100644 --- a/apps/studio/pages/project/[ref]/sql/index.tsx +++ b/apps/studio/pages/project/[ref]/sql/index.tsx @@ -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 ( <> diff --git a/apps/studio/pages/project/[ref]/sql/new.tsx b/apps/studio/pages/project/[ref]/sql/new-upcoming.tsx similarity index 100% rename from apps/studio/pages/project/[ref]/sql/new.tsx rename to apps/studio/pages/project/[ref]/sql/new-upcoming.tsx diff --git a/apps/studio/state/recent-items.ts b/apps/studio/state/recent-items.ts index 1c06f06b527..b400b79ce17 100644 --- a/apps/studio/state/recent-items.ts +++ b/apps/studio/state/recent-items.ts @@ -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(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) } diff --git a/apps/studio/state/tabs.ts b/apps/studio/state/tabs.ts index 9228687c0b6..0b78496df2c 100644 --- a/apps/studio/state/tabs.ts +++ b/apps/studio/state/tabs.ts @@ -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 +}