diff --git a/samples/grids/grid/grid-batch-editing-remote/.devcontainer/devcontainer.json b/samples/grids/grid/grid-batch-editing-remote/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..ff434ecd44 --- /dev/null +++ b/samples/grids/grid/grid-batch-editing-remote/.devcontainer/devcontainer.json @@ -0,0 +1,4 @@ +{ + "name": "Node.js", + "image": "mcr.microsoft.com/devcontainers/javascript-node:22" +} diff --git a/samples/grids/grid/grid-batch-editing-remote/.eslintrc.js b/samples/grids/grid/grid-batch-editing-remote/.eslintrc.js new file mode 100644 index 0000000000..0280480e75 --- /dev/null +++ b/samples/grids/grid/grid-batch-editing-remote/.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/grids/grid/grid-batch-editing-remote/ReadMe.md b/samples/grids/grid/grid-batch-editing-remote/ReadMe.md new file mode 100644 index 0000000000..fb264b9e0d --- /dev/null +++ b/samples/grids/grid/grid-batch-editing-remote/ReadMe.md @@ -0,0 +1,18 @@ +# Grid Batch Editing with Remote Paging Sample + +This sample demonstrates how to use batch editing with remote paging in the Ignite UI for React Grid component. + +## Features + +- Remote paging with simulated network delay +- Batch editing (add, edit, delete rows) +- Undo/Redo support +- Transaction dialog showing pending changes +- Commit/Discard functionality + +## Running the Sample + +```bash +npm install +npm start +``` diff --git a/samples/grids/grid/grid-batch-editing-remote/index.html b/samples/grids/grid/grid-batch-editing-remote/index.html new file mode 100644 index 0000000000..272184ef96 --- /dev/null +++ b/samples/grids/grid/grid-batch-editing-remote/index.html @@ -0,0 +1,12 @@ + + + + Sample | Ignite UI | React | infragistics + + + + +
+ + + diff --git a/samples/grids/grid/grid-batch-editing-remote/package.json b/samples/grids/grid/grid-batch-editing-remote/package.json new file mode 100644 index 0000000000..777c424c5b --- /dev/null +++ b/samples/grids/grid/grid-batch-editing-remote/package.json @@ -0,0 +1,51 @@ +{ + "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-dockmanager": "^1.17.0", + "igniteui-react": "^19.4.0", + "igniteui-react-core": "19.3.1", + "igniteui-react-grids": "^19.5.0-beta.0", + "igniteui-react-inputs": "19.3.1", + "igniteui-react-layouts": "19.3.1", + "igniteui-webcomponents": "^6.3.0", + "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/grids/grid/grid-batch-editing-remote/sandbox.config.json b/samples/grids/grid/grid-batch-editing-remote/sandbox.config.json new file mode 100644 index 0000000000..49a80d1d8b --- /dev/null +++ b/samples/grids/grid/grid-batch-editing-remote/sandbox.config.json @@ -0,0 +1,5 @@ +{ + "infiniteLoopProtection": false, + "hardReloadOnChange": false, + "view": "browser" +} diff --git a/samples/grids/grid/grid-batch-editing-remote/src/ProductsWithPageResponseModel.ts b/samples/grids/grid/grid-batch-editing-remote/src/ProductsWithPageResponseModel.ts new file mode 100644 index 0000000000..ef0db00cd8 --- /dev/null +++ b/samples/grids/grid/grid-batch-editing-remote/src/ProductsWithPageResponseModel.ts @@ -0,0 +1,7 @@ +export interface ProductsWithPageResponseModel { + items: any[]; + totalRecordsCount: number; + pageSize: number; + pageNumber: number; + totalPages: number; +} diff --git a/samples/grids/grid/grid-batch-editing-remote/src/RemotePagingService.ts b/samples/grids/grid/grid-batch-editing-remote/src/RemotePagingService.ts new file mode 100644 index 0000000000..dae0d22586 --- /dev/null +++ b/samples/grids/grid/grid-batch-editing-remote/src/RemotePagingService.ts @@ -0,0 +1,54 @@ +import { ProductsWithPageResponseModel } from './ProductsWithPageResponseModel'; + +// Simulated remote data - in a real app, this would come from an API +const ALL_PRODUCTS = [ + { ProductID: 1, ProductName: 'Chai', SupplierID: 1, CategoryID: 1, QuantityPerUnit: '10 boxes x 20 bags', UnitPrice: 18, UnitsInStock: 39, UnitsOnOrder: 30, ReorderLevel: 10, Discontinued: false }, + { ProductID: 2, ProductName: 'Chang', SupplierID: 1, CategoryID: 1, QuantityPerUnit: '24 - 12 oz bottles', UnitPrice: 19, UnitsInStock: 17, UnitsOnOrder: 40, ReorderLevel: 25, Discontinued: true }, + { ProductID: 3, ProductName: 'Aniseed Syrup', SupplierID: 1, CategoryID: 2, QuantityPerUnit: '12 - 550 ml bottles', UnitPrice: 10, UnitsInStock: 13, UnitsOnOrder: 70, ReorderLevel: 25, Discontinued: false }, + { ProductID: 4, ProductName: 'Chef Anton\'s Cajun Seasoning', SupplierID: 2, CategoryID: 2, QuantityPerUnit: '48 - 6 oz jars', UnitPrice: 22, UnitsInStock: 53, UnitsOnOrder: 0, ReorderLevel: 0, Discontinued: false }, + { ProductID: 5, ProductName: 'Chef Anton\'s Gumbo Mix', SupplierID: 2, CategoryID: 2, QuantityPerUnit: '36 boxes', UnitPrice: 21.35, UnitsInStock: 0, UnitsOnOrder: 0, ReorderLevel: 0, Discontinued: true }, + { ProductID: 6, ProductName: 'Grandma\'s Boysenberry Spread', SupplierID: 3, CategoryID: 2, QuantityPerUnit: '12 - 8 oz jars', UnitPrice: 25, UnitsInStock: 120, UnitsOnOrder: 0, ReorderLevel: 25, Discontinued: false }, + { ProductID: 7, ProductName: 'Uncle Bob\'s Organic Dried Pears', SupplierID: 3, CategoryID: 7, QuantityPerUnit: '12 - 1 lb pkgs.', UnitPrice: 30, UnitsInStock: 15, UnitsOnOrder: 0, ReorderLevel: 10, Discontinued: false }, + { ProductID: 8, ProductName: 'Northwoods Cranberry Sauce', SupplierID: 3, CategoryID: 2, QuantityPerUnit: '12 - 12 oz jars', UnitPrice: 40, UnitsInStock: 6, UnitsOnOrder: 0, ReorderLevel: 0, Discontinued: false }, + { ProductID: 9, ProductName: 'Mishi Kobe Niku', SupplierID: 4, CategoryID: 6, QuantityPerUnit: '18 - 500 g pkgs.', UnitPrice: 97, UnitsInStock: 29, UnitsOnOrder: 0, ReorderLevel: 0, Discontinued: true }, + { ProductID: 10, ProductName: 'Ikura', SupplierID: 4, CategoryID: 8, QuantityPerUnit: '12 - 200 ml jars', UnitPrice: 31, UnitsInStock: 31, UnitsOnOrder: 0, ReorderLevel: 0, Discontinued: false }, + { ProductID: 11, ProductName: 'Queso Cabrales', SupplierID: 5, CategoryID: 4, QuantityPerUnit: '1 kg pkg.', UnitPrice: 21, UnitsInStock: 22, UnitsOnOrder: 30, ReorderLevel: 30, Discontinued: false }, + { ProductID: 12, ProductName: 'Queso Manchego La Pastora', SupplierID: 5, CategoryID: 4, QuantityPerUnit: '10 - 500 g pkgs.', UnitPrice: 38, UnitsInStock: 86, UnitsOnOrder: 0, ReorderLevel: 0, Discontinued: false }, + { ProductID: 13, ProductName: 'Konbu', SupplierID: 6, CategoryID: 8, QuantityPerUnit: '2 kg box', UnitPrice: 6, UnitsInStock: 24, UnitsOnOrder: 0, ReorderLevel: 5, Discontinued: false }, + { ProductID: 14, ProductName: 'Tofu', SupplierID: 6, CategoryID: 7, QuantityPerUnit: '40 - 100 g pkgs.', UnitPrice: 23.25, UnitsInStock: 35, UnitsOnOrder: 0, ReorderLevel: 0, Discontinued: false }, + { ProductID: 15, ProductName: 'Genen Shouyu', SupplierID: 6, CategoryID: 2, QuantityPerUnit: '24 - 250 ml bottles', UnitPrice: 15.5, UnitsInStock: 39, UnitsOnOrder: 0, ReorderLevel: 5, Discontinued: false }, + { ProductID: 16, ProductName: 'Pavlova', SupplierID: 7, CategoryID: 3, QuantityPerUnit: '32 - 500 g boxes', UnitPrice: 17.45, UnitsInStock: 29, UnitsOnOrder: 0, ReorderLevel: 10, Discontinued: false }, + { ProductID: 17, ProductName: 'Alice Mutton', SupplierID: 7, CategoryID: 6, QuantityPerUnit: '20 - 1 kg tins', UnitPrice: 39, UnitsInStock: 0, UnitsOnOrder: 0, ReorderLevel: 0, Discontinued: true }, + { ProductID: 18, ProductName: 'Carnarvon Tigers', SupplierID: 7, CategoryID: 8, QuantityPerUnit: '16 kg pkg.', UnitPrice: 62.5, UnitsInStock: 42, UnitsOnOrder: 0, ReorderLevel: 0, Discontinued: false }, + { ProductID: 19, ProductName: 'Teatime Chocolate Biscuits', SupplierID: 8, CategoryID: 3, QuantityPerUnit: '10 boxes x 12 pieces', UnitPrice: 9.2, UnitsInStock: 25, UnitsOnOrder: 0, ReorderLevel: 5, Discontinued: false }, + { ProductID: 20, ProductName: 'Sir Rodney\'s Marmalade', SupplierID: 8, CategoryID: 3, QuantityPerUnit: '30 gift boxes', UnitPrice: 81, UnitsInStock: 40, UnitsOnOrder: 0, ReorderLevel: 0, Discontinued: false }, +]; + +export class RemotePagingService { + private static delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + public static async getDataWithPaging(pageIndex: number = 0, pageSize: number = 10): Promise { + // Simulate network delay + await this.delay(300); + + const totalRecords = ALL_PRODUCTS.length; + const totalPages = Math.ceil(totalRecords / pageSize); + const startIndex = pageIndex * pageSize; + const endIndex = Math.min(startIndex + pageSize, totalRecords); + const items = ALL_PRODUCTS.slice(startIndex, endIndex); + + return { + items, + totalRecordsCount: totalRecords, + pageSize, + pageNumber: pageIndex, + totalPages + }; + } + + public static getTotalRecords(): number { + return ALL_PRODUCTS.length; + } +} diff --git a/samples/grids/grid/grid-batch-editing-remote/src/index.css b/samples/grids/grid/grid-batch-editing-remote/src/index.css new file mode 100644 index 0000000000..17966fb590 --- /dev/null +++ b/samples/grids/grid/grid-batch-editing-remote/src/index.css @@ -0,0 +1,44 @@ +/* shared styles are loaded from: */ +/* https://dl.infragistics.com/x/css/samples/shared.v8.css */ + +.buttons-wrapper { + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 10px 0; +} + +.buttons-right { + display: flex; + gap: 8px; +} + +.dialog-buttons { + display: flex; + gap: 8px; + justify-content: flex-end; +} + +.transaction--add { + color: #6b3; + font-weight: 600; +} + +.transaction--update { + color: #4a71b9; + font-weight: 600; +} + +.transaction--delete { + color: #ee4920; + font-weight: 600; +} + +.error-banner { + background-color: #fee; + border: 1px solid #fcc; + border-radius: 4px; + color: #c00; + padding: 10px 15px; + margin-bottom: 10px; +} diff --git a/samples/grids/grid/grid-batch-editing-remote/src/index.tsx b/samples/grids/grid/grid-batch-editing-remote/src/index.tsx new file mode 100644 index 0000000000..30d71f94c3 --- /dev/null +++ b/samples/grids/grid/grid-batch-editing-remote/src/index.tsx @@ -0,0 +1,291 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; + +import { IgrGrid, IgrPaginator, IgrColumn, IgrCellTemplateContext } from 'igniteui-react-grids'; +import { IgrButton, IgrDialog, IgrNumberEventArgs } from 'igniteui-react'; +import { RemotePagingService } from './RemotePagingService'; +import { ProductsWithPageResponseModel } from './ProductsWithPageResponseModel'; + +import 'igniteui-react-grids/grids/themes/light/bootstrap.css'; +import 'igniteui-webcomponents/themes/light/bootstrap.css'; + +interface SampleState { + undoDisabled: boolean; + redoDisabled: boolean; + commitDisabled: boolean; + discardDisabled: boolean; + transactionData: any[]; + data: any[]; + page: number; + perPage: number; + totalRecords: number; +} + +export default class Sample extends React.Component, SampleState> { + private grid: IgrGrid | null = null; + private paginator: IgrPaginator | null = null; + private dialog: IgrDialog | null = null; + private addProductId: number = 1000; + private stateUpdateSubscription: (() => void) | null = null; + private isUpdatingTransactionState: boolean = false; + + constructor(props: Record) { + super(props); + this.state = { + undoDisabled: true, + redoDisabled: true, + commitDisabled: true, + discardDisabled: true, + transactionData: [], + data: [], + page: 0, + perPage: 10, + totalRecords: 0 + }; + } + + public componentDidMount(): void { + this.loadGridData(this.state.page, this.state.perPage); + } + + public componentWillUnmount(): void { + if (this.stateUpdateSubscription) { + this.stateUpdateSubscription(); + } + } + + private loadGridData(pageIndex: number, pageSize: number): void { + if (this.grid) { + this.grid.isLoading = true; + } + + RemotePagingService.getDataWithPaging(pageIndex, pageSize) + .then((response: ProductsWithPageResponseModel) => { + this.setState({ + data: response.items, + totalRecords: response.totalRecordsCount + }); + if (this.grid) { + this.grid.isLoading = false; + } + if (this.paginator) { + this.paginator.totalRecords = response.totalRecordsCount; + } + }) + .catch((error) => { + console.error(error.message); + this.setState({ data: [] }); + if (this.grid) { + this.grid.isLoading = false; + } + }); + } + + public render() { + const { undoDisabled, redoDisabled, commitDisabled, discardDisabled, transactionData, data, perPage } = this.state; + + return ( +
+
+ + + + + + + + + + + + + + +
+
+ + Add Row + +
+ + Undo + + + Redo + + + Discard + + + Commit + +
+
+ + + + + + + +
+ + Commit + + + Discard + + + Cancel + +
+
+
+ ); + } + + private gridRef = (r: IgrGrid) => { + if (!r) { return; } + this.grid = r; + (this.grid as any).batchEditing = true; + + this.stateUpdateSubscription = this.grid.transactions.onStateUpdate.subscribe(() => { + if (!this.grid) { return; } + const hasChanges = this.grid.transactions.getAggregatedChanges(false).length > 0; + this.isUpdatingTransactionState = true; + this.setState({ + undoDisabled: !this.grid.transactions.canUndo, + redoDisabled: !this.grid.transactions.canRedo, + commitDisabled: !hasChanges, + discardDisabled: !hasChanges + }, () => { + this.isUpdatingTransactionState = false; + }); + }); + } + + private paginatorRef = (r: IgrPaginator) => { + this.paginator = r; + if (this.paginator && this.state.totalRecords > 0) { + this.paginator.totalRecords = this.state.totalRecords; + } + } + + private dialogRef = (r: IgrDialog) => { + this.dialog = r; + } + + private onPageChange = (args: IgrNumberEventArgs) => { + // Ignore page changes triggered by transaction state updates + if (this.isUpdatingTransactionState) { + return; + } + const newPage = args.detail; + // Only load data if page actually changed + if (newPage !== this.state.page) { + this.setState({ page: newPage }); + this.loadGridData(newPage, this.state.perPage); + } + } + + private onPerPageChange = (args: IgrNumberEventArgs) => { + const newPerPage = args.detail; + this.setState({ perPage: newPerPage, page: 0 }); + this.loadGridData(0, newPerPage); + } + + private randomInt(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1)) + min; + } + + public onAddRow = () => { + this.grid?.addRow({ + CategoryID: this.randomInt(1, 10), + Discontinued: this.randomInt(1, 10) % 2 === 0, + ProductID: this.addProductId++, + ProductName: `Product with index ${this.randomInt(0, 20)}`, + QuantityPerUnit: `${this.randomInt(1, 10) * 10} pcs.`, + ReorderLevel: this.randomInt(10, 20), + SupplierID: this.randomInt(1, 20), + UnitPrice: this.randomInt(10, 1000), + UnitsInStock: this.randomInt(1, 100), + UnitsOnOrder: this.randomInt(1, 20) + }); + } + + public onUndo = () => { + this.grid?.endEdit(true); + this.grid?.transactions.undo(); + } + + public onRedo = () => { + this.grid?.endEdit(true); + this.grid?.transactions.redo(); + } + + public onOpenCommitDialog = () => { + if (!this.grid) { return; } + this.setState({ + transactionData: this.grid.transactions.getAggregatedChanges(true) + }); + this.dialog?.show(); + } + + public onCommit = () => { + if (!this.grid) { return; } + this.grid.transactions.commit(this.grid.data); + this.dialog?.hide(); + } + + public onDiscard = () => { + this.grid?.transactions.clear(); + this.dialog?.hide(); + } + + public onCancel = () => { + this.dialog?.hide(); + } + + public deleteRowCellTemplate = (e: { dataContext: IgrCellTemplateContext }) => { + const id = e.dataContext.cell.id.rowID; + return ( + this.grid?.deleteRow(id)}> + Delete + + ); + } + + public typeColumnTemplate = (e: { dataContext: IgrCellTemplateContext }) => { + const type = e.dataContext.cell.value as string; + return {type.toUpperCase()}; + } + + public valueColumnTemplate = (e: { dataContext: IgrCellTemplateContext }) => { + return {JSON.stringify(e.dataContext.cell.value)}; + } +} + +// rendering above component in the React DOM +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render(); diff --git a/samples/grids/grid/grid-batch-editing-remote/tsconfig.json b/samples/grids/grid/grid-batch-editing-remote/tsconfig.json new file mode 100644 index 0000000000..8c0d146f95 --- /dev/null +++ b/samples/grids/grid/grid-batch-editing-remote/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/grids/grid/grid-batch-editing-remote/vite.config.js b/samples/grids/grid/grid-batch-editing-remote/vite.config.js new file mode 100644 index 0000000000..4fc593892f --- /dev/null +++ b/samples/grids/grid/grid-batch-editing-remote/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/grids/grid/grid-batch-editing/.devcontainer/devcontainer.json b/samples/grids/grid/grid-batch-editing/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..e0b8e9c925 --- /dev/null +++ b/samples/grids/grid/grid-batch-editing/.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/grids/grid/grid-batch-editing/.eslintrc.js b/samples/grids/grid/grid-batch-editing/.eslintrc.js new file mode 100644 index 0000000000..7168b71441 --- /dev/null +++ b/samples/grids/grid/grid-batch-editing/.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" + } + } + ] + }; \ No newline at end of file diff --git a/samples/grids/grid/grid-batch-editing/ReadMe.md b/samples/grids/grid/grid-batch-editing/ReadMe.md new file mode 100644 index 0000000000..2b91d5b2df --- /dev/null +++ b/samples/grids/grid/grid-batch-editing/ReadMe.md @@ -0,0 +1,56 @@ + + + +This folder contains implementation of React application with example of Data Batch Editing Actions feature using [Grid](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/grids/grid/data-batch-editing-actions +``` + +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/grids/grid/grid-batch-editing/index.html b/samples/grids/grid/grid-batch-editing/index.html new file mode 100644 index 0000000000..7ccd41dace --- /dev/null +++ b/samples/grids/grid/grid-batch-editing/index.html @@ -0,0 +1,12 @@ + + + + Sample | Ignite UI | React | infragistics + + + + +
+ + + \ No newline at end of file diff --git a/samples/grids/grid/grid-batch-editing/package.json b/samples/grids/grid/grid-batch-editing/package.json new file mode 100644 index 0000000000..777c424c5b --- /dev/null +++ b/samples/grids/grid/grid-batch-editing/package.json @@ -0,0 +1,51 @@ +{ + "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-dockmanager": "^1.17.0", + "igniteui-react": "^19.4.0", + "igniteui-react-core": "19.3.1", + "igniteui-react-grids": "^19.5.0-beta.0", + "igniteui-react-inputs": "19.3.1", + "igniteui-react-layouts": "19.3.1", + "igniteui-webcomponents": "^6.3.0", + "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/grids/grid/grid-batch-editing/sandbox.config.json b/samples/grids/grid/grid-batch-editing/sandbox.config.json new file mode 100644 index 0000000000..07f53508eb --- /dev/null +++ b/samples/grids/grid/grid-batch-editing/sandbox.config.json @@ -0,0 +1,5 @@ +{ + "infiniteLoopProtection": false, + "hardReloadOnChange": false, + "view": "browser" +} \ No newline at end of file diff --git a/samples/grids/grid/grid-batch-editing/src/NwindData.json b/samples/grids/grid/grid-batch-editing/src/NwindData.json new file mode 100644 index 0000000000..c00b03ec8d --- /dev/null +++ b/samples/grids/grid/grid-batch-editing/src/NwindData.json @@ -0,0 +1,458 @@ +[ + { + "ProductID": 1, + "ProductName": "Chai", + "SupplierID": 1, + "CategoryID": 1, + "QuantityPerUnit": "10 boxes x 20 bags", + "UnitPrice": 18, + "UnitsInStock": 39, + "UnitsOnOrder": 30, + "ReorderLevel": 10, + "Discontinued": false, + "OrderDate": "2012-02-12", + "Rating": 5, + "Locations": [ + { + "Shop": "Fun-Tasty Co.", + "LastInventory": "2018-06-12" + }, + { + "Shop": "Farmer Market", + "LastInventory": "2018-04-04" + } + ] + }, + { + "ProductID": 2, + "ProductName": "Chang", + "SupplierID": 1, + "CategoryID": 1, + "QuantityPerUnit": "24 - 12 oz bottles", + "UnitPrice": 19, + "UnitsInStock": 17, + "UnitsOnOrder": 40, + "ReorderLevel": 25, + "Discontinued": true, + "OrderDate": "2003-03-17", + "Rating": 5, + "Locations": [ + { + "Shop": "Super Market", + "LastInventory": "2018-09-09" + } + ] + }, + { + "ProductID": 3, + "ProductName": "Aniseed Syrup", + "SupplierID": 1, + "CategoryID": 2, + "QuantityPerUnit": "12 - 550 ml bottles", + "UnitPrice": 10, + "UnitsInStock": 13, + "UnitsOnOrder": 70, + "ReorderLevel": 25, + "Discontinued": false, + "OrderDate": "2006-03-17", + "Rating": 3, + "Locations": [ + { + "Shop": "Farmer Market", + "LastInventory": "2018-04-04" + }, + { + "Shop": "Street Market", + "LastInventory": "2018-12-12" + }, + { + "Shop": "24/7 Market", + "LastInventory": "2018-11-11" + } + ] + }, + { + "ProductID": 4, + "ProductName": "Chef Antons Cajun Seasoning", + "SupplierID": 2, + "CategoryID": 2, + "QuantityPerUnit": "48 - 6 oz jars", + "UnitPrice": 22, + "UnitsInStock": 53, + "UnitsOnOrder": 30, + "ReorderLevel": 0, + "Discontinued": false, + "OrderDate": "2016-03-17", + "Rating": 3, + "Locations": [ + { + "Shop": "Fun-Tasty Co.", + "LastInventory": "2018-06-12" + }, + { + "Shop": "Farmer Market", + "LastInventory": "2018-04-04" + }, + { + "Shop": "Street Market", + "LastInventory": "2018-12-12" + } + ] + }, + { + "ProductID": 5, + "ProductName": "Chef Antons Gumbo Mix", + "SupplierID": 2, + "CategoryID": 2, + "QuantityPerUnit": "36 boxes", + "UnitPrice": 21.35, + "UnitsInStock": 0, + "UnitsOnOrder": 30, + "ReorderLevel": 0, + "Discontinued": true, + "OrderDate": "2011-11-11", + "Rating": 5, + "Locations": [ + { + "Shop": "Super Market", + "LastInventory": "2018-09-09" + } + ] + }, + { + "ProductID": 6, + "ProductName": "Grandmas Boysenberry Spread", + "SupplierID": 3, + "CategoryID": 2, + "QuantityPerUnit": "12 - 8 oz jars", + "UnitPrice": 25, + "UnitsInStock": 0, + "UnitsOnOrder": 30, + "ReorderLevel": 25, + "Discontinued": false, + "OrderDate": "2017-12-17", + "Rating": 4, + "Locations": [ + { + "Shop": "Super Market", + "LastInventory": "2018-09-09" + } + ] + }, + { + "ProductID": 7, + "ProductName": "Uncle Bobs Organic Dried Pears", + "SupplierID": 3, + "CategoryID": 7, + "QuantityPerUnit": "12 - 1 lb pkgs.", + "UnitPrice": 30, + "UnitsInStock": 150, + "UnitsOnOrder": 30, + "ReorderLevel": 10, + "Discontinued": false, + "OrderDate": "2016-07-17", + "Rating": 5, + "Locations": [ + { + "Shop": "Fun-Tasty Co.", + "LastInventory": "2018-06-12" + }, + { + "Shop": "Farmer Market", + "LastInventory": "2018-04-04" + }, + { + "Shop": "Street Market", + "LastInventory": "2018-12-12" + } + ] + }, + { + "ProductID": 8, + "ProductName": "Northwoods Cranberry Sauce", + "SupplierID": 3, + "CategoryID": 2, + "QuantityPerUnit": "12 - 12 oz jars", + "UnitPrice": 40, + "UnitsInStock": 6, + "UnitsOnOrder": 30, + "ReorderLevel": 0, + "Discontinued": false, + "OrderDate": "2018-01-17", + "Rating": 4, + "Locations": [ + { + "Shop": "Fun-Tasty Co.", + "LastInventory": "2018-06-12" + }, + { + "Shop": "Farmer Market", + "LastInventory": "2018-04-04" + } + ] + }, + { + "ProductID": 9, + "ProductName": "Mishi Kobe Niku", + "SupplierID": 4, + "CategoryID": 6, + "QuantityPerUnit": "18 - 500 g pkgs.", + "UnitPrice": 97, + "UnitsInStock": 29, + "UnitsOnOrder": 30, + "ReorderLevel": 0, + "Discontinued": true, + "OrderDate": "2010-02-17", + "Rating": 4, + "Locations": [ + { + "Shop": "Farmer Market", + "LastInventory": "2018-04-04" + } + ] + }, + { + "ProductID": 10, + "ProductName": "Ikura", + "SupplierID": 4, + "CategoryID": 8, + "QuantityPerUnit": "12 - 200 ml jars", + "UnitPrice": 31, + "UnitsInStock": 31, + "UnitsOnOrder": 30, + "ReorderLevel": 0, + "Discontinued": false, + "OrderDate": "2008-05-17", + "Rating": 3, + "Locations": [ + { + "Shop": "Wall Market", + "LastInventory": "2018-12-06" + } + ] + }, + { + "ProductID": 11, + "ProductName": "Queso Cabrales", + "SupplierID": 5, + "CategoryID": 4, + "QuantityPerUnit": "1 kg pkg.", + "UnitPrice": 21, + "UnitsInStock": 22, + "UnitsOnOrder": 30, + "ReorderLevel": 30, + "Discontinued": false, + "OrderDate": "2009-01-17", + "Rating": 5, + "Locations": [ + { + "Shop": "Fun-Tasty Co.", + "LastInventory": "2018-06-12" + }, + { + "Shop": "Farmer Market", + "LastInventory": "2018-04-04" + } + ] + }, + { + "ProductID": 12, + "ProductName": "Queso Manchego La Pastora", + "SupplierID": 5, + "CategoryID": 4, + "QuantityPerUnit": "10 - 500 g pkgs.", + "UnitPrice": 38, + "UnitsInStock": 86, + "UnitsOnOrder": 30, + "ReorderLevel": 0, + "Discontinued": false, + "OrderDate": "2015-11-17", + "Rating": 3, + "Locations": [ + { + "Shop": "Farmer Market", + "LastInventory": "2018-04-04" + } + ] + }, + { + "ProductID": 13, + "ProductName": "Konbu", + "SupplierID": 6, + "CategoryID": 8, + "QuantityPerUnit": "2 kg box", + "UnitPrice": 6, + "UnitsInStock": 24, + "UnitsOnOrder": 30, + "ReorderLevel": 5, + "Discontinued": false, + "OrderDate": "2015-03-17", + "Rating": 2, + "Locations": [ + { + "Shop": "Super Market", + "LastInventory": "2018-09-09" + } + ] + }, + { + "ProductID": 14, + "ProductName": "Tofu", + "SupplierID": 6, + "CategoryID": 7, + "QuantityPerUnit": "40 - 100 g pkgs.", + "UnitPrice": 23.25, + "UnitsInStock": 35, + "UnitsOnOrder": 30, + "ReorderLevel": 0, + "Discontinued": false, + "OrderDate": "2017-06-17", + "Rating": 4, + "Locations": [ + { + "Shop": "Farmer Market", + "LastInventory": "2018-04-04" + }, + { + "Shop": "Street Market", + "LastInventory": "2018-12-12" + } + ] + }, + { + "ProductID": 15, + "ProductName": "Genen Shouyu", + "SupplierID": 6, + "CategoryID": 2, + "QuantityPerUnit": "24 - 250 ml bottles", + "UnitPrice": 15.5, + "UnitsInStock": 39, + "UnitsOnOrder": 30, + "ReorderLevel": 5, + "Discontinued": false, + "OrderDate": "2014-03-17", + "Rating": 4, + "Locations": [ + { + "Shop": "Local Market", + "LastInventory": "2018-07-03" + }, + { + "Shop": "Wall Market", + "LastInventory": "2018-12-06" + } + ] + }, + { + "ProductID": 16, + "ProductName": "Pavlova", + "SupplierID": 7, + "CategoryID": 3, + "QuantityPerUnit": "32 - 500 g boxes", + "UnitPrice": 17.45, + "UnitsInStock": 29, + "UnitsOnOrder": 30, + "ReorderLevel": 10, + "Discontinued": false, + "OrderDate": "2018-03-28", + "Rating": 2, + "Locations": [ + { + "Shop": "Farmer Market", + "LastInventory": "2018-04-04" + }, + { + "Shop": "Street Market", + "LastInventory": "2018-12-12" + }, + { + "Shop": "24/7 Market", + "LastInventory": "2018-11-11" + } + ] + }, + { + "ProductID": 17, + "ProductName": "Alice Mutton", + "SupplierID": 7, + "CategoryID": 6, + "QuantityPerUnit": "20 - 1 kg tins", + "UnitPrice": 39, + "UnitsInStock": 0, + "UnitsOnOrder": 30, + "ReorderLevel": 0, + "Discontinued": true, + "OrderDate": "2015-08-17", + "Rating": 2, + "Locations": [ + { + "Shop": "Farmer Market", + "LastInventory": "2018-04-04" + } + ] + }, + { + "ProductID": 18, + "ProductName": "Carnarvon Tigers", + "SupplierID": 7, + "CategoryID": 8, + "QuantityPerUnit": "16 kg pkg.", + "UnitPrice": 62.5, + "UnitsInStock": 42, + "UnitsOnOrder": 30, + "ReorderLevel": 0, + "Discontinued": false, + "OrderDate": "2005-09-27", + "Rating": 2, + "Locations": [ + { + "Shop": "24/7 Market", + "LastInventory": "2018-11-11" + }, + { + "Shop": "Super Market", + "LastInventory": "2018-09-09" + } + ] + }, + { + "ProductID": 19, + "ProductName": "Teatime Chocolate Biscuits", + "SupplierID": 8, + "CategoryID": 3, + "QuantityPerUnit": "", + "UnitPrice": 9.2, + "UnitsInStock": 25, + "UnitsOnOrder": 30, + "ReorderLevel": 5, + "Discontinued": false, + "OrderDate": "2001-03-17", + "Rating": 2, + "Locations": [ + { + "Shop": "Local Market", + "LastInventory": "2018-07-03" + } + ] + }, + { + "ProductID": 20, + "ProductName": "Sir Rodneys Marmalade", + "SupplierID": 8, + "CategoryID": 3, + "QuantityPerUnit": "4 - 100 ml jars", + "UnitPrice": 4.5, + "UnitsInStock": 40, + "UnitsOnOrder": 30, + "ReorderLevel": 0, + "Discontinued": false, + "OrderDate": "2005-03-17", + "Rating": 5, + "Locations": [ + { + "Shop": "Super Market", + "LastInventory": "2018-09-09" + } + ] + } +] \ No newline at end of file diff --git a/samples/grids/grid/grid-batch-editing/src/index.css b/samples/grids/grid/grid-batch-editing/src/index.css new file mode 100644 index 0000000000..7a4d6b21d7 --- /dev/null +++ b/samples/grids/grid/grid-batch-editing/src/index.css @@ -0,0 +1,35 @@ +/* shared styles are loaded from: */ +/* https://dl.infragistics.com/x/css/samples/shared.v8.css */ + +.buttons-wrapper { + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 10px 0; +} + +.buttons-right { + display: flex; + gap: 8px; +} + +.dialog-buttons { + display: flex; + gap: 8px; + justify-content: flex-end; +} + +.transaction--add { + color: #6b3; + font-weight: 600; +} + +.transaction--update { + color: #4a71b9; + font-weight: 600; +} + +.transaction--delete { + color: #ee4920; + font-weight: 600; +} diff --git a/samples/grids/grid/grid-batch-editing/src/index.tsx b/samples/grids/grid/grid-batch-editing/src/index.tsx new file mode 100644 index 0000000000..7e14c1c8d2 --- /dev/null +++ b/samples/grids/grid/grid-batch-editing/src/index.tsx @@ -0,0 +1,222 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; + +import { IgrGrid, IgrColumn, IgrCellTemplateContext } from 'igniteui-react-grids'; +import { IgrButton, IgrDialog } from 'igniteui-react'; +import NwindData from './NwindData.json'; + +import 'igniteui-react-grids/grids/themes/light/bootstrap.css'; +import 'igniteui-webcomponents/themes/light/bootstrap.css'; + +interface SampleState { + undoDisabled: boolean; + redoDisabled: boolean; + commitDisabled: boolean; + discardDisabled: boolean; + transactionData: any[]; +} + +export default class Sample extends React.Component, SampleState> { + private grid: IgrGrid | null = null; + private dialog: IgrDialog | null = null; + private addProductId: number = 1000; + private stateUpdateSubscription: (() => void) | null = null; + + constructor(props: Record) { + super(props); + this.state = { + undoDisabled: true, + redoDisabled: true, + commitDisabled: true, + discardDisabled: true, + transactionData: [] + }; + } + + public componentWillUnmount(): void { + // Clean up subscription to prevent memory leaks + if (this.stateUpdateSubscription) { + this.stateUpdateSubscription(); + } + } + + public render() { + const { undoDisabled, redoDisabled, commitDisabled, discardDisabled, transactionData } = this.state; + + return ( +
+
+ + + + + + + + + + + + + +
+
+ + Add Row + +
+ + Undo + + + Redo + + + Discard + + + Commit + +
+
+ + + + + + + +
+ + Commit + + + Discard + + + Cancel + +
+
+
+ ); + } + + private _nwindData: any[] = NwindData; + public get nwindData(): any[] { + return this._nwindData; + } + + private gridRef = (r: IgrGrid) => { + if (!r) { return; } + this.grid = r; + (this.grid as any).batchEditing = true; + + // Store unsubscribe function for cleanup + this.stateUpdateSubscription = this.grid.transactions.onStateUpdate.subscribe(() => { + if (!this.grid) { return; } + const hasChanges = this.grid.transactions.getAggregatedChanges(false).length > 0; + this.setState({ + undoDisabled: !this.grid.transactions.canUndo, + redoDisabled: !this.grid.transactions.canRedo, + commitDisabled: !hasChanges, + discardDisabled: !hasChanges + }); + }); + } + + private dialogRef = (r: IgrDialog) => { + this.dialog = r; + } + + private randomInt(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1)) + min; + } + + public onAddRow = () => { + this.grid?.addRow({ + CategoryID: this.randomInt(1, 10), + Discontinued: this.randomInt(1, 10) % 2 === 0, + OrderDate: new Date(this.randomInt(2000, 2050), + this.randomInt(0, 11), this.randomInt(1, 25)) + .toISOString().slice(0, 10), + ProductID: this.addProductId++, + ProductName: `Product with index ${this.randomInt(0, 20)}`, + QuantityPerUnit: `${this.randomInt(1, 10) * 10} pcs.`, + ReorderLevel: this.randomInt(10, 20), + SupplierID: this.randomInt(1, 20), + UnitPrice: this.randomInt(10, 1000), + UnitsInStock: this.randomInt(1, 100), + UnitsOnOrder: this.randomInt(1, 20) + }); + } + + public onUndo = () => { + this.grid?.endEdit(true); + this.grid?.transactions.undo(); + } + + public onRedo = () => { + this.grid?.endEdit(true); + this.grid?.transactions.redo(); + } + + public onOpenCommitDialog = () => { + if (!this.grid) { return; } + this.setState({ + transactionData: this.grid.transactions.getAggregatedChanges(true) + }); + this.dialog?.show(); + } + + public onCommit = () => { + if (!this.grid) { return; } + this.grid.transactions.commit(this.grid.data); + this.dialog?.hide(); + } + + public onDiscard = () => { + this.grid?.transactions.clear(); + this.dialog?.hide(); + } + + public onCancel = () => { + this.dialog?.hide(); + } + + public deleteRowCellTemplate = (e: { dataContext: IgrCellTemplateContext }) => { + const id = e.dataContext.cell.id.rowID; + return ( + this.grid?.deleteRow(id)}> + Delete + + ); + } + + public typeColumnTemplate = (e: { dataContext: IgrCellTemplateContext }) => { + const type = e.dataContext.cell.value as string; + return {type.toUpperCase()}; + } + + public valueColumnTemplate = (e: { dataContext: IgrCellTemplateContext }) => { + return {JSON.stringify(e.dataContext.cell.value)}; + } +} + +// rendering above component in the React DOM +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render(); diff --git a/samples/grids/grid/grid-batch-editing/tsconfig.json b/samples/grids/grid/grid-batch-editing/tsconfig.json new file mode 100644 index 0000000000..8c0d146f95 --- /dev/null +++ b/samples/grids/grid/grid-batch-editing/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/grids/grid/grid-batch-editing/vite.config.js b/samples/grids/grid/grid-batch-editing/vite.config.js new file mode 100644 index 0000000000..1744dbc719 --- /dev/null +++ b/samples/grids/grid/grid-batch-editing/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 + }, +}); \ No newline at end of file diff --git a/samples/grids/hierarchical-grid/grid-batch-editing-remote/.devcontainer/devcontainer.json b/samples/grids/hierarchical-grid/grid-batch-editing-remote/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..ff434ecd44 --- /dev/null +++ b/samples/grids/hierarchical-grid/grid-batch-editing-remote/.devcontainer/devcontainer.json @@ -0,0 +1,4 @@ +{ + "name": "Node.js", + "image": "mcr.microsoft.com/devcontainers/javascript-node:22" +} diff --git a/samples/grids/hierarchical-grid/grid-batch-editing-remote/.eslintrc.js b/samples/grids/hierarchical-grid/grid-batch-editing-remote/.eslintrc.js new file mode 100644 index 0000000000..0280480e75 --- /dev/null +++ b/samples/grids/hierarchical-grid/grid-batch-editing-remote/.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/grids/hierarchical-grid/grid-batch-editing-remote/ReadMe.md b/samples/grids/hierarchical-grid/grid-batch-editing-remote/ReadMe.md new file mode 100644 index 0000000000..fca94406e8 --- /dev/null +++ b/samples/grids/hierarchical-grid/grid-batch-editing-remote/ReadMe.md @@ -0,0 +1,19 @@ +# Hierarchical Grid Batch Editing with Remote Paging Sample + +This sample demonstrates how to use batch editing with remote paging in the Ignite UI for React Hierarchical Grid component. + +## Features + +- Remote paging with simulated network delay +- Hierarchical data with nested Albums/Songs and Tours +- Batch editing (add, edit, delete rows) +- Undo/Redo support +- Transaction dialog showing pending changes +- Commit/Discard functionality + +## Running the Sample + +```bash +npm install +npm start +``` diff --git a/samples/grids/hierarchical-grid/grid-batch-editing-remote/index.html b/samples/grids/hierarchical-grid/grid-batch-editing-remote/index.html new file mode 100644 index 0000000000..272184ef96 --- /dev/null +++ b/samples/grids/hierarchical-grid/grid-batch-editing-remote/index.html @@ -0,0 +1,12 @@ + + + + Sample | Ignite UI | React | infragistics + + + + +
+ + + diff --git a/samples/grids/hierarchical-grid/grid-batch-editing-remote/package.json b/samples/grids/hierarchical-grid/grid-batch-editing-remote/package.json new file mode 100644 index 0000000000..777c424c5b --- /dev/null +++ b/samples/grids/hierarchical-grid/grid-batch-editing-remote/package.json @@ -0,0 +1,51 @@ +{ + "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-dockmanager": "^1.17.0", + "igniteui-react": "^19.4.0", + "igniteui-react-core": "19.3.1", + "igniteui-react-grids": "^19.5.0-beta.0", + "igniteui-react-inputs": "19.3.1", + "igniteui-react-layouts": "19.3.1", + "igniteui-webcomponents": "^6.3.0", + "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/grids/hierarchical-grid/grid-batch-editing-remote/sandbox.config.json b/samples/grids/hierarchical-grid/grid-batch-editing-remote/sandbox.config.json new file mode 100644 index 0000000000..49a80d1d8b --- /dev/null +++ b/samples/grids/hierarchical-grid/grid-batch-editing-remote/sandbox.config.json @@ -0,0 +1,5 @@ +{ + "infiniteLoopProtection": false, + "hardReloadOnChange": false, + "view": "browser" +} diff --git a/samples/grids/hierarchical-grid/grid-batch-editing-remote/src/RemotePagingService.ts b/samples/grids/hierarchical-grid/grid-batch-editing-remote/src/RemotePagingService.ts new file mode 100644 index 0000000000..479d505814 --- /dev/null +++ b/samples/grids/hierarchical-grid/grid-batch-editing-remote/src/RemotePagingService.ts @@ -0,0 +1,201 @@ +import { SingersWithPageResponseModel } from './SingersWithPageResponseModel'; + +// Simulated remote data - in a real app, this would come from an API +const ALL_SINGERS = [ + { + ID: 0, + Artist: 'Naomí Yepes', + Debut: 2011, + GrammyNominations: 6, + GrammyAwards: 0, + HasGrammyAward: false, + Tours: [ + { Tour: 'Faithful Tour', StartedOn: 'Sep 12', Location: 'Worldwide', Headliner: 'NO' }, + { Tour: 'City Jam Sessions', StartedOn: 'Aug 13', Location: 'North America', Headliner: 'YES' } + ], + Albums: [ + { + Album: 'Initiation', + LaunchDate: '2013-09-03', + BillboardReview: 86, + USBillboard200: 1, + Songs: [ + { Number: 1, Title: 'Lonely Falling', Released: '2019-05-01', Genre: 'Pop' }, + { Number: 2, Title: 'Bright Breaking', Released: '2019-07-25', Genre: 'Electropop' } + ] + } + ] + }, + { + ID: 1, + Artist: 'Babila Ebwélé', + Debut: 2009, + GrammyNominations: 0, + GrammyAwards: 11, + HasGrammyAward: true, + Tours: [ + { Tour: 'The BIG Tour', StartedOn: 'May 10', Location: 'Worldwide', Headliner: 'YES' } + ], + Albums: [ + { + Album: 'Fahrenheit', + LaunchDate: '2010-05-15', + BillboardReview: 92, + USBillboard200: 3, + Songs: [ + { Number: 1, Title: 'Heat Wave', Released: '2010-05-01', Genre: 'R&B' } + ] + } + ] + }, + { + ID: 2, + Artist: 'Chetana Kadarapu', + Debut: 2003, + GrammyNominations: 3, + GrammyAwards: 2, + HasGrammyAward: true, + Tours: [ + { Tour: 'Eastern Winds', StartedOn: 'Mar 04', Location: 'Asia', Headliner: 'YES' } + ], + Albums: [] + }, + { + ID: 3, + Artist: 'Xin Zhuang', + Debut: 2015, + GrammyNominations: 2, + GrammyAwards: 1, + HasGrammyAward: true, + Tours: [], + Albums: [ + { + Album: 'New Horizons', + LaunchDate: '2016-02-20', + BillboardReview: 88, + USBillboard200: 5, + Songs: [ + { Number: 1, Title: 'Dawn', Released: '2016-02-01', Genre: 'Electronic' } + ] + } + ] + }, + { + ID: 4, + Artist: 'Elena Volkova', + Debut: 2012, + GrammyNominations: 4, + GrammyAwards: 0, + HasGrammyAward: false, + Tours: [ + { Tour: 'Winter Sessions', StartedOn: 'Dec 13', Location: 'Europe', Headliner: 'NO' } + ], + Albums: [] + }, + { + ID: 5, + Artist: 'Marcus Chen', + Debut: 2008, + GrammyNominations: 8, + GrammyAwards: 3, + HasGrammyAward: true, + Tours: [ + { Tour: 'Global Sounds', StartedOn: 'Jun 09', Location: 'Worldwide', Headliner: 'YES' } + ], + Albums: [ + { + Album: 'Fusion', + LaunchDate: '2009-03-10', + BillboardReview: 90, + USBillboard200: 2, + Songs: [ + { Number: 1, Title: 'Blend', Released: '2009-03-01', Genre: 'Jazz Fusion' } + ] + } + ] + }, + { + ID: 6, + Artist: 'Sofia Martinez', + Debut: 2014, + GrammyNominations: 5, + GrammyAwards: 2, + HasGrammyAward: true, + Tours: [], + Albums: [] + }, + { + ID: 7, + Artist: 'James Wilson', + Debut: 2010, + GrammyNominations: 1, + GrammyAwards: 0, + HasGrammyAward: false, + Tours: [ + { Tour: 'Acoustic Sessions', StartedOn: 'Apr 11', Location: 'North America', Headliner: 'NO' } + ], + Albums: [] + }, + { + ID: 8, + Artist: 'Aisha Patel', + Debut: 2016, + GrammyNominations: 7, + GrammyAwards: 4, + HasGrammyAward: true, + Tours: [ + { Tour: 'Rhythm of Life', StartedOn: 'Jan 17', Location: 'Worldwide', Headliner: 'YES' } + ], + Albums: [ + { + Album: 'Essence', + LaunchDate: '2017-06-15', + BillboardReview: 95, + USBillboard200: 1, + Songs: [ + { Number: 1, Title: 'Soul', Released: '2017-06-01', Genre: 'Soul' }, + { Number: 2, Title: 'Spirit', Released: '2017-07-15', Genre: 'R&B' } + ] + } + ] + }, + { + ID: 9, + Artist: 'Oliver Brown', + Debut: 2007, + GrammyNominations: 2, + GrammyAwards: 1, + HasGrammyAward: true, + Tours: [], + Albums: [] + } +]; + +export class RemotePagingService { + private static delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + public static async getDataWithPaging(pageIndex: number = 0, pageSize: number = 10): Promise { + // Simulate network delay + await this.delay(300); + + const totalRecords = ALL_SINGERS.length; + const totalPages = Math.ceil(totalRecords / pageSize); + const startIndex = pageIndex * pageSize; + const endIndex = Math.min(startIndex + pageSize, totalRecords); + const items = ALL_SINGERS.slice(startIndex, endIndex); + + return { + items, + totalRecordsCount: totalRecords, + pageSize, + pageNumber: pageIndex, + totalPages + }; + } + + public static getTotalRecords(): number { + return ALL_SINGERS.length; + } +} diff --git a/samples/grids/hierarchical-grid/grid-batch-editing-remote/src/SingersWithPageResponseModel.ts b/samples/grids/hierarchical-grid/grid-batch-editing-remote/src/SingersWithPageResponseModel.ts new file mode 100644 index 0000000000..248df0825c --- /dev/null +++ b/samples/grids/hierarchical-grid/grid-batch-editing-remote/src/SingersWithPageResponseModel.ts @@ -0,0 +1,7 @@ +export interface SingersWithPageResponseModel { + items: any[]; + totalRecordsCount: number; + pageSize: number; + pageNumber: number; + totalPages: number; +} diff --git a/samples/grids/hierarchical-grid/grid-batch-editing-remote/src/index.css b/samples/grids/hierarchical-grid/grid-batch-editing-remote/src/index.css new file mode 100644 index 0000000000..17966fb590 --- /dev/null +++ b/samples/grids/hierarchical-grid/grid-batch-editing-remote/src/index.css @@ -0,0 +1,44 @@ +/* shared styles are loaded from: */ +/* https://dl.infragistics.com/x/css/samples/shared.v8.css */ + +.buttons-wrapper { + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 10px 0; +} + +.buttons-right { + display: flex; + gap: 8px; +} + +.dialog-buttons { + display: flex; + gap: 8px; + justify-content: flex-end; +} + +.transaction--add { + color: #6b3; + font-weight: 600; +} + +.transaction--update { + color: #4a71b9; + font-weight: 600; +} + +.transaction--delete { + color: #ee4920; + font-weight: 600; +} + +.error-banner { + background-color: #fee; + border: 1px solid #fcc; + border-radius: 4px; + color: #c00; + padding: 10px 15px; + margin-bottom: 10px; +} diff --git a/samples/grids/hierarchical-grid/grid-batch-editing-remote/src/index.tsx b/samples/grids/hierarchical-grid/grid-batch-editing-remote/src/index.tsx new file mode 100644 index 0000000000..0bdf017834 --- /dev/null +++ b/samples/grids/hierarchical-grid/grid-batch-editing-remote/src/index.tsx @@ -0,0 +1,314 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; + +import { IgrHierarchicalGrid, IgrPaginator, IgrColumn, IgrRowIsland, IgrGrid, IgrCellTemplateContext } from 'igniteui-react-grids'; +import { IgrButton, IgrDialog, IgrNumberEventArgs } from 'igniteui-react'; +import { RemotePagingService } from './RemotePagingService'; +import { SingersWithPageResponseModel } from './SingersWithPageResponseModel'; + +import 'igniteui-react-grids/grids/themes/light/bootstrap.css'; +import 'igniteui-webcomponents/themes/light/bootstrap.css'; + +interface SampleState { + undoDisabled: boolean; + redoDisabled: boolean; + commitDisabled: boolean; + discardDisabled: boolean; + transactionData: any[]; + data: any[]; + page: number; + perPage: number; + totalRecords: number; +} + +export default class Sample extends React.Component, SampleState> { + private grid: IgrHierarchicalGrid | null = null; + private paginator: IgrPaginator | null = null; + private dialog: IgrDialog | null = null; + private addId: number = 1000; + private stateUpdateSubscription: (() => void) | null = null; + private isUpdatingTransactionState: boolean = false; + + constructor(props: Record) { + super(props); + this.state = { + undoDisabled: true, + redoDisabled: true, + commitDisabled: true, + discardDisabled: true, + transactionData: [], + data: [], + page: 0, + perPage: 10, + totalRecords: 0 + }; + } + + public componentDidMount(): void { + this.loadGridData(this.state.page, this.state.perPage); + } + + public componentWillUnmount(): void { + if (this.stateUpdateSubscription) { + this.stateUpdateSubscription(); + } + } + + private loadGridData(pageIndex: number, pageSize: number): void { + if (this.grid) { + this.grid.isLoading = true; + } + + RemotePagingService.getDataWithPaging(pageIndex, pageSize) + .then((response: SingersWithPageResponseModel) => { + this.setState({ + data: response.items, + totalRecords: response.totalRecordsCount + }); + if (this.grid) { + this.grid.isLoading = false; + } + if (this.paginator) { + this.paginator.totalRecords = response.totalRecordsCount; + } + }) + .catch((error) => { + console.error(error.message); + this.setState({ data: [] }); + if (this.grid) { + this.grid.isLoading = false; + } + }); + } + + public render() { + const { undoDisabled, redoDisabled, commitDisabled, discardDisabled, transactionData, data, perPage } = this.state; + + return ( +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + Add Row + +
+ + Undo + + + Redo + + + Discard + + + Commit + +
+
+ + + + + + + +
+ + Commit + + + Discard + + + Cancel + +
+
+
+ ); + } + + private gridRef = (r: IgrHierarchicalGrid) => { + if (!r) { return; } + this.grid = r; + (this.grid as any).batchEditing = true; + + this.stateUpdateSubscription = this.grid.transactions.onStateUpdate.subscribe(() => { + if (!this.grid) { return; } + const hasChanges = this.grid.transactions.getAggregatedChanges(false).length > 0; + this.isUpdatingTransactionState = true; + this.setState({ + undoDisabled: !this.grid.transactions.canUndo, + redoDisabled: !this.grid.transactions.canRedo, + commitDisabled: !hasChanges, + discardDisabled: !hasChanges + }, () => { + this.isUpdatingTransactionState = false; + }); + }); + } + + private paginatorRef = (r: IgrPaginator) => { + this.paginator = r; + if (this.paginator && this.state.totalRecords > 0) { + this.paginator.totalRecords = this.state.totalRecords; + } + } + + private dialogRef = (r: IgrDialog) => { + this.dialog = r; + } + + private onPageChange = (args: IgrNumberEventArgs) => { + // Ignore page changes triggered by transaction state updates + if (this.isUpdatingTransactionState) { + return; + } + const newPage = args.detail; + // Only load data if page actually changed + if (newPage !== this.state.page) { + this.setState({ page: newPage }); + this.loadGridData(newPage, this.state.perPage); + } + } + + private onPerPageChange = (args: IgrNumberEventArgs) => { + const newPerPage = args.detail; + this.setState({ perPage: newPerPage, page: 0 }); + this.loadGridData(0, newPerPage); + } + + private randomInt(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1)) + min; + } + + public onAddRow = () => { + this.grid?.addRow({ + ID: this.addId++, + Artist: `New Artist ${this.randomInt(1, 100)}`, + Debut: this.randomInt(1990, 2025), + GrammyNominations: this.randomInt(0, 20), + GrammyAwards: this.randomInt(0, 10), + HasGrammyAward: this.randomInt(0, 1) === 1, + Albums: [], + Tours: [] + }); + } + + public onUndo = () => { + this.grid?.endEdit(true); + this.grid?.transactions.undo(); + } + + public onRedo = () => { + this.grid?.endEdit(true); + this.grid?.transactions.redo(); + } + + public onOpenCommitDialog = () => { + if (!this.grid) { return; } + this.setState({ + transactionData: this.grid.transactions.getAggregatedChanges(true) + }); + this.dialog?.show(); + } + + public onCommit = () => { + if (!this.grid) { return; } + this.grid.transactions.commit(this.grid.data); + this.dialog?.hide(); + } + + public onDiscard = () => { + this.grid?.transactions.clear(); + this.dialog?.hide(); + } + + public onCancel = () => { + this.dialog?.hide(); + } + + public deleteRowCellTemplate = (e: { dataContext: IgrCellTemplateContext }) => { + const id = e.dataContext.cell.id.rowID; + return ( + this.grid?.deleteRow(id)}> + Delete + + ); + } + + public typeColumnTemplate = (e: { dataContext: IgrCellTemplateContext }) => { + const type = e.dataContext.cell.value as string; + return {type.toUpperCase()}; + } + + public valueColumnTemplate = (e: { dataContext: IgrCellTemplateContext }) => { + return {JSON.stringify(e.dataContext.cell.value)}; + } +} + +// rendering above component in the React DOM +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render(); diff --git a/samples/grids/hierarchical-grid/grid-batch-editing-remote/tsconfig.json b/samples/grids/hierarchical-grid/grid-batch-editing-remote/tsconfig.json new file mode 100644 index 0000000000..8c0d146f95 --- /dev/null +++ b/samples/grids/hierarchical-grid/grid-batch-editing-remote/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/grids/hierarchical-grid/grid-batch-editing-remote/vite.config.js b/samples/grids/hierarchical-grid/grid-batch-editing-remote/vite.config.js new file mode 100644 index 0000000000..4fc593892f --- /dev/null +++ b/samples/grids/hierarchical-grid/grid-batch-editing-remote/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/grids/hierarchical-grid/grid-batch-editing/.devcontainer/devcontainer.json b/samples/grids/hierarchical-grid/grid-batch-editing/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..e0b8e9c925 --- /dev/null +++ b/samples/grids/hierarchical-grid/grid-batch-editing/.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/grids/hierarchical-grid/grid-batch-editing/.eslintrc.js b/samples/grids/hierarchical-grid/grid-batch-editing/.eslintrc.js new file mode 100644 index 0000000000..7168b71441 --- /dev/null +++ b/samples/grids/hierarchical-grid/grid-batch-editing/.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" + } + } + ] + }; \ No newline at end of file diff --git a/samples/grids/hierarchical-grid/grid-batch-editing/ReadMe.md b/samples/grids/hierarchical-grid/grid-batch-editing/ReadMe.md new file mode 100644 index 0000000000..96882387ba --- /dev/null +++ b/samples/grids/hierarchical-grid/grid-batch-editing/ReadMe.md @@ -0,0 +1,56 @@ + + + +This folder contains implementation of React application with example of Editing Events feature using [Hierarchical Grid](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/grids/hierarchical-grid/editing-events +``` + +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/grids/hierarchical-grid/grid-batch-editing/index.html b/samples/grids/hierarchical-grid/grid-batch-editing/index.html new file mode 100644 index 0000000000..7ccd41dace --- /dev/null +++ b/samples/grids/hierarchical-grid/grid-batch-editing/index.html @@ -0,0 +1,12 @@ + + + + Sample | Ignite UI | React | infragistics + + + + +
+ + + \ No newline at end of file diff --git a/samples/grids/hierarchical-grid/grid-batch-editing/package.json b/samples/grids/hierarchical-grid/grid-batch-editing/package.json new file mode 100644 index 0000000000..c2fc0d47eb --- /dev/null +++ b/samples/grids/hierarchical-grid/grid-batch-editing/package.json @@ -0,0 +1,49 @@ +{ + "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-dockmanager": "^1.17.0", + "igniteui-react": "^19.4.0", + "igniteui-react-core": "19.3.2", + "igniteui-react-grids": "^19.5.0-beta.0", + "igniteui-webcomponents": "^6.3.0", + "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/grids/hierarchical-grid/grid-batch-editing/sandbox.config.json b/samples/grids/hierarchical-grid/grid-batch-editing/sandbox.config.json new file mode 100644 index 0000000000..07f53508eb --- /dev/null +++ b/samples/grids/hierarchical-grid/grid-batch-editing/sandbox.config.json @@ -0,0 +1,5 @@ +{ + "infiniteLoopProtection": false, + "hardReloadOnChange": false, + "view": "browser" +} \ No newline at end of file diff --git a/samples/grids/hierarchical-grid/grid-batch-editing/src/NwindData.json b/samples/grids/hierarchical-grid/grid-batch-editing/src/NwindData.json new file mode 100644 index 0000000000..c00b03ec8d --- /dev/null +++ b/samples/grids/hierarchical-grid/grid-batch-editing/src/NwindData.json @@ -0,0 +1,458 @@ +[ + { + "ProductID": 1, + "ProductName": "Chai", + "SupplierID": 1, + "CategoryID": 1, + "QuantityPerUnit": "10 boxes x 20 bags", + "UnitPrice": 18, + "UnitsInStock": 39, + "UnitsOnOrder": 30, + "ReorderLevel": 10, + "Discontinued": false, + "OrderDate": "2012-02-12", + "Rating": 5, + "Locations": [ + { + "Shop": "Fun-Tasty Co.", + "LastInventory": "2018-06-12" + }, + { + "Shop": "Farmer Market", + "LastInventory": "2018-04-04" + } + ] + }, + { + "ProductID": 2, + "ProductName": "Chang", + "SupplierID": 1, + "CategoryID": 1, + "QuantityPerUnit": "24 - 12 oz bottles", + "UnitPrice": 19, + "UnitsInStock": 17, + "UnitsOnOrder": 40, + "ReorderLevel": 25, + "Discontinued": true, + "OrderDate": "2003-03-17", + "Rating": 5, + "Locations": [ + { + "Shop": "Super Market", + "LastInventory": "2018-09-09" + } + ] + }, + { + "ProductID": 3, + "ProductName": "Aniseed Syrup", + "SupplierID": 1, + "CategoryID": 2, + "QuantityPerUnit": "12 - 550 ml bottles", + "UnitPrice": 10, + "UnitsInStock": 13, + "UnitsOnOrder": 70, + "ReorderLevel": 25, + "Discontinued": false, + "OrderDate": "2006-03-17", + "Rating": 3, + "Locations": [ + { + "Shop": "Farmer Market", + "LastInventory": "2018-04-04" + }, + { + "Shop": "Street Market", + "LastInventory": "2018-12-12" + }, + { + "Shop": "24/7 Market", + "LastInventory": "2018-11-11" + } + ] + }, + { + "ProductID": 4, + "ProductName": "Chef Antons Cajun Seasoning", + "SupplierID": 2, + "CategoryID": 2, + "QuantityPerUnit": "48 - 6 oz jars", + "UnitPrice": 22, + "UnitsInStock": 53, + "UnitsOnOrder": 30, + "ReorderLevel": 0, + "Discontinued": false, + "OrderDate": "2016-03-17", + "Rating": 3, + "Locations": [ + { + "Shop": "Fun-Tasty Co.", + "LastInventory": "2018-06-12" + }, + { + "Shop": "Farmer Market", + "LastInventory": "2018-04-04" + }, + { + "Shop": "Street Market", + "LastInventory": "2018-12-12" + } + ] + }, + { + "ProductID": 5, + "ProductName": "Chef Antons Gumbo Mix", + "SupplierID": 2, + "CategoryID": 2, + "QuantityPerUnit": "36 boxes", + "UnitPrice": 21.35, + "UnitsInStock": 0, + "UnitsOnOrder": 30, + "ReorderLevel": 0, + "Discontinued": true, + "OrderDate": "2011-11-11", + "Rating": 5, + "Locations": [ + { + "Shop": "Super Market", + "LastInventory": "2018-09-09" + } + ] + }, + { + "ProductID": 6, + "ProductName": "Grandmas Boysenberry Spread", + "SupplierID": 3, + "CategoryID": 2, + "QuantityPerUnit": "12 - 8 oz jars", + "UnitPrice": 25, + "UnitsInStock": 0, + "UnitsOnOrder": 30, + "ReorderLevel": 25, + "Discontinued": false, + "OrderDate": "2017-12-17", + "Rating": 4, + "Locations": [ + { + "Shop": "Super Market", + "LastInventory": "2018-09-09" + } + ] + }, + { + "ProductID": 7, + "ProductName": "Uncle Bobs Organic Dried Pears", + "SupplierID": 3, + "CategoryID": 7, + "QuantityPerUnit": "12 - 1 lb pkgs.", + "UnitPrice": 30, + "UnitsInStock": 150, + "UnitsOnOrder": 30, + "ReorderLevel": 10, + "Discontinued": false, + "OrderDate": "2016-07-17", + "Rating": 5, + "Locations": [ + { + "Shop": "Fun-Tasty Co.", + "LastInventory": "2018-06-12" + }, + { + "Shop": "Farmer Market", + "LastInventory": "2018-04-04" + }, + { + "Shop": "Street Market", + "LastInventory": "2018-12-12" + } + ] + }, + { + "ProductID": 8, + "ProductName": "Northwoods Cranberry Sauce", + "SupplierID": 3, + "CategoryID": 2, + "QuantityPerUnit": "12 - 12 oz jars", + "UnitPrice": 40, + "UnitsInStock": 6, + "UnitsOnOrder": 30, + "ReorderLevel": 0, + "Discontinued": false, + "OrderDate": "2018-01-17", + "Rating": 4, + "Locations": [ + { + "Shop": "Fun-Tasty Co.", + "LastInventory": "2018-06-12" + }, + { + "Shop": "Farmer Market", + "LastInventory": "2018-04-04" + } + ] + }, + { + "ProductID": 9, + "ProductName": "Mishi Kobe Niku", + "SupplierID": 4, + "CategoryID": 6, + "QuantityPerUnit": "18 - 500 g pkgs.", + "UnitPrice": 97, + "UnitsInStock": 29, + "UnitsOnOrder": 30, + "ReorderLevel": 0, + "Discontinued": true, + "OrderDate": "2010-02-17", + "Rating": 4, + "Locations": [ + { + "Shop": "Farmer Market", + "LastInventory": "2018-04-04" + } + ] + }, + { + "ProductID": 10, + "ProductName": "Ikura", + "SupplierID": 4, + "CategoryID": 8, + "QuantityPerUnit": "12 - 200 ml jars", + "UnitPrice": 31, + "UnitsInStock": 31, + "UnitsOnOrder": 30, + "ReorderLevel": 0, + "Discontinued": false, + "OrderDate": "2008-05-17", + "Rating": 3, + "Locations": [ + { + "Shop": "Wall Market", + "LastInventory": "2018-12-06" + } + ] + }, + { + "ProductID": 11, + "ProductName": "Queso Cabrales", + "SupplierID": 5, + "CategoryID": 4, + "QuantityPerUnit": "1 kg pkg.", + "UnitPrice": 21, + "UnitsInStock": 22, + "UnitsOnOrder": 30, + "ReorderLevel": 30, + "Discontinued": false, + "OrderDate": "2009-01-17", + "Rating": 5, + "Locations": [ + { + "Shop": "Fun-Tasty Co.", + "LastInventory": "2018-06-12" + }, + { + "Shop": "Farmer Market", + "LastInventory": "2018-04-04" + } + ] + }, + { + "ProductID": 12, + "ProductName": "Queso Manchego La Pastora", + "SupplierID": 5, + "CategoryID": 4, + "QuantityPerUnit": "10 - 500 g pkgs.", + "UnitPrice": 38, + "UnitsInStock": 86, + "UnitsOnOrder": 30, + "ReorderLevel": 0, + "Discontinued": false, + "OrderDate": "2015-11-17", + "Rating": 3, + "Locations": [ + { + "Shop": "Farmer Market", + "LastInventory": "2018-04-04" + } + ] + }, + { + "ProductID": 13, + "ProductName": "Konbu", + "SupplierID": 6, + "CategoryID": 8, + "QuantityPerUnit": "2 kg box", + "UnitPrice": 6, + "UnitsInStock": 24, + "UnitsOnOrder": 30, + "ReorderLevel": 5, + "Discontinued": false, + "OrderDate": "2015-03-17", + "Rating": 2, + "Locations": [ + { + "Shop": "Super Market", + "LastInventory": "2018-09-09" + } + ] + }, + { + "ProductID": 14, + "ProductName": "Tofu", + "SupplierID": 6, + "CategoryID": 7, + "QuantityPerUnit": "40 - 100 g pkgs.", + "UnitPrice": 23.25, + "UnitsInStock": 35, + "UnitsOnOrder": 30, + "ReorderLevel": 0, + "Discontinued": false, + "OrderDate": "2017-06-17", + "Rating": 4, + "Locations": [ + { + "Shop": "Farmer Market", + "LastInventory": "2018-04-04" + }, + { + "Shop": "Street Market", + "LastInventory": "2018-12-12" + } + ] + }, + { + "ProductID": 15, + "ProductName": "Genen Shouyu", + "SupplierID": 6, + "CategoryID": 2, + "QuantityPerUnit": "24 - 250 ml bottles", + "UnitPrice": 15.5, + "UnitsInStock": 39, + "UnitsOnOrder": 30, + "ReorderLevel": 5, + "Discontinued": false, + "OrderDate": "2014-03-17", + "Rating": 4, + "Locations": [ + { + "Shop": "Local Market", + "LastInventory": "2018-07-03" + }, + { + "Shop": "Wall Market", + "LastInventory": "2018-12-06" + } + ] + }, + { + "ProductID": 16, + "ProductName": "Pavlova", + "SupplierID": 7, + "CategoryID": 3, + "QuantityPerUnit": "32 - 500 g boxes", + "UnitPrice": 17.45, + "UnitsInStock": 29, + "UnitsOnOrder": 30, + "ReorderLevel": 10, + "Discontinued": false, + "OrderDate": "2018-03-28", + "Rating": 2, + "Locations": [ + { + "Shop": "Farmer Market", + "LastInventory": "2018-04-04" + }, + { + "Shop": "Street Market", + "LastInventory": "2018-12-12" + }, + { + "Shop": "24/7 Market", + "LastInventory": "2018-11-11" + } + ] + }, + { + "ProductID": 17, + "ProductName": "Alice Mutton", + "SupplierID": 7, + "CategoryID": 6, + "QuantityPerUnit": "20 - 1 kg tins", + "UnitPrice": 39, + "UnitsInStock": 0, + "UnitsOnOrder": 30, + "ReorderLevel": 0, + "Discontinued": true, + "OrderDate": "2015-08-17", + "Rating": 2, + "Locations": [ + { + "Shop": "Farmer Market", + "LastInventory": "2018-04-04" + } + ] + }, + { + "ProductID": 18, + "ProductName": "Carnarvon Tigers", + "SupplierID": 7, + "CategoryID": 8, + "QuantityPerUnit": "16 kg pkg.", + "UnitPrice": 62.5, + "UnitsInStock": 42, + "UnitsOnOrder": 30, + "ReorderLevel": 0, + "Discontinued": false, + "OrderDate": "2005-09-27", + "Rating": 2, + "Locations": [ + { + "Shop": "24/7 Market", + "LastInventory": "2018-11-11" + }, + { + "Shop": "Super Market", + "LastInventory": "2018-09-09" + } + ] + }, + { + "ProductID": 19, + "ProductName": "Teatime Chocolate Biscuits", + "SupplierID": 8, + "CategoryID": 3, + "QuantityPerUnit": "", + "UnitPrice": 9.2, + "UnitsInStock": 25, + "UnitsOnOrder": 30, + "ReorderLevel": 5, + "Discontinued": false, + "OrderDate": "2001-03-17", + "Rating": 2, + "Locations": [ + { + "Shop": "Local Market", + "LastInventory": "2018-07-03" + } + ] + }, + { + "ProductID": 20, + "ProductName": "Sir Rodneys Marmalade", + "SupplierID": 8, + "CategoryID": 3, + "QuantityPerUnit": "4 - 100 ml jars", + "UnitPrice": 4.5, + "UnitsInStock": 40, + "UnitsOnOrder": 30, + "ReorderLevel": 0, + "Discontinued": false, + "OrderDate": "2005-03-17", + "Rating": 5, + "Locations": [ + { + "Shop": "Super Market", + "LastInventory": "2018-09-09" + } + ] + } +] \ No newline at end of file diff --git a/samples/grids/hierarchical-grid/grid-batch-editing/src/SingersData.json b/samples/grids/hierarchical-grid/grid-batch-editing/src/SingersData.json new file mode 100644 index 0000000000..b0c61e1499 --- /dev/null +++ b/samples/grids/hierarchical-grid/grid-batch-editing/src/SingersData.json @@ -0,0 +1,2480 @@ +[ + { + "ID": 0, + "Artist": "Naomí Yepes", + "Photo": "https://dl.infragistics.com/x/img/people/names/naomi.png", + "Debut": 2011, + "GrammyNominations": 6, + "GrammyAwards": 0, + "HasGrammyAward": false, + "Tours": [ + { + "Tour": "Faithful Tour", + "StartedOn": "Sep 12", + "Location": "Worldwide", + "Headliner": "NO", + "TouredBy": "Naomí Yepes" + }, + { + "Tour": "City Jam Sessions", + "StartedOn": "Aug 13", + "Location": "North America", + "Headliner": "YES", + "TouredBy": "Naomí Yepes" + }, + { + "Tour": "Christmas NYC 2013", + "StartedOn": "Dec 13", + "Location": "United States", + "Headliner": "NO", + "TouredBy": "Naomí Yepes" + }, + { + "Tour": "Christmas NYC 2014", + "StartedOn": "Dec 14", + "Location": "North America", + "Headliner": "NO", + "TouredBy": "Naomí Yepes" + }, + { + "Tour": "Watermelon Tour", + "StartedOn": "Feb 15", + "Location": "Worldwide", + "Headliner": "YES", + "TouredBy": "Naomí Yepes" + }, + { + "Tour": "Christmas NYC 2016", + "StartedOn": "Dec 16", + "Location": "United States", + "Headliner": "NO", + "TouredBy": "Naomí Yepes" + }, + { + "Tour": "The Dragon Tour", + "StartedOn": "Feb 17", + "Location": "Worldwide", + "Headliner": "NO", + "TouredBy": "Naomí Yepes" + }, + { + "Tour": "Organic Sessions", + "StartedOn": "Aug 18", + "Location": "United States, England", + "Headliner": "YES", + "TouredBy": "Naomí Yepes" + }, + { + "Tour": "Hope World Tour", + "StartedOn": "Mar 19", + "Location": "Worldwide", + "Headliner": "NO", + "TouredBy": "Naomí Yepes" + } + ], + "Albums": [ + { + "Album": "Initiation", + "LaunchDate": "2013-09-03", + "BillboardReview": 86, + "USBillboard200": 1, + "Artist": "Naomí Yepes", + "Songs": [ + { + "Number": 1, + "Title": "Lonely Falling", + "Released": "2019-05-01", + "Genre": "*", + "Album": "Initiation" + }, + { + "Number": 2, + "Title": "Bright Breaking", + "Released": "2019-07-25", + "Genre": "Electro house Electropop", + "Album": "Initiation" + }, + { + "Number": 3, + "Title": "River of Whisper", + "Released": "2020-01-29", + "Genre": "Electro house Electropop", + "Album": "Initiation" + }, + { + "Number": 4, + "Title": "Sky of Storm", + "Released": "2019-05-07", + "Genre": "Electro house Electropop", + "Album": "Initiation" + }, + { + "Number": 5, + "Title": "Electric River", + "Released": "2020-01-20", + "Genre": "R&B", + "Album": "Initiation" + }, + { + "Number": 6, + "Title": "Storm of Storm", + "Released": "2019-09-01", + "Genre": "Crunk reggaeton", + "Album": "Initiation" + }, + { + "Number": 7, + "Title": "Fire of Dream", + "Released": "2019-09-12", + "Genre": "Electro house Electropop", + "Album": "Initiation" + }, + { + "Number": 8, + "Title": "Burning in the Light", + "Released": "2019-04-15", + "Genre": "*", + "Album": "Initiation" + }, + { + "Number": 9, + "Title": "Burning in the Storm", + "Released": "2019-09-10", + "Genre": "Electro house Electropop", + "Album": "Initiation" + }, + { + "Number": 10, + "Title": "Shadow of Whisper", + "Released": "2019-01-06", + "Genre": "Crunk reggaeton", + "Album": "Initiation" + } + ] + }, + { + "Album": "Dream Driven", + "LaunchDate": "2014-08-25", + "BillboardReview": 81, + "USBillboard200": 1, + "Artist": "Naomí Yepes", + "Songs": [ + { + "Number": 1, + "Title": "Intro", + "Released": "2019-09-10", + "Genre": "*", + "Album": "Dream Driven" + }, + { + "Number": 2, + "Title": "Ferocious", + "Released": "2014-04-28", + "Genre": "Dance-pop R&B", + "Album": "Dream Driven" + }, + { + "Number": 3, + "Title": "Going crazy", + "Released": "2015-02-10", + "Genre": "Dance-pop EDM", + "Album": "Dream Driven" + }, + { + "Number": 4, + "Title": "Future past", + "Released": "2011-09-20", + "Genre": "*", + "Album": "Dream Driven" + }, + { + "Number": 5, + "Title": "Roaming like them", + "Released": "2014-07-02", + "Genre": "Electro house Electropop", + "Album": "Dream Driven" + }, + { + "Number": 6, + "Title": "Last Wishes", + "Released": "2014-08-12", + "Genre": "R&B", + "Album": "Dream Driven" + }, + { + "Number": 7, + "Title": "Stay where you are", + "Released": "2013-09-14", + "Genre": "*", + "Album": "Dream Driven" + }, + { + "Number": 8, + "Title": "Imaginarium", + "Released": "2021-09-20", + "Genre": "*", + "Album": "Dream Driven" + }, + { + "Number": 9, + "Title": "Tell me", + "Released": "2014-09-30", + "Genre": "Synth-pop R&B", + "Album": "Dream Driven" + }, + { + "Number": 10, + "Title": "Shredded into pieces", + "Released": "2015-09-11", + "Genre": "*", + "Album": "Dream Driven" + }, + { + "Number": 11, + "Title": "Capture this moment", + "Released": "2016-09-13", + "Genre": "*", + "Album": "Dream Driven" + }, + { + "Number": 12, + "Title": "Dream Driven", + "Released": "2014-09-14", + "Genre": "*", + "Album": "Dream Driven" + } + ] + }, + { + "Album": "The dragon journey", + "LaunchDate": "2016-05-20", + "BillboardReview": 60, + "USBillboard200": 2, + "Artist": "Naomí Yepes", + "Songs": [ + { + "Number": 1, + "Title": "Calling in the Storm", + "Released": "2019-03-12", + "Genre": "Electro house Electropop", + "Album": "The dragon journey" + }, + { + "Number": 2, + "Title": "Hiding in the Dream", + "Released": "2019-03-23", + "Genre": "R&B", + "Album": "The dragon journey" + }, + { + "Number": 3, + "Title": "Electric Heart", + "Released": "2019-03-17", + "Genre": "ethno-tunes", + "Album": "The dragon journey" + }, + { + "Number": 4, + "Title": "Shadow of Echo", + "Released": "2019-02-20", + "Genre": "ethno-tunes", + "Album": "The dragon journey" + }, + { + "Number": 5, + "Title": "Flying in the Storm", + "Released": "2019-04-08", + "Genre": "R&B", + "Album": "The dragon journey" + }, + { + "Number": 6, + "Title": "Dark Waiting", + "Released": "2019-10-20", + "Genre": "Synth-pop R&B", + "Album": "The dragon journey" + }, + { + "Number": 7, + "Title": "Fire of River", + "Released": "2019-02-20", + "Genre": "Synth-pop R&B", + "Album": "The dragon journey" + }, + { + "Number": 8, + "Title": "Wild Crying", + "Released": "2019-06-14", + "Genre": "R&B", + "Album": "The dragon journey" + }, + { + "Number": 9, + "Title": "Bright Dancing", + "Released": "2019-03-14", + "Genre": "Electro house Electropop", + "Album": "The dragon journey" + }, + { + "Number": 10, + "Title": "Golden Waiting", + "Released": "2019-09-12", + "Genre": "Synth-pop R&B", + "Album": "The dragon journey" + } + ] + }, + { + "Album": "Organic me", + "LaunchDate": "2018-08-17", + "BillboardReview": 82, + "USBillboard200": 1, + "Artist": "Naomí Yepes", + "Songs": [ + { + "Number": 1, + "Title": "I Love", + "Released": "2019-05-11", + "Genre": "Crunk reggaeton", + "Album": "Organic me" + }, + { + "Number": 2, + "Title": "Early Morning Compass", + "Released": "2020-01-15", + "Genre": "mystical parody-bap ", + "Album": "Organic me" + }, + { + "Number": 3, + "Title": "Key Fields Forever", + "Released": "2020-01-02", + "Genre": "Dance-pop EDM", + "Album": "Organic me" + }, + { + "Number": 4, + "Title": "Stand by Your Goblins", + "Released": "2019-11-20", + "Genre": "*", + "Album": "Organic me" + }, + { + "Number": 5, + "Title": "Mad to Walk", + "Released": "2019-05-12", + "Genre": "Electro house Electropop", + "Album": "Organic me" + }, + { + "Number": 6, + "Title": "Alice's Waiting", + "Released": "2020-01-28", + "Genre": "R&B", + "Album": "Organic me" + }, + { + "Number": 7, + "Title": "We Shall Kiss", + "Released": "2019-10-30", + "Genre": "*", + "Album": "Organic me" + }, + { + "Number": 8, + "Title": "Behind Single Ants", + "Released": "2019-10-02", + "Genre": "*", + "Album": "Organic me" + }, + { + "Number": 9, + "Title": "Soap Autopsy", + "Released": "2019-08-08", + "Genre": "Synth-pop R&B", + "Album": "Organic me" + }, + { + "Number": 10, + "Title": "Have You Met Rich?", + "Released": "2019-07-01", + "Genre": "ethno-tunes", + "Album": "Organic me" + }, + { + "Number": 11, + "Title": "Livin' on a Banana", + "Released": "2019-11-22", + "Genre": "Crunk reggaeton", + "Album": "Organic me" + } + ] + }, + { + "Album": "Curiosity", + "LaunchDate": "2019-12-07", + "BillboardReview": 75, + "USBillboard200": 12, + "Artist": "Naomí Yepes", + "Songs": [ + { + "Number": 1, + "Title": "Storm of Dream", + "Released": "2019-01-05", + "Genre": "*", + "Album": "Curiosity" + }, + { + "Number": 2, + "Title": "Echo of Dream", + "Released": "2019-01-28", + "Genre": "Synth-pop R&B", + "Album": "Curiosity" + }, + { + "Number": 3, + "Title": "Light of Shadow", + "Released": "2019-02-07", + "Genre": "Synth-pop R&B", + "Album": "Curiosity" + }, + { + "Number": 4, + "Title": "Storm of Heart", + "Released": "2020-01-05", + "Genre": "Electro house Electropop", + "Album": "Curiosity" + }, + { + "Number": 5, + "Title": "Shadow of River", + "Released": "2019-02-27", + "Genre": "*", + "Album": "Curiosity" + }, + { + "Number": 6, + "Title": "Wicked Dancing", + "Released": "2020-01-17", + "Genre": "ethno-tunes", + "Album": "Curiosity" + }, + { + "Number": 7, + "Title": "River of Light", + "Released": "2019-02-22", + "Genre": "R&B", + "Album": "Curiosity" + }, + { + "Number": 8, + "Title": "Lonely Breaking", + "Released": "2019-09-09", + "Genre": "ethno-tunes", + "Album": "Curiosity" + }, + { + "Number": 9, + "Title": "Furious Flying", + "Released": "2019-06-08", + "Genre": "R&B", + "Album": "Curiosity" + }, + { + "Number": 10, + "Title": "Hiding in the Storm", + "Released": "2019-05-27", + "Genre": "Electro house Electropop", + "Album": "Curiosity" + } + ] + } + ] + }, + { + "ID": 1, + "Artist": "Babila Ebwélé", + "Photo": "https://dl.infragistics.com/x/img/people/names/babila.png", + "Debut": 2009, + "GrammyNominations": 0, + "GrammyAwards": 11, + "HasGrammyAward": true, + "Tours": [ + { + "Tour": "The last straw", + "StartedOn": "May 09", + "Location": "Europe, Asia", + "Headliner": "NO", + "TouredBy": "Babila Ebwélé" + }, + { + "Tour": "No foundations", + "StartedOn": "Jun 04", + "Location": "United States, Europe", + "Headliner": "YES", + "TouredBy": "Babila Ebwélé" + }, + { + "Tour": "Crazy eyes", + "StartedOn": "Jun 08", + "Location": "North America", + "Headliner": "NO", + "TouredBy": "Babila Ebwélé" + }, + { + "Tour": "Zero gravity", + "StartedOn": "Apr 19", + "Location": "United States", + "Headliner": "NO", + "TouredBy": "Babila Ebwélé" + }, + { + "Tour": "Battle with myself", + "StartedOn": "Mar 08", + "Location": "North America", + "Headliner": "YES", + "TouredBy": "Babila Ebwélé" + } + ], + "Albums": [ + { + "Album": "Pushing up daisies", + "LaunchDate": "2000-05-31", + "BillboardReview": 86, + "USBillboard200": 42, + "Artist": "Babila Ebwélé", + "Songs": [ + { + "Number": 1, + "Title": "Wood Shavings Forever", + "Released": "2019-06-09", + "Genre": "*", + "Album": "Pushing up daisies" + }, + { + "Number": 2, + "Title": "Early Morning Drive", + "Released": "2019-05-20", + "Genre": "*", + "Album": "Pushing up daisies" + }, + { + "Number": 3, + "Title": "Don't Natter", + "Released": "2019-06-10", + "Genre": "adult calypso-industrial", + "Album": "Pushing up daisies" + }, + { + "Number": 4, + "Title": "Stairway to Balloons", + "Released": "2019-06-18", + "Genre": "calypso and mariachi", + "Album": "Pushing up daisies" + }, + { + "Number": 5, + "Title": "The Number of your Apple", + "Released": "2019-10-29", + "Genre": "*", + "Album": "Pushing up daisies" + }, + { + "Number": 6, + "Title": "Your Delightful Heart", + "Released": "2019-02-24", + "Genre": "*", + "Album": "Pushing up daisies" + }, + { + "Number": 7, + "Title": "Nice Weather For Balloons", + "Released": "2019-08-01", + "Genre": "rap-hop", + "Album": "Pushing up daisies" + }, + { + "Number": 8, + "Title": "The Girl From Cornwall", + "Released": "2019-05-04", + "Genre": "enigmatic rock-and-roll", + "Album": "Pushing up daisies" + }, + { + "Number": 9, + "Title": "Here Without Jack", + "Released": "2019-10-24", + "Genre": "*", + "Album": "Pushing up daisies" + }, + { + "Number": 10, + "Title": "Born Rancid", + "Released": "2019-03-19", + "Genre": "*", + "Album": "Pushing up daisies" + } + ] + }, + { + "Album": "Death's dead", + "LaunchDate": "2016-06-08", + "BillboardReview": 85, + "USBillboard200": 95, + "Artist": "Babila Ebwélé", + "Songs": [ + { + "Number": 1, + "Title": "Men Sound Better With You", + "Released": "2019-10-20", + "Genre": "rap-hop", + "Album": "Death's dead" + }, + { + "Number": 2, + "Title": "Ghost in My Rod", + "Released": "2019-10-05", + "Genre": "enigmatic rock-and-roll", + "Album": "Death's dead" + }, + { + "Number": 3, + "Title": "Bed of Men", + "Released": "2019-11-14", + "Genre": "whimsical comedy-grass ", + "Album": "Death's dead" + }, + { + "Number": 4, + "Title": "Don't Push", + "Released": "2020-01-02", + "Genre": "unblack electronic-trip-hop", + "Album": "Death's dead" + }, + { + "Number": 5, + "Title": "Nice Weather For Men", + "Released": "2019-12-18", + "Genre": "*", + "Album": "Death's dead" + }, + { + "Number": 6, + "Title": "Rancid Rhapsody", + "Released": "2019-03-10", + "Genre": "*", + "Album": "Death's dead" + }, + { + "Number": 7, + "Title": "Push, Push, Push!", + "Released": "2019-02-21", + "Genre": "*", + "Album": "Death's dead" + }, + { + "Number": 8, + "Title": "My Name is Sarah", + "Released": "2019-11-15", + "Genre": "*", + "Album": "Death's dead" + }, + { + "Number": 9, + "Title": "The Girl From My Hotel", + "Released": "2019-11-06", + "Genre": "*", + "Album": "Death's dead" + }, + { + "Number": 10, + "Title": "Free Box", + "Released": "2019-04-18", + "Genre": "splitter-funk", + "Album": "Death's dead" + }, + { + "Number": 11, + "Title": "Hotel Cardiff", + "Released": "2019-12-30", + "Genre": "guilty pleasure ebm", + "Album": "Death's dead" + } + ] + } + ] + }, + { + "ID": 2, + "Artist": "Ahmad Nazeri", + "Photo": "https://dl.infragistics.com/x/img/people/names/ahmad.png", + "Debut": 2004, + "GrammyNominations": 3, + "GrammyAwards": 1, + "HasGrammyAward": true, + "Tours": [], + "Albums": [ + { + "Album": "Emergency", + "LaunchDate": "2004-03-06", + "BillboardReview": 98, + "USBillboard200": 69, + "Artist": "Ahmad Nazeri", + "Songs": [ + { + "Number": 1, + "Title": "Gentle Falling", + "Released": "2019-04-26", + "Genre": "Crunk reggaeton", + "Album": "Emergency" + }, + { + "Number": 2, + "Title": "Calling in the Fire", + "Released": "2019-09-03", + "Genre": "ethno-tunes", + "Album": "Emergency" + }, + { + "Number": 3, + "Title": "Fire of Shadow", + "Released": "2019-01-05", + "Genre": "ethno-tunes", + "Album": "Emergency" + }, + { + "Number": 4, + "Title": "Dancing in the Dream", + "Released": "2019-04-15", + "Genre": "R&B", + "Album": "Emergency" + }, + { + "Number": 5, + "Title": "Calling in the Shadow", + "Released": "2019-10-09", + "Genre": "R&B", + "Album": "Emergency" + }, + { + "Number": 6, + "Title": "Falling in the Sky", + "Released": "2019-03-08", + "Genre": "ethno-tunes", + "Album": "Emergency" + }, + { + "Number": 7, + "Title": "Calling in the Storm", + "Released": "2019-12-05", + "Genre": "ethno-tunes", + "Album": "Emergency" + }, + { + "Number": 8, + "Title": "Falling in the River", + "Released": "2019-08-19", + "Genre": "Electro house Electropop", + "Album": "Emergency" + }, + { + "Number": 9, + "Title": "Electric Fire", + "Released": "2019-11-30", + "Genre": "Crunk reggaeton", + "Album": "Emergency" + }, + { + "Number": 10, + "Title": "Lonely River", + "Released": "2019-11-11", + "Genre": "Electro house Electropop", + "Album": "Emergency" + } + ] + }, + { + "Album": "Bursting bubbles", + "LaunchDate": "2006-04-17", + "BillboardReview": 69, + "USBillboard200": 39, + "Artist": "Ahmad Nazeri", + "Songs": [ + { + "Number": 1, + "Title": "Lonely Dream", + "Released": "2019-12-11", + "Genre": "ethno-tunes", + "Album": "Bursting bubbles" + }, + { + "Number": 2, + "Title": "Fire of River", + "Released": "2019-08-01", + "Genre": "Synth-pop R&B", + "Album": "Bursting bubbles" + }, + { + "Number": 3, + "Title": "Wicked Falling", + "Released": "2019-01-25", + "Genre": "*", + "Album": "Bursting bubbles" + }, + { + "Number": 4, + "Title": "Crying in the Shadow", + "Released": "2019-01-04", + "Genre": "Synth-pop R&B", + "Album": "Bursting bubbles" + }, + { + "Number": 5, + "Title": "Wild Burning", + "Released": "2019-05-10", + "Genre": "ethno-tunes", + "Album": "Bursting bubbles" + }, + { + "Number": 6, + "Title": "Waiting in the Heart", + "Released": "2019-08-07", + "Genre": "ethno-tunes", + "Album": "Bursting bubbles" + }, + { + "Number": 7, + "Title": "Fire of Fire", + "Released": "2019-05-16", + "Genre": "Electro house Electropop", + "Album": "Bursting bubbles" + }, + { + "Number": 8, + "Title": "Bright Heart", + "Released": "2019-03-14", + "Genre": "Synth-pop R&B", + "Album": "Bursting bubbles" + }, + { + "Number": 9, + "Title": "Lonely Fire", + "Released": "2019-10-15", + "Genre": "R&B", + "Album": "Bursting bubbles" + }, + { + "Number": 10, + "Title": "Sky of Dream", + "Released": "2019-06-20", + "Genre": "ethno-tunes", + "Album": "Bursting bubbles" + } + ] + } + ] + }, + { + "ID": 3, + "Artist": "Kimmy McIlmorie", + "Photo": "https://dl.infragistics.com/x/img/people/names/kimmy.png", + "Debut": 2007, + "GrammyNominations": 21, + "GrammyAwards": 3, + "HasGrammyAward": true, + "Tours": [], + "Albums": [ + { + "Album": "Here we go again", + "LaunchDate": "2017-11-18", + "BillboardReview": 68, + "USBillboard200": 1, + "Artist": "Kimmy McIlmorie", + "Songs": [ + { + "Number": 1, + "Title": "Storm of Sky", + "Released": "2019-03-04", + "Genre": "ethno-tunes", + "Album": "Here we go again" + }, + { + "Number": 2, + "Title": "Dream of Shadow", + "Released": "2019-01-03", + "Genre": "ethno-tunes", + "Album": "Here we go again" + }, + { + "Number": 3, + "Title": "Dream of Shadow", + "Released": "2019-12-19", + "Genre": "Electro house Electropop", + "Album": "Here we go again" + }, + { + "Number": 4, + "Title": "Golden Fire", + "Released": "2019-01-20", + "Genre": "R&B", + "Album": "Here we go again" + }, + { + "Number": 5, + "Title": "Running in the Light", + "Released": "2020-01-03", + "Genre": "Synth-pop R&B", + "Album": "Here we go again" + }, + { + "Number": 6, + "Title": "Flying in the Heart", + "Released": "2019-01-17", + "Genre": "*", + "Album": "Here we go again" + }, + { + "Number": 7, + "Title": "Fire of Storm", + "Released": "2019-01-26", + "Genre": "ethno-tunes", + "Album": "Here we go again" + }, + { + "Number": 8, + "Title": "Calling in the Sky", + "Released": "2019-10-28", + "Genre": "Synth-pop R&B", + "Album": "Here we go again" + }, + { + "Number": 9, + "Title": "Flying in the Shadow", + "Released": "2019-03-30", + "Genre": "ethno-tunes", + "Album": "Here we go again" + }, + { + "Number": 10, + "Title": "Golden Dancing", + "Released": "2019-10-12", + "Genre": "ethno-tunes", + "Album": "Here we go again" + } + ] + } + ] + }, + { + "ID": 4, + "Artist": "Mar Rueda", + "Photo": "https://dl.infragistics.com/x/img/people/names/mar.png", + "Debut": 1996, + "GrammyNominations": 14, + "GrammyAwards": 2, + "HasGrammyAward": true, + "Tours": [], + "Albums": [] + }, + { + "ID": 5, + "Artist": "Izabella Tabakova", + "Photo": "https://dl.infragistics.com/x/img/people/names/izabella.png", + "Debut": 2017, + "GrammyNominations": 7, + "GrammyAwards": 11, + "HasGrammyAward": true, + "Tours": [ + { + "Tour": "Final breath", + "StartedOn": "Jun 13", + "Location": "Europe", + "Headliner": "YES", + "TouredBy": "Izabella Tabakova" + }, + { + "Tour": "Once bitten", + "StartedOn": "Dec 18", + "Location": "Australia, United States", + "Headliner": "NO", + "TouredBy": "Izabella Tabakova" + }, + { + "Tour": "Code word", + "StartedOn": "Sep 19", + "Location": "United States, Europe", + "Headliner": "NO", + "TouredBy": "Izabella Tabakova" + }, + { + "Tour": "Final draft", + "StartedOn": "Sep 17", + "Location": "United States, Europe", + "Headliner": "YES", + "TouredBy": "Izabella Tabakova" + } + ], + "Albums": [ + { + "Album": "Once bitten", + "LaunchDate": "2007-07-16", + "BillboardReview": 79, + "USBillboard200": 53, + "Artist": "Izabella Tabakova", + "Songs": [ + { + "Number": 1, + "Title": "Whole Lotta Super Cats", + "Released": "2019-05-21", + "Genre": "*", + "Album": "Once bitten" + }, + { + "Number": 2, + "Title": "Enter Becky", + "Released": "2020-01-16", + "Genre": "*", + "Album": "Once bitten" + }, + { + "Number": 3, + "Title": "Your Cheatin' Flamingo", + "Released": "2020-01-14", + "Genre": "*", + "Album": "Once bitten" + }, + { + "Number": 4, + "Title": "Mad to Kiss", + "Released": "2019-11-06", + "Genre": "Synth-pop R&B", + "Album": "Once bitten" + }, + { + "Number": 5, + "Title": "Hotel Prague", + "Released": "2019-10-20", + "Genre": "ethno-tunes", + "Album": "Once bitten" + }, + { + "Number": 6, + "Title": "Jail on My Mind", + "Released": "2019-05-31", + "Genre": "Crunk reggaeton", + "Album": "Once bitten" + }, + { + "Number": 7, + "Title": "Amazing Blues", + "Released": "2019-05-29", + "Genre": "mystical parody-bap ", + "Album": "Once bitten" + }, + { + "Number": 8, + "Title": "Goody Two Iron Filings", + "Released": "2019-07-04", + "Genre": "Electro house Electropop", + "Album": "Once bitten" + }, + { + "Number": 9, + "Title": "I Love in Your Arms", + "Released": "2019-06-07", + "Genre": "R&B", + "Album": "Once bitten" + }, + { + "Number": 10, + "Title": "Truly Madly Amazing", + "Released": "2019-09-12", + "Genre": "ethno-tunes", + "Album": "Once bitten" + } + ] + }, + { + "Album": "Your graciousness", + "LaunchDate": "2004-11-17", + "BillboardReview": 69, + "USBillboard200": 30, + "Artist": "Izabella Tabakova", + "Songs": [ + { + "Number": 1, + "Title": "We Shall Tickle", + "Released": "2019-08-31", + "Genre": "old emo-garage ", + "Album": "Your graciousness" + }, + { + "Number": 2, + "Title": "Snail Boogie", + "Released": "2019-06-14", + "Genre": "*", + "Album": "Your graciousness" + }, + { + "Number": 3, + "Title": "Amazing Liz", + "Released": "2019-10-15", + "Genre": "*", + "Album": "Your graciousness" + }, + { + "Number": 4, + "Title": "When Sexy Aardvarks Cry", + "Released": "2019-10-01", + "Genre": "whimsical comedy-grass ", + "Album": "Your graciousness" + }, + { + "Number": 5, + "Title": "Stand By Dave", + "Released": "2019-08-18", + "Genre": "unblack electronic-trip-hop", + "Album": "Your graciousness" + }, + { + "Number": 6, + "Title": "The Golf Course is Your Land", + "Released": "2019-04-02", + "Genre": "*", + "Album": "Your graciousness" + }, + { + "Number": 7, + "Title": "Where Have All the Men Gone?", + "Released": "2019-04-29", + "Genre": "*", + "Album": "Your graciousness" + }, + { + "Number": 8, + "Title": "Rhythm of the Leg", + "Released": "2019-08-05", + "Genre": "ethno-tunes", + "Album": "Your graciousness" + }, + { + "Number": 9, + "Title": "Baby, I Need Your Hats", + "Released": "2019-12-05", + "Genre": "neuro-tunes", + "Album": "Your graciousness" + }, + { + "Number": 10, + "Title": "Stand by Your Cat", + "Released": "2019-07-25", + "Genre": "*", + "Album": "Your graciousness" + } + ] + }, + { + "Album": "Dark matters", + "LaunchDate": "2002-11-03", + "BillboardReview": 79, + "USBillboard200": 85, + "Artist": "Izabella Tabakova", + "Songs": [ + { + "Number": 1, + "Title": "Hiding in the Light", + "Released": "2019-01-24", + "Genre": "Synth-pop R&B", + "Album": "Dark matters" + }, + { + "Number": 2, + "Title": "Furious River", + "Released": "2020-01-13", + "Genre": "Electro house Electropop", + "Album": "Dark matters" + }, + { + "Number": 3, + "Title": "Wild Crying", + "Released": "2019-02-27", + "Genre": "Electro house Electropop", + "Album": "Dark matters" + }, + { + "Number": 4, + "Title": "Light of Dream", + "Released": "2019-06-01", + "Genre": "Crunk reggaeton", + "Album": "Dark matters" + }, + { + "Number": 5, + "Title": "Light of Dream", + "Released": "2019-08-24", + "Genre": "*", + "Album": "Dark matters" + }, + { + "Number": 6, + "Title": "Storm of Light", + "Released": "2019-02-26", + "Genre": "*", + "Album": "Dark matters" + }, + { + "Number": 7, + "Title": "Dark Storm", + "Released": "2020-01-18", + "Genre": "R&B", + "Album": "Dark matters" + }, + { + "Number": 8, + "Title": "Dark Calling", + "Released": "2019-03-20", + "Genre": "Crunk reggaeton", + "Album": "Dark matters" + }, + { + "Number": 9, + "Title": "Sky of Whisper", + "Released": "2019-01-30", + "Genre": "ethno-tunes", + "Album": "Dark matters" + }, + { + "Number": 10, + "Title": "Dancing in the Light", + "Released": "2019-11-28", + "Genre": "Synth-pop R&B", + "Album": "Dark matters" + } + ] + } + ] + }, + { + "ID": 6, + "Artist": "Nguyễn Diệp Chi", + "Photo": "https://dl.infragistics.com/x/img/people/names/nguyen.png", + "Debut": 1992, + "GrammyNominations": 4, + "GrammyAwards": 2, + "HasGrammyAward": true, + "Tours": [], + "Albums": [ + { + "Album": "Library of liberty", + "LaunchDate": "2003-12-22", + "BillboardReview": 93, + "USBillboard200": 5, + "Artist": "Nguyễn Diệp Chi", + "Songs": [ + { + "Number": 1, + "Title": "Echo of River", + "Released": "2019-03-05", + "Genre": "Synth-pop R&B", + "Album": "Library of liberty" + }, + { + "Number": 2, + "Title": "Heart of River", + "Released": "2020-01-12", + "Genre": "Electro house Electropop", + "Album": "Library of liberty" + }, + { + "Number": 3, + "Title": "Dark Light", + "Released": "2019-08-09", + "Genre": "Electro house Electropop", + "Album": "Library of liberty" + }, + { + "Number": 4, + "Title": "Dark Fire", + "Released": "2019-06-22", + "Genre": "R&B", + "Album": "Library of liberty" + }, + { + "Number": 5, + "Title": "Flying in the Fire", + "Released": "2019-07-22", + "Genre": "*", + "Album": "Library of liberty" + }, + { + "Number": 6, + "Title": "Shadow of Heart", + "Released": "2020-01-02", + "Genre": "*", + "Album": "Library of liberty" + }, + { + "Number": 7, + "Title": "Fire of Fire", + "Released": "2019-01-27", + "Genre": "*", + "Album": "Library of liberty" + }, + { + "Number": 8, + "Title": "Falling in the River", + "Released": "2019-08-05", + "Genre": "Crunk reggaeton", + "Album": "Library of liberty" + }, + { + "Number": 9, + "Title": "Fire of Light", + "Released": "2019-12-31", + "Genre": "ethno-tunes", + "Album": "Library of liberty" + }, + { + "Number": 10, + "Title": "Bright Flying", + "Released": "2019-01-24", + "Genre": "*", + "Album": "Library of liberty" + } + ] + } + ] + }, + { + "ID": 7, + "Artist": "Eva Lee", + "Photo": "https://dl.infragistics.com/x/img/people/names/eva.png", + "Debut": 2008, + "GrammyNominations": 2, + "GrammyAwards": 0, + "HasGrammyAward": false, + "Tours": [], + "Albums": [ + { + "Album": "Just a tease", + "LaunchDate": "2001-05-03", + "BillboardReview": 91, + "USBillboard200": 29, + "Artist": "Eva Lee", + "Songs": [ + { + "Number": 1, + "Title": "Dancing in the Shadow", + "Released": "2019-08-02", + "Genre": "ethno-tunes", + "Album": "Just a tease" + }, + { + "Number": 2, + "Title": "Silent Whisper", + "Released": "2019-07-09", + "Genre": "*", + "Album": "Just a tease" + }, + { + "Number": 3, + "Title": "Crying in the Whisper", + "Released": "2019-05-30", + "Genre": "Crunk reggaeton", + "Album": "Just a tease" + }, + { + "Number": 4, + "Title": "River of Light", + "Released": "2019-01-10", + "Genre": "Electro house Electropop", + "Album": "Just a tease" + }, + { + "Number": 5, + "Title": "Golden River", + "Released": "2019-11-15", + "Genre": "*", + "Album": "Just a tease" + }, + { + "Number": 6, + "Title": "Burning in the Shadow", + "Released": "2019-04-18", + "Genre": "Crunk reggaeton", + "Album": "Just a tease" + }, + { + "Number": 7, + "Title": "Shadow of Sky", + "Released": "2019-09-06", + "Genre": "Crunk reggaeton", + "Album": "Just a tease" + }, + { + "Number": 8, + "Title": "Gentle Waiting", + "Released": "2019-12-05", + "Genre": "R&B", + "Album": "Just a tease" + }, + { + "Number": 9, + "Title": "Bright River", + "Released": "2020-01-27", + "Genre": "R&B", + "Album": "Just a tease" + }, + { + "Number": 10, + "Title": "Heart of Storm", + "Released": "2019-01-07", + "Genre": "Synth-pop R&B", + "Album": "Just a tease" + } + ] + } + ] + }, + { + "ID": 8, + "Artist": "Siri Jakobsson", + "Photo": "https://dl.infragistics.com/x/img/people/names/siri.png", + "Debut": 1990, + "GrammyNominations": 2, + "GrammyAwards": 8, + "HasGrammyAward": true, + "Tours": [ + { + "Tour": "Basket case", + "StartedOn": "Jan 07", + "Location": "Europe, Asia", + "Headliner": "NO", + "TouredBy": "Siri Jakobsson" + }, + { + "Tour": "The bigger fish", + "StartedOn": "Dec 07", + "Location": "United States, Europe", + "Headliner": "YES", + "TouredBy": "Siri Jakobsson" + }, + { + "Tour": "Missed the boat", + "StartedOn": "Jun 09", + "Location": "Europe, Asia", + "Headliner": "NO", + "TouredBy": "Siri Jakobsson" + }, + { + "Tour": "Equivalent exchange", + "StartedOn": "Feb 06", + "Location": "United States, Europe", + "Headliner": "YES", + "TouredBy": "Siri Jakobsson" + }, + { + "Tour": "Damage control", + "StartedOn": "Oct 11", + "Location": "Australia, United States", + "Headliner": "NO", + "TouredBy": "Siri Jakobsson" + } + ], + "Albums": [ + { + "Album": "Under the bus", + "LaunchDate": "2000-05-14", + "BillboardReview": 67, + "USBillboard200": 67, + "Artist": "Siri Jakobsson", + "Songs": [ + { + "Number": 1, + "Title": "Jack Broke My Heart At Tesco's", + "Released": "2020-01-19", + "Genre": "*", + "Album": "Under the bus" + }, + { + "Number": 2, + "Title": "Cat Deep, Hats High", + "Released": "2019-12-05", + "Genre": "*", + "Album": "Under the bus" + }, + { + "Number": 3, + "Title": "In Snail We Trust", + "Released": "2019-05-31", + "Genre": "hardcore opera", + "Album": "Under the bus" + }, + { + "Number": 4, + "Title": "Liz's Waiting", + "Released": "2019-07-22", + "Genre": "emotional C-jam ", + "Album": "Under the bus" + }, + { + "Number": 5, + "Title": "Lifeless Blues", + "Released": "2019-06-14", + "Genre": "*", + "Album": "Under the bus" + }, + { + "Number": 6, + "Title": "I Spin", + "Released": "2019-03-26", + "Genre": "*", + "Album": "Under the bus" + }, + { + "Number": 7, + "Title": "Ring of Rock", + "Released": "2019-12-12", + "Genre": "*", + "Album": "Under the bus" + }, + { + "Number": 8, + "Title": "Livin' on a Rock", + "Released": "2019-04-17", + "Genre": "*", + "Album": "Under the bus" + }, + { + "Number": 9, + "Title": "Your Lifeless Heart", + "Released": "2019-09-15", + "Genre": "adult calypso-industrial", + "Album": "Under the bus" + }, + { + "Number": 10, + "Title": "The High Street on My Mind", + "Released": "2019-11-11", + "Genre": "calypso and mariachi", + "Album": "Under the bus" + }, + { + "Number": 11, + "Title": "Behind Ugly Curtains", + "Released": "2019-05-08", + "Genre": "*", + "Album": "Under the bus" + }, + { + "Number": 12, + "Title": "Where Have All the Curtains Gone?", + "Released": "2019-06-28", + "Genre": "*", + "Album": "Under the bus" + }, + { + "Number": 13, + "Title": "Ghost in My Apple", + "Released": "2019-12-14", + "Genre": "*", + "Album": "Under the bus" + }, + { + "Number": 14, + "Title": "I Chatter", + "Released": "2019-11-30", + "Genre": "*", + "Album": "Under the bus" + } + ] + } + ] + }, + { + "ID": 9, + "Artist": "Pablo Cambeiro", + "Photo": "https://dl.infragistics.com/x/img/people/names/pablo.png", + "Debut": 2011, + "GrammyNominations": 5, + "GrammyAwards": 0, + "HasGrammyAward": false, + "Tours": [ + { + "Tour": "Beads", + "StartedOn": "May 11", + "Location": "Worldwide", + "Headliner": "NO", + "TouredBy": "Pablo Cambeiro" + }, + { + "Tour": "Concept art", + "StartedOn": "Dec 18", + "Location": "United States", + "Headliner": "YES", + "TouredBy": "Pablo Cambeiro" + }, + { + "Tour": "Glass shoe", + "StartedOn": "Jan 20", + "Location": "Worldwide", + "Headliner": "YES", + "TouredBy": "Pablo Cambeiro" + }, + { + "Tour": "Pushing buttons", + "StartedOn": "Feb 15", + "Location": "Europe, Asia", + "Headliner": "NO", + "TouredBy": "Pablo Cambeiro" + }, + { + "Tour": "Dark matters", + "StartedOn": "Jan 04", + "Location": "Australia, United States", + "Headliner": "YES", + "TouredBy": "Pablo Cambeiro" + }, + { + "Tour": "Greener grass", + "StartedOn": "Sep 09", + "Location": "United States, Europe", + "Headliner": "NO", + "TouredBy": "Pablo Cambeiro" + }, + { + "Tour": "Apparatus", + "StartedOn": "Nov 16", + "Location": "Europe", + "Headliner": "NO", + "TouredBy": "Pablo Cambeiro" + } + ], + "Albums": [ + { + "Album": "Fluke", + "LaunchDate": "2017-08-04", + "BillboardReview": 93, + "USBillboard200": 98, + "Artist": "Pablo Cambeiro", + "Songs": [ + { + "Number": 1, + "Title": "Dancing in the Echo", + "Released": "2019-10-03", + "Genre": "Crunk reggaeton", + "Album": "Fluke" + }, + { + "Number": 2, + "Title": "Dream of Dream", + "Released": "2019-03-03", + "Genre": "*", + "Album": "Fluke" + }, + { + "Number": 3, + "Title": "Calling in the Echo", + "Released": "2019-09-16", + "Genre": "*", + "Album": "Fluke" + }, + { + "Number": 4, + "Title": "Light of Light", + "Released": "2019-05-25", + "Genre": "Electro house Electropop", + "Album": "Fluke" + }, + { + "Number": 5, + "Title": "Bright Light", + "Released": "2019-03-21", + "Genre": "R&B", + "Album": "Fluke" + }, + { + "Number": 6, + "Title": "Storm of Echo", + "Released": "2019-07-17", + "Genre": "Synth-pop R&B", + "Album": "Fluke" + }, + { + "Number": 7, + "Title": "Lonely Calling", + "Released": "2019-04-10", + "Genre": "ethno-tunes", + "Album": "Fluke" + }, + { + "Number": 8, + "Title": "Gentle Falling", + "Released": "2019-11-28", + "Genre": "Synth-pop R&B", + "Album": "Fluke" + }, + { + "Number": 9, + "Title": "Wild Flying", + "Released": "2019-11-26", + "Genre": "Crunk reggaeton", + "Album": "Fluke" + }, + { + "Number": 10, + "Title": "Sky of Dream", + "Released": "2019-05-29", + "Genre": "R&B", + "Album": "Fluke" + } + ] + }, + { + "Album": "Crowd control", + "LaunchDate": "2003-08-26", + "BillboardReview": 68, + "USBillboard200": 84, + "Artist": "Pablo Cambeiro", + "Songs": [ + { + "Number": 1, + "Title": "My Bed on My Mind", + "Released": "2019-03-25", + "Genre": "ethno-tunes", + "Album": "Crowd control" + }, + { + "Number": 2, + "Title": "Bright Blues", + "Released": "2019-09-28", + "Genre": "neuro-tunes", + "Album": "Crowd control" + }, + { + "Number": 3, + "Title": "Sail, Sail, Sail!", + "Released": "2019-03-05", + "Genre": "*", + "Album": "Crowd control" + }, + { + "Number": 4, + "Title": "Hotel My Bed", + "Released": "2019-03-22", + "Genre": "*", + "Album": "Crowd control" + }, + { + "Number": 5, + "Title": "Gonna Make You Mash", + "Released": "2019-05-18", + "Genre": "*", + "Album": "Crowd control" + }, + { + "Number": 6, + "Title": "Straight Outta America", + "Released": "2020-01-16", + "Genre": "hardcore opera", + "Album": "Crowd control" + }, + { + "Number": 7, + "Title": "I Drive", + "Released": "2019-02-23", + "Genre": "emotional C-jam ", + "Album": "Crowd control" + }, + { + "Number": 8, + "Title": "Like a Teddy", + "Released": "2019-08-31", + "Genre": "*", + "Album": "Crowd control" + }, + { + "Number": 9, + "Title": "Teddy Boogie", + "Released": "2019-11-30", + "Genre": "*", + "Album": "Crowd control" + } + ] + } + ] + }, + { + "ID": 10, + "Artist": "Athar Malakooti", + "Photo": "https://dl.infragistics.com/x/img/people/names/athar.png", + "Debut": 2017, + "GrammyNominations": 0, + "GrammyAwards": 0, + "HasGrammyAward": false, + "Tours": [], + "Albums": [ + { + "Album": "Pushing up daisies", + "LaunchDate": "2016-02-24", + "BillboardReview": 74, + "USBillboard200": 77, + "Artist": "Athar Malakooti", + "Songs": [ + { + "Number": 1, + "Title": "Hiding in the Whisper", + "Released": "2019-04-03", + "Genre": "R&B", + "Album": "Pushing up daisies" + }, + { + "Number": 2, + "Title": "Wicked Light", + "Released": "2019-08-21", + "Genre": "R&B", + "Album": "Pushing up daisies" + }, + { + "Number": 3, + "Title": "Flying in the River", + "Released": "2020-02-03", + "Genre": "Synth-pop R&B", + "Album": "Pushing up daisies" + }, + { + "Number": 4, + "Title": "Wicked Hiding", + "Released": "2019-09-15", + "Genre": "Synth-pop R&B", + "Album": "Pushing up daisies" + }, + { + "Number": 5, + "Title": "Lonely Light", + "Released": "2019-05-13", + "Genre": "Electro house Electropop", + "Album": "Pushing up daisies" + }, + { + "Number": 6, + "Title": "Bright Dancing", + "Released": "2019-04-10", + "Genre": "Synth-pop R&B", + "Album": "Pushing up daisies" + }, + { + "Number": 7, + "Title": "Gentle Dream", + "Released": "2019-05-21", + "Genre": "*", + "Album": "Pushing up daisies" + }, + { + "Number": 8, + "Title": "Sky of Echo", + "Released": "2019-06-09", + "Genre": "Synth-pop R&B", + "Album": "Pushing up daisies" + }, + { + "Number": 9, + "Title": "Breaking in the Sky", + "Released": "2019-12-27", + "Genre": "Crunk reggaeton", + "Album": "Pushing up daisies" + }, + { + "Number": 10, + "Title": "Whisper of Shadow", + "Released": "2019-01-04", + "Genre": "ethno-tunes", + "Album": "Pushing up daisies" + } + ] + } + ] + }, + { + "ID": 11, + "Artist": "Marti Valencia", + "Photo": "https://dl.infragistics.com/x/img/people/names/marti.png", + "Debut": 2004, + "GrammyNominations": 1, + "GrammyAwards": 1, + "HasGrammyAward": true, + "Tours": [ + { + "Tour": "Cat eat cat world", + "StartedOn": "Sep 00", + "Location": "Worldwide", + "Headliner": "YES", + "TouredBy": "Marti Valencia" + }, + { + "Tour": "Final straw", + "StartedOn": "Sep 06", + "Location": "United States, Europe", + "Headliner": "NO", + "TouredBy": "Marti Valencia" + } + ], + "Albums": [ + { + "Album": "Nemesis", + "LaunchDate": "2004-06-30", + "BillboardReview": 94, + "USBillboard200": 9, + "Artist": "Marti Valencia", + "Songs": [ + { + "Number": 1, + "Title": "Hiding in the Sky", + "Released": "2019-11-26", + "Genre": "Synth-pop R&B", + "Album": "Nemesis" + }, + { + "Number": 2, + "Title": "Waiting in the Echo", + "Released": "2019-07-10", + "Genre": "ethno-tunes", + "Album": "Nemesis" + }, + { + "Number": 3, + "Title": "Wicked Shadow", + "Released": "2019-07-29", + "Genre": "Synth-pop R&B", + "Album": "Nemesis" + }, + { + "Number": 4, + "Title": "Crying in the Whisper", + "Released": "2019-04-09", + "Genre": "*", + "Album": "Nemesis" + }, + { + "Number": 5, + "Title": "Echo of Storm", + "Released": "2019-11-19", + "Genre": "Crunk reggaeton", + "Album": "Nemesis" + }, + { + "Number": 6, + "Title": "Shadow of Sky", + "Released": "2019-07-24", + "Genre": "Crunk reggaeton", + "Album": "Nemesis" + }, + { + "Number": 7, + "Title": "Golden Hiding", + "Released": "2019-12-12", + "Genre": "Electro house Electropop", + "Album": "Nemesis" + }, + { + "Number": 8, + "Title": "Wild Dancing", + "Released": "2019-08-17", + "Genre": "Synth-pop R&B", + "Album": "Nemesis" + }, + { + "Number": 9, + "Title": "Bright Burning", + "Released": "2019-08-30", + "Genre": "Electro house Electropop", + "Album": "Nemesis" + }, + { + "Number": 10, + "Title": "Flying in the River", + "Released": "2019-09-02", + "Genre": "*", + "Album": "Nemesis" + } + ] + }, + { + "Album": "First chance", + "LaunchDate": "2019-01-07", + "BillboardReview": 96, + "USBillboard200": 19, + "Artist": "Marti Valencia", + "Songs": [ + { + "Number": 1, + "Title": "My Name is Jason", + "Released": "2019-07-12", + "Genre": "*", + "Album": "First chance" + }, + { + "Number": 2, + "Title": "Amazing Andy", + "Released": "2019-03-05", + "Genre": "*", + "Album": "First chance" + }, + { + "Number": 3, + "Title": "The Number of your Knight", + "Released": "2019-12-04", + "Genre": "*", + "Album": "First chance" + }, + { + "Number": 4, + "Title": "I Sail", + "Released": "2019-03-03", + "Genre": "*", + "Album": "First chance" + }, + { + "Number": 5, + "Title": "Goody Two Hands", + "Released": "2019-10-11", + "Genre": "Electro house Electropop", + "Album": "First chance" + }, + { + "Number": 6, + "Title": "Careful With That Knife", + "Released": "2019-12-18", + "Genre": "R&B", + "Album": "First chance" + }, + { + "Number": 7, + "Title": "Four Single Ants", + "Released": "2020-01-18", + "Genre": "*", + "Album": "First chance" + }, + { + "Number": 8, + "Title": "Kiss Forever", + "Released": "2019-08-10", + "Genre": "*", + "Album": "First chance" + }, + { + "Number": 9, + "Title": "Rich's Waiting", + "Released": "2019-03-15", + "Genre": "Synth-pop R&B", + "Album": "First chance" + }, + { + "Number": 10, + "Title": "Japan is Your Land", + "Released": "2019-03-07", + "Genre": "ethno-tunes", + "Album": "First chance" + }, + { + "Number": 11, + "Title": "Pencils in My Banana", + "Released": "2019-06-21", + "Genre": "Crunk reggaeton", + "Album": "First chance" + }, + { + "Number": 12, + "Title": "I Sail in Your Arms", + "Released": "2019-04-30", + "Genre": "Synth-pop R&B", + "Album": "First chance" + } + ] + }, + { + "Album": "God's advocate", + "LaunchDate": "2007-04-29", + "BillboardReview": 66, + "USBillboard200": 37, + "Artist": "Marti Valencia", + "Songs": [ + { + "Number": 1, + "Title": "Wild River", + "Released": "2019-01-11", + "Genre": "*", + "Album": "God's advocate" + }, + { + "Number": 2, + "Title": "Wicked Whisper", + "Released": "2019-02-16", + "Genre": "Electro house Electropop", + "Album": "God's advocate" + }, + { + "Number": 3, + "Title": "Storm of Heart", + "Released": "2019-08-11", + "Genre": "*", + "Album": "God's advocate" + }, + { + "Number": 4, + "Title": "Golden Dancing", + "Released": "2019-03-02", + "Genre": "Crunk reggaeton", + "Album": "God's advocate" + }, + { + "Number": 5, + "Title": "Calling in the Sky", + "Released": "2019-09-10", + "Genre": "Electro house Electropop", + "Album": "God's advocate" + }, + { + "Number": 6, + "Title": "Calling in the Heart", + "Released": "2019-01-12", + "Genre": "ethno-tunes", + "Album": "God's advocate" + }, + { + "Number": 7, + "Title": "Running in the Storm", + "Released": "2019-11-10", + "Genre": "Synth-pop R&B", + "Album": "God's advocate" + }, + { + "Number": 8, + "Title": "Wild Sky", + "Released": "2019-04-10", + "Genre": "R&B", + "Album": "God's advocate" + }, + { + "Number": 9, + "Title": "Crying in the Shadow", + "Released": "2019-03-02", + "Genre": "R&B", + "Album": "God's advocate" + }, + { + "Number": 10, + "Title": "Whisper of River", + "Released": "2019-05-12", + "Genre": "*", + "Album": "God's advocate" + } + ] + } + ] + }, + { + "ID": 12, + "Artist": "Alicia Stanger", + "Photo": "https://dl.infragistics.com/x/img/people/names/alicia.png", + "Debut": 2010, + "GrammyNominations": 1, + "GrammyAwards": 0, + "HasGrammyAward": false, + "Tours": [], + "Albums": [ + { + "Album": "Forever alone", + "LaunchDate": "2005-11-03", + "BillboardReview": 82, + "USBillboard200": 7, + "Artist": "Alicia Stanger", + "Songs": [ + { + "Number": 1, + "Title": "Shadow of Light", + "Released": "2019-03-24", + "Genre": "ethno-tunes", + "Album": "Forever alone" + }, + { + "Number": 2, + "Title": "Running in the Echo", + "Released": "2019-05-03", + "Genre": "Crunk reggaeton", + "Album": "Forever alone" + }, + { + "Number": 3, + "Title": "Gentle Dream", + "Released": "2019-08-24", + "Genre": "Crunk reggaeton", + "Album": "Forever alone" + }, + { + "Number": 4, + "Title": "Furious River", + "Released": "2019-04-24", + "Genre": "ethno-tunes", + "Album": "Forever alone" + }, + { + "Number": 5, + "Title": "Wild Whisper", + "Released": "2019-03-09", + "Genre": "ethno-tunes", + "Album": "Forever alone" + }, + { + "Number": 6, + "Title": "Whisper of Sky", + "Released": "2019-07-24", + "Genre": "Crunk reggaeton", + "Album": "Forever alone" + }, + { + "Number": 7, + "Title": "Lonely Storm", + "Released": "2019-05-01", + "Genre": "Crunk reggaeton", + "Album": "Forever alone" + }, + { + "Number": 8, + "Title": "Dancing in the River", + "Released": "2019-12-17", + "Genre": "*", + "Album": "Forever alone" + }, + { + "Number": 9, + "Title": "Electric Fire", + "Released": "2019-10-17", + "Genre": "Electro house Electropop", + "Album": "Forever alone" + }, + { + "Number": 10, + "Title": "Electric Sky", + "Released": "2019-09-25", + "Genre": "ethno-tunes", + "Album": "Forever alone" + } + ] + } + ] + }, + { + "ID": 13, + "Artist": "Peter Taylor", + "Photo": "https://dl.infragistics.com/x/img/people/names/peter.png", + "Debut": 2005, + "GrammyNominations": 0, + "GrammyAwards": 2, + "HasGrammyAward": true, + "Tours": [ + { + "Tour": "Love", + "StartedOn": "Jun 04", + "Location": "Europe, Asia", + "Headliner": "YES", + "TouredBy": "Peter Taylor" + }, + { + "Tour": "Fault of treasures", + "StartedOn": "Oct 13", + "Location": "North America", + "Headliner": "NO", + "TouredBy": "Peter Taylor" + }, + { + "Tour": "For eternity", + "StartedOn": "Mar 05", + "Location": "United States", + "Headliner": "YES", + "TouredBy": "Peter Taylor" + }, + { + "Tour": "Time flies", + "StartedOn": "Jun 03", + "Location": "North America", + "Headliner": "NO", + "TouredBy": "Peter Taylor" + }, + { + "Tour": "Highest difficulty", + "StartedOn": "Nov 01", + "Location": "Worldwide", + "Headliner": "YES", + "TouredBy": "Peter Taylor" + }, + { + "Tour": "Sleeping dogs", + "StartedOn": "May 04", + "Location": "United States, Europe", + "Headliner": "NO", + "TouredBy": "Peter Taylor" + } + ], + "Albums": [ + { + "Album": "Decisions decisions", + "LaunchDate": "2008-04-10", + "BillboardReview": 85, + "USBillboard200": 35, + "Artist": "Peter Taylor", + "Songs": [ + { + "Number": 1, + "Title": "Calling in the Dream", + "Released": "2019-08-01", + "Genre": "R&B", + "Album": "Decisions decisions" + }, + { + "Number": 2, + "Title": "Electric Burning", + "Released": "2019-09-10", + "Genre": "Electro house Electropop", + "Album": "Decisions decisions" + }, + { + "Number": 3, + "Title": "Dark Flying", + "Released": "2019-04-28", + "Genre": "*", + "Album": "Decisions decisions" + }, + { + "Number": 4, + "Title": "Gentle Sky", + "Released": "2019-11-20", + "Genre": "ethno-tunes", + "Album": "Decisions decisions" + }, + { + "Number": 5, + "Title": "Gentle Calling", + "Released": "2019-01-13", + "Genre": "Crunk reggaeton", + "Album": "Decisions decisions" + }, + { + "Number": 6, + "Title": "Golden Falling", + "Released": "2019-02-14", + "Genre": "Crunk reggaeton", + "Album": "Decisions decisions" + }, + { + "Number": 7, + "Title": "Silent River", + "Released": "2019-02-13", + "Genre": "R&B", + "Album": "Decisions decisions" + }, + { + "Number": 8, + "Title": "Furious Calling", + "Released": "2019-06-11", + "Genre": "Synth-pop R&B", + "Album": "Decisions decisions" + }, + { + "Number": 9, + "Title": "Running in the Echo", + "Released": "2019-11-06", + "Genre": "Electro house Electropop", + "Album": "Decisions decisions" + }, + { + "Number": 10, + "Title": "Furious River", + "Released": "2019-03-12", + "Genre": "*", + "Album": "Decisions decisions" + } + ] + }, + { + "Album": "Climate changed", + "LaunchDate": "2015-06-20", + "BillboardReview": 66, + "USBillboard200": 89, + "Artist": "Peter Taylor", + "Songs": [ + { + "Number": 1, + "Title": "Dark Crying", + "Released": "2019-04-27", + "Genre": "Electro house Electropop", + "Album": "Climate changed" + }, + { + "Number": 2, + "Title": "Dark Waiting", + "Released": "2019-11-14", + "Genre": "Synth-pop R&B", + "Album": "Climate changed" + }, + { + "Number": 3, + "Title": "Furious Waiting", + "Released": "2019-05-23", + "Genre": "*", + "Album": "Climate changed" + }, + { + "Number": 4, + "Title": "Running in the Echo", + "Released": "2019-11-29", + "Genre": "Crunk reggaeton", + "Album": "Climate changed" + }, + { + "Number": 5, + "Title": "Dream of Sky", + "Released": "2019-10-31", + "Genre": "Crunk reggaeton", + "Album": "Climate changed" + }, + { + "Number": 6, + "Title": "Hiding in the Heart", + "Released": "2019-08-09", + "Genre": "R&B", + "Album": "Climate changed" + }, + { + "Number": 7, + "Title": "Sky of Storm", + "Released": "2019-06-01", + "Genre": "R&B", + "Album": "Climate changed" + }, + { + "Number": 8, + "Title": "Light of Storm", + "Released": "2020-01-17", + "Genre": "ethno-tunes", + "Album": "Climate changed" + }, + { + "Number": 9, + "Title": "Light of Sky", + "Released": "2019-05-26", + "Genre": "*", + "Album": "Climate changed" + }, + { + "Number": 10, + "Title": "Golden River", + "Released": "2019-06-19", + "Genre": "*", + "Album": "Climate changed" + } + ] + } + ] + } +] \ No newline at end of file diff --git a/samples/grids/hierarchical-grid/grid-batch-editing/src/index.css b/samples/grids/hierarchical-grid/grid-batch-editing/src/index.css new file mode 100644 index 0000000000..7a4d6b21d7 --- /dev/null +++ b/samples/grids/hierarchical-grid/grid-batch-editing/src/index.css @@ -0,0 +1,35 @@ +/* shared styles are loaded from: */ +/* https://dl.infragistics.com/x/css/samples/shared.v8.css */ + +.buttons-wrapper { + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 10px 0; +} + +.buttons-right { + display: flex; + gap: 8px; +} + +.dialog-buttons { + display: flex; + gap: 8px; + justify-content: flex-end; +} + +.transaction--add { + color: #6b3; + font-weight: 600; +} + +.transaction--update { + color: #4a71b9; + font-weight: 600; +} + +.transaction--delete { + color: #ee4920; + font-weight: 600; +} diff --git a/samples/grids/hierarchical-grid/grid-batch-editing/src/index.tsx b/samples/grids/hierarchical-grid/grid-batch-editing/src/index.tsx new file mode 100644 index 0000000000..f8d6eb347d --- /dev/null +++ b/samples/grids/hierarchical-grid/grid-batch-editing/src/index.tsx @@ -0,0 +1,241 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; + +import { IgrHierarchicalGrid, IgrColumn, IgrRowIsland, IgrGrid, IgrCellTemplateContext } from 'igniteui-react-grids'; +import { IgrButton, IgrDialog } from 'igniteui-react'; +import SingersData from './SingersData.json'; + +import 'igniteui-react-grids/grids/themes/light/bootstrap.css'; +import 'igniteui-webcomponents/themes/light/bootstrap.css'; + +interface SampleState { + undoDisabled: boolean; + redoDisabled: boolean; + commitDisabled: boolean; + discardDisabled: boolean; + transactionData: any[]; +} + +export default class Sample extends React.Component, SampleState> { + private grid: IgrHierarchicalGrid | null = null; + private dialog: IgrDialog | null = null; + private addId: number = 1000; + + constructor(props: Record) { + super(props); + this.state = { + undoDisabled: true, + redoDisabled: true, + commitDisabled: true, + discardDisabled: true, + transactionData: [] + }; + } + + public componentWillUnmount(): void { + if (this.stateUpdateSubscription) { + this.stateUpdateSubscription(); + } + } + + private stateUpdateSubscription: (() => void) | null = null; + + public render() { + const { undoDisabled, redoDisabled, commitDisabled, discardDisabled, transactionData } = this.state; + + return ( +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + Add Row + +
+ + Undo + + + Redo + + + Discard + + + Commit + +
+
+ + + + + + + +
+ + Commit + + + Discard + + + Cancel + +
+
+
+ ); + } + + private _singersData: any[] = SingersData; + public get singersData(): any[] { + return this._singersData; + } + + private gridRef = (r: IgrHierarchicalGrid) => { + if (!r) { return; } + this.grid = r; + (this.grid as any).batchEditing = true; + + this.stateUpdateSubscription = this.grid.transactions.onStateUpdate.subscribe(() => { + if (!this.grid) { return; } + const hasChanges = this.grid.transactions.getAggregatedChanges(false).length > 0; + this.setState({ + undoDisabled: !this.grid.transactions.canUndo, + redoDisabled: !this.grid.transactions.canRedo, + commitDisabled: !hasChanges, + discardDisabled: !hasChanges + }); + }); + } + + private dialogRef = (r: IgrDialog) => { + this.dialog = r; + } + + private randomInt(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1)) + min; + } + + public onAddRow = () => { + this.grid?.addRow({ + ID: this.addId++, + Artist: `New Artist ${this.randomInt(1, 100)}`, + Debut: this.randomInt(1990, 2025), + GrammyNominations: this.randomInt(0, 20), + GrammyAwards: this.randomInt(0, 10), + HasGrammyAward: this.randomInt(0, 1) === 1, + Albums: [], + Tours: [] + }); + } + + public onUndo = () => { + this.grid?.endEdit(true); + this.grid?.transactions.undo(); + } + + public onRedo = () => { + this.grid?.endEdit(true); + this.grid?.transactions.redo(); + } + + public onOpenCommitDialog = () => { + if (!this.grid) { return; } + this.setState({ + transactionData: this.grid.transactions.getAggregatedChanges(true) + }); + this.dialog?.show(); + } + + public onCommit = () => { + if (!this.grid) { return; } + this.grid.transactions.commit(this.grid.data); + this.dialog?.hide(); + } + + public onDiscard = () => { + this.grid?.transactions.clear(); + this.dialog?.hide(); + } + + public onCancel = () => { + this.dialog?.hide(); + } + + public deleteRowCellTemplate = (e: { dataContext: IgrCellTemplateContext }) => { + const id = e.dataContext.cell.id.rowID; + return ( + this.grid?.deleteRow(id)}> + Delete + + ); + } + + public typeColumnTemplate = (e: { dataContext: IgrCellTemplateContext }) => { + const type = e.dataContext.cell.value as string; + return {type.toUpperCase()}; + } + + public valueColumnTemplate = (e: { dataContext: IgrCellTemplateContext }) => { + return {JSON.stringify(e.dataContext.cell.value)}; + } +} + +// rendering above component in the React DOM +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render(); diff --git a/samples/grids/hierarchical-grid/grid-batch-editing/tsconfig.json b/samples/grids/hierarchical-grid/grid-batch-editing/tsconfig.json new file mode 100644 index 0000000000..8c0d146f95 --- /dev/null +++ b/samples/grids/hierarchical-grid/grid-batch-editing/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/grids/hierarchical-grid/grid-batch-editing/vite.config.js b/samples/grids/hierarchical-grid/grid-batch-editing/vite.config.js new file mode 100644 index 0000000000..1744dbc719 --- /dev/null +++ b/samples/grids/hierarchical-grid/grid-batch-editing/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 + }, +}); \ No newline at end of file diff --git a/samples/grids/tree-grid/grid-batch-editing-remote/.devcontainer/devcontainer.json b/samples/grids/tree-grid/grid-batch-editing-remote/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..e0b8e9c925 --- /dev/null +++ b/samples/grids/tree-grid/grid-batch-editing-remote/.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/grids/tree-grid/grid-batch-editing-remote/.eslintrc.js b/samples/grids/tree-grid/grid-batch-editing-remote/.eslintrc.js new file mode 100644 index 0000000000..7168b71441 --- /dev/null +++ b/samples/grids/tree-grid/grid-batch-editing-remote/.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" + } + } + ] + }; \ No newline at end of file diff --git a/samples/grids/tree-grid/grid-batch-editing-remote/ReadMe.md b/samples/grids/tree-grid/grid-batch-editing-remote/ReadMe.md new file mode 100644 index 0000000000..31d6ecb845 --- /dev/null +++ b/samples/grids/tree-grid/grid-batch-editing-remote/ReadMe.md @@ -0,0 +1,56 @@ + + + +This folder contains implementation of React application with example of Editing Events feature using [Tree Grid](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/grids/tree-grid/editing-events +``` + +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/grids/tree-grid/grid-batch-editing-remote/index.html b/samples/grids/tree-grid/grid-batch-editing-remote/index.html new file mode 100644 index 0000000000..7ccd41dace --- /dev/null +++ b/samples/grids/tree-grid/grid-batch-editing-remote/index.html @@ -0,0 +1,12 @@ + + + + Sample | Ignite UI | React | infragistics + + + + +
+ + + \ No newline at end of file diff --git a/samples/grids/tree-grid/grid-batch-editing-remote/package.json b/samples/grids/tree-grid/grid-batch-editing-remote/package.json new file mode 100644 index 0000000000..c2fc0d47eb --- /dev/null +++ b/samples/grids/tree-grid/grid-batch-editing-remote/package.json @@ -0,0 +1,49 @@ +{ + "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-dockmanager": "^1.17.0", + "igniteui-react": "^19.4.0", + "igniteui-react-core": "19.3.2", + "igniteui-react-grids": "^19.5.0-beta.0", + "igniteui-webcomponents": "^6.3.0", + "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/grids/tree-grid/grid-batch-editing-remote/sandbox.config.json b/samples/grids/tree-grid/grid-batch-editing-remote/sandbox.config.json new file mode 100644 index 0000000000..07f53508eb --- /dev/null +++ b/samples/grids/tree-grid/grid-batch-editing-remote/sandbox.config.json @@ -0,0 +1,5 @@ +{ + "infiniteLoopProtection": false, + "hardReloadOnChange": false, + "view": "browser" +} \ No newline at end of file diff --git a/samples/grids/tree-grid/grid-batch-editing-remote/src/EmployeesWithPageResponseModel.ts b/samples/grids/tree-grid/grid-batch-editing-remote/src/EmployeesWithPageResponseModel.ts new file mode 100644 index 0000000000..522812133f --- /dev/null +++ b/samples/grids/tree-grid/grid-batch-editing-remote/src/EmployeesWithPageResponseModel.ts @@ -0,0 +1,7 @@ +export interface EmployeesWithPageResponseModel { + items: any[]; + totalRecordsCount: number; + pageSize: number; + pageNumber: number; + totalPages: number; +} diff --git a/samples/grids/tree-grid/grid-batch-editing-remote/src/RemotePagingService.ts b/samples/grids/tree-grid/grid-batch-editing-remote/src/RemotePagingService.ts new file mode 100644 index 0000000000..a660338334 --- /dev/null +++ b/samples/grids/tree-grid/grid-batch-editing-remote/src/RemotePagingService.ts @@ -0,0 +1,53 @@ +import { EmployeesNestedTreeDataItem } from './EmployeesNestedTreeData'; +import { EmployeesWithPageResponseModel } from './EmployeesWithPageResponseModel'; + +// Simulated remote data - in a real app, this would come from an API +const ALL_EMPLOYEES: EmployeesNestedTreeDataItem[] = [ + { Age: 55, HireDate: '2008-03-20', ID: 1, Name: 'Johnathan Winchester', Phone: '0251-031259', OnPTO: false, ParentID: -1, Title: 'Development Manager' }, + { Age: 42, HireDate: '2014-01-22', ID: 4, Name: 'Ana Sanders', Phone: '(21) 555-0091', OnPTO: true, ParentID: -1, Title: 'CEO' }, + { Age: 49, HireDate: '2014-01-22', ID: 18, Name: 'Victoria Lincoln', Phone: '(071) 23 67 22 20', OnPTO: true, ParentID: -1, Title: 'Accounting Manager' }, + { Age: 61, HireDate: '2010-01-01', ID: 10, Name: 'Yang Wang', Phone: '(21) 555-0091', OnPTO: false, ParentID: -1, Title: 'Localization Manager' }, + { Age: 43, HireDate: '2011-06-03', ID: 3, Name: 'Michael Burke', Phone: '0452-076545', OnPTO: true, ParentID: 1, Title: 'Senior Software Developer' }, + { Age: 29, HireDate: '2009-06-19', ID: 2, Name: 'Thomas Anderson', Phone: '(14) 555-8122', OnPTO: false, ParentID: 1, Title: 'Senior Software Developer' }, + { Age: 31, HireDate: '2014-08-18', ID: 11, Name: 'Monica Reyes', Phone: '7675-3425', OnPTO: false, ParentID: 1, Title: 'Software Development Team Lead' }, + { Age: 35, HireDate: '2015-09-17', ID: 6, Name: 'Roland Mendel', Phone: '(505) 555-5939', OnPTO: false, ParentID: 11, Title: 'Senior Software Developer' }, + { Age: 44, HireDate: '2009-10-11', ID: 12, Name: 'Sven Cooper', Phone: '0695-34 67 21', OnPTO: true, ParentID: 11, Title: 'Senior Software Developer' }, + { Age: 44, HireDate: '2014-04-04', ID: 14, Name: 'Laurence Johnson', Phone: '981-443655', OnPTO: false, ParentID: 4, Title: 'Director' }, + { Age: 25, HireDate: '2017-11-09', ID: 5, Name: 'Elizabeth Richards', Phone: '(2) 283-2951', OnPTO: true, ParentID: 4, Title: 'Vice President' }, + { Age: 39, HireDate: '2010-03-22', ID: 13, Name: 'Trevor Ashworth', Phone: '981-443655', OnPTO: true, ParentID: 5, Title: 'Director' }, + { Age: 44, HireDate: '2014-04-04', ID: 17, Name: 'Antonio Moreno', Phone: '(505) 555-5939', OnPTO: false, ParentID: 18, Title: 'Senior Accountant' }, + { Age: 50, HireDate: '2007-11-18', ID: 7, Name: 'Pedro Rodriguez', Phone: '035-640230', OnPTO: false, ParentID: 10, Title: 'Senior Localization Developer' }, + { Age: 27, HireDate: '2016-02-19', ID: 8, Name: 'Casey Harper', Phone: '0342-023176', OnPTO: true, ParentID: 10, Title: 'Senior Localization' }, + { Age: 25, HireDate: '2017-11-09', ID: 15, Name: 'Patricia Simpson', Phone: '069-0245984', OnPTO: false, ParentID: 7, Title: 'Localization Intern' }, + { Age: 39, HireDate: '2010-03-22', ID: 9, Name: 'Francisco Chang', Phone: '(91) 745 6200', OnPTO: false, ParentID: 7, Title: 'Localization Intern' }, + { Age: 25, HireDate: '2018-03-18', ID: 16, Name: 'Peter Lewis', Phone: '069-0245984', OnPTO: true, ParentID: 7, Title: 'Localization Intern' }, +]; + +export class RemotePagingService { + private static delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + public static async getDataWithPaging(pageIndex: number = 0, pageSize: number = 10): Promise { + // Simulate network delay + await this.delay(300); + + const totalRecords = ALL_EMPLOYEES.length; + const totalPages = Math.ceil(totalRecords / pageSize); + const startIndex = pageIndex * pageSize; + const endIndex = Math.min(startIndex + pageSize, totalRecords); + const items = ALL_EMPLOYEES.slice(startIndex, endIndex); + + return { + items, + totalRecordsCount: totalRecords, + pageSize, + pageNumber: pageIndex, + totalPages + }; + } + + public static getTotalRecords(): number { + return ALL_EMPLOYEES.length; + } +} diff --git a/samples/grids/tree-grid/grid-batch-editing-remote/src/index.css b/samples/grids/tree-grid/grid-batch-editing-remote/src/index.css new file mode 100644 index 0000000000..17966fb590 --- /dev/null +++ b/samples/grids/tree-grid/grid-batch-editing-remote/src/index.css @@ -0,0 +1,44 @@ +/* shared styles are loaded from: */ +/* https://dl.infragistics.com/x/css/samples/shared.v8.css */ + +.buttons-wrapper { + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 10px 0; +} + +.buttons-right { + display: flex; + gap: 8px; +} + +.dialog-buttons { + display: flex; + gap: 8px; + justify-content: flex-end; +} + +.transaction--add { + color: #6b3; + font-weight: 600; +} + +.transaction--update { + color: #4a71b9; + font-weight: 600; +} + +.transaction--delete { + color: #ee4920; + font-weight: 600; +} + +.error-banner { + background-color: #fee; + border: 1px solid #fcc; + border-radius: 4px; + color: #c00; + padding: 10px 15px; + margin-bottom: 10px; +} diff --git a/samples/grids/tree-grid/grid-batch-editing-remote/src/index.tsx b/samples/grids/tree-grid/grid-batch-editing-remote/src/index.tsx new file mode 100644 index 0000000000..1abeef309b --- /dev/null +++ b/samples/grids/tree-grid/grid-batch-editing-remote/src/index.tsx @@ -0,0 +1,289 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; + +import { IgrTreeGrid, IgrPaginator, IgrColumn, IgrGrid, IgrCellTemplateContext } from 'igniteui-react-grids'; +import { IgrButton, IgrDialog, IgrNumberEventArgs } from 'igniteui-react'; +import { RemotePagingService } from './RemotePagingService'; +import { EmployeesWithPageResponseModel } from './EmployeesWithPageResponseModel'; + +import 'igniteui-react-grids/grids/themes/light/bootstrap.css'; +import 'igniteui-webcomponents/themes/light/bootstrap.css'; + +interface SampleState { + undoDisabled: boolean; + redoDisabled: boolean; + commitDisabled: boolean; + discardDisabled: boolean; + transactionData: any[]; + data: any[]; + page: number; + perPage: number; + totalRecords: number; +} + +export default class Sample extends React.Component, SampleState> { + private grid: IgrTreeGrid | null = null; + private paginator: IgrPaginator | null = null; + private dialog: IgrDialog | null = null; + private addId: number = 1000; + private stateUpdateSubscription: (() => void) | null = null; + private isUpdatingTransactionState: boolean = false; + + constructor(props: Record) { + super(props); + this.state = { + undoDisabled: true, + redoDisabled: true, + commitDisabled: true, + discardDisabled: true, + transactionData: [], + data: [], + page: 0, + perPage: 10, + totalRecords: 0 + }; + } + + public componentDidMount(): void { + this.loadGridData(this.state.page, this.state.perPage); + } + + public componentWillUnmount(): void { + if (this.stateUpdateSubscription) { + this.stateUpdateSubscription(); + } + } + + private loadGridData(pageIndex: number, pageSize: number): void { + if (this.grid) { + this.grid.isLoading = true; + } + + RemotePagingService.getDataWithPaging(pageIndex, pageSize) + .then((response: EmployeesWithPageResponseModel) => { + this.setState({ + data: response.items, + totalRecords: response.totalRecordsCount + }); + if (this.grid) { + this.grid.isLoading = false; + } + if (this.paginator) { + this.paginator.totalRecords = response.totalRecordsCount; + } + }) + .catch((error) => { + console.error(error.message); + this.setState({ data: [] }); + if (this.grid) { + this.grid.isLoading = false; + } + }); + } + + public render() { + const { undoDisabled, redoDisabled, commitDisabled, discardDisabled, transactionData, data, perPage } = this.state; + + return ( +
+
+ + + + + + + + + + +
+
+ + Add Row + +
+ + Undo + + + Redo + + + Discard + + + Commit + +
+
+ + + + + + + +
+ + Commit + + + Discard + + + Cancel + +
+
+
+ ); + } + + private gridRef = (r: IgrTreeGrid) => { + if (!r) { return; } + this.grid = r; + (this.grid as any).batchEditing = true; + + this.stateUpdateSubscription = this.grid.transactions.onStateUpdate.subscribe(() => { + if (!this.grid) { return; } + const hasChanges = this.grid.transactions.getAggregatedChanges(false).length > 0; + this.isUpdatingTransactionState = true; + this.setState({ + undoDisabled: !this.grid.transactions.canUndo, + redoDisabled: !this.grid.transactions.canRedo, + commitDisabled: !hasChanges, + discardDisabled: !hasChanges + }, () => { + this.isUpdatingTransactionState = false; + }); + }); + } + + private paginatorRef = (r: IgrPaginator) => { + this.paginator = r; + if (this.paginator && this.state.totalRecords > 0) { + this.paginator.totalRecords = this.state.totalRecords; + } + } + + private dialogRef = (r: IgrDialog) => { + this.dialog = r; + } + + private onPageChange = (args: IgrNumberEventArgs) => { + // Ignore page changes triggered by transaction state updates + if (this.isUpdatingTransactionState) { + return; + } + const newPage = args.detail; + // Only load data if page actually changed + if (newPage !== this.state.page) { + this.setState({ page: newPage }); + this.loadGridData(newPage, this.state.perPage); + } + } + + private onPerPageChange = (args: IgrNumberEventArgs) => { + const newPerPage = args.detail; + this.setState({ perPage: newPerPage, page: 0 }); + this.loadGridData(0, newPerPage); + } + + private randomInt(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1)) + min; + } + + public onAddRow = () => { + this.grid?.addRow({ + ID: this.addId++, + ParentID: -1, + Name: `New Employee ${this.randomInt(1, 100)}`, + Title: 'Employee', + Age: this.randomInt(20, 60), + HireDate: new Date(this.randomInt(2000, 2025), + this.randomInt(0, 11), this.randomInt(1, 25)) + .toISOString().slice(0, 10), + Phone: `555-${this.randomInt(1000, 9999)}`, + OnPTO: this.randomInt(0, 1) === 1 + }); + } + + public onUndo = () => { + this.grid?.endEdit(true); + this.grid?.transactions.undo(); + } + + public onRedo = () => { + this.grid?.endEdit(true); + this.grid?.transactions.redo(); + } + + public onOpenCommitDialog = () => { + if (!this.grid) { return; } + this.setState({ + transactionData: this.grid.transactions.getAggregatedChanges(true) + }); + this.dialog?.show(); + } + + public onCommit = () => { + if (!this.grid) { return; } + this.grid.transactions.commit(this.grid.data); + this.dialog?.hide(); + } + + public onDiscard = () => { + this.grid?.transactions.clear(); + this.dialog?.hide(); + } + + public onCancel = () => { + this.dialog?.hide(); + } + + public deleteRowCellTemplate = (e: { dataContext: IgrCellTemplateContext }) => { + const id = e.dataContext.cell.id.rowID; + return ( + this.grid?.deleteRow(id)}> + Delete + + ); + } + + public typeColumnTemplate = (e: { dataContext: IgrCellTemplateContext }) => { + const type = e.dataContext.cell.value as string; + return {type.toUpperCase()}; + } + + public valueColumnTemplate = (e: { dataContext: IgrCellTemplateContext }) => { + return {JSON.stringify(e.dataContext.cell.value)}; + } +} + +// rendering above component in the React DOM +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render(); diff --git a/samples/grids/tree-grid/grid-batch-editing-remote/tsconfig.json b/samples/grids/tree-grid/grid-batch-editing-remote/tsconfig.json new file mode 100644 index 0000000000..8c0d146f95 --- /dev/null +++ b/samples/grids/tree-grid/grid-batch-editing-remote/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/grids/tree-grid/grid-batch-editing-remote/vite.config.js b/samples/grids/tree-grid/grid-batch-editing-remote/vite.config.js new file mode 100644 index 0000000000..1744dbc719 --- /dev/null +++ b/samples/grids/tree-grid/grid-batch-editing-remote/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 + }, +}); \ No newline at end of file diff --git a/samples/grids/tree-grid/grid-batch-editing/.devcontainer/devcontainer.json b/samples/grids/tree-grid/grid-batch-editing/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..e0b8e9c925 --- /dev/null +++ b/samples/grids/tree-grid/grid-batch-editing/.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/grids/tree-grid/grid-batch-editing/.eslintrc.js b/samples/grids/tree-grid/grid-batch-editing/.eslintrc.js new file mode 100644 index 0000000000..7168b71441 --- /dev/null +++ b/samples/grids/tree-grid/grid-batch-editing/.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" + } + } + ] + }; \ No newline at end of file diff --git a/samples/grids/tree-grid/grid-batch-editing/ReadMe.md b/samples/grids/tree-grid/grid-batch-editing/ReadMe.md new file mode 100644 index 0000000000..31d6ecb845 --- /dev/null +++ b/samples/grids/tree-grid/grid-batch-editing/ReadMe.md @@ -0,0 +1,56 @@ + + + +This folder contains implementation of React application with example of Editing Events feature using [Tree Grid](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/grids/tree-grid/editing-events +``` + +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/grids/tree-grid/grid-batch-editing/index.html b/samples/grids/tree-grid/grid-batch-editing/index.html new file mode 100644 index 0000000000..7ccd41dace --- /dev/null +++ b/samples/grids/tree-grid/grid-batch-editing/index.html @@ -0,0 +1,12 @@ + + + + Sample | Ignite UI | React | infragistics + + + + +
+ + + \ No newline at end of file diff --git a/samples/grids/tree-grid/grid-batch-editing/package.json b/samples/grids/tree-grid/grid-batch-editing/package.json new file mode 100644 index 0000000000..c2fc0d47eb --- /dev/null +++ b/samples/grids/tree-grid/grid-batch-editing/package.json @@ -0,0 +1,49 @@ +{ + "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-dockmanager": "^1.17.0", + "igniteui-react": "^19.4.0", + "igniteui-react-core": "19.3.2", + "igniteui-react-grids": "^19.5.0-beta.0", + "igniteui-webcomponents": "^6.3.0", + "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/grids/tree-grid/grid-batch-editing/sandbox.config.json b/samples/grids/tree-grid/grid-batch-editing/sandbox.config.json new file mode 100644 index 0000000000..07f53508eb --- /dev/null +++ b/samples/grids/tree-grid/grid-batch-editing/sandbox.config.json @@ -0,0 +1,5 @@ +{ + "infiniteLoopProtection": false, + "hardReloadOnChange": false, + "view": "browser" +} \ No newline at end of file diff --git a/samples/grids/tree-grid/grid-batch-editing/src/EmployeesNestedTreeData.ts b/samples/grids/tree-grid/grid-batch-editing/src/EmployeesNestedTreeData.ts new file mode 100644 index 0000000000..a51b020df3 --- /dev/null +++ b/samples/grids/tree-grid/grid-batch-editing/src/EmployeesNestedTreeData.ts @@ -0,0 +1,44 @@ +export class EmployeesNestedTreeDataItem { + public constructor(init: Partial) { + Object.assign(this, init); + } + + public Age: number; + public HireDate: string; + public ID: number; + public Name: string; + public Phone: string; + public OnPTO: boolean; + public ParentID: number; + public Title: string; + +} +export class EmployeesNestedTreeData extends Array { + public constructor(items: Array | number = -1) { + if (Array.isArray(items)) { + super(...items); + } else { + const newItems = [ + new EmployeesNestedTreeDataItem({ Age: 55, HireDate: `2008-03-20`, ID: 1, Name: `Johnathan Winchester`, Phone: `0251-031259`, OnPTO: false, ParentID: -1, Title: `Development Manager` }), + new EmployeesNestedTreeDataItem({ Age: 42, HireDate: `2014-01-22`, ID: 4, Name: `Ana Sanders`, Phone: `(21) 555-0091`, OnPTO: true, ParentID: -1, Title: `CEO` }), + new EmployeesNestedTreeDataItem({ Age: 49, HireDate: `2014-01-22`, ID: 18, Name: `Victoria Lincoln`, Phone: `(071) 23 67 22 20`, OnPTO: true, ParentID: -1, Title: `Accounting Manager` }), + new EmployeesNestedTreeDataItem({ Age: 61, HireDate: `2010-01-01`, ID: 10, Name: `Yang Wang`, Phone: `(21) 555-0091`, OnPTO: false, ParentID: -1, Title: `Localization Manager` }), + new EmployeesNestedTreeDataItem({ Age: 43, HireDate: `2011-06-03`, ID: 3, Name: `Michael Burke`, Phone: `0452-076545`, OnPTO: true, ParentID: 1, Title: `Senior Software Developer` }), + new EmployeesNestedTreeDataItem({ Age: 29, HireDate: `2009-06-19`, ID: 2, Name: `Thomas Anderson`, Phone: `(14) 555-8122`, OnPTO: false, ParentID: 1, Title: `Senior Software Developer` }), + new EmployeesNestedTreeDataItem({ Age: 31, HireDate: `2014-08-18`, ID: 11, Name: `Monica Reyes`, Phone: `7675-3425`, OnPTO: false, ParentID: 1, Title: `Software Development Team Lead` }), + new EmployeesNestedTreeDataItem({ Age: 35, HireDate: `2015-09-17`, ID: 6, Name: `Roland Mendel`, Phone: `(505) 555-5939`, OnPTO: false, ParentID: 11, Title: `Senior Software Developer` }), + new EmployeesNestedTreeDataItem({ Age: 44, HireDate: `2009-10-11`, ID: 12, Name: `Sven Cooper`, Phone: `0695-34 67 21`, OnPTO: true, ParentID: 11, Title: `Senior Software Developer` }), + new EmployeesNestedTreeDataItem({ Age: 44, HireDate: `2014-04-04`, ID: 14, Name: `Laurence Johnson`, Phone: `981-443655`, OnPTO: false, ParentID: 4, Title: `Director` }), + new EmployeesNestedTreeDataItem({ Age: 25, HireDate: `2017-11-09`, ID: 5, Name: `Elizabeth Richards`, Phone: `(2) 283-2951`, OnPTO: true, ParentID: 4, Title: `Vice President` }), + new EmployeesNestedTreeDataItem({ Age: 39, HireDate: `2010-03-22`, ID: 13, Name: `Trevor Ashworth`, Phone: `981-443655`, OnPTO: true, ParentID: 5, Title: `Director` }), + new EmployeesNestedTreeDataItem({ Age: 44, HireDate: `2014-04-04`, ID: 17, Name: `Antonio Moreno`, Phone: `(505) 555-5939`, OnPTO: false, ParentID: 18, Title: `Senior Accountant` }), + new EmployeesNestedTreeDataItem({ Age: 50, HireDate: `2007-11-18`, ID: 7, Name: `Pedro Rodriguez`, Phone: `035-640230`, OnPTO: false, ParentID: 10, Title: `Senior Localization Developer` }), + new EmployeesNestedTreeDataItem({ Age: 27, HireDate: `2016-02-19`, ID: 8, Name: `Casey Harper`, Phone: `0342-023176`, OnPTO: true, ParentID: 10, Title: `Senior Localization` }), + new EmployeesNestedTreeDataItem({ Age: 25, HireDate: `2017-11-09`, ID: 15, Name: `Patricia Simpson`, Phone: `069-0245984`, OnPTO: false, ParentID: 7, Title: `Localization Intern` }), + new EmployeesNestedTreeDataItem({ Age: 39, HireDate: `2010-03-22`, ID: 9, Name: `Francisco Chang`, Phone: `(91) 745 6200`, OnPTO: false, ParentID: 7, Title: `Localization Intern` }), + new EmployeesNestedTreeDataItem({ Age: 25, HireDate: `2018-03-18`, ID: 16, Name: `Peter Lewis`, Phone: `069-0245984`, OnPTO: true, ParentID: 7, Title: `Localization Intern` }), + ]; + super(...newItems.slice(0)); + } + } +} diff --git a/samples/grids/tree-grid/grid-batch-editing/src/index.css b/samples/grids/tree-grid/grid-batch-editing/src/index.css new file mode 100644 index 0000000000..7a4d6b21d7 --- /dev/null +++ b/samples/grids/tree-grid/grid-batch-editing/src/index.css @@ -0,0 +1,35 @@ +/* shared styles are loaded from: */ +/* https://dl.infragistics.com/x/css/samples/shared.v8.css */ + +.buttons-wrapper { + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 10px 0; +} + +.buttons-right { + display: flex; + gap: 8px; +} + +.dialog-buttons { + display: flex; + gap: 8px; + justify-content: flex-end; +} + +.transaction--add { + color: #6b3; + font-weight: 600; +} + +.transaction--update { + color: #4a71b9; + font-weight: 600; +} + +.transaction--delete { + color: #ee4920; + font-weight: 600; +} diff --git a/samples/grids/tree-grid/grid-batch-editing/src/index.tsx b/samples/grids/tree-grid/grid-batch-editing/src/index.tsx new file mode 100644 index 0000000000..7b4fcad93c --- /dev/null +++ b/samples/grids/tree-grid/grid-batch-editing/src/index.tsx @@ -0,0 +1,218 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; + +import { IgrTreeGrid, IgrColumn, IgrGrid, IgrCellTemplateContext } from 'igniteui-react-grids'; +import { IgrButton, IgrDialog } from 'igniteui-react'; +import { EmployeesNestedTreeData } from './EmployeesNestedTreeData'; + +import 'igniteui-react-grids/grids/themes/light/bootstrap.css'; +import 'igniteui-webcomponents/themes/light/bootstrap.css'; + +interface SampleState { + undoDisabled: boolean; + redoDisabled: boolean; + commitDisabled: boolean; + discardDisabled: boolean; + transactionData: any[]; +} + +export default class Sample extends React.Component, SampleState> { + private grid: IgrTreeGrid | null = null; + private dialog: IgrDialog | null = null; + private addId: number = 1000; + private stateUpdateSubscription: (() => void) | null = null; + + constructor(props: Record) { + super(props); + this.state = { + undoDisabled: true, + redoDisabled: true, + commitDisabled: true, + discardDisabled: true, + transactionData: [] + }; + } + + public componentWillUnmount(): void { + if (this.stateUpdateSubscription) { + this.stateUpdateSubscription(); + } + } + + public render() { + const { undoDisabled, redoDisabled, commitDisabled, discardDisabled, transactionData } = this.state; + + return ( +
+
+ + + + + + + + + +
+
+ + Add Row + +
+ + Undo + + + Redo + + + Discard + + + Commit + +
+
+ + + + + + + +
+ + Commit + + + Discard + + + Cancel + +
+
+
+ ); + } + + private _employeesData: EmployeesNestedTreeData | null = null; + public get employeesData(): EmployeesNestedTreeData { + if (this._employeesData == null) { + this._employeesData = new EmployeesNestedTreeData(); + } + return this._employeesData; + } + + private gridRef = (r: IgrTreeGrid) => { + if (!r) { return; } + this.grid = r; + (this.grid as any).batchEditing = true; + + this.stateUpdateSubscription = this.grid.transactions.onStateUpdate.subscribe(() => { + if (!this.grid) { return; } + const hasChanges = this.grid.transactions.getAggregatedChanges(false).length > 0; + this.setState({ + undoDisabled: !this.grid.transactions.canUndo, + redoDisabled: !this.grid.transactions.canRedo, + commitDisabled: !hasChanges, + discardDisabled: !hasChanges + }); + }); + } + + private dialogRef = (r: IgrDialog) => { + this.dialog = r; + } + + private randomInt(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1)) + min; + } + + public onAddRow = () => { + this.grid?.addRow({ + ID: this.addId++, + ParentID: -1, + Name: `New Employee ${this.randomInt(1, 100)}`, + Title: 'Employee', + Age: this.randomInt(20, 60), + HireDate: new Date(this.randomInt(2000, 2025), + this.randomInt(0, 11), this.randomInt(1, 25)) + .toISOString().slice(0, 10), + Phone: `555-${this.randomInt(1000, 9999)}`, + OnPTO: this.randomInt(0, 1) === 1 + }); + } + + public onUndo = () => { + this.grid?.endEdit(true); + this.grid?.transactions.undo(); + } + + public onRedo = () => { + this.grid?.endEdit(true); + this.grid?.transactions.redo(); + } + + public onOpenCommitDialog = () => { + if (!this.grid) { return; } + this.setState({ + transactionData: this.grid.transactions.getAggregatedChanges(true) + }); + this.dialog?.show(); + } + + public onCommit = () => { + if (!this.grid) { return; } + this.grid.transactions.commit(this.grid.data); + this.dialog?.hide(); + } + + public onDiscard = () => { + this.grid?.transactions.clear(); + this.dialog?.hide(); + } + + public onCancel = () => { + this.dialog?.hide(); + } + + public deleteRowCellTemplate = (e: { dataContext: IgrCellTemplateContext }) => { + const id = e.dataContext.cell.id.rowID; + return ( + this.grid?.deleteRow(id)}> + Delete + + ); + } + + public typeColumnTemplate = (e: { dataContext: IgrCellTemplateContext }) => { + const type = e.dataContext.cell.value as string; + return {type.toUpperCase()}; + } + + public valueColumnTemplate = (e: { dataContext: IgrCellTemplateContext }) => { + return {JSON.stringify(e.dataContext.cell.value)}; + } +} + +// rendering above component in the React DOM +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render(); diff --git a/samples/grids/tree-grid/grid-batch-editing/tsconfig.json b/samples/grids/tree-grid/grid-batch-editing/tsconfig.json new file mode 100644 index 0000000000..8c0d146f95 --- /dev/null +++ b/samples/grids/tree-grid/grid-batch-editing/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/grids/tree-grid/grid-batch-editing/vite.config.js b/samples/grids/tree-grid/grid-batch-editing/vite.config.js new file mode 100644 index 0000000000..1744dbc719 --- /dev/null +++ b/samples/grids/tree-grid/grid-batch-editing/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 + }, +}); \ No newline at end of file