Skip to content

Commit 3c7f652

Browse files
author
Vojtěch Václav Porteš
committed
2 parents d0ec5e2 + 2feafe2 commit 3c7f652

7 files changed

Lines changed: 1002 additions & 20 deletions

src/builder/builder.test.tsx

Lines changed: 275 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1052,23 +1052,244 @@ describe('#components/Builder', () => {
10521052
/>
10531053
);
10541054

1055-
const previousTextValue = (
1056-
getByDataTest(container, 'CustomTextModeEditorInput') as HTMLTextAreaElement
1057-
).value;
1058-
10591055
fireEvent.change(getByDataTest(container, 'CustomTextModeEditorInput'), {
10601056
target: { value: '(MOCK_NUMBER != 8)' },
10611057
});
10621058

10631059
expect(onChange).not.toHaveBeenCalled();
10641060
expect(getByDataTest(container, 'CustomTextModeEditorInput')).toHaveValue(
1065-
previousTextValue
1061+
'(MOCK_NUMBER != 8)'
10661062
);
10671063
expect(getByDataTest(container, 'CustomTextModeEditorError')).toHaveTextContent(
10681064
'One or more read-only clauses cannot be changed or removed in text mode.'
10691065
);
10701066
});
10711067

1068+
it('allows adding another rule with the same locked field and operator in text mode', async () => {
1069+
const onChange = jest.fn();
1070+
const { container } = render(
1071+
<Builder
1072+
fields={fields}
1073+
data={[
1074+
{
1075+
type: 'GROUP',
1076+
value: 'AND',
1077+
isNegated: false,
1078+
children: [
1079+
{
1080+
field: 'MOCK_FIELD',
1081+
value: 'alpha',
1082+
operator: 'EQUAL',
1083+
readOnly: {
1084+
enabled: true,
1085+
targets: ['field', 'operator'],
1086+
},
1087+
},
1088+
{
1089+
field: 'MOCK_NUMBER',
1090+
value: 5,
1091+
operator: 'NOT_EQUAL',
1092+
},
1093+
],
1094+
},
1095+
]}
1096+
textMode
1097+
defaultMode="text"
1098+
components={{
1099+
...defaultComponents,
1100+
TextModeEditor: CustomTextModeEditor,
1101+
}}
1102+
onChange={onChange}
1103+
/>
1104+
);
1105+
1106+
fireEvent.change(getByDataTest(container, 'CustomTextModeEditorInput'), {
1107+
target: {
1108+
value:
1109+
"(MOCK_FIELD = 'alpha' AND MOCK_FIELD = 'beta' AND MOCK_NUMBER != 5)",
1110+
},
1111+
});
1112+
1113+
await waitFor(() =>
1114+
expect(onChange).toHaveBeenLastCalledWith([
1115+
{
1116+
type: 'GROUP',
1117+
value: 'AND',
1118+
isNegated: false,
1119+
children: [
1120+
{
1121+
field: 'MOCK_FIELD',
1122+
value: 'alpha',
1123+
operator: 'EQUAL',
1124+
readOnly: {
1125+
enabled: true,
1126+
targets: ['field', 'operator'],
1127+
},
1128+
},
1129+
{
1130+
field: 'MOCK_FIELD',
1131+
value: 'beta',
1132+
operator: 'EQUAL',
1133+
},
1134+
{
1135+
field: 'MOCK_NUMBER',
1136+
value: 5,
1137+
operator: 'NOT_EQUAL',
1138+
},
1139+
],
1140+
},
1141+
])
1142+
);
1143+
1144+
expect(queryByDataTest(container, 'CustomTextModeEditorError')).toBeNull();
1145+
});
1146+
1147+
it('allows adding another rule next to a targeted lock when another identical read-only rule exists elsewhere', async () => {
1148+
const onChange = jest.fn();
1149+
const { container } = render(
1150+
<Builder
1151+
fields={fields}
1152+
data={[
1153+
{
1154+
type: 'GROUP',
1155+
value: 'AND',
1156+
isNegated: false,
1157+
children: [
1158+
{
1159+
field: 'MOCK_FIELD',
1160+
value: 'alpha',
1161+
operator: 'EQUAL',
1162+
readOnly: {
1163+
enabled: true,
1164+
targets: ['field', 'operator'],
1165+
},
1166+
},
1167+
{
1168+
type: 'GROUP',
1169+
value: 'OR',
1170+
isNegated: false,
1171+
children: [
1172+
{
1173+
field: 'MOCK_FIELD',
1174+
value: 'gamma',
1175+
operator: 'EQUAL',
1176+
},
1177+
{
1178+
field: 'MOCK_NUMBER',
1179+
value: 5,
1180+
operator: 'NOT_EQUAL',
1181+
},
1182+
],
1183+
},
1184+
{
1185+
type: 'GROUP',
1186+
value: 'AND',
1187+
isNegated: false,
1188+
children: [
1189+
{
1190+
field: 'MOCK_FIELD',
1191+
value: 'gamma',
1192+
operator: 'EQUAL',
1193+
readOnly: true,
1194+
},
1195+
{
1196+
field: 'MOCK_NUMBER',
1197+
value: 8,
1198+
operator: 'NOT_EQUAL',
1199+
},
1200+
],
1201+
},
1202+
],
1203+
},
1204+
]}
1205+
textMode
1206+
defaultMode="text"
1207+
components={{
1208+
...defaultComponents,
1209+
TextModeEditor: CustomTextModeEditor,
1210+
}}
1211+
onChange={onChange}
1212+
/>
1213+
);
1214+
1215+
fireEvent.change(getByDataTest(container, 'CustomTextModeEditorInput'), {
1216+
target: {
1217+
value:
1218+
"((MOCK_FIELD = 'alpha' AND MOCK_FIELD = 'beta') AND (MOCK_FIELD = 'gamma' OR MOCK_NUMBER != 5) AND (MOCK_FIELD = 'gamma' AND MOCK_NUMBER != 8))",
1219+
},
1220+
});
1221+
1222+
await waitFor(() =>
1223+
expect(onChange).toHaveBeenLastCalledWith([
1224+
{
1225+
type: 'GROUP',
1226+
value: 'AND',
1227+
isNegated: false,
1228+
children: [
1229+
{
1230+
type: 'GROUP',
1231+
value: 'AND',
1232+
isNegated: false,
1233+
children: [
1234+
{
1235+
field: 'MOCK_FIELD',
1236+
value: 'alpha',
1237+
operator: 'EQUAL',
1238+
readOnly: {
1239+
enabled: true,
1240+
targets: ['field', 'operator'],
1241+
},
1242+
},
1243+
{
1244+
field: 'MOCK_FIELD',
1245+
value: 'beta',
1246+
operator: 'EQUAL',
1247+
},
1248+
],
1249+
},
1250+
{
1251+
type: 'GROUP',
1252+
value: 'OR',
1253+
isNegated: false,
1254+
children: [
1255+
{
1256+
field: 'MOCK_FIELD',
1257+
value: 'gamma',
1258+
operator: 'EQUAL',
1259+
},
1260+
{
1261+
field: 'MOCK_NUMBER',
1262+
value: 5,
1263+
operator: 'NOT_EQUAL',
1264+
},
1265+
],
1266+
},
1267+
{
1268+
type: 'GROUP',
1269+
value: 'AND',
1270+
isNegated: false,
1271+
children: [
1272+
{
1273+
field: 'MOCK_FIELD',
1274+
value: 'gamma',
1275+
operator: 'EQUAL',
1276+
readOnly: true,
1277+
},
1278+
{
1279+
field: 'MOCK_NUMBER',
1280+
value: 8,
1281+
operator: 'NOT_EQUAL',
1282+
},
1283+
],
1284+
},
1285+
],
1286+
},
1287+
])
1288+
);
1289+
1290+
expect(queryByDataTest(container, 'CustomTextModeEditorError')).toBeNull();
1291+
});
1292+
10721293
it('allows deleting a group with a read-only descendant in text mode when readOnlyProtectsDelete is false', async () => {
10731294
const onChange = jest.fn();
10741295
const { container } = render(
@@ -1755,6 +1976,55 @@ describe('#components/Builder', () => {
17551976
});
17561977
});
17571978

1979+
it('Keeps exact local spacing for valid text-mode edits after a combinator', async () => {
1980+
const onChange = jest.fn();
1981+
const { container } = render(
1982+
<Builder
1983+
fields={fields}
1984+
data={[
1985+
{
1986+
type: 'GROUP',
1987+
value: 'AND',
1988+
isNegated: false,
1989+
children: [
1990+
{ field: 'MOCK_FIELD', value: 'alpha', operator: 'EQUAL' },
1991+
{ field: 'MOCK_NUMBER', value: 2, operator: 'EQUAL' },
1992+
],
1993+
},
1994+
]}
1995+
textMode
1996+
defaultMode="text"
1997+
components={{
1998+
...defaultComponents,
1999+
TextModeEditor: CustomTextModeEditor,
2000+
}}
2001+
onChange={onChange}
2002+
/>
2003+
);
2004+
2005+
fireEvent.change(getByDataTest(container, 'CustomTextModeEditorInput'), {
2006+
target: { value: "(MOCK_FIELD = 'alpha' AND MOCK_NUMBER = 3)" },
2007+
});
2008+
2009+
await waitFor(() =>
2010+
expect(onChange).toHaveBeenLastCalledWith([
2011+
{
2012+
type: 'GROUP',
2013+
value: 'AND',
2014+
isNegated: false,
2015+
children: [
2016+
{ field: 'MOCK_FIELD', value: 'alpha', operator: 'EQUAL' },
2017+
{ field: 'MOCK_NUMBER', value: 3, operator: 'EQUAL' },
2018+
],
2019+
},
2020+
])
2021+
);
2022+
2023+
expect(getByDataTest(container, 'CustomTextModeEditorInput')).toHaveValue(
2024+
"(MOCK_FIELD = 'alpha' AND MOCK_NUMBER = 3)"
2025+
);
2026+
});
2027+
17582028
it('Clones a rule directly below the original rule', () => {
17592029
const onChange = jest.fn();
17602030
const { container } = render(

src/builder/builder.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ export const Builder = forwardRef<IBuilderRef, IBuilderProps>(({
135135
const [textDiagnostics, setTextDiagnostics] = useState<
136136
ReturnType<typeof parseBuilderSqlText>['diagnostics']
137137
>([]);
138+
const pendingLocalTextValue = useRef<string | null>(null);
138139
const lastEmittedData = useRef<DenormalizedQuery | null>(null);
139140
const pendingChangeData = useRef<NormalizedQuery | null>(null);
140141
const fieldOptionsStoreRef = useRef<ReturnType<typeof createBuilderFieldOptionsStore>>();
@@ -584,6 +585,15 @@ export const Builder = forwardRef<IBuilderRef, IBuilderProps>(({
584585

585586
useEffect(() => {
586587
if (!textModeEnabled) {
588+
pendingLocalTextValue.current = null;
589+
return;
590+
}
591+
592+
if (mode === 'text' && pendingLocalTextValue.current !== null) {
593+
setTextValue(pendingLocalTextValue.current);
594+
setTextDiagnostics([]);
595+
setTextErrorMessage(null);
596+
pendingLocalTextValue.current = null;
587597
return;
588598
}
589599

@@ -599,7 +609,7 @@ export const Builder = forwardRef<IBuilderRef, IBuilderProps>(({
599609
setTextProtectedRanges(nextTextState.protectedRanges);
600610
setTextDiagnostics([]);
601611
setTextErrorMessage(null);
602-
}, [data, fields, textModeEnabled]);
612+
}, [data, fields, mode, textModeEnabled]);
603613

604614
const handleAddRootGroup = useCallback(() => {
605615
dispatchAction(
@@ -696,14 +706,7 @@ export const Builder = forwardRef<IBuilderRef, IBuilderProps>(({
696706
: null
697707
);
698708

699-
if (readOnlyTargetDiagnostic) {
700-
setTextValue(textValue);
701-
setTextProtectedRanges((currentProtectedRanges) => [
702-
...currentProtectedRanges,
703-
]);
704-
}
705-
706-
if (readOnlyNegationDiagnostic) {
709+
if (readOnlyTargetDiagnostic || readOnlyNegationDiagnostic) {
707710
setTextProtectedRanges((currentProtectedRanges) => [
708711
...currentProtectedRanges,
709712
]);
@@ -728,6 +731,7 @@ export const Builder = forwardRef<IBuilderRef, IBuilderProps>(({
728731
rootGroupType,
729732
singleRootGroup
730733
);
734+
pendingLocalTextValue.current = nextText;
731735
commitData(createReplaceQueryAction(nextData));
732736
},
733737
[
@@ -737,7 +741,6 @@ export const Builder = forwardRef<IBuilderRef, IBuilderProps>(({
737741
rootGroupType,
738742
singleRootGroup,
739743
strings.textMode,
740-
textValue,
741744
]
742745
);
743746

0 commit comments

Comments
 (0)