Skip to content

Commit 57f5234

Browse files
Vojtěch Václav Portešvojtechportes
authored andcommitted
feat: Added query text editor
- SQL text editor - Optional monaco editor to support more advanced use cases - text editor validation - Updated website - Updated readme
1 parent 8b2c644 commit 57f5234

114 files changed

Lines changed: 5059 additions & 220 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ React Query Builder is a TypeScript library for building nested filter editors,
2525
formatting them into external query syntaxes, and parsing supported expressions
2626
back into builder state.
2727

28+
It also supports an optional SQL text mode for `Builder`, with built-in syntax
29+
and semantic validation, plus an optional Monaco-based advanced editor
30+
integration for protected locked ranges.
31+
2832
Full documentation, API reference, and the interactive demo are available on
2933
the project website:
3034

@@ -164,6 +168,30 @@ export const MyBuilder = () => {
164168
};
165169
```
166170

171+
## Text Mode
172+
173+
`Builder` can optionally switch between the visual query UI and a SQL text
174+
editor view.
175+
176+
```tsx
177+
<Builder
178+
fields={fields}
179+
data={data}
180+
textMode
181+
defaultMode="text"
182+
onChange={setData}
183+
/>;
184+
```
185+
186+
For advanced text editing, the package also exposes
187+
`@vojtechportes/react-query-builder/monaco`. The built-in editor is lightweight
188+
and works without extra dependencies, while the Monaco integration is the
189+
recommended path when locked query segments must stay protected in text mode.
190+
191+
- <a href="https://vojtechportes.github.io/react-query-builder/documentation/text-mode" target="_blank" rel="noopener noreferrer">Documentation: Text Mode</a>
192+
- <a href="https://vojtechportes.github.io/react-query-builder/api/builder" target="_blank" rel="noopener noreferrer">API: Builder</a>
193+
- <a href="https://vojtechportes.github.io/react-query-builder/api/components" target="_blank" rel="noopener noreferrer">API: Components</a>
194+
167195
## Query Conversion
168196

169197
The library also provides parser and formatter helpers through subpath exports:

example/src/app/app.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export const App: React.FC = () => (
5353
<Route path="/documentation/validation" element={<DocumentationPage />} />
5454
<Route path="/documentation/history" element={<DocumentationPage />} />
5555
<Route path="/documentation/builder-behavior" element={<DocumentationPage />} />
56+
<Route path="/documentation/text-mode" element={<DocumentationPage />} />
5657
<Route
5758
path="/documentation/locking-and-read-only"
5859
element={<DocumentationPage />}

example/src/components/demo-playground.tsx

Lines changed: 91 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
type DenormalizedQuery,
66
} from '@vojtechportes/react-query-builder';
77
import { components as antdComponents } from '@vojtechportes/react-query-builder/antd/v6';
8+
import { createMonacoComponents } from '@vojtechportes/react-query-builder/monaco';
89
import { components as muiComponents } from '@vojtechportes/react-query-builder/mui/v9';
910
import type { IColors } from '../../../src/constants/colors';
1011
import { ThemeProvider } from '../../../src/theme-provider/theme-provider';
@@ -54,17 +55,26 @@ const PanelTitle = styled.h3`
5455
color: #0f172a;
5556
`;
5657

57-
const ToggleRow = styled.label`
58+
const ToggleRow = styled.label<{ $disabled?: boolean }>`
5859
display: flex;
5960
align-items: center;
6061
gap: 0.75rem;
6162
font-size: 0.95rem;
6263
color: #334155;
64+
cursor: ${({ $disabled }) => ($disabled ? 'not-allowed' : 'pointer')};
6365
`;
6466

