diff --git a/src/components/HighTable/HighTable.stories.tsx b/src/components/HighTable/HighTable.stories.tsx index 20e04a3a..861111ed 100644 --- a/src/components/HighTable/HighTable.stories.tsx +++ b/src/components/HighTable/HighTable.stories.tsx @@ -344,6 +344,21 @@ export const NonSortableColunns: Story = { data: sortableDataFrame(createUnsortableData(), { sortableColumns: new Set(['ID', 'Count', 'Constant', 'Value1', 'Value2', 'Value3', 'Undefined']) }), }, } +export const ExclusiveSort: Story = { + render: (args) => { + const [orderBy, setOrderBy] = useState([]) + return ( + + ) + }, + args: { + data: sortableDataFrame(createUnsortableData(), { exclusiveSort: true }), + }, +} export const LongStrings: Story = { args: { data: sortableDataFrame(createLongStringsData()), diff --git a/src/components/TableHeader/TableHeader.tsx b/src/components/TableHeader/TableHeader.tsx index bd5970d2..0f744c78 100644 --- a/src/components/TableHeader/TableHeader.tsx +++ b/src/components/TableHeader/TableHeader.tsx @@ -1,5 +1,6 @@ import { useCallback, useMemo } from 'react' -import { OrderBy, toggleColumn } from '../../helpers/sort.js' +import { OrderBy, toggleColumn, toggleColumnExclusive } from '../../helpers/sort.js' +import { useData } from '../../hooks/useData.js' import { ColumnParameters } from '../../hooks/useTableConfig.js' import ColumnHeader from '../ColumnHeader/ColumnHeader.js' @@ -18,12 +19,15 @@ interface TableHeaderProps { export default function TableHeader({ columnsParameters, orderBy, onOrderByChange, canMeasureWidth, ariaRowIndex, columnClassNames = [], }: TableHeaderProps) { + const { data } = useData() + const exclusiveSort = data.exclusiveSort === true // Function to handle click for changing orderBy const getToggleOrderBy = useCallback((columnHeader: string) => { if (!onOrderByChange || !orderBy) return undefined return () => { - onOrderByChange(toggleColumn(columnHeader, orderBy)) - }}, [orderBy, onOrderByChange] + const next = exclusiveSort ? toggleColumnExclusive(columnHeader, orderBy) : toggleColumn(columnHeader, orderBy) + onOrderByChange(next) + }}, [orderBy, onOrderByChange, exclusiveSort] ) const orderByColumn = useMemo(() => { diff --git a/src/helpers/dataframe/helpers.ts b/src/helpers/dataframe/helpers.ts index aab7e588..da56320c 100644 --- a/src/helpers/dataframe/helpers.ts +++ b/src/helpers/dataframe/helpers.ts @@ -47,9 +47,9 @@ export function validateColumn({ column, data: { columnDescriptors } }: { column } } -export function validateOrderBy({ orderBy, data: { columnDescriptors } }: { orderBy?: OrderBy, data: Pick }): void { +export function validateOrderBy({ orderBy, data: { columnDescriptors, exclusiveSort } }: { orderBy?: OrderBy, data: Pick }): void { const sortableColumns = new Set(columnDescriptors.filter(c => c.sortable).map(c => c.name)) - validateOrderByAgainstSortableColumns({ orderBy, sortableColumns }) + validateOrderByAgainstSortableColumns({ orderBy, sortableColumns, exclusiveSort }) } export function checkSignal(signal?: AbortSignal): void { diff --git a/src/helpers/dataframe/sort.ts b/src/helpers/dataframe/sort.ts index a73003cd..a983eea2 100644 --- a/src/helpers/dataframe/sort.ts +++ b/src/helpers/dataframe/sort.ts @@ -4,10 +4,11 @@ import { checkSignal, validateColumn, validateFetchParams, validateRow } from '. import { DataFrame, DataFrameEvents, Obj, ResolvedValue } from './types.js' export function sortableDataFrame( - data: DataFrame, options?: { sortableColumns?: Set } + data: DataFrame, options?: { sortableColumns?: Set, exclusiveSort?: boolean } ): DataFrame { // If sortableColumns is not provided, make all columns sortable. const sortableColumns = options?.sortableColumns ?? new Set(data.columnDescriptors.map(c => c.name)) + const exclusiveSort = options?.exclusiveSort ?? data.exclusiveSort // Validate that all sortable columns are present in the header. for (const column of sortableColumns) { validateColumn({ column, data: { columnDescriptors: data.columnDescriptors } }) @@ -20,6 +21,9 @@ export function sortableDataFrame( return sortable === false || sortable === undefined // If the column is not in sortableColumns, it should not be sortable })) { // TODO(SL): we should return a clone of the data frame (and we should provide a helper function to clone a dataframe). + if (options && 'exclusiveSort' in options && data.exclusiveSort !== options.exclusiveSort) { + return { ...data, exclusiveSort: options.exclusiveSort } + } return data } @@ -39,7 +43,7 @@ export function sortableDataFrame( const getUpstreamRow: ({ row, orderBy }: { row: number, orderBy?: OrderBy }) => ResolvedValue | undefined = function({ row, orderBy }) { validateRow({ row, data: { numRows } }) - validateOrderByAgainstSortableColumns({ orderBy, sortableColumns }) + validateOrderByAgainstSortableColumns({ orderBy, sortableColumns, exclusiveSort }) if (!orderBy || orderBy.length === 0) { // If no orderBy is provided, we can return the upstream row number. return { value: row } @@ -116,7 +120,7 @@ export function sortableDataFrame( } } - return { metadata, numRows, columnDescriptors, getRowNumber, getCell, fetch, eventTarget } + return { metadata, numRows, columnDescriptors, getRowNumber, getCell, fetch, eventTarget, exclusiveSort } } async function fetchFromIndexes({ columns, indexes, signal, fetch }: { columns?: string[], indexes: number[], signal?: AbortSignal, fetch: Exclude }): Promise { diff --git a/src/helpers/dataframe/types.ts b/src/helpers/dataframe/types.ts index 911abfa1..b53e299b 100644 --- a/src/helpers/dataframe/types.ts +++ b/src/helpers/dataframe/types.ts @@ -36,6 +36,10 @@ export interface DataFrame { columnDescriptors: readonly ColumnDescriptor[] metadata?: M + // If true, only one column can be sorted at a time, and any update to orderBy will replace the previous one. + // Defaults to false. + exclusiveSort?: boolean + // Returns the cell value. // undefined means pending, ResolvedValue is a boxed value type (so we can distinguish undefined from pending) // getCell does NOT initiate a fetch, it just returns resolved data diff --git a/src/helpers/sort.ts b/src/helpers/sort.ts index 19653541..9dc3e1db 100644 --- a/src/helpers/sort.ts +++ b/src/helpers/sort.ts @@ -11,12 +11,15 @@ export function serializeOrderBy(orderBy: OrderBy): string { return JSON.stringify(orderBy) } -export function validateOrderByAgainstSortableColumns({ sortableColumns, orderBy }: { sortableColumns?: Set, orderBy?: OrderBy }): void { +export function validateOrderByAgainstSortableColumns({ sortableColumns, orderBy, exclusiveSort }: { sortableColumns?: Set, orderBy?: OrderBy, exclusiveSort?: boolean }): void { if (!orderBy) return const unsortableColumns = orderBy.map(({ column }) => column).filter(column => !sortableColumns?.has(column)) if (unsortableColumns.length > 0) { throw new Error(`Unsortable columns in orderBy field: ${unsortableColumns.join(', ')}`) } + if (exclusiveSort && orderBy.length > 1) { + throw new Error('DataFrame is exclusiveSort, but orderBy contains multiple columns') + } } export function areEqualOrderBy(a?: OrderBy, b?: OrderBy): boolean { @@ -62,6 +65,17 @@ export function toggleColumn(column: string, orderBy: OrderBy): OrderBy { return [{ column, direction: 'ascending' }, ...prefix, ...suffix] } +export function toggleColumnExclusive(column: string, orderBy: OrderBy): OrderBy { + const { item } = partitionOrderBy(orderBy, column) + if (item) { + if (item.direction === 'ascending') { + return [{ column, direction: 'descending' }] + } + return [] + } + return [{ column, direction: 'ascending' }] +} + // TODO(SL): test export function computeRanks(values: any[]): number[] { const valuesWithIndex = values.map((value, index) => ({ value, index }))