@finos/legend-query-builder 4.10.0 → 4.10.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (27) hide show
  1. package/lib/__lib__/QueryBuilderTesting.d.ts +1 -0
  2. package/lib/__lib__/QueryBuilderTesting.d.ts.map +1 -1
  3. package/lib/__lib__/QueryBuilderTesting.js +1 -0
  4. package/lib/__lib__/QueryBuilderTesting.js.map +1 -1
  5. package/lib/components/QueryBuilderResultPanel.d.ts.map +1 -1
  6. package/lib/components/QueryBuilderResultPanel.js +44 -57
  7. package/lib/components/QueryBuilderResultPanel.js.map +1 -1
  8. package/lib/components/filter/QueryBuilderFilterPanel.d.ts +3 -2
  9. package/lib/components/filter/QueryBuilderFilterPanel.d.ts.map +1 -1
  10. package/lib/components/filter/QueryBuilderFilterPanel.js +145 -8
  11. package/lib/components/filter/QueryBuilderFilterPanel.js.map +1 -1
  12. package/lib/index.css +1 -1
  13. package/lib/package.json +1 -1
  14. package/lib/stores/QueryBuilderResultState.d.ts +3 -0
  15. package/lib/stores/QueryBuilderResultState.d.ts.map +1 -1
  16. package/lib/stores/QueryBuilderResultState.js +31 -2
  17. package/lib/stores/QueryBuilderResultState.js.map +1 -1
  18. package/lib/stores/filter/QueryBuilderFilterState.d.ts +7 -1
  19. package/lib/stores/filter/QueryBuilderFilterState.d.ts.map +1 -1
  20. package/lib/stores/filter/QueryBuilderFilterState.js +27 -0
  21. package/lib/stores/filter/QueryBuilderFilterState.js.map +1 -1
  22. package/package.json +5 -5
  23. package/src/__lib__/QueryBuilderTesting.ts +1 -0
  24. package/src/components/QueryBuilderResultPanel.tsx +63 -76
  25. package/src/components/filter/QueryBuilderFilterPanel.tsx +239 -21
  26. package/src/stores/QueryBuilderResultState.ts +36 -1
  27. package/src/stores/filter/QueryBuilderFilterState.ts +40 -1
@@ -52,7 +52,6 @@ import {
52
52
  EnumValueInstanceValue,
53
53
  EnumValueExplicitReference,
54
54
  RelationalExecutionActivities,
55
- getTDSRowRankByColumnInAsc,
56
55
  } from '@finos/legend-graph';
