@mohsinonxrm/pptb-fetchxml-studio 1.0.0 → 1.0.2

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