Merge pull request #12281 from ToolJet/feat/dropdown-option-sort

Feat: Added sorting to options in Dropdown and Multiselect
This commit is contained in:
Johnson Cherian 2025-04-09 15:25:45 +05:30 committed by GitHub
commit e3c02d48ab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 164 additions and 39 deletions

View file

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import Accordion from '@/_ui/Accordion';
import { EventManager } from '../EventManager';
import { renderElement } from '../Utils';
@ -14,6 +14,7 @@ import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import SortableList from '@/_components/SortableList';
import Trash from '@/_ui/Icon/solidIcons/Trash';
import { shallow } from 'zustand/shallow';
import { sortArray } from '@/Editor/Components/DropdownV2/utils';
export function Select({ componentMeta, darkMode, ...restProps }) {
const {
@ -27,10 +28,13 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
allComponents,
pages,
} = restProps;
const isInitialRender = useRef(true);
const getResolvedValue = useStore((state) => state.getResolvedValue, shallow);
const isMultiSelect = component?.component?.component === 'MultiselectV2';
const isDynamicOptionsEnabled = getResolvedValue(component?.component?.definition?.properties?.advanced?.value);
const isSortingEnabled = componentMeta?.properties['sort'] ?? false;
const sort = component?.component?.definition?.properties?.sort?.value;
const constructOptions = () => {
let optionsValue = component?.component?.definition?.properties?.options?.value;
@ -89,6 +93,15 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
paramUpdated({ name: 'options' }, 'value', options, 'properties', false, props);
};
const updateSortParam = (value) => {
paramUpdated({ name: 'sort' }, 'value', value, 'properties');
};
const updateOptions = (options) => {
setOptions(options);
updateAllOptionsParams(options);
};
const generateNewOptions = () => {
let found = false;
let label = '';
@ -114,8 +127,8 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
const handleAddOption = () => {
let _option = generateNewOptions();
const _items = [...options, _option];
setOptions(_items);
updateAllOptionsParams(_items);
const sortedItems = sortArray(_items, sort);
updateOptions(sortedItems);
};
const handleDeleteOption = (index) => {
@ -134,8 +147,7 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
}
return option;
});
setOptions(_options);
updateAllOptionsParams(_options);
updateOptions(_options);
};
const handleValueChange = (value, index) => {
@ -148,16 +160,17 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
}
return option;
});
setOptions(_options);
updateAllOptionsParams(_options);
updateOptions(_options);
};
const reorderOptions = async (startIndex, endIndex) => {
const result = [...options];
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
setOptions(result);
updateAllOptionsParams(result);
updateOptions(result);
if (isSortingEnabled && sort !== 'none') {
updateSortParam('none');
}
};
const onDragEnd = ({ source, destination }) => {
@ -200,9 +213,8 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
};
}
});
setOptions(_options);
updateOptions(_options);
setMarkedAsDefault(_value);
updateAllOptionsParams(_options);
}
};
@ -219,8 +231,7 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
}
return option;
});
setOptions(_options);
updateAllOptionsParams(_options);
updateOptions(_options);
};
const handleDisableChange = (value, index) => {
@ -236,8 +247,7 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
}
return option;
});
setOptions(_options);
updateAllOptionsParams(_options);
updateOptions(_options);
};
const handleOnFxPress = (active, index, key) => {
@ -253,12 +263,20 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
}
return option;
});
setOptions(_options);
updateAllOptionsParams(_options);
updateOptions(_options);
};
useEffect(() => {
setOptions(constructOptions());
if (!isInitialRender.current && isSortingEnabled) {
const sortedOptions = sortArray([...options], sort);
updateOptions(sortedOptions);
}
}, [sort]);
useEffect(() => {
const sortedOptions = sortArray(constructOptions(), sort);
updateOptions(sortedOptions);
isInitialRender.current = false;
}, [isMultiSelect, component?.id]);
const _renderOverlay = (item, index) => {
@ -385,6 +403,12 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
trigger="click"
placement="left"
rootClose
onExited={() => {
if (isSortingEnabled && sort !== 'none') {
const sortedOptions = sortArray([...options], sort);
updateOptions(sortedOptions);
}
}}
overlay={_renderOverlay(item, index)}
onToggle={(isOpen) => {
if (!isOpen) {
@ -515,6 +539,17 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
currentState,
allComponents
)}
{isSortingEnabled &&
renderElement(
component,
componentMeta,
paramUpdated,
dataQueries,
'sort',
'properties',
currentState,
allComponents
)}
</>
),
});

View file

