diff --git a/.version b/.version index e604dbd28b..d1524d4046 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -2.63.0 +2.64.0 diff --git a/frontend/.version b/frontend/.version index e604dbd28b..d1524d4046 100644 --- a/frontend/.version +++ b/frontend/.version @@ -1 +1 @@ -2.63.0 +2.64.0 diff --git a/frontend/assets/images/icons/widgets/dropdownV2.jsx b/frontend/assets/images/icons/widgets/dropdownV2.jsx new file mode 100644 index 0000000000..692084820b --- /dev/null +++ b/frontend/assets/images/icons/widgets/dropdownV2.jsx @@ -0,0 +1,31 @@ +import React from 'react'; + +const DropdownV2 = ({ fill = '#D7DBDF', width = 24, className = '', viewBox = '0 0 49 48' }) => ( + + + + + +); + +export default DropdownV2; diff --git a/frontend/assets/images/icons/widgets/index.jsx b/frontend/assets/images/icons/widgets/index.jsx index 1a17f7caf0..dabe1d42d3 100644 --- a/frontend/assets/images/icons/widgets/index.jsx +++ b/frontend/assets/images/icons/widgets/index.jsx @@ -16,6 +16,7 @@ import Divider from './divider.jsx'; import DividerHorizondal from './dividerhorizontal.jsx'; import Downstatistics from './downstatistics.jsx'; import Dropdown from './dropdown.jsx'; +import DropdownV2 from './dropdownV2.jsx'; import Filepicker from './filepicker.jsx'; import Form from './form.jsx'; import Frame from './frame.jsx'; @@ -31,6 +32,7 @@ import Listview from './listview.jsx'; import Map from './map.jsx'; import Modal from './modal.jsx'; import Multiselect from './multiselect.jsx'; +import MultiselectV2 from './multiselectV2.jsx'; import Numberinput from './numberinput.jsx'; import Pagination from './pagination.jsx'; import Passwordinput from './passwordinput.jsx'; @@ -54,7 +56,6 @@ import Timeline from './timeline.jsx'; import Timer from './timer.jsx'; import Toggleswitch from './toggleswitch.jsx'; import ToggleSwitchV2 from './toggleswitchV2.jsx'; - import Treeselect from './treeselect.jsx'; import Upstatistics from './upstatistics.jsx'; import Verticaldivider from './verticaldivider.jsx'; @@ -95,6 +96,8 @@ const WidgetIcon = (props) => { return ; case 'dropdown': return ; + case 'dropdownV2': + return ; case 'filepicker': return ; case 'form': @@ -125,6 +128,8 @@ const WidgetIcon = (props) => { return ; case 'multiselect': return ; + case 'multiselectV2': + return ; case 'numberinput': return ; case 'pagination': @@ -135,7 +140,7 @@ const WidgetIcon = (props) => { return ; case 'qrscanner': return ; - case 'radio-button': + case 'radiobutton': return ; case 'rangeslider': return ; diff --git a/frontend/assets/images/icons/widgets/multiselectV2.jsx b/frontend/assets/images/icons/widgets/multiselectV2.jsx new file mode 100644 index 0000000000..b95e7b1b0e --- /dev/null +++ b/frontend/assets/images/icons/widgets/multiselectV2.jsx @@ -0,0 +1,27 @@ +import React from 'react'; + +const Multiselect = ({ fill = '#D7DBDF', width = 24, className = '', viewBox = '0 0 49 48' }) => ( + + + + +); + +export default Multiselect; diff --git a/frontend/assets/images/image-not-found.svg b/frontend/assets/images/image-not-found.svg new file mode 100644 index 0000000000..ec20252966 --- /dev/null +++ b/frontend/assets/images/image-not-found.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/assets/translations/en.json b/frontend/assets/translations/en.json index 9e8a3bb8f0..4ea8ea391a 100644 --- a/frontend/assets/translations/en.json +++ b/frontend/assets/translations/en.json @@ -174,9 +174,9 @@ "goToAllDatasources": "Go to all Datasource", "send": "Send" }, - "runQueryOnApplicationLoad": "Run this query on application load?", - "confirmBeforeQueryRun": "Request confirmation before running query?", - "notificationOnSuccess": "Show notification on success?", + "runQueryOnApplicationLoad": "Run this query on application load", + "confirmBeforeQueryRun": "Request confirmation before running query", + "notificationOnSuccess": "Show notification on success", "successMessage": "Success Message", "queryRanSuccessfully": "Query ran successfully", "notificationDuration": "Notification duration (s)", @@ -207,6 +207,7 @@ "pageIndex": "Page index", "component": "Component", "addHandler": "New event handler", + "addNewEvent": "Add new event", "addEventHandler": "+ Add event handler", "emptyMessage": "This {{componentName}} doesn't have any event handlers", "page": "Page" @@ -286,9 +287,9 @@ "createUpdateDelete": "Create/Update/Delete", "folder": "Folder" }, - "groupOptions":{ - "deleteGroup":"Delete Group", - "duplicateGroup":"Duplicate Group" + "groupOptions": { + "deleteGroup": "Delete Group", + "duplicateGroup": "Duplicate Group" } }, "manageSSO": { @@ -496,7 +497,7 @@ "properties": "Properties", "events": "Events", "layout": "Layout", - "devices":"Devices", + "devices": "Devices", "styles": "Styles", "general": "General", "validation": "Validation", @@ -728,10 +729,10 @@ "addColumn": "Add column", "addNewColumn": "Add new column", "noActionMessage": "This table doesn't have any action buttons", - "horizontalAlignment":"Horizontal alignment", - "textAlignment":"Text alignment", - "deciamalPlaces":"Decimal Places", - "imageFit":"Image fit" + "horizontalAlignment": "Horizontal alignment", + "textAlignment": "Text alignment", + "deciamalPlaces": "Decimal Places", + "imageFit": "Image fit" }, "Button": { "displayName": "Button", @@ -944,7 +945,7 @@ "typeComment": "Type your comment here" }, "Settings": { - "text": "Settings", + "text": "Triggers", "tip": "Global Settings", "hideHeader": "Hide header for launched apps", "maintenanceMode": "Maintenance mode", diff --git a/frontend/src/Editor/BoxUI.jsx b/frontend/src/Editor/BoxUI.jsx index 0c580ad105..9ed762257a 100644 --- a/frontend/src/Editor/BoxUI.jsx +++ b/frontend/src/Editor/BoxUI.jsx @@ -18,6 +18,8 @@ const shouldAddBoxShadowAndVisibility = [ 'Checkbox', 'Button', 'ToggleSwitchV2', + 'DropdownV2', + 'MultiselectV2', ]; const BoxUI = (props) => { diff --git a/frontend/src/Editor/CodeBuilder/Elements/Switch.jsx b/frontend/src/Editor/CodeBuilder/Elements/Switch.jsx index db27b574d9..4f1b7c6c65 100644 --- a/frontend/src/Editor/CodeBuilder/Elements/Switch.jsx +++ b/frontend/src/Editor/CodeBuilder/Elements/Switch.jsx @@ -3,7 +3,7 @@ import ToggleGroupItem from '@/ToolJetUI/SwitchGroup/ToggleGroupItem'; import React from 'react'; import cx from 'classnames'; -const Switch = ({ value, onChange, meta, paramName, component }) => { +const Switch = ({ value, onChange, cyLabel, meta, paramName, isIcon, component }) => { const options = meta?.options; const defaultValue = paramName == 'defaultValue' && (component == 'Checkbox' || component == 'ToggleSwitchV2') ? `{{${value}}}` : value; diff --git a/frontend/src/Editor/CodeBuilder/Elements/Toggle.jsx b/frontend/src/Editor/CodeBuilder/Elements/Toggle.jsx index 9d6a968b18..ebc2d4afc3 100644 --- a/frontend/src/Editor/CodeBuilder/Elements/Toggle.jsx +++ b/frontend/src/Editor/CodeBuilder/Elements/Toggle.jsx @@ -9,12 +9,12 @@ export const Toggle = ({ value, onChange, cyLabel, meta }) => { className="form-check form-switch mb-0 d-flex justify-content-end" style={{ marginBottom: '0px', paddingLeft: '28px' }} > - {meta.toggleLabel && ( + {meta?.toggleLabel && ( - {meta.toggleLabel} + {meta?.toggleLabel} )} { showPreview, paramLabel = '', delayOnChange = true, // Added this prop to immediately update the onBlurUpdate callback + readOnly = false, + editable = true, } = props; const context = useContext(CodeHinterContext); @@ -237,6 +239,8 @@ const MultiLineCodeEditor = (props) => { }} className={`codehinter-multi-line-input`} indentWithTab={true} + readOnly={readOnly} + editable={editable} //for transformations in query manager /> {showPreview && ( diff --git a/frontend/src/Editor/CodeEditor/SingleLineCodeEditor.jsx b/frontend/src/Editor/CodeEditor/SingleLineCodeEditor.jsx index 7d1606dff7..18e8d70b7a 100644 --- a/frontend/src/Editor/CodeEditor/SingleLineCodeEditor.jsx +++ b/frontend/src/Editor/CodeEditor/SingleLineCodeEditor.jsx @@ -330,6 +330,10 @@ const DynamicEditorBridge = (props) => { const { t } = useTranslation(); const [_, error, value] = type === 'fxEditor' ? resolveReferences(initialValue) : []; + useEffect(() => { + setForceCodeBox(fxActive); + }, [component]); + const fxClass = isEventManagerParam ? 'justify-content-start' : 'justify-content-end'; return (
diff --git a/frontend/src/Editor/Components/Checkbox.jsx b/frontend/src/Editor/Components/Checkbox.jsx index b144a6e8cf..bce29bad1f 100644 --- a/frontend/src/Editor/Components/Checkbox.jsx +++ b/frontend/src/Editor/Components/Checkbox.jsx @@ -102,7 +102,6 @@ export const Checkbox = ({ setExposedVariable('isLoading', loading); // eslint-disable-next-line react-hooks/exhaustive-deps }, [loading]); - useEffect(() => { setExposedVariable('isVisible', visibility); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -112,12 +111,10 @@ export const Checkbox = ({ setExposedVariable('isDisabled', disable); // eslint-disable-next-line react-hooks/exhaustive-deps }, [disable]); - useEffect(() => { setExposedVariable('isValid', isValid); // eslint-disable-next-line react-hooks/exhaustive-deps }, [isValid]); - useEffect(() => { setExposedVariable('setLoading', async function (loading) { setLoading(loading); @@ -255,7 +252,6 @@ export const Checkbox = ({ )} ); - const checkmarkStyle = { position: 'absolute', top: '1px', diff --git a/frontend/src/Editor/Components/Divider.jsx b/frontend/src/Editor/Components/Divider.jsx index 7c8227cca8..5935181bf7 100644 --- a/frontend/src/Editor/Components/Divider.jsx +++ b/frontend/src/Editor/Components/Divider.jsx @@ -1,14 +1,20 @@ import React from 'react'; -export const Divider = function Divider({ styles, dataCy }) { +export const Divider = function Divider({ styles, dataCy, height, width, darkMode }) { const { visibility, dividerColor, boxShadow } = styles; - const color = dividerColor ?? '#E7E8EA'; + const color = + dividerColor === '' || ['#000', '#000000'].includes(dividerColor) ? (darkMode ? '#fff' : '#000') : dividerColor; return (
+ > +
+
); }; diff --git a/frontend/src/Editor/Components/DropdownV2/CustomMenuList.jsx b/frontend/src/Editor/Components/DropdownV2/CustomMenuList.jsx new file mode 100644 index 0000000000..848de076ae --- /dev/null +++ b/frontend/src/Editor/Components/DropdownV2/CustomMenuList.jsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { components } from 'react-select'; +import SolidIcon from '@/_ui/Icon/SolidIcons'; +import Loader from '@/ToolJetUI/Loader/Loader'; +import './dropdownV2.scss'; +import { FormCheck } from 'react-bootstrap'; +import cx from 'classnames'; + +const { MenuList } = components; + +// This Menulist also used in MultiselectV2 +const CustomMenuList = ({ selectProps, ...props }) => { + const { + onInputChange, + onMenuInputFocus, + showAllOption, + isSelectAllSelected, + optionsLoadingState, + darkMode, + setSelected, + setIsSelectAllSelected, + fireEvent, + inputValue, + menuId, + } = selectProps; + + const handleSelectAll = (e) => { + e.target.checked && fireEvent(); + if (e.target.checked) { + setSelected(props.options); + } else { + setSelected([]); + } + setIsSelectAllSelected(e.target.checked); + }; + return ( +
e.stopPropagation()} + > +
+ + + + + onInputChange(e.currentTarget.value, { + action: 'input-change', + }) + } + onMouseDown={(e) => { + e.stopPropagation(); + e.target.focus(); + }} + onTouchEnd={(e) => { + e.stopPropagation(); + e.target.focus(); + }} + onFocus={onMenuInputFocus} + placeholder="Search" + className="dropdown-multiselect-widget-search-box" + /> +
+ {showAllOption && !optionsLoadingState && ( + + )} + + {optionsLoadingState ? ( +
+ +
+ ) : ( + props.children + )} +
+
+ ); +}; + +export default CustomMenuList; diff --git a/frontend/src/Editor/Components/DropdownV2/CustomOption.jsx b/frontend/src/Editor/Components/DropdownV2/CustomOption.jsx new file mode 100644 index 0000000000..73986ba274 --- /dev/null +++ b/frontend/src/Editor/Components/DropdownV2/CustomOption.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { components } from 'react-select'; +import CheckMark from '@/_ui/Icon/bulkIcons/CheckMark'; +import './dropdownV2.scss'; +import { highlightText } from './utils'; + +const CustomOption = (props) => { + return ( + +
+ {props.isSelected && ( + + + + )} + + {highlightText(props.label?.toString(), props.selectProps.inputValue)} + +
+
+ ); +}; + +export default CustomOption; diff --git a/frontend/src/Editor/Components/DropdownV2/CustomValueContainer.jsx b/frontend/src/Editor/Components/DropdownV2/CustomValueContainer.jsx new file mode 100644 index 0000000000..5c25e3d24d --- /dev/null +++ b/frontend/src/Editor/Components/DropdownV2/CustomValueContainer.jsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { components } from 'react-select'; +import * as Icons from '@tabler/icons-react'; +import './dropdownV2.scss'; + +const { ValueContainer, SingleValue, Placeholder } = components; + +const CustomValueContainer = ({ children, ...props }) => { + const selectProps = props.selectProps; + // eslint-disable-next-line import/namespace + const IconElement = Icons[selectProps?.icon] == undefined ? Icons['IconHome2'] : Icons[selectProps?.icon]; + return ( + +
+ {selectProps?.doShowIcon && ( +
+ +
+ )} + + {React.Children.map(children, (child) => { + return child ? ( + child + ) : props.hasValue ? ( + + {selectProps?.getOptionLabel(props?.getValue()[0])} + + ) : ( + + {selectProps.placeholder} + + ); + })} + +
+
+ ); +}; + +export default CustomValueContainer; diff --git a/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx b/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx new file mode 100644 index 0000000000..5cb6394036 --- /dev/null +++ b/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx @@ -0,0 +1,455 @@ +import { resolveReferences } from '@/_helpers/utils'; +import { useCurrentState } from '@/_stores/currentStateStore'; +import React, { useState, useEffect, useMemo, useRef } from 'react'; +import Select, { components } from 'react-select'; +import ClearIndicatorIcon from '@/_ui/Icon/bulkIcons/ClearIndicator'; +import TriangleDownArrow from '@/_ui/Icon/bulkIcons/TriangleDownArrow'; +import TriangleUpArrow from '@/_ui/Icon/bulkIcons/TriangleUpArrow'; +import { useEditorStore } from '@/_stores/editorStore'; +import Loader from '@/ToolJetUI/Loader/Loader'; +import { has, isObject, pick } from 'lodash'; +const tinycolor = require('tinycolor2'); +import './dropdownV2.scss'; +import CustomValueContainer from './CustomValueContainer'; +import CustomMenuList from './CustomMenuList'; +import CustomOption from './CustomOption'; +import Label from '@/_ui/Label'; +import cx from 'classnames'; +import { getInputBackgroundColor, getInputBorderColor, getInputFocusedColor } from './utils'; + +const { DropdownIndicator, ClearIndicator } = components; +const INDICATOR_CONTAINER_WIDTH = 60; +const ICON_WIDTH = 18; // includes flex gap 2px + +export const CustomDropdownIndicator = (props) => { + const { + selectProps: { menuIsOpen }, + } = props; + return ( + + {menuIsOpen ? ( + + ) : ( + + )} + + ); +}; + +export const CustomClearIndicator = (props) => { + return ( + + + + ); +}; + +export const DropdownV2 = ({ + height, + validate, + properties, + styles, + setExposedVariable, + setExposedVariables, + fireEvent, + darkMode, + onComponentClick, + id, + component, + exposedVariables, + dataCy, +}) => { + const { + label, + value, + advanced, + schema, + placeholder, + loadingState: dropdownLoadingState, + disabledState, + options, + } = properties; + const { + selectedTextColor, + fieldBorderRadius, + justifyContent, + boxShadow, + labelColor, + alignment, + direction, + fieldBorderColor, + fieldBackgroundColor, + labelWidth, + icon, + iconVisibility, + errTextColor, + auto: labelAutoWidth, + iconColor, + accentColor, + padding, + } = styles; + const [currentValue, setCurrentValue] = useState(() => (advanced ? findDefaultItem(schema) : value)); + const { value: exposedValue } = exposedVariables; + const currentState = useCurrentState(); + const isMandatory = resolveReferences(component?.definition?.validation?.mandatory?.value, currentState); + const validationData = validate(currentValue); + const { isValid, validationError } = validationData; + const ref = React.useRef(null); + const [visibility, setVisibility] = useState(properties.visibility); + const [isDropdownLoading, setIsDropdownLoading] = useState(dropdownLoadingState); + const [isDropdownDisabled, setIsDropdownDisabled] = useState(disabledState); + const [isFocused, setIsFocused] = useState(false); + const [searchInputValue, setSearchInputValue] = useState(''); + const _height = padding === 'default' ? `${height}px` : `${height + 4}px`; + const labelRef = useRef(); + + function findDefaultItem(schema) { + let _schema = schema; + if (!Array.isArray(schema)) { + _schema = []; + } + 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)) { + let _selectOptions = _options + .filter((data) => data?.visible?.value) + .map((value) => ({ + ...value, + isDisabled: value?.disable?.value, + })); + return _selectOptions; + } else { + return []; + } + }, [advanced, schema, options]); + + function selectOption(value) { + const val = selectOptions.filter((option) => !option.isDisabled)?.find((option) => option.value === value); + if (val) { + setCurrentValue(value); + fireEvent('onSelect'); + } + } + + function hasVisibleFalse(value) { + for (let i = 0; i < schema?.length; i++) { + if (schema[i].value === value && schema[i].visible === false) { + return true; + } + } + return false; + } + + const onSearchTextChange = (searchText, actionProps) => { + if (actionProps.action === 'input-change') { + setSearchInputValue(searchText); + fireEvent('onSearchTextChanged'); + } + }; + + const handleOutsideClick = (e) => { + let menu = ref.current.querySelector('.select__menu'); + if (!ref.current.contains(e.target) || !menu || !menu.contains(e.target)) { + setIsFocused(false); + setSearchInputValue(''); + } + }; + + useEffect(() => { + if (advanced) { + setCurrentValue(findDefaultItem(schema)); + } else setCurrentValue(value); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [advanced, value, JSON.stringify(schema)]); + + useEffect(() => { + document.addEventListener('mousedown', handleOutsideClick); + return () => { + document.removeEventListener('mousedown', handleOutsideClick); + }; + }, []); + + useEffect(() => { + if (visibility !== properties.visibility) setVisibility(properties.visibility); + if (isDropdownLoading !== dropdownLoadingState) setIsDropdownLoading(dropdownLoadingState); + if (isDropdownDisabled !== disabledState) setIsDropdownDisabled(disabledState); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [properties.visibility, dropdownLoadingState, disabledState]); + + // Exposed variables + useEffect(() => { + if (exposedValue !== currentValue) { + const _selectedOption = selectOptions.find((option) => option.value === currentValue); + setExposedVariable('selectedOption', pick(_selectedOption, ['label', 'value'])); + } + const _options = selectOptions?.map(({ label, value }) => ({ label, value })); + setExposedVariable('options', _options); + + setExposedVariable('selectOption', async function (value) { + let _value = value; + if (isObject(value) && has(value, 'value')) _value = value?.value; + selectOption(_value); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentValue, JSON.stringify(selectOptions)]); + + useEffect(() => { + setExposedVariable('label', label); + setExposedVariable('searchText', searchInputValue); + setExposedVariable('isValid', isValid); + setExposedVariable('isVisible', properties.visibility); + setExposedVariable('isLoading', dropdownLoadingState); + setExposedVariable('isDisabled', disabledState); + setExposedVariable('isMandatory', isMandatory); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [properties.visibility, dropdownLoadingState, disabledState, isMandatory, label, searchInputValue, isValid]); + + useEffect(() => { + const exposedVariables = { + clear: async function () { + setCurrentValue(null); + }, + setVisibility: async function (value) { + setVisibility(value); + }, + setLoading: async function (value) { + setIsDropdownLoading(value); + }, + setDisable: async function (value) { + setIsDropdownDisabled(value); + }, + }; + setExposedVariables(exposedVariables); + }, []); + + const customStyles = { + container: (base) => ({ + ...base, + width: '100%', + minWidth: '72px', + }), + control: (provided, state) => { + return { + ...provided, + minHeight: _height, + height: _height, + boxShadow: state.isFocused ? boxShadow : boxShadow, + borderRadius: Number.parseFloat(fieldBorderRadius), + borderColor: getInputBorderColor({ + isFocused: state.isFocused, + isValid, + fieldBorderColor, + accentColor, + isLoading: isDropdownLoading, + isDisabled: isDropdownDisabled, + }), + backgroundColor: getInputBackgroundColor({ + fieldBackgroundColor, + darkMode, + isLoading: isDropdownLoading, + isDisabled: isDropdownDisabled, + }), + '&:hover': { + borderColor: state.isFocused + ? getInputFocusedColor({ accentColor }) + : tinycolor(fieldBorderColor).darken(24).toString(), + }, + }; + }, + valueContainer: (provided, _state) => ({ + ...provided, + height: _height, + padding: '0 10px', + justifyContent, + display: 'flex', + gap: '0.13rem', + }), + + singleValue: (provided, _state) => ({ + ...provided, + color: + selectedTextColor !== '#1B1F24' + ? selectedTextColor + : isDropdownDisabled || isDropdownLoading + ? 'var(--text-disabled)' + : 'var(--text-primary)', + maxWidth: + ref?.current?.offsetWidth - + (iconVisibility ? INDICATOR_CONTAINER_WIDTH + ICON_WIDTH : INDICATOR_CONTAINER_WIDTH), + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + }), + input: (provided, _state) => ({ + ...provided, + color: darkMode ? 'white' : 'black', + margin: '0px', + }), + indicatorSeparator: (_state) => ({ + display: 'none', + }), + indicatorsContainer: (provided, _state) => ({ + ...provided, + height: _height, + marginRight: '10px', + }), + clearIndicator: (provided, _state) => ({ + ...provided, + padding: '2px', + '&:hover': { + padding: '2px', + backgroundColor: 'var(--interactive-overlays-fill-hover)', + borderRadius: '6px', + }, + }), + dropdownIndicator: (provided, _state) => ({ + ...provided, + padding: '0px', + }), + option: (provided) => ({ + ...provided, + backgroundColor: 'var(--surfaces-surface-01)', + color: + selectedTextColor !== '#1B1F24' + ? selectedTextColor + : isDropdownDisabled || isDropdownLoading + ? 'var(--text-disabled)' + : 'var(--text-primary)', + padding: '8px 6px 8px 38px', + '&:hover': { + backgroundColor: 'var(--interactive-overlays-fill-hover)', + borderRadius: '8px', + }, + display: 'flex', + cursor: 'pointer', + }), + menuList: (provided) => ({ + ...provided, + padding: '8px', + borderRadius: '8px', + // this is needed otherwise :active state doesn't look nice, gap is required + display: 'flex', + flexDirection: 'column', + gap: '4px !important', + overflowY: 'auto', + backgroundColor: 'var(--surfaces-surface-01)', + }), + menu: (provided) => ({ + ...provided, + borderRadius: '8px', + boxShadow: 'unset', + margin: 0, + }), + }; + const _width = (labelWidth / 100) * 70; // Max width which label can go is 70% for better UX calculate width based on this value + return ( + <> +
{ + onComponentClick(id, component, event); + // This following line is needed because sometimes after clicking on canvas then also dropdown remains selected + useEditorStore.getState().actions.setHoveredComponent(''); + }} + > +
+
+ {!isValid && validationError} +
+ + ); +}; diff --git a/frontend/src/Editor/Components/MultiselectV2/multiselectV2.scss b/frontend/src/Editor/Components/MultiselectV2/multiselectV2.scss new file mode 100644 index 0000000000..dd6d505d80 --- /dev/null +++ b/frontend/src/Editor/Components/MultiselectV2/multiselectV2.scss @@ -0,0 +1,38 @@ +.value-container-selected-option { + display: flex; + align-items: center; + border-radius: 6px; + border: 1px solid #ECEEF0; + background-color: var(--surfaces-surface-03); + padding-right: 2px; + margin-right: 4px; + line-height: 20px; + padding: 1px 3px 1px 6px; + color: var(--text-primary); + font-weight: 500; + &:hover { + background-color: var(--interactive-overlays-fill-pressed); + } + .value-container-selected-option-delete-icon { + margin-left: 6px; + cursor: pointer; + } + } + + .value-container-selected-option-popover { + display: flex; + flex-wrap: wrap; + gap : 6px; + padding: 16px; + color: var(--text-primary); + border-radius: 6px; + background-color: var(--surfaces-surface-01); + font-weight: 500; + } + + .multiselect-widget-show-more-popover { + background-color: var(--surfaces-surface-01) !important; + .popover-body { + background-color: var(--surfaces-surface-01) !important + } + } \ No newline at end of file diff --git a/frontend/src/Editor/Components/PasswordInput.jsx b/frontend/src/Editor/Components/PasswordInput.jsx index 03f16976b8..ad9fa007fb 100644 --- a/frontend/src/Editor/Components/PasswordInput.jsx +++ b/frontend/src/Editor/Components/PasswordInput.jsx @@ -1,10 +1,10 @@ import React, { useEffect, useRef, useState } from 'react'; import { resolveWidgetFieldValue } from '@/_helpers/utils'; - import * as Icons from '@tabler/icons-react'; import Loader from '@/ToolJetUI/Loader/Loader'; import SolidIcon from '@/_ui/Icon/SolidIcons'; import Label from '@/_ui/Label'; +import { useEditorStore } from '@/_stores/editorStore'; export const PasswordInput = function PasswordInput({ height, @@ -17,6 +17,7 @@ export const PasswordInput = function PasswordInput({ darkMode, dataCy, isResizing, + id, }) { const textInputRef = useRef(); const labelRef = useRef(); @@ -228,6 +229,23 @@ export const PasswordInput = function PasswordInput({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [disable]); + const currentPageId = useEditorStore.getState().currentPageId; + const components = useEditorStore.getState().appDefinition?.pages?.[currentPageId]?.components || {}; + + const isChildOfForm = Object.keys(components).some((key) => { + if (key == id) { + const { parent } = components[key].component; + if (parent) { + const parentComponentTypes = {}; + Object.keys(components).forEach((key) => { + const { component } = components[key]; + parentComponentTypes[key] = component.component; + }); + if (parentComponentTypes[parent] == 'Form') return true; + } + } + return false; + }); const renderInput = () => ( <>
); + const renderContainer = (children) => { + return !isChildOfForm ?
{children}
:
{children}
; + }; - return
{renderInput()}
; + return renderContainer(renderInput()); }; diff --git a/frontend/src/Editor/Components/Table/CustomSelect.jsx b/frontend/src/Editor/Components/Table/CustomSelect.jsx index 0c7f992974..dcb91cadfa 100644 --- a/frontend/src/Editor/Components/Table/CustomSelect.jsx +++ b/frontend/src/Editor/Components/Table/CustomSelect.jsx @@ -190,7 +190,7 @@ export const CustomSelect = ({ ); }; -const CustomMenuList = ({ optionsLoadingState, children, selectProps, inputRef, ...props }) => { +export const CustomMenuList = ({ optionsLoadingState, children, selectProps, inputRef, ...props }) => { const { onInputChange, inputValue, onMenuInputFocus } = selectProps; return ( diff --git a/frontend/src/Editor/Components/Table/Datepicker.jsx b/frontend/src/Editor/Components/Table/Datepicker.jsx index 9a396542d8..4e5a481dd2 100644 --- a/frontend/src/Editor/Components/Table/Datepicker.jsx +++ b/frontend/src/Editor/Components/Table/Datepicker.jsx @@ -13,22 +13,34 @@ const TjDatepicker = forwardRef( ({ value, onClick, styles, dateInputRef, readOnly, setIsDateInputFocussed, setDateInputValue }, ref) => { return (
- { - setIsDateInputFocussed(true); - setDateInputValue(e.target.value); - }} - onFocus={() => { - setDateInputValue(value); - }} - /> + {readOnly ? ( +
+ {value} +
+ ) : ( + { + setIsDateInputFocussed(true); + setDateInputValue(e.target.value); + }} + onFocus={() => { + setDateInputValue(value); + }} + /> + )} {!readOnly && (
- + { if (e.key === 'Enter') { + ref.current.blur(); if (cellValue !== e.target.textContent) { handleCellValueChange(cell.row.index, column.key || column.name, e.target.textContent, cell.row.original); } @@ -93,7 +94,7 @@ const StringColumn = ({ e.stopPropagation(); }} > - {String(cellValue)} + {String(cellValue)}
); diff --git a/frontend/src/Editor/Components/Table/Table.jsx b/frontend/src/Editor/Components/Table/Table.jsx index 4d9204662e..13baf93098 100644 --- a/frontend/src/Editor/Components/Table/Table.jsx +++ b/frontend/src/Editor/Components/Table/Table.jsx @@ -428,7 +428,11 @@ export function Table({ const tableRef = useRef(); - const columnProperties = useDynamicColumn ? generatedColumn : component.definition.properties.columns.value; + const removeNullValues = (arr) => arr.filter((element) => element !== null); + + const columnProperties = useDynamicColumn + ? generatedColumn + : removeNullValues(component.definition.properties.columns.value); let columnData = generateColumnsData({ columnProperties, diff --git a/frontend/src/Editor/Components/Table/Text.jsx b/frontend/src/Editor/Components/Table/Text.jsx index 8790772b50..ca4525012f 100644 --- a/frontend/src/Editor/Components/Table/Text.jsx +++ b/frontend/src/Editor/Components/Table/Text.jsx @@ -36,6 +36,7 @@ const Text = ({ }); const { isValid, validationError } = validationData; const ref = useRef(); + const editableCellValueRef = useRef(null); const nonEditableCellValueRef = useRef(); const [showOverlay, setShowOverlay] = useState(false); const [hovered, setHovered] = useState(false); @@ -54,6 +55,7 @@ const Text = ({ const _renderTextArea = () => (
{ e.persist(); if (e.key === 'Enter' && !e.shiftKey && isEditable) { + editableCellValueRef.current.blur(); const div = e.target; let content = div.innerHTML; handleCellValueChange(cell.row.index, column.key || column.name, content, cell.row.original); diff --git a/frontend/src/Editor/Components/Table/columns/index.jsx b/frontend/src/Editor/Components/Table/columns/index.jsx index 527ce369bf..a9e8391cde 100644 --- a/frontend/src/Editor/Components/Table/columns/index.jsx +++ b/frontend/src/Editor/Components/Table/columns/index.jsx @@ -561,6 +561,13 @@ export default function generateColumnsData({ {cellValue && ( { + if (!_.get(e, 'target.src', '').includes('/assets/images/image-not-found.svg')) { + e.target.onerror = null; + e.target.src = '/assets/images/image-not-found.svg'; + e.target.style = e.target.style + ' border-radius: 0;'; + } + }} style={{ pointerEvents: 'auto', width: `${column?.width}px`, diff --git a/frontend/src/Editor/Components/ToggleV2.jsx b/frontend/src/Editor/Components/ToggleV2.jsx index bc9b099f49..a9d2619923 100644 --- a/frontend/src/Editor/Components/ToggleV2.jsx +++ b/frontend/src/Editor/Components/ToggleV2.jsx @@ -165,7 +165,6 @@ export const ToggleSwitchV2 = ({ setExposedVariable('isLoading', loading); // eslint-disable-next-line react-hooks/exhaustive-deps }, [loading]); - useEffect(() => { setExposedVariable('isVisible', visibility); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -175,12 +174,10 @@ export const ToggleSwitchV2 = ({ setExposedVariable('isDisabled', disable); // eslint-disable-next-line react-hooks/exhaustive-deps }, [disable]); - useEffect(() => { setExposedVariable('isValid', isValid); // eslint-disable-next-line react-hooks/exhaustive-deps }, [isValid]); - useEffect(() => { setExposedVariable('setLoading', async function (loading) { setLoading(loading); diff --git a/frontend/src/Editor/Components/text.scss b/frontend/src/Editor/Components/text.scss index f3a290a5f8..6c1bce79d0 100644 --- a/frontend/src/Editor/Components/text.scss +++ b/frontend/src/Editor/Components/text.scss @@ -1,7 +1,12 @@ - - .reactMarkdown { - p,h1,h2,h3,h4,h5,h6 { + + p, + h1, + h2, + h3, + h4, + h5, + h6 { margin-bottom: 0px; } } @@ -9,17 +14,21 @@ .text-widget-section { scrollbar-color: transparent transparent; scrollbar-width: thin; - &::-webkit-scrollbar { + + & ::-webkit-scrollbar { background-color: transparent; width: 6px; } - &::-webkit-scrollbar-track { + + & ::-webkit-scrollbar-track { background-color: transparent; } - &::-webkit-scrollbar-thumb { + + & ::-webkit-scrollbar-thumb { background-color: transparent; } - &:hover{ + + & :hover { scrollbar-color: #a0a6ae transparent; } } \ No newline at end of file diff --git a/frontend/src/Editor/Components/verticalDivider.jsx b/frontend/src/Editor/Components/verticalDivider.jsx index 6e54a205f8..ba6a38748c 100644 --- a/frontend/src/Editor/Components/verticalDivider.jsx +++ b/frontend/src/Editor/Components/verticalDivider.jsx @@ -13,7 +13,7 @@ export const VerticalDivider = function Divider({ styles, height, width, dataCy, >
diff --git a/frontend/src/Editor/Container.jsx b/frontend/src/Editor/Container.jsx index af0308b665..8da33af2d4 100644 --- a/frontend/src/Editor/Container.jsx +++ b/frontend/src/Editor/Container.jsx @@ -12,7 +12,12 @@ import { commentsService } from '@/_services'; import config from 'config'; import Spinner from '@/_ui/Spinner'; import { useHotkeys } from 'react-hotkeys-hook'; -import { addComponents, addNewWidgetToTheEditor, isPDFSupported } from '@/_helpers/appUtils'; +import { + addComponents, + addNewWidgetToTheEditor, + isPDFSupported, + calculateMoveableBoxHeight, +} from '@/_helpers/appUtils'; import { useAppVersionStore } from '@/_stores/appVersionStore'; import { useEditorStore } from '@/_stores/editorStore'; import { useAppInfo } from '@/_stores/appDataStore'; @@ -116,7 +121,7 @@ export const Container = ({ // const [isResizing, setIsResizing] = useState(false); const [commentsPreviewList, setCommentsPreviewList] = useState([]); const [newThread, addNewThread] = useState({}); - const [isContainerFocused, setContainerFocus] = useState(false); + const [isContainerFocused, setContainerFocus] = useState(true); const [canvasHeight, setCanvasHeight] = useState(null); useEffect(() => { @@ -157,7 +162,7 @@ export const Container = ({ if (navigator.clipboard && typeof navigator.clipboard.readText === 'function') { try { const cliptext = await navigator.clipboard.readText(); - addComponents( + const newComponent = addComponents( currentPageId, appDefinition, appDefinitionChanged, @@ -165,6 +170,7 @@ export const Container = ({ JSON.parse(cliptext), true ); + setSelectedComponent(newComponent.id, newComponent.component); } catch (err) { console.log(err); } @@ -236,6 +242,7 @@ export const Container = ({ const noOfBoxs = Object.values(boxes || []).length; useEffect(() => { updateCanvasHeight(boxes); + noOfBoxs != 0 && setContainerFocus(true); // eslint-disable-next-line react-hooks/exhaustive-deps }, [noOfBoxs]); @@ -647,8 +654,13 @@ export const Container = ({ return; } if (Object.keys(value)?.length > 0) { - setBoxes((boxes) => - update(boxes, { + setBoxes((boxes) => { + // Ensure boxes[id] exists + if (!boxes[id]) { + console.error(`Box with id ${id} does not exist`); + return boxes; + } + return update(boxes, { [id]: { $merge: { component: { @@ -663,8 +675,9 @@ export const Container = ({ }, }, }, - }) - ); + }); + }); + if (!_.isEmpty(opts)) { paramUpdatesOptsRef.current = opts; } @@ -1027,26 +1040,6 @@ const WidgetWrapper = ({ // const width = (canvasWidth * layoutData.width) / NO_OF_GRIDS; const width = gridWidth * layoutData.width; - const calculateMoveableBoxHeight = () => { - // Early return for non input components - if (!['TextInput', 'PasswordInput', 'NumberInput'].includes(componentType)) { - return layoutData?.height; - } - const { alignment = { value: null }, width = { value: null }, auto = { value: null } } = stylesDefinition ?? {}; - - const resolvedLabel = label?.value?.length ?? 0; - const resolvedWidth = resolveWidgetFieldValue(width?.value) ?? 0; - const resolvedAuto = resolveWidgetFieldValue(auto?.value) ?? false; - - let newHeight = layoutData?.height; - if (alignment.value && resolveWidgetFieldValue(alignment.value) === 'top') { - if ((resolvedLabel > 0 && resolvedWidth > 0) || (resolvedAuto && resolvedWidth === 0 && resolvedLabel > 0)) { - newHeight += 20; - } - } - - return newHeight; - }; const isWidgetActive = (isSelected || isDragging) && mode !== 'view'; const { label = { value: null } } = propertiesDefinition ?? {}; @@ -1055,7 +1048,9 @@ const WidgetWrapper = ({ const styles = { width: width + 'px', - height: resolvedVisibility ? calculateMoveableBoxHeight() + 'px' : '10px', + height: resolvedVisibility + ? calculateMoveableBoxHeight(componentType, layoutData, stylesDefinition, label) + 'px' + : '10px', transform: `translate(${layoutData.left * gridWidth}px, ${layoutData.top}px)`, ...(isGhostComponent ? { opacity: 0.5 } : {}), ...(isWidgetActive ? { zIndex: 3 } : {}), diff --git a/frontend/src/Editor/ControlledComponentToRender.jsx b/frontend/src/Editor/ControlledComponentToRender.jsx index 28ea3971d0..574fd26fba 100644 --- a/frontend/src/Editor/ControlledComponentToRender.jsx +++ b/frontend/src/Editor/ControlledComponentToRender.jsx @@ -1,7 +1,6 @@ import React, { useState, useCallback } from 'react'; import { getComponentToRender } from '@/_helpers/editorHelpers'; import _ from 'lodash'; - import { getComponentsToRenders } from '@/_stores/editorStore'; function deepEqualityCheckusingLoDash(obj1, obj2) { diff --git a/frontend/src/Editor/DataSourceManager/DataSourceManager.jsx b/frontend/src/Editor/DataSourceManager/DataSourceManager.jsx index cb7497bc2c..e3e3a47c1b 100644 --- a/frontend/src/Editor/DataSourceManager/DataSourceManager.jsx +++ b/frontend/src/Editor/DataSourceManager/DataSourceManager.jsx @@ -1021,7 +1021,7 @@ class DataSourceManagerComponent extends React.Component { createSelectedDataSource(dataSourceConfirmModalProps.dataSource)} onCancel={this.resetDataSourceConfirmModal} confirmButtonText={'Add datasource'} diff --git a/frontend/src/Editor/DragContainer.jsx b/frontend/src/Editor/DragContainer.jsx index 15bd198cd8..499e27447d 100644 --- a/frontend/src/Editor/DragContainer.jsx +++ b/frontend/src/Editor/DragContainer.jsx @@ -529,7 +529,7 @@ export default function DragContainer({ isDraggingRef.current = false; } - if (draggedSubContainer) { + if (draggedSubContainer || !e.lastEvent) { return; } @@ -568,8 +568,8 @@ export default function DragContainer({ const _gridWidth = useGridStore.getState().subContainerWidths[draggedOverElemId] || gridWidth; const currentParentId = boxes.find(({ id: widgetId }) => e.target.id === widgetId)?.component?.parent; - let left = e.lastEvent.translate[0]; - let top = e.lastEvent.translate[1]; + let left = e.lastEvent?.translate[0]; + let top = e.lastEvent?.translate[1]; if (['Listview', 'Kanban'].includes(widgets[draggedOverElemId]?.component?.component)) { const elemContainer = e.target.closest('.real-canvas'); diff --git a/frontend/src/Editor/Editor.jsx b/frontend/src/Editor/Editor.jsx index e1e32ac9f7..ae0adffb0e 100644 --- a/frontend/src/Editor/Editor.jsx +++ b/frontend/src/Editor/Editor.jsx @@ -279,7 +279,6 @@ const EditorComponent = (props) => { if (didAppDefinitionChanged) { prevAppDefinition.current = appDefinition; } - if (mounted && didAppDefinitionChanged && currentPageId) { const components = appDefinition?.pages[currentPageId]?.components || {}; @@ -287,7 +286,7 @@ const EditorComponent = (props) => { if (appDiffOptions?.skipAutoSave === true || appDiffOptions?.entityReferenceUpdated === true) return; - handleLowPriorityWork(() => autoSave()); + handleLowPriorityWork(() => autoSave(), 100); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [JSON.stringify({ appDefinition, currentPageId, dataQueries })]); @@ -1968,9 +1967,11 @@ const EditorComponent = (props) => { } const handleCanvasContainerMouseUp = (e) => { + const selectedText = window.getSelection().toString(); if ( ['real-canvas', 'modal'].includes(e.target.className) && - useEditorStore.getState()?.selectedComponents?.length + useEditorStore.getState()?.selectedComponents?.length && + !selectedText ) { setSelectedComponents(EMPTY_ARRAY); } diff --git a/frontend/src/Editor/EditorSelecto.jsx b/frontend/src/Editor/EditorSelecto.jsx index b7c0d6495f..d68a30c3d6 100644 --- a/frontend/src/Editor/EditorSelecto.jsx +++ b/frontend/src/Editor/EditorSelecto.jsx @@ -107,6 +107,26 @@ const EditorSelecto = ({ selectionRef, canvasContainerRef, setSelectedComponent, onScroll={(e) => { canvasContainerRef.current.scrollBy(e.direction[0] * 10, e.direction[1] * 10); }} + dragCondition={(e) => { + // clear browser selection on drag + const selection = window.getSelection(); + if (selection) { + selection.removeAllRanges(); + } + const target = e.inputEvent.target; + if (target.getAttribute('id') === 'real-canvas') { + return true; + } + // if clicked on a component, select it and return false to prevent drag + if (target.closest('.moveable-box')) { + const closest = target.closest('.moveable-box'); + const id = closest.getAttribute('widgetid'); + const component = appDefinition.pages[currentPageId].components[id].component; + const isMultiSelect = e.inputEvent.shiftKey; + setSelectedComponent(id, component, isMultiSelect); + } + return false; + }} /> ); diff --git a/frontend/src/Editor/Inspector/Components/DefaultComponent.jsx b/frontend/src/Editor/Inspector/Components/DefaultComponent.jsx index f963fe908b..e4ddb9eda7 100644 --- a/frontend/src/Editor/Inspector/Components/DefaultComponent.jsx +++ b/frontend/src/Editor/Inspector/Components/DefaultComponent.jsx @@ -13,9 +13,11 @@ const SHOW_ADDITIONAL_ACTIONS = [ 'TextInput', 'NumberInput', 'PasswordInput', - 'Button', 'ToggleSwitchV2', 'Checkbox', + 'DropdownV2', + 'MultiselectV2', + 'Button', ]; const PROPERTIES_VS_ACCORDION_TITLE = { Text: 'Data', @@ -108,6 +110,8 @@ export const baseComponentProperties = ( 'Button', 'ToggleSwitchV2', 'Checkbox', + 'DropdownV2', + 'MultiselectV2', ], Layout: [], }; diff --git a/frontend/src/Editor/Inspector/Components/Select.jsx b/frontend/src/Editor/Inspector/Components/Select.jsx new file mode 100644 index 0000000000..3035047b61 --- /dev/null +++ b/frontend/src/Editor/Inspector/Components/Select.jsx @@ -0,0 +1,621 @@ +import React, { useState, useEffect } from 'react'; +import Accordion from '@/_ui/Accordion'; +import { EventManager } from '../EventManager'; +import { renderElement } from '../Utils'; +import OverlayTrigger from 'react-bootstrap/OverlayTrigger'; +import Popover from 'react-bootstrap/Popover'; +import List from '@/ToolJetUI/List/List'; +import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'; +import CodeHinter from '@/Editor/CodeEditor'; +import { resolveReferences } from '@/_helpers/utils'; +import AddNewButton from '@/ToolJetUI/Buttons/AddNewButton/AddNewButton'; +import ListGroup from 'react-bootstrap/ListGroup'; +import { ButtonSolid } from '@/_ui/AppButton/AppButton'; +import SortableList from '@/_components/SortableList'; +import Trash from '@/_ui/Icon/solidIcons/Trash'; + +export function Select({ componentMeta, darkMode, ...restProps }) { + const { + layoutPropertyChanged, + component, + dataQueries, + paramUpdated, + currentState, + eventsChanged, + apps, + allComponents, + pages, + } = restProps; + + const isMultiSelect = component?.component?.component === 'MultiselectV2'; + + const isDynamicOptionsEnabled = resolveReferences( + component?.component?.definition?.properties?.advanced?.value, + currentState + ); + + const constructOptions = () => { + const optionsValue = component?.component?.definition?.properties?.options?.value; + const valuesToResolve = ['label', 'value']; + let options = []; + + if (isDynamicOptionsEnabled || typeof optionsValue === 'string') { + options = resolveReferences(optionsValue, currentState); + } else { + options = optionsValue?.map((option) => { + const newOption = { ...option }; + + valuesToResolve.forEach((key) => { + if (option[key]) { + newOption[key] = resolveReferences(option[key], currentState); + } + }); + + return newOption; + }); + } + + return options.map((option) => { + const newOption = { ...option }; + + Object.keys(option).forEach((key) => { + if (typeof option[key]?.value === 'boolean') { + newOption[key]['value'] = `{{${option[key]?.value}}}`; + } + }); + + return newOption; + }); + }; + + const _markedAsDefault = resolveReferences( + component?.component?.definition?.properties[isMultiSelect ? 'values' : 'value']?.value, + currentState + ); + + const [options, setOptions] = useState([]); + const [markedAsDefault, setMarkedAsDefault] = useState(_markedAsDefault); + const [hoveredOptionIndex, setHoveredOptionIndex] = useState(null); + const validations = Object.keys(componentMeta.validation || {}); + let properties = []; + let additionalActions = []; + let optionsProperties = []; + + for (const [key] of Object.entries(componentMeta?.properties)) { + if (componentMeta?.properties[key]?.section === 'additionalActions') { + additionalActions.push(key); + } else if (componentMeta?.properties[key]?.accordian === 'Options') { + optionsProperties.push(key); + } else { + properties.push(key); + } + } + + const getItemStyle = (isDragging, draggableStyle) => ({ + userSelect: 'none', + ...draggableStyle, + }); + + const updateAllOptionsParams = (options, props) => { + paramUpdated({ name: 'options' }, 'value', options, 'properties', false, props); + }; + + const generateNewOptions = () => { + let found = false; + let label = ''; + let currentNumber = options.length + 1; + let value = currentNumber; + while (!found) { + label = `option${currentNumber}`; + value = currentNumber.toString(); + if (options.find((option) => option.label === label) === undefined) { + found = true; + } + currentNumber += 1; + } + return { + value, + label, + visible: { value: '{{true}}' }, + disable: { value: '{{false}}' }, + default: { value: '{{false}}' }, + }; + }; + + const handleAddOption = () => { + let _option = generateNewOptions(); + const _items = [...options, _option]; + setOptions(_items); + updateAllOptionsParams(_items); + }; + + const handleDeleteOption = (index) => { + const _items = options.filter((option, i) => i !== index); + setOptions(_items); + updateAllOptionsParams(_items, { isParamFromDropdownOptions: true }); + }; + + const handleLabelChange = (label, index) => { + const _options = options.map((option, i) => { + if (i === index) { + return { + ...option, + label, + }; + } + return option; + }); + setOptions(_options); + updateAllOptionsParams(_options); + }; + + const handleValueChange = (value, index) => { + const _options = options.map((option, i) => { + if (i === index) { + return { + ...option, + value, + }; + } + return option; + }); + setOptions(_options); + updateAllOptionsParams(_options); + }; + + const reorderOptions = async (startIndex, endIndex) => { + const result = [...options]; + const [removed] = result.splice(startIndex, 1); + result.splice(endIndex, 0, removed); + setOptions(result); + updateAllOptionsParams(result); + }; + + const onDragEnd = ({ source, destination }) => { + if (!destination || source?.index === destination?.index) { + return; + } + reorderOptions(source.index, destination.index); + }; + + const handleMarkedAsDefaultChange = (value, index) => { + const isMarkedAsDefault = resolveReferences(value, currentState); + if (isMultiSelect) { + const _value = options[index]?.value; + let _markedAsDefault = []; + if (isMarkedAsDefault && !markedAsDefault.includes(_value)) { + _markedAsDefault = [...markedAsDefault, _value]; + } else { + _markedAsDefault = markedAsDefault.filter((value) => value !== _value); + } + setMarkedAsDefault(_markedAsDefault); + paramUpdated({ name: 'values' }, 'value', _markedAsDefault, 'properties'); + } else { + const _value = isMarkedAsDefault ? options[index]?.value : ''; + const _options = options.map((option, i) => { + if (i === index) { + return { + ...option, + default: { + ...option.default, + value, + }, + }; + } else { + return { + ...option, + default: { + ...option.default, + value: `{{false}}`, + }, + }; + } + }); + setOptions(_options); + updateAllOptionsParams(_options); + setMarkedAsDefault(_value); + paramUpdated({ name: 'value' }, 'value', _value, 'properties'); + } + }; + + const handleVisibilityChange = (value, index) => { + const _options = options.map((option, i) => { + if (i === index) { + return { + ...option, + visible: { + ...option.visible, + value, + }, + }; + } + return option; + }); + setOptions(_options); + updateAllOptionsParams(_options); + }; + + const handleDisableChange = (value, index) => { + const _options = options.map((option, i) => { + if (i === index) { + return { + ...option, + disable: { + ...option.disable, + value, + }, + }; + } + return option; + }); + setOptions(_options); + updateAllOptionsParams(_options); + }; + + const handleOnFxPress = (active, index, key) => { + const _options = options.map((option, i) => { + if (i === index) { + return { + ...option, + [key]: { + ...option[key], + fxActive: active, + }, + }; + } + return option; + }); + setOptions(_options); + updateAllOptionsParams(_options); + }; + + useEffect(() => { + setOptions(constructOptions()); + }, [isMultiSelect]); + + const _renderOverlay = (item, index) => { + return ( + + +
+ + handleLabelChange(value, index)} + /> +
+
+ + handleValueChange(value, index)} + /> +
+
+ handleMarkedAsDefaultChange(value, index)} + onFxPress={(active) => handleOnFxPress(active, index, 'default')} + fxActive={item?.default?.fxActive} + fieldMeta={{ + type: 'toggle', + displayName: 'Make editable', + isFxNotRequired: true, + }} + paramType={'toggle'} + /> +
+
+ handleVisibilityChange(value, index)} + paramName={'visible'} + onFxPress={(active) => handleOnFxPress(active, index, 'visible')} + fxActive={item?.visible?.fxActive} + fieldMeta={{ + type: 'toggle', + displayName: 'Make editable', + }} + paramType={'toggle'} + /> +
+
+ handleDisableChange(value, index)} + onFxPress={(active) => handleOnFxPress(active, index, 'disable')} + fxActive={item?.disable?.fxActive} + fieldMeta={{ + type: 'toggle', + displayName: 'Make editable', + }} + paramType={'toggle'} + /> +
+
+
+ ); + }; + + const _renderOptions = () => { + return ( + + { + onDragEnd(result); + }} + > + + {({ innerRef, droppableProps, placeholder }) => ( +
+ {options?.map((item, index) => { + return ( + + {(provided, snapshot) => ( +
+ +
+ setHoveredOptionIndex(index)} + onMouseLeave={() => setHoveredOptionIndex(null)} + {...restProps} + > +
+
+ +
+
+ {item.label} +
+
+ {index === hoveredOptionIndex && ( + { + e.stopPropagation(); + handleDeleteOption(index); + }} + > + + + + + )} +
+
+
+
+
+
+ )} +
+ ); + })} + {placeholder} +
+ )} +
+
+ + Add new option + +
+ ); + }; + + let items = []; + + items.push({ + title: 'Data', + isOpen: true, + children: properties + .filter((property) => !optionsProperties.includes(property)) + ?.map((property) => + renderElement( + component, + componentMeta, + paramUpdated, + dataQueries, + property, + 'properties', + currentState, + allComponents, + darkMode + ) + ), + }); + + items.push({ + title: 'Options', + isOpen: true, + children: ( + <> + {renderElement( + component, + componentMeta, + paramUpdated, + dataQueries, + 'advanced', + 'properties', + currentState, + allComponents + )} + {isDynamicOptionsEnabled + ? renderElement( + component, + componentMeta, + paramUpdated, + dataQueries, + 'schema', + 'properties', + currentState, + allComponents + ) + : _renderOptions()} + {isDynamicOptionsEnabled && + renderElement( + component, + componentMeta, + paramUpdated, + dataQueries, + 'optionsLoadingState', + 'properties', + currentState, + allComponents + )} + {isMultiSelect && + renderElement( + component, + componentMeta, + paramUpdated, + dataQueries, + 'showAllOption', + 'properties', + currentState, + allComponents + )} + + ), + }); + + items.push({ + title: 'Events', + isOpen: true, + children: ( + + ), + }); + + items.push({ + title: 'Validation', + isOpen: true, + children: validations.map((property) => + renderElement( + component, + componentMeta, + paramUpdated, + dataQueries, + property, + 'validation', + currentState, + allComponents, + darkMode, + componentMeta.validation?.[property]?.placeholder + ) + ), + }); + + items.push({ + title: `Additional Actions`, + isOpen: true, + children: additionalActions.map((property) => { + return renderElement( + component, + componentMeta, + paramUpdated, + dataQueries, + property, + 'properties', + currentState, + allComponents, + darkMode, + componentMeta.properties?.[property]?.placeholder + ); + }), + }); + + items.push({ + title: 'Devices', + isOpen: true, + children: ( + <> + {renderElement( + component, + componentMeta, + layoutPropertyChanged, + dataQueries, + 'showOnDesktop', + 'others', + currentState, + allComponents + )} + {renderElement( + component, + componentMeta, + layoutPropertyChanged, + dataQueries, + 'showOnMobile', + 'others', + currentState, + allComponents + )} + + ), + }); + + return ; +} diff --git a/frontend/src/Editor/Inspector/Components/Table/NoListItem.jsx b/frontend/src/Editor/Inspector/Components/Table/NoListItem.jsx index 4f598e12e2..3b5f4b80b9 100644 --- a/frontend/src/Editor/Inspector/Components/Table/NoListItem.jsx +++ b/frontend/src/Editor/Inspector/Components/Table/NoListItem.jsx @@ -11,7 +11,6 @@ const NoListItem = ({ text, dataCy = '' }) => { borderRadius: '6px', border: '1px dashed var(--slate5)', color: 'var(--slate8)', - marginBottom: '8px', }} > diff --git a/frontend/src/Editor/Inspector/Components/Table/Table.jsx b/frontend/src/Editor/Inspector/Components/Table/Table.jsx index 5330445d11..3363ea4bda 100644 --- a/frontend/src/Editor/Inspector/Components/Table/Table.jsx +++ b/frontend/src/Editor/Inspector/Components/Table/Table.jsx @@ -18,7 +18,6 @@ import NoListItem from './NoListItem'; import { ProgramaticallyHandleProperties } from './ProgramaticallyHandleProperties'; import { ColumnPopoverContent } from './ColumnManager/ColumnPopover'; import { useAppDataStore } from '@/_stores/appDataStore'; - import { checkIfTableColumnDeprecated } from './ColumnManager/DeprecatedColumnTypeMsg'; const NON_EDITABLE_COLUMNS = ['link', 'image']; @@ -79,17 +78,23 @@ class TableComponent extends React.Component { } checkIfAllColumnsAreEditable = (component) => { - const isAllColumnsEditable = component.component?.definition?.properties?.columns?.value - ?.filter((column) => !NON_EDITABLE_COLUMNS.includes(column.columnType)) - .every((column) => resolveReferences(column.isEditable)); + const columns = component?.component?.definition?.properties?.columns?.value || []; + + const filteredColumns = columns.filter((column) => column && !NON_EDITABLE_COLUMNS.includes(column.columnType)); + + const isAllColumnsEditable = filteredColumns.every((column) => + resolveReferences(column.isEditable, this.props.currentState) + ); + return isAllColumnsEditable; }; componentDidUpdate(prevProps) { - const prevPropsColumns = prevProps?.component?.component.definition.properties.columns?.value; - const currentPropsColumns = this.props.component.component.definition.properties.columns?.value; + const prevPropsColumns = prevProps?.component?.component?.definition?.properties?.columns?.value || []; + const currentPropsColumns = this.props?.component?.component?.definition?.properties?.columns?.value || []; if (prevPropsColumns !== currentPropsColumns) { - const isAllColumnsEditable = currentPropsColumns + const filteredColumns = currentPropsColumns.filter((column) => column); + const isAllColumnsEditable = filteredColumns .filter((column) => !NON_EDITABLE_COLUMNS.includes(column.columnType)) .every((column) => resolveReferences(column.isEditable)); this.setState({ isAllColumnsEditable }); @@ -470,15 +475,17 @@ class TableComponent extends React.Component { handleMakeAllColumnsEditable = (value) => { const columns = resolveReferences(this.props.component.component.definition.properties.columns); + const columnValues = columns.value || []; - this.setState({ isAllColumnsEditable: resolveReferences(value) }); - - const newValue = columns.value.map((column) => ({ - ...column, - isEditable: !NON_EDITABLE_COLUMNS.includes(column.columnType) ? value : '{{false}}', - })); + const newValue = columnValues + .filter((column) => column) + .map((column) => ({ + ...column, + isEditable: !NON_EDITABLE_COLUMNS.includes(column.columnType) ? value : '{{false}}', + })); this.props.paramUpdated({ name: 'columns' }, 'value', newValue, 'properties', true); + this.setState({ isAllColumnsEditable: resolveReferences(value) }); }; duplicateColumn = (index) => { @@ -493,6 +500,9 @@ class TableComponent extends React.Component { render() { const { dataQueries, component, paramUpdated, componentMeta, components, currentState, darkMode } = this.props; const columns = component.component.definition.properties.columns; + + // Filter out null or undefined values before mapping + const filteredColumns = (columns.value || []).filter((column) => column); const actions = component.component.definition.properties.actions || { value: [] }; if (!component.component.definition.properties.displaySearchBox) paramUpdated({ name: 'displaySearchBox' }, 'value', true, 'properties'); @@ -564,7 +574,7 @@ class TableComponent extends React.Component { {({ innerRef, droppableProps, placeholder }) => (
- {columns.value.map((item, index) => { + {filteredColumns.map((item, index) => { const resolvedItemName = resolveReferences(item.name); const isEditable = resolveReferences(item.isEditable); const columnVisibility = item?.columnVisibility ?? true; diff --git a/frontend/src/Editor/Inspector/EventManager.jsx b/frontend/src/Editor/Inspector/EventManager.jsx index a1321a846d..e01d0c3e9a 100644 --- a/frontend/src/Editor/Inspector/EventManager.jsx +++ b/frontend/src/Editor/Inspector/EventManager.jsx @@ -1044,7 +1044,7 @@ export const EventManager = ({ const renderAddHandlerBtn = () => { return ( - + {t('editor.inspector.eventManager.addHandler', 'New event handler')} ); @@ -1054,7 +1054,7 @@ export const EventManager = ({ return ( <> {!hideEmptyEventsAlert && } - {renderAddHandlerBtn()} +
{renderAddHandlerBtn()}
); } @@ -1063,7 +1063,7 @@ export const EventManager = ({ if (events.length === 0) { return ( - <> +
{renderAddHandlerBtn()} {!hideEmptyEventsAlert ? (
@@ -1078,7 +1078,7 @@ export const EventManager = ({
) : null} - +
); } diff --git a/frontend/src/Editor/Inspector/Inspector.jsx b/frontend/src/Editor/Inspector/Inspector.jsx index 715bd3e6e5..98ffe9c5d2 100644 --- a/frontend/src/Editor/Inspector/Inspector.jsx +++ b/frontend/src/Editor/Inspector/Inspector.jsx @@ -33,6 +33,7 @@ import Copy from '@/_ui/Icon/solidIcons/Copy'; import Trash from '@/_ui/Icon/solidIcons/Trash'; import classNames from 'classnames'; import { useEditorStore, EMPTY_ARRAY } from '@/_stores/editorStore'; +import { Select } from './Components/Select'; import { deepClone } from '@/_helpers/utilities/utils.helpers'; const INSPECTOR_HEADER_OPTIONS = [ @@ -59,9 +60,11 @@ const NEW_REVAMPED_COMPONENTS = [ 'PasswordInput', 'NumberInput', 'Table', - 'Button', 'ToggleSwitchV2', 'Checkbox', + 'DropdownV2', + 'MultiselectV2', + 'Button', ]; export const Inspector = ({ @@ -168,7 +171,7 @@ export const Inspector = ({ return null; }; - function paramUpdated(param, attr, value, paramType, isParamFromTableColumn = false) { + function paramUpdated(param, attr, value, paramType, isParamFromTableColumn = false, props = {}) { let newComponent = JSON.parse(JSON.stringify(component)); let newDefinition = deepClone(newComponent.component.definition); let allParams = newDefinition[paramType] || {}; @@ -232,6 +235,67 @@ export const Inspector = ({ componentDefinitionChanged(newComponent, { componentPropertyUpdated: true, isParamFromTableColumn: isParamFromTableColumn, + ...props, + }); + } + + // use following function when more than one property needs to be updated + + function paramsUpdated(array, isParamFromTableColumn = false) { + let newComponent = JSON.parse(JSON.stringify(component)); + let newDefinition = _.cloneDeep(newComponent.component.definition); + array.map((item) => { + const { param, attr, value, paramType } = item; + let allParams = newDefinition[paramType] || {}; + const paramObject = allParams[param.name]; + if (!paramObject) { + allParams[param.name] = {}; + } + if (attr) { + allParams[param.name][attr] = value; + const defaultValue = getDefaultValue(value); + // This is needed to have enable pagination in Table as backward compatible + // Whenever enable pagination is false, we turn client and server side pagination as false + if ( + component.component.component === 'Table' && + param.name === 'enablePagination' && + !resolveReferences(value, currentState) + ) { + if (allParams?.['clientSidePagination']?.[attr]) { + allParams['clientSidePagination'][attr] = value; + } + if (allParams['serverSidePagination']?.[attr]) { + allParams['serverSidePagination'][attr] = value; + } + } + // This case is required to handle for older apps when serverSidePagination is connected to Fx + if (param.name === 'serverSidePagination' && !allParams?.['enablePagination']?.[attr]) { + allParams = { + ...allParams, + enablePagination: { + value: true, + }, + }; + } + if (param.type === 'select' && defaultValue) { + allParams[defaultValue.paramName]['value'] = defaultValue.value; + } + if (param.name === 'secondarySignDisplay') { + if (value === 'negative') { + newDefinition['styles']['secondaryTextColour']['value'] = '#EE2C4D'; + } else if (value === 'positive') { + newDefinition['styles']['secondaryTextColour']['value'] = '#36AF8B'; + } + } + } else { + allParams[param.name] = value; + } + newDefinition[paramType] = allParams; + newComponent.component.definition = newDefinition; + }); + componentDefinitionChanged(newComponent, { + componentPropertyUpdated: true, + isParamFromTableColumn, }); } @@ -325,6 +389,7 @@ export const Inspector = ({ layoutPropertyChanged={layoutPropertyChanged} component={component} paramUpdated={paramUpdated} + paramsUpdated={paramsUpdated} dataQueries={dataQueries} componentMeta={componentMeta} components={allComponents} @@ -475,11 +540,20 @@ export const Inspector = ({ ); }; const getDocsLink = (componentMeta) => { - return componentMeta.component == 'ToggleSwitchV2' - ? `https://docs.tooljet.io/docs/widgets/toggle-switch` - : `https://docs.tooljet.io/docs/widgets/${convertToKebabCase(componentMeta?.component ?? '')}`; + const component = componentMeta?.component ?? ''; + switch (component) { + case 'ToggleSwitchV2': + return 'https://docs.tooljet.io/docs/widgets/toggle-switch'; + case 'DropdownV2': + return 'https://docs.tooljet.com/docs/widgets/dropdown'; + case 'DropDown': + return 'https://docs.tooljet.com/docs/widgets/dropdown'; + case 'MultiselectV2': + return 'https://docs.tooljet.com/docs/widgets/multiselect'; + default: + return `https://docs.tooljet.io/docs/widgets/${convertToKebabCase(component)}`; + } }; - const widgetsWithStyleConditions = { Modal: { conditions: [ @@ -633,6 +707,10 @@ const GetAccordion = React.memo( case 'Form': return
; + case 'DropdownV2': + case 'MultiselectV2': + return {}; import { Button } from '@/_ui/LeftSidebar'; import Information from '@/_ui/Icon/solidIcons/Information'; import CodeHinter from '@/Editor/CodeEditor'; +const noop = () => {}; -export const Transformation = ({ changeOption, options, darkMode, queryId }) => { - const { t } = useTranslation(); - - const [lang, setLang] = React.useState(options?.transformationLanguage ?? 'javascript'); - - const defaultValue = { - javascript: `// write your code here +const defaultValue = { + javascript: `// write your code here // return value will be set as data and the original data will be available as rawData return data.filter(row => row.amount > 1000); - `, - python: `# write your code here + `, + python: `# write your code here # return value will be set as data and the original data will be available as rawData [row for row in data if row['amount'] > 1000] - `, - }; + `, +}; - const [enableTransformation, setEnableTransformation] = useState(() => options.enableTransformation); +const labelPopoverContent = (darkMode, t) => ( + +

+ {t( + 'editor.queryManager.transformation.transformationToolTip', + 'Transformations can be enabled on queries to transform the query results. ToolJet allows you to transform the query results using two programming languages: JavaScript and Python' + )} +
+ + {t('globals.readDocumentation', 'Read documentation')} + + . +

+
+); - const [state, setState] = useLocalStorageState('transformation', defaultValue); - - function toggleEnableTransformation() { - setEnableTransformation((prev) => !prev); - changeOption('enableTransformation', !enableTransformation); +const getNonActiveTransformations = (activeLang) => { + switch (activeLang) { + case 'javascript': + return { python: defaultValue.python }; + case 'python': + return { javascript: defaultValue.javascript }; + default: + return {}; } +}; + +const EducativeLabel = ({ darkMode }) => { + const popoverContent = ( + +
+ AI copilot +
+

ToolJet x OpenAI

+

+ AI copilot helps you write your queries + faster. It uses OpenAI's GPT-3.5 to suggest queries based on your data. +

+ +
+
+
+ ); + + return ( +
+ + + + + +
+ ); +}; + +export const Transformation = ({ changeOption, options, darkMode, queryId }) => { + const [lang, setLang] = useState(options?.transformationLanguage ?? 'javascript'); + const [enableTransformation, setEnableTransformation] = useState(options.enableTransformation); + const [state, setState] = useLocalStorageState('transformation', defaultValue); + const { t } = useTranslation(); useEffect(() => { if (lang !== (options.transformationLanguage ?? 'javascript')) { changeOption('transformationLanguage', lang); changeOption('transformation', state[lang]); } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [lang]); @@ -72,138 +135,90 @@ return data.filter(row => row.amount > 1000); // eslint-disable-next-line react-hooks/exhaustive-deps }, [queryId]); - function getNonActiveTransformations(activeLang) { - switch (activeLang) { - case 'javascript': - return { - python: defaultValue.python, - }; - case 'python': - return { - javascript: defaultValue.javascript, - }; - - default: - break; - } - } - - const computeSelectStyles = (darkMode, width) => { - return { - ...queryManagerSelectComponentStyle(darkMode, width), - control: (provided) => ({ - ...provided, - display: 'flex', - boxShadow: 'none', - backgroundColor: darkMode ? '#2b3547' : '#ffffff', - borderRadius: '0 6px 6px 0', - height: 32, - minHeight: 32, - borderWidth: '1px 1px 1px 0', - cursor: 'pointer', - borderColor: darkMode ? 'inherit' : ' #D7DBDF', - '&:hover': { - backgroundColor: darkMode ? '' : '#F8F9FA', - }, - '&:active': { - backgroundColor: darkMode ? '' : '#F8FAFF', - borderColor: '#3E63DD', - borderWidth: '1px 1px 1px 1px', - boxShadow: '0px 0px 0px 2px #C6D4F9 ', - }, - }), - }; + const toggleEnableTransformation = () => { + const newEnableTransformation = !enableTransformation; + setEnableTransformation(newEnableTransformation); + changeOption('enableTransformation', newEnableTransformation); }; - const labelPopoverContent = ( - -

- {t( - 'editor.queryManager.transformation.transformationToolTip', - 'Transformations can be enabled on queries to transform the query results. ToolJet allows you to transform the query results using two programming languages: JavaScript and Python' - )} -
- - {t('globals.readDocumentation', 'Read documentation')} - - . -

-
- ); - return ( -
-
-
- - - {t('editor.queryManager.transformation.transformations', 'Transformations')} - - -
-
-
- - - Enable +
+
+
+
+ + + + + {t('editor.queryManager.transformation.enableTransformation', 'Enable transformation')} -
- -
-
+ + +
+
+

Powered by AI copilot

+
-

-
-
- - {enableTransformation && ( -
+
+
+ {/*
*/} +
+
-
-
- Language -
-
-
URL
+
URL
{dataSourceURL && ( )} -
+
- -
- -
+
+
+
); diff --git a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/CreateRow.jsx b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/CreateRow.jsx index d621f22460..1f5eabf53a 100644 --- a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/CreateRow.jsx +++ b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/CreateRow.jsx @@ -46,7 +46,7 @@ export const CreateRow = React.memo(({ optionchanged, options, darkMode }) => { return (
-
+
diff --git a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DeleteRows.jsx b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DeleteRows.jsx index 9d9988121b..173ef906ee 100644 --- a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DeleteRows.jsx +++ b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DeleteRows.jsx @@ -133,7 +133,7 @@ const RenderFilterFields = ({ return (
-
+
-
+
{operator === 'is' ? (
-
+
)}
-
+
removeFilterConditionPair(id)} width="12" diff --git a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/UpdateRows.jsx b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/UpdateRows.jsx index 6e5e2f996d..48a6d0026c 100644 --- a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/UpdateRows.jsx +++ b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/UpdateRows.jsx @@ -185,7 +185,7 @@ const RenderFilterFields = ({ return (
-
+
-
+
{operator === 'is' ? (