@mohsinonxrm/pptb-fetchxml-studio 1.0.0

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 (74) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/README.md +250 -0
  3. package/eslint.config.js +23 -0
  4. package/index.html +71 -0
  5. package/package.json +53 -0
  6. package/public/vite.svg +1 -0
  7. package/src/app/AppShell.tsx +1733 -0
  8. package/src/assets/20px-Microsoft_Office_Excel_(2025/342/200/223present).svg.png +0 -0
  9. package/src/assets/react.svg +1 -0
  10. package/src/features/fetchxml/api/dataverseMetadata.ts +192 -0
  11. package/src/features/fetchxml/api/excelExport.ts +318 -0
  12. package/src/features/fetchxml/api/formattedValues.ts +107 -0
  13. package/src/features/fetchxml/api/pptbClient.ts +2635 -0
  14. package/src/features/fetchxml/model/displaySettings.ts +18 -0
  15. package/src/features/fetchxml/model/fetchxml.ts +346 -0
  16. package/src/features/fetchxml/model/fetchxmlIntellisense.ts +583 -0
  17. package/src/features/fetchxml/model/fetchxmlParser.ts +708 -0
  18. package/src/features/fetchxml/model/layoutxml.ts +372 -0
  19. package/src/features/fetchxml/model/nodes.ts +255 -0
  20. package/src/features/fetchxml/model/operators.ts +624 -0
  21. package/src/features/fetchxml/model/treeUtils.ts +174 -0
  22. package/src/features/fetchxml/state/builderStore.tsx +1160 -0
  23. package/src/features/fetchxml/state/cache.ts +393 -0
  24. package/src/features/fetchxml/ui/Dialogs/BulkDeleteDialog.tsx +352 -0
  25. package/src/features/fetchxml/ui/Dialogs/DeleteConfirmDialog.tsx +396 -0
  26. package/src/features/fetchxml/ui/Dialogs/SaveViewDialog.tsx +612 -0
  27. package/src/features/fetchxml/ui/Dialogs/SolutionPicker.tsx +114 -0
  28. package/src/features/fetchxml/ui/Dialogs/WorkflowPickerDialog.tsx +437 -0
  29. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/AttributeEditor.tsx +214 -0
  30. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/ConditionEditor.tsx +533 -0
  31. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/FetchOptionsEditor.tsx +296 -0
  32. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/FilterEditor.tsx +106 -0
  33. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/LinkEntityEditor.tsx +433 -0
  34. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/OrderEditor.tsx +161 -0
  35. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/ValueOfPicker.tsx +194 -0
  36. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel.tsx +225 -0
  37. package/src/features/fetchxml/ui/LeftPane/TreeView.tsx +604 -0
  38. package/src/features/fetchxml/ui/RightPane/AddColumnsPanel.tsx +800 -0
  39. package/src/features/fetchxml/ui/RightPane/ColumnConfigDialog.tsx +170 -0
  40. package/src/features/fetchxml/ui/RightPane/DataGridCellRenderers.tsx +356 -0
  41. package/src/features/fetchxml/ui/RightPane/EditColumnsPanel.tsx +372 -0
  42. package/src/features/fetchxml/ui/RightPane/FetchXmlEditor.tsx +399 -0
  43. package/src/features/fetchxml/ui/RightPane/FormattedValueUtils.ts +189 -0
  44. package/src/features/fetchxml/ui/RightPane/LayoutXmlViewer.tsx +171 -0
  45. package/src/features/fetchxml/ui/RightPane/PreviewTabs.tsx +471 -0
  46. package/src/features/fetchxml/ui/RightPane/ResultsCommandBar.tsx +430 -0
  47. package/src/features/fetchxml/ui/RightPane/ResultsGrid.tsx +983 -0
  48. package/src/features/fetchxml/ui/RightPane/XmlTextArea.tsx +65 -0
  49. package/src/features/fetchxml/ui/Settings/SettingsDrawer.tsx +175 -0
  50. package/src/features/fetchxml/ui/Toolbar/EntitySelector.tsx +788 -0
  51. package/src/features/fetchxml/ui/Toolbar/LoadViewPicker.tsx +274 -0
  52. package/src/features/fetchxml/ui/Toolbar/SaveViewButton.tsx +174 -0
  53. package/src/main.tsx +11 -0
  54. package/src/shared/components/AttributePicker.tsx +138 -0
  55. package/src/shared/components/BetweenInputPicker.tsx +120 -0
  56. package/src/shared/components/BooleanValuePicker.tsx +165 -0
  57. package/src/shared/components/DateTimeInputPicker.tsx +170 -0
  58. package/src/shared/components/DynamicValueList.tsx +128 -0
  59. package/src/shared/components/ExcelIcon.tsx +30 -0
  60. package/src/shared/components/MultiValuePicker.tsx +178 -0
  61. package/src/shared/components/NumericInputPicker.tsx +156 -0
  62. package/src/shared/components/OptionSetValuePicker.tsx +150 -0
  63. package/src/shared/components/RelationshipPicker.tsx +268 -0
  64. package/src/shared/contexts/ThemeContext.tsx +19 -0
  65. package/src/shared/hooks/useAccessMode.ts +77 -0
  66. package/src/shared/hooks/useLazyMetadata.ts +132 -0
  67. package/src/shared/hooks/usePptbContext.ts +170 -0
  68. package/src/shared/hooks/usePublisherFilter.ts +214 -0
  69. package/src/shared/hooks/useSolutionFilter.ts +162 -0
  70. package/src/shared/utils/debug.ts +87 -0
  71. package/tsconfig.app.json +28 -0
  72. package/tsconfig.json +7 -0
  73. package/tsconfig.node.json +26 -0
  74. package/vite.config.ts +7 -0
