@finos/legend-query-builder 4.17.98 → 4.17.100

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 (138) hide show
  1. package/lib/components/explorer/QueryBuilderExplorerPanel.d.ts.map +1 -1
  2. package/lib/components/explorer/QueryBuilderExplorerPanel.js +13 -11
  3. package/lib/components/explorer/QueryBuilderExplorerPanel.js.map +1 -1
  4. package/lib/components/fetch-structure/QueryBuilderTDSComponentHelper.d.ts.map +1 -1
  5. package/lib/components/fetch-structure/QueryBuilderTDSComponentHelper.js +13 -11
  6. package/lib/components/fetch-structure/QueryBuilderTDSComponentHelper.js.map +1 -1
  7. package/lib/components/result/QueryBuilderResultPanel.d.ts.map +1 -1
  8. package/lib/components/result/QueryBuilderResultPanel.js +1 -1
  9. package/lib/components/result/QueryBuilderResultPanel.js.map +1 -1
  10. package/lib/components/result/tds/QueryBuilderTDSAsyncCellSelectionStats.d.ts +42 -0
  11. package/lib/components/result/tds/QueryBuilderTDSAsyncCellSelectionStats.d.ts.map +1 -0
  12. package/lib/components/result/tds/QueryBuilderTDSAsyncCellSelectionStats.js +212 -0
  13. package/lib/components/result/tds/QueryBuilderTDSAsyncCellSelectionStats.js.map +1 -0
  14. package/lib/components/result/tds/QueryBuilderTDSCellSelectionStats.d.ts +66 -0
  15. package/lib/components/result/tds/QueryBuilderTDSCellSelectionStats.d.ts.map +1 -0
  16. package/lib/components/result/tds/QueryBuilderTDSCellSelectionStats.js +265 -0
  17. package/lib/components/result/tds/QueryBuilderTDSCellSelectionStats.js.map +1 -0
  18. package/lib/components/result/tds/QueryBuilderTDSCellSelectionStatsBar.d.ts +28 -0
  19. package/lib/components/result/tds/QueryBuilderTDSCellSelectionStatsBar.d.ts.map +1 -0
  20. package/lib/components/result/tds/QueryBuilderTDSCellSelectionStatsBar.js +148 -0
  21. package/lib/components/result/tds/QueryBuilderTDSCellSelectionStatsBar.js.map +1 -0
  22. package/lib/components/result/tds/QueryBuilderTDSGridKeyboardShortcuts.d.ts +19 -0
  23. package/lib/components/result/tds/QueryBuilderTDSGridKeyboardShortcuts.d.ts.map +1 -0
  24. package/lib/components/result/tds/QueryBuilderTDSGridKeyboardShortcuts.js +196 -0
  25. package/lib/components/result/tds/QueryBuilderTDSGridKeyboardShortcuts.js.map +1 -0
  26. package/lib/components/result/tds/QueryBuilderTDSGridResult.d.ts +5 -0
  27. package/lib/components/result/tds/QueryBuilderTDSGridResult.d.ts.map +1 -1
  28. package/lib/components/result/tds/QueryBuilderTDSGridResult.js +64 -87
  29. package/lib/components/result/tds/QueryBuilderTDSGridResult.js.map +1 -1
  30. package/lib/components/result/tds/QueryBuilderTDSGridShared.d.ts +26 -0
  31. package/lib/components/result/tds/QueryBuilderTDSGridShared.d.ts.map +1 -0
  32. package/lib/components/result/tds/QueryBuilderTDSGridShared.js +51 -0
  33. package/lib/components/result/tds/QueryBuilderTDSGridShared.js.map +1 -0
  34. package/lib/components/result/tds/QueryBuilderTDSSimpleGridResult.d.ts +5 -0
  35. package/lib/components/result/tds/QueryBuilderTDSSimpleGridResult.d.ts.map +1 -1
  36. package/lib/components/result/tds/QueryBuilderTDSSimpleGridResult.js +169 -60
  37. package/lib/components/result/tds/QueryBuilderTDSSimpleGridResult.js.map +1 -1
  38. package/lib/data-access-overview.css +1 -1
  39. package/lib/index.css +2 -2
  40. package/lib/index.css.map +1 -1
  41. package/lib/package.json +1 -1
  42. package/lib/stores/QueryBuilderResultState.d.ts.map +1 -1
  43. package/lib/stores/QueryBuilderResultState.js +7 -1
  44. package/lib/stores/QueryBuilderResultState.js.map +1 -1
  45. package/lib/stores/QueryBuilderValueSpecificationHelper.d.ts +7 -0
  46. package/lib/stores/QueryBuilderValueSpecificationHelper.d.ts.map +1 -1
  47. package/lib/stores/QueryBuilderValueSpecificationHelper.js +31 -13
  48. package/lib/stores/QueryBuilderValueSpecificationHelper.js.map +1 -1
  49. package/lib/stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_Contain.d.ts.map +1 -1
  50. package/lib/stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_Contain.js +6 -4
  51. package/lib/stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_Contain.js.map +1 -1
  52. package/lib/stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_EndWith.d.ts.map +1 -1
  53. package/lib/stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_EndWith.js +6 -4
  54. package/lib/stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_EndWith.js.map +1 -1
  55. package/lib/stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_Equal.d.ts.map +1 -1
  56. package/lib/stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_Equal.js +14 -12
  57. package/lib/stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_Equal.js.map +1 -1
  58. package/lib/stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_GreaterThan.d.ts.map +1 -1
  59. package/lib/stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_GreaterThan.js +12 -10
  60. package/lib/stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_GreaterThan.js.map +1 -1
  61. package/lib/stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_In.d.ts.map +1 -1
  62. package/lib/stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_In.js +10 -8
  63. package/lib/stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_In.js.map +1 -1
  64. package/lib/stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_IsEmpty.d.ts.map +1 -1
  65. package/lib/stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_IsEmpty.js +4 -2
  66. package/lib/stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_IsEmpty.js.map +1 -1
  67. package/lib/stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_LessThan.d.ts.map +1 -1
  68. package/lib/stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_LessThan.js +12 -10
  69. package/lib/stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_LessThan.js.map +1 -1
  70. package/lib/stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_StartWith.d.ts.map +1 -1
  71. package/lib/stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_StartWith.js +6 -4
  72. package/lib/stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_StartWith.js.map +1 -1
  73. package/lib/stores/filter/operators/QueryBuilderFilterOperator_Contain.d.ts.map +1 -1
  74. package/lib/stores/filter/operators/QueryBuilderFilterOperator_Contain.js +6 -6
  75. package/lib/stores/filter/operators/QueryBuilderFilterOperator_Contain.js.map +1 -1
  76. package/lib/stores/filter/operators/QueryBuilderFilterOperator_EndWith.d.ts.map +1 -1
  77. package/lib/stores/filter/operators/QueryBuilderFilterOperator_EndWith.js +6 -6
  78. package/lib/stores/filter/operators/QueryBuilderFilterOperator_EndWith.js.map +1 -1
  79. package/lib/stores/filter/operators/QueryBuilderFilterOperator_Equal.d.ts.map +1 -1
  80. package/lib/stores/filter/operators/QueryBuilderFilterOperator_Equal.js +14 -12
  81. package/lib/stores/filter/operators/QueryBuilderFilterOperator_Equal.js.map +1 -1
  82. package/lib/stores/filter/operators/QueryBuilderFilterOperator_GreaterThan.d.ts.map +1 -1
  83. package/lib/stores/filter/operators/QueryBuilderFilterOperator_GreaterThan.js +12 -10
  84. package/lib/stores/filter/operators/QueryBuilderFilterOperator_GreaterThan.js.map +1 -1
  85. package/lib/stores/filter/operators/QueryBuilderFilterOperator_GreaterThanEqual.d.ts.map +1 -1
  86. package/lib/stores/filter/operators/QueryBuilderFilterOperator_GreaterThanEqual.js +12 -10
  87. package/lib/stores/filter/operators/QueryBuilderFilterOperator_GreaterThanEqual.js.map +1 -1
  88. package/lib/stores/filter/operators/QueryBuilderFilterOperator_In.d.ts.map +1 -1
  89. package/lib/stores/filter/operators/QueryBuilderFilterOperator_In.js +17 -13
  90. package/lib/stores/filter/operators/QueryBuilderFilterOperator_In.js.map +1 -1
  91. package/lib/stores/filter/operators/QueryBuilderFilterOperator_IsEmpty.d.ts.map +1 -1
  92. package/lib/stores/filter/operators/QueryBuilderFilterOperator_IsEmpty.js +3 -2
  93. package/lib/stores/filter/operators/QueryBuilderFilterOperator_IsEmpty.js.map +1 -1
  94. package/lib/stores/filter/operators/QueryBuilderFilterOperator_LessThan.d.ts.map +1 -1
  95. package/lib/stores/filter/operators/QueryBuilderFilterOperator_LessThan.js +12 -10
  96. package/lib/stores/filter/operators/QueryBuilderFilterOperator_LessThan.js.map +1 -1
  97. package/lib/stores/filter/operators/QueryBuilderFilterOperator_LessThanEqual.d.ts.map +1 -1
  98. package/lib/stores/filter/operators/QueryBuilderFilterOperator_LessThanEqual.js +12 -10
  99. package/lib/stores/filter/operators/QueryBuilderFilterOperator_LessThanEqual.js.map +1 -1
  100. package/lib/stores/filter/operators/QueryBuilderFilterOperator_StartWith.d.ts.map +1 -1
  101. package/lib/stores/filter/operators/QueryBuilderFilterOperator_StartWith.js +6 -6
  102. package/lib/stores/filter/operators/QueryBuilderFilterOperator_StartWith.js.map +1 -1
  103. package/lib/stores/shared/ValueSpecificationEditorHelper.d.ts.map +1 -1
  104. package/lib/stores/shared/ValueSpecificationEditorHelper.js +3 -2
  105. package/lib/stores/shared/ValueSpecificationEditorHelper.js.map +1 -1
  106. package/package.json +5 -5
  107. package/src/components/explorer/QueryBuilderExplorerPanel.tsx +13 -10
  108. package/src/components/fetch-structure/QueryBuilderTDSComponentHelper.tsx +13 -10
  109. package/src/components/result/QueryBuilderResultPanel.tsx +6 -2
  110. package/src/components/result/tds/QueryBuilderTDSAsyncCellSelectionStats.ts +254 -0
  111. package/src/components/result/tds/QueryBuilderTDSCellSelectionStats.ts +397 -0
  112. package/src/components/result/tds/QueryBuilderTDSCellSelectionStatsBar.tsx +439 -0
  113. package/src/components/result/tds/QueryBuilderTDSGridKeyboardShortcuts.ts +270 -0
  114. package/src/components/result/tds/QueryBuilderTDSGridResult.tsx +107 -112
  115. package/src/components/result/tds/QueryBuilderTDSGridShared.ts +76 -0
  116. package/src/components/result/tds/QueryBuilderTDSSimpleGridResult.tsx +272 -99
  117. package/src/stores/QueryBuilderResultState.ts +10 -1
  118. package/src/stores/QueryBuilderValueSpecificationHelper.ts +35 -10
  119. package/src/stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_Contain.ts +5 -3
  120. package/src/stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_EndWith.ts +5 -3
  121. package/src/stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_Equal.ts +16 -13
  122. package/src/stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_GreaterThan.ts +18 -11
  123. package/src/stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_In.ts +12 -9
  124. package/src/stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_IsEmpty.ts +6 -1
  125. package/src/stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_LessThan.ts +18 -11
  126. package/src/stores/fetch-structure/tds/post-filter/operators/QueryBuilderPostFilterOperator_StartWith.ts +5 -3
  127. package/src/stores/filter/operators/QueryBuilderFilterOperator_Contain.ts +9 -5
  128. package/src/stores/filter/operators/QueryBuilderFilterOperator_EndWith.ts +9 -5
  129. package/src/stores/filter/operators/QueryBuilderFilterOperator_Equal.ts +16 -13
  130. package/src/stores/filter/operators/QueryBuilderFilterOperator_GreaterThan.ts +18 -11
  131. package/src/stores/filter/operators/QueryBuilderFilterOperator_GreaterThanEqual.ts +18 -11
  132. package/src/stores/filter/operators/QueryBuilderFilterOperator_In.ts +23 -14
  133. package/src/stores/filter/operators/QueryBuilderFilterOperator_IsEmpty.ts +3 -1
  134. package/src/stores/filter/operators/QueryBuilderFilterOperator_LessThan.ts +18 -11
  135. package/src/stores/filter/operators/QueryBuilderFilterOperator_LessThanEqual.ts +18 -11
  136. package/src/stores/filter/operators/QueryBuilderFilterOperator_StartWith.ts +9 -5
  137. package/src/stores/shared/ValueSpecificationEditorHelper.ts +6 -2
  138. package/tsconfig.json +5 -0
