@react-awesome-query-builder/mui
Version:
User-friendly query builder for React. MUI 5 widgets
280 lines (261 loc) • 8.56 kB
JSX
import React, {useCallback} from "react";
import omit from "lodash/omit";
import TextField from "@mui/material/TextField";
import FormControl from "@mui/material/FormControl";
import Autocomplete, { createFilterOptions } from "@mui/material/Autocomplete";
import CircularProgress from "@mui/material/CircularProgress";
import Chip from "@mui/material/Chip";
import MenuItem from "@mui/material/MenuItem";
import Check from "@mui/icons-material/Check";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import Tooltip from "@mui/material/Tooltip";
import { Hooks } from "@react-awesome-query-builder/ui";
import { useTheme } from "@mui/material/styles";
const { useListValuesAutocomplete } = Hooks;
const emptyArray = [];
// tip: option can contain `group: {label, title}` intead of `groupTitle`
// but it's internal format, made for field autocomplete
// see `JSON.stringify(option.group)` and `JSON.parse(groupMaybeJson)`
export default (props) => {
const {
allowCustomValues, multiple, disableClearable,
value: selectedValue, customProps, readonly, config, filterOptionsConfig, errorText,
tooltipText, isFieldAutocomplete, dontFixOptionsOrder,
} = props;
const stringifyOption = useCallback((option) => {
const keysForFilter = config.settings.listKeysForSearch;
const valueForFilter = keysForFilter
.map(k => (typeof option[k] == "string" ? option[k] : ""))
.join("\0");
return valueForFilter;
}, [config]);
const defaultFilterOptionsConfig = {
stringify: stringifyOption
};
const filterOptionsFn = createFilterOptions(filterOptionsConfig || defaultFilterOptionsConfig);
// hook
const {
open,
onOpen,
onClose,
onChange,
onInputChange,
inputValue,
options,
isInitialLoading,
isLoading,
aPlaceholder,
extendOptions,
getOptionSelected,
getOptionDisabled,
getOptionIsCustom,
getOptionLabel,
selectedListValue,
} = useListValuesAutocomplete(props, {
debounceTimeout: 100,
multiple,
uif: "mui",
isFieldAutocomplete,
dontFixOptionsOrder,
});
// settings
const {defaultSelectWidth, defaultSearchWidth, renderSize} = config.settings;
const {width, ...rest} = customProps || {};
let customInputProps = rest.input || {};
const inputWidth = customInputProps.width || defaultSearchWidth; // todo: use as min-width for Autocomplete comp
customInputProps = omit(customInputProps, ["width"]);
const customAutocompleteProps = omit(rest, ["showSearch", "showCheckboxes"]);
const fullWidth = false;
const minWidth = width || defaultSelectWidth;
const style = {
width: (multiple ? undefined : minWidth),
minWidth: minWidth
};
const placeholder = !readonly ? aPlaceholder : "";
// For accessibility, always give the input field an aria-label
const ariaLabel = placeholder || config.settings.fieldPlaceholder;
const hasValue = selectedValue != null;
// should be simple value to prevent re-render!s
const value = hasValue ? selectedValue : (multiple ? emptyArray : null);
const filterOptions = (options, params) => {
const filtered = filterOptionsFn(options, params);
const extended = extendOptions(filtered);
return extended;
};
const groupBy = (option) => option?.group ? JSON.stringify(option.group) : option?.groupTitle;
const theme = useTheme();
// render
const renderInput = (params) => {
// parity with Antd
const shouldRenderSelected = !multiple && !open;
const selectedTitle = selectedListValue?.title ?? value?.toString() ?? "";
const shouldHide = multiple && !open;
const renderValue = shouldRenderSelected ? selectedTitle : (shouldHide ? "" : inputValue ?? value?.toString() ?? "");
return (
<TextField
variant="standard"
{...params}
inputProps={{
"aria-label": ariaLabel,
...params.inputProps,
value: renderValue,
}}
InputProps={{
...params.InputProps,
readOnly: readonly,
endAdornment: (
<React.Fragment>
{isLoading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</React.Fragment>
),
}}
size={renderSize}
disabled={readonly}
placeholder={placeholder}
error={!!errorText}
//onChange={onInputChange}
{...customInputProps}
/>
);
};
const GroupHeader = ({group}) => {
let groupLabel = group.label;
if (groupLabel && group.tooltip) {
groupLabel = (
<Tooltip title={group.tooltip} placement="left-start"><span>{groupLabel}</span></Tooltip>
);
}
let res = (
<div style={{
position: "sticky",
top: "-8px",
padding: "4px 10px",
color: theme.palette.primary.main,
backgroundColor: theme.palette.background.default,
}}>
{groupLabel}
</div>
);
return res;
};
const GroupItems = ({children}) => {
return <>{children}</>;
};
const renderGroup = (params) => {
const groupMaybeJson = params.group;
let group;
if (typeof groupMaybeJson === "string" && groupMaybeJson[0] === "{") {
try {
group = JSON.parse(groupMaybeJson);
} catch (_) {
// ignore
}
} else if (groupMaybeJson) {
group = {
label: groupMaybeJson,
};
}
const groups = group ? (group.parentGroups ?? [group]) : [];
let res = (
<div key={params.key}>
{groups.map((gr) => (<GroupHeader key={gr?.path} group={gr} />))}
<GroupItems>{params.children}</GroupItems>
</div>
);
return res;
};
const renderTags = (value, getTagProps) => value.map((option, index) => {
return <Chip
key={option.value}
label={getOptionLabel(option)}
size={"small"}
variant={getOptionIsCustom(option) ? "outlined" : "filled"}
{...getTagProps({ index })}
/>;
});
const isOptionEqualToValue = (option, value) => {
return option?.value == value;
};
const renderOption = (props, option) => {
const { title, renderTitle, value, isHidden, tooltip, group, groupTitle } = option;
const isGrouped = groupTitle || group;
const isSelected = multiple ? (selectedValue || []).includes(value) : selectedValue == value;
const className = getOptionIsCustom(option) ? "customSelectOption" : undefined;
const prefix = !isFieldAutocomplete && isGrouped ? "\u00A0\u00A0" : "";
const finalTitle = (renderTitle || prefix + title);
let titleSpan = (
<span className={className}>
{finalTitle}
</span>
);
if (tooltip) {
titleSpan = (
<Tooltip title={tooltip} placement="left-start">{titleSpan}</Tooltip>
);
}
if (isHidden)
return null;
if (option.specialValue) {
return <div {...props}>{finalTitle}</div>;
} else if (multiple) {
const itemContent = isSelected ? (
<><ListItemIcon><Check /></ListItemIcon>{titleSpan}</>
) : (
<ListItemText inset>{titleSpan}</ListItemText>
);
return (
<MenuItem
{...props}
size={"small"}
selected={isSelected}
>{itemContent}</MenuItem>
);
} else {
return <div {...props}>{titleSpan}</div>;
}
};
let res = (
<Autocomplete
disableClearable={disableClearable}
disableCloseOnSelect={multiple}
fullWidth={fullWidth}
multiple={multiple}
style={style}
freeSolo={allowCustomValues}
loading={isInitialLoading}
open={open}
onOpen={onOpen}
onClose={onClose}
inputValue={inputValue}
onInputChange={onInputChange}
label={placeholder}
onChange={onChange}
value={value}
disabled={readonly}
readOnly={readonly}
options={options}
groupBy={groupBy}
getOptionLabel={getOptionLabel}
getOptionDisabled={getOptionDisabled}
renderInput={renderInput}
renderGroup={renderGroup}
renderTags={renderTags}
renderOption={renderOption}
filterOptions={filterOptions}
isOptionEqualToValue={isOptionEqualToValue}
size={renderSize}
{...customAutocompleteProps}
/>
);
if (tooltipText) {
res = (
<Tooltip title={!open ? tooltipText : null} placement="top-start">{res}</Tooltip>
);
}
res = (
<FormControl fullWidth={fullWidth}>{res}</FormControl>
);
return res;
};