Skip to content

Commit

Permalink
feat(table): table column filter custom input render (#3833)
Browse files Browse the repository at this point in the history
* feat(table): table column filter custom input render

* feat(table): table column filter updated custom input render

* feat(table): table column filter updated custom input render snapshots

* feat(table): table column filter updated custom input render snapshots

* feat(table): table column filter updated custom input render snapshots

* feat(table): table column filter updated custom input render snapshots

* feat(table): table column filter resolved all the comments

* feat(table): table column filter resolved comments
  • Loading branch information
abpaul1993 authored Dec 1, 2023
1 parent 57ea650 commit b668143
Show file tree
Hide file tree
Showing 11 changed files with 5,377 additions and 1,468 deletions.
99 changes: 99 additions & 0 deletions packages/react/src/components/Table/Table.main.story.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
addColumnGroupIds,
getTableActions,
getTableColumns,
getTableCustomColumns,
getTableData,
getTableDataWithEmptySelectFilter,
getTableToolbarActions,
Expand Down Expand Up @@ -1133,6 +1134,104 @@ WithFiltering.parameters = {
},
};

export const WithCustomInputFiltering = () => {
const {
selectedTableType,
hideClearAllFiltersButton,
hasEmptyFilterOption,
hasMultiSelectFilter,
hasFilterRowIcon,
} = getTableKnobs({
knobsToCreate: [
'selectedTableType',
'hideClearAllFiltersButton',
'hasEmptyFilterOption',
'hasMultiSelectFilter',
'hasFilterRowIcon',
],
getDefaultValue: (knobName) => {
if (knobName === 'hideClearAllFiltersButton') {
return false;
}

if (knobName === 'hasEmptyFilterOption') {
return false;
}

if (knobName === 'hasMultiSelectFilter') {
return false;
}

if (knobName === 'hasFilterRowIcon') {
return false;
}

return true;
},
});

const MyTable = selectedTableType === 'StatefulTable' ? StatefulTable : Table;
const data = hasEmptyFilterOption
? getTableDataWithEmptySelectFilter().slice(0, 30)
: getTableData().slice(0, 30);

const columns = decorateTableColumns(
getTableCustomColumns(),
hasEmptyFilterOption,
hasMultiSelectFilter
).map((col) =>
col.id === 'object'
? {
...col,
tooltip: `This column has objects as values and needs a custom filter function that
filters based on an object property.`,
}
: col
);

const activeFilters = object('Active filters (view.filters)', [
{
columnId: 'string',
value: 'whiteboard',
},
]);
const activeBar = select(
'Show filter toolbar (view.toolbar.activeBar)',
['filter', undefined],
'filter'
);

const knobRegeneratedKey = `${JSON.stringify(activeFilters)}`;
return (
<>
<MyTable
key={knobRegeneratedKey}
columns={columns}
data={data}
options={{
hasFilter: true,
hasFilterRowIcon,
}}
view={{
filters: activeFilters,
toolbar: {
activeBar,
hideClearAllFiltersButton,
},
}}
/>
</>
);
};

WithCustomInputFiltering.storyName = 'With custom input filtering';
WithCustomInputFiltering.parameters = {
component: Table,
docs: {
page: FilteringREADME,
},
};

export const WithSelectionAndBatchActions = () => {
const {
selectedTableType,
Expand Down
94 changes: 94 additions & 0 deletions packages/react/src/components/Table/Table.story.helpers.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Add20, TrashCan16, BeeBat16, Activity16, ViewOff16, Error16 } from '@ca
import Arrow from '@carbon/icons-react/es/arrow--right/16';
import Add from '@carbon/icons-react/es/add/16';
import Edit from '@carbon/icons-react/es/edit/16';
import { ComboBox, DatePickerInput, NumberInput } from 'carbon-components-react';

import { Checkbox } from '../Checkbox';
import { TextInput } from '../TextInput';
Expand Down Expand Up @@ -98,6 +99,8 @@ export const getSelectDataOptions = () => [
},
];

const itemToString = (item) => (item ? item.text : '');

