@finos/legend-query-builder 4.10.0 → 4.10.1

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 (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
  }