Tabs.utils.ts156 lines · main
1import { useCallback, useEffect, useRef } from 'react'
2
3import { Entity } from '@/data/entity-types/entity-types-infinite-query'
4import useLatest from '@/hooks/misc/useLatest'
5import { createTabId, editorEntityTypes, useTabsStateSnapshot } from '@/state/tabs'
6
7export function useTableEditorTabsCleanUp() {
8 const tabs = useTabsStateSnapshot()
9 const tabMapRef = useLatest(tabs.tabsMap)
10 const recentItemsRef = useLatest(tabs.recentItems)
11 const openTabsRef = useLatest(tabs.openTabs)
12
13 return useCallback(({ schemas, entities }: { schemas: string[]; entities: Entity[] }) => {
14 // Identity all entities by tab ID
15 const entitiesById: string[] = entities.map((x: Entity) => createTabId(x.type, { id: x.id }))
16
17 const recentItemsFilteredToSchemas: string[] = []
18 for (const schema of schemas) {
19 recentItemsFilteredToSchemas.push(
20 ...recentItemsRef.current.filter((x) => x.metadata?.schema === schema).map((x) => x.id)
21 )
22 }
23
24 const recentItemsToRemove = [
25 ...recentItemsFilteredToSchemas.filter((entityId) => !entitiesById.includes(entityId)),
26 ]
27
28 tabs.removeRecentItems(recentItemsToRemove)
29
30 const tabsFilteredToSchemas: string[] = []
31 for (const schema of schemas) {
32 tabsFilteredToSchemas.push(
33 ...openTabsRef.current.filter((tabId) => {
34 const tab = tabMapRef.current[tabId]
35 return tab?.metadata?.schema === schema
36 })
37 )
38 }
39
40 const tableEditorTabsToBeCleaned = [
41 ...tabsFilteredToSchemas.filter(
42 (id) => !id.startsWith('sql') && !id.startsWith('schema') && !entitiesById.includes(id)
43 ),
44 ]
45
46 // perform tabs cleanup
47 tabs.removeTabs(tableEditorTabsToBeCleaned)
48
49 // [Joshen] Validate for opened tabs, if their label matches the entity's name - update label if not
50 // As the entity could've been renamed outside of the table editor
51 // e.g Using the SQL editor to rename the entity
52 const openTabs = openTabsRef.current
53 .map((id) => tabMapRef.current[id])
54 .filter((tab) => !!tab && editorEntityTypes['table']?.includes(tab.type))
55
56 openTabs.forEach((tab) => {
57 const entity = entities?.find((x) => tab.metadata?.tableId === x.id)
58 if (!!entity && entity.name !== tab.label) tabs.updateTab(tab.id, { label: entity.name })
59 })
60 }, [])
61}
62
63export function useSqlEditorTabsCleanup() {
64 const tabs = useTabsStateSnapshot()
65 const tabMapRef = useLatest(tabs.tabsMap)
66 const openTabsRef = useLatest(tabs.openTabs)
67
68 return useCallback(({ snippets }: { snippets: { id: string; type: string; name: string }[] }) => {
69 // these are tabs that are static content
70 // these canot be removed from localstorage based on this query request
71 const IGNORED_TAB_IDS = ['sql-templates', 'sql-quickstarts']
72
73 // Identify all SQL snippets / content by their tab ids
74 const currentContentIds = [
75 ...snippets
76 .filter((content) => content.type === 'sql')
77 .map((content) => createTabId('sql', { id: content.id })),
78 // append ignored tab IDs
79 ...IGNORED_TAB_IDS,
80 ]
81
82 // Remove any snippet tabs that might no longer be existing (removed outside of the dashboard session)
83 const snippetTabsToBeCleaned = openTabsRef.current.filter(
84 (id: string) => id.startsWith('sql') && !currentContentIds.includes(id)
85 )
86 tabs.removeTabs(snippetTabsToBeCleaned)
87
88 // Remove any recent items that might no longer be existing (removed outside of the dashboard session)
89 const recentItems = tabs.getRecentItemsByType('sql')
90 tabs.removeRecentItems(
91 recentItems
92 ? recentItems.filter((item) => !currentContentIds.includes(item.id)).map((item) => item.id)
93 : []
94 )
95
96 // [Joshen] Validate for opened tabs, if their label matches the snippet's name - update label if not
97 // As the snippets name could've been updated outside of the SQL Editor session
98 // e.g for a shared snippet, the owner could've updated the name of the snippet
99 const openSqlTabs = openTabsRef.current
100 .map((id) => tabMapRef.current[id])
101 .filter((tab) => !!tab && editorEntityTypes['sql']?.includes(tab.type))
102
103 openSqlTabs.forEach((tab) => {
104 const snippet = snippets?.find((x) => tab.metadata?.sqlId === x.id)
105 if (!!snippet && snippet.name !== tab.label) tabs.updateTab(tab.id, { label: snippet.name })
106 })
107 }, [])
108}
109
110interface UseTabsScrollOptions {
111 activeTab: string | null | undefined
112 tabCount: number
113}
114
115export function useTabsScroll({ activeTab, tabCount }: UseTabsScrollOptions) {
116 const tabsListRef = useRef<HTMLDivElement>(null)
117 const prevTabCountRef = useRef<number>(tabCount)
118 const isInitialMount = useRef(true)
119
120 useEffect(() => {
121 if (tabsListRef.current) {
122 tabsListRef.current.scrollLeft = tabsListRef.current.scrollWidth
123 }
124 }, [])
125
126 useEffect(() => {
127 if (isInitialMount.current) {
128 isInitialMount.current = false
129 return
130 }
131
132 if (!tabsListRef.current) return
133
134 const tabCountIncreased = tabCount > prevTabCountRef.current
135
136 if (tabCountIncreased) {
137 tabsListRef.current.scrollLeft = tabsListRef.current.scrollWidth
138 } else if (activeTab) {
139 const activeTabElement = tabsListRef.current.querySelector(
140 `[data-state="active"]`
141 ) as HTMLElement
142
143 if (activeTabElement) {
144 activeTabElement.scrollIntoView({
145 behavior: 'smooth',
146 block: 'nearest',
147 inline: 'nearest',
148 })
149 }
150 }
151
152 prevTabCountRef.current = tabCount
153 }, [activeTab, tabCount])
154
155 return { tabsListRef }
156}