| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344 |
- // @ts-nocheck
- import AwesomeDebouncePromise from 'awesome-debounce-promise'
- import { compact } from 'lodash'
- import { useSearchParams } from 'next/navigation'
- import { parseAsNativeArrayOf, parseAsString, useQueryStates } from 'nuqs'
- import { useEffect, useMemo } from 'react'
- import {
- CalculatedColumn,
- CellKeyboardEvent,
- CellKeyDownArgs,
- RowsChangeData,
- } from 'react-data-grid'
- import { toast } from 'sonner'
- import { copyToClipboard } from 'ui'
- import { FilterOperatorOptions } from './components/header/filter/Filter.constants'
- import { STORAGE_KEY_PREFIX } from './constants'
- import type { Sort, SupaColumn, SupaRow, SupaTable } from './types'
- import { formatClipboardValue } from './utils/common'
- import { isBoolColumn } from './utils/types'
- import type { Filter, SavedState } from '@/components/grid/types'
- import { Entity, isTableLike } from '@/data/table-editor/table-editor-types'
- import { BASE_PATH } from '@/lib/constants'
- import { eventMatchesAnyShortcut } from '@/state/shortcuts/matchEvent'
- import { tableEditorRegistry } from '@/state/shortcuts/registry/table-editor'
- export function formatSortURLParams(tableName: string, sort?: string[]): Sort[] {
- if (Array.isArray(sort)) {
- return compact(
- sort.map((s) => {
- const [column, order] = s.split(':')
- // Reject any possible malformed sort param
- if (!column || !order) return undefined
- else return { table: tableName, column, ascending: order === 'asc' }
- })
- )
- }
- return []
- }
- export function sortsToUrlParams(sorts: { column: string; ascending?: boolean }[]) {
- return sorts.map((sort) => `${sort.column}:${sort.ascending ? 'asc' : 'desc'}`)
- }
- export function formatFilterURLParams(filter?: string[]): Filter[] {
- return (
- Array.isArray(filter)
- ? filter
- .map((f) => {
- const [column, operatorAbbrev, ...value] = f.split(':')
- // Allow usage of : in value, so join them back after spliting
- const formattedValue = value.join(':')
- const operator = FilterOperatorOptions.find(
- (option) => option.abbrev === operatorAbbrev
- )
- // Reject any possible malformed filter param
- if (!column || !operatorAbbrev || !operator) return undefined
- else return { column, operator: operator.value, value: formattedValue || '' }
- })
- .filter((f) => f !== undefined)
- : []
- ) as Filter[]
- }
- export function filtersToUrlParams(
- filters: { column: string | Array<string>; operator: string; value: string }[]
- ) {
- return filters.map((filter) => {
- const selectedOperator = FilterOperatorOptions.find(
- (option) => option.value === filter.operator
- )
- return `${filter.column}:${selectedOperator?.abbrev}:${filter.value}`
- })
- }
- export function parseSupaTable(table: Entity): SupaTable {
- const columns = table.columns
- const primaryKeys = isTableLike(table) ? table.primary_keys : []
- const uniqueIndexes = isTableLike(table) ? table.unique_indexes : []
- const relationships = isTableLike(table) ? table.relationships : []
- const supaColumns: SupaColumn[] = columns.map((column) => {
- const temp = {
- position: column.ordinal_position,
- name: column.name,
- defaultValue: column.default_value as string | null | undefined,
- dataType: column.data_type,
- format: column.format,
- isPrimaryKey: false,
- isIdentity: column.is_identity,
- isGeneratable: column.identity_generation == 'BY DEFAULT',
- isNullable: column.is_nullable,
- isUpdatable: column.is_updatable,
- enum: column.enums,
- comment: column.comment,
- foreignKey: {
- targetTableSchema: null as string | null,
- targetTableName: null as string | null,
- targetColumnName: null as string | null,
- deletionAction: undefined as string | undefined,
- updateAction: undefined as string | undefined,
- },
- }
- const primaryKey = primaryKeys.find((pk) => pk.name == column.name)
- temp.isPrimaryKey = !!primaryKey
- const relationship = relationships.find((relation) => {
- return (
- relation.source_schema === column.schema &&
- relation.source_table_name === column.table &&
- relation.source_column_name === column.name
- )
- })
- if (relationship) {
- temp.foreignKey.targetTableSchema = relationship.target_table_schema
- temp.foreignKey.targetTableName = relationship.target_table_name
- temp.foreignKey.targetColumnName = relationship.target_column_name
- temp.foreignKey.deletionAction = relationship.deletion_action
- temp.foreignKey.updateAction = relationship.update_action
- }
- return temp
- })
- return {
- id: table.id,
- name: table.name,
- comment: table.comment,
- schema: table.schema,
- type: table.entity_type,
- columns: supaColumns,
- estimateRowCount: isTableLike(table) ? table.live_rows_estimate : 0,
- primaryKey: primaryKeys?.length > 0 ? primaryKeys.map((col) => col.name) : undefined,
- uniqueIndexes:
- !!uniqueIndexes && uniqueIndexes.length > 0
- ? uniqueIndexes.map(({ columns }) => columns)
- : undefined,
- }
- }
- export function getStorageKey(prefix: string, ref: string) {
- return `${prefix}_${ref}`
- }
- export function loadTableEditorStateFromLocalStorage(
- projectRef: string,
- tableId: number
- ): SavedState | undefined {
- const storageKey = getStorageKey(STORAGE_KEY_PREFIX, projectRef)
- // Prefer sessionStorage (scoped to current tab) over localStorage
- const jsonStr = sessionStorage.getItem(storageKey) ?? localStorage.getItem(storageKey)
- if (!jsonStr) return
- const json = JSON.parse(jsonStr)
- return json[tableId]
- }
- /**
- * Builds a table editor URL with the given project reference, table ID. It will load the saved state from local storage
- * and add the sort and filter parameters to the URL.
- */
- export function buildTableEditorUrl({
- projectRef = 'default',
- tableId,
- schema,
- }: {
- projectRef?: string
- tableId: number
- schema?: string
- }) {
- const url = new URL(`${BASE_PATH}/project/${projectRef}/editor/${tableId}`, location.origin)
- // If the schema is provided, add it to the URL so that the left sidebar is opened to the correct schema
- if (schema) {
- url.searchParams.set('schema', schema)
- }
- const savedState = loadTableEditorStateFromLocalStorage(projectRef, tableId)
- if (savedState?.sorts && savedState.sorts.length > 0) {
- savedState.sorts?.forEach((sort) => url.searchParams.append('sort', sort))
- }
- if (savedState?.filters && savedState.filters.length > 0) {
- savedState.filters?.forEach((filter) => url.searchParams.append('filter', filter))
- }
- return url.toString()
- }
- export function saveTableEditorStateToLocalStorage({
- projectRef,
- tableId,
- gridColumns,
- sorts,
- filters,
- }: {
- projectRef: string
- tableId: number
- gridColumns?: CalculatedColumn<any, any>[]
- sorts?: string[]
- filters?: string[]
- }) {
- const storageKey = getStorageKey(STORAGE_KEY_PREFIX, projectRef)
- const savedStr = sessionStorage.getItem(storageKey) ?? localStorage.getItem(storageKey)
- const config = {
- ...(gridColumns !== undefined && { gridColumns }),
- ...(sorts !== undefined && { sorts: sorts.filter((sort) => sort !== '') }),
- ...(filters !== undefined && { filters: filters.filter((filter) => filter !== '') }),
- }
- let savedJson
- if (savedStr) {
- savedJson = JSON.parse(savedStr)
- const previousConfig = savedJson[tableId]
- savedJson = { ...savedJson, [tableId]: { ...previousConfig, ...config } }
- } else {
- savedJson = { [tableId]: config }
- }
- // Save to both localStorage and sessionStorage so it's consistent to current tab
- localStorage.setItem(storageKey, JSON.stringify(savedJson))
- sessionStorage.setItem(storageKey, JSON.stringify(savedJson))
- }
- export const saveTableEditorStateToLocalStorageDebounced = AwesomeDebouncePromise(
- saveTableEditorStateToLocalStorage,
- 500
- )
- function getLatestParams() {
- const queryParams = new URLSearchParams(window.location.search)
- const sort = queryParams.getAll('sort')
- const filter = queryParams.getAll('filter')
- return { sort, filter }
- }
- export function useSyncTableEditorStateFromLocalStorageWithUrl({
- projectRef,
- table,
- }: {
- projectRef: string | undefined
- table: Entity | undefined
- }) {
- // Warning: nuxt url state often fails to update to changes to URL
- useQueryStates(
- {
- sort: parseAsNativeArrayOf(parseAsString),
- filter: parseAsNativeArrayOf(parseAsString),
- },
- {
- history: 'replace',
- }
- )
- // Use nextjs useSearchParams to get the latest URL params
- const searchParams = useSearchParams()
- const urlParams = useMemo(() => {
- const sort = searchParams?.getAll('sort') ?? []
- const filter = searchParams?.getAll('filter') ?? []
- return { sort, filter }
- }, [searchParams])
- useEffect(() => {
- if (!projectRef || !table) {
- return
- }
- // `urlParams` from `useQueryStates` can be stale so always get the latest from the URL
- const latestUrlParams = getLatestParams()
- saveTableEditorStateToLocalStorage({
- projectRef,
- tableId: table.id,
- sorts: latestUrlParams.sort,
- filters: latestUrlParams.filter,
- })
- }, [urlParams, table, projectRef])
- }
- export const handleCellKeyDown = <TRow extends SupaRow = SupaRow>(
- args: CellKeyDownArgs<TRow, unknown>,
- event: CellKeyboardEvent,
- context?: {
- rows: TRow[]
- columns: SupaColumn[]
- onRowsChange: (rows: TRow[], data: RowsChangeData<TRow, unknown>) => void
- }
- ) => {
- const { mode, column, row, rowIdx } = args
- if (mode !== 'SELECT') return
- const key = event.key.toLowerCase()
- if (key === 'c' && (event.metaKey || event.ctrlKey)) {
- if (window.getSelection()?.isCollapsed === false) return
- const value = formatClipboardValue(row[column.key] ?? '')
- event.preventDefault()
- event.preventGridDefault()
- void copyToClipboard(value, () => {
- toast.success('Copied cell value to clipboard')
- })
- return
- }
- // Let registered shortcuts win over rdg's "type a key to enter edit mode" default,
- // unless a printable key enters edit mode.
- if (eventMatchesAnyShortcut(event.nativeEvent, tableEditorRegistry)) {
- if (
- event.key.length === 1 &&
- event.key !== ' ' &&
- !event.altKey &&
- !event.ctrlKey &&
- !event.metaKey &&
- !event.shiftKey &&
- column.renderEditCell != null
- ) {
- event.stopPropagation()
- } else {
- event.preventGridDefault()
- return
- }
- }
- // Toggle boolean cells with T/F when no modifier keys are pressed.
- if (context === undefined) return
- if (event.altKey || event.ctrlKey || event.metaKey || (key !== 't' && key !== 'f')) return
- const supaColumn = context.columns.find((c) => c.name === column.key)
- if (
- supaColumn === undefined ||
- !isBoolColumn(supaColumn.dataType) ||
- column.renderEditCell == null
- ) {
- return
- }
- event.preventDefault()
- event.preventGridDefault()
- const nextValue = key === 't'
- if (row[column.key] === nextValue) return
- const updatedRows = [...context.rows]
- updatedRows[rowIdx] = { ...row, [column.key]: nextValue }
- context.onRowsChange(updatedRows, { indexes: [rowIdx], column })
- }
|