@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.
Files changed (77) hide show
  1. package/LICENSE +661 -0
  2. package/dist/assets/index-DD9fzkbz.js +232 -0
  3. package/dist/index.html +70 -0
  4. package/index.html +0 -1
  5. package/npm-shrinkwrap.json +6329 -0
  6. package/package.json +95 -51
  7. package/CHANGELOG.md +0 -120
  8. package/eslint.config.js +0 -23
  9. package/public/vite.svg +0 -1
  10. package/src/app/AppShell.tsx +0 -1733
  11. package/src/assets/20px-Microsoft_Office_Excel_(2025/342/200/223present).svg.png +0 -0
  12. package/src/assets/react.svg +0 -1
  13. package/src/features/fetchxml/api/dataverseMetadata.ts +0 -192
  14. package/src/features/fetchxml/api/excelExport.ts +0 -318
  15. package/src/features/fetchxml/api/formattedValues.ts +0 -107
  16. package/src/features/fetchxml/api/pptbClient.ts +0 -2635
  17. package/src/features/fetchxml/model/displaySettings.ts +0 -18
  18. package/src/features/fetchxml/model/fetchxml.ts +0 -346
  19. package/src/features/fetchxml/model/fetchxmlIntellisense.ts +0 -583
  20. package/src/features/fetchxml/model/fetchxmlParser.ts +0 -708
  21. package/src/features/fetchxml/model/layoutxml.ts +0 -372
  22. package/src/features/fetchxml/model/nodes.ts +0 -255
  23. package/src/features/fetchxml/model/operators.ts +0 -624
  24. package/src/features/fetchxml/model/treeUtils.ts +0 -174
  25. package/src/features/fetchxml/state/builderStore.tsx +0 -1160
  26. package/src/features/fetchxml/state/cache.ts +0 -393
  27. package/src/features/fetchxml/ui/Dialogs/BulkDeleteDialog.tsx +0 -352
  28. package/src/features/fetchxml/ui/Dialogs/DeleteConfirmDialog.tsx +0 -396
  29. package/src/features/fetchxml/ui/Dialogs/SaveViewDialog.tsx +0 -612
  30. package/src/features/fetchxml/ui/Dialogs/SolutionPicker.tsx +0 -114
  31. package/src/features/fetchxml/ui/Dialogs/WorkflowPickerDialog.tsx +0 -437
  32. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/AttributeEditor.tsx +0 -214
  33. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/ConditionEditor.tsx +0 -533
  34. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/FetchOptionsEditor.tsx +0 -296
  35. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/FilterEditor.tsx +0 -106
  36. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/LinkEntityEditor.tsx +0 -433
  37. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/OrderEditor.tsx +0 -161
  38. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/ValueOfPicker.tsx +0 -194
  39. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel.tsx +0 -225
  40. package/src/features/fetchxml/ui/LeftPane/TreeView.tsx +0 -604
  41. package/src/features/fetchxml/ui/RightPane/AddColumnsPanel.tsx +0 -800
  42. package/src/features/fetchxml/ui/RightPane/ColumnConfigDialog.tsx +0 -170
  43. package/src/features/fetchxml/ui/RightPane/DataGridCellRenderers.tsx +0 -356
  44. package/src/features/fetchxml/ui/RightPane/EditColumnsPanel.tsx +0 -372
  45. package/src/features/fetchxml/ui/RightPane/FetchXmlEditor.tsx +0 -399
  46. package/src/features/fetchxml/ui/RightPane/FormattedValueUtils.ts +0 -189
  47. package/src/features/fetchxml/ui/RightPane/LayoutXmlViewer.tsx +0 -171
  48. package/src/features/fetchxml/ui/RightPane/PreviewTabs.tsx +0 -471
  49. package/src/features/fetchxml/ui/RightPane/ResultsCommandBar.tsx +0 -430
  50. package/src/features/fetchxml/ui/RightPane/ResultsGrid.tsx +0 -983
  51. package/src/features/fetchxml/ui/RightPane/XmlTextArea.tsx +0 -65
  52. package/src/features/fetchxml/ui/Settings/SettingsDrawer.tsx +0 -175
  53. package/src/features/fetchxml/ui/Toolbar/EntitySelector.tsx +0 -788
  54. package/src/features/fetchxml/ui/Toolbar/LoadViewPicker.tsx +0 -274
  55. package/src/features/fetchxml/ui/Toolbar/SaveViewButton.tsx +0 -174
  56. package/src/main.tsx +0 -11
  57. package/src/shared/components/AttributePicker.tsx +0 -138
  58. package/src/shared/components/BetweenInputPicker.tsx +0 -120
  59. package/src/shared/components/BooleanValuePicker.tsx +0 -165
  60. package/src/shared/components/DateTimeInputPicker.tsx +0 -170
  61. package/src/shared/components/DynamicValueList.tsx +0 -128
  62. package/src/shared/components/ExcelIcon.tsx +0 -30
  63. package/src/shared/components/MultiValuePicker.tsx +0 -178
  64. package/src/shared/components/NumericInputPicker.tsx +0 -156
  65. package/src/shared/components/OptionSetValuePicker.tsx +0 -150
  66. package/src/shared/components/RelationshipPicker.tsx +0 -268
  67. package/src/shared/contexts/ThemeContext.tsx +0 -19
  68. package/src/shared/hooks/useAccessMode.ts +0 -77
  69. package/src/shared/hooks/useLazyMetadata.ts +0 -132
  70. package/src/shared/hooks/usePptbContext.ts +0 -170
  71. package/src/shared/hooks/usePublisherFilter.ts +0 -214
  72. package/src/shared/hooks/useSolutionFilter.ts +0 -162
  73. package/src/shared/utils/debug.ts +0 -87
  74. package/tsconfig.app.json +0 -28
  75. package/tsconfig.json +0 -7
  76. package/tsconfig.node.json +0 -26
  77. 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
- }