@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,430 @@
1
+ /**
2
+ * Command bar for DataGrid with row actions
3
+ * Similar to Power Apps Model Driven Apps view command bar
4
+ */
5
+
6
+ import { useState, useCallback } from "react";
7
+ import {
8
+ Toolbar,
9
+ ToolbarButton,
10
+ ToolbarDivider,
11
+ makeStyles,
12
+ Tooltip,
13
+ Menu,
14
+ MenuTrigger,
15
+ MenuPopover,
16
+ MenuList,
17
+ MenuItem,
18
+ SplitButton,
19
+ MenuButton,
20
+ type MenuButtonProps,
21
+ } from "@fluentui/react-components";
22
+ import {
23
+ Open20Regular,
24
+ Link20Regular,
25
+ CheckmarkCircle20Regular,
26
+ DismissCircle20Regular,
27
+ Delete20Regular,
28
+ ColumnTriple20Regular,
29
+ Play20Regular,
30
+ DocumentBulletList20Regular,
31
+ } from "@fluentui/react-icons";
32
+ import { ExcelIcon } from "../../../../shared/components/ExcelIcon";
33
+ import { EditColumnsPanel } from "./EditColumnsPanel";
34
+ import {
35
+ AddColumnsPanel,
36
+ type AddColumnSelection,
37
+ type RelatedEntityColumn,
38
+ } from "./AddColumnsPanel";
39
+ import type { LayoutColumn } from "../../model/layoutxml";
40
+ import type { AttributeMetadata, RelationshipMetadata, WorkflowInfo } from "../../api/pptbClient";
41
+
42
+ const useStyles = makeStyles({
43
+ toolbar: {
44
+ padding: "0",
45
+ },
46
+ splitButtonAppearance: {
47
+ // Use subtle appearance for split buttons to match toolbar
48
+ },
49
+ });
50
+
51
+ export interface CommandBarProps {
52
+ selectedCount: number;
53
+ onOpen?: () => void;
54
+ onCopyUrl?: () => void;
55
+ onActivate?: () => void;
56
+ onDeactivate?: () => void;
57
+ /** Delete selected records (or all if none selected) */
58
+ onDelete?: () => void;
59
+ /** Bulk delete (creates async job) */
60
+ onBulkDelete?: () => void;
61
+ /** Whether delete is available (user has privilege) */
62
+ canDelete?: boolean;
63
+ /** Whether bulk delete is available (user has privilege) */
64
+ canBulkDelete?: boolean;
65
+ /** Whether delete/bulk delete actions are disabled (e.g., aggregate query or 1-N relationship) */
66
+ deleteDisabled?: boolean;
67
+ /** Execute a specific workflow directly */
68
+ onRunSpecificWorkflow?: (workflow: WorkflowInfo) => void;
69
+ /** Whether workflow execution is available (user has privilege) */
70
+ canRunWorkflow?: boolean;
71
+ /** Whether workflow action is disabled (e.g., aggregate query or 1-N relationship) */
72
+ workflowDisabled?: boolean;
73
+ /** Fetch available workflows for the entity */
74
+ onFetchWorkflows?: () => Promise<WorkflowInfo[]>;
75
+ /** Export to Excel via Dataverse ExportToExcel API (requires saved view) */
76
+ onExport?: () => void;
77
+ /** Export to Excel locally using exceljs (no view required) */
78
+ onExportLocal?: () => void;
79
+ /** Whether server export is available (requires a saved view and export privilege) */
80
+ canExport?: boolean;
81
+ /** Whether export is in progress */
82
+ isExporting?: boolean;
83
+ /** Tooltip text to show when server export is disabled */
84
+ exportDisabledReason?: string;
85
+ /** Entity logical name */
86
+ entityName?: string;
87
+ /** Entity display name */
88
+ entityDisplayName?: string;
89
+ /** Current column layout */
90
+ columns?: LayoutColumn[];
91
+ /** Callback when columns are reordered */
92
+ onReorderColumns?: (columns: LayoutColumn[]) => void;
93
+ /** Callback when columns are removed */
94
+ onRemoveColumn?: (columnName: string) => void;
95
+ /** Available attributes from metadata for adding columns */
96
+ availableAttributes?: AttributeMetadata[];
97
+ /** Currently selected attributes in the query */
98
+ selectedAttributes?: string[];
99
+ /** Callback when user wants to add a column from root entity */
100
+ onAddColumn?: (attributeName: string) => void;
101
+ /** Callback when user wants to add columns from related entity */
102
+ onAddRelatedColumns?: (columns: RelatedEntityColumn[]) => void;
103
+ /** Lookup relationships (many-to-one) for related entity columns */
104
+ lookupRelationships?: RelationshipMetadata[];
105
+ /** One-to-many relationships for 1-N related entity columns */
106
+ oneToManyRelationships?: RelationshipMetadata[];
107
+ /** Whether relationship data is loading */
108
+ isLoadingRelationships?: boolean;
109
+ /** Callback to load attributes for a related entity */
110
+ onLoadRelatedAttributes?: (entityLogicalName: string) => Promise<AttributeMetadata[]>;
111
+ /** Callback to reset columns to default */
112
+ onResetToDefault?: () => void;
113
+ }
114
+
115
+ export function ResultsCommandBar({
116
+ selectedCount,
117
+ onOpen,
118
+ onCopyUrl,
119
+ onActivate,
120
+ onDeactivate,
121
+ onDelete,
122
+ onBulkDelete,
123
+ canDelete = false,
124
+ canBulkDelete = false,
125
+ deleteDisabled = false,
126
+ onRunSpecificWorkflow,
127
+ canRunWorkflow = false,
128
+ workflowDisabled = false,
129
+ onFetchWorkflows,
130
+ onExport,
131
+ onExportLocal,
132
+ canExport = false,
133
+ isExporting = false,
134
+ exportDisabledReason,
135
+ entityName,
136
+ entityDisplayName,
137
+ columns,
138
+ onReorderColumns,
139
+ onRemoveColumn,
140
+ availableAttributes,
141
+ selectedAttributes,
142
+ onAddColumn,
143
+ onAddRelatedColumns,
144
+ lookupRelationships,
145
+ oneToManyRelationships,
146
+ isLoadingRelationships,
147
+ onLoadRelatedAttributes,
148
+ onResetToDefault,
149
+ }: CommandBarProps) {
150
+ const styles = useStyles();
151
+ const hasSelection = selectedCount > 0;
152
+ const singleSelection = selectedCount === 1;
153
+ const [editPanelOpen, setEditPanelOpen] = useState(false);
154
+ const [addPanelOpen, setAddPanelOpen] = useState(false);
155
+
156
+ // Workflow menu state
157
+ const [workflows, setWorkflows] = useState<WorkflowInfo[]>([]);
158
+ const [isLoadingWorkflows, setIsLoadingWorkflows] = useState(false);
159
+ const [workflowsLoaded, setWorkflowsLoaded] = useState(false);
160
+
161
+ // Load workflows when menu opens (lazy loading)
162
+ const handleWorkflowMenuOpen = useCallback(async () => {
163
+ if (workflowsLoaded || isLoadingWorkflows || !onFetchWorkflows) return;
164
+
165
+ setIsLoadingWorkflows(true);
166
+ try {
167
+ const wfs = await onFetchWorkflows();
168
+ setWorkflows(wfs);
169
+ setWorkflowsLoaded(true);
170
+ } catch (err) {
171
+ console.error("Failed to load workflows:", err);
172
+ } finally {
173
+ setIsLoadingWorkflows(false);
174
+ }
175
+ }, [workflowsLoaded, isLoadingWorkflows, onFetchWorkflows]);
176
+
177
+ const handleEditColumnsClick = useCallback(() => {
178
+ setEditPanelOpen(true);
179
+ }, []);
180
+
181
+ const handleEditPanelClose = useCallback(() => {
182
+ setEditPanelOpen(false);
183
+ }, []);
184
+
185
+ const handleAddPanelClose = useCallback(() => {
186
+ setAddPanelOpen(false);
187
+ }, []);
188
+
189
+ const handleEditApply = useCallback(
190
+ (updatedColumns: LayoutColumn[]) => {
191
+ // Detect removed columns
192
+ if (columns && onRemoveColumn) {
193
+ const updatedNames = new Set(updatedColumns.map((c) => c.name));
194
+ for (const col of columns) {
195
+ if (!updatedNames.has(col.name)) {
196
+ onRemoveColumn(col.name);
197
+ }
198
+ }
199
+ }
200
+ // Apply reordering
201
+ onReorderColumns?.(updatedColumns);
202
+ setEditPanelOpen(false);
203
+ },
204
+ [columns, onReorderColumns, onRemoveColumn]
205
+ );
206
+
207
+ const handleAddApply = useCallback(
208
+ (selection: AddColumnSelection) => {
209
+ // Add root entity attributes
210
+ for (const attrName of selection.rootAttributes) {
211
+ onAddColumn?.(attrName);
212
+ }
213
+ // Add related entity columns
214
+ if (selection.relatedColumns.length > 0 && onAddRelatedColumns) {
215
+ onAddRelatedColumns(selection.relatedColumns);
216
+ }
217
+ setAddPanelOpen(false);
218
+ },
219
+ [onAddColumn, onAddRelatedColumns]
220
+ );
221
+
222
+ const handleOpenAddFromEdit = useCallback(() => {
223
+ setAddPanelOpen(true);
224
+ }, []);
225
+
226
+ return (
227
+ <>
228
+ <Toolbar className={styles.toolbar} size="medium" aria-label="Record actions">
229
+ <ToolbarButton
230
+ appearance="subtle"
231
+ icon={<Open20Regular />}
232
+ disabled={!singleSelection}
233
+ onClick={onOpen}
234
+ aria-label="Open record"
235
+ >
236
+ Open
237
+ </ToolbarButton>
238
+
239
+ <ToolbarButton
240
+ appearance="subtle"
241
+ icon={<Link20Regular />}
242
+ disabled={!singleSelection}
243
+ onClick={onCopyUrl}
244
+ aria-label="Copy record URL"
245
+ >
246
+ Copy URL
247
+ </ToolbarButton>
248
+
249
+ <ToolbarDivider />
250
+
251
+ {/* Activate/Deactivate buttons - always shown, disabled for now */}
252
+ <ToolbarButton
253
+ appearance="subtle"
254
+ icon={<CheckmarkCircle20Regular />}
255
+ disabled={true}
256
+ onClick={onActivate}
257
+ aria-label="Activate selected records"
258
+ >
259
+ Activate
260
+ </ToolbarButton>
261
+
262
+ <ToolbarButton
263
+ appearance="subtle"
264
+ icon={<DismissCircle20Regular />}
265
+ disabled={true}
266
+ onClick={onDeactivate}
267
+ aria-label="Deactivate selected records"
268
+ >
269
+ Deactivate
270
+ </ToolbarButton>
271
+
272
+ <ToolbarDivider />
273
+
274
+ {canDelete && (
275
+ <Menu positioning="below-end">
276
+ <MenuTrigger disableButtonEnhancement>
277
+ {(triggerProps: MenuButtonProps) => (
278
+ <SplitButton
279
+ appearance="subtle"
280
+ menuButton={triggerProps}
281
+ primaryActionButton={{
282
+ onClick: onDelete,
283
+ disabled: deleteDisabled,
284
+ }}
285
+ disabled={deleteDisabled}
286
+ icon={<Delete20Regular />}
287
+ aria-label={
288
+ hasSelection ? "Delete selected records" : "Delete all records from view"
289
+ }
290
+ >
291
+ {hasSelection ? "Delete" : "Delete All"}
292
+ </SplitButton>
293
+ )}
294
+ </MenuTrigger>
295
+ <MenuPopover>
296
+ <MenuList>
297
+ <MenuItem icon={<Delete20Regular />} onClick={onDelete} disabled={deleteDisabled}>
298
+ Delete{hasSelection ? ` (${selectedCount})` : " All"}
299
+ </MenuItem>
300
+ {canBulkDelete && (
301
+ <MenuItem
302
+ icon={<DocumentBulletList20Regular />}
303
+ onClick={onBulkDelete}
304
+ disabled={deleteDisabled}
305
+ >
306
+ Bulk Delete (Job)
307
+ </MenuItem>
308
+ )}
309
+ </MenuList>
310
+ </MenuPopover>
311
+ </Menu>
312
+ )}
313
+
314
+ {canRunWorkflow && (
315
+ <Menu
316
+ positioning="below-end"
317
+ onOpenChange={(_, data) => data.open && handleWorkflowMenuOpen()}
318
+ >
319
+ <MenuTrigger disableButtonEnhancement>
320
+ <MenuButton
321
+ appearance="subtle"
322
+ icon={<Play20Regular />}
323
+ disabled={!hasSelection || workflowDisabled}
324
+ aria-label="Run workflow on selected records"
325
+ >
326
+ Run Workflow
327
+ </MenuButton>
328
+ </MenuTrigger>
329
+ <MenuPopover>
330
+ <MenuList>
331
+ {isLoadingWorkflows && <MenuItem disabled>Loading workflows...</MenuItem>}
332
+ {!isLoadingWorkflows && workflows.length === 0 && workflowsLoaded && (
333
+ <MenuItem disabled>No workflows available</MenuItem>
334
+ )}
335
+ {!isLoadingWorkflows &&
336
+ workflows.map((wf) => (
337
+ <MenuItem
338
+ key={wf.workflowid}
339
+ icon={<Play20Regular />}
340
+ onClick={() => onRunSpecificWorkflow?.(wf)}
341
+ >
342
+ {wf.name}
343
+ </MenuItem>
344
+ ))}
345
+ </MenuList>
346
+ </MenuPopover>
347
+ </Menu>
348
+ )}
349
+
350
+ <ToolbarDivider />
351
+
352
+ {/* Export Menu Button */}
353
+ <Menu>
354
+ <MenuTrigger disableButtonEnhancement>
355
+ <Tooltip
356
+ content={isExporting ? "Export in progress..." : "Export to Excel"}
357
+ relationship="description"
358
+ >
359
+ <MenuButton
360
+ appearance="subtle"
361
+ icon={<ExcelIcon />}
362
+ disabled={isExporting}
363
+ aria-label="Export to Excel"
364
+ >
365
+ {isExporting ? "Exporting..." : "Export to Excel"}
366
+ </MenuButton>
367
+ </Tooltip>
368
+ </MenuTrigger>
369
+ <MenuPopover>
370
+ <MenuList>
371
+ <Tooltip
372
+ content={
373
+ canExport
374
+ ? "Export using Dataverse API (all records)"
375
+ : exportDisabledReason || "Save as a view first to enable server export"
376
+ }
377
+ relationship="description"
378
+ positioning="before"
379
+ >
380
+ <MenuItem onClick={onExport} disabled={!canExport || isExporting}>
381
+ Export (Server)
382
+ </MenuItem>
383
+ </Tooltip>
384
+ <MenuItem onClick={onExportLocal} disabled={isExporting}>
385
+ Export (Local)
386
+ </MenuItem>
387
+ </MenuList>
388
+ </MenuPopover>
389
+ </Menu>
390
+
391
+ <ToolbarDivider />
392
+
393
+ <ToolbarButton
394
+ appearance="subtle"
395
+ icon={<ColumnTriple20Regular />}
396
+ onClick={handleEditColumnsClick}
397
+ aria-label="Edit columns"
398
+ >
399
+ Edit columns
400
+ </ToolbarButton>
401
+ </Toolbar>
402
+
403
+ {/* Edit Columns Panel */}
404
+ <EditColumnsPanel
405
+ open={editPanelOpen}
406
+ columns={columns || []}
407
+ entityDisplayName={entityDisplayName}
408
+ onClose={handleEditPanelClose}
409
+ onApply={handleEditApply}
410
+ onAddColumns={handleOpenAddFromEdit}
411
+ onResetToDefault={onResetToDefault}
412
+ />
413
+
414
+ {/* Add Columns Panel */}
415
+ <AddColumnsPanel
416
+ open={addPanelOpen}
417
+ entityDisplayName={entityDisplayName}
418
+ entityLogicalName={entityName}
419
+ availableAttributes={availableAttributes}
420
+ selectedAttributes={selectedAttributes}
421
+ lookupRelationships={lookupRelationships}
422
+ oneToManyRelationships={oneToManyRelationships}
423
+ isLoadingRelationships={isLoadingRelationships}
424
+ onLoadRelatedAttributes={onLoadRelatedAttributes}
425
+ onClose={handleAddPanelClose}
426
+ onApply={handleAddApply}
427
+ />
428
+ </>
429
+ );
430
+ }