@mohsinonxrm/pptb-fetchxml-studio 1.0.0 → 1.0.2

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 (78) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +1 -1
  3. package/dist/assets/index-DD9fzkbz.js +232 -0
  4. package/dist/index.html +70 -0
  5. package/index.html +0 -1
  6. package/npm-shrinkwrap.json +6329 -0
  7. package/package.json +95 -51
  8. package/CHANGELOG.md +0 -120
  9. package/eslint.config.js +0 -23
  10. package/public/vite.svg +0 -1
  11. package/src/app/AppShell.tsx +0 -1733
  12. package/src/assets/20px-Microsoft_Office_Excel_(2025/342/200/223present).svg.png +0 -0
  13. package/src/assets/react.svg +0 -1
  14. package/src/features/fetchxml/api/dataverseMetadata.ts +0 -192
  15. package/src/features/fetchxml/api/excelExport.ts +0 -318
  16. package/src/features/fetchxml/api/formattedValues.ts +0 -107
  17. package/src/features/fetchxml/api/pptbClient.ts +0 -2635
  18. package/src/features/fetchxml/model/displaySettings.ts +0 -18
  19. package/src/features/fetchxml/model/fetchxml.ts +0 -346
  20. package/src/features/fetchxml/model/fetchxmlIntellisense.ts +0 -583
  21. package/src/features/fetchxml/model/fetchxmlParser.ts +0 -708
  22. package/src/features/fetchxml/model/layoutxml.ts +0 -372
  23. package/src/features/fetchxml/model/nodes.ts +0 -255
  24. package/src/features/fetchxml/model/operators.ts +0 -624
  25. package/src/features/fetchxml/model/treeUtils.ts +0 -174
  26. package/src/features/fetchxml/state/builderStore.tsx +0 -1160
  27. package/src/features/fetchxml/state/cache.ts +0 -393
  28. package/src/features/fetchxml/ui/Dialogs/BulkDeleteDialog.tsx +0 -352
  29. package/src/features/fetchxml/ui/Dialogs/DeleteConfirmDialog.tsx +0 -396
  30. package/src/features/fetchxml/ui/Dialogs/SaveViewDialog.tsx +0 -612
  31. package/src/features/fetchxml/ui/Dialogs/SolutionPicker.tsx +0 -114
  32. package/src/features/fetchxml/ui/Dialogs/WorkflowPickerDialog.tsx +0 -437
  33. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/AttributeEditor.tsx +0 -214
  34. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/ConditionEditor.tsx +0 -533
  35. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/FetchOptionsEditor.tsx +0 -296
  36. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/FilterEditor.tsx +0 -106
  37. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/LinkEntityEditor.tsx +0 -433
  38. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/OrderEditor.tsx +0 -161
  39. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/ValueOfPicker.tsx +0 -194
  40. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel.tsx +0 -225
  41. package/src/features/fetchxml/ui/LeftPane/TreeView.tsx +0 -604
  42. package/src/features/fetchxml/ui/RightPane/AddColumnsPanel.tsx +0 -800
  43. package/src/features/fetchxml/ui/RightPane/ColumnConfigDialog.tsx +0 -170
  44. package/src/features/fetchxml/ui/RightPane/DataGridCellRenderers.tsx +0 -356
  45. package/src/features/fetchxml/ui/RightPane/EditColumnsPanel.tsx +0 -372
  46. package/src/features/fetchxml/ui/RightPane/FetchXmlEditor.tsx +0 -399
  47. package/src/features/fetchxml/ui/RightPane/FormattedValueUtils.ts +0 -189
  48. package/src/features/fetchxml/ui/RightPane/LayoutXmlViewer.tsx +0 -171
  49. package/src/features/fetchxml/ui/RightPane/PreviewTabs.tsx +0 -471
  50. package/src/features/fetchxml/ui/RightPane/ResultsCommandBar.tsx +0 -430
  51. package/src/features/fetchxml/ui/RightPane/ResultsGrid.tsx +0 -983
  52. package/src/features/fetchxml/ui/RightPane/XmlTextArea.tsx +0 -65
  53. package/src/features/fetchxml/ui/Settings/SettingsDrawer.tsx +0 -175
  54. package/src/features/fetchxml/ui/Toolbar/EntitySelector.tsx +0 -788
  55. package/src/features/fetchxml/ui/Toolbar/LoadViewPicker.tsx +0 -274
  56. package/src/features/fetchxml/ui/Toolbar/SaveViewButton.tsx +0 -174
  57. package/src/main.tsx +0 -11
  58. package/src/shared/components/AttributePicker.tsx +0 -138
  59. package/src/shared/components/BetweenInputPicker.tsx +0 -120
  60. package/src/shared/components/BooleanValuePicker.tsx +0 -165
  61. package/src/shared/components/DateTimeInputPicker.tsx +0 -170
  62. package/src/shared/components/DynamicValueList.tsx +0 -128
  63. package/src/shared/components/ExcelIcon.tsx +0 -30
  64. package/src/shared/components/MultiValuePicker.tsx +0 -178
  65. package/src/shared/components/NumericInputPicker.tsx +0 -156
  66. package/src/shared/components/OptionSetValuePicker.tsx +0 -150
  67. package/src/shared/components/RelationshipPicker.tsx +0 -268
  68. package/src/shared/contexts/ThemeContext.tsx +0 -19
  69. package/src/shared/hooks/useAccessMode.ts +0 -77
  70. package/src/shared/hooks/useLazyMetadata.ts +0 -132
  71. package/src/shared/hooks/usePptbContext.ts +0 -170
  72. package/src/shared/hooks/usePublisherFilter.ts +0 -214
  73. package/src/shared/hooks/useSolutionFilter.ts +0 -162
  74. package/src/shared/utils/debug.ts +0 -87
  75. package/tsconfig.app.json +0 -28
  76. package/tsconfig.json +0 -7
  77. package/tsconfig.node.json +0 -26
  78. package/vite.config.ts +0 -7
