mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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:
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])
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user