Files
supabase/apps/studio/state/table-editor-table.tsx
Ali Waseem d6e400620d chore(studio): remove old table editor filter bar (#45220)
## Summary

- Removes the legacy Table Editor filter bar and its
`supabase-ui-table-filter-bar` feature-preview flag (opt-out rate
~0.13%, no plans to keep supporting it).
- Merges `HeaderNew` into `Header`, consolidates `useTableFilterNew`
into `useTableFilter`, and adds
`useOptionalTableEditorTableStateSnapshot` so `useTableFilter` can
safely fall back to URL params when called outside the table-editor
provider (e.g. from the sidebar).
- Drops the associated preview modal entry, screenshot, and local
storage key.

Based on the closed PR https://github.com/supabase/supabase/pull/44867.

Closes
[FE-3071](https://linear.app/supabase/issue/FE-3071/remove-old-table-editor-filter-bar).

## Test plan

- [x] `pnpm --filter=studio typecheck` passes
- [x] `pnpm --filter=studio lint` passes
- [x] Open the Table Editor, confirm the new filter bar renders and
filters apply/clear correctly
- [x] Apply filters, reload the page — filters persist via URL params
- [x] Delete a column that has an active filter — filter is removed
cleanly
- [x] Right-click a cell — "Filter by value" still appears for simple
values
- [x] Select rows — row-selection header (copy / export / delete) still
works
- [x] Foreign Row Selector still renders (`FilterPopoverPrimitive`
retained for this usage)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Table filter bar is now permanently available (no longer gated behind
preview feature).

* **Improvements**
* Reorganized table header layout with improved filter UI placement and
styling.
* Streamlined and unified filter behavior across the grid for more
consistent operation.
* Simplified insert row/column functionality with clearer permission
handling.

* **Refactor**
* Consolidated filter system by removing redundant implementations and
feature flags.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-24 14:45:33 +00:00

305 lines
10 KiB
TypeScript

import { createContext, PropsWithChildren, useContext, useEffect, useRef } from 'react'
import { CalculatedColumn } from 'react-data-grid'
import { proxy, ref, subscribe, useSnapshot } from 'valtio'
import { proxySet } from 'valtio/utils'
import { useTableEditorStateSnapshot } from './table-editor'
import { TableIndexAdvisorProvider } from '@/components/grid/context/TableIndexAdvisorContext'
import {
loadTableEditorStateFromLocalStorage,
parseSupaTable,
saveTableEditorStateToLocalStorageDebounced,
} from '@/components/grid/SupabaseGrid.utils'
import { Filter, SupaRow } from '@/components/grid/types'
import { getInitialGridColumns } from '@/components/grid/utils/column'
import { getGridColumns } from '@/components/grid/utils/gridColumns'
import { Entity } from '@/data/table-editor/table-editor-types'
const FALLBACK_TABLE_STATE = proxy({}) as TableEditorTableState
export const createTableEditorTableState = ({
projectRef,
table: originalTable,
editable = true,
preflightCheck = true,
onAddColumn,
onExpandJSONEditor,
onExpandTextEditor,
}: {
projectRef: string
table: Entity
/** If set to true, render an additional "+" column to support adding a new column in the grid editor */
editable?: boolean
preflightCheck?: boolean
onAddColumn: () => void
onExpandJSONEditor: (column: string, row: SupaRow) => void
onExpandTextEditor: (column: string, row: SupaRow) => void
}) => {
const table = parseSupaTable(originalTable)
const savedState = loadTableEditorStateFromLocalStorage(projectRef, table.id)
const gridColumns = getInitialGridColumns(
getGridColumns(table, {
tableId: table.id,
editable,
onAddColumn: editable ? onAddColumn : undefined,
onExpandJSONEditor,
onExpandTextEditor,
}),
savedState
)
const state = proxy({
/* Table */
table,
originalTable,
/**
* Used for tracking changes to the table
* Do not use outside of table-editor-table.tsx
*/
_originalTableRef: ref(originalTable),
updateTable: (table: Entity) => {
const supaTable = parseSupaTable(table)
const gridColumns = getInitialGridColumns(
getGridColumns(supaTable, {
tableId: table.id,
editable: state.editable,
onAddColumn: state.editable ? onAddColumn : undefined,
onExpandJSONEditor,
onExpandTextEditor,
}),
{ gridColumns: state.gridColumns }
)
state.table = supaTable
state.gridColumns = gridColumns
state.originalTable = table
state._originalTableRef = ref(table)
},
/* Rows */
selectedRows: proxySet<number>(),
allRowsSelected: false,
setSelectedRows: (rows: Set<number>, selectAll?: boolean) => {
state.allRowsSelected = selectAll ?? false
state.selectedRows = proxySet(rows)
},
resetSelectedRows: () => {
state.allRowsSelected = false
state.selectedRows = proxySet(new Set())
},
/* Columns */
gridColumns,
moveColumn: (fromKey: string, toKey: string) => {
const fromIdx = state.gridColumns.findIndex((x) => x.key === fromKey)
const toIdx = state.gridColumns.findIndex((x) => x.key === toKey)
if (fromIdx === -1 || toIdx === -1) return
const moveItem = state.gridColumns[fromIdx]
const overItem = state.gridColumns[toIdx]
if (moveItem.frozen || overItem.frozen) return
state.gridColumns.splice(fromIdx, 1)
state.gridColumns.splice(toIdx, 0, moveItem)
// Update idx values to match new positions
state.gridColumns.forEach((col, i) => {
;(col as CalculatedColumn<any, any> & { idx: number }).idx = i
})
},
updateColumnSize: (index: number, width: number) => {
if (state.gridColumns[index]) {
;(state.gridColumns[index] as CalculatedColumn<any, any> & { width?: number }).width = width
}
},
freezeColumn: (columnKey: string) => {
const index = state.gridColumns.findIndex((x) => x.key === columnKey)
if (index === -1) return
;(state.gridColumns[index] as CalculatedColumn<any, any> & { frozen?: boolean }).frozen = true
// Move the column to just after the last currently-frozen column
const lastFrozenIdx = state.gridColumns.reduce(
(last, col, i) => (col.frozen && i !== index ? i : last),
-1
)
const col = state.gridColumns[index]
state.gridColumns.splice(index, 1)
state.gridColumns.splice(lastFrozenIdx + 1, 0, col)
state.gridColumns.forEach((col, i) => {
;(col as CalculatedColumn<any, any> & { idx: number }).idx = i
})
},
unfreezeColumn: (columnKey: string) => {
const index = state.gridColumns.findIndex((x) => x.key === columnKey)
if (index === -1) return
;(state.gridColumns[index] as CalculatedColumn<any, any> & { frozen?: boolean }).frozen =
false
// Move the column to just after the remaining frozen columns
const col = state.gridColumns[index]
state.gridColumns.splice(index, 1)
const lastFrozenIdx = state.gridColumns.reduce((last, col, i) => (col.frozen ? i : last), -1)
state.gridColumns.splice(lastFrozenIdx + 1, 0, col)
state.gridColumns.forEach((col, i) => {
;(col as CalculatedColumn<any, any> & { idx: number }).idx = i
})
},
updateColumnIdx: (columnKey: string, columnIdx: number) => {
const index = state.gridColumns.findIndex((x) => x.key === columnKey)
if (state.gridColumns[index]) {
;(state.gridColumns[index] as CalculatedColumn<any, any> & { idx?: number }).idx = columnIdx
}
state.gridColumns.sort((a, b) => a.idx - b.idx)
},
/* Cells */
selectedCellPosition: null as { idx: number; rowIdx: number } | null,
setSelectedCellPosition: (position: { idx: number; rowIdx: number } | null) => {
state.selectedCellPosition = position
},
/* Misc */
enforceExactCount: false,
setEnforceExactCount: (value: boolean) => {
state.enforceExactCount = value
},
page: 1,
setPage: (page: number) => {
state.page = page
// reset selected row state
state.setSelectedRows(new Set())
},
editable,
setEditable: (editable: boolean) => {
state.editable = editable
// When changing the editable flag, all grid columns need to be recreated for the editable flag to be propagated.
state.gridColumns = getInitialGridColumns(
getGridColumns(state.table, {
tableId: table.id,
editable,
onAddColumn: editable ? onAddColumn : undefined,
onExpandJSONEditor,
onExpandTextEditor,
}),
{ gridColumns: state.gridColumns }
)
},
/* Filters (NOTE: this is only for the new AI filter bar) */
filters: [] as Filter[],
setFilters: (filters: Filter[]) => {
state.filters = filters
},
clearFilters: () => {
state.filters = []
},
preflightCheck,
setPreflightCheck: (value: boolean) => (state.preflightCheck = value),
})
return state
}
export type TableEditorTableState = ReturnType<typeof createTableEditorTableState>
export const TableEditorTableStateContext = createContext<TableEditorTableState>(undefined as any)
type TableEditorTableStateContextProviderProps = Omit<
Parameters<typeof createTableEditorTableState>[0],
'onAddColumn' | 'onExpandJSONEditor' | 'onExpandTextEditor'
>
export const TableEditorTableStateContextProvider = ({
children,
projectRef,
table,
...props
}: PropsWithChildren<TableEditorTableStateContextProviderProps>) => {
const tableEditorSnap = useTableEditorStateSnapshot()
const state = useRef(
createTableEditorTableState({
...props,
projectRef,
table,
onAddColumn: tableEditorSnap.onAddColumn,
onExpandJSONEditor: (column: string, row: SupaRow) => {
tableEditorSnap.onExpandJSONEditor({
column,
row,
value: JSON.stringify(row[column]) || '',
})
},
onExpandTextEditor: (column: string, row: SupaRow) => {
tableEditorSnap.onExpandTextEditor(column, row)
},
})
).current
useEffect(() => {
if (typeof window !== 'undefined') {
return subscribe(state, () => {
saveTableEditorStateToLocalStorageDebounced({
gridColumns: state.gridColumns,
projectRef,
tableId: state.table.id,
})
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
// We can use a === check here because react-query is good
// about returning objects with the same ref / different ref
if (state._originalTableRef !== table) {
state.updateTable(table)
}
}, [table])
useEffect(() => {
if (state.editable !== props.editable) {
state.setEditable(props.editable ?? true)
}
}, [props.editable, state])
return (
<TableEditorTableStateContext.Provider value={state}>
{state.table.schema ? (
<TableIndexAdvisorProvider schema={state.table.schema ?? 'public'} table={state.table.name}>
{children}
</TableIndexAdvisorProvider>
) : (
children
)}
</TableEditorTableStateContext.Provider>
)
}
export const useTableEditorTableStateSnapshot = (options?: Parameters<typeof useSnapshot>[1]) => {
const state = useContext(TableEditorTableStateContext)
// as TableEditorTableState so this doesn't get marked as readonly,
// making adopting this state easier since we're migrating from react-tracked
return useSnapshot(state, options) as TableEditorTableState
}
/**
* Same as useTableEditorTableStateSnapshot but returns undefined when called
* outside a TableEditorTableStateContextProvider instead of crashing.
* Use this for components that may render outside the provider (e.g. sidebar).
*/
export const useOptionalTableEditorTableStateSnapshot = (): TableEditorTableState | undefined => {
const state = useContext(TableEditorTableStateContext)
const snap = useSnapshot(state ?? FALLBACK_TABLE_STATE)
return state != undefined ? (snap as TableEditorTableState) : undefined
}
export type TableEditorTableStateSnapshot = ReturnType<typeof useTableEditorTableStateSnapshot>