@@ -1,604 +0,0 @@
1
- /**
2
- * Tree view for FetchXML query builder
3
- * Displays hierarchical structure: Entity -> Attributes, Orders, Filters, Link-Entities
4
- */
5
-
6
- import { useState, useEffect } from "react";
7
- import {
8
- Tree,
9
- TreeItem,
10
- TreeItemLayout,
11
- Button,
12
- makeStyles,
13
- Menu,
14
- MenuTrigger,
15
- MenuPopover,
16
- MenuList,
17
- MenuItem,
18
- tokens,
19
- type TreeOpenChangeData,
20
- } from "@fluentui/react-components";
21
- import {
22
- Table20Regular,
23
- Column20Regular,
24
- Filter20Regular,
25
- ArrowSort20Regular,
26
- Link20Regular,
27
- Add20Regular,
28
- Delete20Regular,
29
- } from "@fluentui/react-icons";
30
- import type {
31
- FetchNode,
32
- EntityNode,
33
- AttributeNode,
34
- AllAttributesNode,
35
- OrderNode,
36
- FilterNode,
37
- ConditionNode,
38
- LinkEntityNode,
39
- NodeId,
40
- } from "../../model/nodes";
41
- import { operatorRequiresValue } from "../../model/operators";
42
- import { debugLog, debugWarn } from "../../../../shared/utils/debug";
43
-
44
- /**
45
- * Format a value for display, avoiding scientific notation for numbers
46
- */
47
- function formatDisplayValue(val: unknown): string {
48
- if (typeof val === "number") {
49
- if (Math.abs(val) < 1e-10 && val !== 0) {
50
- // Very small numbers - use high precision
51
- return val.toFixed(20).replace(/\.?0+$/, "");
52
- } else if (Number.isInteger(val)) {
53
- // Integer - no decimal point needed
54
- return val.toString();
55
- } else {
56
- // Decimal - preserve precision but remove trailing zeros
57
- return val.toFixed(10).replace(/\.?0+$/, "");
58
- }
59
- }
60
- if (Array.isArray(val)) {
61
- // Format array values (for between, in, etc.)
62
- return val.map((v) => formatDisplayValue(v)).join(", ");
63
- }
64
- return String(val);
65
- }
66
-
67
- const useStyles = makeStyles({
68
- container: {
69
- flex: 1,
70
- overflow: "auto",
71
- padding: "8px",
72
- minHeight: 0, // Important for flex child with overflow
73
- // Ensure scrollbars are always visible when content overflows
74
- scrollbarGutter: "stable",
75
- },
76
- treeItem: {
77
- "&:hover": {
78
- backgroundColor: tokens.colorNeutralBackground1Hover,
79
- },
80
- },
81
- actions: {
82
- display: "flex",
83
- gap: "4px",
84
- opacity: 0,
85
- transition: "opacity 0.2s",
86
- "&:focus-within": {
87
- opacity: 1,
88
- },
89
- },
90
- treeItemWithActions: {
91
- "&:hover .actions": {
92
- opacity: 1,
93
- },
94
- },
95
- });
96
-
97
- interface TreeViewProps {
98
- fetchQuery: FetchNode;
99
- selectedNodeId: NodeId | null;
100
- onNodeSelect: (nodeId: NodeId) => void;
101
- onAddAttribute: (parentId: NodeId) => void;
102
- onAddAllAttributes: (parentId: NodeId) => void;
103
- onAddOrder: (parentId: NodeId) => void;
104
- onAddFilter: (parentId: NodeId) => void;
105
- onAddSubfilter: (filterId: NodeId) => void;
106
- onAddCondition: (filterId: NodeId) => void;
107
- onAddLinkEntity: (parentId: NodeId) => void;
108
- onRemoveNode: (nodeId: NodeId) => void;
109
- }
110
-
111
- export function TreeView({
112
- fetchQuery,
113
- selectedNodeId,
114
- onNodeSelect,
115
- onAddAttribute,
116
- onAddAllAttributes,
117
- onAddOrder,
118
- onAddFilter,
119
- onAddSubfilter,
120
- onAddCondition,
121
- onAddLinkEntity,
122
- onRemoveNode,
123
- }: TreeViewProps) {
124
- const styles = useStyles();
125
-
126
- // Manage which tree items are open/expanded
127
- const [openItems, setOpenItems] = useState<Set<NodeId>>(
128
- new Set([fetchQuery.id, fetchQuery.entity.id])
129
- );
130
-
131
- // Helper to find path from root to a node (all ancestor IDs)
132
- const findPathToNode = (targetId: NodeId): NodeId[] => {
133
- const path: NodeId[] = [];
134
-
135
- const findInNode = (node: unknown, target: NodeId): boolean => {
136
- if (!node || typeof node !== "object") return false;
137
-
138
- const n = node as Record<string, unknown>;
139
-
140
- if (n.id === target) {
141
- debugLog("treeExpansion", "Found target node:", n.id);
142
- // Don't add the target to path, only ancestors
143
- return true;
144
- }
145
-
146
- // Add current node to path before searching children
147
- const currentId = n.id as NodeId;
148
- debugLog("treeExpansion", "Checking node:", currentId, "type:", n.type);
149
-
150
- // Search in all possible child arrays
151
- const arrayProps = ["attributes", "orders", "filters", "conditions", "subfilters", "links"];
152
- for (const prop of arrayProps) {
153
- if (Array.isArray(n[prop])) {
154
- for (const item of n[prop]) {
155
- if (findInNode(item, target)) {
156
- // Found in this branch, add current to path
157
- path.unshift(currentId);
158
- return true;
159
- }
160
- }
161
- }
162
- }
163
-
164
- // Search in nested entity
165
- if (n.entity && findInNode(n.entity, target)) {
166
- // Found in entity, add current to path
167
- path.unshift(currentId);
168
- return true;
169
- }
170
-
171
- return false;
172
- };
173
-
174
- findInNode(fetchQuery, targetId);
175
- debugLog("treeExpansion", "Final path:", path);
176
- return path;
177
- };
178
-
179
- // Auto-expand ancestors when a new node is selected
180
- useEffect(() => {
181
- if (selectedNodeId) {
182
- debugLog("treeExpansion", "Node selected, expanding path:", selectedNodeId);
183
- const pathToNode = findPathToNode(selectedNodeId);
184
- debugLog("treeExpansion", "Path to node:", pathToNode);
185
-
186
- if (pathToNode.length === 0) {
187
- debugWarn("treeExpansion", "Path is empty! Node might not be in tree yet.");
188
- return;
189
- }
190
-
191
- setOpenItems((prev) => {
192
- const newSet = new Set(prev);
193
- const sizeBefore = newSet.size;
194
- // Add all ancestors to open items (except the leaf node itself)
195
- pathToNode.forEach((id) => newSet.add(id));
196
- const itemsAdded = Array.from(newSet).filter((id) => !prev.has(id));
197
- debugLog("treeExpansion", "Updated openItems:", {
198
- sizeBefore,
199
- sizeAfter: newSet.size,
200
- added: itemsAdded,
201
- addedCount: itemsAdded.length,
202
- allOpen: Array.from(newSet),
203
- });
204
-
205
- // Always return the new set to ensure re-render even if no new items
206
- return newSet;
207
- });
208
-
209
- // Scroll the selected node into view after a short delay to allow DOM updates
210
- // Note: Fluent UI Tree doesn't set aria-selected="true" reliably, so we use
211
- // the last tree item as the target (newly added nodes appear at the end)
212
- setTimeout(() => {
213
- const allTreeItems = document.querySelectorAll('[role="treeitem"]');
214
-
215
- if (allTreeItems.length > 0) {
216
- const targetElement = allTreeItems[allTreeItems.length - 1];
217
- targetElement.scrollIntoView({ behavior: "smooth", block: "nearest" });
218
- debugLog(
219
- "treeExpansion",
220
- `Scrolled to item ${allTreeItems.length} of ${allTreeItems.length}`
221
- );
222
- } else {
223
- debugWarn("treeExpansion", "No tree items found in DOM for scrolling");
224
- }
225
- }, 250);
226
- }
227
- }, [selectedNodeId]); // eslint-disable-line react-hooks/exhaustive-deps
228
-
229
- const handleOpenChange = (_event: unknown, data: TreeOpenChangeData) => {
230
- debugLog("treeExpansion", "handleOpenChange called:", {
231
- open: data.open,
232
- value: data.value,
233
- type: data.type,
234
- openItemsCount: data.openItems.size,
235
- openItems: Array.from(data.openItems),
236
- });
237
- // data.openItems is already the updated Set from Fluent UI
238
- // Use it directly to respect user's expand/collapse actions
239
- setOpenItems(data.openItems as Set<NodeId>);
240
- };
241
-
242
- const renderFetchNode = (fetch: FetchNode) => {
243
- return (
244
- <TreeItem
245
- key={fetch.id}
246
- itemType="branch"
247
- value={fetch.id}
248
- className={styles.treeItemWithActions}
249
- >
250
- <TreeItemLayout iconBefore={<Table20Regular />} onClick={() => onNodeSelect(fetch.id)}>
251
- Fetch Query
252
- </TreeItemLayout>
253
-
254
- <Tree>{renderEntityNode(fetch.entity, true)}</Tree>
255
- </TreeItem>
256
- );
257
- };
258
-
259
- const renderEntityNode = (entity: EntityNode, isRootEntity: boolean = true) => {
260
- // Only allow one top-level filter per entity
261
- const hasFilter = entity.filters.length > 0;
262
-
263
- return (
264
- <TreeItem
265
- key={entity.id}
266
- itemType="branch"
267
- value={entity.id}
268
- className={styles.treeItemWithActions}
269
- >
270
- <TreeItemLayout
271
- iconBefore={<Table20Regular />}
272
- actions={
273
- <div className="actions">
274
- <Menu>
275
- <MenuTrigger disableButtonEnhancement>
276
- <Button appearance="subtle" size="small" icon={<Add20Regular />} title="Add..." />
277
- </MenuTrigger>
278
- <MenuPopover>
279
- <MenuList>
280
- <MenuItem onClick={() => onAddAllAttributes(entity.id)}>
281
- All Attributes
282
- </MenuItem>
283
- <MenuItem onClick={() => onAddAttribute(entity.id)}>Attribute</MenuItem>
284
- <MenuItem onClick={() => onAddOrder(entity.id)}>Order By</MenuItem>
285
- <MenuItem onClick={() => onAddFilter(entity.id)} disabled={hasFilter}>
286
- Filter
287
- </MenuItem>
288
- <MenuItem onClick={() => onAddLinkEntity(entity.id)}>Link-Entity</MenuItem>
289
- </MenuList>
290
- </MenuPopover>
291
- </Menu>
292
- {!isRootEntity && (
293
- <Button
294
- appearance="subtle"
295
- size="small"
296
- icon={<Delete20Regular />}
297
- onClick={(e) => {
298
- e.stopPropagation();
299
- onRemoveNode(entity.id);
300
- }}
301
- title="Remove"
302
- />
303
- )}
304
- </div>
305
- }
306
- onClick={() => onNodeSelect(entity.id)}
307
- >
308
- {entity.name}
309
- </TreeItemLayout>
310
-
311
- <Tree>
312
- {/* All Attributes */}
313
- {entity.allAttributes?.enabled && renderAllAttributesNode(entity.allAttributes)}
314
-
315
- {/* Attributes */}
316
- {entity.attributes.map((attr) => renderAttributeNode(attr))}
317
-
318
- {/* Orders */}
319
- {entity.orders.map((order) => renderOrderNode(order))}
320
-
321
- {/* Filters */}
322
- {entity.filters.map((filter) => renderFilterNode(filter))}
323
-
324
- {/* Link-Entities */}
325
- {entity.links.map((link) => renderLinkEntityNode(link))}
326
- </Tree>
327
- </TreeItem>
328
- );
329
- };
330
-
331
- const renderAllAttributesNode = (node: AllAttributesNode) => {
332
- return (
333
- <TreeItem key={node.id} itemType="leaf" value={node.id} className={styles.treeItem}>
334
- <TreeItemLayout
335
- iconBefore={<Column20Regular />}
336
- actions={
337
- <Button
338
- appearance="subtle"
339
- size="small"
340
- icon={<Delete20Regular />}
341
- onClick={(e) => {
342
- e.stopPropagation();
343
- onRemoveNode(node.id);
344
- }}
345
- title="Remove"
346
- />
347
- }
348
- onClick={() => onNodeSelect(node.id)}
349
- >
350
- All Attributes
351
- </TreeItemLayout>
352
- </TreeItem>
353
- );
354
- };
355
-
356
- const renderAttributeNode = (attr: AttributeNode) => {
357
- const label = attr.alias ? `${attr.name} (as ${attr.alias})` : attr.name;
358
- const suffix = attr.aggregate ? ` [${attr.aggregate}]` : "";
359
-
360
- return (
361
- <TreeItem
362
- key={attr.id}
363
- itemType="leaf"
364
- value={attr.id}
365
- className={styles.treeItemWithActions}
366
- >
367
- <TreeItemLayout
368
- iconBefore={<Column20Regular />}
369
- actions={
370
- <div className="actions">
371
- <Button
372
- appearance="subtle"
373
- size="small"
374
- icon={<Delete20Regular />}
375
- onClick={(e) => {
376
- e.stopPropagation();
377
- onRemoveNode(attr.id);
378
- }}
379
- title="Remove Attribute"
380
- />
381
- </div>
382
- }
383
- onClick={() => onNodeSelect(attr.id)}
384
- >
385
- {label}
386
- {suffix}
387
- </TreeItemLayout>
388
- </TreeItem>
389
- );
390
- };
391
-
392
- const renderOrderNode = (order: OrderNode) => {
393
- const direction = order.descending ? "↓" : "↑";
394
- // Show entityname prefix for link-entity orders
395
- const displayName = order.entityname
396
- ? `${order.entityname}.${order.attribute}`
397
- : order.attribute;
398
- return (
399
- <TreeItem
400
- key={order.id}
401
- itemType="leaf"
402
- value={order.id}
403
- className={styles.treeItemWithActions}
404
- >
405
- <TreeItemLayout
406
- iconBefore={<ArrowSort20Regular />}
407
- actions={
408
- <div className="actions">
409
- <Button
410
- appearance="subtle"
411
- size="small"
412
- icon={<Delete20Regular />}
413
- onClick={(e) => {
414
- e.stopPropagation();
415
- onRemoveNode(order.id);
416
- }}
417
- title="Remove Order"
418
- />
419
- </div>
420
- }
421
- onClick={() => onNodeSelect(order.id)}
422
- >
423
- {displayName} {direction}
424
- </TreeItemLayout>
425
- </TreeItem>
426
- );
427
- };
428
-
429
- const renderFilterNode = (filter: FilterNode) => {
430
- return (
431
- <TreeItem
432
- key={filter.id}
433
- itemType="branch"
434
- value={filter.id}
435
- className={styles.treeItemWithActions}
436
- >
437
- <TreeItemLayout
438
- iconBefore={<Filter20Regular />}
439
- actions={
440
- <div className="actions">
441
- <Menu>
442
- <MenuTrigger disableButtonEnhancement>
443
- <Button appearance="subtle" size="small" icon={<Add20Regular />} title="Add..." />
444
- </MenuTrigger>
445
- <MenuPopover>
446
- <MenuList>
447
- <MenuItem onClick={() => onAddCondition(filter.id)}>Condition</MenuItem>
448
- <MenuItem onClick={() => onAddSubfilter(filter.id)}>Sub-Filter</MenuItem>
449
- <MenuItem onClick={() => onAddLinkEntity(filter.id)}>
450
- Link-Entity (any/not any/all)
451
- </MenuItem>
452
- </MenuList>
453
- </MenuPopover>
454
- </Menu>
455
- <Button
456
- appearance="subtle"
457
- size="small"
458
- icon={<Delete20Regular />}
459
- onClick={(e) => {
460
- e.stopPropagation();
461
- onRemoveNode(filter.id);
462
- }}
463
- title="Remove Filter"
464
- />
465
- </div>
466
- }
467
- onClick={() => onNodeSelect(filter.id)}
468
- >
469
- Filter ({filter.conjunction})
470
- </TreeItemLayout>
471
-
472
- <Tree>
473
- {filter.conditions.map((cond) => renderConditionNode(cond))}
474
- {filter.subfilters.map((subfilter) => renderFilterNode(subfilter))}
475
- {filter.links?.map((link) => renderLinkEntityNode(link))}
476
- </Tree>
477
- </TreeItem>
478
- );
479
- };
480
-
481
- const renderConditionNode = (condition: ConditionNode) => {
482
- // Format the display text based on operator and value
483
- const requiresValue = operatorRequiresValue(condition.operator);
484
- let displayText = `${condition.attribute} ${condition.operator}`;
485
-
486
- if (
487
- requiresValue &&
488
- condition.value !== undefined &&
489
- condition.value !== null &&
490
- condition.value !== ""
491
- ) {
492
- displayText += ` = ${formatDisplayValue(condition.value)}`;
493
- }
494
-
495
- return (
496
- <TreeItem
497
- key={condition.id}
498
- itemType="leaf"
499
- value={condition.id}
500
- className={styles.treeItemWithActions}
501
- >
502
- <TreeItemLayout
503
- actions={
504
- <div className="actions">
505
- <Button
506
- appearance="subtle"
507
- size="small"
508
- icon={<Delete20Regular />}
509
- onClick={(e) => {
510
- e.stopPropagation();
511
- onRemoveNode(condition.id);
512
- }}
513
- title="Remove Condition"
514
- />
515
- </div>
516
- }
517
- onClick={() => onNodeSelect(condition.id)}
518
- >
519
- {displayText}
520
- </TreeItemLayout>
521
- </TreeItem>
522
- );
523
- };
524
-
525
- const renderLinkEntityNode = (link: LinkEntityNode) => {
526
- // Attributes are not allowed in "not any" and "not all" link types
527
- const canHaveAttributes = link.linkType !== "not any" && link.linkType !== "not all";
528
- // Only allow one top-level filter per link-entity
529
- const hasFilter = link.filters.length > 0;
530
-
531
- return (
532
- <TreeItem
533
- key={link.id}
534
- itemType="branch"
535
- value={link.id}
536
- className={styles.treeItemWithActions}
537
- >
538
- <TreeItemLayout
539
- iconBefore={<Link20Regular />}
540
- actions={
541
- <div className="actions">
542
- <Menu>
543
- <MenuTrigger disableButtonEnhancement>
544
- <Button appearance="subtle" size="small" icon={<Add20Regular />} title="Add..." />
545
- </MenuTrigger>
546
- <MenuPopover>
547
- <MenuList>
548
- <MenuItem
549
- onClick={() => onAddAllAttributes(link.id)}
550
- disabled={!canHaveAttributes}
551
- >
552
- All Attributes
553
- </MenuItem>
554
- <MenuItem onClick={() => onAddAttribute(link.id)} disabled={!canHaveAttributes}>
555
- Attribute
556
- </MenuItem>
557
- <MenuItem onClick={() => onAddOrder(link.id)}>Order By</MenuItem>
558
- <MenuItem onClick={() => onAddFilter(link.id)} disabled={hasFilter}>
559
- Filter
560
- </MenuItem>
561
- <MenuItem onClick={() => onAddLinkEntity(link.id)}>Link-Entity</MenuItem>
562
- </MenuList>
563
- </MenuPopover>
564
- </Menu>
565
- <Button
566
- appearance="subtle"
567
- size="small"
568
- icon={<Delete20Regular />}
569
- onClick={(e) => {
570
- e.stopPropagation();
571
- onRemoveNode(link.id);
572
- }}
573
- title="Remove Link"
574
- />
575
- </div>
576
- }
577
- onClick={() => onNodeSelect(link.id)}
578
- >
579
- {link.alias || link.name} ({link.linkType})
580
- </TreeItemLayout>
581
-
582
- <Tree>
583
- {link.allAttributes?.enabled && renderAllAttributesNode(link.allAttributes)}
584
- {link.attributes.map((attr) => renderAttributeNode(attr))}
585
- {link.orders.map((order) => renderOrderNode(order))}
586
- {link.filters.map((filter) => renderFilterNode(filter))}
587
- {link.links.map((nestedLink) => renderLinkEntityNode(nestedLink))}
588
- </Tree>
589
- </TreeItem>
590
- );
591
- };
592
-
593
- return (
594
- <div className={styles.container}>
595
- <Tree
596
- aria-label="FetchXML Query Structure"
597
- openItems={openItems}
598
- onOpenChange={handleOpenChange}
599
- >
600
- {renderFetchNode(fetchQuery)}
601
- </Tree>
602
- </div>
603
- );
604
- }