@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.
- package/LICENSE +661 -0
- package/dist/assets/index-DD9fzkbz.js +232 -0
- package/dist/index.html +70 -0
- package/index.html +0 -1
- package/npm-shrinkwrap.json +6329 -0
- package/package.json +95 -51
- package/CHANGELOG.md +0 -120
- package/eslint.config.js +0 -23
- package/public/vite.svg +0 -1
- package/src/app/AppShell.tsx +0 -1733
- package/src/assets/20px-Microsoft_Office_Excel_(2025/342/200/223present).svg.png +0 -0
- package/src/assets/react.svg +0 -1
- package/src/features/fetchxml/api/dataverseMetadata.ts +0 -192
- package/src/features/fetchxml/api/excelExport.ts +0 -318
- package/src/features/fetchxml/api/formattedValues.ts +0 -107
- package/src/features/fetchxml/api/pptbClient.ts +0 -2635
- package/src/features/fetchxml/model/displaySettings.ts +0 -18
- package/src/features/fetchxml/model/fetchxml.ts +0 -346
- package/src/features/fetchxml/model/fetchxmlIntellisense.ts +0 -583
- package/src/features/fetchxml/model/fetchxmlParser.ts +0 -708
- package/src/features/fetchxml/model/layoutxml.ts +0 -372
- package/src/features/fetchxml/model/nodes.ts +0 -255
- package/src/features/fetchxml/model/operators.ts +0 -624
- package/src/features/fetchxml/model/treeUtils.ts +0 -174
- package/src/features/fetchxml/state/builderStore.tsx +0 -1160
- package/src/features/fetchxml/state/cache.ts +0 -393
- package/src/features/fetchxml/ui/Dialogs/BulkDeleteDialog.tsx +0 -352
- package/src/features/fetchxml/ui/Dialogs/DeleteConfirmDialog.tsx +0 -396
- package/src/features/fetchxml/ui/Dialogs/SaveViewDialog.tsx +0 -612
- package/src/features/fetchxml/ui/Dialogs/SolutionPicker.tsx +0 -114
- package/src/features/fetchxml/ui/Dialogs/WorkflowPickerDialog.tsx +0 -437
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/AttributeEditor.tsx +0 -214
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/ConditionEditor.tsx +0 -533
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/FetchOptionsEditor.tsx +0 -296
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/FilterEditor.tsx +0 -106
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/LinkEntityEditor.tsx +0 -433
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/OrderEditor.tsx +0 -161
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/ValueOfPicker.tsx +0 -194
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel.tsx +0 -225
- package/src/features/fetchxml/ui/LeftPane/TreeView.tsx +0 -604
- package/src/features/fetchxml/ui/RightPane/AddColumnsPanel.tsx +0 -800
- package/src/features/fetchxml/ui/RightPane/ColumnConfigDialog.tsx +0 -170
- package/src/features/fetchxml/ui/RightPane/DataGridCellRenderers.tsx +0 -356
- package/src/features/fetchxml/ui/RightPane/EditColumnsPanel.tsx +0 -372
- package/src/features/fetchxml/ui/RightPane/FetchXmlEditor.tsx +0 -399
- package/src/features/fetchxml/ui/RightPane/FormattedValueUtils.ts +0 -189
- package/src/features/fetchxml/ui/RightPane/LayoutXmlViewer.tsx +0 -171
- package/src/features/fetchxml/ui/RightPane/PreviewTabs.tsx +0 -471
- package/src/features/fetchxml/ui/RightPane/ResultsCommandBar.tsx +0 -430
- package/src/features/fetchxml/ui/RightPane/ResultsGrid.tsx +0 -983
- package/src/features/fetchxml/ui/RightPane/XmlTextArea.tsx +0 -65
- package/src/features/fetchxml/ui/Settings/SettingsDrawer.tsx +0 -175
- package/src/features/fetchxml/ui/Toolbar/EntitySelector.tsx +0 -788
- package/src/features/fetchxml/ui/Toolbar/LoadViewPicker.tsx +0 -274
- package/src/features/fetchxml/ui/Toolbar/SaveViewButton.tsx +0 -174
- package/src/main.tsx +0 -11
- package/src/shared/components/AttributePicker.tsx +0 -138
- package/src/shared/components/BetweenInputPicker.tsx +0 -120
- package/src/shared/components/BooleanValuePicker.tsx +0 -165
- package/src/shared/components/DateTimeInputPicker.tsx +0 -170
- package/src/shared/components/DynamicValueList.tsx +0 -128
- package/src/shared/components/ExcelIcon.tsx +0 -30
- package/src/shared/components/MultiValuePicker.tsx +0 -178
- package/src/shared/components/NumericInputPicker.tsx +0 -156
- package/src/shared/components/OptionSetValuePicker.tsx +0 -150
- package/src/shared/components/RelationshipPicker.tsx +0 -268
- package/src/shared/contexts/ThemeContext.tsx +0 -19
- package/src/shared/hooks/useAccessMode.ts +0 -77
- package/src/shared/hooks/useLazyMetadata.ts +0 -132
- package/src/shared/hooks/usePptbContext.ts +0 -170
- package/src/shared/hooks/usePublisherFilter.ts +0 -214
- package/src/shared/hooks/useSolutionFilter.ts +0 -162
- package/src/shared/utils/debug.ts +0 -87
- package/tsconfig.app.json +0 -28
- package/tsconfig.json +0 -7
- package/tsconfig.node.json +0 -26
- package/vite.config.ts +0 -7
|
@@ -1,399 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Monaco editor for FetchXML with Editor Mode toggle
|
|
3
|
-
* - Read-only mode: Shows generated XML with Copy button
|
|
4
|
-
* - Editor mode: Editable with Paste, Load File, Reset, Validate, Parse to Tree buttons
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { useState, useRef, useCallback } from "react";
|
|
8
|
-
import Editor from "@monaco-editor/react";
|
|
9
|
-
import type * as MonacoEditor from "monaco-editor";
|
|
10
|
-
import {
|
|
11
|
-
Button,
|
|
12
|
-
makeStyles,
|
|
13
|
-
Tooltip,
|
|
14
|
-
Spinner,
|
|
15
|
-
tokens,
|
|
16
|
-
Switch,
|
|
17
|
-
Toolbar,
|
|
18
|
-
ToolbarButton,
|
|
19
|
-
ToolbarDivider,
|
|
20
|
-
MessageBar,
|
|
21
|
-
MessageBarBody,
|
|
22
|
-
MessageBarTitle,
|
|
23
|
-
MessageBarActions,
|
|
24
|
-
MessageBarGroup,
|
|
25
|
-
} from "@fluentui/react-components";
|
|
26
|
-
import {
|
|
27
|
-
Copy24Regular,
|
|
28
|
-
ClipboardPaste24Regular,
|
|
29
|
-
FolderOpen24Regular,
|
|
30
|
-
ArrowReset24Regular,
|
|
31
|
-
Checkmark24Regular,
|
|
32
|
-
TreeDeciduous24Regular,
|
|
33
|
-
Dismiss24Regular,
|
|
34
|
-
} from "@fluentui/react-icons";
|
|
35
|
-
import { useTheme } from "../../../../shared/contexts/ThemeContext";
|
|
36
|
-
import { parseFetchXml, validateFetchXmlSyntax } from "../../model/fetchxmlParser";
|
|
37
|
-
import type { ParseResult, ParseWarning, ParseError } from "../../model/fetchxmlParser";
|
|
38
|
-
import {
|
|
39
|
-
registerFetchXmlIntellisense,
|
|
40
|
-
registerFetchXmlHoverProvider,
|
|
41
|
-
} from "../../model/fetchxmlIntellisense";
|
|
42
|
-
|
|
43
|
-
const useStyles = makeStyles({
|
|
44
|
-
container: {
|
|
45
|
-
display: "flex",
|
|
46
|
-
flexDirection: "column",
|
|
47
|
-
height: "100%",
|
|
48
|
-
overflow: "hidden",
|
|
49
|
-
},
|
|
50
|
-
toolbar: {
|
|
51
|
-
display: "flex",
|
|
52
|
-
alignItems: "center",
|
|
53
|
-
justifyContent: "space-between",
|
|
54
|
-
padding: "4px 8px",
|
|
55
|
-
borderBottom: `1px solid ${tokens.colorNeutralStroke1}`,
|
|
56
|
-
backgroundColor: tokens.colorNeutralBackground2,
|
|
57
|
-
flexShrink: 0,
|
|
58
|
-
},
|
|
59
|
-
toolbarLeft: {
|
|
60
|
-
display: "flex",
|
|
61
|
-
alignItems: "center",
|
|
62
|
-
gap: tokens.spacingHorizontalS,
|
|
63
|
-
},
|
|
64
|
-
toolbarRight: {
|
|
65
|
-
display: "flex",
|
|
66
|
-
alignItems: "center",
|
|
67
|
-
gap: tokens.spacingHorizontalS,
|
|
68
|
-
},
|
|
69
|
-
editorWrapper: {
|
|
70
|
-
flex: 1,
|
|
71
|
-
overflow: "hidden",
|
|
72
|
-
minHeight: 0,
|
|
73
|
-
},
|
|
74
|
-
loadingContainer: {
|
|
75
|
-
display: "flex",
|
|
76
|
-
alignItems: "center",
|
|
77
|
-
justifyContent: "center",
|
|
78
|
-
height: "100%",
|
|
79
|
-
backgroundColor: tokens.colorNeutralBackground1,
|
|
80
|
-
},
|
|
81
|
-
messageBarContainer: {
|
|
82
|
-
padding: tokens.spacingVerticalS,
|
|
83
|
-
borderTop: `1px solid ${tokens.colorNeutralStroke1}`,
|
|
84
|
-
maxHeight: "150px",
|
|
85
|
-
overflowY: "auto",
|
|
86
|
-
flexShrink: 0,
|
|
87
|
-
},
|
|
88
|
-
switchLabel: {
|
|
89
|
-
fontSize: tokens.fontSizeBase200,
|
|
90
|
-
fontWeight: tokens.fontWeightSemibold,
|
|
91
|
-
},
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
interface Message {
|
|
95
|
-
id: number;
|
|
96
|
-
intent: "success" | "warning" | "error" | "info";
|
|
97
|
-
title: string;
|
|
98
|
-
body: string;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
interface FetchXmlEditorProps {
|
|
102
|
-
xml: string;
|
|
103
|
-
onParseToTree?: (xmlString: string) => ParseResult;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function FetchXmlEditor({ xml, onParseToTree }: FetchXmlEditorProps) {
|
|
107
|
-
const styles = useStyles();
|
|
108
|
-
const { isDark } = useTheme();
|
|
109
|
-
const editorRef = useRef<MonacoEditor.editor.IStandaloneCodeEditor | null>(null);
|
|
110
|
-
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
111
|
-
const messageIdRef = useRef(0);
|
|
112
|
-
|
|
113
|
-
// Editor mode state
|
|
114
|
-
const [isEditorMode, setIsEditorMode] = useState(false);
|
|
115
|
-
const [editedXml, setEditedXml] = useState("");
|
|
116
|
-
const [messages, setMessages] = useState<Message[]>([]);
|
|
117
|
-
|
|
118
|
-
// Get current XML value based on mode
|
|
119
|
-
const currentXml = isEditorMode ? editedXml : xml;
|
|
120
|
-
|
|
121
|
-
// Add a message
|
|
122
|
-
const addMessage = useCallback((intent: Message["intent"], title: string, body: string) => {
|
|
123
|
-
const newMessage: Message = {
|
|
124
|
-
id: messageIdRef.current++,
|
|
125
|
-
intent,
|
|
126
|
-
title,
|
|
127
|
-
body,
|
|
128
|
-
};
|
|
129
|
-
setMessages((prev) => [newMessage, ...prev].slice(0, 5)); // Keep max 5 messages
|
|
130
|
-
}, []);
|
|
131
|
-
|
|
132
|
-
// Dismiss a message
|
|
133
|
-
const dismissMessage = useCallback((id: number) => {
|
|
134
|
-
setMessages((prev) => prev.filter((m) => m.id !== id));
|
|
135
|
-
}, []);
|
|
136
|
-
|
|
137
|
-
// Clear all messages
|
|
138
|
-
const clearMessages = useCallback(() => {
|
|
139
|
-
setMessages([]);
|
|
140
|
-
}, []);
|
|
141
|
-
|
|
142
|
-
// Toggle editor mode
|
|
143
|
-
const handleToggleEditorMode = useCallback(
|
|
144
|
-
(checked: boolean) => {
|
|
145
|
-
if (checked) {
|
|
146
|
-
// Entering editor mode - copy current XML to edit buffer
|
|
147
|
-
setEditedXml(xml);
|
|
148
|
-
clearMessages();
|
|
149
|
-
}
|
|
150
|
-
setIsEditorMode(checked);
|
|
151
|
-
},
|
|
152
|
-
[xml, clearMessages]
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
// Copy to clipboard
|
|
156
|
-
const handleCopy = useCallback(async () => {
|
|
157
|
-
try {
|
|
158
|
-
await navigator.clipboard.writeText(currentXml);
|
|
159
|
-
addMessage("success", "Copied", "FetchXML copied to clipboard");
|
|
160
|
-
} catch (err) {
|
|
161
|
-
console.error("Failed to copy:", err);
|
|
162
|
-
addMessage("error", "Copy failed", "Could not copy to clipboard");
|
|
163
|
-
}
|
|
164
|
-
}, [currentXml, addMessage]);
|
|
165
|
-
|
|
166
|
-
// Paste from clipboard
|
|
167
|
-
const handlePaste = useCallback(async () => {
|
|
168
|
-
try {
|
|
169
|
-
const text = await navigator.clipboard.readText();
|
|
170
|
-
if (text.trim()) {
|
|
171
|
-
setEditedXml(text);
|
|
172
|
-
addMessage("info", "Pasted", "Content pasted from clipboard");
|
|
173
|
-
}
|
|
174
|
-
} catch (err) {
|
|
175
|
-
console.error("Failed to paste:", err);
|
|
176
|
-
addMessage("error", "Paste failed", "Could not read from clipboard");
|
|
177
|
-
}
|
|
178
|
-
}, [addMessage]);
|
|
179
|
-
|
|
180
|
-
// Load from file
|
|
181
|
-
const handleLoadFile = useCallback(() => {
|
|
182
|
-
fileInputRef.current?.click();
|
|
183
|
-
}, []);
|
|
184
|
-
|
|
185
|
-
const handleFileChange = useCallback(
|
|
186
|
-
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
187
|
-
const file = event.target.files?.[0];
|
|
188
|
-
if (file) {
|
|
189
|
-
const reader = new FileReader();
|
|
190
|
-
reader.onload = (e) => {
|
|
191
|
-
const content = e.target?.result as string;
|
|
192
|
-
setEditedXml(content);
|
|
193
|
-
addMessage("info", "File loaded", `Loaded ${file.name}`);
|
|
194
|
-
};
|
|
195
|
-
reader.onerror = () => {
|
|
196
|
-
addMessage("error", "Load failed", "Could not read file");
|
|
197
|
-
};
|
|
198
|
-
reader.readAsText(file);
|
|
199
|
-
}
|
|
200
|
-
// Reset input so same file can be loaded again
|
|
201
|
-
event.target.value = "";
|
|
202
|
-
},
|
|
203
|
-
[addMessage]
|
|
204
|
-
);
|
|
205
|
-
|
|
206
|
-
// Reset to tree-generated XML
|
|
207
|
-
const handleReset = useCallback(() => {
|
|
208
|
-
setEditedXml(xml);
|
|
209
|
-
clearMessages();
|
|
210
|
-
addMessage("info", "Reset", "Reverted to tree-generated FetchXML");
|
|
211
|
-
}, [xml, clearMessages, addMessage]);
|
|
212
|
-
|
|
213
|
-
// Validate XML
|
|
214
|
-
const handleValidate = useCallback(() => {
|
|
215
|
-
clearMessages();
|
|
216
|
-
|
|
217
|
-
// First check syntax
|
|
218
|
-
const syntaxResult = validateFetchXmlSyntax(editedXml);
|
|
219
|
-
if (!syntaxResult.valid) {
|
|
220
|
-
addMessage("error", "Syntax Error", syntaxResult.error || "Invalid XML syntax");
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Then do a full parse to get warnings
|
|
225
|
-
const parseResult = parseFetchXml(editedXml);
|
|
226
|
-
if (!parseResult.success) {
|
|
227
|
-
parseResult.errors.forEach((err: ParseError) => {
|
|
228
|
-
addMessage("error", "Parse Error", err.message);
|
|
229
|
-
});
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Show warnings if any
|
|
234
|
-
if (parseResult.warnings.length > 0) {
|
|
235
|
-
parseResult.warnings.forEach((warn: ParseWarning) => {
|
|
236
|
-
const prefix = warn.element ? `<${warn.element}>: ` : "";
|
|
237
|
-
addMessage("warning", "Warning", `${prefix}${warn.message}`);
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
addMessage("success", "Valid", "FetchXML is valid and ready to parse");
|
|
242
|
-
}, [editedXml, clearMessages, addMessage]);
|
|
243
|
-
|
|
244
|
-
// Parse to tree
|
|
245
|
-
const handleParseToTree = useCallback(() => {
|
|
246
|
-
if (!onParseToTree) return;
|
|
247
|
-
|
|
248
|
-
clearMessages();
|
|
249
|
-
const result = onParseToTree(editedXml);
|
|
250
|
-
|
|
251
|
-
if (result.success) {
|
|
252
|
-
// Show warnings but still success
|
|
253
|
-
result.warnings.forEach((warn: ParseWarning) => {
|
|
254
|
-
const prefix = warn.element ? `<${warn.element}>: ` : "";
|
|
255
|
-
addMessage("warning", "Warning", `${prefix}${warn.message}`);
|
|
256
|
-
});
|
|
257
|
-
addMessage("success", "Parsed", "FetchXML loaded into tree builder");
|
|
258
|
-
// Exit editor mode after successful parse
|
|
259
|
-
setIsEditorMode(false);
|
|
260
|
-
} else {
|
|
261
|
-
result.errors.forEach((err: ParseError) => {
|
|
262
|
-
addMessage("error", "Parse Error", err.message);
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
}, [editedXml, onParseToTree, clearMessages, addMessage]);
|
|
266
|
-
|
|
267
|
-
// Handle editor mount
|
|
268
|
-
const handleEditorMount = useCallback(
|
|
269
|
-
(editor: MonacoEditor.editor.IStandaloneCodeEditor, monaco: typeof MonacoEditor) => {
|
|
270
|
-
editorRef.current = editor;
|
|
271
|
-
// Register FetchXML intellisense (only in editor mode, but register once)
|
|
272
|
-
registerFetchXmlIntellisense(monaco);
|
|
273
|
-
registerFetchXmlHoverProvider(monaco);
|
|
274
|
-
},
|
|
275
|
-
[]
|
|
276
|
-
);
|
|
277
|
-
|
|
278
|
-
return (
|
|
279
|
-
<div className={styles.container}>
|
|
280
|
-
{/* Toolbar */}
|
|
281
|
-
<div className={styles.toolbar}>
|
|
282
|
-
<div className={styles.toolbarLeft}>
|
|
283
|
-
{isEditorMode ? (
|
|
284
|
-
<Toolbar size="small">
|
|
285
|
-
<Tooltip content="Paste from clipboard" relationship="label">
|
|
286
|
-
<ToolbarButton icon={<ClipboardPaste24Regular />} onClick={handlePaste}>
|
|
287
|
-
Paste
|
|
288
|
-
</ToolbarButton>
|
|
289
|
-
</Tooltip>
|
|
290
|
-
<Tooltip content="Load from file" relationship="label">
|
|
291
|
-
<ToolbarButton icon={<FolderOpen24Regular />} onClick={handleLoadFile}>
|
|
292
|
-
Load
|
|
293
|
-
</ToolbarButton>
|
|
294
|
-
</Tooltip>
|
|
295
|
-
<Tooltip content="Reset to tree-generated XML" relationship="label">
|
|
296
|
-
<ToolbarButton icon={<ArrowReset24Regular />} onClick={handleReset}>
|
|
297
|
-
Reset
|
|
298
|
-
</ToolbarButton>
|
|
299
|
-
</Tooltip>
|
|
300
|
-
<ToolbarDivider />
|
|
301
|
-
<Tooltip content="Validate FetchXML syntax" relationship="label">
|
|
302
|
-
<ToolbarButton icon={<Checkmark24Regular />} onClick={handleValidate}>
|
|
303
|
-
Validate
|
|
304
|
-
</ToolbarButton>
|
|
305
|
-
</Tooltip>
|
|
306
|
-
<Tooltip content="Parse FetchXML into tree builder" relationship="label">
|
|
307
|
-
<ToolbarButton
|
|
308
|
-
icon={<TreeDeciduous24Regular />}
|
|
309
|
-
onClick={handleParseToTree}
|
|
310
|
-
disabled={!onParseToTree}
|
|
311
|
-
>
|
|
312
|
-
Parse to Tree
|
|
313
|
-
</ToolbarButton>
|
|
314
|
-
</Tooltip>
|
|
315
|
-
</Toolbar>
|
|
316
|
-
) : (
|
|
317
|
-
<Tooltip content="Copy FetchXML to clipboard" relationship="label">
|
|
318
|
-
<Button appearance="subtle" icon={<Copy24Regular />} onClick={handleCopy}>
|
|
319
|
-
Copy
|
|
320
|
-
</Button>
|
|
321
|
-
</Tooltip>
|
|
322
|
-
)}
|
|
323
|
-
</div>
|
|
324
|
-
<div className={styles.toolbarRight}>
|
|
325
|
-
<Switch
|
|
326
|
-
checked={isEditorMode}
|
|
327
|
-
onChange={(_, data) => handleToggleEditorMode(data.checked)}
|
|
328
|
-
label={<span className={styles.switchLabel}>Editor Mode</span>}
|
|
329
|
-
labelPosition="before"
|
|
330
|
-
/>
|
|
331
|
-
</div>
|
|
332
|
-
</div>
|
|
333
|
-
|
|
334
|
-
{/* Hidden file input */}
|
|
335
|
-
<input
|
|
336
|
-
ref={fileInputRef}
|
|
337
|
-
type="file"
|
|
338
|
-
accept=".xml,.fetchxml,.txt"
|
|
339
|
-
style={{ display: "none" }}
|
|
340
|
-
onChange={handleFileChange}
|
|
341
|
-
/>
|
|
342
|
-
|
|
343
|
-
{/* Editor */}
|
|
344
|
-
<div className={styles.editorWrapper}>
|
|
345
|
-
<Editor
|
|
346
|
-
height="100%"
|
|
347
|
-
language="xml"
|
|
348
|
-
theme={isDark ? "vs-dark" : "vs"}
|
|
349
|
-
value={currentXml}
|
|
350
|
-
onChange={isEditorMode ? (value) => setEditedXml(value || "") : undefined}
|
|
351
|
-
loading={
|
|
352
|
-
<div className={styles.loadingContainer}>
|
|
353
|
-
<Spinner size="medium" label="Loading editor..." />
|
|
354
|
-
</div>
|
|
355
|
-
}
|
|
356
|
-
options={{
|
|
357
|
-
readOnly: !isEditorMode,
|
|
358
|
-
minimap: { enabled: false },
|
|
359
|
-
scrollBeyondLastLine: false,
|
|
360
|
-
lineNumbers: "on",
|
|
361
|
-
folding: true,
|
|
362
|
-
wordWrap: "on",
|
|
363
|
-
automaticLayout: true,
|
|
364
|
-
fontFamily: "Consolas, 'Courier New', monospace",
|
|
365
|
-
fontSize: 13,
|
|
366
|
-
}}
|
|
367
|
-
onMount={handleEditorMount}
|
|
368
|
-
/>
|
|
369
|
-
</div>
|
|
370
|
-
|
|
371
|
-
{/* Messages */}
|
|
372
|
-
{messages.length > 0 && (
|
|
373
|
-
<div className={styles.messageBarContainer}>
|
|
374
|
-
<MessageBarGroup animate="both">
|
|
375
|
-
{messages.map((msg) => (
|
|
376
|
-
<MessageBar key={msg.id} intent={msg.intent} shape="rounded">
|
|
377
|
-
<MessageBarBody>
|
|
378
|
-
<MessageBarTitle>{msg.title}</MessageBarTitle>
|
|
379
|
-
{msg.body}
|
|
380
|
-
</MessageBarBody>
|
|
381
|
-
<MessageBarActions
|
|
382
|
-
containerAction={
|
|
383
|
-
<Button
|
|
384
|
-
onClick={() => dismissMessage(msg.id)}
|
|
385
|
-
aria-label="dismiss"
|
|
386
|
-
appearance="transparent"
|
|
387
|
-
icon={<Dismiss24Regular />}
|
|
388
|
-
size="small"
|
|
389
|
-
/>
|
|
390
|
-
}
|
|
391
|
-
/>
|
|
392
|
-
</MessageBar>
|
|
393
|
-
))}
|
|
394
|
-
</MessageBarGroup>
|
|
395
|
-
</div>
|
|
396
|
-
)}
|
|
397
|
-
</div>
|
|
398
|
-
);
|
|
399
|
-
}
|
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Utilities for extracting formatted values and column display names from OData annotated records
|
|
3
|
-
*
|
|
4
|
-
* OData Formatted Value Annotations:
|
|
5
|
-
* - Regular columns: {column}@OData.Community.Display.V1.FormattedValue
|
|
6
|
-
* - Lookup columns: _{column}_value@OData.Community.Display.V1.FormattedValue
|
|
7
|
-
* - Aliased columns: {alias}@OData.Community.Display.V1.FormattedValue
|
|
8
|
-
* - Attribute name: {column/alias}@OData.Community.Display.V1.AttributeName
|
|
9
|
-
*
|
|
10
|
-
* @see https://learn.microsoft.com/en-us/power-apps/developer/data-platform/fetchxml/formatted-values
|
|
11
|
-
* @see https://learn.microsoft.com/en-us/power-apps/developer/data-platform/fetchxml/column-comparison-aliases
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import type { AttributeMetadata } from "../../api/pptbClient";
|
|
15
|
-
|
|
16
|
-
const FORMATTED_VALUE_SUFFIX = "@OData.Community.Display.V1.FormattedValue";
|
|
17
|
-
const ATTRIBUTE_NAME_SUFFIX = "@OData.Community.Display.V1.AttributeName";
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Extracts the formatted value for a column from an OData annotated record.
|
|
21
|
-
* Handles regular columns, lookup columns, and aliased columns.
|
|
22
|
-
* Falls back to raw value if formatted value is not available.
|
|
23
|
-
*
|
|
24
|
-
* @param record - The OData record with annotations
|
|
25
|
-
* @param column - The column name (logical name or alias)
|
|
26
|
-
* @returns The formatted value if available, otherwise the raw value
|
|
27
|
-
*
|
|
28
|
-
* @example
|
|
29
|
-
* // Regular column
|
|
30
|
-
* getFormattedValue({ statuscode: 1, "statuscode@...FormattedValue": "Active" }, "statuscode")
|
|
31
|
-
* // Returns: "Active"
|
|
32
|
-
*
|
|
33
|
-
* @example
|
|
34
|
-
* // Lookup column
|
|
35
|
-
* getFormattedValue({ _ownerid_value: "guid", "_ownerid_value@...FormattedValue": "John Doe" }, "_ownerid_value")
|
|
36
|
-
* // Returns: "John Doe"
|
|
37
|
-
*
|
|
38
|
-
* @example
|
|
39
|
-
* // Aliased column
|
|
40
|
-
* getFormattedValue({ owner_name: "guid", "owner_name@...FormattedValue": "John Doe" }, "owner_name")
|
|
41
|
-
* // Returns: "John Doe"
|
|
42
|
-
*/
|
|
43
|
-
export function getFormattedValue(record: Record<string, unknown>, column: string): unknown {
|
|
44
|
-
if (!record || !column) return null;
|
|
45
|
-
|
|
46
|
-
// Try formatted value with exact column name
|
|
47
|
-
const formattedKey = `${column}${FORMATTED_VALUE_SUFFIX}`;
|
|
48
|
-
if (formattedKey in record) {
|
|
49
|
-
return record[formattedKey];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// For lookup columns that might be in _{column}_value format
|
|
53
|
-
// Check if this is already a lookup pattern or try adding it
|
|
54
|
-
if (column.startsWith("_") && column.endsWith("_value")) {
|
|
55
|
-
// Already in lookup format, formatted key already checked above
|
|
56
|
-
return record[column]; // Return raw value
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Try adding lookup wrapper
|
|
60
|
-
const lookupFormattedKey = `_${column}_value${FORMATTED_VALUE_SUFFIX}`;
|
|
61
|
-
if (lookupFormattedKey in record) {
|
|
62
|
-
return record[lookupFormattedKey];
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Fall back to raw value
|
|
66
|
-
return record[column];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Determines the display name for a column header.
|
|
71
|
-
* Tries in order:
|
|
72
|
-
* 1. AttributeName annotation from the record (for aliases)
|
|
73
|
-
* 2. DisplayName from AttributeMetadata
|
|
74
|
-
* 3. The raw column name
|
|
75
|
-
*
|
|
76
|
-
* @param column - The column name (logical name or alias)
|
|
77
|
-
* @param attributeMetadata - Optional attribute metadata map
|
|
78
|
-
* @param record - Optional record with OData annotations (for AttributeName)
|
|
79
|
-
* @returns The display name for the column
|
|
80
|
-
*
|
|
81
|
-
* @example
|
|
82
|
-
* // With AttributeName annotation (aliased column from link-entity)
|
|
83
|
-
* getColumnDisplayName("owner_name", metadata, { "owner_name@...AttributeName": "ownerid" })
|
|
84
|
-
* // Returns metadata display name for "ownerid"
|
|
85
|
-
*
|
|
86
|
-
* @example
|
|
87
|
-
* // With metadata only
|
|
88
|
-
* getColumnDisplayName("statuscode", metadata)
|
|
89
|
-
* // Returns: "Status Reason" (from metadata)
|
|
90
|
-
*
|
|
91
|
-
* @example
|
|
92
|
-
* // Fallback to column name
|
|
93
|
-
* getColumnDisplayName("customfield", undefined, undefined)
|
|
94
|
-
* // Returns: "customfield"
|
|
95
|
-
*/
|
|
96
|
-
export function getColumnDisplayName(
|
|
97
|
-
column: string,
|
|
98
|
-
attributeMetadata?: Map<string, AttributeMetadata>,
|
|
99
|
-
record?: Record<string, unknown>
|
|
100
|
-
): string {
|
|
101
|
-
if (!column) return "";
|
|
102
|
-
|
|
103
|
-
// Try to get the original attribute name from OData annotation (for aliased columns)
|
|
104
|
-
let originalAttributeName = column;
|
|
105
|
-
if (record) {
|
|
106
|
-
const attributeNameKey = `${column}${ATTRIBUTE_NAME_SUFFIX}`;
|
|
107
|
-
if (attributeNameKey in record && typeof record[attributeNameKey] === "string") {
|
|
108
|
-
originalAttributeName = record[attributeNameKey] as string;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Try to get display name from metadata
|
|
113
|
-
if (attributeMetadata) {
|
|
114
|
-
// First try the original attribute name (in case it's an alias)
|
|
115
|
-
const metadataByOriginal = attributeMetadata.get(originalAttributeName);
|
|
116
|
-
if (metadataByOriginal?.DisplayName?.UserLocalizedLabel?.Label) {
|
|
117
|
-
return metadataByOriginal.DisplayName.UserLocalizedLabel.Label;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Then try the column name directly
|
|
121
|
-
const metadataByColumn = attributeMetadata.get(column);
|
|
122
|
-
if (metadataByColumn?.DisplayName?.UserLocalizedLabel?.Label) {
|
|
123
|
-
return metadataByColumn.DisplayName.UserLocalizedLabel.Label;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// For lookup columns in _{name}_value format, try extracting the base name
|
|
127
|
-
if (column.startsWith("_") && column.endsWith("_value")) {
|
|
128
|
-
const baseName = column.slice(1, -6); // Remove _ prefix and _value suffix
|
|
129
|
-
const metadataByBase = attributeMetadata.get(baseName);
|
|
130
|
-
if (metadataByBase?.DisplayName?.UserLocalizedLabel?.Label) {
|
|
131
|
-
return metadataByBase.DisplayName.UserLocalizedLabel.Label;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Fall back to the column name
|
|
137
|
-
// For lookup columns, show a cleaner version
|
|
138
|
-
if (column.startsWith("_") && column.endsWith("_value")) {
|
|
139
|
-
return column.slice(1, -6); // Remove _ prefix and _value suffix
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return column;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Helper to check if a record has any formatted values available.
|
|
147
|
-
* Useful for optimization - if no formatted values exist, can skip formatting logic.
|
|
148
|
-
*
|
|
149
|
-
* @param record - The OData record to check
|
|
150
|
-
* @returns true if the record has at least one formatted value annotation
|
|
151
|
-
*/
|
|
152
|
-
export function hasFormattedValues(record: Record<string, unknown>): boolean {
|
|
153
|
-
if (!record) return false;
|
|
154
|
-
return Object.keys(record).some((key) => key.endsWith(FORMATTED_VALUE_SUFFIX));
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Checks if a column name is an OData or CRM annotation that should be hidden from display.
|
|
159
|
-
* Filters out:
|
|
160
|
-
* - @OData.Community.Display.V1.FormattedValue
|
|
161
|
-
* - @OData.Community.Display.V1.AttributeName
|
|
162
|
-
* - @Microsoft.Dynamics.CRM.associatednavigationproperty
|
|
163
|
-
* - @Microsoft.Dynamics.CRM.lookuplogicalname
|
|
164
|
-
* - etc.
|
|
165
|
-
*
|
|
166
|
-
* @param columnName - The column name to check
|
|
167
|
-
* @returns true if this is an annotation column that should be hidden
|
|
168
|
-
*/
|
|
169
|
-
export function isAnnotationColumn(columnName: string): boolean {
|
|
170
|
-
if (!columnName) return false;
|
|
171
|
-
return (
|
|
172
|
-
columnName.includes("@OData.") ||
|
|
173
|
-
columnName.includes("@Microsoft.Dynamics.CRM.") ||
|
|
174
|
-
columnName.includes("@odata.") ||
|
|
175
|
-
columnName.includes("@microsoft.dynamics.crm.")
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Filters out annotation columns from a list of column names.
|
|
181
|
-
* Returns only the actual data columns that should be displayed in the grid.
|
|
182
|
-
*
|
|
183
|
-
* @param columns - Array of column names from query results
|
|
184
|
-
* @returns Filtered array with only displayable columns
|
|
185
|
-
*/
|
|
186
|
-
export function filterDisplayableColumns(columns: string[]): string[] {
|
|
187
|
-
if (!columns) return [];
|
|
188
|
-
return columns.filter((col) => !isAnnotationColumn(col));
|
|
189
|
-
}
|