@finos/legend-query-builder 4.14.9 → 4.14.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/lib/__lib__/QueryBuilderEvent.d.ts +2 -1
  2. package/lib/__lib__/QueryBuilderEvent.d.ts.map +1 -1
  3. package/lib/__lib__/QueryBuilderEvent.js +1 -0
  4. package/lib/__lib__/QueryBuilderEvent.js.map +1 -1
  5. package/lib/__lib__/QueryBuilderTesting.d.ts +3 -1
  6. package/lib/__lib__/QueryBuilderTesting.d.ts.map +1 -1
  7. package/lib/__lib__/QueryBuilderTesting.js +3 -0
  8. package/lib/__lib__/QueryBuilderTesting.js.map +1 -1
  9. package/lib/components/QueryBuilder.d.ts.map +1 -1
  10. package/lib/components/QueryBuilder.js +45 -27
  11. package/lib/components/QueryBuilder.js.map +1 -1
  12. package/lib/components/fetch-structure/QueryBuilderResultModifierPanel.d.ts.map +1 -1
  13. package/lib/components/fetch-structure/QueryBuilderResultModifierPanel.js +86 -44
  14. package/lib/components/fetch-structure/QueryBuilderResultModifierPanel.js.map +1 -1
  15. package/lib/components/shared/LambdaEditor.d.ts.map +1 -1
  16. package/lib/components/shared/LambdaEditor.js +10 -5
  17. package/lib/components/shared/LambdaEditor.js.map +1 -1
  18. package/lib/components/shared/QueryBuilderPropertyInfoTooltip.d.ts.map +1 -1
  19. package/lib/components/shared/QueryBuilderPropertyInfoTooltip.js +12 -11
  20. package/lib/components/shared/QueryBuilderPropertyInfoTooltip.js.map +1 -1
  21. package/lib/index.css +2 -2
  22. package/lib/index.css.map +1 -1
  23. package/lib/package.json +1 -1
  24. package/lib/stores/QueryBuilderChangeHistoryState.d.ts +35 -0
  25. package/lib/stores/QueryBuilderChangeHistoryState.d.ts.map +1 -0
  26. package/lib/stores/QueryBuilderChangeHistoryState.js +106 -0
  27. package/lib/stores/QueryBuilderChangeHistoryState.js.map +1 -0
  28. package/lib/stores/QueryBuilderState.d.ts +2 -0
  29. package/lib/stores/QueryBuilderState.d.ts.map +1 -1
  30. package/lib/stores/QueryBuilderState.js +6 -0
  31. package/lib/stores/QueryBuilderState.js.map +1 -1
  32. package/lib/stores/fetch-structure/tds/QueryResultSetModifierState.d.ts +2 -2
  33. package/lib/stores/fetch-structure/tds/QueryResultSetModifierState.d.ts.map +1 -1
  34. package/lib/stores/fetch-structure/tds/QueryResultSetModifierState.js +7 -7
  35. package/lib/stores/fetch-structure/tds/QueryResultSetModifierState.js.map +1 -1
  36. package/package.json +5 -5
  37. package/src/__lib__/QueryBuilderEvent.ts +2 -0
  38. package/src/__lib__/QueryBuilderTesting.ts +3 -0
  39. package/src/components/QueryBuilder.tsx +41 -0
  40. package/src/components/fetch-structure/QueryBuilderResultModifierPanel.tsx +174 -94
  41. package/src/components/shared/LambdaEditor.tsx +10 -8
  42. package/src/components/shared/QueryBuilderPropertyInfoTooltip.tsx +81 -65
  43. package/src/stores/QueryBuilderChangeHistoryState.ts +129 -0
  44. package/src/stores/QueryBuilderState.ts +6 -0
  45. package/src/stores/fetch-structure/tds/QueryResultSetModifierState.ts +7 -12
  46. package/tsconfig.json +1 -0
