@meshmakers/octo-meshboard 3.3.1150 → 3.3.1170

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.
@@ -2545,6 +2545,7 @@ const ExecuteRuntimeQueryDocumentDto = gql `
2545
2545
  columns {
2546
2546
  attributePath
2547
2547
  attributeValueType
2548
+ aggregationType
2548
2549
  }
2549
2550
  rows(after: $after, first: $first, fieldFilter: $fieldFilter) {
2550
2551
  totalCount
@@ -3941,6 +3942,314 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
3941
3942
  type: Input
3942
3943
  }] } });
3943
3944
 
3945
+ /**
3946
+ * Utility functions for widget data transformation.
3947
+ * These are pure functions extracted for testability.
3948
+ */
3949
+ /**
3950
+ * Supported row types for query processing.
3951
+ */
3952
+ const SUPPORTED_ROW_TYPES = [
3953
+ 'RtSimpleQueryRow',
3954
+ 'RtAggregationQueryRow',
3955
+ 'RtGroupingAggregationQueryRow'
3956
+ ];
3957
+ /**
3958
+ * Sanitizes field names for use as object keys.
3959
+ * Replaces dots with underscores so the value can be stored under a single key
3960
+ * without Kendo's dot-path navigation interpreting the dots as nested access.
3961
+ * Used when generating record keys, NOT for path comparison — use
3962
+ * {@link matchesAttributePath} for matching cell paths against widget configs.
3963
+ * @param fieldName The field name to sanitize
3964
+ * @returns Sanitized field name
3965
+ */
3966
+ function sanitizeFieldName(fieldName) {
3967
+ return fieldName.replace(/\./g, '_');
3968
+ }
3969
+ /**
3970
+ * Strips a trailing aggregation function suffix from a path so the base path can
3971
+ * be compared independently of which aggregation produced it. The engine emits
3972
+ * cell paths like `meterreading_count` / `amountvalue_sum` for RT aggregation,
3973
+ * grouped aggregation, and stream-data variants.
3974
+ */
3975
+ function stripAggregationFunctionSuffix(path) {
3976
+ return path.replace(/_(?:count|sum|avg|min|max)$/i, '');
3977
+ }
3978
+ /**
3979
+ * Canonical key form: drop dot and underscore separators and lowercase the rest.
3980
+ * Lets us compare a widget config's stored field name (which may be
3981
+ * `meterReading`, `meter_reading`, or `amount_value`) against a cell path
3982
+ * (which may be wire-form `meterreading_count` or `amountvalue_sum`) on a
3983
+ * single common form.
3984
+ */
3985
+ function toCanonicalAttributeKey(s) {
3986
+ return s.replace(/[._]/g, '').toLowerCase();
3987
+ }
3988
+ /**
3989
+ * Detects whether a path ends with the engine's aggregation function suffix
3990
+ * (`_count`, `_sum`, `_avg`, `_min`, `_max`). Used to decide whether the loose
3991
+ * back-compat fallback in {@link matchesAttributePath} should fire.
3992
+ */
3993
+ function hasAggregationFunctionSuffix(path) {
3994
+ return /_(?:count|sum|avg|min|max)$/i.test(path);
3995
+ }
3996
+ /**
3997
+ * Returns true when a cell's `attributePath` refers to the same source attribute
3998
+ * as a widget config's stored field name. Handles three input shapes:
3999
+ *
4000
+ * - Simple-query cells use the original CK attribute path (e.g. `meterReading`
4001
+ * or `amount.value`).
4002
+ * - RT aggregation cells use the engine's wire-form key with a function suffix
4003
+ * (e.g. `meterreading_count`, `amountvalue_sum`).
4004
+ * - RT grouping cells and stream-data cells use the same wire-form without a
4005
+ * function suffix (e.g. `operatingstatus`).
4006
+ *
4007
+ * Widget configs typically store `sanitizeFieldName(originalPath)`, e.g.
4008
+ * `meterReading` or `amount_value`. This helper returns true when both refer
4009
+ * to the same source attribute, regardless of which form they're in.
4010
+ *
4011
+ * Exact `sanitizeFieldName` match wins (preserves behavior for simple queries
4012
+ * and stream-data widget configs that were saved with wire-form keys); the
4013
+ * canonical-form fallback unbreaks RT-aggregation / grouping widgets whose
4014
+ * configs were saved before the engine emitted wire-form keys.
4015
+ */
4016
+ function matchesAttributePath(cellPath, configField) {
4017
+ if (!cellPath || !configField)
4018
+ return false;
4019
+ if (sanitizeFieldName(cellPath) === configField)
4020
+ return true;
4021
+ // Loose fallback for legacy configs saved with the original CK path before the engine
4022
+ // switched to wire-form column emission. Only fires when configField itself does NOT
4023
+ // carry an aggregation suffix — otherwise we would cross-match MIN against MAX cells
4024
+ // (both would canonicalise to the same base path).
4025
+ if (hasAggregationFunctionSuffix(configField))
4026
+ return false;
4027
+ const cellBase = stripAggregationFunctionSuffix(cellPath);
4028
+ return toCanonicalAttributeKey(cellBase) === toCanonicalAttributeKey(configField);
4029
+ }
4030
+ /**
4031
+ * Parses a value to a number.
4032
+ * Returns 0 for NaN or non-numeric values.
4033
+ * @param value The value to parse
4034
+ * @returns Parsed numeric value or 0
4035
+ */
4036
+ function parseNumericValue(value) {
4037
+ if (typeof value === 'number') {
4038
+ return isNaN(value) ? 0 : value;
4039
+ }
4040
+ const parsed = parseFloat(String(value));
4041
+ return isNaN(parsed) ? 0 : parsed;
4042
+ }
4043
+ /**
4044
+ * Extracts a single value from an aggregation query result.
4045
+ * Used for KPI widgets with 'aggregation' queryMode.
4046
+ * @param queryResult The query result containing rows
4047
+ * @param valueField Optional specific field to extract (uses first cell if not specified)
4048
+ * @returns The extracted numeric value
4049
+ */
4050
+ function extractAggregationValue(queryResult, valueField) {
4051
+ const rows = queryResult.rows?.items ?? [];
4052
+ // Get the first supported row
4053
+ const firstRow = rows.find(row => row && SUPPORTED_ROW_TYPES.includes(row.__typename ?? ''));
4054
+ if (!firstRow)
4055
+ return 0;
4056
+ const cells = firstRow.cells?.items ?? [];
4057
+ // Find the value field if specified
4058
+ if (valueField) {
4059
+ for (const cell of cells) {
4060
+ if (!cell?.attributePath)
4061
+ continue;
4062
+ if (matchesAttributePath(cell.attributePath, valueField)) {
4063
+ return parseNumericValue(cell.value);
4064
+ }
4065
+ }
4066
+ }
4067
+ // Fallback: return first cell value
4068
+ const firstCell = cells.find(c => c !== null);
4069
+ return firstCell ? parseNumericValue(firstCell.value) : 0;
4070
+ }
4071
+ /**
4072
+ * Extracts a value from a grouped aggregation query result.
4073
+ * Used for KPI widgets with 'groupedAggregation' queryMode.
4074
+ * Finds the row where categoryField matches categoryValue and extracts valueField.
4075
+ * @param queryResult The query result containing rows
4076
+ * @param categoryField The field name to match against
4077
+ * @param categoryValue The value to match
4078
+ * @param valueField The field to extract the value from
4079
+ * @returns The extracted numeric value
4080
+ */
4081
+ function extractGroupedAggregationValue(queryResult, categoryField, categoryValue, valueField) {
4082
+ if (!categoryField || !categoryValue || !valueField) {
4083
+ return 0;
4084
+ }
4085
+ const rows = queryResult.rows?.items ?? [];
4086
+ // Find the row where category matches
4087
+ for (const row of rows) {
4088
+ if (!row || !SUPPORTED_ROW_TYPES.includes(row.__typename ?? ''))
4089
+ continue;
4090
+ const cells = row.cells?.items ?? [];
4091
+ let categoryMatch = false;
4092
+ let value = 0;
4093
+ for (const cell of cells) {
4094
+ if (!cell?.attributePath)
4095
+ continue;
4096
+ if (matchesAttributePath(cell.attributePath, categoryField) && String(cell.value) === categoryValue) {
4097
+ categoryMatch = true;
4098
+ }
4099
+ if (matchesAttributePath(cell.attributePath, valueField)) {
4100
+ value = parseNumericValue(cell.value);
4101
+ }
4102
+ }
4103
+ if (categoryMatch) {
4104
+ return value;
4105
+ }
4106
+ }
4107
+ return 0;
4108
+ }
4109
+ /**
4110
+ * Processes bar chart data in Static Series Mode.
4111
+ * Each series in config corresponds to a separate numeric field.
4112
+ * @param rows The query rows to process
4113
+ * @param categoryField The field to use for categories
4114
+ * @param seriesConfigs The series configurations
4115
+ * @returns Processed bar chart data
4116
+ */
4117
+ function processStaticSeriesData(rows, categoryField, seriesConfigs) {
4118
+ const categories = [];
4119
+ const seriesMap = new Map();
4120
+ // Initialize series map from config
4121
+ for (const seriesConfig of seriesConfigs) {
4122
+ seriesMap.set(seriesConfig.field, []);
4123
+ }
4124
+ for (const row of rows) {
4125
+ if (!SUPPORTED_ROW_TYPES.includes(row.__typename ?? ''))
4126
+ continue;
4127
+ const cells = row.cells?.items ?? [];
4128
+ let categoryValue = '';
4129
+ const rowValues = new Map();
4130
+ for (const cell of cells) {
4131
+ if (!cell?.attributePath)
4132
+ continue;
4133
+ if (matchesAttributePath(cell.attributePath, categoryField)) {
4134
+ categoryValue = String(cell.value ?? '');
4135
+ }
4136
+ // Check if this cell is one of our series fields
4137
+ for (const seriesConfig of seriesConfigs) {
4138
+ if (matchesAttributePath(cell.attributePath, seriesConfig.field)) {
4139
+ rowValues.set(seriesConfig.field, parseNumericValue(cell.value));
4140
+ }
4141
+ }
4142
+ }
4143
+ if (categoryValue !== '') {
4144
+ categories.push(categoryValue);
4145
+ // Add values for each series
4146
+ for (const seriesConfig of seriesConfigs) {
4147
+ const value = rowValues.get(seriesConfig.field) ?? 0;
4148
+ seriesMap.get(seriesConfig.field)?.push(value);
4149
+ }
4150
+ }
4151
+ }
4152
+ // Convert to series data array
4153
+ const seriesData = seriesConfigs.map(seriesConfig => ({
4154
+ name: seriesConfig.name ?? seriesConfig.field,
4155
+ data: seriesMap.get(seriesConfig.field) ?? [],
4156
+ color: seriesConfig.color
4157
+ }));
4158
+ return { categories, seriesData };
4159
+ }
4160
+ /**
4161
+ * Processes bar chart data in Dynamic Series Mode.
4162
+ * Series are created dynamically from unique values of seriesGroupField.
4163
+ * @param rows The query rows to process
4164
+ * @param categoryField The field to use for categories
4165
+ * @param seriesGroupField The field to use for series grouping
4166
+ * @param valueField The field to use for values
4167
+ * @returns Processed bar chart data
4168
+ */
4169
+ function processDynamicSeriesData(rows, categoryField, seriesGroupField, valueField) {
4170
+ // Build a map: category -> seriesGroup -> value
4171
+ const dataMap = new Map();
4172
+ const allCategories = new Set();
4173
+ const allSeriesGroups = new Set();
4174
+ for (const row of rows) {
4175
+ if (!SUPPORTED_ROW_TYPES.includes(row.__typename ?? ''))
4176
+ continue;
4177
+ const cells = row.cells?.items ?? [];
4178
+ let categoryValue = '';
4179
+ let seriesGroupValue = '';
4180
+ let numericValue = 0;
4181
+ for (const cell of cells) {
4182
+ if (!cell?.attributePath)
4183
+ continue;
4184
+ if (matchesAttributePath(cell.attributePath, categoryField)) {
4185
+ categoryValue = String(cell.value ?? '');
4186
+ }
4187
+ else if (matchesAttributePath(cell.attributePath, seriesGroupField)) {
4188
+ seriesGroupValue = String(cell.value ?? '');
4189
+ }
4190
+ else if (matchesAttributePath(cell.attributePath, valueField)) {
4191
+ numericValue = parseNumericValue(cell.value);
4192
+ }
4193
+ }
4194
+ if (categoryValue && seriesGroupValue) {
4195
+ allCategories.add(categoryValue);
4196
+ allSeriesGroups.add(seriesGroupValue);
4197
+ if (!dataMap.has(categoryValue)) {
4198
+ dataMap.set(categoryValue, new Map());
4199
+ }
4200
+ dataMap.get(categoryValue).set(seriesGroupValue, numericValue);
4201
+ }
4202
+ }
4203
+ // Convert to arrays (maintain insertion order)
4204
+ const categories = Array.from(allCategories);
4205
+ const seriesGroups = Array.from(allSeriesGroups);
4206
+ // Build series data
4207
+ const seriesData = seriesGroups.map(seriesGroup => {
4208
+ const data = categories.map(category => {
4209
+ return dataMap.get(category)?.get(seriesGroup) ?? 0;
4210
+ });
4211
+ return {
4212
+ name: seriesGroup,
4213
+ data
4214
+ };
4215
+ });
4216
+ return { categories, seriesData };
4217
+ }
4218
+ /**
4219
+ * Processes pie chart data from query rows.
4220
+ * @param rows The query rows to process
4221
+ * @param categoryField The field to use for categories
4222
+ * @param valueField The field to use for values
4223
+ * @returns Array of pie chart data items
4224
+ */
4225
+ function processPieChartData(rows, categoryField, valueField) {
4226
+ const result = [];
4227
+ for (const row of rows) {
4228
+ if (!SUPPORTED_ROW_TYPES.includes(row.__typename ?? ''))
4229
+ continue;
4230
+ const cells = row.cells?.items ?? [];
4231
+ let categoryValue = '';
4232
+ let numericValue = 0;
4233
+ for (const cell of cells) {
4234
+ if (!cell?.attributePath)
4235
+ continue;
4236
+ if (matchesAttributePath(cell.attributePath, categoryField)) {
4237
+ categoryValue = String(cell.value ?? '');
4238
+ }
4239
+ else if (matchesAttributePath(cell.attributePath, valueField)) {
4240
+ numericValue = parseNumericValue(cell.value);
4241
+ }
4242
+ }
4243
+ if (categoryValue) {
4244
+ result.push({
4245
+ category: categoryValue,
4246
+ value: numericValue
4247
+ });
4248
+ }
4249
+ }
4250
+ return result;
4251
+ }
4252
+
3944
4253
  class KpiWidgetComponent {
3945
4254
  dataService = inject(DashboardDataService);
3946
4255
  executeRuntimeQueryGQL = inject(ExecuteRuntimeQueryDtoGQL);
@@ -4266,8 +4575,7 @@ class KpiWidgetComponent {
4266
4575
  for (const cell of cells) {
4267
4576
  if (!cell?.attributePath)
4268
4577
  continue;
4269
- const sanitizedPath = this.sanitizeFieldName(cell.attributePath);
4270
- if (valueField && sanitizedPath === valueField) {
4578
+ if (valueField && matchesAttributePath(cell.attributePath, valueField)) {
4271
4579
  return this.extractCellValue(cell.value);
4272
4580
  }
4273
4581
  }
@@ -4295,11 +4603,10 @@ class KpiWidgetComponent {
4295
4603
  for (const cell of cells) {
4296
4604
  if (!cell?.attributePath)
4297
4605
  continue;
4298
- const sanitizedPath = this.sanitizeFieldName(cell.attributePath);
4299
- if (sanitizedPath === categoryField && String(cell.value) === categoryValue) {
4606
+ if (matchesAttributePath(cell.attributePath, categoryField) && String(cell.value) === categoryValue) {
4300
4607
  categoryMatch = true;
4301
4608
  }
4302
- if (sanitizedPath === valueField) {
4609
+ if (matchesAttributePath(cell.attributePath, valueField)) {
4303
4610
  value = this.extractCellValue(cell.value);
4304
4611
  }
4305
4612
  }
@@ -4309,12 +4616,6 @@ class KpiWidgetComponent {
4309
4616
  }
4310
4617
  return 0;
4311
4618
  }
4312
- parseNumericValue(value) {
4313
- if (typeof value === 'number')
4314
- return value;
4315
- const parsed = parseFloat(String(value));
4316
- return isNaN(parsed) ? 0 : parsed;
4317
- }
4318
4619
  /**
4319
4620
  * Extracts a cell value, preserving strings and converting numbers.
4320
4621
  * Returns the value as-is if it's a string, or parses it as a number.
@@ -4333,9 +4634,6 @@ class KpiWidgetComponent {
4333
4634
  // For other types (boolean, object), convert to string
4334
4635
  return String(value);
4335
4636
  }
4336
- sanitizeFieldName(fieldName) {
4337
- return fieldName.replace(/\./g, '_');
4338
- }
4339
4637
  /**
4340
4638
  * Converts widget filter configuration to GraphQL FieldFilterDto format.
4341
4639
  * Resolves MeshBoard variables in filter values before conversion.
@@ -4354,6 +4652,38 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
4354
4652
  type: Input
4355
4653
  }] } });
4356
4654
 
4655
+ const GetRuntimeQueryColumnsDocumentDto = gql `
4656
+ query getRuntimeQueryColumns($rtId: OctoObjectId!) {
4657
+ runtime {
4658
+ runtimeQuery(rtId: $rtId) {
4659
+ items {
4660
+ queryRtId
4661
+ associatedCkTypeId
4662
+ columns {
4663
+ attributePath
4664
+ attributeValueType
4665
+ aggregationType
4666
+ }
4667
+ }
4668
+ }
4669
+ }
4670
+ }
4671
+ `;
4672
+ class GetRuntimeQueryColumnsDtoGQL extends i1.Query {
4673
+ document = GetRuntimeQueryColumnsDocumentDto;
4674
+ constructor(apollo) {
4675
+ super(apollo);
4676
+ }
4677
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: GetRuntimeQueryColumnsDtoGQL, deps: [{ token: i1.Apollo }], target: i0.ɵɵFactoryTarget.Injectable });
4678
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: GetRuntimeQueryColumnsDtoGQL, providedIn: 'root' });
4679
+ }
4680
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: GetRuntimeQueryColumnsDtoGQL, decorators: [{
4681
+ type: Injectable,
4682
+ args: [{
4683
+ providedIn: 'root'
4684
+ }]
4685
+ }], ctorParameters: () => [{ type: i1.Apollo }] });
4686
+
4357
4687
  /**
4358
4688
  * Configuration dialog for KPI widgets.
4359
4689
  * Allows selecting data source, value attribute, and display options.
@@ -4363,6 +4693,7 @@ class KpiConfigDialogComponent {
4363
4693
  ckTypeSelectorService = inject(CkTypeSelectorService);
4364
4694
  attributeSelectorService = inject(AttributeSelectorService);
4365
4695
  executeRuntimeQueryGQL = inject(ExecuteRuntimeQueryDtoGQL);
4696
+ getRuntimeQueryColumnsGQL = inject(GetRuntimeQueryColumnsDtoGQL);
4366
4697
  meshBoardStateService = inject(MeshBoardStateService);
4367
4698
  windowRef = inject(WindowRef);
4368
4699
  ckTypeSelectorInput;
@@ -4729,27 +5060,32 @@ class KpiConfigDialogComponent {
4729
5060
  return;
4730
5061
  this.isLoadingQueryColumns = true;
4731
5062
  try {
4732
- const result = await firstValueFrom(this.executeRuntimeQueryGQL.fetch({
5063
+ // Metadata-only fetch column resolver runs off the cached query definition
5064
+ // without executing the underlying aggregation, so the dialog opens fast even
5065
+ // for queries that aggregate over large data sets.
5066
+ const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
4733
5067
  variables: {
4734
- rtId: rtId,
4735
- first: 100 // Fetch rows for category values
5068
+ rtId: rtId
4736
5069
  }
4737
5070
  }));
4738
5071
  const queryItems = result.data?.runtime?.runtimeQuery?.items ?? [];
4739
5072
  if (queryItems.length > 0 && queryItems[0]) {
4740
- const queryResult = queryItems[0];
4741
- // Extract columns
4742
- const columns = queryResult.columns ?? [];
5073
+ const columns = queryItems[0].columns ?? [];
4743
5074
  const filteredColumns = columns
4744
5075
  .filter((c) => c !== null);
4745
- // queryColumns use sanitized paths for UI display and matching with query results
5076
+ // Column AttributePath is already in the engine's wire form for aggregation /
5077
+ // grouping columns (e.g. `quantity_sum`, `operatingstatus`) so picker entries
5078
+ // double as both the visible label and the stored config value, and MIN + MAX
5079
+ // of the same source path show up as two distinct entries.
4746
5080
  this.queryColumns = filteredColumns.map(c => ({
4747
- attributePath: this.sanitizeFieldName(c.attributePath ?? ''),
4748
- attributeValueType: c.attributeValueType ?? ''
5081
+ attributePath: c.attributePath ?? '',
5082
+ attributeValueType: c.attributeValueType ?? '',
5083
+ aggregationType: c.aggregationType ?? null
4749
5084
  }));
4750
- // Extract category values for grouped aggregation
4751
- if (this.queryMode === 'groupedAggregation') {
4752
- await this.extractCategoryValues(queryResult);
5085
+ // Category values for grouped aggregation are loaded on-demand by
5086
+ // loadCategoryValuesForField only when a categoryField is actually selected.
5087
+ if (this.queryMode === 'groupedAggregation' && this.form.queryCategoryField) {
5088
+ await this.loadCategoryValuesForField(rtId, this.form.queryCategoryField);
4753
5089
  }
4754
5090
  }
4755
5091
  }
@@ -4792,8 +5128,7 @@ class KpiConfigDialogComponent {
4792
5128
  for (const cell of cells) {
4793
5129
  if (!cell?.attributePath)
4794
5130
  continue;
4795
- const sanitizedPath = this.sanitizeFieldName(cell.attributePath);
4796
- if (sanitizedPath === categoryField && cell.value !== null && cell.value !== undefined) {
5131
+ if (matchesAttributePath(cell.attributePath, categoryField) && cell.value !== null && cell.value !== undefined) {
4797
5132
  values.add(String(cell.value));
4798
5133
  }
4799
5134
  }
@@ -4812,15 +5147,6 @@ class KpiConfigDialogComponent {
4812
5147
  this.isLoadingCategoryValues = false;
4813
5148
  }
4814
5149
  }
4815
- async extractCategoryValues(_queryResult) {
4816
- // Load category values if a category field is already selected
4817
- if (this.queryColumns.length > 0 && this.form.queryCategoryField && this.selectedPersistentQuery) {
4818
- await this.loadCategoryValuesForField(this.selectedPersistentQuery.rtId, this.form.queryCategoryField);
4819
- }
4820
- }
4821
- sanitizeFieldName(fieldName) {
4822
- return fieldName.replace(/\./g, '_');
4823
- }
4824
5150
  // ============================================================================
4825
5151
  // Filter Methods
4826
5152
  // ============================================================================
@@ -7224,15 +7550,19 @@ class TableWidgetDataSourceDirective extends OctoGraphQlDataSource {
7224
7550
  if (hasCkTypeIdColumn) {
7225
7551
  record['ckTypeId'] = queryRow.ckTypeId ?? '';
7226
7552
  }
7227
- // Flatten cells into the record (sanitize field names for grid compatibility)
7228
- // Only add fields that are in the query columns
7553
+ // Flatten cells into the record. Each cell is stored under the COLUMN's
7554
+ // attributePath (which is what the Kendo grid uses as `field`) rather than
7555
+ // the cell's own attributePath — the two may differ now that the engine
7556
+ // emits cell paths in wire-form with a function suffix
7557
+ // (e.g. cell path `meterreading_count` for column path `meterReading`).
7558
+ // `matchesAttributePath` reconciles both forms.
7229
7559
  const cells = queryRow.cells?.items ?? [];
7230
7560
  for (const cell of cells) {
7231
- if (cell?.attributePath) {
7232
- const sanitizedPath = this.sanitizeFieldName(cell.attributePath);
7233
- if (columnPaths.has(sanitizedPath)) {
7234
- record[sanitizedPath] = cell.value;
7235
- }
7561
+ if (!cell?.attributePath)
7562
+ continue;
7563
+ const matchingColumn = columns.find(col => matchesAttributePath(cell.attributePath, col.attributePath));
7564
+ if (matchingColumn) {
7565
+ record[matchingColumn.attributePath] = cell.value;
7236
7566
  }
7237
7567
  }
7238
7568
  return record;
@@ -8412,8 +8742,7 @@ class GaugeWidgetComponent {
8412
8742
  for (const cell of cells) {
8413
8743
  if (!cell?.attributePath)
8414
8744
  continue;
8415
- const sanitizedPath = this.sanitizeFieldName(cell.attributePath);
8416
- if (valueField && sanitizedPath === valueField) {
8745
+ if (valueField && matchesAttributePath(cell.attributePath, valueField)) {
8417
8746
  return this.parseNumericValue(cell.value);
8418
8747
  }
8419
8748
  }
@@ -8441,11 +8770,10 @@ class GaugeWidgetComponent {
8441
8770
  for (const cell of cells) {
8442
8771
  if (!cell?.attributePath)
8443
8772
  continue;
8444
- const sanitizedPath = this.sanitizeFieldName(cell.attributePath);
8445
- if (sanitizedPath === categoryField && String(cell.value) === categoryValue) {
8773
+ if (matchesAttributePath(cell.attributePath, categoryField) && String(cell.value) === categoryValue) {
8446
8774
  categoryMatch = true;
8447
8775
  }
8448
- if (sanitizedPath === valueField) {
8776
+ if (matchesAttributePath(cell.attributePath, valueField)) {
8449
8777
  value = this.parseNumericValue(cell.value);
8450
8778
  }
8451
8779
  }
@@ -8461,9 +8789,6 @@ class GaugeWidgetComponent {
8461
8789
  const parsed = parseFloat(String(value));
8462
8790
  return isNaN(parsed) ? 0 : parsed;
8463
8791
  }
8464
- sanitizeFieldName(fieldName) {
8465
- return fieldName.replace(/\./g, '_');
8466
- }
8467
8792
  /**
8468
8793
  * Converts widget filter configuration to GraphQL FieldFilterDto format.
8469
8794
  * Resolves MeshBoard variables in filter values before conversion.
@@ -8833,6 +9158,7 @@ class GaugeConfigDialogComponent {
8833
9158
  ckTypeSelectorService = inject(CkTypeSelectorService);
8834
9159
  attributeSelectorService = inject(AttributeSelectorService);
8835
9160
  executeRuntimeQueryGQL = inject(ExecuteRuntimeQueryDtoGQL);
9161
+ getRuntimeQueryColumnsGQL = inject(GetRuntimeQueryColumnsDtoGQL);
8836
9162
  meshBoardStateService = inject(MeshBoardStateService);
8837
9163
  windowRef = inject(WindowRef);
8838
9164
  ckTypeSelectorInput;
@@ -9182,27 +9508,29 @@ class GaugeConfigDialogComponent {
9182
9508
  async loadQueryColumnsAndValues(queryRtId) {
9183
9509
  this.isLoadingQueryColumns = true;
9184
9510
  try {
9185
- const result = await firstValueFrom(this.executeRuntimeQueryGQL.fetch({
9511
+ // Metadata-only fetch skips backend aggregation execution so the dialog opens
9512
+ // fast even for large data sets.
9513
+ const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
9186
9514
  variables: {
9187
- rtId: queryRtId,
9188
- first: 100 // Fetch rows for category values
9515
+ rtId: queryRtId
9189
9516
  }
9190
9517
  }));
9191
9518
  const queryItems = result.data?.runtime?.runtimeQuery?.items ?? [];
9192
9519
  if (queryItems.length > 0 && queryItems[0]) {
9193
- const queryResult = queryItems[0];
9194
- // Extract columns
9195
- const columns = queryResult.columns ?? [];
9520
+ const columns = queryItems[0].columns ?? [];
9196
9521
  const filteredColumns = columns
9197
9522
  .filter((c) => c !== null);
9198
- // queryColumns use sanitized paths for UI display and matching with query results
9523
+ // Engine emits column attributePath in wire form for aggregation / grouping
9524
+ // columns, so the picker can use it as both label and stored value verbatim.
9199
9525
  this.queryColumns = filteredColumns.map(c => ({
9200
- attributePath: this.sanitizeFieldName(c.attributePath ?? ''),
9201
- attributeValueType: c.attributeValueType ?? ''
9526
+ attributePath: c.attributePath ?? '',
9527
+ attributeValueType: c.attributeValueType ?? '',
9528
+ aggregationType: c.aggregationType ?? null
9202
9529
  }));
9203
- // Extract category values for grouped aggregation
9204
- if (this.queryMode === 'groupedAggregation') {
9205
- await this.extractCategoryValues(queryResult);
9530
+ // Category values for grouped aggregation are loaded on-demand by
9531
+ // loadCategoryValuesForField only when a categoryField is actually selected.
9532
+ if (this.queryMode === 'groupedAggregation' && this.form.queryCategoryField) {
9533
+ await this.loadCategoryValuesForField(queryRtId, this.form.queryCategoryField);
9206
9534
  }
9207
9535
  }
9208
9536
  }
@@ -9245,8 +9573,7 @@ class GaugeConfigDialogComponent {
9245
9573
  for (const cell of cells) {
9246
9574
  if (!cell?.attributePath)
9247
9575
  continue;
9248
- const sanitizedPath = this.sanitizeFieldName(cell.attributePath);
9249
- if (sanitizedPath === categoryField && cell.value !== null && cell.value !== undefined) {
9576
+ if (matchesAttributePath(cell.attributePath, categoryField) && cell.value !== null && cell.value !== undefined) {
9250
9577
  values.add(String(cell.value));
9251
9578
  }
9252
9579
  }
@@ -9265,15 +9592,6 @@ class GaugeConfigDialogComponent {
9265
9592
  this.isLoadingCategoryValues = false;
9266
9593
  }
9267
9594
  }
9268
- async extractCategoryValues(_queryResult) {
9269
- // Load category values if a category field is already selected
9270
- if (this.queryColumns.length > 0 && this.form.queryCategoryField && this.selectedPersistentQuery) {
9271
- await this.loadCategoryValuesForField(this.selectedPersistentQuery.rtId, this.form.queryCategoryField);
9272
- }
9273
- }
9274
- sanitizeFieldName(fieldName) {
9275
- return fieldName.replace(/\./g, '_');
9276
- }
9277
9595
  // ============================================================================
9278
9596
  // Filter Methods
9279
9597
  // ============================================================================
@@ -10293,13 +10611,15 @@ class PieChartWidgetComponent {
10293
10611
  this._isLoading.set(false);
10294
10612
  return;
10295
10613
  }
10296
- // Extract columns to find field indices
10297
- const columns = (queryResult.columns ?? [])
10614
+ // Extract columns to verify configured fields are present. Both forms (original CK
10615
+ // path and engine wire-form) are accepted so saved configs survive the engine's
10616
+ // switch to wire-form keys without a migration.
10617
+ const columnPaths = (queryResult.columns ?? [])
10298
10618
  .filter((c) => c !== null)
10299
- .map(c => this.sanitizeFieldName(c.attributePath ?? ''));
10300
- const categoryFieldIndex = columns.indexOf(this.sanitizeFieldName(this.config.categoryField));
10301
- const valueFieldIndex = columns.indexOf(this.sanitizeFieldName(this.config.valueField));
10302
- if (categoryFieldIndex === -1 || valueFieldIndex === -1) {
10619
+ .map(c => c.attributePath ?? '');
10620
+ const categoryFieldPresent = columnPaths.some(p => matchesAttributePath(p, this.config.categoryField));
10621
+ const valueFieldPresent = columnPaths.some(p => matchesAttributePath(p, this.config.valueField));
10622
+ if (!categoryFieldPresent || !valueFieldPresent) {
10303
10623
  this._error.set('Configured fields not found in query result');
10304
10624
  this._isLoading.set(false);
10305
10625
  return;
@@ -10318,11 +10638,10 @@ class PieChartWidgetComponent {
10318
10638
  for (const cell of cells) {
10319
10639
  if (!cell?.attributePath)
10320
10640
  continue;
10321
- const sanitizedPath = this.sanitizeFieldName(cell.attributePath);
10322
- if (sanitizedPath === this.sanitizeFieldName(this.config.categoryField)) {
10641
+ if (matchesAttributePath(cell.attributePath, this.config.categoryField)) {
10323
10642
  category = String(cell.value ?? '');
10324
10643
  }
10325
- if (sanitizedPath === this.sanitizeFieldName(this.config.valueField)) {
10644
+ if (matchesAttributePath(cell.attributePath, this.config.valueField)) {
10326
10645
  const numValue = typeof cell.value === 'number' ? cell.value : parseFloat(String(cell.value));
10327
10646
  value = isNaN(numValue) ? 0 : numValue;
10328
10647
  }
@@ -10333,13 +10652,6 @@ class PieChartWidgetComponent {
10333
10652
  this._chartData.set(chartData);
10334
10653
  this._isLoading.set(false);
10335
10654
  }
10336
- /**
10337
- * Sanitizes field names for comparison.
10338
- * Replaces dots with underscores (same as table widget).
10339
- */
10340
- sanitizeFieldName(fieldName) {
10341
- return fieldName.replace(/\./g, '_');
10342
- }
10343
10655
  /**
10344
10656
  * Converts widget filter configuration to GraphQL FieldFilterDto format.
10345
10657
  * Resolves MeshBoard variables in filter values before conversion.
@@ -10442,7 +10754,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
10442
10754
  * Configuration dialog for Pie Chart widgets.
10443
10755
  */
10444
10756
  class PieChartConfigDialogComponent {
10445
- executeRuntimeQueryGQL = inject(ExecuteRuntimeQueryDtoGQL);
10757
+ getRuntimeQueryColumnsGQL = inject(GetRuntimeQueryColumnsDtoGQL);
10446
10758
  stateService = inject(MeshBoardStateService);
10447
10759
  windowRef = inject(WindowRef);
10448
10760
  querySelector;
@@ -10611,10 +10923,12 @@ class PieChartConfigDialogComponent {
10611
10923
  async loadQueryColumns(queryRtId) {
10612
10924
  this.isLoadingColumns = true;
10613
10925
  try {
10614
- const result = await firstValueFrom(this.executeRuntimeQueryGQL.fetch({
10926
+ // Metadata-only query: column resolver runs off the cached query definition without
10927
+ // touching the row execution path, so the dialog opens fast even when the underlying
10928
+ // persistent query aggregates over a large data set.
10929
+ const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
10615
10930
  variables: {
10616
- rtId: queryRtId,
10617
- first: 1 // We only need columns, not data
10931
+ rtId: queryRtId
10618
10932
  }
10619
10933
  }));
10620
10934
  const queryItems = result.data?.runtime?.runtimeQuery?.items ?? [];
@@ -10622,10 +10936,12 @@ class PieChartConfigDialogComponent {
10622
10936
  const columns = queryItems[0].columns ?? [];
10623
10937
  const filteredColumns = columns
10624
10938
  .filter((c) => c !== null);
10625
- // queryColumns use sanitized paths for UI display and matching with query results
10939
+ // Engine emits column attributePath in wire form for aggregation / grouping
10940
+ // columns (e.g. `quantity_sum`, `operatingstatus`); picker uses it verbatim.
10626
10941
  this.queryColumns = filteredColumns.map(c => ({
10627
- attributePath: this.sanitizeFieldName(c.attributePath ?? ''),
10628
- attributeValueType: c.attributeValueType ?? ''
10942
+ attributePath: c.attributePath ?? '',
10943
+ attributeValueType: c.attributeValueType ?? '',
10944
+ aggregationType: c.aggregationType ?? null
10629
10945
  }));
10630
10946
  // Auto-select fields if only 2 columns (typical for grouped aggregations)
10631
10947
  if (this.queryColumns.length === 2 && !this.form.categoryField && !this.form.valueField) {
@@ -10648,9 +10964,6 @@ class PieChartConfigDialogComponent {
10648
10964
  this.isLoadingColumns = false;
10649
10965
  }
10650
10966
  }
10651
- sanitizeFieldName(fieldName) {
10652
- return fieldName.replace(/\./g, '_');
10653
- }
10654
10967
  onFiltersChange(updatedFilters) {
10655
10968
  this.filters = updatedFilters;
10656
10969
  }
@@ -11314,13 +11627,12 @@ class BarChartWidgetComponent {
11314
11627
  for (const cell of cells) {
11315
11628
  if (!cell?.attributePath)
11316
11629
  continue;
11317
- const sanitizedPath = this.sanitizeFieldName(cell.attributePath);
11318
- if (sanitizedPath === this.sanitizeFieldName(this.config.categoryField)) {
11630
+ if (matchesAttributePath(cell.attributePath, this.config.categoryField)) {
11319
11631
  categoryValue = this.formatCategoryValue(cell.value);
11320
11632
  }
11321
11633
  // Check if this cell is one of our series fields
11322
11634
  for (const seriesConfig of (this.config.series ?? [])) {
11323
- if (sanitizedPath === this.sanitizeFieldName(seriesConfig.field)) {
11635
+ if (matchesAttributePath(cell.attributePath, seriesConfig.field)) {
11324
11636
  const numValue = typeof cell.value === 'number' ? cell.value : parseFloat(String(cell.value));
11325
11637
  rowValues.set(seriesConfig.field, isNaN(numValue) ? 0 : numValue);
11326
11638
  }
@@ -11379,14 +11691,13 @@ class BarChartWidgetComponent {
11379
11691
  for (const cell of cells) {
11380
11692
  if (!cell?.attributePath)
11381
11693
  continue;
11382
- const sanitizedPath = this.sanitizeFieldName(cell.attributePath);
11383
- if (sanitizedPath === this.sanitizeFieldName(categoryField)) {
11694
+ if (matchesAttributePath(cell.attributePath, categoryField)) {
11384
11695
  categoryValue = this.formatCategoryValue(cell.value);
11385
11696
  }
11386
- else if (sanitizedPath === this.sanitizeFieldName(seriesGroupField)) {
11697
+ else if (matchesAttributePath(cell.attributePath, seriesGroupField)) {
11387
11698
  seriesGroupValue = String(cell.value ?? '');
11388
11699
  }
11389
- else if (sanitizedPath === this.sanitizeFieldName(valueField)) {
11700
+ else if (matchesAttributePath(cell.attributePath, valueField)) {
11390
11701
  const val = cell.value;
11391
11702
  numericValue = typeof val === 'number' ? val : parseFloat(String(val));
11392
11703
  if (isNaN(numericValue))
@@ -11458,9 +11769,6 @@ class BarChartWidgetComponent {
11458
11769
  }
11459
11770
  return str;
11460
11771
  }
11461
- sanitizeFieldName(fieldName) {
11462
- return fieldName.replace(/\./g, '_');
11463
- }
11464
11772
  /**
11465
11773
  * Converts widget filter configuration to GraphQL FieldFilterDto format.
11466
11774
  * Resolves MeshBoard variables in filter values before conversion.
@@ -11632,7 +11940,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
11632
11940
  * Supports column, bar, stacked, and 100% stacked chart types.
11633
11941
  */
11634
11942
  class BarChartConfigDialogComponent {
11635
- executeRuntimeQueryGQL = inject(ExecuteRuntimeQueryDtoGQL);
11943
+ getRuntimeQueryColumnsGQL = inject(GetRuntimeQueryColumnsDtoGQL);
11636
11944
  stateService = inject(MeshBoardStateService);
11637
11945
  windowRef = inject(WindowRef);
11638
11946
  querySelector;
@@ -11771,10 +12079,11 @@ class BarChartConfigDialogComponent {
11771
12079
  async loadQueryColumns(queryRtId) {
11772
12080
  this.isLoadingColumns = true;
11773
12081
  try {
11774
- const result = await firstValueFrom(this.executeRuntimeQueryGQL.fetch({
12082
+ // Metadata-only column resolver runs off the cached query definition and skips
12083
+ // the row execution path, so the dialog opens fast even on large aggregations.
12084
+ const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
11775
12085
  variables: {
11776
- rtId: queryRtId,
11777
- first: 1 // We only need columns, not data
12086
+ rtId: queryRtId
11778
12087
  }
11779
12088
  }));
11780
12089
  const queryItems = result.data?.runtime?.runtimeQuery?.items ?? [];
@@ -11782,10 +12091,12 @@ class BarChartConfigDialogComponent {
11782
12091
  const columns = queryItems[0].columns ?? [];
11783
12092
  const filteredColumns = columns
11784
12093
  .filter((c) => c !== null);
11785
- // queryColumns use sanitized paths for UI display and matching with query results
12094
+ // Engine emits column attributePath in wire form for aggregation / grouping
12095
+ // columns (e.g. `quantity_sum`, `operatingstatus`); picker uses it verbatim.
11786
12096
  this.queryColumns = filteredColumns.map(c => ({
11787
- attributePath: this.sanitizeFieldName(c.attributePath ?? ''),
11788
- attributeValueType: c.attributeValueType ?? ''
12097
+ attributePath: c.attributePath ?? '',
12098
+ attributeValueType: c.attributeValueType ?? '',
12099
+ aggregationType: c.aggregationType ?? null
11789
12100
  }));
11790
12101
  // Filter numeric and non-numeric columns
11791
12102
  const numericTypes = ['INTEGER', 'FLOAT', 'DOUBLE', 'DECIMAL', 'LONG'];
@@ -11815,9 +12126,6 @@ class BarChartConfigDialogComponent {
11815
12126
  this.isLoadingColumns = false;
11816
12127
  }
11817
12128
  }
11818
- sanitizeFieldName(fieldName) {
11819
- return fieldName.replace(/\./g, '_');
11820
- }
11821
12129
  onSeriesFieldsChange(fields) {
11822
12130
  this.selectedSeriesFields = fields;
11823
12131
  }
@@ -12623,20 +12931,19 @@ class LineChartWidgetComponent {
12623
12931
  for (const cell of cells) {
12624
12932
  if (!cell?.attributePath)
12625
12933
  continue;
12626
- const sanitizedPath = this.sanitizeFieldName(cell.attributePath);
12627
- if (sanitizedPath === this.sanitizeFieldName(categoryField)) {
12934
+ if (matchesAttributePath(cell.attributePath, categoryField)) {
12628
12935
  categoryValue = String(cell.value ?? '');
12629
12936
  }
12630
- else if (sanitizedPath === this.sanitizeFieldName(seriesGroupField)) {
12937
+ else if (matchesAttributePath(cell.attributePath, seriesGroupField)) {
12631
12938
  seriesGroupValue = String(cell.value ?? '');
12632
12939
  }
12633
- else if (sanitizedPath === this.sanitizeFieldName(valueField)) {
12940
+ else if (matchesAttributePath(cell.attributePath, valueField)) {
12634
12941
  const val = cell.value;
12635
12942
  numericValue = typeof val === 'number' ? val : parseFloat(String(val));
12636
12943
  if (isNaN(numericValue))
12637
12944
  numericValue = 0;
12638
12945
  }
12639
- else if (unitField && sanitizedPath === this.sanitizeFieldName(unitField)) {
12946
+ else if (unitField && matchesAttributePath(cell.attributePath, unitField)) {
12640
12947
  unitValue = String(cell.value ?? '');
12641
12948
  }
12642
12949
  }
@@ -12722,9 +13029,6 @@ class LineChartWidgetComponent {
12722
13029
  minute: '2-digit'
12723
13030
  });
12724
13031
  }
12725
- sanitizeFieldName(fieldName) {
12726
- return fieldName.replace(/\./g, '_');
12727
- }
12728
13032
  sanitizeAxisName(name) {
12729
13033
  return name.replace(/[^a-zA-Z0-9]/g, '_');
12730
13034
  }
@@ -12921,7 +13225,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
12921
13225
  * Supports line and area chart types with multi-series and multi-axis configuration.
12922
13226
  */
12923
13227
  class LineChartConfigDialogComponent {
12924
- executeRuntimeQueryGQL = inject(ExecuteRuntimeQueryDtoGQL);
13228
+ getRuntimeQueryColumnsGQL = inject(GetRuntimeQueryColumnsDtoGQL);
12925
13229
  stateService = inject(MeshBoardStateService);
12926
13230
  windowRef = inject(WindowRef);
12927
13231
  querySelector;
@@ -13032,10 +13336,10 @@ class LineChartConfigDialogComponent {
13032
13336
  async loadQueryColumns(queryRtId) {
13033
13337
  this.isLoadingColumns = true;
13034
13338
  try {
13035
- const result = await firstValueFrom(this.executeRuntimeQueryGQL.fetch({
13339
+ // Metadata-only skips the row execution path on the backend.
13340
+ const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
13036
13341
  variables: {
13037
- rtId: queryRtId,
13038
- first: 1
13342
+ rtId: queryRtId
13039
13343
  }
13040
13344
  }));
13041
13345
  const queryItems = result.data?.runtime?.runtimeQuery?.items ?? [];
@@ -13044,8 +13348,9 @@ class LineChartConfigDialogComponent {
13044
13348
  const filteredColumns = columns
13045
13349
  .filter((c) => c !== null);
13046
13350
  this.queryColumns = filteredColumns.map(c => ({
13047
- attributePath: this.sanitizeFieldName(c.attributePath ?? ''),
13048
- attributeValueType: c.attributeValueType ?? ''
13351
+ attributePath: c.attributePath ?? '',
13352
+ attributeValueType: c.attributeValueType ?? '',
13353
+ aggregationType: c.aggregationType ?? null
13049
13354
  }));
13050
13355
  const numericTypes = ['INTEGER', 'FLOAT', 'DOUBLE', 'DECIMAL', 'LONG'];
13051
13356
  this.numericColumns = this.queryColumns.filter(c => numericTypes.includes(c.attributeValueType));
@@ -13062,9 +13367,6 @@ class LineChartConfigDialogComponent {
13062
13367
  this.isLoadingColumns = false;
13063
13368
  }
13064
13369
  }
13065
- sanitizeFieldName(fieldName) {
13066
- return fieldName.replace(/\./g, '_');
13067
- }
13068
13370
  onFiltersChange(updatedFilters) {
13069
13371
  this.filters = updatedFilters;
13070
13372
  }
@@ -13739,9 +14041,9 @@ class HeatmapWidgetComponent {
13739
14041
  * Otherwise aggregates into hourly buckets.
13740
14042
  */
13741
14043
  processHeatmapData(filteredRows) {
13742
- const dateField = this.sanitizeFieldName(this.config.dateField);
13743
- const dateEndField = this.config.dateEndField ? this.sanitizeFieldName(this.config.dateEndField) : null;
13744
- const valueField = this.config.valueField ? this.sanitizeFieldName(this.config.valueField) : null;
14044
+ const dateField = this.config.dateField;
14045
+ const dateEndField = this.config.dateEndField ?? null;
14046
+ const valueField = this.config.valueField ?? null;
13745
14047
  const aggregation = this.config.aggregation ?? 'count';
13746
14048
  const parsedRows = [];
13747
14049
  for (const row of filteredRows) {
@@ -13753,14 +14055,13 @@ class HeatmapWidgetComponent {
13753
14055
  for (const cell of cells) {
13754
14056
  if (!cell?.attributePath)
13755
14057
  continue;
13756
- const sanitizedPath = this.sanitizeFieldName(cell.attributePath);
13757
- if (sanitizedPath === dateField) {
14058
+ if (matchesAttributePath(cell.attributePath, dateField)) {
13758
14059
  dateFrom = this.parseDate(cell.value);
13759
14060
  }
13760
- else if (dateEndField && sanitizedPath === dateEndField) {
14061
+ else if (dateEndField && matchesAttributePath(cell.attributePath, dateEndField)) {
13761
14062
  dateTo = this.parseDate(cell.value);
13762
14063
  }
13763
- else if (valueField && sanitizedPath === valueField) {
14064
+ else if (valueField && matchesAttributePath(cell.attributePath, valueField)) {
13764
14065
  const val = cell.value;
13765
14066
  numericValue = typeof val === 'number' ? val : parseFloat(String(val));
13766
14067
  if (isNaN(numericValue))
@@ -13946,9 +14247,6 @@ class HeatmapWidgetComponent {
13946
14247
  const day = date.getUTCDate().toString().padStart(2, '0');
13947
14248
  return `${year}-${month}-${day}`;
13948
14249
  }
13949
- sanitizeFieldName(fieldName) {
13950
- return fieldName.replace(/\./g, '_');
13951
- }
13952
14250
  convertFiltersToDto(filters) {
13953
14251
  const variables = this.stateService.getVariables();
13954
14252
  return this.variableService.convertToFieldFilterDto(filters, variables);
@@ -14082,7 +14380,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
14082
14380
  }] } });
14083
14381
 
14084
14382
  class HeatmapConfigDialogComponent {
14085
- executeRuntimeQueryGQL = inject(ExecuteRuntimeQueryDtoGQL);
14383
+ getRuntimeQueryColumnsGQL = inject(GetRuntimeQueryColumnsDtoGQL);
14086
14384
  stateService = inject(MeshBoardStateService);
14087
14385
  windowRef = inject(WindowRef);
14088
14386
  querySelector;
@@ -14211,10 +14509,10 @@ class HeatmapConfigDialogComponent {
14211
14509
  async loadQueryColumns(queryRtId) {
14212
14510
  this.isLoadingColumns = true;
14213
14511
  try {
14214
- const result = await firstValueFrom(this.executeRuntimeQueryGQL.fetch({
14512
+ // Metadata-only skips the row execution path on the backend.
14513
+ const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
14215
14514
  variables: {
14216
- rtId: queryRtId,
14217
- first: 1
14515
+ rtId: queryRtId
14218
14516
  }
14219
14517
  }));
14220
14518
  const queryItems = result.data?.runtime?.runtimeQuery?.items ?? [];
@@ -14223,8 +14521,9 @@ class HeatmapConfigDialogComponent {
14223
14521
  const filteredColumns = columns
14224
14522
  .filter((c) => c !== null);
14225
14523
  this.queryColumns = filteredColumns.map(c => ({
14226
- attributePath: this.sanitizeFieldName(c.attributePath ?? ''),
14227
- attributeValueType: c.attributeValueType ?? ''
14524
+ attributePath: c.attributePath ?? '',
14525
+ attributeValueType: c.attributeValueType ?? '',
14526
+ aggregationType: c.aggregationType ?? null
14228
14527
  }));
14229
14528
  const numericTypes = ['INTEGER', 'FLOAT', 'DOUBLE', 'DECIMAL', 'LONG'];
14230
14529
  const dateTimeTypes = ['DATE_TIME', 'DATETIME', 'DATE'];
@@ -14251,9 +14550,6 @@ class HeatmapConfigDialogComponent {
14251
14550
  this.isLoadingColumns = false;
14252
14551
  }
14253
14552
  }
14254
- sanitizeFieldName(fieldName) {
14255
- return fieldName.replace(/\./g, '_');
14256
- }
14257
14553
  onFiltersChange(updatedFilters) {
14258
14554
  this.filters = updatedFilters;
14259
14555
  }
@@ -16306,7 +16602,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
16306
16602
  class WidgetGroupConfigDialogComponent {
16307
16603
  ckTypeSelectorService = inject(CkTypeSelectorService);
16308
16604
  attributeSelectorService = inject(AttributeSelectorService);
16309
- executeRuntimeQueryGQL = inject(ExecuteRuntimeQueryDtoGQL);
16605
+ getRuntimeQueryColumnsGQL = inject(GetRuntimeQueryColumnsDtoGQL);
16310
16606
  getCkTypeAvailableQueryColumnsGQL = inject(GetCkTypeAvailableQueryColumnsDtoGQL);
16311
16607
  meshBoardStateService = inject(MeshBoardStateService);
16312
16608
  windowRef = inject(WindowRef);
@@ -16472,10 +16768,10 @@ class WidgetGroupConfigDialogComponent {
16472
16768
  async loadQueryColumns(queryRtId) {
16473
16769
  this.isLoadingColumns = true;
16474
16770
  try {
16475
- const result = await firstValueFrom(this.executeRuntimeQueryGQL.fetch({
16771
+ // Metadata-only skips the row execution path on the backend.
16772
+ const result = await firstValueFrom(this.getRuntimeQueryColumnsGQL.fetch({
16476
16773
  variables: {
16477
- rtId: queryRtId,
16478
- first: 1
16774
+ rtId: queryRtId
16479
16775
  }
16480
16776
  }));
16481
16777
  const queryItems = result.data?.runtime?.runtimeQuery?.items ?? [];
@@ -16485,8 +16781,9 @@ class WidgetGroupConfigDialogComponent {
16485
16781
  this.availableColumns = columns
16486
16782
  .filter((c) => c !== null)
16487
16783
  .map(c => ({
16488
- attributePath: c.attributePath?.replace(/\./g, '_') ?? '',
16489
- attributeValueType: c.attributeValueType ?? ''
16784
+ attributePath: c.attributePath ?? '',
16785
+ attributeValueType: c.attributeValueType ?? '',
16786
+ aggregationType: c.aggregationType ?? null
16490
16787
  }));
16491
16788
  this.filteredColumns.set(this.availableColumns);
16492
16789
  }
@@ -16541,7 +16838,8 @@ class WidgetGroupConfigDialogComponent {
16541
16838
  .filter((c) => c !== null)
16542
16839
  .map(c => ({
16543
16840
  attributePath: c.attributePath || '',
16544
- attributeValueType: c.attributeValueType
16841
+ attributeValueType: c.attributeValueType,
16842
+ aggregationType: null
16545
16843
  }));
16546
16844
  this.availableColumns = mappedColumns;
16547
16845
  this.filteredColumns.set(mappedColumns);