@finos/legend-query-builder 4.14.9 → 4.14.11

Sign up to get free protection for your applications and to get access to all the features.
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