@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,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Display settings types and defaults
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export type ValueDisplayMode = "formatted" | "raw" | "both";
|
|
6
|
-
|
|
7
|
-
export interface DisplaySettings {
|
|
8
|
-
/** Show logical names in column headers instead of display names */
|
|
9
|
-
useLogicalNames: boolean;
|
|
10
|
-
/** How to display cell values: formatted, raw, or both */
|
|
11
|
-
valueDisplayMode: ValueDisplayMode;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/** Default display settings */
|
|
15
|
-
export const defaultDisplaySettings: DisplaySettings = {
|
|
16
|
-
useLogicalNames: false,
|
|
17
|
-
valueDisplayMode: "formatted",
|
|
18
|
-
};
|
|
@@ -1,346 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure functions to generate FetchXML from tree structure
|
|
3
|
-
* Follows Dataverse FetchXML semantics
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type {
|
|
7
|
-
FetchNode,
|
|
8
|
-
EntityNode,
|
|
9
|
-
AttributeNode,
|
|
10
|
-
AllAttributesNode,
|
|
11
|
-
OrderNode,
|
|
12
|
-
FilterNode,
|
|
13
|
-
ConditionNode,
|
|
14
|
-
LinkEntityNode,
|
|
15
|
-
} from "./nodes";
|
|
16
|
-
import { operatorRequiresValue } from "./operators";
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Convert a value to string for FetchXML, avoiding scientific notation for numbers
|
|
20
|
-
*/
|
|
21
|
-
function formatValueForFetchXml(val: unknown): string {
|
|
22
|
-
if (typeof val === "number") {
|
|
23
|
-
// Avoid scientific notation (1e-12) by using toFixed with sufficient precision
|
|
24
|
-
// Then remove trailing zeros
|
|
25
|
-
if (Math.abs(val) < 1e-10 && val !== 0) {
|
|
26
|
-
// Very small numbers - use high precision
|
|
27
|
-
return val.toFixed(20).replace(/\.?0+$/, "");
|
|
28
|
-
} else if (Number.isInteger(val)) {
|
|
29
|
-
// Integer - no decimal point needed
|
|
30
|
-
return val.toString();
|
|
31
|
-
} else {
|
|
32
|
-
// Decimal - preserve precision but remove trailing zeros
|
|
33
|
-
return val.toFixed(10).replace(/\.?0+$/, "");
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return String(val);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Generate complete FetchXML from FetchNode
|
|
41
|
-
*/
|
|
42
|
-
export function generateFetchXml(fetchNode: FetchNode): string {
|
|
43
|
-
const lines: string[] = [];
|
|
44
|
-
|
|
45
|
-
// Build fetch element with options
|
|
46
|
-
const fetchAttrs: string[] = [];
|
|
47
|
-
if (fetchNode.options.aggregate) fetchAttrs.push('aggregate="true"');
|
|
48
|
-
if (fetchNode.options.distinct) fetchAttrs.push('distinct="true"');
|
|
49
|
-
if (fetchNode.options.top !== undefined) fetchAttrs.push(`top="${fetchNode.options.top}"`);
|
|
50
|
-
if (fetchNode.options.count !== undefined) fetchAttrs.push(`count="${fetchNode.options.count}"`);
|
|
51
|
-
if (fetchNode.options.page !== undefined) fetchAttrs.push(`page="${fetchNode.options.page}"`);
|
|
52
|
-
if (fetchNode.options.returnTotalRecordCount) fetchAttrs.push('returntotalrecordcount="true"');
|
|
53
|
-
if (fetchNode.options.noLock) fetchAttrs.push('no-lock="true"');
|
|
54
|
-
if (fetchNode.options.utcOffset !== undefined)
|
|
55
|
-
fetchAttrs.push(`utc-offset="${fetchNode.options.utcOffset}"`);
|
|
56
|
-
if (fetchNode.options.pagingCookie)
|
|
57
|
-
fetchAttrs.push(`paging-cookie="${escapeXml(fetchNode.options.pagingCookie)}"`);
|
|
58
|
-
|
|
59
|
-
const fetchAttrStr = fetchAttrs.length > 0 ? " " + fetchAttrs.join(" ") : "";
|
|
60
|
-
lines.push(`<fetch${fetchAttrStr}>`);
|
|
61
|
-
|
|
62
|
-
// Generate entity
|
|
63
|
-
lines.push(...generateEntity(fetchNode.entity, 1));
|
|
64
|
-
|
|
65
|
-
lines.push("</fetch>");
|
|
66
|
-
|
|
67
|
-
return lines.join("\n");
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Generate entity element
|
|
72
|
-
*/
|
|
73
|
-
function generateEntity(entity: EntityNode, indent: number): string[] {
|
|
74
|
-
const lines: string[] = [];
|
|
75
|
-
const spaces = " ".repeat(indent);
|
|
76
|
-
|
|
77
|
-
lines.push(`${spaces}<entity name="${entity.name}">`);
|
|
78
|
-
|
|
79
|
-
// All-attributes (single node, not array)
|
|
80
|
-
if (entity.allAttributes?.enabled) {
|
|
81
|
-
lines.push(...generateAllAttributes(entity.allAttributes, indent + 1));
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// CRITICAL: Always include the primary ID attribute for row selection to work
|
|
85
|
-
// The primary ID is {entityname}id (e.g., accountid, contactid)
|
|
86
|
-
const primaryIdAttrName = `${entity.name}id`;
|
|
87
|
-
const hasPrimaryId =
|
|
88
|
-
entity.attributes.some((attr) => attr.name === primaryIdAttrName) ||
|
|
89
|
-
entity.allAttributes?.enabled;
|
|
90
|
-
|
|
91
|
-
if (!hasPrimaryId) {
|
|
92
|
-
// Inject primary ID attribute at the beginning
|
|
93
|
-
const primaryIdSpaces = " ".repeat(indent + 1);
|
|
94
|
-
lines.push(`${primaryIdSpaces}<attribute name="${primaryIdAttrName}" />`);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Attributes
|
|
98
|
-
entity.attributes.forEach((attr) => {
|
|
99
|
-
lines.push(...generateAttribute(attr, indent + 1));
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
// Orders
|
|
103
|
-
entity.orders?.forEach((order) => {
|
|
104
|
-
lines.push(...generateOrder(order, indent + 1));
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
// Filters
|
|
108
|
-
entity.filters?.forEach((filter) => {
|
|
109
|
-
lines.push(...generateFilter(filter, indent + 1));
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// Link-entities
|
|
113
|
-
entity.links?.forEach((link) => {
|
|
114
|
-
lines.push(...generateLinkEntity(link, indent + 1));
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
lines.push(`${spaces}</entity>`);
|
|
118
|
-
|
|
119
|
-
return lines;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Generate all-attributes element
|
|
124
|
-
*/
|
|
125
|
-
function generateAllAttributes(_allAttr: AllAttributesNode, indent: number): string[] {
|
|
126
|
-
const spaces = " ".repeat(indent);
|
|
127
|
-
return [`${spaces}<all-attributes />`];
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Generate attribute element
|
|
132
|
-
*/
|
|
133
|
-
function generateAttribute(attr: AttributeNode, indent: number): string[] {
|
|
134
|
-
const spaces = " ".repeat(indent);
|
|
135
|
-
const attrs: string[] = [`name="${attr.name}"`];
|
|
136
|
-
|
|
137
|
-
if (attr.alias) attrs.push(`alias="${attr.alias}"`);
|
|
138
|
-
if (attr.aggregate) attrs.push(`aggregate="${attr.aggregate}"`);
|
|
139
|
-
if (attr.groupby) attrs.push('groupby="true"');
|
|
140
|
-
if (attr.dategrouping) attrs.push(`dategrouping="${attr.dategrouping}"`);
|
|
141
|
-
if (attr.usertimezone !== undefined) attrs.push(`usertimezone="${attr.usertimezone}"`);
|
|
142
|
-
|
|
143
|
-
return [`${spaces}<attribute ${attrs.join(" ")} />`];
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Generate order element
|
|
148
|
-
*/
|
|
149
|
-
function generateOrder(order: OrderNode, indent: number): string[] {
|
|
150
|
-
const spaces = " ".repeat(indent);
|
|
151
|
-
const attrs: string[] = [`attribute="${order.attribute}"`];
|
|
152
|
-
|
|
153
|
-
if (order.descending) attrs.push('descending="true"');
|
|
154
|
-
if (order.entityname) attrs.push(`entityname="${order.entityname}"`);
|
|
155
|
-
|
|
156
|
-
return [`${spaces}<order ${attrs.join(" ")} />`];
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Generate filter element (recursive for subfilters)
|
|
161
|
-
*/
|
|
162
|
-
function generateFilter(filter: FilterNode, indent: number): string[] {
|
|
163
|
-
const lines: string[] = [];
|
|
164
|
-
const spaces = " ".repeat(indent);
|
|
165
|
-
|
|
166
|
-
const attrs: string[] = [`type="${filter.conjunction}"`];
|
|
167
|
-
if (filter.hint) attrs.push(`hint="${filter.hint}"`);
|
|
168
|
-
|
|
169
|
-
lines.push(`${spaces}<filter ${attrs.join(" ")}>`);
|
|
170
|
-
|
|
171
|
-
// Conditions
|
|
172
|
-
filter.conditions?.forEach((condition) => {
|
|
173
|
-
lines.push(...generateCondition(condition, indent + 1));
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
// Subfilters (recursive)
|
|
177
|
-
filter.subfilters?.forEach((subfilter) => {
|
|
178
|
-
lines.push(...generateFilter(subfilter, indent + 1));
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
// Link-entities inside filter (for any/not any/all/not all)
|
|
182
|
-
filter.links?.forEach((link) => {
|
|
183
|
-
lines.push(...generateLinkEntity(link, indent + 1));
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
lines.push(`${spaces}</filter>`);
|
|
187
|
-
|
|
188
|
-
return lines;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Generate condition element
|
|
193
|
-
*/
|
|
194
|
-
function generateCondition(condition: ConditionNode, indent: number): string[] {
|
|
195
|
-
const lines: string[] = [];
|
|
196
|
-
const spaces = " ".repeat(indent);
|
|
197
|
-
|
|
198
|
-
const attrs: string[] = [
|
|
199
|
-
`attribute="${condition.attribute}"`,
|
|
200
|
-
`operator="${condition.operator}"`,
|
|
201
|
-
];
|
|
202
|
-
|
|
203
|
-
if (condition.entityname) attrs.push(`entityname="${condition.entityname}"`);
|
|
204
|
-
if (condition.aggregate) attrs.push(`aggregate="${condition.aggregate}"`);
|
|
205
|
-
// Note: ConditionNode doesn't have alias in our type definition
|
|
206
|
-
if (condition.valueof) attrs.push(`valueof="${condition.valueof}"`);
|
|
207
|
-
|
|
208
|
-
// Use the centralized operator logic to determine if value is needed
|
|
209
|
-
const needsValue = operatorRequiresValue(condition.operator);
|
|
210
|
-
|
|
211
|
-
if (!needsValue) {
|
|
212
|
-
// Self-closing tag for operators that don't need values (null, eq-userid, etc.)
|
|
213
|
-
return [`${spaces}<condition ${attrs.join(" ")} />`];
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Operators that need value attribute
|
|
217
|
-
if (condition.value !== undefined && condition.value !== null) {
|
|
218
|
-
// Handle array values (for 'in', 'not-in', 'contain-values', 'between', etc.)
|
|
219
|
-
if (Array.isArray(condition.value)) {
|
|
220
|
-
lines.push(`${spaces}<condition ${attrs.join(" ")}>`);
|
|
221
|
-
condition.value.forEach((val) => {
|
|
222
|
-
if (val !== undefined && val !== null) {
|
|
223
|
-
lines.push(`${spaces} <value>${escapeXml(formatValueForFetchXml(val))}</value>`);
|
|
224
|
-
}
|
|
225
|
-
});
|
|
226
|
-
lines.push(`${spaces}</condition>`);
|
|
227
|
-
} else {
|
|
228
|
-
// Single value attribute
|
|
229
|
-
attrs.push(`value="${escapeXml(formatValueForFetchXml(condition.value))}"`);
|
|
230
|
-
lines.push(`${spaces}<condition ${attrs.join(" ")} />`);
|
|
231
|
-
}
|
|
232
|
-
} else {
|
|
233
|
-
// No value provided but operator needs one - still generate with empty value
|
|
234
|
-
lines.push(`${spaces}<condition ${attrs.join(" ")} />`);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
return lines;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Generate link-entity element (recursive)
|
|
242
|
-
*/
|
|
243
|
-
function generateLinkEntity(link: LinkEntityNode, indent: number): string[] {
|
|
244
|
-
const lines: string[] = [];
|
|
245
|
-
const spaces = " ".repeat(indent);
|
|
246
|
-
|
|
247
|
-
const attrs: string[] = [`name="${link.name}"`, `from="${link.from}"`, `to="${link.to}"`];
|
|
248
|
-
|
|
249
|
-
if (link.alias) attrs.push(`alias="${link.alias}"`);
|
|
250
|
-
if (link.linkType !== "inner") attrs.push(`link-type="${link.linkType}"`);
|
|
251
|
-
if (link.intersect) attrs.push('intersect="true"');
|
|
252
|
-
if (link.visible !== undefined) attrs.push(`visible="${link.visible}"`);
|
|
253
|
-
|
|
254
|
-
lines.push(`${spaces}<link-entity ${attrs.join(" ")}>`);
|
|
255
|
-
|
|
256
|
-
// All-attributes (single node, not array)
|
|
257
|
-
if (link.allAttributes?.enabled) {
|
|
258
|
-
lines.push(...generateAllAttributes(link.allAttributes, indent + 1));
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Attributes
|
|
262
|
-
link.attributes?.forEach((attr) => {
|
|
263
|
-
lines.push(...generateAttribute(attr, indent + 1));
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
// Orders
|
|
267
|
-
link.orders?.forEach((order) => {
|
|
268
|
-
lines.push(...generateOrder(order, indent + 1));
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
// Filters
|
|
272
|
-
link.filters?.forEach((filter) => {
|
|
273
|
-
lines.push(...generateFilter(filter, indent + 1));
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
// Nested link-entities (recursive)
|
|
277
|
-
link.links?.forEach((nestedLink) => {
|
|
278
|
-
lines.push(...generateLinkEntity(nestedLink, indent + 1));
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
lines.push(`${spaces}</link-entity>`);
|
|
282
|
-
|
|
283
|
-
return lines;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Escape XML special characters
|
|
288
|
-
*/
|
|
289
|
-
function escapeXml(str: string): string {
|
|
290
|
-
return str
|
|
291
|
-
.replace(/&/g, "&")
|
|
292
|
-
.replace(/</g, "<")
|
|
293
|
-
.replace(/>/g, ">")
|
|
294
|
-
.replace(/"/g, """)
|
|
295
|
-
.replace(/'/g, "'");
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Modify FetchXML string to add/update paging parameters
|
|
300
|
-
* Used for fetching subsequent pages during Retrieve All or infinite scroll
|
|
301
|
-
*
|
|
302
|
-
* Per Dataverse requirements:
|
|
303
|
-
* - page: Page number (1-based)
|
|
304
|
-
* - paging-cookie: XML cookie from previous response, must be XML-entity encoded
|
|
305
|
-
* - count: Page size (records per page)
|
|
306
|
-
*/
|
|
307
|
-
export function addPagingToFetchXml(
|
|
308
|
-
fetchXml: string,
|
|
309
|
-
page: number,
|
|
310
|
-
pagingCookie?: string,
|
|
311
|
-
count?: number
|
|
312
|
-
): string {
|
|
313
|
-
// Parse the fetch element attributes
|
|
314
|
-
const fetchMatch = fetchXml.match(/<fetch([^>]*)>/);
|
|
315
|
-
if (!fetchMatch) return fetchXml;
|
|
316
|
-
|
|
317
|
-
let fetchAttrs = fetchMatch[1];
|
|
318
|
-
|
|
319
|
-
// Remove existing page, paging-cookie, count attributes
|
|
320
|
-
// Note: paging-cookie can contain encoded quotes, so we need a more robust pattern
|
|
321
|
-
fetchAttrs = fetchAttrs
|
|
322
|
-
.replace(/\s*page="[^"]*"/g, "")
|
|
323
|
-
.replace(/\s*paging-cookie="[^"]*"/g, "") // This works because the value is already XML-encoded (no raw quotes)
|
|
324
|
-
.replace(/\s*count="[^"]*"/g, "");
|
|
325
|
-
|
|
326
|
-
// Add new paging attributes
|
|
327
|
-
fetchAttrs = fetchAttrs.trim();
|
|
328
|
-
|
|
329
|
-
// Always add page number (even page 1 for explicit paging requests)
|
|
330
|
-
fetchAttrs += ` page="${page}"`;
|
|
331
|
-
|
|
332
|
-
// Add paging cookie if provided (required for reliable paging after page 1)
|
|
333
|
-
if (pagingCookie) {
|
|
334
|
-
// The paging cookie needs to be XML-escaped when inserted as an attribute value
|
|
335
|
-
// Dataverse returns it as raw XML, we need to encode it for the attribute
|
|
336
|
-
fetchAttrs += ` paging-cookie="${escapeXml(pagingCookie)}"`;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// Add count (page size) if specified
|
|
340
|
-
if (count !== undefined) {
|
|
341
|
-
fetchAttrs += ` count="${count}"`;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// Reconstruct the fetch element
|
|
345
|
-
return fetchXml.replace(/<fetch[^>]*>/, `<fetch${fetchAttrs ? " " + fetchAttrs : ""}>`);
|
|
346
|
-
}
|