diff --git a/samples/interactions/query-builder/overview/.devcontainer/devcontainer.json b/samples/interactions/query-builder/overview/.devcontainer/devcontainer.json
new file mode 100644
index 0000000000..e0b8e9c925
--- /dev/null
+++ b/samples/interactions/query-builder/overview/.devcontainer/devcontainer.json
@@ -0,0 +1,4 @@
+{
+ "name": "Node.js",
+ "image": "mcr.microsoft.com/devcontainers/javascript-node:22"
+}
\ No newline at end of file
diff --git a/samples/interactions/query-builder/overview/.eslintrc.js b/samples/interactions/query-builder/overview/.eslintrc.js
new file mode 100644
index 0000000000..0c41c2db83
--- /dev/null
+++ b/samples/interactions/query-builder/overview/.eslintrc.js
@@ -0,0 +1,78 @@
+// https://www.robertcooper.me/using-eslint-and-prettier-in-a-typescript-project
+module.exports = {
+ parser: "@typescript-eslint/parser", // Specifies the ESLint parser
+ parserOptions: {
+ ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
+ sourceType: "module", // Allows for the use of imports
+ ecmaFeatures: {
+ jsx: true // Allows for the parsing of JSX
+ }
+ },
+ settings: {
+ react: {
+ version: "999.999.999" // Tells eslint-plugin-react to automatically detect the version of React to use
+ }
+ },
+ extends: [
+ "eslint:recommended",
+ "plugin:react/recommended", // Uses the recommended rules from @eslint-plugin-react
+ "plugin:@typescript-eslint/recommended" // Uses the recommended rules from @typescript-eslint/eslint-plugin
+ ],
+ rules: {
+ // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
+ "default-case": "off",
+ "jsx-a11y/alt-text": "off",
+ "jsx-a11y/iframe-has-title": "off",
+ "no-undef": "off",
+ "no-unused-vars": "off",
+ "no-extend-native": "off",
+ "no-throw-literal": "off",
+ "no-useless-concat": "off",
+ "no-mixed-operators": "off",
+ "no-prototype-builtins": "off",
+ "no-mixed-spaces-and-tabs": 0,
+ "prefer-const": "off",
+ "prefer-rest-params": "off",
+ "@typescript-eslint/no-unused-vars": "off",
+ "@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/no-inferrable-types": "off",
+ "@typescript-eslint/no-useless-constructor": "off",
+ "@typescript-eslint/no-use-before-define": "off",
+ "@typescript-eslint/no-non-null-assertion": "off",
+ "@typescript-eslint/interface-name-prefix": "off",
+ "@typescript-eslint/prefer-namespace-keyword": "off",
+ "@typescript-eslint/explicit-function-return-type": "off",
+ "@typescript-eslint/explicit-module-boundary-types": "off"
+ },
+ "overrides": [
+ {
+ "files": ["*.ts", "*.tsx"],
+ "rules": {
+ "default-case": "off",
+ "jsx-a11y/alt-text": "off",
+ "jsx-a11y/iframe-has-title": "off",
+ "no-var": "off",
+ "no-undef": "off",
+ "no-unused-vars": "off",
+ "no-extend-native": "off",
+ "no-throw-literal": "off",
+ "no-useless-concat": "off",
+ "no-mixed-operators": "off",
+ "no-mixed-spaces-and-tabs": 0,
+ "no-prototype-builtins": "off",
+ "prefer-const": "off",
+ "prefer-rest-params": "off",
+ "@typescript-eslint/no-unused-vars": "off",
+ "@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/no-inferrable-types": "off",
+ "@typescript-eslint/no-useless-constructor": "off",
+ "@typescript-eslint/no-use-before-define": "off",
+ "@typescript-eslint/no-non-null-assertion": "off",
+ "@typescript-eslint/interface-name-prefix": "off",
+ "@typescript-eslint/prefer-namespace-keyword": "off",
+ "@typescript-eslint/explicit-function-return-type": "off",
+ "@typescript-eslint/explicit-module-boundary-types": "off"
+ }
+ }
+ ]
+ };
diff --git a/samples/interactions/query-builder/overview/README.md b/samples/interactions/query-builder/overview/README.md
new file mode 100644
index 0000000000..d61a218431
--- /dev/null
+++ b/samples/interactions/query-builder/overview/README.md
@@ -0,0 +1,56 @@
+
+
+
+This folder contains implementation of React application with example of Overview feature using [Query Builder](https://www.infragistics.com/products/ignite-ui-react/react/components/general-getting-started.html) component.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Branches
+
+> **_NOTE:_** You should use [master](https://github.com/IgniteUI/igniteui-react-examples/tree/master) branch of this repository if you want to run samples on your computer. Use the [vnext](https://github.com/IgniteUI/igniteui-react-examples/tree/vnext) branch only when you want to contribute new samples to this repository.
+
+## Instructions
+
+Follow these instructions to run this example:
+
+
+```
+git clone https://github.com/IgniteUI/igniteui-react-examples.git
+git checkout master
+cd ./igniteui-react-examples
+cd ./samples/interactions/query-builder/overview
+```
+
+open above folder in VS Code or type:
+```
+code .
+```
+
+In terminal window, run:
+```
+npm install --legacy-peer-deps
+npm run-script start
+```
+
+Then open http://localhost:4200/ in your browser
+
+
+## Learn More
+
+To learn more about **Ignite UI for React** components, check out the [React documentation](https://www.infragistics.com/products/ignite-ui-react/react/components/general-getting-started.html).
diff --git a/samples/interactions/query-builder/overview/index.html b/samples/interactions/query-builder/overview/index.html
new file mode 100644
index 0000000000..272184ef96
--- /dev/null
+++ b/samples/interactions/query-builder/overview/index.html
@@ -0,0 +1,12 @@
+
+
+
+ Sample | Ignite UI | React | infragistics
+
+
+
+
+
+
+
+
diff --git a/samples/interactions/query-builder/overview/package.json b/samples/interactions/query-builder/overview/package.json
new file mode 100644
index 0000000000..bd516b4cb2
--- /dev/null
+++ b/samples/interactions/query-builder/overview/package.json
@@ -0,0 +1,47 @@
+{
+ "name": "react-query-builder-overview",
+ "description": "This project provides example of Query Builder Overview using Ignite UI for React components",
+ "author": "Infragistics",
+ "version": "1.4.0",
+ "license": "",
+ "homepage": ".",
+ "private": true,
+ "scripts": {
+ "start": "vite --port 4200",
+ "build": "tsc && node --max-old-space-size=4096 node_modules/vite/bin/vite build",
+ "preview": "vite preview",
+ "test": "vitest",
+ "lint": "eslint ./src/**/*.{ts,tsx}"
+ },
+ "dependencies": {
+ "igniteui-react": "19.5.0-beta.2",
+ "igniteui-react-core": "19.3.1",
+ "igniteui-react-grids": "19.5.0-beta.2",
+ "lit-html": "^3.2.0",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0",
+ "tslib": "^2.4.0"
+ },
+ "devDependencies": {
+ "@types/jest": "^29.2.0",
+ "@types/node": "^24.7.1",
+ "@types/react": "^18.0.24",
+ "@types/react-dom": "^18.0.8",
+ "@vitejs/plugin-react": "^5.0.4",
+ "@vitest/browser": "^3.2.4",
+ "eslint": "^8.33.0",
+ "eslint-config-react": "^1.1.7",
+ "eslint-plugin-react": "^7.20.0",
+ "typescript": "5.0.2",
+ "vite": "^7.1.9",
+ "vitest": "^3.2.4",
+ "vitest-canvas-mock": "^0.3.3",
+ "worker-loader": "^3.0.8"
+ },
+ "browserslist": [
+ ">0.2%",
+ "not dead",
+ "not ie <= 11",
+ "not op_mini all"
+ ]
+}
diff --git a/samples/interactions/query-builder/overview/sandbox.config.json b/samples/interactions/query-builder/overview/sandbox.config.json
new file mode 100644
index 0000000000..49a80d1d8b
--- /dev/null
+++ b/samples/interactions/query-builder/overview/sandbox.config.json
@@ -0,0 +1,5 @@
+{
+ "infiniteLoopProtection": false,
+ "hardReloadOnChange": false,
+ "view": "browser"
+}
diff --git a/samples/interactions/query-builder/overview/src/index.css b/samples/interactions/query-builder/overview/src/index.css
new file mode 100644
index 0000000000..ec8d2776dd
--- /dev/null
+++ b/samples/interactions/query-builder/overview/src/index.css
@@ -0,0 +1,11 @@
+.wrapper {
+ margin: 10px;
+ height: 100%;
+ overflow-y: auto;
+}
+
+.output-area {
+ box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.75);
+ border-radius: 4px;
+ margin: 0 20px 20px 20px;
+}
diff --git a/samples/interactions/query-builder/overview/src/index.tsx b/samples/interactions/query-builder/overview/src/index.tsx
new file mode 100644
index 0000000000..d1aed10fbb
--- /dev/null
+++ b/samples/interactions/query-builder/overview/src/index.tsx
@@ -0,0 +1,210 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import './index.css';
+import {
+ IgrQueryBuilder,
+ IgrGrid,
+ IgrFilteringExpressionsTree,
+ IgrExpressionTree,
+ FilteringLogic
+} from 'igniteui-react-grids';
+
+import 'igniteui-react-grids/grids/themes/light/material.css';
+
+const API_ENDPOINT = 'https://data-northwind.indigo.design';
+
+// Field type definitions
+interface Field {
+ field: string;
+ dataType: string;
+}
+
+interface Entity {
+ name: string;
+ fields: Field[];
+}
+
+interface SampleState {
+ expressionTree: IgrExpressionTree | null;
+}
+
+export default class Sample extends React.Component {
+ private queryBuilderRef: React.RefObject;
+ private gridRef: React.RefObject;
+
+ constructor(props: any) {
+ super(props);
+
+ this.queryBuilderRef = React.createRef();
+ this.gridRef = React.createRef();
+
+ this.state = {
+ expressionTree: null
+ };
+ }
+
+ componentDidMount() {
+ // Initialize expression tree
+ const tree = new IgrFilteringExpressionsTree();
+ tree.operator = FilteringLogic.And;
+ tree.entity = 'Orders';
+ tree.returnFields = [
+ 'orderId',
+ 'customerId',
+ 'employeeId',
+ 'shipperId',
+ 'orderDate',
+ 'requiredDate',
+ 'shipVia',
+ 'freight',
+ 'shipName',
+ 'completed'
+ ];
+
+ this.setState({ expressionTree: tree });
+
+ // Set up query builder
+ if (this.queryBuilderRef.current && tree) {
+ const queryBuilder = this.queryBuilderRef.current;
+ queryBuilder.entities = this.entities as any;
+ queryBuilder.expressionTree = tree;
+
+ queryBuilder.addEventListener('expressionTreeChange', this.handleExpressionTreeChange);
+ }
+
+ // Set up grid
+ if (this.gridRef.current) {
+ const grid = this.gridRef.current;
+ grid.height = '420px';
+ grid.autoGenerate = true;
+ }
+ }
+
+ componentDidUpdate(prevProps: any, prevState: any) {
+ // Fetch data when expression tree changes
+ if (prevState.expressionTree !== this.state.expressionTree && this.state.expressionTree) {
+ this.fetchData();
+ }
+
+ // Update query builder if expression tree changed
+ if (this.queryBuilderRef.current && this.state.expressionTree &&
+ prevState.expressionTree !== this.state.expressionTree) {
+ const queryBuilder = this.queryBuilderRef.current;
+ queryBuilder.expressionTree = this.state.expressionTree;
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.queryBuilderRef.current) {
+ this.queryBuilderRef.current.removeEventListener('expressionTreeChange', this.handleExpressionTreeChange);
+ }
+ }
+
+ private handleExpressionTreeChange = (event: any) => {
+ this.setState({ expressionTree: event.detail });
+ };
+
+ private get customersFields(): Field[] {
+ return [
+ { field: 'customerId', dataType: 'string' },
+ { field: 'companyName', dataType: 'string' },
+ { field: 'contactName', dataType: 'string' },
+ { field: 'contactTitle', dataType: 'string' }
+ ];
+ }
+
+ private get ordersFields(): Field[] {
+ return [
+ { field: 'orderId', dataType: 'number' },
+ { field: 'customerId', dataType: 'string' },
+ { field: 'employeeId', dataType: 'number' },
+ { field: 'shipperId', dataType: 'number' },
+ { field: 'orderDate', dataType: 'date' },
+ { field: 'requiredDate', dataType: 'date' },
+ { field: 'shipVia', dataType: 'string' },
+ { field: 'freight', dataType: 'number' },
+ { field: 'shipName', dataType: 'string' },
+ { field: 'completed', dataType: 'boolean' }
+ ];
+ }
+
+ private get entities(): Entity[] {
+ return [
+ { name: 'Customers', fields: this.customersFields },
+ { name: 'Orders', fields: this.ordersFields }
+ ];
+ }
+
+ private calculateColumnsInView = () => {
+ if (!this.gridRef.current || !this.state.expressionTree) return;
+
+ const grid = this.gridRef.current;
+ const expressionTree = this.state.expressionTree;
+ const returnFields = expressionTree.returnFields ?? [];
+
+ if (returnFields.length === 0 || returnFields[0] === '*') {
+ const selectedEntity = this.entities.find(e => e.name === expressionTree.entity);
+ const selectedEntityFields = (selectedEntity?.fields ?? []).map(f => f.field);
+
+ grid.columns.forEach(column => {
+ column.hidden = !selectedEntityFields.includes(column.field);
+ });
+ } else {
+ grid.columns.forEach(column => {
+ column.hidden = !returnFields.includes(column.field);
+ });
+ }
+ };
+
+ private async fetchData() {
+ const grid = this.gridRef.current;
+ const expressionTree = this.state.expressionTree;
+
+ if (!grid || !expressionTree) return;
+
+ grid.isLoading = true;
+
+ try {
+ const response = await fetch(`${API_ENDPOINT}/QueryBuilder/ExecuteQuery`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(expressionTree)
+ });
+
+ if (!response.ok) {
+ throw new Error(`ExecuteQuery failed: ${response.status} ${response.statusText}`);
+ }
+
+ const json = await response.json();
+ const data = (Object.values(json)[0] as any[]) ?? [];
+ grid.data = data;
+
+ // Calculate column visibility after data loads
+ await new Promise(resolve => requestAnimationFrame(() => resolve(null)));
+ this.calculateColumnsInView();
+ } catch (err) {
+ console.error(err);
+ grid.data = [];
+ } finally {
+ grid.isLoading = false;
+ }
+ }
+
+ public render(): JSX.Element {
+ return (
+
+ );
+ }
+}
+
+// rendering above component in the React DOM
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render();
diff --git a/samples/interactions/query-builder/overview/tsconfig.json b/samples/interactions/query-builder/overview/tsconfig.json
new file mode 100644
index 0000000000..8c0d146f95
--- /dev/null
+++ b/samples/interactions/query-builder/overview/tsconfig.json
@@ -0,0 +1,44 @@
+{
+ "compilerOptions": {
+ "resolveJsonModule": true,
+ "esModuleInterop": true,
+ "baseUrl": ".",
+ "outDir": "build/dist",
+ "module": "esnext",
+ "target": "es5",
+ "lib": [
+ "es6",
+ "dom"
+ ],
+ "sourceMap": true,
+ "allowJs": true,
+ "jsx": "react-jsx",
+ "moduleResolution": "node",
+ "rootDir": "src",
+ "forceConsistentCasingInFileNames": true,
+ "noImplicitReturns": true,
+ "noImplicitThis": true,
+ "noImplicitAny": true,
+ "noUnusedLocals": false,
+ "importHelpers": true,
+ "allowSyntheticDefaultImports": true,
+ "skipLibCheck": true,
+ "strict": false,
+ "isolatedModules": true,
+ "noEmit": true
+ },
+ "exclude": [
+ "node_modules",
+ "build",
+ "scripts",
+ "acceptance-tests",
+ "webpack",
+ "jest",
+ "src/setupTests.ts",
+ "**/odatajs-4.0.0.js",
+ "config-overrides.js"
+ ],
+ "include": [
+ "src"
+ ]
+}
diff --git a/samples/interactions/query-builder/overview/vite.config.js b/samples/interactions/query-builder/overview/vite.config.js
new file mode 100644
index 0000000000..4fc593892f
--- /dev/null
+++ b/samples/interactions/query-builder/overview/vite.config.js
@@ -0,0 +1,12 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+export default defineConfig({
+ plugins: [react()],
+ build: {
+ outDir: 'build'
+ },
+ server: {
+ open: false
+ },
+});
diff --git a/samples/interactions/query-builder/template/.devcontainer/devcontainer.json b/samples/interactions/query-builder/template/.devcontainer/devcontainer.json
new file mode 100644
index 0000000000..e0b8e9c925
--- /dev/null
+++ b/samples/interactions/query-builder/template/.devcontainer/devcontainer.json
@@ -0,0 +1,4 @@
+{
+ "name": "Node.js",
+ "image": "mcr.microsoft.com/devcontainers/javascript-node:22"
+}
\ No newline at end of file
diff --git a/samples/interactions/query-builder/template/.eslintrc.js b/samples/interactions/query-builder/template/.eslintrc.js
new file mode 100644
index 0000000000..0c41c2db83
--- /dev/null
+++ b/samples/interactions/query-builder/template/.eslintrc.js
@@ -0,0 +1,78 @@
+// https://www.robertcooper.me/using-eslint-and-prettier-in-a-typescript-project
+module.exports = {
+ parser: "@typescript-eslint/parser", // Specifies the ESLint parser
+ parserOptions: {
+ ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
+ sourceType: "module", // Allows for the use of imports
+ ecmaFeatures: {
+ jsx: true // Allows for the parsing of JSX
+ }
+ },
+ settings: {
+ react: {
+ version: "999.999.999" // Tells eslint-plugin-react to automatically detect the version of React to use
+ }
+ },
+ extends: [
+ "eslint:recommended",
+ "plugin:react/recommended", // Uses the recommended rules from @eslint-plugin-react
+ "plugin:@typescript-eslint/recommended" // Uses the recommended rules from @typescript-eslint/eslint-plugin
+ ],
+ rules: {
+ // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
+ "default-case": "off",
+ "jsx-a11y/alt-text": "off",
+ "jsx-a11y/iframe-has-title": "off",
+ "no-undef": "off",
+ "no-unused-vars": "off",
+ "no-extend-native": "off",
+ "no-throw-literal": "off",
+ "no-useless-concat": "off",
+ "no-mixed-operators": "off",
+ "no-prototype-builtins": "off",
+ "no-mixed-spaces-and-tabs": 0,
+ "prefer-const": "off",
+ "prefer-rest-params": "off",
+ "@typescript-eslint/no-unused-vars": "off",
+ "@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/no-inferrable-types": "off",
+ "@typescript-eslint/no-useless-constructor": "off",
+ "@typescript-eslint/no-use-before-define": "off",
+ "@typescript-eslint/no-non-null-assertion": "off",
+ "@typescript-eslint/interface-name-prefix": "off",
+ "@typescript-eslint/prefer-namespace-keyword": "off",
+ "@typescript-eslint/explicit-function-return-type": "off",
+ "@typescript-eslint/explicit-module-boundary-types": "off"
+ },
+ "overrides": [
+ {
+ "files": ["*.ts", "*.tsx"],
+ "rules": {
+ "default-case": "off",
+ "jsx-a11y/alt-text": "off",
+ "jsx-a11y/iframe-has-title": "off",
+ "no-var": "off",
+ "no-undef": "off",
+ "no-unused-vars": "off",
+ "no-extend-native": "off",
+ "no-throw-literal": "off",
+ "no-useless-concat": "off",
+ "no-mixed-operators": "off",
+ "no-mixed-spaces-and-tabs": 0,
+ "no-prototype-builtins": "off",
+ "prefer-const": "off",
+ "prefer-rest-params": "off",
+ "@typescript-eslint/no-unused-vars": "off",
+ "@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/no-inferrable-types": "off",
+ "@typescript-eslint/no-useless-constructor": "off",
+ "@typescript-eslint/no-use-before-define": "off",
+ "@typescript-eslint/no-non-null-assertion": "off",
+ "@typescript-eslint/interface-name-prefix": "off",
+ "@typescript-eslint/prefer-namespace-keyword": "off",
+ "@typescript-eslint/explicit-function-return-type": "off",
+ "@typescript-eslint/explicit-module-boundary-types": "off"
+ }
+ }
+ ]
+ };
diff --git a/samples/interactions/query-builder/template/README.md b/samples/interactions/query-builder/template/README.md
new file mode 100644
index 0000000000..b61472b1b1
--- /dev/null
+++ b/samples/interactions/query-builder/template/README.md
@@ -0,0 +1,56 @@
+
+
+
+This folder contains implementation of React application with example of Custom Search Template feature using [Query Builder](https://www.infragistics.com/products/ignite-ui-react/react/components/general-getting-started.html) component.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Branches
+
+> **_NOTE:_** You should use [master](https://github.com/IgniteUI/igniteui-react-examples/tree/master) branch of this repository if you want to run samples on your computer. Use the [vnext](https://github.com/IgniteUI/igniteui-react-examples/tree/vnext) branch only when you want to contribute new samples to this repository.
+
+## Instructions
+
+Follow these instructions to run this example:
+
+
+```
+git clone https://github.com/IgniteUI/igniteui-react-examples.git
+git checkout master
+cd ./igniteui-react-examples
+cd ./samples/interactions/query-builder/template
+```
+
+open above folder in VS Code or type:
+```
+code .
+```
+
+In terminal window, run:
+```
+npm install --legacy-peer-deps
+npm run-script start
+```
+
+Then open http://localhost:4200/ in your browser
+
+
+## Learn More
+
+To learn more about **Ignite UI for React** components, check out the [React documentation](https://www.infragistics.com/products/ignite-ui-react/react/components/general-getting-started.html).
diff --git a/samples/interactions/query-builder/template/index.html b/samples/interactions/query-builder/template/index.html
new file mode 100644
index 0000000000..272184ef96
--- /dev/null
+++ b/samples/interactions/query-builder/template/index.html
@@ -0,0 +1,12 @@
+
+
+
+ Sample | Ignite UI | React | infragistics
+
+
+
+
+
+
+
+
diff --git a/samples/interactions/query-builder/template/package.json b/samples/interactions/query-builder/template/package.json
new file mode 100644
index 0000000000..43f9546945
--- /dev/null
+++ b/samples/interactions/query-builder/template/package.json
@@ -0,0 +1,47 @@
+{
+ "name": "react-query-builder-template",
+ "description": "This project provides example of Query Builder Template using Ignite UI for React components",
+ "author": "Infragistics",
+ "version": "1.4.0",
+ "license": "",
+ "homepage": ".",
+ "private": true,
+ "scripts": {
+ "start": "vite --port 4200",
+ "build": "tsc && node --max-old-space-size=4096 node_modules/vite/bin/vite build",
+ "preview": "vite preview",
+ "test": "vitest",
+ "lint": "eslint ./src/**/*.{ts,tsx}"
+ },
+ "dependencies": {
+ "igniteui-react": "19.5.0-beta.2",
+ "igniteui-react-core": "19.3.1",
+ "igniteui-react-grids": "19.5.0-beta.2",
+ "lit-html": "^3.2.0",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0",
+ "tslib": "^2.4.0"
+ },
+ "devDependencies": {
+ "@types/jest": "^29.2.0",
+ "@types/node": "^24.7.1",
+ "@types/react": "^18.0.24",
+ "@types/react-dom": "^18.0.8",
+ "@vitejs/plugin-react": "^5.0.4",
+ "@vitest/browser": "^3.2.4",
+ "eslint": "^8.33.0",
+ "eslint-config-react": "^1.1.7",
+ "eslint-plugin-react": "^7.20.0",
+ "typescript": "5.0.2",
+ "vite": "^7.1.9",
+ "vitest": "^3.2.4",
+ "vitest-canvas-mock": "^0.3.3",
+ "worker-loader": "^3.0.8"
+ },
+ "browserslist": [
+ ">0.2%",
+ "not dead",
+ "not ie <= 11",
+ "not op_mini all"
+ ]
+}
diff --git a/samples/interactions/query-builder/template/sandbox.config.json b/samples/interactions/query-builder/template/sandbox.config.json
new file mode 100644
index 0000000000..49a80d1d8b
--- /dev/null
+++ b/samples/interactions/query-builder/template/sandbox.config.json
@@ -0,0 +1,5 @@
+{
+ "infiniteLoopProtection": false,
+ "hardReloadOnChange": false,
+ "view": "browser"
+}
diff --git a/samples/interactions/query-builder/template/src/index.css b/samples/interactions/query-builder/template/src/index.css
new file mode 100644
index 0000000000..a4b5766918
--- /dev/null
+++ b/samples/interactions/query-builder/template/src/index.css
@@ -0,0 +1,13 @@
+.wrapper {
+ margin: 10px;
+ height: 100%;
+ overflow-y: auto;
+}
+
+.output-area {
+ box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.75);
+ border-radius: 4px;
+ margin: 0 20px 20px 20px;
+ padding: 16px;
+ background: #fff;
+}
diff --git a/samples/interactions/query-builder/template/src/index.tsx b/samples/interactions/query-builder/template/src/index.tsx
new file mode 100644
index 0000000000..ff3ba8dbd4
--- /dev/null
+++ b/samples/interactions/query-builder/template/src/index.tsx
@@ -0,0 +1,440 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import './index.css';
+
+import {
+ IgrQueryBuilder,
+ IgrQueryBuilderModule,
+ IgrQueryBuilderHeader,
+ IgrFilteringExpressionsTree,
+ IgrExpressionTree,
+ FilteringLogic,
+ IgrStringFilteringOperand
+} from 'igniteui-react-grids';
+
+import {
+ IgrDatePicker,
+ IgrDatePickerModule,
+ IgrDateTimeInput,
+ IgrDateTimeInputModule,
+ IgrSelect,
+ IgrSelectModule,
+ IgrSelectItem,
+ IgrRadioGroup,
+ IgrRadioGroupModule,
+ IgrRadio,
+ IgrInput,
+ IgrInputModule,
+ IgrIcon,
+ IgrIconModule
+} from 'igniteui-react';
+
+import 'igniteui-react-grids/grids/themes/light/material.css';
+
+// Register components
+const mods: any[] = [
+ IgrQueryBuilderModule,
+ IgrDatePickerModule,
+ IgrDateTimeInputModule,
+ IgrSelectModule,
+ IgrRadioGroupModule,
+ IgrInputModule,
+ IgrIconModule
+];
+mods.forEach((m) => m.register());
+
+// Types
+interface Field {
+ field: string;
+ dataType: string;
+ formatter?: (value: any) => string;
+}
+
+interface Entity {
+ name: string;
+ fields: Field[];
+}
+
+interface RegionOption {
+ text: string;
+ value: string;
+}
+
+interface StatusOption {
+ text: string;
+ value: number;
+}
+
+interface QueryBuilderSearchValueContext {
+ implicit: { value: any };
+ selectedField?: Field;
+ selectedCondition?: string;
+ defaultSearchValueTemplate?: any;
+}
+
+interface SampleState {
+ expressionTree: IgrExpressionTree | null;
+}
+
+export default class Sample extends React.Component {
+ private queryBuilderRef: React.RefObject;
+ private expressionOutputRef: React.RefObject;
+
+ private regionOptions: RegionOption[] = [
+ { text: 'Central North America', value: 'CNA' },
+ { text: 'Central Europe', value: 'CEU' },
+ { text: 'Mediterranean region', value: 'MED' },
+ { text: 'Central Asia', value: 'CAS' },
+ { text: 'South Asia', value: 'SAS' },
+ { text: 'Western Africa', value: 'WAF' },
+ { text: 'Amazonia', value: 'AMZ' },
+ { text: 'Southern Africa', value: 'SAF' },
+ { text: 'Northern Australia', value: 'NAU' }
+ ];
+
+ private statusOptions: StatusOption[] = [
+ { text: 'New', value: 1 },
+ { text: 'Shipped', value: 2 },
+ { text: 'Done', value: 3 }
+ ];
+
+ constructor(props: any) {
+ super(props);
+
+ this.queryBuilderRef = React.createRef();
+ this.expressionOutputRef = React.createRef();
+
+ this.state = {
+ expressionTree: null
+ };
+ }
+
+ componentDidMount() {
+ // Initialize expression tree
+ const tree = new IgrFilteringExpressionsTree();
+ tree.operator = FilteringLogic.And;
+ tree.entity = 'Orders';
+ tree.returnFields = ['*'];
+ tree.filteringOperands.push({
+ fieldName: 'Region',
+ condition: IgrStringFilteringOperand.instance().condition('equals'),
+ conditionName: 'equals',
+ searchVal: this.regionOptions[0]
+ } as any);
+ tree.filteringOperands.push({
+ fieldName: 'OrderStatus',
+ condition: IgrStringFilteringOperand.instance().condition('equals'),
+ conditionName: 'equals',
+ searchVal: this.statusOptions[0].value
+ } as any);
+
+ this.setState({ expressionTree: tree });
+
+ // Set up query builder
+ if (this.queryBuilderRef.current && tree) {
+ const queryBuilder = this.queryBuilderRef.current;
+ queryBuilder.entities = this.entities as any;
+ queryBuilder.expressionTree = tree;
+
+ queryBuilder.addEventListener('expressionTreeChange', this.handleExpressionTreeChange);
+ }
+ }
+
+ componentDidUpdate(prevProps: any, prevState: any) {
+ // Update query builder if expression tree changed
+ if (this.queryBuilderRef.current && this.state.expressionTree &&
+ prevState.expressionTree !== this.state.expressionTree) {
+ const queryBuilder = this.queryBuilderRef.current;
+ queryBuilder.expressionTree = this.state.expressionTree;
+ }
+
+ // Render expression tree output
+ if (this.expressionOutputRef.current && this.state.expressionTree &&
+ prevState.expressionTree !== this.state.expressionTree) {
+ this.expressionOutputRef.current.textContent = JSON.stringify(this.state.expressionTree, null, 2);
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.queryBuilderRef.current) {
+ this.queryBuilderRef.current.removeEventListener('expressionTreeChange', this.handleExpressionTreeChange);
+ }
+ }
+
+ private handleExpressionTreeChange = (event: any) => {
+ this.setState({ expressionTree: event.detail });
+ };
+
+ private get ordersFields(): Field[] {
+ return [
+ { field: 'CompanyID', dataType: 'string' },
+ { field: 'OrderID', dataType: 'number' },
+ { field: 'Freight', dataType: 'number' },
+ { field: 'ShipCountry', dataType: 'string' },
+ { field: 'IsRushOrder', dataType: 'boolean' },
+ {
+ field: 'RequiredTime',
+ dataType: 'time',
+ formatter: (value: any) => {
+ if (!value || !(value instanceof Date)) return '';
+ return value.toLocaleTimeString('en-US', {
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ }
+ },
+ {
+ field: 'OrderDate',
+ dataType: 'date',
+ formatter: (value: any) => {
+ if (!value || !(value instanceof Date)) return '';
+ return value.toLocaleDateString('en-US', {
+ month: 'short',
+ day: 'numeric',
+ year: 'numeric'
+ });
+ }
+ },
+ {
+ field: 'Region',
+ dataType: 'string',
+ formatter: (value: any) => value?.text ?? value?.value ?? value
+ },
+ {
+ field: 'OrderStatus',
+ dataType: 'number',
+ formatter: (value: number) => this.statusOptions.find(option => option.value === value)?.text ?? value
+ }
+ ];
+ }
+
+ private get entities(): Entity[] {
+ return [
+ {
+ name: 'Orders',
+ fields: this.ordersFields
+ }
+ ];
+ }
+
+ private normalizeTimeValue = (value: unknown): Date | null => {
+ if (!value) return null;
+ if (value instanceof Date) return value;
+
+ if (typeof value === 'string') {
+ const isoCandidate = value.includes('T') ? value : `1970-01-01T${value}`;
+ const parsed = new Date(isoCandidate);
+ return isNaN(parsed.getTime()) ? null : parsed;
+ }
+
+ if (typeof value === 'number') {
+ const parsed = new Date(value);
+ return isNaN(parsed.getTime()) ? null : parsed;
+ }
+
+ return null;
+ };
+
+ private buildSearchValueTemplate = (ctx: QueryBuilderSearchValueContext) => {
+ const field = ctx.selectedField?.field;
+ const condition = ctx.selectedCondition;
+ const matchesEqualityCondition = condition === 'equals' || condition === 'doesNotEqual';
+
+ if (!ctx.implicit) {
+ ctx.implicit = { value: null };
+ }
+
+ if (field === 'Region' && matchesEqualityCondition) {
+ return this.buildRegionSelect(ctx);
+ }
+
+ if (field === 'OrderStatus' && matchesEqualityCondition) {
+ return this.buildStatusRadios(ctx);
+ }
+
+ if (ctx.selectedField?.dataType === 'date') {
+ return this.buildDatePicker(ctx);
+ }
+
+ if (ctx.selectedField?.dataType === 'time') {
+ return this.buildTimeInput(ctx);
+ }
+
+ return this.buildDefaultInput(ctx, matchesEqualityCondition);
+ };
+
+ private buildRegionSelect = (ctx: QueryBuilderSearchValueContext) => {
+ const currentValue = ctx?.implicit?.value?.value ?? '';
+ const key = `region-select-${currentValue}`;
+
+ return (
+ {
+ const value = sender.value;
+ const currentKey = ctx?.implicit?.value?.value ?? '';
+
+ if (!value || value === currentKey) return;
+
+ setTimeout(() => {
+ ctx.implicit.value = this.regionOptions.find(option => option.value === value) ?? null;
+ });
+ }}>
+ {this.regionOptions.map(option => (
+
+ {option.text}
+
+ ))}
+
+ );
+ };
+
+ private buildStatusRadios = (ctx: QueryBuilderSearchValueContext) => {
+ const implicitValue = ctx.implicit?.value;
+ const currentValue = implicitValue === null ? '' : implicitValue.toString();
+ const key = `status-radio-${currentValue}`;
+
+ return (
+ {
+ const value = sender.value;
+ if (value === undefined) return;
+
+ const numericValue = Number(value);
+ if (ctx.implicit.value === numericValue) return;
+
+ setTimeout(() => {
+ ctx.implicit.value = numericValue;
+ });
+ }}>
+ {this.statusOptions.map(option => (
+
+
+ ))}
+
+ );
+ };
+
+ private buildDatePicker = (ctx: QueryBuilderSearchValueContext) => {
+ const implicitValue = ctx.implicit?.value;
+ const currentValue = implicitValue instanceof Date
+ ? implicitValue
+ : implicitValue
+ ? new Date(implicitValue)
+ : null;
+
+ const allowedConditions = ['equals', 'doesNotEqual', 'before', 'after'];
+ const isEnabled = allowedConditions.indexOf(ctx.selectedCondition ?? '') !== -1;
+ const key = `date-picker-${currentValue}`;
+
+ return (
+ sender.show()}
+ change={(sender: any) => {
+ setTimeout(() => {
+ ctx.implicit.value = sender.value;
+ });
+ }}>
+
+ );
+ };
+
+ private buildTimeInput = (ctx: QueryBuilderSearchValueContext) => {
+ const currentValue = this.normalizeTimeValue(ctx.implicit?.value);
+ const allowedConditions = ['at', 'not_at', 'at_before', 'at_after', 'before', 'after'];
+ const isDisabled = ctx.selectedField == null || allowedConditions.indexOf(ctx.selectedCondition ?? '') === -1;
+ const key = `time-input-${currentValue}`;
+
+ return (
+ {
+ setTimeout(() => {
+ ctx.implicit.value = sender.value;
+ });
+ }}>
+
+
+
+
+ );
+ };
+
+ private buildDefaultInput = (ctx: QueryBuilderSearchValueContext, matchesEqualityCondition: boolean) => {
+ const selectedField = ctx.selectedField;
+ const dataType = selectedField?.dataType;
+ const isNumber = dataType === 'number';
+ const isBoolean = dataType === 'boolean';
+
+ const placeholder = ctx.selectedCondition === 'inQuery' || ctx.selectedCondition === 'notInQuery'
+ ? 'Sub-query results'
+ : 'Value';
+
+ const currentValue = typeof ctx.implicit?.value === 'object' && (ctx.implicit.value && 'text' in ctx.implicit.value)
+ ? matchesEqualityCondition ? ctx.implicit.value.text : ''
+ : ctx.implicit?.value;
+
+ const inputValue = currentValue == null ? '' : currentValue;
+ const disabledConditions = ['empty', 'notEmpty', 'null', 'notNull', 'inQuery', 'notInQuery'];
+ const isDisabled = isBoolean || selectedField == null || disabledConditions.indexOf(ctx.selectedCondition ?? '') !== -1;
+ const key = `default-input-${inputValue}`;
+
+ return (
+ {
+ const value = sender.value;
+ setTimeout(() => {
+ ctx.implicit.value = isNumber
+ ? value === '' ? null : Number(value)
+ : value;
+ });
+ }}>
+
+ );
+ };
+
+ public render(): JSX.Element {
+ return (
+
+ );
+ }
+}
+
+// rendering above component in the React DOM
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render();
diff --git a/samples/interactions/query-builder/template/tsconfig.json b/samples/interactions/query-builder/template/tsconfig.json
new file mode 100644
index 0000000000..8c0d146f95
--- /dev/null
+++ b/samples/interactions/query-builder/template/tsconfig.json
@@ -0,0 +1,44 @@
+{
+ "compilerOptions": {
+ "resolveJsonModule": true,
+ "esModuleInterop": true,
+ "baseUrl": ".",
+ "outDir": "build/dist",
+ "module": "esnext",
+ "target": "es5",
+ "lib": [
+ "es6",
+ "dom"
+ ],
+ "sourceMap": true,
+ "allowJs": true,
+ "jsx": "react-jsx",
+ "moduleResolution": "node",
+ "rootDir": "src",
+ "forceConsistentCasingInFileNames": true,
+ "noImplicitReturns": true,
+ "noImplicitThis": true,
+ "noImplicitAny": true,
+ "noUnusedLocals": false,
+ "importHelpers": true,
+ "allowSyntheticDefaultImports": true,
+ "skipLibCheck": true,
+ "strict": false,
+ "isolatedModules": true,
+ "noEmit": true
+ },
+ "exclude": [
+ "node_modules",
+ "build",
+ "scripts",
+ "acceptance-tests",
+ "webpack",
+ "jest",
+ "src/setupTests.ts",
+ "**/odatajs-4.0.0.js",
+ "config-overrides.js"
+ ],
+ "include": [
+ "src"
+ ]
+}
diff --git a/samples/interactions/query-builder/template/vite.config.js b/samples/interactions/query-builder/template/vite.config.js
new file mode 100644
index 0000000000..4fc593892f
--- /dev/null
+++ b/samples/interactions/query-builder/template/vite.config.js
@@ -0,0 +1,12 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+export default defineConfig({
+ plugins: [react()],
+ build: {
+ outDir: 'build'
+ },
+ server: {
+ open: false
+ },
+});