| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 |
- 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/BrivenGrid.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>
|