Tabs.utils.ts156 lines · main
| 1 | import { useCallback, useEffect, useRef } from 'react' |
| 2 | |
| 3 | import { Entity } from '@/data/entity-types/entity-types-infinite-query' |
| 4 | import useLatest from '@/hooks/misc/useLatest' |
| 5 | import { createTabId, editorEntityTypes, useTabsStateSnapshot } from '@/state/tabs' |
| 6 | |
| 7 | export 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 | |
| 63 | export 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 | |
| 110 | interface UseTabsScrollOptions { |
| 111 | activeTab: string | null | undefined |
| 112 | tabCount: number |
| 113 | } |
| 114 | |
| 115 | export 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 | } |