diff --git a/CHANGELOG.md b/CHANGELOG.md index aae9321f803..b7f8bd913f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,51 @@ All notable changes for each version of this project will be documented in this ## 21.0.0 +### New Features + +- `IgxGrid`, `IgxTreeGrid`, `IgxHierarchicalGrid` + - Added PDF export functionality to grid components. Grids can now be exported to PDF format alongside the existing Excel and CSV export options. + + The new `IgxPdfExporterService` follows the same pattern as Excel and CSV exporters: + + ```ts + import { IgxPdfExporterService, IgxPdfExporterOptions } from 'igniteui-angular'; + + constructor(private pdfExporter: IgxPdfExporterService) {} + + exportToPdf() { + const options = new IgxPdfExporterOptions('MyGridExport'); + options.pageOrientation = 'landscape'; // 'portrait' or 'landscape' (default: 'landscape') + options.pageSize = 'a4'; // 'a3', 'a4', 'a5', 'letter', 'legal', etc. + options.fontSize = 10; + options.showTableBorders = true; + + this.pdfExporter.export(this.grid, options); + } + ``` + + The grid toolbar exporter component now includes a PDF export button: + + ```html + + + + + ``` + + Key features: + - **Multi-page support** with automatic page breaks + - **Hierarchical visualization** for TreeGrid (with indentation) and HierarchicalGrid (with child tables) + - **Multi-level column headers** (column groups) support + - **Summary rows** with proper value formatting + - **Text truncation** with ellipsis for long content + - **Landscape orientation** by default (suitable for wide grids) + - **Internationalization** support for all 19 supported languages + - Respects all grid export options (ignoreFiltering, ignoreSorting, ignoreColumnsVisibility, etc.) + ### Breaking Changes #### Multiple Entry Points Support diff --git a/angular.json b/angular.json index e71f9111dd3..2734a531866 100644 --- a/angular.json +++ b/angular.json @@ -305,8 +305,8 @@ "budgets": [ { "type": "allScript", - "maximumWarning": "2.1mb", - "maximumError": "2.5mb" + "maximumWarning": "2.5mb", + "maximumError": "3mb" }, { "type": "bundle", diff --git a/package-lock.json b/package-lock.json index 91a2133ab19..fd1ab7cc89b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "fflate": "^0.8.1", "igniteui-theming": "^24.0.0", "igniteui-trial-watermark": "^3.1.0", + "jspdf": "^3.0.4", "lodash-es": "^4.17.21", "rxjs": "^7.8.2", "tslib": "^2.3.0", @@ -693,7 +694,6 @@ "integrity": "sha512-CVskZnF38IIxVVlKWi1VCz7YH/gHMJu2IY9bD1AVoBBGIe0xA4FRXJkW2Y+EDs9vQqZTkZZljhK5gL65Ro1PeQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@angular-eslint/bundled-angular-compiler": "20.7.0", "eslint-scope": "^9.0.0" @@ -723,7 +723,6 @@ "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.0.0.tgz", "integrity": "sha512-9AX4HFJmSP8SFNiweKNxasBzn3zbL3xRtwaUxw1I+x/WAzubm4ZziLnXqb+tai7C4UmwV+9XDlRVPfw5WxJ9zg==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -907,7 +906,6 @@ "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.0.0.tgz", "integrity": "sha512-uFvQDYU5X5nEnI9C4Bkdxcu4aIzNesGLJzmFlnwChVxB4BxIRF0uHL0oRhdkInGTIzPDJPH4nF6B/22c5gDVqA==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -924,7 +922,6 @@ "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.0.0.tgz", "integrity": "sha512-6jCH3UYga5iokj5F40SR4dlwo9ZRMkT8YzHCTijwZuDX9zvugp9jPof092RvIeNsTvCMVfGWuM9yZ1DRUsU/yg==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -938,7 +935,6 @@ "integrity": "sha512-KTXp+e2UPGyfFew6Wq95ULpHWQ20dhqkAMZ6x6MCYfOe2ccdnGYsAbLLmnWGmSg5BaOI4B0x/1XCFZf/n6WDgA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "7.28.4", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -971,7 +967,6 @@ "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.0.0.tgz", "integrity": "sha512-bqi8fT4csyITeX8vdN5FJDBWx5wuWzdCg4mKSjHd+onVzZLyZ8bcnuAKz4mklgvjvwuXoRYukmclUurLwfq3Rg==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -1042,7 +1037,6 @@ "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.0.0.tgz", "integrity": "sha512-KQrANla4RBLhcGkwlndqsKzBwVFOWQr1640CfBVjj2oz4M3dW5hyMtXivBACvuwyUhYU/qJbqlDMBXl/OUSudQ==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -1083,7 +1077,6 @@ "resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-21.0.0.tgz", "integrity": "sha512-5IcmoftT2hLAbLfSoqGoCg0B1FLSk08xDoUdIyEUo1SmxNJMEEgU6WxhkPf6R7aoOlLAwYBoqGGP1Us1Z7rO7g==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0", "xhr2": "^0.2.0" @@ -1104,7 +1097,6 @@ "resolved": "https://registry.npmjs.org/@angular/router/-/router-21.0.0.tgz", "integrity": "sha512-ARx1R2CmTgAezlMkUpV40V4T/IbXhL7dm4SuMVKbuEOsCKZC0TLOSSTsGYY7HKem45JHlJaByv819cJnabFgBg==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -1123,7 +1115,6 @@ "resolved": "https://registry.npmjs.org/@angular/ssr/-/ssr-21.0.0.tgz", "integrity": "sha512-lzMzMdsAGy5OB7JsOfKK+SZQdxeOAWDg8sC/XcTUzY/BJu31Lz9kO2nuKmqcgr/aPOrD7Sc0F31u/NxGjeCdTw==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -1488,6 +1479,15 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", @@ -1579,7 +1579,6 @@ "integrity": "sha512-eohl3hKTiVyD1ilYdw9T0OiB4hnjef89e3dMYKz+mVKDzj+5IteTseASUsOB+EU9Tf6VNTCjDePcP6wkDGmLKQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@keyv/serialize": "^1.1.1" } @@ -1740,7 +1739,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -1764,7 +1762,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -3521,7 +3518,6 @@ "integrity": "sha512-X7/+dG9SLpSzRkwgG5/xiIzW0oMrV3C0HOa7YHG1WnrLK+vCQHfte4k/T80059YBdei29RBC3s+pSMvPJDU9/A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@inquirer/checkbox": "^4.3.0", "@inquirer/confirm": "^5.1.19", @@ -6517,11 +6513,16 @@ "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", + "license": "MIT" + }, "node_modules/@types/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", @@ -6529,6 +6530,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", @@ -6653,7 +6661,6 @@ "integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.48.0", "@typescript-eslint/types": "8.48.0", @@ -6761,7 +6768,6 @@ "integrity": "sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -6804,7 +6810,6 @@ "integrity": "sha512-yTJO1XuGxCsSfIVt1+1UrLHtue8xz16V8apzPYI06W0HbEbEWHxHXgZaAgavIkoh+GeV6hKKd5jm0sS6OYxWXQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.48.0", @@ -6927,7 +6932,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7363,7 +7367,6 @@ "integrity": "sha512-GaDRs2Mngpw3dr2vc085GnORh98NiXxwIjg/EoQQQl/icZt3Z7s0BRsYHDZ8swkZbOA6wZsqWJdrNirl+iKcDg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@astrojs/compiler": "^2.13.0", "@astrojs/internal-helpers": "0.7.5", @@ -8196,6 +8199,22 @@ "regenerator-runtime": "^0.11.0" } }, + "node_modules/babel-runtime/node_modules/core-js": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", + "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", + "dev": true, + "hasInstallScript": true, + "license": "MIT" + }, + "node_modules/babel-runtime/node_modules/regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true, + "license": "MIT" + }, "node_modules/bach": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/bach/-/bach-2.0.1.tgz", @@ -8333,6 +8352,16 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -8616,7 +8645,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -8985,6 +9013,26 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -9097,7 +9145,6 @@ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "readdirp": "^4.0.1" }, @@ -9809,13 +9856,16 @@ } }, "node_modules/core-js": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", - "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", - "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", - "dev": true, + "version": "3.47.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz", + "integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==", "hasInstallScript": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } }, "node_modules/core-util-is": { "version": "1.0.3", @@ -9917,6 +9967,16 @@ "node": ">=12 || >=16" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/css-select": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz", @@ -10436,8 +10496,7 @@ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1521046.tgz", "integrity": "sha512-vhE6eymDQSKWUXwwA37NtTTVEzjtGVfDr3pRbsWEQ5onH/Snp2c+2xZHWJJawG/0hCCJLRGt4xVtEVUVILol4w==", "dev": true, - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/dfa": { "version": "1.2.0", @@ -10573,6 +10632,16 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.0.tgz", + "integrity": "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", @@ -11260,7 +11329,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -11615,7 +11683,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -11805,6 +11872,17 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-png": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz", + "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==", + "license": "MIT", + "dependencies": { + "@types/pako": "^2.0.3", + "iobuffer": "^5.3.2", + "pako": "^2.1.0" + } + }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", @@ -14067,6 +14145,20 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "optional": true, + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/htmlparser2": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", @@ -14453,6 +14545,12 @@ "node": ">=10.13.0" } }, + "node_modules/iobuffer": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz", + "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==", + "license": "MIT" + }, "node_modules/ip-address": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", @@ -15434,13 +15532,29 @@ ], "license": "MIT" }, + "node_modules/jspdf": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.4.tgz", + "integrity": "sha512-dc6oQ8y37rRcHn316s4ngz/nOjayLF/FFxBF4V9zamQKRqXxyiH1zagkCdktdWhtoQId5K20xt1lB90XzkB+hQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "fast-png": "^6.2.0", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.11", + "core-js": "^3.6.0", + "dompurify": "^3.2.4", + "html2canvas": "^1.0.0-rc.5" + } + }, "node_modules/karma": { "version": "6.4.4", "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@colors/colors": "1.5.0", "body-parser": "^1.19.0", @@ -16121,7 +16235,6 @@ "integrity": "sha512-j1n1IuTX1VQjIy3tT7cyGbX7nvQOsFLoIqobZv4ttI5axP923gA44zUj6miiA6R5Aoms4sEGVIIcucXUbRI14g==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "copy-anything": "^2.0.1", "parse-node-version": "^1.0.1", @@ -18372,7 +18485,6 @@ "integrity": "sha512-2lMGkmS91FyP+p/Tzmu49hY+p1PDgHBNM+Fce8yrzZo8/EbybNPBYfJnwFfl0lwGmqpYLevH2oh12+ikKCLv9g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.3.0", "@rollup/plugin-json": "^6.1.0", @@ -19860,11 +19972,10 @@ } }, "node_modules/pako": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", - "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", - "dev": true, - "license": "MIT" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" }, "node_modules/param-case": { "version": "2.1.1", @@ -20202,6 +20313,13 @@ "dev": true, "license": "MIT" }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT", + "optional": true + }, "node_modules/piccolore": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz", @@ -20329,7 +20447,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -20480,7 +20597,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -20901,6 +21017,16 @@ "dev": true, "license": "MIT" }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -21015,11 +21141,11 @@ "license": "Apache-2.0" }, "node_modules/regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", - "dev": true, - "license": "MIT" + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "optional": true }, "node_modules/regex": { "version": "6.0.1", @@ -21513,6 +21639,16 @@ "dev": true, "license": "MIT" }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -21615,7 +21751,6 @@ "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -21727,7 +21862,6 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -21982,7 +22116,6 @@ "integrity": "sha512-+VUy01yfDqNmIVMd/LLKl2TTtY0ovZN0rTonh+FhKr65mFwIYgU9WzgIZKS7U9/SPCQvWTsTGx9jyt+qRm/XFw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@bufbuild/protobuf": "^2.5.0", "buffer-builder": "^0.2.0", @@ -22584,6 +22717,7 @@ "dev": true, "license": "ISC", "optional": true, + "peer": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -22634,6 +22768,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=8.6" }, @@ -22648,6 +22783,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "picomatch": "^2.2.1" }, @@ -23970,6 +24106,16 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -24282,7 +24428,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", @@ -24628,7 +24773,6 @@ "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -24724,6 +24868,16 @@ "semver": "bin/semver.js" } }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/svg-tags": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", @@ -24981,6 +25135,16 @@ "b4a": "^1.6.4" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -25352,8 +25516,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tuf-js": { "version": "4.0.0", @@ -25440,7 +25603,6 @@ "integrity": "sha512-ftJYPvpVfQvFzpkoSfHLkJybdA/geDJ8BGQt/ZnkkhnBYoYW6lBgPQXu6vqLxO4X75dA55hX8Af847H5KXlEFA==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@gerrit0/mini-shiki": "^3.12.0", "lunr": "^2.3.9", @@ -25475,7 +25637,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -25637,6 +25798,13 @@ "tiny-inflate": "^1.0.0" } }, + "node_modules/unicode-trie/node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "dev": true, + "license": "MIT" + }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", @@ -26184,6 +26352,16 @@ "node": ">= 0.4.0" } }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "optional": true, + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -26585,7 +26763,6 @@ "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -27574,7 +27751,6 @@ "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "dev": true, "license": "ISC", - "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -27727,7 +27903,6 @@ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -27756,8 +27931,7 @@ "version": "0.15.1", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz", "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/zwitch": { "version": "2.0.4", diff --git a/package.json b/package.json index d6f32999c94..83ebf9e7733 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "fflate": "^0.8.1", "igniteui-theming": "^24.0.0", "igniteui-trial-watermark": "^3.1.0", + "jspdf": "^3.0.4", "lodash-es": "^4.17.21", "rxjs": "^7.8.2", "tslib": "^2.3.0", diff --git a/projects/igniteui-angular-elements/src/analyzer/elements.config.ts b/projects/igniteui-angular-elements/src/analyzer/elements.config.ts index e362520db42..179a57a392d 100644 --- a/projects/igniteui-angular-elements/src/analyzer/elements.config.ts +++ b/projects/igniteui-angular-elements/src/analyzer/elements.config.ts @@ -475,7 +475,7 @@ export var registerConfig = [ contentQueries: [], additionalProperties: [], methods: ["export"], - boolProps: ["exportCSV", "exportExcel"], + boolProps: ["exportCSV", "exportExcel", "exportPDF"], }, { component: IgxGridToolbarHidingComponent, diff --git a/projects/igniteui-angular-i18n/src/i18n/BG/grid-resources.ts b/projects/igniteui-angular-i18n/src/i18n/BG/grid-resources.ts index 57b0aa8c6af..b753b41b113 100644 --- a/projects/igniteui-angular-i18n/src/i18n/BG/grid-resources.ts +++ b/projects/igniteui-angular-i18n/src/i18n/BG/grid-resources.ts @@ -142,6 +142,7 @@ export const GridResourceStringsBG = { igx_grid_toolbar_exporter_button_label: 'Експортирай', igx_grid_toolbar_exporter_excel_entry_text: 'Експортирай в Excel', igx_grid_toolbar_exporter_csv_entry_text: 'Експортиране в CSV файл', + igx_grid_toolbar_exporter_pdf_entry_text: 'Експортиране в PDF файл', igx_grid_snackbar_addrow_label: 'Добавен е ред', igx_grid_snackbar_addrow_actiontext: 'Покажи', igx_grid_actions_edit_label: 'Редактирай', diff --git a/projects/igniteui-angular-i18n/src/i18n/CS/grid-resources.ts b/projects/igniteui-angular-i18n/src/i18n/CS/grid-resources.ts index 626005bbd07..fd859ed2f31 100644 --- a/projects/igniteui-angular-i18n/src/i18n/CS/grid-resources.ts +++ b/projects/igniteui-angular-i18n/src/i18n/CS/grid-resources.ts @@ -142,6 +142,7 @@ export const GridResourceStringsCS = { igx_grid_toolbar_exporter_button_label: 'Export', igx_grid_toolbar_exporter_excel_entry_text: 'Export ve formátu Excel', igx_grid_toolbar_exporter_csv_entry_text: 'Export ve formátu CSV', + igx_grid_toolbar_exporter_pdf_entry_text: 'Export ve formátu PDF', igx_grid_snackbar_addrow_label: 'Řádek přidán', igx_grid_snackbar_addrow_actiontext: 'UKÁZAT', igx_grid_actions_edit_label: 'Upravit', diff --git a/projects/igniteui-angular-i18n/src/i18n/DA/grid-resources.ts b/projects/igniteui-angular-i18n/src/i18n/DA/grid-resources.ts index 8ae1ba3457a..f3c10564429 100644 --- a/projects/igniteui-angular-i18n/src/i18n/DA/grid-resources.ts +++ b/projects/igniteui-angular-i18n/src/i18n/DA/grid-resources.ts @@ -142,6 +142,7 @@ export const GridResourceStringsDA = { igx_grid_toolbar_exporter_button_label: 'Eksportér', igx_grid_toolbar_exporter_excel_entry_text: 'Eksportér til Excel', igx_grid_toolbar_exporter_csv_entry_text: 'Eksportér til CSV', + igx_grid_toolbar_exporter_pdf_entry_text: 'Eksportér til PDF', igx_grid_snackbar_addrow_label: 'Række tilføjet', igx_grid_snackbar_addrow_actiontext: 'VIS', igx_grid_actions_edit_label: 'Rediger', diff --git a/projects/igniteui-angular-i18n/src/i18n/DE/grid-resources.ts b/projects/igniteui-angular-i18n/src/i18n/DE/grid-resources.ts index 38b4fcb0871..b5dc719ffb5 100644 --- a/projects/igniteui-angular-i18n/src/i18n/DE/grid-resources.ts +++ b/projects/igniteui-angular-i18n/src/i18n/DE/grid-resources.ts @@ -140,6 +140,7 @@ export const GridResourceStringsDE = { igx_grid_toolbar_exporter_button_label: 'Exportiere', igx_grid_toolbar_exporter_excel_entry_text: 'Exportiere als Excel', igx_grid_toolbar_exporter_csv_entry_text: 'Exportiere als CSV', + igx_grid_toolbar_exporter_pdf_entry_text: 'Exportiere als PDF', igx_grid_groupByArea_select_message: 'Wähle alle Reihen der Gruppe aus mit Feldnamen {0} und Wert {1}.', igx_grid_groupByArea_deselect_message: 'Wähle alle Reihen der Gruppe ab mit Feldnamen {0} und Wert {1}.', igx_grid_snackbar_addrow_label: 'Reihe hinzugefügt', diff --git a/projects/igniteui-angular-i18n/src/i18n/ES/grid-resources.ts b/projects/igniteui-angular-i18n/src/i18n/ES/grid-resources.ts index 18827cb703e..ba77bb0b1c5 100644 --- a/projects/igniteui-angular-i18n/src/i18n/ES/grid-resources.ts +++ b/projects/igniteui-angular-i18n/src/i18n/ES/grid-resources.ts @@ -140,6 +140,7 @@ export const GridResourceStringsES = { igx_grid_toolbar_exporter_button_label: 'Exportar', igx_grid_toolbar_exporter_excel_entry_text: 'Exportar a Excel', igx_grid_toolbar_exporter_csv_entry_text: 'Exportar a CSV', + igx_grid_toolbar_exporter_pdf_entry_text: 'Exportar a PDF', igx_grid_groupByArea_select_message: 'Seleccione todas las filas del grupo con el nombre de campo {0} y el valor {1}.', igx_grid_groupByArea_deselect_message: 'Anule la selección de todas las filas del grupo con el nombre de campo {0} y el valor {1}.', igx_grid_snackbar_addrow_label: 'Fila agregada', diff --git a/projects/igniteui-angular-i18n/src/i18n/FR/grid-resources.ts b/projects/igniteui-angular-i18n/src/i18n/FR/grid-resources.ts index f4862c891a6..e5a6ae7dd34 100644 --- a/projects/igniteui-angular-i18n/src/i18n/FR/grid-resources.ts +++ b/projects/igniteui-angular-i18n/src/i18n/FR/grid-resources.ts @@ -140,6 +140,7 @@ export const GridResourceStringsFR = { igx_grid_toolbar_exporter_button_label: 'Exporter', igx_grid_toolbar_exporter_excel_entry_text: 'Exporter vers Excel', igx_grid_toolbar_exporter_csv_entry_text: 'Exporter vers CSV', + igx_grid_toolbar_exporter_pdf_entry_text: 'Exporter vers PDF', igx_grid_groupByArea_select_message: 'Sélectionnez toutes les lignes du groupe avec le nom de champ {0} et la valeur {1}.', igx_grid_groupByArea_deselect_message: 'Désélectionnez toutes les lignes du groupe avec le nom de champ {0} et la valeur {1}.', igx_grid_snackbar_addrow_label: 'Ligne ajoutée', diff --git a/projects/igniteui-angular-i18n/src/i18n/HU/grid-resources.ts b/projects/igniteui-angular-i18n/src/i18n/HU/grid-resources.ts index 867f0d84e01..11db84bff5f 100644 --- a/projects/igniteui-angular-i18n/src/i18n/HU/grid-resources.ts +++ b/projects/igniteui-angular-i18n/src/i18n/HU/grid-resources.ts @@ -142,6 +142,7 @@ export const GridResourceStringsHU = { igx_grid_toolbar_exporter_button_label: 'Exportálás', igx_grid_toolbar_exporter_excel_entry_text: 'Exportálás Excel formátumba', igx_grid_toolbar_exporter_csv_entry_text: 'Exportálás CSV formátumban', + igx_grid_toolbar_exporter_pdf_entry_text: 'Exportálás PDF formátumban', igx_grid_snackbar_addrow_label: 'Sor hozzáadva', igx_grid_snackbar_addrow_actiontext: 'MEGJELENÍTÉS', igx_grid_actions_edit_label: 'Szerkesztés', diff --git a/projects/igniteui-angular-i18n/src/i18n/IT/grid-resources.ts b/projects/igniteui-angular-i18n/src/i18n/IT/grid-resources.ts index 4a6d2ec76cf..fedf8f9b4cb 100644 --- a/projects/igniteui-angular-i18n/src/i18n/IT/grid-resources.ts +++ b/projects/igniteui-angular-i18n/src/i18n/IT/grid-resources.ts @@ -140,6 +140,7 @@ export const GridResourceStringsIT = { igx_grid_toolbar_exporter_button_label: 'Esporta', igx_grid_toolbar_exporter_excel_entry_text: 'Esporta in Excel', igx_grid_toolbar_exporter_csv_entry_text: 'Esporta in CSV', + igx_grid_toolbar_exporter_pdf_entry_text: 'Esporta in PDF', igx_grid_groupByArea_select_message: 'Selezionare tutte le righe del gruppo con nome campo {0} e valore {1}.', igx_grid_groupByArea_deselect_message: 'Deselezionare tutte le righe del gruppo con il nome campo {0} e il valore {1}.', igx_grid_snackbar_addrow_label: 'Riga aggiunta', diff --git a/projects/igniteui-angular-i18n/src/i18n/JA/grid-resources.ts b/projects/igniteui-angular-i18n/src/i18n/JA/grid-resources.ts index 2e39d6bc4f4..5705c8c1a8e 100644 --- a/projects/igniteui-angular-i18n/src/i18n/JA/grid-resources.ts +++ b/projects/igniteui-angular-i18n/src/i18n/JA/grid-resources.ts @@ -140,6 +140,7 @@ export const GridResourceStringsJA = { igx_grid_toolbar_exporter_button_label: 'エクスポート', igx_grid_toolbar_exporter_excel_entry_text: 'Excel へエクスポート', igx_grid_toolbar_exporter_csv_entry_text: 'CSV へのエクスポート', + igx_grid_toolbar_exporter_pdf_entry_text: 'PDF へのエクスポート', igx_grid_groupByArea_select_message: 'フィールド名 {0}、値 {1} のグループ内のすべての行を選択します。', igx_grid_groupByArea_deselect_message: 'フィールド名 {0}、値 {1} のグループ内のすべての行を選択解除します。', igx_grid_snackbar_addrow_label: '行が追加されました', diff --git a/projects/igniteui-angular-i18n/src/i18n/KO/grid-resources.ts b/projects/igniteui-angular-i18n/src/i18n/KO/grid-resources.ts index 20158a88fcd..852299027d5 100644 --- a/projects/igniteui-angular-i18n/src/i18n/KO/grid-resources.ts +++ b/projects/igniteui-angular-i18n/src/i18n/KO/grid-resources.ts @@ -140,6 +140,7 @@ export const GridResourceStringsKO = { igx_grid_toolbar_exporter_button_label: '내보내기', igx_grid_toolbar_exporter_excel_entry_text: 'Excel 로 내보내기', igx_grid_toolbar_exporter_csv_entry_text: 'CSV 로 내보내기', + igx_grid_toolbar_exporter_pdf_entry_text: 'PDF 로 내보내기', igx_grid_groupByArea_select_message: '필드 이름이 {0} 이고 값이 {1} 인 그룹의 모든 행을 선택하십시오.', igx_grid_groupByArea_deselect_message: '필드 이름이 {0} 이고 값이 {1} 인 그룹의 모든 행을 선택 취소합니다.', igx_grid_snackbar_addrow_label: '추가된 열', diff --git a/projects/igniteui-angular-i18n/src/i18n/NB/grid-resources.ts b/projects/igniteui-angular-i18n/src/i18n/NB/grid-resources.ts index 34be8e54fe1..7a281fd5090 100644 --- a/projects/igniteui-angular-i18n/src/i18n/NB/grid-resources.ts +++ b/projects/igniteui-angular-i18n/src/i18n/NB/grid-resources.ts @@ -142,6 +142,7 @@ export const GridResourceStringsNB = { igx_grid_toolbar_exporter_button_label: 'Eksporter', igx_grid_toolbar_exporter_excel_entry_text: 'Eksporter til Excel', igx_grid_toolbar_exporter_csv_entry_text: 'Eksporter til CSV', + igx_grid_toolbar_exporter_pdf_entry_text: 'Eksporter til PDF', igx_grid_snackbar_addrow_label: 'Rad lagt til', igx_grid_snackbar_addrow_actiontext: 'FORESTILLING', igx_grid_actions_edit_label: 'Redigere', diff --git a/projects/igniteui-angular-i18n/src/i18n/NL/grid-resources.ts b/projects/igniteui-angular-i18n/src/i18n/NL/grid-resources.ts index fbc8eee9d20..d01ae4851a0 100644 --- a/projects/igniteui-angular-i18n/src/i18n/NL/grid-resources.ts +++ b/projects/igniteui-angular-i18n/src/i18n/NL/grid-resources.ts @@ -142,6 +142,7 @@ export const GridResourceStringsNL = { igx_grid_toolbar_exporter_button_label: 'Exporteren', igx_grid_toolbar_exporter_excel_entry_text: 'Exporteren naar Excel', igx_grid_toolbar_exporter_csv_entry_text: 'Exporteren naar CSV', + igx_grid_toolbar_exporter_pdf_entry_text: 'Exporteren naar PDF', igx_grid_snackbar_addrow_label: 'Rij toegevoegd', igx_grid_snackbar_addrow_actiontext: 'WEERGEVEN', igx_grid_actions_edit_label: 'Bewerken', diff --git a/projects/igniteui-angular-i18n/src/i18n/PL/grid-resources.ts b/projects/igniteui-angular-i18n/src/i18n/PL/grid-resources.ts index e01ed99a66f..bd3857860a4 100644 --- a/projects/igniteui-angular-i18n/src/i18n/PL/grid-resources.ts +++ b/projects/igniteui-angular-i18n/src/i18n/PL/grid-resources.ts @@ -142,6 +142,7 @@ export const GridResourceStringsPL = { igx_grid_toolbar_exporter_button_label: 'Eksportuj', igx_grid_toolbar_exporter_excel_entry_text: 'Eksportuj do programu Excel', igx_grid_toolbar_exporter_csv_entry_text: 'Eksportuj do pliku CSV', + igx_grid_toolbar_exporter_pdf_entry_text: 'Eksportuj do pliku PDF', igx_grid_snackbar_addrow_label: 'Dodano wiersz', igx_grid_snackbar_addrow_actiontext: 'POKAŻ', igx_grid_actions_edit_label: 'Edytuj', diff --git a/projects/igniteui-angular-i18n/src/i18n/PT/grid-resources.ts b/projects/igniteui-angular-i18n/src/i18n/PT/grid-resources.ts index fd46c35ace4..146a19f8871 100644 --- a/projects/igniteui-angular-i18n/src/i18n/PT/grid-resources.ts +++ b/projects/igniteui-angular-i18n/src/i18n/PT/grid-resources.ts @@ -142,6 +142,7 @@ export const GridResourceStringsPT = { igx_grid_toolbar_exporter_button_label: 'Exportar', igx_grid_toolbar_exporter_excel_entry_text: 'Exportar para Excel', igx_grid_toolbar_exporter_csv_entry_text: 'Exportar para CSV', + igx_grid_toolbar_exporter_pdf_entry_text: 'Exportar para PDF', igx_grid_snackbar_addrow_label: 'Linha adicionada', igx_grid_snackbar_addrow_actiontext: 'MOSTRAR', igx_grid_actions_edit_label: 'Editar', diff --git a/projects/igniteui-angular-i18n/src/i18n/RO/grid-resources.ts b/projects/igniteui-angular-i18n/src/i18n/RO/grid-resources.ts index 4ce70bb5d4f..29ad678d4f7 100644 --- a/projects/igniteui-angular-i18n/src/i18n/RO/grid-resources.ts +++ b/projects/igniteui-angular-i18n/src/i18n/RO/grid-resources.ts @@ -142,6 +142,7 @@ export const GridResourceStringsRO = { igx_grid_toolbar_exporter_button_label: 'Exportați', igx_grid_toolbar_exporter_excel_entry_text: 'Exportați în Excel', igx_grid_toolbar_exporter_csv_entry_text: 'Exportați în CSV', + igx_grid_toolbar_exporter_pdf_entry_text: 'Exportați în PDF', igx_grid_snackbar_addrow_label: 'Rând adăugat', igx_grid_snackbar_addrow_actiontext: 'ARATĂ', igx_grid_actions_edit_label: 'Editați', diff --git a/projects/igniteui-angular-i18n/src/i18n/SV/grid-resources.ts b/projects/igniteui-angular-i18n/src/i18n/SV/grid-resources.ts index 7555342ad11..972c21e829a 100644 --- a/projects/igniteui-angular-i18n/src/i18n/SV/grid-resources.ts +++ b/projects/igniteui-angular-i18n/src/i18n/SV/grid-resources.ts @@ -142,6 +142,7 @@ export const GridResourceStringsSV = { igx_grid_toolbar_exporter_button_label: 'Exportera', igx_grid_toolbar_exporter_excel_entry_text: 'Exportera till Excel', igx_grid_toolbar_exporter_csv_entry_text: 'Exportera till CSV', + igx_grid_toolbar_exporter_pdf_entry_text: 'Exportera till PDF', igx_grid_snackbar_addrow_label: 'Rad tillagd', igx_grid_snackbar_addrow_actiontext: 'VISA', igx_grid_actions_edit_label: 'Redigera', diff --git a/projects/igniteui-angular-i18n/src/i18n/TR/grid-resources.ts b/projects/igniteui-angular-i18n/src/i18n/TR/grid-resources.ts index 99a5342fdb7..50e395650db 100644 --- a/projects/igniteui-angular-i18n/src/i18n/TR/grid-resources.ts +++ b/projects/igniteui-angular-i18n/src/i18n/TR/grid-resources.ts @@ -142,6 +142,7 @@ export const GridResourceStringsTR = { igx_grid_toolbar_exporter_button_label: 'Dışarı Aktarma', igx_grid_toolbar_exporter_excel_entry_text: 'Excel\'ye Aktar', igx_grid_toolbar_exporter_csv_entry_text: 'CSV\'ye Aktar', + igx_grid_toolbar_exporter_pdf_entry_text: 'PDF\'ye Aktar', igx_grid_snackbar_addrow_label: 'Satır eklendi', igx_grid_snackbar_addrow_actiontext: 'GÖSTER', igx_grid_actions_edit_label: 'Düzenle', diff --git a/projects/igniteui-angular-i18n/src/i18n/ZH-HANS/grid-resources.ts b/projects/igniteui-angular-i18n/src/i18n/ZH-HANS/grid-resources.ts index 8171c056c79..0c5e5f21732 100644 --- a/projects/igniteui-angular-i18n/src/i18n/ZH-HANS/grid-resources.ts +++ b/projects/igniteui-angular-i18n/src/i18n/ZH-HANS/grid-resources.ts @@ -140,6 +140,7 @@ export const GridResourceStringsZHHANS = { igx_grid_toolbar_exporter_button_label: '导出', igx_grid_toolbar_exporter_excel_entry_text: '导出至 Excel', igx_grid_toolbar_exporter_csv_entry_text: '导出为 CSV', + igx_grid_toolbar_exporter_pdf_entry_text: '导出为 PDF', igx_grid_groupByArea_select_message: '选择组中字段名称为 {0} 且值为 {1} 的所有行。', igx_grid_groupByArea_deselect_message: '取消选择组中字段名称为 {0} 且值为 {1} 的所有行。', igx_grid_snackbar_addrow_label: '已添加行', diff --git a/projects/igniteui-angular-i18n/src/i18n/ZH-HANT/grid-resources.ts b/projects/igniteui-angular-i18n/src/i18n/ZH-HANT/grid-resources.ts index 142ebd35c4f..7802e84da0b 100644 --- a/projects/igniteui-angular-i18n/src/i18n/ZH-HANT/grid-resources.ts +++ b/projects/igniteui-angular-i18n/src/i18n/ZH-HANT/grid-resources.ts @@ -140,6 +140,7 @@ export const GridResourceStringsZHHANT = { igx_grid_toolbar_exporter_button_label: '匯出', igx_grid_toolbar_exporter_excel_entry_text: '匯出至 Excel', igx_grid_toolbar_exporter_csv_entry_text: '匯出至 CSV', + igx_grid_toolbar_exporter_pdf_entry_text: '匯出至 PDF', igx_grid_groupByArea_select_message: '選擇欄位名稱為 {0} 且值為 {1} 的群組中的所有行。', igx_grid_groupByArea_deselect_message: '取消選擇組中所有具有欄位名稱 {0} 和值 {1} 的行。', igx_grid_snackbar_addrow_label: '已新增行', diff --git a/projects/igniteui-angular/core/src/core/i18n/grid-resources.ts b/projects/igniteui-angular/core/src/core/i18n/grid-resources.ts index e6e05412c8b..1731bf127d2 100644 --- a/projects/igniteui-angular/core/src/core/i18n/grid-resources.ts +++ b/projects/igniteui-angular/core/src/core/i18n/grid-resources.ts @@ -136,6 +136,7 @@ export interface IGridResourceStrings { igx_grid_toolbar_exporter_button_label?: string; igx_grid_toolbar_exporter_excel_entry_text?: string; igx_grid_toolbar_exporter_csv_entry_text?: string; + igx_grid_toolbar_exporter_pdf_entry_text?: string; igx_grid_snackbar_addrow_label?: string; igx_grid_snackbar_addrow_actiontext?: string; igx_grid_actions_edit_label?: string; @@ -318,6 +319,7 @@ export const GridResourceStringsEN: IGridResourceStrings = { igx_grid_toolbar_exporter_button_label: 'Export', igx_grid_toolbar_exporter_excel_entry_text: 'Export to Excel', igx_grid_toolbar_exporter_csv_entry_text: 'Export to CSV', + igx_grid_toolbar_exporter_pdf_entry_text: 'Export to PDF', igx_grid_snackbar_addrow_label: 'Row added', igx_grid_snackbar_addrow_actiontext: 'SHOW', igx_grid_actions_edit_label: 'Edit', diff --git a/projects/igniteui-angular/core/src/public_api.ts b/projects/igniteui-angular/core/src/public_api.ts index 05d5c039dd3..c76d6e8374d 100644 --- a/projects/igniteui-angular/core/src/public_api.ts +++ b/projects/igniteui-angular/core/src/public_api.ts @@ -35,6 +35,7 @@ export * from './data-operations/grid-sorting-strategy'; export * from './data-operations/paging-state.interface'; export * from './data-operations/data-util'; export * from './data-operations/grid-types'; +export * from './data-operations/operations'; // Services export * from './services/public_api'; diff --git a/projects/igniteui-angular/core/src/services/public_api.ts b/projects/igniteui-angular/core/src/services/public_api.ts index 5a0ac3f116d..69ae76e9358 100644 --- a/projects/igniteui-angular/core/src/services/public_api.ts +++ b/projects/igniteui-angular/core/src/services/public_api.ts @@ -2,14 +2,7 @@ export * from './animation/angular-animation-player'; export * from './animation/angular-animation-service'; export * from './animation/animation'; -export * from './csv/csv-exporter'; -export * from './csv/csv-exporter-options'; -export * from './csv/char-separated-value-data'; export { Direction as ɵDirection, DIR_DOCUMENT as ɵDIR_DOCUMENT, IgxDirectionality as ɵIgxDirectionality } from './direction/directionality'; -export * from './excel/excel-exporter'; -export * from './excel/excel-exporter-options'; -export * from './exporter-common/base-export-service'; -export * from './exporter-common/exporter-options-base'; export * from './overlay/overlay'; export * from './overlay/position'; export * from './overlay/scroll'; diff --git a/projects/igniteui-angular/grids/core/src/common/events.ts b/projects/igniteui-angular/grids/core/src/common/events.ts index 5e66537fc89..f1e9b6d4d9c 100644 --- a/projects/igniteui-angular/grids/core/src/common/events.ts +++ b/projects/igniteui-angular/grids/core/src/common/events.ts @@ -1,7 +1,9 @@ -import { CancelableEventArgs, ColumnType, IBaseEventArgs, IFilteringExpressionsTree, IGroupingExpression, IgxBaseExporter, IgxExporterOptionsBase, ISortingExpression } from 'igniteui-angular/core'; +import { CancelableEventArgs, ColumnType, IBaseEventArgs, IFilteringExpressionsTree, IGroupingExpression, ISortingExpression } from 'igniteui-angular/core'; import { GridKeydownTargetType } from './enums'; import { CellType, GridType, RowType } from './grid.interface'; import { IBaseSearchInfo } from 'igniteui-angular/directives'; +import { IgxBaseExporter } from '../services/exporter-common/base-export-service'; +import { IgxExporterOptionsBase } from '../services/exporter-common/exporter-options-base'; /** The event arguments when data from a grid is being copied. */ export interface IGridClipboardEvent { diff --git a/projects/igniteui-angular/grids/core/src/public_api.ts b/projects/igniteui-angular/grids/core/src/public_api.ts index 29c86eba1fd..1068577f3e6 100644 --- a/projects/igniteui-angular/grids/core/src/public_api.ts +++ b/projects/igniteui-angular/grids/core/src/public_api.ts @@ -114,6 +114,17 @@ export * from './pivot-grid.interface'; export * from './pivot-grid-dimensions'; export * from './pivot-grid-aggregate'; export * from './watch-changes'; +// Exporter services (moved from core) +export * from './services/exporter-common/base-export-service'; +export * from './services/exporter-common/exporter-options-base'; +export * from './services/exporter-common/export-utilities'; +export * from './services/csv/csv-exporter'; +export * from './services/csv/csv-exporter-options'; +export * from './services/csv/char-separated-value-data'; +export * from './services/excel/excel-exporter'; +export * from './services/excel/excel-exporter-options'; +export * from './services/pdf/pdf-exporter'; +export * from './services/pdf/pdf-exporter-options'; /* diff --git a/projects/igniteui-angular/core/src/services/csv/char-separated-value-data.ts b/projects/igniteui-angular/grids/core/src/services/csv/char-separated-value-data.ts similarity index 98% rename from projects/igniteui-angular/core/src/services/csv/char-separated-value-data.ts rename to projects/igniteui-angular/grids/core/src/services/csv/char-separated-value-data.ts index aea2416c319..9e0b3624e86 100644 --- a/projects/igniteui-angular/core/src/services/csv/char-separated-value-data.ts +++ b/projects/igniteui-angular/grids/core/src/services/csv/char-separated-value-data.ts @@ -1,6 +1,6 @@ import { ExportUtilities } from '../exporter-common/export-utilities'; -import { yieldingLoop } from '../../core/utils'; import { IColumnInfo } from '../exporter-common/base-export-service'; +import { yieldingLoop } from 'igniteui-angular/core'; /** * @hidden diff --git a/projects/igniteui-angular/core/src/services/csv/csv-exporter-grid.spec.ts b/projects/igniteui-angular/grids/core/src/services/csv/csv-exporter-grid.spec.ts similarity index 96% rename from projects/igniteui-angular/core/src/services/csv/csv-exporter-grid.spec.ts rename to projects/igniteui-angular/grids/core/src/services/csv/csv-exporter-grid.spec.ts index 4e647ca0152..2ae12e32df9 100644 --- a/projects/igniteui-angular/core/src/services/csv/csv-exporter-grid.spec.ts +++ b/projects/igniteui-angular/grids/core/src/services/csv/csv-exporter-grid.spec.ts @@ -4,26 +4,23 @@ import { ExportUtilities } from '../exporter-common/export-utilities'; import { TestMethods } from '../exporter-common/test-methods.spec'; import { IgxCsvExporterService } from './csv-exporter'; import { CsvFileTypes, IgxCsvExporterOptions } from './csv-exporter-options'; -import { CSVWrapper } from './csv-verification-wrapper.spec'; -import { IgxTreeGridPrimaryForeignKeyComponent } from '../../../../test-utils/tree-grid-components.spec'; +import { IgxTreeGridPrimaryForeignKeyComponent } from '../../../../../test-utils/tree-grid-components.spec'; import { ReorderedColumnsComponent, GridIDNameJobTitleComponent, ProductsComponent, ColumnsAddedOnInitComponent, - EmptyGridComponent } from '../../../../test-utils/grid-samples.spec'; -import { SampleTestData } from '../../../../test-utils/sample-test-data.spec'; + EmptyGridComponent } from '../../../../../test-utils/grid-samples.spec'; +import { SampleTestData } from '../../../../../test-utils/sample-test-data.spec'; import { first } from 'rxjs/operators'; -import { DefaultSortingStrategy, SortingDirection } from '../../data-operations/sorting-strategy'; -import { IgxStringFilteringOperand, IgxNumberFilteringOperand } from '../../data-operations/filtering-condition'; -import { FilteringExpressionsTree } from '../../data-operations/filtering-expressions-tree'; -import { FilteringLogic } from '../../data-operations/filtering-expression.interface'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { wait } from '../../../../test-utils/ui-interactions.spec'; -import { IgxPivotGridTestBaseComponent } from '../../../../test-utils/pivot-grid-samples.spec'; +import { wait } from '../../../../../test-utils/ui-interactions.spec'; +import { IgxPivotGridTestBaseComponent } from '../../../../../test-utils/pivot-grid-samples.spec'; import { IgxGridComponent } from 'igniteui-angular/grids/grid'; import { IgxTreeGridComponent } from 'igniteui-angular/grids/tree-grid'; import { IgxPivotGridComponent } from 'igniteui-angular/grids/pivot-grid'; import { IgxPivotNumericAggregate } from 'igniteui-angular/grids/core'; +import { DefaultSortingStrategy, FilteringExpressionsTree, FilteringLogic, IgxNumberFilteringOperand, IgxStringFilteringOperand, SortingDirection } from 'igniteui-angular/core'; +import { CSVWrapper } from './csv-verification-wrapper.spec'; describe('CSV Grid Exporter', () => { let exporter: IgxCsvExporterService; diff --git a/projects/igniteui-angular/core/src/services/csv/csv-exporter-options.ts b/projects/igniteui-angular/grids/core/src/services/csv/csv-exporter-options.ts similarity index 100% rename from projects/igniteui-angular/core/src/services/csv/csv-exporter-options.ts rename to projects/igniteui-angular/grids/core/src/services/csv/csv-exporter-options.ts diff --git a/projects/igniteui-angular/core/src/services/csv/csv-exporter.spec.ts b/projects/igniteui-angular/grids/core/src/services/csv/csv-exporter.spec.ts similarity index 98% rename from projects/igniteui-angular/core/src/services/csv/csv-exporter.spec.ts rename to projects/igniteui-angular/grids/core/src/services/csv/csv-exporter.spec.ts index 4690f42402a..c4267eaee88 100644 --- a/projects/igniteui-angular/core/src/services/csv/csv-exporter.spec.ts +++ b/projects/igniteui-angular/grids/core/src/services/csv/csv-exporter.spec.ts @@ -2,7 +2,7 @@ import { ExportUtilities } from '../exporter-common/export-utilities'; import { IgxCsvExporterService } from './csv-exporter'; import { CsvFileTypes, IgxCsvExporterOptions } from './csv-exporter-options'; import { CSVWrapper } from './csv-verification-wrapper.spec'; -import { SampleTestData } from '../../../../test-utils/sample-test-data.spec'; +import { SampleTestData } from '../../../../../test-utils/sample-test-data.spec'; import { first } from 'rxjs/operators'; describe('CSV exporter', () => { diff --git a/projects/igniteui-angular/core/src/services/csv/csv-exporter.ts b/projects/igniteui-angular/grids/core/src/services/csv/csv-exporter.ts similarity index 95% rename from projects/igniteui-angular/core/src/services/csv/csv-exporter.ts rename to projects/igniteui-angular/grids/core/src/services/csv/csv-exporter.ts index d6a5c9596ae..5013fdc57e8 100644 --- a/projects/igniteui-angular/core/src/services/csv/csv-exporter.ts +++ b/projects/igniteui-angular/grids/core/src/services/csv/csv-exporter.ts @@ -3,7 +3,7 @@ import { DEFAULT_OWNER, ExportHeaderType, IColumnInfo, IExportRecord, IgxBaseExp import { ExportUtilities } from '../exporter-common/export-utilities'; import { CharSeparatedValueData } from './char-separated-value-data'; import { CsvFileTypes, IgxCsvExporterOptions } from './csv-exporter-options'; -import { IBaseEventArgs } from '../../core/utils'; +import { IBaseEventArgs } from 'igniteui-angular/core'; export interface ICsvExportEndedEventArgs extends IBaseEventArgs { csvData?: string; @@ -51,13 +51,13 @@ export class IgxCsvExporterService extends IgxBaseExporter { protected exportDataImplementation(data: IExportRecord[], options: IgxCsvExporterOptions, done: () => void) { const dimensionKeys = data[0]?.dimensionKeys; - data = dimensionKeys?.length ? + data = dimensionKeys?.length ? data.map((item) => item.rawData): data.map((item) => item.data); const columnList = this._ownersMap.get(DEFAULT_OWNER); const columns = columnList?.columns.filter(c => c.headerType === ExportHeaderType.ColumnHeader); if (dimensionKeys) { - const dimensionCols = dimensionKeys.map((key) => { + const dimensionCols = dimensionKeys.map((key) => { const columnInfo: IColumnInfo = { header: key, field: key, diff --git a/projects/igniteui-angular/core/src/services/csv/csv-verification-wrapper.spec.ts b/projects/igniteui-angular/grids/core/src/services/csv/csv-verification-wrapper.spec.ts similarity index 99% rename from projects/igniteui-angular/core/src/services/csv/csv-verification-wrapper.spec.ts rename to projects/igniteui-angular/grids/core/src/services/csv/csv-verification-wrapper.spec.ts index 65639433acc..45062bdbff1 100644 --- a/projects/igniteui-angular/core/src/services/csv/csv-verification-wrapper.spec.ts +++ b/projects/igniteui-angular/grids/core/src/services/csv/csv-verification-wrapper.spec.ts @@ -1,7 +1,6 @@ export class CSVWrapper { private _data: string; - private _hasValues = true; private _delimiter = ''; private _eor = '\r\n'; diff --git a/projects/igniteui-angular/core/src/services/excel/excel-elements-factory.ts b/projects/igniteui-angular/grids/core/src/services/excel/excel-elements-factory.ts similarity index 100% rename from projects/igniteui-angular/core/src/services/excel/excel-elements-factory.ts rename to projects/igniteui-angular/grids/core/src/services/excel/excel-elements-factory.ts diff --git a/projects/igniteui-angular/core/src/services/excel/excel-enums.ts b/projects/igniteui-angular/grids/core/src/services/excel/excel-enums.ts similarity index 100% rename from projects/igniteui-angular/core/src/services/excel/excel-enums.ts rename to projects/igniteui-angular/grids/core/src/services/excel/excel-enums.ts diff --git a/projects/igniteui-angular/core/src/services/excel/excel-exporter-grid.spec.ts b/projects/igniteui-angular/grids/core/src/services/excel/excel-exporter-grid.spec.ts similarity index 98% rename from projects/igniteui-angular/core/src/services/excel/excel-exporter-grid.spec.ts rename to projects/igniteui-angular/grids/core/src/services/excel/excel-exporter-grid.spec.ts index 7a5ca8a1f01..c38372085d1 100644 --- a/projects/igniteui-angular/core/src/services/excel/excel-exporter-grid.spec.ts +++ b/projects/igniteui-angular/grids/core/src/services/excel/excel-exporter-grid.spec.ts @@ -4,8 +4,6 @@ import { ExportUtilities } from '../exporter-common/export-utilities'; import { TestMethods } from '../exporter-common/test-methods.spec'; import { IgxExcelExporterService } from './excel-exporter'; import { IgxExcelExporterOptions } from './excel-exporter-options'; -import { ZipWrapper } from './zip-verification-wrapper.spec'; -import { FileContentData } from './test-data.service.spec'; import { ReorderedColumnsComponent, GridIDNameJobTitleComponent, @@ -25,32 +23,30 @@ import { GridCustomSummaryWithUndefinedZeroAndValidNumberComponent, GridCustomSummaryWithUndefinedAndNullComponent, GridCustomSummaryWithDateComponent -} from '../../../../test-utils/grid-samples.spec'; -import { SampleTestData } from '../../../../test-utils/sample-test-data.spec'; +} from '../../../../../test-utils/grid-samples.spec'; +import { SampleTestData } from '../../../../../test-utils/sample-test-data.spec'; import { first } from 'rxjs/operators'; -import { DefaultSortingStrategy, SortingDirection } from '../../data-operations/sorting-strategy'; -import { IgxStringFilteringOperand } from '../../data-operations/filtering-condition'; -import { IgxTreeGridPrimaryForeignKeyComponent, IgxTreeGridSummariesKeyComponent } from '../../../../test-utils/tree-grid-components.spec'; +import { IgxTreeGridPrimaryForeignKeyComponent, IgxTreeGridSummariesKeyComponent } from '../../../../../test-utils/tree-grid-components.spec'; -import { IgxNumberFilteringOperand } from '../../data-operations/filtering-condition'; -import { UIInteractions, wait } from '../../../../test-utils/ui-interactions.spec'; +import { UIInteractions, wait } from '../../../../../test-utils/ui-interactions.spec'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { FilteringExpressionsTree } from '../../data-operations/filtering-expressions-tree'; -import { FilteringLogic } from '../../data-operations/filtering-expression.interface'; import { IgxHierarchicalGridExportComponent, IgxHierarchicalGridMCHCollapsibleComponent, IgxHierarchicalGridMultiColumnHeaderIslandsExportComponent, IgxHierarchicalGridMultiColumnHeadersExportComponent, IgxHierarchicalGridSummariesExportComponent -} from '../../../../test-utils/hierarchical-grid-components.spec'; -import { GridFunctions } from '../../../../test-utils/grid-functions.spec'; -import { IgxPivotGridMultipleRowComponent, IgxPivotGridTestComplexHierarchyComponent, SALES_DATA } from '../../../../test-utils/pivot-grid-samples.spec'; +} from '../../../../../test-utils/hierarchical-grid-components.spec'; +import { GridFunctions } from '../../../../../test-utils/grid-functions.spec'; +import { IgxPivotGridMultipleRowComponent, IgxPivotGridTestComplexHierarchyComponent, SALES_DATA } from '../../../../../test-utils/pivot-grid-samples.spec'; import { IgxHierarchicalRowComponent } from 'igniteui-angular/grids/hierarchical-grid/src/hierarchical-row.component'; import { IgxTreeGridComponent } from 'igniteui-angular/grids/tree-grid'; import { IgxPivotGridComponent } from 'igniteui-angular/grids/pivot-grid'; import { IgxPivotNumericAggregate, PivotRowLayoutType } from 'igniteui-angular/grids/core'; import { IgxHierarchicalGridComponent } from 'igniteui-angular/grids/hierarchical-grid'; import { IgxGridComponent } from 'igniteui-angular/grids/grid'; +import { FileContentData } from './test-data.service.spec'; +import { ZipWrapper } from './zip-verification-wrapper.spec'; +import { DefaultSortingStrategy, FilteringExpressionsTree, FilteringLogic, IgxNumberFilteringOperand, IgxStringFilteringOperand, SortingDirection } from 'igniteui-angular/core'; describe('Excel Exporter', () => { let exporter: IgxExcelExporterService; diff --git a/projects/igniteui-angular/core/src/services/excel/excel-exporter-options.ts b/projects/igniteui-angular/grids/core/src/services/excel/excel-exporter-options.ts similarity index 100% rename from projects/igniteui-angular/core/src/services/excel/excel-exporter-options.ts rename to projects/igniteui-angular/grids/core/src/services/excel/excel-exporter-options.ts diff --git a/projects/igniteui-angular/core/src/services/excel/excel-exporter.spec.ts b/projects/igniteui-angular/grids/core/src/services/excel/excel-exporter.spec.ts similarity index 98% rename from projects/igniteui-angular/core/src/services/excel/excel-exporter.spec.ts rename to projects/igniteui-angular/grids/core/src/services/excel/excel-exporter.spec.ts index 534f411e64b..21cfad327dc 100644 --- a/projects/igniteui-angular/core/src/services/excel/excel-exporter.spec.ts +++ b/projects/igniteui-angular/grids/core/src/services/excel/excel-exporter.spec.ts @@ -4,7 +4,7 @@ import { IgxExcelExporterOptions } from './excel-exporter-options'; import { IColumnExportingEventArgs } from '../exporter-common/base-export-service'; import { ZipWrapper } from './zip-verification-wrapper.spec'; import { FileContentData } from './test-data.service.spec'; -import { SampleTestData } from '../../../../test-utils/sample-test-data.spec'; +import { SampleTestData } from '../../../../../test-utils/sample-test-data.spec'; import { first } from 'rxjs/operators'; describe('Excel Exporter', () => { diff --git a/projects/igniteui-angular/core/src/services/excel/excel-exporter.ts b/projects/igniteui-angular/grids/core/src/services/excel/excel-exporter.ts similarity index 99% rename from projects/igniteui-angular/core/src/services/excel/excel-exporter.ts rename to projects/igniteui-angular/grids/core/src/services/excel/excel-exporter.ts index 7935d7e8119..00824131b91 100644 --- a/projects/igniteui-angular/core/src/services/excel/excel-exporter.ts +++ b/projects/igniteui-angular/grids/core/src/services/excel/excel-exporter.ts @@ -8,8 +8,8 @@ import { IExcelFolder } from './excel-interfaces'; import { ExportRecordType, IExportRecord, IgxBaseExporter, DEFAULT_OWNER, ExportHeaderType, GRID_LEVEL_COL } from '../exporter-common/base-export-service'; import { ExportUtilities } from '../exporter-common/export-utilities'; import { WorksheetData } from './worksheet-data'; -import { IBaseEventArgs } from '../../core/utils'; import { WorksheetFile } from './excel-files'; +import { IBaseEventArgs } from 'igniteui-angular/core'; export interface IExcelExportEndedEventArgs extends IBaseEventArgs { xlsx?: Object diff --git a/projects/igniteui-angular/core/src/services/excel/excel-files.ts b/projects/igniteui-angular/grids/core/src/services/excel/excel-files.ts similarity index 99% rename from projects/igniteui-angular/core/src/services/excel/excel-files.ts rename to projects/igniteui-angular/grids/core/src/services/excel/excel-files.ts index 95f41241b01..0d5d81882c7 100644 --- a/projects/igniteui-angular/core/src/services/excel/excel-files.ts +++ b/projects/igniteui-angular/grids/core/src/services/excel/excel-files.ts @@ -3,8 +3,8 @@ import { ExcelStrings } from './excel-strings'; import { WorksheetData } from './worksheet-data'; import { strToU8 } from 'fflate'; -import { yieldingLoop } from '../../core/utils'; import { ExportHeaderType, ExportRecordType, IExportRecord, IColumnList, IColumnInfo, GRID_ROOT_SUMMARY, GRID_PARENT, GRID_LEVEL_COL } from '../exporter-common/base-export-service'; +import { yieldingLoop } from 'igniteui-angular/core'; /** * @hidden @@ -645,7 +645,7 @@ export class WorksheetFile implements IExcelFile { rowCoordinate = startValue + 1; } - const columnValue = currentCol.headerType === ExportHeaderType.PivotMergedHeader ? + const columnValue = currentCol.headerType === ExportHeaderType.PivotMergedHeader ? dictionary.saveValue(currentCol.field, true, true) : dictionary.saveValue(currentCol.header, true, false); diff --git a/projects/igniteui-angular/core/src/services/excel/excel-folders.ts b/projects/igniteui-angular/grids/core/src/services/excel/excel-folders.ts similarity index 100% rename from projects/igniteui-angular/core/src/services/excel/excel-folders.ts rename to projects/igniteui-angular/grids/core/src/services/excel/excel-folders.ts diff --git a/projects/igniteui-angular/core/src/services/excel/excel-interfaces.ts b/projects/igniteui-angular/grids/core/src/services/excel/excel-interfaces.ts similarity index 100% rename from projects/igniteui-angular/core/src/services/excel/excel-interfaces.ts rename to projects/igniteui-angular/grids/core/src/services/excel/excel-interfaces.ts diff --git a/projects/igniteui-angular/core/src/services/excel/excel-strings.ts b/projects/igniteui-angular/grids/core/src/services/excel/excel-strings.ts similarity index 100% rename from projects/igniteui-angular/core/src/services/excel/excel-strings.ts rename to projects/igniteui-angular/grids/core/src/services/excel/excel-strings.ts diff --git a/projects/igniteui-angular/core/src/services/excel/test-data.service.spec.ts b/projects/igniteui-angular/grids/core/src/services/excel/test-data.service.spec.ts similarity index 100% rename from projects/igniteui-angular/core/src/services/excel/test-data.service.spec.ts rename to projects/igniteui-angular/grids/core/src/services/excel/test-data.service.spec.ts diff --git a/projects/igniteui-angular/core/src/services/excel/worksheet-data-dictionary.ts b/projects/igniteui-angular/grids/core/src/services/excel/worksheet-data-dictionary.ts similarity index 100% rename from projects/igniteui-angular/core/src/services/excel/worksheet-data-dictionary.ts rename to projects/igniteui-angular/grids/core/src/services/excel/worksheet-data-dictionary.ts diff --git a/projects/igniteui-angular/core/src/services/excel/worksheet-data.ts b/projects/igniteui-angular/grids/core/src/services/excel/worksheet-data.ts similarity index 99% rename from projects/igniteui-angular/core/src/services/excel/worksheet-data.ts rename to projects/igniteui-angular/grids/core/src/services/excel/worksheet-data.ts index 55f34909197..ecd4aec9817 100644 --- a/projects/igniteui-angular/core/src/services/excel/worksheet-data.ts +++ b/projects/igniteui-angular/grids/core/src/services/excel/worksheet-data.ts @@ -14,7 +14,6 @@ export class WorksheetData { private _hasSummaries: boolean; private _isPivotGrid: boolean; private _isTreeGrid: boolean; - private _isGroupedGrid: boolean; constructor(private _data: IExportRecord[], public options: IgxExcelExporterOptions, diff --git a/projects/igniteui-angular/core/src/services/excel/zip-helper.spec.ts b/projects/igniteui-angular/grids/core/src/services/excel/zip-helper.spec.ts similarity index 100% rename from projects/igniteui-angular/core/src/services/excel/zip-helper.spec.ts rename to projects/igniteui-angular/grids/core/src/services/excel/zip-helper.spec.ts diff --git a/projects/igniteui-angular/core/src/services/excel/zip-verification-wrapper.spec.ts b/projects/igniteui-angular/grids/core/src/services/excel/zip-verification-wrapper.spec.ts similarity index 100% rename from projects/igniteui-angular/core/src/services/excel/zip-verification-wrapper.spec.ts rename to projects/igniteui-angular/grids/core/src/services/excel/zip-verification-wrapper.spec.ts diff --git a/projects/igniteui-angular/core/src/services/exporter-common/base-export-service.ts b/projects/igniteui-angular/grids/core/src/services/exporter-common/base-export-service.ts similarity index 98% rename from projects/igniteui-angular/core/src/services/exporter-common/base-export-service.ts rename to projects/igniteui-angular/grids/core/src/services/exporter-common/base-export-service.ts index 9195571f5e1..17d733b6b59 100644 --- a/projects/igniteui-angular/core/src/services/exporter-common/base-export-service.ts +++ b/projects/igniteui-angular/grids/core/src/services/exporter-common/base-export-service.ts @@ -1,18 +1,10 @@ import { EventEmitter } from '@angular/core'; -import { cloneArray, cloneValue, columnFieldPath, IBaseEventArgs, resolveNestedPath, yieldingLoop } from '../../core/utils'; -import { DataUtil } from '../../data-operations/data-util'; import { ExportUtilities } from './export-utilities'; import { IgxExporterOptionsBase } from './exporter-options-base'; -import type { ITreeGridRecord, ColumnType, GridTypeBase, IPathSegment, IgxSummaryResult, GridColumnDataType } from '../../data-operations/grid-types'; -import { GridSummaryCalculationMode } from '../../data-operations/grid-types'; -import { TreeGridFilteringStrategy } from '../../data-operations/tree-grid-filtering-strategy'; -import { IGroupingState } from '../../data-operations/groupby-state.interface'; -import { getHierarchy, isHierarchyMatch } from '../../data-operations/operations'; -import { IGroupByExpandState } from '../../data-operations/groupby-expand-state.interface'; -import { IFilteringState } from '../../data-operations/filtering-state.interface'; +import { type ITreeGridRecord, type ColumnType, type GridTypeBase, type IPathSegment, type IgxSummaryResult, type GridColumnDataType, DataUtil, FilterUtil, GridSummaryCalculationMode, IBaseEventArgs, IFilteringState, IGroupByExpandState, IGroupByRecord, IGroupingState, TreeGridFilteringStrategy, cloneArray, cloneValue, columnFieldPath, resolveNestedPath, yieldingLoop, getHierarchy, isHierarchyMatch } from 'igniteui-angular/core'; + import { DatePipe, FormatWidth, getLocaleCurrencyCode, getLocaleDateFormat, getLocaleDateTimeFormat } from '@angular/common'; -import { IGroupByRecord } from '../../data-operations/groupby-record.interface'; -import { FilterUtil } from '../../data-operations/filtering-strategy'; + export enum ExportRecordType { GroupedRecord = 'GroupedRecord', @@ -639,7 +631,7 @@ export abstract class IgxBaseExporter { const columnFields = this._ownersMap.get(grid).columns.map(col => col.field); for (const entry of records) { - const expansionStateVal = grid.expansionStates.has(entry) ? grid.expansionStates.get(entry) : false; + const expansionStateVal = grid.expansionStates.has(entry) ? grid.expansionStates.get(entry) : grid.getDefaultExpandState(entry); const dataWithoutChildren = Object.keys(entry) .filter(k => columnFields.includes(k)) diff --git a/projects/igniteui-angular/core/src/services/exporter-common/export-utilities.ts b/projects/igniteui-angular/grids/core/src/services/exporter-common/export-utilities.ts similarity index 100% rename from projects/igniteui-angular/core/src/services/exporter-common/export-utilities.ts rename to projects/igniteui-angular/grids/core/src/services/exporter-common/export-utilities.ts diff --git a/projects/igniteui-angular/core/src/services/exporter-common/exporter-options-base.ts b/projects/igniteui-angular/grids/core/src/services/exporter-common/exporter-options-base.ts similarity index 100% rename from projects/igniteui-angular/core/src/services/exporter-common/exporter-options-base.ts rename to projects/igniteui-angular/grids/core/src/services/exporter-common/exporter-options-base.ts diff --git a/projects/igniteui-angular/core/src/services/exporter-common/test-methods.spec.ts b/projects/igniteui-angular/grids/core/src/services/exporter-common/test-methods.spec.ts similarity index 88% rename from projects/igniteui-angular/core/src/services/exporter-common/test-methods.spec.ts rename to projects/igniteui-angular/grids/core/src/services/exporter-common/test-methods.spec.ts index fb22affc592..9ec3323543d 100644 --- a/projects/igniteui-angular/core/src/services/exporter-common/test-methods.spec.ts +++ b/projects/igniteui-angular/grids/core/src/services/exporter-common/test-methods.spec.ts @@ -1,9 +1,9 @@ import { TestBed } from '@angular/core/testing'; -import { GridIDNameJobTitleComponent } from '../../../../test-utils/grid-samples.spec'; -import { IgxStringFilteringOperand } from '../../data-operations/filtering-condition'; -import { wait } from '../../../../test-utils/ui-interactions.spec'; +import { GridIDNameJobTitleComponent } from '../../../../../test-utils/grid-samples.spec'; +import { wait } from '../../../../../test-utils/ui-interactions.spec'; import { IgxGridComponent } from 'igniteui-angular/grids/grid'; +import { IgxStringFilteringOperand } from 'igniteui-angular/core'; export class TestMethods { diff --git a/projects/igniteui-angular/grids/core/src/services/pdf/pdf-exporter-grid.spec.ts b/projects/igniteui-angular/grids/core/src/services/pdf/pdf-exporter-grid.spec.ts new file mode 100644 index 00000000000..f41085540c2 --- /dev/null +++ b/projects/igniteui-angular/grids/core/src/services/pdf/pdf-exporter-grid.spec.ts @@ -0,0 +1,514 @@ +import { TestBed, waitForAsync } from '@angular/core/testing'; +import { ExportUtilities } from '../exporter-common/export-utilities'; +import { IgxPdfExporterService } from './pdf-exporter'; +import { IgxPdfExporterOptions } from './pdf-exporter-options'; +import { GridIDNameJobTitleComponent } from '../../../../../test-utils/grid-samples.spec'; +import { first } from 'rxjs/operators'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { NestedColumnGroupsGridComponent, ColumnGroupTestComponent, BlueWhaleGridComponent } from '../../../../../test-utils/grid-mch-sample.spec'; +import { IgxHierarchicalGridTestBaseComponent } from '../../../../../test-utils/hierarchical-grid-components.spec'; +import { IgxTreeGridSortingComponent, IgxTreeGridPrimaryForeignKeyComponent } from '../../../../../test-utils/tree-grid-components.spec'; +import { CustomSummariesComponent } from 'igniteui-angular/grids/grid/src/grid-summary.spec'; +import { IgxHierarchicalGridComponent } from 'igniteui-angular/grids/hierarchical-grid'; +import { IgxPivotGridMultipleRowComponent, IgxPivotGridTestComplexHierarchyComponent } from '../../../../../test-utils/pivot-grid-samples.spec'; +import { IgxPivotGridComponent } from 'igniteui-angular/grids/pivot-grid'; +import { PivotRowLayoutType } from 'igniteui-angular/grids/core'; +import { wait } from 'igniteui-angular/test-utils/ui-interactions.spec'; + +describe('PDF Grid Exporter', () => { + let exporter: IgxPdfExporterService; + let options: IgxPdfExporterOptions; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + NoopAnimationsModule, + GridIDNameJobTitleComponent, + IgxPivotGridMultipleRowComponent, + IgxPivotGridTestComplexHierarchyComponent + ] + }).compileComponents(); + })); + + beforeEach(() => { + exporter = new IgxPdfExporterService(); + options = new IgxPdfExporterOptions('PdfGridExport'); + + // Spy the saveBlobToFile method so the files are not really created + spyOn(ExportUtilities as any, 'saveBlobToFile'); + }); + + it('should export grid as displayed.', (done) => { + const fix = TestBed.createComponent(GridIDNameJobTitleComponent); + fix.detectChanges(); + + const grid = fix.componentInstance.grid; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.export(grid, options); + }); + + it('should export grid with custom page orientation', (done) => { + const fix = TestBed.createComponent(GridIDNameJobTitleComponent); + fix.detectChanges(); + + const grid = fix.componentInstance.grid; + options.pageOrientation = 'landscape'; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.export(grid, options); + }); + + it('should honor ignoreColumnsVisibility option', (done) => { + const fix = TestBed.createComponent(GridIDNameJobTitleComponent); + fix.detectChanges(); + + const grid = fix.componentInstance.grid; + grid.columnList.get(0).hidden = true; + options.ignoreColumnsVisibility = false; + + fix.detectChanges(); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.export(grid, options); + }); + + it('should handle empty grid', (done) => { + const fix = TestBed.createComponent(GridIDNameJobTitleComponent); + fix.detectChanges(); + + const grid = fix.componentInstance.grid; + grid.data = []; + fix.detectChanges(); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.export(grid, options); + }); + + it('should export grid with landscape orientation', (done) => { + const fix = TestBed.createComponent(GridIDNameJobTitleComponent); + fix.detectChanges(); + + const grid = fix.componentInstance.grid; + options.pageOrientation = 'landscape'; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.export(grid, options); + }); + + it('should export with table borders disabled', (done) => { + const fix = TestBed.createComponent(GridIDNameJobTitleComponent); + fix.detectChanges(); + + const grid = fix.componentInstance.grid; + options.showTableBorders = false; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.export(grid, options); + }); + + it('should export with custom font size', (done) => { + const fix = TestBed.createComponent(GridIDNameJobTitleComponent); + fix.detectChanges(); + + const grid = fix.componentInstance.grid; + options.fontSize = 14; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.export(grid, options); + }); + + it('should export with different page sizes', (done) => { + const fix = TestBed.createComponent(GridIDNameJobTitleComponent); + fix.detectChanges(); + + const grid = fix.componentInstance.grid; + options.pageSize = 'letter'; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.export(grid, options); + }); + + it('should honor ignoreColumnsOrder option', (done) => { + const fix = TestBed.createComponent(GridIDNameJobTitleComponent); + fix.detectChanges(); + + const grid = fix.componentInstance.grid; + options.ignoreColumnsOrder = true; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.export(grid, options); + }); + + it('should honor ignoreFiltering option', (done) => { + const fix = TestBed.createComponent(GridIDNameJobTitleComponent); + fix.detectChanges(); + + const grid = fix.componentInstance.grid; + options.ignoreFiltering = false; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.export(grid, options); + }); + + it('should honor ignoreSorting option', (done) => { + const fix = TestBed.createComponent(GridIDNameJobTitleComponent); + fix.detectChanges(); + + const grid = fix.componentInstance.grid; + options.ignoreSorting = false; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.export(grid, options); + }); + + + + it('should handle grid with multiple columns', (done) => { + const fix = TestBed.createComponent(GridIDNameJobTitleComponent); + fix.detectChanges(); + + const grid = fix.componentInstance.grid; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.export(grid, options); + }); + + it('should export with custom filename from options', (done) => { + const fix = TestBed.createComponent(GridIDNameJobTitleComponent); + fix.detectChanges(); + + const grid = fix.componentInstance.grid; + const customOptions = new IgxPdfExporterOptions('MyCustomGrid'); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + const callArgs = (ExportUtilities.saveBlobToFile as jasmine.Spy).calls.mostRecent().args; + expect(callArgs[1]).toBe('MyCustomGrid.pdf'); + done(); + }); + + exporter.export(grid, customOptions); + }); + + it('should export grid with multi-column headers', (done) => { + TestBed.configureTestingModule({ + imports: [ + NoopAnimationsModule, + ColumnGroupTestComponent + ] + }).compileComponents(); + + const fix = TestBed.createComponent(ColumnGroupTestComponent); + fix.detectChanges(); + + const grid = fix.componentInstance.grid; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.export(grid, options); + }); + + it('should export grid with nested multi-column headers', (done) => { + TestBed.configureTestingModule({ + imports: [ + NoopAnimationsModule, + NestedColumnGroupsGridComponent + ] + }).compileComponents(); + + const fix = TestBed.createComponent(NestedColumnGroupsGridComponent); + fix.detectChanges(); + + const grid = fix.componentInstance.grid; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.export(grid, options); + }); + + it('should export grid with summaries', (done) => { + TestBed.configureTestingModule({ + imports: [ + NoopAnimationsModule, + CustomSummariesComponent + ] + }).compileComponents(); + + const fix = TestBed.createComponent(CustomSummariesComponent); + fix.detectChanges(); + + const grid = fix.componentInstance.grid; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.export(grid, options); + }); + + it('should export hierarchical grid', (done) => { + TestBed.configureTestingModule({ + imports: [ + NoopAnimationsModule, + IgxHierarchicalGridTestBaseComponent + ] + }).compileComponents(); + + const fix = TestBed.createComponent(IgxHierarchicalGridTestBaseComponent); + fix.detectChanges(); + + const grid = fix.componentInstance.hgrid; + grid.expandChildren = true; + grid.getChildGrids().forEach((childGrid: IgxHierarchicalGridComponent) => { + childGrid.expandChildren = true; + }); + fix.detectChanges(); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.export(grid, options); + }); + + it('should export tree grid with hierarchical data', (done) => { + TestBed.configureTestingModule({ + imports: [ + NoopAnimationsModule, + IgxTreeGridSortingComponent + ] + }).compileComponents(); + + const fix = TestBed.createComponent(IgxTreeGridSortingComponent); + fix.detectChanges(); + + const grid = fix.componentInstance.treeGrid; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.export(grid, options); + }); + + it('should export tree grid with flat self-referencing data', (done) => { + TestBed.configureTestingModule({ + imports: [ + NoopAnimationsModule, + IgxTreeGridPrimaryForeignKeyComponent + ] + }).compileComponents(); + + const fix = TestBed.createComponent(IgxTreeGridPrimaryForeignKeyComponent); + fix.detectChanges(); + + const grid = fix.componentInstance.treeGrid; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.export(grid, options); + }); + + it('should truncate long header text with ellipsis in multi-column headers', (done) => { + TestBed.configureTestingModule({ + imports: [ + NoopAnimationsModule, + BlueWhaleGridComponent + ] + }).compileComponents(); + + const fix = TestBed.createComponent(BlueWhaleGridComponent); + fix.detectChanges(); + + const grid = fix.componentInstance.grid; + + exporter.exportEnded.pipe(first()).subscribe((args) => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + // The PDF should be created successfully even with long header text + expect(args.pdf).toBeDefined(); + done(); + }); + + // Use smaller page size to force truncation + options.pageSize = 'a5'; + exporter.export(grid, options); + }); + + describe('Pivot Grid PDF Export', () => { + let pivotGrid: IgxPivotGridComponent; + let fix; + beforeEach(async () => { + fix = TestBed.createComponent(IgxPivotGridMultipleRowComponent); + fix.detectChanges(); + await wait(); + + pivotGrid = fix.componentInstance.pivotGrid; + }); + + it('should export basic pivot grid', (done) => { + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.export(pivotGrid, options); + }); + + it('should export pivot grid with row headers', (done) => { + pivotGrid.pivotUI.showRowHeaders = true; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.export(pivotGrid, options); + }); + + it('should export pivot grid with horizontal row layout', (done) => { + pivotGrid.pivotUI.showRowHeaders = true; + pivotGrid.pivotUI.rowLayout = PivotRowLayoutType.Horizontal; + pivotGrid.pivotConfiguration.rows = [{ + memberName: 'ProductCategory', + memberFunction: (data) => data.ProductCategory, + enabled: true, + childLevel: { + memberName: 'Country', + enabled: true, + childLevel: { + memberName: 'Date', + enabled: true + } + } + }]; + fix.detectChanges(); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.export(pivotGrid, options); + }); + + it('should export pivot grid with custom page size', (done) => { + options.pageSize = 'letter'; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.export(pivotGrid, options); + }); + + it('should export pivot grid with landscape orientation', (done) => { + options.pageOrientation = 'landscape'; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.export(pivotGrid, options); + }); + + it('should export pivot grid without table borders', (done) => { + options.showTableBorders = false; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.export(pivotGrid, options); + }); + + it('should export pivot grid with custom font size', (done) => { + options.fontSize = 14; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.export(pivotGrid, options); + }); + + it('should export hierarchical pivot grid', (done) => { + fix = TestBed.createComponent(IgxPivotGridTestComplexHierarchyComponent); + fix.detectChanges(); + fix.whenStable().then(() => { + pivotGrid = fix.componentInstance.pivotGrid; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.export(pivotGrid, options); + }); + }); + }); +}); diff --git a/projects/igniteui-angular/grids/core/src/services/pdf/pdf-exporter-options.ts b/projects/igniteui-angular/grids/core/src/services/pdf/pdf-exporter-options.ts new file mode 100644 index 00000000000..5f8e6fa4a47 --- /dev/null +++ b/projects/igniteui-angular/grids/core/src/services/pdf/pdf-exporter-options.ts @@ -0,0 +1,54 @@ +import { IgxExporterOptionsBase } from '../exporter-common/exporter-options-base'; + +/** + * Objects of this class are used to configure the PDF exporting process. + */ +export class IgxPdfExporterOptions extends IgxExporterOptionsBase { + /** + * Specifies the page orientation. (portrait or landscape, landscape by default) + * ```typescript + * let pageOrientation = this.exportOptions.pageOrientation; + * this.exportOptions.pageOrientation = 'portrait'; + * ``` + * + * @memberof IgxPdfExporterOptions + */ + public pageOrientation: 'portrait' | 'landscape' = 'landscape'; + + /** + * Specifies the page size. (a4, a3, letter, legal, etc., a4 by default) + * ```typescript + * let pageSize = this.exportOptions.pageSize; + * this.exportOptions.pageSize = 'letter'; + * ``` + * + * @memberof IgxPdfExporterOptions + */ + public pageSize: string = 'a4'; + + /** + * Specifies whether to show table borders. (True by default) + * ```typescript + * let showTableBorders = this.exportOptions.showTableBorders; + * this.exportOptions.showTableBorders = false; + * ``` + * + * @memberof IgxPdfExporterOptions + */ + public showTableBorders = true; + + /** + * Specifies the font size for the table content. (10 by default) + * ```typescript + * let fontSize = this.exportOptions.fontSize; + * this.exportOptions.fontSize = 12; + * ``` + * + * @memberof IgxPdfExporterOptions + */ + public fontSize = 10; + + constructor(fileName: string) { + super(fileName, '.pdf'); + } +} diff --git a/projects/igniteui-angular/grids/core/src/services/pdf/pdf-exporter.spec.ts b/projects/igniteui-angular/grids/core/src/services/pdf/pdf-exporter.spec.ts new file mode 100644 index 00000000000..2dbde14d859 --- /dev/null +++ b/projects/igniteui-angular/grids/core/src/services/pdf/pdf-exporter.spec.ts @@ -0,0 +1,2891 @@ +import { ExportUtilities } from '../exporter-common/export-utilities'; +import { IgxPdfExporterService } from './pdf-exporter'; +import { IgxPdfExporterOptions } from './pdf-exporter-options'; +import { SampleTestData } from '../../../../../test-utils/sample-test-data.spec'; +import { first } from 'rxjs/operators'; +import { ExportRecordType, ExportHeaderType, DEFAULT_OWNER, IExportRecord, IColumnInfo, IColumnList, GRID_LEVEL_COL } from '../exporter-common/base-export-service'; + +describe('PDF Exporter', () => { + let exporter: IgxPdfExporterService; + let options: IgxPdfExporterOptions; + + beforeEach(() => { + exporter = new IgxPdfExporterService(); + options = new IgxPdfExporterOptions('PdfExport'); + + // Clear owners map between tests + (exporter as any)._ownersMap.clear(); + + // Spy the saveBlobToFile method so the files are not really created + spyOn(ExportUtilities, 'saveBlobToFile'); + }); + + it('should be created', () => { + expect(exporter).toBeTruthy(); + }); + + it('should export empty data without errors', (done) => { + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData([], options); + }); + + it('should export simple data successfully', (done) => { + const simpleData = [ + { Name: 'John', Age: 30 }, + { Name: 'Jane', Age: 25 } + ]; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(simpleData, options); + }); + + it('should export contacts data successfully', (done) => { + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(SampleTestData.contactsData(), options); + }); + + it('should export with custom page orientation', (done) => { + options.pageOrientation = 'landscape'; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(SampleTestData.contactsData(), options); + }); + + it('should export with custom page size', (done) => { + options.pageSize = 'letter'; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(SampleTestData.contactsData(), options); + }); + + it('should export without table borders', (done) => { + options.showTableBorders = false; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(SampleTestData.contactsData(), options); + }); + + it('should export with custom font size', (done) => { + options.fontSize = 12; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(SampleTestData.contactsData(), options); + }); + + it('should handle null and undefined values', (done) => { + const dataWithNulls = [ + { Name: 'John', Age: null }, + { Name: undefined, Age: 25 } + ]; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(dataWithNulls, options); + }); + + it('should handle date values', (done) => { + const dataWithDates = [ + { Name: 'John', BirthDate: new Date('1990-01-01') }, + { Name: 'Jane', BirthDate: new Date('1995-06-15') } + ]; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(dataWithDates, options); + }); + + it('should export with portrait orientation', (done) => { + options.pageOrientation = 'portrait'; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(SampleTestData.contactsData(), options); + }); + + it('should export with various page sizes', (done) => { + const pageSizes = ['a3', 'a5', 'legal']; + let completed = 0; + + const exportNext = (index: number) => { + if (index >= pageSizes.length) { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(pageSizes.length); + done(); + return; + } + + const opts = new IgxPdfExporterOptions('Test'); + opts.pageSize = pageSizes[index] as any; + + exporter.exportEnded.pipe(first()).subscribe(() => { + completed++; + exportNext(completed); + }); + + exporter.exportData(SampleTestData.contactsData(), opts); + }; + + exportNext(0); + }); + + it('should export with different font sizes', (done) => { + options.fontSize = 14; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(SampleTestData.contactsData(), options); + }); + + it('should export large dataset requiring pagination', (done) => { + const largeData = []; + for (let i = 0; i < 100; i++) { + largeData.push({ Name: `Person ${i}`, Age: 20 + (i % 50), City: `City ${i % 10}` }); + } + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(largeData, options); + }); + + it('should handle long text values with truncation', (done) => { + const dataWithLongText = [ + { Name: 'John', Description: 'This is a very long description that should be truncated with ellipsis in the PDF export to fit within the cell width' }, + { Name: 'Jane', Description: 'Another extremely long text that needs to be handled properly in the PDF export without breaking the layout' } + ]; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(dataWithLongText, options); + }); + + it('should export data with mixed data types', (done) => { + const mixedData = [ + { String: 'Text', Number: 42, Boolean: true, Date: new Date('2023-01-01'), Null: null, Undefined: undefined }, + { String: 'More text', Number: 3.14, Boolean: false, Date: new Date('2023-12-31'), Null: null, Undefined: undefined } + ]; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(mixedData, options); + }); + + it('should export with custom filename', (done) => { + const customOptions = new IgxPdfExporterOptions('CustomFileName'); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + const callArgs = (ExportUtilities.saveBlobToFile as jasmine.Spy).calls.mostRecent().args; + expect(callArgs[1]).toBe('CustomFileName.pdf'); + done(); + }); + + exporter.exportData(SampleTestData.contactsData(), customOptions); + }); + + it('should handle empty rows in data', (done) => { + const dataWithEmptyRows = [ + { Name: 'John', Age: 30 }, + {}, + { Name: 'Jane', Age: 25 } + ]; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(dataWithEmptyRows, options); + }); + + it('should emit exportEnded event with pdf object', (done) => { + exporter.exportEnded.pipe(first()).subscribe((args) => { + expect(args).toBeDefined(); + expect(args.pdf).toBeDefined(); + done(); + }); + + exporter.exportData(SampleTestData.contactsData(), options); + }); + + describe('Pivot Grid Export', () => { + it('should export pivot grid with single dimension', (done) => { + const pivotData: IExportRecord[] = [ + { + data: { Product: 'Product A', 'City-London-Sum': 100, 'City-Paris-Sum': 200 }, + level: 0, + type: ExportRecordType.PivotGridRecord, + dimensionKeys: ['Product'] + }, + { + data: { Product: 'Product B', 'City-London-Sum': 150, 'City-Paris-Sum': 250 }, + level: 0, + type: ExportRecordType.PivotGridRecord, + dimensionKeys: ['Product'] + } + ]; + + const columns: IColumnInfo[] = [ + { + header: 'Product', + field: 'Product', + skip: false, + headerType: ExportHeaderType.PivotRowHeader, + startIndex: 0, + level: 0 + }, + { + header: 'London', + field: 'City', + skip: false, + headerType: ExportHeaderType.MultiColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1, + columnGroup: 'London' + }, + { + header: 'Sum', + field: 'City-London-Sum', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 1, + columnSpan: 1, + columnGroupParent: 'London' + }, + { + header: 'Paris', + field: 'City', + skip: false, + headerType: ExportHeaderType.MultiColumnHeader, + startIndex: 1, + level: 0, + columnSpan: 1, + columnGroup: 'Paris' + }, + { + header: 'Sum', + field: 'City-Paris-Sum', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 1, + level: 1, + columnSpan: 1, + columnGroupParent: 'Paris' + } + ]; + + const owner: IColumnList = { + columns: columns, + columnWidths: [200, 200, 200], + indexOfLastPinnedColumn: 0, + maxLevel: 1, + maxRowLevel: 1 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(pivotData, options); + }); + + it('should export multi-dimensional pivot grid with multiple row dimensions', (done) => { + const pivotData: IExportRecord[] = [ + { + data: { Product: 'Product A', Category: 'Category 1', 'City-London-Sum': 100, 'City-Paris-Sum': 200 }, + level: 0, + type: ExportRecordType.PivotGridRecord, + dimensionKeys: ['Product', 'Category'] + }, + { + data: { Product: 'Product A', Category: 'Category 2', 'City-London-Sum': 150, 'City-Paris-Sum': 250 }, + level: 1, + type: ExportRecordType.PivotGridRecord, + dimensionKeys: ['Product', 'Category'] + }, + { + data: { Product: 'Product B', Category: 'Category 1', 'City-London-Sum': 120, 'City-Paris-Sum': 220 }, + level: 0, + type: ExportRecordType.PivotGridRecord, + dimensionKeys: ['Product', 'Category'] + } + ]; + + const columns: IColumnInfo[] = [ + { + header: 'Product', + field: 'Product', + skip: false, + headerType: ExportHeaderType.PivotRowHeader, + startIndex: 0, + level: 0 + }, + { + header: 'Category', + field: 'Category', + skip: false, + headerType: ExportHeaderType.PivotRowHeader, + startIndex: 1, + level: 1 + }, + { + header: 'London', + field: 'City', + skip: false, + headerType: ExportHeaderType.MultiColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1, + columnGroup: 'London' + }, + { + header: 'Sum', + field: 'City-London-Sum', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 1, + columnSpan: 1, + columnGroupParent: 'London' + }, + { + header: 'Paris', + field: 'City', + skip: false, + headerType: ExportHeaderType.MultiColumnHeader, + startIndex: 1, + level: 0, + columnSpan: 1, + columnGroup: 'Paris' + }, + { + header: 'Sum', + field: 'City-Paris-Sum', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 1, + level: 1, + columnSpan: 1, + columnGroupParent: 'Paris' + }, + { + header: 'Product A', + field: 'Product', + skip: false, + headerType: ExportHeaderType.RowHeader, + startIndex: 0, + level: 0 + }, + { + header: 'Category 1', + field: 'Category', + skip: false, + headerType: ExportHeaderType.RowHeader, + startIndex: 0, + level: 1 + } + ]; + + const owner: IColumnList = { + columns: columns, + columnWidths: [200, 200, 200, 200], + indexOfLastPinnedColumn: 1, + maxLevel: 1, + maxRowLevel: 2 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(pivotData, options); + }); + + it('should export pivot grid with row dimension headers and multi-level column headers', (done) => { + const pivotData: IExportRecord[] = [ + { + data: { Product: 'Product A', 'City-London-Sum': 100, 'City-London-Avg': 50, 'City-Paris-Sum': 200 }, + level: 0, + type: ExportRecordType.PivotGridRecord, + dimensionKeys: ['Product'] + } + ]; + + const columns: IColumnInfo[] = [ + { + header: 'Product', + field: 'Product', + skip: false, + headerType: ExportHeaderType.PivotRowHeader, + startIndex: 0, + level: 0 + }, + { + header: 'London', + field: 'City', + skip: false, + headerType: ExportHeaderType.MultiColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 2, + columnGroup: 'London' + }, + { + header: 'Sum', + field: 'City-London-Sum', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 1, + columnSpan: 1, + columnGroupParent: 'London' + }, + { + header: 'Avg', + field: 'City-London-Avg', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 1, + level: 1, + columnSpan: 1, + columnGroupParent: 'London' + }, + { + header: 'Paris', + field: 'City', + skip: false, + headerType: ExportHeaderType.MultiColumnHeader, + startIndex: 2, + level: 0, + columnSpan: 1, + columnGroup: 'Paris' + }, + { + header: 'Sum', + field: 'City-Paris-Sum', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 2, + level: 1, + columnSpan: 1, + columnGroupParent: 'Paris' + } + ]; + + const owner: IColumnList = { + columns: columns, + columnWidths: [200, 200, 200, 200], + indexOfLastPinnedColumn: 0, + maxLevel: 1, + maxRowLevel: 1 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(pivotData, options); + }); + + it('should export pivot grid with PivotMergedHeader columns', (done) => { + const pivotData: IExportRecord[] = [ + { + data: { Product: 'Product A', 'City-London-Sum': 100 }, + level: 0, + type: ExportRecordType.PivotGridRecord, + dimensionKeys: ['Product'] + } + ]; + + const columns: IColumnInfo[] = [ + { + header: 'Product', + field: 'Product', + skip: false, + headerType: ExportHeaderType.PivotRowHeader, + startIndex: 0, + level: 0 + }, + { + header: '', + field: '', + skip: false, + headerType: ExportHeaderType.PivotMergedHeader, + startIndex: 0, + level: 0 + }, + { + header: 'Sum', + field: 'City-London-Sum', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + } + ]; + + const owner: IColumnList = { + columns: columns, + columnWidths: [200, 200], + indexOfLastPinnedColumn: 0, + maxLevel: 0, + maxRowLevel: 1 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(pivotData, options); + }); + + it('should export pivot grid when dimensionKeys are inferred from record data', (done) => { + const pivotData: IExportRecord[] = [ + { + data: { Product: 'Product A', Category: 'Category 1', 'City-London-Sum': 100 }, + level: 0, + type: ExportRecordType.PivotGridRecord + // No dimensionKeys - should be inferred + } + ]; + + const columns: IColumnInfo[] = [ + { + header: 'Product', + field: 'Product', + skip: false, + headerType: ExportHeaderType.RowHeader, + startIndex: 0, + level: 0 + }, + { + header: 'Category', + field: 'Category', + skip: false, + headerType: ExportHeaderType.RowHeader, + startIndex: 1, + level: 1 + }, + { + header: 'Sum', + field: 'City-London-Sum', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + } + ]; + + const owner: IColumnList = { + columns: columns, + columnWidths: [200, 200, 200], + indexOfLastPinnedColumn: 1, + maxLevel: 0, + maxRowLevel: 2 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(pivotData, options); + }); + + it('should export pivot grid with MultiRowHeader columns', (done) => { + const pivotData: IExportRecord[] = [ + { + data: { Product: 'Product A', Category: 'Category 1', 'City-London-Sum': 100 }, + level: 0, + type: ExportRecordType.PivotGridRecord, + dimensionKeys: ['Product', 'Category'] + }, + { + data: { Product: 'Product A', Category: 'Category 2', 'City-London-Sum': 150 }, + level: 1, + type: ExportRecordType.PivotGridRecord, + dimensionKeys: ['Product', 'Category'] + } + ]; + + const columns: IColumnInfo[] = [ + { + header: 'Product', + field: 'Product', + skip: false, + headerType: ExportHeaderType.PivotRowHeader, + startIndex: 0, + level: 0 + }, + { + header: 'Category', + field: 'Category', + skip: false, + headerType: ExportHeaderType.PivotRowHeader, + startIndex: 1, + level: 1 + }, + { + header: 'Product A', + field: 'Product', + skip: false, + headerType: ExportHeaderType.MultiRowHeader, + startIndex: 0, + level: 0, + rowSpan: 2 + }, + { + header: 'Category 1', + field: 'Category', + skip: false, + headerType: ExportHeaderType.RowHeader, + startIndex: 0, + level: 1 + }, + { + header: 'Category 2', + field: 'Category', + skip: false, + headerType: ExportHeaderType.RowHeader, + startIndex: 1, + level: 1 + }, + { + header: 'Sum', + field: 'City-London-Sum', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + } + ]; + + const owner: IColumnList = { + columns: columns, + columnWidths: [200, 200, 200], + indexOfLastPinnedColumn: 1, + maxLevel: 0, + maxRowLevel: 2 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(pivotData, options); + }); + + it('should export pivot grid with row dimension columns by level', (done) => { + const pivotData: IExportRecord[] = [ + { + data: { Product: 'Product A', Category: 'Category 1', 'City-London-Sum': 100 }, + level: 0, + type: ExportRecordType.PivotGridRecord, + dimensionKeys: ['Product', 'Category'] + }, + { + data: { Product: 'Product A', Category: 'Category 2', 'City-London-Sum': 150 }, + level: 1, + type: ExportRecordType.PivotGridRecord, + dimensionKeys: ['Product', 'Category'] + } + ]; + + const columns: IColumnInfo[] = [ + { + header: 'Product', + field: 'Product', + skip: false, + headerType: ExportHeaderType.PivotRowHeader, + startIndex: 0, + level: 0 + }, + { + header: 'Category', + field: 'Category', + skip: false, + headerType: ExportHeaderType.PivotRowHeader, + startIndex: 1, + level: 1 + }, + { + header: 'Product A', + field: 'Product', + skip: false, + headerType: ExportHeaderType.RowHeader, + startIndex: 0, + level: 0 + }, + { + header: 'Category 1', + field: 'Category', + skip: false, + headerType: ExportHeaderType.RowHeader, + startIndex: 0, + level: 1 + }, + { + header: 'Category 2', + field: 'Category', + skip: false, + headerType: ExportHeaderType.RowHeader, + startIndex: 1, + level: 1 + }, + { + header: 'Sum', + field: 'City-London-Sum', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + } + ]; + + const owner: IColumnList = { + columns: columns, + columnWidths: [200, 200, 200], + indexOfLastPinnedColumn: 1, + maxLevel: 0, + maxRowLevel: 2 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(pivotData, options); + }); + }); + + describe('Hierarchical Grid Export', () => { + it('should export hierarchical grid with child records', (done) => { + const childOwner = 'child1'; + const childColumns: IColumnInfo[] = [ + { + header: 'Child Name', + field: 'name', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + }, + { + header: 'Child Age', + field: 'age', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 1, + level: 0, + columnSpan: 1 + } + ]; + + const childOwnerList: IColumnList = { + columns: childColumns, + columnWidths: [200, 200], + indexOfLastPinnedColumn: -1, + maxLevel: 0 + }; + + (exporter as any)._ownersMap.set(childOwner, childOwnerList); + + const parentColumns: IColumnInfo[] = [ + { + header: 'Name', + field: 'name', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + }, + { + header: 'Age', + field: 'age', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 1, + level: 0, + columnSpan: 1 + } + ]; + + const parentOwner: IColumnList = { + columns: parentColumns, + columnWidths: [200, 200], + indexOfLastPinnedColumn: -1, + maxLevel: 0 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, parentOwner); + + const hierarchicalData: IExportRecord[] = [ + { + data: { name: 'Parent 1', age: 40 }, + level: 0, + type: ExportRecordType.HierarchicalGridRecord, + owner: DEFAULT_OWNER + }, + { + data: { name: 'Child 1', age: 10 }, + level: 1, + type: ExportRecordType.HierarchicalGridRecord, + owner: childOwner + }, + { + data: { name: 'Child 2', age: 12 }, + level: 1, + type: ExportRecordType.HierarchicalGridRecord, + owner: childOwner + }, + { + data: { name: 'Parent 2', age: 45 }, + level: 0, + type: ExportRecordType.HierarchicalGridRecord, + owner: DEFAULT_OWNER + } + ]; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(hierarchicalData, options); + }); + + it('should export hierarchical grid with multiple child levels', (done) => { + const grandChildOwner = 'grandchild1'; + const childOwner = 'child1'; + + const grandChildColumns: IColumnInfo[] = [ + { + header: 'Grandchild Name', + field: 'name', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + } + ]; + + const grandChildOwnerList: IColumnList = { + columns: grandChildColumns, + columnWidths: [200], + indexOfLastPinnedColumn: -1, + maxLevel: 0 + }; + + (exporter as any)._ownersMap.set(grandChildOwner, grandChildOwnerList); + + const childColumns: IColumnInfo[] = [ + { + header: 'Child Name', + field: 'name', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + } + ]; + + const childOwnerList: IColumnList = { + columns: childColumns, + columnWidths: [200], + indexOfLastPinnedColumn: -1, + maxLevel: 0 + }; + + (exporter as any)._ownersMap.set(childOwner, childOwnerList); + + const parentColumns: IColumnInfo[] = [ + { + header: 'Name', + field: 'name', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + } + ]; + + const parentOwner: IColumnList = { + columns: parentColumns, + columnWidths: [200], + indexOfLastPinnedColumn: -1, + maxLevel: 0 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, parentOwner); + + const hierarchicalData: IExportRecord[] = [ + { + data: { name: 'Parent 1' }, + level: 0, + type: ExportRecordType.HierarchicalGridRecord, + owner: DEFAULT_OWNER + }, + { + data: { name: 'Child 1' }, + level: 1, + type: ExportRecordType.HierarchicalGridRecord, + owner: childOwner + }, + { + data: { name: 'Grandchild 1' }, + level: 2, + type: ExportRecordType.HierarchicalGridRecord, + owner: grandChildOwner + }, + { + data: { name: 'Grandchild 2' }, + level: 2, + type: ExportRecordType.HierarchicalGridRecord, + owner: grandChildOwner + } + ]; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(hierarchicalData, options); + }); + + it('should export hierarchical grid with multi-level headers in child grid', (done) => { + const childOwner = 'child1'; + + const childColumns: IColumnInfo[] = [ + { + header: 'Location', + field: 'location', + skip: false, + headerType: ExportHeaderType.MultiColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 2 + }, + { + header: 'City', + field: 'city', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 1, + columnSpan: 1 + }, + { + header: 'Country', + field: 'country', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 1, + level: 1, + columnSpan: 1 + } + ]; + + const childOwnerList: IColumnList = { + columns: childColumns, + columnWidths: [200, 200], + indexOfLastPinnedColumn: -1, + maxLevel: 1 + }; + + (exporter as any)._ownersMap.set(childOwner, childOwnerList); + + const parentColumns: IColumnInfo[] = [ + { + header: 'Name', + field: 'name', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + } + ]; + + const parentOwner: IColumnList = { + columns: parentColumns, + columnWidths: [200], + indexOfLastPinnedColumn: -1, + maxLevel: 0 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, parentOwner); + + const hierarchicalData: IExportRecord[] = [ + { + data: { name: 'Parent 1' }, + level: 0, + type: ExportRecordType.HierarchicalGridRecord, + owner: DEFAULT_OWNER + }, + { + data: { city: 'London', country: 'UK' }, + level: 1, + type: ExportRecordType.HierarchicalGridRecord, + owner: childOwner + } + ]; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(hierarchicalData, options); + }); + }); + + describe('Tree Grid Export', () => { + it('should export tree grid with hierarchical levels', (done) => { + const treeData: IExportRecord[] = [ + { + data: { name: 'Root 1', value: 100 }, + level: 0, + type: ExportRecordType.TreeGridRecord + }, + { + data: { name: 'Child 1', value: 50 }, + level: 1, + type: ExportRecordType.TreeGridRecord + }, + { + data: { name: 'Grandchild 1', value: 25 }, + level: 2, + type: ExportRecordType.TreeGridRecord + }, + { + data: { name: 'Root 2', value: 200 }, + level: 0, + type: ExportRecordType.TreeGridRecord + } + ]; + + const columns: IColumnInfo[] = [ + { + header: 'Name', + field: 'name', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + }, + { + header: 'Value', + field: 'value', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 1, + level: 0, + columnSpan: 1 + } + ]; + + const owner: IColumnList = { + columns: columns, + columnWidths: [200, 200], + indexOfLastPinnedColumn: -1, + maxLevel: 0 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(treeData, options); + }); + }); + + describe('Summary Records Export', () => { + it('should export summary records with label and value', (done) => { + const summaryData: IExportRecord[] = [ + { + data: { name: 'Total', value: { label: 'Sum', value: 500 } }, + level: 0, + type: ExportRecordType.SummaryRecord + } + ]; + + const columns: IColumnInfo[] = [ + { + header: 'Name', + field: 'name', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + }, + { + header: 'Value', + field: 'value', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 1, + level: 0, + columnSpan: 1 + } + ]; + + const owner: IColumnList = { + columns: columns, + columnWidths: [200, 200], + indexOfLastPinnedColumn: -1, + maxLevel: 0 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(summaryData, options); + }); + + it('should export summary records with summaryResult property', (done) => { + const summaryData: IExportRecord[] = [ + { + data: { name: 'Total', value: { summaryResult: 1000 } }, + level: 0, + type: ExportRecordType.SummaryRecord + } + ]; + + const columns: IColumnInfo[] = [ + { + header: 'Name', + field: 'name', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + }, + { + header: 'Value', + field: 'value', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 1, + level: 0, + columnSpan: 1 + } + ]; + + const owner: IColumnList = { + columns: columns, + columnWidths: [200, 200], + indexOfLastPinnedColumn: -1, + maxLevel: 0 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(summaryData, options); + }); + }); + + describe('Edge Cases and Special Scenarios', () => { + it('should skip hidden records', (done) => { + const dataWithHidden: IExportRecord[] = [ + { + data: { Name: 'Visible', Age: 30 }, + level: 0, + type: ExportRecordType.DataRecord + }, + { + data: { Name: 'Hidden', Age: 25 }, + level: 0, + type: ExportRecordType.DataRecord, + hidden: true + }, + { + data: { Name: 'Visible 2', Age: 35 }, + level: 0, + type: ExportRecordType.DataRecord + } + ]; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(dataWithHidden, options); + }); + + it('should handle pagination when data exceeds page height', (done) => { + const largeData: IExportRecord[] = []; + for (let i = 0; i < 50; i++) { + largeData.push({ + data: { Name: `Person ${i}`, Age: 20 + i, City: `City ${i % 10}` }, + level: 0, + type: ExportRecordType.DataRecord + }); + } + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(largeData, options); + }); + + it('should handle pivot grid with empty row dimension fields', (done) => { + const pivotData: IExportRecord[] = [ + { + data: { 'City-London-Sum': 100 }, + level: 0, + type: ExportRecordType.PivotGridRecord, + dimensionKeys: [] + } + ]; + + const columns: IColumnInfo[] = [ + { + header: 'Sum', + field: 'City-London-Sum', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + } + ]; + + const owner: IColumnList = { + columns: columns, + columnWidths: [200], + indexOfLastPinnedColumn: -1, + maxLevel: 0, + maxRowLevel: 0 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(pivotData, options); + }); + + it('should handle pivot grid when no columns are defined', (done) => { + const pivotData: IExportRecord[] = [ + { + data: { Product: 'Product A', Value: 100 }, + level: 0, + type: ExportRecordType.PivotGridRecord + } + ]; + + const owner: IColumnList = { + columns: [], + columnWidths: [], + indexOfLastPinnedColumn: -1, + maxLevel: 0 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(pivotData, options); + }); + + it('should handle pivot grid with row dimension headers longer than fields', (done) => { + const pivotData: IExportRecord[] = [ + { + data: { Product: 'Product A', 'City-London-Sum': 100 }, + level: 0, + type: ExportRecordType.PivotGridRecord, + dimensionKeys: ['Product'] + } + ]; + + const columns: IColumnInfo[] = [ + { + header: 'Product', + field: 'Product', + skip: false, + headerType: ExportHeaderType.PivotRowHeader, + startIndex: 0, + level: 0 + }, + { + header: 'Category', + field: 'Category', + skip: false, + headerType: ExportHeaderType.PivotRowHeader, + startIndex: 1, + level: 1 + }, + { + header: 'Sum', + field: 'City-London-Sum', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + } + ]; + + const owner: IColumnList = { + columns: columns, + columnWidths: [200, 200, 200], + indexOfLastPinnedColumn: 1, + maxLevel: 0, + maxRowLevel: 2 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(pivotData, options); + }); + + it('should handle pivot grid with date values in row dimensions', (done) => { + const pivotData: IExportRecord[] = [ + { + data: { Date: new Date('2023-01-01'), 'City-London-Sum': 100 }, + level: 0, + type: ExportRecordType.PivotGridRecord, + dimensionKeys: ['Date'] + } + ]; + + const columns: IColumnInfo[] = [ + { + header: 'Date', + field: 'Date', + skip: false, + headerType: ExportHeaderType.PivotRowHeader, + startIndex: 0, + level: 0 + }, + { + header: 'Sum', + field: 'City-London-Sum', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + } + ]; + + const owner: IColumnList = { + columns: columns, + columnWidths: [200, 200], + indexOfLastPinnedColumn: 0, + maxLevel: 0, + maxRowLevel: 1 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(pivotData, options); + }); + + it('should handle hierarchical grid with HeaderRecord type', (done) => { + const childOwner = 'child1'; + const childColumns: IColumnInfo[] = [ + { + header: 'Name', + field: 'name', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + } + ]; + + const childOwnerList: IColumnList = { + columns: childColumns, + columnWidths: [200], + indexOfLastPinnedColumn: -1, + maxLevel: 0 + }; + + (exporter as any)._ownersMap.set(childOwner, childOwnerList); + + const parentColumns: IColumnInfo[] = [ + { + header: 'Name', + field: 'name', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + } + ]; + + const parentOwner: IColumnList = { + columns: parentColumns, + columnWidths: [200], + indexOfLastPinnedColumn: -1, + maxLevel: 0 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, parentOwner); + + const hierarchicalData: IExportRecord[] = [ + { + data: { name: 'Parent 1' }, + level: 0, + type: ExportRecordType.HierarchicalGridRecord, + owner: DEFAULT_OWNER + }, + { + data: {}, + level: 1, + type: ExportRecordType.HeaderRecord, + owner: childOwner + }, + { + data: { name: 'Child 1' }, + level: 1, + type: ExportRecordType.HierarchicalGridRecord, + owner: childOwner + } + ]; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(hierarchicalData, options); + }); + + it('should handle hierarchical grid with empty child columns', (done) => { + const childOwner = 'child1'; + const childOwnerList: IColumnList = { + columns: [], + columnWidths: [], + indexOfLastPinnedColumn: -1, + maxLevel: 0 + }; + + (exporter as any)._ownersMap.set(childOwner, childOwnerList); + + const parentColumns: IColumnInfo[] = [ + { + header: 'Name', + field: 'name', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + } + ]; + + const parentOwner: IColumnList = { + columns: parentColumns, + columnWidths: [200], + indexOfLastPinnedColumn: -1, + maxLevel: 0 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, parentOwner); + + const hierarchicalData: IExportRecord[] = [ + { + data: { name: 'Parent 1' }, + level: 0, + type: ExportRecordType.HierarchicalGridRecord, + owner: DEFAULT_OWNER + }, + { + data: { name: 'Child 1' }, + level: 1, + type: ExportRecordType.HierarchicalGridRecord, + owner: childOwner + } + ]; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(hierarchicalData, options); + }); + + it('should handle pagination with hierarchical grid', (done) => { + const childOwner = 'child1'; + const childColumns: IColumnInfo[] = [ + { + header: 'Child Name', + field: 'name', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + } + ]; + + const childOwnerList: IColumnList = { + columns: childColumns, + columnWidths: [200], + indexOfLastPinnedColumn: -1, + maxLevel: 0 + }; + + (exporter as any)._ownersMap.set(childOwner, childOwnerList); + + const parentColumns: IColumnInfo[] = [ + { + header: 'Name', + field: 'name', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + } + ]; + + const parentOwner: IColumnList = { + columns: parentColumns, + columnWidths: [200], + indexOfLastPinnedColumn: -1, + maxLevel: 0 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, parentOwner); + + const hierarchicalData: IExportRecord[] = []; + // Create many parent-child pairs to trigger pagination + for (let i = 0; i < 30; i++) { + hierarchicalData.push({ + data: { name: `Parent ${i}` }, + level: 0, + type: ExportRecordType.HierarchicalGridRecord, + owner: DEFAULT_OWNER + }); + hierarchicalData.push({ + data: { name: `Child ${i}` }, + level: 1, + type: ExportRecordType.HierarchicalGridRecord, + owner: childOwner + }); + } + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(hierarchicalData, options); + }); + }); + + describe('Additional Edge Cases and Error Paths', () => { + it('should handle pivot grid with no defaultOwner', (done) => { + const pivotData: IExportRecord[] = [ + { + data: { Product: 'Product A', 'City-London-Sum': 100 }, + level: 0, + type: ExportRecordType.PivotGridRecord, + dimensionKeys: ['Product'] + } + ]; + + // Don't set DEFAULT_OWNER in the map + (exporter as any)._ownersMap.clear(); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(pivotData, options); + }); + + it('should handle pivot grid dimension inference from columnGroup', (done) => { + const pivotData: IExportRecord[] = [ + { + data: { Product: 'Product A', Category: 'Category 1', 'City-London-Sum': 100 }, + level: 0, + type: ExportRecordType.PivotGridRecord + // No dimensionKeys - should be inferred + } + ]; + + const columns: IColumnInfo[] = [ + { + header: 'Product', + field: 'Product', + skip: false, + headerType: ExportHeaderType.RowHeader, + startIndex: 0, + level: 0, + columnGroup: 'Product' + }, + { + header: 'Category', + field: 'Category', + skip: false, + headerType: ExportHeaderType.RowHeader, + startIndex: 1, + level: 1, + columnGroupParent: 'Product' + }, + { + header: 'Sum', + field: 'City-London-Sum', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + } + ]; + + const owner: IColumnList = { + columns: columns, + columnWidths: [200, 200, 200], + indexOfLastPinnedColumn: 1, + maxLevel: 0, + maxRowLevel: 2 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(pivotData, options); + }); + + it('should handle pivot grid with simple keys inference', (done) => { + const pivotData: IExportRecord[] = [ + { + data: { SimpleKey: 'Value1', 'Complex-Key-With-Separators': 100, 'Another_Complex_Key': 200 }, + level: 0, + type: ExportRecordType.PivotGridRecord + // No dimensionKeys and no matching row headers + } + ]; + + const columns: IColumnInfo[] = [ + { + header: 'Sum', + field: 'Complex-Key-With-Separators', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + } + ]; + + const owner: IColumnList = { + columns: columns, + columnWidths: [200], + indexOfLastPinnedColumn: -1, + maxLevel: 0, + maxRowLevel: 0 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(pivotData, options); + }); + + it('should handle pivot grid with row dimension headers longer than fields and trim them', (done) => { + const pivotData: IExportRecord[] = [ + { + data: { Product: 'Product A', 'City-London-Sum': 100 }, + level: 0, + type: ExportRecordType.PivotGridRecord, + dimensionKeys: ['Product'] + } + ]; + + const columns: IColumnInfo[] = [ + { + header: 'Product', + field: 'Product', + skip: false, + headerType: ExportHeaderType.PivotRowHeader, + startIndex: 0, + level: 0 + }, + { + header: 'Category', + field: 'Category', + skip: false, + headerType: ExportHeaderType.PivotRowHeader, + startIndex: 1, + level: 1 + }, + { + header: 'SubCategory', + field: 'SubCategory', + skip: false, + headerType: ExportHeaderType.PivotRowHeader, + startIndex: 2, + level: 2 + }, + { + header: 'Sum', + field: 'City-London-Sum', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + } + ]; + + const owner: IColumnList = { + columns: columns, + columnWidths: [200, 200, 200, 200], + indexOfLastPinnedColumn: 2, + maxLevel: 0, + maxRowLevel: 3 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(pivotData, options); + }); + + it('should handle multi-level headers with empty headersForLevel', (done) => { + const columns: IColumnInfo[] = [ + { + header: 'Name', + field: 'name', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + }, + { + header: 'Parent', + field: 'parent', + skip: false, + headerType: ExportHeaderType.MultiColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1, + columnGroup: 'Parent' + }, + { + header: 'Child', + field: 'child', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 1, + columnSpan: 1, + columnGroupParent: 'Parent' + } + ]; + + const owner: IColumnList = { + columns: columns, + columnWidths: [200, 200, 200], + indexOfLastPinnedColumn: -1, + maxLevel: 2 // Level 2 exists but no columns at that level + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + const data: IExportRecord[] = [ + { + data: { name: 'Test', parent: 'Parent', child: 'Child' }, + level: 0, + type: ExportRecordType.DataRecord + } + ]; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(data, options); + }); + + it('should handle columns with skip: true', (done) => { + const columns: IColumnInfo[] = [ + { + header: 'Name', + field: 'name', + skip: true, // Should be skipped + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + }, + { + header: 'Age', + field: 'age', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 1, + level: 0, + columnSpan: 1 + } + ]; + + const owner: IColumnList = { + columns: columns, + columnWidths: [200, 200], + indexOfLastPinnedColumn: -1, + maxLevel: 0 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + const data: IExportRecord[] = [ + { + data: { name: 'John', age: 30 }, + level: 0, + type: ExportRecordType.DataRecord + } + ]; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(data, options); + }); + + it('should handle GRID_LEVEL_COL column', (done) => { + const columns: IColumnInfo[] = [ + { + header: 'Name', + field: 'name', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + }, + { + header: GRID_LEVEL_COL, + field: GRID_LEVEL_COL, + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 1, + level: 0, + columnSpan: 1 + } + ]; + + const owner: IColumnList = { + columns: columns, + columnWidths: [200, 200], + indexOfLastPinnedColumn: -1, + maxLevel: 0 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + const data: IExportRecord[] = [ + { + data: { name: 'John', [GRID_LEVEL_COL]: 0 }, + level: 0, + type: ExportRecordType.DataRecord + } + ]; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(data, options); + }); + + it('should handle records with missing data property', (done) => { + const data: IExportRecord[] = [ + { + data: { Name: 'John', Age: 30 }, + level: 0, + type: ExportRecordType.DataRecord + }, + { + data: undefined as any, + level: 0, + type: ExportRecordType.DataRecord + } + ]; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(data, options); + }); + + it('should handle pivot grid with fuzzy key matching', (done) => { + const pivotData: IExportRecord[] = [ + { + data: { 'ProductName': 'Product A', 'City-London-Sum': 100 }, + level: 0, + type: ExportRecordType.PivotGridRecord, + dimensionKeys: ['Product'] // Field name doesn't match exactly + } + ]; + + const columns: IColumnInfo[] = [ + { + header: 'Product', + field: 'Product', + skip: false, + headerType: ExportHeaderType.PivotRowHeader, + startIndex: 0, + level: 0 + }, + { + header: 'Sum', + field: 'City-London-Sum', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + } + ]; + + const owner: IColumnList = { + columns: columns, + columnWidths: [200, 200], + indexOfLastPinnedColumn: 0, + maxLevel: 0, + maxRowLevel: 1 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(pivotData, options); + }); + + it('should handle pivot grid with possible dimension keys by index fallback', (done) => { + const pivotData: IExportRecord[] = [ + { + data: { SimpleKey1: 'Value1', SimpleKey2: 'Value2', 'Complex-Key': 100 }, + level: 0, + type: ExportRecordType.PivotGridRecord, + dimensionKeys: ['UnknownKey'] // Key doesn't exist in data + } + ]; + + const columns: IColumnInfo[] = [ + { + header: 'Unknown', + field: 'UnknownKey', + skip: false, + headerType: ExportHeaderType.PivotRowHeader, + startIndex: 0, + level: 0 + }, + { + header: 'Sum', + field: 'Complex-Key', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + } + ]; + + const owner: IColumnList = { + columns: columns, + columnWidths: [200, 200], + indexOfLastPinnedColumn: 0, + maxLevel: 0, + maxRowLevel: 1 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(pivotData, options); + }); + + it('should handle summary records with only label', (done) => { + const summaryData: IExportRecord[] = [ + { + data: { name: 'Total', value: { label: 'Sum' } }, + level: 0, + type: ExportRecordType.SummaryRecord + } + ]; + + const columns: IColumnInfo[] = [ + { + header: 'Name', + field: 'name', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + }, + { + header: 'Value', + field: 'value', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 1, + level: 0, + columnSpan: 1 + } + ]; + + const owner: IColumnList = { + columns: columns, + columnWidths: [200, 200], + indexOfLastPinnedColumn: -1, + maxLevel: 0 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(summaryData, options); + }); + + it('should handle summary records with only value', (done) => { + const summaryData: IExportRecord[] = [ + { + data: { name: 'Total', value: { value: 500 } }, + level: 0, + type: ExportRecordType.SummaryRecord + } + ]; + + const columns: IColumnInfo[] = [ + { + header: 'Name', + field: 'name', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + }, + { + header: 'Value', + field: 'value', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 1, + level: 0, + columnSpan: 1 + } + ]; + + const owner: IColumnList = { + columns: columns, + columnWidths: [200, 200], + indexOfLastPinnedColumn: -1, + maxLevel: 0 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(summaryData, options); + }); + + it('should handle pivot grid with empty PivotRowHeader columns', (done) => { + const pivotData: IExportRecord[] = [ + { + data: { Product: 'Product A', 'City-London-Sum': 100 }, + level: 0, + type: ExportRecordType.PivotGridRecord, + dimensionKeys: ['Product'] + } + ]; + + const columns: IColumnInfo[] = [ + { + header: '', + field: '', + skip: false, + headerType: ExportHeaderType.PivotRowHeader, + startIndex: 0, + level: 0 + }, + { + header: 'Sum', + field: 'City-London-Sum', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + } + ]; + + const owner: IColumnList = { + columns: columns, + columnWidths: [200, 200], + indexOfLastPinnedColumn: 0, + maxLevel: 0, + maxRowLevel: 1 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(pivotData, options); + }); + + it('should handle hierarchical grid with owner not in map', (done) => { + const childOwner = 'nonexistent-owner'; + const parentColumns: IColumnInfo[] = [ + { + header: 'Name', + field: 'name', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + } + ]; + + const parentOwner: IColumnList = { + columns: parentColumns, + columnWidths: [200], + indexOfLastPinnedColumn: -1, + maxLevel: 0 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, parentOwner); + // Don't set childOwner in map + + const hierarchicalData: IExportRecord[] = [ + { + data: { name: 'Parent 1' }, + level: 0, + type: ExportRecordType.HierarchicalGridRecord, + owner: DEFAULT_OWNER + }, + { + data: { name: 'Child 1' }, + level: 1, + type: ExportRecordType.HierarchicalGridRecord, + owner: childOwner + } + ]; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(hierarchicalData, options); + }); + + it('should handle tree grid with undefined level', (done) => { + const treeData: IExportRecord[] = [ + { + data: { name: 'Root 1', value: 100 }, + level: undefined as any, + type: ExportRecordType.TreeGridRecord + } + ]; + + const columns: IColumnInfo[] = [ + { + header: 'Name', + field: 'name', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + }, + { + header: 'Value', + field: 'value', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 1, + level: 0, + columnSpan: 1 + } + ]; + + const owner: IColumnList = { + columns: columns, + columnWidths: [200, 200], + indexOfLastPinnedColumn: -1, + maxLevel: 0 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(treeData, options); + }); + + it('should handle pivot grid with columnGroupParent as non-string', (done) => { + const pivotData: IExportRecord[] = [ + { + data: { Product: 'Product A', Category: 'Category 1', 'City-London-Sum': 100 }, + level: 0, + type: ExportRecordType.PivotGridRecord + } + ]; + + const columns: IColumnInfo[] = [ + { + header: 'Product', + field: 'Product', + skip: false, + headerType: ExportHeaderType.RowHeader, + startIndex: 0, + level: 0, + columnGroup: { id: 'product' } as any // Non-string columnGroup + }, + { + header: 'Category', + field: 'Category', + skip: false, + headerType: ExportHeaderType.RowHeader, + startIndex: 1, + level: 1, + columnGroupParent: { id: 'product' } as any // Non-string columnGroupParent + }, + { + header: 'Sum', + field: 'City-London-Sum', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + } + ]; + + const owner: IColumnList = { + columns: columns, + columnWidths: [200, 200, 200], + indexOfLastPinnedColumn: 1, + maxLevel: 0, + maxRowLevel: 2 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(pivotData, options); + }); + + it('should handle pivot grid with column header matching record values', (done) => { + const pivotData: IExportRecord[] = [ + { + data: { Product: 'Product A', 'City-London-Sum': 100 }, + level: 0, + type: ExportRecordType.PivotGridRecord, + dimensionKeys: ['Product'] + } + ]; + + const columns: IColumnInfo[] = [ + { + header: 'Product A', // Header matches value in data + field: 'Product', + skip: false, + headerType: ExportHeaderType.RowHeader, + startIndex: 0, + level: 0 + }, + { + header: 'Sum', + field: 'City-London-Sum', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + } + ]; + + const owner: IColumnList = { + columns: columns, + columnWidths: [200, 200], + indexOfLastPinnedColumn: 0, + maxLevel: 0, + maxRowLevel: 1 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(pivotData, options); + }); + + it('should handle pivot grid with record index-based column selection', (done) => { + const pivotData: IExportRecord[] = [ + { + data: { Product: 'Product A', 'City-London-Sum': 100 }, + level: 0, + type: ExportRecordType.PivotGridRecord, + dimensionKeys: ['Product'] + }, + { + data: { Product: 'Product B', 'City-London-Sum': 200 }, + level: 0, + type: ExportRecordType.PivotGridRecord, + dimensionKeys: ['Product'] + } + ]; + + const columns: IColumnInfo[] = [ + { + header: 'Product', + field: 'Product', + skip: false, + headerType: ExportHeaderType.PivotRowHeader, + startIndex: 0, + level: 0 + }, + { + header: 'Product A', + field: 'Product', + skip: false, + headerType: ExportHeaderType.RowHeader, + startIndex: 0, + level: 0 + }, + { + header: 'Product B', + field: 'Product', + skip: false, + headerType: ExportHeaderType.RowHeader, + startIndex: 1, + level: 0 + }, + { + header: 'Sum', + field: 'City-London-Sum', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + } + ]; + + const owner: IColumnList = { + columns: columns, + columnWidths: [200, 200], + indexOfLastPinnedColumn: 0, + maxLevel: 0, + maxRowLevel: 1 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(pivotData, options); + }); + + it('should handle pivot grid with empty allColumns in drawDataRow', (done) => { + const pivotData: IExportRecord[] = [ + { + data: { Product: 'Product A', 'City-London-Sum': 100 }, + level: 0, + type: ExportRecordType.PivotGridRecord, + dimensionKeys: ['Product'] + } + ]; + + const columns: IColumnInfo[] = [ + { + header: 'Sum', + field: 'City-London-Sum', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + } + ]; + + const owner: IColumnList = { + columns: columns, + columnWidths: [200], + indexOfLastPinnedColumn: -1, + maxLevel: 0, + maxRowLevel: 0 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(pivotData, options); + }); + + it('should handle very long header text truncation', (done) => { + const longHeaderText = 'This is a very long header text that should be truncated because it exceeds the maximum width of the column header cell in the PDF export'; + const columns: IColumnInfo[] = [ + { + header: longHeaderText, + field: 'name', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + } + ]; + + const owner: IColumnList = { + columns: columns, + columnWidths: [200], + indexOfLastPinnedColumn: -1, + maxLevel: 0 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + const data: IExportRecord[] = [ + { + data: { name: 'Test' }, + level: 0, + type: ExportRecordType.DataRecord + } + ]; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(data, options); + }); + + it('should handle pivot grid with row dimension columns but no matching data', (done) => { + const pivotData: IExportRecord[] = [ + { + data: { 'City-London-Sum': 100 }, // No dimension fields in data + level: 0, + type: ExportRecordType.PivotGridRecord, + dimensionKeys: ['Product'] // But dimensionKeys says Product exists + } + ]; + + const columns: IColumnInfo[] = [ + { + header: 'Product', + field: 'Product', + skip: false, + headerType: ExportHeaderType.PivotRowHeader, + startIndex: 0, + level: 0 + }, + { + header: 'Sum', + field: 'City-London-Sum', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + } + ]; + + const owner: IColumnList = { + columns: columns, + columnWidths: [200, 200], + indexOfLastPinnedColumn: 0, + maxLevel: 0, + maxRowLevel: 1 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(pivotData, options); + }); + + it('should handle column field as non-string gracefully', (done) => { + // This test verifies that non-string fields are handled without crashing + // The base exporter may filter these out, so we test with valid data structure + const columns: IColumnInfo[] = [ + { + header: 'Name', + field: 'name', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + }, + { + header: 'Value', + field: 'value', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 1, + level: 0, + columnSpan: 1 + } + ]; + + const owner: IColumnList = { + columns: columns, + columnWidths: [200, 200], + indexOfLastPinnedColumn: -1, + maxLevel: 0 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + const data: IExportRecord[] = [ + { + data: { name: 'Test', value: 123 }, + level: 0, + type: ExportRecordType.DataRecord + } + ]; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(data, options); + }); + + it('should handle empty rowDimensionHeaders fallback path', (done) => { + const pivotData: IExportRecord[] = [ + { + data: { Product: 'Product A', 'City-London-Sum': 100 }, + level: 0, + type: ExportRecordType.PivotGridRecord, + dimensionKeys: ['Product'] + } + ]; + + const columns: IColumnInfo[] = [ + { + header: 'Sum', + field: 'City-London-Sum', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + } + ]; + + const owner: IColumnList = { + columns: columns, + columnWidths: [200], + indexOfLastPinnedColumn: -1, + maxLevel: 0, + maxRowLevel: 0 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(pivotData, options); + }); + + it('should handle PivotMergedHeader with empty header text', (done) => { + const pivotData: IExportRecord[] = [ + { + data: { Product: 'Product A', 'City-London-Sum': 100 }, + level: 0, + type: ExportRecordType.PivotGridRecord, + dimensionKeys: ['Product'] + } + ]; + + const columns: IColumnInfo[] = [ + { + header: '', + field: '', + skip: false, + headerType: ExportHeaderType.PivotMergedHeader, + startIndex: 0, + level: 0 + }, + { + header: 'Sum', + field: 'City-London-Sum', + skip: false, + headerType: ExportHeaderType.ColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1 + } + ]; + + const owner: IColumnList = { + columns: columns, + columnWidths: [200, 200], + indexOfLastPinnedColumn: 0, + maxLevel: 0, + maxRowLevel: 1 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(pivotData, options); + }); + + it('should handle resolveLayoutStartIndex with no child columns', (done) => { + const columns: IColumnInfo[] = [ + { + header: 'Parent', + field: 'parent', + skip: false, + headerType: ExportHeaderType.MultiColumnHeader, + startIndex: 0, + level: 0, + columnSpan: 1, + columnGroup: 'Parent' + // No child columns with columnGroupParent === 'Parent' + } + ]; + + const owner: IColumnList = { + columns: columns, + columnWidths: [200], + indexOfLastPinnedColumn: -1, + maxLevel: 1 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + const data: IExportRecord[] = [ + { + data: { parent: 'Value' }, + level: 0, + type: ExportRecordType.DataRecord + } + ]; + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(data, options); + }); + + it('should handle data with zero total columns', (done) => { + const data: IExportRecord[] = [ + { + data: {}, + level: 0, + type: ExportRecordType.DataRecord + } + ]; + + const owner: IColumnList = { + columns: [], + columnWidths: [], + indexOfLastPinnedColumn: -1, + maxLevel: 0 + }; + + (exporter as any)._ownersMap.set(DEFAULT_OWNER, owner); + + exporter.exportEnded.pipe(first()).subscribe(() => { + expect(ExportUtilities.saveBlobToFile).toHaveBeenCalledTimes(1); + done(); + }); + + exporter.exportData(data, options); + }); + }); +}); diff --git a/projects/igniteui-angular/grids/core/src/services/pdf/pdf-exporter.ts b/projects/igniteui-angular/grids/core/src/services/pdf/pdf-exporter.ts new file mode 100644 index 00000000000..2b0c4299011 --- /dev/null +++ b/projects/igniteui-angular/grids/core/src/services/pdf/pdf-exporter.ts @@ -0,0 +1,1142 @@ +import { EventEmitter, Injectable } from '@angular/core'; +import { DEFAULT_OWNER, ExportHeaderType, ExportRecordType, GRID_LEVEL_COL, IExportRecord, IgxBaseExporter } from '../exporter-common/base-export-service'; +import { ExportUtilities } from '../exporter-common/export-utilities'; +import { IgxPdfExporterOptions } from './pdf-exporter-options'; +import { IBaseEventArgs } from 'igniteui-angular/core'; +import type { jsPDF } from 'jspdf'; + +export interface IPdfExportEndedEventArgs extends IBaseEventArgs { + pdf?: jsPDF; +} + +/** + * **Ignite UI for Angular PDF Exporter Service** - + * [Documentation](https://www.infragistics.com/products/ignite-ui-angular/angular/components/exporter_pdf.html) + * + * The Ignite UI for Angular PDF Exporter service can export data in PDF format from both raw data + * (array) or from an `IgxGrid`. + * + * Example: + * ```typescript + * public localData = [ + * { Name: "Eric Ridley", Age: "26" }, + * { Name: "Alanis Brook", Age: "22" }, + * { Name: "Jonathan Morris", Age: "23" } + * ]; + * + * constructor(private pdfExportService: IgxPdfExporterService) { + * } + * + * this.pdfExportService.exportData(this.localData, new IgxPdfExporterOptions("FileName")); + * ``` + */ +@Injectable({ + providedIn: 'root', +}) +export class IgxPdfExporterService extends IgxBaseExporter { + + /** + * This event is emitted when the export process finishes. + * ```typescript + * this.exporterService.exportEnded.subscribe((args: IPdfExportEndedEventArgs) => { + * // put event handler code here + * }); + * ``` + * + * @memberof IgxPdfExporterService + */ + public override exportEnded = new EventEmitter(); + + protected exportDataImplementation(data: IExportRecord[], options: IgxPdfExporterOptions, done: () => void): void { + const firstDataElement = data[0]; + const isHierarchicalGrid = firstDataElement?.type === ExportRecordType.HierarchicalGridRecord; + const isPivotGrid = firstDataElement?.type === ExportRecordType.PivotGridRecord; + + const defaultOwner = isHierarchicalGrid ? + this._ownersMap.get(firstDataElement.owner) : + this._ownersMap.get(DEFAULT_OWNER); + + // Get all columns (including multi-column headers) + const allColumns = defaultOwner?.columns.filter(col => !col.skip) || []; + + // Extract pivot grid row dimension fields (these are in the data, rendered as row headers) + // For pivot grids, the row dimension fields appear in each record's data + const rowDimensionFields: string[] = []; + const rowDimensionHeaders: string[] = []; + if (isPivotGrid && defaultOwner) { + const uniqueFields = new Set(); + + // Primary source: use dimensionKeys from the first record (set by base exporter) + // This is the authoritative source for dimension field names + if (firstDataElement?.dimensionKeys && Array.isArray(firstDataElement.dimensionKeys) && firstDataElement.dimensionKeys.length > 0) { + firstDataElement.dimensionKeys.forEach(key => { + if (!uniqueFields.has(key)) { + uniqueFields.add(key); + rowDimensionFields.push(key); + } + }); + } + + // If we still don't have fields, try to get them from the record data + if (rowDimensionFields.length === 0 && firstDataElement && firstDataElement.data) { + // Fallback: Try to infer dimension keys from the record data structure + // Get row dimension columns to understand the structure + const rowHeaderCols = allColumns.filter(col => + (col.headerType === ExportHeaderType.RowHeader || + col.headerType === ExportHeaderType.MultiRowHeader || + col.headerType === ExportHeaderType.PivotMergedHeader) && + !col.skip + ); + + const recordKeys = Object.keys(firstDataElement.data); + // Try to match row dimension columns to record keys + rowHeaderCols.forEach(col => { + const fieldName = typeof col.field === 'string' ? col.field : null; + const columnGroup = typeof col.columnGroup === 'string' ? col.columnGroup : + (typeof col.columnGroupParent === 'string' ? col.columnGroupParent : null); + // Check if the field or column group exists in record data + if (fieldName && recordKeys.includes(fieldName) && !uniqueFields.has(fieldName)) { + uniqueFields.add(fieldName); + rowDimensionFields.push(fieldName); + } else if (columnGroup && recordKeys.includes(columnGroup) && !uniqueFields.has(columnGroup)) { + uniqueFields.add(columnGroup); + rowDimensionFields.push(columnGroup); + } + }); + + // If still no fields found, use the first few simple keys from record data + // (dimension keys are usually simple, aggregation keys are often complex) + if (rowDimensionFields.length === 0) { + const simpleKeys = recordKeys.filter(key => { + // Dimension keys are typically simple (no separators, reasonable length) + return !key.includes('-') && !key.includes('_') && + key.length < 50 && + key === key.trim(); + }); + // Take up to the number of row dimensions (usually 1-3) + simpleKeys.slice(0, Math.min(3, simpleKeys.length)).forEach(key => { + if (!uniqueFields.has(key)) { + uniqueFields.add(key); + rowDimensionFields.push(key); + } + }); + } + } + + // Ensure we have at least some fields - if not, we can't display dimension values + // In this case, we'll still draw the columns but they'll be empty + + // Get PivotRowHeader columns - these are the dimension names (like "All My Products", "Product", "City") + // These should match the enabled row dimensions in order + const pivotRowHeaders = allColumns + .filter(col => col.headerType === ExportHeaderType.PivotRowHeader) + .sort((a, b) => (a.startIndex ?? 0) - (b.startIndex ?? 0)); + + // Use PivotRowHeader names as column headers + const sortedPivotRowHeaders = pivotRowHeaders.map(col => col.header || col.field).filter(h => h); + rowDimensionHeaders.push(...sortedPivotRowHeaders); + + // For hierarchical dimensions, we might need to add child level headers + // Check if we have row dimension columns at different levels that aren't covered by PivotRowHeaders + if (rowDimensionHeaders.length < rowDimensionFields.length) { + // Get row dimension columns to find missing headers + const rowHeaderCols = allColumns + .filter(col => + (col.headerType === ExportHeaderType.RowHeader || + col.headerType === ExportHeaderType.MultiRowHeader || + col.headerType === ExportHeaderType.PivotMergedHeader) && + col.field && + !col.skip + ) + .sort((a, b) => { + const levelDiff = (a.level ?? 0) - (b.level ?? 0); + if (levelDiff !== 0) return levelDiff; + return (a.startIndex ?? 0) - (b.startIndex ?? 0); + }); + + // Add missing headers using the header property from row dimension columns + const existingHeaders = new Set(rowDimensionHeaders); + rowHeaderCols.forEach(col => { + const fieldName = typeof col.field === 'string' ? col.field : null; + const headerName = (typeof col.header === 'string' ? col.header : fieldName) || ''; + // If this field is in rowDimensionFields but header is missing, add it + if (fieldName && rowDimensionFields.includes(fieldName) && !existingHeaders.has(headerName)) { + // Only add if we haven't reached the target count + if (rowDimensionHeaders.length < rowDimensionFields.length) { + rowDimensionHeaders.push(headerName); + existingHeaders.add(headerName); + } + } + }); + + // If still missing, use field names + for (let i = rowDimensionHeaders.length; i < rowDimensionFields.length; i++) { + rowDimensionHeaders.push(rowDimensionFields[i]); + } + } else if (rowDimensionHeaders.length > rowDimensionFields.length) { + // Trim excess headers to match fields count + rowDimensionHeaders.splice(rowDimensionFields.length); + } + } + + // Get leaf columns (actual data columns), excluding GRID_LEVEL_COL and row dimension fields + // For pivot grids, we need to exclude row dimension fields since they're rendered separately + let leafColumns = allColumns.filter(col => { + if (col.field === GRID_LEVEL_COL) return false; + if (col.headerType !== ExportHeaderType.ColumnHeader) return false; + // For pivot grids, exclude row dimension fields from regular columns + if (isPivotGrid && rowDimensionFields.includes(col.field)) return false; + return true; + }); + + // Sort leaf columns by startIndex to maintain proper order + leafColumns = leafColumns.sort((a, b) => (a.startIndex ?? 0) - (b.startIndex ?? 0)); + + // Check if we have multi-level headers + const maxLevel = defaultOwner?.maxLevel || 0; + const maxRowLevel = defaultOwner?.maxRowLevel || 0; + const hasMultiColumnHeaders = maxLevel > 0 && allColumns.some(col => col.headerType === ExportHeaderType.MultiColumnHeader); + const hasMultiRowHeaders = maxRowLevel > 0 && rowDimensionFields.length > 0; + + if (leafColumns.length === 0 && data.length > 0 && firstDataElement) { + // If no columns are defined, use the keys from the first data record + const keys = Object.keys(firstDataElement.data); + + keys.forEach((key) => { + leafColumns.push({ + header: key, + field: key, + skip: false, + headerType: ExportHeaderType.ColumnHeader, + columnSpan: 1, + startIndex: 0 + }); + }); + } + // Dynamically import jsPDF to reduce initial bundle size + import('jspdf').then(({ jsPDF }) => { + // Create PDF document + const pdf = new jsPDF({ + orientation: options.pageOrientation, + unit: 'pt', + format: options.pageSize + }); + + const pageWidth = pdf.internal.pageSize.getWidth(); + const pageHeight = pdf.internal.pageSize.getHeight(); + const margin = 40; + const usableWidth = pageWidth - (2 * margin); + + // Calculate column widths + // For pivot grids with row dimensions, we need space for both row dimension columns and data columns + // Use the maximum of headers and fields to ensure we have space for all columns + // Headers determine how many columns to display, fields determine what data to show + const rowDimensionColumnCount = isPivotGrid ? Math.max(rowDimensionHeaders.length, rowDimensionFields.length) : 0; + const totalColumns = rowDimensionColumnCount + leafColumns.length; + const columnWidth = usableWidth / (totalColumns > 0 ? totalColumns : 1); + const rowHeight = 20; + const headerHeight = 25; + const indentSize = 15; // Indentation per level for hierarchical data (visual indent in first column) + const childTableIndent = 30; // Indent for child tables + + let yPosition = margin; + + // Set font + pdf.setFontSize(options.fontSize); + + // Draw multi-level headers if present + // For pivot grids, always draw row dimension headers if they exist, even if there are no multi-column headers + if (hasMultiColumnHeaders || (isPivotGrid && rowDimensionHeaders.length > 0)) { + yPosition = this.drawMultiLevelHeaders( + pdf, + allColumns, + rowDimensionHeaders, + maxLevel, + maxRowLevel, + margin, + yPosition, + columnWidth, + headerHeight, + usableWidth, + options, + allColumns + ); + } else { + // Draw simple single-level headers + this.drawTableHeaders(pdf, leafColumns, rowDimensionHeaders, margin, yPosition, columnWidth, headerHeight, usableWidth, options); + yPosition += headerHeight; + } + + // Draw data rows + pdf.setFont('helvetica', 'normal'); + + // For pivot grids, get row dimension columns to help with value lookup + const rowDimensionColumnsByLevel: Map = new Map(); + if (isPivotGrid && defaultOwner) { + const allRowDimCols = allColumns.filter(col => + (col.headerType === ExportHeaderType.RowHeader || + col.headerType === ExportHeaderType.MultiRowHeader || + col.headerType === ExportHeaderType.PivotMergedHeader) && + !col.skip + ); + // Group by level + allRowDimCols.forEach(col => { + const level = col.level ?? 0; + if (!rowDimensionColumnsByLevel.has(level)) { + rowDimensionColumnsByLevel.set(level, []); + } + rowDimensionColumnsByLevel.get(level)!.push(col); + }); + // Sort each level by startIndex + rowDimensionColumnsByLevel.forEach((cols, level) => { + cols.sort((a, b) => (a.startIndex ?? 0) - (b.startIndex ?? 0)); + }); + } + + let i = 0; + while (i < data.length) { + const record = data[i]; + + // Skip hidden records (collapsed hierarchy) + if (record.hidden) { + i++; + continue; + } + + // Check if we need a new page + if (yPosition + rowHeight > pageHeight - margin) { + pdf.addPage(); + yPosition = margin; + + // Redraw headers on new page + if (hasMultiColumnHeaders || hasMultiRowHeaders) { + yPosition = this.drawMultiLevelHeaders( + pdf, + allColumns, + rowDimensionHeaders, + maxLevel, + maxRowLevel, + margin, + yPosition, + columnWidth, + headerHeight, + usableWidth, + options, + allColumns + ); + } else { + this.drawTableHeaders(pdf, leafColumns, rowDimensionHeaders, margin, yPosition, columnWidth, headerHeight, usableWidth, options); + yPosition += headerHeight; + } + } + + // Calculate indentation for hierarchical records + // TreeGrid supports both hierarchical data and flat self-referencing data (with foreignKey) + // In both cases, the base exporter sets the level property on TreeGridRecord + const isTreeGrid = record.type === 'TreeGridRecord'; + const recordIsHierarchicalGrid = record.type === 'HierarchicalGridRecord'; + + // For tree grids, indentation is visual (in the first column text) + // For hierarchical grids, we don't use indentation (level determines column offset instead) + const indentLevel = isTreeGrid ? (record.level || 0) : 0; + const indent = indentLevel * indentSize; + + // Draw parent row + this.drawDataRow(pdf, record, leafColumns, rowDimensionFields, margin, yPosition, columnWidth, rowHeight, indent, options, allColumns, isPivotGrid, rowDimensionColumnsByLevel, i, rowDimensionHeaders); + yPosition += rowHeight; + + // For hierarchical grids, check if this record has child records + if (recordIsHierarchicalGrid) { + const allDescendants = []; + + // Collect all descendant records (children, grandchildren, etc.) that belong to this parent + // Child records have a different owner (island object) than the parent + let j = i + 1; + while (j < data.length && data[j].level > record.level) { + // Include all descendants (any level deeper) + if (!data[j].hidden) { + allDescendants.push(data[j]); + } + j++; + } + + // If there are descendant records, draw child table(s) + if (allDescendants.length > 0) { + // Group descendants by owner to separate different child grids + // Owner is the actual island object, not a string + // Only collect DIRECT children (one level deeper) for initial grouping + const directDescendantsByOwner = new Map(); + + for (const desc of allDescendants) { + // Only include records that are exactly one level deeper (direct children) + if (desc.level === record.level + 1) { + const owner = desc.owner; + if (!directDescendantsByOwner.has(owner)) { + directDescendantsByOwner.set(owner, []); + } + directDescendantsByOwner.get(owner)!.push(desc); + } + } + + // Draw each child grid separately with its direct children only + for (const [owner, directChildren] of directDescendantsByOwner) { + yPosition = this.drawHierarchicalChildren( + pdf, + data, + allDescendants, // Pass all descendants so grandchildren can be found + directChildren, + owner, + yPosition, + margin, + childTableIndent, + usableWidth, + pageHeight, + headerHeight, + rowHeight, + options + ); + } + + // Skip the descendant records we just processed + i = j - 1; + } + } + + i++; + } + + // Save the PDF + this.saveFile(pdf, options.fileName); + this.exportEnded.emit({ pdf }); + done(); + }); + } + + private drawMultiLevelHeaders( + pdf: jsPDF, + columns: any[], + rowDimensionHeaders: string[], + maxLevel: number, + maxRowLevel: number, + xStart: number, + yStart: number, + baseColumnWidth: number, + headerHeight: number, + tableWidth: number, + options: IgxPdfExporterOptions, + allColumns?: any[] + ): number { + let yPosition = yStart; + pdf.setFont('helvetica', 'bold'); + + // First, draw row dimension header labels (for pivot grids) if present + // Draw headers if we have any row dimension headers, regardless of maxRowLevel + if (rowDimensionHeaders.length > 0 && allColumns) { + // Get PivotRowHeader columns - these are the dimension header names + const pivotRowHeaderCols = allColumns.filter(col => + col.headerType === ExportHeaderType.PivotRowHeader && + !col.skip + ).sort((a, b) => (a.startIndex ?? 0) - (b.startIndex ?? 0)); + + // Calculate how many header rows the data columns have (cities + number/value = 2 rows) + // The row dimension headers should span across all data column header rows + const dataColumnHeaderRows = maxLevel + 1; // maxLevel is 0-based, so +1 gives us the number of rows + const rowDimensionHeaderRowSpan = Math.max(dataColumnHeaderRows, 1); + + // Draw each PivotRowHeader with rowSpan to span across data column headers + pivotRowHeaderCols.forEach((pivotCol, index) => { + const xPosition = xStart + (index * baseColumnWidth); + const headerText = pivotCol.header || pivotCol.field || rowDimensionHeaders[index] || ''; + const width = baseColumnWidth; + const height = headerHeight * rowDimensionHeaderRowSpan; + + // Skip if this is a merged/empty header that shouldn't be drawn + // PivotMergedHeader columns are typically placeholders and shouldn't be drawn separately + // Also skip if header text is empty and it's not a valid header + if ((pivotCol.headerType === ExportHeaderType.PivotMergedHeader && !headerText) || + (!headerText && !pivotCol.header && !pivotCol.field)) { + return; + } + + // Set fill color to light gray for header background (explicitly set before each cell) + pdf.setFillColor(240, 240, 240); + // Set stroke color to black for borders + pdf.setDrawColor(0, 0, 0); + + if (options.showTableBorders) { + // Draw filled rectangle for background (light gray) + pdf.rect(xPosition, yPosition, width, height, 'F'); + // Draw border (black outline) - this should not fill, just stroke + pdf.rect(xPosition, yPosition, width, height); + } else { + // Even without borders, draw background + pdf.rect(xPosition, yPosition, width, height, 'F'); + } + + // Only draw text if we have content + if (headerText) { + // Center text in merged cell + let displayText = headerText; + const maxTextWidth = width - 10; + + if (pdf.getTextWidth(displayText) > maxTextWidth) { + while (pdf.getTextWidth(displayText + '...') > maxTextWidth && displayText.length > 0) { + displayText = displayText.substring(0, displayText.length - 1); + } + displayText += '...'; + } + + const textWidth = pdf.getTextWidth(displayText); + const textX = xPosition + (width - textWidth) / 2; + const textY = yPosition + (height / 2) + options.fontSize / 3; + + pdf.text(displayText, textX, textY); + } + }); + + // Don't move yPosition yet - data column headers will be drawn at the same yPosition + // We'll move yPosition after drawing all header rows + } else if (rowDimensionHeaders.length > 0) { + // Fallback: draw simple headers without merging + rowDimensionHeaders.forEach((headerText, index) => { + const width = baseColumnWidth; + const height = headerHeight; + const xPosition = xStart + (index * baseColumnWidth); + + if (options.showTableBorders) { + pdf.rect(xPosition, yPosition, width, height, 'F'); + pdf.rect(xPosition, yPosition, width, height); + } + + // Center text in cell + let displayText = headerText || ''; + const maxTextWidth = width - 10; + + if (pdf.getTextWidth(displayText) > maxTextWidth) { + while (pdf.getTextWidth(displayText + '...') > maxTextWidth && displayText.length > 0) { + displayText = displayText.substring(0, displayText.length - 1); + } + displayText += '...'; + } + + const textWidth = pdf.getTextWidth(displayText); + const textX = xPosition + (width - textWidth) / 2; + const textY = yPosition + height / 2 + options.fontSize / 3; + + pdf.text(displayText, textX, textY); + }); + yPosition += headerHeight; + } + + // Filter out row header types and GRID_LEVEL_COL from column rendering + const columnHeaders = columns.filter(col => + col.headerType !== ExportHeaderType.PivotRowHeader && + col.headerType !== ExportHeaderType.RowHeader && + col.headerType !== ExportHeaderType.MultiRowHeader && + col.headerType !== ExportHeaderType.PivotMergedHeader && + col.field !== GRID_LEVEL_COL + ); + + const rowDimensionOffset = rowDimensionHeaders.length * baseColumnWidth; + + const totalHeaderLevels = maxLevel + 1; + + // Map layout positions based on actual leaf order so headers align with child data columns + const headerLayoutMap = new Map(); + const leafHeaders = columnHeaders + .filter(col => col.headerType === ExportHeaderType.ColumnHeader && col.columnSpan > 0) + .sort((a, b) => (a.startIndex ?? 0) - (b.startIndex ?? 0)); + + leafHeaders.forEach((col, idx) => headerLayoutMap.set(col, idx)); + + const resolveLayoutStartIndex = (col: any): number => { + if (headerLayoutMap.has(col)) { + return headerLayoutMap.get(col)!; + } + + if (col.headerType === ExportHeaderType.MultiColumnHeader) { + const childColumns = columnHeaders.filter(child => + child.columnGroupParent === col.columnGroup && child.columnSpan > 0); + const childIndices = childColumns.map(child => resolveLayoutStartIndex(child)); + + if (childIndices.length > 0) { + const minIndex = Math.min(...childIndices); + headerLayoutMap.set(col, minIndex); + return minIndex; + } + } + + headerLayoutMap.set(col, 0); + return 0; + }; + + // Draw column headers level by level (from top/parent to bottom/children) + for (let level = 0; level <= maxLevel; level++) { + // Get headers for this level + const headersForLevel = columnHeaders + .filter(col => + col.level === level && + (col.headerType === ExportHeaderType.MultiColumnHeader || col.headerType === ExportHeaderType.ColumnHeader) + ) + .filter(col => col.columnSpan > 0); + + if (headersForLevel.length === 0) { + yPosition += headerHeight; + continue; + } + + // Sort by startIndex to maintain order + headersForLevel.sort((a, b) => a.startIndex - b.startIndex); + + // Draw each header in this level + headersForLevel.forEach((col, idx) => { + const colSpan = col.columnSpan || 1; + const width = baseColumnWidth * colSpan; + const normalizedStartIndex = resolveLayoutStartIndex(col); + const xPosition = xStart + rowDimensionOffset + (normalizedStartIndex * baseColumnWidth); + const rowSpan = col.headerType === ExportHeaderType.ColumnHeader ? + Math.max(1, (totalHeaderLevels - (col.level ?? 0))) : + 1; + const height = headerHeight * rowSpan; + + if (options.showTableBorders) { + pdf.setFillColor(240, 240, 240); + pdf.setDrawColor(0, 0, 0); + pdf.rect(xPosition, yPosition, width, height, 'F'); + pdf.rect(xPosition, yPosition, width, height); + } + + // Center text in cell with truncation if needed + let headerText = col.header || col.field || ''; + const maxTextWidth = width - 10; // Leave 5px padding on each side + + // Truncate text if it's too long + if (pdf.getTextWidth(headerText) > maxTextWidth) { + while (pdf.getTextWidth(headerText + '...') > maxTextWidth && headerText.length > 0) { + headerText = headerText.substring(0, headerText.length - 1); + } + headerText += '...'; + } + + const textWidth = pdf.getTextWidth(headerText); + const textX = xPosition + (width - textWidth) / 2; + const textY = yPosition + (height / 2) + options.fontSize / 3; + + pdf.text(headerText, textX, textY); + }); + + yPosition += headerHeight; + } + + // After drawing all headers, move yPosition down by the total header height + // For pivot grids with row dimension headers, this should be the max of row dimension header height and data column header height + if (rowDimensionHeaders.length > 0 && allColumns) { + const dataColumnHeaderRows = maxLevel + 1; + const rowDimensionHeaderRowSpan = Math.max(dataColumnHeaderRows, 1); + const totalHeaderHeight = headerHeight * rowDimensionHeaderRowSpan; + yPosition = yStart + totalHeaderHeight; + } + + pdf.setFont('helvetica', 'normal'); + return yPosition; + } + + private drawHierarchicalChildren( + pdf: jsPDF, + allData: IExportRecord[], + allDescendants: IExportRecord[], // All descendants to search for grandchildren + childRecords: IExportRecord[], // Direct children to render at this level + childOwner: any, // Owner is the island object, not a string + yPosition: number, + margin: number, + indentPerLevel: number, + usableWidth: number, + pageHeight: number, + headerHeight: number, + rowHeight: number, + options: IgxPdfExporterOptions + ): number { + // Get columns for this child owner + const childOwnerObj = this._ownersMap.get(childOwner); + + const allChildColumns = childOwnerObj?.columns.filter( + col => col.field !== GRID_LEVEL_COL && !col.skip + ) || []; + + const childColumns = allChildColumns.filter( + col => col.headerType === ExportHeaderType.ColumnHeader + ); + + if (childColumns.length === 0) { + return yPosition; + } + + // Filter out header records - they should not be rendered as data rows + const dataRecords = childRecords.filter(r => r.type !== 'HeaderRecord'); + + if (dataRecords.length === 0) { + return yPosition; + } + + // Add some spacing before child table + yPosition += 5; + + // Calculate available width after indentation + const availableWidth = usableWidth - indentPerLevel; + + // Calculate total column span for proper width distribution + const maxLevel = childOwnerObj?.maxLevel || 0; + + // Fix startIndex for all child columns + let currentIndex = 0; + for (const col of allChildColumns) { + if (col.level === 0 && (col.headerType === ExportHeaderType.MultiColumnHeader || col.headerType === ExportHeaderType.ColumnHeader)) { + col.startIndex = currentIndex; + currentIndex += col.columnSpan || 1; + } + } + + let totalColumnSpan = 0; + if (maxLevel > 0) { + const baseLevelColumns = allChildColumns.filter(col => + col.level === 0 && + (col.headerType === ExportHeaderType.MultiColumnHeader || col.headerType === ExportHeaderType.ColumnHeader) + ); + totalColumnSpan = baseLevelColumns.reduce((sum, col) => sum + (col.columnSpan || 1), 0); + } else { + totalColumnSpan = childColumns.length; + } + + // Recalculate column width based on child's column count and available width + const childColumnWidth = availableWidth / totalColumnSpan; + const actualChildTableWidth = childColumnWidth * totalColumnSpan; + const childTableX = margin + indentPerLevel; + + // Check if we need a new page for headers + if (yPosition + headerHeight > pageHeight - margin) { + pdf.addPage(); + yPosition = margin; + } + + // Draw child table headers + const hasMultiColumnHeaders = maxLevel > 0 && childOwnerObj.columns.some(col => col.headerType === ExportHeaderType.MultiColumnHeader); + + if (hasMultiColumnHeaders) { + yPosition = this.drawMultiLevelHeaders( + pdf, + allChildColumns, + [], // rowDimensionHeaders, if any + maxLevel, + 0, // maxRowLevel + childTableX, + yPosition, + childColumnWidth, + headerHeight, + actualChildTableWidth, + options + ); + } else { + this.drawTableHeaders(pdf, childColumns, [], childTableX, yPosition, childColumnWidth, headerHeight, actualChildTableWidth, options); + yPosition += headerHeight; + } + + // Find the minimum level in these records (direct children of parent) + const minLevel = Math.min(...dataRecords.map(r => r.level)); + + // Process each record at the minimum level (direct children) + const directChildren = dataRecords.filter(r => r.level === minLevel); + + for (const childRecord of directChildren) { + // Check if we need a new page + if (yPosition + rowHeight > pageHeight - margin) { + pdf.addPage(); + yPosition = margin; + // Redraw headers on new page + if (hasMultiColumnHeaders) { + yPosition = this.drawMultiLevelHeaders( + pdf, allChildColumns, [], maxLevel, 0, + childTableX, yPosition, childColumnWidth, headerHeight, + actualChildTableWidth, options + ); + } else { + this.drawTableHeaders(pdf, childColumns, [], childTableX, yPosition, childColumnWidth, headerHeight, actualChildTableWidth, options); + yPosition += headerHeight; + } + } + + // Draw the child record + this.drawDataRow(pdf, childRecord, childColumns, [], childTableX, yPosition, childColumnWidth, rowHeight, 0, options); + yPosition += rowHeight; + + // Check if this child has grandchildren (deeper levels in different child grids) + // Look for grandchildren in allDescendants that are direct descendants of this childRecord + const grandchildrenForThisRecord = allDescendants.filter(r => + r.level === childRecord.level + 1 && r.type !== 'HeaderRecord' + ); + + if (grandchildrenForThisRecord.length > 0) { + // Group grandchildren by their owner (different child islands under this record) + const grandchildrenByOwner = new Map(); + + for (const gc of grandchildrenForThisRecord) { + // Use the actual owner object + const gcOwner = gc.owner; + // Only include grandchildren that have a different owner (separate child grid) + if (gcOwner !== childOwner) { + if (!grandchildrenByOwner.has(gcOwner)) { + grandchildrenByOwner.set(gcOwner, []); + } + grandchildrenByOwner.get(gcOwner)!.push(gc); + } + } + + // Recursively draw each grandchild owner's records with increased indentation + for (const [gcOwner, directGrandchildren] of grandchildrenByOwner) { + yPosition = this.drawHierarchicalChildren( + pdf, + allData, + allDescendants, // Pass all descendants so great-grandchildren can be found + directGrandchildren, // Direct grandchildren to render + gcOwner, + yPosition, + margin, + indentPerLevel + 20, // Increase indentation for next level + usableWidth, + pageHeight, + headerHeight, + rowHeight, + options + ); + } + } + } + + // Add spacing after child table + yPosition += 5; + + return yPosition; + } + + private drawTableHeaders( + pdf: jsPDF, + columns: any[], + rowDimensionHeaders: string[], + xStart: number, + yPosition: number, + columnWidth: number, + headerHeight: number, + tableWidth: number, + options: IgxPdfExporterOptions + ): void { + pdf.setFont('helvetica', 'bold'); + pdf.setFillColor(240, 240, 240); + + if (options.showTableBorders) { + pdf.rect(xStart, yPosition, tableWidth, headerHeight, 'F'); + } + + // Draw row dimension headers first (for pivot grids) + rowDimensionHeaders.forEach((headerText, index) => { + const xPosition = xStart + (index * columnWidth); + let displayText = headerText; + + if (options.showTableBorders) { + pdf.rect(xPosition, yPosition, columnWidth, headerHeight); + } + + // Truncate text if it's too long + const maxTextWidth = columnWidth - 10; + if (pdf.getTextWidth(displayText) > maxTextWidth) { + while (pdf.getTextWidth(displayText + '...') > maxTextWidth && displayText.length > 0) { + displayText = displayText.substring(0, displayText.length - 1); + } + displayText += '...'; + } + + // Center text in cell + const textWidth = pdf.getTextWidth(displayText); + const textX = xPosition + (columnWidth - textWidth) / 2; + const textY = yPosition + headerHeight / 2 + options.fontSize / 3; + + pdf.text(displayText, textX, textY); + }); + + const rowDimensionOffset = rowDimensionHeaders.length * columnWidth; + + // Draw data column headers + columns.forEach((col, index) => { + // Skip GRID_LEVEL_COL - it shouldn't be rendered + if (col.field === GRID_LEVEL_COL) { + return; + } + + const xPosition = xStart + rowDimensionOffset + (index * columnWidth); + let headerText = col.header || col.field; + + if (options.showTableBorders) { + pdf.rect(xPosition, yPosition, columnWidth, headerHeight); + } + + // Truncate text if it's too long + const maxTextWidth = columnWidth - 10; // Leave 5px padding on each side + if (pdf.getTextWidth(headerText) > maxTextWidth) { + while (pdf.getTextWidth(headerText + '...') > maxTextWidth && headerText.length > 0) { + headerText = headerText.substring(0, headerText.length - 1); + } + headerText += '...'; + } + + // Center text in cell + const textWidth = pdf.getTextWidth(headerText); + const textX = xPosition + (columnWidth - textWidth) / 2; + const textY = yPosition + headerHeight / 2 + options.fontSize / 3; + + pdf.text(headerText, textX, textY); + }); + + pdf.setFont('helvetica', 'normal'); + } + + private drawDataRow( + pdf: jsPDF, + record: IExportRecord, + columns: any[], + rowDimensionFields: string[], + xStart: number, + yPosition: number, + columnWidth: number, + rowHeight: number, + indent: number, + options: IgxPdfExporterOptions, + allColumns?: any[], + isPivotGrid?: boolean, + rowDimensionColumnsByLevel?: Map, + recordIndex?: number, + rowDimensionHeaders?: string[] + ): void { + const isSummaryRecord = record.type === 'SummaryRecord'; + + // Draw row dimension cells first (for pivot grids) + // For pivot grids, the row dimension columns have 'header' property that contains the actual dimension values + // Use the maximum of fields and headers to ensure we draw all columns + const maxRowDimCols = Math.max(rowDimensionFields.length, rowDimensionHeaders?.length || 0); + for (let index = 0; index < maxRowDimCols; index++) { + const xPosition = xStart + (index * columnWidth); + let cellValue: any = null; + + // Primary approach: Get the value from row dimension columns' header property + // The row dimension columns are created with header = actual dimension value to display + if (isPivotGrid && allColumns) { + // Get all row dimension columns sorted by level and startIndex + const allRowDimCols = allColumns.filter(col => + (col.headerType === ExportHeaderType.RowHeader || + col.headerType === ExportHeaderType.MultiRowHeader || + col.headerType === ExportHeaderType.PivotMergedHeader) && + !col.skip + ).sort((a, b) => { + const levelDiff = (a.level ?? 0) - (b.level ?? 0); + if (levelDiff !== 0) return levelDiff; + return (a.startIndex ?? 0) - (b.startIndex ?? 0); + }); + + // For hierarchical dimensions, match columns by level + // The index corresponds to the dimension level (0 = first dimension, 1 = second, etc.) + const colsForLevel = allRowDimCols.filter(col => (col.level ?? 0) === index); + + // The row dimension columns are created in the same order as records appear + // We can use the record index to find the corresponding column + // However, for hierarchical dimensions, we need to account for row spans + if (colsForLevel.length > 0) { + // Try to find the column that matches this record + // First, try matching by checking if column field/header matches record data + let matchedCol = null; + if (record.data) { + for (const col of colsForLevel) { + const colField = typeof col.field === 'string' ? col.field : null; + const colHeader = typeof col.header === 'string' ? col.header : null; + + // Check if column field exists as a key in record data + if (colField && record.data[colField] !== undefined) { + matchedCol = col; + break; + } + // Check if column header matches a value in record data + if (colHeader) { + const recordValues = Object.values(record.data).map(v => String(v)); + if (recordValues.includes(colHeader)) { + matchedCol = col; + break; + } + } + } + } + + // If no match found, try to use record index to select column + // This works because columns are created in the same order as records + if (!matchedCol && recordIndex !== undefined) { + // For hierarchical dimensions with row spans, we need to account for that + // For now, use a simple index-based approach + const colIndex = Math.min(recordIndex, colsForLevel.length - 1); + matchedCol = colsForLevel[colIndex]; + } + + // If still no match, use the first column at this level + if (!matchedCol && colsForLevel.length > 0) { + matchedCol = colsForLevel[0]; + } + + // Use the header property - it contains the actual dimension value to display + if (matchedCol) { + if (matchedCol.header && typeof matchedCol.header === 'string') { + cellValue = matchedCol.header; + } else if (matchedCol.field && typeof matchedCol.field === 'string') { + cellValue = matchedCol.field; + } + } + } + } + + // Fallback: Try to get value using dimensionKeys (member names as keys in record.data) + if ((cellValue === null || cellValue === undefined) && record.data) { + const fieldName = rowDimensionFields[index]; + if (fieldName) { + cellValue = record.data[fieldName]; + } + } + + // Last resort: Try to find it by checking all keys in record data + if ((cellValue === null || cellValue === undefined) && record.data) { + const recordKeys = Object.keys(record.data); + const fieldName = rowDimensionFields[index]; + + // If we have a fieldName, try exact and fuzzy matching + if (fieldName) { + const matchingKey = recordKeys.find(key => + key.toLowerCase() === fieldName.toLowerCase() || + key === fieldName || + fieldName.toLowerCase().includes(key.toLowerCase()) || + key.toLowerCase().includes(fieldName.toLowerCase()) + ); + if (matchingKey) { + cellValue = record.data[matchingKey]; + } + } + + // For hierarchical dimensions, try using dimension keys by index + if ((cellValue === null || cellValue === undefined) && isPivotGrid && recordKeys.length > 0) { + const possibleDimKeys = recordKeys.filter(key => { + return !key.includes('-') && !key.includes('_') && + key === key.trim() && + key.length < 50; + }); + + if (possibleDimKeys.length > index) { + cellValue = record.data[possibleDimKeys[index]]; + } else if (possibleDimKeys.length > 0) { + cellValue = record.data[possibleDimKeys[0]]; + } + } + } + + // Convert value to string + if (cellValue === null || cellValue === undefined) { + cellValue = ''; + } else if (cellValue instanceof Date) { + cellValue = cellValue.toLocaleDateString(); + } else { + cellValue = String(cellValue); + } + + if (options.showTableBorders) { + pdf.setFillColor(255, 255, 255); + pdf.setDrawColor(0, 0, 0); + pdf.rect(xPosition, yPosition, columnWidth, rowHeight); + } + + // Truncate text if it's too long + const maxTextWidth = columnWidth - 10; + let displayText = cellValue; + + if (pdf.getTextWidth(displayText) > maxTextWidth) { + while (pdf.getTextWidth(displayText + '...') > maxTextWidth && displayText.length > 0) { + displayText = displayText.substring(0, displayText.length - 1); + } + displayText += '...'; + } + + const textY = yPosition + rowHeight / 2 + options.fontSize / 3; + pdf.text(displayText, xPosition + 5, textY); + } + + const rowDimensionOffset = maxRowDimCols * columnWidth; + + // Draw data columns + columns.forEach((col, index) => { + // Skip GRID_LEVEL_COL - it's an internal column + if (col.field === GRID_LEVEL_COL) { + return; + } + + const xPosition = xStart + rowDimensionOffset + (index * columnWidth); + let cellValue = record.data[col.field]; + + // Handle summary records - cellValue is an IgxSummaryResult object + if (isSummaryRecord && cellValue) { + // For summary records, the cellValue has label and value properties + // or it might be summaryResult property + if (cellValue.label !== undefined || cellValue.value !== undefined) { + const label = cellValue.label?.toString() || ''; + const value = cellValue.value?.toString() || cellValue.summaryResult?.toString() || ''; + if (label && value) { + cellValue = `${label}: ${value}`; + } else if (label) { + cellValue = label; + } else if (value) { + cellValue = value; + } else { + cellValue = ''; + } + } else if (cellValue.summaryResult !== undefined) { + cellValue = cellValue.summaryResult; + } + } + + // Convert value to string + if (cellValue === null || cellValue === undefined) { + cellValue = ''; + } else if (cellValue instanceof Date) { + cellValue = cellValue.toLocaleDateString(); + } else { + cellValue = String(cellValue); + } + + if (options.showTableBorders) { + pdf.setFillColor(255, 255, 255); + pdf.setDrawColor(0, 0, 0); + pdf.rect(xPosition, yPosition, columnWidth, rowHeight); + } + + // Apply indentation to the first column for hierarchical data + const textIndent = (index === 0) ? indent : 0; + + // Truncate text if it's too long, accounting for indentation + const maxTextWidth = columnWidth - 10 - textIndent; + let displayText = cellValue; + + if (pdf.getTextWidth(displayText) > maxTextWidth) { + while (pdf.getTextWidth(displayText + '...') > maxTextWidth && displayText.length > 0) { + displayText = displayText.substring(0, displayText.length - 1); + } + displayText += '...'; + } + + const textY = yPosition + rowHeight / 2 + options.fontSize / 3; + pdf.text(displayText, xPosition + 5 + textIndent, textY); + }); + } + + private saveFile(pdf: jsPDF, fileName: string): void { + const blob = pdf.output('blob'); + ExportUtilities.saveBlobToFile(blob, fileName); + } +} diff --git a/projects/igniteui-angular/grids/core/src/toolbar/common.ts b/projects/igniteui-angular/grids/core/src/toolbar/common.ts index ebb2f964554..f1624f0c5e8 100644 --- a/projects/igniteui-angular/grids/core/src/toolbar/common.ts +++ b/projects/igniteui-angular/grids/core/src/toolbar/common.ts @@ -13,6 +13,12 @@ export class IgxExcelTextDirective { } }) export class IgxCSVTextDirective { } +@Directive({ + selector: '[pdfText],pdf-text', + standalone: true +}) +export class IgxPdfTextDirective { } + /* blazorElement */ /* wcElementTag: igc-grid-toolbar-title */ /* blazorAlternateBaseType: GridToolbarContent */ diff --git a/projects/igniteui-angular/grids/core/src/toolbar/grid-toolbar-exporter.component.html b/projects/igniteui-angular/grids/core/src/toolbar/grid-toolbar-exporter.component.html index adbc7c8cf71..1c74723149c 100644 --- a/projects/igniteui-angular/grids/core/src/toolbar/grid-toolbar-exporter.component.html +++ b/projects/igniteui-angular/grids/core/src/toolbar/grid-toolbar-exporter.component.html @@ -42,5 +42,19 @@ } } + + @if (exportPDF) { +
  • + + + + @if (!pdf.childNodes.length) { + + {{ grid?.resourceStrings.igx_grid_toolbar_exporter_pdf_entry_text }} + + } +
  • + } diff --git a/projects/igniteui-angular/grids/core/src/toolbar/grid-toolbar-exporter.component.ts b/projects/igniteui-angular/grids/core/src/toolbar/grid-toolbar-exporter.component.ts index 15aca7608b6..f52cf03c41b 100644 --- a/projects/igniteui-angular/grids/core/src/toolbar/grid-toolbar-exporter.component.ts +++ b/projects/igniteui-angular/grids/core/src/toolbar/grid-toolbar-exporter.component.ts @@ -1,21 +1,20 @@ import { Component, Input, Output, EventEmitter, Inject, booleanAttribute } from '@angular/core'; import { first } from 'rxjs/operators'; import { BaseToolbarDirective } from './grid-toolbar.base'; -import { IgxExcelTextDirective, IgxCSVTextDirective } from './common'; -import { - CsvFileTypes, - IgxBaseExporter, - IgxCsvExporterOptions, - IgxCsvExporterService, - IgxExcelExporterOptions, - IgxExcelExporterService -} from 'igniteui-angular/core'; +import { IgxExcelTextDirective, IgxCSVTextDirective, IgxPdfTextDirective } from './common'; import { GridType } from '../common/grid.interface'; import { IgxToolbarToken } from './token'; import { IgxButtonDirective, IgxRippleDirective, IgxToggleDirective } from 'igniteui-angular/directives'; import { IgxIconComponent } from 'igniteui-angular/icon'; +import { CsvFileTypes, IgxCsvExporterOptions } from '../services/csv/csv-exporter-options'; +import { IgxExcelExporterOptions } from '../services/excel/excel-exporter-options'; +import { IgxPdfExporterOptions } from '../services/pdf/pdf-exporter-options'; +import { IgxBaseExporter } from '../services/exporter-common/base-export-service'; +import { IgxExcelExporterService } from '../services/excel/excel-exporter'; +import { IgxCsvExporterService } from '../services/csv/csv-exporter'; +import { IgxPdfExporterService } from '../services/pdf/pdf-exporter'; -export type IgxExporterOptions = IgxCsvExporterOptions | IgxExcelExporterOptions; +export type IgxExporterOptions = IgxCsvExporterOptions | IgxExcelExporterOptions | IgxPdfExporterOptions; /* jsonAPIComplexObject */ /* wcAlternateName: ExporterEventArgs */ @@ -47,7 +46,7 @@ export interface IgxExporterEvent { @Component({ selector: 'igx-grid-toolbar-exporter', templateUrl: './grid-toolbar-exporter.component.html', - imports: [IgxButtonDirective, IgxRippleDirective, IgxIconComponent, IgxToggleDirective, IgxExcelTextDirective, IgxCSVTextDirective] + imports: [IgxButtonDirective, IgxRippleDirective, IgxIconComponent, IgxToggleDirective, IgxExcelTextDirective, IgxCSVTextDirective, IgxPdfTextDirective] }) export class IgxGridToolbarExporterComponent extends BaseToolbarDirective { @@ -63,6 +62,12 @@ export class IgxGridToolbarExporterComponent extends BaseToolbarDirective { @Input({ transform: booleanAttribute }) public exportExcel = true; + /** + * Show entry for PDF export. + */ + @Input({ transform: booleanAttribute }) + public exportPDF = true; + /** * The name for the exported file. */ @@ -91,11 +96,12 @@ export class IgxGridToolbarExporterComponent extends BaseToolbarDirective { @Inject(IgxToolbarToken) toolbar: IgxToolbarToken, private excelExporter: IgxExcelExporterService, private csvExporter: IgxCsvExporterService, + private pdfExporter: IgxPdfExporterService, ) { super(toolbar); } - protected exportClicked(type: 'excel' | 'csv', toggleRef?: IgxToggleDirective) { + protected exportClicked(type: 'excel' | 'csv' | 'pdf', toggleRef?: IgxToggleDirective) { toggleRef?.close(); this.export(type); } @@ -105,7 +111,7 @@ export class IgxGridToolbarExporterComponent extends BaseToolbarDirective { * Export the grid's data * @param type File type to export */ - public export(type: 'excel' | 'csv'): void { + public export(type: 'excel' | 'csv' | 'pdf'): void { let options: IgxExporterOptions; let exporter: IgxBaseExporter; @@ -117,6 +123,10 @@ export class IgxGridToolbarExporterComponent extends BaseToolbarDirective { case 'excel': options = new IgxExcelExporterOptions(this.filename); exporter = this.excelExporter; + break; + case 'pdf': + options = new IgxPdfExporterOptions(this.filename); + exporter = this.pdfExporter; } const args = { exporter, options, grid: this.grid, cancel: false } as IgxExporterEvent; diff --git a/projects/igniteui-angular/grids/core/src/toolbar/public_api.ts b/projects/igniteui-angular/grids/core/src/toolbar/public_api.ts index aa162dc71c1..4a00f01b138 100644 --- a/projects/igniteui-angular/grids/core/src/toolbar/public_api.ts +++ b/projects/igniteui-angular/grids/core/src/toolbar/public_api.ts @@ -11,6 +11,7 @@ export * from './grid-toolbar-advanced-filtering.component'; export * from './grid-toolbar-exporter.component'; export * from './grid-toolbar-hiding.component'; export * from './grid-toolbar-pinning.component'; +export * from './grid-toolbar-exporter.component'; export * from './token'; /* NOTE: Grid toolbar directives collection for ease-of-use import in standalone components scenario */ diff --git a/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts b/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts index ac4d2fe594d..28f3eca8d43 100644 --- a/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts +++ b/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts @@ -62,7 +62,6 @@ import { StateUpdateEvent, TransactionEventOrigin, getCurrentResourceStrings, - CharSeparatedValueData, DataUtil, DefaultDataCloneStrategy, DefaultMergeStrategy, @@ -108,7 +107,7 @@ import { import { IgxGridRowComponent } from './grid-row.component'; import { IgxPaginatorToken, type IgxPaginatorComponent } from 'igniteui-angular/paginator'; import { IgxSnackbarComponent } from 'igniteui-angular/snackbar'; -import { DropPosition, FilterMode, getUUID, GridCellMergeMode, GridKeydownTargetType, GridPagingMode, GridSelectionMode, GridSelectionRange, GridServiceType, GridSummaryPosition, GridType, GridValidationTrigger, IActiveNode, IActiveNodeChangeEventArgs, ICellPosition, IClipboardOptions, IColumnMovingEndEventArgs, IColumnMovingEventArgs, IColumnMovingStartEventArgs, IColumnResizeEventArgs, IColumnsAutoGeneratedEventArgs, IColumnSelectionEventArgs, IColumnVisibilityChangedEventArgs, IColumnVisibilityChangingEventArgs, IFilteringEventArgs, IGridCellEventArgs, IGridClipboardEvent, IGridContextMenuEventArgs, IGridEditDoneEventArgs, IGridEditEventArgs, IGridFormGroupCreatedEventArgs, IGridKeydownEventArgs, IGridRowEventArgs, IGridScrollEventArgs, IGridToolbarExportEventArgs, IGridValidationStatusEventArgs, IGX_GRID_SERVICE_BASE, IgxAdvancedFilteringDialogComponent, IgxCell, IgxColumnComponent, IgxColumnGroupComponent, IgxColumnResizingService, IgxDragIndicatorIconDirective, IgxEditRow, IgxExcelStyleHeaderIconDirective, IgxExcelStyleLoadingValuesTemplateDirective, IgxFilteringService, IgxGridBodyDirective, IgxGridCellComponent, IgxGridColumnResizerComponent, IgxGridEmptyTemplateContext, IgxGridEmptyTemplateDirective, IgxGridExcelStyleFilteringComponent, IgxGridFilteringCellComponent, IgxGridFilteringRowComponent, IgxGridGroupByAreaComponent, IgxGridHeaderComponent, IgxGridHeaderGroupComponent, IgxGridHeaderRowComponent, IgxGridHeaderTemplateContext, IgxGridLoadingTemplateDirective, IgxGridNavigationService, IgxGridRowDragGhostContext, IgxGridRowEditActionsTemplateContext, IgxGridRowEditTemplateContext, IgxGridRowEditTextTemplateContext, IgxGridRowTemplateContext, IgxGridSelectionService, IgxGridSummaryService, IgxGridTemplateContext, IgxGridToolbarComponent, IgxGridTransaction, IgxGridValidationService, IgxHeaderCollapsedIndicatorDirective, IgxHeaderExpandedIndicatorDirective, IgxHeadSelectorDirective, IgxHeadSelectorTemplateContext, IgxRowAddTextDirective, IgxRowCollapsedIndicatorDirective, IgxRowDirective, IgxRowDragGhostDirective, IgxRowEditActionsDirective, IgxRowEditTabStopDirective, IgxRowEditTemplateDirective, IgxRowEditTextDirective, IgxRowExpandedIndicatorDirective, IgxRowSelectorDirective, IgxRowSelectorTemplateContext, IgxSortAscendingHeaderIconDirective, IgxSortDescendingHeaderIconDirective, IgxSortHeaderIconDirective, IgxSummaryRowComponent, IgxToolbarToken, IPinColumnCancellableEventArgs, IPinColumnEventArgs, IPinningConfig, IPinRowEventArgs, IRowDataCancelableEventArgs, IRowDataEventArgs, IRowDragEndEventArgs, IRowDragStartEventArgs, IRowSelectionEventArgs, IRowToggleEventArgs, ISearchInfo, ISizeInfo, ISortingEventArgs, RowEditPositionStrategy, RowPinningPosition, RowType, WatchChanges } from 'igniteui-angular/grids/core'; +import { CharSeparatedValueData, DropPosition, FilterMode, getUUID, GridCellMergeMode, GridKeydownTargetType, GridPagingMode, GridSelectionMode, GridSelectionRange, GridServiceType, GridSummaryPosition, GridType, GridValidationTrigger, IActiveNode, IActiveNodeChangeEventArgs, ICellPosition, IClipboardOptions, IColumnMovingEndEventArgs, IColumnMovingEventArgs, IColumnMovingStartEventArgs, IColumnResizeEventArgs, IColumnsAutoGeneratedEventArgs, IColumnSelectionEventArgs, IColumnVisibilityChangedEventArgs, IColumnVisibilityChangingEventArgs, IFilteringEventArgs, IGridCellEventArgs, IGridClipboardEvent, IGridContextMenuEventArgs, IGridEditDoneEventArgs, IGridEditEventArgs, IGridFormGroupCreatedEventArgs, IGridKeydownEventArgs, IGridRowEventArgs, IGridScrollEventArgs, IGridToolbarExportEventArgs, IGridValidationStatusEventArgs, IGX_GRID_SERVICE_BASE, IgxAdvancedFilteringDialogComponent, IgxCell, IgxColumnComponent, IgxColumnGroupComponent, IgxColumnResizingService, IgxDragIndicatorIconDirective, IgxEditRow, IgxExcelStyleHeaderIconDirective, IgxExcelStyleLoadingValuesTemplateDirective, IgxFilteringService, IgxGridBodyDirective, IgxGridCellComponent, IgxGridColumnResizerComponent, IgxGridEmptyTemplateContext, IgxGridEmptyTemplateDirective, IgxGridExcelStyleFilteringComponent, IgxGridFilteringCellComponent, IgxGridFilteringRowComponent, IgxGridGroupByAreaComponent, IgxGridHeaderComponent, IgxGridHeaderGroupComponent, IgxGridHeaderRowComponent, IgxGridHeaderTemplateContext, IgxGridLoadingTemplateDirective, IgxGridNavigationService, IgxGridRowDragGhostContext, IgxGridRowEditActionsTemplateContext, IgxGridRowEditTemplateContext, IgxGridRowEditTextTemplateContext, IgxGridRowTemplateContext, IgxGridSelectionService, IgxGridSummaryService, IgxGridTemplateContext, IgxGridToolbarComponent, IgxGridTransaction, IgxGridValidationService, IgxHeaderCollapsedIndicatorDirective, IgxHeaderExpandedIndicatorDirective, IgxHeadSelectorDirective, IgxHeadSelectorTemplateContext, IgxRowAddTextDirective, IgxRowCollapsedIndicatorDirective, IgxRowDirective, IgxRowDragGhostDirective, IgxRowEditActionsDirective, IgxRowEditTabStopDirective, IgxRowEditTemplateDirective, IgxRowEditTextDirective, IgxRowExpandedIndicatorDirective, IgxRowSelectorDirective, IgxRowSelectorTemplateContext, IgxSortAscendingHeaderIconDirective, IgxSortDescendingHeaderIconDirective, IgxSortHeaderIconDirective, IgxSummaryRowComponent, IgxToolbarToken, IPinColumnCancellableEventArgs, IPinColumnEventArgs, IPinningConfig, IPinRowEventArgs, IRowDataCancelableEventArgs, IRowDataEventArgs, IRowDragEndEventArgs, IRowDragStartEventArgs, IRowSelectionEventArgs, IRowToggleEventArgs, ISearchInfo, ISizeInfo, ISortingEventArgs, RowEditPositionStrategy, RowPinningPosition, RowType, WatchChanges } from 'igniteui-angular/grids/core'; interface IMatchInfoCache { row: any; diff --git a/projects/igniteui-angular/grids/grid/src/grid-summary.spec.ts b/projects/igniteui-angular/grids/grid/src/grid-summary.spec.ts index e4dee26e187..923ab8e26fd 100644 --- a/projects/igniteui-angular/grids/grid/src/grid-summary.spec.ts +++ b/projects/igniteui-angular/grids/grid/src/grid-summary.spec.ts @@ -207,7 +207,7 @@ describe('IgxGrid - Summaries #grid', () => { beforeEach(() => { fixture = TestBed.createComponent(CustomSummariesComponent); fixture.detectChanges(); - grid = fixture.componentInstance.grid1; + grid = fixture.componentInstance.grid; }); it('should properly render custom summaries', () => { @@ -2761,7 +2761,7 @@ class AllDataAvgSummary extends IgxSummaryOperand { export class CustomSummariesComponent { @ViewChild('grid1', { read: IgxGridComponent, static: true }) - public grid1: IgxGridComponent; + public grid: IgxGridComponent; public data = SampleTestData.foodProductData(); public dealsSummary = DealsSummary; public dealsSummaryMinMax = DealsSummaryMinMax; diff --git a/projects/igniteui-angular/grids/grid/src/grid-toolbar.spec.ts b/projects/igniteui-angular/grids/grid/src/grid-toolbar.spec.ts index d0a9607c855..5c974efbd89 100644 --- a/projects/igniteui-angular/grids/grid/src/grid-toolbar.spec.ts +++ b/projects/igniteui-angular/grids/grid/src/grid-toolbar.spec.ts @@ -4,8 +4,9 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { IgxGridComponent } from './public_api'; import { GridFunctions } from "../../../test-utils/grid-functions.spec"; import { By } from "@angular/platform-browser"; -import { AbsoluteScrollStrategy, GlobalPositionStrategy, IgxCsvExporterService, IgxExcelExporterService } from 'igniteui-angular/core'; -import { IgxGridToolbarComponent, IgxGridToolbarActionsComponent, IgxGridToolbarTitleComponent, IgxGridToolbarPinningComponent, IgxGridToolbarHidingComponent, IgxGridToolbarAdvancedFilteringComponent, IgxGridToolbarExporterComponent } from 'igniteui-angular'; +import { AbsoluteScrollStrategy, GlobalPositionStrategy } from 'igniteui-angular/core'; +import { IgxCsvExporterService, IgxExcelExporterService, IgxGridToolbarActionsComponent, IgxGridToolbarAdvancedFilteringComponent, IgxGridToolbarComponent, IgxGridToolbarExporterComponent, IgxGridToolbarHidingComponent, IgxGridToolbarPinningComponent, IgxGridToolbarTitleComponent } from 'igniteui-angular/grids/core'; +import { ExportUtilities } from 'igniteui-angular/grids/core'; const TOOLBAR_TAG = 'igx-grid-toolbar'; const TOOLBAR_TITLE_TAG = 'igx-grid-toolbar-title'; @@ -180,6 +181,87 @@ describe('IgxGrid - Grid Toolbar #grid - ', () => { expect(instance.exporterAction.toolbar.showProgress).toBeFalse(); }); + it('toolbar exporter should include PDF option by default', () => { + const exporterButton = $(TOOLBAR_EXPORTER_TAG).querySelector('button'); + exporterButton.click(); + fixture.detectChanges(); + + expect($('#pdfEntry')).not.toBeNull(); + }); + + it('toolbar exporter should hide PDF option when exportPDF is false', () => { + instance.exportPDF = false; + fixture.detectChanges(); + + const exporterButton = $(TOOLBAR_EXPORTER_TAG).querySelector('button'); + exporterButton.click(); + fixture.detectChanges(); + + expect($('#pdfEntry')).toBeNull(); + }); + + it('toolbar exporter should show PDF option when exportPDF is true', () => { + instance.exportPDF = true; + fixture.detectChanges(); + + const exporterButton = $(TOOLBAR_EXPORTER_TAG).querySelector('button'); + exporterButton.click(); + fixture.detectChanges(); + + expect($('#pdfEntry')).not.toBeNull(); + }); + + it('toolbar exporter should display custom PDF text', () => { + const exporterButton = $(TOOLBAR_EXPORTER_TAG).querySelector('button'); + exporterButton.click(); + fixture.detectChanges(); + + expect($('#pdfEntry').textContent).toMatch(instance.customPDFText); + }); + + it('toolbar exporter should export to PDF when clicked', () => { + const exporterButton = $(TOOLBAR_EXPORTER_TAG).querySelector('button'); + exporterButton.click(); + fixture.detectChanges(); + + spyOn(instance.exporterAction, 'export'); + $('#pdfEntry').click(); + fixture.detectChanges(); + + expect(instance.exporterAction.export).toHaveBeenCalledWith('pdf'); + }); + + it('toolbar exporter should emit exportStarted event for PDF export', () => { + let exportStartedFired = false; + instance.exporterAction.exportStarted.subscribe(() => { + exportStartedFired = true; + }); + + const exporterButton = $(TOOLBAR_EXPORTER_TAG).querySelector('button'); + exporterButton.click(); + fixture.detectChanges(); + + spyOn(ExportUtilities, 'saveBlobToFile'); + $('#pdfEntry').click(); + fixture.detectChanges(); + + expect(exportStartedFired).toBe(true); + }); + + it('toolbar exporter PDF export can be cancelled', () => { + fixture.componentInstance.exportStartCancelled = true; + fixture.detectChanges(); + + const exporterButton = $(TOOLBAR_EXPORTER_TAG).querySelector('button'); + exporterButton.click(); + fixture.detectChanges(); + $('#pdfEntry').click(); + fixture.detectChanges(); + + expect(instance.exporterAction.isExporting).toBeFalse(); + expect(instance.exporterAction.toolbar.showProgress).toBeFalse(); + }); + it('Setting overlaySettings for each toolbar columns action', () => { const defaultSettings = instance.pinningAction.overlaySettings; const defaultFiltSettings = instance.advancedFiltAction.overlaySettings; @@ -304,10 +386,11 @@ export class DefaultToolbarComponent { {{ advancedFilteringTitle }} - + {{ exporterText }} {{ customExcelText }} {{ customCSVText }} + {{ customPDFText }} @@ -343,10 +426,12 @@ export class ToolbarActionsComponent { public advancedFilteringTitle = 'Custom button text'; public exportCSV = true; public exportExcel = true; + public exportPDF = true; public exportFilename = ''; public exporterText = 'Exporter Options'; public customExcelText = '<< Excel export >>'; public customCSVText = '<< CSV export >>'; + public customPDFText = '<< PDF export >>'; public overlaySettings = { positionStrategy: new GlobalPositionStrategy(), scrollStrategy: new AbsoluteScrollStrategy(), diff --git a/projects/igniteui-angular/grids/tree-grid/src/tree-grid-integration.spec.ts b/projects/igniteui-angular/grids/tree-grid/src/tree-grid-integration.spec.ts index ba1f1f3a5b3..592925bec61 100644 --- a/projects/igniteui-angular/grids/tree-grid/src/tree-grid-integration.spec.ts +++ b/projects/igniteui-angular/grids/tree-grid/src/tree-grid-integration.spec.ts @@ -123,30 +123,34 @@ describe('IgxTreeGrid - Integration #tGrid', () => { TreeGridFunctions.verifyTreeColumn(fix, 'ID', 3); }); - it('(API) should transform a non-tree column into a tree column when moving the original tree-column through', () => { + it('(UI) should transform a non-tree column into a tree column when moving the original tree-column through', async () => { TreeGridFunctions.verifyTreeColumn(fix, 'ID', 4); - // Move tree-column - const sourceColumn = treeGrid.columnList.filter(c => c.field === 'ID')[0]; - const targetColumn = treeGrid.columnList.filter(c => c.field === 'HireDate')[0]; - treeGrid.moveColumn(sourceColumn, targetColumn); + treeGrid.moving = true; + + const header = TreeGridFunctions.getHeaderCell(fix, 'ID').nativeElement; + const headerRect = header.getBoundingClientRect(); + const startX = headerRect.width / 2; + const startY = headerRect.height / 2; + + UIInteractions.simulatePointerEvent('pointerdown', header, startX, startY); + await wait(16); + UIInteractions.simulatePointerEvent('pointermove', header, startX + headerRect.width, startY); + await wait(16); + UIInteractions.simulatePointerEvent('pointerup', header, startX + headerRect.width, startY); + await wait(16); fix.detectChanges(); TreeGridFunctions.verifyTreeColumn(fix, 'Name', 4); }); - it('(UI) should transform a non-tree column into a tree column when moving the original tree-column through', async () => { + it('(API) should transform a non-tree column into a tree column when moving the original tree-column through', () => { TreeGridFunctions.verifyTreeColumn(fix, 'ID', 4); - treeGrid.moving = true; - - const header = TreeGridFunctions.getHeaderCell(fix, 'ID').nativeElement; - UIInteractions.simulatePointerEvent('pointerdown', header, 50, 50); - UIInteractions.simulatePointerEvent('pointermove', header, 56, 56); - await wait(); - UIInteractions.simulatePointerEvent('pointermove', header, 490, 30); - UIInteractions.simulatePointerEvent('pointerup', header, 490, 30); - await wait(); + // Move tree-column + const sourceColumn = treeGrid.columnList.filter(c => c.field === 'ID')[0]; + const targetColumn = treeGrid.columnList.filter(c => c.field === 'HireDate')[0]; + treeGrid.moveColumn(sourceColumn, targetColumn); fix.detectChanges(); TreeGridFunctions.verifyTreeColumn(fix, 'Name', 4); @@ -303,31 +307,34 @@ describe('IgxTreeGrid - Integration #tGrid', () => { TreeGridFunctions.verifyTreeColumn(fix, 'ID', 4); }); - it('(API) should transform a non-tree column into a tree column when moving the original tree-column through', () => { + it('(UI) should transform a non-tree column into a tree column when moving the original tree-column through', async () => { TreeGridFunctions.verifyTreeColumn(fix, 'ID', 5); - // Move tree-column - const sourceColumn = treeGrid.columnList.filter(c => c.field === 'ID')[0]; - const targetColumn = treeGrid.columnList.filter(c => c.field === 'JobTitle')[0]; - treeGrid.moveColumn(sourceColumn, targetColumn); + treeGrid.moving = true; + + const header = TreeGridFunctions.getHeaderCell(fix, 'ID').nativeElement; + const headerRect = header.getBoundingClientRect(); + const startX = headerRect.width / 2; + const startY = headerRect.height / 2; + + UIInteractions.simulatePointerEvent('pointerdown', header, startX, startY); + await wait(16); + UIInteractions.simulatePointerEvent('pointermove', header, startX + headerRect.width, startY); + await wait(16); + UIInteractions.simulatePointerEvent('pointerup', header, startX + headerRect.width, startY); + await wait(16); fix.detectChanges(); TreeGridFunctions.verifyTreeColumn(fix, 'ParentID', 5); }); - it('(UI) should transform a non-tree column into a tree column when moving the original tree-column through', async () => { + it('(API) should transform a non-tree column into a tree column when moving the original tree-column through', () => { TreeGridFunctions.verifyTreeColumn(fix, 'ID', 5); - treeGrid.moving = true; - fix.detectChanges(); - - const header = TreeGridFunctions.getHeaderCell(fix, 'ID').nativeElement; - UIInteractions.simulatePointerEvent('pointerdown', header, 50, 50); - UIInteractions.simulatePointerEvent('pointermove', header, 56, 56); - await wait(); - UIInteractions.simulatePointerEvent('pointermove', header, 490, 30); - UIInteractions.simulatePointerEvent('pointerup', header, 490, 30); - await wait() + // Move tree-column + const sourceColumn = treeGrid.columnList.filter(c => c.field === 'ID')[0]; + const targetColumn = treeGrid.columnList.filter(c => c.field === 'JobTitle')[0]; + treeGrid.moveColumn(sourceColumn, targetColumn); fix.detectChanges(); TreeGridFunctions.verifyTreeColumn(fix, 'ParentID', 5); @@ -1380,17 +1387,21 @@ describe('IgxTreeGrid - Integration #tGrid', () => { treeGrid.moving = true; fix.detectChanges(); - // const header = fix.debugElement.queryAll(By.css('.igx-grid-thead__item'))[0].nativeElement; - const header = treeGrid.headerGroups[0].nativeElement; + const header = fix.debugElement.queryAll(By.css('.igx-grid-thead__item'))[3].nativeElement; + // const header = treeGrid.headerGroups[0].nativeElement; UIInteractions.simulatePointerEvent('pointerdown', header, 100, 40); - await wait(); + fix.detectChanges(); + await wait(100); + UIInteractions.simulatePointerEvent('pointermove', header, 106, 46); - await wait(); + fix.detectChanges(); + await wait(100); + UIInteractions.simulatePointerEvent('pointermove', header, 700, 40); - await wait(); + fix.detectChanges(); + await wait(100); UIInteractions.simulatePointerEvent('pointerup', header, 700, 40); - await wait(); fix.detectChanges(); TreeGridFunctions.verifyTreeColumnInMultiColHeaders(fix, 'HireDate', 4); diff --git a/projects/igniteui-angular/migrations/update-21_0_0_import-migration/index.ts b/projects/igniteui-angular/migrations/update-21_0_0_import-migration/index.ts index fc7f6e604bc..9f78fe1383b 100644 --- a/projects/igniteui-angular/migrations/update-21_0_0_import-migration/index.ts +++ b/projects/igniteui-angular/migrations/update-21_0_0_import-migration/index.ts @@ -399,6 +399,65 @@ const ENTRY_POINT_MAP = new Map([ ['IgxTreeGridGroupingPipe', 'grids/tree-grid'], ['IGridCreatedEventArgs', 'grids/hierarchical-grid'], + // Exporter services and types (moved from core to grids/core in 21.0.0) + ['IgxBaseExporter', 'grids/core'], + ['IgxExporterOptionsBase', 'grids/core'], + ['ExportUtilities', 'grids/core'], + ['ExportRecordType', 'grids/core'], + ['ExportHeaderType', 'grids/core'], + ['IExportRecord', 'grids/core'], + ['IColumnList', 'grids/core'], + ['IColumnInfo', 'grids/core'], + ['IRowExportingEventArgs', 'grids/core'], + ['IColumnExportingEventArgs', 'grids/core'], + ['DEFAULT_OWNER', 'grids/core'], + ['GRID_ROOT_SUMMARY', 'grids/core'], + ['GRID_PARENT', 'grids/core'], + ['GRID_LEVEL_COL', 'grids/core'], + // CSV Exporter + ['IgxCsvExporterService', 'grids/core'], + ['IgxCsvExporterOptions', 'grids/core'], + ['ICsvExportEndedEventArgs', 'grids/core'], + ['CsvFileTypes', 'grids/core'], + ['CharSeparatedValueData', 'grids/core'], + // Excel Exporter + ['IgxExcelExporterService', 'grids/core'], + ['IgxExcelExporterOptions', 'grids/core'], + ['IExcelExportEndedEventArgs', 'grids/core'], + ['ExcelFolderTypes', 'grids/core'], + ['ExcelFileTypes', 'grids/core'], + ['IExcelFile', 'grids/core'], + ['IExcelFolder', 'grids/core'], + ['ExcelStrings', 'grids/core'], + ['ExcelElementsFactory', 'grids/core'], + ['WorksheetData', 'grids/core'], + ['WorksheetDataDictionary', 'grids/core'], + ['RootExcelFolder', 'grids/core'], + ['RootRelsExcelFolder', 'grids/core'], + ['DocPropsExcelFolder', 'grids/core'], + ['XLExcelFolder', 'grids/core'], + ['XLRelsExcelFolder', 'grids/core'], + ['ThemeExcelFolder', 'grids/core'], + ['WorksheetsExcelFolder', 'grids/core'], + ['TablesExcelFolder', 'grids/core'], + ['WorksheetsRelsExcelFolder', 'grids/core'], + ['RootRelsFile', 'grids/core'], + ['AppFile', 'grids/core'], + ['CoreFile', 'grids/core'], + ['WorkbookRelsFile', 'grids/core'], + ['ThemeFile', 'grids/core'], + ['WorksheetFile', 'grids/core'], + ['StyleFile', 'grids/core'], + ['WorkbookFile', 'grids/core'], + ['ContentTypesFile', 'grids/core'], + ['SharedStringsFile', 'grids/core'], + ['TablesFile', 'grids/core'], + ['WorksheetRelsFile', 'grids/core'], + // PDF Exporter + ['IgxPdfExporterService', 'grids/core'], + ['IgxPdfExporterOptions', 'grids/core'], + ['IPdfExportEndedEventArgs', 'grids/core'], + // Icon ['IgxIconComponent', 'icon'], ['IgxIconModule', 'icon'], @@ -837,6 +896,7 @@ export default function migrate(): Rule { context.logger.info(' - Input directives moved to igniteui-angular/input-group'); context.logger.info(' - IgxAutocompleteDirective moved to igniteui-angular/drop-down'); context.logger.info(' - IgxRadioGroupDirective moved to igniteui-angular/radio'); + context.logger.info(' - Exporter services (CSV, Excel, PDF) moved to igniteui-angular/grids/core'); context.logger.info('Type renames:'); context.logger.info(' - Direction → CarouselAnimationDirection'); }; diff --git a/projects/igniteui-angular/ng-package.json b/projects/igniteui-angular/ng-package.json index 6a528535930..183215b2851 100644 --- a/projects/igniteui-angular/ng-package.json +++ b/projects/igniteui-angular/ng-package.json @@ -9,6 +9,7 @@ "@types/hammerjs", "hammerjs", "fflate", + "jspdf", "igniteui-trial-watermark", "lodash-es", "@igniteui/material-icons-extended", diff --git a/projects/igniteui-angular/ng-package.prod.json b/projects/igniteui-angular/ng-package.prod.json index b1ae10482e4..7af1254752a 100644 --- a/projects/igniteui-angular/ng-package.prod.json +++ b/projects/igniteui-angular/ng-package.prod.json @@ -8,6 +8,7 @@ "@types/hammerjs", "hammerjs", "fflate", + "jspdf", "igniteui-trial-watermark", "lodash-es", "@igniteui/material-icons-extended", diff --git a/projects/igniteui-angular/package.json b/projects/igniteui-angular/package.json index 9f4010225fd..264200c1cb9 100644 --- a/projects/igniteui-angular/package.json +++ b/projects/igniteui-angular/package.json @@ -40,6 +40,7 @@ "drag drop", "drop down", "excel export", + "pdf export", "expansion panel", "grid", "hierarchical grid", @@ -72,6 +73,7 @@ "fflate": "^0.8.2", "tslib": "^2.8.1", "igniteui-trial-watermark": "^3.1.0", + "jspdf": "^3.0.2", "lodash-es": "^4.17.21", "igniteui-theming": "^24.0.0", "@igniteui/material-icons-extended": "^3.1.0" diff --git a/projects/igniteui-angular/schematics/ng-add/index.spec.ts b/projects/igniteui-angular/schematics/ng-add/index.spec.ts index f8e73b2fca1..6b4662db54b 100644 --- a/projects/igniteui-angular/schematics/ng-add/index.spec.ts +++ b/projects/igniteui-angular/schematics/ng-add/index.spec.ts @@ -89,7 +89,6 @@ describe('ng-add schematics', () => { it('should add the correct igniteui-angular packages to package.json dependencies', async () => { await runner.runSchematic('ng-add', { normalizeCss: false }, tree); const pkgJsonData = JSON.parse(tree.readContent('/package.json')); - expect(pkgJsonData.dependencies['fflate']).toBeTruthy(); // hammer is optional now. expect(pkgJsonData.dependencies['hammerjs']).toBeFalsy(); }); @@ -97,7 +96,6 @@ describe('ng-add schematics', () => { it('should add hammerjs dependency to package.json dependencies if addHammer prompt is set.', async () => { await runner.runSchematic('ng-add', { normalizeCss: false, addHammer: true }, tree); const pkgJsonData = JSON.parse(tree.readContent('/package.json')); - expect(pkgJsonData.dependencies['fflate']).toBeTruthy(); expect(pkgJsonData.dependencies['hammerjs']).toBeTruthy(); }); diff --git a/projects/igniteui-angular/schematics/utils/dependency-handler.ts b/projects/igniteui-angular/schematics/utils/dependency-handler.ts index 4595fee52af..a7fdb148e62 100644 --- a/projects/igniteui-angular/schematics/utils/dependency-handler.ts +++ b/projects/igniteui-angular/schematics/utils/dependency-handler.ts @@ -20,7 +20,8 @@ const schematicsPackage = '@igniteui/angular-schematics'; */ export const DEPENDENCIES_MAP: PackageEntry[] = [ // dependencies - { name: 'fflate', target: PackageTarget.REGULAR }, + { name: 'fflate', target: PackageTarget.NONE }, + { name: 'jspdf', target: PackageTarget.NONE }, { name: 'tslib', target: PackageTarget.NONE }, { name: 'igniteui-trial-watermark', target: PackageTarget.NONE }, { name: 'lodash-es', target: PackageTarget.NONE }, diff --git a/projects/igniteui-angular/test-utils/sample-test-data.spec.ts b/projects/igniteui-angular/test-utils/sample-test-data.spec.ts index d4992adee0f..d94b76511b6 100644 --- a/projects/igniteui-angular/test-utils/sample-test-data.spec.ts +++ b/projects/igniteui-angular/test-utils/sample-test-data.spec.ts @@ -1,5 +1,5 @@ import { Calendar } from 'igniteui-angular/calendar'; -import { ValueData } from '../core/src/services/excel/test-data.service.spec'; +import { ValueData } from '../grids/core/src/services/excel/test-data.service.spec'; import { ymd } from './helper-utils.spec'; import { cloneValue } from 'igniteui-angular/core'; diff --git a/projects/igniteui-angular/test-utils/tree-grid-components.spec.ts b/projects/igniteui-angular/test-utils/tree-grid-components.spec.ts index 5bc2d39b9a4..704f9ef36c8 100644 --- a/projects/igniteui-angular/test-utils/tree-grid-components.spec.ts +++ b/projects/igniteui-angular/test-utils/tree-grid-components.spec.ts @@ -722,7 +722,7 @@ export class IgxTreeGridLoadOnDemandComponent { } @Component({ template: ` - + @@ -767,7 +767,7 @@ export class IgxTreeGridLoadOnDemandChildDataComponent { @Component({ template: ` - + diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 711c7618b6d..c6cc68f4294 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -422,6 +422,11 @@ export class AppComponent implements OnInit { icon: 'view_column', name: 'Grid Export' }, + { + link: '/gridPdfExport', + icon: 'view_column', + name: 'Grid PDF Export' + }, { link: '/gridSearch', icon: 'view_column', diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 5234d396dc1..1b4ee86a042 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -141,6 +141,7 @@ import { QueryBuilderComponent } from './query-builder/query-builder.sample'; import { PivotGridStateSampleComponent } from './pivot-grid-state/pivot-grid-state.sample'; import { GridValidationSampleComponent } from './grid-validation/grid-validation.sample.component'; import { GridExportComponent } from './grid-export/grid-export.sample'; +import { GridPdfExportSampleComponent } from './grid-pdf-export/grid-pdf-export.sample'; import { DividerComponent } from './divider/divider.component'; import { MonthPickerSampleComponent } from './month-picker/month-picker.sample'; import { GridDockManagerSampleComponent } from './dockmanager-grid/dockmanager-grid.sample'; @@ -532,6 +533,10 @@ export const appRoutes: Routes = [ path: 'gridExport', component: GridExportComponent }, + { + path: 'gridPdfExport', + component: GridPdfExportSampleComponent + }, { path: 'buttonGroup', component: ButtonGroupSampleComponent diff --git a/src/app/grid-column-groups/grid-column-groups.sample.html b/src/app/grid-column-groups/grid-column-groups.sample.html index 6940f70925c..e79e92edbc8 100644 --- a/src/app/grid-column-groups/grid-column-groups.sample.html +++ b/src/app/grid-column-groups/grid-column-groups.sample.html @@ -8,12 +8,13 @@ - - + + + - + @@ -26,13 +27,14 @@ - - - - - - + + + + + + + diff --git a/src/app/grid-column-groups/grid-column-groups.sample.ts b/src/app/grid-column-groups/grid-column-groups.sample.ts index 728764ac961..4409bb09d8a 100644 --- a/src/app/grid-column-groups/grid-column-groups.sample.ts +++ b/src/app/grid-column-groups/grid-column-groups.sample.ts @@ -1,11 +1,25 @@ import { Component, HostBinding, ViewChild } from '@angular/core'; -import { ColumnPinningPosition, GridSelectionMode, IgxButtonDirective, IgxButtonGroupComponent, IgxCollapsibleIndicatorTemplateDirective, IgxColumnComponent, IgxColumnGroupComponent, IgxGridComponent, IgxGridToolbarActionsComponent, IgxGridToolbarAdvancedFilteringComponent, IgxGridToolbarComponent, IgxGridToolbarHidingComponent, IgxGridToolbarPinningComponent, IgxIconComponent } from 'igniteui-angular'; +import { ColumnPinningPosition, GridSelectionMode, IgxButtonDirective, IgxButtonGroupComponent, IgxCollapsibleIndicatorTemplateDirective, IgxColumnComponent, IgxColumnGroupComponent, IgxGridComponent, IgxGridToolbarActionsComponent, IgxGridToolbarAdvancedFilteringComponent, IgxGridToolbarComponent, IgxGridToolbarHidingComponent, IgxGridToolbarPinningComponent, IgxIconComponent, IgxGridToolbarExporterComponent } from 'igniteui-angular'; @Component({ selector: 'app-grid-column-groups-sample', styleUrls: ['grid-column-groups.sample.scss'], templateUrl: 'grid-column-groups.sample.html', - imports: [IgxCollapsibleIndicatorTemplateDirective, IgxIconComponent, IgxGridComponent, IgxGridToolbarComponent, IgxGridToolbarActionsComponent, IgxGridToolbarPinningComponent, IgxGridToolbarHidingComponent, IgxGridToolbarAdvancedFilteringComponent, IgxColumnComponent, IgxColumnGroupComponent, IgxButtonDirective, IgxButtonGroupComponent] + imports: [ + IgxCollapsibleIndicatorTemplateDirective, + IgxIconComponent, + IgxGridComponent, + IgxGridToolbarComponent, + IgxGridToolbarActionsComponent, + IgxGridToolbarPinningComponent, + IgxGridToolbarHidingComponent, + IgxGridToolbarAdvancedFilteringComponent, + IgxGridToolbarExporterComponent, + IgxColumnComponent, + IgxColumnGroupComponent, + IgxButtonDirective, + IgxButtonGroupComponent + ] }) export class GridColumnGroupsSampleComponent { @HostBinding('style.--ig-size') diff --git a/src/app/grid-groupby/grid-groupby.sample.html b/src/app/grid-groupby/grid-groupby.sample.html index bcbc0549e20..81eaa068ea5 100644 --- a/src/app/grid-groupby/grid-groupby.sample.html +++ b/src/app/grid-groupby/grid-groupby.sample.html @@ -35,6 +35,14 @@ + + + + + + + + @for (c of columns; track c) { +

    PDF Export Service Demo

    +

    + This demo shows how to use the IgxPdfExporterService API directly to export grids to PDF format. + Configure the export options using the controls below and click the export buttons. +

    + + +
    +

    Export Options Configuration

    + +
    + + + + +
    + +
    + + + Portrait + Landscape + +
    + +
    + + + @for (size of pageSizes; track size) { + {{ size.toUpperCase() }} + } + +
    + +
    + + + + +
    + +
    + Show Table Borders +
    +
    + + +
    +
    +

    Regular Grid (with Multi-Column Headers and Summaries)

    + +
    + + + + + + + + + + + + + + + + +
    + + +
    +
    +

    Tree Grid (with Hierarchy and Summaries)

    + +
    + + + + + + +
    + + +
    +
    +

    Hierarchical Grid (with Multi-Column Headers and Summaries)

    + +
    + + + + + + + + + + + + + + + + + + + + +
    + + +
    +
    +

    Pivot Grid (with Aggregated Data)

    + +
    + +
    + + + +
    +
    + +
    +

    How to Use:

    +
      +
    1. Configure the export options using the controls at the top
    2. +
    3. Click any of the "Export to PDF" buttons to export the corresponding grid
    4. +
    5. The PDF will be downloaded with your configured settings
    6. +
    7. Tree and Hierarchical grids will show indentation in the exported PDF
    8. +
    9. Multi-column headers and summaries are automatically included in the export
    10. +
    11. Pivot grids export with aggregated data values
    12. +
    + +

    Key Features:

    +
      +
    • Direct API Usage: Uses IgxPdfExporterService.export() method directly
    • +
    • Configurable Options: Adjust orientation, page size, font size, and borders
    • +
    • Hierarchy Support: Tree and Hierarchical grids export with proper indentation
    • +
    • Multi-Column Headers: Column groups are preserved in the exported PDF
    • +
    • Summaries Support: Grid summaries are automatically exported to PDF
    • +
    • Pivot Grid Support: Pivot grids export with aggregated data and pivot structure
    • +
    • Multiple Grids: Export different grid types with the same configuration
    • +
    +
    + diff --git a/src/app/grid-pdf-export/grid-pdf-export.sample.scss b/src/app/grid-pdf-export/grid-pdf-export.sample.scss new file mode 100644 index 00000000000..b7b84f0356e --- /dev/null +++ b/src/app/grid-pdf-export/grid-pdf-export.sample.scss @@ -0,0 +1,120 @@ +.sample-wrapper { + padding: 20px; + max-width: 1400px; + margin: 0 auto; +} + +.sample-title { + color: #333; + margin-bottom: 10px; +} + +.sample-description { + color: #666; + margin-bottom: 30px; + line-height: 1.6; +} + +.config-panel { + background: #f5f5f5; + border-radius: 8px; + padding: 20px; + margin-bottom: 30px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + + h3 { + margin-top: 0; + margin-bottom: 20px; + color: #333; + } +} + +.config-row { + margin-bottom: 15px; + display: inline-block; + margin-right: 20px; + min-width: 200px; + + igx-input-group { + width: 100%; + } +} + +.grid-container { + margin-bottom: 40px; + border: 1px solid #e0e0e0; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); +} + +.grid-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px 20px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + + h3 { + margin: 0; + font-size: 18px; + } + + button { + background: white; + color: #667eea; + font-weight: 500; + + &:hover { + background: #f0f0f0; + } + } +} + +.info-section { + background: #e8f4f8; + border-left: 4px solid #2196F3; + padding: 20px; + border-radius: 4px; + margin-top: 30px; + + h4 { + color: #1976D2; + margin-top: 0; + margin-bottom: 15px; + } + + ol, ul { + line-height: 1.8; + color: #555; + + li { + margin-bottom: 8px; + } + } + + strong { + color: #1976D2; + } +} + +// Responsive design +@media (max-width: 768px) { + .config-row { + display: block; + width: 100%; + margin-right: 0; + min-width: unset; + } + + .grid-header { + flex-direction: column; + gap: 10px; + align-items: flex-start; + + button { + width: 100%; + } + } +} diff --git a/src/app/grid-pdf-export/grid-pdf-export.sample.ts b/src/app/grid-pdf-export/grid-pdf-export.sample.ts new file mode 100644 index 00000000000..410ca32f6e6 --- /dev/null +++ b/src/app/grid-pdf-export/grid-pdf-export.sample.ts @@ -0,0 +1,195 @@ +import { Component, ViewChild, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { + IgxGridComponent, + IgxColumnComponent, + IgxPdfExporterService, + IgxPdfExporterOptions, + IgxTreeGridComponent, + IgxHierarchicalGridComponent, + IgxRowIslandComponent, + IgxButtonDirective, + IgxSwitchComponent, + IgxSelectComponent, + IgxSelectItemComponent, + IgxInputGroupComponent, + IgxLabelDirective, + IgxInputDirective, + IgxColumnGroupComponent, + IgxPivotGridComponent, + IgxPivotDataSelectorComponent +} from 'igniteui-angular'; + +@Component({ + selector: 'app-grid-pdf-export-sample', + templateUrl: 'grid-pdf-export.sample.html', + styleUrls: ['grid-pdf-export.sample.scss'], + imports: [ + IgxGridComponent, + IgxColumnComponent, + IgxColumnGroupComponent, + IgxTreeGridComponent, + IgxHierarchicalGridComponent, + IgxPivotGridComponent, + IgxPivotDataSelectorComponent, + IgxRowIslandComponent, + IgxButtonDirective, + IgxSwitchComponent, + IgxSelectComponent, + IgxSelectItemComponent, + IgxInputGroupComponent, + IgxLabelDirective, + IgxInputDirective, + FormsModule + ], + providers: [IgxPdfExporterService] +}) +export class GridPdfExportSampleComponent { + private pdfExporter = inject(IgxPdfExporterService); + + @ViewChild('grid1', { static: true }) + public grid1: IgxGridComponent; + + @ViewChild('treeGrid', { static: true }) + public treeGrid: IgxTreeGridComponent; + + @ViewChild('hierarchicalGrid', { static: true }) + public hierarchicalGrid: IgxHierarchicalGridComponent; + + @ViewChild('pivotGrid', { static: true }) + public pivotGrid: IgxPivotGridComponent; + + // Grid data + public gridData = [ + { ID: 1, Name: 'Product A', Category: 'Electronics', Price: 299.99, InStock: true, LaunchDate: new Date(2023, 0, 15) }, + { ID: 2, Name: 'Product B', Category: 'Clothing', Price: 49.99, InStock: true, LaunchDate: new Date(2023, 1, 20) }, + { ID: 3, Name: 'Product C', Category: 'Electronics', Price: 599.99, InStock: false, LaunchDate: new Date(2023, 2, 10) }, + { ID: 4, Name: 'Product D', Category: 'Books', Price: 19.99, InStock: true, LaunchDate: new Date(2023, 3, 5) }, + { ID: 5, Name: 'Product E', Category: 'Clothing', Price: 79.99, InStock: true, LaunchDate: new Date(2023, 4, 12) }, + { ID: 6, Name: 'Product F', Category: 'Electronics', Price: 899.99, InStock: false, LaunchDate: new Date(2023, 5, 8) }, + { ID: 7, Name: 'Product G', Category: 'Books', Price: 24.99, InStock: true, LaunchDate: new Date(2023, 6, 22) }, + { ID: 8, Name: 'Product H', Category: 'Clothing', Price: 39.99, InStock: true, LaunchDate: new Date(2023, 7, 18) }, + { ID: 9, Name: 'Product I', Category: 'Electronics', Price: 1299.99, InStock: true, LaunchDate: new Date(2023, 8, 5) }, + { ID: 10, Name: 'Product J', Category: 'Books', Price: 34.99, InStock: true, LaunchDate: new Date(2023, 9, 14) }, + { ID: 11, Name: 'Product K', Category: 'Clothing', Price: 89.99, InStock: false, LaunchDate: new Date(2023, 10, 3) }, + { ID: 12, Name: 'Product L', Category: 'Electronics', Price: 449.99, InStock: true, LaunchDate: new Date(2023, 11, 1) } + ]; + + // Tree Grid data + public treeGridData = [ + { ID: 1, ParentID: -1, Name: 'Electronics', Budget: 5000 }, + { ID: 2, ParentID: 1, Name: 'Laptops', Budget: 2000 }, + { ID: 3, ParentID: 1, Name: 'Phones', Budget: 1500 }, + { ID: 4, ParentID: 1, Name: 'Tablets', Budget: 1500 }, + { ID: 5, ParentID: -1, Name: 'Furniture', Budget: 3000 }, + { ID: 6, ParentID: 5, Name: 'Chairs', Budget: 800 }, + { ID: 7, ParentID: 5, Name: 'Desks', Budget: 1200 }, + { ID: 8, ParentID: 5, Name: 'Cabinets', Budget: 1000 }, + { ID: 9, ParentID: -1, Name: 'Office Supplies', Budget: 2500 }, + { ID: 10, ParentID: 9, Name: 'Paper Products', Budget: 600 }, + { ID: 11, ParentID: 9, Name: 'Writing Instruments', Budget: 400 }, + { ID: 12, ParentID: 9, Name: 'Storage Solutions', Budget: 1500 } + ]; + + // Hierarchical Grid data + public hierarchicalGridData = [ + { + ID: 1, + CompanyName: 'Company A', + Revenue: 1000000, + Employees: [ + { ID: 1, Name: 'John Doe', Position: 'Manager', Salary: 80000 }, + { ID: 2, Name: 'Jane Smith', Position: 'Developer', Salary: 70000 }, + { ID: 3, Name: 'Mike Wilson', Position: 'Developer', Salary: 72000 } + ] + }, + { + ID: 2, + CompanyName: 'Company B', + Revenue: 2000000, + Employees: [ + { ID: 4, Name: 'Bob Johnson', Position: 'CEO', Salary: 150000 }, + { ID: 5, Name: 'Alice Brown', Position: 'Designer', Salary: 65000 }, + { ID: 6, Name: 'Carol Davis', Position: 'Developer', Salary: 75000 } + ] + }, + { + ID: 3, + CompanyName: 'Company C', + Revenue: 1500000, + Employees: [ + { ID: 7, Name: 'David Lee', Position: 'Manager', Salary: 85000 }, + { ID: 8, Name: 'Emma Taylor', Position: 'Analyst', Salary: 68000 }, + { ID: 9, Name: 'Frank Martinez', Position: 'Developer', Salary: 73000 }, + { ID: 10, Name: 'Grace Anderson', Position: 'Designer', Salary: 67000 } + ] + } + ]; + + public pivotGridData = [ + { + ProductCategory: 'Clothing', UnitPrice: 12.81, SellerName: 'Stanley', + Country: 'Bulgaria', City: 'Sofia', Date: '01/01/2021', UnitsSold: 282 + }, + { + ProductCategory: 'Clothing', UnitPrice: 49.57, SellerName: 'Elisa', + Country: 'USA', City: 'New York', Date: '01/05/2019', UnitsSold: 296 + }, + { + ProductCategory: 'Bikes', UnitPrice: 3.56, SellerName: 'Lydia', + Country: 'Uruguay', City: 'Ciudad de la Costa', Date: '01/06/2020', UnitsSold: 68 + }, + { + ProductCategory: 'Accessories', UnitPrice: 85.58, SellerName: 'David', + Country: 'USA', City: 'New York', Date: '04/07/2021', UnitsSold: 293 + }, + { + ProductCategory: 'Components', UnitPrice: 18.13, SellerName: 'John', + Country: 'USA', City: 'New York', Date: '12/08/2021', UnitsSold: 240 + }, + { + ProductCategory: 'Clothing', UnitPrice: 68.33, SellerName: 'Larry', + Country: 'Uruguay', City: 'Ciudad de la Costa', Date: '05/12/2020', UnitsSold: 456 + } + ]; + + // Export options + public fileName = 'GridExport'; + public pageOrientation: 'portrait' | 'landscape' = 'landscape'; + public pageSize = 'a4'; + public showTableBorders = true; + public fontSize = 10; + public pageSizes = ['a3', 'a4', 'a5', 'letter', 'legal']; + + public exportGrid() { + const options = this.createExportOptions(); + this.pdfExporter.export(this.grid1, options); + } + + public exportTreeGrid() { + const options = this.createExportOptions(); + options.fileName = `TreeGrid_${this.fileName}`; + this.pdfExporter.export(this.treeGrid, options); + } + + public exportHierarchicalGrid() { + const options = this.createExportOptions(); + options.fileName = `HierarchicalGrid_${this.fileName}`; + this.pdfExporter.export(this.hierarchicalGrid, options); + } + + public exportPivotGrid() { + const options = this.createExportOptions(); + options.fileName = `PivotGrid_${this.fileName}`; + this.pdfExporter.export(this.pivotGrid, options); + } + + private createExportOptions(): IgxPdfExporterOptions { + const options = new IgxPdfExporterOptions(this.fileName); + options.pageOrientation = this.pageOrientation; + options.pageSize = this.pageSize; + options.showTableBorders = this.showTableBorders; + options.fontSize = this.fontSize; + return options; + } +} diff --git a/src/app/hierarchical-grid-advanced-filtering/hierarchical-grid-advanced-filtering.sample.html b/src/app/hierarchical-grid-advanced-filtering/hierarchical-grid-advanced-filtering.sample.html index b7fe0a8600a..82a158fa779 100644 --- a/src/app/hierarchical-grid-advanced-filtering/hierarchical-grid-advanced-filtering.sample.html +++ b/src/app/hierarchical-grid-advanced-filtering/hierarchical-grid-advanced-filtering.sample.html @@ -1,7 +1,14 @@

    Sample One

    - + + + + + + + +