@ -63,6 +63,18 @@ export const dropdownV2Config = {
},
accordian: 'Options',
},
sort: {
type: 'switch',
displayName: 'Sort options',
validation: { schema: { type: 'string' }, defaultValue: 'asc' },
options: [
{ displayName: 'None', value: 'none' },
{ displayName: 'a-z', value: 'asc' },
{ displayName: 'z-a', value: 'desc' },
],
accordian: 'Options',
isFxNotRequired: true,
},
loadingState: {
type: 'toggle',
displayName: 'Loading state',
@ -300,6 +312,7 @@ export const dropdownV2Config = {
},
label: { value: 'Select' },
optionsLoadingState: { value: '{{false}}' },
sort: { value: 'asc' },
placeholder: { value: 'Select an option' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },

View file

@ -130,6 +130,18 @@ export const multiselectV2Config = {
},
accordian: 'Options',
},
sort: {
type: 'switch',
displayName: 'Sort options',
validation: { schema: { type: 'string' }, defaultValue: 'asc' },
options: [
{ displayName: 'None', value: 'none' },
{ displayName: 'a-z', value: 'asc' },
{ displayName: 'z-a', value: 'desc' },
],
accordian: 'Options',
isFxNotRequired: true,
},
loadingState: {
type: 'toggle',
displayName: 'Loading state',
@ -313,6 +325,7 @@ export const multiselectV2Config = {
advanced: { value: `{{false}}` },
showAllOption: { value: '{{false}}' },
optionsLoadingState: { value: '{{false}}' },
sort: { value: 'asc' },
placeholder: { value: 'Select the options' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },

View file

@ -13,7 +13,7 @@ import CustomMenuList from './CustomMenuList';
import CustomOption from './CustomOption';
import Label from '@/_ui/Label';
import cx from 'classnames';
import { getInputBackgroundColor, getInputBorderColor, getInputFocusedColor } from './utils';
import { getInputBackgroundColor, getInputBorderColor, getInputFocusedColor, sortArray } from './utils';
import { isMobileDevice } from '@/_helpers/appUtils';
import useStore from '@/AppBuilder/_stores/store';
@ -68,6 +68,7 @@ export const DropdownV2 = ({
loadingState: dropdownLoadingState,
disabledState,
optionsLoadingState,
sort,
} = properties;
const {
selectedTextColor,
@ -116,6 +117,7 @@ export const DropdownV2 = ({
const foundItem = _schema?.find((item) => item?.default === true);
return !hasVisibleFalse(foundItem?.value) ? foundItem?.value : undefined;
}
const selectOptions = useMemo(() => {
let _options = advanced ? schema : options;
if (Array.isArray(_options)) {
@ -128,11 +130,11 @@ export const DropdownV2 = ({
isDisabled: data?.disable ?? false,
}));
return _selectOptions;
return sortArray(_selectOptions, sort);
} else {
return [];
}
}, [advanced, schema, options]);
}, [advanced, schema, options, sort]);
function selectOption(value) {
const val = selectOptions.filter((option) => !option.isDisabled)?.find((option) => option.value === value);

View file

@ -67,3 +67,12 @@ export const highlightText = (text = '', highlight) => {
</span>
);
};
export const sortArray = (arr, sort) => {
if (sort === 'asc') {
return arr.sort((a, b) => a.label?.localeCompare(b.label));
} else if (sort === 'desc') {
return arr.sort((a, b) => b.label?.localeCompare(a.label));
}
return arr;
};

View file

@ -11,7 +11,7 @@ import cx from 'classnames';
import Label from '@/_ui/Label';
const tinycolor = require('tinycolor2');
import { CustomDropdownIndicator, CustomClearIndicator } from '../DropdownV2/DropdownV2';
import { getInputBackgroundColor, getInputBorderColor, getInputFocusedColor } from '../DropdownV2/utils';
import { getInputBackgroundColor, getInputBorderColor, getInputFocusedColor, sortArray } from '../DropdownV2/utils';
import useStore from '@/AppBuilder/_stores/store';
export const MultiselectV2 = ({
@ -37,6 +37,7 @@ export const MultiselectV2 = ({
placeholder,
loadingState: multiSelectLoadingState,
optionsLoadingState,
sort,
} = properties;
const {
selectedTextColor,
@ -92,17 +93,17 @@ export const MultiselectV2 = ({
const _options = advanced ? schema : options;
let _selectOptions = Array.isArray(_options)
? _options
.filter((data) => data?.visible ?? true)
.map((data) => ({
...data,
label: data?.label,
value: data?.value,
isDisabled: data?.disable ?? false,
}))
.filter((data) => data?.visible ?? true)
.map((data) => ({
...data,
label: data?.label,
value: data?.value,
isDisabled: data?.disable ?? false,
}))
: [];
return _selectOptions;
return sortArray(_selectOptions, sort);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [advanced, JSON.stringify(schema), JSON.stringify(options)]);
}, [advanced, JSON.stringify(schema), JSON.stringify(options), sort]);
function findDefaultItem(value, isAdvanced, isDefault) {
if (isAdvanced) {
@ -364,8 +365,8 @@ export const MultiselectV2 = ({
selectedTextColor !== '#1B1F24'
? selectedTextColor
: isMultiSelectLoading || isMultiSelectDisabled
? 'var(--text-disabled)'
: 'var(--text-primary)',
? 'var(--text-disabled)'
: 'var(--text-primary)',
}),
input: (provided, _state) => ({
@ -400,10 +401,10 @@ export const MultiselectV2 = ({
color: _state.isDisabled
? 'var(_--text-disbled)'
: selectedTextColor !== '#1B1F24'
? selectedTextColor
: isMultiSelectDisabled || isMultiSelectLoading
? 'var(--text-disabled)'
: 'var(--text-primary)',
? selectedTextColor
: isMultiSelectDisabled || isMultiSelectLoading
? 'var(--text-disabled)'
: 'var(--text-primary)',
borderRadius: _state.isFocused && '8px',
padding: '8px 6px 8px 12px',
'&:hover': {
@ -435,7 +436,7 @@ export const MultiselectV2 = ({
data-cy={`label-${String(componentName).toLowerCase()} `}
className={cx('multiselect-widget', 'd-flex', {
[alignment === 'top' &&
((labelWidth != 0 && label?.length != 0) || (auto && labelWidth == 0 && label && label?.length != 0))
((labelWidth != 0 && label?.length != 0) || (auto && labelWidth == 0 && label && label?.length != 0))
? 'flex-column'
: 'align-items-center']: true,
'flex-row-reverse': direction === 'right' && alignment === 'side',

View file

@ -63,6 +63,18 @@ export const dropdownV2Config = {
},
accordian: 'Options',
},
sort: {
type: 'switch',
displayName: 'Sort options',
validation: { schema: { type: 'string' }, defaultValue: 'asc' },
options: [
{ displayName: 'None', value: 'none' },
{ displayName: 'a-z', value: 'asc' },
{ displayName: 'z-a', value: 'desc' },
],
accordian: 'Options',
isFxNotRequired: true,
},
loadingState: {
type: 'toggle',
displayName: 'Loading state',
@ -300,6 +312,7 @@ export const dropdownV2Config = {
},
label: { value: 'Select' },
optionsLoadingState: { value: '{{false}}' },
sort: { value: 'asc' },
placeholder: { value: 'Select an option' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },

View file

@ -130,6 +130,18 @@ export const multiselectV2Config = {
},
accordian: 'Options',
},
sort: {
type: 'switch',
displayName: 'Sort options',
validation: { schema: { type: 'string' }, defaultValue: 'asc' },
options: [
{ displayName: 'None', value: 'none' },
{ displayName: 'a-z', value: 'asc' },
{ displayName: 'z-a', value: 'desc' },
],
accordian: 'Options',
isFxNotRequired: true,
},
loadingState: {
type: 'toggle',
displayName: 'Loading state',
@ -313,6 +325,7 @@ export const multiselectV2Config = {
advanced: { value: `{{false}}` },
showAllOption: { value: '{{false}}' },
optionsLoadingState: { value: '{{false}}' },
sort: { value: 'asc' },
placeholder: { value: 'Select the options' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },

View file

@ -63,6 +63,18 @@ export const dropdownV2Config = {
},
accordian: 'Options',
},
sort: {
type: 'switch',
displayName: 'Sort options',
validation: { schema: { type: 'string' }, defaultValue: 'asc' },
options: [
{ displayName: 'None', value: 'none' },
{ displayName: 'a-z', value: 'asc' },
{ displayName: 'z-a', value: 'desc' },
],
accordian: 'Options',
isFxNotRequired: true,
},
loadingState: {
type: 'toggle',
displayName: 'Loading state',
@ -300,6 +312,7 @@ export const dropdownV2Config = {
},
label: { value: 'Select' },
optionsLoadingState: { value: '{{false}}' },
sort: { value: 'asc' },
placeholder: { value: 'Select an option' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },

View file

@ -130,6 +130,18 @@ export const multiselectV2Config = {
},
accordian: 'Options',
},
sort: {
type: 'switch',
displayName: 'Sort options',
validation: { schema: { type: 'string' }, defaultValue: 'asc' },
options: [
{ displayName: 'None', value: 'none' },
{ displayName: 'a-z', value: 'asc' },
{ displayName: 'z-a', value: 'desc' },
],
accordian: 'Options',
isFxNotRequired: true,
},
loadingState: {
type: 'toggle',
displayName: 'Loading state',
@ -313,6 +325,7 @@ export const multiselectV2Config = {
advanced: { value: `{{false}}` },
showAllOption: { value: '{{false}}' },
optionsLoadingState: { value: '{{false}}' },
sort: { value: 'asc' },
placeholder: { value: 'Select the options' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },