Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DataGrid] Allow to customize label and value for singleSelect #7684

Merged
merged 10 commits into from
Feb 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion docs/data/data-grid/column-definition/column-definition.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,29 @@ However, some types require additional properties to be set to make them work co
```

:::warning
When using objects values for `valueOptions` you need to provide `value` and `label` fields for each option: `{ value: string, label: string }`
When using objects values for `valueOptions` you need to provide the `value` and `label` attributes for each option.
However, you can customize which attribute is used as value and label by using `getOptionValue` and `getOptionLabel`, respectively.

```tsx
// Without getOptionValue and getOptionLabel
{
valueOptions: [
{ value: 'BR', label: 'Brazil' }
{ value: 'FR', label: 'France' }
]
}

// With getOptionValue and getOptionLabel
{
getOptionValue: (value: any) => value.code,
getOptionLabel: (value: any) => value.name,
valueOptions: [
{ code: 'BR', name: 'Brazil' }
{ code: 'FR', name: 'France' }
]
}
```

:::

- If the column type is `'actions'`, you need to provide a `getActions` function that returns an array of actions available for each row (React elements).
Expand Down
2 changes: 2 additions & 0 deletions docs/pages/x/api/data-grid/grid-single-select-col-def.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import { GridSingleSelectColDef } from '@mui/x-data-grid';
| <span class="prop-name optional">filterOperators<sup><abbr title="optional">?</abbr></sup></span> | <span class="prop-type">GridFilterOperator&lt;R, V, F&gt;[]</span> | | Allows setting the filter operators for this column. |
| <span class="prop-name optional">flex<sup><abbr title="optional">?</abbr></sup></span> | <span class="prop-type">number</span> | | If set, it indicates that a column has fluid width. Range [0, ∞). |
| <span class="prop-name optional">getApplyQuickFilterFn<sup><abbr title="optional">?</abbr></sup></span> | <span class="prop-type">(value: any, colDef: GridStateColDef, apiRef: React.MutableRefObject&lt;GridApiCommunity&gt;) =&gt; null \| ((params: GridCellParams&lt;R, V, F&gt;) =&gt; boolean)</span> | | The callback that generates a filtering function for a given quick filter value.<br />This function can return `null` to skip filtering for this value and column. |
| <span class="prop-name optional">getOptionLabel<sup><abbr title="optional">?</abbr></sup></span> | <span class="prop-type">(value: ValueOptions) =&gt; string</span> | | Used to determine the label displayed for a given value option. |
| <span class="prop-name optional">getOptionValue<sup><abbr title="optional">?</abbr></sup></span> | <span class="prop-type">(value: ValueOptions) =&gt; any</span> | | Used to determine the value used for a value option. |
| <span class="prop-name optional">groupable<sup><abbr title="optional">?</abbr></sup></span> | <span class="prop-type">boolean</span> | <span class="prop-default">true</span> | If `true`, the rows can be grouped based on this column values (pro-plan only).<br />Only available in DataGridPremium. |
| <span class="prop-name optional">groupingValueGetter<sup><abbr title="optional">?</abbr></sup> [<span class="plan-premium" title="Premium plan"></span>](/x/introduction/licensing/#premium-plan)</span> | <span class="prop-type">(params: GridGroupingValueGetterParams&lt;R, V&gt;) =&gt; GridKeyValue \| null \| undefined</span> | | Function that transforms a complex cell value into a key that be used for grouping the rows. |
| <span class="prop-name optional">headerAlign<sup><abbr title="optional">?</abbr></sup></span> | <span class="prop-type">GridAlignment</span> | | Header cell element alignment. |
Expand Down
24 changes: 16 additions & 8 deletions packages/grid/x-data-grid/src/colDef/gridSingleSelectColDef.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,26 @@ import { GRID_STRING_COL_DEF } from './gridStringColDef';
import { GridSingleSelectColDef, ValueOptions } from '../models/colDef/gridColDef';
import { renderEditSingleSelectCell } from '../components/cell/GridEditSingleSelectCell';
import { getGridSingleSelectOperators } from './gridSingleSelectOperators';
import {
getLabelFromValueOption,
isSingleSelectColDef,
} from '../components/panel/filterPanel/filterPanelUtils';
import { isSingleSelectColDef } from '../components/panel/filterPanel/filterPanelUtils';
import { isObject } from '../utils/utils';

const isArrayOfObjects = (options: any): options is Array<{ value: any; label: string }> => {
const isArrayOfObjects = (options: any): options is Array<Record<string, any>> => {
return typeof options[0] === 'object';
};

const defaultGetOptionValue = (value: ValueOptions) => {
return isObject(value) ? value.value : value;
};

const defaultGetOptionLabel = (value: ValueOptions) => {
return isObject(value) ? value.label : String(value);
};

export const GRID_SINGLE_SELECT_COL_DEF: Omit<GridSingleSelectColDef, 'field'> = {
...GRID_STRING_COL_DEF,
type: 'singleSelect',
getOptionLabel: defaultGetOptionLabel,
getOptionValue: defaultGetOptionValue,
valueFormatter(params) {
const { id, field, value, api } = params;
const colDef = params.api.getColumn(field);
Expand All @@ -38,11 +46,11 @@ export const GRID_SINGLE_SELECT_COL_DEF: Omit<GridSingleSelectColDef, 'field'> =
}

if (!isArrayOfObjects(valueOptions)) {
return getLabelFromValueOption(value);
return colDef.getOptionLabel!(value);
}

const valueOption = valueOptions.find((option) => option.value === value);
return valueOption ? getLabelFromValueOption(valueOption) : '';
const valueOption = valueOptions.find((option) => colDef.getOptionValue!(option) === value);
return valueOption ? colDef.getOptionLabel!(valueOption) : '';
},
renderEditCell: renderEditSingleSelectCell,
filterOperators: getGridSingleSelectOperators(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@ import { GridRenderEditCellParams } from '../../models/params/gridCellParams';
import { isEscapeKey } from '../../utils/keyboardUtils';
import { useGridRootProps } from '../../hooks/utils/useGridRootProps';
import { GridEditModes } from '../../models/gridEditRowModel';
import { ValueOptions } from '../../models/colDef/gridColDef';
import { GridSingleSelectColDef, ValueOptions } from '../../models/colDef/gridColDef';
import {
getLabelFromValueOption,
getValueFromValueOptions,
isSingleSelectColDef,
} from '../panel/filterPanel/filterPanelUtils';
import { useGridApiContext } from '../../hooks/utils/useGridApiContext';

export interface GridEditSingleSelectCellProps
extends GridRenderEditCellParams,
Omit<SelectProps, 'id' | 'tabIndex' | 'value'> {
Omit<SelectProps, 'id' | 'tabIndex' | 'value'>,
Pick<GridSingleSelectColDef, 'getOptionLabel' | 'getOptionValue'> {
/**
* Callback called when the value is changed by the user.
* @param {SelectChangeEvent<any>} event The event source of the callback.
Expand All @@ -29,12 +29,6 @@ export interface GridEditSingleSelectCellProps
* If true, the select opens by default.
*/
initialOpen?: boolean;
/**
* Used to determine the text displayed for a given value option.
* @param {ValueOptions} value The current value option.
* @returns {string} The text to be displayed.
*/
getOptionLabel?: (value: ValueOptions) => string;
}

function isKeyboardEvent(event: any): event is React.KeyboardEvent {
Expand Down Expand Up @@ -62,7 +56,8 @@ function GridEditSingleSelectCell(props: GridEditSingleSelectCellProps) {
error,
onValueChange,
initialOpen = rootProps.editMode === GridEditModes.Cell,
getOptionLabel = getLabelFromValueOption,
getOptionLabel: getOptionLabelProp,
getOptionValue: getOptionValueProp,
...other
} = props;

Expand Down Expand Up @@ -95,11 +90,22 @@ function GridEditSingleSelectCell(props: GridEditSingleSelectCellProps) {
return null;
}

const getOptionValue = getOptionValueProp || colDef.getOptionValue!;
const getOptionLabel = getOptionLabelProp || colDef.getOptionLabel!;

const handleChange: SelectProps['onChange'] = async (event) => {
if (!isSingleSelectColDef(colDef) || !valueOptions) {
return;
}

setOpen(false);
const target = event.target as HTMLInputElement;
// NativeSelect casts the value to a string.
const formattedTargetValue = getValueFromValueOptions(target.value, valueOptions);
const formattedTargetValue = getValueFromValueOptions(
target.value,
valueOptions,
getOptionValue,
);

if (onValueChange) {
await onValueChange(event, formattedTargetValue);
Expand Down Expand Up @@ -127,6 +133,10 @@ function GridEditSingleSelectCell(props: GridEditSingleSelectCellProps) {

const OptionComponent = isSelectNative ? 'option' : MenuItem;

if (!valueOptions || !colDef) {
return null;
}

return (
<rootProps.components.BaseSelect
ref={ref}
Expand All @@ -145,7 +155,7 @@ function GridEditSingleSelectCell(props: GridEditSingleSelectCellProps) {
{...rootProps.componentsProps?.baseSelect}
>
{valueOptions.map((valueOption) => {
const value = typeof valueOption === 'object' ? valueOption.value : valueOption;
const value = getOptionValue(valueOption);

return (
<OptionComponent key={value} value={value}>
Expand Down Expand Up @@ -184,11 +194,17 @@ GridEditSingleSelectCell.propTypes = {
*/
formattedValue: PropTypes.any,
/**
* Used to determine the text displayed for a given value option.
* Used to determine the label displayed for a given value option.
* @param {ValueOptions} value The current value option.
* @returns {string} The text to be displayed.
*/
getOptionLabel: PropTypes.func,
/**
* Used to determine the value used for a value option.
* @param {ValueOptions} value The current value option.
* @returns {string} The value to be used.
*/
getOptionValue: PropTypes.func,
/**
* If true, the cell is the active element.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,7 @@ import PropTypes from 'prop-types';
import Autocomplete, { AutocompleteProps, createFilterOptions } from '@mui/material/Autocomplete';
import Chip from '@mui/material/Chip';
import { unstable_useId as useId } from '@mui/utils';
import {
getLabelFromValueOption,
getValueFromOption,
isSingleSelectColDef,
} from './filterPanelUtils';
import { isSingleSelectColDef } from './filterPanelUtils';
import { useGridRootProps } from '../../../hooks/utils/useGridRootProps';
import { GridFilterInputValueProps } from './GridFilterInputValueProps';
import type { GridSingleSelectColDef, ValueOptions } from '../../../models/colDef/gridColDef';
Expand All @@ -26,24 +22,11 @@ export interface GridFilterInputMultipleSingleSelectProps
| 'color'
| 'getOptionLabel'
>,
Pick<GridSingleSelectColDef, 'getOptionLabel' | 'getOptionValue'>,
GridFilterInputValueProps {
type?: 'singleSelect';
/**
* Used to determine the text displayed for a given value option.
* @param {ValueOptions} value The current value option.
* @returns {string} The text to be displayed.
*/
getOptionLabel?: (value: ValueOptions) => string;
}

const isOptionEqualToValue: AutocompleteProps<
ValueOptions,
true,
false,
true
>['isOptionEqualToValue'] = (option, value) =>
getValueFromOption(option) === getValueFromOption(value);

const filter = createFilterOptions<any>();

function GridFilterInputMultipleSingleSelect(props: GridFilterInputMultipleSingleSelectProps) {
Expand All @@ -58,7 +41,8 @@ function GridFilterInputMultipleSingleSelect(props: GridFilterInputMultipleSingl
helperText,
size,
variant = 'standard',
getOptionLabel = getLabelFromValueOption,
getOptionLabel: getOptionLabelProp,
getOptionValue: getOptionValueProp,
...other
} = props;
const TextFieldProps = {
Expand All @@ -80,6 +64,14 @@ function GridFilterInputMultipleSingleSelect(props: GridFilterInputMultipleSingl
}
}

const getOptionValue = getOptionValueProp || resolvedColumn?.getOptionValue!;
const getOptionLabel = getOptionLabelProp || resolvedColumn?.getOptionLabel!;

const isOptionEqualToValue = React.useCallback(
(option: ValueOptions, value: ValueOptions) => getOptionValue(option) === getOptionValue(value),
[getOptionValue],
);

const resolvedValueOptions = React.useMemo(() => {
if (!resolvedColumn?.valueOptions) {
return [];
Expand All @@ -93,8 +85,8 @@ function GridFilterInputMultipleSingleSelect(props: GridFilterInputMultipleSingl
}, [resolvedColumn]);

const resolvedFormattedValueOptions = React.useMemo(() => {
return resolvedValueOptions?.map(getValueFromOption);
}, [resolvedValueOptions]);
return resolvedValueOptions?.map(getOptionValue);
}, [resolvedValueOptions, getOptionValue]);

// The value is computed from the item.value and used directly
// If it was done by a useEffect/useState, the Autocomplete could receive incoherent value and options
Expand All @@ -104,14 +96,10 @@ function GridFilterInputMultipleSingleSelect(props: GridFilterInputMultipleSingl
}
if (resolvedValueOptions !== undefined) {
const itemValueIndexes = item.value.map((element) => {
// get the index matching between values and valueOptions
const formattedElement = getValueFromOption(element);
const index =
resolvedFormattedValueOptions?.findIndex(
(formatedOption) => formatedOption === formattedElement,
) || 0;

return index;
// Gets the index matching between values and valueOptions
return resolvedFormattedValueOptions?.findIndex(
(formatedOption) => formatedOption === element,
);
});

return itemValueIndexes
Expand All @@ -124,17 +112,17 @@ function GridFilterInputMultipleSingleSelect(props: GridFilterInputMultipleSingl
React.useEffect(() => {
if (!Array.isArray(item.value) || filteredValues.length !== item.value.length) {
// Updates the state if the filter value has been cleaned by the component
applyValue({ ...item, value: filteredValues.map(getValueFromOption) });
applyValue({ ...item, value: filteredValues.map(getOptionValue) });
}
}, [item, filteredValues, applyValue]);
}, [item, filteredValues, applyValue, getOptionValue]);

const handleChange = React.useCallback<
NonNullable<AutocompleteProps<ValueOptions, true, false, true>['onChange']>
>(
(event, value) => {
applyValue({ ...item, value: [...value.map(getValueFromOption)] });
applyValue({ ...item, value: value.map(getOptionValue) });
},
[applyValue, item],
[applyValue, item, getOptionValue],
);

return (
Expand Down Expand Up @@ -191,11 +179,17 @@ GridFilterInputMultipleSingleSelect.propTypes = {
PropTypes.object,
]),
/**
* Used to determine the text displayed for a given value option.
* Used to determine the label displayed for a given value option.
* @param {ValueOptions} value The current value option.
* @returns {string} The text to be displayed.
*/
getOptionLabel: PropTypes.func,
/**
* Used to determine the value used for a value option.
* @param {ValueOptions} value The current value option.
* @returns {string} The value to be used.
*/
getOptionValue: PropTypes.func,
item: PropTypes.shape({
field: PropTypes.string.isRequired,
id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
Expand Down
Loading