57
56
  import {
58
57
  ActionAlertActionType,
@@ -63,8 +62,6 @@ import {
63
62
  import {
64
63
  assertErrorThrown,
65
64
  guaranteeNonNullable,
66
- isBoolean,
67
- type PlainObject,
68
65
  prettyDuration,
69
66
  filterByType,
70
67
  isValidURL,
@@ -420,9 +417,7 @@ const QueryBuilderGridResultContextMenu = observer(
420
417
  ),
421
418
  );
422
419
 
423
- const findRowFromRowIndex = (
424
- rowIndex: number,
425
- ): (string | number | boolean | null)[] => {
420
+ const findSelectedCellRowData = (): string => {
426
421
  if (
427
422
  !tdsState.queryBuilderState.resultState.executionResult ||
428
423
  !(
@@ -430,21 +425,31 @@ const QueryBuilderGridResultContextMenu = observer(
430
425
  TDSExecutionResult
431
426
  )
432
427
  ) {
433
- return [''];
428
+ return '';
434
429
  }
435
- return (
436
- tdsState.queryBuilderState.resultState.executionResult.result.rows[
437
- rowIndex
438
- ]?.values ?? ['']
430
+ const rowData = tdsState.queryBuilderState.resultState.rowData.find(
431
+ (rData) =>
432
+ rData.rowNumber ===
433
+ tdsState.queryBuilderState.resultState.selectedCells[0]?.coordinates
434
+ .rowIndex,
439
435
  );
436
+ // try to get the entire row value separated by comma
437
+ // rowData is in format of {columnName: value, columnName1: value, ...., rowNumber:value}
438
+ const valueArr: (string | number | boolean | null | undefined)[] = [];
439
+ if (rowData) {
440
+ Object.entries(rowData).forEach((entry) => {
441
+ if (entry[0] !== 'rowNumber') {
442
+ valueArr.push(entry[1]);
443
+ }
444
+ });
445
+ return valueArr.join(',');
446
+ }
447
+ return '';
440
448
  };
441
449
 
442
450
  const handleCopyRowValue = applicationStore.guardUnhandledError(() =>
443
451
  applicationStore.clipboardService.copyTextToClipboard(
444
- findRowFromRowIndex(
445
- tdsState.queryBuilderState.resultState.selectedCells[0]?.coordinates
446
- .rowIndex ?? 0,
447
- ).toString(),
452
+ findSelectedCellRowData(),
448
453
  ),
449
454
  );
450
455
 
@@ -502,18 +507,18 @@ const QueryResultCellRenderer = observer(
502
507
  isString(cellValue) && isValidURL(cellValue) ? cellValue : undefined;
503
508
  const columnName = params.column?.getColId() ?? '';
504
509
  const findCoordinatesFromResultValue = (
505
- colId: string,
510
+ colName: string,
506
511
  rowNumber: number,
507
512
  ): QueryBuilderTDSResultCellCoordinate => {
508
513
  const colIndex = tdsExecutionResult.result.columns.findIndex(
509
- (col) => col === colId,
514
+ (col) => col === colName,
510
515
  );
511
516
  return { rowIndex: rowNumber, colIndex: colIndex };
512
517
  };
513
518
 
514
519
  const currentCellCoordinates = findCoordinatesFromResultValue(
515
520
  columnName,
516
- params.rowIndex,
521
+ params.data.rowNumber,
517
522
  );
518
523
  const cellInFilteredResults = resultState.selectedCells.some(
519
524
  (result) =>
@@ -521,9 +526,9 @@ const QueryResultCellRenderer = observer(
521
526
  result.coordinates.rowIndex === currentCellCoordinates.rowIndex,
522
527
  );
523
528
 
524
- const findColumnFromCoordinates = (
529
+ const findColumnNameFromColumnIndex = (
525
530
  colIndex: number,
526
- ): string | number | boolean | null | undefined => {
531
+ ): string | undefined => {
527
532
  if (
528
533
  !resultState.executionResult ||
529
534
  !(resultState.executionResult instanceof TDSExecutionResult)
@@ -534,30 +539,12 @@ const QueryResultCellRenderer = observer(
534
539
  };
535
540
 
536
541
  const findResultValueFromCoordinates = (
537
- resultCoordinate: [number, number],
538
- ): string | number | boolean | null | undefined => {
539
- const rowIndex = resultCoordinate[0];
540
- const colIndex = resultCoordinate[1];
541
-
542
- if (
543
- !resultState.executionResult ||
544
- !(resultState.executionResult instanceof TDSExecutionResult)
545
- ) {
546
- return undefined;
547
- }
548
- if (params.columnApi.getColumnState()[colIndex]?.sort === 'asc') {
549
- resultState.executionResult.result.rows.sort((a, b) =>
550
- getTDSRowRankByColumnInAsc(a, b, colIndex),
551
- );
552
- } else if (params.columnApi.getColumnState()[colIndex]?.sort === 'desc') {
553
- resultState.executionResult.result.rows.sort((a, b) =>
554
- getTDSRowRankByColumnInAsc(b, a, colIndex),
555
- );
556
- }
557
- return resultState.executionResult.result.rows[rowIndex]?.values[
558
- colIndex
559
- ];
560
- };
542
+ rowIndex: number,
543
+ colName: string,
544
+ ): string | number | boolean | null | undefined =>
545
+ resultState.rowData.find((data) => data.rowNumber === rowIndex)![
546
+ colName
547
+ ] as string | number | boolean | null | undefined;
561
548
 
562
549
  const isCoordinatesSelected = (
563
550
  resultCoordinate: QueryBuilderTDSResultCellCoordinate,
@@ -574,12 +561,12 @@ const QueryResultCellRenderer = observer(
574
561
  if (event.shiftKey) {
575
562
  const coordinates = findCoordinatesFromResultValue(
576
563
  columnName,
577
- params.rowIndex,
564
+ params.data.rowNumber,
578
565
  );
579
- const actualValue = findResultValueFromCoordinates([
566
+ const actualValue = findResultValueFromCoordinates(
580
567
  coordinates.rowIndex,
581
- coordinates.colIndex,
582
- ]);
568
+ columnName,
569
+ );
583
570
  resultState.addSelectedCell({
584
571
  value: actualValue,
585
572
  columnName: columnName,
@@ -593,12 +580,12 @@ const QueryResultCellRenderer = observer(
593
580
  resultState.setSelectedCells([]);
594
581
  const coordinates = findCoordinatesFromResultValue(
595
582
  columnName,
596
- params.rowIndex,
583
+ params.data.rowNumber,
597
584
  );
598
- const actualValue = findResultValueFromCoordinates([
585
+ const actualValue = findResultValueFromCoordinates(
599
586
  coordinates.rowIndex,
600
- coordinates.colIndex,
601
- ]);
587
+ columnName,
588
+ );
602
589
  resultState.setSelectedCells([
603
590
  {
604
591
  value: actualValue,
@@ -612,14 +599,14 @@ const QueryResultCellRenderer = observer(
612
599
  if (event.button === 2) {
613
600
  const coordinates = findCoordinatesFromResultValue(
614
601
  columnName,
615
- params.rowIndex,
602
+ params.data.rowNumber,
616
603
  );
617
604
  const isInSelected = isCoordinatesSelected(coordinates);
618
605
  if (!isInSelected) {
619
- const actualValue = findResultValueFromCoordinates([
606
+ const actualValue = findResultValueFromCoordinates(
620
607
  coordinates.rowIndex,
621
- coordinates.colIndex,
622
- ]);
608
+ columnName,
609
+ );
623
610
  resultState.setSelectedCells([
624
611
  {
625
612
  value: actualValue,
@@ -649,7 +636,7 @@ const QueryResultCellRenderer = observer(
649
636
  const firstCorner = results.coordinates;
650
637
  const secondCorner = findCoordinatesFromResultValue(
651
638
  columnName,
652
- params.rowIndex,
639
+ params.data.rowNumber,
653
640
  );
654
641
 
655
642
  resultState.setSelectedCells([results]);
@@ -661,11 +648,13 @@ const QueryResultCellRenderer = observer(
661
648
 
662
649
  for (let x = minRow; x <= maxRow; x++) {
663
650
  for (let y = minCol; y <= maxCol; y++) {
664
- const actualValue = findResultValueFromCoordinates([x, y]);
665
-
651
+ const actualValue = findResultValueFromCoordinates(
652
+ x,
653
+ findColumnNameFromColumnIndex(y) as string,
654
+ );
666
655
  const valueAndColumnId = {
667
656
  value: actualValue,
668
- columnName: findColumnFromCoordinates(y),
657
+ columnName: findColumnNameFromColumnIndex(y),
669
658
  coordinates: {
670
659
  rowIndex: x,
671
660
  colIndex: y,
@@ -684,7 +673,6 @@ const QueryResultCellRenderer = observer(
684
673
  }
685
674
  }
686
675
  }
687
-
688
676
  resultState.setMouseOverCell(resultState.selectedCells[0] ?? null);
689
677
  };
690
678
 
@@ -741,20 +729,6 @@ const QueryBuilderGridResult = observer(
741
729
 
742
730
  const resultState = queryBuilderState.resultState;
743
731
 
744
- const rowData = executionResult.result.rows.map((_row, rowIdx) => {
745
- const row: PlainObject = {};
746
- const cols = executionResult.result.columns;
747
- _row.values.forEach((value, colIdx) => {
748
- // `ag-grid` shows `false` value as empty string so we have
749
- // call `.toString()` to avoid this behavior.
750
- // See https://github.com/finos/legend-studio/issues/1008
751
- row[cols[colIdx] as string] = isBoolean(value) ? String(value) : value;
752
- });
753
-
754
- row.rowNumber = rowIdx;
755
- return row;
756
- });
757
-
758
732
  return (
759
733
  <div className="query-builder__result__values__table">
760
734
  <div
@@ -763,7 +737,20 @@ const QueryBuilderGridResult = observer(
763
737
  )}
764
738
  >
765
739
  <DataGrid
766
- rowData={rowData}
740
+ rowData={queryBuilderState.resultState.getRowData()}
741
+ onSortChanged={(params) => {
742
+ const sortedData: Record<
743
+ string,
744
+ string | number | boolean | null
745
+ >[] = [];
746
+ params.api.forEachNodeAfterFilterAndSort((node, index) => {
747
+ node.rowIndex = index;
748
+ // rowNumber has to be manually updated after sorting the column
749
+ node.data.rowNumber = index;
750
+ sortedData.push(node.data);
751
+ });
752
+ queryBuilderState.resultState.setRowData(sortedData);
753
+ }}
767
754
  gridOptions={{
768
755
  suppressScrollOnNewData: true,
769
756
  getRowId: (data) => data.data.rowNumber,
@@ -113,6 +113,7 @@ import {
113
113
  import { QueryBuilderTelemetryHelper } from '../../__lib__/QueryBuilderTelemetryHelper.js';
114
114
  import { getPropertyChainName } from '../../stores/QueryBuilderPropertyEditorState.js';
115
115
  import { QUERY_BUILDER_SUPPORTED_FUNCTIONS } from '../../graph/QueryBuilderMetaModelConst.js';
116
+ import { buildPropertyExpressionChain } from '../../stores/QueryBuilderValueSpecificationBuilderHelper.js';
116
117
 
117
118
  const isCollectionProperty = (
118
119
  propertyExpression: AbstractPropertyExpression,
@@ -151,7 +152,7 @@ const isCollectionProperty = (
151
152
  export const buildFilterTreeWithExists = (
152
153
  propertyExpression: AbstractPropertyExpression,
153
154
  filterState: QueryBuilderFilterState,
154
- targetDropNode?: QueryBuilderFilterTreeOperationNodeData,
155
+ targetDropNode?: QueryBuilderFilterTreeNodeData,
155
156
  ): void => {
156
157
  // 1. Decompose property expression
157
158
  const expressions: (AbstractPropertyExpression | SimpleFunctionExpression)[] =
@@ -294,7 +295,21 @@ export const buildFilterTreeWithExists = (
294
295
  .value.path,
295
296
  );
296
297
  if (parentPropertyChainIndex >= 0) {
297
- parentNode = targetDropNode;
298
+ // Here we choose parentNode based on what the type of targetDropNode
299
+ // 1. QueryBuilderFilterTreeConditionNodeData: In this case we would want to
300
+ // add a new group condition from the targetDropNode and add new exists node
301
+ // getting created as it's child so the parent would be the new group node
302
+ // 2. QueryBuilderFilterTreeGroupNodeData: Parent node would be same as
303
+ // targetDropNode
304
+ // 3. QueryBuilderFilterTreeBlankConditionNodeData: parentNode would be the
305
+ // parent of the targetDropNode. At the end we would deelete this blank node
306
+ // that got created.
307
+ parentNode =
308
+ targetDropNode instanceof QueryBuilderFilterTreeConditionNodeData
309
+ ? filterState.newGroupConditionFromNode(targetDropNode)
310
+ : targetDropNode instanceof QueryBuilderFilterTreeGroupNodeData
311
+ ? targetDropNode
312
+ : filterState.getParentNode(targetDropNode);
298
313
  existsLambdaPropertyChains = existsLambdaPropertyChains.slice(
299
314
  parentPropertyChainIndex + 1,
300
315
  );
@@ -303,7 +318,12 @@ export const buildFilterTreeWithExists = (
303
318
  );
304
319
  }
305
320
  } else if (!parentId) {
306
- parentNode = targetDropNode;
321
+ parentNode =
322
+ targetDropNode instanceof QueryBuilderFilterTreeConditionNodeData
323
+ ? filterState.newGroupConditionFromNode(targetDropNode)
324
+ : targetDropNode instanceof QueryBuilderFilterTreeGroupNodeData
325
+ ? targetDropNode
326
+ : filterState.getParentNode(targetDropNode);
307
327
  }
308
328
  }
309
329
  }
@@ -332,6 +352,127 @@ export const buildFilterTreeWithExists = (
332
352
  filterConditionState,
333
353
  );
334
354
  filterState.addNodeFromNode(treeNode, parentNode);
355
+ if (targetDropNode instanceof QueryBuilderFilterTreeBlankConditionNodeData) {
356
+ filterState.removeNodeAndPruneBranch(targetDropNode);
357
+ }
358
+ };
359
+
360
+ export const buildPropertyExpressionFromExistsNode = (
361
+ filterState: QueryBuilderFilterState,
362
+ existsNode: QueryBuilderFilterTreeExistsNodeData,
363
+ node: QueryBuilderFilterTreeConditionNodeData,
364
+ ): AbstractPropertyExpression => {
365
+ let nodeParent = filterState.getParentNode(node);
366
+ let existsLambdaParameterNames: string[] = [];
367
+ let existsLambdaExpressions: AbstractPropertyExpression[] = [];
368
+ existsLambdaExpressions.push(
369
+ node.condition.propertyExpressionState.propertyExpression,
370
+ );
371
+ existsLambdaParameterNames.push(
372
+ nodeParent?.lambdaParameterName ?? filterState.lambdaParameterName,
373
+ );
374
+ while (nodeParent && nodeParent.id !== existsNode.id) {
375
+ if (nodeParent instanceof QueryBuilderFilterTreeExistsNodeData) {
376
+ existsLambdaExpressions.push(
377
+ nodeParent.propertyExpressionState.propertyExpression,
378
+ );
379
+ existsLambdaParameterNames.push(
380
+ nodeParent.lambdaParameterName ?? filterState.lambdaParameterName,
381
+ );
382
+ }
383
+ nodeParent = filterState.getParentNode(nodeParent);
384
+ }
385
+ if (nodeParent?.id === existsNode.id) {
386
+ existsLambdaExpressions.push(
387
+ existsNode.propertyExpressionState.propertyExpression,
388
+ );
389
+ existsLambdaParameterNames.push(
390
+ existsNode.lambdaParameterName ?? filterState.lambdaParameterName,
391
+ );
392
+ }
393
+ existsLambdaParameterNames = existsLambdaParameterNames.reverse();
394
+ existsLambdaExpressions = existsLambdaExpressions.reverse();
395
+
396
+ const initialPropertyExpression = guaranteeNonNullable(
397
+ existsLambdaExpressions[0],
398
+ );
399
+ existsLambdaParameterNames = existsLambdaParameterNames.slice(1);
400
+ existsLambdaExpressions = existsLambdaExpressions.slice(1);
401
+
402
+ let flattenedPropertyExpressionChain = new AbstractPropertyExpression('');
403
+ flattenedPropertyExpressionChain.func = initialPropertyExpression.func;
404
+ flattenedPropertyExpressionChain.parametersValues = [
405
+ ...initialPropertyExpression.parametersValues,
406
+ ];
407
+ for (const exp of existsLambdaExpressions) {
408
+ // when rebuilding the property expression chain, disregard the initial variable that starts the chain
409
+ const expressions: (
410
+ | AbstractPropertyExpression
411
+ | SimpleFunctionExpression
412
+ )[] = [];
413
+ let currentExpression: ValueSpecification = exp;
414
+ while (
415
+ currentExpression instanceof AbstractPropertyExpression ||
416
+ (currentExpression instanceof SimpleFunctionExpression &&
417
+ matchFunctionName(
418
+ currentExpression.functionName,
419
+ QUERY_BUILDER_SUPPORTED_FUNCTIONS.SUBTYPE,
420
+ ))
421
+ ) {
422
+ if (currentExpression instanceof SimpleFunctionExpression) {
423
+ const functionExpression = new SimpleFunctionExpression(
424
+ extractElementNameFromPath(QUERY_BUILDER_SUPPORTED_FUNCTIONS.SUBTYPE),
425
+ );
426
+ functionExpression.parametersValues.unshift(
427
+ guaranteeNonNullable(currentExpression.parametersValues[1]),
428
+ );
429
+ expressions.push(functionExpression);
430
+ } else if (currentExpression instanceof AbstractPropertyExpression) {
431
+ const propertyExp = new AbstractPropertyExpression('');
432
+ propertyExp.func = currentExpression.func;
433
+ // NOTE: we must retain the rest of the parameters as those are derived property parameters
434
+ propertyExp.parametersValues =
435
+ currentExpression.parametersValues.length > 1
436
+ ? currentExpression.parametersValues.slice(1)
437
+ : [];
438
+ expressions.push(propertyExp);
439
+ }
440
+ currentExpression = guaranteeNonNullable(
441
+ currentExpression.parametersValues[0],
442
+ );
443
+ }
444
+ assertTrue(
445
+ expressions.length > 0,
446
+ `Can't process exists() expression: exists() usage with non-chain property expression is not supported`,
447
+ );
448
+ for (let i = 0; i < expressions.length - 1; ++i) {
449
+ (
450
+ expressions[i] as AbstractPropertyExpression | SimpleFunctionExpression
451
+ ).parametersValues.unshift(
452
+ expressions[i + 1] as
453
+ | AbstractPropertyExpression
454
+ | SimpleFunctionExpression,
455
+ );
456
+ }
457
+ (
458
+ expressions[expressions.length - 1] as
459
+ | AbstractPropertyExpression
460
+ | SimpleFunctionExpression
461
+ ).parametersValues.unshift(flattenedPropertyExpressionChain);
462
+ flattenedPropertyExpressionChain = guaranteeType(
463
+ expressions[0],
464
+ AbstractPropertyExpression,
465
+ `Can't process exists() expression: can't flatten to a property expression`,
466
+ );
467
+ }
468
+ return guaranteeType(
469
+ buildPropertyExpressionChain(
470
+ flattenedPropertyExpressionChain,
471
+ filterState.queryBuilderState,
472
+ existsNode.lambdaParameterName ?? filterState.lambdaParameterName,
473
+ ),
474
+ AbstractPropertyExpression,
475
+ );
335
476
  };
336
477
 
337
478
  /**
@@ -340,7 +481,7 @@ export const buildFilterTreeWithExists = (
340
481
  const buildFilterTree = (
341
482
  propertyExpression: AbstractPropertyExpression,
342
483
  filterState: QueryBuilderFilterState,
343
- targetDropNode?: QueryBuilderFilterTreeOperationNodeData | undefined,
484
+ targetDropNode?: QueryBuilderFilterTreeNodeData | undefined,
344
485
  ): void => {
345
486
  if (isCollectionProperty(propertyExpression)) {
346
487
  const propertyChainName = getPropertyChainName(
@@ -383,12 +524,88 @@ const buildFilterTree = (
383
524
  undefined,
384
525
  filterConditionState,
385
526
  );
386
- filterState.addNodeFromNode(
387
- treeNode,
388
- targetDropNode instanceof QueryBuilderFilterTreeGroupNodeData
389
- ? targetDropNode
390
- : undefined,
391
- );
527
+ // Check if there are any exists node present in the parent nodes of the target.
528
+ // This would change the way we build the filter tree
529
+ let cn: QueryBuilderFilterTreeNodeData | undefined = targetDropNode;
530
+ let existsNode: QueryBuilderFilterTreeExistsNodeData | undefined =
531
+ undefined;
532
+ let parentId = targetDropNode?.parentId;
533
+ while (parentId) {
534
+ cn = filterState.nodes.get(parentId);
535
+ parentId = cn?.parentId;
536
+ if (cn instanceof QueryBuilderFilterTreeExistsNodeData) {
537
+ existsNode = cn;
538
+ }
539
+ }
540
+ if (cn instanceof QueryBuilderFilterTreeExistsNodeData) {
541
+ existsNode = cn;
542
+ }
543
+ if (targetDropNode instanceof QueryBuilderFilterTreeGroupNodeData) {
544
+ if (existsNode) {
545
+ filterState.newGroupConditionFromNode(
546
+ existsNode,
547
+ treeNode,
548
+ targetDropNode.groupOperation,
549
+ );
550
+ } else {
551
+ filterState.addNodeFromNode(treeNode, targetDropNode);
552
+ }
553
+ } else if (
554
+ targetDropNode instanceof QueryBuilderFilterTreeBlankConditionNodeData
555
+ ) {
556
+ if (existsNode) {
557
+ filterState.queryBuilderState.applicationStore.notificationService.notifyError(
558
+ `Can't drag and drop here: property expression of target and source doesn't match`,
559
+ );
560
+ } else {
561
+ filterState.replaceBlankNodeWithNode(treeNode, targetDropNode);
562
+ }
563
+ } else if (
564
+ existsNode &&
565
+ targetDropNode instanceof QueryBuilderFilterTreeExistsNodeData
566
+ ) {
567
+ filterState.newGroupConditionFromNode(existsNode, treeNode);
568
+ } else if (
569
+ targetDropNode instanceof QueryBuilderFilterTreeConditionNodeData
570
+ ) {
571
+ const parentNode = filterState.getParentNode(targetDropNode);
572
+ if (
573
+ existsNode &&
574
+ parentNode instanceof QueryBuilderFilterTreeExistsNodeData &&
575
+ parentNode.childrenIds.length === 1
576
+ ) {
577
+ filterState.newGroupConditionFromNode(
578
+ existsNode,
579
+ treeNode,
580
+ QUERY_BUILDER_GROUP_OPERATION.AND,
581
+ );
582
+ } else if (
583
+ existsNode &&
584
+ parentNode instanceof QueryBuilderFilterTreeGroupNodeData
585
+ ) {
586
+ const propertyExpression1 = buildPropertyExpressionFromExistsNode(
587
+ filterState,
588
+ existsNode,
589
+ targetDropNode,
590
+ );
591
+ filterState.removeNodeAndPruneBranch(targetDropNode);
592
+ filterState.newGroupConditionFromNode(
593
+ existsNode,
594
+ treeNode,
595
+ parentNode.groupOperation,
596
+ );
597
+ const newParentNode = filterState.getParentNode(treeNode);
598
+ buildFilterTreeWithExists(
599
+ propertyExpression1,
600
+ filterState,
601
+ newParentNode,
602
+ );
603
+ } else {
604
+ filterState.newGroupWithConditionFromNode(treeNode, targetDropNode);
605
+ }
606
+ } else {
607
+ filterState.addNodeFromNode(treeNode, undefined);
608
+ }
392
609
  }
393
610
  };
394
611
 
@@ -604,7 +821,12 @@ const QueryBuilderFilterConditionEditor = observer(
604
821
  };
605
822
 
606
823
  return (
607
- <div className="query-builder-filter-tree__node__label__content dnd__entry__container">
824
+ <div
825
+ className="query-builder-filter-tree__node__label__content dnd__entry__container"
826
+ data-testid={
827
+ QUERY_BUILDER_TEST_ID.QUERY_BUILDER_FILTER_TREE_CONDITION_NODE_CONTENT
828
+ }
829
+ >
608
830
  <PanelEntryDropZonePlaceholder
609
831
  isDragOver={isDragOver}
610
832
  label="Add New Logical Group"
@@ -845,21 +1067,17 @@ const QueryBuilderFilterTreeNodeContainer = observer(
845
1067
  node,
846
1068
  );
847
1069
  } else if (node instanceof QueryBuilderFilterTreeConditionNodeData) {
848
- filterState.newGroupWithConditionFromNode(
849
- new QueryBuilderFilterTreeConditionNodeData(
850
- undefined,
851
- filterConditionState,
852
- ),
1070
+ buildFilterTree(
1071
+ filterConditionState.propertyExpressionState.propertyExpression,
1072
+ filterState,
853
1073
  node,
854
1074
  );
855
1075
  } else if (
856
1076
  node instanceof QueryBuilderFilterTreeBlankConditionNodeData
857
1077
  ) {
858
- filterState.replaceBlankNodeWithNode(
859
- new QueryBuilderFilterTreeConditionNodeData(
860
- undefined,
861
- filterConditionState,
862
- ),
1078
+ buildFilterTree(
1079
+ filterConditionState.propertyExpressionState.propertyExpression,
1080
+ filterState,
863
1081
  node,
864
1082
  );
865
1083
  }
@@ -17,14 +17,15 @@
17
17
  import { action, flow, makeObservable, observable } from 'mobx';
18
18
  import {
19
19
  type GeneratorFn,
20
+ type ContentType,
20
21
  assertErrorThrown,
21
22
  LogEvent,
22
23
  guaranteeNonNullable,
23
- type ContentType,
24
24
  downloadFileUsingDataURI,
25
25
  ActionState,
26
26
  StopWatch,
27
27
  getContentTypeFileExtension,
28
+ isBoolean,
28
29
  } from '@finos/legend-shared';
29
30
  import type { QueryBuilderState } from './QueryBuilderState.js';
30
31
  import {
@@ -37,6 +38,7 @@ import {
37
38
  buildRawLambdaFromLambdaFunction,
38
39
  reportGraphAnalytics,
39
40
  extractExecutionResultValues,
41
+ TDSExecutionResult,
40
42
  } from '@finos/legend-graph';
41
43
  import { buildLambdaFunction } from './QueryBuilderValueSpecificationBuilder.js';
42
44
  import { DEFAULT_TAB_SIZE } from '@finos/legend-application';
@@ -81,6 +83,7 @@ export class QueryBuilderResultState {
81
83
  latestRunHashCode?: string | undefined;
82
84
  queryRunPromise: Promise<ExecutionResult> | undefined = undefined;
83
85
  isQueryUsageViewerOpened = false;
86
+ rowData: Record<string, string | number | boolean | null>[] = [];
84
87
 
85
88
  selectedCells: QueryBuilderTDSResultCellData[];
86
89
  mousedOverCell: QueryBuilderTDSResultCellData | null = null;
@@ -110,6 +113,8 @@ export class QueryBuilderResultState {
110
113
  setQueryRunPromise: action,
111
114
  setIsQueryUsageViewerOpened: action,
112
115
  exportData: flow,
116
+ getRowData: action,
117
+ setRowData: action,
113
118
  runQuery: flow,
114
119
  cancelQuery: flow,
115
120
  generatePlan: flow,
@@ -124,6 +129,36 @@ export class QueryBuilderResultState {
124
129
  );
125
130
  }
126
131
 
132
+ getRowData(): Record<string, string | number | boolean | null>[] {
133
+ if (
134
+ this.executionResult &&
135
+ this.executionResult instanceof TDSExecutionResult
136
+ ) {
137
+ const data = this.executionResult.result.rows.map((_row, rowIdx) => {
138
+ const row: Record<string, string | number | boolean | null> = {};
139
+ const cols = (this.executionResult as TDSExecutionResult).result
140
+ .columns;
141
+ _row.values.forEach((value, colIdx) => {
142
+ // `ag-grid` shows `false` value as empty string so we have
143
+ // call `.toString()` to avoid this behavior.
144
+ // See https://github.com/finos/legend-studio/issues/1008
145
+ row[cols[colIdx] as string] = isBoolean(value)
146
+ ? String(value)
147
+ : value;
148
+ });
149
+ row.rowNumber = rowIdx;
150
+ return row;
151
+ });
152
+ this.rowData = data;
153
+ return data;
154
+ }
155
+ return [];
156
+ }
157
+
158
+ setRowData(val: Record<string, string | number | boolean | null>[]): void {
159
+ this.rowData = val;
160
+ }
161
+
127
162
  setIsSelectingCells(val: boolean): void {
128
163
  this.isSelectingCells = val;
129
164
  }