diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c9297421a..01f749662 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,7 +18,7 @@ jobs: - name: Prettify code uses: creyD/prettier_action@v3.1 with: - prettier_options: --write packages/*/src/** ./*.js examples/**/* + prettier_options: --write packages/*/src/** packages/*/dev/** ./*.js examples/**/* prettier_version: 2.6.2 commit_message: 'Prettified code' env: diff --git a/.prettierrc.json b/.prettierrc.json index a52a3491b..2448074f4 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -6,5 +6,13 @@ "trailingComma": "es5", "bracketSpacing": true, "bracketSameLine": true, - "arrowParens": "avoid" + "arrowParens": "avoid", + "overrides": [ + { + "files": "examples/*/**", + "options": { + "printWidth": 80 + } + } + ] } diff --git a/examples/_template/src/index.scss b/examples/_template/src/index.scss index c76dbdb71..96dc3eca5 100644 --- a/examples/_template/src/index.scss +++ b/examples/_template/src/index.scss @@ -6,7 +6,8 @@ body { } code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; } // __SCSS_POST__ diff --git a/examples/antd/src/index.scss b/examples/antd/src/index.scss index e693fb993..a41767987 100644 --- a/examples/antd/src/index.scss +++ b/examples/antd/src/index.scss @@ -6,7 +6,8 @@ body { } code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; } .queryBuilder { diff --git a/examples/basic-ts/src/App.tsx b/examples/basic-ts/src/App.tsx index b46eb1285..0ec21d9a7 100644 --- a/examples/basic-ts/src/App.tsx +++ b/examples/basic-ts/src/App.tsx @@ -17,7 +17,11 @@ export const App = () => { return (
- setQuery(q)} /> + setQuery(q)} + />

Query

         {formatQuery(query, 'json')}
diff --git a/examples/basic-ts/src/index.scss b/examples/basic-ts/src/index.scss
index 8976afdcd..655a922b9 100644
--- a/examples/basic-ts/src/index.scss
+++ b/examples/basic-ts/src/index.scss
@@ -5,5 +5,6 @@ body {
 }
 
 code {
-  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
+  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+    monospace;
 }
diff --git a/examples/basic/src/App.jsx b/examples/basic/src/App.jsx
index 8e561fca1..37430e9b9 100644
--- a/examples/basic/src/App.jsx
+++ b/examples/basic/src/App.jsx
@@ -15,7 +15,11 @@ export const App = () => {
 
   return (
     
- setQuery(q)} /> + setQuery(q)} + />

Query

         {formatQuery(query, 'json')}
diff --git a/examples/basic/src/index.css b/examples/basic/src/index.css
index 695be9711..311f6d39f 100644
--- a/examples/basic/src/index.css
+++ b/examples/basic/src/index.css
@@ -5,5 +5,6 @@ body {
 }
 
 code {
-  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
+  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+    monospace;
 }
