Skip to content

Commit f61fdf2

Browse files
Vojtěch Václav Portešvojtechportes
authored andcommitted
feat: Added lockable functionality into GUI
1 parent c02a2f8 commit f61fdf2

18 files changed

Lines changed: 746 additions & 31 deletions

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ const initialData: DenormalizedQuery = [
7474
value: 'AND',
7575
isNegated: false,
7676
children: [
77+
{
78+
field: 'STATE',
79+
operator: 'EQUAL',
80+
value: 'CZ',
81+
readOnly: true,
82+
},
7783
{
7884
field: 'IS_IN_EU',
7985
operator: 'EQUAL',

example/src/components/demo-playground.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ export const DemoPlayground: React.FC<IDemoPlaygroundProps> = ({
116116
const [data, setData] = React.useState<DenormalizedQuery>(initialData);
117117
const [outputFormat, setOutputFormat] = React.useState<OutputFormat>('Native');
118118
const [readOnly, setReadOnly] = React.useState(false);
119+
const [lockable, setLockable] = React.useState(false);
119120
const [draggable, setDraggable] = React.useState(false);
120121
const [singleRootGroup, setSingleRootGroup] = React.useState(true);
121122
const [showValidation, setShowValidation] = React.useState(true);
@@ -146,6 +147,15 @@ export const DemoPlayground: React.FC<IDemoPlaygroundProps> = ({
146147
<span>Read-only mode</span>
147148
</ToggleRow>
148149

150+
<ToggleRow>
151+
<Toggle
152+
type="checkbox"
153+
checked={lockable}
154+
onChange={event => setLockable(event.target.checked)}
155+
/>
156+
<span>Lock controls</span>
157+
</ToggleRow>
158+
149159
<ToggleRow>
150160
<Toggle
151161
type="checkbox"
@@ -187,6 +197,7 @@ export const DemoPlayground: React.FC<IDemoPlaygroundProps> = ({
187197
data={data}
188198
fields={demoFields}
189199
readOnly={readOnly}
200+
lockable={lockable}
190201
onChange={setData}
191202
draggable={draggable}
192203
groupTypes="both"

example/src/constants/demo-data.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ export const initialQueryTree: DenormalizedQuery = [
134134
field: 'CUSTOMER_COUNTRY',
135135
operator: 'EQUAL',
136136
value: 'CZ',
137+
readOnly: true,
137138
},
138139
{
139140
type: 'GROUP',

example/src/pages/api-page/pages/api-content.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const builderSignature = `export interface IBuilderProps {
1515
components?: IBuilderComponentsProps;
1616
strings?: IStrings;
1717
readOnly?: boolean;
18+
lockable?: boolean;
1819
draggable?: boolean;
1920
singleRootGroup?: boolean;
2021
groupTypes?: 'with-modifiers' | 'without-modifiers' | 'both';
@@ -92,6 +93,7 @@ const componentsSignature = `export interface IBuilderComponentsProps {
9293
};
9394
Remove?: React.ComponentType<IButtonProps>;
9495
Add?: React.ComponentType<IButtonProps>;
96+
LockToggle?: React.ComponentType<ILockToggleProps>;
9597
Rule?: React.ComponentType<IRuleContainerProps>;
9698
Group?: React.ComponentType<IGroupContainerProps>;
9799
GroupHeaderOption?: React.ComponentType<IGroupHeaderOptionProps>;
@@ -102,6 +104,18 @@ const componentsSignature = `export interface IBuilderComponentsProps {
102104
PopoverItem?: React.ComponentType<IPopoverItemProps>;
103105
}`;
104106

107+
const lockToggleSignature = `export type BuilderLockState = 'unlocked' | 'self' | 'all';
108+
109+
export interface ILockToggleProps {
110+
state: BuilderLockState;
111+
nodeType: 'rule' | 'group';
112+
disabled?: boolean;
113+
onChange?: (nextState: BuilderLockState) => void;
114+
className?: string;
115+
title?: string;
116+
'data-test'?: string;
117+
}`;
118+
105119
const themeProviderSignature = `export interface IThemeProps {
106120
colors?: IColors;
107121
}
@@ -275,6 +289,7 @@ export const apiPages: IApiPage[] = [
275289
<li><ItemTitle><InlineCode>components</InlineCode>:</ItemTitle> Optional overrides for internal UI pieces. Omitted entries fall back to default components.</li>
276290
<li><ItemTitle><InlineCode>strings</InlineCode>:</ItemTitle> Optional localized UI strings used by the built-in controls.</li>
277291
<li><ItemTitle><InlineCode>readOnly</InlineCode>:</ItemTitle> Defaults to <InlineCode>false</InlineCode>. Disables editing actions when enabled.</li>
292+
<li><ItemTitle><InlineCode>lockable</InlineCode>:</ItemTitle> Defaults to <InlineCode>false</InlineCode>. Renders lock controls for rules and groups and writes the resulting lock state back into emitted query data.</li>
278293
<li><ItemTitle><InlineCode>draggable</InlineCode>:</ItemTitle> Defaults to <InlineCode>false</InlineCode>. Enables drag-and-drop reordering and movement of query nodes.</li>
279294
<li><ItemTitle><InlineCode>singleRootGroup</InlineCode>:</ItemTitle> Defaults to <InlineCode>true</InlineCode>. Wraps root-level items into a single root group and prevents deleting that root group.</li>
280295
<li><ItemTitle><InlineCode>groupTypes</InlineCode>:</ItemTitle> Defaults to <InlineCode>'with-modifiers'</InlineCode>. Controls whether groups use combinator/negation controls, modifierless groups, or both.</li>
@@ -346,6 +361,7 @@ export const apiPages: IApiPage[] = [
346361
<li><ItemTitle><InlineCode>value</InlineCode>:</ItemTitle> Optional rule value. Supported scalar/array forms are defined by <InlineCode>QueryRuleValue</InlineCode>.</li>
347362
<li><ItemTitle><InlineCode>operators</InlineCode>:</ItemTitle> Optional rule-level operator override list.</li>
348363
<li><ItemTitle><InlineCode>readOnly</InlineCode>:</ItemTitle> Optional per-rule lock flag. It locks only that rule and does not affect siblings, parents, or descendants.</li>
364+
<li><ItemTitle>GUI cycle:</ItemTitle> When <InlineCode>lockable</InlineCode> is enabled, rules cycle between unlocked and locked, which maps to omitted <InlineCode>readOnly</InlineCode> and <InlineCode>readOnly: true</InlineCode>.</li>
349365
<li><ItemTitle><InlineCode>id</InlineCode> and <InlineCode>parent</InlineCode>:</ItemTitle> Optional in denormalized input. The builder can ingest data without them.</li>
350366
</List>
351367
<SectionTitle>Group props</SectionTitle>
@@ -356,6 +372,7 @@ export const apiPages: IApiPage[] = [
356372
<li><ItemTitle><InlineCode>isNegated</InlineCode>:</ItemTitle> Present only for groups with modifiers.</li>
357373
<li><ItemTitle><InlineCode>readOnly</InlineCode>:</ItemTitle> Can be a boolean or an object with <InlineCode>enabled</InlineCode> and optional <InlineCode>inheritToChildren</InlineCode>.</li>
358374
<li><ItemTitle><InlineCode>readOnly: true</InlineCode>:</ItemTitle> Locks only the group&apos;s own controls by default. Descendant rules and groups remain editable unless inheritance is enabled.</li>
375+
<li><ItemTitle>GUI cycle:</ItemTitle> When <InlineCode>lockable</InlineCode> is enabled, groups cycle through unlocked, locked group only, and locked group with descendants.</li>
359376
<li><ItemTitle><InlineCode>inheritToChildren</InlineCode>:</ItemTitle> Applies the group lock to all descendants when the group is enabled. Descendants cannot override an inherited lock from an ancestor.</li>
360377
</List>
361378
<AlertBox title="Documentation" variant="info">
@@ -378,10 +395,12 @@ export const apiPages: IApiPage[] = [
378395
content: (
379396
<>
380397
<CodeBlock code={componentsSignature} language="ts" label="Component overrides" />
398+
<CodeBlock code={lockToggleSignature} language="ts" label="LockToggle props" />
381399
<SectionTitle>Props</SectionTitle>
382400
<List>
383401
<li><ItemTitle><InlineCode>form.Select</InlineCode> / <InlineCode>form.SelectMulti</InlineCode> / <InlineCode>form.Switch</InlineCode> / <InlineCode>form.Input</InlineCode>:</ItemTitle> Replace the built-in form controls used by rules and groups.</li>
384402
<li><ItemTitle><InlineCode>Remove</InlineCode> and <InlineCode>Add</InlineCode>:</ItemTitle> Replace action buttons used for structural editing.</li>
403+
<li><ItemTitle><InlineCode>LockToggle</InlineCode>:</ItemTitle> Replaces the built-in lock control used when <InlineCode>lockable</InlineCode> is enabled.</li>
385404
<li><ItemTitle><InlineCode>Rule</InlineCode> and <InlineCode>Group</InlineCode>:</ItemTitle> Replace the main structural containers.</li>
386405
<li><ItemTitle><InlineCode>GroupHeaderOption</InlineCode>:</ItemTitle> Replaces the header option control used in group UIs.</li>
387406
<li><ItemTitle><InlineCode>Text</InlineCode>:</ItemTitle> Replaces the built-in text rendering component.</li>
@@ -394,6 +413,7 @@ export const apiPages: IApiPage[] = [
394413
<li><ItemTitle><InlineCode>form.Select</InlineCode>:</ItemTitle> Receives <InlineCode>values</InlineCode>, <InlineCode>selectedValue</InlineCode>, <InlineCode>emptyValue</InlineCode>, and <InlineCode>onChange(value)</InlineCode>.</li>
395414
<li><ItemTitle><InlineCode>form.SelectMulti</InlineCode>:</ItemTitle> Receives <InlineCode>selectedValue</InlineCode>, <InlineCode>values</InlineCode>, <InlineCode>onChange(value)</InlineCode>, and <InlineCode>onDelete(value)</InlineCode>.</li>
396415
<li><ItemTitle><InlineCode>form.Switch</InlineCode>:</ItemTitle> Receives <InlineCode>switched</InlineCode>, optional <InlineCode>onChange(value)</InlineCode>, and optional <InlineCode>disabled</InlineCode>.</li>
416+
<li><ItemTitle><InlineCode>LockToggle</InlineCode>:</ItemTitle> Receives <InlineCode>state</InlineCode>, <InlineCode>nodeType</InlineCode>, optional <InlineCode>disabled</InlineCode>, and <InlineCode>onChange(nextState)</InlineCode>.</li>
397417
<li><ItemTitle><InlineCode>Rule</InlineCode>:</ItemTitle> Receives already-built <InlineCode>children</InlineCode>, <InlineCode>controls</InlineCode>, and optional <InlineCode>dragHandle</InlineCode>.</li>
398418
<li><ItemTitle><InlineCode>Group</InlineCode>:</ItemTitle> Receives <InlineCode>controlsLeft</InlineCode>, <InlineCode>controlsRight</InlineCode>, <InlineCode>children</InlineCode>, and optional overlays or drag handles.</li>
399419
<li><ItemTitle><InlineCode>DropZone</InlineCode>:</ItemTitle> Receives <InlineCode>id</InlineCode>, <InlineCode>index</InlineCode>, optional <InlineCode>parentId</InlineCode>, and drag-state flags.</li>

example/src/pages/documentation-page/pages/documentation-content.tsx

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ const initialData: DenormalizedQuery = [
5858
value: 'AND',
5959
isNegated: false,
6060
children: [
61+
{
62+
field: 'STATE',
63+
operator: 'EQUAL',
64+
value: 'CZ',
65+
readOnly: true,
66+
},
6167
{
6268
field: 'IS_IN_EU',
6369
operator: 'EQUAL',
@@ -186,12 +192,17 @@ const validationSnippet = `const fields: IBuilderFieldProps[] = [
186192
const builderBehaviorSnippet = `<Builder
187193
fields={fields}
188194
data={data}
195+
lockable
189196
draggable
190197
singleRootGroup={false}
191198
groupTypes="both"
192199
onChange={setData}
193200
/>;
194201
202+
// lockable:
203+
// Renders built-in lock controls for rules and groups and writes the resulting
204+
// lock state back into the emitted query via rule/group readOnly values.
205+
//
195206
// draggable:
196207
// Enables drag-and-drop for editable rules and groups.
197208
//
@@ -248,6 +259,42 @@ const lockingSnippet = `const data: DenormalizedQuery = [
248259
249260
<Builder fields={fields} data={data} onChange={setData} />;`;
250261

262+
const lockingGuiSnippet = `<Builder
263+
fields={fields}
264+
data={data}
265+
lockable
266+
onChange={setData}
267+
/>;
268+
269+
// Rules cycle through:
270+
// unlocked -> locked
271+
//
272+
// Groups cycle through:
273+
// unlocked -> locked group only -> locked group and descendants
274+
//
275+
// The emitted query stores those states in readOnly:
276+
// rule: readOnly: true
277+
// group: readOnly: true
278+
// group + descendants: readOnly: { enabled: true, inheritToChildren: true }`;
279+
280+
const lockToggleSnippet = `const components = {
281+
LockToggle: MyLockToggle,
282+
};
283+
284+
<Builder
285+
fields={fields}
286+
data={data}
287+
lockable
288+
components={components}
289+
onChange={setData}
290+
/>;
291+
292+
// LockToggle receives:
293+
// state: 'unlocked' | 'self' | 'all'
294+
// nodeType: 'rule' | 'group'
295+
// disabled?: boolean
296+
// onChange?: (nextState) => void`;
297+
251298
const stringsSnippet = `import { strings } from '@vojtechportes/react-query-builder';
252299
253300
<Builder
@@ -351,6 +398,11 @@ export const documentationPages: IDocumentationPage[] = [
351398
<>
352399
<p>Basic controlled usage.</p>
353400
<CodeBlock code={basicUsageSnippet} language="tsx" label="Basic setup" />
401+
<p>
402+
The example includes a single rule with <InlineCode>readOnly: true</InlineCode>{' '}
403+
to show that locking can live directly in the query data without changing
404+
the rest of the builder configuration.
405+
</p>
354406
<AlertBox title="API reference" variant="info">
355407
<TextLink to="/api/builder">Builder</TextLink>,{' '}
356408
<TextLink to="/api/fields">Fields</TextLink>, and{' '}
@@ -428,6 +480,7 @@ export const documentationPages: IDocumentationPage[] = [
428480
<CodeBlock code={builderBehaviorSnippet} language="tsx" label="Builder behavior" />
429481
<SectionTitle>draggable</SectionTitle>
430482
<List>
483+
<li>Use <InlineCode>lockable</InlineCode> to expose lock controls directly in the UI.</li>
431484
<li>Enables drag-and-drop reordering and movement for editable rules and groups.</li>
432485
<li>Read-only rules and groups are excluded from dragging.</li>
433486
<li>When the entire builder is read-only, drag-and-drop is disabled as well.</li>
@@ -487,6 +540,20 @@ export const documentationPages: IDocumentationPage[] = [
487540
distinction is that rules lock only themselves, while groups can lock
488541
either just their own controls or their entire subtree.
489542
</p>
543+
<SectionTitle>GUI Locking</SectionTitle>
544+
<p>
545+
Set <InlineCode>lockable</InlineCode> on <TextLink to="/api/builder">Builder</TextLink>{' '}
546+
to render lock controls directly in the UI. The built-in controls update
547+
the same <InlineCode>readOnly</InlineCode> fields that are already part of
548+
the query data model, so the resulting lock state is preserved in output.
549+
</p>
550+
<CodeBlock code={lockingGuiSnippet} language="tsx" label="GUI locking" />
551+
<List>
552+
<li>Rules cycle through two states: unlocked and locked.</li>
553+
<li>Groups cycle through three states: unlocked, locked group only, and locked group with descendants.</li>
554+
<li>The default group cycle maps to <InlineCode>false</InlineCode>, <InlineCode>true</InlineCode>, and <InlineCode>{`{ enabled: true, inheritToChildren: true }`}</InlineCode>.</li>
555+
<li>When a parent group inherits a lock to descendants, child lock controls render disabled because descendants cannot override that inherited state.</li>
556+
</List>
490557
<SectionTitle>How each level behaves</SectionTitle>
491558
<List>
492559
<li>
@@ -508,6 +575,12 @@ export const documentationPages: IDocumentationPage[] = [
508575
</li>
509576
</List>
510577
<CodeBlock code={lockingSnippet} language="tsx" label="Locking examples" />
578+
<SectionTitle>Custom Lock Control</SectionTitle>
579+
<p>
580+
The default lock button can be replaced through{' '}
581+
<InlineCode>components.LockToggle</InlineCode>.
582+
</p>
583+
<CodeBlock code={lockToggleSnippet} language="tsx" label="LockToggle override" />
511584
<SectionTitle>What &quot;locked&quot; means in the UI</SectionTitle>
512585
<List>
513586
<li>Locked rules cannot change field, operator, or value, and cannot be deleted.</li>

src/builder-context.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export interface IBuilderContextProps {
1313
fields: IBuilderFieldProps[];
1414
data: NormalizedQuery;
1515
readOnly: boolean;
16+
lockable?: boolean;
1617
draggable?: boolean;
1718
singleRootGroup?: boolean;
1819
groupTypes?: BuilderGroupMode;
@@ -35,6 +36,7 @@ export interface IBuilderContextProviderProps {
3536
fields: IBuilderFieldProps[];
3637
data: NormalizedQuery;
3738
readOnly: boolean;
39+
lockable?: boolean;
3840
draggable?: boolean;
3941
singleRootGroup?: boolean;
4042
groupTypes?: BuilderGroupMode;
@@ -56,6 +58,7 @@ export const BuilderContextProvider: FC<IBuilderContextProviderProps> = ({
5658
strings,
5759
data,
5860
readOnly,
61+
lockable,
5962
draggable,
6063
singleRootGroup,
6164
groupTypes,
@@ -105,6 +108,7 @@ export const BuilderContextProvider: FC<IBuilderContextProviderProps> = ({
105108
strings: resolvedStrings,
106109
data,
107110
readOnly,
111+
lockable,
108112
draggable,
109113
singleRootGroup,
110114
groupTypes,

0 commit comments

Comments
 (0)