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. + + + + + + View Docs + + + View Code + + + Run Sample + + + Run Sample + + + + +## 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. + + + + + + View Docs + + + View Code + + + Run Sample + + + Run Sample + + + + +## 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 + }, +});