diff --git a/examples/bootstrap/src/index.scss b/examples/bootstrap/src/index.scss
index cdb432cd3..73426a509 100644
--- a/examples/bootstrap/src/index.scss
+++ b/examples/bootstrap/src/index.scss
@@ -7,7 +7,8 @@ body {
 }
 
 code {
-  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
+  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+    monospace;
 }
 
 .queryBuilder {
diff --git a/examples/bulma/src/index.scss b/examples/bulma/src/index.scss
index aa35d5d00..d1a4a667c 100644
--- a/examples/bulma/src/index.scss
+++ b/examples/bulma/src/index.scss
@@ -8,7 +8,8 @@ body {
 }
 
 code {
-  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
+  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+    monospace;
 }
 
 .queryBuilder {
diff --git a/examples/chakra/src/index.scss b/examples/chakra/src/index.scss
index cb4407b25..7ca0d9daa 100644
--- a/examples/chakra/src/index.scss
+++ b/examples/chakra/src/index.scss
@@ -5,7 +5,8 @@ body {
 }
 
 code {
-  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
+  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+    monospace;
 }
 
 .queryBuilder {
diff --git a/examples/ci/index.html b/examples/ci/index.html
index 8e069ad58..7d9fb7241 100644
--- a/examples/ci/index.html
+++ b/examples/ci/index.html
@@ -2,7 +2,9 @@
 
   
     
-    
+    
     
     React Query Builder CI
   
diff --git a/examples/ci/src/App.tsx b/examples/ci/src/App.tsx
index dce2c4157..f6173636a 100644
--- a/examples/ci/src/App.tsx
+++ b/examples/ci/src/App.tsx
@@ -1,17 +1,32 @@
 import { useReducer, useState } from 'react';
-import type { DefaultRuleGroupType, QueryBuilderProps } from 'react-querybuilder';
-import { defaultValidator, formatQuery, QueryBuilder } from 'react-querybuilder';
+import type {
+  DefaultRuleGroupType,
+  QueryBuilderProps,
+} from 'react-querybuilder';
+import {
+  defaultValidator,
+  formatQuery,
+  QueryBuilder,
+} from 'react-querybuilder';
 import { fields } from './fields';
 import './index.scss';
 import { initialQuery, initialQueryIC } from './initialQuery';
-import type { DefaultQBPropsNoDefaultQuery, DefaultQBPropsNoDefaultQueryIC } from './types';
+import type {
+  DefaultQBPropsNoDefaultQuery,
+  DefaultQBPropsNoDefaultQueryIC,
+} from './types';
 import { defaultOptions, optionsOrder, optionsReducer } from './utils';
 
 export const App = () => {
   const [query, setQuery] = useState(initialQuery);
   const [queryIC, setQueryIC] = useState(initialQueryIC);
   const [options, dispatch] = useReducer(optionsReducer, defaultOptions);
-  const { useValidation, independentCombinators, parseNumbers, ...commonOptions } = options;
+  const {
+    useValidation,
+    independentCombinators,
+    parseNumbers,
+    ...commonOptions
+  } = options;
   const commonProps: QueryBuilderProps = {
     fields,
     ...commonOptions,
@@ -46,7 +61,10 @@ export const App = () => {
               type="checkbox"
               checked={options[optionName]}
               onChange={e =>
-                dispatch({ type: 'update', payload: { optionName, value: e.target.checked } })
+                dispatch({
+                  type: 'update',
+                  payload: { optionName, value: e.target.checked },
+                })
               }
             />
             {optionName}
@@ -58,7 +76,12 @@ export const App = () => {
       
       
         {JSON.stringify(
-          JSON.parse(formatQuery(queryForFormatting, { format: 'json_without_ids', parseNumbers })),
+          JSON.parse(
+            formatQuery(queryForFormatting, {
+              format: 'json_without_ids',
+              parseNumbers,
+            })
+          ),
           null,
           2
         )}
@@ -66,7 +89,10 @@ export const App = () => {
       
Parameterized SQL
         {JSON.stringify(
-          formatQuery(queryForFormatting, { format: 'parameterized', parseNumbers }),
+          formatQuery(queryForFormatting, {
+            format: 'parameterized',
+            parseNumbers,
+          }),
           null,
           2
         )}
@@ -74,23 +100,37 @@ export const App = () => {
       
Parameterized (Named) SQL
         {JSON.stringify(
-          formatQuery(queryForFormatting, { format: 'parameterized_named', parseNumbers }),
+          formatQuery(queryForFormatting, {
+            format: 'parameterized_named',
+            parseNumbers,
+          }),
           null,
           2
         )}
       
SQL
-
{formatQuery(queryForFormatting, { format: 'sql', parseNumbers })}
+
+        {formatQuery(queryForFormatting, { format: 'sql', parseNumbers })}
+      
MongoDB
-
{formatQuery(queryForFormatting, { format: 'mongodb', parseNumbers })}
+
+        {formatQuery(queryForFormatting, { format: 'mongodb', parseNumbers })}
+      
CEL
-
{formatQuery(queryForFormatting, { format: 'cel', parseNumbers })}
+
+        {formatQuery(queryForFormatting, { format: 'cel', parseNumbers })}
+      
SpEL
-
{formatQuery(queryForFormatting, { format: 'spel', parseNumbers })}
+
+        {formatQuery(queryForFormatting, { format: 'spel', parseNumbers })}
+      
JsonLogic
         {JSON.stringify(
-          formatQuery(queryForFormatting, { format: 'jsonlogic', parseNumbers }),
+          formatQuery(queryForFormatting, {
+            format: 'jsonlogic',
+            parseNumbers,
+          }),
           null,
           2
         )}
diff --git a/examples/ci/src/utils.ts b/examples/ci/src/utils.ts
index 0edb2165f..304f4f48d 100644
--- a/examples/ci/src/utils.ts
+++ b/examples/ci/src/utils.ts
@@ -34,7 +34,10 @@ export const optionsOrder: CIOption[] = [
   'parseNumbers',
 ];
 
-export const optionsReducer = (state: CIOptions, action: CIOptionsAction): CIOptions => {
+export const optionsReducer = (
+  state: CIOptions,
+  action: CIOptionsAction
+): CIOptions => {
   const { optionName, value } = action.payload;
   return { ...state, [optionName]: value };
 };
diff --git a/examples/generateExamples.mjs b/examples/generateExamples.mjs
index a6897ea39..ced94b64a 100644
--- a/examples/generateExamples.mjs
+++ b/examples/generateExamples.mjs
@@ -151,6 +151,7 @@ for (const exampleID in configs) {
     const fileContents = (await readFile(filePath)).toString('utf-8');
     const prettified = prettier.format(fileContents, {
       ...prettierConfig,
+      printWidth: 80, // narrower since codesandbox code panel is narrow
       filepath: filePath,
       plugins: ['prettier-plugin-organize-imports'],
     });
diff --git a/examples/material/src/index.scss b/examples/material/src/index.scss
index 8976afdcd..655a922b9 100644
--- a/examples/material/src/index.scss
+++ b/examples/material/src/index.scss
@@ -5,5 +5,6 @@ body {
 }
 
 code {
-  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
+  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+    monospace;
 }
diff --git a/package.json b/package.json
index 4bcf48de7..ff16b3f24 100644
--- a/package.json
+++ b/package.json
@@ -29,7 +29,7 @@
     "test:watch": "jest --watch",
     "type-check": "yarn workspaces run type-check && tsc -p examples",
     "publish:demo": "yarn build && node gh-pages.publish.js",
-    "pretty-print": "prettier --write packages/*/src/** ./*.js examples/**/*",
+    "pretty-print": "prettier --write packages/*/src/** packages/*/dev/** ./*.js examples/**/*",
     "generate-changelog": "github-changes --owner react-querybuilder --repository react-querybuilder --auth --use-commit-body --only-pulls --date-format=\"(YYYY-MM-DD)\"",
     "generate-examples": "node ./examples/generateExamples.mjs"
   },
diff --git a/packages/antd/dev/main.tsx b/packages/antd/dev/main.tsx
index 63c1208ea..ead034e51 100644
--- a/packages/antd/dev/main.tsx
+++ b/packages/antd/dev/main.tsx
@@ -1,8 +1,8 @@
+import 'antd/dist/antd.compact.css';
 import { StrictMode } from 'react';
 import { createRoot } from 'react-dom/client';
 import { App } from 'react-querybuilder/dev';
 import { antdControlElements } from '../src';
-import 'antd/dist/antd.compact.css';
 import './styles.scss';
 
 createRoot(document.getElementById('app')!).render(
diff --git a/packages/antd/dev/styles.scss b/packages/antd/dev/styles.scss
index 4ab0a0153..24af87637 100644
--- a/packages/antd/dev/styles.scss
+++ b/packages/antd/dev/styles.scss
@@ -1,3 +1,3 @@
-body {
-  margin: 1rem;
+.ant-input {
+  width: auto;
 }
diff --git a/packages/antd/src/AntD.test.tsx b/packages/antd/src/AntD.test.tsx
index 7ab17ca59..5d31b4260 100644
--- a/packages/antd/src/AntD.test.tsx
+++ b/packages/antd/src/AntD.test.tsx
@@ -1,22 +1,20 @@
-import { render, screen, within } from '@testing-library/react';
+import { render, screen } from '@testing-library/react';
+import type { SelectProps } from 'antd';
+import type { OptionProps } from 'antd/lib/select';
 import moment from 'moment';
+import type { OptionGroupFC } from 'rc-select/lib/OptGroup';
 import {
   defaultNotToggleProps,
   defaultValueEditorProps,
-  defaultValueSelectorProps,
   findInput,
   hasOrInheritsClass,
   testActionElement,
   testDragHandle,
   testValueEditor,
+  testValueSelector,
   userEventSetup,
 } from 'react-querybuilder/genericTests';
-import type {
-  NameLabelPair,
-  NotToggleProps,
-  ValueEditorProps,
-  ValueSelectorProps,
-} from 'react-querybuilder/src';
+import type { NotToggleProps, ValueEditorProps } from 'react-querybuilder/src';
 import {
   AntDActionElement,
   AntDDragHandle,
@@ -25,96 +23,39 @@ import {
   AntDValueSelector,
 } from '.';
 
-const antdValueSelectorProps: ValueSelectorProps = {
-  ...defaultValueSelectorProps,
-  title: AntDValueSelector.displayName,
-};
-const antdValueEditorProps: ValueEditorProps = {
-  ...defaultValueEditorProps,
-  type: 'select',
-  title: AntDValueEditor.displayName,
-  values: defaultValueSelectorProps.options,
-};
-
-const testAntDValueSelector = (
-  title: string,
-  Component: React.ComponentType | React.ComponentType,
-  props: any
-) => {
-  const user = userEventSetup();
-  const testValues: NameLabelPair[] = props.values ?? props.options;
-  const testVal = testValues[1];
-
-  describe(title, () => {
-    it('should render the correct number of options', async () => {
-      render();
-      await user.click(screen.getByRole('combobox'));
-      const listbox = within(screen.getByRole('listbox'));
-      expect(listbox.getAllByRole('option')).toHaveLength(2);
-    });
-
-    it('should have the options passed into the ', () => {
-        const value = testValues.map(v => v.name).join(',');
-        const multiselectProps = 'values' in props ? { type: 'multiselect' } : { multiple: true };
-        render();
-        expect(
-          screen.getByTitle(props.title).querySelectorAll('.ant-select-selection-item-content')
-        ).toHaveLength(testValues.length);
-      });
-    }
-
-    if (('values' in props && props.type !== 'multiselect') || 'options' in props) {
-      it('should have the value passed into the ', () => {
-      render();
-      expect(screen.getByTitle(props.title)).toHaveClass('foo');
-    });
-
-    it('should render optgroups', async () => {
-      const optGroups = [
-        { label: 'Test Option Group', options: 'values' in props ? props.values : props.options },
-      ];
-      const newProps =
-        'values' in props ? { ...props, values: optGroups } : { ...props, options: optGroups };
-      render();
-      await user.click(screen.getByRole('combobox'));
-      const listbox = within(screen.getByRole('listbox'));
-      expect(listbox.getAllByRole('option').pop()).toHaveTextContent(testVal.name);
-    });
-
-    it('should be disabled by the disabled prop', async () => {
-      const handleOnChange = jest.fn();
-      render();
-      expect(screen.getByRole('combobox')).toBeDisabled();
-      await user.click(screen.getByRole('combobox'));
-      expect(() => screen.getByRole('listbox')).toThrow();
-      expect(handleOnChange).not.toHaveBeenCalled();
-    });
-  });
-};
+jest.mock('antd', () => {
+  // We only mock Select. Everything else can use the real antd components.
+  const AntD = jest.requireActual('antd');
+
+  const Select = (props: SelectProps) => (
+    
+  );
+  Select.Option = ({ value, children }: OptionProps) => ;
+  Select.OptGroup = (({ label, children }) => (
+    {children}
+  )) as OptionGroupFC;
+
+  return { ...AntD, Select };
+});
 
+const valueEditorTitle = AntDValueEditor.displayName;
 const notToggleTitle = AntDNotToggle.displayName;
+
 describe(notToggleTitle, () => {
   const user = userEventSetup();
   const label = 'Not';
@@ -146,7 +87,6 @@ describe(notToggleTitle, () => {
   });
 });
 
-const valueEditorTitle = AntDValueEditor.displayName;
 describe(`${valueEditorTitle} as switch`, () => {
   const user = userEventSetup();
   const props: ValueEditorProps = {
@@ -284,16 +224,5 @@ describe(`${valueEditorTitle} date/time pickers`, () => {
 
 testActionElement(AntDActionElement);
 testDragHandle(AntDDragHandle);
-testValueEditor(AntDValueEditor, { multiselect: true, select: true, switch: true });
-const valueEditorAsSelectTitle = `${antdValueEditorProps.title} (as ValueSelector)`;
-testAntDValueSelector(valueEditorAsSelectTitle, AntDValueEditor, {
-  ...antdValueEditorProps,
-  title: valueEditorAsSelectTitle,
-});
-const valueEditorAsMultiselectTitle = `${antdValueEditorProps.title} (as ValueSelector multiselect)`;
-testAntDValueSelector(valueEditorAsMultiselectTitle, AntDValueEditor, {
-  ...antdValueEditorProps,
-  title: valueEditorAsMultiselectTitle,
-  type: 'multiselect',
-});
-testAntDValueSelector(antdValueSelectorProps.title!, AntDValueSelector, antdValueSelectorProps);
+testValueEditor(AntDValueEditor, { switch: true });
+testValueSelector(AntDValueSelector);
diff --git a/packages/antd/src/AntDValueEditor.tsx b/packages/antd/src/AntDValueEditor.tsx
index 20471543f..9b3c37da0 100644
--- a/packages/antd/src/AntDValueEditor.tsx
+++ b/packages/antd/src/AntDValueEditor.tsx
@@ -1,7 +1,13 @@
 import { Checkbox, DatePicker, Input, Radio, Switch, TimePicker } from 'antd';
 import moment from 'moment';
-import { useEffect } from 'react';
-import type { ValueEditorProps } from 'react-querybuilder';
+import {
+  joinWith,
+  splitBy,
+  standardClassnames,
+  toArray,
+  useValueEditor,
+  type ValueEditorProps,
+} from 'react-querybuilder';
 import { AntDValueSelector } from './AntDValueSelector';
 
 export const AntDValueEditor = ({
@@ -13,21 +19,14 @@ export const AntDValueEditor = ({
   className,
   type,
   inputType,
-  values,
+  values = [],
+  listsAsArrays,
   valueSource: _vs,
   disabled,
+  testID,
   ...props
 }: ValueEditorProps) => {
-  useEffect(() => {
-    if (
-      inputType === 'number' &&
-      !['between', 'notBetween', 'in', 'notIn'].includes(operator) &&
-      typeof value === 'string' &&
-      value.includes(',')
-    ) {
-      handleOnChange('');
-    }
-  }, [inputType, operator, value, handleOnChange]);
+  useValueEditor({ handleOnChange, inputType, operator, value });
 
   if (operator === 'null' || operator === 'notNull') {
     return null;
@@ -42,6 +41,40 @@ export const AntDValueEditor = ({
       ? 'text'
       : inputType || 'text';
 
+  if ((operator === 'between' || operator === 'notBetween') && type === 'select') {
+    const valArray = toArray(value);
+    const selector1handler = (v: string) => {
+      const val = [v, valArray[1] ?? values[0]?.name, ...valArray.slice(2)];
+      handleOnChange(listsAsArrays ? val : joinWith(val, ','));
+    };
+    const selector2handler = (v: string) => {
+      const val = [valArray[0], v, ...valArray.slice(2)];
+      handleOnChange(listsAsArrays ? val : joinWith(val, ','));
+    };
+    return (
+      
+        
+        
+      
+    );
+  }
+
   switch (type) {
     case 'select':
     case 'multiselect':
@@ -50,11 +83,12 @@ export const AntDValueEditor = ({
           {...props}
           className={className}
           handleOnChange={handleOnChange}
-          options={values!}
+          options={values}
           value={value}
           title={title}
           disabled={disabled}
           multiple={type === 'multiselect'}
+          listsAsArrays={listsAsArrays}
         />
       );
 
@@ -96,7 +130,7 @@ export const AntDValueEditor = ({
     case 'radio':
       return (
         
-          {values!.map(v => (
+          {values.map(v => (
              moment(v)) as [moment.Moment, moment.Moment])
+              ? (splitBy(value, ',').map(v => moment(v)) as [moment.Moment, moment.Moment])
               : undefined
           }
           showTime={inputTypeCoerced === 'datetime-local'}
diff --git a/packages/antd/src/AntDValueSelector.tsx b/packages/antd/src/AntDValueSelector.tsx
index 31044e2d1..9c17f6b79 100644
--- a/packages/antd/src/AntDValueSelector.tsx
+++ b/packages/antd/src/AntDValueSelector.tsx
@@ -1,6 +1,6 @@
 import { Select } from 'antd';
 import { useMemo, type ComponentPropsWithoutRef } from 'react';
-import type { VersatileSelectorProps } from 'react-querybuilder';
+import { joinWith, splitBy, type VersatileSelectorProps } from 'react-querybuilder';
 import { toOptions } from './utils';
 
 type AntDValueSelectorProps = VersatileSelectorProps &
@@ -14,6 +14,7 @@ export const AntDValueSelector = ({
   title,
   disabled,
   multiple,
+  listsAsArrays,
   // Props that should not be in extraProps
   testID: _testID,
   rules: _rules,
@@ -26,18 +27,25 @@ export const AntDValueSelector = ({
   fieldData: _fieldData,
   ...extraProps
 }: AntDValueSelectorProps) => {
-  const onChange = useMemo(() => {
-    if (multiple) {
-      return (v: string | string[]) =>
-        handleOnChange(Array.isArray(v) ? v.join(',') : /* istanbul ignore next */ v);
-    }
-    return (v: string) => handleOnChange(v);
-  }, [handleOnChange, multiple]);
+  const onChange = useMemo(
+    () =>
+      multiple
+        ? (v: string | string[]) =>
+            handleOnChange(
+              Array.isArray(v)
+                ? listsAsArrays
+                  ? v
+                  : joinWith(v, ',')
+                : /* istanbul ignore next */ v
+            )
+        : (v: string) => handleOnChange(v),
+    [handleOnChange, listsAsArrays, multiple]
+  );
 
   const val = multiple
     ? Array.isArray(value)
       ? /* istanbul ignore next */ value
-      : value?.split(',')
+      : splitBy(value, ',')
     : value;
 
   const modeObj = multiple ? { mode: 'multiple' as const } : {};
diff --git a/packages/bootstrap/dev/styles.scss b/packages/bootstrap/dev/styles.scss
index 3b7937fcc..0ee5b1afa 100644
--- a/packages/bootstrap/dev/styles.scss
+++ b/packages/bootstrap/dev/styles.scss
@@ -2,10 +2,6 @@ $code-color: #333333;
 
 @import 'bootstrap/scss/bootstrap.scss';
 
-body {
-  margin: 1rem;
-}
-
 #app > div:first-child > label > input {
   margin: 3px;
 }
diff --git a/packages/bootstrap/src/BootstrapValueEditor.tsx b/packages/bootstrap/src/BootstrapValueEditor.tsx
index 8390e1ac1..bfe5cc4a1 100644
--- a/packages/bootstrap/src/BootstrapValueEditor.tsx
+++ b/packages/bootstrap/src/BootstrapValueEditor.tsx
@@ -1,5 +1,11 @@
-import { useEffect } from 'react';
-import { ValueSelector, type ValueEditorProps } from 'react-querybuilder';
+import {
+  joinWith,
+  standardClassnames as sc,
+  toArray,
+  useValueEditor,
+  ValueSelector,
+  type ValueEditorProps,
+} from 'react-querybuilder';
 
 export const BootstrapValueEditor = ({
   fieldData,
@@ -10,20 +16,13 @@ export const BootstrapValueEditor = ({
   className,
   type,
   inputType,
-  values,
+  values = [],
+  listsAsArrays,
   disabled,
+  testID,
   ...props
 }: ValueEditorProps) => {
-  useEffect(() => {
-    if (
-      inputType === 'number' &&
-      !['between', 'notBetween', 'in', 'notIn'].includes(operator) &&
-      typeof value === 'string' &&
-      value.includes(',')
-    ) {
-      handleOnChange('');
-    }
-  }, [inputType, operator, value, handleOnChange]);
+  useValueEditor({ handleOnChange, inputType, operator, value });
 
   if (operator === 'null' || operator === 'notNull') {
     return null;
@@ -34,6 +33,40 @@ export const BootstrapValueEditor = ({
     ? 'text'
     : inputType || 'text';
 
+  if ((operator === 'between' || operator === 'notBetween') && type === 'select') {
+    const valArray = toArray(value);
+    const selector1handler = (v: string) => {
+      const val = [v, valArray[1] ?? values[0]?.name, ...valArray.slice(2)];
+      handleOnChange(listsAsArrays ? val : joinWith(val, ','));
+    };
+    const selector2handler = (v: string) => {
+      const val = [valArray[0], v, ...valArray.slice(2)];
+      handleOnChange(listsAsArrays ? val : joinWith(val, ','));
+    };
+    return (
+      
+        
+        
+      
+    );
+  }
+
   switch (type) {
     case 'select':
     case 'multiselect':
@@ -46,7 +79,8 @@ export const BootstrapValueEditor = ({
           value={value}
           disabled={disabled}
           multiple={type === 'multiselect'}
-          options={values!}
+          listsAsArrays={listsAsArrays}
+          options={values}
         />
       );
 
@@ -91,7 +125,7 @@ export const BootstrapValueEditor = ({
     case 'radio':
       return (
         
-          {values!.map(v => (
+          {values.map(v => (
             
{ - useEffect(() => { - if ( - inputType === 'number' && - !['between', 'notBetween', 'in', 'notIn'].includes(operator) && - typeof value === 'string' && - value.includes(',') - ) { - handleOnChange(''); - } - }, [inputType, operator, value, handleOnChange]); + useValueEditor({ handleOnChange, inputType, operator, value }); if (operator === 'null' || operator === 'notNull') { return null; @@ -35,6 +33,40 @@ export const BulmaValueEditor = ({ ? 'text' : inputType || 'text'; + if ((operator === 'between' || operator === 'notBetween') && type === 'select') { + const valArray = toArray(value); + const selector1handler = (v: string) => { + const val = [v, valArray[1] ?? values[0]?.name, ...valArray.slice(2)]; + handleOnChange(listsAsArrays ? val : joinWith(val, ',')); + }; + const selector2handler = (v: string) => { + const val = [valArray[0], v, ...valArray.slice(2)]; + handleOnChange(listsAsArrays ? val : joinWith(val, ',')); + }; + return ( + + + + + ); + } + switch (type) { case 'select': case 'multiselect': @@ -44,10 +76,11 @@ export const BulmaValueEditor = ({ title={title} className={className} handleOnChange={handleOnChange} - options={values!} + options={values} value={value} disabled={disabled} multiple={type === 'multiselect'} + listsAsArrays={listsAsArrays} /> ); @@ -81,7 +114,7 @@ export const BulmaValueEditor = ({ case 'radio': return (
- {values!.map(v => ( + {values.map(v => ( - ))} + {values.map(v => ( + + ))} ); } diff --git a/packages/react-querybuilder/src/controls/ValueSelector.tsx b/packages/react-querybuilder/src/controls/ValueSelector.tsx index 1e62a2bdd..f5b46e54d 100644 --- a/packages/react-querybuilder/src/controls/ValueSelector.tsx +++ b/packages/react-querybuilder/src/controls/ValueSelector.tsx @@ -1,6 +1,6 @@ import { useMemo, type ChangeEvent } from 'react'; import type { ValueSelectorProps } from '../types'; -import { toOptions } from '../utils'; +import { joinWith, toArray, toOptions } from '../utils'; export const ValueSelector = ({ className, @@ -9,27 +9,26 @@ export const ValueSelector = ({ title, value, multiple, + listsAsArrays, disabled, testID, }: ValueSelectorProps) => { - const onChange = useMemo(() => { - if (multiple) { - return (e: ChangeEvent) => - handleOnChange( - [...e.target.options] - .filter(o => o.selected) - .map(o => o.value) - .join(',') - ); - } - return (e: ChangeEvent) => handleOnChange(e.target.value); - }, [handleOnChange, multiple]); + const onChange = useMemo( + () => + multiple + ? (e: ChangeEvent) => { + const valArray = Array.from(e.target.selectedOptions).map(o => o.value); + handleOnChange(listsAsArrays ? valArray : joinWith(valArray, ',')); + } + : (e: ChangeEvent) => handleOnChange(e.target.value), + [handleOnChange, listsAsArrays, multiple] + ); return (