@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.
- package/CHANGELOG.md +120 -0
- package/README.md +250 -0
- package/eslint.config.js +23 -0
- package/index.html +71 -0
- package/package.json +53 -0
- package/public/vite.svg +1 -0
- package/src/app/AppShell.tsx +1733 -0
- package/src/assets/20px-Microsoft_Office_Excel_(2025/342/200/223present).svg.png +0 -0
- package/src/assets/react.svg +1 -0
- package/src/features/fetchxml/api/dataverseMetadata.ts +192 -0
- package/src/features/fetchxml/api/excelExport.ts +318 -0
- package/src/features/fetchxml/api/formattedValues.ts +107 -0
- package/src/features/fetchxml/api/pptbClient.ts +2635 -0
- package/src/features/fetchxml/model/displaySettings.ts +18 -0
- package/src/features/fetchxml/model/fetchxml.ts +346 -0
- package/src/features/fetchxml/model/fetchxmlIntellisense.ts +583 -0
- package/src/features/fetchxml/model/fetchxmlParser.ts +708 -0
- package/src/features/fetchxml/model/layoutxml.ts +372 -0
- package/src/features/fetchxml/model/nodes.ts +255 -0
- package/src/features/fetchxml/model/operators.ts +624 -0
- package/src/features/fetchxml/model/treeUtils.ts +174 -0
- package/src/features/fetchxml/state/builderStore.tsx +1160 -0
- package/src/features/fetchxml/state/cache.ts +393 -0
- package/src/features/fetchxml/ui/Dialogs/BulkDeleteDialog.tsx +352 -0
- package/src/features/fetchxml/ui/Dialogs/DeleteConfirmDialog.tsx +396 -0
- package/src/features/fetchxml/ui/Dialogs/SaveViewDialog.tsx +612 -0
- package/src/features/fetchxml/ui/Dialogs/SolutionPicker.tsx +114 -0
- package/src/features/fetchxml/ui/Dialogs/WorkflowPickerDialog.tsx +437 -0
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/AttributeEditor.tsx +214 -0
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/ConditionEditor.tsx +533 -0
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/FetchOptionsEditor.tsx +296 -0
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/FilterEditor.tsx +106 -0
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/LinkEntityEditor.tsx +433 -0
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/OrderEditor.tsx +161 -0
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/ValueOfPicker.tsx +194 -0
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel.tsx +225 -0
- package/src/features/fetchxml/ui/LeftPane/TreeView.tsx +604 -0
- package/src/features/fetchxml/ui/RightPane/AddColumnsPanel.tsx +800 -0
- package/src/features/fetchxml/ui/RightPane/ColumnConfigDialog.tsx +170 -0
- package/src/features/fetchxml/ui/RightPane/DataGridCellRenderers.tsx +356 -0
- package/src/features/fetchxml/ui/RightPane/EditColumnsPanel.tsx +372 -0
- package/src/features/fetchxml/ui/RightPane/FetchXmlEditor.tsx +399 -0
- package/src/features/fetchxml/ui/RightPane/FormattedValueUtils.ts +189 -0
- package/src/features/fetchxml/ui/RightPane/LayoutXmlViewer.tsx +171 -0
- package/src/features/fetchxml/ui/RightPane/PreviewTabs.tsx +471 -0
- package/src/features/fetchxml/ui/RightPane/ResultsCommandBar.tsx +430 -0
- package/src/features/fetchxml/ui/RightPane/ResultsGrid.tsx +983 -0
- package/src/features/fetchxml/ui/RightPane/XmlTextArea.tsx +65 -0
- package/src/features/fetchxml/ui/Settings/SettingsDrawer.tsx +175 -0
- package/src/features/fetchxml/ui/Toolbar/EntitySelector.tsx +788 -0
- package/src/features/fetchxml/ui/Toolbar/LoadViewPicker.tsx +274 -0
- package/src/features/fetchxml/ui/Toolbar/SaveViewButton.tsx +174 -0
- package/src/main.tsx +11 -0
- package/src/shared/components/AttributePicker.tsx +138 -0
- package/src/shared/components/BetweenInputPicker.tsx +120 -0
- package/src/shared/components/BooleanValuePicker.tsx +165 -0
- package/src/shared/components/DateTimeInputPicker.tsx +170 -0
- package/src/shared/components/DynamicValueList.tsx +128 -0
- package/src/shared/components/ExcelIcon.tsx +30 -0
- package/src/shared/components/MultiValuePicker.tsx +178 -0
- package/src/shared/components/NumericInputPicker.tsx +156 -0
- package/src/shared/components/OptionSetValuePicker.tsx +150 -0
- package/src/shared/components/RelationshipPicker.tsx +268 -0
- package/src/shared/contexts/ThemeContext.tsx +19 -0
- package/src/shared/hooks/useAccessMode.ts +77 -0
- package/src/shared/hooks/useLazyMetadata.ts +132 -0
- package/src/shared/hooks/usePptbContext.ts +170 -0
- package/src/shared/hooks/usePublisherFilter.ts +214 -0
- package/src/shared/hooks/useSolutionFilter.ts +162 -0
- package/src/shared/utils/debug.ts +87 -0
- package/tsconfig.app.json +28 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/vite.config.ts +7 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Property editor for Link-Entity nodes
|
|
3
|
+
* Controls joins/relationships to related entities
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useEffect } from "react";
|
|
7
|
+
import {
|
|
8
|
+
Field,
|
|
9
|
+
Input,
|
|
10
|
+
Dropdown,
|
|
11
|
+
Option,
|
|
12
|
+
Checkbox,
|
|
13
|
+
Label,
|
|
14
|
+
Tooltip,
|
|
15
|
+
MessageBar,
|
|
16
|
+
MessageBarBody,
|
|
17
|
+
Dialog,
|
|
18
|
+
DialogSurface,
|
|
19
|
+
DialogTitle,
|
|
20
|
+
DialogContent,
|
|
21
|
+
DialogBody,
|
|
22
|
+
DialogActions,
|
|
23
|
+
Button,
|
|
24
|
+
makeStyles,
|
|
25
|
+
tokens,
|
|
26
|
+
} from "@fluentui/react-components";
|
|
27
|
+
import { Info16Regular } from "@fluentui/react-icons";
|
|
28
|
+
import { debugLog } from "../../../../../../shared/utils/debug";
|
|
29
|
+
import type { LinkEntityNode, LinkType } from "../../../../model/nodes";
|
|
30
|
+
import { RelationshipPicker } from "../../../../../../shared/components/RelationshipPicker";
|
|
31
|
+
|
|
32
|
+
const useStyles = makeStyles({
|
|
33
|
+
container: {
|
|
34
|
+
display: "flex",
|
|
35
|
+
flexDirection: "column",
|
|
36
|
+
gap: tokens.spacingVerticalM,
|
|
37
|
+
padding: tokens.spacingVerticalM,
|
|
38
|
+
},
|
|
39
|
+
section: {
|
|
40
|
+
display: "flex",
|
|
41
|
+
flexDirection: "column",
|
|
42
|
+
gap: tokens.spacingVerticalS,
|
|
43
|
+
},
|
|
44
|
+
fieldWithTooltip: {
|
|
45
|
+
display: "flex",
|
|
46
|
+
alignItems: "center",
|
|
47
|
+
gap: tokens.spacingHorizontalXS,
|
|
48
|
+
},
|
|
49
|
+
tooltipIcon: {
|
|
50
|
+
color: tokens.colorNeutralForeground3,
|
|
51
|
+
cursor: "help",
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const LINK_TYPES: { value: LinkType; label: string; description: string }[] = [
|
|
56
|
+
{
|
|
57
|
+
value: "inner",
|
|
58
|
+
label: "Inner Join",
|
|
59
|
+
description: "Only records with matching related records",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
value: "outer",
|
|
63
|
+
label: "Left Outer Join",
|
|
64
|
+
description: "All parent records, even without related records",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
value: "any",
|
|
68
|
+
label: "Any (EXISTS)",
|
|
69
|
+
description: "At least one related record matches conditions",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
value: "not any",
|
|
73
|
+
label: "Not Any (NOT EXISTS)",
|
|
74
|
+
description: "No related records match conditions",
|
|
75
|
+
},
|
|
76
|
+
{ value: "all", label: "All", description: "All related records match conditions" },
|
|
77
|
+
{ value: "not all", label: "Not All", description: "Not all related records match" },
|
|
78
|
+
{ value: "exists", label: "Exists", description: "Semi-join existence check" },
|
|
79
|
+
{ value: "in", label: "In", description: "Similar to exists" },
|
|
80
|
+
{
|
|
81
|
+
value: "matchfirstrowusingcrossapply",
|
|
82
|
+
label: "Match First Row (CROSS APPLY)",
|
|
83
|
+
description: "Performance: match only first related record",
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
interface LinkEntityEditorProps {
|
|
88
|
+
node: LinkEntityNode;
|
|
89
|
+
parentEntityName: string;
|
|
90
|
+
onUpdate: (updates: Record<string, unknown>) => void;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function LinkEntityEditor({ node, parentEntityName, onUpdate }: LinkEntityEditorProps) {
|
|
94
|
+
const styles = useStyles();
|
|
95
|
+
|
|
96
|
+
// Dialog state for confirming link-type change
|
|
97
|
+
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
|
98
|
+
const [pendingLinkType, setPendingLinkType] = useState<LinkType | null>(null);
|
|
99
|
+
|
|
100
|
+
// Determine if we should show the relationship picker
|
|
101
|
+
// Show it if the node has a placeholder name or hasn't been properly configured
|
|
102
|
+
const shouldShowPicker = node.name === "new_entity" || (!node.name && !node.from && !node.to);
|
|
103
|
+
const [useRelationshipPicker, setUseRelationshipPicker] = useState(shouldShowPicker);
|
|
104
|
+
|
|
105
|
+
// Reset picker state when switching to a different node or when node properties change
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
debugLog("linkEntityEditor", "Node changed or props updated:", {
|
|
108
|
+
nodeId: node.id,
|
|
109
|
+
nodeName: node.name,
|
|
110
|
+
from: node.from,
|
|
111
|
+
to: node.to,
|
|
112
|
+
parentEntityName,
|
|
113
|
+
shouldShowPicker,
|
|
114
|
+
});
|
|
115
|
+
setUseRelationshipPicker(shouldShowPicker);
|
|
116
|
+
}, [node.id, node.name, node.from, node.to, parentEntityName, shouldShowPicker]);
|
|
117
|
+
|
|
118
|
+
const handleTextChange = (field: string) => (_: unknown, data: { value: string }) => {
|
|
119
|
+
onUpdate({ [field]: data.value || undefined });
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Special handler for link-type changes that may require confirmation
|
|
123
|
+
const handleLinkTypeChange = (_: unknown, data: { optionValue?: string }) => {
|
|
124
|
+
const newLinkType = data.optionValue as LinkType | undefined;
|
|
125
|
+
if (!newLinkType) return;
|
|
126
|
+
|
|
127
|
+
// Check if switching to "not any" or "not all" and there are children that would need to be removed
|
|
128
|
+
const isRestrictiveLinkType = newLinkType === "not any" || newLinkType === "not all";
|
|
129
|
+
const hasChildren =
|
|
130
|
+
node.attributes.length > 0 ||
|
|
131
|
+
node.filters.length > 0 ||
|
|
132
|
+
node.links.length > 0 ||
|
|
133
|
+
node.allAttributes?.enabled;
|
|
134
|
+
|
|
135
|
+
if (isRestrictiveLinkType && hasChildren) {
|
|
136
|
+
// Show confirmation dialog
|
|
137
|
+
setPendingLinkType(newLinkType);
|
|
138
|
+
setShowConfirmDialog(true);
|
|
139
|
+
} else {
|
|
140
|
+
// Safe to change directly
|
|
141
|
+
onUpdate({ linkType: newLinkType });
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Handle dialog confirmation - proceed with change and remove children
|
|
146
|
+
const handleConfirmLinkTypeChange = () => {
|
|
147
|
+
if (pendingLinkType) {
|
|
148
|
+
// Update link type and clear children
|
|
149
|
+
onUpdate({
|
|
150
|
+
linkType: pendingLinkType,
|
|
151
|
+
attributes: [],
|
|
152
|
+
allAttributes: undefined, // Remove all-attributes completely
|
|
153
|
+
filters: [],
|
|
154
|
+
links: [],
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
setShowConfirmDialog(false);
|
|
158
|
+
setPendingLinkType(null);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Handle dialog cancellation - revert to current link type
|
|
162
|
+
const handleCancelLinkTypeChange = () => {
|
|
163
|
+
setShowConfirmDialog(false);
|
|
164
|
+
setPendingLinkType(null);
|
|
165
|
+
// No need to update anything - just close the dialog
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const handleCheckboxChange =
|
|
169
|
+
(field: string) => (_: unknown, data: { checked: boolean | "mixed" }) => {
|
|
170
|
+
onUpdate({ [field]: data.checked === true ? true : undefined });
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const handleRelationshipSelect = (relationship: {
|
|
174
|
+
schemaName: string;
|
|
175
|
+
referencedEntity: string;
|
|
176
|
+
referencingEntity: string;
|
|
177
|
+
referencedAttribute: string;
|
|
178
|
+
referencingAttribute: string;
|
|
179
|
+
type: "1N" | "N1" | "NN";
|
|
180
|
+
}) => {
|
|
181
|
+
debugLog("linkEntityEditor", "Relationship selected:", {
|
|
182
|
+
nodeId: node.id,
|
|
183
|
+
parentEntityName,
|
|
184
|
+
relationship,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Auto-populate from/to based on relationship type and direction
|
|
188
|
+
const updates: Record<string, unknown> = {
|
|
189
|
+
name: "",
|
|
190
|
+
from: "",
|
|
191
|
+
to: "",
|
|
192
|
+
relationshipType: relationship.type,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// Determine which entity is the related/target entity
|
|
196
|
+
// For 1:N - parent is ReferencedEntity, child is ReferencingEntity
|
|
197
|
+
// For N:1 - parent is ReferencedEntity, child is ReferencingEntity
|
|
198
|
+
// For N:N - both are referenced in the relationship
|
|
199
|
+
|
|
200
|
+
if (relationship.type === "1N") {
|
|
201
|
+
// Parent (current) → Child (related)
|
|
202
|
+
// from = child's FK attribute, to = parent's PK attribute
|
|
203
|
+
if (relationship.referencedEntity === parentEntityName) {
|
|
204
|
+
// We're the referenced (parent), linking to referencing (child)
|
|
205
|
+
updates.name = relationship.referencingEntity;
|
|
206
|
+
updates.from = relationship.referencingAttribute;
|
|
207
|
+
updates.to = relationship.referencedAttribute;
|
|
208
|
+
} else {
|
|
209
|
+
// Reverse direction
|
|
210
|
+
updates.name = relationship.referencedEntity;
|
|
211
|
+
updates.from = relationship.referencedAttribute;
|
|
212
|
+
updates.to = relationship.referencingAttribute;
|
|
213
|
+
}
|
|
214
|
+
} else if (relationship.type === "N1") {
|
|
215
|
+
// Child (current) → Parent (related)
|
|
216
|
+
// from = parent's PK attribute, to = child's FK attribute
|
|
217
|
+
if (relationship.referencingEntity === parentEntityName) {
|
|
218
|
+
// We're the referencing (child), linking to referenced (parent)
|
|
219
|
+
updates.name = relationship.referencedEntity;
|
|
220
|
+
updates.from = relationship.referencedAttribute;
|
|
221
|
+
updates.to = relationship.referencingAttribute;
|
|
222
|
+
} else {
|
|
223
|
+
// Reverse direction
|
|
224
|
+
updates.name = relationship.referencingEntity;
|
|
225
|
+
updates.from = relationship.referencingAttribute;
|
|
226
|
+
updates.to = relationship.referencedAttribute;
|
|
227
|
+
}
|
|
228
|
+
} else if (relationship.type === "NN") {
|
|
229
|
+
// Many-to-Many
|
|
230
|
+
// Determine target entity (the one that's not parentEntityName)
|
|
231
|
+
const targetEntity =
|
|
232
|
+
relationship.referencedEntity === parentEntityName
|
|
233
|
+
? relationship.referencingEntity
|
|
234
|
+
: relationship.referencedEntity;
|
|
235
|
+
|
|
236
|
+
updates.name = targetEntity;
|
|
237
|
+
updates.from = relationship.referencingAttribute;
|
|
238
|
+
updates.to = relationship.referencedAttribute;
|
|
239
|
+
updates.intersect = true;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
debugLog("linkEntityEditor", "Applying updates:", updates);
|
|
243
|
+
onUpdate(updates);
|
|
244
|
+
setUseRelationshipPicker(false); // Switch to manual mode after selection
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<div className={styles.container}>
|
|
249
|
+
{/* Relationship Picker Section */}
|
|
250
|
+
{useRelationshipPicker && parentEntityName && (
|
|
251
|
+
<div className={styles.section}>
|
|
252
|
+
<Label weight="semibold">Select Relationship</Label>
|
|
253
|
+
<Field
|
|
254
|
+
label="Choose a relationship to auto-populate link details"
|
|
255
|
+
hint="Or skip this and manually configure the link below"
|
|
256
|
+
>
|
|
257
|
+
<RelationshipPicker
|
|
258
|
+
entityLogicalName={parentEntityName}
|
|
259
|
+
onChange={handleRelationshipSelect}
|
|
260
|
+
placeholder="Select a relationship..."
|
|
261
|
+
/>
|
|
262
|
+
</Field>
|
|
263
|
+
</div>
|
|
264
|
+
)}
|
|
265
|
+
|
|
266
|
+
{/* Basic Properties */}
|
|
267
|
+
<div className={styles.section}>
|
|
268
|
+
<Label weight="semibold">Link-Entity Properties</Label>
|
|
269
|
+
|
|
270
|
+
<Field label="Related Entity Name" required>
|
|
271
|
+
<div className={styles.fieldWithTooltip}>
|
|
272
|
+
<Input
|
|
273
|
+
value={node.name}
|
|
274
|
+
onChange={handleTextChange("name")}
|
|
275
|
+
placeholder="e.g., contact, account, systemuser"
|
|
276
|
+
/>
|
|
277
|
+
<Tooltip
|
|
278
|
+
content="Logical name of the related entity to join."
|
|
279
|
+
relationship="description"
|
|
280
|
+
>
|
|
281
|
+
<Info16Regular className={styles.tooltipIcon} />
|
|
282
|
+
</Tooltip>
|
|
283
|
+
</div>
|
|
284
|
+
</Field>
|
|
285
|
+
|
|
286
|
+
<Field label="From Attribute" required>
|
|
287
|
+
<div className={styles.fieldWithTooltip}>
|
|
288
|
+
<Input
|
|
289
|
+
value={node.from}
|
|
290
|
+
onChange={handleTextChange("from")}
|
|
291
|
+
placeholder="e.g., contactid, accountid"
|
|
292
|
+
/>
|
|
293
|
+
<Tooltip
|
|
294
|
+
content="Attribute on the related entity (the entity being joined)."
|
|
295
|
+
relationship="description"
|
|
296
|
+
>
|
|
297
|
+
<Info16Regular className={styles.tooltipIcon} />
|
|
298
|
+
</Tooltip>
|
|
299
|
+
</div>
|
|
300
|
+
</Field>
|
|
301
|
+
|
|
302
|
+
<Field label="To Attribute" required>
|
|
303
|
+
<div className={styles.fieldWithTooltip}>
|
|
304
|
+
<Input
|
|
305
|
+
value={node.to}
|
|
306
|
+
onChange={handleTextChange("to")}
|
|
307
|
+
placeholder="e.g., parentcustomerid, createdby"
|
|
308
|
+
/>
|
|
309
|
+
<Tooltip
|
|
310
|
+
content="Attribute on the parent entity (the entity being joined from)."
|
|
311
|
+
relationship="description"
|
|
312
|
+
>
|
|
313
|
+
<Info16Regular className={styles.tooltipIcon} />
|
|
314
|
+
</Tooltip>
|
|
315
|
+
</div>
|
|
316
|
+
</Field>
|
|
317
|
+
|
|
318
|
+
<Field label="Join Type" required>
|
|
319
|
+
<div className={styles.fieldWithTooltip}>
|
|
320
|
+
<Dropdown
|
|
321
|
+
value={node.linkType}
|
|
322
|
+
selectedOptions={[node.linkType]}
|
|
323
|
+
onOptionSelect={handleLinkTypeChange}
|
|
324
|
+
>
|
|
325
|
+
{LINK_TYPES.map((type) => (
|
|
326
|
+
<Option key={type.value} value={type.value} text={type.description}>
|
|
327
|
+
{type.label}
|
|
328
|
+
</Option>
|
|
329
|
+
))}
|
|
330
|
+
</Dropdown>
|
|
331
|
+
<Tooltip
|
|
332
|
+
content="Type of join. Inner = matching only. Outer = include parent records without match. EXISTS variations for existence checks."
|
|
333
|
+
relationship="description"
|
|
334
|
+
>
|
|
335
|
+
<Info16Regular className={styles.tooltipIcon} />
|
|
336
|
+
</Tooltip>
|
|
337
|
+
</div>
|
|
338
|
+
</Field>
|
|
339
|
+
|
|
340
|
+
{/* Warning for "not any" and "not all" link types */}
|
|
341
|
+
{(node.linkType === "not any" || node.linkType === "not all") && (
|
|
342
|
+
<MessageBar intent="warning">
|
|
343
|
+
<MessageBarBody>
|
|
344
|
+
<strong>Note:</strong> The "{node.linkType}" link type does not support attributes.
|
|
345
|
+
Attributes added to this link-entity will cause query execution errors.
|
|
346
|
+
</MessageBarBody>
|
|
347
|
+
</MessageBar>
|
|
348
|
+
)}
|
|
349
|
+
</div>
|
|
350
|
+
|
|
351
|
+
{/* Optional Properties */}
|
|
352
|
+
<div className={styles.section}>
|
|
353
|
+
<Label weight="semibold">Optional Properties</Label>
|
|
354
|
+
|
|
355
|
+
<Field label="Alias">
|
|
356
|
+
<div className={styles.fieldWithTooltip}>
|
|
357
|
+
<Input
|
|
358
|
+
value={node.alias ?? ""}
|
|
359
|
+
onChange={handleTextChange("alias")}
|
|
360
|
+
placeholder="e.g., parent_account, created_by_user"
|
|
361
|
+
/>
|
|
362
|
+
<Tooltip
|
|
363
|
+
content="Alias for referencing this link in conditions, order-by, or nested links."
|
|
364
|
+
relationship="description"
|
|
365
|
+
>
|
|
366
|
+
<Info16Regular className={styles.tooltipIcon} />
|
|
367
|
+
</Tooltip>
|
|
368
|
+
</div>
|
|
369
|
+
</Field>
|
|
370
|
+
|
|
371
|
+
<Field>
|
|
372
|
+
<div className={styles.fieldWithTooltip}>
|
|
373
|
+
<Checkbox
|
|
374
|
+
checked={node.intersect ?? false}
|
|
375
|
+
onChange={handleCheckboxChange("intersect")}
|
|
376
|
+
label="Intersect (Many-to-Many)"
|
|
377
|
+
/>
|
|
378
|
+
<Tooltip
|
|
379
|
+
content="Use intersect entity for N:N relationships. Dataverse automatically handles the intersect table."
|
|
380
|
+
relationship="description"
|
|
381
|
+
>
|
|
382
|
+
<Info16Regular className={styles.tooltipIcon} />
|
|
383
|
+
</Tooltip>
|
|
384
|
+
</div>
|
|
385
|
+
</Field>
|
|
386
|
+
|
|
387
|
+
<Field>
|
|
388
|
+
<div className={styles.fieldWithTooltip}>
|
|
389
|
+
<Checkbox
|
|
390
|
+
checked={node.visible ?? true}
|
|
391
|
+
onChange={handleCheckboxChange("visible")}
|
|
392
|
+
label="Visible in Results"
|
|
393
|
+
/>
|
|
394
|
+
<Tooltip
|
|
395
|
+
content="Include linked entity data in query results. Set to false to use link only for filtering."
|
|
396
|
+
relationship="description"
|
|
397
|
+
>
|
|
398
|
+
<Info16Regular className={styles.tooltipIcon} />
|
|
399
|
+
</Tooltip>
|
|
400
|
+
</div>
|
|
401
|
+
</Field>
|
|
402
|
+
</div>
|
|
403
|
+
|
|
404
|
+
{/* Confirmation Dialog for Link Type Change */}
|
|
405
|
+
<Dialog
|
|
406
|
+
open={showConfirmDialog}
|
|
407
|
+
onOpenChange={(_, data) => !data.open && handleCancelLinkTypeChange()}
|
|
408
|
+
>
|
|
409
|
+
<DialogSurface>
|
|
410
|
+
<DialogBody>
|
|
411
|
+
<DialogTitle>Confirm Link Type Change</DialogTitle>
|
|
412
|
+
<DialogContent>
|
|
413
|
+
<p>
|
|
414
|
+
Changing the link type to <strong>"{pendingLinkType}"</strong> will remove all
|
|
415
|
+
attributes, filters, and nested link-entities from this link-entity because these
|
|
416
|
+
link types do not support child elements.
|
|
417
|
+
</p>
|
|
418
|
+
<p>This action cannot be undone. Do you want to proceed?</p>
|
|
419
|
+
</DialogContent>
|
|
420
|
+
<DialogActions>
|
|
421
|
+
<Button appearance="primary" onClick={handleConfirmLinkTypeChange}>
|
|
422
|
+
Yes, Change Link Type
|
|
423
|
+
</Button>
|
|
424
|
+
<Button appearance="secondary" onClick={handleCancelLinkTypeChange}>
|
|
425
|
+
Cancel
|
|
426
|
+
</Button>
|
|
427
|
+
</DialogActions>
|
|
428
|
+
</DialogBody>
|
|
429
|
+
</DialogSurface>
|
|
430
|
+
</Dialog>
|
|
431
|
+
</div>
|
|
432
|
+
);
|
|
433
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Property editor for Order nodes
|
|
3
|
+
* Controls sorting/ordering of query results
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
Field,
|
|
8
|
+
Dropdown,
|
|
9
|
+
Option,
|
|
10
|
+
Radio,
|
|
11
|
+
RadioGroup,
|
|
12
|
+
Label,
|
|
13
|
+
Tooltip,
|
|
14
|
+
makeStyles,
|
|
15
|
+
tokens,
|
|
16
|
+
} from "@fluentui/react-components";
|
|
17
|
+
import { Info16Regular } from "@fluentui/react-icons";
|
|
18
|
+
import type { OrderNode, FetchNode } from "../../../../model/nodes";
|
|
19
|
+
import { AttributePicker } from "../../../../../../shared/components/AttributePicker";
|
|
20
|
+
import { collectLinkEntityReferences } from "../../../../model/treeUtils";
|
|
21
|
+
import { useMemo } from "react";
|
|
22
|
+
|
|
23
|
+
const useStyles = makeStyles({
|
|
24
|
+
container: {
|
|
25
|
+
display: "flex",
|
|
26
|
+
flexDirection: "column",
|
|
27
|
+
gap: tokens.spacingVerticalM,
|
|
28
|
+
padding: tokens.spacingVerticalM,
|
|
29
|
+
},
|
|
30
|
+
section: {
|
|
31
|
+
display: "flex",
|
|
32
|
+
flexDirection: "column",
|
|
33
|
+
gap: tokens.spacingVerticalS,
|
|
34
|
+
},
|
|
35
|
+
fieldWithTooltip: {
|
|
36
|
+
display: "flex",
|
|
37
|
+
alignItems: "center",
|
|
38
|
+
gap: tokens.spacingHorizontalXS,
|
|
39
|
+
},
|
|
40
|
+
tooltipIcon: {
|
|
41
|
+
color: tokens.colorNeutralForeground3,
|
|
42
|
+
cursor: "help",
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
interface OrderEditorProps {
|
|
47
|
+
node: OrderNode;
|
|
48
|
+
entityName: string;
|
|
49
|
+
fetchQuery: FetchNode | null;
|
|
50
|
+
onUpdate: (updates: Record<string, unknown>) => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function OrderEditor({ node, entityName, fetchQuery, onUpdate }: OrderEditorProps) {
|
|
54
|
+
const styles = useStyles();
|
|
55
|
+
|
|
56
|
+
// Collect all link-entity references for the entity dropdown
|
|
57
|
+
const linkEntityOptions = useMemo(() => {
|
|
58
|
+
const refs = collectLinkEntityReferences(fetchQuery);
|
|
59
|
+
return refs;
|
|
60
|
+
}, [fetchQuery]);
|
|
61
|
+
|
|
62
|
+
// Determine which entity's attributes to show
|
|
63
|
+
// If entityname is set, find the matching link-entity's logical name
|
|
64
|
+
const attributeEntityName = useMemo(() => {
|
|
65
|
+
if (!node.entityname) {
|
|
66
|
+
// No entityname = root entity
|
|
67
|
+
return entityName;
|
|
68
|
+
}
|
|
69
|
+
// Find the link-entity with matching alias/identifier
|
|
70
|
+
const linkRef = linkEntityOptions.find(
|
|
71
|
+
(ref) => ref.identifier === node.entityname || ref.alias === node.entityname
|
|
72
|
+
);
|
|
73
|
+
return linkRef?.entityName ?? entityName;
|
|
74
|
+
}, [node.entityname, entityName, linkEntityOptions]);
|
|
75
|
+
|
|
76
|
+
const handleRadioChange = (_: unknown, data: { value: string }) => {
|
|
77
|
+
onUpdate({ descending: data.value === "desc" });
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const handleEntityChange = (_: unknown, data: { optionValue?: string }) => {
|
|
81
|
+
const value = data.optionValue;
|
|
82
|
+
if (value === "__root__") {
|
|
83
|
+
// Root entity selected - clear entityname and attribute
|
|
84
|
+
onUpdate({ entityname: undefined, attribute: "" });
|
|
85
|
+
} else {
|
|
86
|
+
// Link-entity selected - set entityname and clear attribute
|
|
87
|
+
onUpdate({ entityname: value || undefined, attribute: "" });
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Get selected entity option value
|
|
92
|
+
const selectedEntityValue = node.entityname ?? "__root__";
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div className={styles.container}>
|
|
96
|
+
{/* Basic Properties */}
|
|
97
|
+
<div className={styles.section}>
|
|
98
|
+
<Label weight="semibold">Order By Properties</Label>
|
|
99
|
+
|
|
100
|
+
<Field label="Entity" hint="Select root entity or a link-entity to sort by">
|
|
101
|
+
<div className={styles.fieldWithTooltip}>
|
|
102
|
+
<Dropdown
|
|
103
|
+
value={
|
|
104
|
+
selectedEntityValue === "__root__"
|
|
105
|
+
? `Root: ${entityName}`
|
|
106
|
+
: linkEntityOptions.find((o) => o.identifier === selectedEntityValue)
|
|
107
|
+
?.displayLabel ?? selectedEntityValue
|
|
108
|
+
}
|
|
109
|
+
selectedOptions={[selectedEntityValue]}
|
|
110
|
+
onOptionSelect={handleEntityChange}
|
|
111
|
+
>
|
|
112
|
+
<Option text={`Root: ${entityName}`} value="__root__">
|
|
113
|
+
Root: {entityName}
|
|
114
|
+
</Option>
|
|
115
|
+
{linkEntityOptions.map((ref) => (
|
|
116
|
+
<Option key={ref.identifier} text={ref.displayLabel} value={ref.identifier}>
|
|
117
|
+
{ref.displayLabel}
|
|
118
|
+
</Option>
|
|
119
|
+
))}
|
|
120
|
+
</Dropdown>
|
|
121
|
+
<Tooltip
|
|
122
|
+
content="Select the root entity or a link-entity to sort by its attributes."
|
|
123
|
+
relationship="description"
|
|
124
|
+
>
|
|
125
|
+
<Info16Regular className={styles.tooltipIcon} />
|
|
126
|
+
</Tooltip>
|
|
127
|
+
</div>
|
|
128
|
+
</Field>
|
|
129
|
+
|
|
130
|
+
<Field label="Attribute Name" required>
|
|
131
|
+
<div className={styles.fieldWithTooltip}>
|
|
132
|
+
<AttributePicker
|
|
133
|
+
entityLogicalName={attributeEntityName}
|
|
134
|
+
value={node.attribute}
|
|
135
|
+
onChange={(logicalName) => onUpdate({ attribute: logicalName })}
|
|
136
|
+
placeholder="Select or type attribute name"
|
|
137
|
+
/>
|
|
138
|
+
<Tooltip content="Logical name of the attribute to sort by." relationship="description">
|
|
139
|
+
<Info16Regular className={styles.tooltipIcon} />
|
|
140
|
+
</Tooltip>
|
|
141
|
+
</div>
|
|
142
|
+
</Field>
|
|
143
|
+
|
|
144
|
+
<Field label="Sort Direction">
|
|
145
|
+
<div className={styles.fieldWithTooltip}>
|
|
146
|
+
<RadioGroup value={node.descending ? "desc" : "asc"} onChange={handleRadioChange}>
|
|
147
|
+
<Radio value="asc" label="Ascending (A-Z, 0-9, oldest first)" />
|
|
148
|
+
<Radio value="desc" label="Descending (Z-A, 9-0, newest first)" />
|
|
149
|
+
</RadioGroup>
|
|
150
|
+
<Tooltip
|
|
151
|
+
content="Sort order: ascending (default) or descending. Multiple order nodes are applied in sequence."
|
|
152
|
+
relationship="description"
|
|
153
|
+
>
|
|
154
|
+
<Info16Regular className={styles.tooltipIcon} />
|
|
155
|
+
</Tooltip>
|
|
156
|
+
</div>
|
|
157
|
+
</Field>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
}
|