From deda9820741a91738bb7610bc70e87fccf51e3bb Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 10 Sep 2025 22:19:08 +0800 Subject: [PATCH] Chore/refactor dashboard history logic (#38586) * Shift dashboard history logic outside of app state, into its own hook * Self remediate malformed history for sql editor * Update apps/studio/hooks/misc/useDashboardHistory.ts Co-authored-by: Jordi Enric <37541088+jordienr@users.noreply.github.com> * Update apps/studio/hooks/misc/useDashboardHistory.ts Co-authored-by: Jordi Enric <37541088+jordienr@users.noreply.github.com> --------- Co-authored-by: Jordi Enric <37541088+jordienr@users.noreply.github.com> --- .../interfaces/App/RouteValidationWrapper.tsx | 31 +++++----------- .../TableGridEditor/TableGridEditor.tsx | 23 ++++++------ apps/studio/components/layouts/Tabs/Tabs.tsx | 10 +++--- apps/studio/hooks/misc/useDashboardHistory.ts | 30 ++++++++++++++++ .../pages/project/[ref]/editor/index.tsx | 26 +++++++------- apps/studio/pages/project/[ref]/index.tsx | 1 + apps/studio/pages/project/[ref]/sql/[id].tsx | 30 +++++++++------- apps/studio/pages/project/[ref]/sql/index.tsx | 36 ++++++++++--------- apps/studio/state/app-state.ts | 22 +----------- 9 files changed, 110 insertions(+), 99 deletions(-) create mode 100644 apps/studio/hooks/misc/useDashboardHistory.ts diff --git a/apps/studio/components/interfaces/App/RouteValidationWrapper.tsx b/apps/studio/components/interfaces/App/RouteValidationWrapper.tsx index 55aa8393983..c0ee4634e34 100644 --- a/apps/studio/components/interfaces/App/RouteValidationWrapper.tsx +++ b/apps/studio/components/interfaces/App/RouteValidationWrapper.tsx @@ -5,28 +5,22 @@ import { toast } from 'sonner' import { LOCAL_STORAGE_KEYS, useIsLoggedIn, useIsMFAEnabled, useParams } from 'common' import { useOrganizationsQuery } from 'data/organizations/organizations-query' import { useProjectsQuery } from 'data/projects/projects-query' +import { useDashboardHistory } from 'hooks/misc/useDashboardHistory' import useLatest from 'hooks/misc/useLatest' import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { IS_PLATFORM } from 'lib/constants' -import { useAppStateSnapshot } from 'state/app-state' // Ideally these could all be within a _middleware when we use Next 12 const RouteValidationWrapper = ({ children }: PropsWithChildren<{}>) => { const router = useRouter() const { ref, slug, id } = useParams() - - const isLoggedIn = useIsLoggedIn() - const snap = useAppStateSnapshot() - const isUserMFAEnabled = useIsMFAEnabled() - const { data: organization } = useSelectedOrganizationQuery() - const [dashboardHistory, _, { isSuccess: isSuccessStorage }] = useLocalStorageQuery( - LOCAL_STORAGE_KEYS.DASHBOARD_HISTORY(ref ?? ''), - { editor: undefined, sql: undefined } - ) + const isLoggedIn = useIsLoggedIn() + const isUserMFAEnabled = useIsMFAEnabled() + const { setLastVisitedSnippet, setLastVisitedTable } = useDashboardHistory() const [__, setLastVisitedOrganization] = useLocalStorageQuery( LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION, '' @@ -106,22 +100,13 @@ const RouteValidationWrapper = ({ children }: PropsWithChildren<{}>) => { useEffect(() => { if (ref !== undefined && id !== undefined) { if (router.pathname.endsWith('/sql/[id]') && id !== 'new') { - snap.setDashboardHistory(ref, 'sql', id) + setLastVisitedSnippet(id) + } else if (router.pathname.endsWith('/editor/[id]')) { + setLastVisitedTable(id) } - if (router.pathname.endsWith('/editor/[id]')) { - snap.setDashboardHistory(ref, 'editor', id) - } - } - }, [ref, id]) - - useEffect(() => { - // Load dashboard history into app state - if (isSuccessStorage && ref) { - snap.setDashboardHistory(ref, 'editor', dashboardHistory.editor) - snap.setDashboardHistory(ref, 'sql', dashboardHistory.sql) } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isSuccessStorage, ref]) + }, [ref, id]) useEffect(() => { if (organization) { diff --git a/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx index fdeef8867e4..5a31e04f07c 100644 --- a/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx @@ -13,9 +13,9 @@ import { isView, } from 'data/table-editor/table-editor-types' import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' +import { useDashboardHistory } from 'hooks/misc/useDashboardHistory' import { useUrlState } from 'hooks/ui/useUrlState' import { useIsProtectedSchema } from 'hooks/useProtectedSchemas' -import { useAppStateSnapshot } from 'state/app-state' import { TableEditorTableStateContextProvider } from 'state/table-editor-table' import { createTabId, useTabsStateSnapshot } from 'state/tabs' import { Button } from 'ui' @@ -34,8 +34,8 @@ export const TableGridEditor = ({ selectedTable, }: TableGridEditorProps) => { const router = useRouter() - const appSnap = useAppStateSnapshot() const { ref: projectRef, id } = useParams() + const { setLastVisitedTable } = useDashboardHistory() const tabs = useTabsStateSnapshot() @@ -58,10 +58,6 @@ export const TableGridEditor = ({ const tabId = !!id ? tabs.openTabs.find((x) => x.endsWith(id)) : undefined const openTabs = tabs.openTabs.filter((x) => !x.startsWith('sql')) - const onClearDashboardHistory = useCallback(() => { - if (projectRef) appSnap.setDashboardHistory(projectRef, 'editor', undefined) - }, [appSnap, projectRef]) - const onTableCreated = useCallback( (table: { id: number }) => { router.push(`/project/${projectRef}/editor/${table.id}`) @@ -74,9 +70,14 @@ export const TableGridEditor = ({ if (selectedTable) { // Close tab const tabId = createTabId(selectedTable.entity_type, { id: selectedTable.id }) - tabs.handleTabClose({ id: tabId, router, editor: 'table', onClearDashboardHistory }) + tabs.handleTabClose({ + id: tabId, + router, + editor: 'table', + onClearDashboardHistory: () => setLastVisitedTable(undefined), + }) } - }, [onClearDashboardHistory, router, selectedTable, tabs]) + }, [router, selectedTable, tabs]) const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedTable?.schema ?? '' }) @@ -111,7 +112,7 @@ export const TableGridEditor = ({ id: tabId, router, editor: 'table', - onClearDashboardHistory, + onClearDashboardHistory: () => setLastVisitedTable(undefined), }) }} > @@ -122,7 +123,7 @@ export const TableGridEditor = ({ asChild type="default" className="mt-2" - onClick={() => appSnap.setDashboardHistory(projectRef, 'editor', undefined)} + onClick={() => setLastVisitedTable(undefined)} > Close tab @@ -133,7 +134,7 @@ export const TableGridEditor = ({ asChild type="default" className="mt-2" - onClick={() => appSnap.setDashboardHistory(projectRef, 'editor', undefined)} + onClick={() => setLastVisitedTable(undefined)} > Head back diff --git a/apps/studio/components/layouts/Tabs/Tabs.tsx b/apps/studio/components/layouts/Tabs/Tabs.tsx index c8d1b017aae..0816bd0a3f9 100644 --- a/apps/studio/components/layouts/Tabs/Tabs.tsx +++ b/apps/studio/components/layouts/Tabs/Tabs.tsx @@ -12,7 +12,7 @@ import { Plus, X } from 'lucide-react' import { useRouter } from 'next/router' import { useParams } from 'common' -import { useAppStateSnapshot } from 'state/app-state' +import { useDashboardHistory } from 'hooks/misc/useDashboardHistory' import { editorEntityTypes, useTabsStateSnapshot, type Tab } from 'state/tabs' import { cn, @@ -32,7 +32,7 @@ import { TabPreview } from './TabPreview' export const EditorTabs = () => { const { ref, id } = useParams() const router = useRouter() - const appSnap = useAppStateSnapshot() + const { setLastVisitedSnippet, setLastVisitedTable } = useDashboardHistory() const editor = useEditorType() const tabs = useTabsStateSnapshot() @@ -68,8 +68,10 @@ export const EditorTabs = () => { } const onClearDashboardHistory = () => { - if (ref && editor) { - appSnap.setDashboardHistory(ref, editor === 'table' ? 'editor' : editor, undefined) + if (editor === 'table') { + setLastVisitedTable(undefined) + } else if (editor === 'sql') { + setLastVisitedSnippet(undefined) } } diff --git a/apps/studio/hooks/misc/useDashboardHistory.ts b/apps/studio/hooks/misc/useDashboardHistory.ts new file mode 100644 index 00000000000..11da087ace1 --- /dev/null +++ b/apps/studio/hooks/misc/useDashboardHistory.ts @@ -0,0 +1,30 @@ +import { LOCAL_STORAGE_KEYS, useParams } from 'common' +import { useLocalStorageQuery } from './useLocalStorage' + +type DashboardHistory = { editor?: string; sql?: string } +const DEFAULT_HISTORY = { editor: undefined, sql: undefined } + +export const useDashboardHistory = () => { + // [Joshen] History should always refer to the project that the user is currently on + const { ref } = useParams() + + const [history, setHistory, { isSuccess }] = useLocalStorageQuery( + LOCAL_STORAGE_KEYS.DASHBOARD_HISTORY(ref ?? ''), + DEFAULT_HISTORY + ) + + const setLastVisitedTable = (id?: string) => { + setHistory({ ...history, editor: id }) + } + + const setLastVisitedSnippet = (id?: string) => { + setHistory({ ...history, sql: id }) + } + + return { + history, + setLastVisitedTable, + setLastVisitedSnippet, + isHistoryLoaded: isSuccess, + } +} diff --git a/apps/studio/pages/project/[ref]/editor/index.tsx b/apps/studio/pages/project/[ref]/editor/index.tsx index 6ba1073515f..6133db86b66 100644 --- a/apps/studio/pages/project/[ref]/editor/index.tsx +++ b/apps/studio/pages/project/[ref]/editor/index.tsx @@ -8,7 +8,7 @@ import { EditorBaseLayout } from 'components/layouts/editors/EditorBaseLayout' import TableEditorLayout from 'components/layouts/TableEditorLayout/TableEditorLayout' import { TableEditorMenu } from 'components/layouts/TableEditorLayout/TableEditorMenu' import { NewTab } from 'components/layouts/Tabs/NewTab' -import { useAppStateSnapshot } from 'state/app-state' +import { useDashboardHistory } from 'hooks/misc/useDashboardHistory' import { editorEntityTypes, useTabsStateSnapshot } from 'state/tabs' import type { NextPageWithLayout } from 'types' @@ -16,26 +16,28 @@ const TableEditorPage: NextPageWithLayout = () => { const router = useRouter() const { ref: projectRef } = useParams() const tabStore = useTabsStateSnapshot() - const appSnap = useAppStateSnapshot() + const { history, isHistoryLoaded } = useDashboardHistory() const onTableCreated = (table: { id: number }) => { router.push(`/project/${projectRef}/editor/${table.id}`) } useEffect(() => { - const lastOpenedTab = appSnap.dashboardHistory.editor + if (isHistoryLoaded) { + const lastOpenedTable = history.editor + const lastTabId = tabStore.openTabs.find((id) => + editorEntityTypes.table.includes(tabStore.tabsMap[id]?.type) + ) - const lastTabId = tabStore.openTabs.find((id) => - editorEntityTypes.table.includes(tabStore.tabsMap[id]?.type) - ) - if (lastOpenedTab !== undefined) { - router.push(`/project/${projectRef}/editor/${appSnap.dashboardHistory.editor}`) - } else if (lastTabId) { // Handle redirect to last opened table tab, or last table tab - const lastTab = tabStore.tabsMap[lastTabId] - if (lastTab) router.push(`/project/${projectRef}/editor/${lastTab.metadata?.tableId}`) + if (lastOpenedTable !== undefined) { + router.push(`/project/${projectRef}/editor/${history.editor}`) + } else if (lastTabId) { + const lastTab = tabStore.tabsMap[lastTabId] + if (lastTab) router.push(`/project/${projectRef}/editor/${lastTab.metadata?.tableId}`) + } } - }, []) + }, [isHistoryLoaded]) return ( <> diff --git a/apps/studio/pages/project/[ref]/index.tsx b/apps/studio/pages/project/[ref]/index.tsx index b786f450fe3..ec00ec1dd47 100644 --- a/apps/studio/pages/project/[ref]/index.tsx +++ b/apps/studio/pages/project/[ref]/index.tsx @@ -7,6 +7,7 @@ import type { NextPageWithLayout } from 'types' const HomePage: NextPageWithLayout = () => { const isHomeNew = useFlag('homeNew') + if (isHomeNew) { return } diff --git a/apps/studio/pages/project/[ref]/sql/[id].tsx b/apps/studio/pages/project/[ref]/sql/[id].tsx index 36acdb1ef89..558cfb8a90e 100644 --- a/apps/studio/pages/project/[ref]/sql/[id].tsx +++ b/apps/studio/pages/project/[ref]/sql/[id].tsx @@ -1,3 +1,4 @@ +import Link from 'next/link' import { useRouter } from 'next/router' import { useEffect } from 'react' @@ -9,8 +10,8 @@ import { useEditorType } from 'components/layouts/editors/EditorsLayout.hooks' import SQLEditorLayout from 'components/layouts/SQLEditorLayout/SQLEditorLayout' import { SQLEditorMenu } from 'components/layouts/SQLEditorLayout/SQLEditorMenu' import { useContentIdQuery } from 'data/content/content-id-query' -import Link from 'next/link' -import { useAppStateSnapshot } from 'state/app-state' +import { useDashboardHistory } from 'hooks/misc/useDashboardHistory' +import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { SnippetWithContent, useSnippets, useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2' import { createTabId, useTabsStateSnapshot } from 'state/tabs' import type { NextPageWithLayout } from 'types' @@ -20,11 +21,12 @@ import { Admonition } from 'ui-patterns' const SqlEditor: NextPageWithLayout = () => { const router = useRouter() const { id, ref, content, skip } = useParams() + const { data: project } = useSelectedProjectQuery() const editor = useEditorType() const tabs = useTabsStateSnapshot() - const appSnap = useAppStateSnapshot() const snapV2 = useSqlEditorV2StateSnapshot() + const { history, setLastVisitedSnippet } = useDashboardHistory() const allSnippets = useSnippets(ref!) const snippet = allSnippets.find((x) => x.id === id) @@ -49,21 +51,27 @@ const SqlEditor: NextPageWithLayout = () => { const invalidId = isError && error.code === 400 && error.message.includes('Invalid uuid') useEffect(() => { - if (ref && data) { - snapV2.setSnippet(ref, data as unknown as SnippetWithContent) + if (ref && data && project) { + // [Joshen] Check if snippet belongs to the current project + if (data.project_id === project.id) { + snapV2.setSnippet(ref, data as unknown as SnippetWithContent) + } else { + setLastVisitedSnippet(undefined) + router.push(`/project/${ref}/sql/new`) + } } - }, [ref, data]) + }, [ref, data, project]) // Load the last visited snippet when landing on /new useEffect(() => { if ( id === 'new' && skip !== 'true' && // [Joshen] Skip flag implies to skip loading the last visited snippet - appSnap.dashboardHistory.sql !== undefined && + history.sql !== undefined && content === undefined ) { - const snippet = allSnippets.find((snippet) => snippet.id === appSnap.dashboardHistory.sql) - if (snippet !== undefined) router.push(`/project/${ref}/sql/${appSnap.dashboardHistory.sql}`) + const snippet = allSnippets.find((snippet) => snippet.id === history.sql) + if (snippet !== undefined) router.push(`/project/${ref}/sql/${history.sql}`) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [id, allSnippets, content]) @@ -104,9 +112,7 @@ const SqlEditor: NextPageWithLayout = () => { id: tabId, router, editor, - onClearDashboardHistory: () => { - if (ref) appSnap.setDashboardHistory(ref, 'sql', undefined) - }, + onClearDashboardHistory: () => setLastVisitedSnippet(undefined), }) }} > diff --git a/apps/studio/pages/project/[ref]/sql/index.tsx b/apps/studio/pages/project/[ref]/sql/index.tsx index 893eee8c440..79aff70fbf1 100644 --- a/apps/studio/pages/project/[ref]/sql/index.tsx +++ b/apps/studio/pages/project/[ref]/sql/index.tsx @@ -6,35 +6,39 @@ import DefaultLayout from 'components/layouts/DefaultLayout' import { EditorBaseLayout } from 'components/layouts/editors/EditorBaseLayout' import SQLEditorLayout from 'components/layouts/SQLEditorLayout/SQLEditorLayout' import { SQLEditorMenu } from 'components/layouts/SQLEditorLayout/SQLEditorMenu' -import { useAppStateSnapshot } from 'state/app-state' +import { useDashboardHistory } from 'hooks/misc/useDashboardHistory' import { useTabsStateSnapshot } from 'state/tabs' import type { NextPageWithLayout } from 'types' -const TableEditorPage: NextPageWithLayout = () => { +const SQLEditorIndexPage: NextPageWithLayout = () => { const router = useRouter() const { ref: projectRef } = useParams() const store = useTabsStateSnapshot() - const appSnap = useAppStateSnapshot() + + const { history, isHistoryLoaded } = useDashboardHistory() useEffect(() => { - // Handle redirect to last opened snippet tab, or last snippet tab - const lastOpenedTab = appSnap.dashboardHistory.sql - const lastTabId = store.openTabs.find((id) => store.tabsMap[id]?.type === 'sql') - if (lastOpenedTab !== undefined) { - router.push(`/project/${projectRef}/sql/${appSnap.dashboardHistory.sql}`) - } else if (lastTabId) { - const lastTab = store.tabsMap[lastTabId] - if (lastTab) router.push(`/project/${projectRef}/sql/${lastTab.id.replace('sql-', '')}`) - } else { - router.push(`/project/${projectRef}/sql/new`) + if (isHistoryLoaded) { + // Handle redirect to last opened snippet tab, or last snippet tab + const lastOpenedTab = history.sql + const lastTabId = store.openTabs.find((id) => store.tabsMap[id]?.type === 'sql') + + if (lastOpenedTab !== undefined) { + router.push(`/project/${projectRef}/sql/${history.sql}`) + } else if (lastTabId) { + const lastTab = store.tabsMap[lastTabId] + if (lastTab) router.push(`/project/${projectRef}/sql/${lastTab.id.replace('sql-', '')}`) + } else { + router.push(`/project/${projectRef}/sql/new`) + } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + }, [isHistoryLoaded]) return null } -TableEditorPage.getLayout = (page) => ( +SQLEditorIndexPage.getLayout = (page) => ( } product="SQL Editor"> {page} @@ -42,4 +46,4 @@ TableEditorPage.getLayout = (page) => ( ) -export default TableEditorPage +export default SQLEditorIndexPage diff --git a/apps/studio/state/app-state.ts b/apps/studio/state/app-state.ts index 133c53edd2e..d4533c5462e 100644 --- a/apps/studio/state/app-state.ts +++ b/apps/studio/state/app-state.ts @@ -1,19 +1,9 @@ import { proxy, snapshot, useSnapshot } from 'valtio' -import { LOCAL_STORAGE_KEYS as COMMON_LOCAL_STORAGE_KEYS, LOCAL_STORAGE_KEYS } from 'common' -type DashboardHistoryType = { - sql?: string - editor?: string -} - -const EMPTY_DASHBOARD_HISTORY: DashboardHistoryType = { - sql: undefined, - editor: undefined, -} +import { LOCAL_STORAGE_KEYS as COMMON_LOCAL_STORAGE_KEYS } from 'common' const getInitialState = () => { return { - dashboardHistory: EMPTY_DASHBOARD_HISTORY, activeDocsSection: ['introduction'], docsLanguage: 'js', showProjectApiDocs: false, @@ -31,16 +21,6 @@ const getInitialState = () => { export const appState = proxy({ ...getInitialState(), - setDashboardHistory: (ref: string, key: 'sql' | 'editor', id: string | undefined) => { - if (appState.dashboardHistory[key] !== id) { - appState.dashboardHistory[key] = id - localStorage.setItem( - LOCAL_STORAGE_KEYS.DASHBOARD_HISTORY(ref), - JSON.stringify(appState.dashboardHistory) - ) - } - }, - activeDocsSection: ['introduction'], docsLanguage: 'js' as 'js' | 'bash', showProjectApiDocs: false,