mirror of
https://github.com/supabase/supabase.git
synced 2026-10-10 11:55:05 +03:00
* init * Update inner-side-menu.mdx * chore: update SQL sidebar to use ui pattern components * mor * Update * Update index.tsx * init: merge table editor and sql editor and schema visualization together * more * move to valtio * fix issue with Command+B shortcut * now shows in treeviews if item is opened in tab * Update ProjectLayout.tsx * fix sidebar * fix schema selector for non explorer version * show schema name in tabs * added schema names to tabs * tabs have been updated to support preview tabs * fix URL issue * add empty state stuff * Update SQLEditorNav.tsx * preview tab works now * more tabs stuff. 'new' tab also added * new tab concept * updates * fix type errors * remove unused files * update test * move back button, fix width issues on sidebar * update sidebar logic * Update ProjectLayout.tsx * lots of updates. layouts now streamlined. localstorage for tabs in use * moar * bunch of new tab logic * fix empty tab issue * Update tabs.ts * layouts switched * new pages now have fixed layouts * fix tabs * fix code bg * add tabs support for multiple project refs * intialization issue * update ID handling * fixed isOpened state for SQL snippets * remove old assistant because its bugging up panels * preview style works in sql editor * fix border * removes preview tab if there is one * fix background of loading skeleton * lots of issues with types/icons/redirect * new tab cards * snippets in empty state now work * moar stuff * tabs now in feature flags * Update tabs.ts * Update tabs.ts * moar * add feature previews * remove code not needed * Update next-env.d.ts * Delete FeaturePreviewModal.tsx * fix typescript errors. remove more explorer stuff * remove explorer files * fixed issues with templates and quickstarts tab * fixed active state when tabs are not opted in * logic error * fix open/highlight issue when opted out of tabs * templates/quickstarts now displayed with new cards * Update recent-items.tsx * Update new-tab.tsx * add icon back in * add old empty state back in * recent items updated to respect project ref * localstorage cleanup on deletion * moar * overflow tabs now working * correct tab names used for new sql templates/quickstarts * ongoing queries fix * cleanup * update images * Update RouteValidationWrapper.tsx * Update AppLayout.tsx * Update NavigationBar.tsx * add headers back into side panels * improve writing * tabs now drag and drop a billion times better * Update tabs.tsx * Update tabs.tsx * init issues on stores, which caused a race condition. * fix hydration error * fix new tab issue in sql * Update ProjectLayout.tsx * Update pnpm-lock.yaml * Update new-tab.tsx * move EditorMenuListSkeleton * Fix type issues * fixes: DESIGN-87 fixes: DESIGN-87 * refactor sort/filter components * Update rules-set-button-text.tsx * remove discussions for now * small styling fixes * Update FeaturePreviewModal.tsx * Update FeaturePreviewModal.tsx * Update RouteValidationWrapper.tsx * revert * revert * revert * revert * revert * more revert * Update collapse-button.tsx * Update SQLEditorTreeViewItem.tsx * revert * Update SchemaGraph.tsx * Delete new-upcoming.tsx * revert * Update ProjectLayout.tsx * fix home link * Update table-editor.spec.ts * test update * Update table-editor.spec.ts * Fix the playwright tests. * layout fixes * layout fix * revert sort/filter * Update LastSignInWrapper.tsx * revert * revert * remove * update file names * revert * revert * revert * Fix TreeView console error props * Add guards in SQL Editor to ensure that feature preview tabs changes do not affect existing UI when flag is off * Fix missing DefaultLayout in SQL editor templates + fix New tab * Remove console log * Remove DatabaseSelector for SQL editor on local * Fix SQL editor shared favorites for local * Fix test * Ensure NewTab doesn't show up if flag is not toggled for SQL editor * Decouple UI state changes from content-query and entity-types-infinite-query * Fix tab closing unnecessary rerouting * Beef up feature previews * Fix create new table from table editor new tab * Fix tabs getting incorrectly reset when going between table and SQL editors * Fix last visited SQL snippet for both tabs and not tabs * Fix last visited table for table editor tabs * Clear dashboard history when closing last tab * Fix loading dashboard history * Add comment to refactor stores * Ensure we only save up to 8 items for recent items for each type * Remove unneccesary logic in tabs * Smol style fix for DeleteAccountButton * Smol fix * Fix inability to close New tab --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com> Co-authored-by: Alaister Young <a@alaisteryoung.com> Co-authored-by: Ivan Vasilov <vasilov.ivan@gmail.com>
393 lines
11 KiB
TypeScript
393 lines
11 KiB
TypeScript
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<TabsStateMap>({})
|
|
|
|
// 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<T extends TabType>(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)
|
|
}
|