Skip to content

Commit

Permalink
Merge pull request #252 from Dias999/feature/MultiSelect
Browse files Browse the repository at this point in the history
Feature/multi select
  • Loading branch information
andreneto97 authored Oct 11, 2024
2 parents c6752cd + c62be6a commit 69ca80f
Show file tree
Hide file tree
Showing 8 changed files with 346 additions and 7 deletions.
41 changes: 38 additions & 3 deletions packages/react-material-ui/src/components/Filter/Filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
SelectOption,
allOption,
} from '../../components/SelectField/SelectField';
import { MultiSelect } from '../../components/MultiSelect';
import { SearchFieldProps } from '../../components/SearchField/SearchField';
import { OrderableDropDown, ListItem } from '../OrderableDropDown';
import { DatePickerProps } from '@mui/x-date-pickers';
Expand All @@ -19,7 +20,12 @@ import DatePickerField from '../../components/DatePickerField';
/**
* Type of filter variants available.
*/
export type FilterVariant = 'text' | 'autocomplete' | 'select' | 'date';
export type FilterVariant =
| 'text'
| 'autocomplete'
| 'select'
| 'multiSelect'
| 'date';

/**
* Common properties for all filters.
Expand Down Expand Up @@ -86,13 +92,27 @@ type SelectFilter = {
} & FilterCommon;

/**
* Type for filter properties that can be text, date, autocomplete, or select.
* Properties for the multiSelect filter.
*/
type MultiSelectFilter = {
type: 'multiSelect';
options: SelectOption[];
multiple?: boolean;
defaultValue?: string[];
size?: SelectFieldProps['size'];
onChange: (value: string[] | null) => void;
value?: string[] | null;
} & FilterCommon;

/**
* Type for filter properties that can be text, date, autocomplete, select or MultiSelect.
*/
export type FilterType =
| TextFilter
| DateFilter
| AutocompleteFilter
| SelectFilter;
| SelectFilter
| MultiSelectFilter;

/**
* Renders the appropriate component based on the filter type.
Expand Down Expand Up @@ -151,6 +171,21 @@ const renderComponent = (filter: FilterType) => {
/>
);

case 'multiSelect':
return (
<MultiSelect
fullWidth
size={filter.size ?? 'small'}
label={filter.label}
isLoading={filter.isLoading}
options={filter.options}
defaultValue={filter.defaultValue}
onChange={filter.onChange}
value={filter.value}
variant="outlined"
/>
);

case 'text':
return (
<SearchField
Expand Down
211 changes: 211 additions & 0 deletions packages/react-material-ui/src/components/MultiSelect/MultiSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import React from 'react';
import {
Box,
Checkbox,
Chip,
FormControl,
InputLabel,
ListItemText,
MenuItem,
Select,
SelectChangeEvent,
SelectProps,
} from '@mui/material';

import { FormFieldSkeleton } from '../FormFieldSkeleton';
import { FormLabel } from '../FormLabel';

import { TextProps } from 'interfaces';

/**
* Default option representing "All" in the select field.
*/
export const allOption: SelectOption = {
value: 'all',
label: 'All',
};

/**
* Option type used in the MultiSelect component.
*/
export type SelectOption = {
/** The value of the option */
value: string;
/** The label to display for the option */
label: string;
};

/**
* MultiSelect component props.
*/
export type MultiSelectProps = {
/** Array of options to display in the select dropdown */
options: SelectOption[];
/** The default selected value */
defaultValue?: string[];
/** Whether to include the "All" option in the dropdown. True by default. */
hasAllOption?: boolean;
/** Whether the component is in a loading state */
isLoading?: boolean;
/** Callback function triggered when the selected value changes */
onChange: (value: string[]) => void;
/** Wheter to show the default input display or the chips style */
displayVariant?: 'default' | 'chips';
/** Wheter to display the default shrinking label or Rockets style, above the field */
labelVariant?: 'default' | 'rockets';
/** Additional properties to pass to the `Text` component used for rendering the label */
labelProps?: TextProps;
} & Omit<SelectProps<string[]>, 'onChange'>;

