@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,171 @@
1
+ /**
2
+ * Monaco editor for LayoutXML (read-only)
3
+ * Displays the LayoutXML that defines column configuration for the grid
4
+ */
5
+
6
+ import { useRef, useCallback } from "react";
7
+ import Editor from "@monaco-editor/react";
8
+ import type * as MonacoEditor from "monaco-editor";
9
+ import { makeStyles, Spinner, tokens, Toolbar, ToolbarButton } from "@fluentui/react-components";
10
+ import { Copy24Regular } from "@fluentui/react-icons";
11
+ import { useTheme } from "../../../../shared/contexts/ThemeContext";
12
+
13
+ const useStyles = makeStyles({
14
+ container: {
15
+ display: "flex",
16
+ flexDirection: "column",
17
+ height: "100%",
18
+ overflow: "hidden",
19
+ },
20
+ toolbar: {
21
+ display: "flex",
22
+ alignItems: "center",
23
+ justifyContent: "space-between",
24
+ padding: "4px 8px",
25
+ borderBottom: `1px solid ${tokens.colorNeutralStroke1}`,
26
+ backgroundColor: tokens.colorNeutralBackground2,
27
+ flexShrink: 0,
28
+ },
29
+ toolbarLeft: {
30
+ display: "flex",
31
+ alignItems: "center",
32
+ gap: tokens.spacingHorizontalS,
33
+ },
34
+ toolbarRight: {
35
+ display: "flex",
36
+ alignItems: "center",
37
+ gap: tokens.spacingHorizontalS,
38
+ },
39
+ editorWrapper: {
40
+ flex: 1,
41
+ overflow: "hidden",
42
+ minHeight: 0,
43
+ },
44
+ loadingContainer: {
45
+ display: "flex",
46
+ alignItems: "center",
47
+ justifyContent: "center",
48
+ height: "100%",
49
+ backgroundColor: tokens.colorNeutralBackground1,
50
+ },
51
+ label: {
52
+ fontSize: tokens.fontSizeBase200,
53
+ fontWeight: tokens.fontWeightSemibold,
54
+ color: tokens.colorNeutralForeground2,
55
+ },
56
+ });
57
+
58
+ interface LayoutXmlViewerProps {
59
+ /** The LayoutXML string to display */
60
+ layoutXml: string;
61
+ }
62
+
63
+ export function LayoutXmlViewer({ layoutXml }: LayoutXmlViewerProps) {
64
+ const styles = useStyles();
65
+ const { isDark } = useTheme();
66
+ const editorRef = useRef<MonacoEditor.editor.IStandaloneCodeEditor | null>(null);
67
+
68
+ // Format XML with indentation for better readability
69
+ const formatXml = (xml: string): string => {
70
+ if (!xml) return "";
71
+ try {
72
+ // Simple XML formatting - add newlines and indentation
73
+ let formatted = "";
74
+ let indent = 0;
75
+ const parts = xml.replace(/>\s*</g, ">\n<").split("\n");
76
+
77
+ for (const part of parts) {
78
+ const trimmed = part.trim();
79
+ if (!trimmed) continue;
80
+
81
+ // Decrease indent for closing tags
82
+ if (trimmed.startsWith("</")) {
83
+ indent = Math.max(0, indent - 1);
84
+ }
85
+
86
+ formatted += " ".repeat(indent) + trimmed + "\n";
87
+
88
+ // Increase indent for opening tags (not self-closing)
89
+ if (
90
+ trimmed.startsWith("<") &&
91
+ !trimmed.startsWith("</") &&
92
+ !trimmed.startsWith("<?") &&
93
+ !trimmed.endsWith("/>") &&
94
+ !trimmed.includes("</")
95
+ ) {
96
+ indent++;
97
+ }
98
+ }
99
+
100
+ return formatted.trim();
101
+ } catch {
102
+ return xml;
103
+ }
104
+ };
105
+
106
+ const formattedXml = formatXml(layoutXml);
107
+
108
+ const handleEditorMount = useCallback((editor: MonacoEditor.editor.IStandaloneCodeEditor) => {
109
+ editorRef.current = editor;
110
+ }, []);
111
+
112
+ const handleCopy = useCallback(async () => {
113
+ try {
114
+ await navigator.clipboard.writeText(formattedXml);
115
+ } catch (error) {
116
+ console.error("Failed to copy to clipboard:", error);
117
+ }
118
+ }, [formattedXml]);
119
+
120
+ return (
121
+ <div className={styles.container}>
122
+ <div className={styles.toolbar}>
123
+ <div className={styles.toolbarLeft}>
124
+ <span className={styles.label}>LayoutXML (Read-only)</span>
125
+ </div>
126
+ <Toolbar size="small">
127
+ <ToolbarButton
128
+ icon={<Copy24Regular />}
129
+ onClick={handleCopy}
130
+ disabled={!layoutXml}
131
+ aria-label="Copy LayoutXML"
132
+ >
133
+ Copy
134
+ </ToolbarButton>
135
+ </Toolbar>
136
+ </div>
137
+ <div className={styles.editorWrapper}>
138
+ <Editor
139
+ height="100%"
140
+ language="xml"
141
+ theme={isDark ? "vs-dark" : "vs"}
142
+ value={formattedXml}
143
+ options={{
144
+ readOnly: true,
145
+ minimap: { enabled: false },
146
+ lineNumbers: "on",
147
+ scrollBeyondLastLine: false,
148
+ wordWrap: "on",
149
+ fontSize: 13,
150
+ tabSize: 2,
151
+ automaticLayout: true,
152
+ folding: true,
153
+ renderLineHighlight: "line",
154
+ scrollbar: {
155
+ vertical: "visible",
156
+ horizontal: "visible",
157
+ verticalScrollbarSize: 10,
158
+ horizontalScrollbarSize: 10,
159
+ },
160
+ }}
161
+ onMount={handleEditorMount}
162
+ loading={
163
+ <div className={styles.loadingContainer}>
164
+ <Spinner size="medium" label="Loading editor..." />
165
+ </div>
166
+ }
167
+ />
168
+ </div>
169
+ </div>
170
+ );
171
+ }
@@ -0,0 +1,471 @@
1
+ /**
2
+ * Tabbed preview panel with FetchXML editor and Results grid
3
+ */
4
+
5
+ import { useState, useCallback, type ReactNode } from "react";
6
+ import {
7
+ TabList,
8
+ Tab,
9
+ type SelectTabData,
10
+ type SelectTabEvent,
11
+ Button,
12
+ makeStyles,
13
+ Toolbar,
14
+ ToolbarDivider,
15
+ ProgressBar,
16
+ tokens,
17
+ MessageBar,
18
+ MessageBarBody,
19
+ MessageBarTitle,
20
+ MessageBarActions,
21
+ } from "@fluentui/react-components";
22
+ import { Play24Regular, Dismiss16Regular, Settings20Regular } from "@fluentui/react-icons";
23
+ import { FetchXmlEditor } from "./FetchXmlEditor";
24
+ import { LayoutXmlViewer } from "./LayoutXmlViewer";
25
+ import { ResultsGrid, type QueryResult, type SortChangeData } from "./ResultsGrid";
26
+ import { ResultsCommandBar } from "./ResultsCommandBar";
27
+ import type { RelatedEntityColumn } from "./AddColumnsPanel";
28
+ import type { AttributeMetadata, RelationshipMetadata } from "../../api/pptbClient";
29
+ import type { FetchNode } from "../../model/nodes";
30
+ import type { ParseResult } from "../../model/fetchxmlParser";
31
+ import type { LayoutXmlConfig } from "../../model/layoutxml";
32
+ import type { WorkflowInfo } from "../../api/pptbClient";
33
+ import type { DisplaySettings } from "../../model/displaySettings";
34
+
35
+ const useStyles = makeStyles({
36
+ container: {
37
+ display: "flex",
38
+ flexDirection: "column",
39
+ height: "100%",
40
+ overflow: "hidden",
41
+ },
42
+ header: {
43
+ display: "flex",
44
+ alignItems: "center",
45
+ justifyContent: "space-between",
46
+ borderBottom: `1px solid ${tokens.colorNeutralStroke1}`,
47
+ paddingRight: "16px",
48
+ },
49
+ progressContainer: {
50
+ paddingLeft: "16px",
51
+ paddingRight: "16px",
52
+ paddingTop: "8px",
53
+ paddingBottom: "8px",
54
+ borderBottom: `1px solid ${tokens.colorNeutralStroke1}`,
55
+ },
56
+ tabContent: {
57
+ flex: 1,
58
+ overflow: "hidden",
59
+ padding: "6px",
60
+ backgroundColor: tokens.colorNeutralBackground3, // Neutral canvas background
61
+ },
62
+ // Shared surface card styling (floating cards with shadow)
63
+ surfaceCard: {
64
+ borderRadius: tokens.borderRadiusMedium,
65
+ boxShadow: tokens.shadow16,
66
+ backgroundColor: tokens.colorNeutralBackground1,
67
+ border: `1px solid ${tokens.colorNeutralStroke2}`,
68
+ },
69
+ // Toolbar card styling
70
+ toolbarCard: {
71
+ borderRadius: tokens.borderRadiusMedium,
72
+ boxShadow: tokens.shadow16,
73
+ backgroundColor: tokens.colorNeutralBackground1,
74
+ border: `1px solid ${tokens.colorNeutralStroke2}`,
75
+ padding: "4px 8px",
76
+ flexShrink: 0,
77
+ },
78
+ // Grid card styling
79
+ gridCard: {
80
+ borderRadius: tokens.borderRadiusMedium,
81
+ boxShadow: tokens.shadow16,
82
+ backgroundColor: tokens.colorNeutralBackground1,
83
+ border: `1px solid ${tokens.colorNeutralStroke2}`,
84
+ display: "flex",
85
+ flexDirection: "column",
86
+ flex: 1,
87
+ minHeight: "240px",
88
+ minWidth: "480px",
89
+ overflow: "hidden",
90
+ marginTop: "8px",
91
+ },
92
+ // Code card for FetchXML tab
93
+ codeCard: {
94
+ borderRadius: tokens.borderRadiusMedium,
95
+ boxShadow: tokens.shadow16,
96
+ backgroundColor: tokens.colorNeutralBackground1,
97
+ border: `1px solid ${tokens.colorNeutralStroke2}`,
98
+ height: "100%",
99
+ display: "flex",
100
+ flexDirection: "column",
101
+ },
102
+ // Results tab layout grid
103
+ resultsLayout: {
104
+ display: "flex",
105
+ flexDirection: "column",
106
+ height: "100%",
107
+ overflow: "hidden",
108
+ },
109
+ messageBarContainer: {
110
+ paddingLeft: "16px",
111
+ paddingRight: "16px",
112
+ paddingTop: "8px",
113
+ paddingBottom: "8px",
114
+ borderBottom: `1px solid ${tokens.colorNeutralStroke1}`,
115
+ },
116
+ });
117
+
118
+ interface PreviewTabsProps {
119
+ xml: string;
120
+ /** LayoutXML string for the grid column configuration */
121
+ layoutXml?: string;
122
+ result: QueryResult | null;
123
+ isExecuting?: boolean;
124
+ /** Whether more pages are being loaded (for progress display) */
125
+ isLoadingMore?: boolean;
126
+ onExecute?: () => void;
127
+ /** Export via Dataverse ExportToExcel API */
128
+ onExport?: () => void;
129
+ /** Export locally using exceljs */
130
+ onExportLocal?: () => void;
131
+ onParseToTree?: (xmlString: string) => ParseResult;
132
+ /** Multi-entity attribute metadata: Map<entityLogicalName, Map<attributeLogicalName, AttributeMetadata>> */
133
+ attributeMetadata?: Map<string, Map<string, AttributeMetadata>>;
134
+ fetchQuery?: FetchNode | null;
135
+ /** Column layout configuration for ordering and sizing */
136
+ columnConfig?: LayoutXmlConfig | null;
137
+ /** Callback when column width changes */
138
+ onColumnResize?: (columnName: string, width: number) => void;
139
+ /** Callback when columns are reordered */
140
+ onReorderColumns?: (columns: LayoutXmlConfig["columns"]) => void;
141
+ /** Callback when user wants to add a column from available attributes */
142
+ onAddColumn?: (attributeName: string) => void;
143
+ /** Callback when user wants to add columns from related entity */
144
+ onAddRelatedColumns?: (columns: RelatedEntityColumn[]) => void;
145
+ /** Callback when user removes a column */
146
+ onRemoveColumn?: (columnName: string) => void;
147
+ /** Callback when user changes sort on a column */
148
+ onSortChange?: (data: SortChangeData) => void;
149
+ /** Optional SaveViewButton to render in the toolbar */
150
+ saveViewButton?: ReactNode;
151
+ /** Callback when user scrolls near bottom (infinite scroll) */
152
+ onLoadMore?: () => void;
153
+ /** Whether export is available (requires a saved view) */
154
+ canExport?: boolean;
155
+ /** Whether export is in progress */
156
+ isExporting?: boolean;
157
+ /** Export error message */
158
+ exportError?: string;
159
+ /** Callback to dismiss export error */
160
+ onDismissExportError?: () => void;
161
+ /** Tooltip text for disabled export button */
162
+ exportDisabledReason?: string;
163
+ /** Entity display name */
164
+ entityDisplayName?: string;
165
+ /** Lookup relationships for related columns */
166
+ lookupRelationships?: RelationshipMetadata[];
167
+ /** One-to-many relationships for 1-N related columns */
168
+ oneToManyRelationships?: RelationshipMetadata[];
169
+ /** Whether relationship data is loading */
170
+ isLoadingRelationships?: boolean;
171
+ /** Callback to load attributes for a related entity */
172
+ onLoadRelatedAttributes?: (entityLogicalName: string) => Promise<AttributeMetadata[]>;
173
+ /** Callback to reset columns to default */
174
+ onResetToDefault?: () => void;
175
+ // Record action callbacks
176
+ /** Callback when user clicks Open (opens record(s) in new tab) */
177
+ onOpenRecord?: (recordIds: string[]) => void;
178
+ /** Callback when user clicks Copy URL (copies record URL(s)) */
179
+ onCopyRecordUrl?: (recordIds: string[]) => void;
180
+ /** Callback when user clicks Delete */
181
+ onDeleteRecords?: (recordIds: string[]) => void;
182
+ /** Callback when user clicks Bulk Delete */
183
+ onBulkDeleteRecords?: (recordIds: string[]) => void;
184
+ /** Callback when user clicks Activate */
185
+ onActivateRecords?: (recordIds: string[]) => void;
186
+ /** Callback when user clicks Deactivate */
187
+ onDeactivateRecords?: (recordIds: string[]) => void;
188
+ /** Callback when user wants to run a specific workflow directly */
189
+ onRunSpecificWorkflow?: (workflow: WorkflowInfo, recordIds: string[]) => void;
190
+ /** Whether user can delete records */
191
+ canDelete?: boolean;
192
+ /** Whether user can bulk delete records */
193
+ canBulkDelete?: boolean;
194
+ /** Whether user can run workflows */
195
+ canRunWorkflow?: boolean;
196
+ /** Fetch available workflows for the entity */
197
+ onFetchWorkflows?: () => Promise<WorkflowInfo[]>;
198
+ /** Whether the current query is an aggregate query (disables delete/workflow) */
199
+ isAggregateQuery?: boolean;
200
+ /** Whether the query has 1-N or N-N relationships that cause row duplication */
201
+ hasOneToManyRelationship?: boolean;
202
+ /** Callback when selection changes in ResultsGrid */
203
+ onSelectionChange?: (recordIds: string[]) => void;
204
+ /** Callback to get currently selected record IDs */
205
+ getSelectedRecordIds?: () => string[];
206
+ /** Callback when user clicks Settings button */
207
+ onOpenSettings?: () => void;
208
+ /** Display settings (logical names, value format) */
209
+ displaySettings?: DisplaySettings;
210
+ }
211
+
212
+ export function PreviewTabs({
213
+ xml,
214
+ layoutXml,
215
+ result,
216
+ isExecuting,
217
+ isLoadingMore,
218
+ onExecute,
219
+ onExport,
220
+ onExportLocal,
221
+ onParseToTree,
222
+ attributeMetadata,
223
+ fetchQuery,
224
+ columnConfig,
225
+ onColumnResize,
226
+ onReorderColumns,
227
+ onAddColumn,
228
+ onAddRelatedColumns,
229
+ onRemoveColumn,
230
+ onSortChange,
231
+ saveViewButton,
232
+ onLoadMore,
233
+ canExport,
234
+ isExporting,
235
+ exportError,
236
+ onDismissExportError,
237
+ exportDisabledReason,
238
+ entityDisplayName,
239
+ lookupRelationships,
240
+ oneToManyRelationships,
241
+ isLoadingRelationships,
242
+ onLoadRelatedAttributes,
243
+ onResetToDefault,
244
+ onOpenRecord,
245
+ onCopyRecordUrl,
246
+ onDeleteRecords,
247
+ onBulkDeleteRecords,
248
+ onActivateRecords,
249
+ onDeactivateRecords,
250
+ onRunSpecificWorkflow,
251
+ canDelete,
252
+ canBulkDelete,
253
+ canRunWorkflow,
254
+ onFetchWorkflows,
255
+ isAggregateQuery,
256
+ hasOneToManyRelationship,
257
+ onSelectionChange,
258
+ getSelectedRecordIds,
259
+ onOpenSettings,
260
+ displaySettings,
261
+ }: PreviewTabsProps) {
262
+ const styles = useStyles();
263
+ const [selectedTab, setSelectedTab] = useState<"xml" | "layout" | "results">("xml");
264
+ const [toolbarSelectedCount, setToolbarSelectedCount] = useState(0);
265
+ const [selectedRecordIds, setSelectedRecordIds] = useState<string[]>([]);
266
+
267
+ const handleTabSelect = (_event: SelectTabEvent, data: SelectTabData) => {
268
+ setSelectedTab(data.value as "xml" | "layout" | "results");
269
+ };
270
+
271
+ const handleExecute = () => {
272
+ // Switch to Results tab when executing
273
+ setSelectedTab("results");
274
+ onExecute?.();
275
+ };
276
+
277
+ const handleSelectionChange = useCallback(
278
+ (recordIds: string[]) => {
279
+ setSelectedRecordIds(recordIds);
280
+ // Also notify parent
281
+ onSelectionChange?.(recordIds);
282
+ },
283
+ [onSelectionChange]
284
+ );
285
+
286
+ // If parent provided a getter, use ours; otherwise use internal state
287
+ const getSelectedIds = useCallback(() => {
288
+ return getSelectedRecordIds ? getSelectedRecordIds() : selectedRecordIds;
289
+ }, [getSelectedRecordIds, selectedRecordIds]);
290
+
291
+ return (
292
+ <div className={styles.container}>
293
+ <div className={styles.header}>
294
+ <TabList selectedValue={selectedTab} onTabSelect={handleTabSelect}>
295
+ <Tab value="xml">FetchXML</Tab>
296
+ <Tab value="layout">LayoutXML</Tab>
297
+ <Tab value="results">Results</Tab>
298
+ </TabList>
299
+ <Toolbar size="small">
300
+ <Button
301
+ appearance="primary"
302
+ icon={<Play24Regular />}
303
+ onClick={handleExecute}
304
+ disabled={isExecuting || isLoadingMore || !xml}
305
+ >
306
+ {isExecuting ? "Executing..." : "Execute"}
307
+ </Button>
308
+ {selectedTab === "xml" && saveViewButton && (
309
+ <>
310
+ <ToolbarDivider />
311
+ {saveViewButton}
312
+ </>
313
+ )}
314
+ <ToolbarDivider />
315
+ <Button
316
+ appearance="subtle"
317
+ icon={<Settings20Regular />}
318
+ onClick={onOpenSettings}
319
+ aria-label="Settings"
320
+ />
321
+ </Toolbar>
322
+ </div>
323
+ {(isExecuting || isLoadingMore) && (
324
+ <div className={styles.progressContainer}>
325
+ <ProgressBar />
326
+ </div>
327
+ )}
328
+ {/* Export status MessageBar */}
329
+ {isExporting && (
330
+ <div className={styles.messageBarContainer}>
331
+ <MessageBar intent="info">
332
+ <MessageBarBody>
333
+ <MessageBarTitle>Exporting</MessageBarTitle>
334
+ Exporting view to Excel...
335
+ </MessageBarBody>
336
+ </MessageBar>
337
+ </div>
338
+ )}
339
+ {exportError && !isExporting && (
340
+ <div className={styles.messageBarContainer}>
341
+ <MessageBar intent="error">
342
+ <MessageBarBody>
343
+ <MessageBarTitle>Export Failed</MessageBarTitle>
344
+ {exportError}
345
+ </MessageBarBody>
346
+ <MessageBarActions
347
+ containerAction={
348
+ <Button
349
+ appearance="transparent"
350
+ icon={<Dismiss16Regular />}
351
+ onClick={onDismissExportError}
352
+ aria-label="Dismiss"
353
+ />
354
+ }
355
+ />
356
+ </MessageBar>
357
+ </div>
358
+ )}
359
+ <div className={styles.tabContent}>
360
+ {selectedTab === "xml" && (
361
+ <div className={styles.codeCard}>
362
+ <FetchXmlEditor xml={xml} onParseToTree={onParseToTree} />
363
+ </div>
364
+ )}
365
+ {selectedTab === "layout" && (
366
+ <div className={styles.codeCard}>
367
+ <LayoutXmlViewer layoutXml={layoutXml || ""} />
368
+ </div>
369
+ )}
370
+ {selectedTab === "results" && (
371
+ <div className={styles.resultsLayout}>
372
+ <div className={styles.toolbarCard}>
373
+ <ResultsCommandBar
374
+ selectedCount={toolbarSelectedCount}
375
+ onOpen={() => {
376
+ const ids = getSelectedIds();
377
+ if (ids?.length && onOpenRecord) {
378
+ onOpenRecord(ids);
379
+ }
380
+ }}
381
+ onCopyUrl={() => {
382
+ const ids = getSelectedIds();
383
+ if (ids?.length && onCopyRecordUrl) {
384
+ onCopyRecordUrl(ids);
385
+ }
386
+ }}
387
+ onActivate={() => {
388
+ const ids = getSelectedIds();
389
+ if (ids?.length && onActivateRecords) {
390
+ onActivateRecords(ids);
391
+ }
392
+ }}
393
+ onDeactivate={() => {
394
+ const ids = getSelectedIds();
395
+ if (ids?.length && onDeactivateRecords) {
396
+ onDeactivateRecords(ids);
397
+ }
398
+ }}
399
+ onDelete={() => {
400
+ const ids = getSelectedIds();
401
+ // For delete, we allow empty selection to trigger "delete all" dialog
402
+ if (onDeleteRecords) {
403
+ onDeleteRecords(ids || []);
404
+ }
405
+ }}
406
+ onBulkDelete={() => {
407
+ const ids = getSelectedIds();
408
+ if (onBulkDeleteRecords) {
409
+ onBulkDeleteRecords(ids || []);
410
+ }
411
+ }}
412
+ canDelete={canDelete}
413
+ canBulkDelete={canBulkDelete}
414
+ deleteDisabled={isAggregateQuery || hasOneToManyRelationship}
415
+ onRunSpecificWorkflow={(workflow: WorkflowInfo) => {
416
+ const ids = getSelectedIds();
417
+ if (ids?.length && onRunSpecificWorkflow) {
418
+ onRunSpecificWorkflow(workflow, ids);
419
+ }
420
+ }}
421
+ canRunWorkflow={canRunWorkflow}
422
+ workflowDisabled={isAggregateQuery || hasOneToManyRelationship}
423
+ onFetchWorkflows={onFetchWorkflows}
424
+ onExport={onExport || (() => console.log("Export not yet implemented"))}
425
+ onExportLocal={onExportLocal}
426
+ canExport={canExport}
427
+ isExporting={isExporting}
428
+ exportDisabledReason={exportDisabledReason}
429
+ entityName={result?.entityLogicalName}
430
+ entityDisplayName={entityDisplayName}
431
+ columns={columnConfig?.columns}
432
+ onReorderColumns={onReorderColumns}
433
+ onRemoveColumn={onRemoveColumn}
434
+ availableAttributes={
435
+ // Get root entity attributes from multi-entity map
436
+ attributeMetadata && fetchQuery?.entity?.name
437
+ ? Array.from(attributeMetadata.get(fetchQuery.entity.name)?.values() || [])
438
+ : undefined
439
+ }
440
+ selectedAttributes={fetchQuery?.entity.attributes.map((a) => a.name)}
441
+ onAddColumn={onAddColumn}
442
+ onAddRelatedColumns={onAddRelatedColumns}
443
+ lookupRelationships={lookupRelationships}
444
+ oneToManyRelationships={oneToManyRelationships}
445
+ isLoadingRelationships={isLoadingRelationships}
446
+ onLoadRelatedAttributes={onLoadRelatedAttributes}
447
+ onResetToDefault={onResetToDefault}
448
+ />
449
+ </div>
450
+ <div className={styles.gridCard}>
451
+ <ResultsGrid
452
+ result={result}
453
+ isLoading={isExecuting}
454
+ isLoadingMore={isLoadingMore}
455
+ attributeMetadata={attributeMetadata}
456
+ fetchQuery={fetchQuery}
457
+ onSelectedCountChange={setToolbarSelectedCount}
458
+ onSelectionChange={handleSelectionChange}
459
+ columnConfig={columnConfig}
460
+ onColumnResize={onColumnResize}
461
+ onSortChange={onSortChange}
462
+ onLoadMore={onLoadMore}
463
+ displaySettings={displaySettings}
464
+ />
465
+ </div>
466
+ </div>
467
+ )}
468
+ </div>
469
+ </div>
470
+ );
471
+ }