@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,433 +0,0 @@
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
- }
@@ -1,161 +0,0 @@
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
- }