@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.
- package/LICENSE +661 -0
- package/README.md +1 -1
- 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
|
Binary file
|
package/src/assets/react.svg
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Lazy metadata loading with caching and promise memoization
|
|
3
|
-
* Prevents duplicate API calls for the same entity
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import * as pptbClient from "../api/pptbClient";
|
|
7
|
-
import { metadataCache } from "../state/cache";
|
|
8
|
-
import type { EntityMetadata, AttributeMetadata, RelationshipMetadata } from "../api/pptbClient";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Load all entities (with caching and de-duplication)
|
|
12
|
-
*/
|
|
13
|
-
export async function loadAllEntities(advancedFindOnly: boolean = true): Promise<EntityMetadata[]> {
|
|
14
|
-
// Check cache first
|
|
15
|
-
const cached = metadataCache.getAllEntities();
|
|
16
|
-
if (cached) {
|
|
17
|
-
return cached;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Check if request is already in-flight
|
|
21
|
-
const inFlight = metadataCache.getAllEntitiesPromise();
|
|
22
|
-
if (inFlight) {
|
|
23
|
-
return inFlight;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Make new request
|
|
27
|
-
const promise = pptbClient.getAllEntities(advancedFindOnly);
|
|
28
|
-
metadataCache.setAllEntitiesPromise(promise);
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
const entities = await promise;
|
|
32
|
-
metadataCache.setAllEntities(entities);
|
|
33
|
-
metadataCache.clearAllEntitiesPromise();
|
|
34
|
-
return entities;
|
|
35
|
-
} catch (error) {
|
|
36
|
-
console.error("loadAllEntities: API error:", error);
|
|
37
|
-
metadataCache.clearAllEntitiesPromise();
|
|
38
|
-
throw error;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Load entity metadata (with caching)
|
|
44
|
-
*/
|
|
45
|
-
export async function loadEntityMetadata(logicalName: string): Promise<EntityMetadata> {
|
|
46
|
-
// Check cache first
|
|
47
|
-
const cached = metadataCache.getEntityMetadata(logicalName);
|
|
48
|
-
if (cached) {
|
|
49
|
-
return cached;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Make request
|
|
53
|
-
const metadata = await pptbClient.getEntityMetadata(logicalName);
|
|
54
|
-
metadataCache.setEntityMetadata(logicalName, metadata);
|
|
55
|
-
return metadata;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Load entity attributes (with caching and de-duplication)
|
|
60
|
-
* @param logicalName - Entity logical name
|
|
61
|
-
* @param advancedFindOnly - Filter to only attributes valid for advanced find (default: true)
|
|
62
|
-
*/
|
|
63
|
-
export async function loadEntityAttributes(
|
|
64
|
-
logicalName: string,
|
|
65
|
-
advancedFindOnly: boolean = true
|
|
66
|
-
): Promise<AttributeMetadata[]> {
|
|
67
|
-
// Check cache first
|
|
68
|
-
const cacheKey = `${logicalName}_${advancedFindOnly}`;
|
|
69
|
-
const cached = metadataCache.getEntityAttributes(cacheKey);
|
|
70
|
-
if (cached) {
|
|
71
|
-
return cached;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Check if request is already in-flight
|
|
75
|
-
const inFlight = metadataCache.getAttributesPromise(cacheKey);
|
|
76
|
-
if (inFlight) {
|
|
77
|
-
return inFlight;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Make new request
|
|
81
|
-
const promise = pptbClient.getEntityAttributes(logicalName, advancedFindOnly);
|
|
82
|
-
metadataCache.setAttributesPromise(cacheKey, promise);
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
const attributes = await promise;
|
|
86
|
-
metadataCache.setEntityAttributes(cacheKey, attributes);
|
|
87
|
-
metadataCache.clearAttributesPromise(cacheKey);
|
|
88
|
-
return attributes;
|
|
89
|
-
} catch (error) {
|
|
90
|
-
metadataCache.clearAttributesPromise(cacheKey);
|
|
91
|
-
throw error;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Load a single attribute with full metadata including OptionSet expansion
|
|
97
|
-
* Used for loading picklist/boolean options for value pickers
|
|
98
|
-
* @param entityLogicalName - Entity logical name
|
|
99
|
-
* @param attributeLogicalName - Attribute logical name
|
|
100
|
-
*/
|
|
101
|
-
export async function loadAttributeWithOptionSet(
|
|
102
|
-
entityLogicalName: string,
|
|
103
|
-
attributeLogicalName: string
|
|
104
|
-
): Promise<AttributeMetadata> {
|
|
105
|
-
// Check cache first
|
|
106
|
-
const cacheKey = `${entityLogicalName}_${attributeLogicalName}_optionset`;
|
|
107
|
-
const cached = metadataCache.getEntityAttributes(cacheKey);
|
|
108
|
-
if (cached && cached.length > 0) {
|
|
109
|
-
return cached[0];
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Make request with OptionSet expansion
|
|
113
|
-
const attribute = await pptbClient.getAttributeWithOptionSet(
|
|
114
|
-
entityLogicalName,
|
|
115
|
-
attributeLogicalName
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
// Cache the result
|
|
119
|
-
metadataCache.setEntityAttributes(cacheKey, [attribute]);
|
|
120
|
-
return attribute;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Load a single attribute with detailed type-specific metadata (MinValue, MaxValue, Precision, Format)
|
|
125
|
-
* Used for numeric and datetime attributes that need validation constraints
|
|
126
|
-
* @param entityLogicalName - Entity logical name
|
|
127
|
-
* @param attributeLogicalName - Attribute logical name
|
|
128
|
-
* @param attributeType - Attribute type (Integer, BigInt, Decimal, Double, DateTime)
|
|
129
|
-
*/
|
|
130
|
-
export async function loadAttributeDetailedMetadata(
|
|
131
|
-
entityLogicalName: string,
|
|
132
|
-
attributeLogicalName: string,
|
|
133
|
-
attributeType: string
|
|
134
|
-
): Promise<AttributeMetadata> {
|
|
135
|
-
// Check cache first
|
|
136
|
-
const cacheKey = `${entityLogicalName}_${attributeLogicalName}_detailed`;
|
|
137
|
-
const cached = metadataCache.getEntityAttributes(cacheKey);
|
|
138
|
-
if (cached && cached.length > 0) {
|
|
139
|
-
return cached[0];
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Make request with type casting to get detailed properties
|
|
143
|
-
const attribute = await pptbClient.getAttributeDetailedMetadata(
|
|
144
|
-
entityLogicalName,
|
|
145
|
-
attributeLogicalName,
|
|
146
|
-
attributeType
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
// Cache the result
|
|
150
|
-
metadataCache.setEntityAttributes(cacheKey, [attribute]);
|
|
151
|
-
return attribute;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Load entity relationships (with caching)
|
|
156
|
-
* @param logicalName - Entity logical name
|
|
157
|
-
* @param advancedFindOnly - Filter to only relationships valid for advanced find (default: true)
|
|
158
|
-
*/
|
|
159
|
-
export async function loadEntityRelationships(
|
|
160
|
-
logicalName: string,
|
|
161
|
-
advancedFindOnly: boolean = true
|
|
162
|
-
): Promise<{
|
|
163
|
-
oneToMany: RelationshipMetadata[];
|
|
164
|
-
manyToOne: RelationshipMetadata[];
|
|
165
|
-
manyToMany: RelationshipMetadata[];
|
|
166
|
-
}> {
|
|
167
|
-
// Check cache first
|
|
168
|
-
const cacheKey = `${logicalName}_${advancedFindOnly}`;
|
|
169
|
-
const cached = metadataCache.getEntityRelationships(cacheKey);
|
|
170
|
-
if (cached) {
|
|
171
|
-
return cached;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Make request
|
|
175
|
-
const relationships = await pptbClient.getEntityRelationships(logicalName, advancedFindOnly);
|
|
176
|
-
metadataCache.setEntityRelationships(cacheKey, relationships);
|
|
177
|
-
return relationships;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Clear all metadata cache
|
|
182
|
-
*/
|
|
183
|
-
export function clearMetadataCache(): void {
|
|
184
|
-
metadataCache.clear();
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Clear cache for specific entity
|
|
189
|
-
*/
|
|
190
|
-
export function clearEntityCache(logicalName: string): void {
|
|
191
|
-
metadataCache.clearEntity(logicalName);
|
|
192
|
-
}
|
|
@@ -1,318 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Local Excel export using exceljs library
|
|
3
|
-
* Exports query results with native Excel data types (numbers, dates, etc.)
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import ExcelJS from "exceljs";
|
|
7
|
-
import type { AttributeMetadata } from "./pptbClient";
|
|
8
|
-
import { getFormattedValue } from "../ui/RightPane/FormattedValueUtils";
|
|
9
|
-
import type { DisplaySettings } from "../model/displaySettings";
|
|
10
|
-
|
|
11
|
-
interface ExportToExcelLocalOptions {
|
|
12
|
-
/** Records to export */
|
|
13
|
-
records: Record<string, unknown>[];
|
|
14
|
-
/** Column keys in the order they should appear */
|
|
15
|
-
columns: string[];
|
|
16
|
-
/** Map of column key to display name for header */
|
|
17
|
-
columnDisplayNames: Map<string, string>;
|
|
18
|
-
/** Multi-entity attribute metadata for type conversion */
|
|
19
|
-
attributeMetadata?: Map<string, Map<string, AttributeMetadata>>;
|
|
20
|
-
/** Entity logical name for single-entity metadata lookup */
|
|
21
|
-
entityName?: string;
|
|
22
|
-
/** Filename without extension */
|
|
23
|
-
fileName?: string;
|
|
24
|
-
/** Display settings for column headers and value modes */
|
|
25
|
-
displaySettings?: DisplaySettings;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Export records to Excel with native data types
|
|
30
|
-
* Returns the workbook buffer for download
|
|
31
|
-
*/
|
|
32
|
-
export async function exportToExcelLocal(
|
|
33
|
-
options: ExportToExcelLocalOptions
|
|
34
|
-
): Promise<{ buffer: ArrayBuffer; fileName: string }> {
|
|
35
|
-
const {
|
|
36
|
-
records,
|
|
37
|
-
columns,
|
|
38
|
-
columnDisplayNames,
|
|
39
|
-
attributeMetadata,
|
|
40
|
-
entityName,
|
|
41
|
-
fileName = "export",
|
|
42
|
-
displaySettings,
|
|
43
|
-
} = options;
|
|
44
|
-
|
|
45
|
-
// Extract display settings with defaults
|
|
46
|
-
const useLogicalNames = displaySettings?.useLogicalNames ?? false;
|
|
47
|
-
const valueDisplayMode = displaySettings?.valueDisplayMode ?? "formatted";
|
|
48
|
-
|
|
49
|
-
const workbook = new ExcelJS.Workbook();
|
|
50
|
-
workbook.creator = "FetchXML Studio";
|
|
51
|
-
workbook.created = new Date();
|
|
52
|
-
|
|
53
|
-
const worksheet = workbook.addWorksheet("Data");
|
|
54
|
-
|
|
55
|
-
// Helper to get attribute metadata
|
|
56
|
-
const getAttrMeta = (col: string): AttributeMetadata | undefined => {
|
|
57
|
-
if (!attributeMetadata) return undefined;
|
|
58
|
-
|
|
59
|
-
// Try with entity name first
|
|
60
|
-
if (entityName) {
|
|
61
|
-
const entityAttrs = attributeMetadata.get(entityName);
|
|
62
|
-
if (entityAttrs) {
|
|
63
|
-
// Direct column name
|
|
64
|
-
if (entityAttrs.has(col)) return entityAttrs.get(col);
|
|
65
|
-
// Lookup field (_attr_value -> attr)
|
|
66
|
-
if (col.startsWith("_") && col.endsWith("_value")) {
|
|
67
|
-
const baseAttr = col.slice(1, -6);
|
|
68
|
-
if (entityAttrs.has(baseAttr)) return entityAttrs.get(baseAttr);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Try to find in any entity's metadata
|
|
74
|
-
for (const [, entityAttrs] of attributeMetadata) {
|
|
75
|
-
if (entityAttrs.has(col)) return entityAttrs.get(col);
|
|
76
|
-
if (col.startsWith("_") && col.endsWith("_value")) {
|
|
77
|
-
const baseAttr = col.slice(1, -6);
|
|
78
|
-
if (entityAttrs.has(baseAttr)) return entityAttrs.get(baseAttr);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return undefined;
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
// Helper to check if a column has formatted values in the data
|
|
86
|
-
const hasFormattedValues = (col: string): boolean => {
|
|
87
|
-
return records.some((record) => {
|
|
88
|
-
const rawValue = record[col];
|
|
89
|
-
const formattedValue = getFormattedValue(record, col);
|
|
90
|
-
return formattedValue !== undefined && formattedValue !== rawValue;
|
|
91
|
-
});
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
// Build export columns list - for "both" mode, add raw columns where formatted values exist
|
|
95
|
-
interface ExportColumn {
|
|
96
|
-
sourceCol: string;
|
|
97
|
-
headerName: string;
|
|
98
|
-
isRawColumn: boolean;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const exportColumns: ExportColumn[] = [];
|
|
102
|
-
for (const col of columns) {
|
|
103
|
-
// Determine header name based on useLogicalNames setting
|
|
104
|
-
const displayName = columnDisplayNames.get(col) || col;
|
|
105
|
-
const headerName = useLogicalNames ? col : displayName;
|
|
106
|
-
|
|
107
|
-
if (valueDisplayMode === "both" && hasFormattedValues(col)) {
|
|
108
|
-
// Add formatted column first, then raw column
|
|
109
|
-
exportColumns.push({ sourceCol: col, headerName, isRawColumn: false });
|
|
110
|
-
exportColumns.push({ sourceCol: col, headerName: `${headerName} (Raw)`, isRawColumn: true });
|
|
111
|
-
} else {
|
|
112
|
-
// Just one column
|
|
113
|
-
exportColumns.push({ sourceCol: col, headerName, isRawColumn: false });
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Create header row
|
|
118
|
-
const headerRow = worksheet.addRow(exportColumns.map((ec) => ec.headerName));
|
|
119
|
-
headerRow.font = { bold: true };
|
|
120
|
-
headerRow.fill = {
|
|
121
|
-
type: "pattern",
|
|
122
|
-
pattern: "solid",
|
|
123
|
-
fgColor: { argb: "FFE0E0E0" },
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
// Set column widths based on attribute type and header length
|
|
127
|
-
worksheet.columns = exportColumns.map((ec, index) => {
|
|
128
|
-
const attr = getAttrMeta(ec.sourceCol);
|
|
129
|
-
let width = Math.max(ec.headerName.length + 2, 10);
|
|
130
|
-
|
|
131
|
-
// Adjust width based on attribute type
|
|
132
|
-
switch (attr?.AttributeType) {
|
|
133
|
-
case "DateTime":
|
|
134
|
-
width = Math.max(width, 18);
|
|
135
|
-
break;
|
|
136
|
-
case "Money":
|
|
137
|
-
case "Decimal":
|
|
138
|
-
case "Double":
|
|
139
|
-
width = Math.max(width, 14);
|
|
140
|
-
break;
|
|
141
|
-
case "Lookup":
|
|
142
|
-
case "Customer":
|
|
143
|
-
case "Owner":
|
|
144
|
-
width = Math.max(width, 25);
|
|
145
|
-
break;
|
|
146
|
-
case "Memo":
|
|
147
|
-
width = Math.max(width, 40);
|
|
148
|
-
break;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return { key: `col_${index}`, width };
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
// Helper to get cell value based on display mode and column type
|
|
155
|
-
const getCellValue = (
|
|
156
|
-
record: Record<string, unknown>,
|
|
157
|
-
col: string,
|
|
158
|
-
isRawColumn: boolean,
|
|
159
|
-
attr: AttributeMetadata | undefined
|
|
160
|
-
): unknown => {
|
|
161
|
-
const rawValue = record[col];
|
|
162
|
-
const formattedValue = getFormattedValue(record, col);
|
|
163
|
-
|
|
164
|
-
// Handle null/undefined
|
|
165
|
-
if (rawValue === null || rawValue === undefined) {
|
|
166
|
-
return "";
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// For raw columns or raw display mode, return raw value with type conversion
|
|
170
|
-
if (isRawColumn || valueDisplayMode === "raw") {
|
|
171
|
-
switch (attr?.AttributeType) {
|
|
172
|
-
case "Integer":
|
|
173
|
-
case "BigInt":
|
|
174
|
-
return typeof rawValue === "number" ? rawValue : parseInt(String(rawValue), 10);
|
|
175
|
-
case "Decimal":
|
|
176
|
-
case "Double":
|
|
177
|
-
case "Money":
|
|
178
|
-
return typeof rawValue === "number" ? rawValue : parseFloat(String(rawValue));
|
|
179
|
-
case "DateTime":
|
|
180
|
-
if (rawValue instanceof Date) return rawValue;
|
|
181
|
-
if (typeof rawValue === "string") {
|
|
182
|
-
const date = new Date(rawValue);
|
|
183
|
-
return isNaN(date.getTime()) ? rawValue : date;
|
|
184
|
-
}
|
|
185
|
-
return rawValue;
|
|
186
|
-
case "Boolean":
|
|
187
|
-
return rawValue;
|
|
188
|
-
default:
|
|
189
|
-
return rawValue;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// For formatted or "both" mode (main column), return formatted values
|
|
194
|
-
switch (attr?.AttributeType) {
|
|
195
|
-
case "Boolean":
|
|
196
|
-
return formattedValue || (rawValue ? "Yes" : "No");
|
|
197
|
-
|
|
198
|
-
case "Integer":
|
|
199
|
-
case "BigInt":
|
|
200
|
-
// Numbers still get typed, but formatted value is used if different
|
|
201
|
-
return typeof rawValue === "number" ? rawValue : parseInt(String(rawValue), 10);
|
|
202
|
-
|
|
203
|
-
case "Decimal":
|
|
204
|
-
case "Double":
|
|
205
|
-
case "Money":
|
|
206
|
-
return typeof rawValue === "number" ? rawValue : parseFloat(String(rawValue));
|
|
207
|
-
|
|
208
|
-
case "DateTime":
|
|
209
|
-
// Return as Date object for Excel to format properly
|
|
210
|
-
if (rawValue instanceof Date) return rawValue;
|
|
211
|
-
if (typeof rawValue === "string") {
|
|
212
|
-
const date = new Date(rawValue);
|
|
213
|
-
return isNaN(date.getTime()) ? rawValue : date;
|
|
214
|
-
}
|
|
215
|
-
return rawValue;
|
|
216
|
-
|
|
217
|
-
case "Picklist":
|
|
218
|
-
case "State":
|
|
219
|
-
case "Status":
|
|
220
|
-
// Return formatted value (label) for choice fields
|
|
221
|
-
return formattedValue || rawValue;
|
|
222
|
-
|
|
223
|
-
case "Lookup":
|
|
224
|
-
case "Customer":
|
|
225
|
-
case "Owner":
|
|
226
|
-
// Return formatted value (name) for lookup fields
|
|
227
|
-
return formattedValue || rawValue;
|
|
228
|
-
|
|
229
|
-
default:
|
|
230
|
-
// For strings and unknown types
|
|
231
|
-
return formattedValue !== undefined && formattedValue !== rawValue
|
|
232
|
-
? formattedValue
|
|
233
|
-
: String(rawValue);
|
|
234
|
-
}
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
// Add data rows with proper types
|
|
238
|
-
for (const record of records) {
|
|
239
|
-
const rowValues: unknown[] = exportColumns.map((ec) => {
|
|
240
|
-
const attr = getAttrMeta(ec.sourceCol);
|
|
241
|
-
return getCellValue(record, ec.sourceCol, ec.isRawColumn, attr);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
const dataRow = worksheet.addRow(rowValues);
|
|
245
|
-
|
|
246
|
-
// Apply number format based on attribute type (only for non-raw columns in formatted mode)
|
|
247
|
-
exportColumns.forEach((ec, index) => {
|
|
248
|
-
const attr = getAttrMeta(ec.sourceCol);
|
|
249
|
-
const cell = dataRow.getCell(index + 1);
|
|
250
|
-
|
|
251
|
-
// Skip formatting for raw columns (show actual values)
|
|
252
|
-
if (ec.isRawColumn) return;
|
|
253
|
-
|
|
254
|
-
switch (attr?.AttributeType) {
|
|
255
|
-
case "DateTime":
|
|
256
|
-
cell.numFmt = "yyyy-mm-dd hh:mm:ss";
|
|
257
|
-
break;
|
|
258
|
-
case "Money":
|
|
259
|
-
// Use currency format
|
|
260
|
-
cell.numFmt = '"$"#,##0.00';
|
|
261
|
-
break;
|
|
262
|
-
case "Decimal":
|
|
263
|
-
case "Double": {
|
|
264
|
-
// Get precision from metadata or default to 2
|
|
265
|
-
const precision = (attr as { Precision?: number })?.Precision ?? 2;
|
|
266
|
-
cell.numFmt = `0.${"0".repeat(precision)}`;
|
|
267
|
-
break;
|
|
268
|
-
}
|
|
269
|
-
case "Integer":
|
|
270
|
-
case "BigInt":
|
|
271
|
-
cell.numFmt = "#,##0";
|
|
272
|
-
break;
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Freeze the header row
|
|
278
|
-
worksheet.views = [{ state: "frozen", ySplit: 1 }];
|
|
279
|
-
|
|
280
|
-
// Auto-filter
|
|
281
|
-
if (exportColumns.length > 0) {
|
|
282
|
-
worksheet.autoFilter = {
|
|
283
|
-
from: { row: 1, column: 1 },
|
|
284
|
-
to: { row: records.length + 1, column: exportColumns.length },
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Generate buffer
|
|
289
|
-
const buffer = await workbook.xlsx.writeBuffer();
|
|
290
|
-
// Include full timestamp (date + time) to avoid overwrite prompts on repeated exports
|
|
291
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
292
|
-
const finalFileName = `${fileName}_${timestamp}.xlsx`;
|
|
293
|
-
|
|
294
|
-
return {
|
|
295
|
-
buffer: buffer as ArrayBuffer,
|
|
296
|
-
fileName: finalFileName,
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Trigger browser download of the Excel file
|
|
302
|
-
*/
|
|
303
|
-
export function downloadExcelFile(buffer: ArrayBuffer, fileName: string): void {
|
|
304
|
-
const blob = new Blob([buffer], {
|
|
305
|
-
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
306
|
-
});
|
|
307
|
-
const url = URL.createObjectURL(blob);
|
|
308
|
-
|
|
309
|
-
const link = document.createElement("a");
|
|
310
|
-
link.href = url;
|
|
311
|
-
link.download = fileName;
|
|
312
|
-
document.body.appendChild(link);
|
|
313
|
-
link.click();
|
|
314
|
-
|
|
315
|
-
// Cleanup
|
|
316
|
-
document.body.removeChild(link);
|
|
317
|
-
URL.revokeObjectURL(url);
|
|
318
|
-
}
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Utilities for working with Dataverse formatted values
|
|
3
|
-
* Formatted values come from Web API with @OData.Community.Display.V1.FormattedValue annotations
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Get the formatted value for a property if available
|
|
8
|
-
* Falls back to raw value if no formatted value exists
|
|
9
|
-
* Handles both regular properties and lookup properties (with _propertyname_value pattern)
|
|
10
|
-
*
|
|
11
|
-
* @example
|
|
12
|
-
* // Record from API:
|
|
13
|
-
* // {
|
|
14
|
-
* // "statuscode": 1,
|
|
15
|
-
* // "statuscode@OData.Community.Display.V1.FormattedValue": "Active",
|
|
16
|
-
* // "createdon": "2024-01-15T10:30:00Z",
|
|
17
|
-
* // "createdon@OData.Community.Display.V1.FormattedValue": "1/15/2024 10:30 AM",
|
|
18
|
-
* // "_createdby_value": "guid-here",
|
|
19
|
-
* // "_createdby_value@OData.Community.Display.V1.FormattedValue": "John Doe"
|
|
20
|
-
* // }
|
|
21
|
-
*
|
|
22
|
-
* getFormattedValue(record, "statuscode") // Returns "Active"
|
|
23
|
-
* getFormattedValue(record, "createdon") // Returns "1/15/2024 10:30 AM"
|
|
24
|
-
* getFormattedValue(record, "_createdby_value") // Returns "John Doe"
|
|
25
|
-
* getFormattedValue(record, "name") // Returns raw name value
|
|
26
|
-
*/
|
|
27
|
-
export function getFormattedValue(
|
|
28
|
-
record: Record<string, unknown>,
|
|
29
|
-
propertyName: string
|
|
30
|
-
): unknown {
|
|
31
|
-
const formattedKey = `${propertyName}@OData.Community.Display.V1.FormattedValue`;
|
|
32
|
-
|
|
33
|
-
// Return formatted value if it exists, otherwise return raw value
|
|
34
|
-
if (formattedKey in record && record[formattedKey] !== null && record[formattedKey] !== undefined) {
|
|
35
|
-
return record[formattedKey];
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return record[propertyName];
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Check if a property has a formatted value annotation
|
|
43
|
-
*/
|
|
44
|
-
export function hasFormattedValue(
|
|
45
|
-
record: Record<string, unknown>,
|
|
46
|
-
propertyName: string
|
|
47
|
-
): boolean {
|
|
48
|
-
const formattedKey = `${propertyName}@OData.Community.Display.V1.FormattedValue`;
|
|
49
|
-
return formattedKey in record;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Get both raw and formatted values for a property
|
|
54
|
-
*/
|
|
55
|
-
export function getValuePair(
|
|
56
|
-
record: Record<string, unknown>,
|
|
57
|
-
propertyName: string
|
|
58
|
-
): {
|
|
59
|
-
raw: unknown;
|
|
60
|
-
formatted?: unknown;
|
|
61
|
-
} {
|
|
62
|
-
const raw = record[propertyName];
|
|
63
|
-
const formattedKey = `${propertyName}@OData.Community.Display.V1.FormattedValue`;
|
|
64
|
-
const formatted = record[formattedKey];
|
|
65
|
-
|
|
66
|
-
return {
|
|
67
|
-
raw,
|
|
68
|
-
formatted: formatted !== null && formatted !== undefined ? formatted : undefined,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Transform a record to use formatted values where available
|
|
74
|
-
* Creates a new object with formatted values replacing raw values
|
|
75
|
-
* Useful for displaying data in grids
|
|
76
|
-
*/
|
|
77
|
-
export function transformRecordWithFormattedValues(
|
|
78
|
-
record: Record<string, unknown>
|
|
79
|
-
): Record<string, unknown> {
|
|
80
|
-
const transformed: Record<string, unknown> = {};
|
|
81
|
-
|
|
82
|
-
// Get all property names (excluding OData annotations)
|
|
83
|
-
const propertyNames = Object.keys(record).filter(
|
|
84
|
-
(key) => !key.includes("@OData") && !key.includes("@Microsoft")
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
for (const prop of propertyNames) {
|
|
88
|
-
transformed[prop] = getFormattedValue(record, prop);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return transformed;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Check if the response includes formatted values
|
|
96
|
-
* Useful for detecting if the API was called with Prefer: odata.include-annotations="*"
|
|
97
|
-
*/
|
|
98
|
-
export function responseHasFormattedValues(
|
|
99
|
-
records: Record<string, unknown>[]
|
|
100
|
-
): boolean {
|
|
101
|
-
if (records.length === 0) return false;
|
|
102
|
-
|
|
103
|
-
const firstRecord = records[0];
|
|
104
|
-
const keys = Object.keys(firstRecord);
|
|
105
|
-
|
|
106
|
-
return keys.some((key) => key.includes("@OData.Community.Display.V1.FormattedValue"));
|
|
107
|
-
}
|