diff --git a/vuu-ui/packages/vuu-table/src/Table.tsx b/vuu-ui/packages/vuu-table/src/Table.tsx index 13e5c0bb4..ee8b54576 100644 --- a/vuu-ui/packages/vuu-table/src/Table.tsx +++ b/vuu-ui/packages/vuu-table/src/Table.tsx @@ -121,6 +121,13 @@ export interface TableProps * TODO this should just live in CSS */ selectionBookendWidth?: number; + /** + * Selection behaviour for Table: + * `none` selection disabled + * `single` no more than one row may be selected + * `extended` (default) multiple rows can be selected + * `checkbox` same behaviour as extended, with checkbox column for selection + */ selectionModel?: TableSelectionModel; /** * if false, table rendered without headers. Useful when table is being included in a diff --git a/vuu-ui/packages/vuu-table/src/cell-renderers/checkbox-cell/CheckboxCell.tsx b/vuu-ui/packages/vuu-table/src/cell-renderers/checkbox-cell/CheckboxCell.tsx index cc87b91a5..fb99f4d43 100644 --- a/vuu-ui/packages/vuu-table/src/cell-renderers/checkbox-cell/CheckboxCell.tsx +++ b/vuu-ui/packages/vuu-table/src/cell-renderers/checkbox-cell/CheckboxCell.tsx @@ -1,4 +1,4 @@ -import React, { memo, useCallback } from "react"; +import { memo, useCallback } from "react"; import { TableCellRendererProps } from "@finos/vuu-table-types"; import { CheckboxIcon, WarnCommit } from "@finos/vuu-ui-controls"; import { Checkbox } from "@salt-ds/core"; diff --git a/vuu-ui/packages/vuu-table/src/cell-renderers/checkbox-row-selector/CheckboxRowSelectorCell.css b/vuu-ui/packages/vuu-table/src/cell-renderers/checkbox-row-selector/CheckboxRowSelectorCell.css new file mode 100644 index 000000000..4607c7b1f --- /dev/null +++ b/vuu-ui/packages/vuu-table/src/cell-renderers/checkbox-row-selector/CheckboxRowSelectorCell.css @@ -0,0 +1,5 @@ +.vuuTableCell { + .vuuCheckboxRowSelectorIcon { + margin-top: calc(var(--row-height) / 2 - 6px ); + } +} \ No newline at end of file diff --git a/vuu-ui/packages/vuu-table/src/cell-renderers/checkbox-row-selector/CheckboxRowSelectorCell.tsx b/vuu-ui/packages/vuu-table/src/cell-renderers/checkbox-row-selector/CheckboxRowSelectorCell.tsx new file mode 100644 index 000000000..1b26cdf19 --- /dev/null +++ b/vuu-ui/packages/vuu-table/src/cell-renderers/checkbox-row-selector/CheckboxRowSelectorCell.tsx @@ -0,0 +1,32 @@ +import { TableCellRendererProps } from "@finos/vuu-table-types"; +import { isRowSelected, registerComponent } from "@finos/vuu-utils"; +import { Checkbox } from "@salt-ds/core"; +import { useComponentCssInjection } from "@salt-ds/styles"; +import { useWindow } from "@salt-ds/window"; + +import checkboxRowSelectorCss from "./CheckboxRowSelectorCell.css"; + +export const CheckboxRowSelectorCell: React.FC = ({ + row, +}) => { + const targetWindow = useWindow(); + useComponentCssInjection({ + testId: "vuu-checkbox-row-selector-cell", + css: checkboxRowSelectorCss, + window: targetWindow, + }); + + const isChecked = isRowSelected(row); + + return ; +}; +CheckboxRowSelectorCell.displayName = "CheckboxCell"; + +registerComponent( + "checkbox-row-selector-cell", + CheckboxRowSelectorCell, + "cell-renderer", + { + serverDataType: "boolean", + } +); diff --git a/vuu-ui/packages/vuu-table/src/cell-renderers/checkbox-row-selector/index.ts b/vuu-ui/packages/vuu-table/src/cell-renderers/checkbox-row-selector/index.ts new file mode 100644 index 000000000..f5cf1c24b --- /dev/null +++ b/vuu-ui/packages/vuu-table/src/cell-renderers/checkbox-row-selector/index.ts @@ -0,0 +1 @@ +export * from "./CheckboxRowSelectorCell"; diff --git a/vuu-ui/packages/vuu-table/src/cell-renderers/index.ts b/vuu-ui/packages/vuu-table/src/cell-renderers/index.ts index fa1102243..b1689ed1a 100644 --- a/vuu-ui/packages/vuu-table/src/cell-renderers/index.ts +++ b/vuu-ui/packages/vuu-table/src/cell-renderers/index.ts @@ -1,3 +1,4 @@ export * from "./checkbox-cell"; +export * from "./checkbox-row-selector"; export * from "./input-cell"; export * from "./toggle-cell"; diff --git a/vuu-ui/packages/vuu-table/src/useSelection.ts b/vuu-ui/packages/vuu-table/src/useSelection.ts index a2de1543c..ce030a204 100644 --- a/vuu-ui/packages/vuu-table/src/useSelection.ts +++ b/vuu-ui/packages/vuu-table/src/useSelection.ts @@ -9,6 +9,7 @@ import { getRowElementAtIndex, isRowSelected, metadataKeys, + queryClosest, selectItem, } from "@finos/vuu-utils"; import { Selection, SelectionChangeHandler } from "@finos/vuu-data-types"; @@ -51,13 +52,20 @@ export const useSelection = ({ ); const handleRowClick = useCallback( - (evt, row, rangeSelect, keepExistingSelection) => { + (e, row, rangeSelect, keepExistingSelection) => { const { [IDX]: idx } = row; const { current: active } = lastActiveRef; const { current: selected } = selectedRef; const selectOperation = isRowSelected(row) ? deselectItem : selectItem; + if (selectionModel === "checkbox") { + const cell = queryClosest(e.target, ".vuuTableCell"); + if (!cell?.querySelector(".vuuCheckboxRowSelector")) { + return; + } + } + const newSelected = selectOperation( selectionModel, selected, diff --git a/vuu-ui/packages/vuu-table/src/useTable.ts b/vuu-ui/packages/vuu-table/src/useTable.ts index 20fe63f85..4dc30f016 100644 --- a/vuu-ui/packages/vuu-table/src/useTable.ts +++ b/vuu-ui/packages/vuu-table/src/useTable.ts @@ -169,7 +169,7 @@ export const useTable = ({ headings, tableAttributes, tableConfig, - } = useTableModel(config, dataSource); + } = useTableModel(config, dataSource, selectionModel); useLayoutEffectSkipFirst(() => { dispatchTableModelAction({ diff --git a/vuu-ui/packages/vuu-table/src/useTableModel.ts b/vuu-ui/packages/vuu-table/src/useTableModel.ts index be1ac311c..160d12439 100644 --- a/vuu-ui/packages/vuu-table/src/useTableModel.ts +++ b/vuu-ui/packages/vuu-table/src/useTableModel.ts @@ -6,6 +6,7 @@ import { TableAttributes, TableConfig, TableHeadings, + TableSelectionModel, } from "@finos/vuu-table-types"; import { applyFilterToColumns, @@ -60,6 +61,20 @@ const getDataType = ( } }; +const checkboxColumnDescriptor: ColumnDescriptor = { + label: "", + name: "", + width: 25, + sortable: false, + isSystemColumn: true, + type: { + name: "checkbox", + renderer: { + name: "checkbox-row-selector-cell", + }, + }, +}; + /** * TableModel represents state used internally to manage Table. It is * derived initially from the TableConfig provided by user, along with the @@ -219,12 +234,17 @@ const columnReducer: GridModelReducer = (state, action) => { export const useTableModel = ( tableConfigProp: TableConfig, - dataSource: DataSource + dataSource: DataSource, + selectionModel: TableSelectionModel ) => { const [state, dispatchTableModelAction] = useReducer< GridModelReducer, InitialConfig - >(columnReducer, { tableConfig: tableConfigProp, dataSource }, init); + >( + columnReducer, + { tableConfig: tableConfigProp, dataSource, selectionModel }, + init + ); const { columns, headings, tableConfig, ...tableAttributes } = state; @@ -239,24 +259,41 @@ export const useTableModel = ( type InitialConfig = { dataSource: DataSource; + // TODO are we at risk of losing selectionModel on updates ? + selectionModel?: TableSelectionModel; tableConfig: TableConfig; }; -function init({ dataSource, tableConfig }: InitialConfig): InternalTableModel { +function init({ + dataSource, + selectionModel, + tableConfig, +}: InitialConfig): InternalTableModel { const { columns, ...tableAttributes } = tableConfig; const { config: dataSourceConfig, tableSchema } = dataSource; + const toRuntimeColumnDescriptor = columnDescriptorToRuntimeColumDescriptor( + tableAttributes, + tableSchema + ); const runtimeColumns = columns .filter(subscribedOnly(dataSourceConfig?.columns)) - .map( - columnDescriptorToRuntimeColumDescriptor(tableAttributes, tableSchema) - ); + .map(toRuntimeColumnDescriptor); - const maybePinnedColumns = runtimeColumns.some(isPinned) + const columnsInRenderOrder = runtimeColumns.some(isPinned) ? sortPinnedColumns(runtimeColumns) : runtimeColumns; + + if (selectionModel === "checkbox") { + columnsInRenderOrder.splice( + 0, + 0, + toRuntimeColumnDescriptor(checkboxColumnDescriptor, -1) + ); + } + let state: InternalTableModel = { - columns: maybePinnedColumns, - headings: getTableHeadings(maybePinnedColumns), + columns: columnsInRenderOrder, + headings: getTableHeadings(columnsInRenderOrder), tableConfig, ...tableAttributes, }; diff --git a/vuu-ui/packages/vuu-utils/src/row-utils.ts b/vuu-ui/packages/vuu-utils/src/row-utils.ts index ffe2c4eb6..ee90a1ad4 100644 --- a/vuu-ui/packages/vuu-utils/src/row-utils.ts +++ b/vuu-ui/packages/vuu-utils/src/row-utils.ts @@ -2,8 +2,9 @@ import type { DataSourceRow, DataSourceRowObject } from "@finos/vuu-data-types"; import type { MutableRefObject } from "react"; import { ColumnMap, metadataKeys } from "./column-utils"; +import { isRowSelected } from "./selection-utils"; -const { IS_LEAF, KEY, IDX, SELECTED } = metadataKeys; +const { IS_LEAF, KEY, IDX } = metadataKeys; export type RowOffsetFunc = ( row: DataSourceRow, @@ -95,19 +96,13 @@ export const asDataSourceRowObject = ( row: DataSourceRow, columnMap: ColumnMap ): DataSourceRowObject => { - console.log({ columnMap }); - const { - [IS_LEAF]: isLeaf, - [KEY]: key, - [IDX]: index, - [SELECTED]: selected, - } = row; + const { [IS_LEAF]: isLeaf, [KEY]: key, [IDX]: index } = row; const rowObject: DataSourceRowObject = { key, index, isGroupRow: !isLeaf, - isSelected: selected > 0, + isSelected: isRowSelected(row), data: {}, }; diff --git a/vuu-ui/packages/vuu-utils/src/selection-utils.ts b/vuu-ui/packages/vuu-utils/src/selection-utils.ts index 7f0a70eb8..5c724cefb 100644 --- a/vuu-ui/packages/vuu-utils/src/selection-utils.ts +++ b/vuu-ui/packages/vuu-utils/src/selection-utils.ts @@ -20,6 +20,7 @@ export const RowSelected = { export const isRowSelected = (row: DataSourceRow): boolean => (row[SELECTED] & RowSelected.True) === RowSelected.True; + export const isRowSelectedLast = (row?: DataSourceRow): boolean => row !== undefined && (row[SELECTED] & RowSelected.Last) === RowSelected.Last; diff --git a/vuu-ui/showcase/src/examples/Table/TableSelection.examples.tsx b/vuu-ui/showcase/src/examples/Table/TableSelection.examples.tsx new file mode 100644 index 000000000..b5d8873a3 --- /dev/null +++ b/vuu-ui/showcase/src/examples/Table/TableSelection.examples.tsx @@ -0,0 +1,45 @@ +import { getSchema, SimulTableName, vuuModule } from "@finos/vuu-data-test"; +import { Table, TableProps } from "@finos/vuu-table"; +import { useCallback, useMemo } from "react"; + +import "./Table.examples.css"; + +let displaySequence = 1; + +export const CheckboxSelection = () => { + const tableProps = useMemo< + Pick + >(() => { + const tableName: SimulTableName = "instruments"; + return { + config: { + columns: getSchema(tableName).columns, + rowSeparators: true, + zebraStripes: true, + }, + dataSource: + vuuModule("SIMUL").createDataSource(tableName), + selectionModel: "checkbox", + }; + }, []); + + const onSelect = useCallback((row) => { + console.log("onSelect", { row }); + }, []); + const onSelectionChange = useCallback((selected) => { + console.log("onSelectionChange", { selected }); + }, []); + + return ( + + ); +}; +CheckboxSelection.displaySequence = displaySequence++; diff --git a/vuu-ui/showcase/src/examples/Table/index.ts b/vuu-ui/showcase/src/examples/Table/index.ts index 977657973..632686bf2 100644 --- a/vuu-ui/showcase/src/examples/Table/index.ts +++ b/vuu-ui/showcase/src/examples/Table/index.ts @@ -1,5 +1,6 @@ export * as TableList from "./TableList.examples"; export * as Table from "./Table.examples"; +export * as TableSelection from "./TableSelection.examples"; export * as BASKET from "./BASKET.examples"; export * as SIMUL from "./SIMUL.examples"; export * as TEST from "./TEST.examples";