65-
const Toggle = styled.input`
67+
const Toggle = styled.input<{ $disabled?: boolean }>`
6668
width: 18px;
6769
height: 18px;
70+
margin: 0;
71+
accent-color: #2563eb;
72+
cursor: ${({ $disabled }) => ($disabled ? 'not-allowed' : 'pointer')};
73+
flex: 0 0 18px;
74+
75+
&:disabled {
76+
opacity: 1;
77+
}
6878
`;
6979

7080
const ChoiceGroup = styled.div`
@@ -152,6 +162,9 @@ export const DemoPlayground: React.FC<IDemoPlaygroundProps> = ({
152162
const [cloneable, setCloneable] = React.useState(false);
153163
const [draggable, setDraggable] = React.useState(false);
154164
const [history, setHistory] = React.useState(false);
165+
const [textMode, setTextMode] = React.useState(false);
166+
const [defaultMode, setDefaultMode] = React.useState<'builder' | 'text'>('builder');
167+
const [useMonacoTextEditor, setUseMonacoTextEditor] = React.useState(false);
155168
const [singleRootGroup, setSingleRootGroup] = React.useState(true);
156169
const [showValidation, setShowValidation] = React.useState(true);
157170
const [customizationMode, setCustomizationMode] =
@@ -164,6 +177,27 @@ export const DemoPlayground: React.FC<IDemoPlaygroundProps> = ({
164177
const isAntdMode = customizationMode === 'antd';
165178
const usesAdapterMode = isMuiMode || isAntdMode;
166179

180+
React.useEffect(() => {
181+
if (!singleRootGroup && textMode) {
182+
setTextMode(false);
183+
}
184+
}, [singleRootGroup, textMode]);
185+
186+
const builderComponents = React.useMemo(() => {
187+
const baseComponents =
188+
customizationMode === 'mui'
189+
? muiComponents
190+
: customizationMode === 'antd'
191+
? antdComponents
192+
: undefined;
193+
194+
if (!useMonacoTextEditor) {
195+
return baseComponents;
196+
}
197+
198+
return createMonacoComponents(baseComponents || {});
199+
}, [customizationMode, useMonacoTextEditor]);
200+
167201
const builderProps = {
168202
data,
169203
fields: demoFields,
@@ -173,9 +207,12 @@ export const DemoPlayground: React.FC<IDemoPlaygroundProps> = ({
173207
onChange: setData,
174208
draggable,
175209
history,
210+
textMode,
211+
defaultMode,
176212
groupTypes: 'both' as const,
177213
singleRootGroup,
178214
showValidation,
215+
...(builderComponents ? { components: builderComponents } : {}),
179216
};
180217

181218
const outputText = React.useMemo(() => {
@@ -194,6 +231,9 @@ export const DemoPlayground: React.FC<IDemoPlaygroundProps> = ({
194231
cloneable,
195232
draggable,
196233
history,
234+
textMode,
235+
defaultMode,
236+
useMonacoTextEditor,
197237
singleRootGroup,
198238
showValidation,
199239
customizationMode,
@@ -206,6 +246,9 @@ export const DemoPlayground: React.FC<IDemoPlaygroundProps> = ({
206246
cloneable,
207247
draggable,
208248
history,
249+
textMode,
250+
defaultMode,
251+
useMonacoTextEditor,
209252
singleRootGroup,
210253
showValidation,
211254
customizationMode,
@@ -264,6 +307,50 @@ export const DemoPlayground: React.FC<IDemoPlaygroundProps> = ({
264307
<span>Undo / redo history</span>
265308
</ToggleRow>
266309

310+
<ToggleRow $disabled={!singleRootGroup}>
311+
<Toggle
312+
type="checkbox"
313+
checked={textMode}
314+
disabled={!singleRootGroup}
315+
$disabled={!singleRootGroup}
316+
onChange={event => setTextMode(event.target.checked)}
317+
/>
318+
<span>
319+
Text editor mode
320+
{!singleRootGroup ? ' (requires single root group)' : ''}
321+
</span>
322+
</ToggleRow>
323+
324+
<ToggleRow $disabled={!textMode}>
325+
<Toggle
326+
type="checkbox"
327+
checked={defaultMode === 'text'}
328+
disabled={!textMode}
329+
$disabled={!textMode}
330+
onChange={event =>
331+
setDefaultMode(event.target.checked ? 'text' : 'builder')
332+
}
333+
/>
334+
<span>
335+
Open in text mode
336+
{!textMode ? ' (requires text editor mode)' : ''}
337+
</span>
338+
</ToggleRow>
339+
340+
<ToggleRow $disabled={!textMode}>
341+
<Toggle
342+
type="checkbox"
343+
checked={useMonacoTextEditor}
344+
disabled={!textMode}
345+
$disabled={!textMode}
346+
onChange={event => setUseMonacoTextEditor(event.target.checked)}
347+
/>
348+
<span>
349+
Monaco text editor
350+
{!textMode ? ' (requires text editor mode)' : ''}
351+
</span>
352+
</ToggleRow>
353+
267354
<ToggleRow>
268355
<Toggle
269356
type="checkbox"
@@ -355,15 +442,9 @@ export const DemoPlayground: React.FC<IDemoPlaygroundProps> = ({
355442
<BuilderCard>
356443
<BuilderSurface>
357444
{isMuiMode ? (
358-
<Builder
359-
{...builderProps}
360-
components={muiComponents}
361-
/>
445+
<Builder {...builderProps} />
362446
) : isAntdMode ? (
363-
<Builder
364-
{...builderProps}
365-
components={antdComponents}
366-
/>
447+
<Builder {...builderProps} />
367448
) : (
368449
<ThemeProvider colors={themeColors}>
369450
<Builder {...builderProps} />

example/src/components/theme-editor.tsx

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,14 @@ type ColorPath =
7676
| 'grey.700'
7777
| 'grey.800'
7878
| 'grey.900'
79+
| 'info.primary'
80+
| 'info.light'
81+
| 'success.primary'
82+
| 'success.light'
83+
| 'warning.primary'
84+
| 'warning.light'
85+
| 'error.primary'
86+
| 'error.light'
7987
| 'white';
8088

8189
const controls: { label: string; name: ColorPath }[] = [
@@ -96,6 +104,14 @@ const controls: { label: string; name: ColorPath }[] = [
96104
{ label: 'Grey 700', name: 'grey.700' },
97105
{ label: 'Grey 800', name: 'grey.800' },
98106
{ label: 'Grey 900', name: 'grey.900' },
107+
{ label: 'Info primary', name: 'info.primary' },
108+
{ label: 'Info light', name: 'info.light' },
109+
{ label: 'Success primary', name: 'success.primary' },
110+
{ label: 'Success light', name: 'success.light' },
111+
{ label: 'Warning primary', name: 'warning.primary' },
112+
{ label: 'Warning light', name: 'warning.light' },
113+
{ label: 'Error primary', name: 'error.primary' },
114+
{ label: 'Error light', name: 'error.light' },
99115
{ label: 'White', name: 'white' },
100116
];
101117

@@ -104,7 +120,18 @@ const getColorValue = (themeColors: IColors, path: ColorPath) => {
104120
return themeColors.white;
105121
}
106122

107-
const [group, key] = path.split('.') as [
123+
const keys = path.split('.');
124+
125+
if (keys[0] === 'info' || keys[0] === 'success' || keys[0] === 'warning' || keys[0] === 'error') {
126+
const [group, key] = keys as [
127+
keyof Pick<IColors, 'info' | 'success' | 'warning' | 'error'>,
128+
keyof IColors['info'],
129+
];
130+
131+
return themeColors[group][key];
132+
}
133+
134+
const [group, key] = keys as [
108135
'primary' | 'secondary' | 'grey',
109136
keyof IColors['primary'] & keyof IColors['grey'],
110137
];
@@ -121,7 +148,24 @@ const setColorValue = (
121148
return { ...themeColors, white: nextValue };
122149
}
123150

124-
const [group, key] = path.split('.') as [
151+
const keys = path.split('.');
152+
153+
if (keys[0] === 'info' || keys[0] === 'success' || keys[0] === 'warning' || keys[0] === 'error') {
154+
const [group, key] = keys as [
155+
keyof Pick<IColors, 'info' | 'success' | 'warning' | 'error'>,
156+
keyof IColors['info'],
157+
];
158+
159+
return {
160+
...themeColors,
161+
[group]: {
162+
...themeColors[group],
163+
[key]: nextValue,
164+
},
165+
};
166+
}
167+
168+
const [group, key] = keys as [
125169
'primary' | 'secondary' | 'grey',
126170
keyof IColors['primary'] & keyof IColors['grey'],
127171
];

0 commit comments

Comments
 (0)