@mohsinonxrm/pptb-fetchxml-studio 1.4.1 → 1.5.0-beta.12

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 (102) hide show
  1. package/CHANGELOG.md +1485 -1039
  2. package/LICENSE +661 -661
  3. package/README.md +462 -426
  4. package/SECURITY.md +13 -13
  5. package/dist/assets/codicon-DF1abBS2.ttf +0 -0
  6. package/dist/assets/index-CaYaZM-I.js +109 -0
  7. package/dist/assets/monaco-editor--wwQ80g0.css +1 -0
  8. package/dist/assets/monaco-editor-HrWgH4_j.js +988 -0
  9. package/dist/icons/icon.svg +16 -12
  10. package/dist/index.html +15 -15
  11. package/index.html +70 -70
  12. package/package.json +112 -102
  13. package/pptb.config.json +46 -46
  14. package/dist/assets/abap-CRCWOmpq.js +0 -1
  15. package/dist/assets/apex-DnsZk_dE.js +0 -1
  16. package/dist/assets/azcli-1IWB1ccx.js +0 -1
  17. package/dist/assets/bat-DPkNLes8.js +0 -1
  18. package/dist/assets/bicep-Corcdgou.js +0 -2
  19. package/dist/assets/cameligo-CGrWLZr3.js +0 -1
  20. package/dist/assets/clojure-D9WOWImG.js +0 -1
  21. package/dist/assets/codicon-B_Z2XQ3P.ttf +0 -0
  22. package/dist/assets/coffee-B7EJu28W.js +0 -1
  23. package/dist/assets/cpp-SEyurbux.js +0 -1
  24. package/dist/assets/csharp-BoL64M5l.js +0 -1
  25. package/dist/assets/csp-C46ZqvIl.js +0 -1
  26. package/dist/assets/css-DQU6DXDx.js +0 -3
  27. package/dist/assets/cssMode-lrJE5Iiz.js +0 -4
  28. package/dist/assets/cypher-D84EuPTj.js +0 -1
  29. package/dist/assets/dart-D8lhlL1r.js +0 -1
  30. package/dist/assets/dockerfile-DLk6rpji.js +0 -1
  31. package/dist/assets/ecl-BO6FnfXk.js +0 -1
  32. package/dist/assets/elixir-BRjLKONM.js +0 -1
  33. package/dist/assets/flow9-Cac8vKd7.js +0 -1
  34. package/dist/assets/freemarker2-DwmPKpl4.js +0 -3
  35. package/dist/assets/fsharp-fd1GTHhf.js +0 -1
  36. package/dist/assets/go-O9LJTZXk.js +0 -1
  37. package/dist/assets/graphql-LQdxqEYJ.js +0 -1
  38. package/dist/assets/handlebars-Cf8Yk1XH.js +0 -1
  39. package/dist/assets/hcl-DxDQ3s82.js +0 -1
  40. package/dist/assets/html-B8r17viV.js +0 -1
  41. package/dist/assets/htmlMode-Deu2Cnyl.js +0 -4
  42. package/dist/assets/index-BQgTOj7B.js +0 -107
  43. package/dist/assets/ini-BvajGCUy.js +0 -1
  44. package/dist/assets/java-SYsfObOQ.js +0 -1
  45. package/dist/assets/javascript-21fivyfm.js +0 -1
  46. package/dist/assets/jsonMode-C6fJmDrp.js +0 -10
  47. package/dist/assets/julia-DQXNmw_w.js +0 -1
  48. package/dist/assets/kotlin-qQ0MG-9I.js +0 -1
  49. package/dist/assets/less-GGFNNJHn.js +0 -2
  50. package/dist/assets/lexon-Canl7DCW.js +0 -1
  51. package/dist/assets/liquid-CBhIUaM_.js +0 -1
  52. package/dist/assets/lua-D28Ae8-K.js +0 -1
  53. package/dist/assets/m3-DPitgjJI.js +0 -1
  54. package/dist/assets/markdown-B811l8j2.js +0 -1
  55. package/dist/assets/mdx-DKh1a8vs.js +0 -1
  56. package/dist/assets/mips-CdjsipkG.js +0 -1
  57. package/dist/assets/monaco-editor-BhPcksyq.css +0 -1
  58. package/dist/assets/monaco-editor-CSKh0Kpy.js +0 -901
  59. package/dist/assets/msdax-CYqgjx_P.js +0 -1
  60. package/dist/assets/mysql-BHd6q0vd.js +0 -1
  61. package/dist/assets/objective-c-B1aVtJYH.js +0 -1
  62. package/dist/assets/pascal-BhNW15KB.js +0 -1
  63. package/dist/assets/pascaligo-5jv8CcQD.js +0 -1
  64. package/dist/assets/perl-DlYyT36c.js +0 -1
  65. package/dist/assets/pgsql-Dy0bjov7.js +0 -1
  66. package/dist/assets/php-120yhfDK.js +0 -1
  67. package/dist/assets/pla-CjnFlu4u.js +0 -1
  68. package/dist/assets/postiats-CQpG440k.js +0 -1
  69. package/dist/assets/powerquery-DdJtto1Z.js +0 -1
  70. package/dist/assets/powershell-Bu_VLpJB.js +0 -1
  71. package/dist/assets/protobuf-IBS6jZEB.js +0 -2
  72. package/dist/assets/pug-kFxLfcjb.js +0 -1
  73. package/dist/assets/python-C1DvjtTK.js +0 -1
  74. package/dist/assets/qsharp-q7JyzKFN.js +0 -1
  75. package/dist/assets/r-BIFz-_sK.js +0 -1
  76. package/dist/assets/razor-CxbdKBwV.js +0 -1
  77. package/dist/assets/redis-CHOsPHWR.js +0 -1
  78. package/dist/assets/redshift-CBifECDb.js +0 -1
  79. package/dist/assets/restructuredtext-CghPJEOS.js +0 -1
  80. package/dist/assets/ruby-CYWGW-b1.js +0 -1
  81. package/dist/assets/rust-DMDD0SHb.js +0 -1
  82. package/dist/assets/sb-BYAiYHFx.js +0 -1
  83. package/dist/assets/scala-Bqvq8jcR.js +0 -1
  84. package/dist/assets/scheme-Dhb-2j9p.js +0 -1
  85. package/dist/assets/scss-CTwUZ5N7.js +0 -3
  86. package/dist/assets/shell-CsDZo4DB.js +0 -1
  87. package/dist/assets/solidity-CME5AdoB.js +0 -1
  88. package/dist/assets/sophia-RYC1BQQz.js +0 -1
  89. package/dist/assets/sparql-KEyrF7De.js +0 -1
  90. package/dist/assets/sql-BdTr02Mf.js +0 -1
  91. package/dist/assets/st-C7iG7M4S.js +0 -1
  92. package/dist/assets/swift-D7IUmUK8.js +0 -1
  93. package/dist/assets/systemverilog-DgMryOEJ.js +0 -1
  94. package/dist/assets/tcl-PloMZuKG.js +0 -1
  95. package/dist/assets/tsMode-CoKGlBK3.js +0 -11
  96. package/dist/assets/twig-BfRIq3la.js +0 -1
  97. package/dist/assets/typescript-DE4VX4Fi.js +0 -1
  98. package/dist/assets/typespec-CzxlYoT_.js +0 -1
  99. package/dist/assets/vb-BwAE3J76.js +0 -1
  100. package/dist/assets/wgsl-B_1kOXbF.js +0 -298
  101. package/dist/assets/xml-D5FP6KmI.js +0 -1
  102. package/dist/assets/yaml-_F_B7Ufj.js +0 -1
