From a2b6dabb3e91b2123dfbb7a0e62af72d74856da0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 22:36:47 +0000 Subject: [PATCH 1/6] Initial plan From 03f54e25be786e26963697a1cc2f2e12619814ee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 22:41:14 +0000 Subject: [PATCH 2/6] Add Query Builder React samples - Overview and Template Co-authored-by: onlyexeption <19392175+onlyexeption@users.noreply.github.com> --- .../query-builder/overview/.eslintrc.js | 78 ++++ .../query-builder/overview/README.md | 33 ++ .../query-builder/overview/index.html | 12 + .../query-builder/overview/package.json | 47 +++ .../overview/sandbox.config.json | 5 + .../query-builder/overview/src/index.css | 11 + .../query-builder/overview/src/index.tsx | 187 ++++++++ .../query-builder/overview/tsconfig.json | 44 ++ .../query-builder/overview/vite.config.js | 12 + .../query-builder/template/.eslintrc.js | 78 ++++ .../query-builder/template/README.md | 49 +++ .../query-builder/template/index.html | 12 + .../query-builder/template/package.json | 48 +++ .../template/sandbox.config.json | 5 + .../query-builder/template/src/index.css | 13 + .../query-builder/template/src/index.tsx | 399 ++++++++++++++++++ .../query-builder/template/tsconfig.json | 44 ++ .../query-builder/template/vite.config.js | 12 + 18 files changed, 1089 insertions(+) create mode 100644 samples/interactions/query-builder/overview/.eslintrc.js create mode 100644 samples/interactions/query-builder/overview/README.md create mode 100644 samples/interactions/query-builder/overview/index.html create mode 100644 samples/interactions/query-builder/overview/package.json create mode 100644 samples/interactions/query-builder/overview/sandbox.config.json create mode 100644 samples/interactions/query-builder/overview/src/index.css create mode 100644 samples/interactions/query-builder/overview/src/index.tsx create mode 100644 samples/interactions/query-builder/overview/tsconfig.json create mode 100644 samples/interactions/query-builder/overview/vite.config.js create mode 100644 samples/interactions/query-builder/template/.eslintrc.js create mode 100644 samples/interactions/query-builder/template/README.md create mode 100644 samples/interactions/query-builder/template/index.html create mode 100644 samples/interactions/query-builder/template/package.json create mode 100644 samples/interactions/query-builder/template/sandbox.config.json create mode 100644 samples/interactions/query-builder/template/src/index.css create mode 100644 samples/interactions/query-builder/template/src/index.tsx create mode 100644 samples/interactions/query-builder/template/tsconfig.json create mode 100644 samples/interactions/query-builder/template/vite.config.js diff --git a/samples/interactions/query-builder/overview/.eslintrc.js b/samples/interactions/query-builder/overview/.eslintrc.js new file mode 100644 index 0000000000..0280480e75 --- /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..b9fb56c46f --- /dev/null +++ b/samples/interactions/query-builder/overview/README.md @@ -0,0 +1,33 @@ +# Query Builder Overview Sample + +This sample demonstrates the Query Builder component with Grid integration. + +## Features + +- Query Builder with entity selection (Customers/Orders) +- Dynamic field management based on selected entity +- Expression tree construction with filtering logic +- Grid integration with auto-generated columns +- API integration with Northwind backend +- Dynamic column visibility based on return fields +- Loading state handling + +## Running the Sample + +```bash +npm install +npm start +``` + +## API Integration + +The sample connects to the Northwind Query Builder API: +- Endpoint: `https://data-northwind.indigo.design/QueryBuilder/ExecuteQuery` +- Method: POST +- Body: Expression tree JSON + +## Components Used + +- `IgcQueryBuilderComponent` - Main query builder component +- `IgcGridComponent` - Data grid for displaying results +- `IgcFilteringExpressionsTree` - Expression tree structure 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..b02461bcbd --- /dev/null +++ b/samples/interactions/query-builder/overview/package.json @@ -0,0 +1,47 @@ +{ + "name": "example-ignite-ui-react", + "description": "This project provides example of 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.5.0-beta.2", + "igniteui-webcomponents-grids": "6.3.0-alpha.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": "^4.8.4", + "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..c74ad71b1a --- /dev/null +++ b/samples/interactions/query-builder/overview/src/index.tsx @@ -0,0 +1,187 @@ +import React, { useState, useEffect, useRef } from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; + +import { + IgcQueryBuilderComponent, + IgcGridComponent, + IgcFilteringExpressionsTree, + IgcExpressionTree, + FilteringLogic, + defineComponents +} from 'igniteui-webcomponents-grids/grids'; + +import 'igniteui-webcomponents-grids/grids/themes/light/material.css'; + +// Register components +defineComponents(IgcQueryBuilderComponent, IgcGridComponent); + +const API_ENDPOINT = 'https://data-northwind.indigo.design'; + +// Field type definitions +interface Field { + field: string; + dataType: string; +} + +interface Entity { + name: string; + fields: Field[]; +} + +const QueryBuilderOverview: React.FC = () => { + const queryBuilderRef = useRef(null); + const gridRef = useRef(null); + const [expressionTree, setExpressionTree] = useState(null); + + // Define field structures + const customersFields: Field[] = [ + { field: 'customerId', dataType: 'string' }, + { field: 'companyName', dataType: 'string' }, + { field: 'contactName', dataType: 'string' }, + { field: 'contactTitle', dataType: 'string' } + ]; + + const ordersFields: Field[] = [ + { 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' } + ]; + + const entities: Entity[] = [ + { name: 'Customers', fields: customersFields }, + { name: 'Orders', fields: ordersFields } + ]; + + // Initialize expression tree + useEffect(() => { + const tree = new IgcFilteringExpressionsTree(); + tree.operator = FilteringLogic.And; + tree.entity = 'Orders'; + tree.returnFields = [ + 'orderId', + 'customerId', + 'employeeId', + 'shipperId', + 'orderDate', + 'requiredDate', + 'shipVia', + 'freight', + 'shipName', + 'completed' + ]; + + setExpressionTree(tree); + }, []); + + // Set up query builder + useEffect(() => { + if (!queryBuilderRef.current || !expressionTree) return; + + const queryBuilder = queryBuilderRef.current; + queryBuilder.entities = entities as any; + queryBuilder.expressionTree = expressionTree; + + const handleExpressionTreeChange = (event: CustomEvent) => { + setExpressionTree(event.detail); + }; + + queryBuilder.addEventListener('expressionTreeChange', handleExpressionTreeChange as EventListener); + + return () => { + queryBuilder.removeEventListener('expressionTreeChange', handleExpressionTreeChange as EventListener); + }; + }, [expressionTree?.entity]); // Only re-run if entity changes + + // Set up grid + useEffect(() => { + if (!gridRef.current) return; + + const grid = gridRef.current; + grid.height = '420px'; + grid.autoGenerate = true; + }, []); + + // Fetch data when expression tree changes + useEffect(() => { + if (!expressionTree || !gridRef.current) return; + + const fetchData = async () => { + const grid = gridRef.current; + if (!grid) 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))); + calculateColumnsInView(); + } catch (err) { + console.error(err); + grid.data = []; + } finally { + grid.isLoading = false; + } + }; + + fetchData(); + }, [expressionTree]); + + // Calculate which columns should be visible based on returnFields + const calculateColumnsInView = () => { + if (!gridRef.current || !expressionTree) return; + + const grid = gridRef.current; + const returnFields = expressionTree.returnFields ?? []; + + if (returnFields.length === 0 || returnFields[0] === '*') { + const selectedEntity = 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); + }); + } + }; + + return ( +
+
+ + +
+ +
+
+
+ ); +}; + +// Rendering 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/.eslintrc.js b/samples/interactions/query-builder/template/.eslintrc.js new file mode 100644 index 0000000000..0280480e75 --- /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..2a87b7e69a --- /dev/null +++ b/samples/interactions/query-builder/template/README.md @@ -0,0 +1,49 @@ +# Query Builder Template Sample + +This sample demonstrates the Query Builder component with custom search value templates. + +## Features + +- Query Builder with custom search value templates +- **Region field**: Custom Select dropdown with predefined options +- **OrderStatus field**: Radio button group for status selection +- **Date fields**: Date picker component +- **Time fields**: Time input with custom clock icon +- **Default fields**: Standard input (text/number) +- Expression tree JSON output display +- Query Builder Header with custom title +- Field formatters for display values + +## Running the Sample + +```bash +npm install +npm start +``` + +## Custom Templates + +The sample demonstrates the `searchValueTemplate` prop which allows customization of the search value input based on: +- Field name +- Data type +- Condition type + +### Template Examples + +- **Region**: Dropdown with predefined region options (CNA, CEU, MED, etc.) +- **OrderStatus**: Radio buttons for New, Shipped, Done +- **Date fields**: Date picker for dates +- **Time fields**: Time input with clock icon +- **Boolean fields**: Automatically handled by Query Builder +- **Other fields**: Standard text/number inputs + +## Components Used + +- `IgcQueryBuilderComponent` - Main query builder component +- `IgcQueryBuilderHeaderComponent` - Header with custom title +- `IgcDatePickerComponent` - Date selection +- `IgcDateTimeInputComponent` - Time selection +- `IgcSelectComponent` - Dropdown for region +- `IgcRadioGroupComponent` - Radio buttons for status +- `IgcInputComponent` - Standard input fields +- `IgcIconComponent` - Clock icon for time input 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..907622b310 --- /dev/null +++ b/samples/interactions/query-builder/template/package.json @@ -0,0 +1,48 @@ +{ + "name": "example-ignite-ui-react", + "description": "This project provides example of 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.5.0-beta.2", + "igniteui-webcomponents": "^6.3.0", + "igniteui-webcomponents-grids": "6.3.0-alpha.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": "^4.8.4", + "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..a0847cd055 --- /dev/null +++ b/samples/interactions/query-builder/template/src/index.tsx @@ -0,0 +1,399 @@ +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; + +import { + IgcQueryBuilderComponent, + IgcQueryBuilderHeaderComponent, + IgcFilteringExpressionsTree, + IgcExpressionTree, + FilteringLogic, + IgcStringFilteringOperand, + defineComponents as defineGridComponents +} from 'igniteui-webcomponents-grids/grids'; + +import { + IgcDatePickerComponent, + IgcDateTimeInputComponent, + IgcSelectComponent, + IgcSelectItemComponent, + IgcRadioGroupComponent, + IgcRadioComponent, + IgcInputComponent, + IgcIconComponent, + defineComponents, + registerIconFromText +} from 'igniteui-webcomponents'; + +import { html, render } from 'lit-html'; + +import 'igniteui-webcomponents-grids/grids/themes/light/material.css'; + +// Register components +defineGridComponents(IgcQueryBuilderComponent, IgcQueryBuilderHeaderComponent); +defineComponents( + IgcDatePickerComponent, + IgcDateTimeInputComponent, + IgcSelectComponent, + IgcSelectItemComponent, + IgcRadioGroupComponent, + IgcRadioComponent, + IgcInputComponent, + IgcIconComponent +); + +// 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; +} + +const QueryBuilderTemplate: React.FC = () => { + const queryBuilderRef = useRef(null); + const expressionOutputRef = useRef(null); + const [expressionTree, setExpressionTree] = useState(null); + + const 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' } + ]; + + const statusOptions: StatusOption[] = [ + { text: 'New', value: 1 }, + { text: 'Shipped', value: 2 }, + { text: 'Done', value: 3 } + ]; + + // Register icon + useEffect(() => { + const clockIcon = ""; + registerIconFromText('clock', clockIcon, 'material'); + }, []); + + // Define fields with formatters + const ordersFields: Field[] = [ + { 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) => statusOptions.find(option => option.value === value)?.text ?? value + } + ]; + + const entities: Entity[] = [ + { + name: 'Orders', + fields: ordersFields + } + ]; + + // Initialize expression tree + useEffect(() => { + const tree = new IgcFilteringExpressionsTree(); + tree.operator = FilteringLogic.And; + tree.entity = 'Orders'; + tree.returnFields = ['*']; + tree.filteringOperands.push({ + fieldName: 'Region', + condition: IgcStringFilteringOperand.instance().condition('equals'), + conditionName: 'equals', + searchVal: regionOptions[0] + } as any); + tree.filteringOperands.push({ + fieldName: 'OrderStatus', + condition: IgcStringFilteringOperand.instance().condition('equals'), + conditionName: 'equals', + searchVal: statusOptions[0].value + } as any); + + setExpressionTree(tree); + }, []); + + // Normalize time value + const 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; + }; + + // Build search value template + const buildSearchValueTemplate = useCallback((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 buildRegionSelect(ctx); + } + + if (field === 'OrderStatus' && matchesEqualityCondition) { + return buildStatusRadios(ctx); + } + + if (ctx.selectedField?.dataType === 'date') { + return buildDatePicker(ctx); + } + + if (ctx.selectedField?.dataType === 'time') { + return buildTimeInput(ctx); + } + + return buildDefaultInput(ctx, matchesEqualityCondition); + }, []); + + // Build region select template + const buildRegionSelect = (ctx: QueryBuilderSearchValueContext) => { + const currentValue = ctx?.implicit?.value?.value ?? ''; + + return html` + ) => { + const value = event.detail?.value; + const currentKey = ctx?.implicit?.value?.value ?? ''; + + if (!value || value === currentKey) return; + + ctx.implicit.value = regionOptions.find(option => option.value === value) ?? null; + }}> + ${regionOptions.map(option => html` + ${option.text} + `)} + + `; + }; + + // Build status radios template + const buildStatusRadios = (ctx: QueryBuilderSearchValueContext) => { + const implicitValue = ctx.implicit?.value; + const currentValue = implicitValue === null ? '' : implicitValue.toString(); + + return html` + ) => { + const value = event.detail?.value; + if (value === undefined) return; + + const numericValue = Number(value); + if (ctx.implicit.value === numericValue) return; + + ctx.implicit.value = numericValue; + }}> + ${statusOptions.map(option => html` + + ${option.text} + + `)} + + `; + }; + + // Build date picker template + const 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.includes(ctx.selectedCondition ?? ''); + + return html` + (event.currentTarget as IgcDatePickerComponent).show()} + @igcChange=${(event: CustomEvent) => { + ctx.implicit.value = event.detail; + }}> + + `; + }; + + // Build time input template + const buildTimeInput = (ctx: QueryBuilderSearchValueContext) => { + const currentValue = normalizeTimeValue(ctx.implicit?.value); + const allowedConditions = ['at', 'not_at', 'at_before', 'at_after', 'before', 'after']; + const isDisabled = ctx.selectedField == null || !allowedConditions.includes(ctx.selectedCondition ?? ''); + + return html` + { + const picker = event.currentTarget as IgcDateTimeInputComponent; + ctx.implicit.value = picker.value; + }}> + + + `; + }; + + // Build default input template + const 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.includes(ctx.selectedCondition ?? ''); + + return html` + { + const target = event.target as HTMLInputElement; + ctx.implicit.value = isNumber + ? target.value === '' ? null : Number(target.value) + : target.value; + }}> + + `; + }; + + // Set up query builder + useEffect(() => { + if (!queryBuilderRef.current || !expressionTree) return; + + const queryBuilder = queryBuilderRef.current; + queryBuilder.entities = entities as any; + queryBuilder.expressionTree = expressionTree; + queryBuilder.searchValueTemplate = buildSearchValueTemplate as any; + + const handleExpressionTreeChange = (event: CustomEvent) => { + setExpressionTree(event.detail); + }; + + queryBuilder.addEventListener('expressionTreeChange', handleExpressionTreeChange as EventListener); + + return () => { + queryBuilder.removeEventListener('expressionTreeChange', handleExpressionTreeChange as EventListener); + }; + }, [expressionTree?.entity, buildSearchValueTemplate]); + + // Render expression tree output + useEffect(() => { + if (expressionOutputRef.current && expressionTree) { + expressionOutputRef.current.textContent = JSON.stringify(expressionTree, null, 2); + } + }, [expressionTree]); + + return ( +
+
+ + + + +
+