export const MultiSelect = ({
options = [],
defaultValue,
hasAllOption = true,
isLoading = false,
label,
placeholder,
onChange,
fullWidth,
size,
variant = 'outlined',
value,
required,
displayVariant = 'default',
labelVariant = 'default',
name,
labelProps,
...rest
}: MultiSelectProps) => {
const isChips = displayVariant === 'chips';

const handleChange = (event: SelectChangeEvent<string[]>) => {
const {
target: { value },
} = event;
// On autofill we get a stringified value.
const values = typeof value === 'string' ? value.split(',') : value;

if (values.includes(allOption.value) && hasAllOption) {
return onChange([]);
}

onChange(values);
};

const removeValue = (id: string) => {
const valueIndex = value?.indexOf(id);
if (!value || typeof valueIndex !== 'number' || valueIndex === -1) return;
const newValue = [...value];
newValue.splice(valueIndex, 1);
onChange(newValue);
};

const finalOptions = [...(hasAllOption ? [allOption] : []), ...options];

const renderValues = (selected?: string[]) => {
const valuesIds: string[] = (selected as string[]) || value || [];

const valueLabels = valuesIds.map(
(selectedItem: string) =>
options?.find((item) => item.value === selectedItem)?.label,
);

if (isChips) {
return valueLabels.map((label, index) => (
<Chip
key={label}
label={label}
className="Rockets-MultiSelect-Chip"
onDelete={() => removeValue(valuesIds[index])}
sx={{
borderRadius: '6px',
mt: 1,
'&:not(:last-child)': { mr: 1 },
}}
/>
));
}

return valueLabels.join(', ');
};

const renderInputValue = (selected: string[]) => {
if (isChips) {
return placeholder || label;
}

if (selected?.length === 0 && hasAllOption) return allOption.label;

return renderValues(selected);
};

const labelId = `label-${name}`;
return (
<FormFieldSkeleton isLoading={isLoading} hideLabel>
<FormControl fullWidth={fullWidth} size={size}>
{labelVariant === 'default' && (
<InputLabel
id={labelId}
shrink={hasAllOption || !!value?.length}
className="Rockets-MultiSelect-InputLabel"
htmlFor={name}
>
{label}
</InputLabel>
)}

{labelVariant === 'rockets' && label && typeof label === 'string' && (
<FormLabel
id={labelId}
name={name}
label={label}
required={required}
labelProps={labelProps}
/>
)}

<Select
labelId={labelId}
className="Rockets-MultiSelect"
defaultValue={defaultValue}
onChange={handleChange}
label={labelVariant === 'rockets' ? '' : label}
fullWidth={fullWidth}
size={size}
variant={variant}
value={value}
multiple
renderValue={renderInputValue}
displayEmpty={hasAllOption || labelVariant === 'rockets'}
name={name}
required={required}
hiddenLabel={labelVariant === 'rockets'}
{...rest}
>
{finalOptions?.map((opt) => {
const { value: val, label } = opt;
const checked = value?.includes(val);

return (
<MenuItem
key={val}
value={val}
className="Rockets-MultiSelect-MenuItem"
>
<Checkbox
checked={checked}
className="Rockets-MultiSelect-MenuItem-Checkbox"
/>
<ListItemText
primary={label}
className="Rockets-MultiSelect-ListItemText"
/>
</MenuItem>
);
})}
</Select>
{isChips && <Box>{renderValues()}</Box>}
</FormControl>
</FormFieldSkeleton>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { MultiSelect, MultiSelectProps } from './MultiSelect';
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,16 @@ type Operator =
| 'ends'
| 'cont'
| 'excl'
| 'in'
| 'notin'
| 'eqL'
| 'neL'
| 'startsL'
| 'endsL'
| 'contL'
| 'exclL';
| 'exclL'
| 'inL'
| 'notinL';

export type FilterDetails = {
type: FilterVariant;
Expand Down Expand Up @@ -82,6 +86,8 @@ const FilterSubmodule = (props: Props) => {
if (!filter.operator) return acc;
if (typeof value === 'undefined') return acc;

const emptyArray = Array.isArray(value) && value.length === 0;

const data =
format === 'simpleFilter'
? `||$${filter.operator}||${value}`
Expand All @@ -90,7 +96,9 @@ const FilterSubmodule = (props: Props) => {
return {
...acc,
[filter.id]:
value === null || value === 'all' || value === '' ? null : data,
value === null || value === 'all' || value === '' || emptyArray
? null
: data,
};
}, {});

Expand Down Expand Up @@ -122,7 +130,7 @@ const FilterSubmodule = (props: Props) => {

const onFilterChange = (
id: string,
value: string | Date | null,
value: string | string[] | Date | null,
updateFilter?: boolean,
) => {
setFilterValues((prv) => {
Expand Down Expand Up @@ -202,6 +210,15 @@ const FilterSubmodule = (props: Props) => {
onChange: (val: string | null) => onFilterChange(id, val, true),
};

case 'multiSelect':
return {
...commonFields,
type,
options,
value: (value as unknown as string[]) || [],
onChange: (val: string[] | null) => onFilterChange(id, val, true),
};

case 'date':
return {
...commonFields,
Expand Down
1 change: 1 addition & 0 deletions packages/react-material-ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export { TextField, TextFieldProps } from './components/TextField';
export { default as SearchField } from './components/SearchField';
export { default as AutocompleteField } from './components/AutocompleteField';
export { SelectField, SelectFieldProps } from './components/SelectField';
export { MultiSelect, MultiSelectProps } from './components/MultiSelect';
export { default as SimpleForm } from './components/SimpleForm';
export {
Filter,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { UseTableResult } from '../../components/Table/useTable';
import { Search, SimpleFilter } from '../../components/Table/types';
import { FilterDetails } from '../../components/submodules/Filter';

export type FilterValues = Record<string, string | Date | null>;
export type FilterValues = Record<string, string | string[] | Date | null>;

export type CrudContextProps = {
/**
Expand Down
Loading

0 comments on commit 69ca80f

Please sign in to comment.