package/README.md CHANGED
@@ -1,426 +1,462 @@
1
- # FetchXML Studio for Power Platform ToolBox
2
-
3
- A powerful, modern FetchXML query builder and data explorer for [Power Platform ToolBox](https://github.com/PowerPlatformToolBox/desktop-app). Inspired by the XrmToolBox FetchXML Builder, reimagined with React 18, Fluent UI v9, and seamless Dataverse integration.
4
-
5
- ![Version](https://img.shields.io/badge/version-1.4.1-blue)
6
- [![npm](https://img.shields.io/npm/v/@mohsinonxrm/pptb-fetchxml-studio?label=npm)](https://www.npmjs.com/package/@mohsinonxrm/pptb-fetchxml-studio)
7
- ![React](https://img.shields.io/badge/React-18-61DAFB?logo=react)
8
- ![TypeScript](https://img.shields.io/badge/TypeScript-5.9-3178C6?logo=typescript)
9
- ![Fluent UI](https://img.shields.io/badge/Fluent%20UI-v9-0078D4?logo=microsoft)
10
- ![PPTB Types](https://img.shields.io/badge/%40pptb%2Ftypes-1.2.3-blue)
11
-
12
- ## ✨ Features
13
-
14
- ### 🔧 Visual Query Builder
15
- - **Tree-based hierarchy** — Build FetchXML queries visually with entities, attributes, filters, orders, and link-entities
16
- - **Property panel for every node** — Fetch options, entity, attributes, all-attributes, sorts, filters, conditions and link-entities each have an editor covering the full FetchXML reference. Every field explains itself in an info popover with a link to Microsoft Learn.
17
- - **Metadata-driven** — Attribute pickers, operators, value pickers, aggregate functions, date grouping, sortable attributes and compare-to-attribute choices follow each attribute's metadata, so the panel only offers what Dataverse accepts.
18
- - **Shows problems where they are** — Rule errors appear on the field they're about and as an **Error** badge in the tree; settings Dataverse ignores are listed with a **Remove** button.
19
- - **Tree badges** — Aggregate, Distinct, Retained and Hints on the query; Group by, date grouping and aggregate functions on attributes; Union and Quick find on filters.
20
- - **Safe mode changes** — Turning off aggregate, or switching a link-entity to a type that returns no attributes, lists what has to change and asks first.
21
- - **Full FetchXML support** — Aggregate queries, grouping, distinct, top/count, paging, and advanced hints
22
- - **Nested filters** — Create complex AND/OR filter groups with unlimited nesting
23
- - **Link-entity relationships** — Browse and add 1:N, N:1, and N:N relationships with inner/outer joins; N:N relationships join through their intersect entity, as documented; intersect entities (such as `systemuserroles` or `teammembership`) can be joined to each side of their N:N relationship
24
-
25
- ### 📊 Results Grid
26
- - **Virtualized DataGrid** — Handle large result sets with smooth scrolling (powered by react-window)
27
- - **Paging and Retrieve All** — Scrolling to the end loads the next page; **Retrieve All** loads every page, with **Stop** to keep what's loaded so far. Pages use the documented paging cookie and the query's own `count` and starting `page`. Queries Dataverse returns no cookie for (sorted by a link-entity attribute, or distinct) page by page number, with a note. Views follow `@odata.nextLink`. A failed page shows how many rows loaded, with **Retry**
28
- - **Rich cell rendering** — Power Apps-style display for lookups, option sets, dates, currency, and more
29
- - **Multi-column sorting** — Click headers to sort; Shift+click for multi-column sort. Each click writes a valid `<order>`: by alias in aggregate queries, with `entityname` for link-entity columns, and not at all for attribute types Dataverse can't sort (multi-select option sets)
30
- - **Edit / Add columns** — Remove, reorder and add columns from the root entity or related entities (N:1 and 1:N); the tree and FetchXML change to match. Removing a column removes its attribute (aliased or not, root or link-entity), any sort on its alias, and an outer link-entity left empty. At least one column must remain (a query without attributes returns every attribute). Add columns isn't available in aggregate queries
31
- - **Link-entities without an alias** — Their columns show in the grid: Dataverse names them `{name}{N}.attribute`, and the grid uses the same names
32
- - **Row selection** — Select single or multiple records for bulk operations. Record actions use the table's primary key, which Dataverse returns automatically for regular queries; for aggregate and distinct queries (which return no record IDs) record actions are disabled with an explanation
33
- - **Resizable & reorderable columns** — Customize your view with drag-and-drop columns
34
- - **Formatted values** — Display OData formatted values or raw values (configurable)
35
- - **Works with every entity** — Queries run against the entity set name from Dataverse metadata, so entities with irregular names (for example `feedback`, `msdyn_conversationdata`) run correctly. The Power Platform ToolBox host's own name guess is wrong for about 4% of entities.
36
-
37
- ### 📝 FetchXML Editor
38
- - **Monaco editor** — Full-featured XML editor with XML syntax highlighting and line numbers
39
- - **Bi-directional editing** — Edit XML directly and parse back to the visual builder
40
- - **Intellisense from the reference** — Element, attribute and value suggestions follow the FetchXML reference (every documented attribute, including `aggregatelimit`, `datasource`, `options`, `rowaggregate`, order `entityname` and the quick find overrides). Undocumented attributes are suggested only where Dataverse was verified to accept them, and say so. Value suggestions work for hyphenated attributes such as `link-type`
41
- - **Power Automate–safe values** — Apostrophes stay literal in the generated FetchXML (attribute values use double quotes), so expressions such as `@{body('x')}` paste into Power Automate unchanged; `"`, `&`, `<` and `>` are escaped everywhere, including names, aliases and `from` / `to`
42
- - **Editor mode actions run what you see** — In editor mode, **Execute**, **Save** and **Send to Tool** first load your FetchXML into the tree, so results, saved views and sent queries always match the editor. Execute is available as soon as the editor has content, even before an entity is chosen. If the tree would change anything, a confirmation lists the changes first, and the editor then shows the cleaned FetchXML that ran. Invalid FetchXML is reported and nothing runs.
43
- - **What you build is what runs** — The generated FetchXML contains exactly what is in the tree; nothing is injected, so what you copy, save as a view, or send to another tool matches what executes
44
- - **Copy to clipboard** — One-click copy of generated FetchXML, available in both read-only and editor modes
45
- - **Alias validation** — Invalid alias characters are sanitized in real time in the property editors; Parse to Tree corrects invalid aliases, lists each correction, and updates every reference to a corrected alias (condition and order `entityname`, `valueof`, aggregate order aliases) so the query keeps working
46
- - **Accurate validation** — Everything in the FetchXML reference is supported, plus undocumented behaviour verified to work (e.g. `link-type="natural"`). Every rule is checked against a live Dataverse environment:
47
- - **errors** block execution — only for FetchXML Dataverse is known to reject;
48
- - **warnings** offer **Execute anyway** — only where it can't be determined in the client (e.g. a link-entity without `from`/`to`, which is valid only for system many-to-many relationships);
49
- - **notes** never block (e.g. `xmlns:*` from other tools, which Dataverse ignores).
50
- - **Query-level checks** — Validate and Execute catch the mistakes Dataverse rejects, each verified on a live environment:
51
- - aggregate queries where an attribute has no Group by / aggregate or no alias;
52
- - an aggregate order using an alias from another entity;
53
- - `aggregatelimit` combined with an order;
54
- - duplicate aliases;
55
- - an invalid union hint;
56
- - misuse of `rowaggregate`;
57
- - `top` together with `count`, `page` or `returntotalrecordcount`, and `page` below 1;
58
- - comparing to another attribute (`valueof`) with an operator other than eq, ne, gt, ge, lt or le, or through a link-entity without an alias;
59
- - a link-entity inside a filter whose type isn't any / not any / all / not all;
60
- - attributes, all-attributes or orders in an exists, in, any, not any, all or not all link-entity.
61
-
62
- Settings Dataverse accepts but ignores (such as `aggregatelimit` on a non-aggregate query, `aggregate` on a condition, which FetchXML can't use to filter on aggregate values, or `visible` on a link-entity) are shown as notes only.
63
- - **Boolean spellings** — `"1"`/`"0"` and other spellings Dataverse accepts are read correctly (e.g. `usertimezone="1"`, `descending="1"`, `enableprefiltering="1"`), so Parse to Tree never flips or drops them.
64
- - **Lossless Parse to Tree** — Every attribute in the FetchXML reference is kept, including `datasource`, `aggregatelimit`, `useraworderby`, query hints (`options`), `latematerialize`, `<order alias>`, `distinct` counts, `rowaggregate` and quick find flags. So are unknown, namespaced and legacy attributes (e.g. `xmlns:generator` from SQL 4 CDS, `version`/`mapping` from Advanced Find, `uiname`/`uitype` from saved views). Every FetchXML example in the Microsoft Learn FetchXML articles round-trips without loss.
65
- - **No silent changes** — If Parse to Tree must correct something Dataverse would reject (for example an invalid alias), a confirmation dialog lists every change first
66
- - **LayoutXML preview** — See the column layout configuration
67
-
68
- ### 📥 Load & Save Views
69
- - **Load system/personal views** — Browse and load existing Dataverse views
70
- - **Optimized view execution** — Uses SavedQuery/UserQuery APIs for better performance
71
- - **Save to Dataverse** — Save your queries as new personal views or update existing ones
72
- - **Solution-aware** — Add views to solutions during save
73
-
74
- ### 📤 Export & Data Operations
75
- - **Local Excel export** — Native `.xlsx` generation via ExcelJS with proper data types (numbers, dates, currencies, booleans)
76
- - **Dataverse Excel export** — Server-side export via the `ExportToExcel` action (requires a saved view)
77
- - **Native save dialog** — When running inside PPTB, both export paths use `fileSystem.saveFile` for a native OS save-file dialog; falls back to browser blob download in standalone/dev mode
78
- - **Formatted value options** — Export formatted values, raw values, or both side-by-side columns
79
- - **Record deletion** — Delete selected records with confirmation dialog
80
- - **Bulk delete jobs** — Submit async `BulkDelete` jobs to Dataverse with job-tracking URL
81
- - **Batch delete** — Parallel per-record delete for small sets (up to 100 records) with progress and ETA
82
- - **Run workflows** — Execute on-demand workflows on selected records with batch execution and progress tracking
83
-
84
- ### 📋 Bulk Attribute Selection
85
- - **Select Attributes dialog** — Multi-select attributes from a sortable, resizable grid (display name, logical name, type). Long names end with an ellipsis and show in full on hover. Search by display or logical name, filter by type, show selected only, and see how many are selected. Opens from the entity and link-entity Add menus
86
- - **Smart updates** — Adds new selections and removes deselected attributes while preserving existing order and properties
87
- - **Search & filter** — Real-time filtering across logical name, display name, and data type columns
88
-
89
- ### 🔀 Code Generation
90
- - **Code tab** — New tab in the right pane that generates ready-to-use code from the current query in seven formats
91
- - **C# QueryExpression** — Full SDK object graph (`QueryExpression`, `ColumnSet`, `FilterExpression`, `ConditionExpression`, `LinkEntity`, `OrderExpression`) via the Dataverse `FetchXmlToQueryExpression` API; supports aggregate queries with `XrmAggregateType` and `XrmDateTimeGrouping`
92
- - **C# FetchExpression** — Simple `new FetchExpression(@"...")` wrapper using a verbatim string literal
93
- - **JavaScript** — `Xrm.WebApi.retrieveMultipleRecords` call for model-driven apps and PCF components
94
- - **pac CLI** — `pac org fetch --xml "..."` command for the Power Platform CLI
95
- - **Power Automate** — Field-by-field form mirroring the Dataverse "List rows" connector UI; table name resolved from `EntityDefinitions` metadata (not a heuristic), each field individually copyable; Top Count is taken only from `top` (`count` is a page size)
96
- - **Web API** — Full OData URL with request headers (including `Prefer: odata.include-annotations="*"` for formatted values and paging cookies), the documented paging-cookie steps, a cURL snippet and a PowerShell `Invoke-RestMethod` example; entity set name resolved from metadata
97
- - **SQL** — Dataverse-generated T-SQL via `FetchXMLToSQL` (undocumented API, labelled Preview)
98
-
99
- ### 🎨 User Experience
100
- - **Dark/Light themes** — Every surface (tree, properties, editors, results grid, panels and dialogs) uses the Fluent UI v9 light or dark theme and follows the Power Platform ToolBox theme live: changing it in ToolBox settings, including **System**, and changing the OS theme while ToolBox is on System, update the tool without a reload
101
- - **Lazy metadata loading** — Loads only what's needed, when it's needed
102
- - **Intelligent caching** — In-memory cache prevents duplicate API calls per session
103
- - **Resizable panes** — Adjust split-pane layout to your preference
104
- - **Keyboard shortcuts** — `Ctrl+Enter` to execute query, copy XML to clipboard
105
- - **Display settings** — Toggle logical names vs display names in column headers; choose formatted, raw, or both value modes in the grid and exports
106
- - **Query scope settings** — Control which entities appear in the entity picker: *Publisher + Solution* (full filter), *Solution Only*, or *All Entities* (no filter). Persisted across sessions.
107
- - **Advanced Find Only toggle** — When on (default), entities, attributes and relationships are limited to the ones Advanced Find shows (`IsValidForAdvancedFind`), in every entity scope. Turn it off to build queries Advanced Find can't: system and customization entities (saved views, forms, plug-in steps, solution components), intersect entities (team membership, user roles), and hidden attributes and relationships. Attributes that can't be read (`IsValidForRead = false`) are never listed. Hidden label attributes (such as `statuscodename`) can be filtered and sorted. Toggling is instant: metadata is fetched once and filtered locally. Attributes already in a query keep their operators and value inputs either way.
108
-
109
- ### 🔗 Tool-to-Tool (T2T) Integration
110
- - **Send to Tool button** — Launch another PPTB tool directly from FetchXML Studio, pre-loading it with the current FetchXML query and active Dataverse connection (one-way handoff).
111
- - **Automatic tool discovery** — Targets are discovered via the PPTB capability registry — installed tools that declare the `fetchxml` capability. The Send to Tool control always renders as a dropdown listing the discovered tools (even a single match). No configuration needed.
112
- - **Active connection forwarding** — The active Dataverse connection is forwarded automatically so the target tool opens against the same environment.
113
- - **Inbound prefill** — FetchXML Studio also accepts incoming T2T invocations from other tools (see [Callee Contract](#-callee-contract-pptbconfigjson) below).
114
-
115
- ### 🔒 Privilege-Aware
116
- - **Export privilege check** — Only shows Dataverse export option if user has access
117
- - **Delete privilege check** — Validates entity-specific delete permissions before enabling delete actions
118
- - **Bulk delete privilege check** — Validates `prvBulkDelete` before surfacing bulk delete
119
- - **Workflow privilege check** — Validates `prvReadWorkflow` + `prvWorkflowExecution` before showing workflow picker
120
- - **View save privilege check** — Validates `prvWriteQuery` / `prvWriteCustomization` / `prvPublishCustomization` / `prvWriteUserQuery` before save operations
121
-
122
- ## 🖼️ Interface Overview
123
-
124
- ```
125
- ┌─────────────────────────────────────────────────────────────────────────┐
126
- │ [Entity Selector ▼] [Load View ▼] [Save View] [Send to Tool ▼] [⚙] │
127
- ├──────────────────────┬──────────────────────────────────────────────────┤
128
- │ │ [FetchXML] [LayoutXML] [Results] [Code] [▶ Execute]│
129
- │ Query Tree ├──────────────────────────────────────────────────┤
130
- │ │ │
131
- │ 📁 fetch │ Results Grid / Monaco Editor │
132
- │ └─📁 entity │ │
133
- │ ├─📋 attribute │ ┌────────────┬────────────┬──────────────┐ │
134
- │ ├─📋 attribute │ │ Column 1 │ Column 2 │ Column 3 │ │
135
- │ ├─🔽 order │ ├────────────┼────────────┼──────────────┤ │
136
- │ ├─🔍 filter │ │ Value │ Value │ Value │ │
137
- │ │ └─ condition │ │ Value │ Value │ Value │ │
138
- │ └─🔗 link-entity│ └────────────┴────────────┴──────────────┘ │
139
- │ │ │
140
- ├──────────────────────┤ [◀ Prev] Page 1 of 10 [Next ▶] 100 records │
141
- │ │ │
142
- │ Properties Panel │ [+ Add Columns] [✎ Edit Columns] [⬇ Export] │
143
- │ (context-aware) │ [🗑 Delete] [Bulk Delete] [⚡ Run Workflow] │
144
- │ │ │
145
- └──────────────────────┴──────────────────────────────────────────────────┘
146
- ```
147
-
148
- ## 🚀 Getting Started
149
-
150
- ### Prerequisites
151
- - [Power Platform ToolBox](https://github.com/PowerPlatformToolBox/desktop-app) desktop application
152
- - Connection to a Dataverse environment
153
-
154
- ### Installation
155
- FetchXML Studio is available as a tool in Power Platform ToolBox. Install it from the tool gallery or load as a custom tool. The package is published on npm as [`@mohsinonxrm/pptb-fetchxml-studio`](https://www.npmjs.com/package/@mohsinonxrm/pptb-fetchxml-studio).
156
-
157
- ## 🛠️ Development
158
-
159
- ### Setup
160
-
161
- ```bash
162
- # Clone the repository
163
- git clone https://github.com/mohsinonxrm/pptb-mxrm-fetchxml-studio.git
164
- cd pptb-mxrm-fetchxml-studio
165
-
166
- # Install the exact dependency versions from package-lock.json
167
- npm ci
168
-
169
- # Start development server
170
- npm run dev
171
- ```
172
-
173
- ### Building
174
-
175
- ```bash
176
- # Production build
177
- npm run build
178
-
179
- # Preview production build
180
- npm run preview
181
-
182
- # Validate package manifest against PPTB registry rules
183
- npm run validate
184
-
185
- # Finalize for publishing (validate + build)
186
- npm run finalize-package
187
- ```
188
-
189
- **Tool icon.** `package.json` points `icon` at `icons/icon.svg` under `dist/`, where Power Platform ToolBox looks for it. The build bundles `icons/icon-insider.svg` (gold stamp) for prerelease versions such as `1.4.1-beta.8`, and `icons/icon.svg` for releases.
190
-
191
- **Dependencies.** `package-lock.json` is committed and pins every dependency with an integrity hash. Use `npm ci` for a clean install. After changing dependencies, run `npm install` and commit the updated lockfile.
192
-
193
- ### Testing
194
-
195
- ```bash
196
- # Unit tests (Vitest + jsdom): parser, serializer, rules, round-trip over the Microsoft Learn examples
197
- npm test
198
-
199
- # Watch mode / coverage
200
- npm run test:watch
201
- npm run coverage
202
-
203
- # Lint
204
- npm run lint
205
- ```
206
-
207
- ### Debug Logging
208
-
209
- Enable debug logging in the browser console:
210
-
211
- ```javascript
212
- // Enable specific category
213
- enableDebug("metadataAPI"); // API calls and responses
214
- enableDebug("treeExpansion"); // Tree node operations
215
- enableDebug("relationshipPicker"); // Relationship loading
216
- enableDebug("linkEntityEditor"); // Link-entity configuration
217
- enableDebug("propertiesPanel"); // Property editor routing
218
-
219
- // Enable all
220
- enableAllDebug();
221
-
222
- // Disable
223
- disableDebug("metadataAPI");
224
- disableAllDebug();
225
- ```
226
-
227
- ## 📦 Tech Stack
228
-
229
- | Technology | Version | Purpose |
230
- |------------|---------|--------|
231
- | **React** | 18.3 | UI framework |
232
- | **TypeScript** | 5.9 | Type-safe development |
233
- | **Vite** | 7 | Build tooling and HMR |
234
- | **Fluent UI v9** | 9.74 | Microsoft design system (Tree, DataGrid, Tabs, Drawer, etc.) |
235
- | **Monaco Editor** | 0.54 | VS Code XML editor for FetchXML authoring |
236
- | **react-window** | 2 | Virtualized list rendering for large datasets |
237
- | **ExcelJS** | 4.4 | Native `.xlsx` generation with typed cells |
238
- | **@pptb/types** | 1.2.3 | Power Platform ToolBox host API types (`window.dataverseAPI`, `window.toolboxAPI`) |
239
- | **Vitest** | 5 | Unit tests (jsdom) |
240
-
241
- ## 📁 Project Structure
242
-
243
- ```
244
- src/
245
- ├── app/
246
- │ └── AppShell.tsx # Main layout with resizable split panes
247
- ├── features/fetchxml/
248
- │ ├── api/
249
- │ │ ├── pptbClient.ts # window.dataverseAPI wrapper (all Dataverse ops)
250
- │ │ ├── dataverseMetadata.ts # Lazy metadata loading with cache + dedup
251
- │ │ ├── excelExport.ts # Local ExcelJS export with native types
252
- │ │ ├── formattedValues.ts # OData @FormattedValue annotation helpers
253
- │ │ └── invocation.ts # T2T caller API (isT2TSupported, sendFetchXmlToTool)
254
- │ ├── model/
255
- │ │ ├── nodes.ts # FetchXML node TypeScript definitions
256
- │ │ ├── fetchxml.ts # FetchXML XML generation
257
- │ │ ├── fetchxmlParser.ts # FetchXML XML → node tree parser (severity-classified findings)
258
- │ │ ├── fetchxmlRules.ts # Query-level rules shared by the parser and editors
259
- │ │ ├── attributeCapabilities.ts # What each attribute type supports (sort, group by, aggregates, valueof)
260
- │ │ ├── aggregateToggle.ts # What turning off an aggregate query changes
261
- │ │ ├── fetchxmlIntellisense.ts # Intellisense / autocomplete helpers
262
- │ │ ├── layoutxml.ts # LayoutXML generation
263
- │ │ ├── operators.ts # Operator definitions by attribute type
264
- │ │ ├── displaySettings.ts # Display settings types and defaults
265
- │ │ ├── treeUtils.ts # Tree traversal utilities
266
- │ │ └── __fixtures__/ # FetchXML examples from Microsoft Learn (MIT) for round-trip tests
267
- │ ├── engine/
268
- │ │ ├── queryExpressionTypes.ts # QE JSON schema interfaces (number | string enums)
269
- │ │ ├── queryExpressionCodegen.ts # C# SDK code emitter (resolveEnum, unwrapODataValue)
270
- │ │ └── fetchxmlCodeGenerators.ts # Sync generators: FetchExpr, JS, pac, PA, WebAPI
271
- │ ├── state/
272
- │ │ ├── builderStore.tsx # React context + reducer state management
273
- │ │ └── cache.ts # Per-session in-memory metadata cache
274
- │ └── ui/
275
- │ ├── LeftPane/ # Tree view + context-aware properties panel
276
- │ │ └── PropertiesPanel/
277
- │ │ ├── editors/ # One editor per node type
278
- │ │ ├── fields.tsx # InfoField, InfoCheckbox, NumberField, LeftoverSetting
279
- │ │ ├── docLinks.ts # Microsoft Learn links used by the info popovers
280
- │ │ ├── ruleFindings.ts # Rule findings → field validation
281
- │ │ └── useEditorMetadata.ts # Attribute / entity metadata hooks
282
- │ ├── RightPane/ # Monaco editor, LayoutXML viewer, results grid,
283
- │ │ # code generation panel, Power Automate pane
284
- │ ├── Toolbar/ # Entity selector, load view picker, save button,
285
- │ │ # send-to-tool button
286
-
287
- │ ├── Dialogs/ # Save view, select attributes, delete, bulk
288
- │ │ # delete, workflow picker, solution picker
289
- │ └── Settings/ # Settings drawer (display preferences)
290
- └── shared/
291
- ├── components/ # Reusable value pickers (option set, boolean,
292
- │ # date, numeric, multi-value, relationship, etc.)
293
- ├── contexts/ # ThemeContext
294
- ├── hooks/ # usePptbContext, useLazyMetadata, useAccessMode,
295
- │ # usePublisherFilter, useSolutionFilter
296
- └── utils/ # Debug logging utilities
297
- ```
298
-
299
- ## 🔧 FetchXML Features Support
300
-
301
- | Feature | Status |
302
- |---------|--------|
303
- | Basic queries | ✅ |
304
- | Attributes (select columns) | ✅ |
305
- | All-attributes | ✅ |
306
- | Filters (and/or) | ✅ |
307
- | Nested filters | ✅ |
308
- | All condition operators (offered per attribute from the documented data types and a live operator × attribute-type matrix; value inputs match each operator: whole numbers for "x" operators, period + year for fiscal period operators) | ✅ |
309
- | Link-entity (joins) | ✅ |
310
- | Inner/outer joins | ✅ |
311
- | N:N relationships (through the intersect entity) | ✅ |
312
- | Orders (sorting) | ✅ |
313
- | Multi-column sort | ✅ |
314
- | Aggregate queries | ✅ |
315
- | Groupby | ✅ |
316
- | Distinct | ✅ |
317
- | Top/Count | ✅ |
318
- | Paging with cookies (`fetchxmlpagingcookie`), page-number paging when Dataverse returns no cookie, views via `@odata.nextLink` | ✅ |
319
- | Value-of conditions | ✅ |
320
- | Entity name on conditions | ✅ |
321
- | Filter link-entity (any/all) | ✅ |
322
- | Query hints | ✅ |
323
- | Order by alias (aggregate) | ✅ |
324
- | Quick find, union hint | ✅ |
325
- | Row aggregate (hierarchy child count) | ✅ |
326
- | Long-term retention (`datasource`) | ✅ |
327
- | Validation against Dataverse rules | ✅ |
328
-
329
- ## 🔗 Callee Contract (`pptb.config.json`)
330
-
331
- FetchXML Studio declares a PPTB [Inter-Tool Invocation](https://github.com/PowerPlatformToolBox/desktop-app) callee contract in `pptb.config.json` at the repository root, and declares the `fetchxml` capability so callers can discover it. Any other PPTB tool can launch FetchXML Studio and pre-populate it by passing a prefill payload that matches the following schema:
332
-
333
- ```json
334
- {
335
- "fetchXml": "<fetch><entity name=\"account\">...</entity></fetch>"
336
- }
337
- ```
338
-
339
- | Field | Type | Required | Description |
340
- |---|---|---|---|
341
- | `fetchXml` | `string` | One of these three | Full serialized FetchXML query string. The root entity is parsed from this. |
342
- | `viewRef` | `object` | One of these three | EntityReference to an existing view. The FetchXML is retrieved from the view record. |
343
- | `entityLogicalName` | `string` | One of these three | Root entity logical name (e.g. `"account"`). Starts a fresh query on that entity. **Ignored when `fetchXml` is present** — the root entity is then derived from the FetchXML, since a mismatched value would break this metadata-driven tool's lookups. |
344
- | `viewRef.id` | `string` (uuid) | Required if viewRef | GUID of the view record |
345
- | `viewRef.entityLogicalName` | `"savedquery"` \| `"userquery"` | Required if viewRef | Dataverse entity logical name — `savedquery` = system/public view, `userquery` = personal view |
346
-
347
- Resolution priority at runtime: **`fetchXml` → `viewRef` → `entityLogicalName`**. With no prefill (a standalone launch) the tool opens empty.
348
-
349
- ### Returning a query to the caller
350
-
351
- When FetchXML Studio is launched by another tool, a **Return FetchXML** button appears in the FetchXML toolbar. Clicking it returns the current query to the caller:
352
-
353
- ```json
354
- { "fetchXml": "<fetch>...</fetch>" }
355
- ```
356
-
357
- The caller receives this as the resolved value of `launchTool(...)`. This is distinct from the host-injected "Return to [Caller]" banner, which simply navigates back and resolves the caller's promise with `null` (no data).
358
-
359
- ### Sending a query from another PPTB tool
360
-
361
- To launch FetchXML Studio from your own PPTB tool with a pre-loaded query:
362
-
363
- ```typescript
364
- const result = await window.toolboxAPI.invocation.launchTool(
365
- "@mohsinonxrm/pptb-fetchxml-studio",
366
- { fetchXml: "<fetch><entity name=\"account\"><attribute name=\"name\"/></entity></fetch>" },
367
- { primaryConnectionId: connection?.id ?? null },
368
- );
369
-
370
- // If the user clicks "Return FetchXML", result is { fetchXml }.
371
- // If they close the window or click the host's back banner, result is null.
372
- const editedFetchXml = (result as { fetchXml?: string } | null)?.fetchXml;
373
- ```
374
-
375
- ### Sending a query to another tool (Send to Tool)
376
-
377
- FetchXML Studio discovers send targets automatically — no configuration needed. It calls the host capability registry (`toolboxAPI.invocation.findToolsByCapability("fetchxml")`) to find installed tools that declare the `fetchxml` capability in their own `pptb.config.json`, and lists them in a **Send to Tool** button (a single button for one match, a dropdown picker for several). The button is hidden when no other `fetchxml`-capable tools are installed, or on hosts that don't support capability discovery.
378
-
379
- The launch is one-way (`noReturn: true`) — FetchXML Studio hands the current query off and does not wait for data back. Any tool that wants to appear here only needs to declare:
380
-
381
- ```json
382
- { "invocation": { "version": "1.0.0", "capabilities": ["fetchxml"], "prefill": { "properties": { "fetchXml": { "type": "string" } } } } }
383
- ```
384
-
385
- ---
386
-
387
- ## 🤝 Contributing
388
-
389
- Contributions are welcome! Please feel free to submit a Pull Request.
390
-
391
- 1. Fork the repository
392
- 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
393
- 3. Commit your changes (`git commit -m 'feat: add amazing feature'`)
394
- 4. Push to the branch (`git push origin feature/amazing-feature`)
395
- 5. Open a Pull Request
396
-
397
- ### Commit Convention
398
- This project follows [Conventional Commits](https://www.conventionalcommits.org/):
399
- - `feat:` New feature
400
- - `fix:` Bug fix
401
- - `refactor:` Code refactoring
402
- - `docs:` Documentation
403
- - `chore:` Maintenance
404
-
405
- ## 📋 Roadmap
406
-
407
- - [ ] Import/export query definitions (JSON)
408
- - [ ] Quick query templates
409
- - [ ] Query history
410
- - [ ] Query performance insights
411
- - [ ] Syntax validation in Monaco
412
-
413
- ## 📄 License
414
-
415
- Licensed under the GNU Affero General Public License v3.0 (AGPL-3.0-only). See [LICENSE](LICENSE) for details.
416
-
417
- ## 🙏 Acknowledgments
418
-
419
- - [XrmToolBox FetchXML Builder](https://github.com/rappen/FetchXMLBuilder) - Original inspiration
420
- - [Power Platform ToolBox](https://github.com/PowerPlatformToolBox/desktop-app) - Host platform
421
- - [Fluent UI](https://react.fluentui.dev/) - UI component library
422
- - [Monaco Editor](https://microsoft.github.io/monaco-editor/) - Code editor
423
-
424
- ---
425
-
426
- Built with ❤️ for the Power Platform community
1
+ # FetchXML Studio for Power Platform ToolBox
2
+
3
+ A powerful, modern FetchXML query builder and data explorer for [Power Platform ToolBox](https://github.com/PowerPlatformToolBox/desktop-app). Inspired by the XrmToolBox FetchXML Builder, reimagined with React 18, Fluent UI v9, and seamless Dataverse integration.
4
+
5
+ ![Version](https://img.shields.io/badge/version-1.5.0--beta.12-blue)
6
+ [![npm](https://img.shields.io/npm/v/@mohsinonxrm/pptb-fetchxml-studio?label=npm)](https://www.npmjs.com/package/@mohsinonxrm/pptb-fetchxml-studio)
7
+ ![React](https://img.shields.io/badge/React-18-61DAFB?logo=react)
8
+ ![TypeScript](https://img.shields.io/badge/TypeScript-5.9-3178C6?logo=typescript)
9
+ ![Fluent UI](https://img.shields.io/badge/Fluent%20UI-v9-0078D4?logo=microsoft)
10
+ ![PPTB Types](https://img.shields.io/badge/%40pptb%2Ftypes-1.2.3-blue)
11
+
12
+ ## ✨ Features
13
+
14
+ ### 🔧 Visual Query Builder
15
+ - **Tree-based hierarchy** — Build FetchXML queries visually with entities, attributes, filters, orders, and link-entities
16
+ - **Add menus and right-click** — Each row's + menu offers All attributes, Select attributes…, Blank attribute, Order by, Filter and Link-entity, with the tree's icons. Right-click (or Shift+F10) a row for the same items plus **Delete**. Deleting a node with items inside asks first.
17
+ - **Readable rows** — Rows with nothing inside have no chevron. A link-entity that can return attributes but has none says "(no attributes)". A link-entity without an alias is labelled by its lookup, e.g. "contact via primarycontactid (outer)".
18
+ - **Query scope** — Publishers, Solutions, Entity and Load view sit in a section that collapses to one line (entity · view · solutions), with **New** always visible. Your publisher and solution selection is remembered for each environment. Solutions show their unique name and version, grouped Unmanaged / Managed.
19
+ - **Property panel for every node** — Fetch options, entity, attributes, all-attributes, sorts, filters, conditions and link-entities each have an editor covering the full FetchXML reference. Every field explains itself in an info popover with a link to Microsoft Learn.
20
+ - **Docs-driven helpers** — Like / begins-with / ends-with values have a wildcard helper: Contains, Begins with, Ends with or a custom pattern, plus "Match literally", which escapes `%`, `_` and `[`. Hierarchy operators (under, above…) pick a record by name, with a Record ID fallback. Undocumented settings (UTC offset, condition aggregate) are labelled as such.
21
+ - **Metadata-driven** — Attribute pickers, operators, value pickers, aggregate functions, date grouping, sortable attributes and compare-to-attribute choices follow each attribute's metadata, so the panel only offers what Dataverse accepts.
22
+ - **Shows problems where they are** — Rule errors appear on the field they're about and as an **Error** badge in the tree; settings Dataverse ignores are listed with a **Remove** button.
23
+ - **Tree badges** — Aggregate, Distinct, Retained and Hints on the query; Group by, date grouping and aggregate functions on attributes; Union and Quick find on filters.
24
+ - **Safe mode changes** — Turning off aggregate, or switching a link-entity to a type that returns no attributes, lists what has to change and asks first.
25
+ - **Full FetchXML support** — Aggregate queries, grouping, distinct, top/count, paging, and advanced hints
26
+ - **Nested filters** — Create complex AND/OR filter groups with unlimited nesting
27
+ - **Link-entity relationships** — Browse and add 1:N, N:1, and N:N relationships with inner/outer joins. Each option shows the related entity, then "via {lookup} · {schema name}", and search matches any part; N:N relationships join through their intersect entity, as documented; intersect entities (such as `systemuserroles` or `teammembership`) can be joined to each side of their N:N relationship
28
+
29
+ ### 📊 Results Grid
30
+ - **Virtualized DataGrid** — Handle large result sets with smooth scrolling (powered by react-window)
31
+ - **Copy results** — Right-click a cell to copy its value, its raw value (GUID, number, ISO date or option value), the row, the selected rows or the column. **Ctrl+C** copies the selected rows, or the focused cell. The command bar's **Copy** menu copies the selected or all loaded rows, as shown or as raw values. Rows copy as tab-separated values with a header row, so they paste into Excel as cells
32
+ - **Total record count** — With *Return total record count*, the info bar shows "Rows: 50 of 1,234 total", or "5,000+ total" when Dataverse caps the count. **Count all {entity} records** then gets the entity's unfiltered total (`RetrieveTotalRecordCount`, a snapshot that ignores the query's filters)
33
+ - **Paging and Retrieve All** — Scrolling to the end loads the next page; **Retrieve All** loads every page, with **Stop** to keep what's loaded so far. Pages use the documented paging cookie and the query's own `count` and starting `page`. Queries Dataverse returns no cookie for (sorted by a link-entity attribute, or distinct) page by page number, with a note. Views follow `@odata.nextLink`. A failed page shows how many rows loaded, with **Retry**
34
+ - **Rich cell rendering** — Power Apps-style display for lookups, option sets, dates, currency, and more
35
+ - **Column header menu** — Click a header for **Sort** (A to Z, smallest to largest or oldest to newest, by type), **Filter by…** and **Clear filter**. Shift+click a header still adds it to a multi-column sort.
36
+ - **Column filters** — **Filter by…** adds a `<condition>` to the query and runs it again, so the filter covers every record, not just the loaded ones. Operators and value pickers follow the attribute's metadata, as in the property panel. Column filters are kept in their own `<filter type="and">` inside the root filter, apart from the conditions you built. An `or` root filter is wrapped so the column filters narrow the results; it's unwrapped again when the last column filter goes. Link-entity columns use `entityname`, which filters after the join. Active filters show as removable tags above the grid, and filtered columns show a filter icon
37
+ - **Quick search** — The search box above the grid filters the loaded rows as you type: any column, values as shown, with matches highlighted. The info bar shows how many loaded rows match. Rows it hides are deselected, and scrolling doesn't load more pages while searching
38
+ - **Multi-column sorting** — Shift+click headers for multi-column sort. Each click writes a valid `<order>`: by alias in aggregate queries, with `entityname` for link-entity columns, and not at all for attribute types Dataverse can't sort (multi-select option sets)
39
+ - **Edit / Add columns** — Remove, reorder and add columns from the root entity or related entities (N:1 and 1:N); the tree and FetchXML change to match. Removing a column removes its attribute (aliased or not, root or link-entity), any sort on its alias, and an outer link-entity left empty. At least one column must remain (a query without attributes returns every attribute). Add columns isn't available in aggregate queries
40
+ - **Link-entities without an alias** — Their columns show in the grid: Dataverse names them `{name}{N}.attribute`, and the grid uses the same names
41
+ - **Row selection** — Select single or multiple records for bulk operations. Record actions use the table's primary key, which Dataverse returns automatically for regular queries; for aggregate and distinct queries (which return no record IDs) record actions are disabled with an explanation
42
+ - **Resizable & reorderable columns** — Customize your view with drag-and-drop columns
43
+ - **Formatted values** — Display OData formatted values or raw values (configurable)
44
+ - **Works with every entity** — Queries run against the entity set name from Dataverse metadata, so entities with irregular names (for example `feedback`, `msdyn_conversationdata`) run correctly. The Power Platform ToolBox host's own name guess is wrong for about 4% of entities.
45
+
46
+ ### 📝 FetchXML Editor
47
+ - **Monaco editor** — Full-featured XML editor with XML syntax highlighting and line numbers
48
+ - **Bi-directional editing** — Edit XML directly and parse back to the visual builder
49
+ - **Intellisense from the reference** — Element, attribute and value suggestions follow the FetchXML reference (every documented attribute, including `aggregatelimit`, `datasource`, `options`, `rowaggregate`, order `entityname` and the quick find overrides). Undocumented attributes are suggested only where Dataverse was verified to accept them, and say so. Value suggestions work for hyphenated attributes such as `link-type`
50
+ - **Power Automate–safe values** — Apostrophes stay literal in the generated FetchXML (attribute values use double quotes), so expressions such as `@{body('x')}` paste into Power Automate unchanged; `"`, `&`, `<` and `>` are escaped everywhere, including names, aliases and `from` / `to`
51
+ - **Editor mode actions run what you see** — In editor mode, **Execute**, **Save** and **Send to Tool** first load your FetchXML into the tree, so results, saved views and sent queries always match the editor. Execute is available as soon as the editor has content, even before an entity is chosen. If the tree would change anything, a confirmation lists the changes first, and the editor then shows the cleaned FetchXML that ran. Invalid FetchXML is reported and nothing runs.
52
+ - **What you build is what runs** — The generated FetchXML contains exactly what is in the tree; nothing is injected, so what you copy, save as a view, or send to another tool matches what executes
53
+ - **Copy to clipboard** — One-click copy of generated FetchXML, available in both read-only and editor modes
54
+ - **Alias validation** — Invalid alias characters are sanitized in real time in the property editors; Parse to Tree corrects invalid aliases, lists each correction, and updates every reference to a corrected alias (condition and order `entityname`, `valueof`, aggregate order aliases) so the query keeps working
55
+ - **Accurate validation** — Everything in the FetchXML reference is supported, plus undocumented behaviour verified to work (e.g. `link-type="natural"`). Every rule is checked against a live Dataverse environment:
56
+ - **errors** block execution — only for FetchXML Dataverse is known to reject;
57
+ - **warnings** offer **Execute anyway** — only where it can't be determined in the client (e.g. a link-entity without `from`/`to`, which is valid only for system many-to-many relationships);
58
+ - **notes** never block (e.g. `xmlns:*` from other tools, which Dataverse ignores).
59
+ - **Query-level checks** — Validate and Execute catch the mistakes Dataverse rejects, each verified on a live environment:
60
+ - aggregate queries where an attribute has no Group by / aggregate or no alias;
61
+ - an aggregate order using an alias from another entity;
62
+ - `aggregatelimit` combined with an order;
63
+ - duplicate aliases;
64
+ - an invalid union hint;
65
+ - misuse of `rowaggregate`;
66
+ - `top` together with `count`, `page` or `returntotalrecordcount`, and `page` below 1;
67
+ - comparing to another attribute (`valueof`) with an operator other than eq, ne, gt, ge, lt or le, or through a link-entity without an alias;
68
+ - a link-entity inside a filter whose type isn't any / not any / all / not all;
69
+ - attributes, all-attributes or orders in an exists, in, any, not any, all or not all link-entity.
70
+
71
+ Settings Dataverse accepts but ignores (such as `aggregatelimit` on a non-aggregate query, `aggregate` on a condition, which FetchXML can't use to filter on aggregate values, or `visible` on a link-entity) are shown as notes only.
72
+ - **Boolean spellings** — `"1"`/`"0"` and other spellings Dataverse accepts are read correctly (e.g. `usertimezone="1"`, `descending="1"`, `enableprefiltering="1"`), so Parse to Tree never flips or drops them.
73
+ - **Lossless Parse to Tree** — Every attribute in the FetchXML reference is kept, including `datasource`, `aggregatelimit`, `useraworderby`, query hints (`options`), `latematerialize`, `<order alias>`, `distinct` counts, `rowaggregate` and quick find flags. So are unknown, namespaced and legacy attributes (e.g. `xmlns:generator` from SQL 4 CDS, `version`/`mapping` from Advanced Find, `uiname`/`uitype` from saved views). Every FetchXML example in the Microsoft Learn FetchXML articles round-trips without loss.
74
+ - **No silent changes** — If Parse to Tree must correct something Dataverse would reject (for example an invalid alias), a confirmation dialog lists every change first
75
+ - **LayoutXML preview** — See the column layout configuration
76
+
77
+ ### 📥 Load & Save Views
78
+ - **Load system/personal views** — Browse and load existing Dataverse views of every query type. Personal views shared with you show "Shared by {owner}", and system views show their type or "Default view"
79
+ - **Optimized view execution** — An unmodified loaded view runs by its ID (SavedQuery/UserQuery). Quick find views run as FetchXML with the test value you enter for `{0}`; the query keeps `{0}`
80
+ - **Save to Dataverse** — Save your queries as new personal or system views, or update existing ones.
81
+ - Before overwriting a shared personal view, your write access is checked; without it you get **Save a copy…**.
82
+ - Overwriting a managed system view warns first.
83
+ - Public system views can be **set as the default view**.
84
+ - The description is pre-filled.
85
+ - **Solution-aware** — Add views to solutions during save
86
+
87
+ ### 📤 Export & Data Operations
88
+ - **Local Excel export** — Native `.xlsx` generation via ExcelJS with proper data types (numbers, dates, currencies, booleans)
89
+ - **Dataverse Excel export** — Server-side export via the `ExportToExcel` action (requires a saved view)
90
+ - **Native save dialog** — When running inside PPTB, both export paths use `fileSystem.saveFile` for a native OS save-file dialog; falls back to browser blob download in standalone/dev mode
91
+ - **Formatted value options** — Export formatted values, raw values, or both side-by-side columns
92
+ - **Record deletion** — Delete selected records with confirmation dialog
93
+ - **Bulk delete jobs** — Submit async `BulkDelete` jobs to Dataverse with job-tracking URL
94
+ - **Batch delete** — Parallel per-record delete for small sets (up to 100 records) with progress and ETA
95
+ - **Run workflows** — Execute on-demand workflows on selected records with batch execution and progress tracking
96
+
97
+ ### 📋 Bulk Attribute Selection
98
+ - **Select Attributes dialog** — Multi-select attributes from a sortable, resizable grid (display name, logical name, type). Long names end with an ellipsis and show in full on hover. Search by display or logical name, filter by type, show selected only, and see how many are selected. Opens from the entity and link-entity Add menus
99
+ - **Smart updates** — Adds new selections and removes deselected attributes while preserving existing order and properties
100
+ - **Search & filter** — Real-time filtering across logical name, display name, and data type columns
101
+
102
+ ### 🔀 Code Generation
103
+ - **Code tab** — New tab in the right pane that generates ready-to-use code from the current query in seven formats
104
+ - **C# QueryExpression** — Full SDK object graph (`QueryExpression`, `ColumnSet`, `FilterExpression`, `ConditionExpression`, `LinkEntity`, `OrderExpression`) via the Dataverse `FetchXmlToQueryExpression` API; supports aggregate queries with `XrmAggregateType` and `XrmDateTimeGrouping`
105
+ - **C# FetchExpression** — Simple `new FetchExpression(@"...")` wrapper using a verbatim string literal
106
+ - **JavaScript** — `Xrm.WebApi.retrieveMultipleRecords` call for model-driven apps and PCF components
107
+ - **pac CLI** — `pac org fetch --xml "..."` command for the Power Platform CLI
108
+ - **Power Automate** — Field-by-field form mirroring the Dataverse "List rows" connector UI; table name resolved from `EntityDefinitions` metadata (not a heuristic), each field individually copyable; Top Count is taken only from `top` (`count` is a page size)
109
+ - **Web API** — Full OData URL with request headers (including `Prefer: odata.include-annotations="*"` for formatted values and paging cookies), the documented paging-cookie steps, a cURL snippet and a PowerShell `Invoke-RestMethod` example; entity set name resolved from metadata
110
+ - **SQL** — Dataverse-generated T-SQL via `FetchXMLToSQL` (undocumented API, labelled Preview)
111
+
112
+ ### 🎨 User Experience
113
+ - **Dark/Light themes** — Every surface (tree, properties, editors, results grid, panels and dialogs) uses the Fluent UI v9 light or dark theme and follows the Power Platform ToolBox theme live: changing it in ToolBox settings, including **System**, and changing the OS theme while ToolBox is on System, update the tool without a reload
114
+ - **Lazy metadata loading** — Loads only what's needed, when it's needed
115
+ - **Intelligent caching** — In-memory cache prevents duplicate API calls per session
116
+ - **Resizable panes** — Adjust split-pane layout to your preference
117
+ - **Keyboard shortcuts** — `Ctrl+Enter` to execute query, copy XML to clipboard
118
+ - **Display settings** — Toggle logical names vs display names in column headers; choose formatted, raw, or both value modes in the grid and exports
119
+ - **Query scope settings** — Control which entities appear in the entity picker: *Publisher + Solution* (full filter), *Solution Only*, or *All Entities* (no filter). Persisted across sessions.
120
+ - **Advanced Find Only toggle** — When on (default), entities, attributes and relationships are limited to the ones Advanced Find shows (`IsValidForAdvancedFind`), in every entity scope. Turn it off to build queries Advanced Find can't: system and customization entities (saved views, forms, plug-in steps, solution components), intersect entities (team membership, user roles), and hidden attributes and relationships. Attributes that can't be read (`IsValidForRead = false`) are never listed. Hidden label attributes (such as `statuscodename`) can be filtered and sorted. Toggling is instant: metadata is fetched once and filtered locally. Attributes already in a query keep their operators and value inputs either way.
121
+
122
+ ### 🔗 Tool-to-Tool (T2T) Integration
123
+ - **Send to Tool button** — Launch another PPTB tool directly from FetchXML Studio, pre-loading it with the current FetchXML query and active Dataverse connection (one-way handoff).
124
+ - **Automatic tool discovery** — Targets are discovered via the PPTB capability registry — installed tools that declare the `fetchxml` capability. The Send to Tool control always renders as a dropdown listing the discovered tools (even a single match). No configuration needed.
125
+ - **Active connection forwarding** — The active Dataverse connection is forwarded automatically so the target tool opens against the same environment.
126
+ - **Inbound prefill** — FetchXML Studio also accepts incoming T2T invocations from other tools (see [Callee Contract](#-callee-contract-pptbconfigjson) below).
127
+
128
+ ### 🔒 Privilege-Aware
129
+ - **Export privilege check** — Only shows Dataverse export option if user has access
130
+ - **Delete privilege check** — Validates entity-specific delete permissions before enabling delete actions
131
+ - **Bulk delete privilege check** — Validates `prvBulkDelete` before surfacing bulk delete
132
+ - **Workflow privilege check** — Validates `prvReadWorkflow` + `prvWorkflowExecution` before showing workflow picker
133
+ - **View save privilege check** — Validates `prvWriteQuery` / `prvWriteCustomization` / `prvPublishCustomization` / `prvWriteUserQuery` before save operations
134
+
135
+ ## ✏️ What this tool changes
136
+
137
+ FetchXML Studio only reads from Dataverse until you start one of the operations below. Each one asks for confirmation first, and the confirmation names the target: the environment (connection name, URL and type, with Production highlighted), the entity, and how many records (or which view). For the record operations, on a Production connection, for more than 100 records, or when the total isn't known (Bulk Delete with no rows selected), you type the entity logical name before the button enables. They all run as the connected user, or as the user PPTB is impersonating, so Dataverse applies that user's security roles.
138
+
139
+ | Operation | When | What it does in Dataverse |
140
+ |---|---|---|
141
+ | **Delete records** | Results → **Delete** with 1–100 rows selected | Deletes the selected records. 1–3 records are deleted one request each; 4–100 are deleted in a batch, with progress. Needs the entity's Delete privilege. |
142
+ | **Bulk Delete** | Results → **Delete** with more than 100 rows selected, or with **no rows selected**, or **Bulk Delete** | Submits an asynchronous Bulk Delete job (`BulkDelete`). It runs on the server, and you can track it under Settings → Data Management → Bulk Record Deletion. With no rows selected, the job deletes **every record the query matches**, not just the rows loaded in the grid. Needs the Bulk Delete privilege (`prvBulkDelete`). |
143
+ | **Activate / Deactivate** | Results → **Activate** or **Deactivate** with rows selected | Sets each selected record's Status and Status Reason with an update (`PATCH {EntitySetName}(id)` with `statecode` and `statuscode`), in batches with progress. The options come from the entity's metadata: entities with two states and one status reason each (such as account and contact) go to the state's default status reason; others let you pick the status reason, grouped by state, limited to the allowed transitions when the entity enforces status reason transitions. Off for entities whose states change through their own actions (opportunity, case, quote, order, invoice, contract) and for entities with no Status column. Needs the entity's Write privilege. |
144
+ | **Run workflow** | Results → **Run Workflow** on selected rows | Starts the chosen on-demand workflow (`ExecuteWorkflow`) once per selected record. What the workflow does is up to the workflow. |
145
+ | **Save view (personal)** | **Save As** / **Save**, Personal view | Creates or updates a `userquery` record (your personal view). |
146
+ | **Save view (system)** | **Save As** / **Save**, System view | Checks the query with `ValidateSavedQuery`, then creates or updates a `savedquery` record and publishes it (`PublishXml`). Optionally adds it to the unmanaged solution you choose (`AddSolutionComponent`). Overwriting a view that isn't customizable is blocked. |
147
+
148
+ Nothing else writes to Dataverse:
149
+ - **Export (Server)** asks Dataverse to build an Excel file (`ExportToExcel`) and changes no data.
150
+ - **Export (Local)** writes an `.xlsx` file to the location you choose.
151
+ - **Display settings** (logical names, value format, entity source, Advanced Find only) are stored by Power Platform ToolBox on your machine, not in Dataverse.
152
+
153
+ ## 🖼️ Interface Overview
154
+
155
+ ```
156
+ ┌─────────────────────────────────────────────────────────────────────────┐
157
+ │ [Entity Selector ▼] [Load View ▼] [Save View] [Send to Tool ▼] [⚙] │
158
+ ├──────────────────────┬──────────────────────────────────────────────────┤
159
+ │ │ [FetchXML] [LayoutXML] [Results] [Code] [▶ Execute]│
160
+ │ Query Tree ├──────────────────────────────────────────────────┤
161
+ │ │ │
162
+ │ 📁 fetch │ Results Grid / Monaco Editor │
163
+ │ └─📁 entity │ │
164
+ │ ├─📋 attribute │ ┌────────────┬────────────┬──────────────┐ │
165
+ │ ├─📋 attribute │ │ Column 1 │ Column 2 │ Column 3 │ │
166
+ │ ├─🔽 order │ ├────────────┼────────────┼──────────────┤ │
167
+ │ ├─🔍 filter │ │ Value │ Value │ Value │ │
168
+ │ │ └─ condition │ │ Value │ Value │ Value │ │
169
+ │ └─🔗 link-entity│ └────────────┴────────────┴──────────────┘ │
170
+ │ │ │
171
+ ├──────────────────────┤ [◀ Prev] Page 1 of 10 [Next ▶] 100 records │
172
+ │ │ │
173
+ │ Properties Panel │ [+ Add Columns] [✎ Edit Columns] [⬇ Export] │
174
+ │ (context-aware) │ [🗑 Delete] [Bulk Delete] [⚡ Run Workflow] │
175
+ │ │ │
176
+ └──────────────────────┴──────────────────────────────────────────────────┘
177
+ ```
178
+
179
+ ## 🚀 Getting Started
180
+
181
+ ### Prerequisites
182
+ - [Power Platform ToolBox](https://github.com/PowerPlatformToolBox/desktop-app) desktop application, **version 1.2.7 or later**. FetchXML Studio 1.5 targets PPTB 1.2.7 (`minAPI` 1.2.7); 1.4.x works with 1.2.2 or later
183
+ - Connection to a Dataverse environment
184
+
185
+ ### Installation
186
+ FetchXML Studio is available as a tool in Power Platform ToolBox. Install it from the tool gallery or load as a custom tool. The package is published on npm as [`@mohsinonxrm/pptb-fetchxml-studio`](https://www.npmjs.com/package/@mohsinonxrm/pptb-fetchxml-studio).
187
+
188
+ ## 🛠️ Development
189
+
190
+ ### Setup
191
+
192
+ ```bash
193
+ # Clone the repository
194
+ git clone https://github.com/mohsinonxrm/pptb-mxrm-fetchxml-studio.git
195
+ cd pptb-mxrm-fetchxml-studio
196
+
197
+ # Install the exact dependency versions from package-lock.json
198
+ npm ci
199
+
200
+ # Start development server
201
+ npm run dev
202
+
203
+ # Run the real UI in a browser against a live Dataverse org (needs `az login`; see dev/README.md)
204
+ npm run dev:harness
205
+ ```
206
+
207
+ ### Building
208
+
209
+ ```bash
210
+ # Production build
211
+ npm run build
212
+
213
+ # Preview production build
214
+ npm run preview
215
+
216
+ # Validate package manifest against PPTB registry rules
217
+ npm run validate
218
+ ```
219
+
220
+ **Releases.** CI (`.github/workflows/ci.yml`) runs typecheck, lint, tests, PPTB validation, the build and a package-contents check on every pull request. `main` holds the current stable release and `dev` the next one. A `vX.Y.Z` tag on `main` publishes to npm `latest`, and a `vX.Y.Z-beta.N` tag on `dev` publishes to npm `beta` as a GitHub pre-release, both with provenance; see [RELEASING.md](RELEASING.md).
221
+
222
+ **Tool icon.** `package.json` points `icon` at `icons/icon.svg` under `dist/`, where Power Platform ToolBox looks for it. The build bundles `icons/icon-insider.svg` (gold stamp) for prerelease versions such as `1.4.2-beta.7`, and `icons/icon.svg` for releases.
223
+
224
+ **Dependencies.** `package-lock.json` is committed and pins every dependency with an integrity hash. Use `npm ci` for a clean install. After changing dependencies, run `npm install` and commit the updated lockfile.
225
+
226
+ ### Testing
227
+
228
+ ```bash
229
+ # Unit tests (Vitest + jsdom): parser, serializer, rules, round-trip over the Microsoft Learn examples
230
+ npm test
231
+
232
+ # Watch mode
233
+ npm run test:watch
234
+
235
+ # Type check and lint
236
+ npm run typecheck
237
+ npm run lint
238
+
239
+ # Formatting (Prettier)
240
+ npm run format:check
241
+ ```
242
+
243
+ ### Debug Logging
244
+
245
+ Enable debug logging in the browser console:
246
+
247
+ ```javascript
248
+ // Enable specific category
249
+ enableDebug("metadataAPI"); // API calls and responses
250
+ enableDebug("treeExpansion"); // Tree node operations
251
+ enableDebug("relationshipPicker"); // Relationship loading
252
+ enableDebug("linkEntityEditor"); // Link-entity configuration
253
+ enableDebug("propertiesPanel"); // Property editor routing
254
+
255
+ // Enable all
256
+ enableAllDebug();
257
+
258
+ // Disable
259
+ disableDebug("metadataAPI");
260
+ disableAllDebug();
261
+ ```
262
+
263
+ ## 📦 Tech Stack
264
+
265
+ | Technology | Version | Purpose |
266
+ |------------|---------|--------|
267
+ | **React** | 18.3 | UI framework |
268
+ | **TypeScript** | 5.9 | Type-safe development |
269
+ | **Vite** | 7 | Build tooling and HMR |
270
+ | **Fluent UI v9** | 9.74 | Microsoft design system (Tree, DataGrid, Tabs, Drawer, etc.) |
271
+ | **Monaco Editor** | 0.54 | VS Code XML editor for FetchXML authoring |
272
+ | **react-window** | 2 | Virtualized list rendering for large datasets |
273
+ | **ExcelJS** | 4.4 | Native `.xlsx` generation with typed cells |
274
+ | **@pptb/types** | 1.2.3 | Power Platform ToolBox host API types (`window.dataverseAPI`, `window.toolboxAPI`) |
275
+ | **Vitest** | 5 | Unit tests (jsdom) |
276
+
277
+ ## 📁 Project Structure
278
+
279
+ ```
280
+ src/
281
+ ├── app/
282
+ │ └── AppShell.tsx # Main layout with resizable split panes
283
+ ├── features/fetchxml/
284
+ │ ├── api/
285
+ │ │ ├── pptbClient.ts # window.dataverseAPI wrapper (all Dataverse ops)
286
+ │ │ ├── dataverseMetadata.ts # Lazy metadata loading with cache + dedup
287
+ │ │ ├── excelExport.ts # Local ExcelJS export with native types
288
+ │ │ ├── formattedValues.ts # OData @FormattedValue annotation helpers
289
+ │ │ └── invocation.ts # T2T caller API (isT2TSupported, sendFetchXmlToTool)
290
+ │ ├── model/
291
+ │ │ ├── nodes.ts # FetchXML node TypeScript definitions
292
+ │ │ ├── fetchxml.ts # FetchXML XML generation
293
+ │ │ ├── fetchxmlParser.ts # FetchXML XML → node tree parser (severity-classified findings)
294
+ │ │ ├── fetchxmlRules.ts # Query-level rules shared by the parser and editors
295
+ │ │ ├── attributeCapabilities.ts # What each attribute type supports (sort, group by, aggregates, valueof)
296
+ │ │ ├── aggregateToggle.ts # What turning off an aggregate query changes
297
+ │ │ ├── fetchxmlIntellisense.ts # Intellisense / autocomplete helpers
298
+ │ │ ├── layoutxml.ts # LayoutXML generation
299
+ │ │ ├── operators.ts # Operator definitions by attribute type
300
+ │ │ ├── displaySettings.ts # Display settings types and defaults
301
+ │ │ ├── treeUtils.ts # Tree traversal utilities
302
+ │ │ └── __fixtures__/ # FetchXML examples from Microsoft Learn (MIT) for round-trip tests
303
+ │ ├── engine/
304
+ │ │ ├── queryExpressionTypes.ts # QE JSON schema interfaces (number | string enums)
305
+ │ │ ├── queryExpressionCodegen.ts # C# SDK code emitter (resolveEnum, unwrapODataValue)
306
+ │ │ └── fetchxmlCodeGenerators.ts # Sync generators: FetchExpr, JS, pac, PA, WebAPI
307
+ │ ├── state/
308
+ │ │ ├── builderStore.tsx # React context + reducer state management
309
+ │ │ └── cache.ts # Per-session in-memory metadata cache
310
+ │ └── ui/
311
+ │ ├── LeftPane/ # Tree view + context-aware properties panel
312
+ │ │ └── PropertiesPanel/
313
+ │ │ ├── editors/ # One editor per node type
314
+ │ │ ├── fields.tsx # InfoField, InfoCheckbox, NumberField, LeftoverSetting
315
+ │ │ ├── docLinks.ts # Microsoft Learn links used by the info popovers
316
+ │ │ ├── ruleFindings.ts # Rule findings → field validation
317
+ │ │ └── useEditorMetadata.ts # Attribute / entity metadata hooks
318
+ │ ├── RightPane/ # Monaco editor, LayoutXML viewer, results grid,
319
+ │ │ # code generation panel, Power Automate pane
320
+ │ ├── Toolbar/ # Entity selector, load view picker, save button,
321
+ │ │ # send-to-tool button
322
+
323
+ │ ├── Dialogs/ # Save view, select attributes, delete, bulk
324
+ │ │ # delete, workflow picker, solution picker
325
+ │ └── Settings/ # Settings drawer (display preferences)
326
+ └── shared/
327
+ ├── components/ # Reusable value pickers (option set, boolean,
328
+ │ # date, numeric, multi-value, relationship, etc.)
329
+ ├── contexts/ # ThemeContext
330
+ ├── hooks/ # usePptbContext, useLazyMetadata, useAccessMode,
331
+ │ # usePublisherFilter, useSolutionFilter
332
+ └── utils/ # Debug logging utilities
333
+ ```
334
+
335
+ ## 🔧 FetchXML Features Support
336
+
337
+ | Feature | Status |
338
+ |---------|--------|
339
+ | Basic queries | ✅ |
340
+ | Attributes (select columns) | ✅ |
341
+ | All-attributes | ✅ |
342
+ | Filters (and/or) | ✅ |
343
+ | Nested filters | ✅ |
344
+ | All condition operators (offered per attribute from the documented data types and a live operator × attribute-type matrix; value inputs match each operator: whole numbers for "x" operators, period + year for fiscal period operators) | ✅ |
345
+ | Link-entity (joins) | ✅ |
346
+ | Inner/outer joins | ✅ |
347
+ | N:N relationships (through the intersect entity) | ✅ |
348
+ | Orders (sorting) | ✅ |
349
+ | Multi-column sort | ✅ |
350
+ | Aggregate queries | ✅ |
351
+ | Groupby | ✅ |
352
+ | Distinct | ✅ |
353
+ | Top/Count | ✅ |
354
+ | Paging with cookies (`fetchxmlpagingcookie`), page-number paging when Dataverse returns no cookie, views via `@odata.nextLink` | ✅ |
355
+ | Value-of conditions | ✅ |
356
+ | Entity name on conditions | ✅ |
357
+ | Filter link-entity (any/all) | ✅ |
358
+ | Query hints | ✅ |
359
+ | Order by alias (aggregate) | ✅ |
360
+ | Quick find, union hint | ✅ |
361
+ | Row aggregate (hierarchy child count) | ✅ |
362
+ | Long-term retention (`datasource`) | ✅ |
363
+ | Validation against Dataverse rules | ✅ |
364
+
365
+ ## 🔗 Callee Contract (`pptb.config.json`)
366
+
367
+ FetchXML Studio declares a PPTB [Inter-Tool Invocation](https://github.com/PowerPlatformToolBox/desktop-app) callee contract in `pptb.config.json` at the repository root, and declares the `fetchxml` capability so callers can discover it. Any other PPTB tool can launch FetchXML Studio and pre-populate it by passing a prefill payload that matches the following schema:
368
+
369
+ ```json
370
+ {
371
+ "fetchXml": "<fetch><entity name=\"account\">...</entity></fetch>"
372
+ }
373
+ ```
374
+
375
+ | Field | Type | Required | Description |
376
+ |---|---|---|---|
377
+ | `fetchXml` | `string` | One of these three | Full serialized FetchXML query string. The root entity is parsed from this. |
378
+ | `viewRef` | `object` | One of these three | EntityReference to an existing view. The FetchXML is retrieved from the view record. |
379
+ | `entityLogicalName` | `string` | One of these three | Root entity logical name (e.g. `"account"`). Starts a fresh query on that entity. **Ignored when `fetchXml` is present** — the root entity is then derived from the FetchXML, since a mismatched value would break this metadata-driven tool's lookups. |
380
+ | `viewRef.id` | `string` (uuid) | Required if viewRef | GUID of the view record |
381
+ | `viewRef.entityLogicalName` | `"savedquery"` \| `"userquery"` | Required if viewRef | Dataverse entity logical name — `savedquery` = system/public view, `userquery` = personal view |
382
+
383
+ Resolution priority at runtime: **`fetchXml` → `viewRef` → `entityLogicalName`**. With no prefill (a standalone launch) the tool opens empty.
384
+
385
+ ### Returning a query to the caller
386
+
387
+ When FetchXML Studio is launched by another tool, a **Return FetchXML** button appears in the FetchXML toolbar. Clicking it returns the current query to the caller:
388
+
389
+ ```json
390
+ { "fetchXml": "<fetch>...</fetch>" }
391
+ ```
392
+
393
+ The caller receives this as the resolved value of `launchTool(...)`. This is distinct from the host-injected "Return to [Caller]" banner, which simply navigates back and resolves the caller's promise with `null` (no data).
394
+
395
+ ### Sending a query from another PPTB tool
396
+
397
+ To launch FetchXML Studio from your own PPTB tool with a pre-loaded query:
398
+
399
+ ```typescript
400
+ const result = await window.toolboxAPI.invocation.launchTool(
401
+ "@mohsinonxrm/pptb-fetchxml-studio",
402
+ { fetchXml: "<fetch><entity name=\"account\"><attribute name=\"name\"/></entity></fetch>" },
403
+ { primaryConnectionId: connection?.id ?? null },
404
+ );
405
+
406
+ // If the user clicks "Return FetchXML", result is { fetchXml }.
407
+ // If they close the window or click the host's back banner, result is null.
408
+ const editedFetchXml = (result as { fetchXml?: string } | null)?.fetchXml;
409
+ ```
410
+
411
+ ### Sending a query to another tool (Send to Tool)
412
+
413
+ FetchXML Studio discovers send targets automatically — no configuration needed. It calls the host capability registry (`toolboxAPI.invocation.findToolsByCapability("fetchxml")`) to find installed tools that declare the `fetchxml` capability in their own `pptb.config.json`, and lists them in a **Send to Tool** button (a single button for one match, a dropdown picker for several). The button is hidden when no other `fetchxml`-capable tools are installed, or on hosts that don't support capability discovery.
414
+
415
+ The launch is one-way (`noReturn: true`) — FetchXML Studio hands the current query off and does not wait for data back. Any tool that wants to appear here only needs to declare:
416
+
417
+ ```json
418
+ { "invocation": { "version": "1.0.0", "capabilities": ["fetchxml"], "prefill": { "properties": { "fetchXml": { "type": "string" } } } } }
419
+ ```
420
+
421
+ ---
422
+
423
+ ## 🤝 Contributing
424
+
425
+ Contributions are welcome! Please feel free to submit a Pull Request.
426
+
427
+ 1. Fork the repository
428
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
429
+ 3. Commit your changes (`git commit -m 'feat: add amazing feature'`)
430
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
431
+ 5. Open a Pull Request
432
+
433
+ ### Commit Convention
434
+ This project follows [Conventional Commits](https://www.conventionalcommits.org/):
435
+ - `feat:` New feature
436
+ - `fix:` Bug fix
437
+ - `refactor:` Code refactoring
438
+ - `docs:` Documentation
439
+ - `chore:` Maintenance
440
+
441
+ ## 📋 Roadmap
442
+
443
+ - [ ] Import/export query definitions (JSON)
444
+ - [ ] Quick query templates
445
+ - [ ] Query history
446
+ - [ ] Query performance insights
447
+ - [ ] Syntax validation in Monaco
448
+
449
+ ## 📄 License
450
+
451
+ Licensed under the GNU Affero General Public License v3.0 (AGPL-3.0-only). See [LICENSE](LICENSE) for details.
452
+
453
+ ## 🙏 Acknowledgments
454
+
455
+ - [XrmToolBox FetchXML Builder](https://github.com/rappen/FetchXMLBuilder) - Original inspiration
456
+ - [Power Platform ToolBox](https://github.com/PowerPlatformToolBox/desktop-app) - Host platform
457
+ - [Fluent UI](https://react.fluentui.dev/) - UI component library
458
+ - [Monaco Editor](https://microsoft.github.io/monaco-editor/) - Code editor
459
+
460
+ ---
461
+
462
+ Built with ❤️ for the Power Platform community