@@ -0,0 +1,533 @@
1
+ /**
2
+ * Property editor for Condition nodes
3
+ * Controls filter criteria: attribute, operator, value
4
+ */
5
+
6
+ import { useState, useEffect, useMemo } from "react";
7
+ import {
8
+ Field,
9
+ Input,
10
+ Dropdown,
11
+ Option,
12
+ Label,
13
+ Tooltip,
14
+ Switch,
15
+ makeStyles,
16
+ tokens,
17
+ } from "@fluentui/react-components";
18
+ import { Info16Regular } from "@fluentui/react-icons";
19
+ import type { ConditionNode, FetchNode } from "../../../../model/nodes";
20
+ import { AttributePicker } from "../../../../../../shared/components/AttributePicker";
21
+ import { OptionSetValuePicker } from "../../../../../../shared/components/OptionSetValuePicker";
22
+ import { BooleanValuePicker } from "../../../../../../shared/components/BooleanValuePicker";
23
+ import { NumericInputPicker } from "../../../../../../shared/components/NumericInputPicker";
24
+ import { DateTimeInputPicker } from "../../../../../../shared/components/DateTimeInputPicker";
25
+ import { BetweenInputPicker } from "../../../../../../shared/components/BetweenInputPicker";
26
+ import { MultiValuePicker } from "../../../../../../shared/components/MultiValuePicker";
27
+ import {
28
+ getOperatorsForAttributeType,
29
+ operatorRequiresValue,
30
+ type OperatorDefinition,
31
+ } from "../../../../model/operators";
32
+ import {
33
+ loadEntityAttributes,
34
+ loadAttributeDetailedMetadata,
35
+ } from "../../../../api/dataverseMetadata";
36
+ import type { AttributeMetadata } from "../../../../api/pptbClient";
37
+ import {
38
+ collectLinkEntityReferences,
39
+ isConditionInRootEntityFilter,
40
+ } from "../../../../model/treeUtils";
41
+ import { ValueOfPicker } from "./ValueOfPicker";
42
+
43
+ const useStyles = makeStyles({
44
+ container: {
45
+ display: "flex",
46
+ flexDirection: "column",
47
+ gap: tokens.spacingVerticalM,
48
+ padding: tokens.spacingVerticalM,
49
+ },
50
+ section: {
51
+ display: "flex",
52
+ flexDirection: "column",
53
+ gap: tokens.spacingVerticalS,
54
+ },
55
+ fieldWithTooltip: {
56
+ display: "flex",
57
+ alignItems: "center",
58
+ gap: tokens.spacingHorizontalXS,
59
+ },
60
+ tooltipIcon: {
61
+ color: tokens.colorNeutralForeground3,
62
+ cursor: "help",
63
+ },
64
+ compareToggle: {
65
+ display: "flex",
66
+ alignItems: "center",
67
+ gap: tokens.spacingHorizontalS,
68
+ marginBottom: tokens.spacingVerticalXS,
69
+ },
70
+ });
71
+
72
+ interface ConditionEditorProps {
73
+ node: ConditionNode;
74
+ entityName: string;
75
+ onUpdate: (updates: Record<string, unknown>) => void;
76
+ isAggregateQuery?: boolean;
77
+ fetchQuery?: FetchNode | null;
78
+ }
79
+
80
+ /**
81
+ * Returns the exact attribute type for valueof comparisons.
82
+ * Strict matching: source and target attributes must have the same type.
83
+ * e.g., Money can only compare to Money, Integer to Integer, etc.
84
+ */
85
+ function getCompatibleTypesForValueof(attributeType: string | undefined): string[] {
86
+ if (!attributeType) return [];
87
+
88
+ // Strict type matching - return only the exact same type
89
+ // This ensures Money compares to Money, Integer to Integer, etc.
90
+ return [attributeType];
91
+ }
92
+
93
+ export function ConditionEditor({
94
+ node,
95
+ entityName,
96
+ onUpdate,
97
+ isAggregateQuery = false,
98
+ fetchQuery = null,
99
+ }: ConditionEditorProps) {
100
+ const styles = useStyles();
101
+
102
+ // State for filtered operators based on selected attribute type
103
+ const [availableOperators, setAvailableOperators] = useState<OperatorDefinition[]>([]);
104
+ // State for selected attribute metadata
105
+ const [selectedAttribute, setSelectedAttribute] = useState<AttributeMetadata | null>(null);
106
+
107
+ // Collect available link-entity references for entityname picker
108
+ const linkEntityReferences = useMemo(() => collectLinkEntityReferences(fetchQuery), [fetchQuery]);
109
+
110
+ // Check if this condition is in the root entity's filter (where entityname is applicable)
111
+ const isInRootFilter = useMemo(
112
+ () => isConditionInRootEntityFilter(fetchQuery, node.id),
113
+ [fetchQuery, node.id]
114
+ );
115
+
116
+ // Determine the effective entity name for attribute loading
117
+ // If entityname is set, use the linked entity's name; otherwise use the parent entity
118
+ const effectiveEntityName = useMemo(() => {
119
+ if (node.entityname && isInRootFilter) {
120
+ // Find the link-entity reference and get its entity name
121
+ const ref = linkEntityReferences.find((r) => r.identifier === node.entityname);
122
+ if (ref) {
123
+ return ref.entityName;
124
+ }
125
+ }
126
+ return entityName;
127
+ }, [node.entityname, isInRootFilter, linkEntityReferences, entityName]);
128
+
129
+ // Get compatible attribute types for valueof comparison
130
+ const compatibleTypesForValueof = useMemo(
131
+ () => getCompatibleTypesForValueof(selectedAttribute?.AttributeType),
132
+ [selectedAttribute?.AttributeType]
133
+ );
134
+
135
+ // Load attribute metadata when attribute changes to get its type
136
+ useEffect(() => {
137
+ if (!node.attribute || !effectiveEntityName) {
138
+ // No attribute selected - show all operators
139
+ setAvailableOperators(getOperatorsForAttributeType(undefined));
140
+ setSelectedAttribute(null);
141
+ return;
142
+ }
143
+
144
+ // Load entity attributes to find the selected attribute's type
145
+ loadEntityAttributes(effectiveEntityName)
146
+ .then(async (attributes: AttributeMetadata[]) => {
147
+ const attr = attributes.find((a) => a.LogicalName === node.attribute);
148
+ const type = attr?.AttributeType;
149
+
150
+ // For numeric and datetime types, load detailed metadata
151
+ const needsDetailedMetadata =
152
+ type === "Integer" ||
153
+ type === "BigInt" ||
154
+ type === "Decimal" ||
155
+ type === "Double" ||
156
+ type === "DateTime";
157
+
158
+ let detailedAttr = attr;
159
+ if (needsDetailedMetadata && attr) {
160
+ try {
161
+ // Load detailed metadata with type casting to get MinValue, MaxValue, Precision, Format
162
+ detailedAttr = await loadAttributeDetailedMetadata(entityName, node.attribute, type);
163
+ console.log(`Loaded detailed metadata for ${node.attribute}:`, detailedAttr);
164
+ } catch (error) {
165
+ console.error(`Failed to load detailed metadata for ${node.attribute}:`, error);
166
+ // Fall back to basic metadata
167
+ detailedAttr = attr;
168
+ }
169
+ }
170
+
171
+ // Store the full attribute metadata (with detailed properties if available)
172
+ setSelectedAttribute(detailedAttr || null);
173
+
174
+ // Filter operators based on attribute type
175
+ let operators = getOperatorsForAttributeType(type);
176
+
177
+ // For DateTime with DateOnly behavior, exclude operators that require hours/minutes
178
+ if (type === "DateTime" && detailedAttr) {
179
+ const dateTimeBehavior = detailedAttr.DateTimeBehavior?.Value;
180
+ const format = detailedAttr.Format;
181
+
182
+ // DateOnly format or UserLocal behavior with DateOnly format should exclude hour/minute operators
183
+ const isDateOnly = format === "DateOnly" || dateTimeBehavior === "DateOnly";
184
+
185
+ if (isDateOnly) {
186
+ const excludedOperators = [
187
+ "last-x-hours",
188
+ "next-x-hours",
189
+ "olderthan-x-hours",
190
+ "olderthan-x-minutes",
191
+ ];
192
+ operators = operators.filter((op) => !excludedOperators.includes(op.value));
193
+ }
194
+ }
195
+
196
+ setAvailableOperators(operators);
197
+ })
198
+ .catch((error) => {
199
+ console.error("Failed to load attribute metadata for operator filtering:", error);
200
+ // Fallback to all operators on error
201
+ setAvailableOperators(getOperatorsForAttributeType(undefined));
202
+ setSelectedAttribute(null);
203
+ });
204
+ }, [node.attribute, effectiveEntityName]);
205
+
206
+ const handleTextChange = (field: string) => (_: unknown, data: { value: string }) => {
207
+ onUpdate({ [field]: data.value || undefined });
208
+ };
209
+
210
+ const handleDropdownChange = (field: string) => (_: unknown, data: { optionValue?: string }) => {
211
+ onUpdate({ [field]: data.optionValue });
212
+ };
213
+
214
+ const handleAttributeChange = (logicalName: string | undefined) => {
215
+ // When attribute is cleared or changed, reset operator and value
216
+ if (!logicalName) {
217
+ onUpdate({ attribute: undefined, operator: undefined, value: undefined });
218
+ } else {
219
+ onUpdate({ attribute: logicalName, operator: undefined, value: undefined });
220
+ }
221
+ };
222
+
223
+ const handleEntityNameChange = (identifier: string | undefined) => {
224
+ // When entityname changes, also clear attribute, operator, and value
225
+ // since the attribute must come from the selected entity
226
+ onUpdate({
227
+ entityname: identifier || undefined,
228
+ attribute: undefined,
229
+ operator: undefined,
230
+ value: undefined,
231
+ });
232
+ };
233
+ // Check if the current operator requires a value (using the smart function)
234
+ const requiresValue = operatorRequiresValue(node.operator);
235
+
236
+ // Get the operator definition to check for multi-value requirements
237
+ const operatorDef = availableOperators.find((op) => op.value === node.operator);
238
+ const requiresTwoValues = operatorDef?.requiresTwoValues ?? false;
239
+ const requiresMultipleValues = operatorDef?.requiresMultipleValues ?? false;
240
+
241
+ return (
242
+ <div className={styles.container}>
243
+ {/* Basic Properties */}
244
+ <div className={styles.section}>
245
+ <Label weight="semibold">Condition Properties</Label>
246
+ <Field label="Attribute Name" required>
247
+ <div className={styles.fieldWithTooltip}>
248
+ <AttributePicker
249
+ entityLogicalName={effectiveEntityName}
250
+ value={node.attribute}
251
+ onChange={handleAttributeChange}
252
+ placeholder="Select or type attribute name"
253
+ />
254
+ <Tooltip
255
+ content={
256
+ node.entityname
257
+ ? `Logical name of the attribute from the linked entity "${node.entityname}".`
258
+ : "Logical name of the attribute to filter on. Must exist in the parent entity."
259
+ }
260
+ relationship="description"
261
+ >
262
+ <Info16Regular className={styles.tooltipIcon} />
263
+ </Tooltip>
264
+ </div>
265
+ </Field>
266
+ <Field label="Operator" required>
267
+ <div className={styles.fieldWithTooltip}>
268
+ <Dropdown
269
+ value={node.operator}
270
+ selectedOptions={[node.operator]}
271
+ onOptionSelect={handleDropdownChange("operator")}
272
+ placeholder="Select operator"
273
+ >
274
+ {availableOperators.map((op) => (
275
+ <Option key={op.value} value={op.value}>
276
+ {op.label}
277
+ </Option>
278
+ ))}
279
+ </Dropdown>
280
+ <Tooltip
281
+ content="Comparison operator. Available operators depend on attribute type (string, number, datetime, lookup, etc.)."
282
+ relationship="description"
283
+ >
284
+ <Info16Regular className={styles.tooltipIcon} />
285
+ </Tooltip>
286
+ </div>
287
+ </Field>
288
+ {requiresValue && (
289
+ <>
290
+ {/* Multi-value operators don't support valueof */}
291
+ {requiresMultipleValues && selectedAttribute ? (
292
+ <Field label="Values">
293
+ <div className={styles.fieldWithTooltip}>
294
+ <MultiValuePicker
295
+ entityLogicalName={effectiveEntityName}
296
+ attributeLogicalName={node.attribute}
297
+ attribute={selectedAttribute}
298
+ value={Array.isArray(node.value) ? node.value : undefined}
299
+ onChange={(val) => onUpdate({ value: val })}
300
+ placeholder="Enter values (comma-separated)"
301
+ />
302
+ <Tooltip
303
+ content="Enter multiple values separated by commas."
304
+ relationship="description"
305
+ >
306
+ <Info16Regular className={styles.tooltipIcon} />
307
+ </Tooltip>
308
+ </div>
309
+ </Field>
310
+ ) : /* Dual-value operators don't support valueof */
311
+ requiresTwoValues && selectedAttribute ? (
312
+ <Field label="Value Range">
313
+ <div className={styles.fieldWithTooltip}>
314
+ <BetweenInputPicker
315
+ attribute={selectedAttribute}
316
+ value={
317
+ Array.isArray(node.value) && node.value.length === 2
318
+ ? [node.value[0], node.value[1]]
319
+ : [undefined, undefined]
320
+ }
321
+ onChange={(val) => onUpdate({ value: val })}
322
+ placeholder1="Start value"
323
+ placeholder2="End value"
324
+ />
325
+ <Tooltip
326
+ content="Enter start and end values for the range."
327
+ relationship="description"
328
+ >
329
+ <Info16Regular className={styles.tooltipIcon} />
330
+ </Tooltip>
331
+ </div>
332
+ </Field>
333
+ ) : (
334
+ /* Single-value operators support valueof comparison */
335
+ <>
336
+ {/* Toggle between literal value and compare to column */}
337
+ <div className={styles.compareToggle}>
338
+ <Switch
339
+ checked={node.valueof !== undefined}
340
+ onChange={(_ev, data) => {
341
+ if (data.checked) {
342
+ // Switching to compare mode - clear value, set valueof to empty
343
+ onUpdate({ value: undefined, valueof: "" });
344
+ } else {
345
+ // Switching to literal mode - clear valueof
346
+ onUpdate({ valueof: undefined, value: undefined });
347
+ }
348
+ }}
349
+ />
350
+ <Label size="small">
351
+ {node.valueof !== undefined ? "Compare to column" : "Literal value"}
352
+ </Label>
353
+ <Tooltip
354
+ content="Toggle between comparing to a literal value or another column's value (same row)."
355
+ relationship="description"
356
+ >
357
+ <Info16Regular className={styles.tooltipIcon} />
358
+ </Tooltip>
359
+ </div>
360
+
361
+ {node.valueof !== undefined ? (
362
+ /* Compare to column mode - show entity selector + attribute picker */
363
+ <ValueOfPicker
364
+ fetchQuery={fetchQuery}
365
+ rootEntityName={fetchQuery?.entity?.name || ""}
366
+ effectiveEntityName={effectiveEntityName}
367
+ valueof={node.valueof || ""}
368
+ compatibleTypes={compatibleTypesForValueof}
369
+ onChange={(valueof) => {
370
+ // Keep valueof as empty string (not undefined) when entity changes
371
+ // to prevent the switch from reverting to literal mode
372
+ onUpdate({ valueof: valueof ?? undefined });
373
+ }}
374
+ />
375
+ ) : (
376
+ /* Literal value mode - show type-specific pickers */
377
+ <Field label="Value">
378
+ <div className={styles.fieldWithTooltip}>
379
+ {selectedAttribute?.AttributeType === "Picklist" ||
380
+ selectedAttribute?.AttributeType === "State" ||
381
+ selectedAttribute?.AttributeType === "Status" ? (
382
+ <OptionSetValuePicker
383
+ entityLogicalName={effectiveEntityName}
384
+ attributeLogicalName={node.attribute}
385
+ value={typeof node.value === "number" ? node.value : undefined}
386
+ onChange={(val) => onUpdate({ value: val })}
387
+ placeholder="Select a value"
388
+ />
389
+ ) : selectedAttribute?.AttributeType === "Boolean" ? (
390
+ <BooleanValuePicker
391
+ entityLogicalName={effectiveEntityName}
392
+ attributeLogicalName={node.attribute}
393
+ value={
394
+ typeof node.value === "boolean"
395
+ ? node.value
396
+ : typeof node.value === "string"
397
+ ? node.value === "true" || node.value === "1"
398
+ : undefined
399
+ }
400
+ onChange={(val) => onUpdate({ value: val })}
401
+ placeholder="Select true or false"
402
+ />
403
+ ) : selectedAttribute?.AttributeType === "Integer" ||
404
+ selectedAttribute?.AttributeType === "BigInt" ||
405
+ selectedAttribute?.AttributeType === "Decimal" ||
406
+ selectedAttribute?.AttributeType === "Double" ? (
407
+ <NumericInputPicker
408
+ attribute={selectedAttribute}
409
+ value={typeof node.value === "number" ? node.value : undefined}
410
+ onChange={(val) => onUpdate({ value: val })}
411
+ placeholder="Enter a number"
412
+ />
413
+ ) : selectedAttribute?.AttributeType === "DateTime" ? (
414
+ <DateTimeInputPicker
415
+ attribute={selectedAttribute}
416
+ value={
417
+ typeof node.value === "string"
418
+ ? node.value
419
+ : node.value instanceof Date
420
+ ? node.value
421
+ : undefined
422
+ }
423
+ onChange={(val) => onUpdate({ value: val?.toISOString() })}
424
+ placeholder="Select a date"
425
+ />
426
+ ) : (
427
+ <Input
428
+ value={
429
+ typeof node.value === "string" || typeof node.value === "number"
430
+ ? String(node.value)
431
+ : ""
432
+ }
433
+ onChange={handleTextChange("value")}
434
+ placeholder="Filter value"
435
+ />
436
+ )}
437
+ <Tooltip
438
+ content={
439
+ selectedAttribute?.AttributeType === "Picklist" ||
440
+ selectedAttribute?.AttributeType === "State" ||
441
+ selectedAttribute?.AttributeType === "Status"
442
+ ? "Select a value from the available options."
443
+ : selectedAttribute?.AttributeType === "Boolean"
444
+ ? "Select true or false."
445
+ : selectedAttribute?.AttributeType === "Integer" ||
446
+ selectedAttribute?.AttributeType === "BigInt" ||
447
+ selectedAttribute?.AttributeType === "Decimal" ||
448
+ selectedAttribute?.AttributeType === "Double"
449
+ ? "Enter a numeric value."
450
+ : selectedAttribute?.AttributeType === "DateTime"
451
+ ? "Select a date and optionally a time."
452
+ : "Value to compare against."
453
+ }
454
+ relationship="description"
455
+ >
456
+ <Info16Regular className={styles.tooltipIcon} />
457
+ </Tooltip>
458
+ </div>
459
+ </Field>
460
+ )}
461
+ </>
462
+ )}
463
+ </>
464
+ )}
465
+ </div>
466
+
467
+ {/* Advanced Section */}
468
+ <div className={styles.section}>
469
+ <Label weight="semibold">Advanced</Label>
470
+
471
+ {/* Entity Name - only show for conditions in root entity filters where there are link-entities */}
472
+ {isInRootFilter && linkEntityReferences.length > 0 && (
473
+ <Field
474
+ label="Entity Name (optional)"
475
+ hint="Filter on a column from a linked entity (outer join scenario)."
476
+ >
477
+ <div className={styles.fieldWithTooltip}>
478
+ <Dropdown
479
+ value={node.entityname ?? ""}
480
+ selectedOptions={node.entityname ? [node.entityname] : []}
481
+ onOptionSelect={(_ev, data) => {
482
+ const value = data.optionValue;
483
+ handleEntityNameChange(value === "" ? undefined : value);
484
+ }}
485
+ placeholder="Select linked entity..."
486
+ >
487
+ <Option value="">None (current entity)</Option>
488
+ {linkEntityReferences.map((ref) => (
489
+ <Option key={ref.identifier} value={ref.identifier}>
490
+ {ref.displayLabel}
491
+ </Option>
492
+ ))}
493
+ </Dropdown>
494
+ <Tooltip
495
+ content="When filtering on an outer-joined link-entity, specify which entity's column to filter. Use the alias if defined, otherwise the entity name."
496
+ relationship="description"
497
+ >
498
+ <Info16Regular className={styles.tooltipIcon} />
499
+ </Tooltip>
500
+ </div>
501
+ </Field>
502
+ )}
503
+
504
+ {isAggregateQuery && (
505
+ <Field label="Aggregate Function (optional)">
506
+ <div className={styles.fieldWithTooltip}>
507
+ <Dropdown
508
+ value={node.aggregate ?? "none"}
509
+ selectedOptions={[node.aggregate ?? "none"]}
510
+ onOptionSelect={handleDropdownChange("aggregate")}
511
+ placeholder="None"
512
+ >
513
+ <Option value="none">None</Option>
514
+ <Option value="count">Count</Option>
515
+ <Option value="countcolumn">Count Column</Option>
516
+ <Option value="sum">Sum</Option>
517
+ <Option value="avg">Average</Option>
518
+ <Option value="min">Minimum</Option>
519
+ <Option value="max">Maximum</Option>
520
+ </Dropdown>
521
+ <Tooltip
522
+ content="Apply aggregate function before comparing. Used in HAVING clauses for aggregate queries."
523
+ relationship="description"
524
+ >
525
+ <Info16Regular className={styles.tooltipIcon} />
526
+ </Tooltip>
527
+ </div>
528
+ </Field>
529
+ )}
530
+ </div>
531
+ </div>
532
+ );
533
+ }