@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
|
@@ -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
|
-
}
|