From 4063d11e8ae9847885df334ac7c0ed1fbf3717cd Mon Sep 17 00:00:00 2001 From: shubhamsharma9199 Date: Wed, 19 Nov 2025 06:02:23 +0530 Subject: [PATCH] feat(editor): replace CKEditor5 with self-hosted TinyMCE GPL setup Removed unnecessary as unknown as EditorOptions type cast WEB-422 unit test coverage for ViewTaxComponentComponent feat(editor): implement self-hosted TinyMCE GPL editor --- angular.json | 1 - package-lock.json | 456 +------------- package.json | 4 +- .../configuration-wizard.component.ts | 4 - .../view-tax-component.component.spec.ts | 560 ++++++++++++++++++ .../create-edit-template.component.html | 19 +- .../create-edit-template.component.scss | 18 +- .../create-edit-template.component.ts | 111 ++-- src/app/templates/templates.module.ts | 5 +- src/app/templates/tinymce-loader.ts | 28 + src/typings.d.ts | 6 - 11 files changed, 722 insertions(+), 490 deletions(-) create mode 100644 src/app/products/manage-tax-components/view-tax-component/view-tax-component.component.spec.ts create mode 100644 src/app/templates/tinymce-loader.ts diff --git a/angular.json b/angular.json index 4b90ba9755..3720eac136 100644 --- a/angular.json +++ b/angular.json @@ -49,7 +49,6 @@ "scripts": [], "allowedCommonJsDependencies": [ "lodash", - "@ckeditor/ckeditor5-build-classic", "moment" ], "browser": "src/main.ts", diff --git a/package-lock.json b/package-lock.json index ceed020b47..038dc28933 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,8 +22,6 @@ "@angular/platform-browser": "19.2.14", "@angular/platform-browser-dynamic": "19.2.14", "@angular/router": "19.2.14", - "@ckeditor/ckeditor5-angular": "6.0.1", - "@ckeditor/ckeditor5-build-classic": "40.2.0", "@fortawesome/angular-fontawesome": "^1.0.0", "@fortawesome/fontawesome-free": "^6.7.2", "@fortawesome/fontawesome-svg-core": "6.2.1", @@ -33,6 +31,7 @@ "@ngx-translate/http-loader": "^16.0.1", "@swimlane/ngx-graph": "^10.0.0", "@tailwindcss/forms": "^0.5.4", + "@tinymce/tinymce-angular": "^9.1.1", "chart.js": "3.0.0-alpha", "d3": "^7.9.0", "exceljs": "^4.4.0", @@ -43,6 +42,7 @@ "moment": "^2.29.4", "ngx-mat-select-search": "^7.0.10", "rxjs": "7.8.1", + "tinymce": "^8.2.2", "tslib": "^2.0.0", "vkbeautify": "^0.99.3", "zone.js": "0.15.1" @@ -3095,374 +3095,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@ckeditor/ckeditor5-adapter-ckfinder": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-adapter-ckfinder/-/ckeditor5-adapter-ckfinder-40.2.0.tgz", - "integrity": "sha512-YKdydg4DzaMk91saOL55KBNQE3St2NEj1E9hlk9CzHKQaHc79dYzHDNBolSE7ZmzkNJ4ToVbY7kRW5CDGfG5Rg==", - "license": "GPL-2.0-or-later", - "dependencies": { - "ckeditor5": "40.2.0" - } - }, - "node_modules/@ckeditor/ckeditor5-angular": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-angular/-/ckeditor5-angular-6.0.1.tgz", - "integrity": "sha512-HryWnHaZTJ4KFIP6hjx5dCOUfhRuxmn/I0JFzrXXbPCVXXwlsGAtx2sjczmKnpTyofRSlDq8vTqw7+4hV4zB9g==", - "license": "GPL-2.0-or-later", - "dependencies": { - "@ckeditor/ckeditor5-watchdog": "^37.0.0", - "tslib": "^2.3.0" - }, - "peerDependencies": { - "@angular/common": ">=13.0.0", - "@angular/core": ">=13.0.0", - "@angular/forms": ">=13.0.0", - "rxjs": ">=6.0.0" - } - }, - "node_modules/@ckeditor/ckeditor5-autoformat": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-autoformat/-/ckeditor5-autoformat-40.2.0.tgz", - "integrity": "sha512-F3w5k7ti5l6V8U07eSQ3gup3ivltRZQXdtvstBXMmTzDb2ceazNcUDLb6TKSHp5y30ETN0dRGgbhx9xiDL0TXg==", - "license": "GPL-2.0-or-later", - "dependencies": { - "ckeditor5": "40.2.0" - } - }, - "node_modules/@ckeditor/ckeditor5-basic-styles": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-basic-styles/-/ckeditor5-basic-styles-40.2.0.tgz", - "integrity": "sha512-P7jYddLnRpaR4zVCqDa8InsZ6YNRHdF0RrX6+Uz81+A1IfyfmSd+5IaiLxxdnFWQ4JlEhJutjy9vMwSmOhZocQ==", - "license": "GPL-2.0-or-later", - "dependencies": { - "ckeditor5": "40.2.0" - } - }, - "node_modules/@ckeditor/ckeditor5-block-quote": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-block-quote/-/ckeditor5-block-quote-40.2.0.tgz", - "integrity": "sha512-t03Yp+MeAyQhwdGZqUlkJEx25VSiigpzkIGGOhccSaTIIZ9XcWDkrTevDhwA4Pq4Q9IRQ8Loj3KCVSBuAqkBgw==", - "license": "GPL-2.0-or-later", - "dependencies": { - "ckeditor5": "40.2.0" - } - }, - "node_modules/@ckeditor/ckeditor5-build-classic": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-build-classic/-/ckeditor5-build-classic-40.2.0.tgz", - "integrity": "sha512-b9zt1kU0S2Azco8mXraxj56pctHRs9y/XfdVrpFzxiRbbzyBZ42WgX1ThJNTqH6WkXCxDPYSbqPhNIvPHpuEyg==", - "license": "GPL-2.0-or-later", - "dependencies": { - "@ckeditor/ckeditor5-adapter-ckfinder": "40.2.0", - "@ckeditor/ckeditor5-autoformat": "40.2.0", - "@ckeditor/ckeditor5-basic-styles": "40.2.0", - "@ckeditor/ckeditor5-block-quote": "40.2.0", - "@ckeditor/ckeditor5-ckbox": "40.2.0", - "@ckeditor/ckeditor5-ckfinder": "40.2.0", - "@ckeditor/ckeditor5-cloud-services": "40.2.0", - "@ckeditor/ckeditor5-easy-image": "40.2.0", - "@ckeditor/ckeditor5-editor-classic": "40.2.0", - "@ckeditor/ckeditor5-essentials": "40.2.0", - "@ckeditor/ckeditor5-heading": "40.2.0", - "@ckeditor/ckeditor5-image": "40.2.0", - "@ckeditor/ckeditor5-indent": "40.2.0", - "@ckeditor/ckeditor5-link": "40.2.0", - "@ckeditor/ckeditor5-list": "40.2.0", - "@ckeditor/ckeditor5-media-embed": "40.2.0", - "@ckeditor/ckeditor5-paragraph": "40.2.0", - "@ckeditor/ckeditor5-paste-from-office": "40.2.0", - "@ckeditor/ckeditor5-table": "40.2.0", - "@ckeditor/ckeditor5-typing": "40.2.0" - } - }, - "node_modules/@ckeditor/ckeditor5-ckbox": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-ckbox/-/ckeditor5-ckbox-40.2.0.tgz", - "integrity": "sha512-w/A3RA7qpR7Scl4hgUTt8j+oV7oD9IFPNGTpp0xoyfWEV8Ymm2NrMAvV0PAJiavYS3+FT4GO4RBOM6BvLHuV3w==", - "license": "GPL-2.0-or-later", - "dependencies": { - "blurhash": "2.0.5", - "ckeditor5": "40.2.0", - "lodash-es": "4.17.21" - } - }, - "node_modules/@ckeditor/ckeditor5-ckfinder": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-ckfinder/-/ckeditor5-ckfinder-40.2.0.tgz", - "integrity": "sha512-kiW5TZOLHYd6hhWeDIrep8FXLo0q14b0e0Jit0XSi3z4PQfdDSRk9UuMJPkkf4EdF3PUSdMJ9bccJG76DYXzFQ==", - "license": "GPL-2.0-or-later", - "dependencies": { - "ckeditor5": "40.2.0" - } - }, - "node_modules/@ckeditor/ckeditor5-clipboard": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-clipboard/-/ckeditor5-clipboard-40.2.0.tgz", - "integrity": "sha512-8/xPH9/i86ukcEiHdmTgNuPVJeYTrivbx5ZYqycPO4Eem7VM99gIbOe7pIYpuV+klr9ymVxIHbGyTJDJ3oUO8A==", - "license": "GPL-2.0-or-later", - "dependencies": { - "@ckeditor/ckeditor5-core": "40.2.0", - "@ckeditor/ckeditor5-engine": "40.2.0", - "@ckeditor/ckeditor5-ui": "40.2.0", - "@ckeditor/ckeditor5-utils": "40.2.0", - "@ckeditor/ckeditor5-widget": "40.2.0", - "lodash-es": "4.17.21" - } - }, - "node_modules/@ckeditor/ckeditor5-cloud-services": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-cloud-services/-/ckeditor5-cloud-services-40.2.0.tgz", - "integrity": "sha512-xrPQjFGGy1ZfXyKIMw0uzn7OIriSv13YMw0M2ZTh+V1jRN1HW9KzzNb2DlQc+6wQfRk0bq69XGLp3M296/cVTQ==", - "license": "GPL-2.0-or-later", - "dependencies": { - "ckeditor5": "40.2.0" - } - }, - "node_modules/@ckeditor/ckeditor5-core": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-core/-/ckeditor5-core-40.2.0.tgz", - "integrity": "sha512-0fqIaN+ZhkXXA3mpBN+alycBzPMc8ruO8VrP0OnvCjowqZVS2HXC2AaXNBdxc75xGI3ScXIor7FsgFHxVJIYYQ==", - "license": "GPL-2.0-or-later", - "dependencies": { - "@ckeditor/ckeditor5-engine": "40.2.0", - "@ckeditor/ckeditor5-utils": "40.2.0", - "lodash-es": "4.17.21" - } - }, - "node_modules/@ckeditor/ckeditor5-easy-image": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-easy-image/-/ckeditor5-easy-image-40.2.0.tgz", - "integrity": "sha512-bE9YHGYEY1ql5mIqZSI2fkoBA4th28M0GDaJutwJYKM3t+Bost3Zh1UfmYKE2kvYgmeLY1L3hjQpN9w7NYyLpw==", - "license": "GPL-2.0-or-later", - "dependencies": { - "ckeditor5": "40.2.0" - } - }, - "node_modules/@ckeditor/ckeditor5-editor-classic": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-editor-classic/-/ckeditor5-editor-classic-40.2.0.tgz", - "integrity": "sha512-dftfDBxANOgqgQZ4SB3YTsEV/XX1u0g9jopbOBwqIABnVVa8zoGcktgFdGnLUFk51sL65baSx2z8Z1NNYdZcFQ==", - "license": "GPL-2.0-or-later", - "dependencies": { - "ckeditor5": "40.2.0", - "lodash-es": "4.17.21" - } - }, - "node_modules/@ckeditor/ckeditor5-engine": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-engine/-/ckeditor5-engine-40.2.0.tgz", - "integrity": "sha512-sgboUX8Ps+LcEgywyT3BeK1nzLHjNVIiZU1qvRxR3ixzIw4w2xRNXCGfESWLW5Y5rv9+ypUCrX61oLnZU64PQQ==", - "license": "GPL-2.0-or-later", - "dependencies": { - "@ckeditor/ckeditor5-utils": "40.2.0", - "lodash-es": "4.17.21" - } - }, - "node_modules/@ckeditor/ckeditor5-enter": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-enter/-/ckeditor5-enter-40.2.0.tgz", - "integrity": "sha512-GjTRaKNX8QEDJ3YYKG3GfPZfGHrcigGBxbo+1WDT7NaOsR2DA/CIZfHlAPfgJDAMV17bhWsT3gy3+oQZsExtnQ==", - "license": "GPL-2.0-or-later", - "dependencies": { - "@ckeditor/ckeditor5-core": "40.2.0", - "@ckeditor/ckeditor5-engine": "40.2.0", - "@ckeditor/ckeditor5-utils": "40.2.0" - } - }, - "node_modules/@ckeditor/ckeditor5-essentials": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-essentials/-/ckeditor5-essentials-40.2.0.tgz", - "integrity": "sha512-7iUUy0Uwiei4yLrn145SOcyzriMeVFVc5ontQkxQE5b9alFdAc/6ZoDPZqwD7V0zi5RQ/2YsoVMRLFa4hbPfNA==", - "license": "GPL-2.0-or-later", - "dependencies": { - "ckeditor5": "40.2.0" - } - }, - "node_modules/@ckeditor/ckeditor5-heading": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-heading/-/ckeditor5-heading-40.2.0.tgz", - "integrity": "sha512-uDT1sttMy+KrKi90jnqEI43886o1wfKrROWqaMbmKOerTbIi58GNH9LvX04sf1RyHV3+3566RRmB248fsLkYjA==", - "license": "GPL-2.0-or-later", - "dependencies": { - "ckeditor5": "40.2.0" - } - }, - "node_modules/@ckeditor/ckeditor5-image": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-image/-/ckeditor5-image-40.2.0.tgz", - "integrity": "sha512-0Dunw1o5k2+5Q5XiWLDG1r8k9awosfIFuDZwqKJGWtDaNE4QQbJ9+iJSwiiRw2QjcGr7D3JdH7xwJZFra7kYmA==", - "license": "GPL-2.0-or-later", - "dependencies": { - "@ckeditor/ckeditor5-ui": "40.2.0", - "ckeditor5": "40.2.0", - "lodash-es": "4.17.21" - } - }, - "node_modules/@ckeditor/ckeditor5-indent": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-indent/-/ckeditor5-indent-40.2.0.tgz", - "integrity": "sha512-gSlRGoyAslB2OpqghimIY6Oiflf3Z2/MdLBzvFipU5N4X66cL29HuWZc/bOkcFzWwNeDK5LgzfLdvXNzkdv5Xw==", - "license": "GPL-2.0-or-later", - "dependencies": { - "ckeditor5": "40.2.0" - } - }, - "node_modules/@ckeditor/ckeditor5-link": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-link/-/ckeditor5-link-40.2.0.tgz", - "integrity": "sha512-/r4Ti9USdrURBX+qutvyDGOb75sNuSgtXdI8xK503EVfx5yBIi6qsYIYWoFvnGJKkLYkVo+940ilduhwzq0M7g==", - "license": "GPL-2.0-or-later", - "dependencies": { - "@ckeditor/ckeditor5-ui": "40.2.0", - "ckeditor5": "40.2.0", - "lodash-es": "4.17.21" - } - }, - "node_modules/@ckeditor/ckeditor5-list": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-list/-/ckeditor5-list-40.2.0.tgz", - "integrity": "sha512-lsQWSLSFRHRQ2AxA6vgTib9YELjF2J5jpR6H4RDW1gM//dL3FjvLxKPPN/V7rMcp15rrpSiOya+qB99l24DEpQ==", - "license": "GPL-2.0-or-later", - "dependencies": { - "ckeditor5": "40.2.0" - } - }, - "node_modules/@ckeditor/ckeditor5-media-embed": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-media-embed/-/ckeditor5-media-embed-40.2.0.tgz", - "integrity": "sha512-ORY7VebL7UTuBG/4++UxzqEKjnlZZKAFqUrIom7xXpQNfo6oJFtZLnKYwESZ6iNk7NBOAeiHEecP2tKWyFQd1g==", - "license": "GPL-2.0-or-later", - "dependencies": { - "@ckeditor/ckeditor5-ui": "40.2.0", - "ckeditor5": "40.2.0" - } - }, - "node_modules/@ckeditor/ckeditor5-paragraph": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-paragraph/-/ckeditor5-paragraph-40.2.0.tgz", - "integrity": "sha512-NotxWP1cKvbJSY1UwdTe/Oy1NnAj9Etsi4Z7XA908EvCsNSnFtzdMhYzLhFZJ18avrQFDa7PpSKSyN3M64CbSA==", - "license": "GPL-2.0-or-later", - "dependencies": { - "@ckeditor/ckeditor5-core": "40.2.0", - "@ckeditor/ckeditor5-ui": "40.2.0", - "@ckeditor/ckeditor5-utils": "40.2.0" - } - }, - "node_modules/@ckeditor/ckeditor5-paste-from-office": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-paste-from-office/-/ckeditor5-paste-from-office-40.2.0.tgz", - "integrity": "sha512-kdk7uJlSa9mvyuNAwmIfV6Kc1tfWI6DbCs19jyseA/F0vySKibb0DsBVSZ7xa5ihcjphfJvwpypWYL0BYdYKLQ==", - "license": "GPL-2.0-or-later", - "dependencies": { - "ckeditor5": "40.2.0" - } - }, - "node_modules/@ckeditor/ckeditor5-select-all": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-select-all/-/ckeditor5-select-all-40.2.0.tgz", - "integrity": "sha512-yaYCqhdMcoEH3BsilhweNdbOfuO/cexQ1r1/mYoBoW4CypIuAeq8J/3qLpvFaThmCRPzJBn1J7v2Yjs/0UnamA==", - "license": "GPL-2.0-or-later", - "dependencies": { - "@ckeditor/ckeditor5-core": "40.2.0", - "@ckeditor/ckeditor5-ui": "40.2.0", - "@ckeditor/ckeditor5-utils": "40.2.0" - } - }, - "node_modules/@ckeditor/ckeditor5-table": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-table/-/ckeditor5-table-40.2.0.tgz", - "integrity": "sha512-yODne7az/aJ9lsuI7w476pgGV2QBoH2tOKp3JFh/e2DdHC20637LCVd0cx8sUe3zk61X/eYPY+wOiRJx/mIUqg==", - "license": "GPL-2.0-or-later", - "dependencies": { - "ckeditor5": "40.2.0", - "lodash-es": "4.17.21" - } - }, - "node_modules/@ckeditor/ckeditor5-typing": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-typing/-/ckeditor5-typing-40.2.0.tgz", - "integrity": "sha512-2E7LkmC4RHdenMUwow0EZDKxlbX00c5UHysUVT51EBGrXiJcN++0cqxQaeJzQ262oTDpk94qE5IZdGXt3ntzrw==", - "license": "GPL-2.0-or-later", - "dependencies": { - "@ckeditor/ckeditor5-core": "40.2.0", - "@ckeditor/ckeditor5-engine": "40.2.0", - "@ckeditor/ckeditor5-utils": "40.2.0", - "lodash-es": "4.17.21" - } - }, - "node_modules/@ckeditor/ckeditor5-ui": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-ui/-/ckeditor5-ui-40.2.0.tgz", - "integrity": "sha512-K8oC9zrJokZD5Nl4uQjJMo8Couds0eHmfNI/go6iU4A4OAdDzph+W50QnyMed4etKnMdhvUSbnuZnPtQjnsvFA==", - "license": "GPL-2.0-or-later", - "dependencies": { - "@ckeditor/ckeditor5-core": "40.2.0", - "@ckeditor/ckeditor5-utils": "40.2.0", - "color-convert": "2.0.1", - "color-parse": "1.4.2", - "lodash-es": "4.17.21", - "vanilla-colorful": "0.7.2" - } - }, - "node_modules/@ckeditor/ckeditor5-undo": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-undo/-/ckeditor5-undo-40.2.0.tgz", - "integrity": "sha512-k2VZS5x4SJtYk3zhdwHYg+D00DgD0iWR0H4qQgcWmQMFRipYvXJRixP3hSLZGJciQanPFeYcjZgxNQ+rU1s8ug==", - "license": "GPL-2.0-or-later", - "dependencies": { - "@ckeditor/ckeditor5-core": "40.2.0", - "@ckeditor/ckeditor5-engine": "40.2.0", - "@ckeditor/ckeditor5-ui": "40.2.0" - } - }, - "node_modules/@ckeditor/ckeditor5-upload": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-upload/-/ckeditor5-upload-40.2.0.tgz", - "integrity": "sha512-AdJSKvWEQbSSyA/DfxbCHRhFN6S4ew4kuYETO57e6AS3aOuYGLBRdu9Mub7IAQcOyy1LL6ktr9u5WEOoWS2h0w==", - "license": "GPL-2.0-or-later", - "dependencies": { - "@ckeditor/ckeditor5-core": "40.2.0", - "@ckeditor/ckeditor5-ui": "40.2.0", - "@ckeditor/ckeditor5-utils": "40.2.0" - } - }, - "node_modules/@ckeditor/ckeditor5-utils": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-utils/-/ckeditor5-utils-40.2.0.tgz", - "integrity": "sha512-f+kTJBwwk7Y/LXm8pEPxBTXVlJwQrH7Levzye9zxEDB0Jtj7+brGr87o666fPmL/ATQc5M+VPhbvnk2sOv7WKg==", - "license": "GPL-2.0-or-later", - "dependencies": { - "lodash-es": "4.17.21" - } - }, - "node_modules/@ckeditor/ckeditor5-watchdog": { - "version": "37.1.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-watchdog/-/ckeditor5-watchdog-37.1.0.tgz", - "integrity": "sha512-0d4WU2BO5n0tNzJl9iamnrFK+XEaK7gVEMIXcduznbupfFGVYFdrOXfDTdW0Yr59kpKEG8JbaWOF3aILjBRRWA==", - "license": "GPL-2.0-or-later", - "dependencies": { - "lodash-es": "^4.17.15" - }, - "engines": { - "node": ">=16.0.0", - "npm": ">=5.7.1" - } - }, - "node_modules/@ckeditor/ckeditor5-widget": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-widget/-/ckeditor5-widget-40.2.0.tgz", - "integrity": "sha512-okeUSwbnu6TUKvwBOl0YdED6Me0/vvs1ybfKZPNEJNwGl989iG0LQO4oYUye8BTCZvzCZ2cBTb1Cvnwr8KRcbg==", - "license": "GPL-2.0-or-later", - "dependencies": { - "@ckeditor/ckeditor5-core": "40.2.0", - "@ckeditor/ckeditor5-engine": "40.2.0", - "@ckeditor/ckeditor5-enter": "40.2.0", - "@ckeditor/ckeditor5-typing": "40.2.0", - "@ckeditor/ckeditor5-ui": "40.2.0", - "@ckeditor/ckeditor5-utils": "40.2.0", - "lodash-es": "4.17.21" - } - }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -7422,6 +7054,27 @@ "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1" } }, + "node_modules/@tinymce/tinymce-angular": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/@tinymce/tinymce-angular/-/tinymce-angular-9.1.1.tgz", + "integrity": "sha512-4W15Ruheqg9hnCUhtNoYX/SnvytT9N0ugv1Tk/aXXXVQMUCQwL3MQBNn8SNer8iZvkDbP+11au0zWW9dWVGSVA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=16.0.0", + "@angular/core": ">=16.0.0", + "@angular/forms": ">=16.0.0", + "rxjs": "^7.4.0", + "tinymce": "^8.0.0 || ^7.0.0 || ^6.0.0 || ^5.5.0" + }, + "peerDependenciesMeta": { + "tinymce": { + "optional": true + } + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -9607,12 +9260,6 @@ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", "license": "MIT" }, - "node_modules/blurhash": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/blurhash/-/blurhash-2.0.5.tgz", - "integrity": "sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w==", - "license": "MIT" - }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -10213,36 +9860,6 @@ "dev": true, "license": "MIT" }, - "node_modules/ckeditor5": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/ckeditor5/-/ckeditor5-40.2.0.tgz", - "integrity": "sha512-JaFuY/6DX1wbA6yRB2xQVMr+9W1C3HvSX4AT10ccoKBKe9OctIatekDt2ztV+cMaVHLF1wocskS/Ql9XFRy2Eg==", - "license": "GPL-2.0-or-later", - "dependencies": { - "@ckeditor/ckeditor5-clipboard": "40.2.0", - "@ckeditor/ckeditor5-core": "40.2.0", - "@ckeditor/ckeditor5-engine": "40.2.0", - "@ckeditor/ckeditor5-enter": "40.2.0", - "@ckeditor/ckeditor5-paragraph": "40.2.0", - "@ckeditor/ckeditor5-select-all": "40.2.0", - "@ckeditor/ckeditor5-typing": "40.2.0", - "@ckeditor/ckeditor5-ui": "40.2.0", - "@ckeditor/ckeditor5-undo": "40.2.0", - "@ckeditor/ckeditor5-upload": "40.2.0", - "@ckeditor/ckeditor5-utils": "40.2.0", - "@ckeditor/ckeditor5-watchdog": "40.2.0", - "@ckeditor/ckeditor5-widget": "40.2.0" - } - }, - "node_modules/ckeditor5/node_modules/@ckeditor/ckeditor5-watchdog": { - "version": "40.2.0", - "resolved": "https://registry.npmjs.org/@ckeditor/ckeditor5-watchdog/-/ckeditor5-watchdog-40.2.0.tgz", - "integrity": "sha512-ets7o2dUR7l23G9o/RAbu+gJzUkc2Ul269E3TEhZnbQXFjshvEGK2kzuay7I+/waL3ADuYe4zuoBqsqdPoAhfg==", - "license": "GPL-2.0-or-later", - "dependencies": { - "lodash-es": "4.17.21" - } - }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -10480,15 +10097,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, - "node_modules/color-parse": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/color-parse/-/color-parse-1.4.2.tgz", - "integrity": "sha512-RI7s49/8yqDj3fECFZjUI1Yi0z/Gq1py43oNJivAIIDSyJiOZLfYCRQEgn8HEVAj++PcRe8AnL2XF0fRJ3BTnA==", - "license": "MIT", - "dependencies": { - "color-name": "^1.0.0" - } - }, "node_modules/colord": { "version": "2.9.3", "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", @@ -17055,12 +16663,6 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, - "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "license": "MIT" - }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -22508,6 +22110,12 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinymce": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-8.2.2.tgz", + "integrity": "sha512-CFDSZwciMvFGW2czK/Xig1HcOGpXI0qcQMIqaIcG2F4RuuTdf+LQTreyEZunAJoFTQ9L0KAugOqL7OA5TJkoAA==", + "license": "GPL-2.0-or-later" + }, "node_modules/tldts": { "version": "6.1.86", "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", @@ -23259,12 +22867,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/vanilla-colorful": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/vanilla-colorful/-/vanilla-colorful-0.7.2.tgz", - "integrity": "sha512-z2YZusTFC6KnLERx1cgoIRX2CjPRP0W75N+3CC6gbvdX5Ch47rZkEMGO2Xnf+IEmi3RiFLxS18gayMA27iU7Kg==", - "license": "MIT" - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 0cbfe3162a..7a8a002792 100644 --- a/package.json +++ b/package.json @@ -43,8 +43,6 @@ "@angular/platform-browser": "19.2.14", "@angular/platform-browser-dynamic": "19.2.14", "@angular/router": "19.2.14", - "@ckeditor/ckeditor5-angular": "6.0.1", - "@ckeditor/ckeditor5-build-classic": "40.2.0", "@fortawesome/angular-fontawesome": "^1.0.0", "@fortawesome/fontawesome-free": "^6.7.2", "@fortawesome/fontawesome-svg-core": "6.2.1", @@ -54,6 +52,7 @@ "@ngx-translate/http-loader": "^16.0.1", "@swimlane/ngx-graph": "^10.0.0", "@tailwindcss/forms": "^0.5.4", + "@tinymce/tinymce-angular": "^9.1.1", "chart.js": "3.0.0-alpha", "d3": "^7.9.0", "exceljs": "^4.4.0", @@ -64,6 +63,7 @@ "moment": "^2.29.4", "ngx-mat-select-search": "^7.0.10", "rxjs": "7.8.1", + "tinymce": "^8.2.2", "tslib": "^2.0.0", "vkbeautify": "^0.99.3", "zone.js": "0.15.1" diff --git a/src/app/configuration-wizard/configuration-wizard.component.ts b/src/app/configuration-wizard/configuration-wizard.component.ts index 3197fec71a..79e94dfbd6 100644 --- a/src/app/configuration-wizard/configuration-wizard.component.ts +++ b/src/app/configuration-wizard/configuration-wizard.component.ts @@ -9,7 +9,6 @@ import { MatDialogActions } from '@angular/material/dialog'; import { CdkScrollable } from '@angular/cdk/scrolling'; -import { MatGridList, MatGridTile } from '@angular/material/grid-list'; import { MatProgressBar } from '@angular/material/progress-bar'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @@ -25,15 +24,12 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; MatDialogTitle, CdkScrollable, MatDialogContent, - MatGridList, - MatGridTile, MatDialogClose, MatProgressBar, MatDialogActions ] }) export class ConfigurationWizardComponent { - show: number; /** * @param {MatDialogRef} dialogRef MatDialogRef. */ diff --git a/src/app/products/manage-tax-components/view-tax-component/view-tax-component.component.spec.ts b/src/app/products/manage-tax-components/view-tax-component/view-tax-component.component.spec.ts new file mode 100644 index 0000000000..6747bb3565 --- /dev/null +++ b/src/app/products/manage-tax-components/view-tax-component/view-tax-component.component.spec.ts @@ -0,0 +1,560 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute, Router } from '@angular/router'; +import { of, BehaviorSubject } from 'rxjs'; +import { ViewTaxComponentComponent } from './view-tax-component.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { provideNativeDateAdapter } from '@angular/material/core'; +import { FaIconLibrary } from '@fortawesome/angular-fontawesome'; +import * as solidIcons from '@fortawesome/free-solid-svg-icons'; +import { describe, it, expect, jest, beforeEach } from '@jest/globals'; +import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; +import { AuthenticationService } from 'app/core/authentication/authentication.service'; +import { SettingsService } from 'app/settings/settings.service'; +import { DecimalPipe, DatePipe } from '@angular/common'; + +describe('ViewTaxComponentComponent - Integration Tests', () => { + let component: ViewTaxComponentComponent; + let fixture: ComponentFixture; + let mockRouter: jest.Mocked; + let routeDataSubject: BehaviorSubject; + + const mockTaxComponentData: any = { + id: 1, + name: 'VAT', + percentage: 15.5, + startDate: [ + 2024, + 1, + 1 + ], + creditAccount: { + id: 101, + name: 'Tax Payable', + glCode: '2001' + }, + creditAccountId: 101, + creditAccountName: 'Tax Payable', + creditAccountType: { + id: 2, + code: 'LIABILITY', + value: 'Liability' + } + }; + + beforeEach(async () => { + mockRouter = { + navigate: jest.fn() + } as any; + + const mockAuthenticationService = { + isAuthenticated: jest.fn<() => boolean>().mockReturnValue(true), + getCredentials: jest.fn().mockReturnValue({ + username: 'testuser', + accessToken: 'test-token', + permissions: ['ALL_FUNCTIONS'] + }) + }; + + const mockSettingsService = { + language: { code: 'en' }, + dateFormat: 'dd MMMM yyyy', + decimals: '2' + }; + + routeDataSubject = new BehaviorSubject({ + taxComponent: { ...mockTaxComponentData } + }); + + await TestBed.configureTestingModule({ + imports: [ + ViewTaxComponentComponent, + TranslateModule.forRoot() + ], + providers: [ + { provide: Router, useValue: mockRouter }, + { provide: AuthenticationService, useValue: mockAuthenticationService }, + { provide: SettingsService, useValue: mockSettingsService }, + { + provide: ActivatedRoute, + useValue: { + data: routeDataSubject.asObservable(), + snapshot: { data: routeDataSubject.value } + } + }, + DatePipe, + DecimalPipe, + provideNativeDateAdapter(), + provideAnimationsAsync() + + ] + }).compileComponents(); + + // Add all FontAwesome solid icons to handle all child component icon requirements + const faIconLibrary = TestBed.inject(FaIconLibrary); + const iconList = Object.keys(solidIcons) + .filter((key) => key !== 'fas' && key !== 'prefix' && key.startsWith('fa')) + .map((icon) => (solidIcons as any)[icon]); + faIconLibrary.addIcons(...iconList); + + fixture = TestBed.createComponent(ViewTaxComponentComponent); + component = fixture.componentInstance; + }); + + describe('Component Initialization', () => { + it('should create the component', () => { + expect(component).toBeTruthy(); + }); + + it('should load tax component data from route resolver', () => { + fixture.detectChanges(); + expect(component.taxComponentData).toEqual(mockTaxComponentData); + }); + + it('should subscribe to route data on initialization', () => { + expect(component.taxComponentData).toBeDefined(); + expect(component.taxComponentData.id).toBe(1); + expect(component.taxComponentData.name).toBe('VAT'); + }); + + it('should handle tax component data with all properties', () => { + fixture.detectChanges(); + expect(component.taxComponentData.percentage).toBe(15.5); + expect(component.taxComponentData.creditAccountId).toBe(101); + expect(component.taxComponentData.creditAccountName).toBe('Tax Payable'); + }); + }); + + describe('Route Data Subscription', () => { + it('should update tax component data when route data changes', () => { + const updatedTaxData = { + id: 2, + name: 'GST', + percentage: 18.0, + startDate: [ + 2024, + 6, + 1 + ], + creditAccountId: 102, + creditAccountName: 'GST Payable' + }; + + routeDataSubject.next({ + taxComponent: updatedTaxData + }); + + expect(component.taxComponentData).toEqual(updatedTaxData); + expect(component.taxComponentData.name).toBe('GST'); + expect(component.taxComponentData.percentage).toBe(18.0); + }); + + it('should handle multiple route data updates', () => { + const firstUpdate = { + id: 2, + name: 'Sales Tax', + percentage: 10.0 + }; + + const secondUpdate = { + id: 3, + name: 'Service Tax', + percentage: 12.0 + }; + + routeDataSubject.next({ taxComponent: firstUpdate }); + expect(component.taxComponentData.name).toBe('Sales Tax'); + + routeDataSubject.next({ taxComponent: secondUpdate }); + expect(component.taxComponentData.name).toBe('Service Tax'); + }); + + it('should maintain subscription throughout component lifecycle', () => { + fixture.detectChanges(); + const initialData = component.taxComponentData; + expect(initialData).toBeDefined(); + + const newData = { + id: 99, + name: 'Updated Tax', + percentage: 20.0 + }; + + routeDataSubject.next({ taxComponent: newData }); + expect(component.taxComponentData).toEqual(newData); + expect(component.taxComponentData).not.toEqual(initialData); + }); + }); + + describe('Tax Component Data Properties', () => { + it('should correctly handle tax component with id', () => { + fixture.detectChanges(); + expect(component.taxComponentData.id).toBe(1); + }); + + it('should correctly handle tax component name', () => { + fixture.detectChanges(); + expect(component.taxComponentData.name).toBe('VAT'); + expect(typeof component.taxComponentData.name).toBe('string'); + }); + + it('should correctly handle tax component percentage', () => { + fixture.detectChanges(); + expect(component.taxComponentData.percentage).toBe(15.5); + expect(typeof component.taxComponentData.percentage).toBe('number'); + }); + + it('should correctly handle tax component start date', () => { + fixture.detectChanges(); + expect(component.taxComponentData.startDate).toEqual([ + 2024, + 1, + 1 + ]); + expect(Array.isArray(component.taxComponentData.startDate)).toBe(true); + }); + + it('should correctly handle credit account information', () => { + fixture.detectChanges(); + expect(component.taxComponentData.creditAccountId).toBe(101); + expect(component.taxComponentData.creditAccountName).toBe('Tax Payable'); + }); + + it('should correctly handle nested credit account object', () => { + fixture.detectChanges(); + expect(component.taxComponentData.creditAccount).toBeDefined(); + expect(component.taxComponentData.creditAccount.id).toBe(101); + expect(component.taxComponentData.creditAccount.name).toBe('Tax Payable'); + expect(component.taxComponentData.creditAccount.glCode).toBe('2001'); + }); + + it('should correctly handle credit account type', () => { + fixture.detectChanges(); + expect(component.taxComponentData.creditAccountType).toBeDefined(); + expect(component.taxComponentData.creditAccountType.code).toBe('LIABILITY'); + expect(component.taxComponentData.creditAccountType.value).toBe('Liability'); + }); + }); + + describe('Edge Cases and Error Handling', () => { + it('should handle tax component data with minimal properties', () => { + const minimalTaxData = { + id: 1, + name: 'Simple Tax' + }; + + routeDataSubject.next({ taxComponent: minimalTaxData }); + expect(component.taxComponentData).toEqual(minimalTaxData); + expect(component.taxComponentData.name).toBe('Simple Tax'); + }); + + it('should handle tax component data with zero percentage', () => { + const zeroPercentageTax = { + id: 1, + name: 'Zero Tax', + percentage: 0 + }; + + routeDataSubject.next({ taxComponent: zeroPercentageTax }); + expect(component.taxComponentData.percentage).toBe(0); + }); + + it('should handle tax component data with high percentage', () => { + const highPercentageTax = { + id: 1, + name: 'High Tax', + percentage: 99.99 + }; + + routeDataSubject.next({ taxComponent: highPercentageTax }); + expect(component.taxComponentData.percentage).toBe(99.99); + }); + + it('should handle tax component with null credit account', () => { + const taxWithNullAccount: any = { + id: 1, + name: 'Tax Without Account', + percentage: 10, + creditAccount: null + }; + + routeDataSubject.next({ taxComponent: taxWithNullAccount }); + expect(component.taxComponentData.creditAccount).toBeNull(); + }); + + it('should handle tax component with undefined properties', () => { + const taxWithUndefined: any = { + id: 1, + name: 'Tax', + percentage: undefined, + startDate: undefined + }; + + routeDataSubject.next({ taxComponent: taxWithUndefined }); + expect(component.taxComponentData.percentage).toBeUndefined(); + expect(component.taxComponentData.startDate).toBeUndefined(); + }); + + it('should handle empty tax component data object', () => { + const emptyTaxData = {}; + + routeDataSubject.next({ taxComponent: emptyTaxData }); + expect(component.taxComponentData).toEqual(emptyTaxData); + }); + + it('should handle tax component with different date formats', () => { + const taxWithDateString = { + id: 1, + name: 'Tax', + startDate: '2024-01-01' + }; + + routeDataSubject.next({ taxComponent: taxWithDateString }); + expect(component.taxComponentData.startDate).toBe('2024-01-01'); + }); + + it('should handle tax component with special characters in name', () => { + const taxWithSpecialChars = { + id: 1, + name: 'Tax & Service @ 15%', + percentage: 15 + }; + + routeDataSubject.next({ taxComponent: taxWithSpecialChars }); + expect(component.taxComponentData.name).toBe('Tax & Service @ 15%'); + }); + + it('should handle tax component with very long name', () => { + const longName = 'A'.repeat(200); + const taxWithLongName = { + id: 1, + name: longName, + percentage: 10 + }; + + routeDataSubject.next({ taxComponent: taxWithLongName }); + expect(component.taxComponentData.name).toBe(longName); + expect(component.taxComponentData.name.length).toBe(200); + }); + + it('should handle tax component with negative id', () => { + const taxWithNegativeId = { + id: -1, + name: 'Invalid Tax', + percentage: 10 + }; + + routeDataSubject.next({ taxComponent: taxWithNegativeId }); + expect(component.taxComponentData.id).toBe(-1); + }); + }); + + describe('Component Rendering', () => { + it('should render component without errors when data is provided', () => { + expect(() => { + fixture.detectChanges(); + }).not.toThrow(); + }); + + it('should update view when tax component data changes', () => { + fixture.detectChanges(); + const initialName = component.taxComponentData.name; + + const newTaxData = { + id: 2, + name: 'Updated Tax Name', + percentage: 20 + }; + + routeDataSubject.next({ taxComponent: newTaxData }); + fixture.detectChanges(); + + expect(component.taxComponentData.name).not.toBe(initialName); + expect(component.taxComponentData.name).toBe('Updated Tax Name'); + }); + + it('should handle component destruction gracefully', () => { + fixture.detectChanges(); + expect(() => { + fixture.destroy(); + }).not.toThrow(); + }); + }); + + describe('Integration with ActivatedRoute', () => { + it('should correctly inject ActivatedRoute', () => { + const activatedRoute = TestBed.inject(ActivatedRoute); + expect(activatedRoute).toBeDefined(); + }); + + it('should access route data through subscription', () => { + let dataEmitted = false; + const activatedRoute = TestBed.inject(ActivatedRoute); + + activatedRoute.data.subscribe((data) => { + dataEmitted = true; + expect(data.taxComponent).toBeDefined(); + }); + + expect(dataEmitted).toBe(true); + }); + + it('should handle route snapshot data', () => { + const activatedRoute = TestBed.inject(ActivatedRoute); + expect(activatedRoute.snapshot.data.taxComponent).toBeDefined(); + expect(activatedRoute.snapshot.data.taxComponent).toEqual(mockTaxComponentData); + }); + }); + + describe('Complete Tax Component View Workflow', () => { + it('should complete full view workflow', () => { + // 1. Component initializes + fixture.detectChanges(); + expect(component).toBeTruthy(); + + // 2. Tax component data is loaded + expect(component.taxComponentData).toBeDefined(); + expect(component.taxComponentData.id).toBe(1); + + // 3. Data is correctly populated + expect(component.taxComponentData.name).toBe('VAT'); + expect(component.taxComponentData.percentage).toBe(15.5); + expect(component.taxComponentData.creditAccountId).toBe(101); + + // 4. Component can handle data updates + const updatedData = { + id: 1, + name: 'Updated VAT', + percentage: 16.0 + }; + + routeDataSubject.next({ taxComponent: updatedData }); + expect(component.taxComponentData.name).toBe('Updated VAT'); + expect(component.taxComponentData.percentage).toBe(16.0); + }); + + it('should maintain data integrity throughout component lifecycle', () => { + // Initial state + fixture.detectChanges(); + const initialId = component.taxComponentData.id; + + // Multiple updates + for (let i = 1; i <= 5; i++) { + const newData = { + id: initialId, + name: `Tax Update ${i}`, + percentage: 10 + i + }; + routeDataSubject.next({ taxComponent: newData }); + expect(component.taxComponentData.name).toBe(`Tax Update ${i}`); + expect(component.taxComponentData.percentage).toBe(10 + i); + } + + // Final state verification + expect(component.taxComponentData.id).toBe(initialId); + }); + }); + + describe('Data Type Validation', () => { + it('should handle tax component with all string values', () => { + const taxWithStrings = { + id: '1', + name: 'String Tax', + percentage: '15.5', + creditAccountId: '101' + }; + + routeDataSubject.next({ taxComponent: taxWithStrings }); + expect(component.taxComponentData.id).toBe('1'); + expect(component.taxComponentData.percentage).toBe('15.5'); + }); + + it('should handle tax component with mixed data types', () => { + const taxWithMixedTypes = { + id: 1, + name: 'Mixed Tax', + percentage: '15.5', + isActive: true, + creditAccountId: 101 + }; + + routeDataSubject.next({ taxComponent: taxWithMixedTypes }); + expect(component.taxComponentData.isActive).toBe(true); + expect(typeof component.taxComponentData.id).toBe('number'); + }); + + it('should handle tax component with array properties', () => { + const taxWithArrays = { + id: 1, + name: 'Array Tax', + startDate: [ + 2024, + 1, + 1 + ], + applicableRegions: [ + 'North', + 'South', + 'East' + ] + }; + + routeDataSubject.next({ taxComponent: taxWithArrays }); + expect(Array.isArray(component.taxComponentData.startDate)).toBe(true); + expect(Array.isArray(component.taxComponentData.applicableRegions)).toBe(true); + expect(component.taxComponentData.applicableRegions.length).toBe(3); + }); + + it('should handle tax component with nested objects', () => { + const taxWithNestedObjects = { + id: 1, + name: 'Nested Tax', + creditAccount: { + id: 101, + name: 'Tax Account', + details: { + glCode: '2001', + type: 'LIABILITY' + } + } + }; + + routeDataSubject.next({ taxComponent: taxWithNestedObjects }); + expect(component.taxComponentData.creditAccount.details).toBeDefined(); + expect(component.taxComponentData.creditAccount.details.glCode).toBe('2001'); + }); + }); + + describe('Memory and Performance', () => { + it('should not create memory leaks with multiple updates', () => { + fixture.detectChanges(); + + for (let i = 0; i < 100; i++) { + const newData = { + id: i, + name: `Tax ${i}`, + percentage: i * 0.1 + }; + routeDataSubject.next({ taxComponent: newData }); + } + + expect(component.taxComponentData.id).toBe(99); + expect(component.taxComponentData.name).toBe('Tax 99'); + }); + + it('should handle rapid consecutive updates', () => { + fixture.detectChanges(); + + const updates = [ + { id: 1, name: 'Tax 1' }, + { id: 2, name: 'Tax 2' }, + { id: 3, name: 'Tax 3' } + ]; + + updates.forEach((update) => { + routeDataSubject.next({ taxComponent: update }); + }); + + expect(component.taxComponentData.name).toBe('Tax 3'); + }); + }); +}); diff --git a/src/app/templates/create-edit-template/create-edit-template.component.html b/src/app/templates/create-edit-template/create-edit-template.component.html index a13e811ee8..b499e34686 100644 --- a/src/app/templates/create-edit-template/create-edit-template.component.html +++ b/src/app/templates/create-edit-template/create-edit-template.component.html @@ -81,15 +81,20 @@
-
diff --git a/src/app/templates/create-edit-template/create-edit-template.component.scss b/src/app/templates/create-edit-template/create-edit-template.component.scss index 506a5d4818..5300b6eada 100644 --- a/src/app/templates/create-edit-template/create-edit-template.component.scss +++ b/src/app/templates/create-edit-template/create-edit-template.component.scss @@ -14,7 +14,7 @@ } } -// Editor wrapper - prevents CKEditor from touching borders on left and right +// Editor wrapper - prevents TinyMCE from touching borders on left and right .editor-wrapper { margin-top: 16px; @@ -29,15 +29,19 @@ color: #f44336; } - // CKEditor container spacing + .editor-error { + color: #f44336; + font-size: 12px; + margin-top: 6px; + } + + // TinyMCE container spacing ::ng-deep { - .ck-editor { + .tox-tinymce { margin: 0; - .ck-editor__main { - .ck-content { - min-height: 200px; - } + .tox-edit-area__iframe { + min-height: 260px; } } } diff --git a/src/app/templates/create-edit-template/create-edit-template.component.ts b/src/app/templates/create-edit-template/create-edit-template.component.ts index bfc1b0e806..1849e810af 100644 --- a/src/app/templates/create-edit-template/create-edit-template.component.ts +++ b/src/app/templates/create-edit-template/create-edit-template.component.ts @@ -1,16 +1,11 @@ /** Angular Imports */ import { Component, OnInit, ViewChild } from '@angular/core'; -import { - UntypedFormGroup, - UntypedFormBuilder, - UntypedFormControl, - Validators, - ReactiveFormsModule -} from '@angular/forms'; -import { Router, ActivatedRoute, RouterLink } from '@angular/router'; +import { UntypedFormGroup, UntypedFormBuilder, UntypedFormControl, Validators } from '@angular/forms'; +import { Router, ActivatedRoute } from '@angular/router'; -/** CKEditor5 Imports */ -import ClassicEditor from '@ckeditor/ckeditor5-build-classic'; +/** TinyMCE Imports */ +import { EditorComponent, EditorModule } from '@tinymce/tinymce-angular'; +import type { EditorOptions } from 'tinymce'; /** Custom Imports */ import { clientParameterLabels, loanParameterLabels, repaymentParameterLabels } from '../template-parameter-labels'; @@ -18,7 +13,6 @@ import { clientParameterLabels, loanParameterLabels, repaymentParameterLabels } /** Custom Services */ import { TemplatesService } from '../templates.service'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; -import { CKEditorModule } from '@ckeditor/ckeditor5-angular'; import { MatAccordion, MatExpansionPanel, @@ -26,6 +20,7 @@ import { MatExpansionPanelTitle } from '@angular/material/expansion'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import '../tinymce-loader'; /** * Create Template Component. @@ -37,7 +32,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; imports: [ ...STANDALONE_SHARED_IMPORTS, FaIconComponent, - CKEditorModule, + EditorModule, MatAccordion, MatExpansionPanel, MatExpansionPanelHeader, @@ -45,10 +40,41 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; ] }) export class CreateEditComponent implements OnInit { - /** CKEditor5 */ - public Editor = ClassicEditor; - /** CKEditor5 Template Reference */ - @ViewChild('ckEditor', { static: true }) ckEditor: any; + /** TinyMCE Template Reference */ + @ViewChild('tinyEditor') tinyEditor?: EditorComponent; + /** Default TinyMCE editor options (using bundled version via tinymce-loader.ts). */ + readonly tinyMceInit: Partial = { + skin: false, + content_css: [], + menubar: false, + statusbar: false, + height: 340, + min_height: 260, + // Prevent external resource loading + base_url: '', + suffix: '', + plugins: [ + 'advlist', + 'autolink', + 'lists', + 'link', + 'charmap', + 'preview', + 'searchreplace', + 'visualblocks', + 'code', + 'fullscreen', + 'table', + 'wordcount', + 'image', + 'media' + ], + toolbar: + 'undo redo | blocks | bold italic | link image table blockquote media | ' + + 'bullist numlist | outdent indent | alignleft aligncenter alignright alignjustify | removeformat code fullscreen preview', + toolbar_mode: 'wrap', + content_style: 'body { font-family: var(--mat-body-large-font-family, Inter, Arial, sans-serif); font-size: 14px; }' + }; /** Template form. */ templateForm: UntypedFormGroup; @@ -67,6 +93,14 @@ export class CreateEditComponent implements OnInit { loanParameterLabels: string[] = loanParameterLabels; /** Repayment Parameter Labels */ repaymentParameterLabels: string[] = repaymentParameterLabels; + /** Convenience getter for editor form control. */ + private get textControl(): UntypedFormControl { + return this.templateForm.get('text') as UntypedFormControl; + } + /** Exposes the editor control to the template. */ + get textFormControl(): UntypedFormControl { + return this.textControl; + } /** * Retrieves the template data from `resolve`. @@ -166,6 +200,8 @@ export class CreateEditComponent implements OnInit { }); } this.setEditorContent(''); + this.textControl.markAsPristine(); + this.textControl.markAsUntouched(); }); if (this.mode === 'create') { this.templateForm.get('entity').patchValue(0); @@ -192,42 +228,49 @@ export class CreateEditComponent implements OnInit { } /** - * Adds text to CKEditor at cursor position. + * Adds text to the TinyMCE editor at cursor position. * @param {string} label Template parameter label. */ addText(label: string) { - if (this.ckEditor && this.ckEditor.editorInstance) { - this.ckEditor.editorInstance.model.change((writer: any) => { - const insertPosition = this.ckEditor.editorInstance.model.document.selection.getFirstPosition(); - writer.insertText(label, insertPosition); - }); + const editorInstance = this.tinyEditor?.editor; + if (editorInstance) { + editorInstance.focus(); + editorInstance.insertContent(label); + const updatedContent = editorInstance.getContent() || ''; + this.textControl.setValue(updatedContent); + this.textControl.markAsDirty(); + this.textControl.markAsTouched(); + return; } + const fallbackValue = this.textControl.value || ''; + this.textControl.setValue(`${fallbackValue}${label}`); + this.textControl.markAsDirty(); + this.textControl.markAsTouched(); } /** - * Gets the contents of CKEditor. + * Gets the contents of the editor. */ getEditorContent() { - if (this.ckEditor && this.ckEditor.editorInstance) { - return this.ckEditor.editorInstance.getData(); + if (this.tinyEditor?.editor) { + return this.tinyEditor.editor.getContent() || ''; } - return ''; + return this.textControl?.value || ''; } /** - * Sets the contents of CKEditor. + * Sets the contents of the editor. * @param {string} content Editor Content */ setEditorContent(content: string) { - if (this.ckEditor && this.ckEditor.editorInstance) { - return this.ckEditor.editorInstance.setData(content); - } - return ''; + this.textControl.setValue(content || ''); } - onEditorChange(event: any) { - const editorContent = event.editor.getData(); - this.templateForm.get('text').setValue(editorContent); + /** + * Marks the editor control as touched when focus leaves TinyMCE. + */ + onEditorBlur() { + this.textControl.markAsTouched(); } /** diff --git a/src/app/templates/templates.module.ts b/src/app/templates/templates.module.ts index 03ada4a8e8..54885477dd 100644 --- a/src/app/templates/templates.module.ts +++ b/src/app/templates/templates.module.ts @@ -2,7 +2,7 @@ import { NgModule } from '@angular/core'; /** Custom Modules */ -import { CKEditorModule } from '@ckeditor/ckeditor5-angular'; +import { EditorModule } from '@tinymce/tinymce-angular'; import { SharedModule } from '../shared/shared.module'; import { DirectivesModule } from '../directives/directives.module'; import { TemplatesRoutingModule } from './templates-routing.module'; @@ -16,10 +16,11 @@ import { CreateEditComponent } from './create-edit-template/create-edit-template * Templates Module * * Templates components should be declared here. + * TinyMCE is bundled locally via tinymce-loader.ts (no CDN script needed) */ @NgModule({ imports: [ - CKEditorModule, + EditorModule, SharedModule, DirectivesModule, TemplatesRoutingModule, diff --git a/src/app/templates/tinymce-loader.ts b/src/app/templates/tinymce-loader.ts new file mode 100644 index 0000000000..91da573283 --- /dev/null +++ b/src/app/templates/tinymce-loader.ts @@ -0,0 +1,28 @@ +import tinymce from 'tinymce/tinymce'; +import 'tinymce/models/dom'; +import 'tinymce/icons/default'; +import 'tinymce/themes/silver'; + +// Plugins used by CreateEditComponent +import 'tinymce/plugins/advlist'; +import 'tinymce/plugins/autolink'; +import 'tinymce/plugins/lists'; +import 'tinymce/plugins/link'; +import 'tinymce/plugins/charmap'; +import 'tinymce/plugins/preview'; +import 'tinymce/plugins/searchreplace'; +import 'tinymce/plugins/visualblocks'; +import 'tinymce/plugins/code'; +import 'tinymce/plugins/fullscreen'; +import 'tinymce/plugins/table'; +import 'tinymce/plugins/wordcount'; +import 'tinymce/plugins/image'; +import 'tinymce/plugins/media'; + +// Skins (UI + content) to ensure TinyMCE renders correctly +import 'tinymce/skins/ui/oxide/skin.css'; +import 'tinymce/skins/content/default/content.css'; + +if (typeof window !== 'undefined') { + (window as any).tinymce = tinymce; +} diff --git a/src/typings.d.ts b/src/typings.d.ts index 032cc380c2..74f54beda1 100644 --- a/src/typings.d.ts +++ b/src/typings.d.ts @@ -43,9 +43,3 @@ interface Window { } declare module 'chart.js'; - -declare module '@ckeditor/ckeditor5-build-classic' { - const ClassicEditorBuild: any; - - export = ClassicEditorBuild; -}