fluentui-extended 2026.8.35 → 2026.8.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -473,21 +473,31 @@ const handleChange = (result: QueryBuilderApplyResult) => {
473
473
 
474
474
  ### Features
475
475
 
476
- #### Import/Export FetchXML
476
+ #### Import/Edit/Export FetchXML
477
477
 
478
- Users can download the current query as FetchXML or import existing FetchXML:
478
+ Download the current query as FetchXML, import FetchXML from elsewhere, or open the current
479
+ query as editable FetchXML:
479
480
 
480
481
  ```tsx
481
482
  <QueryBuilder
482
483
  entityName="account"
483
484
  showDownloadFetchXmlButton={true} // Default: true
484
485
  showUploadFetchXmlButton={true} // Default: true
486
+ showEditFetchXmlButton={true} // Default: true
485
487
  />
486
488
  ```
487
489
 
490
+ **Import FetchXML** opens an empty dialog for pasting in a query from elsewhere.
491
+
492
+ **Edit FetchXML** opens the same dialog prefilled with the current query's FetchXML, so you can
493
+ tweak it in place or select-all and paste a different query over it. Applying rebuilds the
494
+ builder from whatever is in the box. If the XML doesn't parse, your text is kept and the error
495
+ is shown inline.
496
+
488
497
  #### Live Preview
489
498
 
490
- Show real-time preview of the generated queries:
499
+ Show real-time preview of the generated queries. The previews can also be toggled from the
500
+ toolbar, so these props set the *initial* visibility rather than hiding the previews outright:
491
501
 
492
502
  ```tsx
493
503
  <QueryBuilder
@@ -495,9 +505,40 @@ Show real-time preview of the generated queries:
495
505
  fields={fields}
496
506
  showODataPreview={true}
497
507
  showFetchXmlPreview={true}
508
+ showPreviewToggleButtons={true} // Default: true
509
+ />
510
+ ```
511
+
512
+ #### Queries That OData Cannot Express
513
+
514
+ FetchXML has operators the OData `$filter` syntax has no equivalent for — relative dates
515
+ (`last-x-days`, `this-month`), fiscal periods, user context (`eq-userid`) and hierarchy
516
+ operators (`under`, `above`). These are evaluated by the FetchXML engine itself.
517
+
518
+ When a query uses one, it is **omitted from the OData filter** and reported on the result:
519
+
520
+ ```tsx
521
+ <QueryBuilder
522
+ entityName="account"
523
+ fields={fields}
524
+ onSerializedChange={(result) => {
525
+ if (result.odataUnsupported.length > 0) {
526
+ // The OData filter is NOT equivalent to the FetchXML - use result.fetchXml instead
527
+ console.warn('Not expressible in OData:', result.odataUnsupported);
528
+ }
529
+ }}
498
530
  />
499
531
  ```
500
532
 
533
+ Each entry gives the field and operator that could not be translated:
534
+
535
+ ```ts
536
+ { fieldId: 'createdon', fieldLabel: 'Created On', operator: 'last-x-days', operatorLabel: 'Last X Days' }
537
+ ```
538
+
539
+ The OData preview shows the same information as a warning. Use `isOperatorConvertibleToOData`
540
+ to check a single operator yourself.
541
+
501
542
  #### Validation with Dynamics 365 API
502
543
 
503
544
  The Validate button checks query structure and optionally tests against the Dynamics 365 API:
@@ -597,11 +638,13 @@ const fields: QueryBuilderField[] = [
597
638
  | `initialState` | `QueryBuilderState` | - | Initial query state object |
598
639
  | `onSerializedChange` | `(result: QueryBuilderApplyResult) => void` | - | Called when query changes |
599
640
  | `onLookupSearch` | `(fieldId: string, searchText: string) => Promise<LookupOption[]>` | - | Lookup field search handler |
600
- | `showODataPreview` | `boolean` | `false` | Show OData filter preview |
601
- | `showFetchXmlPreview` | `boolean` | `false` | Show FetchXML preview |
641
+ | `showODataPreview` | `boolean` | `false` | Initial visibility of the OData filter preview |
642
+ | `showFetchXmlPreview` | `boolean` | `false` | Initial visibility of the FetchXML preview |
643
+ | `showPreviewToggleButtons` | `boolean` | `true` | Show toolbar buttons that toggle the previews |
602
644
  | `showResetToDefaultButton` | `boolean` | `true` | Show Reset button |
603
645
  | `showDownloadFetchXmlButton` | `boolean` | `true` | Show Download FetchXML button |
604
646
  | `showUploadFetchXmlButton` | `boolean` | `true` | Show Import FetchXML button |
647
+ | `showEditFetchXmlButton` | `boolean` | `true` | Show Edit FetchXML button |
605
648
  | `showValidateButton` | `boolean` | `true` | Show Validate button |
606
649
  | `showDeleteAllFiltersButton` | `boolean` | `true` | Show Delete All button |
607
650
  | `onTrace` | `(message: string, data?: any) => void` | - | Debug/trace callback for component behavior |
@@ -613,10 +656,14 @@ interface QueryBuilderField {
613
656
  id: string; // Logical attribute name
614
657
  label: string; // Display label
615
658
  dataType: 'string' | 'number' | 'datetime' | 'boolean' | 'optionset' | 'lookup';
616
- options?: Array<{ label: string; value: number }>; // For optionset fields
659
+ options?: Array<{ label: string; value: string | number }>; // Optionset and boolean fields
617
660
  }
618
661
  ```
619
662
 
663
+ Options are loaded automatically from entity metadata when `fields` is omitted. Boolean fields
664
+ pick up their Dynamics labels (for example "Allowed" / "Not Allowed" rather than Yes / No), with
665
+ values `'1'` and `'0'` to match the FetchXML representation.
666
+
620
667
  ### QueryBuilderApplyResult
621
668
 
622
669
  ```ts
@@ -625,9 +672,21 @@ interface QueryBuilderApplyResult {
625
672
  fetchXmlFilter: string; // Just the <filter> element
626
673
  fetchXml: string; // Complete FetchXML document
627
674
  odataFilter: string; // OData $filter value
675
+ odataQuery?: string; // Full OData query URL (requires entitySetName)
676
+ odataUnsupported: QueryBuilderODataUnsupported[]; // Conditions OData cannot express
677
+ }
678
+
679
+ interface QueryBuilderODataUnsupported {
680
+ fieldId: string; // e.g. "createdon"
681
+ fieldLabel: string; // e.g. "Created On"
682
+ operator: string; // e.g. "last-x-days"
683
+ operatorLabel: string; // e.g. "Last X Days"
628
684
  }
629
685
  ```
630
686
 
687
+ When `odataUnsupported` is non-empty, `odataFilter` is **not** equivalent to `fetchXml` — the
688
+ untranslatable conditions have been left out. Use `fetchXml` to run the query.
689
+
631
690
  ### Programmatic API
632
691
 
633
692
  #### Serialize State
@@ -670,9 +729,21 @@ if (!result.isValid) {
670
729
 
671
730
  | Data Type | Operators |
672
731
  |-----------|-----------|
673
- | `string` | Contains, Does Not Contain, Starts With, Ends With, Equals, Not Equals, Is Empty, Has Value |
674
- | `number`, `datetime` | Greater Than, Greater Than Or Equal, Less Than, Less Than Or Equal, Between, Equals, Not Equals, Is Empty, Has Value |
675
- | `optionset`, `lookup`, `boolean` | Equals, Not Equals, Is Empty, Has Value |
732
+ | `string` | Contains, Does Not Contain, Begins With, Does Not Begin With, Ends With, Does Not End With, Like, Not Like, Equals, Not Equals, Is Empty, Has Value |
733
+ | `number` | Greater Than, Greater Than Or Equal, Less Than, Less Than Or Equal, Between, Not Between, Equals, Not Equals, Is One Of, Is Not One Of, Is Empty, Has Value |
734
+ | `datetime` | All number comparisons, plus On / On Or Before / On Or After, relative dates (Today, This Month, Last X Days, Older Than X Months, ...) and fiscal period operators |
735
+ | `optionset` | Equals, Not Equals, Is One Of, Is Not One Of, Is Empty, Has Value |
736
+ | `lookup` | Equals, Not Equals, Is One Of, Is Not One Of, Is Empty, Has Value, plus user-context (Equals Current User, ...) and hierarchy (Under, Above, ...) operators |
737
+ | `boolean` | Equals, Not Equals, Is Empty, Has Value |
738
+
739
+ Operators map to the [FetchXML condition operators][fetchxml-operators]. Note that FetchXML has
740
+ no `contains` operator — "Contains" and "Does Not Contain" are serialized as `like` / `not-like`
741
+ with `%` wildcards around the value.
742
+
743
+ Relative date, fiscal period, user-context and hierarchy operators are FetchXML-only and cannot
744
+ be expressed in OData — see [Queries That OData Cannot Express](#queries-that-odata-cannot-express).
745
+
746
+ [fetchxml-operators]: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/fetchxml/reference/operators
676
747
 
677
748
  ---
678
749
 
@@ -690,6 +761,42 @@ This library extends [Microsoft's Fluent UI React v9](https://react.fluentui.dev
690
761
 
691
762
  > Version format: `YYYY.M.DD` (e.g., `2026.8.30` = August 30, 2026)
692
763
 
764
+ ### 2026.8.36
765
+
766
+ QueryBuilder field-type and FetchXML correctness pass.
767
+
768
+ - 🐛 **Every field resolved as `string`.** `dataTypeFromAttribute` compared `AttributeTypeName.Value`
769
+ (which is suffixed — `MoneyType`, `PicklistType`, `BooleanType`) against unsuffixed names, so only
770
+ lookups were typed correctly. Money fields offered "Contains", optionsets rendered a text box, and
771
+ booleans never reached their branch.
772
+ - 🐛 **Optionset and boolean options were never loaded** for main-entity fields. The component's field
773
+ loader fetched attributes and lookup targets but no option metadata.
774
+ - 🐛 **Global option sets returned no options** — only `OptionSet` was expanded, never `GlobalOptionSet`.
775
+ - 🐛 **Boolean fields** now use their Dynamics labels ("Allowed" / "Not Allowed") instead of hardcoded
776
+ Yes/No, and match on truthiness so a saved `value="1"` no longer displays as "No".
777
+ - 🐛 **"Does Not Contain" produced invalid FetchXML** (`operator="not-contain"`, which does not exist).
778
+ Now serialized as `not-like` with `%` wildcards.
779
+ - 🐛 **Date picker shifted the day** in UTC+ timezones — `toISOString()` converted local midnight to
780
+ the previous UTC day.
781
+ - 🐛 **`IsValidForAdvancedFind` was never requested**, so the filter meant to hide non-filterable
782
+ attributes did nothing.
783
+ - 🐛 **"Has Value" left the value box enabled**; no-value operators now disable it correctly.
784
+ - 🐛 **"Last X Days" rendered a date picker** instead of a number input.
785
+ - 🐛 **`not-between` and fiscal period-and-year operators had no second value input.**
786
+ - 🐛 **`link-entity` guessed `from="<entity>id"`**, which is wrong for activity entities
787
+ (`email`, `task`, `appointment` all use `activityid`). Now uses `PrimaryIdAttribute`.
788
+ - 🐛 **Invalid OData output.** Untranslatable operators were emitted as a `/* comment */` in the filter
789
+ string; nested related-entity conditions were silently coerced to `eq`. Both are now omitted.
790
+ - ✨ **Edit FetchXML** toolbar button — opens the current query as editable FetchXML to tweak or paste
791
+ over (`showEditFetchXmlButton`).
792
+ - ✨ **Show/Hide OData and FetchXML** toolbar toggles (`showPreviewToggleButtons`).
793
+ - ✨ `QueryBuilderApplyResult.odataUnsupported` reports conditions OData cannot express, surfaced as a
794
+ warning in the OData preview. New `isOperatorConvertibleToOData` helper.
795
+ - ✨ Option metadata is fetched once per attribute type rather than once per field.
796
+ - 💄 Softer, more rounded containers matching other Dynamics surfaces.
797
+ - ⚠️ **Breaking:** `odataUnsupported` is a required field on `QueryBuilderApplyResult`. Consumers only
798
+ reading the result are unaffected; anyone constructing the type will need to add it.
799
+
693
800
  ### 2026.8.30
694
801
 
695
802
  - ✨ Added multi-entity filter pattern with drill-down header ("← All" back button) in test harness
package/dist/index.d.mts CHANGED
@@ -96,6 +96,8 @@ interface QueryBuilderLookupTarget {
96
96
  displayName?: string;
97
97
  /** Primary name attribute for searching (e.g., "fullname") */
98
98
  primaryNameAttribute?: string;
99
+ /** Primary key attribute (e.g., "contactid", or "activityid" for activity entities) */
100
+ primaryIdAttribute?: string;
99
101
  }
100
102
  interface QueryBuilderField {
101
103
  id: string;
@@ -132,6 +134,11 @@ interface QueryBuilderCondition {
132
134
  relatedEntityName?: string;
133
135
  /** The target entity logical name (e.g., "systemuser") - used as "name" in link-entity */
134
136
  relatedEntityTarget?: string;
137
+ /**
138
+ * Primary key attribute of the target entity - used as "from" in link-entity.
139
+ * Falls back to "<entity>id" when absent, which is wrong for activity entities.
140
+ */
141
+ relatedEntityPrimaryId?: string;
135
142
  /** Alias for the link-entity (e.g., "S" in alias="S") */
136
143
  relatedEntityAlias?: string;
137
144
  /** Nested conditions for related entity (link-entity filter) */
@@ -151,6 +158,17 @@ interface QueryBuilderGroup {
151
158
  interface QueryBuilderState {
152
159
  groups: QueryBuilderGroup[];
153
160
  }
161
+ /** A condition that has no OData equivalent and was omitted from the OData filter */
162
+ interface QueryBuilderODataUnsupported {
163
+ /** Logical name of the field the condition applies to */
164
+ fieldId: string;
165
+ /** Display label of the field */
166
+ fieldLabel: string;
167
+ /** The operator that cannot be translated (e.g., "last-x-days") */
168
+ operator: string;
169
+ /** Display label of the operator (e.g., "Last X Days") */
170
+ operatorLabel: string;
171
+ }
154
172
  interface QueryBuilderApplyResult {
155
173
  state: QueryBuilderState;
156
174
  fetchXmlFilter: string;
@@ -158,6 +176,12 @@ interface QueryBuilderApplyResult {
158
176
  odataFilter: string;
159
177
  /** Full OData query URL (e.g., "accounts?$filter=...") - requires entitySetName */
160
178
  odataQuery?: string;
179
+ /**
180
+ * Conditions dropped from odataFilter because OData has no equivalent
181
+ * (relative dates, fiscal periods, user context, hierarchy operators).
182
+ * When non-empty the OData output is NOT equivalent to the FetchXML - use FetchXML instead.
183
+ */
184
+ odataUnsupported: QueryBuilderODataUnsupported[];
161
185
  }
162
186
  interface QueryBuilderRelatedEntity {
163
187
  /** Unique identifier - typically the lookup field name (e.g., "primarycontactid") */
@@ -170,6 +194,8 @@ interface QueryBuilderRelatedEntity {
170
194
  targetEntity?: string;
171
195
  /** The target entity set name for OData (e.g., "contacts") */
172
196
  targetEntitySetName?: string;
197
+ /** The target entity's primary key attribute - used as "from" in the generated link-entity */
198
+ targetPrimaryIdAttribute?: string;
173
199
  }
174
200
  interface QueryBuilderLookupOption {
175
201
  /** Unique identifier (typically a GUID) */
@@ -199,6 +225,13 @@ interface QueryBuilderProps {
199
225
  showResetToDefaultButton?: boolean;
200
226
  showDownloadFetchXmlButton?: boolean;
201
227
  showUploadFetchXmlButton?: boolean;
228
+ /**
229
+ * Show the "Edit FetchXML" toolbar button, which opens the current query as editable
230
+ * FetchXML that can be tweaked or overwritten by pasting. Defaults to true.
231
+ */
232
+ showEditFetchXmlButton?: boolean;
233
+ /** Show the toolbar buttons that toggle the OData and FetchXML previews. Defaults to true. */
234
+ showPreviewToggleButtons?: boolean;
202
235
  showDeleteAllFiltersButton?: boolean;
203
236
  showValidateButton?: boolean;
204
237
  showDataSourceToggle?: boolean;
package/dist/index.d.ts CHANGED
@@ -96,6 +96,8 @@ interface QueryBuilderLookupTarget {
96
96
  displayName?: string;
97
97
  /** Primary name attribute for searching (e.g., "fullname") */
98
98
  primaryNameAttribute?: string;
99
+ /** Primary key attribute (e.g., "contactid", or "activityid" for activity entities) */
100
+ primaryIdAttribute?: string;
99
101
  }
100
102
  interface QueryBuilderField {
101
103
  id: string;
@@ -132,6 +134,11 @@ interface QueryBuilderCondition {
132
134
  relatedEntityName?: string;
133
135
  /** The target entity logical name (e.g., "systemuser") - used as "name" in link-entity */
134
136
  relatedEntityTarget?: string;
137
+ /**
138
+ * Primary key attribute of the target entity - used as "from" in link-entity.
139
+ * Falls back to "<entity>id" when absent, which is wrong for activity entities.
140
+ */
141
+ relatedEntityPrimaryId?: string;
135
142
  /** Alias for the link-entity (e.g., "S" in alias="S") */
136
143
  relatedEntityAlias?: string;
137
144
  /** Nested conditions for related entity (link-entity filter) */
@@ -151,6 +158,17 @@ interface QueryBuilderGroup {
151
158
  interface QueryBuilderState {
152
159
  groups: QueryBuilderGroup[];
153
160
  }
161
+ /** A condition that has no OData equivalent and was omitted from the OData filter */
162
+ interface QueryBuilderODataUnsupported {
163
+ /** Logical name of the field the condition applies to */
164
+ fieldId: string;
165
+ /** Display label of the field */
166
+ fieldLabel: string;
167
+ /** The operator that cannot be translated (e.g., "last-x-days") */
168
+ operator: string;
169
+ /** Display label of the operator (e.g., "Last X Days") */
170
+ operatorLabel: string;
171
+ }
154
172
  interface QueryBuilderApplyResult {
155
173
  state: QueryBuilderState;
156
174
  fetchXmlFilter: string;
@@ -158,6 +176,12 @@ interface QueryBuilderApplyResult {
158
176
  odataFilter: string;
159
177
  /** Full OData query URL (e.g., "accounts?$filter=...") - requires entitySetName */
160
178
  odataQuery?: string;
179
+ /**
180
+ * Conditions dropped from odataFilter because OData has no equivalent
181
+ * (relative dates, fiscal periods, user context, hierarchy operators).
182
+ * When non-empty the OData output is NOT equivalent to the FetchXML - use FetchXML instead.
183
+ */
184
+ odataUnsupported: QueryBuilderODataUnsupported[];
161
185
  }
162
186
  interface QueryBuilderRelatedEntity {
163
187
  /** Unique identifier - typically the lookup field name (e.g., "primarycontactid") */
@@ -170,6 +194,8 @@ interface QueryBuilderRelatedEntity {
170
194
  targetEntity?: string;
171
195
  /** The target entity set name for OData (e.g., "contacts") */
172
196
  targetEntitySetName?: string;
197
+ /** The target entity's primary key attribute - used as "from" in the generated link-entity */
198
+ targetPrimaryIdAttribute?: string;
173
199
  }
174
200
  interface QueryBuilderLookupOption {
175
201
  /** Unique identifier (typically a GUID) */
@@ -199,6 +225,13 @@ interface QueryBuilderProps {
199
225
  showResetToDefaultButton?: boolean;
200
226
  showDownloadFetchXmlButton?: boolean;
201
227
  showUploadFetchXmlButton?: boolean;
228
+ /**
229
+ * Show the "Edit FetchXML" toolbar button, which opens the current query as editable
230
+ * FetchXML that can be tweaked or overwritten by pasting. Defaults to true.
231
+ */
232
+ showEditFetchXmlButton?: boolean;
233
+ /** Show the toolbar buttons that toggle the OData and FetchXML previews. Defaults to true. */
234
+ showPreviewToggleButtons?: boolean;
202
235
  showDeleteAllFiltersButton?: boolean;
203
236
  showValidateButton?: boolean;
204
237
  showDataSourceToggle?: boolean;