@@ -75,6 +75,8 @@ import { QUERY_BUILDER_SETTING_KEY } from '../__lib__/QueryBuilderSetting.js';
75
75
  import { QUERY_BUILDER_COMPONENT_ELEMENT_ID } from './QueryBuilderComponentElement.js';
76
76
  import { DataAccessOverview } from './data-access/DataAccessOverview.js';
77
77
  import { QueryChat } from './QueryChat.js';
78
+ import { useEffect, useRef } from 'react';
79
+ import { RedoButton, UndoButton } from '@finos/legend-lego/application';
78
80
 
79
81
  const QueryBuilderStatusBar = observer(
80
82
  (props: { queryBuilderState: QueryBuilderState }) => {
@@ -226,6 +228,7 @@ const QueryBuilderPostGraphFetchPanel = observer(
226
228
  export const QueryBuilder = observer(
227
229
  (props: { queryBuilderState: QueryBuilderState }) => {
228
230
  const { queryBuilderState } = props;
231
+ const queryBuilderRef = useRef<HTMLDivElement>(null);
229
232
  const isQuerySupported = queryBuilderState.isQuerySupported;
230
233
  const fetchStructureState = queryBuilderState.fetchStructureState;
231
234
  const isTDSState =
@@ -368,10 +371,30 @@ export const QueryBuilder = observer(
368
371
  }
369
372
  return null;
370
373
  };
374
+
375
+ const undo = (): void => {
376
+ queryBuilderState.changeHistoryState.undo();
377
+ };
378
+
379
+ const redo = (): void => {
380
+ queryBuilderState.changeHistoryState.redo();
381
+ };
382
+
383
+ useEffect(() => {
384
+ // this condition is for passing all exisitng tests because when we initialize a queryBuilderState for a test,
385
+ // we use an empty RawLambda with an empty class and this useEffect is called earlier than initializeWithQuery()
386
+ if (queryBuilderState.isQuerySupported && queryBuilderState.class) {
387
+ queryBuilderState.changeHistoryState.cacheNewQuery(
388
+ queryBuilderState.buildQuery(),
389
+ );
390
+ }
391
+ }, [queryBuilderState, queryBuilderState.hashCode]);
392
+
371
393
  return (
372
394
  <div
373
395
  data-testid={QUERY_BUILDER_TEST_ID.QUERY_BUILDER}
374
396
  className="query-builder"
397
+ ref={queryBuilderRef}
375
398
  >
376
399
  <BackdropContainer
377
400
  elementId={QUERY_BUILDER_COMPONENT_ELEMENT_ID.BACKDROP_CONTAINER}
@@ -408,6 +431,24 @@ export const QueryBuilder = observer(
408
431
  )}
409
432
  </div>
410
433
  <div className="query-builder__header__actions">
434
+ <div className="query-builder__header__actions__undo-redo">
435
+ <UndoButton
436
+ parent={queryBuilderRef}
437
+ canUndo={
438
+ queryBuilderState.changeHistoryState.canUndo &&
439
+ queryBuilderState.isQuerySupported
440
+ }
441
+ undo={undo}
442
+ />
443
+ <RedoButton
444
+ parent={queryBuilderRef}
445
+ canRedo={
446
+ queryBuilderState.changeHistoryState.canRedo &&
447
+ queryBuilderState.isQuerySupported
448
+ }
449
+ redo={redo}
450
+ />
451
+ </div>
411
452
  <DropdownMenu
412
453
  className="query-builder__header__advanced-dropdown"
413
454
  title="Show Advanced Menu..."
@@ -31,20 +31,32 @@ import {
31
31
  MenuContent,
32
32
  MenuContentItem,
33
33
  ModalFooterButton,
34
+ InputWithInlineValidation,
34
35
  } from '@finos/legend-art';
35
36
  import { SortColumnState } from '../../stores/fetch-structure/tds/QueryResultSetModifierState.js';
36
- import { guaranteeNonNullable } from '@finos/legend-shared';
37
+ import {
38
+ addUniqueEntry,
39
+ deleteEntry,
40
+ guaranteeNonNullable,
41
+ } from '@finos/legend-shared';
37
42
  import { useApplicationStore } from '@finos/legend-application';
38
43
  import type { QueryBuilderTDSState } from '../../stores/fetch-structure/tds/QueryBuilderTDSState.js';
39
44
  import type { QueryBuilderTDSColumnState } from '../../stores/fetch-structure/tds/QueryBuilderTDSColumnState.js';
40
45
  import { COLUMN_SORT_TYPE } from '../../graph/QueryBuilderMetaModelConst.js';
46
+ import { useEffect, useState } from 'react';
47
+ import type { QueryBuilderProjectionColumnState } from '../../stores/fetch-structure/tds/projection/QueryBuilderProjectionColumnState.js';
48
+ import { QUERY_BUILDER_TEST_ID } from '../../__lib__/QueryBuilderTesting.js';
41
49
 
42
50
  const ColumnSortEditor = observer(
43
- (props: { tdsState: QueryBuilderTDSState; sortState: SortColumnState }) => {
44
- const { tdsState, sortState } = props;
51
+ (props: {
52
+ sortColumns: SortColumnState[];
53
+ setSortColumns: (sortColumns: SortColumnState[]) => void;
54
+ sortState: SortColumnState;
55
+ tdsColumns: QueryBuilderTDSColumnState[];
56
+ }) => {
57
+ const { sortColumns, setSortColumns, sortState, tdsColumns } = props;
45
58
  const applicationStore = useApplicationStore();
46
- const sortColumns = tdsState.resultSetModifierState.sortColumns;
47
- const projectionOptions = tdsState.tdsColumns
59
+ const projectionOptions = tdsColumns
48
60
  .filter(
49
61
  (projectionCol) =>
50
62
  projectionCol === sortState.columnState ||
@@ -58,6 +70,8 @@ const ColumnSortEditor = observer(
58
70
  label: sortState.columnState.columnName,
59
71
  value: sortState,
60
72
  };
73
+ const sortType = sortState.sortType;
74
+
61
75
  const onChange = (
62
76
  val: { label: string; value: QueryBuilderTDSColumnState } | null,
63
77
  ): void => {
@@ -65,13 +79,17 @@ const ColumnSortEditor = observer(
65
79
  sortState.setColumnState(val.value);
66
80
  }
67
81
  };
68
- const sortType = sortState.sortType;
69
82
 
70
- const deleteColumnSort = (): void =>
71
- tdsState.resultSetModifierState.deleteSortColumn(sortState);
83
+ const deleteColumnSort = (): void => {
84
+ const newSortColumns = [...sortColumns];
85
+ deleteEntry(newSortColumns, sortState);
86
+ setSortColumns(newSortColumns);
87
+ };
88
+
72
89
  const changeSortBy = (sortOp: COLUMN_SORT_TYPE) => (): void => {
73
90
  sortState.setSortType(sortOp);
74
91
  };
92
+
75
93
  return (
76
94
  <div className="panel__content__form__section__list__item query-builder__projection__options__sort">
77
95
  <CustomSelectorInput
@@ -120,6 +138,9 @@ const ColumnSortEditor = observer(
120
138
  onClick={deleteColumnSort}
121
139
  tabIndex={-1}
122
140
  title="Remove"
141
+ data-testid={
142
+ QUERY_BUILDER_TEST_ID.QUERY_BUILDER_RESULT_MODIFIER_PANEL_SORT_REMOVE_BTN
143
+ }
123
144
  >
124
145
  <TimesIcon />
125
146
  </button>
@@ -129,11 +150,15 @@ const ColumnSortEditor = observer(
129
150
  );
130
151
 
131
152
  const ColumnsSortEditor = observer(
132
- (props: { tdsState: QueryBuilderTDSState }) => {
133
- const { tdsState } = props;
134
- const resultSetModifierState = tdsState.resultSetModifierState;
135
- const sortColumns = resultSetModifierState.sortColumns;
136
- const projectionOptions = tdsState.projectionColumns
153
+ (props: {
154
+ projectionColumns: QueryBuilderProjectionColumnState[];
155
+ sortColumns: SortColumnState[];
156
+ setSortColumns: (sortColumns: SortColumnState[]) => void;
157
+ tdsColumns: QueryBuilderTDSColumnState[];
158
+ }) => {
159
+ const { projectionColumns, sortColumns, setSortColumns, tdsColumns } =
160
+ props;
161
+ const projectionOptions = projectionColumns
137
162
  .filter(
138
163
  (projectionCol) =>
139
164
  !sortColumns.some((sortCol) => sortCol.columnState === projectionCol),
@@ -147,7 +172,9 @@ const ColumnsSortEditor = observer(
147
172
  const sortColumn = new SortColumnState(
148
173
  guaranteeNonNullable(projectionOptions[0]).value,
149
174
  );
150
- resultSetModifierState.addSortColumn(sortColumn);
175
+ const newSortColumns = [...sortColumns];
176
+ addUniqueEntry(newSortColumns, sortColumn);
177
+ setSortColumns(newSortColumns);
151
178
  }
152
179
  };
153
180
 
@@ -166,8 +193,10 @@ const ColumnsSortEditor = observer(
166
193
  {sortColumns.map((value) => (
167
194
  <ColumnSortEditor
168
195
  key={value.columnState.uuid}
169
- tdsState={tdsState}
196
+ sortColumns={sortColumns}
197
+ setSortColumns={setSortColumns}
170
198
  sortState={value}
199
+ tdsColumns={tdsColumns}
171
200
  />
172
201
  ))}
173
202
  </div>
@@ -189,75 +218,127 @@ const ColumnsSortEditor = observer(
189
218
 
190
219
  export const QueryResultModifierModal = observer(
191
220
  (props: { tdsState: QueryBuilderTDSState }) => {
192
- const { tdsState: tdsState } = props;
221
+ // Read current state
222
+ const { tdsState } = props;
193
223
  const resultSetModifierState = tdsState.resultSetModifierState;
194
- const limitResults = resultSetModifierState.limit;
195
- const distinct = resultSetModifierState.distinct;
196
- const close = (): void => resultSetModifierState.setShowModal(false);
197
- const toggleDistinct = (): void => resultSetModifierState.toggleDistinct();
198
- const changeValue: React.ChangeEventHandler<HTMLInputElement> = (event) => {
199
- const val = event.target.value;
200
- resultSetModifierState.setLimit(
201
- val === '' ? undefined : parseInt(val, 10),
202
- );
203
- };
224
+ const stateSortColumns = resultSetModifierState.sortColumns;
225
+ const stateDistinct = resultSetModifierState.distinct;
226
+ const stateLimitResults = resultSetModifierState.limit;
227
+ const stateSlice = resultSetModifierState.slice;
228
+
229
+ // Set up temp state for modal lifecycle
230
+ const [sortColumns, setSortColumns] = useState([...stateSortColumns]);
231
+ const [distinct, setDistinct] = useState(stateDistinct);
232
+ const [limitResults, setLimitResults] = useState(stateLimitResults);
233
+ const [slice, setSlice] = useState<
234
+ [number | undefined, number | undefined]
235
+ >(stateSlice ?? [undefined, undefined]);
204
236
 
205
- const handleSliceStartChange = (start: number, end: number): void => {
206
- const slice: [number, number] = [start, end];
207
- resultSetModifierState.setSlice(slice);
237
+ // Sync temp state with tdsState when modal is opened/closed
238
+ useEffect(() => {
239
+ setSortColumns([...stateSortColumns]);
240
+ setDistinct(stateDistinct);
241
+ setLimitResults(stateLimitResults);
242
+ setSlice(stateSlice ?? [undefined, undefined]);
243
+ }, [
244
+ resultSetModifierState.showModal,
245
+ stateSortColumns,
246
+ stateDistinct,
247
+ stateLimitResults,
248
+ stateSlice,
249
+ ]);
250
+
251
+ // Handle user actions
252
+ const closeModal = (): void => resultSetModifierState.setShowModal(false);
253
+ const applyChanges = (): void => {
254
+ resultSetModifierState.setSortColumns(sortColumns);
255
+ resultSetModifierState.setDistinct(distinct);
256
+ resultSetModifierState.setLimit(limitResults);
257
+ if (slice[0] !== undefined && slice[1] !== undefined) {
258
+ resultSetModifierState.setSlice([slice[0], slice[1]]);
259
+ } else {
260
+ resultSetModifierState.setSlice(undefined);
261
+ }
262
+ resultSetModifierState.setShowModal(false);
208
263
  };
209
264
 
210
- const clearSlice = (): void => {
211
- resultSetModifierState.setSlice(undefined);
265
+ const handleLimitResultsChange: React.ChangeEventHandler<
266
+ HTMLInputElement
267
+ > = (event) => {
268
+ const val = event.target.value.replace(/[^0-9]/g, '');
269
+ setLimitResults(val === '' ? undefined : parseInt(val, 10));
212
270
  };
213
271
 
214
- const addSlice = (): void => {
215
- resultSetModifierState.setSlice([0, 1]);
272
+ const handleSliceChange = (
273
+ start: number | undefined,
274
+ end: number | undefined,
275
+ ): void => {
276
+ const newSlice: [number | undefined, number | undefined] = [start, end];
277
+ setSlice(newSlice);
216
278
  };
217
279
 
218
280
  const changeSliceStart: React.ChangeEventHandler<HTMLInputElement> = (
219
281
  event,
220
282
  ) => {
221
- const currentSlice = resultSetModifierState.slice;
222
- const val = event.target.value;
223
- const start = typeof val === 'number' ? val : parseInt(val, 10);
224
- if (currentSlice) {
225
- handleSliceStartChange(start, currentSlice[1]);
283
+ const val = event.target.value.replace(/[^0-9]/g, '');
284
+ if (val === '') {
285
+ handleSliceChange(undefined, slice[1]);
286
+ } else {
287
+ const start = typeof val === 'number' ? val : parseInt(val, 10);
288
+ handleSliceChange(start, slice[1]);
226
289
  }
227
290
  };
228
291
  const changeSliceEnd: React.ChangeEventHandler<HTMLInputElement> = (
229
292
  event,
230
293
  ) => {
231
- const currentSlice = resultSetModifierState.slice;
232
- const val = event.target.value;
233
- const end = typeof val === 'number' ? val : parseInt(val, 10);
234
- if (currentSlice) {
235
- handleSliceStartChange(currentSlice[0], end);
294
+ const val = event.target.value.replace(/[^0-9]/g, '');
295
+ if (val === '') {
296
+ handleSliceChange(slice[0], undefined);
297
+ } else {
298
+ const end = typeof val === 'number' ? val : parseInt(val, 10);
299
+ handleSliceChange(slice[0], end);
236
300
  }
237
301
  };
238
302
 
303
+ // Error states
304
+ const isInvalidSlice =
305
+ (slice[0] === undefined && slice[1] !== undefined) ||
306
+ (slice[0] !== undefined && slice[1] === undefined) ||
307
+ (slice[0] !== undefined &&
308
+ slice[1] !== undefined &&
309
+ slice[0] >= slice[1]);
310
+
239
311
  return (
240
312
  <Dialog
241
313
  open={Boolean(resultSetModifierState.showModal)}
242
- onClose={close}
314
+ onClose={closeModal}
243
315
  classes={{
244
316
  root: 'editor-modal__root-container',
245
317
  container: 'editor-modal__container',
246
318
  paper: 'editor-modal__content',
247
319
  }}
320
+ data-testid={QUERY_BUILDER_TEST_ID.QUERY_BUILDER_RESULT_MODIFIER_PANEL}
248
321
  >
249
- <Modal darkMode={true} className="editor-modal">
322
+ <Modal
323
+ darkMode={true}
324
+ className="editor-modal query-builder__projection__modal"
325
+ >
250
326
  <ModalHeader title="Result Set Modifier" />
251
327
  <ModalBody className="query-builder__projection__modal__body">
252
328
  <div className="query-builder__projection__options">
253
- <ColumnsSortEditor tdsState={tdsState} />
329
+ <ColumnsSortEditor
330
+ projectionColumns={tdsState.projectionColumns}
331
+ sortColumns={sortColumns}
332
+ setSortColumns={setSortColumns}
333
+ tdsColumns={tdsState.tdsColumns}
334
+ />
254
335
  <div className="panel__content__form__section">
255
336
  <div className="panel__content__form__section__header__label">
256
337
  Eliminate Duplicate Rows
257
338
  </div>
258
339
  <div
259
340
  className="panel__content__form__section__toggler"
260
- onClick={toggleDistinct}
341
+ onClick={() => setDistinct(!distinct)}
261
342
  >
262
343
  <button
263
344
  className={clsx(
@@ -277,75 +358,74 @@ export const QueryResultModifierModal = observer(
277
358
  </div>
278
359
  </div>
279
360
  <div className="panel__content__form__section">
280
- <div className="panel__content__form__section__header__label">
361
+ <label
362
+ htmlFor="query-builder__projection__modal__limit-results-input"
363
+ className="panel__content__form__section__header__label"
364
+ >
281
365
  Limit Results
282
- </div>
366
+ </label>
283
367
  <div className="panel__content__form__section__header__prompt">
284
368
  Specify the maximum total number of rows the output will
285
369
  produce
286
370
  </div>
287
371
  <input
372
+ id="query-builder__projection__modal__limit-results-input"
288
373
  className="panel__content__form__section__input panel__content__form__section__number-input"
289
374
  spellCheck={false}
290
- type="number"
375
+ type="text"
291
376
  value={limitResults ?? ''}
292
- onChange={changeValue}
377
+ onChange={handleLimitResultsChange}
293
378
  />
294
379
  </div>
295
380
  <div className="panel__content__form__section">
296
- <div className="panel__content__form__section__header__label">
381
+ <label
382
+ htmlFor="query-builder__projection__modal__slice-start-input"
383
+ className="panel__content__form__section__header__label"
384
+ >
297
385
  Slice
298
- </div>
386
+ </label>
299
387
  <div className="panel__content__form__section__header__prompt">
300
388
  Reduce the number of rows in the provided TDS, selecting the
301
389
  set of rows in the specified range between start and stop
302
390
  </div>
303
- {resultSetModifierState.slice ? (
304
- <>
305
- <div className="query-builder__result__slice">
306
- <input
307
- className="input--dark query-builder__result__slice__input"
308
- spellCheck={false}
309
- value={resultSetModifierState.slice[0]}
310
- onChange={changeSliceStart}
311
- type="number"
312
- />
313
- <div className="query-builder__result__slice__range">
314
- ..
315
- </div>
316
- <input
317
- className="input--dark query-builder__result__slice__input"
318
- spellCheck={false}
319
- value={resultSetModifierState.slice[1]}
320
- onChange={changeSliceEnd}
321
- type="number"
322
- />
323
- <button
324
- className="query-builder__projection__options__sort__remove-btn btn--dark btn--caution"
325
- onClick={clearSlice}
326
- tabIndex={-1}
327
- title="Remove"
328
- >
329
- <TimesIcon />
330
- </button>
331
- </div>
332
- </>
333
- ) : (
334
- <div className="panel__content__form__section__list__new-item__add">
335
- <button
336
- className="panel__content__form__section__list__new-item__add-btn btn btn--dark"
337
- onClick={addSlice}
338
- tabIndex={-1}
339
- >
340
- Add Slice
341
- </button>
391
+ <div className="query-builder__result__slice">
392
+ <div className="query-builder__result__slice__input__wrapper">
393
+ <InputWithInlineValidation
394
+ id="query-builder__projection__modal__slice-start-input"
395
+ className="input--dark query-builder__result__slice__input"
396
+ spellCheck={false}
397
+ value={slice[0] ?? ''}
398
+ onChange={changeSliceStart}
399
+ type="text"
400
+ error={isInvalidSlice ? 'Invalid slice' : undefined}
401
+ />
402
+ </div>
403
+ <div className="query-builder__result__slice__range">..</div>
404
+ <div className="query-builder__result__slice__input__wrapper">
405
+ <InputWithInlineValidation
406
+ className="input--dark query-builder__result__slice__input"
407
+ spellCheck={false}
408
+ value={slice[1] ?? ''}
409
+ onChange={changeSliceEnd}
410
+ type="text"
411
+ error={isInvalidSlice ? 'Invalid slice' : undefined}
412
+ />
342
413
  </div>
343
- )}
414
+ </div>
344
415
  </div>
345
416
  </div>
346
417
  </ModalBody>
347
418
  <ModalFooter>
348
- <ModalFooterButton onClick={close} text="Close" />
419
+ <ModalFooterButton
420
+ onClick={applyChanges}
421
+ text="Apply"
422
+ disabled={isInvalidSlice}
423
+ />
424
+ <ModalFooterButton
425
+ onClick={closeModal}
426
+ text="Cancel"
427
+ type="secondary"
428
+ />
349
429
  </ModalFooter>
350
430
  </Modal>
351
431
  </Dialog>
@@ -159,6 +159,8 @@ const LambdaEditor_Inner = observer(
159
159
  applicationStore.alertUnhandledError,
160
160
  );
161
161
  setExpanded(!isExpanded);
162
+ } else if (!forceExpansion && parserError) {
163
+ setExpanded(!isExpanded);
162
164
  }
163
165
  };
164
166
 
@@ -399,7 +401,6 @@ const LambdaEditor_Inner = observer(
399
401
  <button
400
402
  className="lambda-editor__editor__expand-btn"
401
403
  onClick={toggleExpandedMode}
402
- disabled={Boolean(parserError)}
403
404
  tabIndex={-1}
404
405
  title="Toggle Expand"
405
406
  >
@@ -410,7 +411,6 @@ const LambdaEditor_Inner = observer(
410
411
  <button
411
412
  className="lambda-editor__action"
412
413
  onClick={openInPopUp}
413
- disabled={Boolean(parserError)}
414
414
  tabIndex={-1}
415
415
  title="Open in a popup..."
416
416
  >
@@ -569,12 +569,14 @@ const LambdaEditor_PopUp = observer(
569
569
  }
570
570
 
571
571
  useEffect(() => {
572
- flowResult(
573
- lambdaEditorState.convertLambdaObjectToGrammarString({
574
- pretty: true,
575
- preserveCompilationError: true,
576
- }),
577
- ).catch(applicationStore.alertUnhandledError);
572
+ if (!lambdaEditorState.parserError) {
573
+ flowResult(
574
+ lambdaEditorState.convertLambdaObjectToGrammarString({
575
+ pretty: true,
576
+ preserveCompilationError: true,
577
+ }),
578
+ ).catch(applicationStore.alertUnhandledError);
579
+ }
578
580
  }, [applicationStore, lambdaEditorState]);
579
581
 
580
582
  // dispose editor