diff --git a/CHANGELOG.md b/CHANGELOG.md index a8c99a851..9c00dd863 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,6 @@ # Changelog +- 4.4.2 + - Added support of autocomplete for multiselect widget in MUI (PR #475) - 4.4.1 - feat: possibility to add custom operators for groups (PR #462) - 4.4.0 diff --git a/CONFIG.adoc b/CONFIG.adoc index 56aeb9258..beee9feb7 100644 --- a/CONFIG.adoc +++ b/CONFIG.adoc @@ -335,11 +335,14 @@ Render settings: |customFieldSelectProps |`{}` |You can pass props to `Select` field widget. Example: `{showSearch: true}` |groupActionsPosition |`topRight` |You can change the position of the group actions to the following: + `topLeft, topCenter, topRight, bottomLeft, bottomCenter, bottomRight` -|renderBeforeWidget| | -|renderAfterWidget| | -|renderBeforeActions| | -|renderAfterActions| | -|defaultSliderWidth|"200px" |Width for slider +|renderBeforeWidget | | +|renderAfterWidget | | +|renderBeforeActions | | +|renderAfterActions | | +|defaultSliderWidth |`200px` |Width for slider +|defaultSelectWidth |`200px` |Width for select +|defaultSearchWidth |`100px` |Width for search in autocomplete +|defaultMaxRows |5 | Max rows for textarea |=== Other settings: diff --git a/examples/demo/config.tsx b/examples/demo/config.tsx index a0571cdec..029d119e4 100644 --- a/examples/demo/config.tsx +++ b/examples/demo/config.tsx @@ -131,6 +131,19 @@ export default (skin: string) => { showSearch: true } }, + select: { + ...InitialConfig.widgets.select, + }, + multiselect: { + ...InitialConfig.widgets.multiselect, + customProps: { + //showCheckboxes: false, + width: "200px", + input: { + width: "100px" + } + } + }, treeselect: { ...InitialConfig.widgets.treeselect, customProps: { @@ -202,6 +215,9 @@ export default (skin: string) => { ...localeSettings, defaultSliderWidth: "200px", + defaultSelectWidth: "200px", + defaultSearchWidth: "100px", + defaultMaxRows: 5, valueSourcesInfo: { value: { @@ -454,6 +470,7 @@ export default (skin: string) => { label: "Colors", type: "multiselect", fieldSettings: { + showSearch: true, listValues: { yellow: "Yellow", green: "Green", diff --git a/modules/components/widgets/antd/value/MultiSelect.jsx b/modules/components/widgets/antd/value/MultiSelect.jsx index 69215ebc0..49851e8a1 100644 --- a/modules/components/widgets/antd/value/MultiSelect.jsx +++ b/modules/components/widgets/antd/value/MultiSelect.jsx @@ -4,6 +4,7 @@ import { Select } from "antd"; import {calcTextWidth, SELECT_WIDTH_OFFSET_RIGHT} from "../../../../utils/domUtils"; import {mapListValues} from "../../../../utils/stuff"; import {useOnPropsChanged} from "../../../../utils/reactUtils"; +import omit from "lodash/omit"; const Option = Select.Option; export default class MultiSelectWidget extends PureComponent { @@ -59,6 +60,7 @@ export default class MultiSelectWidget extends PureComponent { const aValue = value && value.length ? value : undefined; const width = aValue ? null : placeholderWidth + SELECT_WIDTH_OFFSET_RIGHT; const dropdownWidth = this.optionsMaxWidth + SELECT_WIDTH_OFFSET_RIGHT; + const customSelectProps = omit(customProps, ["showCheckboxes"]); return ( ); diff --git a/modules/components/widgets/antd/value/Select.jsx b/modules/components/widgets/antd/value/Select.jsx index 600b828dd..882b27a13 100644 --- a/modules/components/widgets/antd/value/Select.jsx +++ b/modules/components/widgets/antd/value/Select.jsx @@ -4,6 +4,7 @@ import {calcTextWidth, SELECT_WIDTH_OFFSET_RIGHT} from "../../../../utils/domUti import {mapListValues} from "../../../../utils/stuff"; import {useOnPropsChanged} from "../../../../utils/reactUtils"; import { Select } from "antd"; +import omit from "lodash/omit"; const Option = Select.Option; export default class SelectWidget extends PureComponent { @@ -55,6 +56,7 @@ export default class SelectWidget extends PureComponent { const dropdownWidth = this.optionsMaxWidth + SELECT_WIDTH_OFFSET_RIGHT; const width = value ? dropdownWidth : placeholderWidth + SELECT_WIDTH_OFFSET_RIGHT; const aValue = value != undefined ? value+"" : undefined; + const customSelectProps = omit(customProps, [""]); return ( ); diff --git a/modules/components/widgets/antd/value/TextArea.jsx b/modules/components/widgets/antd/value/TextArea.jsx index f077c3271..5f8ac77b5 100644 --- a/modules/components/widgets/antd/value/TextArea.jsx +++ b/modules/components/widgets/antd/value/TextArea.jsx @@ -2,7 +2,6 @@ import React, { PureComponent } from "react"; import PropTypes from "prop-types"; import { Input, Col } from "antd"; const { TextArea } = Input; -const defaultMaxRows = 5; export default class TextAreaWidget extends PureComponent { static propTypes = { @@ -25,7 +24,7 @@ export default class TextAreaWidget extends PureComponent { render() { const {config, placeholder, customProps, value, readonly, maxLength, maxRows, fullWidth} = this.props; - const {renderSize} = config.settings; + const {renderSize, defaultMaxRows} = config.settings; const aValue = value != undefined ? value : null; return ( diff --git a/modules/components/widgets/material/value/MaterialAutocomplete.jsx b/modules/components/widgets/material/value/MaterialAutocomplete.jsx index a44495204..f93d9d919 100644 --- a/modules/components/widgets/material/value/MaterialAutocomplete.jsx +++ b/modules/components/widgets/material/value/MaterialAutocomplete.jsx @@ -4,17 +4,24 @@ import TextField from "@material-ui/core/TextField"; import FormControl from "@material-ui/core/FormControl"; import Autocomplete, { createFilterOptions } from "@material-ui/lab/Autocomplete"; import CircularProgress from "@material-ui/core/CircularProgress"; +import Chip from "@material-ui/core/Chip"; +import Checkbox from "@material-ui/core/Checkbox"; +import { makeStyles } from "@material-ui/core/styles"; +import CheckBoxOutlineBlankIcon from "@material-ui/icons/CheckBoxOutlineBlank"; +import CheckBoxIcon from "@material-ui/icons/CheckBox"; import useListValuesAutocomplete from "../../../../hooks/useListValuesAutocomplete"; +const nonCheckedIcon = ; +const checkedIcon = ; const defaultFilterOptions = createFilterOptions(); +const emptyArray = []; export default (props) => { const { - allowCustomValues, + allowCustomValues, multiple, value: selectedValue, customProps, readonly, config } = props; - const hasValue = selectedValue != null; // hook const { @@ -33,22 +40,58 @@ export default (props) => { getOptionDisabled, getOptionLabel, } = useListValuesAutocomplete(props, { - debounceTimeout: 100 + debounceTimeout: 100, + multiple }); // setings - const {defaultSliderWidth} = config.settings; - const {width, ...rest} = customProps || {}; - const customInputProps = rest.input || {}; - const customAutocompleteProps = omit(rest.autocomplete || rest, ["showSearch"]); + const {defaultSelectWidth, defaultSearchWidth} = config.settings; + const {width, showCheckboxes, ...rest} = customProps || {}; + let customInputProps = rest.input || {}; + const inputWidth = customInputProps.width || defaultSearchWidth; + customInputProps = omit(customInputProps, ["width"]); + const customAutocompleteProps = omit(rest, ["showSearch", "showCheckboxes"]); + const fullWidth = true; + const minWidth = width || defaultSelectWidth; + const style = { + width: (multiple ? undefined : minWidth), + minWidth: minWidth + }; + const placeholder = !readonly ? aPlaceholder : ""; + 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 = defaultFilterOptions(options, params); const extended = extendOptions(filtered, params); return extended; }; - // Render + // styles + const useStyles = makeStyles((theme) => ({ + // fix too small width + input: { + minWidth: inputWidth + " !important", + } + })); + + const useStylesChip = makeStyles((theme) => ({ + // fix height + root: { + height: "auto" + }, + label: { + marginTop: "3px", + marginBottom: "3px", + } + })); + + const classesChip = useStylesChip(); + const classes = useStyles(); + + // render const renderInput = (params) => { return ( { ), }} disabled={readonly} - placeholder={!readonly ? aPlaceholder : ""} + placeholder={placeholder} //onChange={onInputChange} {...customInputProps} /> ); }; + const renderTags = (value, getTagProps) => value.map((option, index) => { + return ; + }); + + const renderOption = (option, { selected }) => { + if (multiple && showCheckboxes != false) { + return + + {option.title} + ; + } else { + return {option.title}; + } + }; + return ( - + { onClose={onClose} inputValue={inputValue} onInputChange={onInputChange} - label={!readonly ? aPlaceholder : ""} + label={placeholder} onChange={onChange} - value={hasValue ? selectedValue : null} // should be simple value to prevent re-render! + value={value} getOptionSelected={getOptionSelected} disabled={readonly} readOnly={readonly} @@ -93,6 +164,8 @@ export default (props) => { getOptionLabel={getOptionLabel} getOptionDisabled={getOptionDisabled} renderInput={renderInput} + renderTags={renderTags} + renderOption={renderOption} filterOptions={filterOptions} {...customAutocompleteProps} > diff --git a/modules/components/widgets/material/value/MaterialMultiSelect.jsx b/modules/components/widgets/material/value/MaterialMultiSelect.jsx index fb72891c8..bc0135296 100644 --- a/modules/components/widgets/material/value/MaterialMultiSelect.jsx +++ b/modules/components/widgets/material/value/MaterialMultiSelect.jsx @@ -46,7 +46,7 @@ export default ({listValues, value, setValue, allowCustomValues, readonly, place disabled={readonly} readOnly={readonly} renderValue={renderValue} - {...omit(customProps, ["showSearch"])} + {...omit(customProps, ["showSearch", "input", "showCheckboxes"])} > {renderOptions(hasValue ? value : [])} diff --git a/modules/components/widgets/material/value/MaterialSelect.jsx b/modules/components/widgets/material/value/MaterialSelect.jsx index d758b24d0..0ff95d69e 100644 --- a/modules/components/widgets/material/value/MaterialSelect.jsx +++ b/modules/components/widgets/material/value/MaterialSelect.jsx @@ -43,7 +43,7 @@ export default ({listValues, value, setValue, allowCustomValues, readonly, place disabled={readonly} readOnly={readonly} renderValue={renderValue} - {...omit(customProps, ["showSearch"])} + {...omit(customProps, ["showSearch", "input"])} > {renderOptions()} diff --git a/modules/components/widgets/material/value/MaterialTextArea.jsx b/modules/components/widgets/material/value/MaterialTextArea.jsx index e73cacb45..510ba1df1 100644 --- a/modules/components/widgets/material/value/MaterialTextArea.jsx +++ b/modules/components/widgets/material/value/MaterialTextArea.jsx @@ -1,10 +1,10 @@ import React from "react"; import TextField from "@material-ui/core/TextField"; import FormControl from "@material-ui/core/FormControl"; -const defaultMaxRows = 5; export default (props) => { const {value, setValue, config, readonly, placeholder, customProps, maxLength, maxRows, fullWidth} = props; + const {defaultMaxRows} = config.settings; const onChange = e => { let val = e.target.value; diff --git a/modules/config/basic.js b/modules/config/basic.js index 925a75de8..62e4b2131 100644 --- a/modules/config/basic.js +++ b/modules/config/basic.js @@ -1031,7 +1031,10 @@ const settings = { showSearch: true }, - defaultSliderWidth: "200px" + defaultSliderWidth: "200px", + defaultSelectWidth: "200px", + defaultSearchWidth: "100px", + defaultMaxRows: 5, }; //---------------------------- diff --git a/modules/config/material/index.js b/modules/config/material/index.js index 3c6da9a50..9b5c508c3 100644 --- a/modules/config/material/index.js +++ b/modules/config/material/index.js @@ -61,7 +61,11 @@ const widgets = { }, multiselect: { ...BasicConfig.widgets.multiselect, - factory: (props) => , + factory: (props) => { + return (props.asyncFetch || props.showSearch) + ? + : ; + }, }, select: { ...BasicConfig.widgets.select, diff --git a/modules/hooks/useListValuesAutocomplete.jsx b/modules/hooks/useListValuesAutocomplete.jsx index 2154bb922..8c228f2c1 100644 --- a/modules/hooks/useListValuesAutocomplete.jsx +++ b/modules/hooks/useListValuesAutocomplete.jsx @@ -10,7 +10,8 @@ const useListValuesAutocomplete = ({ listValues: staticListValues, allowCustomValues, value: selectedValue, setValue, placeholder }, { - debounceTimeout + debounceTimeout, + multiple }) => { const loadMoreTitle = "Load more..."; const loadingMoreTitle = "Loading more..."; @@ -45,8 +46,8 @@ const useListValuesAutocomplete = ({ const canShowLoadMore = !isLoading && canLoadMore; const options = mapListValues(listValues, listValueToOption); const hasValue = selectedValue != null; - const selectedListValue = hasValue ? getListValue(selectedValue, listValues) : null; - const selectedOption = listValueToOption(selectedListValue); + // const selectedListValue = hasValue ? getListValue(selectedValue, listValues) : null; + // const selectedOption = listValueToOption(selectedListValue); // fetch const fetchListValues = async (filter = null, isLoadMore = false) => { @@ -145,7 +146,18 @@ const useListValuesAutocomplete = ({ } else if (option && option.specialValue == "LOADING_MORE") { isSelectedLoadMore.current = true; } else { - setValue(option == null ? undefined : option.value, [option]); + if (multiple) { + let newSelectedListValues = option.map( o => + o.value != null ? o : getListValue(o, listValues) + ); + let newSelectedValues = newSelectedListValues.map(o => o.value); + if (!newSelectedValues.length) + newSelectedValues = undefined; //not allow [] + setValue(newSelectedValues, newSelectedListValues); + } else { + const v = option == null ? undefined : option.value; + setValue(v, [option]); + } } }; @@ -153,14 +165,18 @@ const useListValuesAutocomplete = ({ const val = newInputValue; //const isTypeToSearch = e.type == 'change'; - if (val === loadMoreTitle || val == loadingMoreTitle) { + if (val === loadMoreTitle || val === loadingMoreTitle) { return; } setInputValue(val); if (allowCustomValues) { - setValue(val, [val]); + if (multiple) { + //todo + } else { + setValue(val, [val]); + } } const canSearchAsync = useAsyncSearch && (forceAsyncSearch ? !!val : true); @@ -238,15 +254,15 @@ const useListValuesAutocomplete = ({ isInitialLoading, isLoading, isLoadingMore, - + extendOptions, getOptionSelected, getOptionDisabled, getOptionLabel, // unused - selectedListValue, - selectedOption, + //selectedListValue, + //selectedOption, aPlaceholder, }; }; diff --git a/modules/index.d.ts b/modules/index.d.ts index 3c7934adc..d212daa28 100644 --- a/modules/index.d.ts +++ b/modules/index.d.ts @@ -443,6 +443,7 @@ export interface SelectFieldSettings extends BasicFieldSettings { listValues?: ListValues, allowCustomValues?: boolean, showSearch?: boolean, + showCheckboxes?: boolean, asyncFetch?: AsyncFetchListValuesFn, useLoadMore?: boolean, useAsyncSearch?: boolean, @@ -619,6 +620,9 @@ export interface RenderSettings { renderAfterActions?: Factory, renderRuleError?: Factory, defaultSliderWidth?: string, + defaultSelectWidth?: string, + defaultSearchWidth?: string, + defaultMaxRows?: number, } export interface BehaviourSettings { diff --git a/package.json b/package.json index 025255865..7ca919b01 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-awesome-query-builder", - "version": "4.4.1", + "version": "4.4.2", "description": "User-friendly query builder for React. Demo: https://ukrbublik.github.io/react-awesome-query-builder", "keywords": [ "query-builder",