diff --git a/packages/pl-fe/package.json b/packages/pl-fe/package.json index 98e00ed82..0cf3b4f19 100644 --- a/packages/pl-fe/package.json +++ b/packages/pl-fe/package.json @@ -103,7 +103,6 @@ "localforage": "^1.10.0", "lodash": "^4.17.21", "mini-css-extract-plugin": "^2.9.2", - "multiselect-react-dropdown": "^2.0.25", "mutative": "^1.1.0", "path-browserify": "^1.0.1", "pl-api": "workspace:*", diff --git a/packages/pl-fe/src/components/ui/multiselect.tsx b/packages/pl-fe/src/components/ui/multiselect.tsx index 76481d7f6..f90e07689 100644 --- a/packages/pl-fe/src/components/ui/multiselect.tsx +++ b/packages/pl-fe/src/components/ui/multiselect.tsx @@ -25,10 +25,9 @@ THE SOFTWARE. /* eslint-disable jsdoc/require-jsdoc */ // @ts-nocheck +import clsx from 'clsx'; import React, { useRef, useEffect } from 'react'; -// import './styles.css'; - import Icon from './icon'; function useOutsideAlerter(ref, clickEvent) { @@ -56,38 +55,20 @@ const OutsideAlerter = (props) => { }; interface IMultiselectProps { - options: any; - disablePreSelectedValues?: boolean; - selectedValues?: any; - isObject?: boolean; - displayValue?: string; - showCheckbox?: boolean; - selectionLimit?: any; - placeholder?: string; - groupBy?: string; - loading?: boolean; - style?: object; - emptyRecordMsg?: string; - onSelect?: (selectedList:any, selectedItem: any) => void; - onRemove?: (selectedList:any, selectedItem: any) => void; - onSearch?: (value:string) => void; - onKeyPressFn?: (event:any, value:string) => void; - closeIcon?: string; - singleSelect?: boolean; - caseSensitiveSearch?: boolean; - id?: string; - closeOnSelect?: boolean; - avoidHighlightFirstOption?: boolean; - hidePlaceholder?: boolean; - showArrow?: boolean; - keepSearchTerm?: boolean; - customCloseIcon?: React.ReactNode | string; - customArrow?: any; - disable?: boolean; - className?: string; - selectedValueDecorator?: (v:string, option: any) => React.ReactNode | string; - optionValueDecorator?: (v:string, option: any) => React.ReactNode | string; - hideSelectedList?: boolean; + options: any; + selectedValues?: any; + displayValue?: string; + placeholder?: string; + loading?: boolean; + emptyRecordMsg?: string; + onSelect?: (selectedList:any, selectedItem: any) => void; + onRemove?: (selectedList:any, selectedItem: any) => void; + onSearch?: (value:string) => void; + onKeyPressFn?: (event:any, value:string) => void; + id?: string; + name?: string; + disabled?: boolean; + className?: string; } export class Multiselect extends React.Component { @@ -102,12 +83,8 @@ export class Multiselect extends React.Component { filteredOptions: props.options, unfilteredOptions: props.options, selectedValues: Object.assign([], props.selectedValues), - preSelectedValues: Object.assign([], props.selectedValues), toggleOptionsList: false, - highlightOption: props.avoidHighlightFirstOption ? -1 : 0, - showCheckbox: props.showCheckbox, - keepSearchTerm: props.keepSearchTerm, - groupedObject: [], + highlightOption: 0, }; // @ts-ignore this.optionTimeout = null; @@ -122,61 +99,19 @@ export class Multiselect extends React.Component { this.renderMultiselectContainer = this.renderMultiselectContainer.bind(this); this.renderSelectedList = this.renderSelectedList.bind(this); this.onRemoveSelectedItem = this.onRemoveSelectedItem.bind(this); - this.toggelOptionList = this.toggelOptionList.bind(this); + this.toggleOptionList = this.toggleOptionList.bind(this); this.onArrowKeyNavigation = this.onArrowKeyNavigation.bind(this); this.onSelectItem = this.onSelectItem.bind(this); this.filterOptionsByInput = this.filterOptionsByInput.bind(this); this.removeSelectedValuesFromOptions = this.removeSelectedValuesFromOptions.bind(this); this.isSelectedValue = this.isSelectedValue.bind(this); - this.fadeOutSelection = this.fadeOutSelection.bind(this); - this.isDisablePreSelectedValues = this.isDisablePreSelectedValues.bind(this); - this.renderGroupByOptions = this.renderGroupByOptions.bind(this); - this.renderNormalOption = this.renderNormalOption.bind(this); + this.renderOption = this.renderOption.bind(this); this.listenerCallback = this.listenerCallback.bind(this); - this.resetSelectedValues = this.resetSelectedValues.bind(this); - this.getSelectedItems = this.getSelectedItems.bind(this); - this.getSelectedItemsCount = this.getSelectedItemsCount.bind(this); - this.hideOnClickOutside = this.hideOnClickOutside.bind(this); this.onCloseOptionList = this.onCloseOptionList.bind(this); - this.isVisible = this.isVisible.bind(this); } initialSetValue() { - const { showCheckbox, groupBy, singleSelect } = this.props; - const { options } = this.state; - if (!showCheckbox && !singleSelect) { - this.removeSelectedValuesFromOptions(false); - } - // if (singleSelect) { - // this.hideOnClickOutside(); - // } - if (groupBy) { - this.groupByOptions(options); - } - } - - resetSelectedValues() { - const { unfilteredOptions } = this.state; - return new Promise((resolve) => { - this.setState({ - selectedValues: [], - preSelectedValues: [], - options: unfilteredOptions, - filteredOptions: unfilteredOptions, - }, () => { - // @ts-ignore - resolve(); - this.initialSetValue(); - }); - }); - } - - getSelectedItems() { - return this.state.selectedValues; - } - - getSelectedItemsCount() { - return this.state.selectedValues.length; + this.removeSelectedValuesFromOptions(false); } componentDidMount() { @@ -192,7 +127,7 @@ export class Multiselect extends React.Component { this.setState({ options, filteredOptions: options, unfilteredOptions: options }, this.initialSetValue); } if (JSON.stringify(prevSelectedvalues) !== JSON.stringify(selectedValues)) { - this.setState({ selectedValues: Object.assign([], selectedValues), preSelectedValues: Object.assign([], selectedValues) }, this.initialSetValue); + this.setState({ selectedValues: Object.assign([], selectedValues) }, this.initialSetValue); } } @@ -213,51 +148,23 @@ export class Multiselect extends React.Component { // Skipcheck flag - value will be true when the func called from on deselect anything. removeSelectedValuesFromOptions(skipCheck) { - const { isObject, displayValue, groupBy } = this.props; - const { selectedValues = [], unfilteredOptions, options } = this.state; - if (!skipCheck && groupBy) { - this.groupByOptions(options); - } + const { displayValue } = this.props; + const { selectedValues = [], unfilteredOptions } = this.state; if (!selectedValues.length && !skipCheck) { return; } - if (isObject) { - const optionList = unfilteredOptions.filter(item => { - return selectedValues.findIndex( - v => v[displayValue] === item[displayValue], - ) === -1 - ? true - : false; - }); - if (groupBy) { - this.groupByOptions(optionList); - } - this.setState( - { options: optionList, filteredOptions: optionList }, - this.filterOptionsByInput, - ); - return; - } - const optionList = unfilteredOptions.filter( - item => selectedValues.indexOf(item) === -1, - ); - + const optionList = unfilteredOptions.filter(item => { + return selectedValues.findIndex( + v => v[displayValue] === item[displayValue], + ) === -1 + ? true + : false; + }); this.setState( { options: optionList, filteredOptions: optionList }, this.filterOptionsByInput, ); - } - - groupByOptions(options) { - const { groupBy } = this.props; - const groupedObject = options.reduce(function(r, a) { - const key = a[groupBy] || 'Others'; - r[key] = r[key] || []; - r[key].push(a); - return r; - }, Object.create({})); - - this.setState({ groupedObject }); + return; } onChange(event) { @@ -281,20 +188,12 @@ export class Multiselect extends React.Component { filterOptionsByInput() { let { options } = this.state; const { filteredOptions, inputValue } = this.state; - const { isObject, displayValue } = this.props; - if (isObject) { - options = filteredOptions.filter(i => this.matchValues(i[displayValue], inputValue)); - } else { - options = filteredOptions.filter(i => this.matchValues(i, inputValue)); - } - this.groupByOptions(options); + const { displayValue } = this.props; + options = filteredOptions.filter(i => this.matchValues(i[displayValue], inputValue)); this.setState({ options }); } matchValues(value, search) { - if (this.props.caseSensitiveSearch) { - return value.indexOf(search) > -1; - } if (value.toLowerCase) { return value.toLowerCase().indexOf(search.toLowerCase()) > -1; } @@ -309,8 +208,7 @@ export class Multiselect extends React.Component { inputValue, selectedValues, } = this.state; - const { disablePreSelectedValues } = this.props; - if (e.keyCode === 8 && !inputValue && !disablePreSelectedValues && selectedValues.length) { + if (e.keyCode === 8 && !inputValue && selectedValues.length) { this.onRemoveSelectedItem(selectedValues.length - 1); } if (!options.length) { @@ -350,220 +248,103 @@ export class Multiselect extends React.Component { onRemoveSelectedItem(item) { const { selectedValues } = this.state; let { index = 0 } = this.state; - const { onRemove, showCheckbox, displayValue, isObject } = this.props; - if (isObject) { - index = selectedValues.findIndex( - i => i[displayValue] === item[displayValue], - ); - } else { - index = selectedValues.indexOf(item); - } + const { onRemove, displayValue } = this.props; + index = selectedValues.findIndex( + i => i[displayValue] === item[displayValue], + ); selectedValues.splice(index, 1); onRemove(selectedValues, item); this.setState({ selectedValues }, () => { - if (!showCheckbox) { - this.removeSelectedValuesFromOptions(true); - } + this.removeSelectedValuesFromOptions(true); }); - if (!this.props.closeOnSelect) { - // @ts-ignore - this.searchBox.current.focus(); - } } onSelectItem(item) { const { selectedValues } = this.state; - const { selectionLimit, onSelect, singleSelect, showCheckbox } = this.props; - if (!this.state.keepSearchTerm){ - this.setState({ - inputValue: '', - }); - } - if (singleSelect) { - this.onSingleSelect(item); - onSelect([item], item); - return; - } + const { onSelect } = this.props; + this.setState({ + inputValue: '', + }); if (this.isSelectedValue(item)) { this.onRemoveSelectedItem(item); return; } - if (selectionLimit === selectedValues.length) { - return; - } selectedValues.push(item); onSelect(selectedValues, item); this.setState({ selectedValues }, () => { - if (!showCheckbox) { - this.removeSelectedValuesFromOptions(true); - } else { - this.filterOptionsByInput(); - } + this.removeSelectedValuesFromOptions(true); }); - if (!this.props.closeOnSelect) { - // @ts-ignore - this.searchBox.current.focus(); - } - } - - onSingleSelect(item) { - this.setState({ selectedValues: [item], toggleOptionsList: false }); } isSelectedValue(item) { - const { isObject, displayValue } = this.props; + const { displayValue } = this.props; const { selectedValues } = this.state; - if (isObject) { - return ( - selectedValues.filter(i => i[displayValue] === item[displayValue]) - .length > 0 - ); - } - return selectedValues.filter(i => i === item).length > 0; + return ( + selectedValues.filter(i => i[displayValue] === item[displayValue]) + .length > 0 + ); } renderOptionList() { - const { groupBy, style, emptyRecordMsg, loading, loadingMessage = 'loading...' } = this.props; + const { emptyRecordMsg, loading, loadingMessage = 'loading...' } = this.props; const { options } = this.state; if (loading) { return ( -
    - {typeof loadingMessage === 'string' && {loadingMessage}} +
      + {typeof loadingMessage === 'string' && {loadingMessage}} {typeof loadingMessage !== 'string' && loadingMessage}
    ); } return ( -
      - {options.length === 0 && {emptyRecordMsg}} - {!groupBy ? this.renderNormalOption() : this.renderGroupByOptions()} +
        + {options.length === 0 && {emptyRecordMsg}} + {this.renderOption()}
      ); } - renderGroupByOptions() { - const { isObject = false, displayValue, showCheckbox, style, singleSelect } = this.props; - const { groupedObject } = this.state; - return Object.keys(groupedObject).map(obj => { - return ( - -
    • {obj}
    • - {groupedObject[obj].map((option, i) => { - const isSelected = this.isSelectedValue(option); - return ( -
    • this.onSelectItem(option)} - > - {showCheckbox && !singleSelect && ( - - )} - {this.props.optionValueDecorator(isObject ? option[displayValue] : (option || '').toString(), option)} -
    • - ); - }, - )} -
      - ); - }); - } - - renderNormalOption() { - const { isObject = false, displayValue, showCheckbox, style, singleSelect } = this.props; + renderOption() { + const { displayValue } = this.props; const { highlightOption } = this.state; return this.state.options.map((option, i) => { const isSelected = this.isSelectedValue(option); return (
    • this.onSelectItem(option)} > - {showCheckbox && !singleSelect && ( - - )} - {this.props.optionValueDecorator(isObject ? option[displayValue] : (option || '').toString(), option)} + {option[displayValue]}
    • ); }); } renderSelectedList() { - const { isObject = false, displayValue, style, singleSelect } = this.props; + const { displayValue } = this.props; const { selectedValues } = this.state; return selectedValues.map((value, index) => ( - - {this.props.selectedValueDecorator(!isObject ? (value || '').toString() : value[displayValue], value)} - {!this.isDisablePreSelectedValues(value) && ( this.onRemoveSelectedItem(value)}> + + {value[displayValue]} + )); } - isDisablePreSelectedValues(value) { - const { isObject, disablePreSelectedValues, displayValue } = this.props; - const { preSelectedValues } = this.state; - if (!disablePreSelectedValues || !preSelectedValues.length) { - return false; - } - if (isObject) { - return ( - preSelectedValues.filter(i => i[displayValue] === value[displayValue]) - .length > 0 - ); - } - return preSelectedValues.filter(i => i === value).length > 0; - } - - fadeOutSelection(item) { - const { selectionLimit, showCheckbox, singleSelect } = this.props; - if (singleSelect) { - return; - } - const { selectedValues } = this.state; - if (selectionLimit === -1) { - return false; - } - if (selectionLimit !== selectedValues.length) { - return false; - } - if (selectionLimit === selectedValues.length) { - if (!showCheckbox) { - return true; - } else { - if (this.isSelectedValue(item)) { - return false; - } - return true; - } - } - } - - toggelOptionList() { + toggleOptionList() { this.setState({ toggleOptionsList: !this.state.toggleOptionsList, - highlightOption: this.props.avoidHighlightFirstOption ? -1 : 0, + highlightOption: 0, }); } onCloseOptionList() { this.setState({ toggleOptionsList: false, - highlightOption: this.props.avoidHighlightFirstOption ? -1 : 0, + highlightOption: 0, inputValue: '', }); } @@ -573,7 +354,7 @@ export class Multiselect extends React.Component { // @ts-ignore clearTimeout(this.optionTimeout); } else { - this.toggelOptionList(); + this.toggleOptionList(); } } @@ -583,53 +364,31 @@ export class Multiselect extends React.Component { this.optionTimeout = setTimeout(this.onCloseOptionList, 250); } - isVisible(elem) { - return !!elem && !!(elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length); - } - - hideOnClickOutside() { - const element = document.getElementsByClassName('multiselect-container')[0]; - const outsideClickListener = event => { - if (element && !element.contains(event.target) && this.isVisible(element)) { - this.toggelOptionList(); - } - }; - document.addEventListener('click', outsideClickListener); - } - renderMultiselectContainer() { - const { inputValue, toggleOptionsList, selectedValues } = this.state; - const { placeholder, style, singleSelect, id, name, hidePlaceholder, disable, showArrow, className, customArrow, hideSelectedList } = this.props; + const { inputValue, toggleOptionsList } = this.state; + const { placeholder, id, name, disabled, className } = this.props; return ( -
      +
      {}} + className='searchWrapper' + ref={this.searchWrapper} > - {!hideSelectedList && this.renderSelectedList()} + {this.renderSelectedList()} - {(singleSelect || showArrow) && ( - <> - {customArrow ? {customArrow} : } - - )}
      { Multiselect.defaultProps = { options: [], - disablePreSelectedValues: false, selectedValues: [], - isObject: true, displayValue: 'model', - showCheckbox: false, - selectionLimit: -1, placeholder: 'Select', - groupBy: '', - style: {}, emptyRecordMsg: 'No Options Available', onSelect: () => {}, onRemove: () => {}, onKeyPressFn: () => {}, - closeIcon: 'circle2', - singleSelect: false, - caseSensitiveSearch: false, id: '', name: '', - closeOnSelect: true, - avoidHighlightFirstOption: false, - hidePlaceholder: false, - showArrow: false, - keepSearchTerm: false, - customCloseIcon: '', + disabled: false, className: '', - customArrow: undefined, - selectedValueDecorator: v => v, - optionValueDecorator: v => v, } as IMultiselectProps; diff --git a/packages/pl-fe/src/features/forms/index.tsx b/packages/pl-fe/src/features/forms/index.tsx index 0ff459d7b..bda5419f1 100644 --- a/packages/pl-fe/src/features/forms/index.tsx +++ b/packages/pl-fe/src/features/forms/index.tsx @@ -2,7 +2,6 @@ import clsx from 'clsx'; import React, { useMemo, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import Icon from 'pl-fe/components/ui/icon'; import { Multiselect } from 'pl-fe/components/ui/multiselect'; import Select from 'pl-fe/components/ui/select'; @@ -153,8 +152,7 @@ const Mutliselect: React.FC = (props) => { onSelect={handleChange} onRemove={handleChange} displayValue='value' - disable={disabled} - customCloseIcon={} + disabled={disabled} placeholder={intl.formatMessage(messages.selectPlaceholder)} emptyRecordMsg={intl.formatMessage(messages.selectNoOptions)} /> diff --git a/packages/pl-fe/src/styles/forms.scss b/packages/pl-fe/src/styles/forms.scss index 112e96ef7..af256c9cf 100644 --- a/packages/pl-fe/src/styles/forms.scss +++ b/packages/pl-fe/src/styles/forms.scss @@ -11,12 +11,50 @@ select { @apply flex text-sm items-center; } -.plfe-multiselect { - .chip { - @apply bg-primary-600 my-1; +// Adapted from [multiselect-react-dropdown](https://github.com/srigar/multiselect-react-dropdown), licensed under MIT License. +.multiselect-container { + position: relative; + text-align: left; + width: 100%; + + &--disabled { + pointer-events: none; + opacity: 0.5; +} + + input { + border: none; + margin-top: 3px; + background: transparent; + + &:focus { + outline: none; + } } - .search-wrapper { + ul { + display: block; + padding: 0; + margin: 0; + border: 1px solid #ccc; + border-radius: 4px; + max-height: 250px; + overflow-y: auto; + } + + li { + padding: 10px; + + &:hover { + background: #0096fb; + color: #fff; + cursor: pointer; + } + } + + .searchWrapper { + border: 1px solid; + position: relative; @apply rounded-md border-gray-300 bg-white min-h-[38px] max-w-[400px] py-0 pl-3 pr-10 text-base focus:border-primary-500 focus:outline-none focus:ring-primary-500 disabled:opacity-50 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-100 dark:ring-1 dark:ring-gray-800 dark:focus:border-primary-500 dark:focus:ring-primary-500 sm:text-sm w-auto; > input { @@ -24,6 +62,46 @@ select { } } + .chip { + padding: 4px 10px; + margin-right: 5px; + border-radius: 11px; + display: inline-flex; + align-items: center; + font-size: 13px; + line-height: 19px; + color: #fff; + white-space: nowrap; + @apply bg-primary-600 my-1; + } + + .optionListContainer { + position: absolute; + width: 100%; + background: #fff; + border-radius: 4px; + margin-top: 1px; + z-index: 2; + } + + .highlightOption { + @apply bg-primary-600; + color: #fff; + } + + .displayBlock { + display: block; + } + + .displayNone { + display: none; + } + + .notFound { + padding: 10px; + display: block; + } + .optionContainer { @apply border-gray-300 dark:border-gray-800 dark:bg-gray-900; } @@ -31,8 +109,4 @@ select { .option { @apply hover:bg-primary-600; } - - .highlightOption { - @apply bg-primary-600; - } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b495b7aff..7cfd3406c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -310,9 +310,6 @@ importers: mini-css-extract-plugin: specifier: ^2.9.2 version: 2.9.2(webpack@5.101.0(esbuild@0.24.2)) - multiselect-react-dropdown: - specifier: ^2.0.25 - version: 2.0.25(react@18.3.1) mutative: specifier: ^1.1.0 version: 1.2.0 @@ -4880,11 +4877,6 @@ packages: muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} - multiselect-react-dropdown@2.0.25: - resolution: {integrity: sha512-z8kUSyBNOuV7vn4Dk35snzXWtIfTdSEEXhgDdLMvOmR+xJFx35vc1voUlSuOvrk3khb+hXC219Qs9szOvNm25Q==} - peerDependencies: - react: ^16.7.0 || ^17.0.0 || ^18.0.0 - mutative@1.2.0: resolution: {integrity: sha512-1muFw45Lwjso6TSBGiXfbjKS01fVSD/qaqBfTo/gXgp79e8KM4Sa1XP/S4iN2/DvSdIZgjFJI+JIhC7eKf3GTg==} engines: {node: '>=14.0'} @@ -11734,10 +11726,6 @@ snapshots: muggle-string@0.4.1: {} - multiselect-react-dropdown@2.0.25(react@18.3.1): - dependencies: - react: 18.3.1 - mutative@1.2.0: {} mz@2.7.0: