Files
supabase/apps/studio/components/layouts/Tabs/Tabs.utils.ts
2026-04-01 10:22:37 +02:00

157 lines
5.4 KiB
TypeScript

import { useCallback, useEffect, useRef } from 'react'
import { Entity } from '@/data/entity-types/entity-types-infinite-query'
import useLatest from '@/hooks/misc/useLatest'
import { createTabId, editorEntityTypes, useTabsStateSnapshot } from '@/state/tabs'
export function useTableEditorTabsCleanUp() {
const tabs = useTabsStateSnapshot()
const tabMapRef = useLatest(tabs.tabsMap)
const recentItemsRef = useLatest(tabs.recentItems)
const openTabsRef = useLatest(tabs.openTabs)
return useCallback(({ schemas, entities }: { schemas: string[]; entities: Entity[] }) => {
// Identity all entities by tab ID
const entitiesById: string[] = entities.map((x: Entity) => createTabId(x.type, { id: x.id }))
const recentItemsFilteredToSchemas: string[] = []
for (const schema of schemas) {
recentItemsFilteredToSchemas.push(
...recentItemsRef.current.filter((x) => x.metadata?.schema === schema).map((x) => x.id)
)
}
const recentItemsToRemove = [
...recentItemsFilteredToSchemas.filter((entityId) => !entitiesById.includes(entityId)),
]
tabs.removeRecentItems(recentItemsToRemove)
const tabsFilteredToSchemas: string[] = []
for (const schema of schemas) {
tabsFilteredToSchemas.push(
...openTabsRef.current.filter((tabId) => {
const tab = tabMapRef.current[tabId]
return tab?.metadata?.schema === schema
})
)
}
const tableEditorTabsToBeCleaned = [
...tabsFilteredToSchemas.filter(
(id) => !id.startsWith('sql') && !id.startsWith('schema') && !entitiesById.includes(id)
),
]
// perform tabs cleanup
tabs.removeTabs(tableEditorTabsToBeCleaned)
// [Joshen] Validate for opened tabs, if their label matches the entity's name - update label if not
// As the entity could've been renamed outside of the table editor
// e.g Using the SQL editor to rename the entity
const openTabs = openTabsRef.current
.map((id) => tabMapRef.current[id])
.filter((tab) => !!tab && editorEntityTypes['table']?.includes(tab.type))
openTabs.forEach((tab) => {
const entity = entities?.find((x) => tab.metadata?.tableId === x.id)
if (!!entity && entity.name !== tab.label) tabs.updateTab(tab.id, { label: entity.name })
})
}, [])
}
export function useSqlEditorTabsCleanup() {
const tabs = useTabsStateSnapshot()
const tabMapRef = useLatest(tabs.tabsMap)
const openTabsRef = useLatest(tabs.openTabs)
return useCallback(({ snippets }: { snippets: { id: string; type: string; name: 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 snippetTabsToBeCleaned = openTabsRef.current.filter(
(id: string) => id.startsWith('sql') && !currentContentIds.includes(id)
)
tabs.removeTabs(snippetTabsToBeCleaned)
// Remove any recent items that might no longer be existing (removed outside of the dashboard session)
const recentItems = tabs.getRecentItemsByType('sql')
tabs.removeRecentItems(
recentItems
? recentItems.filter((item) => !currentContentIds.includes(item.id)).map((item) => item.id)
: []
)
// [Joshen] Validate for opened tabs, if their label matches the snippet's name - update label if not
// As the snippets name could've been updated outside of the SQL Editor session
// e.g for a shared snippet, the owner could've updated the name of the snippet
const openSqlTabs = openTabsRef.current
.map((id) => tabMapRef.current[id])
.filter((tab) => !!tab && editorEntityTypes['sql']?.includes(tab.type))
openSqlTabs.forEach((tab) => {
const snippet = snippets?.find((x) => tab.metadata?.sqlId === x.id)
if (!!snippet && snippet.name !== tab.label) tabs.updateTab(tab.id, { label: snippet.name })
})
}, [])
}
interface UseTabsScrollOptions {
activeTab: string | null | undefined
tabCount: number
}
export function useTabsScroll({ activeTab, tabCount }: UseTabsScrollOptions) {
const tabsListRef = useRef<HTMLDivElement>(null)
const prevTabCountRef = useRef<number>(tabCount)
const isInitialMount = useRef(true)
useEffect(() => {
if (tabsListRef.current) {
tabsListRef.current.scrollLeft = tabsListRef.current.scrollWidth
}
}, [])
useEffect(() => {
if (isInitialMount.current) {
isInitialMount.current = false
return
}
if (!tabsListRef.current) return
const tabCountIncreased = tabCount > prevTabCountRef.current
if (tabCountIncreased) {
tabsListRef.current.scrollLeft = tabsListRef.current.scrollWidth
} else if (activeTab) {
const activeTabElement = tabsListRef.current.querySelector(
`[data-state="active"]`
) as HTMLElement
if (activeTabElement) {
activeTabElement.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'nearest',
})
}
}
prevTabCountRef.current = tabCount
}, [activeTab, tabCount])
return { tabsListRef }
}