@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,1160 +0,0 @@
1
- /**
2
- * Builder state management with React Context and useReducer
3
- * Manages FetchXML query structure and tree operations
4
- */
5
-
6
- import { createContext, useContext, useReducer, type ReactNode } from "react";
7
- import type {
8
- FetchNode,
9
- EntityNode,
10
- AttributeNode,
11
- AllAttributesNode,
12
- OrderNode,
13
- FilterNode,
14
- ConditionNode,
15
- LinkEntityNode,
16
- NodeId,
17
- } from "../model/nodes";
18
- import type { LayoutXmlConfig } from "../model/layoutxml";
19
- import {
20
- generateLayoutFromFetchXml,
21
- mergeLayoutWithFetchXml,
22
- updateColumnWidth as updateLayoutColumnWidth,
23
- reorderColumns as reorderLayoutColumns,
24
- parseLayoutXml,
25
- } from "../model/layoutxml";
26
- import { parseFetchXml, type ParseResult } from "../model/fetchxmlParser";
27
- import { generateFetchXml } from "../model/fetchxml";
28
-
29
- // Temporary ID generator
30
- let idCounter = 0;
31
- const generateId = (): string => `node_${++idCounter}`;
32
-
33
- type SelectedNode =
34
- | FetchNode
35
- | EntityNode
36
- | AttributeNode
37
- | AllAttributesNode
38
- | OrderNode
39
- | FilterNode
40
- | ConditionNode
41
- | LinkEntityNode
42
- | null;
43
-
44
- /**
45
- * Information about a loaded view for execution optimization
46
- * Used to determine if the view can be executed via savedQuery/userQuery
47
- */
48
- interface LoadedViewState {
49
- /** View ID (savedqueryid or userqueryid) */
50
- id: string;
51
- /** View type for determining execution method */
52
- type: "system" | "personal";
53
- /** Original FetchXML from the view - used for comparison */
54
- originalFetchXml: string;
55
- /** Entity set name for the execution URL */
56
- entitySetName: string;
57
- /** View name for display purposes */
58
- name: string;
59
- }
60
-
61
- interface BuilderState {
62
- fetchQuery: FetchNode | null;
63
- selectedNodeId: NodeId | null;
64
- selectedNode: SelectedNode;
65
- /** Loaded view info - null if query was not loaded from a view or has been modified */
66
- loadedView: LoadedViewState | null;
67
- /** Column layout configuration - generated from FetchXML or loaded from view */
68
- columnConfig: LayoutXmlConfig | null;
69
- /** Track if layout needs regeneration due to FetchXML changes */
70
- layoutNeedsSync: boolean;
71
- /** When enabled, execution fetches all pages of results progressively */
72
- retrieveAllRecords: boolean;
73
- }
74
-
75
- type BuilderAction =
76
- | { type: "SET_ENTITY"; entityName: string }
77
- | { type: "SELECT_NODE"; nodeId: NodeId }
78
- | { type: "ADD_ATTRIBUTE"; parentId: NodeId }
79
- | { type: "ADD_ATTRIBUTE_BY_NAME"; parentId: NodeId; attributeName: string }
80
- | { type: "ADD_ALL_ATTRIBUTES"; parentId: NodeId }
81
- | { type: "ADD_ORDER"; parentId: NodeId }
82
- | { type: "ADD_FILTER"; parentId: NodeId }
83
- | { type: "ADD_SUBFILTER"; filterId: NodeId }
84
- | { type: "ADD_CONDITION"; filterId: NodeId }
85
- | { type: "ADD_LINK_ENTITY"; parentId: NodeId }
86
- | {
87
- type: "ADD_LINK_ENTITY_WITH_CONFIG";
88
- id: NodeId;
89
- parentId: NodeId;
90
- name: string;
91
- from: string;
92
- to: string;
93
- linkType: "inner" | "outer";
94
- relationshipType?: "N1" | "1N" | "NN";
95
- }
96
- | { type: "REMOVE_NODE"; nodeId: NodeId }
97
- | { type: "UPDATE_NODE"; nodeId: NodeId; updates: Record<string, unknown> }
98
- | { type: "NEW_QUERY" }
99
- | { type: "LOAD_FETCHXML"; fetchNode: FetchNode }
100
- | { type: "LOAD_VIEW"; fetchNode: FetchNode; viewInfo: LoadedViewState; layoutXml?: string }
101
- | { type: "CLEAR_LOADED_VIEW" }
102
- | {
103
- type: "SET_LOADED_VIEW";
104
- viewInfo: LoadedViewState;
105
- }
106
- | { type: "SET_COLUMN_CONFIG"; config: LayoutXmlConfig }
107
- | { type: "UPDATE_COLUMN_WIDTH"; columnName: string; width: number }
108
- | { type: "REORDER_COLUMNS"; fromIndex: number; toIndex: number }
109
- | { type: "SYNC_LAYOUT_WITH_FETCHXML"; attributeTypeMap?: Map<string, string> }
110
- | {
111
- type: "SET_SORT";
112
- attribute: string;
113
- descending: boolean;
114
- isMultiSort: boolean;
115
- entityName?: string;
116
- }
117
- | { type: "SET_RETRIEVE_ALL_RECORDS"; enabled: boolean };
118
-
119
- const initialState: BuilderState = {
120
- fetchQuery: null,
121
- selectedNodeId: null,
122
- selectedNode: null,
123
- loadedView: null,
124
- columnConfig: null,
125
- layoutNeedsSync: false,
126
- retrieveAllRecords: false,
127
- };
128
-
129
- // Helper to find node by ID in the tree
130
- function findNodeById(node: unknown, targetId: NodeId): SelectedNode {
131
- if (!node || typeof node !== "object") return null;
132
-
133
- const n = node as Record<string, unknown>;
134
- if (n.id === targetId) return node as SelectedNode;
135
-
136
- // Search in arrays
137
- const arrayProps = ["attributes", "orders", "filters", "conditions", "subfilters", "links"];
138
- for (const prop of arrayProps) {
139
- if (Array.isArray(n[prop])) {
140
- for (const item of n[prop]) {
141
- const found = findNodeById(item, targetId);
142
- if (found) return found;
143
- }
144
- }
145
- }
146
-
147
- // Search in nested objects
148
- if (n.entity) {
149
- const found = findNodeById(n.entity, targetId);
150
- if (found) return found;
151
- }
152
-
153
- if (n.allAttributes) {
154
- const found = findNodeById(n.allAttributes, targetId);
155
- if (found) return found;
156
- }
157
-
158
- return null;
159
- }
160
-
161
- /**
162
- * Helper to collect all link-entity aliases from the tree
163
- * Used to generate unique aliases when adding new link-entities
164
- */
165
- function collectLinkEntityAliases(node: unknown): Set<string> {
166
- const aliases = new Set<string>();
167
-
168
- function traverse(n: unknown): void {
169
- if (!n || typeof n !== "object") return;
170
-
171
- const record = n as Record<string, unknown>;
172
-
173
- // If this is a link-entity with an alias, add it
174
- if (record.type === "link-entity" && typeof record.alias === "string") {
175
- aliases.add(record.alias);
176
- }
177
-
178
- // Traverse entity
179
- if (record.entity) {
180
- traverse(record.entity);
181
- }
182
-
183
- // Traverse links array
184
- if (Array.isArray(record.links)) {
185
- for (const link of record.links) {
186
- traverse(link);
187
- }
188
- }
189
- }
190
-
191
- traverse(node);
192
- return aliases;
193
- }
194
-
195
- /**
196
- * Generate a unique alias for a link-entity
197
- * Appends a short alphanumeric suffix if the base alias already exists
198
- */
199
- function generateUniqueAlias(baseName: string, existingAliases: Set<string>): string {
200
- // If the base name isn't taken, use it
201
- if (!existingAliases.has(baseName)) {
202
- return baseName;
203
- }
204
-
205
- // Generate a unique suffix (e.g., contact_a1, contact_b2, etc.)
206
- const chars = "abcdefghijklmnopqrstuvwxyz";
207
- let suffix = 1;
208
-
209
- while (true) {
210
- // Generate suffixes like: _a1, _b1, ..., _z1, _a2, _b2, etc.
211
- const charIndex = (suffix - 1) % 26;
212
- const numPart = Math.floor((suffix - 1) / 26) + 1;
213
- const candidate = `${baseName}_${chars[charIndex]}${numPart}`;
214
-
215
- if (!existingAliases.has(candidate)) {
216
- return candidate;
217
- }
218
-
219
- suffix++;
220
-
221
- // Safety limit to prevent infinite loops
222
- if (suffix > 1000) {
223
- return `${baseName}_${Date.now()}`;
224
- }
225
- }
226
- }
227
-
228
- // Helper to update a node in the tree immutably
229
- function updateNodeInTree<T extends { id: NodeId }>(
230
- node: T,
231
- targetId: NodeId,
232
- updater: (node: unknown) => unknown
233
- ): T {
234
- // If this is the target node, apply the updater
235
- if (node.id === targetId) {
236
- return updater(node) as T;
237
- }
238
-
239
- // Clone the node
240
- const updated = { ...node } as Record<string, unknown>;
241
- let changed = false;
242
-
243
- // Update arrays
244
- const arrayProps = ["attributes", "orders", "filters", "conditions", "subfilters", "links"];
245
- for (const prop of arrayProps) {
246
- const value = (node as Record<string, unknown>)[prop];
247
- if (Array.isArray(value)) {
248
- const updatedArray = (value as Array<{ id: NodeId }>).map((item) => {
249
- const result = updateNodeInTree(item, targetId, updater);
250
- if (result !== item) changed = true;
251
- return result;
252
- });
253
- if (changed) {
254
- updated[prop] = updatedArray;
255
- }
256
- }
257
- }
258
-
259
- // Update nested objects
260
- const nodeRecord = node as Record<string, unknown>;
261
- if (nodeRecord.entity) {
262
- const updatedEntity = updateNodeInTree(nodeRecord.entity as EntityNode, targetId, updater);
263
- if (updatedEntity !== nodeRecord.entity) {
264
- updated.entity = updatedEntity;
265
- changed = true;
266
- }
267
- }
268
-
269
- if (nodeRecord.allAttributes) {
270
- const updatedAllAttrs = updateNodeInTree(
271
- nodeRecord.allAttributes as AllAttributesNode,
272
- targetId,
273
- updater
274
- );
275
- if (updatedAllAttrs !== nodeRecord.allAttributes) {
276
- updated.allAttributes = updatedAllAttrs;
277
- changed = true;
278
- }
279
- }
280
-
281
- return (changed ? updated : node) as T;
282
- }
283
-
284
- // Helper to remove a node from the tree immutably
285
- function removeNodeFromTree<T extends { id: NodeId }>(node: T, targetId: NodeId): T | null {
286
- const nodeRecord = node as Record<string, unknown>;
287
-
288
- // Can't remove the root fetch node or its entity
289
- if (node.id === targetId && (nodeRecord.type === "fetch" || nodeRecord.type === "entity")) {
290
- return node;
291
- }
292
-
293
- // Clone the node
294
- const updated = { ...node } as Record<string, unknown>;
295
- let changed = false;
296
-
297
- // Remove from arrays
298
- const arrayProps = ["attributes", "orders", "filters", "conditions", "subfilters", "links"];
299
- for (const prop of arrayProps) {
300
- const value = nodeRecord[prop];
301
- if (Array.isArray(value)) {
302
- const filteredArray = (value as Array<{ id: NodeId }>)
303
- .filter((item) => item.id !== targetId)
304
- .map((item) => removeNodeFromTree(item, targetId))
305
- .filter((item) => item !== null) as Array<{ id: NodeId }>;
306
-
307
- // Check if anything changed (length or nested changes)
308
- if (
309
- filteredArray.length !== value.length ||
310
- filteredArray.some((item, i) => item !== value[i])
311
- ) {
312
- updated[prop] = filteredArray;
313
- changed = true;
314
- }
315
- }
316
- }
317
-
318
- // Remove from nested objects
319
- if (nodeRecord.entity) {
320
- const updatedEntity = removeNodeFromTree(nodeRecord.entity as EntityNode, targetId);
321
- if (updatedEntity !== nodeRecord.entity) {
322
- updated.entity = updatedEntity;
323
- changed = true;
324
- }
325
- }
326
-
327
- // Handle allAttributes removal
328
- if (nodeRecord.allAttributes && (nodeRecord.allAttributes as AllAttributesNode).id === targetId) {
329
- delete updated.allAttributes;
330
- changed = true;
331
- }
332
-
333
- return (changed ? updated : node) as T;
334
- }
335
-
336
- function builderReducer(state: BuilderState, action: BuilderAction): BuilderState {
337
- switch (action.type) {
338
- case "NEW_QUERY":
339
- return initialState;
340
-
341
- case "LOAD_FETCHXML": {
342
- // Load a pre-parsed FetchNode tree (from parser or editor)
343
- // This clears loadedView since it's a manual load, not from a saved view
344
- const fetchNode = action.fetchNode;
345
- // Generate initial layout from the FetchXML
346
- const newConfig = generateLayoutFromFetchXml(fetchNode);
347
- return {
348
- fetchQuery: fetchNode,
349
- selectedNodeId: fetchNode.entity.id,
350
- selectedNode: fetchNode.entity,
351
- loadedView: null, // Clear view info - this was manual edit
352
- columnConfig: newConfig,
353
- layoutNeedsSync: false,
354
- retrieveAllRecords: state.retrieveAllRecords, // Preserve execution option
355
- };
356
- }
357
-
358
- case "LOAD_VIEW": {
359
- // Load a pre-parsed FetchNode tree from a saved view
360
- // Preserves view info for execution optimization
361
- const fetchNode = action.fetchNode;
362
-
363
- // If layoutXml is provided, parse it; otherwise generate from FetchXML
364
- let newConfig: LayoutXmlConfig;
365
- if (action.layoutXml) {
366
- try {
367
- newConfig = parseLayoutXml(action.layoutXml);
368
- } catch (e) {
369
- console.warn("Failed to parse view layoutxml, generating from FetchXML:", e);
370
- newConfig = generateLayoutFromFetchXml(fetchNode);
371
- }
372
- } else {
373
- newConfig = generateLayoutFromFetchXml(fetchNode);
374
- }
375
-
376
- return {
377
- fetchQuery: fetchNode,
378
- selectedNodeId: fetchNode.entity.id,
379
- selectedNode: fetchNode.entity,
380
- loadedView: action.viewInfo, // Track the loaded view
381
- columnConfig: newConfig,
382
- layoutNeedsSync: false,
383
- retrieveAllRecords: state.retrieveAllRecords, // Preserve execution option
384
- };
385
- }
386
-
387
- case "CLEAR_LOADED_VIEW": {
388
- // Called when FetchXML is edited in the editor
389
- return {
390
- ...state,
391
- loadedView: null,
392
- layoutNeedsSync: true, // May need to regenerate layout
393
- };
394
- }
395
-
396
- case "SET_LOADED_VIEW": {
397
- // Called after successfully saving a view - update the loadedView state
398
- // This allows subsequent saves to overwrite the same view
399
- return {
400
- ...state,
401
- loadedView: action.viewInfo,
402
- };
403
- }
404
-
405
- case "SET_ENTITY": {
406
- const entityNode: EntityNode = {
407
- id: generateId(),
408
- type: "entity",
409
- name: action.entityName,
410
- attributes: [],
411
- orders: [],
412
- filters: [],
413
- links: [],
414
- };
415
-
416
- const fetchNode: FetchNode = {
417
- id: generateId(),
418
- type: "fetch",
419
- entity: entityNode,
420
- options: {},
421
- };
422
-
423
- return {
424
- fetchQuery: fetchNode,
425
- selectedNodeId: entityNode.id,
426
- selectedNode: entityNode,
427
- loadedView: null, // Clear view info - entity changed
428
- columnConfig: null, // Reset layout - no attributes yet
429
- layoutNeedsSync: false,
430
- retrieveAllRecords: state.retrieveAllRecords, // Preserve execution option
431
- };
432
- }
433
-
434
- case "SELECT_NODE": {
435
- if (!state.fetchQuery) return state;
436
- const node = findNodeById(state.fetchQuery, action.nodeId);
437
- return {
438
- ...state,
439
- selectedNodeId: action.nodeId,
440
- selectedNode: node,
441
- };
442
- }
443
-
444
- case "ADD_ATTRIBUTE": {
445
- if (!state.fetchQuery) return state;
446
-
447
- // Find parent node (entity or link-entity)
448
- const parent = findNodeById(state.fetchQuery, action.parentId);
449
- if (!parent || (parent.type !== "entity" && parent.type !== "link-entity")) {
450
- return state;
451
- }
452
-
453
- // Create new attribute node
454
- const newAttribute: AttributeNode = {
455
- id: generateId(),
456
- type: "attribute",
457
- name: "new_attribute",
458
- };
459
-
460
- // Update the tree immutably
461
- const updatedFetch = updateNodeInTree(state.fetchQuery, action.parentId, (node) => {
462
- const n = node as EntityNode | LinkEntityNode;
463
- return {
464
- ...n,
465
- attributes: [...n.attributes, newAttribute],
466
- };
467
- });
468
-
469
- return {
470
- ...state,
471
- fetchQuery: updatedFetch,
472
- selectedNodeId: newAttribute.id,
473
- selectedNode: newAttribute,
474
- loadedView: null, // Clear view info - query modified
475
- layoutNeedsSync: true, // New attribute added - layout needs update
476
- };
477
- }
478
-
479
- case "ADD_ATTRIBUTE_BY_NAME": {
480
- if (!state.fetchQuery) return state;
481
-
482
- // Find parent node (entity or link-entity)
483
- const parent = findNodeById(state.fetchQuery, action.parentId);
484
- if (!parent || (parent.type !== "entity" && parent.type !== "link-entity")) {
485
- return state;
486
- }
487
-
488
- // Check if attribute already exists
489
- const existingAttr = (parent as EntityNode | LinkEntityNode).attributes.find(
490
- (a) => a.name === action.attributeName
491
- );
492
- if (existingAttr) {
493
- // Attribute already exists, just select it
494
- return {
495
- ...state,
496
- selectedNodeId: existingAttr.id,
497
- selectedNode: existingAttr,
498
- };
499
- }
500
-
501
- // Create new attribute node with the specified name
502
- const newAttrByName: AttributeNode = {
503
- id: generateId(),
504
- type: "attribute",
505
- name: action.attributeName,
506
- };
507
-
508
- // Update the tree immutably
509
- const updatedFetchByName = updateNodeInTree(state.fetchQuery, action.parentId, (node) => {
510
- const n = node as EntityNode | LinkEntityNode;
511
- return {
512
- ...n,
513
- attributes: [...n.attributes, newAttrByName],
514
- };
515
- });
516
-
517
- return {
518
- ...state,
519
- fetchQuery: updatedFetchByName,
520
- selectedNodeId: newAttrByName.id,
521
- selectedNode: newAttrByName,
522
- loadedView: null, // Clear view info - query modified
523
- layoutNeedsSync: true, // New attribute added - layout needs update
524
- };
525
- }
526
-
527
- case "ADD_ALL_ATTRIBUTES": {
528
- if (!state.fetchQuery) return state;
529
-
530
- // Find parent node (entity or link-entity)
531
- const parent = findNodeById(state.fetchQuery, action.parentId);
532
- if (!parent || (parent.type !== "entity" && parent.type !== "link-entity")) {
533
- return state;
534
- }
535
-
536
- // Create new all-attributes node
537
- const newAllAttributes: AllAttributesNode = {
538
- id: generateId(),
539
- type: "all-attributes",
540
- enabled: true,
541
- };
542
-
543
- // Update the tree immutably
544
- const updatedFetch = updateNodeInTree(state.fetchQuery, action.parentId, (node) => {
545
- const n = node as EntityNode | LinkEntityNode;
546
- return {
547
- ...n,
548
- allAttributes: newAllAttributes,
549
- };
550
- });
551
-
552
- return {
553
- ...state,
554
- fetchQuery: updatedFetch,
555
- selectedNodeId: newAllAttributes.id,
556
- selectedNode: newAllAttributes,
557
- loadedView: null, // Clear view info - query modified
558
- layoutNeedsSync: true, // All attributes changes layout
559
- };
560
- }
561
-
562
- case "ADD_ORDER": {
563
- if (!state.fetchQuery) return state;
564
-
565
- // Find parent node (entity or link-entity)
566
- const parent = findNodeById(state.fetchQuery, action.parentId);
567
- if (!parent || (parent.type !== "entity" && parent.type !== "link-entity")) {
568
- return state;
569
- }
570
-
571
- // Create new order node
572
- const newOrder: OrderNode = {
573
- id: generateId(),
574
- type: "order",
575
- attribute: "new_attribute",
576
- descending: false,
577
- };
578
-
579
- // Update the tree immutably
580
- const updatedFetch = updateNodeInTree(state.fetchQuery, action.parentId, (node) => {
581
- const n = node as EntityNode | LinkEntityNode;
582
- return {
583
- ...n,
584
- orders: [...n.orders, newOrder],
585
- };
586
- });
587
-
588
- return {
589
- ...state,
590
- fetchQuery: updatedFetch,
591
- selectedNodeId: newOrder.id,
592
- selectedNode: newOrder,
593
- loadedView: null, // Clear view info - query modified
594
- };
595
- }
596
-
597
- case "ADD_FILTER": {
598
- if (!state.fetchQuery) return state;
599
-
600
- // Find parent node (entity or link-entity)
601
- const parent = findNodeById(state.fetchQuery, action.parentId);
602
- if (!parent || (parent.type !== "entity" && parent.type !== "link-entity")) {
603
- return state;
604
- }
605
-
606
- // Create new filter node
607
- const newFilter: FilterNode = {
608
- id: generateId(),
609
- type: "filter",
610
- conjunction: "and",
611
- conditions: [],
612
- subfilters: [],
613
- links: [],
614
- };
615
-
616
- // Update the tree immutably
617
- const updatedFetch = updateNodeInTree(state.fetchQuery, action.parentId, (node) => {
618
- const n = node as EntityNode | LinkEntityNode;
619
- return {
620
- ...n,
621
- filters: [...n.filters, newFilter],
622
- };
623
- });
624
-
625
- return {
626
- ...state,
627
- fetchQuery: updatedFetch,
628
- selectedNodeId: newFilter.id,
629
- selectedNode: newFilter,
630
- loadedView: null, // Clear view info - query modified
631
- };
632
- }
633
-
634
- case "ADD_SUBFILTER": {
635
- if (!state.fetchQuery) return state;
636
-
637
- // Find parent filter node
638
- const parent = findNodeById(state.fetchQuery, action.filterId);
639
- if (!parent || parent.type !== "filter") {
640
- return state;
641
- }
642
-
643
- // Create new nested filter node
644
- const newSubfilter: FilterNode = {
645
- id: generateId(),
646
- type: "filter",
647
- conjunction: "and",
648
- conditions: [],
649
- subfilters: [],
650
- links: [],
651
- };
652
-
653
- // Update the tree immutably
654
- const updatedFetch = updateNodeInTree(state.fetchQuery, action.filterId, (node) => {
655
- const n = node as FilterNode;
656
- return {
657
- ...n,
658
- subfilters: [...n.subfilters, newSubfilter],
659
- };
660
- });
661
-
662
- return {
663
- ...state,
664
- fetchQuery: updatedFetch,
665
- selectedNodeId: newSubfilter.id,
666
- selectedNode: newSubfilter,
667
- loadedView: null, // Clear view info - query modified
668
- };
669
- }
670
-
671
- case "ADD_CONDITION": {
672
- if (!state.fetchQuery) return state;
673
-
674
- // Find parent filter node
675
- const parent = findNodeById(state.fetchQuery, action.filterId);
676
- if (!parent || parent.type !== "filter") {
677
- return state;
678
- }
679
-
680
- // Create new condition node
681
- const newCondition: ConditionNode = {
682
- id: generateId(),
683
- type: "condition",
684
- attribute: "new_attribute",
685
- operator: "eq",
686
- };
687
-
688
- // Update the tree immutably
689
- const updatedFetch = updateNodeInTree(state.fetchQuery, action.filterId, (node) => {
690
- const n = node as FilterNode;
691
- return {
692
- ...n,
693
- conditions: [...n.conditions, newCondition],
694
- };
695
- });
696
-
697
- return {
698
- ...state,
699
- fetchQuery: updatedFetch,
700
- selectedNodeId: newCondition.id,
701
- selectedNode: newCondition,
702
- loadedView: null, // Clear view info - query modified
703
- };
704
- }
705
-
706
- case "ADD_LINK_ENTITY": {
707
- if (!state.fetchQuery) return state;
708
-
709
- // Find parent node (entity, link-entity, or filter)
710
- const parent = findNodeById(state.fetchQuery, action.parentId);
711
- if (
712
- !parent ||
713
- (parent.type !== "entity" && parent.type !== "link-entity" && parent.type !== "filter")
714
- ) {
715
- return state;
716
- }
717
-
718
- // Collect existing aliases to ensure uniqueness
719
- const existingAliases = collectLinkEntityAliases(state.fetchQuery);
720
- const uniqueAlias = generateUniqueAlias("new_entity", existingAliases);
721
-
722
- // Create new link-entity node
723
- // For filter parents, use "any" link type by default (common use case)
724
- const newLinkEntity: LinkEntityNode = {
725
- id: generateId(),
726
- type: "link-entity",
727
- name: "new_entity",
728
- alias: uniqueAlias,
729
- from: "id_field",
730
- to: "parent_id_field",
731
- linkType: parent.type === "filter" ? "any" : "inner",
732
- attributes: [],
733
- orders: [],
734
- filters: [],
735
- links: [],
736
- };
737
-
738
- // Update the tree immutably
739
- const updatedFetch = updateNodeInTree(state.fetchQuery, action.parentId, (node) => {
740
- const typedNode = node as EntityNode | LinkEntityNode | FilterNode;
741
- if (typedNode.type === "filter") {
742
- const n = typedNode as FilterNode;
743
- return {
744
- ...n,
745
- links: [...(n.links || []), newLinkEntity],
746
- };
747
- } else {
748
- const n = typedNode as EntityNode | LinkEntityNode;
749
- return {
750
- ...n,
751
- links: [...n.links, newLinkEntity],
752
- };
753
- }
754
- });
755
-
756
- return {
757
- ...state,
758
- fetchQuery: updatedFetch,
759
- selectedNodeId: newLinkEntity.id,
760
- selectedNode: newLinkEntity,
761
- loadedView: null, // Clear view info - query modified
762
- layoutNeedsSync: true, // Link-entity may add columns
763
- };
764
- }
765
-
766
- case "ADD_LINK_ENTITY_WITH_CONFIG": {
767
- if (!state.fetchQuery) return state;
768
-
769
- // Find parent node (entity or link-entity)
770
- const parent = findNodeById(state.fetchQuery, action.parentId);
771
- if (!parent || (parent.type !== "entity" && parent.type !== "link-entity")) {
772
- return state;
773
- }
774
-
775
- // Collect existing aliases to ensure uniqueness
776
- const existingAliases = collectLinkEntityAliases(state.fetchQuery);
777
- const uniqueAlias = generateUniqueAlias(action.name, existingAliases);
778
-
779
- // Create new link-entity node with provided configuration
780
- const newLinkEntity: LinkEntityNode = {
781
- id: action.id, // Use pre-generated ID from caller
782
- type: "link-entity",
783
- name: action.name,
784
- alias: uniqueAlias, // Use unique alias
785
- from: action.from,
786
- to: action.to,
787
- linkType: action.linkType,
788
- relationshipType: action.relationshipType ?? "N1", // Default to N1 if not specified
789
- attributes: [],
790
- orders: [],
791
- filters: [],
792
- links: [],
793
- };
794
-
795
- // Update the tree immutably
796
- const updatedFetch = updateNodeInTree(state.fetchQuery, action.parentId, (node) => {
797
- const typedNode = node as EntityNode | LinkEntityNode;
798
- return {
799
- ...typedNode,
800
- links: [...typedNode.links, newLinkEntity],
801
- };
802
- });
803
-
804
- return {
805
- ...state,
806
- fetchQuery: updatedFetch,
807
- selectedNodeId: newLinkEntity.id,
808
- selectedNode: newLinkEntity,
809
- loadedView: null, // Clear view info - query modified
810
- layoutNeedsSync: true, // Link-entity may add columns
811
- };
812
- }
813
-
814
- case "REMOVE_NODE": {
815
- if (!state.fetchQuery) return state;
816
-
817
- // Remove the node from the tree
818
- const updatedFetch = removeNodeFromTree(state.fetchQuery, action.nodeId);
819
- if (!updatedFetch) return state;
820
-
821
- // If we're removing the selected node, clear selection
822
- const newSelectedId = state.selectedNodeId === action.nodeId ? null : state.selectedNodeId;
823
- const newSelectedNode = newSelectedId ? findNodeById(updatedFetch, newSelectedId) : null;
824
-
825
- return {
826
- ...state,
827
- fetchQuery: updatedFetch,
828
- selectedNodeId: newSelectedId,
829
- selectedNode: newSelectedNode,
830
- loadedView: null, // Clear view info - query modified
831
- layoutNeedsSync: true, // Removed node may affect columns
832
- };
833
- }
834
-
835
- case "UPDATE_NODE": {
836
- if (!state.fetchQuery) return state;
837
-
838
- // Find the node to update
839
- const targetNode = findNodeById(state.fetchQuery, action.nodeId);
840
- if (!targetNode) return state;
841
-
842
- // Update the tree immutably - apply updates to the target node
843
- const updatedFetch = updateNodeInTree(state.fetchQuery, action.nodeId, (node) => {
844
- const n = node as Record<string, unknown>;
845
-
846
- // For FetchNode, merge updates into options property
847
- if (n.type === "fetch") {
848
- const fetchNode = node as unknown as FetchNode;
849
- return {
850
- ...fetchNode,
851
- options: {
852
- ...fetchNode.options,
853
- ...action.updates,
854
- },
855
- } as FetchNode;
856
- }
857
-
858
- // For other nodes, apply updates directly
859
- return {
860
- ...n,
861
- ...action.updates,
862
- };
863
- });
864
-
865
- // Update selected node if it's the one we just modified
866
- const newSelectedNode =
867
- state.selectedNodeId === action.nodeId
868
- ? findNodeById(updatedFetch, action.nodeId)
869
- : state.selectedNode;
870
-
871
- return {
872
- ...state,
873
- fetchQuery: updatedFetch,
874
- selectedNode: newSelectedNode,
875
- loadedView: null, // Clear view info - query modified
876
- layoutNeedsSync: true, // Layout may need to sync with FetchXML changes
877
- };
878
- }
879
-
880
- case "SET_COLUMN_CONFIG": {
881
- return {
882
- ...state,
883
- columnConfig: action.config,
884
- layoutNeedsSync: false,
885
- };
886
- }
887
-
888
- case "UPDATE_COLUMN_WIDTH": {
889
- if (!state.columnConfig) return state;
890
- return {
891
- ...state,
892
- columnConfig: updateLayoutColumnWidth(state.columnConfig, action.columnName, action.width),
893
- };
894
- }
895
-
896
- case "REORDER_COLUMNS": {
897
- if (!state.columnConfig) return state;
898
- return {
899
- ...state,
900
- columnConfig: reorderLayoutColumns(state.columnConfig, action.fromIndex, action.toIndex),
901
- };
902
- }
903
-
904
- case "SYNC_LAYOUT_WITH_FETCHXML": {
905
- if (!state.fetchQuery) return state;
906
-
907
- let newConfig: LayoutXmlConfig;
908
- if (state.columnConfig) {
909
- // Merge existing config with FetchXML changes (preserves widths/order where possible)
910
- newConfig = mergeLayoutWithFetchXml(
911
- state.columnConfig,
912
- state.fetchQuery,
913
- action.attributeTypeMap
914
- );
915
- } else {
916
- // Generate new config from FetchXML
917
- newConfig = generateLayoutFromFetchXml(state.fetchQuery, action.attributeTypeMap);
918
- }
919
-
920
- return {
921
- ...state,
922
- columnConfig: newConfig,
923
- layoutNeedsSync: false,
924
- };
925
- }
926
-
927
- case "SET_SORT": {
928
- if (!state.fetchQuery?.entity) return state;
929
-
930
- const { attribute, descending, isMultiSort, entityName } = action;
931
-
932
- // Create new order node
933
- const newOrder: OrderNode = {
934
- id: generateId(),
935
- type: "order",
936
- attribute,
937
- descending: descending || undefined,
938
- entityname: entityName,
939
- };
940
-
941
- // Get current orders
942
- const currentOrders = state.fetchQuery.entity.orders || [];
943
-
944
- let newOrders: OrderNode[];
945
-
946
- if (isMultiSort) {
947
- // Multi-sort: check if this attribute already has an order
948
- const existingIndex = currentOrders.findIndex(
949
- (o) =>
950
- o.attribute === attribute && (o.entityname || undefined) === (entityName || undefined)
951
- );
952
-
953
- if (existingIndex >= 0) {
954
- // Update existing order's direction
955
- newOrders = currentOrders.map((o, i) =>
956
- i === existingIndex ? { ...o, descending: descending || undefined } : o
957
- );
958
- } else {
959
- // Add new order to the end
960
- newOrders = [...currentOrders, newOrder];
961
- }
962
- } else {
963
- // Single sort: replace all orders with just this one
964
- newOrders = [newOrder];
965
- }
966
-
967
- // Update the entity with new orders
968
- const newFetchQuery: FetchNode = {
969
- ...state.fetchQuery,
970
- entity: {
971
- ...state.fetchQuery.entity,
972
- orders: newOrders,
973
- },
974
- };
975
-
976
- return {
977
- ...state,
978
- fetchQuery: newFetchQuery,
979
- };
980
- }
981
-
982
- case "SET_RETRIEVE_ALL_RECORDS": {
983
- return {
984
- ...state,
985
- retrieveAllRecords: action.enabled,
986
- };
987
- }
988
-
989
- default:
990
- return state;
991
- }
992
- }
993
-
994
- /** View info for load operations */
995
- interface ViewLoadInfo {
996
- id: string;
997
- type: "system" | "personal";
998
- entitySetName: string;
999
- name: string;
1000
- }
1001
-
1002
- interface BuilderContextValue extends BuilderState {
1003
- setEntity: (entityName: string) => void;
1004
- selectNode: (nodeId: NodeId) => void;
1005
- addAttribute: (parentId: NodeId) => void;
1006
- /** Add a specific attribute by name to an entity or link-entity */
1007
- addAttributeByName: (parentId: NodeId, attributeName: string) => void;
1008
- addAllAttributes: (parentId: NodeId) => void;
1009
- addOrder: (parentId: NodeId) => void;
1010
- addFilter: (parentId: NodeId) => void;
1011
- addSubfilter: (filterId: NodeId) => void;
1012
- addCondition: (filterId: NodeId) => void;
1013
- addLinkEntity: (parentId: NodeId) => void;
1014
- /** Add a link-entity with specific configuration, returns the new link-entity's ID */
1015
- addLinkEntityWithConfig: (
1016
- parentId: NodeId,
1017
- name: string,
1018
- from: string,
1019
- to: string,
1020
- linkType: "inner" | "outer",
1021
- relationshipType?: "N1" | "1N" | "NN"
1022
- ) => NodeId;
1023
- removeNode: (nodeId: NodeId) => void;
1024
- updateNode: (nodeId: NodeId, updates: Record<string, unknown>) => void;
1025
- newQuery: () => void;
1026
- loadFetchXml: (xmlString: string) => ParseResult;
1027
- /** Load a view's FetchXML while preserving view info for execution optimization */
1028
- loadView: (xmlString: string, viewInfo: ViewLoadInfo, layoutXml?: string) => ParseResult;
1029
- /** Clear the loaded view info (called when FetchXML is manually edited) */
1030
- clearLoadedView: () => void;
1031
- /** Update loaded view info after save (without re-parsing FetchXML) */
1032
- setLoadedView: (viewInfo: ViewLoadInfo) => void;
1033
- /** Set the column configuration (from parsed layoutxml or generated) */
1034
- setColumnConfig: (config: LayoutXmlConfig) => void;
1035
- /** Update a single column's width */
1036
- updateColumnWidth: (columnName: string, width: number) => void;
1037
- /** Reorder columns by moving one from fromIndex to toIndex */
1038
- reorderColumns: (fromIndex: number, toIndex: number) => void;
1039
- /** Sync layout with current FetchXML (regenerate/merge as needed) */
1040
- syncLayoutWithFetchXml: (attributeTypeMap?: Map<string, string>) => void;
1041
- /**
1042
- * Set sort order on an attribute. If isMultiSort is true, adds to existing sorts.
1043
- * If false, replaces all existing sorts.
1044
- */
1045
- setSort: (
1046
- attribute: string,
1047
- descending: boolean,
1048
- isMultiSort: boolean,
1049
- entityName?: string
1050
- ) => void;
1051
- /**
1052
- * Enable or disable "Retrieve All Records" mode.
1053
- * When enabled, execution fetches all pages progressively.
1054
- */
1055
- setRetrieveAllRecords: (enabled: boolean) => void;
1056
- }
1057
-
1058
- const BuilderContext = createContext<BuilderContextValue | null>(null);
1059
-
1060
- export function BuilderProvider({ children }: { children: ReactNode }) {
1061
- const [state, dispatch] = useReducer(builderReducer, initialState);
1062
-
1063
- const contextValue: BuilderContextValue = {
1064
- ...state,
1065
- setEntity: (entityName: string) => dispatch({ type: "SET_ENTITY", entityName }),
1066
- selectNode: (nodeId: NodeId) => dispatch({ type: "SELECT_NODE", nodeId }),
1067
- addAttribute: (parentId: NodeId) => dispatch({ type: "ADD_ATTRIBUTE", parentId }),
1068
- addAttributeByName: (parentId: NodeId, attributeName: string) =>
1069
- dispatch({ type: "ADD_ATTRIBUTE_BY_NAME", parentId, attributeName }),
1070
- addAllAttributes: (parentId: NodeId) => dispatch({ type: "ADD_ALL_ATTRIBUTES", parentId }),
1071
- addOrder: (parentId: NodeId) => dispatch({ type: "ADD_ORDER", parentId }),
1072
- addFilter: (parentId: NodeId) => dispatch({ type: "ADD_FILTER", parentId }),
1073
- addSubfilter: (filterId: NodeId) => dispatch({ type: "ADD_SUBFILTER", filterId }),
1074
- addCondition: (filterId: NodeId) => dispatch({ type: "ADD_CONDITION", filterId }),
1075
- addLinkEntity: (parentId: NodeId) => dispatch({ type: "ADD_LINK_ENTITY", parentId }),
1076
- addLinkEntityWithConfig: (
1077
- parentId: NodeId,
1078
- name: string,
1079
- from: string,
1080
- to: string,
1081
- linkType: "inner" | "outer",
1082
- relationshipType?: "N1" | "1N" | "NN"
1083
- ): NodeId => {
1084
- const id = generateId();
1085
- dispatch({
1086
- type: "ADD_LINK_ENTITY_WITH_CONFIG",
1087
- id,
1088
- parentId,
1089
- name,
1090
- from,
1091
- to,
1092
- linkType,
1093
- relationshipType,
1094
- });
1095
- return id;
1096
- },
1097
- removeNode: (nodeId: NodeId) => dispatch({ type: "REMOVE_NODE", nodeId }),
1098
- updateNode: (nodeId: NodeId, updates: Record<string, unknown>) =>
1099
- dispatch({ type: "UPDATE_NODE", nodeId, updates }),
1100
- newQuery: () => dispatch({ type: "NEW_QUERY" }),
1101
- loadFetchXml: (xmlString: string): ParseResult => {
1102
- const result = parseFetchXml(xmlString);
1103
- if (result.success && result.fetchNode) {
1104
- dispatch({ type: "LOAD_FETCHXML", fetchNode: result.fetchNode });
1105
- }
1106
- return result;
1107
- },
1108
- loadView: (xmlString: string, viewInfo: ViewLoadInfo, layoutXml?: string): ParseResult => {
1109
- const result = parseFetchXml(xmlString);
1110
- if (result.success && result.fetchNode) {
1111
- dispatch({
1112
- type: "LOAD_VIEW",
1113
- fetchNode: result.fetchNode,
1114
- viewInfo: {
1115
- id: viewInfo.id,
1116
- type: viewInfo.type,
1117
- originalFetchXml: xmlString,
1118
- entitySetName: viewInfo.entitySetName,
1119
- name: viewInfo.name,
1120
- },
1121
- layoutXml,
1122
- });
1123
- }
1124
- return result;
1125
- },
1126
- clearLoadedView: () => dispatch({ type: "CLEAR_LOADED_VIEW" }),
1127
- setLoadedView: (viewInfo: ViewLoadInfo) =>
1128
- dispatch({
1129
- type: "SET_LOADED_VIEW",
1130
- viewInfo: {
1131
- id: viewInfo.id,
1132
- type: viewInfo.type,
1133
- originalFetchXml: state.fetchQuery ? generateFetchXml(state.fetchQuery) : "",
1134
- entitySetName: viewInfo.entitySetName,
1135
- name: viewInfo.name,
1136
- },
1137
- }),
1138
- setColumnConfig: (config: LayoutXmlConfig) => dispatch({ type: "SET_COLUMN_CONFIG", config }),
1139
- updateColumnWidth: (columnName: string, width: number) =>
1140
- dispatch({ type: "UPDATE_COLUMN_WIDTH", columnName, width }),
1141
- reorderColumns: (fromIndex: number, toIndex: number) =>
1142
- dispatch({ type: "REORDER_COLUMNS", fromIndex, toIndex }),
1143
- syncLayoutWithFetchXml: (attributeTypeMap?: Map<string, string>) =>
1144
- dispatch({ type: "SYNC_LAYOUT_WITH_FETCHXML", attributeTypeMap }),
1145
- setSort: (attribute: string, descending: boolean, isMultiSort: boolean, entityName?: string) =>
1146
- dispatch({ type: "SET_SORT", attribute, descending, isMultiSort, entityName }),
1147
- setRetrieveAllRecords: (enabled: boolean) =>
1148
- dispatch({ type: "SET_RETRIEVE_ALL_RECORDS", enabled }),
1149
- };
1150
-
1151
- return <BuilderContext.Provider value={contextValue}>{children}</BuilderContext.Provider>;
1152
- }
1153
-
1154
- export function useBuilder() {
1155
- const context = useContext(BuilderContext);
1156
- if (!context) {
1157
- throw new Error("useBuilder must be used within BuilderProvider");
1158
- }
1159
- return context;
1160
- }