+        
+
+
+ ); +}; + +// Rendering 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 + }, +}); From ff8557e314afc5856bbd1215d508bc293af677ee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 22:45:44 +0000 Subject: [PATCH 3/6] Fix TypeScript version and build errors for Query Builder samples Co-authored-by: onlyexeption <19392175+onlyexeption@users.noreply.github.com> --- .../query-builder/overview/package.json | 4 +- .../query-builder/overview/src/index.tsx | 62 +++++++++++-------- .../query-builder/template/package.json | 4 +- .../query-builder/template/src/index.tsx | 18 ++++-- 4 files changed, 54 insertions(+), 34 deletions(-) diff --git a/samples/interactions/query-builder/overview/package.json b/samples/interactions/query-builder/overview/package.json index b02461bcbd..8553d05c28 100644 --- a/samples/interactions/query-builder/overview/package.json +++ b/samples/interactions/query-builder/overview/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "igniteui-react": "19.5.0-beta.2", - "igniteui-react-core": "19.5.0-beta.2", + "igniteui-react-core": "19.3.1", "igniteui-webcomponents-grids": "6.3.0-alpha.2", "lit-html": "^3.2.0", "react": "^19.2.0", @@ -32,7 +32,7 @@ "eslint": "^8.33.0", "eslint-config-react": "^1.1.7", "eslint-plugin-react": "^7.20.0", - "typescript": "^4.8.4", + "typescript": "^5.0.0", "vite": "^7.1.9", "vitest": "^3.2.4", "vitest-canvas-mock": "^0.3.3", diff --git a/samples/interactions/query-builder/overview/src/index.tsx b/samples/interactions/query-builder/overview/src/index.tsx index c74ad71b1a..fca6e4c9d9 100644 --- a/samples/interactions/query-builder/overview/src/index.tsx +++ b/samples/interactions/query-builder/overview/src/index.tsx @@ -7,14 +7,24 @@ import { IgcGridComponent, IgcFilteringExpressionsTree, IgcExpressionTree, - FilteringLogic, - defineComponents + FilteringLogic } from 'igniteui-webcomponents-grids/grids'; import 'igniteui-webcomponents-grids/grids/themes/light/material.css'; // Register components -defineComponents(IgcQueryBuilderComponent, IgcGridComponent); +IgcQueryBuilderComponent.register(); +IgcGridComponent.register(); + +// Declare JSX types for custom elements +declare global { + namespace JSX { + interface IntrinsicElements { + 'igc-query-builder': any; + 'igc-grid': any; + } + } +} const API_ENDPOINT = 'https://data-northwind.indigo.design'; @@ -83,7 +93,7 @@ const QueryBuilderOverview: React.FC = () => { // Set up query builder useEffect(() => { - if (!queryBuilderRef.current || !expressionTree) return; + if (!queryBuilderRef.current || !expressionTree) return undefined; const queryBuilder = queryBuilderRef.current; queryBuilder.entities = entities as any; @@ -102,13 +112,34 @@ const QueryBuilderOverview: React.FC = () => { // Set up grid useEffect(() => { - if (!gridRef.current) return; + if (!gridRef.current) return undefined; const grid = gridRef.current; grid.height = '420px'; grid.autoGenerate = true; }, []); + // Calculate which columns should be visible based on returnFields + const calculateColumnsInView = () => { + if (!gridRef.current || !expressionTree) return; + + const grid = gridRef.current; + const returnFields = expressionTree.returnFields ?? []; + + if (returnFields.length === 0 || returnFields[0] === '*') { + const selectedEntity = 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); + }); + } + }; + // Fetch data when expression tree changes useEffect(() => { if (!expressionTree || !gridRef.current) return; @@ -148,27 +179,6 @@ const QueryBuilderOverview: React.FC = () => { fetchData(); }, [expressionTree]); - // Calculate which columns should be visible based on returnFields - const calculateColumnsInView = () => { - if (!gridRef.current || !expressionTree) return; - - const grid = gridRef.current; - const returnFields = expressionTree.returnFields ?? []; - - if (returnFields.length === 0 || returnFields[0] === '*') { - const selectedEntity = 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); - }); - } - }; - return (
diff --git a/samples/interactions/query-builder/template/package.json b/samples/interactions/query-builder/template/package.json index 907622b310..55f004921c 100644 --- a/samples/interactions/query-builder/template/package.json +++ b/samples/interactions/query-builder/template/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "igniteui-react": "19.5.0-beta.2", - "igniteui-react-core": "19.5.0-beta.2", + "igniteui-react-core": "19.3.1", "igniteui-webcomponents": "^6.3.0", "igniteui-webcomponents-grids": "6.3.0-alpha.2", "lit-html": "^3.2.0", @@ -33,7 +33,7 @@ "eslint": "^8.33.0", "eslint-config-react": "^1.1.7", "eslint-plugin-react": "^7.20.0", - "typescript": "^4.8.4", + "typescript": "^5.9.3", "vite": "^7.1.9", "vitest": "^3.2.4", "vitest-canvas-mock": "^0.3.3", diff --git a/samples/interactions/query-builder/template/src/index.tsx b/samples/interactions/query-builder/template/src/index.tsx index a0847cd055..8dd452423f 100644 --- a/samples/interactions/query-builder/template/src/index.tsx +++ b/samples/interactions/query-builder/template/src/index.tsx @@ -8,8 +8,7 @@ import { IgcFilteringExpressionsTree, IgcExpressionTree, FilteringLogic, - IgcStringFilteringOperand, - defineComponents as defineGridComponents + IgcStringFilteringOperand } from 'igniteui-webcomponents-grids/grids'; import { @@ -30,7 +29,8 @@ import { html, render } from 'lit-html'; import 'igniteui-webcomponents-grids/grids/themes/light/material.css'; // Register components -defineGridComponents(IgcQueryBuilderComponent, IgcQueryBuilderHeaderComponent); +IgcQueryBuilderComponent.register(); +IgcQueryBuilderHeaderComponent.register(); defineComponents( IgcDatePickerComponent, IgcDateTimeInputComponent, @@ -42,6 +42,16 @@ defineComponents( IgcIconComponent ); +// Declare JSX types for custom elements +declare global { + namespace JSX { + interface IntrinsicElements { + 'igc-query-builder': any; + 'igc-query-builder-header': any; + } + } +} + // Types interface Field { field: string; @@ -354,7 +364,7 @@ const QueryBuilderTemplate: React.FC = () => { // Set up query builder useEffect(() => { - if (!queryBuilderRef.current || !expressionTree) return; + if (!queryBuilderRef.current || !expressionTree) return undefined; const queryBuilder = queryBuilderRef.current; queryBuilder.entities = entities as any; From 2aa59bc23d4102cd9ff66dbbf5ee4c8bb517d6d1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 22:50:20 +0000 Subject: [PATCH 4/6] Address code review feedback: fix TypeScript version consistency and indentation Co-authored-by: onlyexeption <19392175+onlyexeption@users.noreply.github.com> --- samples/interactions/query-builder/overview/.eslintrc.js | 2 +- samples/interactions/query-builder/overview/package.json | 2 +- samples/interactions/query-builder/template/.eslintrc.js | 2 +- samples/interactions/query-builder/template/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/samples/interactions/query-builder/overview/.eslintrc.js b/samples/interactions/query-builder/overview/.eslintrc.js index 0280480e75..0c41c2db83 100644 --- a/samples/interactions/query-builder/overview/.eslintrc.js +++ b/samples/interactions/query-builder/overview/.eslintrc.js @@ -58,7 +58,7 @@ module.exports = { "no-throw-literal": "off", "no-useless-concat": "off", "no-mixed-operators": "off", - "no-mixed-spaces-and-tabs": 0, + "no-mixed-spaces-and-tabs": 0, "no-prototype-builtins": "off", "prefer-const": "off", "prefer-rest-params": "off", diff --git a/samples/interactions/query-builder/overview/package.json b/samples/interactions/query-builder/overview/package.json index 8553d05c28..6a9253739d 100644 --- a/samples/interactions/query-builder/overview/package.json +++ b/samples/interactions/query-builder/overview/package.json @@ -32,7 +32,7 @@ "eslint": "^8.33.0", "eslint-config-react": "^1.1.7", "eslint-plugin-react": "^7.20.0", - "typescript": "^5.0.0", + "typescript": "5.0.2", "vite": "^7.1.9", "vitest": "^3.2.4", "vitest-canvas-mock": "^0.3.3", diff --git a/samples/interactions/query-builder/template/.eslintrc.js b/samples/interactions/query-builder/template/.eslintrc.js index 0280480e75..0c41c2db83 100644 --- a/samples/interactions/query-builder/template/.eslintrc.js +++ b/samples/interactions/query-builder/template/.eslintrc.js @@ -58,7 +58,7 @@ module.exports = { "no-throw-literal": "off", "no-useless-concat": "off", "no-mixed-operators": "off", - "no-mixed-spaces-and-tabs": 0, + "no-mixed-spaces-and-tabs": 0, "no-prototype-builtins": "off", "prefer-const": "off", "prefer-rest-params": "off", diff --git a/samples/interactions/query-builder/template/package.json b/samples/interactions/query-builder/template/package.json index 55f004921c..2991dec303 100644 --- a/samples/interactions/query-builder/template/package.json +++ b/samples/interactions/query-builder/template/package.json @@ -33,7 +33,7 @@ "eslint": "^8.33.0", "eslint-config-react": "^1.1.7", "eslint-plugin-react": "^7.20.0", - "typescript": "^5.9.3", + "typescript": "5.0.2", "vite": "^7.1.9", "vitest": "^3.2.4", "vitest-canvas-mock": "^0.3.3", From be087d8e8bc033f7ab27ba9e4213b241574c145a Mon Sep 17 00:00:00 2001 From: onlyexeption Date: Mon, 2 Feb 2026 12:40:27 +0200 Subject: [PATCH 5/6] fix: convert to class component & minor changes --- .../overview/.devcontainer/devcontainer.json | 4 + .../query-builder/overview/README.md | 69 +++-- .../query-builder/overview/package.json | 4 +- .../query-builder/overview/src/index.tsx | 258 +++++++++------- .../template/.devcontainer/devcontainer.json | 4 + .../query-builder/template/README.md | 81 ++--- .../query-builder/template/package.json | 4 +- .../query-builder/template/src/index.tsx | 286 ++++++++++-------- 8 files changed, 397 insertions(+), 313 deletions(-) create mode 100644 samples/interactions/query-builder/overview/.devcontainer/devcontainer.json create mode 100644 samples/interactions/query-builder/template/.devcontainer/devcontainer.json 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/README.md b/samples/interactions/query-builder/overview/README.md index b9fb56c46f..d61a218431 100644 --- a/samples/interactions/query-builder/overview/README.md +++ b/samples/interactions/query-builder/overview/README.md @@ -1,33 +1,56 @@ -# Query Builder Overview Sample + + -This sample demonstrates the Query Builder component with Grid integration. +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. -## Features -- Query Builder with entity selection (Customers/Orders) -- Dynamic field management based on selected entity -- Expression tree construction with filtering logic -- Grid integration with auto-generated columns -- API integration with Northwind backend -- Dynamic column visibility based on return fields -- Loading state handling + + + + View Docs + + + View Code + + + Run Sample + + + Run Sample + + + -## Running the Sample +## Branches -```bash -npm install -npm start +> **_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 ``` -## API Integration +Then open http://localhost:4200/ in your browser -The sample connects to the Northwind Query Builder API: -- Endpoint: `https://data-northwind.indigo.design/QueryBuilder/ExecuteQuery` -- Method: POST -- Body: Expression tree JSON -## Components Used +## Learn More -- `IgcQueryBuilderComponent` - Main query builder component -- `IgcGridComponent` - Data grid for displaying results -- `IgcFilteringExpressionsTree` - Expression tree structure +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/package.json b/samples/interactions/query-builder/overview/package.json index 6a9253739d..9d39178a3d 100644 --- a/samples/interactions/query-builder/overview/package.json +++ b/samples/interactions/query-builder/overview/package.json @@ -1,6 +1,6 @@ { - "name": "example-ignite-ui-react", - "description": "This project provides example of using Ignite UI for React components", + "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": "", diff --git a/samples/interactions/query-builder/overview/src/index.tsx b/samples/interactions/query-builder/overview/src/index.tsx index fca6e4c9d9..4247ffd539 100644 --- a/samples/interactions/query-builder/overview/src/index.tsx +++ b/samples/interactions/query-builder/overview/src/index.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; @@ -39,39 +39,27 @@ interface Entity { fields: Field[]; } -const QueryBuilderOverview: React.FC = () => { - const queryBuilderRef = useRef(null); - const gridRef = useRef(null); - const [expressionTree, setExpressionTree] = useState(null); - - // Define field structures - const customersFields: Field[] = [ - { field: 'customerId', dataType: 'string' }, - { field: 'companyName', dataType: 'string' }, - { field: 'contactName', dataType: 'string' }, - { field: 'contactTitle', dataType: 'string' } - ]; - - const ordersFields: Field[] = [ - { 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' } - ]; - - const entities: Entity[] = [ - { name: 'Customers', fields: customersFields }, - { name: 'Orders', fields: ordersFields } - ]; - - // Initialize expression tree - useEffect(() => { +interface SampleState { + expressionTree: IgcExpressionTree | 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 IgcFilteringExpressionsTree(); tree.operator = FilteringLogic.And; tree.entity = 'Orders'; @@ -88,46 +76,89 @@ const QueryBuilderOverview: React.FC = () => { 'completed' ]; - setExpressionTree(tree); - }, []); + this.setState({ expressionTree: tree }); - // Set up query builder - useEffect(() => { - if (!queryBuilderRef.current || !expressionTree) return undefined; + // Set up query builder + if (this.queryBuilderRef.current && tree) { + const queryBuilder = this.queryBuilderRef.current; + queryBuilder.entities = this.entities as any; + queryBuilder.expressionTree = tree; - const queryBuilder = queryBuilderRef.current; - queryBuilder.entities = entities as any; - queryBuilder.expressionTree = expressionTree; + queryBuilder.addEventListener('expressionTreeChange', this.handleExpressionTreeChange); + } - const handleExpressionTreeChange = (event: CustomEvent) => { - setExpressionTree(event.detail); - }; + // Set up grid + if (this.gridRef.current) { + const grid = this.gridRef.current; + grid.height = '420px'; + grid.autoGenerate = true; + } + } - queryBuilder.addEventListener('expressionTreeChange', handleExpressionTreeChange as EventListener); + componentDidUpdate(prevProps: any, prevState: any) { + // Fetch data when expression tree changes + if (prevState.expressionTree !== this.state.expressionTree && this.state.expressionTree) { + this.fetchData(); + } - return () => { - queryBuilder.removeEventListener('expressionTreeChange', handleExpressionTreeChange as EventListener); - }; - }, [expressionTree?.entity]); // Only re-run if entity changes - - // Set up grid - useEffect(() => { - if (!gridRef.current) return undefined; - - const grid = gridRef.current; - grid.height = '420px'; - grid.autoGenerate = true; - }, []); - - // Calculate which columns should be visible based on returnFields - const calculateColumnsInView = () => { - if (!gridRef.current || !expressionTree) return; - - const grid = gridRef.current; + // 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: CustomEvent) => { + 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 = entities.find(e => e.name === expressionTree.entity); + const selectedEntity = this.entities.find(e => e.name === expressionTree.entity); const selectedEntityFields = (selectedEntity?.fields ?? []).map(f => f.field); grid.columns.forEach(column => { @@ -140,58 +171,55 @@ const QueryBuilderOverview: React.FC = () => { } }; - // Fetch data when expression tree changes - useEffect(() => { - if (!expressionTree || !gridRef.current) return; - - const fetchData = async () => { - const grid = gridRef.current; - if (!grid) 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))); - calculateColumnsInView(); - } catch (err) { - console.error(err); - grid.data = []; - } finally { - grid.isLoading = false; + 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}`); } - }; - fetchData(); - }, [expressionTree]); + 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; + } + } - return ( -
-
- - -
- + public render(): JSX.Element { + return ( +
+
+ + +
+ +
-
- ); -}; + ); + } +} -// Rendering component in the React DOM -const root = ReactDOM.createRoot(document.getElementById('root')!); -root.render(); +// rendering above component in the React DOM +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render(); 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/README.md b/samples/interactions/query-builder/template/README.md index 2a87b7e69a..b61472b1b1 100644 --- a/samples/interactions/query-builder/template/README.md +++ b/samples/interactions/query-builder/template/README.md @@ -1,49 +1,56 @@ -# Query Builder Template Sample + + -This sample demonstrates the Query Builder component with custom search value templates. +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. -## Features -- Query Builder with custom search value templates -- **Region field**: Custom Select dropdown with predefined options -- **OrderStatus field**: Radio button group for status selection -- **Date fields**: Date picker component -- **Time fields**: Time input with custom clock icon -- **Default fields**: Standard input (text/number) -- Expression tree JSON output display -- Query Builder Header with custom title -- Field formatters for display values + + + + View Docs + + + View Code + + + Run Sample + + + Run Sample + + + -## Running the Sample +## Branches -```bash -npm install -npm start +> **_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 ``` -## Custom Templates +open above folder in VS Code or type: +``` +code . +``` -The sample demonstrates the `searchValueTemplate` prop which allows customization of the search value input based on: -- Field name -- Data type -- Condition type +In terminal window, run: +``` +npm install --legacy-peer-deps +npm run-script start +``` -### Template Examples +Then open http://localhost:4200/ in your browser -- **Region**: Dropdown with predefined region options (CNA, CEU, MED, etc.) -- **OrderStatus**: Radio buttons for New, Shipped, Done -- **Date fields**: Date picker for dates -- **Time fields**: Time input with clock icon -- **Boolean fields**: Automatically handled by Query Builder -- **Other fields**: Standard text/number inputs -## Components Used +## Learn More -- `IgcQueryBuilderComponent` - Main query builder component -- `IgcQueryBuilderHeaderComponent` - Header with custom title -- `IgcDatePickerComponent` - Date selection -- `IgcDateTimeInputComponent` - Time selection -- `IgcSelectComponent` - Dropdown for region -- `IgcRadioGroupComponent` - Radio buttons for status -- `IgcInputComponent` - Standard input fields -- `IgcIconComponent` - Clock icon for time input +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/package.json b/samples/interactions/query-builder/template/package.json index 2991dec303..22bed77a06 100644 --- a/samples/interactions/query-builder/template/package.json +++ b/samples/interactions/query-builder/template/package.json @@ -1,6 +1,6 @@ { - "name": "example-ignite-ui-react", - "description": "This project provides example of using Ignite UI for React components", + "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": "", diff --git a/samples/interactions/query-builder/template/src/index.tsx b/samples/interactions/query-builder/template/src/index.tsx index 8dd452423f..23f540d70c 100644 --- a/samples/interactions/query-builder/template/src/index.tsx +++ b/samples/interactions/query-builder/template/src/index.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef, useCallback } from 'react'; +import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; @@ -81,12 +81,15 @@ interface QueryBuilderSearchValueContext { defaultSearchValueTemplate?: any; } -const QueryBuilderTemplate: React.FC = () => { - const queryBuilderRef = useRef(null); - const expressionOutputRef = useRef(null); - const [expressionTree, setExpressionTree] = useState(null); +interface SampleState { + expressionTree: IgcExpressionTree | null; +} + +export default class Sample extends React.Component { + private queryBuilderRef: React.RefObject; + private expressionOutputRef: React.RefObject; - const regionOptions: RegionOption[] = [ + private regionOptions: RegionOption[] = [ { text: 'Central North America', value: 'CNA' }, { text: 'Central Europe', value: 'CEU' }, { text: 'Mediterranean region', value: 'MED' }, @@ -98,69 +101,29 @@ const QueryBuilderTemplate: React.FC = () => { { text: 'Northern Australia', value: 'NAU' } ]; - const statusOptions: StatusOption[] = [ + private statusOptions: StatusOption[] = [ { text: 'New', value: 1 }, { text: 'Shipped', value: 2 }, { text: 'Done', value: 3 } ]; - // Register icon - useEffect(() => { + constructor(props: any) { + super(props); + + this.queryBuilderRef = React.createRef(); + this.expressionOutputRef = React.createRef(); + + this.state = { + expressionTree: null + }; + } + + componentDidMount() { + // Register icon const clockIcon = ""; registerIconFromText('clock', clockIcon, 'material'); - }, []); - - // Define fields with formatters - const ordersFields: Field[] = [ - { 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) => statusOptions.find(option => option.value === value)?.text ?? value - } - ]; - const entities: Entity[] = [ - { - name: 'Orders', - fields: ordersFields - } - ]; - - // Initialize expression tree - useEffect(() => { + // Initialize expression tree const tree = new IgcFilteringExpressionsTree(); tree.operator = FilteringLogic.And; tree.entity = 'Orders'; @@ -169,20 +132,106 @@ const QueryBuilderTemplate: React.FC = () => { fieldName: 'Region', condition: IgcStringFilteringOperand.instance().condition('equals'), conditionName: 'equals', - searchVal: regionOptions[0] + searchVal: this.regionOptions[0] } as any); tree.filteringOperands.push({ fieldName: 'OrderStatus', condition: IgcStringFilteringOperand.instance().condition('equals'), conditionName: 'equals', - searchVal: statusOptions[0].value + searchVal: this.statusOptions[0].value } as any); - setExpressionTree(tree); - }, []); + 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.searchValueTemplate = this.buildSearchValueTemplate as any; + + 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); + } + } - // Normalize time value - const normalizeTimeValue = (value: unknown): Date | null => { + componentWillUnmount() { + if (this.queryBuilderRef.current) { + this.queryBuilderRef.current.removeEventListener('expressionTreeChange', this.handleExpressionTreeChange); + } + } + + private handleExpressionTreeChange = (event: CustomEvent) => { + 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; @@ -200,8 +249,7 @@ const QueryBuilderTemplate: React.FC = () => { return null; }; - // Build search value template - const buildSearchValueTemplate = useCallback((ctx: QueryBuilderSearchValueContext) => { + private buildSearchValueTemplate = (ctx: QueryBuilderSearchValueContext) => { const field = ctx.selectedField?.field; const condition = ctx.selectedCondition; const matchesEqualityCondition = condition === 'equals' || condition === 'doesNotEqual'; @@ -211,26 +259,25 @@ const QueryBuilderTemplate: React.FC = () => { } if (field === 'Region' && matchesEqualityCondition) { - return buildRegionSelect(ctx); + return this.buildRegionSelect(ctx); } if (field === 'OrderStatus' && matchesEqualityCondition) { - return buildStatusRadios(ctx); + return this.buildStatusRadios(ctx); } if (ctx.selectedField?.dataType === 'date') { - return buildDatePicker(ctx); + return this.buildDatePicker(ctx); } if (ctx.selectedField?.dataType === 'time') { - return buildTimeInput(ctx); + return this.buildTimeInput(ctx); } - return buildDefaultInput(ctx, matchesEqualityCondition); - }, []); + return this.buildDefaultInput(ctx, matchesEqualityCondition); + }; - // Build region select template - const buildRegionSelect = (ctx: QueryBuilderSearchValueContext) => { + private buildRegionSelect = (ctx: QueryBuilderSearchValueContext) => { const currentValue = ctx?.implicit?.value?.value ?? ''; return html` @@ -243,17 +290,16 @@ const QueryBuilderTemplate: React.FC = () => { if (!value || value === currentKey) return; - ctx.implicit.value = regionOptions.find(option => option.value === value) ?? null; + ctx.implicit.value = this.regionOptions.find(option => option.value === value) ?? null; }}> - ${regionOptions.map(option => html` + ${this.regionOptions.map(option => html` ${option.text} `)} `; }; - // Build status radios template - const buildStatusRadios = (ctx: QueryBuilderSearchValueContext) => { + private buildStatusRadios = (ctx: QueryBuilderSearchValueContext) => { const implicitValue = ctx.implicit?.value; const currentValue = implicitValue === null ? '' : implicitValue.toString(); @@ -271,7 +317,7 @@ const QueryBuilderTemplate: React.FC = () => { ctx.implicit.value = numericValue; }}> - ${statusOptions.map(option => html` + ${this.statusOptions.map(option => html` { `; }; - // Build date picker template - const buildDatePicker = (ctx: QueryBuilderSearchValueContext) => { + private buildDatePicker = (ctx: QueryBuilderSearchValueContext) => { const implicitValue = ctx.implicit?.value; const currentValue = implicitValue instanceof Date ? implicitValue @@ -293,7 +338,7 @@ const QueryBuilderTemplate: React.FC = () => { : null; const allowedConditions = ['equals', 'doesNotEqual', 'before', 'after']; - const isEnabled = allowedConditions.includes(ctx.selectedCondition ?? ''); + const isEnabled = allowedConditions.indexOf(ctx.selectedCondition ?? '') !== -1; return html` { `; }; - // Build time input template - const buildTimeInput = (ctx: QueryBuilderSearchValueContext) => { - const currentValue = normalizeTimeValue(ctx.implicit?.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.includes(ctx.selectedCondition ?? ''); + const isDisabled = ctx.selectedField == null || allowedConditions.indexOf(ctx.selectedCondition ?? '') === -1; return html` { `; }; - // Build default input template - const buildDefaultInput = (ctx: QueryBuilderSearchValueContext, matchesEqualityCondition: boolean) => { + private buildDefaultInput = (ctx: QueryBuilderSearchValueContext, matchesEqualityCondition: boolean) => { const selectedField = ctx.selectedField; const dataType = selectedField?.dataType; const isNumber = dataType === 'number'; @@ -344,7 +387,7 @@ const QueryBuilderTemplate: React.FC = () => { const inputValue = currentValue == null ? '' : currentValue; const disabledConditions = ['empty', 'notEmpty', 'null', 'notNull', 'inQuery', 'notInQuery']; - const isDisabled = isBoolean || selectedField == null || disabledConditions.includes(ctx.selectedCondition ?? ''); + const isDisabled = isBoolean || selectedField == null || disabledConditions.indexOf(ctx.selectedCondition ?? '') !== -1; return html` { `; }; - // Set up query builder - useEffect(() => { - if (!queryBuilderRef.current || !expressionTree) return undefined; - - const queryBuilder = queryBuilderRef.current; - queryBuilder.entities = entities as any; - queryBuilder.expressionTree = expressionTree; - queryBuilder.searchValueTemplate = buildSearchValueTemplate as any; - - const handleExpressionTreeChange = (event: CustomEvent) => { - setExpressionTree(event.detail); - }; - - queryBuilder.addEventListener('expressionTreeChange', handleExpressionTreeChange as EventListener); - - return () => { - queryBuilder.removeEventListener('expressionTreeChange', handleExpressionTreeChange as EventListener); - }; - }, [expressionTree?.entity, buildSearchValueTemplate]); - - // Render expression tree output - useEffect(() => { - if (expressionOutputRef.current && expressionTree) { - expressionOutputRef.current.textContent = JSON.stringify(expressionTree, null, 2); - } - }, [expressionTree]); - - return ( -
-
- - - - -
-