const getSelectDataOptionsWithEmptyOption = () => {
const selectOptions = getSelectDataOptions();
selectOptions.splice(2, 1, {
Expand Down Expand Up @@ -290,6 +293,97 @@ export const getTableColumns = () => [
},
];

export const getTableCustomColumns = () => [
{
id: 'string',
name: 'String',
filter: { placeholderText: 'enter a string' },
},

{
id: 'date',
name: 'Date',
filter: {
customInput: ({ onChange, value }) => (
<DatePickerInput placeholder="enter a data" value={value} onChange={onChange} />
),
},
},
{
id: 'select',
name: 'Select',
filter: {
customInput: ({ value, onChange }) => (
<ComboBox
placeholder="Filter col"
value={value}
itemToString={itemToString}
items={getSelectDataOptions()}
onChange={onChange}
/>
),
},
},
{
id: 'secretField',
name: 'Secret Information',
filter: {
customInput: ({ value, onChange, id }) => (
<TextInput placeholder="Filter col" value={value} id={id} onChange={onChange} />
),
},
},
{
id: 'status',
name: 'Status',
renderDataFunction: renderStatusIcon,
sortFunction: customColumnSort,
},
{
id: 'number',
name: 'Number',
filter: {
customInput: ({ value, onChange }) => (
<NumberInput
placeholder="enter a number"
step={1}
allowEmpty
onChange={onChange}
value={value}
/>
),
},
},
{
id: 'boolean',
name: 'Boolean',
},
{
id: 'node',
name: 'React Node',
},
{
id: 'object',
name: 'Object Id',
renderDataFunction: ({ value }) => {
return value?.id;
},
sortFunction: ({ data, columnId, direction }) => {
// clone inputData because sort mutates the array
const sortedData = data.map((i) => i);
sortedData.sort((a, b) => {
const aId = a.values[columnId].id;
const bId = b.values[columnId].id;
const compare = aId.localeCompare(bId);

return direction === 'ASC' ? compare : -compare;
});

return sortedData;
},
},
];

export const getTableToolbarActions = () => [
{
id: 'edit',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ class FilterHeaderRow extends Component {
),
/** if isMultiselect and isFilterable are true, the table is filtered based on a multiselect */
isMultiselect: PropTypes.bool,
/**
* customInput should be a React component that will accept the following props. value, onChange, id, column, columns etc.
* The expectation is that the input component will use value as the default value and onChange will be called when the value changes. The FilterTableRow will debounce the values so the component does not need to do this
*/
customInput: PropTypes.elementType,
})
).isRequired,
/** internationalized string */
Expand Down Expand Up @@ -352,7 +357,11 @@ class FilterHeaderRow extends Component {
'--filter-header-dropdown-max-height': dropdownMaxHeight,
}}
>
{hasDragAndDrop && <TableHeader className={`${iotPrefix}--filter-header-row--header`} />}
{hasDragAndDrop ? (
/* istanbul ignore next */ <TableHeader
className={`${iotPrefix}--filter-header-row--header`}
/>
) : null}
{hasRowSelection === 'multi' ||
(hasRowSelection === 'single' && useRadioButtonSingleSelect) ? (
<TableHeader className={`${iotPrefix}--filter-header-row--header`} ref={this.rowRef} />
Expand All @@ -373,11 +382,40 @@ class FilterHeaderRow extends Component {
const isLastColumn = visibleColumns.length - 1 === i;
const lastVisibleColumn = visibleColumns.slice(-1)[0];
const isLastVisibleColumn = column.id === lastVisibleColumn.columnId;
/* istanbul ignore next */
const CustomInput = column.customInput;
// undefined check has the effect of making isFilterable default to true
// if unspecified
const headerContent =
column.isFilterable !== undefined && !column.isFilterable ? (
<div />
) : /* istanbul ignore next */
column.customInput !== undefined ? (
<CustomInput
ref={this.setFirstFilterableRef}
id={`column-${i}`}
onChange={(event) => {
if (event.persist) {
event.persist();
}
this.setState(
(state) => ({
filterValues: {
...state.filterValues,
[column.id]: event.selectedItem
? getFilterValue(event.selectedItem)
: event.selectedItems?.length > 0
? event.selectedItems.map(getMultiselectFilterValue)
: event.target.value,
},
}),
debounce(this.handleApplyFilter, 1000)
);
}}
column={column}
columns={columns}
value={filterValues[column.id]}
/>
) : column.options ? (
column.isMultiselect ? (
<FilterableMultiSelect
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TextInput } from 'carbon-components-react';

import * as utils from '../../../../utils/componentUtilityFunctions';
import { settings } from '../../../../constants/Settings';
Expand Down Expand Up @@ -790,6 +791,49 @@ describe('FilterHeaderRow', () => {
expect(screen.getByPlaceholderText('Choose an option')).toHaveValue('empty string');
});

it('should display custom input when not undefined', () => {
render(
<FilterHeaderRow
{...commonFilterProps}
ordering={[{ columnId: 'col1' }, { columnId: 'col2' }]}
columns={[{ id: 'col1', customInput: () => <div>customInput</div> }, { id: 'col2' }]}
/>
);

expect(screen.getAllByText('customInput')[0]).toBeInTheDocument();
});

it('should display custom input and filter data in the table', async () => {
const initialValue = 'initial value';
render(
<FilterHeaderRow
{...commonFilterProps}
ordering={[{ columnId: 'col1' }, { columnId: 'col2' }]}
columns={[
{
id: 'col1',
customInput: ({ onChange, id }) => (
<TextInput
placeholder="Filter col1"
value={initialValue}
id={id}
onChange={onChange}
/>
),
},
{ id: 'col2' },
]}
/>
);

// Get the custom input field by its placeholder text
const filterInput = screen.getByPlaceholderText('Filter col1');
expect(filterInput).toHaveValue('initial value');
// Simulate typing in the filter input
fireEvent.change(filterInput, { target: { value: 'Value A' }, persist: jest.fn() });
expect(commonFilterProps.onApplyFilter).toHaveBeenCalledTimes(1);
});

it('should pass empty string filter value to handler', () => {
const columnId = 'col1';
render(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,7 @@ const TableHead = ({
isFilterable: !isNil(column.filter),
isMultiselect: column.filter?.isMultiselect,
width: column.width,
customInput: column.filter?.customInput,
}))}
hasFastFilter={hasFastFilter}
clearFilterText={clearFilterText}
Expand Down
5 changes: 5 additions & 0 deletions packages/react/src/components/Table/TablePropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ export const TableColumnsPropTypes = PropTypes.arrayOf(
placeholderText: PropTypes.string,
/** if isMultiselect is true, the table is filtered based on a multiselect */
isMultiselect: PropTypes.bool,
/**
* customInput should be a React component that will accept the following props. value, onChange, id, column, columns etc.
* The expectation is that the input component will use value as the default value and onChange will be called when the value changes. The FilterTableRow will debounce the values so the component does not need to do this
*/
customInput: PropTypes.elementType,
options: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]).isRequired,
Expand Down
Loading

0 comments on commit b668143

Please sign in to comment.