@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.
Files changed (74) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/README.md +250 -0
  3. package/eslint.config.js +23 -0
  4. package/index.html +71 -0
  5. package/package.json +53 -0
  6. package/public/vite.svg +1 -0
  7. package/src/app/AppShell.tsx +1733 -0
  8. package/src/assets/20px-Microsoft_Office_Excel_(2025/342/200/223present).svg.png +0 -0
  9. package/src/assets/react.svg +1 -0
  10. package/src/features/fetchxml/api/dataverseMetadata.ts +192 -0
  11. package/src/features/fetchxml/api/excelExport.ts +318 -0
  12. package/src/features/fetchxml/api/formattedValues.ts +107 -0
  13. package/src/features/fetchxml/api/pptbClient.ts +2635 -0
  14. package/src/features/fetchxml/model/displaySettings.ts +18 -0
  15. package/src/features/fetchxml/model/fetchxml.ts +346 -0
  16. package/src/features/fetchxml/model/fetchxmlIntellisense.ts +583 -0
  17. package/src/features/fetchxml/model/fetchxmlParser.ts +708 -0
  18. package/src/features/fetchxml/model/layoutxml.ts +372 -0
  19. package/src/features/fetchxml/model/nodes.ts +255 -0
  20. package/src/features/fetchxml/model/operators.ts +624 -0
  21. package/src/features/fetchxml/model/treeUtils.ts +174 -0
  22. package/src/features/fetchxml/state/builderStore.tsx +1160 -0
  23. package/src/features/fetchxml/state/cache.ts +393 -0
  24. package/src/features/fetchxml/ui/Dialogs/BulkDeleteDialog.tsx +352 -0
  25. package/src/features/fetchxml/ui/Dialogs/DeleteConfirmDialog.tsx +396 -0
  26. package/src/features/fetchxml/ui/Dialogs/SaveViewDialog.tsx +612 -0
  27. package/src/features/fetchxml/ui/Dialogs/SolutionPicker.tsx +114 -0
  28. package/src/features/fetchxml/ui/Dialogs/WorkflowPickerDialog.tsx +437 -0
  29. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/AttributeEditor.tsx +214 -0
  30. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/ConditionEditor.tsx +533 -0
  31. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/FetchOptionsEditor.tsx +296 -0
  32. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/FilterEditor.tsx +106 -0
  33. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/LinkEntityEditor.tsx +433 -0
  34. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/OrderEditor.tsx +161 -0
  35. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/ValueOfPicker.tsx +194 -0
  36. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel.tsx +225 -0
  37. package/src/features/fetchxml/ui/LeftPane/TreeView.tsx +604 -0
  38. package/src/features/fetchxml/ui/RightPane/AddColumnsPanel.tsx +800 -0
  39. package/src/features/fetchxml/ui/RightPane/ColumnConfigDialog.tsx +170 -0
  40. package/src/features/fetchxml/ui/RightPane/DataGridCellRenderers.tsx +356 -0
  41. package/src/features/fetchxml/ui/RightPane/EditColumnsPanel.tsx +372 -0
  42. package/src/features/fetchxml/ui/RightPane/FetchXmlEditor.tsx +399 -0
  43. package/src/features/fetchxml/ui/RightPane/FormattedValueUtils.ts +189 -0
  44. package/src/features/fetchxml/ui/RightPane/LayoutXmlViewer.tsx +171 -0
  45. package/src/features/fetchxml/ui/RightPane/PreviewTabs.tsx +471 -0
  46. package/src/features/fetchxml/ui/RightPane/ResultsCommandBar.tsx +430 -0
  47. package/src/features/fetchxml/ui/RightPane/ResultsGrid.tsx +983 -0
  48. package/src/features/fetchxml/ui/RightPane/XmlTextArea.tsx +65 -0
  49. package/src/features/fetchxml/ui/Settings/SettingsDrawer.tsx +175 -0
  50. package/src/features/fetchxml/ui/Toolbar/EntitySelector.tsx +788 -0
  51. package/src/features/fetchxml/ui/Toolbar/LoadViewPicker.tsx +274 -0
  52. package/src/features/fetchxml/ui/Toolbar/SaveViewButton.tsx +174 -0
  53. package/src/main.tsx +11 -0
  54. package/src/shared/components/AttributePicker.tsx +138 -0
  55. package/src/shared/components/BetweenInputPicker.tsx +120 -0
  56. package/src/shared/components/BooleanValuePicker.tsx +165 -0
  57. package/src/shared/components/DateTimeInputPicker.tsx +170 -0
  58. package/src/shared/components/DynamicValueList.tsx +128 -0
  59. package/src/shared/components/ExcelIcon.tsx +30 -0
  60. package/src/shared/components/MultiValuePicker.tsx +178 -0
  61. package/src/shared/components/NumericInputPicker.tsx +156 -0
  62. package/src/shared/components/OptionSetValuePicker.tsx +150 -0
  63. package/src/shared/components/RelationshipPicker.tsx +268 -0
  64. package/src/shared/contexts/ThemeContext.tsx +19 -0
  65. package/src/shared/hooks/useAccessMode.ts +77 -0
  66. package/src/shared/hooks/useLazyMetadata.ts +132 -0
  67. package/src/shared/hooks/usePptbContext.ts +170 -0
  68. package/src/shared/hooks/usePublisherFilter.ts +214 -0
  69. package/src/shared/hooks/useSolutionFilter.ts +162 -0
  70. package/src/shared/utils/debug.ts +87 -0
  71. package/tsconfig.app.json +28 -0
  72. package/tsconfig.json +7 -0
  73. package/tsconfig.node.json +26 -0
  74. package/vite.config.ts +7 -0
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Column Configuration Dialog
3
+ * Allows users to reorder columns by moving them up/down
4
+ */
5
+
6
+ import { useState, useCallback, useEffect } from "react";
7
+ import {
8
+ Dialog,
9
+ DialogSurface,
10
+ DialogTitle,
11
+ DialogBody,
12
+ DialogContent,
13
+ DialogActions,
14
+ Button,
15
+ makeStyles,
16
+ tokens,
17
+ } from "@fluentui/react-components";
18
+ import { ArrowUp24Regular, ArrowDown24Regular } from "@fluentui/react-icons";
19
+ import type { LayoutColumn } from "../../model/layoutxml";
20
+
21
+ const useStyles = makeStyles({
22
+ columnList: {
23
+ display: "flex",
24
+ flexDirection: "column",
25
+ gap: tokens.spacingVerticalXS,
26
+ maxHeight: "400px",
27
+ overflowY: "auto",
28
+ padding: tokens.spacingVerticalS,
29
+ border: `1px solid ${tokens.colorNeutralStroke1}`,
30
+ borderRadius: tokens.borderRadiusMedium,
31
+ },
32
+ columnItem: {
33
+ display: "flex",
34
+ alignItems: "center",
35
+ justifyContent: "space-between",
36
+ padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalM}`,
37
+ backgroundColor: tokens.colorNeutralBackground1,
38
+ border: `1px solid ${tokens.colorNeutralStroke2}`,
39
+ borderRadius: tokens.borderRadiusSmall,
40
+ "&:hover": {
41
+ backgroundColor: tokens.colorNeutralBackground1Hover,
42
+ },
43
+ },
44
+ columnName: {
45
+ flex: 1,
46
+ overflow: "hidden",
47
+ textOverflow: "ellipsis",
48
+ whiteSpace: "nowrap",
49
+ },
50
+ buttonGroup: {
51
+ display: "flex",
52
+ gap: tokens.spacingHorizontalXS,
53
+ },
54
+ indexBadge: {
55
+ minWidth: "24px",
56
+ textAlign: "center",
57
+ color: tokens.colorNeutralForeground3,
58
+ fontSize: tokens.fontSizeBase200,
59
+ marginRight: tokens.spacingHorizontalS,
60
+ },
61
+ });
62
+
63
+ interface ColumnConfigDialogProps {
64
+ /** Whether the dialog is open */
65
+ open: boolean;
66
+ /** Columns to display for reordering */
67
+ columns: LayoutColumn[];
68
+ /** Called when dialog is closed without applying */
69
+ onClose: () => void;
70
+ /** Called when Apply is clicked with the new column order */
71
+ onReorder: (columns: LayoutColumn[]) => void;
72
+ }
73
+
74
+ export function ColumnConfigDialog({ open, columns, onClose, onReorder }: ColumnConfigDialogProps) {
75
+ const styles = useStyles();
76
+ const [localColumns, setLocalColumns] = useState<LayoutColumn[]>([]);
77
+
78
+ // Initialize local state when dialog opens or columns change
79
+ useEffect(() => {
80
+ if (open && columns.length > 0) {
81
+ setLocalColumns([...columns]);
82
+ }
83
+ }, [open, columns]);
84
+
85
+ const moveColumn = useCallback((index: number, direction: "up" | "down") => {
86
+ setLocalColumns((prev) => {
87
+ const newColumns = [...prev];
88
+ const targetIndex = direction === "up" ? index - 1 : index + 1;
89
+
90
+ if (targetIndex < 0 || targetIndex >= newColumns.length) {
91
+ return prev;
92
+ }
93
+
94
+ // Swap columns
95
+ [newColumns[index], newColumns[targetIndex]] = [newColumns[targetIndex], newColumns[index]];
96
+ return newColumns;
97
+ });
98
+ }, []);
99
+
100
+ const handleApply = useCallback(() => {
101
+ onReorder(localColumns);
102
+ }, [localColumns, onReorder]);
103
+
104
+ if (columns.length === 0) {
105
+ return null;
106
+ }
107
+
108
+ return (
109
+ <Dialog open={open} onOpenChange={(_e, data) => !data.open && onClose()}>
110
+ <DialogSurface>
111
+ <DialogBody>
112
+ <DialogTitle>Configure Columns</DialogTitle>
113
+ <DialogContent>
114
+ <p
115
+ style={{
116
+ marginBottom: tokens.spacingVerticalM,
117
+ color: tokens.colorNeutralForeground2,
118
+ }}
119
+ >
120
+ Use the arrow buttons to reorder columns. Changes apply when you click Apply.
121
+ </p>
122
+ <div className={styles.columnList}>
123
+ {localColumns.map((column, index) => (
124
+ <div key={column.name} className={styles.columnItem}>
125
+ <span className={styles.indexBadge}>{index + 1}</span>
126
+ <span className={styles.columnName}>{column.displayName || column.name}</span>
127
+ <span
128
+ style={{
129
+ color: tokens.colorNeutralForeground3,
130
+ fontSize: tokens.fontSizeBase200,
131
+ marginRight: tokens.spacingHorizontalS,
132
+ }}
133
+ >
134
+ {column.width}px
135
+ </span>
136
+ <div className={styles.buttonGroup}>
137
+ <Button
138
+ appearance="subtle"
139
+ size="small"
140
+ icon={<ArrowUp24Regular />}
141
+ disabled={index === 0}
142
+ onClick={() => moveColumn(index, "up")}
143
+ title="Move up"
144
+ />
145
+ <Button
146
+ appearance="subtle"
147
+ size="small"
148
+ icon={<ArrowDown24Regular />}
149
+ disabled={index === localColumns.length - 1}
150
+ onClick={() => moveColumn(index, "down")}
151
+ title="Move down"
152
+ />
153
+ </div>
154
+ </div>
155
+ ))}
156
+ </div>
157
+ </DialogContent>
158
+ <DialogActions>
159
+ <Button appearance="secondary" onClick={onClose}>
160
+ Cancel
161
+ </Button>
162
+ <Button appearance="primary" onClick={handleApply}>
163
+ Apply
164
+ </Button>
165
+ </DialogActions>
166
+ </DialogBody>
167
+ </DialogSurface>
168
+ </Dialog>
169
+ );
170
+ }
@@ -0,0 +1,356 @@
1
+ /**
2
+ * Rich cell renderers for different attribute types in DataGrid
3
+ * Renders values using appropriate Fluent UI v9 components
4
+ * Prefers formatted values from OData annotations when available
5
+ */
6
+
7
+ import { Switch, Badge, Label, SkeletonItem, makeStyles, tokens } from "@fluentui/react-components";
8
+ import type { AttributeMetadata } from "../../api/pptbClient";
9
+
10
+ const useStyles = makeStyles({
11
+ booleanCell: {
12
+ display: "flex",
13
+ alignItems: "center",
14
+ gap: tokens.spacingHorizontalXS,
15
+ },
16
+ lookupCell: {
17
+ display: "flex",
18
+ flexDirection: "column",
19
+ gap: "2px",
20
+ },
21
+ lookupName: {
22
+ fontSize: tokens.fontSizeBase200,
23
+ fontWeight: tokens.fontWeightSemibold,
24
+ },
25
+ lookupType: {
26
+ fontSize: tokens.fontSizeBase100,
27
+ color: tokens.colorNeutralForeground3,
28
+ },
29
+ numberCell: {
30
+ textAlign: "right",
31
+ fontVariantNumeric: "tabular-nums",
32
+ },
33
+ dateCell: {
34
+ fontVariantNumeric: "tabular-nums",
35
+ },
36
+ picklistCell: {
37
+ display: "flex",
38
+ alignItems: "center",
39
+ },
40
+ });
41
+
42
+ /**
43
+ * Render a boolean/two-option value as a read-only Switch
44
+ * Uses formatted value from OData annotations if available
45
+ */
46
+ export function BooleanCellRenderer({
47
+ value,
48
+ formattedValue,
49
+ attribute,
50
+ }: {
51
+ value: unknown;
52
+ formattedValue?: unknown;
53
+ attribute?: AttributeMetadata;
54
+ }) {
55
+ const styles = useStyles();
56
+ const boolValue = Boolean(value);
57
+
58
+ // Prefer formatted value, otherwise use metadata labels
59
+ const displayValue =
60
+ formattedValue ||
61
+ (boolValue
62
+ ? attribute?.OptionSet?.TrueOption?.Label?.UserLocalizedLabel?.Label || "Yes"
63
+ : attribute?.OptionSet?.FalseOption?.Label?.UserLocalizedLabel?.Label || "No");
64
+
65
+ return (
66
+ <div className={styles.booleanCell}>
67
+ <Switch checked={boolValue} disabled />
68
+ <Label size="small">{String(displayValue)}</Label>
69
+ </div>
70
+ );
71
+ }
72
+
73
+ /**
74
+ * Render a picklist/state/status value as a Badge
75
+ * Uses formatted value from OData annotations if available
76
+ */
77
+ export function PicklistCellRenderer({
78
+ value,
79
+ formattedValue,
80
+ attribute,
81
+ }: {
82
+ value: unknown;
83
+ formattedValue?: unknown;
84
+ attribute?: AttributeMetadata;
85
+ }) {
86
+ const styles = useStyles();
87
+
88
+ // Prefer formatted value
89
+ if (formattedValue) {
90
+ const appearance =
91
+ attribute?.AttributeType === "State" || attribute?.AttributeType === "Status"
92
+ ? "filled"
93
+ : "tint";
94
+ return (
95
+ <div className={styles.picklistCell}>
96
+ <Badge appearance={appearance}>{String(formattedValue)}</Badge>
97
+ </div>
98
+ );
99
+ }
100
+
101
+ // Fallback to raw value with metadata lookup
102
+ const numValue = typeof value === "number" ? value : parseInt(String(value), 10);
103
+
104
+ if (isNaN(numValue)) {
105
+ return <span>—</span>;
106
+ }
107
+
108
+ // Find the option label from metadata
109
+ const option = attribute?.OptionSet?.Options?.find((opt) => opt.Value === numValue);
110
+ const label = option?.Label?.UserLocalizedLabel?.Label || String(numValue);
111
+
112
+ // Use different badge colors for different states
113
+ const appearance =
114
+ attribute?.AttributeType === "State" || attribute?.AttributeType === "Status"
115
+ ? "filled"
116
+ : "tint";
117
+
118
+ return (
119
+ <div className={styles.picklistCell}>
120
+ <Badge appearance={appearance}>{label}</Badge>
121
+ </div>
122
+ );
123
+ }
124
+
125
+ /**
126
+ * Render a lookup/customer/owner value
127
+ * Uses formatted value from OData annotations (preferred)
128
+ */
129
+ export function LookupCellRenderer({
130
+ value,
131
+ formattedValue,
132
+ }: {
133
+ value: unknown;
134
+ formattedValue?: unknown;
135
+ }) {
136
+ const styles = useStyles();
137
+
138
+ // Prefer formatted value (just the name string)
139
+ if (formattedValue) {
140
+ return <span className={styles.lookupName}>{String(formattedValue)}</span>;
141
+ }
142
+
143
+ // Fallback to raw value (structured object)
144
+ if (!value || typeof value !== "object") {
145
+ return <span>—</span>;
146
+ }
147
+
148
+ const lookupValue = value as { Id?: string; Name?: string; LogicalName?: string };
149
+
150
+ return (
151
+ <div className={styles.lookupCell}>
152
+ <span className={styles.lookupName}>{lookupValue.Name || "—"}</span>
153
+ {lookupValue.LogicalName && (
154
+ <span className={styles.lookupType}>{lookupValue.LogicalName}</span>
155
+ )}
156
+ </div>
157
+ );
158
+ }
159
+
160
+ /**
161
+ * Render a numeric value (integer, decimal, money)
162
+ * Uses formatted value from OData annotations if available
163
+ */
164
+ export function NumberCellRenderer({
165
+ value,
166
+ formattedValue,
167
+ attribute,
168
+ }: {
169
+ value: unknown;
170
+ formattedValue?: unknown;
171
+ attribute?: AttributeMetadata;
172
+ }) {
173
+ const styles = useStyles();
174
+
175
+ // Prefer formatted value (already includes currency symbol, etc.)
176
+ if (formattedValue) {
177
+ return <span className={styles.numberCell}>{String(formattedValue)}</span>;
178
+ }
179
+
180
+ // Fallback to manual formatting
181
+ if (value === null || value === undefined || value === "") {
182
+ return <span>—</span>;
183
+ }
184
+
185
+ const numValue = typeof value === "number" ? value : parseFloat(String(value));
186
+
187
+ if (isNaN(numValue)) {
188
+ return <span>—</span>;
189
+ }
190
+
191
+ // Format based on attribute type
192
+ if (attribute?.AttributeType === "Money") {
193
+ // Format as currency (would need locale info for proper currency symbol)
194
+ return (
195
+ <span className={styles.numberCell}>
196
+ $
197
+ {numValue.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
198
+ </span>
199
+ );
200
+ }
201
+
202
+ if (attribute?.AttributeType === "Integer" || attribute?.AttributeType === "BigInt") {
203
+ return <span className={styles.numberCell}>{numValue.toLocaleString()}</span>;
204
+ }
205
+
206
+ // Decimal/Double - use precision from metadata if available
207
+ const precision = attribute?.Precision ?? 2;
208
+ return (
209
+ <span className={styles.numberCell}>
210
+ {numValue.toLocaleString(undefined, {
211
+ minimumFractionDigits: 0,
212
+ maximumFractionDigits: precision,
213
+ })}
214
+ </span>
215
+ );
216
+ }
217
+
218
+ /**
219
+ * Render a date/time value
220
+ * Uses formatted value from OData annotations if available
221
+ */
222
+ export function DateTimeCellRenderer({
223
+ value,
224
+ formattedValue,
225
+ attribute,
226
+ }: {
227
+ value: unknown;
228
+ formattedValue?: unknown;
229
+ attribute?: AttributeMetadata;
230
+ }) {
231
+ const styles = useStyles();
232
+
233
+ // Prefer formatted value
234
+ if (formattedValue) {
235
+ return <span className={styles.dateCell}>{String(formattedValue)}</span>;
236
+ }
237
+
238
+ // Fallback to manual formatting
239
+ if (!value) {
240
+ return <span>—</span>;
241
+ }
242
+
243
+ const dateValue = new Date(String(value));
244
+
245
+ if (isNaN(dateValue.getTime())) {
246
+ return <span>—</span>;
247
+ }
248
+
249
+ // Check if DateOnly format
250
+ const isDateOnly =
251
+ attribute?.Format === "DateOnly" || attribute?.DateTimeBehavior?.Value === "DateOnly";
252
+
253
+ if (isDateOnly) {
254
+ return (
255
+ <span className={styles.dateCell}>
256
+ {dateValue.toLocaleDateString(undefined, {
257
+ year: "numeric",
258
+ month: "short",
259
+ day: "numeric",
260
+ })}
261
+ </span>
262
+ );
263
+ }
264
+
265
+ // Full date-time
266
+ return (
267
+ <span className={styles.dateCell}>
268
+ {dateValue.toLocaleString(undefined, {
269
+ year: "numeric",
270
+ month: "short",
271
+ day: "numeric",
272
+ hour: "2-digit",
273
+ minute: "2-digit",
274
+ })}
275
+ </span>
276
+ );
277
+ }
278
+
279
+ /**
280
+ * Default text renderer for strings and other types
281
+ * Uses formatted value from OData annotations if available
282
+ */
283
+ export function TextCellRenderer({
284
+ value,
285
+ formattedValue,
286
+ }: {
287
+ value: unknown;
288
+ formattedValue?: unknown;
289
+ }) {
290
+ // Prefer formatted value
291
+ const displayValue = formattedValue ?? value;
292
+
293
+ if (displayValue === null || displayValue === undefined || displayValue === "") {
294
+ return <span>—</span>;
295
+ }
296
+
297
+ return <span>{String(displayValue)}</span>;
298
+ }
299
+
300
+ /**
301
+ * Loading skeleton for virtualized cells
302
+ */
303
+ export function LoadingCellRenderer() {
304
+ return <SkeletonItem />;
305
+ }
306
+
307
+ /**
308
+ * Get the appropriate cell renderer for an attribute type
309
+ * Now accepts formatted value from OData annotations
310
+ */
311
+ export function getCellRenderer(
312
+ attributeType: string | undefined,
313
+ value: unknown,
314
+ formattedValue: unknown | undefined,
315
+ attribute?: AttributeMetadata
316
+ ) {
317
+ if (value === null || value === undefined) {
318
+ return <span>—</span>;
319
+ }
320
+
321
+ switch (attributeType) {
322
+ case "Boolean":
323
+ return (
324
+ <BooleanCellRenderer value={value} formattedValue={formattedValue} attribute={attribute} />
325
+ );
326
+
327
+ case "Picklist":
328
+ case "State":
329
+ case "Status":
330
+ return (
331
+ <PicklistCellRenderer value={value} formattedValue={formattedValue} attribute={attribute} />
332
+ );
333
+
334
+ case "Lookup":
335
+ case "Customer":
336
+ case "Owner":
337
+ return <LookupCellRenderer value={value} formattedValue={formattedValue} />;
338
+
339
+ case "Integer":
340
+ case "BigInt":
341
+ case "Decimal":
342
+ case "Double":
343
+ case "Money":
344
+ return (
345
+ <NumberCellRenderer value={value} formattedValue={formattedValue} attribute={attribute} />
346
+ );
347
+
348
+ case "DateTime":
349
+ return (
350
+ <DateTimeCellRenderer value={value} formattedValue={formattedValue} attribute={attribute} />
351
+ );
352
+
353
+ default:
354
+ return <TextCellRenderer value={value} formattedValue={formattedValue} />;
355
+ }
356
+ }