@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,583 +0,0 @@
1
- /**
2
- * FetchXML Intellisense Provider for Monaco Editor
3
- * Provides autocomplete suggestions for FetchXML elements, attributes, and values
4
- * Based on the official Fetch.xsd schema
5
- */
6
-
7
- import type * as Monaco from "monaco-editor";
8
-
9
- /**
10
- * Attribute definition types
11
- */
12
- interface AttributeDef {
13
- description: string;
14
- type?: "boolean" | "integer" | string;
15
- values?: string[];
16
- required?: boolean;
17
- default?: string;
18
- }
19
-
20
- interface ElementDef {
21
- description: string;
22
- attributes: Record<string, AttributeDef>;
23
- children: string[];
24
- }
25
-
26
- /**
27
- * FetchXML element definitions with their allowed attributes and children
28
- * Derived from Fetch.xsd schema
29
- */
30
- const FETCHXML_SCHEMA: { elements: Record<string, ElementDef> } = {
31
- elements: {
32
- fetch: {
33
- description: "Root element for a FetchXML query",
34
- attributes: {
35
- version: { description: "FetchXML version" },
36
- count: { description: "Number of records per page", type: "integer" },
37
- page: { description: "Page number to retrieve", type: "integer" },
38
- "paging-cookie": { description: "Paging cookie for subsequent pages" },
39
- "utc-offset": { description: "UTC offset in minutes", type: "integer" },
40
- aggregate: { description: "Enable aggregate queries", type: "boolean" },
41
- distinct: { description: "Return only distinct records", type: "boolean" },
42
- top: { description: "Maximum number of records to return", type: "integer" },
43
- mapping: { description: "Data mapping type", values: ["internal", "logical"] },
44
- "min-active-row-version": { description: "Minimum active row version", type: "boolean" },
45
- "output-format": {
46
- description: "Output format",
47
- values: ["xml-ado", "xml-auto", "xml-elements", "xml-raw", "xml-platform"],
48
- },
49
- returntotalrecordcount: { description: "Return total record count", type: "boolean" },
50
- "no-lock": { description: "Disable row locking", type: "boolean" },
51
- },
52
- children: ["entity", "order"],
53
- },
54
- entity: {
55
- description: "Primary entity for the query",
56
- attributes: {
57
- name: { description: "Logical name of the entity", required: true },
58
- enableprefiltering: { description: "Enable pre-filtering for reports", type: "boolean" },
59
- prefilterparametername: { description: "Pre-filter parameter name" },
60
- },
61
- children: ["all-attributes", "attribute", "order", "filter", "link-entity"],
62
- },
63
- "link-entity": {
64
- description: "Join to a related entity",
65
- attributes: {
66
- name: { description: "Logical name of the linked entity", required: true },
67
- from: { description: "Attribute on the linked entity to join from" },
68
- to: { description: "Attribute on the parent entity to join to" },
69
- alias: { description: "Alias for the linked entity" },
70
- "link-type": {
71
- description: "Type of join",
72
- values: [
73
- "inner",
74
- "outer",
75
- "any",
76
- "not any",
77
- "all",
78
- "not all",
79
- "exists",
80
- "in",
81
- "matchfirstrowusingcrossapply",
82
- ],
83
- },
84
- visible: { description: "Include in results", type: "boolean" },
85
- intersect: { description: "Used for N:N relationships", type: "boolean" },
86
- enableprefiltering: { description: "Enable pre-filtering for reports", type: "boolean" },
87
- prefilterparametername: { description: "Pre-filter parameter name" },
88
- },
89
- children: ["all-attributes", "attribute", "order", "filter", "link-entity"],
90
- },
91
- "all-attributes": {
92
- description: "Select all attributes from the entity",
93
- attributes: {},
94
- children: [],
95
- },
96
- attribute: {
97
- description: "Select a specific attribute",
98
- attributes: {
99
- name: { description: "Logical name of the attribute", required: true },
100
- alias: { description: "Alias for the attribute in results" },
101
- aggregate: {
102
- description: "Aggregate function",
103
- values: ["count", "countcolumn", "sum", "avg", "min", "max"],
104
- },
105
- groupby: { description: "Group by this attribute", type: "boolean" },
106
- dategrouping: {
107
- description: "Date grouping",
108
- values: ["day", "week", "month", "quarter", "year", "fiscal-period", "fiscal-year"],
109
- },
110
- usertimezone: { description: "Use user timezone", type: "boolean" },
111
- distinct: { description: "Distinct values only", type: "boolean" },
112
- },
113
- children: [],
114
- },
115
- order: {
116
- description: "Sort order specification",
117
- attributes: {
118
- attribute: { description: "Attribute to sort by" },
119
- alias: { description: "Alias of attribute to sort by (for aggregates)" },
120
- descending: { description: "Sort in descending order", type: "boolean" },
121
- },
122
- children: [],
123
- },
124
- filter: {
125
- description: "Filter criteria (WHERE clause)",
126
- attributes: {
127
- type: { description: "Logical operator", values: ["and", "or"], default: "and" },
128
- isquickfindfields: { description: "Quick find filter", type: "boolean" },
129
- },
130
- children: ["condition", "filter", "link-entity"],
131
- },
132
- condition: {
133
- description: "Filter condition",
134
- attributes: {
135
- attribute: { description: "Attribute to filter on", required: true },
136
- operator: {
137
- description: "Comparison operator",
138
- required: true,
139
- values: [
140
- // Comparison
141
- "eq",
142
- "ne",
143
- "neq",
144
- "gt",
145
- "ge",
146
- "lt",
147
- "le",
148
- // String
149
- "like",
150
- "not-like",
151
- "begins-with",
152
- "not-begin-with",
153
- "ends-with",
154
- "not-end-with",
155
- // Collection
156
- "in",
157
- "not-in",
158
- "between",
159
- "not-between",
160
- "contain-values",
161
- "not-contain-values",
162
- // Null
163
- "null",
164
- "not-null",
165
- // Date relative
166
- "yesterday",
167
- "today",
168
- "tomorrow",
169
- "last-seven-days",
170
- "next-seven-days",
171
- "last-week",
172
- "this-week",
173
- "next-week",
174
- "last-month",
175
- "this-month",
176
- "next-month",
177
- "last-year",
178
- "this-year",
179
- "next-year",
180
- "on",
181
- "on-or-before",
182
- "on-or-after",
183
- // Date X
184
- "last-x-hours",
185
- "next-x-hours",
186
- "last-x-days",
187
- "next-x-days",
188
- "last-x-weeks",
189
- "next-x-weeks",
190
- "last-x-months",
191
- "next-x-months",
192
- "last-x-years",
193
- "next-x-years",
194
- "olderthan-x-minutes",
195
- "olderthan-x-hours",
196
- "olderthan-x-days",
197
- "olderthan-x-weeks",
198
- "olderthan-x-months",
199
- "olderthan-x-years",
200
- // Fiscal
201
- "this-fiscal-year",
202
- "this-fiscal-period",
203
- "next-fiscal-year",
204
- "next-fiscal-period",
205
- "last-fiscal-year",
206
- "last-fiscal-period",
207
- "last-x-fiscal-years",
208
- "last-x-fiscal-periods",
209
- "next-x-fiscal-years",
210
- "next-x-fiscal-periods",
211
- "in-fiscal-year",
212
- "in-fiscal-period",
213
- "in-fiscal-period-and-year",
214
- "in-or-before-fiscal-period-and-year",
215
- "in-or-after-fiscal-period-and-year",
216
- // User/Business
217
- "eq-userid",
218
- "ne-userid",
219
- "eq-userteams",
220
- "eq-useroruserteams",
221
- "eq-useroruserhierarchy",
222
- "eq-useroruserhierarchyandteams",
223
- "eq-businessid",
224
- "ne-businessid",
225
- "eq-userlanguage",
226
- // Hierarchy
227
- "under",
228
- "eq-or-under",
229
- "not-under",
230
- "above",
231
- "eq-or-above",
232
- ],
233
- },
234
- value: { description: "Value to compare against" },
235
- valueof: { description: "Compare to another attribute value" },
236
- entityname: { description: "Entity alias for link-entity conditions" },
237
- aggregate: {
238
- description: "Aggregate function for HAVING clause",
239
- values: ["count", "countcolumn", "sum", "avg", "min", "max"],
240
- },
241
- alias: { description: "Alias for the condition" },
242
- uiname: { description: "UI display name" },
243
- uitype: { description: "UI type" },
244
- uihidden: { description: "Hide in UI", values: ["0", "1"] },
245
- },
246
- children: ["value"],
247
- },
248
- value: {
249
- description: "Value element for multi-value operators (in, not-in, etc.)",
250
- attributes: {
251
- uiname: { description: "UI display name" },
252
- uitype: { description: "UI type" },
253
- },
254
- children: [],
255
- },
256
- },
257
- } as const;
258
-
259
- type ElementName = keyof typeof FETCHXML_SCHEMA.elements;
260
-
261
- /**
262
- * Get the current element context from cursor position
263
- */
264
- function getElementContext(
265
- model: Monaco.editor.ITextModel,
266
- position: Monaco.Position
267
- ): {
268
- element: string | null;
269
- inAttribute: boolean;
270
- attributeName: string | null;
271
- inValue: boolean;
272
- } {
273
- const textUntilPosition = model.getValueInRange({
274
- startLineNumber: 1,
275
- startColumn: 1,
276
- endLineNumber: position.lineNumber,
277
- endColumn: position.column,
278
- });
279
-
280
- // Find the last opened element
281
- const elementMatches = textUntilPosition.match(/<(\/?)([\w-]+)(?:\s|>|$)/g);
282
- const openElements: string[] = [];
283
-
284
- if (elementMatches) {
285
- for (const match of elementMatches) {
286
- const isClosing = match.startsWith("</");
287
- const elementName = match.replace(/<\/?/, "").replace(/[\s>].*/, "");
288
-
289
- if (isClosing) {
290
- // Pop the last matching open element
291
- const idx = openElements.lastIndexOf(elementName);
292
- if (idx !== -1) {
293
- openElements.splice(idx, 1);
294
- }
295
- } else if (!match.includes("/>")) {
296
- openElements.push(elementName);
297
- }
298
- }
299
- }
300
-
301
- const currentElement = openElements[openElements.length - 1] || null;
302
-
303
- // Check if we're inside an attribute value
304
- const lineText = model.getLineContent(position.lineNumber);
305
- const textBeforeCursor = lineText.substring(0, position.column - 1);
306
-
307
- // Check if inside attribute value (after = and inside quotes)
308
- const attrValueMatch = textBeforeCursor.match(/(\w+)\s*=\s*["']([^"']*)$/);
309
- if (attrValueMatch) {
310
- return {
311
- element: currentElement,
312
- inAttribute: true,
313
- attributeName: attrValueMatch[1],
314
- inValue: true,
315
- };
316
- }
317
-
318
- // Check if we're in an opening tag (for attribute suggestions)
319
- const inOpeningTag = /<[\w-]+[^>]*$/.test(textBeforeCursor);
320
-
321
- return {
322
- element: currentElement,
323
- inAttribute: inOpeningTag,
324
- attributeName: null,
325
- inValue: false,
326
- };
327
- }
328
-
329
- /**
330
- * Create Monaco completion items for FetchXML
331
- */
332
- function createCompletionItem(
333
- label: string,
334
- kind: Monaco.languages.CompletionItemKind,
335
- detail: string,
336
- insertText: string,
337
- range: Monaco.IRange,
338
- documentation?: string
339
- ): Monaco.languages.CompletionItem {
340
- return {
341
- label,
342
- kind,
343
- detail,
344
- insertText,
345
- range,
346
- documentation,
347
- insertTextRules: insertText.includes("$") ? 4 : undefined, // InsertAsSnippet if contains $
348
- };
349
- }
350
-
351
- /**
352
- * Register FetchXML completion provider with Monaco
353
- */
354
- export function registerFetchXmlIntellisense(monaco: typeof Monaco): Monaco.IDisposable {
355
- return monaco.languages.registerCompletionItemProvider("xml", {
356
- triggerCharacters: ["<", " ", "=", '"', "'"],
357
-
358
- provideCompletionItems(
359
- model: Monaco.editor.ITextModel,
360
- position: Monaco.Position
361
- ): Monaco.languages.ProviderResult<Monaco.languages.CompletionList> {
362
- const word = model.getWordUntilPosition(position);
363
- const range: Monaco.IRange = {
364
- startLineNumber: position.lineNumber,
365
- startColumn: word.startColumn,
366
- endLineNumber: position.lineNumber,
367
- endColumn: word.endColumn,
368
- };
369
-
370
- const context = getElementContext(model, position);
371
- const lineText = model.getLineContent(position.lineNumber);
372
- const textBeforeCursor = lineText.substring(0, position.column - 1);
373
-
374
- const suggestions: Monaco.languages.CompletionItem[] = [];
375
-
376
- // Inside attribute value - suggest valid values
377
- if (context.inValue && context.attributeName && context.element) {
378
- const elementDef = FETCHXML_SCHEMA.elements[context.element as ElementName];
379
- if (elementDef) {
380
- const attrDef =
381
- elementDef.attributes[context.attributeName as keyof typeof elementDef.attributes];
382
- if (attrDef && "values" in attrDef && attrDef.values) {
383
- for (const value of attrDef.values) {
384
- suggestions.push(
385
- createCompletionItem(
386
- value,
387
- monaco.languages.CompletionItemKind.Value,
388
- `Value for ${context.attributeName}`,
389
- value,
390
- range
391
- )
392
- );
393
- }
394
- } else if (attrDef && "type" in attrDef && attrDef.type === "boolean") {
395
- suggestions.push(
396
- createCompletionItem(
397
- "true",
398
- monaco.languages.CompletionItemKind.Value,
399
- "Boolean true",
400
- "true",
401
- range
402
- )
403
- );
404
- suggestions.push(
405
- createCompletionItem(
406
- "false",
407
- monaco.languages.CompletionItemKind.Value,
408
- "Boolean false",
409
- "false",
410
- range
411
- )
412
- );
413
- }
414
- }
415
- return { suggestions };
416
- }
417
-
418
- // Inside opening tag - suggest attributes
419
- if (context.inAttribute && context.element && !context.inValue) {
420
- const elementDef = FETCHXML_SCHEMA.elements[context.element as ElementName];
421
- if (elementDef) {
422
- for (const [attrName, attrDef] of Object.entries(elementDef.attributes)) {
423
- // Check if attribute already exists on this line
424
- if (lineText.includes(`${attrName}=`)) continue;
425
-
426
- const required = "required" in attrDef && attrDef.required;
427
- const hasValues = "values" in attrDef && attrDef.values;
428
-
429
- suggestions.push(
430
- createCompletionItem(
431
- attrName,
432
- monaco.languages.CompletionItemKind.Property,
433
- required ? "(required)" : "(optional)",
434
- hasValues ? `${attrName}="$1"` : `${attrName}="$1"`,
435
- range,
436
- attrDef.description
437
- )
438
- );
439
- }
440
- }
441
- return { suggestions };
442
- }
443
-
444
- // After < - suggest elements
445
- if (textBeforeCursor.endsWith("<") || textBeforeCursor.match(/<\w*$/)) {
446
- // Determine valid child elements based on parent
447
- let validElements: string[] = [];
448
-
449
- if (!context.element) {
450
- // Root level - only fetch is valid
451
- validElements = ["fetch"];
452
- } else {
453
- const parentDef = FETCHXML_SCHEMA.elements[context.element as ElementName];
454
- if (parentDef) {
455
- validElements = [...parentDef.children];
456
- }
457
- }
458
-
459
- // Add closing tag suggestion if in an element
460
- if (context.element) {
461
- suggestions.push(
462
- createCompletionItem(
463
- `/${context.element}`,
464
- monaco.languages.CompletionItemKind.Keyword,
465
- `Close <${context.element}> tag`,
466
- `/${context.element}>`,
467
- range
468
- )
469
- );
470
- }
471
-
472
- for (const elemName of validElements) {
473
- const elemDef = FETCHXML_SCHEMA.elements[elemName as ElementName];
474
- if (elemDef) {
475
- // Find required attributes
476
- const requiredAttrs = Object.entries(elemDef.attributes)
477
- .filter(([, def]) => "required" in def && def.required)
478
- .map(([name]) => name);
479
-
480
- // Create snippet with required attributes
481
- let snippet = elemName;
482
- let tabStop = 1;
483
- for (const attr of requiredAttrs) {
484
- snippet += ` ${attr}="$${tabStop++}"`;
485
- }
486
-
487
- // Self-closing if no children, otherwise add closing tag
488
- if (elemDef.children.length === 0) {
489
- snippet += " />";
490
- } else {
491
- snippet += ">$0</" + elemName + ">";
492
- }
493
-
494
- suggestions.push(
495
- createCompletionItem(
496
- elemName,
497
- monaco.languages.CompletionItemKind.Class,
498
- "FetchXML element",
499
- snippet,
500
- range,
501
- elemDef.description
502
- )
503
- );
504
- }
505
- }
506
-
507
- return { suggestions };
508
- }
509
-
510
- return { suggestions };
511
- },
512
- });
513
- }
514
-
515
- /**
516
- * Get FetchXML hover information
517
- */
518
- export function registerFetchXmlHoverProvider(monaco: typeof Monaco): Monaco.IDisposable {
519
- return monaco.languages.registerHoverProvider("xml", {
520
- provideHover(
521
- model: Monaco.editor.ITextModel,
522
- position: Monaco.Position
523
- ): Monaco.languages.ProviderResult<Monaco.languages.Hover> {
524
- const word = model.getWordAtPosition(position);
525
- if (!word) return null;
526
-
527
- const lineText = model.getLineContent(position.lineNumber);
528
-
529
- // Check if hovering over an element name
530
- const elementMatch = lineText.match(new RegExp(`<(/?)(${word.word})(?:\\s|>|/)`));
531
- if (elementMatch) {
532
- const elemDef = FETCHXML_SCHEMA.elements[word.word as ElementName];
533
- if (elemDef) {
534
- return {
535
- contents: [
536
- { value: `**<${word.word}>**` },
537
- { value: elemDef.description },
538
- {
539
- value:
540
- elemDef.children.length > 0
541
- ? `\n**Child elements:** ${elemDef.children.join(", ")}`
542
- : "\n*Self-closing element*",
543
- },
544
- ],
545
- };
546
- }
547
- }
548
-
549
- // Check if hovering over an attribute name
550
- const attrMatch = lineText.match(new RegExp(`(${word.word})\\s*=`));
551
- if (attrMatch) {
552
- // Find parent element
553
- const context = getElementContext(model, position);
554
- if (context.element) {
555
- const elemDef = FETCHXML_SCHEMA.elements[context.element as ElementName];
556
- if (elemDef) {
557
- const attrDef = elemDef.attributes[word.word as keyof typeof elemDef.attributes];
558
- if (attrDef) {
559
- const contents: Monaco.IMarkdownString[] = [
560
- { value: `**${word.word}**` },
561
- { value: attrDef.description },
562
- ];
563
-
564
- if ("values" in attrDef && attrDef.values) {
565
- contents.push({ value: `\n**Valid values:** ${attrDef.values.join(", ")}` });
566
- }
567
- if ("type" in attrDef && attrDef.type) {
568
- contents.push({ value: `\n**Type:** ${attrDef.type}` });
569
- }
570
- if ("required" in attrDef && attrDef.required) {
571
- contents.push({ value: "\n*Required attribute*" });
572
- }
573
-
574
- return { contents };
575
- }
576
- }
577
- }
578
- }
579
-
580
- return null;
581
- },
582
- });
583
- }