Improve stability and explicitness of table editor tab state syncing between URL params and local storage (#38931)

* Improve stability and explicitness of table editor tab cache <-> url syncing

* fix: maintain filter state when using nav bar to switch back to Table Editor

---------

Co-authored-by: Charis Lam <26616127+charislam@users.noreply.github.com>
This commit is contained in:
matlinandCharis Lam authored and GitHub committed 2025-09-23 13:03:56 -05:00
1 parent 9820707d71
commit 172f6ba5fc
11 files changed
+72 -103

No files matched your search

@@ -1,6 +1,6 @@
import AwesomeDebouncePromise from 'awesome-debounce-promise'
import { compact } from 'lodash'
import { useEffect } from 'react'
import { compact, filter } from 'lodash'
import { useEffect, useState } from 'react'
import { CalculatedColumn, CellKeyboardEvent } from 'react-data-grid'
import type { Filter, SavedState } from 'components/grid/types'
@@ -11,6 +11,9 @@ import { FilterOperatorOptions } from './components/header/filter/Filter.constan
import { STORAGE_KEY_PREFIX } from './constants'
import type { Sort, SupaColumn, SupaTable } from './types'
import { formatClipboardValue } from './utils/common'
import { parseAsArrayOf, parseAsBoolean, parseAsString, useQueryStates } from 'nuqs'
export const LOAD_TAB_FROM_CACHE_PARAM = 'loadFromCache'
export function formatSortURLParams(tableName: string, sort?: string[]): Sort[] {
if (Array.isArray(sort)) {
@@ -176,41 +179,60 @@ export const saveTableEditorStateToLocalStorageDebounced = AwesomeDebouncePromis
500
)
export function useLoadTableEditorStateFromLocalStorageIntoUrl({
function getLatestParams() {
const queryParams = new URLSearchParams(window.location.search)
const sort = queryParams.getAll('sort')
const filter = queryParams.getAll('filter')
const loadFromCache = !!queryParams.get(LOAD_TAB_FROM_CACHE_PARAM)
return { sort, filter, loadFromCache }
}
export function useSyncTableEditorStateFromLocalStorageWithUrl({
projectRef,
table,
}: {
projectRef: string | undefined
table: Entity | undefined
}) {
const [_, setParams] = useUrlState({
arrayKeys: ['sort', 'filter'],
})
const [urlParams, updateUrlParams] = useQueryStates(
{
sort: parseAsArrayOf(parseAsString).withDefault([]),
filter: parseAsArrayOf(parseAsString).withDefault([]),
[LOAD_TAB_FROM_CACHE_PARAM]: parseAsBoolean.withDefault(false),
},
{
history: 'replace',
}
)
useEffect(() => {
if (!projectRef || !table) {
return
}
const searchParams = new URLSearchParams(window.location.search)
// `urlParams` from `useQueryStates` can be stale so always get the latest from the URL
const latestUrlParams = getLatestParams()
const savedState = loadTableEditorStateFromLocalStorage(projectRef, table.name, table.schema)
// If no sort params are set, use saved state
let params: { sort?: string[]; filter?: string[] } | undefined
if (searchParams.getAll('sort').length <= 0 && savedState?.sorts) {
params = { ...params, sort: savedState.sorts }
if (latestUrlParams.loadFromCache) {
const savedState = loadTableEditorStateFromLocalStorage(projectRef, table.name, table.schema)
updateUrlParams(
{
sort: savedState?.sorts ?? [],
filter: savedState?.filters ?? [],
loadFromCache: false,
},
{ clearOnDefault: true }
)
} else {
saveTableEditorStateToLocalStorage({
projectRef,
tableName: table.name,
schema: table.schema,
sorts: latestUrlParams.sort,
filters: latestUrlParams.filter,
})
}
if (searchParams.getAll('filter').length <= 0 && savedState?.filters) {
params = { ...params, filter: savedState.filters }
}
if (params) {
setParams((prevParams) => ({ ...prevParams, ...params }))
}
}, [projectRef, table])
}, [urlParams, table, projectRef])
}
export const handleCopyCell = (
@@ -1,58 +0,0 @@
import { useCallback } from 'react'
import { saveTableEditorStateToLocalStorage } from 'components/grid/SupabaseGrid.utils'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { useTableEditorTableStateSnapshot } from 'state/table-editor-table'
/**
* Hook for saving state and triggering side effects.
*/
export function useSaveTableEditorState() {
const { data: project } = useSelectedProjectQuery()
const snap = useTableEditorTableStateSnapshot()
const saveDataAndTriggerSideEffects = useCallback(
(dataToSave: { filters?: string[]; sorts?: string[] }) => {
const projectRef = project?.ref
if (!projectRef) {
return console.warn(
'[useSaveTableEditorState] ProjectRef missing, cannot save or trigger side effects.'
)
}
try {
snap.setPage(1)
snap.setEnforceExactCount(false)
const tableName = snap.table?.name
const schema = snap.table?.schema
if (tableName) {
saveTableEditorStateToLocalStorage({
projectRef,
tableName,
schema,
...dataToSave,
})
} else {
console.warn('[useSaveTableEditorState] Table name missing, skipping localStorage save.')
}
} catch (error) {
console.error('[useSaveTableEditorState] Error during interaction with snapshot:', error)
}
},
[snap, project]
)
const saveFiltersAndTriggerSideEffects = useCallback(
(urlFilters: string[]) => saveDataAndTriggerSideEffects({ filters: urlFilters }),
[saveDataAndTriggerSideEffects]
)
const saveSortsAndTriggerSideEffects = useCallback(
(urlSorts: string[]) => saveDataAndTriggerSideEffects({ sorts: urlSorts }),
[saveDataAndTriggerSideEffects]
)
return { saveFiltersAndTriggerSideEffects, saveSortsAndTriggerSideEffects }
}
@@ -3,7 +3,6 @@ import { useCallback } from 'react'
import { filtersToUrlParams, formatFilterURLParams } from 'components/grid/SupabaseGrid.utils'
import type { Filter } from 'components/grid/types'
import { useTableEditorFiltersSort } from 'hooks/misc/useTableEditorFiltersSort'
import { useSaveTableEditorState } from './useSaveTableEditorState'
/**
* Hook for managing table filter URL parameters and saving.
@@ -11,7 +10,6 @@ import { useSaveTableEditorState } from './useSaveTableEditorState'
*/
export function useTableFilter() {
const { filters: urlFilters, setParams } = useTableEditorFiltersSort()
const { saveFiltersAndTriggerSideEffects } = useSaveTableEditorState()
const filters = formatFilterURLParams(urlFilters)
@@ -19,9 +17,8 @@ export function useTableFilter() {
(appliedFilters: Filter[]) => {
const newUrlFilters = filtersToUrlParams(appliedFilters)
setParams((prevParams) => ({ ...prevParams, filter: newUrlFilters }))
saveFiltersAndTriggerSideEffects(newUrlFilters)
},
[setParams, saveFiltersAndTriggerSideEffects]
[setParams]
)
return {
@@ -4,19 +4,16 @@ import { formatSortURLParams, sortsToUrlParams } from 'components/grid/SupabaseG
import type { Sort } from 'components/grid/types'
import { useTableEditorFiltersSort } from 'hooks/misc/useTableEditorFiltersSort'
import { useTableEditorTableStateSnapshot } from 'state/table-editor-table'
import { useSaveTableEditorState } from './useSaveTableEditorState'
/**
* Hook for managing table sort URL parameters and saving.
* Uses snapshot ONLY to get table name for formatting/mapping.
* Uses useSaveTableEditorState for saving and side effects.
* Does NOT format initial sorts (needs table name externally).
* Does NOT interact with snapshot directly.
*/
export function useTableSort() {
const { sorts: urlSorts, setParams } = useTableEditorFiltersSort()
const snap = useTableEditorTableStateSnapshot()
const { saveSortsAndTriggerSideEffects } = useSaveTableEditorState()
const tableName = useMemo(() => snap.table?.name || '', [snap])
@@ -36,10 +33,8 @@ export function useTableSort() {
const newUrlSorts = sortsToUrlParams(sortsWithTable)
setParams((prevParams) => ({ ...prevParams, sort: newUrlSorts }))
saveSortsAndTriggerSideEffects(newUrlSorts)
},
[snap, setParams, saveSortsAndTriggerSideEffects]
[snap, setParams]
)
/**
@@ -1,3 +1,4 @@
import { LOAD_TAB_FROM_CACHE_PARAM } from 'components/grid/SupabaseGrid.utils'
import { DiamondIcon, ExternalLink, Fingerprint, Hash, Key, Table2 } from 'lucide-react'
import Link from 'next/link'
import { Handle, NodeProps } from 'reactflow'
@@ -67,7 +68,9 @@ const TableNode = ({
</div>
{data.id && !placeholder && (
<Button asChild type="text" className="px-0 w-[16px] h-[16px] rounded">
<Link href={`/project/${data.ref}/editor/${data.id}`}>
<Link
href={`/project/${data.ref}/editor/${data.id}?${LOAD_TAB_FROM_CACHE_PARAM}=true`}
>
<ExternalLink size={10} className="text-foreground-light" />
</Link>
</Button>
@@ -63,6 +63,7 @@ import {
} from 'ui'
import { ProtectedSchemaWarning } from '../ProtectedSchemaWarning'
import { formatAllEntities } from './Tables.utils'
import { LOAD_TAB_FROM_CACHE_PARAM } from 'components/grid/SupabaseGrid.utils'
interface TableListProps {
onAddTable: () => void
@@ -488,7 +489,9 @@ export const TableList = ({
<DropdownMenuItem
className="flex items-center space-x-2"
onClick={() =>
router.push(`/project/${project?.ref}/editor/${x.id}`)
router.push(
`/project/${project?.ref}/editor/${x.id}?${LOAD_TAB_FROM_CACHE_PARAM}=true`
)
}
onMouseEnter={() =>
prefetchEditorTablePage({
@@ -5,7 +5,7 @@ import { useCallback } from 'react'
import { useParams } from 'common'
import { SupabaseGrid } from 'components/grid/SupabaseGrid'
import { useLoadTableEditorStateFromLocalStorageIntoUrl } from 'components/grid/SupabaseGrid.utils'
import { useSyncTableEditorStateFromLocalStorageWithUrl } from 'components/grid/SupabaseGrid.utils'
import {
Entity,
isForeignTable,
@@ -41,13 +41,12 @@ export const TableGridEditor = ({
const tabs = useTabsStateSnapshot()
useLoadTableEditorStateFromLocalStorageIntoUrl({
useSyncTableEditorStateFromLocalStorageWithUrl({
projectRef,
table: selectedTable,
})
const [{ view: selectedView = 'data' }] = useUrlState()
const { can: canEditTables } = useAsyncCheckPermissions(
PermissionAction.TENANT_SQL_ADMIN_WRITE,
'tables'
@@ -9,7 +9,7 @@ import {
MAX_EXPORT_ROW_COUNT,
MAX_EXPORT_ROW_COUNT_MESSAGE,
} from 'components/grid/components/header/Header'
import { parseSupaTable } from 'components/grid/SupabaseGrid.utils'
import { LOAD_TAB_FROM_CACHE_PARAM, parseSupaTable } from 'components/grid/SupabaseGrid.utils'
import {
formatTableRowsToSQL,
getEntityLintDetails,
@@ -237,7 +237,7 @@ const EntityListItem: ItemRenderer<Entity, EntityListItemProps> = ({
<EditorTablePageLink
title={entity.name}
id={String(entity.id)}
href={`/project/${projectRef}/editor/${entity.id}?schema=${entity.schema}`}
href={`/project/${projectRef}/editor/${entity.id}?schema=${entity.schema}&${LOAD_TAB_FROM_CACHE_PARAM}=true`}
role="button"
aria-label={`View ${entity.name}`}
className={cn(
@@ -7,6 +7,7 @@ import { EntityTypeIcon } from 'components/ui/EntityTypeIcon'
import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
import { editorEntityTypes, useTabsStateSnapshot } from 'state/tabs'
import { useEditorType } from '../editors/EditorsLayout.hooks'
import { LOAD_TAB_FROM_CACHE_PARAM } from 'components/grid/SupabaseGrid.utils'
export function RecentItems() {
const { ref } = useParams()
@@ -60,7 +61,7 @@ export function RecentItems() {
item.type === 'm' ||
item.type === 'f' ||
item.type === 'p'
? `editor/${item.metadata?.tableId}?schema=${item.metadata?.schema}`
? `editor/${item.metadata?.tableId}?schema=${item.metadata?.schema}&${LOAD_TAB_FROM_CACHE_PARAM}=true`
: `explorer/${item.type}/${item.metadata?.schema}/${item.metadata?.name}`
}`}
className="flex items-center gap-4 rounded-lg bg-surface-100 py-2 transition-colors hover:bg-surface-200"
@@ -2,6 +2,7 @@ import { useRouter } from 'next/router'
import { useEffect } from 'react'
import { useParams } from 'common'
import { LOAD_TAB_FROM_CACHE_PARAM } from 'components/grid/SupabaseGrid.utils'
import SidePanelEditor from 'components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor'
import DefaultLayout from 'components/layouts/DefaultLayout'
import { EditorBaseLayout } from 'components/layouts/editors/EditorBaseLayout'
@@ -31,10 +32,15 @@ const TableEditorPage: NextPageWithLayout = () => {
// Handle redirect to last opened table tab, or last table tab
if (lastOpenedTable !== undefined) {
router.push(`/project/${projectRef}/editor/${history.editor}`)
router.push(
`/project/${projectRef}/editor/${history.editor}?${LOAD_TAB_FROM_CACHE_PARAM}=true`
)
} else if (lastTabId) {
const lastTab = tabStore.tabsMap[lastTabId]
if (lastTab) router.push(`/project/${projectRef}/editor/${lastTab.metadata?.tableId}`)
if (lastTab)
router.push(
`/project/${projectRef}/editor/${lastTab.metadata?.tableId}?${LOAD_TAB_FROM_CACHE_PARAM}=true`
)
}
}
}, [isHistoryLoaded])
+2 -1
View File
@@ -1,3 +1,4 @@
import { LOAD_TAB_FROM_CACHE_PARAM } from 'components/grid/SupabaseGrid.utils'
import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { partition } from 'lodash'
@@ -286,7 +287,7 @@ function createTabsState(projectRef: string) {
case 'f':
case 'p':
router.push(
`/project/${router.query.ref}/editor/${tab.metadata?.tableId}?schema=${tab.metadata?.schema}`
`/project/${router.query.ref}/editor/${tab.metadata?.tableId}?schema=${tab.metadata?.schema}&${LOAD_TAB_FROM_CACHE_PARAM}=true`
)
break
}