+  public render(): JSX.Element {
+    return (
+      
+
+ + + + +
+

+          
-
- ); -}; + ); + } +} -// Rendering component in the React DOM -const root = ReactDOM.createRoot(document.getElementById('root')!); -root.render(); +// rendering above component in the React DOM +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render(); From a15988169c326c4dd8875db9090d141290d21dd3 Mon Sep 17 00:00:00 2001 From: onlyexeption Date: Mon, 2 Feb 2026 14:58:24 +0200 Subject: [PATCH 6/6] fix: use react grids instead of wc --- .../query-builder/overview/package.json | 2 +- .../query-builder/overview/src/index.tsx | 41 +-- .../query-builder/template/package.json | 3 +- .../query-builder/template/src/index.tsx | 251 +++++++++--------- 4 files changed, 147 insertions(+), 150 deletions(-) diff --git a/samples/interactions/query-builder/overview/package.json b/samples/interactions/query-builder/overview/package.json index 9d39178a3d..bd516b4cb2 100644 --- a/samples/interactions/query-builder/overview/package.json +++ b/samples/interactions/query-builder/overview/package.json @@ -16,7 +16,7 @@ "dependencies": { "igniteui-react": "19.5.0-beta.2", "igniteui-react-core": "19.3.1", - "igniteui-webcomponents-grids": "6.3.0-alpha.2", + "igniteui-react-grids": "19.5.0-beta.2", "lit-html": "^3.2.0", "react": "^19.2.0", "react-dom": "^19.2.0", diff --git a/samples/interactions/query-builder/overview/src/index.tsx b/samples/interactions/query-builder/overview/src/index.tsx index 4247ffd539..d1aed10fbb 100644 --- a/samples/interactions/query-builder/overview/src/index.tsx +++ b/samples/interactions/query-builder/overview/src/index.tsx @@ -1,30 +1,15 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; - import { - IgcQueryBuilderComponent, - IgcGridComponent, - IgcFilteringExpressionsTree, - IgcExpressionTree, + IgrQueryBuilder, + IgrGrid, + IgrFilteringExpressionsTree, + IgrExpressionTree, FilteringLogic -} from 'igniteui-webcomponents-grids/grids'; - -import 'igniteui-webcomponents-grids/grids/themes/light/material.css'; - -// Register components -IgcQueryBuilderComponent.register(); -IgcGridComponent.register(); +} from 'igniteui-react-grids'; -// Declare JSX types for custom elements -declare global { - namespace JSX { - interface IntrinsicElements { - 'igc-query-builder': any; - 'igc-grid': any; - } - } -} +import 'igniteui-react-grids/grids/themes/light/material.css'; const API_ENDPOINT = 'https://data-northwind.indigo.design'; @@ -40,12 +25,12 @@ interface Entity { } interface SampleState { - expressionTree: IgcExpressionTree | null; + expressionTree: IgrExpressionTree | null; } export default class Sample extends React.Component { - private queryBuilderRef: React.RefObject; - private gridRef: React.RefObject; + private queryBuilderRef: React.RefObject; + private gridRef: React.RefObject; constructor(props: any) { super(props); @@ -60,7 +45,7 @@ export default class Sample extends React.Component { componentDidMount() { // Initialize expression tree - const tree = new IgcFilteringExpressionsTree(); + const tree = new IgrFilteringExpressionsTree(); tree.operator = FilteringLogic.And; tree.entity = 'Orders'; tree.returnFields = [ @@ -115,7 +100,7 @@ export default class Sample extends React.Component { } } - private handleExpressionTreeChange = (event: CustomEvent) => { + private handleExpressionTreeChange = (event: any) => { this.setState({ expressionTree: event.detail }); }; @@ -209,10 +194,10 @@ export default class Sample extends React.Component { return (
- +
- +
diff --git a/samples/interactions/query-builder/template/package.json b/samples/interactions/query-builder/template/package.json index 22bed77a06..43f9546945 100644 --- a/samples/interactions/query-builder/template/package.json +++ b/samples/interactions/query-builder/template/package.json @@ -16,8 +16,7 @@ "dependencies": { "igniteui-react": "19.5.0-beta.2", "igniteui-react-core": "19.3.1", - "igniteui-webcomponents": "^6.3.0", - "igniteui-webcomponents-grids": "6.3.0-alpha.2", + "igniteui-react-grids": "19.5.0-beta.2", "lit-html": "^3.2.0", "react": "^19.2.0", "react-dom": "^19.2.0", diff --git a/samples/interactions/query-builder/template/src/index.tsx b/samples/interactions/query-builder/template/src/index.tsx index 23f540d70c..ff3ba8dbd4 100644 --- a/samples/interactions/query-builder/template/src/index.tsx +++ b/samples/interactions/query-builder/template/src/index.tsx @@ -3,54 +3,45 @@ import ReactDOM from 'react-dom/client'; import './index.css'; import { - IgcQueryBuilderComponent, - IgcQueryBuilderHeaderComponent, - IgcFilteringExpressionsTree, - IgcExpressionTree, + IgrQueryBuilder, + IgrQueryBuilderModule, + IgrQueryBuilderHeader, + IgrFilteringExpressionsTree, + IgrExpressionTree, FilteringLogic, - IgcStringFilteringOperand -} from 'igniteui-webcomponents-grids/grids'; + IgrStringFilteringOperand +} from 'igniteui-react-grids'; import { - IgcDatePickerComponent, - IgcDateTimeInputComponent, - IgcSelectComponent, - IgcSelectItemComponent, - IgcRadioGroupComponent, - IgcRadioComponent, - IgcInputComponent, - IgcIconComponent, - defineComponents, - registerIconFromText -} from 'igniteui-webcomponents'; - -import { html, render } from 'lit-html'; - -import 'igniteui-webcomponents-grids/grids/themes/light/material.css'; + 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 -IgcQueryBuilderComponent.register(); -IgcQueryBuilderHeaderComponent.register(); -defineComponents( - IgcDatePickerComponent, - IgcDateTimeInputComponent, - IgcSelectComponent, - IgcSelectItemComponent, - IgcRadioGroupComponent, - IgcRadioComponent, - IgcInputComponent, - IgcIconComponent -); - -// Declare JSX types for custom elements -declare global { - namespace JSX { - interface IntrinsicElements { - 'igc-query-builder': any; - 'igc-query-builder-header': any; - } - } -} +const mods: any[] = [ + IgrQueryBuilderModule, + IgrDatePickerModule, + IgrDateTimeInputModule, + IgrSelectModule, + IgrRadioGroupModule, + IgrInputModule, + IgrIconModule +]; +mods.forEach((m) => m.register()); // Types interface Field { @@ -82,11 +73,11 @@ interface QueryBuilderSearchValueContext { } interface SampleState { - expressionTree: IgcExpressionTree | null; + expressionTree: IgrExpressionTree | null; } export default class Sample extends React.Component { - private queryBuilderRef: React.RefObject; + private queryBuilderRef: React.RefObject; private expressionOutputRef: React.RefObject; private regionOptions: RegionOption[] = [ @@ -119,24 +110,20 @@ export default class Sample extends React.Component { } componentDidMount() { - // Register icon - const clockIcon = ""; - registerIconFromText('clock', clockIcon, 'material'); - // Initialize expression tree - const tree = new IgcFilteringExpressionsTree(); + const tree = new IgrFilteringExpressionsTree(); tree.operator = FilteringLogic.And; tree.entity = 'Orders'; tree.returnFields = ['*']; tree.filteringOperands.push({ fieldName: 'Region', - condition: IgcStringFilteringOperand.instance().condition('equals'), + condition: IgrStringFilteringOperand.instance().condition('equals'), conditionName: 'equals', searchVal: this.regionOptions[0] } as any); tree.filteringOperands.push({ fieldName: 'OrderStatus', - condition: IgcStringFilteringOperand.instance().condition('equals'), + condition: IgrStringFilteringOperand.instance().condition('equals'), conditionName: 'equals', searchVal: this.statusOptions[0].value } as any); @@ -148,7 +135,6 @@ export default class Sample extends React.Component { const queryBuilder = this.queryBuilderRef.current; queryBuilder.entities = this.entities as any; queryBuilder.expressionTree = tree; - queryBuilder.searchValueTemplate = this.buildSearchValueTemplate as any; queryBuilder.addEventListener('expressionTreeChange', this.handleExpressionTreeChange); } @@ -175,7 +161,7 @@ export default class Sample extends React.Component { } } - private handleExpressionTreeChange = (event: CustomEvent) => { + private handleExpressionTreeChange = (event: any) => { this.setState({ expressionTree: event.detail }); }; @@ -279,54 +265,65 @@ export default class Sample extends React.Component { private buildRegionSelect = (ctx: QueryBuilderSearchValueContext) => { const currentValue = ctx?.implicit?.value?.value ?? ''; + const key = `region-select-${currentValue}`; - return html` - ) => { - const value = event.detail?.value; + return ( + { + const value = sender.value; const currentKey = ctx?.implicit?.value?.value ?? ''; if (!value || value === currentKey) return; - ctx.implicit.value = this.regionOptions.find(option => option.value === value) ?? null; + setTimeout(() => { + ctx.implicit.value = this.regionOptions.find(option => option.value === value) ?? null; + }); }}> - ${this.regionOptions.map(option => html` - ${option.text} - `)} - - `; + {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 html` - ) => { - const value = event.detail?.value; + return ( + { + const value = sender.value; if (value === undefined) return; const numericValue = Number(value); if (ctx.implicit.value === numericValue) return; - ctx.implicit.value = numericValue; + setTimeout(() => { + ctx.implicit.value = numericValue; + }); }}> - ${this.statusOptions.map(option => html` - ( + - ${option.text} - - `)} - - `; + value={option.value.toString()} + checked={option.value.toString() === currentValue} + labelText={option.text}> + + ))} + + ); }; private buildDatePicker = (ctx: QueryBuilderSearchValueContext) => { @@ -339,36 +336,45 @@ export default class Sample extends React.Component { const allowedConditions = ['equals', 'doesNotEqual', 'before', 'after']; const isEnabled = allowedConditions.indexOf(ctx.selectedCondition ?? '') !== -1; + const key = `date-picker-${currentValue}`; - return html` - (event.currentTarget as IgcDatePickerComponent).show()} - @igcChange=${(event: CustomEvent) => { - ctx.implicit.value = event.detail; + 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 html` - { - const picker = event.currentTarget as IgcDateTimeInputComponent; - ctx.implicit.value = picker.value; + return ( + { + setTimeout(() => { + ctx.implicit.value = sender.value; + }); }}> - - - `; +
+ +
+ + ); }; private buildDefaultInput = (ctx: QueryBuilderSearchValueContext, matchesEqualityCondition: boolean) => { @@ -388,30 +394,37 @@ export default class Sample extends React.Component { 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 html` - { - const target = event.target as HTMLInputElement; - ctx.implicit.value = isNumber - ? target.value === '' ? null : Number(target.value) - : target.value; + return ( + { + const value = sender.value; + setTimeout(() => { + ctx.implicit.value = isNumber + ? value === '' ? null : Number(value) + : value; + }); }}> - - `; + + ); }; public render(): JSX.Element { return (
- - - + + +