diff --git a/studio/components/grid/SupabaseGrid.tsx b/studio/components/grid/SupabaseGrid.tsx index b580b32b07b..560162d331f 100644 --- a/studio/components/grid/SupabaseGrid.tsx +++ b/studio/components/grid/SupabaseGrid.tsx @@ -1,4 +1,5 @@ import { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react' +import { isEqual } from 'lodash' import { createPortal } from 'react-dom' import { useMonaco } from '@monaco-editor/react' import { DndProvider } from 'react-dnd' @@ -19,10 +20,12 @@ import { cleanupProps, initTable, saveStorageDebounced } from './SupabaseGrid.ut import { useUrlState } from 'hooks' /** - * Supabase Grid. - * - * React component to render database table. + * Supabase Grid: React component to render database table. */ + +// [JOSHEN TODO] Updating of rows when filters updating feels very choppy +// Rows do not get updated immediately for some reason despite a short debounce + export const SupabaseGrid = forwardRef((props, ref) => { const monaco = useMonaco() const _props = cleanupProps(props) @@ -60,8 +63,9 @@ const SupabaseGridLayout = forwardRef((props const { editable, storageRef, gridProps, headerActions } = props const dispatch = useDispatch() const state = useTrackedState() + const gridRef = useRef(null) - const [mounted, setMount] = useState(false) + const [mounted, setMounted] = useState(false) const [{ sort: sorts, filter: filters }, setParams] = useUrlState({ arrayKeys: ['sort', 'filter'], @@ -83,19 +87,23 @@ const SupabaseGridLayout = forwardRef((props })) useEffect(() => { - if (!mounted) setMount(true) + if (!mounted) setMounted(true) }, []) - // [Joshen] This where we refetch the data is say a sort or filter was updated useEffect(() => { if (state.refreshPageFlag == REFRESH_PAGE_IMMEDIATELY) { - console.log('Refresh page') - fetchPage(state, dispatch) - } else if (state.refreshPageFlag != 0) { - refreshPageDebounced(state, dispatch) + fetchPage(state, dispatch, sorts as string[], filters as string[]) + } else if (state.refreshPageFlag !== 0) { + refreshPageDebounced(state, dispatch, sorts as string[], filters as string[]) } }, [state.refreshPageFlag]) + useEffect(() => { + if (mounted) { + dispatch({ type: 'UPDATE_FILTERS_SORTS', payload: {} }) + } + }, [JSON.stringify(filters)]) + useEffect(() => { if (state.isInitialComplete && storageRef && state.table) { saveStorageDebounced(state, storageRef) @@ -104,14 +112,14 @@ const SupabaseGridLayout = forwardRef((props state.table, state.isInitialComplete, state.gridColumns, - state.sorts, - state.filters, + state.sorts, // [JOSHEN TODO] To update accordingly + // state.filters, // [JOSHEN TODO] To update accordingly storageRef, ]) useEffect(() => { if (state.totalRows === TOTAL_ROWS_RESET) { - fetchCount(state, dispatch) + fetchCount(state, dispatch, filters as string[]) } }, [state.totalRows]) diff --git a/studio/components/grid/SupabaseGrid.utils.ts b/studio/components/grid/SupabaseGrid.utils.ts index f371b31c556..30ba02996a7 100644 --- a/studio/components/grid/SupabaseGrid.utils.ts +++ b/studio/components/grid/SupabaseGrid.utils.ts @@ -3,9 +3,10 @@ import { uuidv4 } from 'lib/helpers' import { STORAGE_KEY_PREFIX } from './constants' import { IMetaService } from './services/meta' import { InitialStateType } from './store/reducers' -import { Dictionary, SupabaseGridProps, SupaColumn, SupaTable } from './types' +import { Dictionary, Sort, SupabaseGridProps, SupaColumn, SupaTable } from './types' import { getGridColumns } from './utils/gridColumns' import { FilterOperatorOptions } from './components/header/filter' +import { Filter } from 'components/grid/types' export function defaultErrorHandler(error: any) { console.log('Supabase grid error: ', error) @@ -31,6 +32,41 @@ export function cleanupProps(props: SupabaseGridProps) { } } +// [JOSHEN TODO] Write tests for this +export function formatSortURLParams(sort?: string[]) { + return ( + Array.isArray(sort) + ? sort + .map((s) => { + const [column, order] = s.split(':') + // Reject any possible malformed sort param + if (!column || !order) return undefined + else return { column, ascending: order === 'asc' } + }) + .filter((s) => s !== undefined) + : [] + ) as Sort[] +} + +// [JOSHEN TODO] Write tests for this +export function formatFilterURLParams(filter?: string[]): Filter[] { + return ( + Array.isArray(filter) + ? filter + .map((f) => { + const [column, operatorAbbrev, value] = f.split(':') + const operator = FilterOperatorOptions.find( + (option) => option.abbrev === operatorAbbrev + ) + // Reject any possible malformed filter param + if (!column || !operatorAbbrev || !operator) return undefined + else return { id: uuidv4(), column, operator: operator.value, value: value || '' } + }) + .filter((f) => f !== undefined) + : [] + ) as Filter[] +} + export function initTable( props: SupabaseGridProps, state: InitialStateType, @@ -45,43 +81,17 @@ export function initTable( onAddColumn: props.editable ? props.onAddColumn : undefined, }) - // [JOSHEN WE LEFT OFF HERE TO FIGURE OUT HOW TO REPLACE EXISTING FILTER AND SORT] - const sorts = Array.isArray(sort) - ? sort - .map((s) => { - const [column, order] = s.split(':') - // Reject any possible malformed sort param - if (!column || !order) return undefined - else return { column, ascending: order === 'asc' } - }) - .filter((s) => s !== undefined) - : [] + const savedState = props.storageRef + ? onLoadStorage(props.storageRef, table.name, table.schema) + : undefined - const filters = Array.isArray(filter) - ? filter - .map((f) => { - const [column, operatorAbbrev, value] = f.split(':') - const operator = FilterOperatorOptions.find( - (option) => option.abbrev === operatorAbbrev - ) - // Reject any possible malformed filter param - if (!column || !operatorAbbrev || !operator || !value) return undefined - else return { id: uuidv4(), column, operator: operator.value, value } - }) - .filter((f) => f !== undefined) - : [] - - let savedState - if (props.storageRef) { - savedState = onLoadStorage(props.storageRef, table.name, table.schema) - } - - console.log('initTable', savedState, { - sort, - sorts, - filter, - filters, - }) + // [JOSHEN TODO] Temp force update savedState to use URL param + // Load sort and filters via URL param only if given, otherwise get + // from saved state in local storage + const sorts = formatSortURLParams(sort) + const filters = formatFilterURLParams(filter) + if (sorts.length > 0) savedState.sorts = sorts + if (filters.length > 0) savedState.filters = filters dispatch({ type: 'INIT_TABLE', @@ -227,13 +237,14 @@ export function onLoadStorage(storageRef: string, tableName: string, schema?: st export const saveStorageDebounced = AwesomeDebouncePromise(saveStorage, 500) +// [JOSHEN TODO] Need to fix this function saveStorage(state: InitialStateType, storageRef: string) { if (!state.table) return const config = { gridColumns: state.gridColumns, sorts: state.sorts, - filters: state.filters, + // filters: state.filters, } const storageKey = getStorageKey(STORAGE_KEY_PREFIX, storageRef) const savedStr = localStorage.getItem(storageKey) diff --git a/studio/components/grid/components/header/filter/Filter.utils.ts b/studio/components/grid/components/header/filter/Filter.utils.ts deleted file mode 100644 index 9e317a3f920..00000000000 --- a/studio/components/grid/components/header/filter/Filter.utils.ts +++ /dev/null @@ -1,19 +0,0 @@ -import AwesomeDebouncePromise from 'awesome-debounce-promise'; -import { Filter } from '../../../types'; - -const updateFilterValue = ( - payload: { - filterIdx: number; - value: Filter; - }, - dispatch: (value: unknown) => void -) => { - dispatch({ - type: 'UPDATE_FILTER', - payload: payload, - }); -}; -export const updateFilterValueDebounced = AwesomeDebouncePromise( - updateFilterValue, - 550 -); diff --git a/studio/components/grid/components/header/filter/FilterPopover.tsx b/studio/components/grid/components/header/filter/FilterPopover.tsx index 8c7d9a56224..1c8ed4408e6 100644 --- a/studio/components/grid/components/header/filter/FilterPopover.tsx +++ b/studio/components/grid/components/header/filter/FilterPopover.tsx @@ -1,14 +1,16 @@ import { FC } from 'react' import { Button, IconPlus, IconFilter, Popover } from '@supabase/ui' -import { uuidv4 } from 'lib/helpers' -import { useDispatch, useTrackedState } from '../../../store' + +import { useUrlState } from 'hooks' import FilterRow from './FilterRow' +import { useTrackedState } from 'components/grid/store' +import { formatFilterURLParams } from 'components/grid/SupabaseGrid.utils' const FilterPopover: FC = () => { - const state = useTrackedState() + const [{ filter: filters }]: any = useUrlState({ arrayKeys: ['filter'] }) const btnText = - state.filters.length > 0 - ? `Filtered by ${state.filters.length} rule${state.filters.length > 1 ? 's' : ''}` + filters.length > 0 + ? `Filtered by ${filters.length} rule${filters.length > 1 ? 's' : ''}` : 'Filter' return ( @@ -31,27 +33,32 @@ export default FilterPopover const Filter: FC = () => { const state = useTrackedState() - const dispatch = useDispatch() + + const [{ filter: filters }, setParams] = useUrlState({ arrayKeys: ['filter'] }) + const formattedFilters = formatFilterURLParams(filters as string[]) function onAddFilter() { - dispatch({ - type: 'ADD_FILTER', - payload: { - id: uuidv4(), - column: state.table?.columns[0].name, - operator: '=', - value: '', - }, + setParams((prevParams) => { + const existingFilters = (prevParams?.filter ?? []) as string[] + const column = state.table?.columns[0].name + return { + ...prevParams, + filter: existingFilters.concat([`${column}:eq:`]), + } }) } return (
- {state.filters.map((filter, index) => ( - + {formattedFilters.map((filter, index) => ( + ))} - {state.filters.length == 0 && ( + {formattedFilters.length == 0 && (
No filters applied to this view

Add a column below to filter the view

diff --git a/studio/components/grid/components/header/filter/FilterRow.tsx b/studio/components/grid/components/header/filter/FilterRow.tsx index ad733640278..b6c979f21bb 100644 --- a/studio/components/grid/components/header/filter/FilterRow.tsx +++ b/studio/components/grid/components/header/filter/FilterRow.tsx @@ -1,23 +1,24 @@ -import { FC, memo, useState, useEffect, ChangeEvent } from 'react' +import { debounce } from 'lodash' +import { FC, memo, useState, useEffect, ChangeEvent, useCallback } from 'react' import { Button, Input, IconChevronDown, IconX } from '@supabase/ui' -import { DropdownControl } from '../../common' -import { useDispatch, useTrackedState } from '../../../store' -import { FilterOperatorOptions } from './Filter.constants' -import { updateFilterValueDebounced } from './Filter.utils' -/** - * use `now` to trigger re-render as filterIdx won't change value - * if not filterText state will not updated on delete filter - */ +import { useUrlState } from 'hooks' +import { Filter } from 'components/grid/types' +import { DropdownControl } from 'components/grid/components/common' +import { useTrackedState } from 'components/grid/store' +import { FilterOperatorOptions } from './Filter.constants' + type FilterRowProps = { + filter: Filter filterIdx: number - now: number } -const FilterRow: FC = ({ filterIdx }) => { +// [JOSHEN TODO] Area of improvement: Input field loses focus after the debounce (because of useUrlState?) +// [JOSHEN TODO] Area of improvement: Pretty sure we can refactor for DRY for updates on each column, operator and value +const FilterRow: FC = ({ filter, filterIdx }) => { const state = useTrackedState() - const dispatch = useDispatch() - const filter = state.filters[filterIdx] + const [_, setParams] = useUrlState() + const column = state.table?.columns.find((x) => x.name === filter.column) const columnOptions = state.table?.columns?.map((x) => { @@ -26,43 +27,87 @@ const FilterRow: FC = ({ filterIdx }) => { const [filterValue, setFilterValue] = useState(filter.value) useEffect(() => { - const filter = state.filters[filterIdx] setFilterValue(filter.value) }, [filterIdx]) + function onRemoveFilter() { + setParams((prevParams) => { + const existingFilters = (prevParams?.filter ?? []) as string[] + const updatedFilters = existingFilters.filter((filter: string, idx: number) => { + if (idx !== filterIdx) return filter + }) + return { + ...prevParams, + filter: updatedFilters, + } + }) + } + function onColumnChange(column: string | number) { - dispatch({ - type: 'UPDATE_FILTER', - payload: { filterIdx, value: { ...filter, column } }, + setParams((prevParams) => { + const existingFilters = (prevParams?.filter ?? []) as string[] + const updatedFilters = existingFilters.map((filter: string, idx: number) => { + if (idx === filterIdx) { + const [_, operator, value] = filter.split(':') + return `${column}:${operator}:${value}` + } else { + return filter + } + }) + return { + ...prevParams, + filter: updatedFilters, + } }) } function onOperatorChange(operator: string | number) { - dispatch({ - type: 'UPDATE_FILTER', - payload: { filterIdx, value: { ...filter, operator } }, + setParams((prevParams) => { + const existingFilters = (prevParams?.filter ?? []) as string[] + const updatedFilters = existingFilters.map((filter: string, idx: number) => { + if (idx === filterIdx) { + const [column, _, value] = filter.split(':') + const selectedOperator = FilterOperatorOptions.find((option) => option.value === operator) + return `${column}:${selectedOperator?.abbrev}:${value}` + } else { + return filter + } + }) + return { + ...prevParams, + filter: updatedFilters, + } }) } - function onFilterChange(event: ChangeEvent) { + function onValueChange(event: ChangeEvent) { const value = event.target.value setFilterValue(value) - updateFilterValueDebounced( - { - filterIdx, - value: { ...filter, value: value }, - }, - dispatch - ) - } - - function onRemoveFilter() { - dispatch({ - type: 'REMOVE_FILTER', - payload: { index: filterIdx }, + debounceHandler({ + filterIdx, + value: { ...filter, value: value }, }) } + const updateFilterValue = (payload: { filterIdx: number; value: Filter }) => { + setParams((prevParams) => { + const existingFilters = (prevParams?.filter ?? []) as string[] + const updatedFilters = existingFilters.map((filter: string, idx: number) => { + if (idx === filterIdx) { + const [column, operator] = filter.split(':') + return `${column}:${operator}:${payload.value.value}` + } else { + return filter + } + }) + return { + ...prevParams, + filter: updatedFilters, + } + }) + } + const debounceHandler = useCallback(debounce(updateFilterValue, 600), []) + return (
@@ -97,7 +142,7 @@ const FilterRow: FC = ({ filterIdx }) => { className="w-full" placeholder="Enter a value" value={filterValue} - onChange={onFilterChange} + onChange={onValueChange} />