Merge pull request #11264 from gitstart/feat/filter-sort-on-apply-workflow

feat: Move table filters and sorting to an "Apply" workflow
This commit is contained in:
Alaister Young authored and GitHub committed 2022-12-29 04:31:02 +11:00
commit 2124dc6ab0
4 files changed
+174 -180

No files matched your search

@@ -1,10 +1,14 @@
import { FC } from 'react'
import { FC, useState, useMemo, useCallback } from 'react'
import { isEqual } from 'lodash'
import { Button, IconPlus, IconFilter, Popover } from 'ui'
import update from 'immutability-helper'
import { useUrlState } from 'hooks'
import FilterRow from './FilterRow'
import { useTrackedState } from 'components/grid/store'
import { formatFilterURLParams } from 'components/grid/SupabaseGrid.utils'
import { Filter } from 'components/grid/types'
import { FilterOperatorOptions } from './Filter.constants'
const FilterPopover: FC = () => {
const [{ filter: filters }]: any = useUrlState({ arrayKeys: ['filter'] })
@@ -14,7 +18,12 @@ const FilterPopover: FC = () => {
: 'Filter'
return (
<Popover size="large" align="start" className="sb-grid-filter-popover" overlay={<Filter />}>
<Popover
size="large"
align="start"
className="sb-grid-filter-popover"
overlay={<FilterOverlay />}
>
<Button
as="span"
type={(filters || []).length > 0 ? 'link' : 'text'}
@@ -31,19 +40,60 @@ const FilterPopover: FC = () => {
}
export default FilterPopover
const Filter: FC = () => {
const FilterOverlay: FC = () => {
const state = useTrackedState()
const [{ filter: filters }, setParams] = useUrlState({ arrayKeys: ['filter'] })
const formattedFilters = formatFilterURLParams(filters as string[])
const [{ filter: filtersFromUrl }, setParams] = useUrlState({ arrayKeys: ['filter'] })
const initialFilters = useMemo(
() => formatFilterURLParams((filtersFromUrl as string[]) ?? []),
[filtersFromUrl]
)
const [filters, setFilters] = useState<Filter[]>(initialFilters)
function onAddFilter() {
const column = state.table?.columns[0]?.name
if (column) {
setFilters([
...filters,
{
column,
operator: '=',
value: '',
},
])
}
}
const onChangeFilter = useCallback((index: number, filter: Filter) => {
setFilters((currentFilters) =>
update(currentFilters, {
[index]: {
$set: filter,
},
})
)
}, [])
const onDeleteFilter = useCallback((index: number) => {
setFilters((currentFilters) =>
update(currentFilters, {
$splice: [[index, 1]],
})
)
}, [])
function onApplyFilter() {
setParams((prevParams) => {
const existingFilters = (prevParams?.filter ?? []) as string[]
const column = state.table?.columns[0].name
return {
...prevParams,
filter: existingFilters.concat([`${column}:eq:`]),
filter: filters.map((filter) => {
const selectedOperator = FilterOperatorOptions.find(
(option) => option.value === filter.operator
)
return `${filter.column}:${selectedOperator?.abbrev}:${filter.value}`
}),
}
})
}
@@ -51,14 +101,16 @@ const Filter: FC = () => {
return (
<div className="space-y-2 py-2">
<div className="space-y-2">
{formattedFilters.map((filter, index) => (
{filters.map((filter, index) => (
<FilterRow
key={`filter-${filter.column}-${[index]}`}
filter={filter}
filterIdx={index}
onChange={onChangeFilter}
onDelete={onDeleteFilter}
/>
))}
{formattedFilters.length == 0 && (
{filters.length == 0 && (
<div className="space-y-1 px-3">
<h5 className="text-sm text-scale-1100">No filters applied to this view</h5>
<p className="text-xs text-scale-900">Add a column below to filter the view</p>
@@ -66,10 +118,13 @@ const Filter: FC = () => {
)}
</div>
<Popover.Separator />
<div className="px-3">
<div className="px-3 flex flex-row justify-between">
<Button icon={<IconPlus />} type="text" onClick={onAddFilter}>
Add filter
</Button>
<Button disabled={isEqual(filters, initialFilters)} type="default" onClick={onApplyFilter}>
Apply filter
</Button>
</div>
</div>
)
@@ -1,113 +1,26 @@
import { debounce } from 'lodash'
import { FC, memo, useState, useEffect, ChangeEvent, useCallback } from 'react'
import { FC, memo } from 'react'
import { Button, Input, IconChevronDown, IconX } from 'ui'
import { useUrlState } from 'hooks'
import { Filter } from 'components/grid/types'
import { Filter, FilterOperator } from 'components/grid/types'
import { DropdownControl } from 'components/grid/components/common'
import { useTrackedState } from 'components/grid/store'
import { FilterOperatorOptions } from './Filter.constants'
interface Props {
filter: Filter
filterIdx: number
filter: Filter
onChange: (index: number, filter: Filter) => void
onDelete: (index: number) => void
}
// [Area of improvement] Input field loses focus after the debounce (because of useUrlState?)
const FilterRow: FC<Props> = ({ filter, filterIdx }) => {
const FilterRow: FC<Props> = ({ filter, filterIdx, onChange, onDelete }) => {
const state = useTrackedState()
const [{ filter: filters }, setParams] = useUrlState({ arrayKeys: ['filter'] })
const column = state.table?.columns.find((x) => x.name === filter.column)
const columnOptions =
state.table?.columns?.map((x) => {
return { value: x.name, label: x.name, postLabel: x.dataType }
}) || []
const [filterValue, setFilterValue] = useState(filter.value)
useEffect(() => {
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) {
setParams((prevParams) => {
const existingFilters = (prevParams?.filter ?? []) as string[]
const updatedFilters = existingFilters.map((filter: string, idx: number) => {
if (idx === filterIdx) {
const [_, operator, ...value] = filter.split(':')
const formattedValue = value.join(':')
return `${column}:${operator}:${formattedValue}`
} else {
return filter
}
})
return {
...prevParams,
filter: updatedFilters,
}
})
}
function onOperatorChange(operator: string | number) {
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 formattedValue = value.join(':')
const selectedOperator = FilterOperatorOptions.find((option) => option.value === operator)
return `${column}:${selectedOperator?.abbrev}:${formattedValue}`
} else {
return filter
}
})
return {
...prevParams,
filter: updatedFilters,
}
})
}
function onValueChange(event: ChangeEvent<HTMLInputElement>) {
const value = event.target.value
setFilterValue(value)
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), [filters])
const placeholder =
column?.format === 'timestamptz'
@@ -118,7 +31,11 @@ const FilterRow: FC<Props> = ({ filter, filterIdx }) => {
return (
<div className="sb-grid-filter-row px-3">
<DropdownControl align="start" options={columnOptions} onSelect={onColumnChange}>
<DropdownControl
align="start"
options={columnOptions}
onSelect={(nextColumn) => onChange(filterIdx, { ...filter, column: nextColumn as string })}
>
<Button
as="span"
type="outline"
@@ -129,10 +46,19 @@ const FilterRow: FC<Props> = ({ filter, filterIdx }) => {
}
className="w-32"
>
{column?.name || ''}
{column?.name ?? ''}
</Button>
</DropdownControl>
<DropdownControl align="start" options={FilterOperatorOptions} onSelect={onOperatorChange}>
<DropdownControl
align="start"
options={FilterOperatorOptions}
onSelect={(nextOperator) =>
onChange(filterIdx, {
...filter,
operator: nextOperator as FilterOperator,
})
}
>
<Button
as="span"
type="outline"
@@ -149,14 +75,19 @@ const FilterRow: FC<Props> = ({ filter, filterIdx }) => {
size="tiny"
className="w-full"
placeholder={placeholder}
value={filterValue}
onChange={onValueChange}
value={filter.value}
onChange={(event) =>
onChange(filterIdx, {
...filter,
value: event.target.value,
})
}
/>
<Button
icon={<IconX strokeWidth={1.5} size={14} />}
size="tiny"
type="text"
onClick={onRemoveFilter}
onClick={() => onDelete(filterIdx)}
/>
</div>
)
@@ -1,10 +1,13 @@
import React, { FC } from 'react'
import React, { FC, useState, useMemo, useCallback } from 'react'
import { isEqual } from 'lodash'
import update from 'immutability-helper'
import { Button, IconList, IconChevronDown, Popover } from 'ui'
import { useUrlState } from 'hooks'
import SortRow from './SortRow'
import { useTrackedState } from 'components/grid/store'
import { DropdownControl } from 'components/grid/components/common'
import { Sort } from 'components/grid/types'
import { formatSortURLParams } from 'components/grid/SupabaseGrid.utils'
const SortPopover: FC = () => {
@@ -15,7 +18,7 @@ const SortPopover: FC = () => {
: 'Sort'
return (
<Popover size="large" align="start" className="sb-grid-sort-popover" overlay={<Sort />}>
<Popover size="large" align="start" className="sb-grid-sort-popover" overlay={<SortOverlay />}>
<Button
as="span"
type={(sorts || []).length > 0 ? 'link' : 'text'}
@@ -32,14 +35,18 @@ const SortPopover: FC = () => {
}
export default SortPopover
const Sort: FC = () => {
const SortOverlay: FC = () => {
const state = useTrackedState()
const [{ sort: sorts }, setParams] = useUrlState({ arrayKeys: ['sort'] })
const formattedSorts = formatSortURLParams(sorts as string[])
const [{ sort: sortsFromUrl }, setParams] = useUrlState({ arrayKeys: ['sort'] })
const initialSorts = useMemo(
() => formatSortURLParams((sortsFromUrl as string[]) ?? []),
[sortsFromUrl]
)
const [sorts, setSorts] = useState<Sort[]>(initialSorts)
const columns = state?.table?.columns!.filter((x) => {
const found = formattedSorts.find((y) => y.column == x.name)
const found = sorts.find((y) => y.column == x.name)
return !found
})
@@ -49,21 +56,59 @@ const Sort: FC = () => {
}) || []
function onAddSort(columnName: string | number) {
setSorts([...sorts, { column: columnName as string, ascending: true }])
}
function onApplySort() {
setParams((prevParams) => {
const existingSorts = (prevParams?.sort ?? []) as string[]
return {
...prevParams,
sort: existingSorts.concat([`${columnName}:asc`]),
sort: sorts.map((sort) => `${sort.column}:${sort.ascending ? 'asc' : 'desc'}`),
}
})
}
const onDeleteSort = useCallback((column: string) => {
setSorts((currentSorts) => currentSorts.filter((sort) => sort.column !== column))
}, [])
const onToggleSort = useCallback((column: string, ascending: boolean) => {
setSorts((currentSorts) => {
const idx = currentSorts.findIndex((x) => x.column === column)
return update(currentSorts, {
[idx]: {
$merge: { ascending },
},
})
})
}, [])
const onDragSort = useCallback((dragIndex: number, hoverIndex: number) => {
setSorts((currentSort) =>
update(currentSort, {
$splice: [
[dragIndex, 1],
[hoverIndex, 0, currentSort[dragIndex]],
],
})
)
}, [])
return (
<div className="space-y-2 py-2">
{formattedSorts.map((sort, index) => (
<SortRow key={sort.column} index={index} columnName={sort.column} sort={sort} />
{sorts.map((sort, index) => (
<SortRow
key={sort.column}
index={index}
columnName={sort.column}
sort={sort}
onDelete={onDeleteSort}
onToggle={onToggleSort}
onDrag={onDragSort}
/>
))}
{formattedSorts.length === 0 && (
{sorts.length === 0 && (
<div className="space-y-1 px-3">
<h5 className="text-sm text-scale-1100">No sorts applied to this view</h5>
<p className="text-xs text-scale-900">Add a column below to sort the view</p>
@@ -71,7 +116,7 @@ const Sort: FC = () => {
)}
<Popover.Separator />
<div className="px-3">
<div className="px-3 flex flex-row justify-between">
{columns && columns.length > 0 ? (
<DropdownControl
options={dropdownOptions}
@@ -85,12 +130,15 @@ const Sort: FC = () => {
iconRight={<IconChevronDown />}
className="sb-grid-dropdown__item-trigger"
>
{`Pick ${formattedSorts.length > 1 ? 'another' : 'a'} column to sort by`}
{`Pick ${sorts.length > 1 ? 'another' : 'a'} column to sort by`}
</Button>
</DropdownControl>
) : (
<p className="text-sm text-scale-1100">All columns have been added</p>
)}
<Button disabled={isEqual(sorts, initialSorts)} type="default" onClick={onApplySort}>
Apply sorting
</Button>
</div>
</div>
)
@@ -1,10 +1,8 @@
import update from 'immutability-helper'
import { FC, useRef, memo } from 'react'
import { Button, IconMenu, Toggle, IconX } from 'ui'
import { XYCoord } from 'dnd-core'
import { useDrag, useDrop, DropTargetMonitor } from 'react-dnd'
import { useUrlState } from 'hooks'
import { DragItem, Sort } from 'components/grid/types'
import { useTrackedState } from 'components/grid/store'
@@ -12,11 +10,13 @@ type SortRowProps = {
index: number
columnName: string
sort: Sort
onDelete: (columnName: string) => void
onToggle: (columnName: string, ascending: boolean) => void
onDrag: (dragIndex: number, hoverIndex: number) => void
}
const SortRow: FC<SortRowProps> = ({ index, columnName, sort }) => {
const SortRow: FC<SortRowProps> = ({ index, columnName, sort, onDelete, onToggle, onDrag }) => {
const state = useTrackedState()
const [_, setParams] = useUrlState({ arrayKeys: ['sort'] })
const column = state?.table?.columns.find((x) => x.name === columnName)
if (!column) return null
@@ -89,49 +89,9 @@ const SortRow: FC<SortRowProps> = ({ index, columnName, sort }) => {
},
})
function onToggle(value: boolean) {
setParams((prevParams) => {
const existingSorts = (prevParams?.sort ?? []) as string[]
const updatedSorts = existingSorts.map((sort: string) => {
const [column] = sort.split(':')
return column === columnName ? `${columnName}:${value ? 'asc' : 'desc'}` : sort
})
return {
...prevParams,
sort: updatedSorts,
}
})
}
function onRemoveSort() {
setParams((prevParams) => {
const existingSorts = (prevParams?.sort ?? []) as string[]
const updatedSorts = existingSorts.filter((sort: string) => {
const [column] = sort.split(':')
if (column !== columnName) return sort
})
return {
...prevParams,
sort: updatedSorts,
}
})
}
const moveSort = (dragIndex: number, hoverIndex: number) => {
if (dragIndex == hoverIndex) return
setParams((prevParams) => {
const existingSorts = (prevParams?.sort ?? []) as string[]
const updatedSorts = update(existingSorts, {
$splice: [
[dragIndex, 1],
[hoverIndex, 0, existingSorts[dragIndex]],
],
})
return {
...prevParams,
sort: updatedSorts,
}
})
onDrag(dragIndex, hoverIndex)
}
const opacity = isDragging ? 0 : 1
@@ -144,12 +104,9 @@ const SortRow: FC<SortRowProps> = ({ index, columnName, sort }) => {
style={{ opacity }}
data-handler-id={handlerId}
>
<Button
icon={<IconX strokeWidth={1.5} size={14} />}
size="tiny"
type="text"
onClick={onRemoveSort}
/>
<span className="transition-color text-scale-900 hover:text-scale-1100">
<IconMenu strokeWidth={2} size={16} />
</span>
<div className="grow">
<span className="flex grow items-center gap-1 truncate text-sm text-scale-1200">
<span className="text-xs text-scale-900">{index > 0 ? 'then by' : 'sort by'}</span>
@@ -163,12 +120,15 @@ const SortRow: FC<SortRowProps> = ({ index, columnName, sort }) => {
layout="flex"
defaultChecked={sort.ascending}
// @ts-ignore
onChange={(e: boolean) => onToggle(e)}
onChange={(e: boolean) => onToggle(columnName, e)}
/>
</div>
<span className="transition-color text-scale-900 hover:text-scale-1100">
<IconMenu strokeWidth={2} size={16} />
</span>
<Button
icon={<IconX strokeWidth={1.5} size={14} />}
size="tiny"
type="text"
onClick={() => onDelete(columnName)}
/>
</div>
)
}