@@ -14,21 +14,23 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
- import { ContextMenu, WarningIcon, clsx } from '@finos/legend-art';
17
+ import { WarningIcon, clsx } from '@finos/legend-art';
18
18
  import { observer } from 'mobx-react-lite';
19
19
  import type { QueryBuilderState } from '../../../stores/QueryBuilderState.js';
20
20
  import {
21
21
  DataGrid,
22
22
  type DataGridColumnDefinition,
23
23
  type DataGridCustomHeaderProps,
24
+ type DataGridGetContextMenuItemsParams,
25
+ type DataGridMenuItemDef,
26
+ type DataGridDefaultMenuItem,
24
27
  } from '@finos/legend-lego/data-grid';
25
28
  import {
26
29
  getRowDataFromExecutionResult,
27
- QueryBuilderGridResultContextMenu,
28
30
  type IQueryRendererParamsWithGridType,
29
31
  } from './QueryBuilderTDSResultShared.js';
30
- import { QueryBuilderTDSState } from '../../../stores/fetch-structure/tds/QueryBuilderTDSState.js';
31
32
  import { DEFAULT_LOCALE } from '../../../graph-manager/QueryBuilderConst.js';
33
+ import type { QueryBuilderResultState } from '../../../stores/QueryBuilderResultState.js';
32
34
  import {
33
35
  guaranteeNonNullable,
34
36
  isBoolean,
@@ -36,6 +38,8 @@ import {
36
38
  isString,
37
39
  isValidURL,
38
40
  } from '@finos/legend-shared';
41
+ import { useApplicationStore } from '@finos/legend-application';
42
+ import { useCallback, useEffect, useRef } from 'react';
39
43
  import {
40
44
  type TDSResultCellCoordinate,
41
45
  type TDSResultCellData,
@@ -46,6 +50,9 @@ import {
46
50
  PRIMITIVE_TYPE,
47
51
  } from '@finos/legend-graph';
48
52
  import { QUERY_BUILDER_TEST_ID } from '../../../__lib__/QueryBuilderTesting.js';
53
+ import { QueryBuilderTDSCellSelectionStatsBar } from './QueryBuilderTDSCellSelectionStatsBar.js';
54
+ import { useAsyncCellSelectionStats } from './QueryBuilderTDSAsyncCellSelectionStats.js';
55
+ import { buildTDSGridContextMenuItems } from './QueryBuilderTDSGridShared.js';
49
56
 
50
57
  export const MAXIMUM_FRACTION_DIGITS = 4;
51
58
 
@@ -114,13 +121,8 @@ const QueryResultCellRenderer = observer(
114
121
  (params: IQueryRendererParamsWithGridType) => {
115
122
  const resultState = params.resultState;
116
123
  const tdsExecutionResult = params.tdsExecutionResult;
117
- const fetchStructureImplementation =
118
- resultState.queryBuilderState.fetchStructureState.implementation;
119
- const applicationStore = resultState.queryBuilderState.applicationStore;
120
124
  const cellValue = params.value as TDSResultCellDataType;
121
125
  const nodeRowIndex = guaranteeNonNullable(params.node.rowIndex);
122
- const darkMode =
123
- !applicationStore.layoutService.TEMPORARY__isLightColorThemeEnabled;
124
126
 
125
127
  const formattedCellValue = (): TDSResultCellDataType => {
126
128
  if (isNumber(cellValue)) {
@@ -321,104 +323,136 @@ const QueryResultCellRenderer = observer(
321
323
 
322
324
  resultState.setMouseOverCell(resultState.selectedCells[0] ?? null);
323
325
  };
324
- const getContextMenuRenderer = (): React.ReactNode => {
325
- if (fetchStructureImplementation instanceof QueryBuilderTDSState) {
326
- const copyCellValue = applicationStore.guardUnhandledError(() =>
327
- applicationStore.clipboardService.copyTextToClipboard(
328
- fetchStructureImplementation.queryBuilderState.resultState.selectedCells
329
- .map((cellData) => cellData.value)
330
- .join(','),
331
- ),
332
- );
333
- const findRowFromRowIndex = (rowIndex: number): string => {
334
- if (
335
- !fetchStructureImplementation.queryBuilderState.resultState
336
- .executionResult ||
337
- !(
338
- fetchStructureImplementation.queryBuilderState.resultState
339
- .executionResult instanceof TDSExecutionResult
340
- )
341
- ) {
342
- return '';
343
- }
344
- // try to get the entire row value separated by comma
345
- // rowData is in format of {columnName: value, columnName1: value, ...., rowNumber:value}
346
- const valueArr: TDSResultCellDataType[] = [];
347
- Object.entries(
348
- params.api.getRenderedNodes().find((n) => n.rowIndex === rowIndex)
349
- ?.data as TDSRowDataType,
350
- ).forEach((entry) => {
351
- if (entry[0] !== 'rowNumber') {
352
- valueArr.push(entry[1]);
353
- }
354
- });
355
- return valueArr.join(',');
356
- };
357
- const copyRowValue = applicationStore.guardUnhandledError(() =>
358
- applicationStore.clipboardService.copyTextToClipboard(
359
- findRowFromRowIndex(
360
- fetchStructureImplementation.queryBuilderState.resultState
361
- .selectedCells[0]?.coordinates.rowIndex ?? 0,
362
- ),
363
- ),
364
- );
365
- return (
366
- <QueryBuilderGridResultContextMenu
367
- data={resultState.mousedOverCell}
368
- tdsState={fetchStructureImplementation}
369
- copyCellValueFunc={copyCellValue}
370
- copyCellRowValueFunc={copyRowValue}
371
- />
372
- );
373
- }
374
- return null;
375
- };
376
326
 
377
327
  return (
378
- <ContextMenu
379
- content={getContextMenuRenderer()}
380
- disabled={
381
- !(
382
- resultState.queryBuilderState.fetchStructureState
383
- .implementation instanceof QueryBuilderTDSState
384
- ) ||
385
- !resultState.queryBuilderState.isQuerySupported ||
386
- !resultState.mousedOverCell
387
- }
388
- menuProps={{ elevation: 7 }}
389
- className={clsx('query-builder__result__tds-grid', {
390
- 'ag-theme-balham': !darkMode,
391
- 'ag-theme-balham-dark': darkMode,
328
+ <div
329
+ className={clsx('query-builder__result__values__table__cell', {
330
+ 'query-builder__result__values__table__cell--active':
331
+ cellInFilteredResults,
392
332
  })}
333
+ onMouseDown={(event) => mouseDown(event)}
334
+ onMouseUp={(event) => mouseUp(event)}
335
+ onMouseOver={(event) => mouseOver(event)}
393
336
  >
394
- <div
395
- className={clsx('query-builder__result__values__table__cell', {
396
- 'query-builder__result__values__table__cell--active':
397
- cellInFilteredResults,
398
- })}
399
- onMouseDown={(event) => mouseDown(event)}
400
- onMouseUp={(event) => mouseUp(event)}
401
- onMouseOver={(event) => mouseOver(event)}
402
- >
403
- {cellValueUrlLink ? (
404
- <a href={cellValueUrlLink} target="_blank" rel="noreferrer">
405
- {cellValueUrlLink}
406
- </a>
407
- ) : (
408
- <span>{formattedCellValue()}</span>
409
- )}
410
- </div>
411
- </ContextMenu>
337
+ {cellValueUrlLink ? (
338
+ <a href={cellValueUrlLink} target="_blank" rel="noreferrer">
339
+ {cellValueUrlLink}
340
+ </a>
341
+ ) : (
342
+ <span>{formattedCellValue()}</span>
343
+ )}
344
+ </div>
412
345
  );
413
346
  },
414
347
  );
415
348
 
349
+ // ---------------------------------------------------------------------------
350
+ // Simple-grid keyboard shortcut helpers (extracted to avoid >4-level nesting)
351
+ // ---------------------------------------------------------------------------
352
+
353
+ type SimpleGridRow = { values: (string | number | boolean | null)[] };
354
+
355
+ /** Build TDSResultCellData[] for all cells in the grid. */
356
+ const buildAllCells = (
357
+ columns: string[],
358
+ rows: SimpleGridRow[],
359
+ ): TDSResultCellData[] => {
360
+ const cells: TDSResultCellData[] = [];
361
+ rows.forEach((row, rowIndex) => {
362
+ columns.forEach((colName, colIndex) => {
363
+ cells.push({
364
+ value: row.values[colIndex],
365
+ columnName: colName,
366
+ coordinates: { rowIndex, colIndex },
367
+ });
368
+ });
369
+ });
370
+ return cells;
371
+ };
372
+
373
+ /** Build TDSResultCellData[] for specific column indices across all rows. */
374
+ const buildColumnCells = (
375
+ columns: string[],
376
+ rows: SimpleGridRow[],
377
+ colIndices: number[],
378
+ ): TDSResultCellData[] => {
379
+ const cells: TDSResultCellData[] = [];
380
+ rows.forEach((row, ri) => {
381
+ colIndices.forEach((ci) => {
382
+ cells.push({
383
+ value: row.values[ci],
384
+ columnName: columns[ci] ?? '',
385
+ coordinates: { rowIndex: ri, colIndex: ci },
386
+ });
387
+ });
388
+ });
389
+ return cells;
390
+ };
391
+
392
+ /** Build TDSResultCellData[] for specific row indices across all columns. */
393
+ const buildRowCells = (
394
+ columns: string[],
395
+ rows: SimpleGridRow[],
396
+ rowIndices: number[],
397
+ ): TDSResultCellData[] => {
398
+ const cells: TDSResultCellData[] = [];
399
+ rowIndices.forEach((ri) => {
400
+ columns.forEach((cn, ci) => {
401
+ cells.push({
402
+ value: rows[ri]?.values[ci],
403
+ columnName: cn,
404
+ coordinates: { rowIndex: ri, colIndex: ci },
405
+ });
406
+ });
407
+ });
408
+ return cells;
409
+ };
410
+
411
+ /** Resolve the distinct column indices to select for Ctrl+Space. */
412
+ const resolveColumnIndices = (
413
+ selectedCells: TDSResultCellData[],
414
+ fallbackColIndex: number,
415
+ ): number[] =>
416
+ selectedCells.length > 0
417
+ ? [...new Set(selectedCells.map((c) => c.coordinates.colIndex))]
418
+ : [fallbackColIndex];
419
+
420
+ /** Resolve the distinct row indices to select for Shift+Space. */
421
+ const resolveRowIndices = (
422
+ selectedCells: TDSResultCellData[],
423
+ fallbackRowIndex: number,
424
+ ): number[] =>
425
+ selectedCells.length > 0
426
+ ? [...new Set(selectedCells.map((c) => c.coordinates.rowIndex))]
427
+ : [fallbackRowIndex];
428
+
429
+ /** Apply a cell selection to the result state if non-empty. */
430
+ const applySelection = (
431
+ cells: TDSResultCellData[],
432
+ resultState: QueryBuilderResultState,
433
+ ): void => {
434
+ if (cells.length > 0) {
435
+ resultState.setSelectedCells(cells);
436
+ resultState.setMouseOverCell(cells[0] ?? null);
437
+ }
438
+ };
439
+
416
440
  export const QueryBuilderTDSSimpleGridResult = observer(
417
441
  (props: {
418
442
  executionResult: TDSExecutionResult;
419
443
  queryBuilderState: QueryBuilderState;
444
+ /**
445
+ * Whether to show the cell-selection summary statistics bar below the grid.
446
+ * Defaults to `true`.
447
+ */
448
+ showSummaryStats?: boolean;
420
449
  }) => {
421
- const { executionResult, queryBuilderState } = props;
450
+ const {
451
+ executionResult,
452
+ queryBuilderState,
453
+ showSummaryStats = true,
454
+ } = props;
455
+ const applicationStore = useApplicationStore();
422
456
  const resultState = queryBuilderState.resultState;
423
457
  const darkMode =
424
458
  !queryBuilderState.applicationStore.layoutService
@@ -441,13 +475,129 @@ export const QueryBuilderTDSSimpleGridResult = observer(
441
475
  }) as DataGridColumnDefinition,
442
476
  );
443
477
 
478
+ // Simple grid: no AG Grid cell-range API, so we keep selectedCells in
479
+ // MobX and pass a null gridApiRef. The hook falls back to reading from
480
+ // selectedCells directly when the gridApiRef is null.
481
+ const selectedCells = resultState.selectedCells;
482
+ const columnTypes = new Map<string, string | undefined>(
483
+ executionResult.builder.columns.map((c) => [c.name, c.type]),
484
+ );
485
+ const simpleGridApiRef = useRef<null>(null);
486
+ const cellSelectionStats = useAsyncCellSelectionStats(
487
+ selectedCells.length, // version counter — changes when selection changes
488
+ columnTypes,
489
+ simpleGridApiRef,
490
+ selectedCells, // fallback cells for when gridApiRef is null
491
+ );
492
+
493
+ const gridContainerRef = useRef<HTMLDivElement>(null);
494
+ // Track the last cell the user clicked so keyboard shortcuts know
495
+ // which row/column to act on (AG Grid internal focus is not used here
496
+ // because we move DOM focus to the container div, not into a grid cell).
497
+ const lastClickedCellRef = useRef<{
498
+ colName: string;
499
+ colIndex: number;
500
+ rowIndex: number;
501
+ } | null>(null);
502
+
503
+ // All keyboard shortcuts handled in the native capture-phase listener so
504
+ // they fire reliably when the container div has focus.
505
+ useEffect(() => {
506
+ const el = gridContainerRef.current;
507
+ if (!el) {
508
+ return undefined;
509
+ }
510
+ const handler = (e: KeyboardEvent): void => {
511
+ const columns = executionResult.result.columns;
512
+ const rows = executionResult.result.rows;
513
+
514
+ // Ctrl+A — select all cells
515
+ if (e.ctrlKey && e.code === 'KeyA') {
516
+ e.preventDefault();
517
+ // eslint-disable-next-line no-console
518
+ console.debug(
519
+ `[TDS Simple Grid] Ctrl+A → selecting all: ${rows.length} rows × ${columns.length} columns`,
520
+ );
521
+ const allCells = buildAllCells(columns, rows);
522
+ applySelection(allCells, resultState);
523
+ return;
524
+ }
525
+
526
+ // Ctrl+Space — select entire columns for all columns in the current selection.
527
+ if (e.ctrlKey && e.code === 'Space') {
528
+ e.preventDefault();
529
+ const cell = lastClickedCellRef.current;
530
+ if (!cell) {
531
+ return;
532
+ }
533
+ const colIndices = resolveColumnIndices(
534
+ resultState.selectedCells,
535
+ cell.colIndex,
536
+ );
537
+ // eslint-disable-next-line no-console
538
+ console.debug(
539
+ `[TDS Simple Grid] Ctrl+Space → selecting ${colIndices.length} column(s), ${rows.length} rows`,
540
+ );
541
+ const newCells = buildColumnCells(columns, rows, colIndices);
542
+ applySelection(newCells, resultState);
543
+ return;
544
+ }
545
+
546
+ // Shift+Space — select entire rows for all rows in the current selection.
547
+ if (e.shiftKey && e.code === 'Space') {
548
+ e.preventDefault();
549
+ const cell = lastClickedCellRef.current;
550
+ if (!cell) {
551
+ return;
552
+ }
553
+ const rowIndices = resolveRowIndices(
554
+ resultState.selectedCells,
555
+ cell.rowIndex,
556
+ );
557
+ // eslint-disable-next-line no-console
558
+ console.debug(
559
+ `[TDS Simple Grid] Shift+Space → selecting ${rowIndices.length} row(s), ${columns.length} columns`,
560
+ );
561
+ const newCells = buildRowCells(columns, rows, rowIndices);
562
+ applySelection(newCells, resultState);
563
+ }
564
+ };
565
+ el.addEventListener('keydown', handler, { capture: true });
566
+ return () =>
567
+ el.removeEventListener('keydown', handler, { capture: true });
568
+ }, [
569
+ executionResult.result.columns,
570
+ executionResult.result.rows,
571
+ resultState,
572
+ ]);
573
+
574
+ const getContextMenuItems = useCallback(
575
+ (
576
+ params: DataGridGetContextMenuItemsParams<TDSRowDataType>,
577
+ ): (DataGridDefaultMenuItem | DataGridMenuItemDef)[] =>
578
+ buildTDSGridContextMenuItems(
579
+ params,
580
+ applicationStore,
581
+ resultState,
582
+ queryBuilderState.applicationStore.alertUnhandledError,
583
+ ),
584
+ [
585
+ applicationStore,
586
+ resultState,
587
+ queryBuilderState.applicationStore.alertUnhandledError,
588
+ ],
589
+ );
590
+
444
591
  return (
445
592
  <div
446
593
  data-testid={QUERY_BUILDER_TEST_ID.QUERY_BUILDER_RESULT_VALUES_TDS}
447
594
  className="query-builder__result__values__table"
448
595
  >
449
596
  <div
597
+ ref={gridContainerRef}
598
+ tabIndex={-1}
450
599
  className={clsx('query-builder__result__tds-grid', {
600
+ 'query-builder__result__tds-grid--with-stats-bar': showSummaryStats,
451
601
  'ag-theme-balham': !darkMode,
452
602
  'ag-theme-balham-dark': darkMode,
453
603
  })}
@@ -457,11 +607,6 @@ export const QueryBuilderTDSSimpleGridResult = observer(
457
607
  gridOptions={{
458
608
  suppressScrollOnNewData: true,
459
609
  getRowId: (data) => `${data.data.rowNumber}`,
460
- rowSelection: {
461
- mode: 'multiRow',
462
- checkboxes: false,
463
- headerCheckbox: false,
464
- },
465
610
  }}
466
611
  // NOTE: when column definition changed, we need to force refresh the cell to make sure the cell renderer is updated
467
612
  // See https://stackoverflow.com/questions/56341073/how-to-refresh-an-ag-grid-when-a-change-occurs-inside-a-custom-cell-renderer-com
@@ -469,9 +614,37 @@ export const QueryBuilderTDSSimpleGridResult = observer(
469
614
  params.api.refreshCells({ force: true });
470
615
  }}
471
616
  suppressFieldDotNotation={true}
472
- suppressContextMenu={false}
473
617
  columnDefs={colDefs}
618
+ getContextMenuItems={(params) => getContextMenuItems(params)}
619
+ onCellClicked={(event) => {
620
+ // Skip clicks originated from keyboard shortcuts (Ctrl/Shift+Space fires
621
+ // a synthetic click that would reset the range selection we just set).
622
+ const e = event.event as MouseEvent | undefined;
623
+ if (e?.ctrlKey || e?.shiftKey) {
624
+ return;
625
+ }
626
+ const colName = event.column.getColId();
627
+ const colIndex = executionResult.result.columns.indexOf(colName);
628
+ // eslint-disable-next-line no-console
629
+ console.debug(
630
+ `[TDS Simple Grid] onCellClicked: col=${colName} row=${event.rowIndex}`,
631
+ );
632
+ lastClickedCellRef.current = {
633
+ colName,
634
+ colIndex,
635
+ rowIndex: event.rowIndex ?? 0,
636
+ };
637
+ gridContainerRef.current?.focus({ preventScroll: true });
638
+ }}
474
639
  />
640
+ {showSummaryStats && cellSelectionStats !== undefined && (
641
+ <QueryBuilderTDSCellSelectionStatsBar
642
+ stats={cellSelectionStats.stats}
643
+ cellCount={cellSelectionStats.cellCount}
644
+ countReady={cellSelectionStats.countReady}
645
+ darkMode={darkMode}
646
+ />
647
+ )}
475
648
  </div>
476
649
  </div>
477
650
  );
@@ -224,7 +224,16 @@ export class QueryBuilderResultState {
224
224
  }
225
225
 
226
226
  addSelectedCell(val: TDSResultCellData): void {
227
- this.selectedCells.push(val);
227
+ // Deduplicate by coordinates — Ctrl+clicking an already-selected cell
228
+ // must not inflate the count or corrupt statistics.
229
+ const alreadyPresent = this.selectedCells.some(
230
+ (c) =>
231
+ c.coordinates.rowIndex === val.coordinates.rowIndex &&
232
+ c.coordinates.colIndex === val.coordinates.colIndex,
233
+ );
234
+ if (!alreadyPresent) {
235
+ this.selectedCells.push(val);
236
+ }
228
237
  }
229
238
 
230
239
  setSelectedCells(val: TDSResultCellData[]): void {
@@ -47,6 +47,7 @@ import {
47
47
  isSubType,
48
48
  type ObserverContext,
49
49
  InstanceValue,
50
+ getCorrespondingStandardPrimitiveType,
50
51
  } from '@finos/legend-graph';
51
52
  import {
52
53
  addUniqueEntry,
@@ -211,6 +212,21 @@ export const isPropertyExpressionChainOptional = (
211
212
  return isOptional;
212
213
  };
213
214
 
215
+ /**
216
+ * Maps a type to its equivalent standard PRIMITIVE_TYPE for compatibility checks.
217
+ * For standard primitive types, returns their own enum value.
218
+ * For precise primitive types, returns their standard equivalent.
219
+ * Returns undefined for non-primitive types (e.g. Enumeration, Class).
220
+ */
221
+ export const getStandardPrimitiveTypeEquivalent = (
222
+ type: Type,
223
+ ): PRIMITIVE_TYPE | undefined => {
224
+ if (type instanceof PrimitiveType) {
225
+ return type.path as PRIMITIVE_TYPE;
226
+ }
227
+ return getCorrespondingStandardPrimitiveType(type.path);
228
+ };
229
+
214
230
  export const isTypeCompatibleForAssignment = (
215
231
  type: Type | undefined,
216
232
  assignmentType: Type,
@@ -228,20 +244,29 @@ export const isTypeCompatibleForAssignment = (
228
244
  PRIMITIVE_TYPE.LATESTDATE,
229
245
  ] as string[];
230
246
 
247
+ if (type === undefined) {
248
+ return false;
249
+ }
250
+
251
+ // Normalize precise primitive types to their standard equivalents
252
+ const normalizedTypePath =
253
+ getStandardPrimitiveTypeEquivalent(type) ?? type.path;
254
+ const normalizedAssignmentTypePath =
255
+ getStandardPrimitiveTypeEquivalent(assignmentType) ?? assignmentType.path;
256
+
231
257
  // When changing the return type for LHS, the RHS value should be adjusted accordingly.
232
258
  return (
233
- type !== undefined &&
234
259
  // Numeric value is handled loosely because of autoboxing
235
260
  // e.g. LHS (integer) = RHS (float) is acceptable
236
- ((NUMERIC_PRIMITIVE_TYPES.includes(type.path) &&
237
- NUMERIC_PRIMITIVE_TYPES.includes(assignmentType.path)) ||
238
- // Date value is handled loosely as well if the LHS is of type DateTime
239
- // This is because we would simulate auto-boxing for date by altering the
240
- // Pure function used for the operation
241
- // e.g. LHS(DateTime) = RHS(Date) -> we use isOnDay() instead of is()
242
- DATE_PRIMITIVE_TYPES.includes(type.path) ||
243
- type.path === assignmentType.path ||
244
- isSuperType(assignmentType, type))
261
+ (NUMERIC_PRIMITIVE_TYPES.includes(normalizedTypePath) &&
262
+ NUMERIC_PRIMITIVE_TYPES.includes(normalizedAssignmentTypePath)) ||
263
+ // Date value is handled loosely as well if the LHS is of type DateTime
264
+ // This is because we would simulate auto-boxing for date by altering the
265
+ // Pure function used for the operation
266
+ // e.g. LHS(DateTime) = RHS(Date) -> we use isOnDay() instead of is()
267
+ DATE_PRIMITIVE_TYPES.includes(normalizedTypePath) ||
268
+ normalizedTypePath === normalizedAssignmentTypePath ||
269
+ isSuperType(assignmentType, type)
245
270
  );
246
271
  };
247
272
 
@@ -21,7 +21,6 @@ import {
21
21
  type LambdaFunction,
22
22
  type FunctionExpression,
23
23
  PRIMITIVE_TYPE,
24
- PrimitiveType,
25
24
  } from '@finos/legend-graph';
26
25
  import {
27
26
  guaranteeNonNullable,
@@ -37,6 +36,7 @@ import type {
37
36
  } from '../QueryBuilderPostFilterState.js';
38
37
  import {
39
38
  buildNotExpression,
39
+ getStandardPrimitiveTypeEquivalent,
40
40
  unwrapNotExpression,
41
41
  } from '../../../../QueryBuilderValueSpecificationHelper.js';
42
42
  import { buildPostFilterConditionExpressionHelper } from './QueryBuilderPostFilterOperatorValueSpecificationBuilder.js';
@@ -53,15 +53,17 @@ export class QueryBuilderPostFilterOperator_Contain
53
53
  }
54
54
 
55
55
  isCompatibleWithType(type: Type): boolean {
56
- return type === PrimitiveType.STRING;
56
+ return getStandardPrimitiveTypeEquivalent(type) === PRIMITIVE_TYPE.STRING;
57
57
  }
58
58
 
59
59
  isCompatibleWithConditionValue(
60
60
  postFilterConditionState: PostFilterConditionState,
61
61
  ): boolean {
62
+ const rhsType = postFilterConditionState.rightConditionValue.type;
62
63
  return (
63
64
  !postFilterConditionState.rightConditionValue.isCollection &&
64
- PrimitiveType.STRING === postFilterConditionState.rightConditionValue.type
65
+ rhsType !== undefined &&
66
+ getStandardPrimitiveTypeEquivalent(rhsType) === PRIMITIVE_TYPE.STRING
65
67
  );
66
68
  }
67
69
 
@@ -20,7 +20,6 @@ import {
20
20
  type FunctionExpression,
21
21
  type LambdaFunction,
22
22
  PRIMITIVE_TYPE,
23
- PrimitiveType,
24
23
  } from '@finos/legend-graph';
25
24
  import {
26
25
  type Hashable,
@@ -35,6 +34,7 @@ import type {
35
34
  } from '../QueryBuilderPostFilterState.js';
36
35
  import {
37
36
  buildNotExpression,
37
+ getStandardPrimitiveTypeEquivalent,
38
38
  unwrapNotExpression,
39
39
  } from '../../../../QueryBuilderValueSpecificationHelper.js';
40
40
  import { buildPostFilterConditionExpressionHelper } from './QueryBuilderPostFilterOperatorValueSpecificationBuilder.js';
@@ -51,15 +51,17 @@ export class QueryBuilderPostFilterOperator_EndWith
51
51
  }
52
52
 
53
53
  isCompatibleWithType(type: Type): boolean {
54
- return PrimitiveType.STRING === type;
54
+ return getStandardPrimitiveTypeEquivalent(type) === PRIMITIVE_TYPE.STRING;
55
55
  }
56
56
 
57
57
  isCompatibleWithConditionValue(
58
58
  postFilterConditionState: PostFilterConditionState,
59
59
  ): boolean {
60
+ const rhsType = postFilterConditionState.rightConditionValue.type;
60
61
  return (
61
62
  !postFilterConditionState.rightConditionValue.isCollection &&
62
- PrimitiveType.STRING === postFilterConditionState.rightConditionValue.type
63
+ rhsType !== undefined &&
64
+ getStandardPrimitiveTypeEquivalent(rhsType) === PRIMITIVE_TYPE.STRING
63
65
  );
64
66
  }
65
67
 
@@ -38,6 +38,7 @@ import {
38
38
  } from '../QueryBuilderPostFilterState.js';
39
39
  import {
40
40
  buildNotExpression,
41
+ getStandardPrimitiveTypeEquivalent,
41
42
  isTypeCompatibleForAssignment,
42
43
  unwrapNotExpression,
43
44
  } from '../../../../QueryBuilderValueSpecificationHelper.js';
@@ -55,20 +56,22 @@ export class QueryBuilderPostFilterOperator_Equal
55
56
  }
56
57
 
57
58
  isCompatibleWithType(type: Type): boolean {
59
+ const normalizedType = getStandardPrimitiveTypeEquivalent(type);
58
60
  return (
59
- (
60
- [
61
- PRIMITIVE_TYPE.STRING,
62
- PRIMITIVE_TYPE.BOOLEAN,
63
- PRIMITIVE_TYPE.NUMBER,
64
- PRIMITIVE_TYPE.INTEGER,
65
- PRIMITIVE_TYPE.DECIMAL,
66
- PRIMITIVE_TYPE.FLOAT,
67
- PRIMITIVE_TYPE.DATE,
68
- PRIMITIVE_TYPE.STRICTDATE,
69
- PRIMITIVE_TYPE.DATETIME,
70
- ] as string[]
71
- ).includes(type.path) ||
61
+ (normalizedType !== undefined &&
62
+ (
63
+ [
64
+ PRIMITIVE_TYPE.STRING,
65
+ PRIMITIVE_TYPE.BOOLEAN,
66
+ PRIMITIVE_TYPE.NUMBER,
67
+ PRIMITIVE_TYPE.INTEGER,
68
+ PRIMITIVE_TYPE.DECIMAL,
69
+ PRIMITIVE_TYPE.FLOAT,
70
+ PRIMITIVE_TYPE.DATE,
71
+ PRIMITIVE_TYPE.STRICTDATE,
72
+ PRIMITIVE_TYPE.DATETIME,
73
+ ] as string[]
74
+ ).includes(normalizedType)) ||
72
75
  // if the type is enumeration, make sure the enumeration has some value
73
76
  (type instanceof Enumeration && type.values.length > 0)
74
77
  );