@mohsinonxrm/pptb-fetchxml-studio 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/dist/assets/index-DD9fzkbz.js +232 -0
- package/dist/index.html +70 -0
- package/index.html +0 -1
- package/npm-shrinkwrap.json +6329 -0
- package/package.json +95 -51
- package/CHANGELOG.md +0 -120
- package/eslint.config.js +0 -23
- package/public/vite.svg +0 -1
- package/src/app/AppShell.tsx +0 -1733
- package/src/assets/20px-Microsoft_Office_Excel_(2025/342/200/223present).svg.png +0 -0
- package/src/assets/react.svg +0 -1
- package/src/features/fetchxml/api/dataverseMetadata.ts +0 -192
- package/src/features/fetchxml/api/excelExport.ts +0 -318
- package/src/features/fetchxml/api/formattedValues.ts +0 -107
- package/src/features/fetchxml/api/pptbClient.ts +0 -2635
- package/src/features/fetchxml/model/displaySettings.ts +0 -18
- package/src/features/fetchxml/model/fetchxml.ts +0 -346
- package/src/features/fetchxml/model/fetchxmlIntellisense.ts +0 -583
- package/src/features/fetchxml/model/fetchxmlParser.ts +0 -708
- package/src/features/fetchxml/model/layoutxml.ts +0 -372
- package/src/features/fetchxml/model/nodes.ts +0 -255
- package/src/features/fetchxml/model/operators.ts +0 -624
- package/src/features/fetchxml/model/treeUtils.ts +0 -174
- package/src/features/fetchxml/state/builderStore.tsx +0 -1160
- package/src/features/fetchxml/state/cache.ts +0 -393
- package/src/features/fetchxml/ui/Dialogs/BulkDeleteDialog.tsx +0 -352
- package/src/features/fetchxml/ui/Dialogs/DeleteConfirmDialog.tsx +0 -396
- package/src/features/fetchxml/ui/Dialogs/SaveViewDialog.tsx +0 -612
- package/src/features/fetchxml/ui/Dialogs/SolutionPicker.tsx +0 -114
- package/src/features/fetchxml/ui/Dialogs/WorkflowPickerDialog.tsx +0 -437
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/AttributeEditor.tsx +0 -214
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/ConditionEditor.tsx +0 -533
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/FetchOptionsEditor.tsx +0 -296
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/FilterEditor.tsx +0 -106
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/LinkEntityEditor.tsx +0 -433
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/OrderEditor.tsx +0 -161
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/ValueOfPicker.tsx +0 -194
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel.tsx +0 -225
- package/src/features/fetchxml/ui/LeftPane/TreeView.tsx +0 -604
- package/src/features/fetchxml/ui/RightPane/AddColumnsPanel.tsx +0 -800
- package/src/features/fetchxml/ui/RightPane/ColumnConfigDialog.tsx +0 -170
- package/src/features/fetchxml/ui/RightPane/DataGridCellRenderers.tsx +0 -356
- package/src/features/fetchxml/ui/RightPane/EditColumnsPanel.tsx +0 -372
- package/src/features/fetchxml/ui/RightPane/FetchXmlEditor.tsx +0 -399
- package/src/features/fetchxml/ui/RightPane/FormattedValueUtils.ts +0 -189
- package/src/features/fetchxml/ui/RightPane/LayoutXmlViewer.tsx +0 -171
- package/src/features/fetchxml/ui/RightPane/PreviewTabs.tsx +0 -471
- package/src/features/fetchxml/ui/RightPane/ResultsCommandBar.tsx +0 -430
- package/src/features/fetchxml/ui/RightPane/ResultsGrid.tsx +0 -983
- package/src/features/fetchxml/ui/RightPane/XmlTextArea.tsx +0 -65
- package/src/features/fetchxml/ui/Settings/SettingsDrawer.tsx +0 -175
- package/src/features/fetchxml/ui/Toolbar/EntitySelector.tsx +0 -788
- package/src/features/fetchxml/ui/Toolbar/LoadViewPicker.tsx +0 -274
- package/src/features/fetchxml/ui/Toolbar/SaveViewButton.tsx +0 -174
- package/src/main.tsx +0 -11
- package/src/shared/components/AttributePicker.tsx +0 -138
- package/src/shared/components/BetweenInputPicker.tsx +0 -120
- package/src/shared/components/BooleanValuePicker.tsx +0 -165
- package/src/shared/components/DateTimeInputPicker.tsx +0 -170
- package/src/shared/components/DynamicValueList.tsx +0 -128
- package/src/shared/components/ExcelIcon.tsx +0 -30
- package/src/shared/components/MultiValuePicker.tsx +0 -178
- package/src/shared/components/NumericInputPicker.tsx +0 -156
- package/src/shared/components/OptionSetValuePicker.tsx +0 -150
- package/src/shared/components/RelationshipPicker.tsx +0 -268
- package/src/shared/contexts/ThemeContext.tsx +0 -19
- package/src/shared/hooks/useAccessMode.ts +0 -77
- package/src/shared/hooks/useLazyMetadata.ts +0 -132
- package/src/shared/hooks/usePptbContext.ts +0 -170
- package/src/shared/hooks/usePublisherFilter.ts +0 -214
- package/src/shared/hooks/useSolutionFilter.ts +0 -162
- package/src/shared/utils/debug.ts +0 -87
- package/tsconfig.app.json +0 -28
- package/tsconfig.json +0 -7
- package/tsconfig.node.json +0 -26
- 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
|
-
}
|