From 327a53db1e93dea62b2e8f46a33d93240b015346 Mon Sep 17 00:00:00 2001 From: Jay Date: Mon, 17 Feb 2025 09:50:21 +0100 Subject: [PATCH 01/13] fix: ensure user cookies are cleared before logout redirect (#1748) ## Description This PR ensures users cookies being cleared out when users click the logout button. ## Motivation Background on use case, changes needed ## Fixes: Please provide a list of the fixes implemented in this PR * Items added ## Changes: Please provide a list of the changes implemented by this PR * changes made ## Tests included - [ ] Included for each change/fix? - [ ] Passing? (Merge will not be approved unless this is checked) ## Documentation - [ ] swagger documentation updated \[required\] - [ ] official documentation updated \[nice-to-have\] ### official documentation info If you have updated the official documentation, please provide PR # and URL of the pages where the updates are included ## Backend version - [ ] Does it require a specific version of the backend - which version of the backend is required: ## Summary by Sourcery Bug Fixes: - Clear user cookies before redirecting on logout to ensure user data is properly cleared after logout action is triggered --- .../state-management/effects/user.effects.ts | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/app/state-management/effects/user.effects.ts b/src/app/state-management/effects/user.effects.ts index ec2dde01b..f9994f4c4 100644 --- a/src/app/state-management/effects/user.effects.ts +++ b/src/app/state-management/effects/user.effects.ts @@ -216,25 +216,23 @@ export class UserEffects { return this.actions$.pipe( ofType(fromActions.logoutAction), filter(() => this.authService.isAuthenticated()), - switchMap(() => - this.sharedAuthService.authControllerLogout().pipe( - switchMap(({ logoutURL }) => { - this.authService.clear(); - return [ - clearDatasetsStateAction(), - clearInstrumentsStateAction(), - clearJobsStateAction(), - clearLogbooksStateAction(), - clearPoliciesStateAction(), - clearProposalsStateAction(), - clearPublishedDataStateAction(), - clearSamplesStateAction(), - fromActions.logoutCompleteAction({ logoutURL }), - ]; - }), + switchMap(() => { + this.authService.clear(); + return this.sharedAuthService.authControllerLogout().pipe( + switchMap(({ logoutURL }) => [ + clearDatasetsStateAction(), + clearInstrumentsStateAction(), + clearJobsStateAction(), + clearLogbooksStateAction(), + clearPoliciesStateAction(), + clearProposalsStateAction(), + clearPublishedDataStateAction(), + clearSamplesStateAction(), + fromActions.logoutCompleteAction({ logoutURL }), + ]), catchError(() => of(fromActions.logoutFailedAction())), - ), - ), + ); + }), ); }); From 14a1cb708cc15bb32686ea7cf7807f88ab61932a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2025 18:52:01 +0000 Subject: [PATCH 02/13] chore(deps-dev): bump the eslint group with 2 updates Bumps the eslint group with 2 updates: [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) and [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser). Updates `@typescript-eslint/eslint-plugin` from 8.23.0 to 8.24.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.24.1/packages/eslint-plugin) Updates `@typescript-eslint/parser` from 8.23.0 to 8.24.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.24.1/packages/parser) --- updated-dependencies: - dependency-name: "@typescript-eslint/eslint-plugin" dependency-type: direct:development update-type: version-update:semver-minor dependency-group: eslint - dependency-name: "@typescript-eslint/parser" dependency-type: direct:development update-type: version-update:semver-minor dependency-group: eslint ... Signed-off-by: dependabot[bot] --- package-lock.json | 96 +++++++++++++++++++++++------------------------ package.json | 2 +- 2 files changed, 49 insertions(+), 49 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7df2321d8..b53e291ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "scicat-frontend", - "version": "4.5.0", + "version": "local.dev", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "scicat-frontend", - "version": "4.5.0", + "version": "local.dev", "license": "BSD-3-Clause", "dependencies": { "@angular-material-components/datetime-picker": "^16", @@ -68,7 +68,7 @@ "@types/node": "^22.0.0", "@types/shortid": "2.2.0", "@types/source-map-support": "^0.5.3", - "@typescript-eslint/eslint-plugin": "8.23.0", + "@typescript-eslint/eslint-plugin": "8.24.1", "@typescript-eslint/parser": "^8.0.0", "coveralls": "^3.0.7", "cypress": "^14.0.0", @@ -5705,17 +5705,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.23.0.tgz", - "integrity": "sha512-vBz65tJgRrA1Q5gWlRfvoH+w943dq9K1p1yDBY2pc+a1nbBLZp7fB9+Hk8DaALUbzjqlMfgaqlVPT1REJdkt/w==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.24.1.tgz", + "integrity": "sha512-ll1StnKtBigWIGqvYDVuDmXJHVH4zLVot1yQ4fJtLpL7qacwkxJc1T0bptqw+miBQ/QfUbhl1TcQ4accW5KUyA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.23.0", - "@typescript-eslint/type-utils": "8.23.0", - "@typescript-eslint/utils": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", + "@typescript-eslint/scope-manager": "8.24.1", + "@typescript-eslint/type-utils": "8.24.1", + "@typescript-eslint/utils": "8.24.1", + "@typescript-eslint/visitor-keys": "8.24.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -5735,14 +5735,14 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.23.0.tgz", - "integrity": "sha512-iIuLdYpQWZKbiH+RkCGc6iu+VwscP5rCtQ1lyQ7TYuKLrcZoeJVpcLiG8DliXVkUxirW/PWlmS+d6yD51L9jvA==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.24.1.tgz", + "integrity": "sha512-/Do9fmNgCsQ+K4rCz0STI7lYB4phTtEXqqCAs3gZW0pnK7lWNkvWd5iW545GSmApm4AzmQXmSqXPO565B4WVrw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.23.0", - "@typescript-eslint/utils": "8.23.0", + "@typescript-eslint/typescript-estree": "8.24.1", + "@typescript-eslint/utils": "8.24.1", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, @@ -5759,16 +5759,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.23.0.tgz", - "integrity": "sha512-uB/+PSo6Exu02b5ZEiVtmY6RVYO7YU5xqgzTIVZwTHvvK3HsL8tZZHFaTLFtRG3CsV4A5mhOv+NZx5BlhXPyIA==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.24.1.tgz", + "integrity": "sha512-OOcg3PMMQx9EXspId5iktsI3eMaXVwlhC8BvNnX6B5w9a4dVgpkQZuU8Hy67TolKcl+iFWq0XX+jbDGN4xWxjQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.23.0", - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/typescript-estree": "8.23.0" + "@typescript-eslint/scope-manager": "8.24.1", + "@typescript-eslint/types": "8.24.1", + "@typescript-eslint/typescript-estree": "8.24.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5811,16 +5811,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.23.0.tgz", - "integrity": "sha512-h2lUByouOXFAlMec2mILeELUbME5SZRN/7R9Cw2RD2lRQQY08MWMM+PmVVKKJNK1aIwqTo9t/0CvOxwPbRIE2Q==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.24.1.tgz", + "integrity": "sha512-Tqoa05bu+t5s8CTZFaGpCH2ub3QeT9YDkXbPd3uQ4SfsLoh1/vv2GEYAioPoxCWJJNsenXlC88tRjwoHNts1oQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.23.0", - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/typescript-estree": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", + "@typescript-eslint/scope-manager": "8.24.1", + "@typescript-eslint/types": "8.24.1", + "@typescript-eslint/typescript-estree": "8.24.1", + "@typescript-eslint/visitor-keys": "8.24.1", "debug": "^4.3.4" }, "engines": { @@ -5836,14 +5836,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.23.0.tgz", - "integrity": "sha512-OGqo7+dXHqI7Hfm+WqkZjKjsiRtFUQHPdGMXzk5mYXhJUedO7e/Y7i8AK3MyLMgZR93TX4bIzYrfyVjLC+0VSw==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.24.1.tgz", + "integrity": "sha512-OdQr6BNBzwRjNEXMQyaGyZzgg7wzjYKfX2ZBV3E04hUCBDv3GQCHiz9RpqdUIiVrMgJGkXm3tcEh4vFSHreS2Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0" + "@typescript-eslint/types": "8.24.1", + "@typescript-eslint/visitor-keys": "8.24.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5967,9 +5967,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.23.0.tgz", - "integrity": "sha512-1sK4ILJbCmZOTt9k4vkoulT6/y5CHJ1qUYxqpF1K/DBAd8+ZUL4LlSCxOssuH5m4rUaaN0uS0HlVPvd45zjduQ==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.1.tgz", + "integrity": "sha512-9kqJ+2DkUXiuhoiYIUvIYjGcwle8pcPpdlfkemGvTObzgmYfJ5d0Qm6jwb4NBXP9W1I5tss0VIAnWFumz3mC5A==", "dev": true, "license": "MIT", "engines": { @@ -5981,14 +5981,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.23.0.tgz", - "integrity": "sha512-LcqzfipsB8RTvH8FX24W4UUFk1bl+0yTOf9ZA08XngFwMg4Kj8A+9hwz8Cr/ZS4KwHrmo9PJiLZkOt49vPnuvQ==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.1.tgz", + "integrity": "sha512-UPyy4MJ/0RE648DSKQe9g0VDSehPINiejjA6ElqnFaFIhI6ZEiZAkUI0D5MCk0bQcTf/LVqZStvQ6K4lPn/BRg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", + "@typescript-eslint/types": "8.24.1", + "@typescript-eslint/visitor-keys": "8.24.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -6041,9 +6041,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.0.tgz", - "integrity": "sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, "license": "ISC", "bin": { @@ -6205,13 +6205,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.23.0.tgz", - "integrity": "sha512-oWWhcWDLwDfu++BGTZcmXWqpwtkwb5o7fxUIGksMQQDSdPW9prsSnfIOZMlsj4vBOSrcnjIUZMiIjODgGosFhQ==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.1.tgz", + "integrity": "sha512-EwVHlp5l+2vp8CoqJm9KikPZgi3gbdZAtabKT9KPShGeOcJhsv4Zdo3oc8T8I0uKEmYoU4ItyxbptjF08enaxg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.23.0", + "@typescript-eslint/types": "8.24.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { diff --git a/package.json b/package.json index 2783c36a1..02d22d185 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "@types/node": "^22.0.0", "@types/shortid": "2.2.0", "@types/source-map-support": "^0.5.3", - "@typescript-eslint/eslint-plugin": "8.23.0", + "@typescript-eslint/eslint-plugin": "8.24.1", "@typescript-eslint/parser": "^8.0.0", "coveralls": "^3.0.7", "cypress": "^14.0.0", From 8bc302a8063e5d5321e82bdc5e1690c4c0467966 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2025 19:01:03 +0000 Subject: [PATCH 03/13] chore(deps-dev): bump @types/node in the types group Bumps the types group with 1 update: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node). Updates `@types/node` from 22.13.0 to 22.13.4 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-type: direct:development update-type: version-update:semver-patch dependency-group: types ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b53e291ca..3033f29bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5553,9 +5553,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "22.13.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.0.tgz", - "integrity": "sha512-ClIbNe36lawluuvq3+YYhnIN2CELi+6q8NpnM7PYp4hBn/TatfboPgVSm2rwKRfnV2M+Ty9GWDFI64KEe+kysA==", + "version": "22.13.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz", + "integrity": "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==", "dev": true, "license": "MIT", "dependencies": { From 8ffa1fe393f1d2e311bf109f4174c5da31169dca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2025 19:09:42 +0000 Subject: [PATCH 04/13] chore(deps-dev): bump jasmine-core from 5.5.0 to 5.6.0 Bumps [jasmine-core](https://github.com/jasmine/jasmine) from 5.5.0 to 5.6.0. - [Release notes](https://github.com/jasmine/jasmine/releases) - [Changelog](https://github.com/jasmine/jasmine/blob/main/RELEASE.md) - [Commits](https://github.com/jasmine/jasmine/compare/v5.5.0...v5.6.0) --- updated-dependencies: - dependency-name: jasmine-core dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3033f29bb..02cca2bd8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12173,10 +12173,11 @@ } }, "node_modules/jasmine-core": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.5.0.tgz", - "integrity": "sha512-NHOvoPO6o9gVR6pwqEACTEpbgcH+JJ6QDypyymGbSUIFIFsMMbBJ/xsFNud8MSClfnWclXd7RQlAZBz7yVo5TQ==", - "dev": true + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.6.0.tgz", + "integrity": "sha512-niVlkeYVRwKFpmfWg6suo6H9CrNnydfBLEqefM5UjibYS+UoTjZdmvPJSiuyrRLGnFj1eYRhFd/ch+5hSlsFVA==", + "dev": true, + "license": "MIT" }, "node_modules/jasmine-marbles": { "version": "0.9.2", @@ -12219,6 +12220,13 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/jasmine/node_modules/jasmine-core": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.5.0.tgz", + "integrity": "sha512-NHOvoPO6o9gVR6pwqEACTEpbgcH+JJ6QDypyymGbSUIFIFsMMbBJ/xsFNud8MSClfnWclXd7RQlAZBz7yVo5TQ==", + "dev": true, + "license": "MIT" + }, "node_modules/jasmine/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", From 9e1d0194d35c32004be26b6e4ffbee49859d1a31 Mon Sep 17 00:00:00 2001 From: Jay Date: Thu, 20 Feb 2025 11:12:03 +0100 Subject: [PATCH 05/13] chore: allow setting the date format via app configuration (#1754) ## Description This PR removes hardcoded date formats from the HTML templates, replacing them with a configurable dateFormat property in frontend.config.json. This allows a single, centralized date format definition, improving flexibility and easing localization efforts. ## Motivation Background on use case, changes needed ## Fixes: Please provide a list of the fixes implemented in this PR * Items added ## Changes: Please provide a list of the changes implemented by this PR * changes made ## Tests included - [ ] Included for each change/fix? - [ ] Passing? (Merge will not be approved unless this is checked) ## Documentation - [ ] swagger documentation updated \[required\] - [ ] official documentation updated \[nice-to-have\] ### official documentation info If you have updated the official documentation, please provide PR # and URL of the pages where the updates are included ## Backend version - [ ] Does it require a specific version of the backend - which version of the backend is required: ## Summary by Sourcery This pull request introduces the ability to configure the date format used throughout the application via the application configuration. It replaces all hardcoded date formats with the configured format, providing a more flexible and user-friendly experience. New Features: - Allows configuring the date format via the application configuration. Enhancements: - Removes the hardcoded date format in the application, using the configured format instead. --- src/app/app-config.service.ts | 1 + src/app/app.module.ts | 11 +++++++++++ src/app/datasets/batch-view/batch-view.component.html | 2 +- .../dataset-detail/dataset-detail.component.html | 2 +- .../dataset-lifecycle.component.html | 10 +++++----- .../dataset-table/dataset-table.component.html | 3 +-- src/app/datasets/reduce/reduce.component.html | 2 +- .../datasets/sample-edit/sample-edit.component.html | 2 +- src/app/jobs/jobs-detail/jobs-detail.component.html | 10 +++++----- .../logbooks-detail/logbooks-detail.component.html | 2 +- .../logbooks-table/logbooks-table.component.html | 5 +---- .../publisheddata-details.component.html | 2 +- .../sample-detail/sample-detail.component.html | 2 +- 13 files changed, 31 insertions(+), 23 deletions(-) diff --git a/src/app/app-config.service.ts b/src/app/app-config.service.ts index 1724f252d..c906f8a98 100644 --- a/src/app/app-config.service.ts +++ b/src/app/app-config.service.ts @@ -105,6 +105,7 @@ export interface AppConfig { maxFileUploadSizeInMb?: string; datasetDetailComponent?: DatasetDetailComponentConfig; labelsLocalization?: LabelsLocalization; + dateFormat?: string; } @Injectable() diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 47b362472..5a6cf2996 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -30,6 +30,7 @@ import { InternalStorage, SDKStorage } from "shared/services/auth/base.storage"; import { CookieService } from "ngx-cookie-service"; import { TranslateLoader, TranslateModule } from "@ngx-translate/core"; import { CustomTranslateLoader } from "shared/loaders/custom-translate.loader"; +import { DATE_PIPE_DEFAULT_OPTIONS } from "@angular/common"; const appConfigInitializerFn = (appConfig: AppConfigService) => { return () => appConfig.loadAppConfig(); @@ -113,6 +114,16 @@ const apiConfigurationFn = ( subscriptSizing: "dynamic", }, }, + { + provide: DATE_PIPE_DEFAULT_OPTIONS, + useFactory: (appConfigService: AppConfigService) => { + return { + dateFormat: + appConfigService.getConfig().dateFormat || "yyyy-MM-dd HH:mm", + }; + }, + deps: [AppConfigService], + }, AuthService, AppThemeService, Title, diff --git a/src/app/datasets/batch-view/batch-view.component.html b/src/app/datasets/batch-view/batch-view.component.html index c84f89a60..cce14bc9f 100644 --- a/src/app/datasets/batch-view/batch-view.component.html +++ b/src/app/datasets/batch-view/batch-view.component.html @@ -116,7 +116,7 @@ - {{ dataset.creationTime | date: "yyyy-MM-dd HH:mm" }} + {{ dataset.creationTime | date }} diff --git a/src/app/datasets/dataset-detail/dataset-detail/dataset-detail.component.html b/src/app/datasets/dataset-detail/dataset-detail/dataset-detail.component.html index 8db1e02b5..cc02a38af 100644 --- a/src/app/datasets/dataset-detail/dataset-detail/dataset-detail.component.html +++ b/src/app/datasets/dataset-detail/dataset-detail/dataset-detail.component.html @@ -79,7 +79,7 @@ {{ "Creation time" | translate }} - {{ value | date: "yyyy-MM-dd HH:mm" }} + {{ value | date }} {{ "Keywords" | translate }} diff --git a/src/app/datasets/dataset-lifecycle/dataset-lifecycle.component.html b/src/app/datasets/dataset-lifecycle/dataset-lifecycle.component.html index 56d6d6506..845dde9e0 100644 --- a/src/app/datasets/dataset-lifecycle/dataset-lifecycle.component.html +++ b/src/app/datasets/dataset-lifecycle/dataset-lifecycle.component.html @@ -13,7 +13,7 @@ - + - + - + - + - +
Creation Date{{ value | date: "yyyy-MM-dd" }}{{ value | date }}
End of Embargo Period{{ value | date: "yyyy-MM-dd" }}{{ value | date }}
Publication Date{{ value | date: "yyyy-MM-dd" }}{{ value | date }}
Data Deletion Date{{ value | date: "yyyy-MM-dd" }}{{ value | date }}
Archive Retention Date{{ value | date: "yyyy-MM-dd" }}{{ value | date }}
diff --git a/src/app/datasets/dataset-table/dataset-table.component.html b/src/app/datasets/dataset-table/dataset-table.component.html index 9aa6f3dd9..fe8299015 100644 --- a/src/app/datasets/dataset-table/dataset-table.component.html +++ b/src/app/datasets/dataset-table/dataset-table.component.html @@ -176,8 +176,7 @@
-
{{ dataset.creationTime | date: "yyyy-MM-dd" }}
-
{{ dataset.creationTime | date: "EEE HH:mm" }}
+
{{ dataset.creationTime | date }}
diff --git a/src/app/datasets/reduce/reduce.component.html b/src/app/datasets/reduce/reduce.component.html index 9236d3d8d..d70d7e645 100644 --- a/src/app/datasets/reduce/reduce.component.html +++ b/src/app/datasets/reduce/reduce.component.html @@ -195,7 +195,7 @@

Description

- {{ dataset.createdAt | date: "yyyy-MM-dd, EEE HH:mm" }} + {{ dataset.createdAt | date }} diff --git a/src/app/datasets/sample-edit/sample-edit.component.html b/src/app/datasets/sample-edit/sample-edit.component.html index cffc792b7..ad37628f8 100644 --- a/src/app/datasets/sample-edit/sample-edit.component.html +++ b/src/app/datasets/sample-edit/sample-edit.component.html @@ -56,7 +56,7 @@

Edit Dataset Sample

Creation Time - {{ row.createdAt | date: "yyyy-MM-dd, hh:mm" }} + {{ row.createdAt | date }} diff --git a/src/app/jobs/jobs-detail/jobs-detail.component.html b/src/app/jobs/jobs-detail/jobs-detail.component.html index ac4678930..e7eacda48 100644 --- a/src/app/jobs/jobs-detail/jobs-detail.component.html +++ b/src/app/jobs/jobs-detail/jobs-detail.component.html @@ -21,14 +21,14 @@ brightness_high Creation Time - {{ value | date: "yyyy-MM-dd HH:mm" }} + {{ value | date }} gavel Execution Time - {{ value | date: "yyyy-MM-dd HH:mm" }} + {{ value | date }} @@ -42,7 +42,7 @@ markunread Date Of Last Message - {{ value | date: "yyyy-MM-dd HH:mm" }} + {{ value | date }} @@ -58,14 +58,14 @@ calendar_today Created At - {{ value | date: "yyyy-MM-dd HH:mm" }} + {{ value | date }} calendar_today Updated At - {{ value | date: "yyyy-MM-dd HH:mm" }} + {{ value | date }} diff --git a/src/app/logbooks/logbooks-detail/logbooks-detail.component.html b/src/app/logbooks/logbooks-detail/logbooks-detail.component.html index 6dea84c13..904a49666 100644 --- a/src/app/logbooks/logbooks-detail/logbooks-detail.component.html +++ b/src/app/logbooks/logbooks-detail/logbooks-detail.component.html @@ -28,7 +28,7 @@ - {{ message.origin_server_ts | date: "yyyy-MM-dd, EEE HH:mm" }} + {{ message.origin_server_ts | date }} diff --git a/src/app/logbooks/logbooks-table/logbooks-table.component.html b/src/app/logbooks/logbooks-table/logbooks-table.component.html index 8095e4e12..5a83d3ca0 100644 --- a/src/app/logbooks/logbooks-table/logbooks-table.component.html +++ b/src/app/logbooks/logbooks-table/logbooks-table.component.html @@ -28,10 +28,7 @@
- {{ - logbook.messages[0].origin_server_ts - | date: "yyyy-MM-dd, EEE HH:mm" - }} + {{ logbook.messages[0].origin_server_ts | date }}
No entries. diff --git a/src/app/publisheddata/publisheddata-details/publisheddata-details.component.html b/src/app/publisheddata/publisheddata-details/publisheddata-details.component.html index 2e04f39e9..03c53a276 100644 --- a/src/app/publisheddata/publisheddata-details/publisheddata-details.component.html +++ b/src/app/publisheddata/publisheddata-details/publisheddata-details.component.html @@ -20,7 +20,7 @@ > Registered Time - {{ value | date: "yyyy-MM-dd, HH:mm" }} + {{ value | date }} diff --git a/src/app/samples/sample-detail/sample-detail.component.html b/src/app/samples/sample-detail/sample-detail.component.html index 16fc9e192..cae3733f1 100644 --- a/src/app/samples/sample-detail/sample-detail.component.html +++ b/src/app/samples/sample-detail/sample-detail.component.html @@ -25,7 +25,7 @@ Creation Time - {{ value | date: "yyyy-MM-dd HH:mm" }} + {{ value | date }} From 05ed3be20d690512781d011e5dc556b0dc65ed78 Mon Sep 17 00:00:00 2001 From: Martin Date: Fri, 21 Feb 2025 15:37:45 +0100 Subject: [PATCH 06/13] feat: add dynamic material table (#1735) ## Description Add dynamic material table to the proposals dashboard, replacing the old table. This table should add more flexibility and it includes some new features like reordering of columns, resizing of columns and hide/show columns feature. New Features: Display proposals using the new dynamic material table. ## Motivation This table should be the new shared table across all the SciCat frontend. The point is to start with proposals and test it there. ## Fixes: Please provide a list of the fixes implemented in this PR * Items added ## Changes: Please provide a list of the changes implemented by this PR * introducing of the new dynamic material table * replacing the proposals table with the new table ## Tests included - [ ] Included for each change/fix? - [ ] Passing? (Merge will not be approved unless this is checked) ## Documentation - [ ] swagger documentation updated \[required\] - [ ] official documentation updated \[nice-to-have\] ### official documentation info If you have updated the official documentation, please provide PR # and URL of the pages where the updates are included ## Backend version - [ ] Does it require a specific version of the backend - which version of the backend is required: ## Summary by Sourcery Add dynamic material table to the proposals dashboard, replacing the old table. Include related proposals and datasets in separate tabs. New Features: - Display related proposals on the proposal details page. Tests: - Update tests for view-proposal-page component. ## Summary by Sourcery Replace the old proposals table with a new dynamic material table. New Features: - Introduce a dynamic material table for proposals, providing enhanced flexibility. Tests: - Update Cypress tests to reflect the changes in the table implementation. --------- Co-authored-by: Max Novelli --- angular.json | 4 +- cypress/e2e/proposals/proposals-general.cy.js | 16 +- package-lock.json | 8 +- package.json | 2 +- .../app-main-layout.component.scss | 2 +- .../proposal-dashboard.component.html | 28 +- .../proposal-dashboard.component.spec.ts | 13 +- .../proposal-dashboard.component.ts | 313 ++++-- ...ixed-size-table-virtual-scroll-strategy.ts | 164 ++++ .../cores/table-data-source.spec.ts | 89 ++ .../cores/table-data-source.ts | 177 ++++ .../cores/table-item-size.directive.ts | 209 ++++ .../cores/table-virtual-scroll.module.ts | 9 + .../cores/table.core.directive.ts | 473 +++++++++ .../dynamic-material-table/cores/type.ts | 94 ++ .../models/column-filter.model.ts | 5 + .../models/context-menu.model.ts | 11 + .../models/language-pack.model.ts | 61 ++ .../models/pipe.model.ts | 6 + .../models/print-config.model.ts | 14 + .../models/resize-column.mode.ts | 12 + .../models/table-field.model.ts | 72 ++ .../models/table-footer.model.ts | 9 + .../models/table-pagination.model.ts | 11 + .../models/table-row.model.ts | 57 ++ .../models/table-setting.model.ts | 54 + .../table/dynamic-mat-table.component.html | 343 +++++++ .../table/dynamic-mat-table.component.scss | 307 ++++++ .../table/dynamic-mat-table.component.ts | 919 ++++++++++++++++++ .../table/dynamic-mat-table.module.ts | 89 ++ .../table/dynamic-mat-table.service.ts | 150 +++ .../table/dynamic-mat-table.style.scss | 214 ++++ .../filter/compare/abstract-filter.ts | 33 + .../filter/compare/number-filter.ts | 103 ++ .../extensions/filter/compare/text-filter.ts | 118 +++ .../filter/filter-event.directive.ts | 14 + .../filter/header-filter.component.html | 93 ++ .../filter/header-filter.component.scss | 128 +++ .../filter/header-filter.component.ts | 188 ++++ .../extensions/filter/header-filter.module.ts | 30 + .../print-dialog/print-dialog.component.html | 42 + .../print-dialog/print-dialog.component.scss | 28 + .../print-dialog/print-dialog.component.ts | 39 + .../row-menu/row-menu.component.html | 37 + .../row-menu/row-menu.component.scss | 9 + .../extensions/row-menu/row-menu.component.ts | 53 + .../extensions/row-menu/row-menu.module.ts | 22 + .../table-menu/table-menu.component.html | 362 +++++++ .../table-menu/table-menu.component.scss | 295 ++++++ .../table-menu/table-menu.component.ts | 199 ++++ .../table-menu/table-menu.module.ts | 30 + .../tooltip/template-or-string.directive.ts | 17 + .../tooltip/tooltip.component.html | 5 + .../tooltip/tooltip.component.scss | 27 + .../tooltip/tooltip.component.ts | 27 + .../tooltip/tooltip.directive.ts | 66 ++ .../utilizes/html.helper.ts | 13 + .../utilizes/utilizes.ts | 30 + .../shared-table/_shared-table-theme.scss | 9 + src/app/shared/shared.module.ts | 3 + .../actions/proposals.actions.spec.ts | 37 +- .../actions/proposals.actions.ts | 21 +- .../effects/proposals.effects.spec.ts | 110 +-- .../effects/proposals.effects.ts | 51 +- .../reducers/proposals.reducer.spec.ts | 46 - .../reducers/proposals.reducer.ts | 24 - 66 files changed, 5885 insertions(+), 359 deletions(-) create mode 100644 src/app/shared/modules/dynamic-material-table/cores/fixed-size-table-virtual-scroll-strategy.ts create mode 100644 src/app/shared/modules/dynamic-material-table/cores/table-data-source.spec.ts create mode 100644 src/app/shared/modules/dynamic-material-table/cores/table-data-source.ts create mode 100644 src/app/shared/modules/dynamic-material-table/cores/table-item-size.directive.ts create mode 100644 src/app/shared/modules/dynamic-material-table/cores/table-virtual-scroll.module.ts create mode 100644 src/app/shared/modules/dynamic-material-table/cores/table.core.directive.ts create mode 100644 src/app/shared/modules/dynamic-material-table/cores/type.ts create mode 100644 src/app/shared/modules/dynamic-material-table/models/column-filter.model.ts create mode 100644 src/app/shared/modules/dynamic-material-table/models/context-menu.model.ts create mode 100644 src/app/shared/modules/dynamic-material-table/models/language-pack.model.ts create mode 100644 src/app/shared/modules/dynamic-material-table/models/pipe.model.ts create mode 100644 src/app/shared/modules/dynamic-material-table/models/print-config.model.ts create mode 100644 src/app/shared/modules/dynamic-material-table/models/resize-column.mode.ts create mode 100644 src/app/shared/modules/dynamic-material-table/models/table-field.model.ts create mode 100644 src/app/shared/modules/dynamic-material-table/models/table-footer.model.ts create mode 100644 src/app/shared/modules/dynamic-material-table/models/table-pagination.model.ts create mode 100644 src/app/shared/modules/dynamic-material-table/models/table-row.model.ts create mode 100644 src/app/shared/modules/dynamic-material-table/models/table-setting.model.ts create mode 100644 src/app/shared/modules/dynamic-material-table/table/dynamic-mat-table.component.html create mode 100644 src/app/shared/modules/dynamic-material-table/table/dynamic-mat-table.component.scss create mode 100644 src/app/shared/modules/dynamic-material-table/table/dynamic-mat-table.component.ts create mode 100644 src/app/shared/modules/dynamic-material-table/table/dynamic-mat-table.module.ts create mode 100644 src/app/shared/modules/dynamic-material-table/table/dynamic-mat-table.service.ts create mode 100644 src/app/shared/modules/dynamic-material-table/table/dynamic-mat-table.style.scss create mode 100644 src/app/shared/modules/dynamic-material-table/table/extensions/filter/compare/abstract-filter.ts create mode 100644 src/app/shared/modules/dynamic-material-table/table/extensions/filter/compare/number-filter.ts create mode 100644 src/app/shared/modules/dynamic-material-table/table/extensions/filter/compare/text-filter.ts create mode 100644 src/app/shared/modules/dynamic-material-table/table/extensions/filter/filter-event.directive.ts create mode 100644 src/app/shared/modules/dynamic-material-table/table/extensions/filter/header-filter.component.html create mode 100644 src/app/shared/modules/dynamic-material-table/table/extensions/filter/header-filter.component.scss create mode 100644 src/app/shared/modules/dynamic-material-table/table/extensions/filter/header-filter.component.ts create mode 100644 src/app/shared/modules/dynamic-material-table/table/extensions/filter/header-filter.module.ts create mode 100644 src/app/shared/modules/dynamic-material-table/table/extensions/print-dialog/print-dialog.component.html create mode 100644 src/app/shared/modules/dynamic-material-table/table/extensions/print-dialog/print-dialog.component.scss create mode 100644 src/app/shared/modules/dynamic-material-table/table/extensions/print-dialog/print-dialog.component.ts create mode 100644 src/app/shared/modules/dynamic-material-table/table/extensions/row-menu/row-menu.component.html create mode 100644 src/app/shared/modules/dynamic-material-table/table/extensions/row-menu/row-menu.component.scss create mode 100644 src/app/shared/modules/dynamic-material-table/table/extensions/row-menu/row-menu.component.ts create mode 100644 src/app/shared/modules/dynamic-material-table/table/extensions/row-menu/row-menu.module.ts create mode 100644 src/app/shared/modules/dynamic-material-table/table/extensions/table-menu/table-menu.component.html create mode 100644 src/app/shared/modules/dynamic-material-table/table/extensions/table-menu/table-menu.component.scss create mode 100644 src/app/shared/modules/dynamic-material-table/table/extensions/table-menu/table-menu.component.ts create mode 100644 src/app/shared/modules/dynamic-material-table/table/extensions/table-menu/table-menu.module.ts create mode 100644 src/app/shared/modules/dynamic-material-table/tooltip/template-or-string.directive.ts create mode 100644 src/app/shared/modules/dynamic-material-table/tooltip/tooltip.component.html create mode 100644 src/app/shared/modules/dynamic-material-table/tooltip/tooltip.component.scss create mode 100644 src/app/shared/modules/dynamic-material-table/tooltip/tooltip.component.ts create mode 100644 src/app/shared/modules/dynamic-material-table/tooltip/tooltip.directive.ts create mode 100644 src/app/shared/modules/dynamic-material-table/utilizes/html.helper.ts create mode 100644 src/app/shared/modules/dynamic-material-table/utilizes/utilizes.ts diff --git a/angular.json b/angular.json index a908781cf..d4ef21a2f 100644 --- a/angular.json +++ b/angular.json @@ -58,8 +58,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "2kb", - "maximumError": "4kb" + "maximumWarning": "8kb", + "maximumError": "8kb" } ], "fileReplacements": [ diff --git a/cypress/e2e/proposals/proposals-general.cy.js b/cypress/e2e/proposals/proposals-general.cy.js index df942cbff..74c89fba6 100644 --- a/cypress/e2e/proposals/proposals-general.cy.js +++ b/cypress/e2e/proposals/proposals-general.cy.js @@ -36,9 +36,9 @@ describe("Proposals general", () => { cy.get("mat-table mat-row").should("contain", proposal.proposalId); - cy.get("mat-row") + cy.get("mat-cell") .contains(proposal.proposalId) - .parent() + .closest("mat-row") .contains(proposal.title) .click(); @@ -64,9 +64,9 @@ describe("Proposals general", () => { cy.get("mat-table mat-row").should("contain", newProposal.proposalId); cy.get("mat-table mat-row").should("contain", proposal.proposalId); - cy.get("mat-row") + cy.get("mat-cell") .contains(newProposal.proposalId) - .parent() + .closest("mat-row") .contains(newProposal.title) .click(); @@ -200,9 +200,9 @@ describe("Proposals general", () => { cy.get("mat-table mat-row").should("contain", newProposal.proposalId); cy.get("mat-table mat-row").should("contain", newProposal2.proposalId); - cy.get("mat-row") + cy.get("mat-cell.mat-column-proposalId") .contains(newProposal.proposalId) - .parent() + .closest("mat-row") .contains(newProposal.title) .click(); @@ -244,9 +244,9 @@ describe("Proposals general", () => { cy.get("mat-table mat-row").should("contain", proposal.proposalId); - cy.get("mat-row") + cy.get("mat-cell.mat-column-proposalId") .contains(proposal.proposalId) - .parent() + .closest("mat-row") .contains(proposal.title) .click(); diff --git a/package-lock.json b/package-lock.json index 02cca2bd8..c162318ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "@ngrx/router-store": "^16", "@ngrx/store": "^16", "@ngx-translate/core": "^16.0.4", - "@scicatproject/scicat-sdk-ts-angular": "^4.12.0", + "@scicatproject/scicat-sdk-ts-angular": "^4.12.2", "autolinker": "^4.0.0", "deep-equal": "^2.0.5", "exceljs": "^4.3.0", @@ -5179,9 +5179,9 @@ } }, "node_modules/@scicatproject/scicat-sdk-ts-angular": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@scicatproject/scicat-sdk-ts-angular/-/scicat-sdk-ts-angular-4.12.0.tgz", - "integrity": "sha512-AwMvY9SdP46ab0bGQKR/7zFqknVeslhWNWFpgQJW6BEFSyiyx7Jwtu7lnPo36Lt7V5n02lanPs4F6kUyzdY+QQ==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@scicatproject/scicat-sdk-ts-angular/-/scicat-sdk-ts-angular-4.12.2.tgz", + "integrity": "sha512-4HoTniDoDXE1IYt6AVApqOly00ZSZgZNPjD8Xeabns9aRGUOKD8wAz1w0pkArZ/Cm/OkzhBz4O1U8czAdgOEDg==", "dependencies": { "tslib": "^2.3.0" }, diff --git a/package.json b/package.json index 02d22d185..9d4bacbf8 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "@ngrx/router-store": "^16", "@ngrx/store": "^16", "@ngx-translate/core": "^16.0.4", - "@scicatproject/scicat-sdk-ts-angular": "^4.12.0", + "@scicatproject/scicat-sdk-ts-angular": "^4.12.2", "autolinker": "^4.0.0", "deep-equal": "^2.0.5", "exceljs": "^4.3.0", diff --git a/src/app/_layout/app-main-layout/app-main-layout.component.scss b/src/app/_layout/app-main-layout/app-main-layout.component.scss index 7db0bd823..f0fd0d86c 100644 --- a/src/app/_layout/app-main-layout/app-main-layout.component.scss +++ b/src/app/_layout/app-main-layout/app-main-layout.component.scss @@ -1,5 +1,5 @@ .main-app { height: 100%; - min-height: calc(100vh - 3.5rem); + min-height: calc(100vh - 6.5rem); padding: 1.5rem; } diff --git a/src/app/proposals/proposal-dashboard/proposal-dashboard.component.html b/src/app/proposals/proposal-dashboard/proposal-dashboard.component.html index d7719296c..27822c54f 100644 --- a/src/app/proposals/proposal-dashboard/proposal-dashboard.component.html +++ b/src/app/proposals/proposal-dashboard/proposal-dashboard.component.html @@ -1,7 +1,25 @@ - - + diff --git a/src/app/proposals/proposal-dashboard/proposal-dashboard.component.spec.ts b/src/app/proposals/proposal-dashboard/proposal-dashboard.component.spec.ts index 77fcac220..e04620099 100644 --- a/src/app/proposals/proposal-dashboard/proposal-dashboard.component.spec.ts +++ b/src/app/proposals/proposal-dashboard/proposal-dashboard.component.spec.ts @@ -19,19 +19,18 @@ import { EffectsModule } from "@ngrx/effects"; import { HttpClient } from "@angular/common/http"; import { ScicatDataService } from "shared/services/scicat-data-service"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; -import { DatasetsService } from "@scicatproject/scicat-sdk-ts-angular"; +import { + DatasetsService, + ProposalClass, +} from "@scicatproject/scicat-sdk-ts-angular"; +import { BehaviorSubject } from "rxjs"; describe("ProposalDashboardComponent", () => { let component: ProposalDashboardComponent; let fixture: ComponentFixture; const getConfig = () => ({}); - const dataSource = new MockScicatDataSource( - new MockAppConfigService(null) as unknown as AppConfigService, - null, - null, - { collections: null, columns: null }, - ); + const dataSource = new BehaviorSubject([]); beforeEach(async () => { await TestBed.configureTestingModule({ diff --git a/src/app/proposals/proposal-dashboard/proposal-dashboard.component.ts b/src/app/proposals/proposal-dashboard/proposal-dashboard.component.ts index 75acd4c20..b63404fc5 100644 --- a/src/app/proposals/proposal-dashboard/proposal-dashboard.component.ts +++ b/src/app/proposals/proposal-dashboard/proposal-dashboard.component.ts @@ -1,104 +1,243 @@ +import { Component, EventEmitter, OnInit, Output } from "@angular/core"; +import { BehaviorSubject } from "rxjs"; +import { PageChangeEvent } from "shared/modules/table/table.component"; +import { TableField } from "shared/modules/dynamic-material-table/models/table-field.model"; import { - AfterViewChecked, - ChangeDetectorRef, - Component, - OnDestroy, -} from "@angular/core"; -import { Router } from "@angular/router"; -import { AppConfigService } from "app-config.service"; -import { Column } from "shared/modules/shared-table/shared-table.module"; + TableSetting, + VisibleActionMenu, +} from "shared/modules/dynamic-material-table/models/table-setting.model"; +import { + TablePagination, + TablePaginationMode, +} from "shared/modules/dynamic-material-table/models/table-pagination.model"; +import { PrintConfig } from "shared/modules/dynamic-material-table/models/print-config.model"; +import { + IRowEvent, + RowEventType, + TableSelectionMode, +} from "shared/modules/dynamic-material-table/models/table-row.model"; +import { Store } from "@ngrx/store"; +import { + selectProposals, + selectProposalsCount, +} from "state-management/selectors/proposals.selectors"; +import { fetchProposalsAction } from "state-management/actions/proposals.actions"; +import { ActivatedRoute, Router } from "@angular/router"; import { ProposalClass } from "@scicatproject/scicat-sdk-ts-angular"; -import { ExportExcelService } from "shared/services/export-excel.service"; -import { ScicatDataService } from "shared/services/scicat-data-service"; -import { SciCatDataSource } from "shared/services/scicat.datasource"; +import { Direction } from "@angular/cdk/bidi"; + +export const tableColumnsConfig: TableField[] = [ + { + name: "proposalId", + header: "Proposal ID", + icon: "perm_device_information", + type: "text", + }, + { + name: "title", + icon: "description", + width: 250, + }, + { + name: "abstract", + icon: "chrome_reader_mode", + width: 250, + }, + { + name: "firstname", + header: "First Name", + icon: "person", + }, + { + name: "lastname", + header: "Last Name", + }, + { name: "email", icon: "email", width: 200 }, + { name: "type", icon: "badge", width: 200 }, + { + name: "parentProposalId", + header: "Parent Proposal", + icon: "badge", + }, + { + name: "pi_firstname", + header: "PI First Name", + icon: "person_pin", + }, + { + name: "pi_lastname", + header: "PI Last Name", + icon: "person_pin", + }, + { + name: "pi_email", + header: "PI Email", + icon: "email", + }, +]; + +const actionMenu: VisibleActionMenu = { + json: true, + csv: true, + print: true, + columnSettingPin: true, + columnSettingFilter: true, + clearFilter: true, +}; + +const tableSettingsConfig: TableSetting = { + direction: "ltr", + visibleActionMenu: actionMenu, + autoHeight: false, + saveSettingMode: "multi", + rowStyle: { + "border-bottom": "1px solid #d2d2d2", + }, +}; + +const DEFAULT_PAGE_SIZE = 10; @Component({ selector: "app-proposal-dashboard", templateUrl: "./proposal-dashboard.component.html", styleUrls: ["./proposal-dashboard.component.scss"], }) -export class ProposalDashboardComponent implements OnDestroy, AfterViewChecked { - columns: Column[] = [ - { - id: "proposalId", - label: "Proposal ID", - canSort: true, - icon: "perm_device_information", - matchMode: "contains", - hideOrder: 0, - }, - { - id: "title", - label: "Title", - icon: "description", - canSort: true, - matchMode: "contains", - hideOrder: 1, - }, - { - id: "firstname", - label: "First Name", - icon: "badge", - canSort: true, - matchMode: "contains", - hideOrder: 2, - }, - { - id: "lastname", - label: "Last Name", - icon: "badge", - canSort: true, - matchMode: "contains", - hideOrder: 3, - }, - { - id: "startTime", - icon: "timer", - label: "Start Date", - format: "date", - canSort: true, - matchMode: "between", - hideOrder: 4, - sortDefault: "desc", - }, - { - id: "endTime", - icon: "timer_off", - label: "End Date", - format: "date", - canSort: true, - matchMode: "between", - hideOrder: 5, - }, - ]; - tableDefinition = { - collection: "Proposals", - columns: this.columns, - }; - dataSource: SciCatDataSource; +export class ProposalDashboardComponent implements OnInit { + vm$ = this.store.select(selectProposals); + proposalsCount$ = this.store.select(selectProposalsCount); + + columns!: TableField[]; + + direction: Direction = "ltr"; + + showReloadData = true; + + rowHeight = 50; + + pending = true; + + setting: TableSetting = {}; + + paginationMode: TablePaginationMode = "server-side"; + + showNoData = true; + + dataSource: BehaviorSubject = new BehaviorSubject< + ProposalClass[] + >([]); + + pagination: TablePagination = {}; + + stickyHeader = true; + + printConfig: PrintConfig = {}; + + showProgress = true; + + rowSelectionMode: TableSelectionMode = "none"; + + globalTextSearch = ""; + + @Output() pageChange = new EventEmitter(); + constructor( - private appConfigService: AppConfigService, - private cdRef: ChangeDetectorRef, - private dataService: ScicatDataService, - private exportService: ExportExcelService, + private store: Store, private router: Router, - ) { - this.dataSource = new SciCatDataSource( - this.appConfigService, - this.dataService, - this.exportService, - this.tableDefinition, + private route: ActivatedRoute, + ) {} + + ngOnInit(): void { + const { queryParams } = this.route.snapshot; + if (queryParams.textSearch) { + this.globalTextSearch = queryParams.textSearch; + } + this.store.dispatch( + fetchProposalsAction({ + limit: queryParams.pageSize || DEFAULT_PAGE_SIZE, + skip: queryParams.pageIndex * queryParams.pageSize, + search: queryParams.textSearch, + }), ); + + this.vm$.subscribe((data) => { + this.dataSource.next(data); + this.pending = false; + }); + + this.proposalsCount$.subscribe((count) => { + const pagginationConfig = { + pageSizeOptions: [5, 10, 25, 100], + pageIndex: queryParams.pageIndex, + pageSize: queryParams.pageSize || DEFAULT_PAGE_SIZE, + length: count, + }; + + this.initTable( + tableColumnsConfig, + tableSettingsConfig, + pagginationConfig, + ); + }); } - ngAfterViewChecked() { - this.cdRef.detectChanges(); + initTable( + columnsConfig: TableField[], + settingConfig: TableSetting, + paginationConfig: TablePagination, + ): void { + this.columns = columnsConfig; + this.setting = settingConfig; + this.pagination = paginationConfig; } - ngOnDestroy() { - this.dataSource.disconnectExportData(); + + onPaginationChange(pagination: TablePagination) { + this.pending = true; + const queryParams: Record = { + pageIndex: pagination.pageIndex, + pageSize: pagination.pageSize, + }; + + if (this.route.snapshot.queryParams.textSearch) { + queryParams.textSearch = this.route.snapshot.queryParams.textSearch; + } + this.router.navigate([], { + queryParams, + queryParamsHandling: "merge", + }); + + this.store.dispatch( + fetchProposalsAction({ + limit: pagination.pageSize, + skip: pagination.pageIndex * pagination.pageSize, + search: queryParams.textSearch as string, + }), + ); } - onRowClick(proposal: ProposalClass) { - const id = encodeURIComponent(proposal.proposalId); - this.router.navigateByUrl("/proposals/" + id); + + onGlobalTextSearchChange(text: string) { + this.pending = true; + this.pagination.pageIndex = 0; + this.router.navigate([], { + queryParams: { + textSearch: text, + pageIndex: 0, + }, + queryParamsHandling: "merge", + }); + + this.store.dispatch( + fetchProposalsAction({ + limit: this.pagination.pageSize, + skip: this.pagination.pageIndex * this.pagination.pageSize, + search: text, + }), + ); + } + + onRowClick(event: IRowEvent) { + if (event.event === RowEventType.RowClick) { + const id = encodeURIComponent(event.sender.row.proposalId); + this.router.navigateByUrl("/proposals/" + id); + } } } diff --git a/src/app/shared/modules/dynamic-material-table/cores/fixed-size-table-virtual-scroll-strategy.ts b/src/app/shared/modules/dynamic-material-table/cores/fixed-size-table-virtual-scroll-strategy.ts new file mode 100644 index 000000000..f78ac8d57 --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/cores/fixed-size-table-virtual-scroll-strategy.ts @@ -0,0 +1,164 @@ +import { Injectable, OnDestroy } from "@angular/core"; +import { distinctUntilChanged } from "rxjs/operators"; +import { BehaviorSubject, Subject } from "rxjs"; +import { + CdkVirtualScrollViewport, + VirtualScrollStrategy, +} from "@angular/cdk/scrolling"; +import { ListRange } from "@angular/cdk/collections"; +import { Subscription } from "rxjs"; + +export interface TSVStrategyConfigs { + rowHeight: number; + headerHeight: number; + footerHeight: number; + bufferMultiplier: number; +} + +export declare type TableScrollStrategy = "fixed-size" | "none" | null; + +@Injectable() +export class FixedSizeTableVirtualScrollStrategy + implements VirtualScrollStrategy, OnDestroy +{ + private eventsSubscription: Subscription; + private length = 0; + private rowHeight!: number; + private headerHeight!: number; + private footerHeight!: number; + private bufferMultiplier!: number; + private indexChange = new Subject(); + public stickyChange = new Subject(); + public scrollStrategyMode: TableScrollStrategy = "fixed-size"; + + public viewport: CdkVirtualScrollViewport; + + public renderedRangeStream = new BehaviorSubject({ + start: 0, + end: 0, + }); + public offsetChange = new BehaviorSubject(0); + + public scrolledIndexChange = this.indexChange.pipe(distinctUntilChanged()); + + private updateContent() { + if (!this.viewport || !this.rowHeight) { + return; + } + let start = 0; + let end = this.dataLength; + + if ( + this.scrollStrategyMode === "none" && + this.viewport.getRenderedRange().start === start && + this.viewport.getRenderedRange().end === end + ) { + return; + } + + const scrollOffset = this.viewport.measureScrollOffset(); + const amount = Math.ceil(this.getViewportSize() / this.rowHeight); + const offset = Math.max(scrollOffset - this.headerHeight, 0); + const buffer = Math.ceil(amount * this.bufferMultiplier); + + const skip = Math.round(offset / this.rowHeight); + const index = Math.max(0, skip); + + if (this.scrollStrategyMode === "fixed-size") { + start = Math.max(0, index - buffer); + end = Math.min(this.dataLength, index + amount + buffer); + } + const renderedOffset = start * this.rowHeight; + + this.viewport.setRenderedContentOffset(renderedOffset); + this.viewport.setRenderedRange({ start, end }); + this.indexChange.next(index); + this.stickyChange.next(renderedOffset); + this.offsetChange.next(offset); + } + + get dataLength(): number { + return this.length; + } + + set dataLength(value: number) { + this.length = value; + this.onDataLengthChanged(); + } + + ngOnDestroy(): void { + this.eventsSubscription.unsubscribe(); + } + + public attach(viewport: CdkVirtualScrollViewport): void { + this.viewport = viewport; + this.eventsSubscription = this.viewport.renderedRangeStream.subscribe( + this.renderedRangeStream, + ); + this.onDataLengthChanged(); + } + + public detach(): void { + this.indexChange.complete(); + this.stickyChange.complete(); + this.renderedRangeStream.complete(); + } + + public onContentScrolled(): void { + this.updateContent(); + } + + public onDataLengthChanged(): void { + if (this.viewport) { + this.viewport.setTotalContentSize( + this.dataLength * this.rowHeight + + this.headerHeight + + this.footerHeight, + ); + } + this.updateContent(); + } + + public onContentRendered(): void { + // no-op + } + + public onRenderedOffsetChanged(): void { + // no-op + } + + public scrollToIndex(index: number, behavior: ScrollBehavior): void { + if (!this.viewport || !this.rowHeight) { + return; + } + this.viewport.scrollToOffset( + (index - 1) * this.rowHeight + this.headerHeight, + ); + } + + public setConfig(configs: TSVStrategyConfigs) { + const { rowHeight, headerHeight, footerHeight, bufferMultiplier } = configs; + if ( + this.rowHeight === rowHeight && + this.headerHeight === headerHeight && + this.footerHeight === footerHeight && + this.bufferMultiplier === bufferMultiplier + ) { + return; + } + this.rowHeight = rowHeight; + this.headerHeight = headerHeight; + this.footerHeight = footerHeight; + this.bufferMultiplier = bufferMultiplier; + this.onDataLengthChanged(); + } + + // bug fixed some time viewport is zero height (i dont know why!) + public getViewportSize() { + if (this.viewport.getViewportSize() === 0) { + return this.viewport.elementRef.nativeElement.clientHeight + 52; + } else { + return this.viewport.getViewportSize(); + } + } +} diff --git a/src/app/shared/modules/dynamic-material-table/cores/table-data-source.spec.ts b/src/app/shared/modules/dynamic-material-table/cores/table-data-source.spec.ts new file mode 100644 index 000000000..03a1ab9f8 --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/cores/table-data-source.spec.ts @@ -0,0 +1,89 @@ +import { TestBed } from "@angular/core/testing"; +import { TableVirtualScrollDataSource } from "./table-data-source"; +import { MatTableDataSource } from "@angular/material/table"; +import { Subject } from "rxjs"; +import { ListRange } from "@angular/cdk/collections"; +import { map, switchMap } from "rxjs/operators"; +import { TableRow } from "../models/table-row.model"; + +interface TestData extends TableRow { + index: number; +} + +function getTestData(n = 10): TestData[] { + return Array.from({ length: n }).map((e, i) => ({ index: i })); +} + +// tslint:disable:no-string-literal +describe("TableVirtualScrollDataSource", () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it("should be created", () => { + const dataSource: TableVirtualScrollDataSource = + new TableVirtualScrollDataSource(); + expect(dataSource).toBeTruthy(); + + const dataSource2: TableVirtualScrollDataSource = + new TableVirtualScrollDataSource([{ index: 0 }]); + expect(dataSource2).toBeTruthy(); + }); + + it("should extend MatTableDataSource", () => { + const dataSource: TableVirtualScrollDataSource = + new TableVirtualScrollDataSource(); + expect(dataSource instanceof MatTableDataSource).toBeTruthy(); + }); + + it("should have reaction on dataOfRange$ changes", () => { + const testData: TestData[] = getTestData(); + const dataSource: TableVirtualScrollDataSource = + new TableVirtualScrollDataSource(testData); + const stream = new Subject(); + + stream.subscribe(dataSource.dataOfRange$); + + const renderData: Subject = dataSource["_renderData"]; + + let count = -1; // renderData is BehaviorSubject with base value '[]' + renderData.subscribe(() => { + count++; + }); + + stream.next(testData.slice(0, 1)); + stream.next(testData); + + expect(count).toBe(2); + }); + + it("should provide correct data", () => { + const testData: TestData[] = getTestData(10); + const dataSource: TableVirtualScrollDataSource = + new TableVirtualScrollDataSource(testData); + const stream = new Subject(); + + dataSource.dataToRender$ + .pipe( + switchMap((data) => + stream.pipe(map(({ start, end }) => data.slice(start, end))), + ), + ) + .subscribe(dataSource.dataOfRange$); + + const renderData: Subject = dataSource["_renderData"]; + + const results: TestData[][] = []; + + renderData.subscribe((data) => { + results.push(data); + }); + + stream.next({ start: 0, end: 2 }); + stream.next({ start: 8, end: testData.length }); + + expect(results).toEqual([ + [], + [{ index: 0 }, { index: 1 }], + [{ index: 8 }, { index: 9 }], + ]); + }); +}); diff --git a/src/app/shared/modules/dynamic-material-table/cores/table-data-source.ts b/src/app/shared/modules/dynamic-material-table/cores/table-data-source.ts new file mode 100644 index 000000000..759009d33 --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/cores/table-data-source.ts @@ -0,0 +1,177 @@ +import { + BehaviorSubject, + combineLatest, + merge, + Observable, + of, + ReplaySubject, + Subject, + Subscription, +} from "rxjs"; +import { map } from "rxjs/operators"; +import { MatTableDataSource } from "@angular/material/table"; +import { MatSort, Sort } from "@angular/material/sort"; +import { MatPaginator, PageEvent } from "@angular/material/paginator"; +import { titleCase } from "../utilizes/utilizes"; +import { HashMap } from "./type"; +import { TableField } from "../models/table-field.model"; +import { TableRow } from "../models/table-row.model"; +import { AbstractFilter } from "../table/extensions/filter/compare/abstract-filter"; + +export class TableVirtualScrollDataSource< + T extends TableRow, +> extends MatTableDataSource { + private streamsReady: boolean; + private filterMap: HashMap = {}; + public dataToRender$: Subject; + public dataOfRange$: Subject; + public columns: TableField[] = []; + get allData(): T[] { + return this.data; + } + + private initStreams() { + if (!this.streamsReady) { + this.dataToRender$ = new ReplaySubject(1); + this.dataOfRange$ = new ReplaySubject(1); + this.streamsReady = true; + } + } + + toTranslate(): any[] { + const tranList = []; + const keys: string[] = Object.keys(this.filterMap); + for (const k of keys) { + let fieldTotalTran = ""; + for (const f of this.filterMap[k]) { + fieldTotalTran += f.toPrint(); + } + if (fieldTotalTran !== "") { + tranList.push({ key: titleCase(k), value: fieldTotalTran }); + } + } + return tranList; + } + + getFilter(fieldName: string): AbstractFilter[] { + return this.filterMap[fieldName]; + } + + setFilter(fieldName: string, filters: AbstractFilter[]): Observable { + this.filterMap[fieldName] = filters; + return new Observable((subscriber) => { + this.refreshFilterPredicate(); + subscriber.next(); + subscriber.complete(); + }); + } + + clearFilter(fieldName: string = null) { + if (fieldName != null) { + delete this.filterMap[fieldName]; + } else { + this.filterMap = {}; + } + this.refreshFilterPredicate(); + } + + clearData() { + this.data = []; + } + + public refreshFilterPredicate() { + let conditionsString = ""; + Object.keys(this.filterMap).forEach((key) => { + let fieldCondition = ""; + this.filterMap[key].forEach((fieldFilter, row, array) => { + if (row < array.length - 1) { + fieldCondition += + fieldFilter.toString(key) + + (fieldFilter.type === "and" ? " && " : " || "); + } else { + fieldCondition += fieldFilter.toString(key); + } + }); + if (fieldCondition !== "") { + conditionsString += ` ${conditionsString === "" ? "" : " && "} ( ${fieldCondition} )`; + } + }); + if (conditionsString !== "") { + const filterFunction = new Function("_a$", "return " + conditionsString); + this.filterPredicate = (data: T, filter: string) => + filterFunction(data) as boolean; + } else { + this.filterPredicate = (data: T, filter: string) => true; + } + this.filter = conditionsString; + } + + // When client paging active use for retrieve paging data + pagingData(data) { + const p: MatPaginator = (this as any)._paginator; + if (p && p !== null) { + const end = (p.pageIndex + 1) * p.pageSize; + const start = p.pageIndex * p.pageSize; + return data.slice(start, end); + } + return data; + } + + _updateChangeSubscription() { + this.initStreams(); + const sort: MatSort | null = (this as any)._sort; + const paginator: MatPaginator | null = (this as any)._paginator; + const internalPageChanges: Subject = (this as any) + ._internalPageChanges; + const filter: BehaviorSubject = (this as any)._filter; + const renderData: BehaviorSubject = (this as any)._renderData; + const dataStream: BehaviorSubject = (this as any)._data; + + const sortChange: Observable = sort + ? (merge(sort.sortChange, sort.initialized) as Observable) + : of(null); + const pageChange: Observable = paginator + ? (merge( + paginator.page, + internalPageChanges, + paginator.initialized, + ) as Observable) + : of(null); + + // First Filter + const filteredData = combineLatest([dataStream, filter]).pipe( + map(([data]) => this._filterData(data)), + ); + // Second Order + const orderedData = combineLatest([filteredData, sortChange]).pipe( + map(([data, sortColumn]) => { + const sc: Sort = sortColumn as Sort; + if (!sc) { + return data; + } else if (sc.active !== "") { + const column = this.columns.filter((c) => c.name == sc.active)[0]; + if (column.sort === "server-side") { + return data; + } else if (column.sort === "client-side") { + return this._orderData(data); + } + } + + return data; + }), + ); + // Last Paging + const paginatedData = combineLatest([orderedData, pageChange]).pipe( + map(([data]) => this.pagingData(data)), + ); + + this._renderChangesSubscription?.unsubscribe(); + this._renderChangesSubscription = new Subscription(); + this._renderChangesSubscription.add( + paginatedData.subscribe((data) => this.dataToRender$.next(data)), + ); + this._renderChangesSubscription.add( + this.dataOfRange$.subscribe((data) => renderData.next(data)), + ); + } +} diff --git a/src/app/shared/modules/dynamic-material-table/cores/table-item-size.directive.ts b/src/app/shared/modules/dynamic-material-table/cores/table-item-size.directive.ts new file mode 100644 index 000000000..44aceb51f --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/cores/table-item-size.directive.ts @@ -0,0 +1,209 @@ +import { + AfterContentInit, + ContentChild, + Directive, + forwardRef, + Input, + NgZone, + OnChanges, + OnDestroy, +} from "@angular/core"; +import { VIRTUAL_SCROLL_STRATEGY } from "@angular/cdk/scrolling"; +import { + distinctUntilChanged, + filter, + map, + switchMap, + takeUntil, + takeWhile, + tap, +} from "rxjs/operators"; +import { TableVirtualScrollDataSource } from "./table-data-source"; +import { MatTable } from "@angular/material/table"; +import { FixedSizeTableVirtualScrollStrategy } from "./fixed-size-table-virtual-scroll-strategy"; +import { CdkHeaderRowDef } from "@angular/cdk/table"; +import { Subject } from "rxjs"; + +export function tableVirtualScrollDirectiveStrategyFactory( + tableDir: TableItemSizeDirective, +) { + return tableDir.scrollStrategy; +} + +const stickyHeaderSelector = ".mat-mdc-header-row .mat-mdc-table-sticky"; +const stickyFooterSelector = ".mat-mdc-footer-row .mat-mdc-table-sticky"; + +const defaults = { + rowHeight: 48, + headerHeight: 56, + headerEnabled: true, + footerHeight: 48, + footerEnabled: false, + bufferMultiplier: 0.7, +}; + +@Directive({ + // eslint-disable-next-line @angular-eslint/directive-selector + selector: "cdk-virtual-scroll-viewport[tvsItemSize]", + providers: [ + { + provide: VIRTUAL_SCROLL_STRATEGY, + useFactory: tableVirtualScrollDirectiveStrategyFactory, + deps: [forwardRef(() => TableItemSizeDirective)], + }, + ], +}) +export class TableItemSizeDirective + implements OnChanges, AfterContentInit, OnDestroy +{ + private alive = true; + private stickyPositions: Map; + + @Input("tvsItemSize") + rowHeight = defaults.rowHeight; + + @Input() + headerEnabled = defaults.headerEnabled; + + @Input() + headerHeight = defaults.headerHeight; + + @Input() + footerEnabled = defaults.footerEnabled; + + @Input() + footerHeight = defaults.footerHeight; + + @Input() + bufferMultiplier = defaults.bufferMultiplier; + + @ContentChild(MatTable, { static: true }) table: MatTable; + + scrollStrategy = new FixedSizeTableVirtualScrollStrategy(); + + dataSourceChanges = new Subject(); + + constructor(private zone: NgZone) {} + + private isAlive() { + return () => this.alive; + } + + private isStickyEnabled(): boolean { + return ( + !!this.scrollStrategy.viewport && + ((this.table as any)._headerRowDefs as CdkHeaderRowDef[]) + .map((def) => def.sticky) + .reduce((prevState, state) => prevState && state, true) + ); + } + + private initStickyPositions() { + this.stickyPositions = new Map(); + this.scrollStrategy.viewport.elementRef.nativeElement + .querySelectorAll(stickyHeaderSelector) + .forEach((el) => { + const parent = el.parentElement; + if (!this.stickyPositions.has(parent)) { + this.stickyPositions.set(parent, parent.offsetTop); + } + }); + } + + ngOnDestroy() { + this.alive = false; + this.dataSourceChanges.complete(); + } + + ngAfterContentInit() { + const switchDataSourceOrigin = (this.table as any)._switchDataSource; + (this.table as any)._switchDataSource = (dataSource: any) => { + switchDataSourceOrigin.call(this.table, dataSource); + this.connectDataSource(dataSource); + }; + + this.connectDataSource(this.table.dataSource); + + this.scrollStrategy.stickyChange + .pipe( + filter(() => this.isStickyEnabled()), + tap(() => { + if (!this.stickyPositions) { + this.initStickyPositions(); + } + }), + takeWhile(this.isAlive()), + ) + .subscribe((stickyOffset) => { + this.setSticky(stickyOffset); + }); + } + + connectDataSource(dataSource: any) { + this.dataSourceChanges.next(); + if (dataSource instanceof TableVirtualScrollDataSource) { + dataSource.dataToRender$ + .pipe( + distinctUntilChanged(), + takeUntil(this.dataSourceChanges), + takeWhile(this.isAlive()), + tap((data) => (this.scrollStrategy.dataLength = data?.length)), + switchMap((data) => + this.scrollStrategy.renderedRangeStream.pipe( + map(({ start, end }) => { + return typeof start !== "number" || typeof end !== "number" + ? data + : data.slice(start, end); + }), + ), + ), + ) + .subscribe((data) => { + this.zone.run(() => { + dataSource.dataOfRange$.next(data); + }); + }); + } else { + throw new Error( + "[tvsItemSize] requires TableVirtualScrollDataSource be set as [dataSource] of [mat-table]", + ); + } + } + + ngOnChanges() { + const config = { + rowHeight: +this.rowHeight || defaults.rowHeight, + headerHeight: this.headerEnabled + ? +this.headerHeight || defaults.headerHeight + : 0, + footerHeight: this.footerEnabled + ? +this.footerHeight || defaults.footerHeight + : 0, + bufferMultiplier: +this.bufferMultiplier || defaults.bufferMultiplier, + }; + this.scrollStrategy.setConfig(config); + } + + setSticky(offset) { + this.scrollStrategy.viewport.elementRef.nativeElement + .querySelectorAll(stickyHeaderSelector) + .forEach((el: HTMLElement) => { + const parent = el.parentElement; + let baseOffset = 0; + if (this.stickyPositions.has(parent)) { + baseOffset = this.stickyPositions.get(parent); + } + el.style.top = `${baseOffset - offset}px`; + }); + this.scrollStrategy.viewport.elementRef.nativeElement + .querySelectorAll(stickyFooterSelector) + .forEach((el: HTMLElement) => { + const parent = el.parentElement; + let baseOffset = 0; + if (this.stickyPositions.has(parent)) { + baseOffset = this.stickyPositions.get(parent); + } + el.style.bottom = `${-baseOffset + offset}px`; + }); + } +} diff --git a/src/app/shared/modules/dynamic-material-table/cores/table-virtual-scroll.module.ts b/src/app/shared/modules/dynamic-material-table/cores/table-virtual-scroll.module.ts new file mode 100644 index 000000000..531034153 --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/cores/table-virtual-scroll.module.ts @@ -0,0 +1,9 @@ +import { NgModule } from "@angular/core"; +import { TableItemSizeDirective } from "./table-item-size.directive"; + +@NgModule({ + declarations: [TableItemSizeDirective], + imports: [], + exports: [TableItemSizeDirective], +}) +export class TableVirtualScrollModule {} diff --git a/src/app/shared/modules/dynamic-material-table/cores/table.core.directive.ts b/src/app/shared/modules/dynamic-material-table/cores/table.core.directive.ts new file mode 100644 index 000000000..2cd495dd9 --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/cores/table.core.directive.ts @@ -0,0 +1,473 @@ +import { + IRowEvent, + TableRow, + TableSelectionMode, + ITableEvent, + RowEventType, +} from "../models/table-row.model"; +import { TableVirtualScrollDataSource } from "./table-data-source"; +import { + ViewChild, + Input, + Output, + EventEmitter, + HostBinding, + ChangeDetectorRef, +} from "@angular/core"; +import { TableField } from "../models/table-field.model"; +import { titleCase } from "../utilizes/utilizes"; +import { CdkVirtualScrollViewport } from "@angular/cdk/scrolling"; +import { moveItemInArray } from "@angular/cdk/drag-drop"; +import { SelectionModel } from "@angular/cdk/collections"; +import { TableService } from "../table/dynamic-mat-table.service"; +import { + TablePagination, + TablePaginationMode, +} from "../models/table-pagination.model"; +import { PrintConfig } from "../models/print-config.model"; +import { TableSetting, Direction } from "../models/table-setting.model"; +import { MatSort } from "@angular/material/sort"; +import { MatPaginator } from "@angular/material/paginator"; +import { MatTable } from "@angular/material/table"; +import { Directive } from "@angular/core"; +import { clone, getObjectProp, isNullorUndefined } from "./type"; +import { TableScrollStrategy } from "./fixed-size-table-virtual-scroll-strategy"; +import { ContextMenuItem } from "../models/context-menu.model"; +import { BehaviorSubject } from "rxjs"; + +@Directive({ + // eslint-disable-next-line @angular-eslint/directive-selector + selector: "[core]", +}) +export class TableCoreDirective { + private _expandComponent: any; + private _rowSelectionModel = new SelectionModel(true, []); + private _tablePagination: TablePagination = { + pageIndex: 0, + pageSize: 10, + pageSizeOptions: [5, 10, 100, 1000, 10000], + }; + protected _rowSelectionMode: TableSelectionMode; + public expandColumn = []; + public noData = true; + @ViewChild(MatSort, { static: true }) sort: MatSort; + @ViewChild(MatPaginator, { static: true }) paginator: MatPaginator; + @Input() public dataSource: BehaviorSubject; + @Input() backgroundColor: string = null; + @Input() public rowContextMenuItems: ContextMenuItem[]; + @Input() defaultWidth: number = null; + @Input() minWidth = 120; + @Input() printConfig: PrintConfig = {}; + @Input() sticky: boolean; + @Input() pending: boolean; + @Input() rowHeight = 48; + @Input() headerHeight = 56; + @Input() footerHeight = 48; + @Input() headerEnable = true; + @Input() footerEnable = false; + @Input() showNoData: boolean; + @Input() showReload: boolean; + @Input() showGlobalTextSearch = true; + @Input() globalTextSearch = ""; + @Input() globalTextSearchPlaceholder = "Search"; + // eslint-disable-next-line @angular-eslint/no-output-on-prefix + @Output() onTableEvent: EventEmitter = new EventEmitter(); + // eslint-disable-next-line @angular-eslint/no-output-on-prefix + @Output() onRowEvent: EventEmitter> = new EventEmitter(); + @Output() settingChange: EventEmitter<{ + type: + | "create" + | "save" + | "apply" + | "delete" + | "default" + | "select" + | "error"; + setting: TableSetting; + }> = new EventEmitter(); + @Output() paginationChange: EventEmitter = + new EventEmitter(); + @Output() globalTextSearchChange: EventEmitter = new EventEmitter(); + + /*************************************** Expand Row *********************************/ + expandedElement: TableRow | null; + @Input() contextMenuItems: ContextMenuItem[] = []; + + // Variables // + progressColumn: string[] = []; + searchRowColumns = ["placeholder-column", "global-search"]; + displayedColumns: string[] = []; + displayedFooter: string[] = []; + public tableColumns: TableField[]; + public tvsDataSource: TableVirtualScrollDataSource = + new TableVirtualScrollDataSource([]); + + public tablePagingMode: TablePaginationMode = "none"; + public viewportClass: "viewport" | "viewport-with-pagination" = + "viewport-with-pagination"; + tableSetting: TableSetting; + + /**************************************** Reference Variables ***************************************/ + @ViewChild(MatTable, { static: true }) table!: MatTable; + @ViewChild(CdkVirtualScrollViewport, { static: true }) + viewport!: CdkVirtualScrollViewport; + /**************************************** Methods **********************************************/ + + constructor( + public tableService: TableService, + public cdr: ChangeDetectorRef, + public readonly config: TableSetting, + ) { + this.showProgress = true; + this.tableSetting = { + direction: "ltr", + columnSetting: null, + visibleActionMenu: null, + }; + if (this.config) { + this.tableSetting = { ...this.tableSetting, ...this.config }; + } + } + + @Input() + @HostBinding("style.direction") + get direction(): Direction { + return this.tableSetting?.direction; + } + set direction(value: Direction) { + this.tableSetting.direction = value; + } + + @Input() + get ScrollStrategyType() { + return this.tableSetting.scrollStrategy; + } + set ScrollStrategyType(value: TableScrollStrategy) { + this.viewport["_scrollStrategy"].scrollStrategyMode = value; + this.tableSetting.scrollStrategy = value; + } + + @Input() + get pagingMode() { + return this.tablePagingMode; + } + set pagingMode(value: TablePaginationMode) { + this.tablePagingMode = value; + this.updatePagination(); + } + + @Input() + get pagination() { + return this._tablePagination; + } + set pagination(value: TablePagination) { + if (value && value !== null) { + this._tablePagination = value; + if (isNullorUndefined(this._tablePagination.pageSizeOptions)) { + this._tablePagination.pageSizeOptions = [5, 10, 25, 100]; + } + if (isNullorUndefined(this._tablePagination.pageSize)) { + this._tablePagination.pageSize = + this._tablePagination.pageSizeOptions[0]; + } + this.updatePagination(); + } + } + + @Input() + get rowSelectionModel() { + return this._rowSelectionModel; + } + set rowSelectionModel(value: SelectionModel) { + if (!isNullorUndefined(value)) { + if ( + this._rowSelectionMode && + value && + this._rowSelectionMode !== "none" + ) { + this._rowSelectionMode = + value.isMultipleSelection() === true ? "multi" : "single"; + } + this._rowSelectionModel = value; + } + } + + @Input() + get rowSelectionMode() { + return this._rowSelectionMode; + } + set rowSelectionMode(selection: TableSelectionMode) { + selection = selection || "none"; + const isSelectionColumn = selection === "single" || selection === "multi"; + if ( + this._rowSelectionModel === null || + (this._rowSelectionModel.isMultipleSelection() === true && + selection === "single") || + (this._rowSelectionModel.isMultipleSelection() === false && + selection === "multi") + ) { + this._rowSelectionModel = new SelectionModel( + selection === "multi", + [], + ); + } + if ( + this.displayedColumns?.length > 0 && + !isSelectionColumn && + this.displayedColumns[0] === "row-checkbox" + ) { + this.displayedColumns.shift(); + } else if ( + this.displayedColumns?.length > 0 && + isSelectionColumn && + this.displayedColumns[0] !== "row-checkbox" + ) { + this.displayedColumns.unshift("row-checkbox"); + } + this._rowSelectionMode = selection; + } + + @Input() + get tableName() { + return this.tableService.tableName; + } + set tableName(value: string) { + this.tableService.tableName = value; + } + + @Input() + get showProgress() { + return this.progressColumn.length > 0; + } + set showProgress(value: boolean) { + this.progressColumn = []; + if (value === true) { + this.progressColumn.push("progress"); + } + } + + protected initSystemField(data: any[]) { + if (data) { + data = data.map((item, index) => { + item.id = index; + item.option = item.option || {}; + return item; + }); + } + } + + @Input() + get expandComponent(): any { + return this._expandComponent; + } + set expandComponent(value: any) { + this._expandComponent = value; + if (this._expandComponent !== null && this._expandComponent !== undefined) { + this.expandColumn = ["expandedDetail"]; + } else { + this.expandColumn = []; + } + } + + @Input() + get columns() { + return this.tableColumns; + } + set columns(fields: TableField[]) { + (fields || []).forEach((f, i) => { + // key name error // + if (f.name.toLowerCase() === "id") { + throw 'Field name is reserved.["id"]'; + } + const settingFields = (this.tableSetting.columnSetting || []).filter( + (s) => s.name === f.name, + ); + const settingField = settingFields.length > 0 ? settingFields[0] : null; + /* default value for fields */ + f.printable = f.printable || true; + f.exportable = f.exportable || true; + f.toExport = + f.toExport || + ((row, type) => (typeof row === "object" ? row[f.name] : "")); + f.toPrint = (row) => (typeof row === "object" ? row[f.name] : ""); + f.enableContextMenu = f.enableContextMenu || true; + f.header = f.header || titleCase(f.name); + f.display = getObjectProp("display", "visible", settingField, f); + f.filter = getObjectProp("filter", "client-side", settingField, f); + f.sort = getObjectProp("sort", "client-side", settingField, f); + f.sticky = getObjectProp("sticky", "none", settingField, f); + f.width = getObjectProp("width", this.defaultWidth, settingField, f); + const unit = f.widthUnit || "px"; + const style = + unit === "px" ? f.width + "px" : `calc( ${f.widthPercentage}% )`; + if (f.width) { + f.style = { + ...f.style, + "max-width": style, + "min-width": style, + }; + } + }); + this.tableColumns = fields; + + this.updateColumn(); + } + + public updateColumn() { + if (this.tableColumns) { + this.tableSetting.columnSetting = clone(this.tableColumns); + } + this.setDisplayedColumns(); + } + + updatePagination() { + if (isNullorUndefined(this.tvsDataSource)) { + return; + } + if ( + this.tablePagingMode === "client-side" || + this.tablePagingMode === "server-side" + ) { + this.viewportClass = "viewport-with-pagination"; + if (!isNullorUndefined(this.tvsDataSource.paginator)) { + let dataLen = this.tvsDataSource.paginator.length; + if ( + !isNullorUndefined(this._tablePagination.length) && + this._tablePagination.length > dataLen + ) { + dataLen = this._tablePagination.length; + } + this.tvsDataSource.paginator.length = dataLen; + } + } else { + this.viewportClass = "viewport"; + if ((this.tvsDataSource as any)._paginator !== undefined) { + delete (this.tvsDataSource as any)._paginator; + } + } + this.tvsDataSource.refreshFilterPredicate(); + } + + public clearSelection() { + if (this._rowSelectionModel) { + this._rowSelectionModel.clear(); + } + } + + public clear() { + if (!isNullorUndefined(this.tvsDataSource)) { + if (this.viewport) { + this.viewport.scrollTo({ top: 0, behavior: "auto" }); + } + this.tvsDataSource.clearData(); + this.expandedElement = null; + } + this.clearSelection(); + this.cdr.detectChanges(); + } + + setDisplayedColumns() { + if (this.columns) { + this.displayedColumns.splice(0, this.displayedColumns.length); + this.columns.forEach((column, index) => { + column.index = index; + if ( + column.display === undefined || + column.display === "visible" || + column.display === "prevent-hidden" + ) { + this.displayedColumns.push(column.name); + } + }); + if ( + (this._rowSelectionMode === "multi" || + this._rowSelectionMode === "single") && + this.displayedColumns.indexOf("row-checkbox") === -1 + ) { + this.displayedColumns.unshift("row-checkbox"); + } + this.displayedFooter = this.columns + .filter((item) => item.footer !== null && item.footer !== undefined) + .map((item) => item.name); + if (this.tableSetting.visibleTableMenu !== false) { + this.displayedColumns.push("table-menu"); + } + } + } + + /************************************ Drag & Drop Column *******************************************/ + public refreshGrid() { + this.cdr.detectChanges(); + this.refreshColumn(this.tableColumns); + this.table.renderRows(); + this.viewport.checkViewportSize(); + } + + public moveRow(from: number, to: number) { + if ( + from >= 0 && + from < this.tvsDataSource.data.length && + to >= 0 && + to < this.tvsDataSource.data.length + ) { + this.tvsDataSource.data[from].id = to; + this.tvsDataSource.data[to].id = from; + moveItemInArray(this.tvsDataSource.data, from, to); + this.tvsDataSource.data = Object.assign([], this.tvsDataSource.data); + } + } + + moveColumn(from: number, to: number) { + moveItemInArray(this.columns, from, to); + this.refreshColumn(this.columns); + } + + refreshColumn(columns: TableField[]) { + if (this.viewport) { + const currentOffset = this.viewport.measureScrollOffset(); + this.columns = columns; + setTimeout( + () => this.viewport.scrollTo({ top: currentOffset, behavior: "auto" }), + 0, + ); + } + } + + /************************************ Selection Table Row *******************************************/ + + /** Whether the number of selected elements matches the total number of rows. */ + isAllSelected() { + const numSelected = this._rowSelectionModel.selected.length; + const numRows = this.tvsDataSource.filteredData.length; + return numSelected === numRows; + } + + /** Selects all rows if they are not all selected; otherwise clear selection. */ + masterToggle() { + const isAllSelected = this.isAllSelected(); + if (isAllSelected === false) { + this.tvsDataSource.filteredData.forEach((row) => + this._rowSelectionModel.select(row), + ); + } else { + this._rowSelectionModel.clear(); + } + this.onRowEvent.emit({ + event: RowEventType.MasterSelectionChange, + sender: { selectionModel: this._rowSelectionModel }, + }); + } + + onRowSelectionChange(e: any, row: T) { + if (e) { + this._rowSelectionModel.toggle(row); + this.onRowEvent.emit({ + event: RowEventType.RowSelectionChange, + sender: { + selectionModel: this._rowSelectionModel, + row: row, + }, + }); + } + } +} diff --git a/src/app/shared/modules/dynamic-material-table/cores/type.ts b/src/app/shared/modules/dynamic-material-table/cores/type.ts new file mode 100644 index 000000000..83eb31f96 --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/cores/type.ts @@ -0,0 +1,94 @@ +// ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| + +// |||||||||||||||||||||||||||||||||||||| Utils |||||||||||||||||||||||||||||||||||||||||||||||||| +/** + * check object is null or undefined + */ +export function isNullorUndefined(value: any): boolean { + if (value === null || value === undefined) { + return true; + } else { + return false; + } +} + +/** + * clone object but reference variable not change + */ +export function clone(obj: any): any { + if (obj === null || obj === undefined) { + return obj; + } else if (Array.isArray(obj)) { + const array: T[] = []; + obj.forEach((item) => array.push(Object.assign({}, item))); + return array; + } else { + return Object.assign({}, obj); + } +} + +/** + * clone object and all reference variable but may be there is a circle loop. + */ +export function deepClone(obj: any) { + if (obj === null || obj === undefined) { + return obj; + } else if (Array.isArray(obj)) { + const array: T[] = []; + obj.forEach((item) => array.push(deepClone(item))); + return array as T[]; + } else { + const c = Object.assign({} as T, obj); + const fields: string[] = Object.getOwnPropertyNames(obj); + fields.forEach((f) => { + const field = obj[f]; + if (field !== null && typeof field === "object") { + c[f] = deepClone(field); + } + }); + return c; + } +} + +export function getObjectProp( + fieldName: string, + defaultValue: any, + ...variable: any[] +) { + for (const v in variable) { + if (variable[v] && !isNullorUndefined(variable[v][fieldName])) { + return variable[v][fieldName]; + } + } + return defaultValue; +} + +export function copy( + from: any, + to: any, + forced = false, + nullSkip = true, + undefinedSkip = true, +) { + if (from === null || from === undefined) { + return; + } + if (to === null || to === undefined) { + to = {}; + } + const f: string[] = Object.keys(from); + const t: string[] = Object.keys(to); + f.forEach((fi) => { + if ( + (forced === true || t.includes(fi) === true) && + !(from[fi] === null && nullSkip === true) && + !(from[fi] === undefined && undefinedSkip === true) + ) { + to[fi] = from[fi]; + } + }); +} + +export interface HashMap { + [key: string]: T; +} diff --git a/src/app/shared/modules/dynamic-material-table/models/column-filter.model.ts b/src/app/shared/modules/dynamic-material-table/models/column-filter.model.ts new file mode 100644 index 000000000..31cad7054 --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/models/column-filter.model.ts @@ -0,0 +1,5 @@ +export interface ColumnFilter { + key: string; + predicate: (value: any) => boolean; + valueFn: (item: T) => any; +} diff --git a/src/app/shared/modules/dynamic-material-table/models/context-menu.model.ts b/src/app/shared/modules/dynamic-material-table/models/context-menu.model.ts new file mode 100644 index 000000000..584dc71d6 --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/models/context-menu.model.ts @@ -0,0 +1,11 @@ +import { ThemePalette } from "@angular/material/core"; + +export interface ContextMenuItem { + name: string; + text: string; + color: ThemePalette; + icon?: string; + disabled?: boolean; + visible?: boolean; + divider?: boolean; +} diff --git a/src/app/shared/modules/dynamic-material-table/models/language-pack.model.ts b/src/app/shared/modules/dynamic-material-table/models/language-pack.model.ts new file mode 100644 index 000000000..757e1d913 --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/models/language-pack.model.ts @@ -0,0 +1,61 @@ +export interface TableLabels { + NoData: string; +} + +export interface FilterLabels { + Clear: string; + Search: string; + And: string; + Or: string; + /* Text Compare */ + Text: string; + TextContains: string; + TextEquals: string; + TextStartsWith: string; + TextEndsWith: string; + TextEmpty: string; + TextNotEmpty: string; + /* Number Compare */ + Number: string; + NumberEquals: string; + NumberNotEquals: string; + NumberGreaterThan: string; + NumberLessThan: string; + NumberEmpty: string; + NumberNotEmpty: string; + /* Category List Compare */ + CategoryContains: string; + CategoryNotContains: string; + /* Boolean Compare */ + /* Date Compare */ + + /* Paginator */ +} + +export interface MenuLabels { + saveData: string; + newSetting: string; + defaultSetting: string; + noSetting: string; + columnSetting: string; + saveTableSetting: string; + fullScreen: string; + clearFilter: string; + jsonFile: string; + csvFile: string; + printTable: string; + filterMode: string; + filterLocalMode: string; + filterServerMode: string; + sortMode: string; + sortLocalMode: string; + sortServerMode: string; + printMode: string; + printYesMode: string; + printNoMode: string; + pinMode: string; + pinNoneMode: string; + pinStartMode: string; + pinEndMode: string; + thereIsNoColumn: string; +} diff --git a/src/app/shared/modules/dynamic-material-table/models/pipe.model.ts b/src/app/shared/modules/dynamic-material-table/models/pipe.model.ts new file mode 100644 index 000000000..45e55c377 --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/models/pipe.model.ts @@ -0,0 +1,6 @@ +import { Type } from "@angular/core"; + +export interface IPipe { + token?: Type; + data?: any[]; +} diff --git a/src/app/shared/modules/dynamic-material-table/models/print-config.model.ts b/src/app/shared/modules/dynamic-material-table/models/print-config.model.ts new file mode 100644 index 000000000..a6437f74c --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/models/print-config.model.ts @@ -0,0 +1,14 @@ +import { AbstractField } from "./table-field.model"; +import { Direction } from "./table-setting.model"; + +export interface PrintConfig { + displayedFields?: string[]; + title?: string; + userPrintParameters?: { key: string; value: string }[]; + tablePrintParameters?: { key: string; value: string }[]; + showParameters?: boolean; + data?: any[]; + columns?: AbstractField[]; + direction?: Direction; + pregenerate?: (html: string) => string; +} diff --git a/src/app/shared/modules/dynamic-material-table/models/resize-column.mode.ts b/src/app/shared/modules/dynamic-material-table/models/resize-column.mode.ts new file mode 100644 index 000000000..830ad870a --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/models/resize-column.mode.ts @@ -0,0 +1,12 @@ +import { Subject } from "rxjs"; + +export class ResizeColumn { + startX: number; + startWidth: number; + columnIndex: number; + resizeHandler?: "left" | "right" = null; + public widthUpdate: Subject<{ e: ResizeColumn; w: number }> = new Subject<{ + e: ResizeColumn; + w: number; + }>(); +} diff --git a/src/app/shared/modules/dynamic-material-table/models/table-field.model.ts b/src/app/shared/modules/dynamic-material-table/models/table-field.model.ts new file mode 100644 index 000000000..b70e92f6c --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/models/table-field.model.ts @@ -0,0 +1,72 @@ +import { IPipe } from "./pipe.model"; +import { FooterCell } from "./table-footer.model"; +import { TableRow } from "./table-row.model"; + +export declare type AtRenderFunc = (row: R) => string; +export declare type AtClassFunc = (row: any, col: any) => string; +export declare type AtSortFunc = ( + data: R[], + col: any, +) => string; +export declare type AtFilterFunc = ( + data: R[], + col: any, +) => string; +export declare type ToPrint = (row: any) => any; +export declare type ToExport = (row: any, type: any) => any; +export declare type FieldType = "text" | "number" | "date" | "category"; +export declare type FieldDisplay = "visible" | "hidden" | "prevent-hidden"; +export declare type FieldSticky = "start" | "end" | "none"; +export declare type FieldFilter = "client-side" | "server-side" | "none"; +export declare type FieldSort = "client-side" | "server-side" | "none"; + +export interface TableField extends AbstractField { + classNames?: string; + rowClass?: string | AtClassFunc; + customSortFunction?: AtSortFunc; + customFilterFunction?: AtSortFunc; + toPrint?: ToPrint; + toExport?: ToExport; +} + +export interface AbstractField { + index?: number; + name: string /* The key of the data */; + type?: FieldType /* Type of data in the field */; + minWidth?: number /* min width of column */; + width?: number /* width of column */; + widthPercentage?: number; + widthUnit?: "px" | "%" /* width unit */; + style?: any /* private property used only in html */; + header?: string /* The title of the column */; + isKey?: boolean; + inlineEdit?: boolean; + display?: FieldDisplay /* Hide and visible this column */; + sticky?: FieldSticky /* sticky this column to start or end */; + filter?: FieldFilter; + sort?: FieldSort; + cellClass?: string /* Apply a class to a cell, class name must be in the global stylesheet */; + cellStyle?: any /* Apply a style to a cell, style must be object ex: [...].cellStyle = {'color' : 'red'} */; + icon?: string /* Set Icon in Column */; + iconColor?: string /* Set Icon Color */; + dynamicCellComponent?: any /* Set Dynamic Component in Cell */; + draggable?: boolean; + clickable?: boolean; + clickType?: "cell" | "label" | "custom"; + printable?: boolean /* display in printing view by default is true */; + exportable?: boolean; + enableContextMenu?: boolean; + rowSelectable?: boolean; + /* Footer */ + footer?: FooterCell[]; + /* For Ellipsis Text */ + cellEllipsisRow?: number; + cellTooltipEnable?: boolean; + headerEllipsisRow?: number; + headerTooltipEnable?: boolean; + option?: any; // for store share data show in cell of column + categoryData?: any[]; + pipes?: IPipe[]; + toString?: (column: TableField, row: TableRow) => string; + customSort?: (column: TableField, row: any) => string; +} diff --git a/src/app/shared/modules/dynamic-material-table/models/table-footer.model.ts b/src/app/shared/modules/dynamic-material-table/models/table-footer.model.ts new file mode 100644 index 000000000..2c935bda0 --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/models/table-footer.model.ts @@ -0,0 +1,9 @@ +export interface FooterCell { + aggregateText?: string /* The title of the aggregate text */; + aggregateIcon?: string /* Set Icon in Column */; + aggregateIconColor?: string /* Set Icon Color */; + footerClass?: string /* Apply a class to a cell, class name must be in the global stylesheet */; + footerStyle?: any /* Apply a style to a cell, style must be object ex: [...].cellStyle = {'color' : 'red'} */; + printable?: boolean /* disply in printing view by defualt is true */; + exportable?: boolean; +} diff --git a/src/app/shared/modules/dynamic-material-table/models/table-pagination.model.ts b/src/app/shared/modules/dynamic-material-table/models/table-pagination.model.ts new file mode 100644 index 000000000..f27521ada --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/models/table-pagination.model.ts @@ -0,0 +1,11 @@ +export declare type TablePaginationMode = + | "client-side" + | "server-side" + | "none"; +export interface TablePagination { + length?: number; + pageIndex?: number; + pageSize?: number; + pageSizeOptions?: number[]; + showFirstLastButtons?: boolean; +} diff --git a/src/app/shared/modules/dynamic-material-table/models/table-row.model.ts b/src/app/shared/modules/dynamic-material-table/models/table-row.model.ts new file mode 100644 index 000000000..40f793661 --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/models/table-row.model.ts @@ -0,0 +1,57 @@ +import { SelectionModel } from "@angular/cdk/collections"; +import { HashMap } from "../cores/type"; +import { ContextMenuItem } from "./context-menu.model"; +import { TableField } from "./table-field.model"; + +// this fields are for each row data +export interface TableRow { + id?: number; + rowActionMenu?: { [key: string]: ContextMenuItem }; + option?: RowOption; +} + +export type TableSelectionMode = "single" | "multi" | "none"; +export enum RowEventType { + MasterSelectionChange = "MasterSelectionChange", + RowSelectionChange = "RowSelectionChange", + RowActionMenu = "RowActionMenu", + RowClick = "RowClick", + DoubleClick = "DoubleClick", + CellClick = "CellClick", + LabelClick = "LabelClick", + BeforeContextMenuOpen = "BeforeContextMenuOpen", + ContextMenuClick = "ContextMenuClick", +} + +export interface IRowEvent { + event: RowEventType | any; + sender: { + row?: T; + column?: TableField; + selectionModel?: SelectionModel; + [t: string]: any; + }; +} + +export enum TableEventType { + ReloadData = "ReloadData", + SortChanged = "SortChanged", + ExportData = "ExportData", +} + +export interface ITableEvent { + event: TableEventType | any; + sender: any | undefined; +} + +export interface IRowActionMenuEvent { + actionItem: ContextMenuItem; + rowItem: T; +} + +export interface RowOption extends HashMap { + style?: any; + class?: any; + selected?: boolean; + expand?: boolean; +} diff --git a/src/app/shared/modules/dynamic-material-table/models/table-setting.model.ts b/src/app/shared/modules/dynamic-material-table/models/table-setting.model.ts new file mode 100644 index 000000000..c5eec747e --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/models/table-setting.model.ts @@ -0,0 +1,54 @@ +import { TableScrollStrategy } from "../cores/fixed-size-table-virtual-scroll-strategy"; +import { AbstractField } from "./table-field.model"; + +export type Direction = "rtl" | "ltr"; +export type DisplayMode = "visible" | "hidden" | "none"; +export interface TableSetting { + pageSize?: number; + direction?: Direction; + columnSetting?: AbstractField[] | null; + visibleActionMenu?: VisibleActionMenu | null; + visibleTableMenu?: boolean; + alternativeRowStyle?: any; + normalRowStyle?: any; + scrollStrategy?: TableScrollStrategy; + rowStyle?: any; + enableContextMenu?: boolean; + autoHeight?: boolean; + saveSettingMode?: "simple" | "multi" | "none"; + settingName?: string; + settingList?: SettingItem[]; + showColumnSettingMenu?: boolean; +} + +export interface SettingItem extends TableSetting { + isCurrentSetting?: boolean; + isDefaultSetting?: boolean; +} + +export interface VisibleActionMenu { + fullscreen?: boolean; + json?: boolean; + csv?: boolean; + print?: boolean; + columnSettingPin?: boolean; + columnSettingOrder?: boolean; + columnSettingFilter?: boolean; + columnSettingSort?: boolean; + columnSettingPrint?: boolean; + saveTableSetting?: boolean; + clearFilter?: boolean; +} + +export class TableSetting implements TableSetting { + direction?: Direction = "ltr"; + visibleActionMenu?: VisibleActionMenu | null = null; + visibleTableMenu?: boolean; + alternativeRowStyle?: any; + normalRowStyle?: any; + rowStyle?: any; + enableContextMenu?: boolean; + autoHeight?: boolean; + saveSettingMode?: "simple" | "multi" | "none"; + showColumnSettingMenu?: boolean = false; +} diff --git a/src/app/shared/modules/dynamic-material-table/table/dynamic-mat-table.component.html b/src/app/shared/modules/dynamic-material-table/table/dynamic-mat-table.component.html new file mode 100644 index 000000000..db5961563 --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/table/dynamic-mat-table.component.html @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + {{ column?.icon }} + drag_indicator +
+ {{ column.header }} +
+
+ +
+
+ + + + + + + + + + +
+ + + + + + + + + + +
+ + search + + +
+
+
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ No data +
+ +
+
+ + +
+ + + + + + + + diff --git a/src/app/shared/modules/dynamic-material-table/table/dynamic-mat-table.component.scss b/src/app/shared/modules/dynamic-material-table/table/dynamic-mat-table.component.scss new file mode 100644 index 000000000..971e588ba --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/table/dynamic-mat-table.component.scss @@ -0,0 +1,307 @@ +@import "./dynamic-mat-table.style.scss"; + +:host { + display: flex; + flex-direction: column; + table-layout: fixed; + min-height: 200px; + position: relative; + overflow: auto; + transition: 0.3s cubic-bezier(0.46, -0.72, 0.46, 1.54); + border: 2px rgb(0, 150, 136); + + .global-search-wrapper { + text-align: right; + padding: 0 20px; + + mat-form-field { + min-width: 30%; + } + } +} + +/* Fixed Scroll */ +::ng-deep .cdk-virtual-scroll-content-wrapper { + left: auto !important; +} + +::ng-deep .mat-mdc-menu-panel { + min-height: 48px; +} + +.label-cell { + width: 100%; +} + +mat-cell .label-cell { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +mat-cell:first-of-type, +mat-header-cell:first-of-type:not(.row-checkbox), +mat-footer-cell:first-of-type { + padding-left: 0 !important; +} + +.rtl-cell { + padding-right: 20px; +} + +.ltr-cell { + padding-left: 20px; +} + +.viewport { + height: calc(100% - 0px); +} + +.viewport-with-pagination { + height: calc(100% - 48px); +} + +.table-paginator { + position: sticky; + bottom: 0; + display: flex; + flex-wrap: wrap; + max-height: 48px; + align-items: center; + overflow: hidden; + direction: ltr; +} + +mat-footer-row, +mat-row { + min-height: auto !important; +} + +mat-row, +mat-header-row, +mat-footer-row { + display: flex; + border-width: 0; + border-bottom-width: 1px; + border-bottom-color: #d2d2d2; + border-style: solid; + align-items: center; + box-sizing: border-box; +} + +mat-cell, +mat-footer-cell, +mat-header-cell { + align-self: stretch; + color: inherit; + background-color: inherit; +} + +@include table-base; +@include progress; +@include no-records; +@include dmf-class; +@include resize-column; +@include context-menu; +@include header-sort; + +cdk-virtual-scroll-viewport { + min-height: 100px; + height: inherit; + overflow: auto; +} + +// Header Text +.header-caption { + font-weight: bolder; + font-size: 14px; + width: 100%; +} + +.header { + user-select: none; + background-color: white; +} + +.footer { + user-select: none; + background-color: white; +} + +.mdc-data-table__header-cell { + padding: 0; +} + +// Table Column Select +.row-checkbox { + padding-left: 0 !important; + padding-right: 0 !important; + max-width: 46px; + min-width: 46px; + mat-checkbox { + padding: 10px; + } + mat-icon { + padding: 11px !important; + } +} + +// table action menu +.table-global-search { + max-width: 300px; + min-width: initial; + padding: 0 !important; + background-color: inherit; + + .global-search-wrapper { + width: 100%; + } +} + +.table-menu { + max-width: 42px; + min-width: initial; + padding: 0 !important; + background-color: inherit; +} + +.mat-mdc-header-cell .column-icon { + padding-right: 16px; +} + +.column-icon { + :host .mat-mdc-header-row > .mat-mdc-header-cell:hover & { + opacity: 0; + transform: translateY(5px); + transition: all 0.2s; + } +} + +.drag-indicator { + position: absolute; + @include header-icon-base; + cursor: move; + :host .mat-mdc-header-row > .mat-mdc-header-cell:hover & { + opacity: 1; + pointer-events: fill; + transform: translateY(0px); + } +} + +.drag-indicator:hover { + color: #bfc0c0 !important; +} + +.cdk-drag-preview { + color: black; + min-height: 55px; // fixed drag and height max-mized ;) used with min-height + border: solid 1px #d4d4d4; + background-color: rgba(245, 245, 245); + box-sizing: border-box; + border-radius: 4px; + box-shadow: + 0 5px 5px -3px rgba(0, 0, 0, 0.2), + 0 8px 10px 1px rgba(0, 0, 0, 0.14), + 0 3px 14px 2px rgba(0, 0, 0, 0.12); +} + +.cdk-drag-placeholder { + border: dotted 1px rgb(156, 156, 156); + background-color: rgb(211, 211, 211); + content: none; +} + +.cdk-drag-animating { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} + +.cdk-drop-list-dragging { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} + +.detail-row { + height: 0px; + display: inline !important; + width: 100%; +} + +.table-row:not(.expanded-row):hover { + background: whitesmoke; +} + +.table-row:not(.expanded-row):active { + background: #efefef; +} + +.table-row mat-cell { + border-bottom-width: 0; +} + +.expanded-detail { + overflow: hidden; + display: flex; + background-color: #fafafa; +} + +.expanded-detail-cell { + display: block; + border-width: 0; + padding: 0px !important; + width: 100%; + z-index: 2; +} + +::ng-deep .cell-tooltip { + padding: 8px; + font-size: 12px; + min-width: 100px; + text-align: center; + margin-right: -20px; +} + +/* Custom Tooltip */ +.tooltip { + position: relative; + display: inline-block; + border-bottom: 1px dotted black; +} + +.tooltip .tooltiptext { + visibility: hidden; + min-width: 120px; + background-color: #e91e63; + color: #fff; + text-align: center; + border-radius: 6px; + padding: 5px 0; + position: absolute; + z-index: 1; + left: 0; + top: 43px; + margin-left: -86%; +} + +.tooltip:hover .tooltiptext { + visibility: visible; + white-space: pre; +} +::ng-deep .mat-mdc-footer-cell { + flex-direction: column !important; +} + +.footer-column { + display: flex; + flex-direction: column; + .footer-row { + display: flex; + flex-direction: row; + span { + display: inherit; + align-items: center; + } + } +} + +::ng-deep .mat-mdc-progress-bar .mdc-linear-progress__buffer-bar { + background: white !important; +} diff --git a/src/app/shared/modules/dynamic-material-table/table/dynamic-mat-table.component.ts b/src/app/shared/modules/dynamic-material-table/table/dynamic-mat-table.component.ts new file mode 100644 index 000000000..c6235acc7 --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/table/dynamic-mat-table.component.ts @@ -0,0 +1,919 @@ +import { + Component, + OnInit, + AfterViewInit, + QueryList, + ElementRef, + ViewChild, + TemplateRef, + Renderer2, + ChangeDetectorRef, + Input, + OnDestroy, + ContentChildren, + Injector, + ComponentRef, + HostBinding, + ChangeDetectionStrategy, + EventEmitter, + Directive, + OnChanges, + ViewContainerRef, + SimpleChanges, +} from "@angular/core"; +import { TableCoreDirective } from "../cores/table.core.directive"; +import { TableService } from "./dynamic-mat-table.service"; +import { TableField } from "../models/table-field.model"; +import { AbstractFilter } from "./extensions/filter/compare/abstract-filter"; +import { HeaderFilterComponent } from "./extensions/filter/header-filter.component"; +import { MatDialog } from "@angular/material/dialog"; +import { + trigger, + transition, + style, + animate, + query, + stagger, + state, +} from "@angular/animations"; +import { ResizeColumn } from "../models/resize-column.mode"; +import { TableMenuActionChange } from "./extensions/table-menu/table-menu.component"; +import { CdkDragDrop, moveItemInArray } from "@angular/cdk/drag-drop"; +import { HashMap, isNullorUndefined } from "../cores/type"; +import { SettingItem, TableSetting } from "../models/table-setting.model"; +import { + debounceTime, + delay, + distinctUntilChanged, + filter, +} from "rxjs/operators"; +import { FixedSizeTableVirtualScrollStrategy } from "../cores/fixed-size-table-virtual-scroll-strategy"; +import { Subject, Subscription } from "rxjs"; +import { MatMenuTrigger } from "@angular/material/menu"; +import { ContextMenuItem } from "../models/context-menu.model"; +import { + Overlay, + OverlayContainer, + OverlayPositionBuilder, + OverlayRef, +} from "@angular/cdk/overlay"; +import { requestFullscreen } from "../utilizes/html.helper"; +import { TooltipComponent } from "../tooltip/tooltip.component"; +import { ComponentPortal } from "@angular/cdk/portal"; +import { PageEvent } from "@angular/material/paginator"; +import { + IRowEvent, + RowEventType, + TableEventType, + TableRow, +} from "../models/table-row.model"; +import { PrintTableDialogComponent } from "./extensions/print-dialog/print-dialog.component"; + +export interface IDynamicCell { + row: TableRow; + column: TableField; + parent: DynamicMatTableComponent; + onRowEvent?: EventEmitter>; +} + +// NOTE: This directive is in the same file as the parent component to avoind production build error (https://angular.dev/errors/NG3003). +// (https://github.com/angular/angular/issues/43227#issuecomment-904173738) +@Directive({ + selector: "[appDynamicCell]", +}) +export class DynamicCellDirective implements OnChanges, OnDestroy { + @Input() component: any; + @Input() column: TableField; + @Input() row: any; + @Input() onRowEvent: EventEmitter>; + componentRef: ComponentRef = null; + + constructor( + private vc: ViewContainerRef, + private parent: DynamicMatTableComponent, + ) {} + + ngOnChanges(changes: SimpleChanges): void { + if (this.componentRef === null || this.componentRef === undefined) { + this.initComponent(); + } + // pass input parameters + if (changes.column && changes.column.currentValue) { + this.componentRef.instance.column = this.column; + } + if (changes.row && changes.row.currentValue) { + (this.componentRef.instance as any).row = this.row; + } + if (changes.onRowEvent && changes.onRowEvent.currentValue) { + (this.componentRef.instance as any).onRowEvent = this.onRowEvent; + } + } + + ngOnDestroy(): void { + if (this.componentRef) { + this.componentRef.destroy(); + } + } + + initComponent() { + try { + this.componentRef = this.vc.createComponent(this.component); + this.updateInput(); + } catch (e) { + console.warn(e); + } + } + + updateInput() { + if (this.parent) { + (this.componentRef.instance as IDynamicCell).parent = this.parent; + } + if (this.column) { + this.componentRef.instance.column = this.column; + } + if (this.row) { + (this.componentRef.instance as IDynamicCell).row = this.row; + } + if (this.onRowEvent) { + (this.componentRef.instance as IDynamicCell).onRowEvent = this.onRowEvent; + } + } +} + +export const tableAnimation = trigger("tableAnimation", [ + transition("void => *", [ + query(":enter", style({ transform: "translateX(-50%)", opacity: 0 }), { + //limit: 5, + optional: true, + }), + query( + ":enter", + stagger("0.01s", [ + animate( + "0.5s ease", + style({ transform: "translateX(0%)", opacity: 1 }), + ), + ]), + { + optional: true, + }, + ), + ]), +]); + +export const expandAnimation = trigger("detailExpand", [ + state("collapsed", style({ height: "0px", minHeight: "0" })), + state("expanded", style({ height: "*" })), + transition( + "expanded <=> collapsed", + animate("100ms cubic-bezier(0.4, 0.0, 0.2, 1)"), + ), +]); + +@Component({ + selector: "dynamic-mat-table", + templateUrl: "./dynamic-mat-table.component.html", + styleUrls: ["./dynamic-mat-table.component.scss"], + animations: [tableAnimation, expandAnimation], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DynamicMatTableComponent + extends TableCoreDirective + implements OnInit, AfterViewInit, OnDestroy +{ + private dragDropData = { dragColumnIndex: -1, dropColumnIndex: -1 }; + private eventsSubscription: Subscription; + currentContextMenuSender: any = {}; + globalSearchUpdate = new Subject(); + + @ViewChild("tbl", { static: true }) tbl; + @Input() + get setting() { + return this.tableSetting; + } + set setting(value: TableSetting) { + if (!isNullorUndefined(value)) { + value.alternativeRowStyle = + value.alternativeRowStyle || this.tableSetting.alternativeRowStyle; + value.columnSetting = + value.columnSetting || this.tableSetting.columnSetting; + value.direction = value.direction || this.tableSetting.direction; + value.normalRowStyle = + value.normalRowStyle || this.tableSetting.normalRowStyle; + value.visibleActionMenu = + value.visibleActionMenu || this.tableSetting.visibleActionMenu; + value.visibleTableMenu = + value.visibleTableMenu || this.tableSetting.visibleTableMenu; + value.autoHeight = value.autoHeight || this.tableSetting.autoHeight; + value.saveSettingMode = + value.saveSettingMode || this.tableSetting.saveSettingMode || "simple"; + this.pagination.pageSize = + value.pageSize || + this.tableSetting.pageSize || + this.pagination.pageSize; + /* Dynamic Cell must update when setting change */ + value?.columnSetting?.forEach((column) => { + const originalColumn = this.columns?.find( + (c) => c.name === column.name, + ); + if (originalColumn) { + column = { ...originalColumn, ...column }; + } + }); + this.tableSetting = value; + this.setDisplayedColumns(); + } + } + init = false; + + @HostBinding("style.height.px") height = null; + + @ViewChild("tooltip") tooltipRef!: TemplateRef; + @ViewChild(MatMenuTrigger) contextMenu: MatMenuTrigger; + public contextMenuPosition = { x: "0px", y: "0px" }; + @ViewChild("printRef", { static: true }) printRef!: TemplateRef; + @ViewChild("printContentRef", { static: true }) printContentRef!: ElementRef; + @ContentChildren(HeaderFilterComponent) + headerFilterList!: QueryList; + + printing = true; + printTemplate: TemplateRef = null; + public resizeColumn: ResizeColumn = new ResizeColumn(); + /* mouse resize */ + resizableMousemove: () => void; + resizableMouseup: () => void; + /* Tooltip */ + overlayRef: OverlayRef = null; + + constructor( + public dialog: MatDialog, + private renderer: Renderer2, + public tableService: TableService, + public cdr: ChangeDetectorRef, + public overlay: Overlay, + private overlayContainer: OverlayContainer, + private overlayPositionBuilder: OverlayPositionBuilder, + public readonly config: TableSetting, + ) { + super(tableService, cdr, config); + this.overlayContainer + .getContainerElement() + .addEventListener("contextmenu", (e) => { + e.preventDefault(); + return false; + }); + + this.eventsSubscription = this.resizeColumn.widthUpdate + .pipe( + delay(150), + filter((data) => data.e.columnIndex >= 0) /* Checkbox Column */, + ) + .subscribe((data) => { + let i = data.e.columnIndex; + if (data.e.resizeHandler === "left") { + const visibleColumns = this.columns.filter( + (c) => c.display !== "hidden" && c.index < data.e.columnIndex, + ); + i = visibleColumns[visibleColumns.length - 1].index; + } + const unit = this.columns[i].widthUnit || "px"; + let style = ""; + if (this.columns[i].minWidth) { + data.w = Math.min(this.columns[i].minWidth, data.w); + } + if (unit === "px") { + style = data.w + "px"; + } else if (unit === "%") { + const widthChanges = + (this.tableSetting.columnSetting[i].width ?? 0) - data.w; + console.log( + this.tableSetting.columnSetting[i].width, + data.w, + widthChanges, + ); + style = `calc( ${this.columns[i].widthPercentage}% + ${widthChanges}px)`; + } + this.columns[i].style = { + ...this.columns[i].style, + "max-width": style, + "min-width": style, + }; + /* store latest width in setting if exists */ + if (this.tableSetting.columnSetting[i]) { + this.tableSetting.columnSetting[i].width = data.w; + } + this.refreshGrid(); + }); + + this.globalSearchUpdate + .pipe(debounceTime(500), distinctUntilChanged()) + .subscribe((value) => { + this.globalTextSearch_onChange(value); + }); + } + + ngAfterViewInit(): void { + this.tvsDataSource.paginator = this.paginator; + this.tvsDataSource.sort = this.sort; + this.dataSource.subscribe((x) => { + x = x || []; + this.rowSelectionModel.clear(); + this.tvsDataSource.data = []; + this.initSystemField(x); + this.tvsDataSource.data = x; + this.refreshUI(); + }); + + this.tvsDataSource.sort.sortChange.subscribe((sort) => { + this.pagination.pageIndex = 0; + this.onTableEvent.emit({ + event: TableEventType.SortChanged, + sender: sort, + }); + }); + } + + tooltip_onChanged( + column: TableField, + row: any, + elementRef: any, + show: boolean, + ) { + if (column.cellTooltipEnable === true) { + if (show === true && row[column.name]) { + if (this.overlayRef !== null) { + this.closeTooltip(); + } + + const positionStrategy = this.overlayPositionBuilder + .flexibleConnectedTo(elementRef) + .withPositions([ + { + originX: "center", + originY: "top", + overlayX: "center", + overlayY: "bottom", + offsetY: -8, + }, + ]); + + this.overlayRef = this.overlay.create({ positionStrategy }); + + const option = { + providers: [ + { + provide: "tooltipConfig", + useValue: row[column.name], + }, + ], + }; + + const injector = Injector.create(option); + const tooltipRef: ComponentRef = + this.overlayRef.attach( + new ComponentPortal(TooltipComponent, null, injector), + ); + setTimeout(() => { + tooltipRef.destroy(); + }, 5000); + } else if (show === false && this.overlayRef !== null) { + this.closeTooltip(); + } + } + } + + closeTooltip() { + this.overlayRef?.detach(); + this.overlayRef = null; + } + ellipsis(column: TableField, cell = true) { + if (cell === true && column.cellEllipsisRow > 0) { + return { + display: "-webkit-box", + "-webkit-line-clamp": column?.cellEllipsisRow, + "-webkit-box-orient": "vertical", + overflow: "hidden", + "white-space": "pre-wrap", + }; + } else if (cell === true && column.headerEllipsisRow > 0) { + return { + display: "-webkit-box", + "-webkit-line-clamp": column?.headerEllipsisRow, + "-webkit-box-orient": "vertical", + overflow: "hidden", + "white-space": "pre-wrap", + }; + } + + return {}; + } + + indexTrackFn = (index: number) => { + return index; + }; + + trackColumn(index: number, item: TableField): string { + return `${item.index}`; + } + + ngOnDestroy(): void { + if (this.eventsSubscription) { + this.eventsSubscription.unsubscribe(); + } + } + + public refreshUI() { + if (this.tableSetting.autoHeight === true) { + this.height = this.autoHeight(); + } else { + this.height = null; + } + this.refreshColumn(this.tableColumns); + this.tvsDataSource.columns = this.columns; + const scrollStrategy: FixedSizeTableVirtualScrollStrategy = + this.viewport["_scrollStrategy"]; + scrollStrategy?.viewport?.checkViewportSize(); + scrollStrategy?.viewport?.scrollToOffset(0); + this.cdr.detectChanges(); + } + + ngOnInit() { + setTimeout(() => { + this.init = true; + }, 1000); + const scrollStrategy: FixedSizeTableVirtualScrollStrategy = + this.viewport["_scrollStrategy"]; + + scrollStrategy.offsetChange.subscribe((offset) => {}); + this.viewport.renderedRangeStream.subscribe((t) => { + // in expanding row scrolling make not good appearance therefor close it. + if ( + this.expandedElement && + this.expandedElement.option && + this.expandedElement.option.expand + ) { + // this.expandedElement.option.expand = false; + // this.expandedElement = null; + } + }); + } + + public get inverseOfTranslation(): number { + if (!this.viewport || !this.viewport["_renderedContentOffset"]) { + return -0; + } + const offset = this.viewport["_renderedContentOffset"]; + return -offset; + } + + headerClass(column: TableField) { + return column?.classNames; + } + + rowStyle(row) { + let style: any = row?.option?.style || {}; + if (this.setting.alternativeRowStyle && row.id % 2 === 0) { + // style is high priority + style = { ...this.setting.alternativeRowStyle, ...style }; + } + if (this.setting.rowStyle) { + style = { ...this.setting.rowStyle, ...style }; + } + return style; + } + + cellClass(option, column) { + let className = null; + if (option && column.name) { + className = option[column.name] ? option[column.name].style : null; + } + + if (className === null) { + return column.cellClass; + } else { + return { ...className, ...column.cellClass }; + } + } + + cellStyle(option: HashMap, column) { + let style = null; + if (option && column.name) { + style = option[column.name] ? option[column.name].style : null; + } + /* consider to column width resize */ + if (style === null) { + return { ...column.cellStyle, ...column.style }; + } else { + return { ...style, ...column.cellStyle, ...column?.style }; + } + } + + cellIcon(option, cellName) { + if (option && cellName) { + return option[cellName] ? option[cellName].icon : null; + } else { + return null; + } + } + + filter_onChanged(column: TableField, filter: AbstractFilter[]) { + this.tvsDataSource.setFilter(column.name, filter).subscribe(() => { + this.clearSelection(); + }); + } + + onContextMenu(event: MouseEvent, column: TableField, row: any) { + if ( + this.currentContextMenuSender?.time && + new Date().getTime() - this.currentContextMenuSender.time < 500 + ) { + return; + } + this.contextMenu.closeMenu(); + if (this.contextMenuItems?.length === 0) { + return; + } + event.preventDefault(); + this.contextMenuPosition.x = event.clientX + "px"; + this.contextMenuPosition.y = event.clientY + "px"; + this.currentContextMenuSender = { + column: column, + row: row, + time: new Date().getTime(), + }; + this.contextMenu.menuData = this.currentContextMenuSender; + this.contextMenu.menu.focusFirstItem("mouse"); + this.onRowEvent.emit({ + event: RowEventType.BeforeContextMenuOpen, + sender: { row: row, column: column, contextMenu: this.contextMenuItems }, + }); + this.contextMenu.openMenu(); + } + + onContextMenuItemClick(data: ContextMenuItem) { + this.contextMenu.menuData.item = data; + this.onRowEvent.emit({ + event: RowEventType.ContextMenuClick, + sender: this.contextMenu.menuData, + }); + } + + tableMenuActionChange(e: TableMenuActionChange) { + if (e.type === "TableSetting") { + this.settingChange.emit({ type: "apply", setting: this.tableSetting }); + this.refreshColumn(this.tableSetting.columnSetting); + } else if (e.type === "DefaultSetting") { + (this.setting.settingList || []).forEach((setting) => { + if (setting.settingName === e.data) { + setting.isDefaultSetting = true; + } else { + setting.isDefaultSetting = false; + } + }); + this.settingChange.emit({ type: "default", setting: this.tableSetting }); + } else if (e.type === "SaveSetting") { + const newSetting = Object.assign({}, this.setting); + delete newSetting.settingList; + newSetting.settingName = e.data; + const settingIndex = (this.setting.settingList || []).findIndex( + (f) => f.settingName === e.data, + ); + if (settingIndex === -1) { + this.setting.settingList.push(JSON.parse(JSON.stringify(newSetting))); + this.settingChange.emit({ type: "create", setting: this.tableSetting }); + } else { + this.setting.settingList[settingIndex] = JSON.parse( + JSON.stringify(newSetting), + ); + this.settingChange.emit({ type: "save", setting: this.tableSetting }); + } + } else if (e.type === "DeleteSetting") { + this.setting.settingList = this.setting.settingList.filter( + (s) => s.settingName !== e.data.settingName, + ); + this.setting.columnSetting + .filter((f) => f.display === "hidden") + .forEach((f) => (f.display = "visible")); + this.refreshColumn(this.setting.columnSetting); + this.settingChange.emit({ type: "delete", setting: this.tableSetting }); + } else if (e.type === "SelectSetting") { + if (e.data != null) { + let setting: SettingItem = null; + this.setting.settingList.forEach((s) => { + if (s.settingName === e.data) { + s.isCurrentSetting = true; + setting = Object.assign( + {}, + this.setting.settingList.find((s) => s.settingName === e.data), + ); + } else { + s.isCurrentSetting = false; + } + }); + setting.settingList = this.setting.settingList; + delete setting.isCurrentSetting; + delete setting.isDefaultSetting; + if ( + this.pagingMode !== "none" && + this.pagination.pageSize != setting?.pageSize + ) { + this.pagination.pageSize = + setting?.pageSize || this.pagination.pageSize; + this.paginationChange.emit(this.pagination); + } + /* Dynamic Cell must update when setting change */ + setting.columnSetting?.forEach((column) => { + const originalColumn = this.columns.find( + (c) => c.name === column.name, + ); + column = { ...originalColumn, ...column }; + }); + this.tableSetting = setting; + this.refreshColumn(this.setting.columnSetting); + this.settingChange.emit({ type: "select", setting: this.tableSetting }); + } else { + const columns = []; + this.columns.forEach((c) => { + columns.push(Object.assign({}, c)); + }); + this.refreshColumn(columns); + this.refreshUI(); + } + } else if (e.type === "FullScreenMode") { + requestFullscreen(this.tbl.elementRef); + } else if (e.type === "Download") { + this.onTableEvent.emit({ + event: TableEventType.ExportData, + sender: { + type: e.data, + columns: this.columns, + data: this.tvsDataSource.filteredData, + dataSelection: this.rowSelectionModel, + }, + }); + if (e.data === "CSV") { + this.tableService.exportToCsv( + this.columns, + this.tvsDataSource.filteredData, + this.rowSelectionModel, + ); + } else if (e.data === "JSON") { + this.tableService.exportToJson( + this.tvsDataSource.filteredData, + this.rowSelectionModel, + ); + } + } else if (e.type === "FilterClear") { + this.tvsDataSource.clearFilter(); + this.headerFilterList.forEach((hf) => hf.clearColumn_OnClick()); + } else if (e.type === "Print") { + this.onTableEvent.emit({ + event: TableEventType.ExportData, + sender: { + type: "Print", + columns: this.columns, + data: this.tvsDataSource.filteredData, + dataSelection: this.rowSelectionModel, + }, + }); + this.printConfig.title = this.printConfig.title || this.tableName; + this.printConfig.direction = this.tableSetting.direction || "ltr"; + this.printConfig.columns = this.tableColumns.filter( + (t) => t.display !== "hidden" && t.printable !== false, + ); + this.printConfig.displayedFields = this.printConfig.columns.map( + (o) => o.name, + ); + this.printConfig.data = this.tvsDataSource.filteredData; + const params = this.tvsDataSource.toTranslate(); + this.printConfig.tablePrintParameters = []; + params.forEach((item) => { + this.printConfig.tablePrintParameters.push(item); + }); + + this.dialog.open(PrintTableDialogComponent, { + width: "90vw", + data: this.printConfig, + }); + } + } + + rowMenuActionChange(contextMenuItem: ContextMenuItem, row: any) { + this.onRowEvent.emit({ + event: RowEventType.RowActionMenu, + sender: { row: row, action: contextMenuItem }, + }); + } + + pagination_onChange(e: PageEvent) { + if (this.pagingMode !== "none") { + this.tvsDataSource.refreshFilterPredicate(); + this.pagination.length = e.length; + this.pagination.pageIndex = e.pageIndex; + this.pagination.pageSize = e.pageSize; + this.setting.pageSize = + e.pageSize; /* Save Page Size when need in setting config */ + this.paginationChange.emit(this.pagination); + } + } + + globalTextSearch_onChange(newValue: string) { + if (this.showGlobalTextSearch) { + this.globalTextSearch = newValue; + this.globalTextSearchChange.emit(this.globalTextSearch); + } + } + + autoHeight() { + const minHeight = + this.headerHeight + + (this.rowHeight + 1) * this.dataSource.value.length + + this.footerHeight * 0; + return minHeight.toString(); + } + + reload_onClick() { + this.onTableEvent.emit({ sender: null, event: TableEventType.ReloadData }); + } + + ///////////////////////////////////////////////////////////////// + + onResizeColumn(event: MouseEvent, index: number, type: "left" | "right") { + this.resizeColumn.resizeHandler = type; + this.resizeColumn.startX = event.pageX; + if (this.resizeColumn.resizeHandler === "right") { + this.resizeColumn.startWidth = ( + event.target as Node + ).parentElement.clientWidth; + this.resizeColumn.columnIndex = index; + } else if ( + (event.target as Node).parentElement.previousElementSibling === null + ) { + /* for first column not resize */ + return; + } else { + this.resizeColumn.startWidth = ( + event.target as Node + ).parentElement.previousElementSibling.clientWidth; + this.resizeColumn.columnIndex = index; + } + event.preventDefault(); + this.mouseMove(index); + } + + mouseMove(index: number) { + this.resizableMousemove = this.renderer.listen( + "document", + "mousemove", + (event) => { + if (this.resizeColumn.resizeHandler !== null && event.buttons) { + const rtl = this.direction === "rtl" ? -1 : 1; + let width = 0; + if (this.resizeColumn.resizeHandler === "right") { + const dx = event.pageX - this.resizeColumn.startX; + width = this.resizeColumn.startWidth + rtl * dx; + } else { + const dx = this.resizeColumn.startX - event.pageX; + width = this.resizeColumn.startWidth - rtl * dx; + } + if ( + this.resizeColumn.columnIndex === index && + width > this.minWidth + ) { + this.resizeColumn.widthUpdate.next({ + e: this.resizeColumn, + w: width, + }); + } + } + }, + ); + this.resizableMouseup = this.renderer.listen( + "document", + "mouseup", + (event) => { + if (this.resizeColumn.resizeHandler !== null) { + this.resizeColumn.resizeHandler = null; + this.resizeColumn.columnIndex = -1; + /* fix issue sticky column */ + this.table.updateStickyColumnStyles(); + /* Remove Event Listen */ + this.resizableMousemove(); + } + }, + ); + } + + public expandRow(rowIndex: number, mode = true) { + if (rowIndex === null || rowIndex === undefined) { + throw "Row index is not defined."; + } + if (this.expandedElement === this.tvsDataSource.allData[rowIndex]) { + this.expandedElement.option.expand = mode; + this.expandedElement = + this.expandedElement === this.tvsDataSource.allData[rowIndex] + ? null + : this.tvsDataSource.allData[rowIndex]; + } else { + if ( + this.expandedElement && + this.expandedElement !== this.tvsDataSource.allData[rowIndex] + ) { + this.expandedElement.option.expand = false; + } + this.expandedElement = null; + if (mode === true) { + this.expandedElement = + this.expandedElement === this.tvsDataSource.allData[rowIndex] + ? null + : this.tvsDataSource.allData[rowIndex]; + if ( + this.expandedElement.option === undefined || + this.expandedElement.option === null + ) { + this.expandedElement.option = { expand: false }; + } + this.expandedElement.option.expand = true; + } + } + } + + onRowSelection(e, row, column: TableField) { + if ( + this.rowSelectionMode && + this.rowSelectionMode !== "none" && + column.rowSelectable !== false + ) { + this.onRowSelectionChange(e, row); + } + } + + onCellClick(e, row, column: TableField) { + if (column.cellTooltipEnable === true) { + this.closeTooltip(); /* Fixed BUG: Open Overlay when redirect to other route */ + } + if ( + column.clickable !== false && + (column.clickType === null || column.clickType === "cell") + ) { + this.onRowEvent.emit({ + event: RowEventType.CellClick, + sender: { row: row, column: column }, + }); + } + } + + onLabelClick(e, row, column: TableField) { + if (column.clickable !== false && column.clickType === "label") { + this.onRowEvent.emit({ + event: RowEventType.LabelClick, + sender: { row: row, column: column, e: e }, + }); + } + } + + onRowDblClick(e, row) { + this.onRowEvent.emit({ + event: RowEventType.DoubleClick, + sender: { row: row, e: e }, + }); + } + + onRowClick(e, row) { + this.onRowEvent.emit({ + event: RowEventType.RowClick, + sender: { row: row, e: e }, + }); + } + + /************************************ Drag & Drop Column *******************************************/ + + dragStarted(event: Event) {} + + dropListDropped(event: CdkDragDrop) { + if (event) { + this.dragDropData.dropColumnIndex = event.currentIndex; + this.moveColumn(event.previousIndex, event.currentIndex); + } + } + + drop(event: CdkDragDrop) { + moveItemInArray( + event.container.data, + event.previousIndex, + event.currentIndex, + ); + } + /************************************ *******************************************/ + + copyProperty(from: any, to: any) { + const keys = Object.keys(from); + keys.forEach((key) => { + if (from[key] !== undefined && from[key] === null) { + to[key] = Array.isArray(from[key]) + ? Object.assign([], from[key]) + : Object.assign({}, from[key]); + } + }); + } +} diff --git a/src/app/shared/modules/dynamic-material-table/table/dynamic-mat-table.module.ts b/src/app/shared/modules/dynamic-material-table/table/dynamic-mat-table.module.ts new file mode 100644 index 000000000..66424ba64 --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/table/dynamic-mat-table.module.ts @@ -0,0 +1,89 @@ +import { NgModule, ModuleWithProviders } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { MatIconModule } from "@angular/material/icon"; +import { MatSortModule } from "@angular/material/sort"; +import { DragDropModule } from "@angular/cdk/drag-drop"; +import { MatTableModule } from "@angular/material/table"; +import { ScrollingModule } from "@angular/cdk/scrolling"; +import { MatInputModule } from "@angular/material/input"; +import { MatButtonModule } from "@angular/material/button"; +import { MatDialogModule } from "@angular/material/dialog"; +import { MatCheckboxModule } from "@angular/material/checkbox"; +import { MatFormFieldModule } from "@angular/material/form-field"; +import { MatProgressBarModule } from "@angular/material/progress-bar"; +import { MatPaginatorModule } from "@angular/material/paginator"; +import { MatDividerModule } from "@angular/material/divider"; +import { TableCoreDirective } from "../cores/table.core.directive"; +import { RowMenuModule } from "./extensions/row-menu/row-menu.module"; +import { + DynamicCellDirective, + DynamicMatTableComponent, +} from "./dynamic-mat-table.component"; +import { TableMenuModule } from "./extensions/table-menu/table-menu.module"; +import { HeaderFilterModule } from "./extensions/filter/header-filter.module"; +import { TableVirtualScrollModule } from "../cores/table-virtual-scroll.module"; +import { PrintTableDialogComponent } from "./extensions/print-dialog/print-dialog.component"; +import { MatMenuModule } from "@angular/material/menu"; +import { MatTooltipModule } from "@angular/material/tooltip"; +import { MatRippleModule } from "@angular/material/core"; +import { TooltipComponent } from "../tooltip/tooltip.component"; +import { OverlayModule } from "@angular/cdk/overlay"; +import { TooltipDirective } from "../tooltip/tooltip.directive"; +import { TemplateOrStringDirective } from "../tooltip/template-or-string.directive"; +import { FormsModule } from "@angular/forms"; +import { TableSetting } from "../models/table-setting.model"; + +// eslint-disable-next-line @typescript-eslint/naming-convention +const ExtensionsModule = [HeaderFilterModule, RowMenuModule]; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + MatTableModule, + ScrollingModule, + TableVirtualScrollModule, + MatCheckboxModule, + MatFormFieldModule, + MatInputModule, + MatSortModule, + MatProgressBarModule, + MatIconModule, + DragDropModule, + TableMenuModule, + MatPaginatorModule, + MatDialogModule, + MatButtonModule, + MatMenuModule, + MatDividerModule, + MatTooltipModule, + MatRippleModule, + OverlayModule, + ExtensionsModule, + ], + exports: [DynamicMatTableComponent], + declarations: [ + DynamicMatTableComponent, + PrintTableDialogComponent, + TableCoreDirective, + DynamicCellDirective, + TooltipComponent, + TooltipDirective, + TemplateOrStringDirective, + ], +}) +export class DynamicMatTableModule { + static forRoot( + config: TableSetting, + ): ModuleWithProviders { + return { + ngModule: DynamicMatTableModule, + providers: [ + { + provide: TableSetting, + useValue: config, + }, + ], + }; + } +} diff --git a/src/app/shared/modules/dynamic-material-table/table/dynamic-mat-table.service.ts b/src/app/shared/modules/dynamic-material-table/table/dynamic-mat-table.service.ts new file mode 100644 index 000000000..30541c1e3 --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/table/dynamic-mat-table.service.ts @@ -0,0 +1,150 @@ +import { Injectable } from "@angular/core"; +import { TableField } from "../models/table-field.model"; +import { SelectionModel } from "@angular/cdk/collections"; +import { TableRow } from "../models/table-row.model"; +@Injectable({ + providedIn: "root", +}) +export class TableService { + /************************************* Local Export *****************************************/ + static getFormattedTime() { + const today = new Date(); + const y = today.getFullYear(); + const m = today.getMonth() + 1; + const d = today.getDate(); + const h = today.getHours(); + const mi = today.getMinutes(); + const s = today.getSeconds(); + return y + "-" + m + "-" + d + "-" + h + "-" + mi + "-" + s; + } + + public tableName: string; + + constructor() {} + + private downloadBlob(blob: Blob | any, filename: string) { + if ((navigator as any).msSaveBlob) { + // IE 10+ + (navigator as any).msSaveBlob(blob, filename); + } else { + const link = document.createElement("a"); + if (link.download !== undefined) { + // Browsers that support HTML5 download attribute + const url = URL.createObjectURL(blob); + link.setAttribute("href", url); + link.setAttribute("download", filename); + link.style.visibility = "hidden"; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + } + } + + public exportToCsv( + columns: TableField[], + rows: object[], + selectionModel: SelectionModel, + filename = "", + ) { + const dataToExport = selectionModel.selected.length + ? selectionModel.selected + : rows; + + filename = + filename === "" + ? this.tableName + TableService.getFormattedTime() + ".csv" + : filename; + if (!dataToExport || !dataToExport.length) { + return; + } + const fields = columns.filter( + (c) => c.exportable !== false && c.display !== "hidden", + ); + const separator = ","; + const CR_LF = "\n"; //'\u0D0A'; + const keys = fields.map((f) => f.name); + const headers = fields.map((f) => f.header); + const csvContent = + headers.join(separator) + + CR_LF + + dataToExport + .map((row) => { + return fields + .map((f) => { + let cell = f.toExport(row, "csv") || ""; + cell = + cell instanceof Date + ? cell.toLocaleString() + : cell.toString().replace(/"/g, '""'); + if (cell.search(/("|,|\n)/g) >= 0) { + cell = `"${cell}"`; + } + return cell; + }) + .join(separator); + }) + .join(CR_LF); + + const blob = new Blob( + [new Uint8Array([0xef, 0xbb, 0xbf]) /* UTF-8 BOM */, csvContent], + { type: "text/csv;charset=utf-8" }, + ); + this.downloadBlob(blob, filename); + } + + public exportToJson( + rows: object[], + selectionModel: SelectionModel, + filename = "", + ) { + const dataToExport = selectionModel.selected.length + ? selectionModel.selected + : rows; + + filename = + filename === "" + ? this.tableName + TableService.getFormattedTime() + ".json" + : filename; + const blob = new Blob([JSON.stringify(dataToExport)], { + type: "text/csv;charset=utf-8;", + }); + this.downloadBlob(blob, filename); + } + + /************************************* Save Setting into storage *****************************************/ + public loadSavedColumnInfo( + columnInfo: TableField[], + saveName?: string, + ): TableField[] { + // Only load if a save name is passed in + if (saveName) { + if (!localStorage) { + return null; + } + + const loadedInfo = localStorage.getItem(`${saveName}-columns`); + + if (loadedInfo) { + return JSON.parse(loadedInfo); + } + this.saveColumnInfo(columnInfo); + return columnInfo; + } + + return null; + } + + public saveColumnInfo( + columnInfo: TableField[], + saveName: string = this.tableName, + ): void { + if (saveName) { + if (!localStorage) { + return; + } + + localStorage.setItem(`${saveName}-columns`, JSON.stringify(columnInfo)); + } + } +} diff --git a/src/app/shared/modules/dynamic-material-table/table/dynamic-mat-table.style.scss b/src/app/shared/modules/dynamic-material-table/table/dynamic-mat-table.style.scss new file mode 100644 index 000000000..11acd29db --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/table/dynamic-mat-table.style.scss @@ -0,0 +1,214 @@ +@mixin header-icon-base { + color: rgba(0, 0, 0, 0.3); + display: flex; + opacity: 0; + transform: translateY(-5px); + cursor: pointer; + + transition-duration: 0.4s; + transition-property: opacity, transform; +} + +@mixin table-base { + .mat-mdc-table { + .row-selection { + background-color: #f7f5f5; + } + + .mat-mdc-row:hover { + background-color: #fafafa; + } + + mat-cell { + box-sizing: border-box; + } + } +} + +@mixin progress { + .mat-mdc-header-row.progress { + border: none; + max-height: 4px; + min-height: 4px; + height: 0; + margin-top: -4px; + background-color: transparent !important; + border-top: transparent !important; + background: transparent !important; + + .mat-mdc-header-cell { + border: 0; + padding: 0; + z-index: 1; + } + + mat-progress-bar { + transition: + height 0.3s, + opacity 0.25s linear; + + &:not(.show) { + height: 0; + opacity: 0; + } + } + } +} + +@mixin no-records { + .no-records { + display: flex; + align-items: center; + top: 50%; + left: 50%; + margin: -42px 0 0 -25px; + line-height: 42px; + position: absolute; + z-index: 1; + pointer-events: none; + + button { + pointer-events: initial; + } + } +} + +@mixin dmf-class { + // remove botton padding + ::ng-deep .dmf { + min-width: 100%; + } + + ::ng-deep + dynamic-mat-table + cdk-virtual-scroll-viewport + .cdk-virtual-scroll-content-wrapper + .mat-mdc-table + mat-row + .mat-mdc-cell + mat-form-field { + max-width: 100%; + + .mat-mdc-form-field-wrapper { + padding-bottom: 0 !important; + } + + ::ng-deep .mat-mdc-form-field-underline { + bottom: 0 !important; + } + } +} + +@mixin resize-column { + mat-header-cell:hover { + .left-resize-handler { + height: 100%; + transition: height 0.4s ease-out; + } + + .right-resize-handler { + height: 100%; + transition: height 0.4s ease-out; + } + } + + .resize-handler { + display: inline-block; + min-width: 1px; + height: 0; + position: sticky; + cursor: col-resize; + border-width: 0; + z-index: 10; + } + + .left-resize-handler { + left: 0; + padding-right: 10px; + margin-right: -10px; + border-left: solid 2px #8b8b8b; + } + + .right-resize-handler { + right: 0px; + padding-left: 10px; + margin-left: -10px; + border-right: solid 2px #8b8b8b; + } + + .active-resize { + background-color: #f5f5f566; + } +} + +@mixin header-sort { + .mat-mdc-sort-header-arrow { + margin: 0 6px !important; + } +} + +@mixin context-menu { + .ltr-menu { + span { + float: left; + } + } + + .button-menu { + width: 100%; + line-height: 48px; + + &::ng-deep .mat-mdc-button-wrapper { + display: flex; + + span { + display: inline-block; + width: 100%; + text-align: left; + } + + mat-icon { + line-height: 48px; + height: 48px; + margin: 0 5px; + } + } + } + + mat-button-wrapper .button-menu { + display: inline-block !important; + } + + .text-align-left { + text-align: left !important; + } + + .text-align-right { + text-align: right !important; + } + + .mat-mdc-menu-panel { + min-height: unset !important; + } +} + +@media print { + .print-preview { + background-color: white; + position: fixed; + width: 100%; + height: auto; + z-index: 99999999; + margin: 0; + padding: 0; + top: 0; + left: 0; + overflow: visible; + display: block; + } +} + +.disable-backdrop-click + .cdk-overlay-backdrop.cdk-overlay-transparent-backdrop.cdk-overlay-backdrop-showing { + pointer-events: none; +} diff --git a/src/app/shared/modules/dynamic-material-table/table/extensions/filter/compare/abstract-filter.ts b/src/app/shared/modules/dynamic-material-table/table/extensions/filter/compare/abstract-filter.ts new file mode 100644 index 000000000..475a3a3ca --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/table/extensions/filter/compare/abstract-filter.ts @@ -0,0 +1,33 @@ +export abstract class AbstractFilter { + /* + type variable is array because in future may be + control have two or more parameters such as ranger[from, to] + */ + public parameters?: [{ value: T; text: string }]; + public type: "and" | "or"; + abstract selectedIndex: number; + abstract readonly selectedValue: FilterOperation; + abstract toString(dynamicVariable: any): string; + abstract toPrint(): string; + abstract toSql(): string; + abstract getOperations(): FilterOperation[]; + public hasValue() { + if (this.parameters !== null) { + return ( + this.parameters.filter( + (p) => + p.value != null && + p.value !== undefined && + p.value.toString() !== "", + ).length > 0 + ); + } + + return false; + } +} + +export interface FilterOperation { + predicate: string; + text: string; +} diff --git a/src/app/shared/modules/dynamic-material-table/table/extensions/filter/compare/number-filter.ts b/src/app/shared/modules/dynamic-material-table/table/extensions/filter/compare/number-filter.ts new file mode 100644 index 000000000..7e70e2ba2 --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/table/extensions/filter/compare/number-filter.ts @@ -0,0 +1,103 @@ +import { AbstractFilter, FilterOperation } from "./abstract-filter"; + +const equals = "a === b"; +const notEquals = "a !== b"; +const greaterThan = "a > b"; +const lessThan = "a < b"; +const empty = "!a"; +const notEmpty = "!!a"; +const operations = [equals, notEquals, greaterThan, lessThan, empty, notEmpty]; + +export class NumberFilter extends AbstractFilter { + private static sql = ["=", "<>", ">", "<", "IS NULL", "IS NOT NULL"]; + private static operationList: FilterOperation[] = []; + private _selectedIndex: number = null; + + constructor() { + super(); + if (NumberFilter.operationList.length === 0) { + operations.forEach((fn) => { + NumberFilter.operationList.push({ predicate: fn, text: null }); + }); + } + NumberFilter.operationList[0].text = "equals"; // equals // + NumberFilter.operationList[1].text = "notEquals"; // notEquals // + NumberFilter.operationList[2].text = "greaterThan"; // greaterThan // + NumberFilter.operationList[3].text = "lessThan"; // lessThan // + NumberFilter.operationList[4].text = "empty"; // empty // + NumberFilter.operationList[5].text = "notEmpty"; // notEmpty // + } + + get selectedIndex(): number { + return this._selectedIndex; + } + set selectedIndex(value: number) { + this._selectedIndex = value; + // init filter parameters + if (value === 0 || value === 1 || value === 2 || value === 3) { + // equals notEquals greaterThan lessThan + this.parameters = [{ value: null, text: "number" }]; + } else { + // empty notEmpty + this.parameters = null; + } + } + + get selectedValue(): FilterOperation { + if (this._selectedIndex !== null) { + return NumberFilter.operationList[this._selectedIndex]; + } else { + return null; + } + } + + public getOperations(): FilterOperation[] { + return NumberFilter.operationList; + } + + public toString(dynamicVariable: any): string { + const a = "_a$"; + const b = "_b$"; + const predicate = this.selectedValue.predicate + .replace("a", a) + .replace("b", b); + const statement = predicate.replace(a, `${a}['${dynamicVariable}']`); + // one static variable (equals, notEquals,greaterThan,lessThan) + if ( + this._selectedIndex === 0 || + this._selectedIndex === 1 || + this._selectedIndex === 2 || + this._selectedIndex === 3 + ) { + const value = this.parameters[0].value + ? this.parameters[0].value.toString() + : " null "; + return statement.replace(b, value); + } else { + // none static variable (empty, notEmpty) + return statement; + } + } + + public toPrint(): string { + return ( + NumberFilter.operationList[this._selectedIndex].text + + " " + + this.parameters[0].value + + " " + + (this.type || "") + + " " + ); + } + + public toSql(): string { + return ( + NumberFilter.sql[this._selectedIndex] + + " " + + (this.parameters[0].value || "") + + " " + + (this.type || "") + + " " + ); + } +} diff --git a/src/app/shared/modules/dynamic-material-table/table/extensions/filter/compare/text-filter.ts b/src/app/shared/modules/dynamic-material-table/table/extensions/filter/compare/text-filter.ts new file mode 100644 index 000000000..5653ed7ff --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/table/extensions/filter/compare/text-filter.ts @@ -0,0 +1,118 @@ +import { AbstractFilter, FilterOperation } from "./abstract-filter"; + +const contains = "a.toString().includes(b)"; +const equals = "a.toString() === b.toString()"; +const startsWith = "a.toString().startsWith(b)"; +const endsWith = "a.toString().endsWith(b.toString())"; +const empty = "!a"; +const notEmpty = "!!a"; +const operations = [contains, equals, startsWith, endsWith, empty, notEmpty]; + +export class TextFilter extends AbstractFilter { + private static sql = [ + 'LIKE "%[*]%"', + '= "[*]"', + 'LIKE "%[*]"', + 'LIKE "[*]%"', + "IS NULL", + "IS NOT NULL", + ]; + private static operationList: FilterOperation[] = []; + private _selectedIndex: number = null; + + constructor() { + super(); + this._selectedIndex = 0; + if (TextFilter.operationList.length === 0) { + // init for first time + operations.forEach((fn) => { + TextFilter.operationList.push({ predicate: fn, text: null }); + }); + } + TextFilter.operationList[0].text = "contains"; // contains // + TextFilter.operationList[1].text = "equals"; // equals // + TextFilter.operationList[2].text = "startsWith"; // startsWith // + TextFilter.operationList[3].text = "endsWith"; // endsWith // + TextFilter.operationList[4].text = "empty"; // empty // + TextFilter.operationList[5].text = "notEmpty"; // notEmpty // + } + + get selectedIndex(): number { + return this._selectedIndex; + } + set selectedIndex(value: number) { + this._selectedIndex = value; + // init filter parameters + if (value === 0 || value === 1 || value === 2 || value === 3) { + // contains equals startsWith endsWith + this.parameters = [{ value: "", text: "Text" }]; + } else { + // empty notEmpty + this.parameters = null; + } + } + + get selectedValue(): FilterOperation { + if (this._selectedIndex !== null) { + return TextFilter.operationList[this._selectedIndex]; + } else { + return null; + } + } + + public getOperations(): FilterOperation[] { + return TextFilter.operationList; + } + + public toString(dynamicVariable: any): string { + const a = "_a$"; + const b = "_b$"; + const predicate = this.selectedValue.predicate + .replace("a", a) + .replace("b", b); + const statement = predicate.replace( + a, + `${a}['${dynamicVariable}']?.toString()?.toLowerCase()`, + ); + // one static parameters equals notEquals greaterThan lessThan // + if ( + this._selectedIndex === 0 || + this._selectedIndex === 1 || + this._selectedIndex === 2 || + this._selectedIndex === 3 + ) { + const value = + "'" + + (this.parameters[0].value !== null + ? this.parameters[0].value.toLowerCase() + : " null ") + + "'"; + return statement.replace("_b$", value); + } else { + // without static parameters + return statement; + } + } + + public toPrint(): string { + return ( + TextFilter.operationList[this._selectedIndex].text + + " " + + this.parameters[0].value + + " " + + (this.type || "") + + " " + ); + } + + public toSql(): string { + return ( + TextFilter.sql[this._selectedIndex].replace( + "[*]", + this.parameters[0].value || "", + ) + + (this.type || "") + + " " + ); + } +} diff --git a/src/app/shared/modules/dynamic-material-table/table/extensions/filter/filter-event.directive.ts b/src/app/shared/modules/dynamic-material-table/table/extensions/filter/filter-event.directive.ts new file mode 100644 index 000000000..64e32bcb6 --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/table/extensions/filter/filter-event.directive.ts @@ -0,0 +1,14 @@ +import { Directive, HostListener } from "@angular/core"; + +@Directive({ + selector: "[appFilterEvent]", +}) +export class FilterEventDirective { + @HostListener("click", ["$event"]) + onClick(e: MouseEvent) { + e.stopPropagation(); + e.preventDefault(); + + return false; + } +} diff --git a/src/app/shared/modules/dynamic-material-table/table/extensions/filter/header-filter.component.html b/src/app/shared/modules/dynamic-material-table/table/extensions/filter/header-filter.component.html new file mode 100644 index 000000000..255382f0d --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/table/extensions/filter/header-filter.component.html @@ -0,0 +1,93 @@ + + + + + +
+ + + + {{ op.text }} + + + + +
+ + {{ ctrl.text }} + + +
+ +
+ {{ filter?.type === "and" ? "and" : "or" }} + + add + + + drag_handle + + + clear + +
+
+ + +
+
+ + + filter_list + diff --git a/src/app/shared/modules/dynamic-material-table/table/extensions/filter/header-filter.component.scss b/src/app/shared/modules/dynamic-material-table/table/extensions/filter/header-filter.component.scss new file mode 100644 index 000000000..f6c0d2a40 --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/table/extensions/filter/header-filter.component.scss @@ -0,0 +1,128 @@ +@import "../../dynamic-mat-table.style.scss"; + +:host { + display: flex; + align-items: center; + width: 100%; + align-self: stretch; +} + +.trigger { + @include header-icon-base; + position: sticky; + right: 0px; + z-index: 1; + padding-left: 8px; + padding-right: 8px; + :host.has-value & { + opacity: 1; + color: rgba(0, 0, 0, 0.54); + } + :host:hover &, + :host.show-trigger & { + opacity: 1; + transform: translateY(-1px); + } +} + +::ng-deep.mat-mdc-menu-content:not(:empty) { + padding: 0 !important; +} + +.mat-mdc-menu-item-highlighted:not([disabled]), +.mat-mdc-menu-item.cdk-keyboard-focused:not([disabled]), +.mat-mdc-menu-item.cdk-program-focused:not([disabled]), +.mat-mdc-menu-item:hover:not([disabled]) { + background-color: inherit; +} + +.menu-title { + font-weight: bolder; + top: -8px; + position: sticky; + background-color: white; + z-index: 1; +} + +.menu-action { + position: sticky; + bottom: -8px; + padding-top: 10px; + padding-bottom: 0px; + background-color: white; + button { + width: calc(50% - 10px); + margin: 5px; + border-radius: 10px; + } +} + +.filter-panel { + border-radius: 5px; + background-color: #fdfbfb; + border: solid 1px #efefef; + transition: all 0.5s; + padding: 5px; + overflow: hidden; + font-size: 14px; + margin-top: 10px; + display: flex; + flex-direction: column; +} + +.filter-panel:nth-child(2) { + margin-top: 0 !important; +} + +.filter-panel:hover { + border: solid 1px #d1d1d1; + .svg { + opacity: 1; + transform: translateY(-1px); + } +} + +.or-and { + display: inherit !important; + text-align: right; + height: 35px; + cursor: inherit; + font-size: 12px; +} + +.svg { + @include header-icon-base; + margin-left: 5px; + padding: 2px; + border-radius: 5px; + color: rgb(76, 76, 76); + cursor: pointer; + display: inline-block !important; + height: 24px; + mat-icon { + margin: 0; + vertical-align: top; + border-radius: 5px; + } + mat-icon:hover { + color: white; + background-color: #89898a; + } +} +.svg:hover { + background-color: rgb(248, 248, 248); +} + +.selected-filter-type { + float: left; + color: #ffffff; + background-color: #89898a; + border-radius: 5px; + padding: 0 4px 0px 4px; + line-height: 24px; +} + +::ng-deep .menu { + padding: 8px; + user-select: none; +} diff --git a/src/app/shared/modules/dynamic-material-table/table/extensions/filter/header-filter.component.ts b/src/app/shared/modules/dynamic-material-table/table/extensions/filter/header-filter.component.ts new file mode 100644 index 000000000..56caacf9c --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/table/extensions/filter/header-filter.component.ts @@ -0,0 +1,188 @@ +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + HostBinding, + Output, + ViewChild, + Input, + EventEmitter, + OnInit, + ChangeDetectorRef, + QueryList, + OnDestroy, + ContentChildren, +} from "@angular/core"; +import { TableField } from "./../../../models/table-field.model"; +import { TableService } from "../../dynamic-mat-table.service"; +import { TextFilter } from "./compare/text-filter"; +import { NumberFilter } from "./compare/number-filter"; +import { AbstractFilter } from "./compare/abstract-filter"; +import { + transition, + trigger, + query, + style, + stagger, + animate, +} from "@angular/animations"; +import { MatInput } from "@angular/material/input"; +import { MatMenuTrigger } from "@angular/material/menu"; +import { isNullorUndefined } from "../../../cores/type"; +import { Subscription } from "rxjs"; + +export enum FilterAction { + And = 0, + Or = 1, + Delete = 2, +} + +const listAnimation = trigger("listAnimation", [ + transition("* <=> *", [ + query( + ":enter", + [ + style({ opacity: 0 }), + stagger("10ms", animate("400ms ease-out", style({ opacity: 1 }))), + ], + { optional: true }, + ), + ]), +]); + +@Component({ + // tslint:disable-next-line:component-selector + selector: "header-filter", + templateUrl: "./header-filter.component.html", + styleUrls: ["./header-filter.component.scss"], + changeDetection: ChangeDetectionStrategy.OnPush, + animations: [listAnimation], +}) +export class HeaderFilterComponent implements OnInit, AfterViewInit, OnDestroy { + private filterList: AbstractFilter[] = []; + private eventsSubscription: Subscription; + + @Input() field?: TableField; + @Output() filterChanged: EventEmitter = new EventEmitter< + AbstractFilter[] + >(); + + @ContentChildren("filterInput") filterInputList!: QueryList; + @ViewChild(MatMenuTrigger, { static: true }) menu!: MatMenuTrigger; + + @Input() + get filters(): AbstractFilter[] { + if ( + isNullorUndefined(this.filterList) === true || + this.filterList.length === 0 + ) { + this.filterList = []; + this.addNewFilter(this.field.type || "text"); + } + return this.filterList; + } + set filters(values: AbstractFilter[]) { + this.filterList = values; + } + + @HostBinding("class.has-value") + get hasValue(): boolean { + return ( + this.filterList && + this.filterList.filter((f) => f.hasValue() === true).length > 0 + ); + } + + @HostBinding("class.show-trigger") + get showTrigger(): boolean { + if (this.menu === undefined) { + return false; + } else { + return this.menu.menuOpen || this.hasValue; + } + } + + constructor( + public service: TableService, + private cdr: ChangeDetectorRef, + ) {} + + ngOnDestroy(): void { + if (this.eventsSubscription) { + this.eventsSubscription.unsubscribe(); + } + } + + ngOnInit(): void { + if (isNullorUndefined(this.filters)) { + this.filters = []; + this.addNewFilter(this.field.type); + } + } + + addNewFilter(type = "text") { + switch (type || "text") { + case "text": { + this.filterList.push(new TextFilter()); + break; + } + case "number": { + this.filterList.push(new NumberFilter()); + break; + } + case "date": { + // this.compare = new DateCompare(service); + break; + } + case "boolean": { + // this.compare = new BooleanCompare(service); + break; + } + default: + this.filterList.push(new TextFilter()); + } + this.filters[this.filters.length - 1].selectedIndex = 0; + return this.filters[this.filters.length - 1]; + } + + ngAfterViewInit() { + if (this.menu) { + this.eventsSubscription = this.menu.menuOpened.subscribe(() => + this.focusToLastInput(), + ); + } + } + + focusToLastInput() { + if (this.filterInputList.length > 0) { + this.filterInputList.last.focus(); + } + } + + filterAction_OnClick(event, index, action: FilterAction) { + event.stopPropagation(); + event.preventDefault(); + if (action === FilterAction.And || action === FilterAction.Or) { + // and or + this.filters[index].type = action === FilterAction.And ? "and" : "or"; + if (this.filters.length === index + 1) { + this.addNewFilter(this.field.type); + this.focusToLastInput(); + } + } else if (action === FilterAction.Delete && this.filters.length > 1) { + // delete + this.filters.splice(index, 1); + this.cdr.detectChanges(); + this.focusToLastInput(); + } + } + + clearColumn_OnClick() { + this.filterList = []; + this.filterChanged.emit(this.filterList); + } + + applyFilter_OnClick() { + this.filterChanged.emit(this.filterList); + } +} diff --git a/src/app/shared/modules/dynamic-material-table/table/extensions/filter/header-filter.module.ts b/src/app/shared/modules/dynamic-material-table/table/extensions/filter/header-filter.module.ts new file mode 100644 index 000000000..6fea0c837 --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/table/extensions/filter/header-filter.module.ts @@ -0,0 +1,30 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { ReactiveFormsModule, FormsModule } from "@angular/forms"; +import { HeaderFilterComponent } from "./header-filter.component"; +import { FilterEventDirective } from "./filter-event.directive"; +import { MatFormFieldModule } from "@angular/material/form-field"; +import { MatIconModule } from "@angular/material/icon"; +import { MatInputModule } from "@angular/material/input"; +import { MatMenuModule } from "@angular/material/menu"; +import { MatSelectModule } from "@angular/material/select"; +import { MatButtonModule } from "@angular/material/button"; + +const components = [HeaderFilterComponent, FilterEventDirective]; + +@NgModule({ + declarations: components, + exports: components, + imports: [ + CommonModule, + MatFormFieldModule, + MatIconModule, + MatInputModule, + MatMenuModule, + MatSelectModule, + ReactiveFormsModule, + MatButtonModule, + FormsModule, + ], +}) +export class HeaderFilterModule {} diff --git a/src/app/shared/modules/dynamic-material-table/table/extensions/print-dialog/print-dialog.component.html b/src/app/shared/modules/dynamic-material-table/table/extensions/print-dialog/print-dialog.component.html new file mode 100644 index 000000000..e990d84b3 --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/table/extensions/print-dialog/print-dialog.component.html @@ -0,0 +1,42 @@ + +
+

+ {{ printTable?.title }} +

+
+
+ {{ param.key }} : {{ param.value }} +
+
+ {{ param.key }} : {{ param.value }} +
+
+ + + + + + + + +
+
+ + + + + diff --git a/src/app/shared/modules/dynamic-material-table/table/extensions/print-dialog/print-dialog.component.scss b/src/app/shared/modules/dynamic-material-table/table/extensions/print-dialog/print-dialog.component.scss new file mode 100644 index 000000000..837e72177 --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/table/extensions/print-dialog/print-dialog.component.scss @@ -0,0 +1,28 @@ +#print-section { + text-align: center; + margin: 30px; +} + +h2 { + text-align: center; +} + +.param-list { + width: 100%; + display: inline-block; + border: solid gray; + border-width: 0px 0px 2px 0; + margin-bottom: 10px; + padding-bottom: 10px; +} +.param { + display: inline-block; + margin: 10px; +} + +.print-table { + width: 100%; + th.mat-mdc-header-cell { + font-size: initial; + } +} diff --git a/src/app/shared/modules/dynamic-material-table/table/extensions/print-dialog/print-dialog.component.ts b/src/app/shared/modules/dynamic-material-table/table/extensions/print-dialog/print-dialog.component.ts new file mode 100644 index 000000000..676abc5a9 --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/table/extensions/print-dialog/print-dialog.component.ts @@ -0,0 +1,39 @@ +import { Component, ViewChild, ElementRef, Inject } from "@angular/core"; +import { MatDialogRef, MAT_DIALOG_DATA } from "@angular/material/dialog"; +import { PrintConfig } from "../../../models/print-config.model"; + +const styles = + "body{margin:15px;}table{width:100%;border-collapse:collapse;}h2{text-align:center;}th.mat-mdc-header-cell{text-align:center;}div{text-align:center;margin:30px }tr{border-bottom:1px solid }td,th{padding:10px; text-align: center }.param-list{text-align: left;border:solid gray;border-width: 0px 0px 2px 0;margin-bottom: 10px;padding-bottom: 10px;}.param {display: inline-block;margin: 10px;}"; + +@Component({ + selector: "print-dialog", + templateUrl: "./print-dialog.component.html", + styleUrls: ["./print-dialog.component.scss"], +}) +export class PrintTableDialogComponent { + @ViewChild("printContentRef", { static: true }) printContentRef!: ElementRef; + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public printTable: PrintConfig, + ) {} + + print() { + const dialogConfig = + "width=600,height=700,scrollbars=no,menubar=no,toolbar=no,location=no,status=no,titlebar=no"; + const printDoc = ` + + + + + + ${this.printContentRef.nativeElement.innerHTML} + + + `; + + const popupWinindow = window.open("", "_blank", dialogConfig); + popupWinindow.document.write(printDoc); + popupWinindow.document.close(); + } +} diff --git a/src/app/shared/modules/dynamic-material-table/table/extensions/row-menu/row-menu.component.html b/src/app/shared/modules/dynamic-material-table/table/extensions/row-menu/row-menu.component.html new file mode 100644 index 000000000..e35137570 --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/table/extensions/row-menu/row-menu.component.html @@ -0,0 +1,37 @@ + + + + + + + diff --git a/src/app/shared/modules/dynamic-material-table/table/extensions/row-menu/row-menu.component.scss b/src/app/shared/modules/dynamic-material-table/table/extensions/row-menu/row-menu.component.scss new file mode 100644 index 000000000..21c8f0db7 --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/table/extensions/row-menu/row-menu.component.scss @@ -0,0 +1,9 @@ +@import "../.././dynamic-mat-table.style.scss"; + +@include context-menu; + +:host { + display: flex; + align-items: center; + justify-content: space-between; +} diff --git a/src/app/shared/modules/dynamic-material-table/table/extensions/row-menu/row-menu.component.ts b/src/app/shared/modules/dynamic-material-table/table/extensions/row-menu/row-menu.component.ts new file mode 100644 index 000000000..30002a96d --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/table/extensions/row-menu/row-menu.component.ts @@ -0,0 +1,53 @@ +import { + ChangeDetectionStrategy, + Component, + Output, + Input, + EventEmitter, +} from "@angular/core"; +import { isNullorUndefined } from "../../../cores/type"; +import { ContextMenuItem } from "../../../models/context-menu.model"; +import { TableSetting } from "../../../models/table-setting.model"; + +@Component({ + selector: "row-menu", + templateUrl: "./row-menu.component.html", + styleUrls: ["./row-menu.component.scss"], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class RowMenuComponent { + @Output() rowActionChange: EventEmitter = + new EventEmitter(); + @Input() actionMenus: ContextMenuItem[] = []; + @Input() tableSetting: TableSetting; + @Input() rowActionMenu?: { [key: string]: ContextMenuItem }; + visibleActionMenus: ContextMenuItem[] = []; + + constructor() {} + + menuOnClick(e) { + e.stopPropagation(); + e.preventDefault(); + this.visibleActionMenus = []; + this.actionMenus.forEach((menu) => { + const am: ContextMenuItem = + isNullorUndefined(this.rowActionMenu) || + isNullorUndefined(this.rowActionMenu[menu.name]) + ? menu + : this.rowActionMenu[menu.name]; + if (isNullorUndefined(am.visible) || am.visible) { + this.visibleActionMenus.push({ + name: menu.name, + text: am.text || menu.text, + disabled: am.disabled || menu.disabled, + icon: am.icon || menu.icon, + color: am.color || menu.color, + }); + } + }); + } + + menuButton_OnClick(menu: ContextMenuItem) { + this.rowActionChange.emit(menu); + } +} diff --git a/src/app/shared/modules/dynamic-material-table/table/extensions/row-menu/row-menu.module.ts b/src/app/shared/modules/dynamic-material-table/table/extensions/row-menu/row-menu.module.ts new file mode 100644 index 000000000..6fa17e67c --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/table/extensions/row-menu/row-menu.module.ts @@ -0,0 +1,22 @@ +import { NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { FormsModule } from "@angular/forms"; +import { MatButtonModule } from "@angular/material/button"; +import { MatIconModule } from "@angular/material/icon"; +import { MatMenuModule } from "@angular/material/menu"; +import { RowMenuComponent } from "./row-menu.component"; + +const components = [RowMenuComponent]; + +@NgModule({ + declarations: [components], + exports: components, + imports: [ + CommonModule, + FormsModule, + MatButtonModule, + MatIconModule, + MatMenuModule, + ], +}) +export class RowMenuModule {} diff --git a/src/app/shared/modules/dynamic-material-table/table/extensions/table-menu/table-menu.component.html b/src/app/shared/modules/dynamic-material-table/table/extensions/table-menu/table-menu.component.html new file mode 100644 index 000000000..581cbb588 --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/table/extensions/table-menu/table-menu.component.html @@ -0,0 +1,362 @@ + + + + + + + + + + + + + + + + +
+
+ +
+
+ close + done +
+
+ + + + +
+ + {{ + setting.settingName + }} + save + delete +
+
+ lightbulb + No setting +
+
+ + + + + + + + + + + + + +
+
+ drag_indicator + + {{ column.header }} + + settings +
+ drag_indicator + + {{ column.header }} + +
+
+
+ +
+ + +
+
+ + +
No column
+
+
+ + +
+ +
+ filter_altFilter mode +
+ + Filter local mode + Filter server mode + +
+ + +
+ sortSort mode +
+ + Sort local mode + Sort server mode + +
+ + +
+ printPrint mode +
+ + Print yes mode + + Print no mode + + +
+ + +
+ push_pinPin mode +
+ + Pin none mode + + Pin start mode + + Pin end mode + + +
+
+
diff --git a/src/app/shared/modules/dynamic-material-table/table/extensions/table-menu/table-menu.component.scss b/src/app/shared/modules/dynamic-material-table/table/extensions/table-menu/table-menu.component.scss new file mode 100644 index 000000000..e264ae101 --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/table/extensions/table-menu/table-menu.component.scss @@ -0,0 +1,295 @@ +:host { + display: flex; + align-items: center; + justify-content: space-between; +} + +.ltr-menu { + span { + float: left; + } +} + +.main-menu { + width: 38px !important; + line-height: 24px !important; +} + +.va-mat-button-no-input { + border: none; + background-color: transparent; + outline: none; +} + +.va-mat-table-dragable-container { + min-width: 200px; + padding: 8px 0 8px 0; + user-select: none; + max-height: 100vh; +} + +.va-mat-table-dragable-container:dir(rtl) { + background-color: green !important; +} + +.dragable-row { + mat-checkbox { + width: calc(100% - 54px); + line-height: 28px; + display: inline-flex; + } +} + +.va-mat-table-dragable-container .dragable-row { + background-color: white; + display: flex; + width: 100%; + height: 30px; + font-size: 14px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.column-setting-button { + cursor: pointer !important; + font-size: 24px; + margin-right: 5px; +} + +.new-setting { + text-align: center; + margin-left: 0px; + margin-top: 0px; + align-items: center; + user-select: none; + outline: none; + border: none; + line-height: 48px; + height: 48px; + display: flex; + flex-wrap: nowrap; + width: 100%; + padding: 0 16px; + box-sizing: border-box; + .input-container { + overflow: hidden; + } + input { + line-height: 33px; + background-color: white; + border: none; + padding-left: 5px; + border-radius: 4px; + outline: none; + text-align: center; + direction: ltr; + } +} + +.setting-item { + line-height: 48px; + display: inline-flex; + flex-direction: row; + align-items: center; + width: 100%; + font-size: 14px; + mat-icon { + width: 38px; + height: 38px; + line-height: 38px; + color: #0000008a; + cursor: pointer; + text-align: center; + border-radius: 50%; + &:hover { + background-color: #dbdbdb; + } + } + span { + cursor: pointer; + width: 154px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: list-item; + } +} + +.setting-item-active { + background-color: #fdd78a; +} + +::ng-deep .mat-mdc-menu-panel { + min-height: auto !important; +} + +.save-table-setting { + display: flex; + min-width: 70px; + mat-icon { + width: 32px; + height: 32px; + line-height: 32px; + cursor: pointer; + } +} + +.delete-table-setting { + position: absolute; + cursor: pointer; + line-height: 38px !important; + height: 38px; + width: 38px; + border-radius: 100%; + text-align: center; + pointer-events: none; + margin-top: 5px; + &:hover { + color: white; + background-color: #afafaf; + } +} + +.va-mat-table-dragable-container .dragable-row mat-icon { + line-height: 30px; + opacity: 0.15; + transition: opacity 0.5s; + color: #616161; + cursor: grab; + background-color: white; +} +.va-mat-table-dragable-container .dragable-row:hover mat-icon { + opacity: 1; +} + +.va-mat-table-drag-preview { + direction: ltr; + background-color: rgb(236, 236, 236); + padding: 4px 8px 4px 4px !important; + cursor: grabbing !important; + margin-top: -4px; + margin-left: -4px; + font-size: 14px; + border-radius: 5px; +} +.va-mat-table-drag-preview mat-icon, +.va-mat-table-drag-preview mat-checkbox { + vertical-align: top; +} +.va-mat-table-drag-preview mat-icon { + padding-left: 4px; + color: #616161; +} + +/* Animate items as they're being sorted. */ +.cdk-drop-list-dragging .cdk-drag { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} + +/* Animate an item that has been dropped. */ +.cdk-drag-animating { + transition: transform 300ms cubic-bezier(0, 0, 0.2, 1); +} + +.done-setting { + width: 50% !important; + display: inline-flex; + text-align: center; + height: 42px; + mat-icon { + line-height: 38px !important; + opacity: 0.6; + transition: opacity 0.5s; + color: #616161; + width: 100%; + text-align: center; + margin: 0; + } + mat-icon:hover { + opacity: 1; + } +} + +.column-setting { + font-family: Roboto, "Helvetica Neue", sans-serif; + padding: 10px; + .radio { + width: 100%; + display: flex; + font-size: 12px; + margin-top: -2px; + mat-radio-button { + padding: 5px; + width: 50%; + } + + mat-radio-button:last-child { + margin-right: 10px; + } + } + .column-setting-header { + line-height: 30px; + padding: 5px 5px 0px 5px; + font-size: 14px; + border-top: 1px solid #f3f3f3; + margin-top: 5px; + mat-icon { + opacity: 0.7; + font-size: 22px; + line-height: 30px; + float: right; + color: #616161; + } + } +} + +.column-setting-header:first-child { + border-top: none !important; + padding: 0px 5px 0 !important; + margin-top: -5px !important; +} + +.first-menu-item { + width: 100px; + display: inline-block; + text-align: left; +} + +::ng-deep [dir="rtl"] .mat-mdc-checkbox-inner-container { + margin-left: auto !important; + margin-right: 5px !important; +} + +.mat-mdc-menu-item { + display: inline-flex; + width: 100%; + box-sizing: border-box; + span { + width: 100%; + } + mat-icon { + line-height: 48px !important; + height: 48px; + } +} + +::ng-deep .column-config { + .mat-mdc-checkbox-layout { + width: 100%; + } + .mat-mdc-checkbox-layout .mat-mdc-checkbox-label { + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} + +.column-config-apply { + border-top: 1px solid rgb(231, 231, 231); + position: sticky; + bottom: 0px; + z-index: 2147483647; + background-color: white; +} diff --git a/src/app/shared/modules/dynamic-material-table/table/extensions/table-menu/table-menu.component.ts b/src/app/shared/modules/dynamic-material-table/table/extensions/table-menu/table-menu.component.ts new file mode 100644 index 000000000..e5a4290e9 --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/table/extensions/table-menu/table-menu.component.ts @@ -0,0 +1,199 @@ +import { + ChangeDetectionStrategy, + Component, + Output, + Input, + EventEmitter, + ViewChild, + ElementRef, +} from "@angular/core"; +import { moveItemInArray, CdkDragDrop } from "@angular/cdk/drag-drop"; +import { TableService } from "../../dynamic-mat-table.service"; +import { TableSetting } from "../../../models/table-setting.model"; +import { deepClone, isNullorUndefined } from "../../../cores/type"; +import { AbstractField } from "../../../models/table-field.model"; +import { Direction } from "@angular/cdk/bidi"; + +@Component({ + selector: "table-menu", + templateUrl: "./table-menu.component.html", + styleUrls: ["./table-menu.component.scss"], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TableMenuComponent { + @Output() menuActionChange: EventEmitter = + new EventEmitter(); + + @Input() + get tableSetting(): TableSetting { + return this.currentTableSetting; + } + + set tableSetting(value: TableSetting) { + value.settingList = + value.settingList === undefined ? [] : value.settingList; + this.originalTableSetting = value; + this.reverseDirection = value.direction === "rtl" ? "ltr" : "rtl"; + this.currentTableSetting = value; + } + + get isSaveDataActive(): boolean { + if (!this.tableSetting?.visibleActionMenu) { + return false; + } else { + return ( + this.tableSetting.visibleActionMenu.csv !== false || + this.tableSetting.visibleActionMenu.json !== false || + this.tableSetting.visibleActionMenu.print !== false + ); + } + } + + @Output() tableSettingChange = new EventEmitter(); + @ViewChild("newSetting", { static: false }) newSettingElement: ElementRef; + + newSettingName = ""; + showNewSetting = false; + + currentColumn: number = null; + reverseDirection: "auto" | Direction = "auto"; + originalTableSetting: TableSetting; + currentTableSetting: TableSetting; + + constructor(public tableService: TableService) {} + + screenMode_onClick() { + this.menuActionChange.emit({ + type: "FullScreenMode", + data: this.currentTableSetting, + }); + } + + /***** Column Setting ******/ + columnMenuDropped(event: CdkDragDrop): void { + moveItemInArray( + this.currentTableSetting.columnSetting, + event.item.data.columnIndex, + event.currentIndex, + ); + } + + toggleSelectedColumn(column: AbstractField) { + column.display = column.display === "visible" ? "hidden" : "visible"; + } + + apply_onClick(e) { + e.stopPropagation(); + e.preventDefault(); + this.menuActionChange.emit({ + type: "TableSetting", + data: this.currentTableSetting, + }); + this.tableService.saveColumnInfo(this.currentTableSetting.columnSetting); + } + + setting_onClick(i) { + this.currentColumn = i; + } + + cancel_onClick() { + this.currentTableSetting = deepClone(this.originalTableSetting); + } + + isVisible(visible: boolean) { + return isNullorUndefined(visible) ? true : visible; + } + + /***** Save ********/ + saveSetting_onClick(e, setting) { + e.stopPropagation(); + this.menuActionChange.emit({ + type: "SaveSetting", + data: setting?.settingName, + }); + } + + newSetting_onClick(e) { + this.showNewSetting = true; + this.newSettingName = ""; + window.requestAnimationFrame(() => { + this.newSettingElement.nativeElement.focus(); + }); + e.stopPropagation(); + } + + selectSetting_onClick(e, setting: TableSetting) { + e.stopPropagation(); + this.menuActionChange.emit({ + type: "SelectSetting", + data: setting.settingName, + }); + } + + resetDefault_onClick(e) { + e.stopPropagation(); + this.menuActionChange.emit({ + type: "SelectSetting", + data: null, + }); + } + + default_onClick(e, setting) { + e.stopPropagation(); + this.menuActionChange.emit({ + type: "DefaultSetting", + data: setting.settingName, + }); + } + + applySaveSetting_onClick(e) { + e.stopPropagation(); + this.menuActionChange.emit({ + type: "SaveSetting", + data: this.newSettingName, + }); + this.showNewSetting = false; + } + + cancelSaveSetting_onClick(e) { + e.stopPropagation(); + this.newSettingName = ""; + this.showNewSetting = false; + } + + deleteSetting_onClick(e, setting) { + e.stopPropagation(); + this.menuActionChange.emit({ type: "DeleteSetting", data: setting }); + this.newSettingName = ""; + this.showNewSetting = false; + } + + /***** Filter ********/ + clearFilter_onClick() { + this.menuActionChange.emit({ type: "FilterClear" }); + } + + /******* Save File (JSON, CSV, Print)***********/ + download_onClick(type: string) { + this.menuActionChange.emit({ type: "Download", data: type }); + } + + print_onClick(menu) { + menu._overlayRef._host.parentElement.click(); + this.menuActionChange.emit({ type: "Print", data: null }); + } +} + +export interface TableMenuActionChange { + type: + | "FilterClear" + | "TableSetting" + | "Download" + | "SaveSetting" + | "DeleteSetting" + | "SelectSetting" + | "DefaultSetting" + | "Print" + | "FullScreenMode"; + data?: any; +} diff --git a/src/app/shared/modules/dynamic-material-table/table/extensions/table-menu/table-menu.module.ts b/src/app/shared/modules/dynamic-material-table/table/extensions/table-menu/table-menu.module.ts new file mode 100644 index 000000000..a57b931e1 --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/table/extensions/table-menu/table-menu.module.ts @@ -0,0 +1,30 @@ +import { NgModule } from "@angular/core"; +import { FormsModule } from "@angular/forms"; +import { CommonModule } from "@angular/common"; +import { MatIconModule } from "@angular/material/icon"; +import { MatMenuModule } from "@angular/material/menu"; +import { DragDropModule } from "@angular/cdk/drag-drop"; +import { MatRadioModule } from "@angular/material/radio"; +import { MatButtonModule } from "@angular/material/button"; +import { TableMenuComponent } from "./table-menu.component"; +import { MatCheckboxModule } from "@angular/material/checkbox"; +import { MatDividerModule } from "@angular/material/divider"; + +const components = [TableMenuComponent]; + +@NgModule({ + declarations: [components], + exports: components, + imports: [ + CommonModule, + FormsModule, + MatButtonModule, + MatCheckboxModule, + MatIconModule, + DragDropModule, + MatMenuModule, + MatRadioModule, + MatDividerModule, + ], +}) +export class TableMenuModule {} diff --git a/src/app/shared/modules/dynamic-material-table/tooltip/template-or-string.directive.ts b/src/app/shared/modules/dynamic-material-table/tooltip/template-or-string.directive.ts new file mode 100644 index 000000000..a9b195946 --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/tooltip/template-or-string.directive.ts @@ -0,0 +1,17 @@ +import { Directive, Input, TemplateRef, ViewContainerRef } from "@angular/core"; + +@Directive({ + selector: "[appTemplateOrString]", +}) +export class TemplateOrStringDirective { + @Input() set templateOrString(content: string | TemplateRef) { + const template = content instanceof TemplateRef ? content : this.defaultTpl; + this.vcr.clear(); + this.vcr.createEmbeddedView(template); + } + + constructor( + private defaultTpl: TemplateRef, + private vcr: ViewContainerRef, + ) {} +} diff --git a/src/app/shared/modules/dynamic-material-table/tooltip/tooltip.component.html b/src/app/shared/modules/dynamic-material-table/tooltip/tooltip.component.html new file mode 100644 index 000000000..28917d1bf --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/tooltip/tooltip.component.html @@ -0,0 +1,5 @@ +
+ + {{ content }} + +
diff --git a/src/app/shared/modules/dynamic-material-table/tooltip/tooltip.component.scss b/src/app/shared/modules/dynamic-material-table/tooltip/tooltip.component.scss new file mode 100644 index 000000000..c785acb6a --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/tooltip/tooltip.component.scss @@ -0,0 +1,27 @@ +:host { + display: block; + pointer-events: none; + border: solid 1px #ffffff; + border-radius: 8px; + padding: 5px; + background-color: black; + color: white; +} + +div { + background-color: #292929; + color: white; + padding: 0.5rem 1rem; + border-radius: 4px; +} + +.cell-tooltip { + pointer-events: none; + max-width: 200px; + background-color: #000000b3; + border: solid 1px black; + color: white; + border-radius: 8px; + margin: 0 5px; + font-size: 14px; +} diff --git a/src/app/shared/modules/dynamic-material-table/tooltip/tooltip.component.ts b/src/app/shared/modules/dynamic-material-table/tooltip/tooltip.component.ts new file mode 100644 index 000000000..6303938ca --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/tooltip/tooltip.component.ts @@ -0,0 +1,27 @@ +import { animate, style, transition, trigger } from "@angular/animations"; +import { + ChangeDetectionStrategy, + Component, + HostBinding, + Inject, +} from "@angular/core"; + +@Component({ + selector: "app-tooltip", + templateUrl: "./tooltip.component.html", + changeDetection: ChangeDetectionStrategy.OnPush, + animations: [ + trigger("tooltip", [ + transition(":enter", [ + style({ opacity: 0 }), + animate(300, style({ opacity: 1 })), + ]), + transition(":leave", [animate(300, style({ opacity: 0 }))]), + ]), + ], +}) +export class TooltipComponent { + @HostBinding("class") class = "cell-tooltip"; + + constructor(@Inject("tooltipConfig") public content) {} +} diff --git a/src/app/shared/modules/dynamic-material-table/tooltip/tooltip.directive.ts b/src/app/shared/modules/dynamic-material-table/tooltip/tooltip.directive.ts new file mode 100644 index 000000000..f31723817 --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/tooltip/tooltip.directive.ts @@ -0,0 +1,66 @@ +import { + Directive, + ElementRef, + HostListener, + Input, + OnInit, + TemplateRef, + Injector, + OnDestroy, +} from "@angular/core"; +import { + Overlay, + OverlayPositionBuilder, + OverlayRef, +} from "@angular/cdk/overlay"; +import { ComponentPortal } from "@angular/cdk/portal"; +import { TooltipComponent } from "./tooltip.component"; + +@Directive({ + selector: "[appTooltip]:not([click-to-open])", +}) +export class TooltipDirective implements OnInit, OnDestroy { + private overlayRef: OverlayRef; + @Input("appTooltip") content: string | TemplateRef; + + constructor( + private overlay: Overlay, + private overlayPositionBuilder: OverlayPositionBuilder, + private elementRef: ElementRef, + ) {} + + ngOnDestroy(): void { + this.hide(); + } + + ngOnInit() { + const positionStrategy = this.overlayPositionBuilder + .flexibleConnectedTo(this.elementRef) + .withPositions([ + { + originX: "center", + originY: "top", + overlayX: "center", + overlayY: "bottom", + offsetY: -8, + }, + ]); + + this.overlayRef = this.overlay.create({ positionStrategy }); + } + + @HostListener("mouseenter") + show() { + const injector = Injector.create({ + providers: [{ provide: "tooltipConfig", useValue: this.content }], + }); + this.overlayRef.attach( + new ComponentPortal(TooltipComponent, null, injector), + ); + } + + @HostListener("mouseleave") + hide() { + this.overlayRef.detach(); + } +} diff --git a/src/app/shared/modules/dynamic-material-table/utilizes/html.helper.ts b/src/app/shared/modules/dynamic-material-table/utilizes/html.helper.ts new file mode 100644 index 000000000..64887cdb6 --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/utilizes/html.helper.ts @@ -0,0 +1,13 @@ +import { ElementRef } from "@angular/core"; + +export function requestFullscreen(element: ElementRef) { + if (element.nativeElement.requestFullscreen) { + element.nativeElement.requestFullscreen(); + } else if (element.nativeElement.webkitRequestFullscreen) { + /* Safari */ + element.nativeElement.webkitRequestFullscreen(); + } else if (element.nativeElement.msRequestFullscreen) { + /* IE11 */ + element.nativeElement.msRequestFullscreen(); + } +} diff --git a/src/app/shared/modules/dynamic-material-table/utilizes/utilizes.ts b/src/app/shared/modules/dynamic-material-table/utilizes/utilizes.ts new file mode 100644 index 000000000..8c77f194f --- /dev/null +++ b/src/app/shared/modules/dynamic-material-table/utilizes/utilizes.ts @@ -0,0 +1,30 @@ +/** + * Simplifies a string (trims and lowerCases) + */ +export function simplify(s: string): string { + return `${s}`.trim().toLowerCase(); +} + +/** + * Transforms a camelCase string into a readable text format + * @example textify('helloWorld!') + * // Hello world! + */ +export function textify(text: string) { + return text + .replace(/([A-Z])/g, (char) => ` ${char.toLowerCase()}`) + .replace(/^([a-z])/, (char) => char.toUpperCase()); +} + +/** + * Transforms a text string into a title case text format + * @example titleCase('hello world!') + * // Hello World! + */ +export function titleCase(value: string) { + const sentence = value.toLowerCase().split(" "); + for (let i = 0; i < sentence.length; i++) { + sentence[i] = sentence[i][0].toUpperCase() + sentence[i].slice(1); + } + return sentence.join(" "); +} diff --git a/src/app/shared/modules/shared-table/_shared-table-theme.scss b/src/app/shared/modules/shared-table/_shared-table-theme.scss index 4124c88a0..286fa43e1 100644 --- a/src/app/shared/modules/shared-table/_shared-table-theme.scss +++ b/src/app/shared/modules/shared-table/_shared-table-theme.scss @@ -42,6 +42,15 @@ } } } + + dynamic-mat-table { + mat-header-row.mat-mdc-header-row.header { + background-color: mat.get-color-from-palette( + $primary, + "default" + ) !important; + } + } } @mixin theme($theme) { diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index c4736c092..97ba600f1 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -18,6 +18,7 @@ import { ScicatDataService } from "./services/scicat-data-service"; import { ScientificMetadataTreeModule } from "./modules/scientific-metadata-tree/scientific-metadata-tree.modules"; import { FiltersModule } from "./modules/filters/filters.module"; import { AttachmentService } from "./services/attachment.service"; +import { DynamicMatTableModule } from "./modules/dynamic-material-table/table/dynamic-mat-table.module"; import { TranslateModule } from "@ngx-translate/core"; @NgModule({ imports: [ @@ -36,6 +37,7 @@ import { TranslateModule } from "@ngx-translate/core"; FormsModule, SharedTableModule, ScientificMetadataTreeModule, + DynamicMatTableModule.forRoot({}), TranslateModule, ], providers: [ @@ -59,6 +61,7 @@ import { TranslateModule } from "@ngx-translate/core"; SharedTableModule, ScientificMetadataTreeModule, FiltersModule, + DynamicMatTableModule, TranslateModule, ], }) diff --git a/src/app/state-management/actions/proposals.actions.spec.ts b/src/app/state-management/actions/proposals.actions.spec.ts index 8194afedc..a5e25cfcf 100644 --- a/src/app/state-management/actions/proposals.actions.spec.ts +++ b/src/app/state-management/actions/proposals.actions.spec.ts @@ -9,7 +9,7 @@ import { describe("Proposal Actions", () => { describe("fetchProposalsAction", () => { it("should create an action", () => { - const action = fromActions.fetchProposalsAction(); + const action = fromActions.fetchProposalsAction({}); expect({ ...action }).toEqual({ type: "[Proposal] Fetch Proposals" }); }); }); @@ -36,7 +36,7 @@ describe("Proposal Actions", () => { describe("fetchCountAction", () => { it("should create an action", () => { - const action = fromActions.fetchCountAction(); + const action = fromActions.fetchCountAction({}); expect({ ...action }).toEqual({ type: "[Proposal] Fetch Count" }); }); }); @@ -304,26 +304,6 @@ describe("Proposal Actions", () => { }); }); - describe("clearFacetsAction", () => { - it("should create an action", () => { - const action = fromActions.clearFacetsAction(); - expect({ ...action }).toEqual({ type: "[Proposal] Clear Facets" }); - }); - }); - - describe("changePageAction", () => { - it("should create an action", () => { - const page = 0; - const limit = 25; - const action = fromActions.changePageAction({ page, limit }); - expect({ ...action }).toEqual({ - type: "[Proposal] Change Page", - page, - limit, - }); - }); - }); - describe("changeDatasetsPageAction", () => { it("should create an action", () => { const page = 0; @@ -337,19 +317,6 @@ describe("Proposal Actions", () => { }); }); - describe("sortByColumnAction", () => { - it("should create an action", () => { - const column = "test"; - const direction = "asc"; - const action = fromActions.sortByColumnAction({ column, direction }); - expect({ ...action }).toEqual({ - type: "[Proposal] Sort By Column", - column, - direction, - }); - }); - }); - describe("clearProposalsStateAction", () => { it("should create an action", () => { const action = fromActions.clearProposalsStateAction(); diff --git a/src/app/state-management/actions/proposals.actions.ts b/src/app/state-management/actions/proposals.actions.ts index 7134dd964..2de829cb8 100644 --- a/src/app/state-management/actions/proposals.actions.ts +++ b/src/app/state-management/actions/proposals.actions.ts @@ -6,7 +6,10 @@ import { } from "@scicatproject/scicat-sdk-ts-angular"; import { ProposalFilters } from "state-management/state/proposals.store"; -export const fetchProposalsAction = createAction("[Proposal] Fetch Proposals"); +export const fetchProposalsAction = createAction( + "[Proposal] Fetch Proposals", + props<{ skip?: number; limit?: number; search?: string; order?: string }>(), +); export const clearCurrentProposalAction = createAction( "[Proposal] Clear proposal", ); @@ -18,7 +21,10 @@ export const fetchProposalsFailedAction = createAction( "[Proposal] Fetch Proposals Failed", ); -export const fetchCountAction = createAction("[Proposal] Fetch Count"); +export const fetchCountAction = createAction( + "[Proposal] Fetch Count", + props<{ fields?: Record }>(), +); export const fetchCountCompleteAction = createAction( "[Proposal] Fetch Count Complete", props<{ count: number }>(), @@ -143,22 +149,11 @@ export const setDateRangeFilterAction = createAction( props<{ begin: string; end: string }>(), ); -export const clearFacetsAction = createAction("[Proposal] Clear Facets"); - -export const changePageAction = createAction( - "[Proposal] Change Page", - props<{ page: number; limit: number }>(), -); export const changeDatasetsPageAction = createAction( "[Proposal] Change Datasets Page", props<{ page: number; limit: number }>(), ); -export const sortByColumnAction = createAction( - "[Proposal] Sort By Column", - props<{ column: string; direction: string }>(), -); - export const clearProposalsStateAction = createAction("[Proposal] Clear State"); export const clearCurrentProposalStateAction = createAction( "[Proposal] Clear Current Proposal State", diff --git a/src/app/state-management/effects/proposals.effects.spec.ts b/src/app/state-management/effects/proposals.effects.spec.ts index 6e1f6134e..d00745f52 100644 --- a/src/app/state-management/effects/proposals.effects.spec.ts +++ b/src/app/state-management/effects/proposals.effects.spec.ts @@ -99,107 +99,13 @@ describe("ProposalEffects", () => { describe("ofType fetchProposalsAction", () => { it("should result in a fetchProposalsCompleteAction and a fetchCountAction", () => { const proposals = [proposal]; - const action = fromActions.fetchProposalsAction(); + const action = fromActions.fetchProposalsAction({}); const outcome1 = fromActions.fetchProposalsCompleteAction({ proposals, }); - const outcome2 = fromActions.fetchCountAction(); - - actions = hot("-a", { a: action }); - const response = cold("-a|", { a: proposals }); - proposalApi.proposalsControllerFullquery.and.returnValue(response); - - const expected = cold("--(bc)", { b: outcome1, c: outcome2 }); - expect(effects.fetchProposals$).toBeObservable(expected); - }); - - it("should result in a fetchProposalsFailedAction", () => { - const action = fromActions.fetchProposalsAction(); - const outcome = fromActions.fetchProposalsFailedAction(); - - actions = hot("-a", { a: action }); - const response = cold("-#", {}); - proposalApi.proposalsControllerFullquery.and.returnValue(response); - - const expected = cold("--b", { b: outcome }); - expect(effects.fetchProposals$).toBeObservable(expected); - }); - }); - - describe("ofType changePageAction", () => { - const page = 1; - const limit = 25; - - it("should result in a fetchProposalsCompleteAction and a fetchCountAction", () => { - const proposals = [proposal]; - const action = fromActions.changePageAction({ page, limit }); - const outcome1 = fromActions.fetchProposalsCompleteAction({ - proposals, - }); - const outcome2 = fromActions.fetchCountAction(); - - actions = hot("-a", { a: action }); - const response = cold("-a|", { a: proposals }); - proposalApi.proposalsControllerFullquery.and.returnValue(response); - - const expected = cold("--(bc)", { b: outcome1, c: outcome2 }); - expect(effects.fetchProposals$).toBeObservable(expected); - }); - - it("should result in a fetchProposalsFailedAction", () => { - const action = fromActions.changePageAction({ page, limit }); - const outcome = fromActions.fetchProposalsFailedAction(); - - actions = hot("-a", { a: action }); - const response = cold("-#", {}); - proposalApi.proposalsControllerFullquery.and.returnValue(response); - - const expected = cold("--b", { b: outcome }); - expect(effects.fetchProposals$).toBeObservable(expected); - }); - }); - - describe("ofType sortByColumnAction", () => { - const column = "test"; - const direction = "desc"; - - it("should result in a fetchProposalsCompleteAction and a fetchCountAction", () => { - const proposals = [proposal]; - const action = fromActions.sortByColumnAction({ column, direction }); - const outcome1 = fromActions.fetchProposalsCompleteAction({ - proposals, - }); - const outcome2 = fromActions.fetchCountAction(); - - actions = hot("-a", { a: action }); - const response = cold("-a|", { a: proposals }); - proposalApi.proposalsControllerFullquery.and.returnValue(response); - - const expected = cold("--(bc)", { b: outcome1, c: outcome2 }); - expect(effects.fetchProposals$).toBeObservable(expected); - }); - - it("should result in a fetchProposalsFailedAction", () => { - const action = fromActions.sortByColumnAction({ column, direction }); - const outcome = fromActions.fetchProposalsFailedAction(); - - actions = hot("-a", { a: action }); - const response = cold("-#", {}); - proposalApi.proposalsControllerFullquery.and.returnValue(response); - - const expected = cold("--b", { b: outcome }); - expect(effects.fetchProposals$).toBeObservable(expected); - }); - }); - - describe("ofType clearFacetsAction", () => { - it("should result in a fetchProposalsCompleteAction and a fetchCountAction", () => { - const proposals = [proposal]; - const action = fromActions.clearFacetsAction(); - const outcome1 = fromActions.fetchProposalsCompleteAction({ - proposals, + const outcome2 = fromActions.fetchCountAction({ + fields: { text: undefined }, }); - const outcome2 = fromActions.fetchCountAction(); actions = hot("-a", { a: action }); const response = cold("-a|", { a: proposals }); @@ -210,7 +116,7 @@ describe("ProposalEffects", () => { }); it("should result in a fetchProposalsFailedAction", () => { - const action = fromActions.clearFacetsAction(); + const action = fromActions.fetchProposalsAction({}); const outcome = fromActions.fetchProposalsFailedAction(); actions = hot("-a", { a: action }); @@ -226,7 +132,7 @@ describe("ProposalEffects", () => { describe("fetchCount$", () => { it("should result in a fetchCountCompleteAction", () => { const count = 1; - const action = fromActions.fetchCountAction(); + const action = fromActions.fetchCountAction({}); const outcome = fromActions.fetchCountCompleteAction({ count, }); @@ -240,7 +146,7 @@ describe("ProposalEffects", () => { }); it("should result in a fetchCountFailedAction", () => { - const action = fromActions.fetchCountAction(); + const action = fromActions.fetchCountAction({}); const outcome = fromActions.fetchCountFailedAction(); actions = hot("-a", { a: action }); @@ -507,7 +413,7 @@ describe("ProposalEffects", () => { describe("loading$", () => { describe("ofType fetchProposalsAction", () => { it("should dispatch a loadingAction", () => { - const action = fromActions.fetchProposalsAction(); + const action = fromActions.fetchProposalsAction({}); const outcome = loadingAction(); actions = hot("-a", { a: action }); @@ -519,7 +425,7 @@ describe("ProposalEffects", () => { describe("ofType fetchCountAction", () => { it("should dispatch a loadingAction", () => { - const action = fromActions.fetchCountAction(); + const action = fromActions.fetchCountAction({}); const outcome = loadingAction(); actions = hot("-a", { a: action }); diff --git a/src/app/state-management/effects/proposals.effects.ts b/src/app/state-management/effects/proposals.effects.ts index 3f18ca22c..a3f81f7c4 100644 --- a/src/app/state-management/effects/proposals.effects.ts +++ b/src/app/state-management/effects/proposals.effects.ts @@ -29,36 +29,41 @@ export class ProposalEffects { fetchProposals$ = createEffect(() => { return this.actions$.pipe( - ofType( - fromActions.fetchProposalsAction, - fromActions.changePageAction, - fromActions.sortByColumnAction, - fromActions.clearFacetsAction, - ), - concatLatestFrom(() => this.fullqueryParams$), - map(([action, params]) => params), - mergeMap(({ query, limits }) => - this.proposalsService - .proposalsControllerFullquery(JSON.stringify(limits), query) + ofType(fromActions.fetchProposalsAction), + mergeMap(({ skip, limit, search, order }) => { + const limitsParam = { + order: order, + skip: skip, + limit: limit, + }; + + const queryParam = { text: search || undefined }; + + return this.proposalsService + .proposalsControllerFullquery( + JSON.stringify(limitsParam), + JSON.stringify(queryParam), + ) .pipe( mergeMap((proposals) => [ fromActions.fetchProposalsCompleteAction({ proposals }), - fromActions.fetchCountAction(), + // TODO: Maybe this part should be refactored. Now we need to send 2 separate requests to get the data and count + fromActions.fetchCountAction({ + fields: queryParam, + }), ]), catchError(() => of(fromActions.fetchProposalsFailedAction())), - ), - ), + ); + }), ); }); fetchCount$ = createEffect(() => { return this.actions$.pipe( ofType(fromActions.fetchCountAction), - concatLatestFrom(() => this.fullqueryParams$), - map(([action, params]) => params), - switchMap((filters) => + switchMap(({ fields }) => this.proposalsService - .proposalsControllerCount(JSON.stringify(filters)) + .proposalsControllerCount(JSON.stringify(fields)) .pipe( map(({ count }) => fromActions.fetchCountCompleteAction({ count })), catchError(() => of(fromActions.fetchCountFailedAction())), @@ -260,12 +265,10 @@ export class ProposalEffects { concatLatestFrom(() => [this.currentProposal$]), switchMap(([, proposal]) => { const queryFilter = { - where: { - $or: [ - { proposalId: { $in: [proposal.parentProposalId] } }, - { parentProposalId: { $in: [proposal.proposalId] } }, - ], - }, + $or: [ + { proposalId: { $in: [proposal.parentProposalId] } }, + { parentProposalId: { $in: [proposal.proposalId] } }, + ], }; return this.proposalsService diff --git a/src/app/state-management/reducers/proposals.reducer.spec.ts b/src/app/state-management/reducers/proposals.reducer.spec.ts index ff2185783..037131145 100644 --- a/src/app/state-management/reducers/proposals.reducer.spec.ts +++ b/src/app/state-management/reducers/proposals.reducer.spec.ts @@ -180,39 +180,6 @@ describe("ProposalsReducer", () => { }); }); - describe("on clearFacetsAction", () => { - it("should clear filters while saving the filters limit", () => { - const limit = 10; - const page = 1; - const skip = limit * page; - - const act = fromActions.changePageAction({ page, limit }); - const sta = proposalsReducer(initialProposalsState, act); - - expect(sta.proposalFilters.skip).toEqual(skip); - - const action = fromActions.clearFacetsAction(); - const state = proposalsReducer(sta, action); - - expect(state.proposalFilters.skip).toEqual(0); - expect(state.proposalFilters.limit).toEqual(limit); - expect(state.proposalFilters.text).toEqual(""); - }); - }); - - describe("on changePageAction", () => { - it("should set skip and limit filters", () => { - const page = 1; - const limit = 25; - const skip = page * limit; - const action = fromActions.changePageAction({ page, limit }); - const state = proposalsReducer(initialProposalsState, action); - - expect(state.proposalFilters.skip).toEqual(skip); - expect(state.proposalFilters.limit).toEqual(limit); - }); - }); - describe("on changeDatasetsPageAction", () => { it("should set skip and limit dataset filters", () => { const page = 1; @@ -226,19 +193,6 @@ describe("ProposalsReducer", () => { }); }); - describe("on sortByColumnAction", () => { - it("should set sortField filter and set skip to 0", () => { - const column = "test"; - const direction = "asc"; - const sortField = column + ":" + direction; - const action = fromActions.sortByColumnAction({ column, direction }); - const state = proposalsReducer(initialProposalsState, action); - - expect(state.proposalFilters.sortField).toEqual(sortField); - expect(state.proposalFilters.skip).toEqual(0); - }); - }); - describe("on clearProposalsStateAction", () => { it("it should set proposals state to initialProposState", () => { const action = fromActions.clearProposalsStateAction(); diff --git a/src/app/state-management/reducers/proposals.reducer.ts b/src/app/state-management/reducers/proposals.reducer.ts index f80b3074e..7350dccc4 100644 --- a/src/app/state-management/reducers/proposals.reducer.ts +++ b/src/app/state-management/reducers/proposals.reducer.ts @@ -119,21 +119,6 @@ const reducer = createReducer( }, ), - on(fromActions.clearFacetsAction, (state): ProposalsState => { - const limit = state.proposalFilters.limit; // Save limit - const proposalFilters = { - ...initialProposalsState.proposalFilters, - skip: 0, - limit, - }; - return { ...state, proposalFilters }; - }), - - on(fromActions.changePageAction, (state, { page, limit }): ProposalsState => { - const skip = page * limit; - const proposalFilters = { ...state.proposalFilters, skip, limit }; - return { ...state, proposalFilters }; - }), on( fromActions.changeDatasetsPageAction, (state, { page, limit }): ProposalsState => { @@ -143,15 +128,6 @@ const reducer = createReducer( }, ), - on( - fromActions.sortByColumnAction, - (state, { column, direction }): ProposalsState => { - const sortField = column + (direction ? ":" + direction : ""); - const proposalFilters = { ...state.proposalFilters, sortField, skip: 0 }; - return { ...state, proposalFilters }; - }, - ), - on(fromActions.clearProposalsStateAction, () => ({ ...initialProposalsState, })), From 89ce55f08b1d891748a1157095aff802941413a5 Mon Sep 17 00:00:00 2001 From: Jay Date: Mon, 24 Feb 2025 15:01:41 +0100 Subject: [PATCH 07/13] fix: fix minor icon misalignment (#1755) ## Description - Added enter and escape key handlers for text search input - Removed unused edit mode state in datasets filter - Reduced debounce time for text filter to improve responsiveness - Adjusted cart icon position image ## Motivation Background on use case, changes needed ## Fixes: Please provide a list of the fixes implemented in this PR * Items added ## Changes: Please provide a list of the changes implemented by this PR * changes made ## Tests included - [ ] Included for each change/fix? - [ ] Passing? (Merge will not be approved unless this is checked) ## Documentation - [ ] swagger documentation updated \[required\] - [ ] official documentation updated \[nice-to-have\] ### official documentation info If you have updated the official documentation, please provide PR # and URL of the pages where the updates are included ## Backend version - [ ] Does it require a specific version of the backend - which version of the backend is required: ## Summary by Sourcery This pull request enhances the search functionality and improves filter performance. It adds keyboard support to the search bar, improves the responsiveness of the text filter, and removes an unused state variable. New Features: - The search bar now supports submitting the search query by pressing the Enter key. - The search bar now supports clearing the search query by pressing the Escape key. Bug Fixes: - Removed unused `isInEditMode` state in the datasets filter component. Enhancements: - Improved the responsiveness of the text filter by reducing the debounce time. --- .../_layout/app-header/app-header.component.html | 16 +++++++--------- .../full-text-search-bar.component.html | 2 ++ .../datasets-filter/datasets-filter.component.ts | 3 --- .../modules/filters/text-filter.component.ts | 2 +- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/app/_layout/app-header/app-header.component.html b/src/app/_layout/app-header/app-header.component.html index b844e8659..c19064f56 100644 --- a/src/app/_layout/app-header/app-header.component.html +++ b/src/app/_layout/app-header/app-header.component.html @@ -94,15 +94,13 @@
- diff --git a/src/app/datasets/dashboard/full-text-search/full-text-search-bar.component.html b/src/app/datasets/dashboard/full-text-search/full-text-search-bar.component.html index f9895119c..c31289539 100644 --- a/src/app/datasets/dashboard/full-text-search/full-text-search-bar.component.html +++ b/src/app/datasets/dashboard/full-text-search/full-text-search-bar.component.html @@ -8,6 +8,8 @@ type="search" [(ngModel)]="searchTerm" (ngModelChange)="onSearchTermChange($event)" + (keydown.enter)="onSearch()" + (keydown.escape)="onClear()" />
diff --git a/src/app/datasets/datasets-filter/datasets-filter.component.ts b/src/app/datasets/datasets-filter/datasets-filter.component.ts index 14562efaf..973aca8e3 100644 --- a/src/app/datasets/datasets-filter/datasets-filter.component.ts +++ b/src/app/datasets/datasets-filter/datasets-filter.component.ts @@ -78,8 +78,6 @@ export class DatasetsFilterComponent implements OnInit, OnDestroy { hasAppliedFilters$ = this.store.select(selectHasAppliedFilters); - isInEditMode = false; - labelMaps: { [key: string]: string } = {}; constructor( @@ -176,7 +174,6 @@ export class DatasetsFilterComponent implements OnInit, OnDestroy { } applyFilters() { - this.isInEditMode = false; this.store.dispatch(fetchDatasetsAction()); this.store.dispatch(fetchFacetCountsAction()); } diff --git a/src/app/shared/modules/filters/text-filter.component.ts b/src/app/shared/modules/filters/text-filter.component.ts index 356c58984..5307b3bed 100644 --- a/src/app/shared/modules/filters/text-filter.component.ts +++ b/src/app/shared/modules/filters/text-filter.component.ts @@ -35,7 +35,7 @@ export class TextFilterComponent this.subscription = this.textSubject .pipe( skipWhile((terms) => terms === ""), - debounceTime(500), + debounceTime(200), distinctUntilChanged(), ) .subscribe((terms) => { From bc905f8b453f5e0d3e077c7a42fae4487a1abce5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Feb 2025 18:45:10 +0000 Subject: [PATCH 08/13] chore(deps-dev): bump the eslint group with 2 updates Bumps the eslint group with 2 updates: [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) and [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser). Updates `@typescript-eslint/eslint-plugin` from 8.24.1 to 8.25.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.25.0/packages/eslint-plugin) Updates `@typescript-eslint/parser` from 8.24.1 to 8.25.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.25.0/packages/parser) --- updated-dependencies: - dependency-name: "@typescript-eslint/eslint-plugin" dependency-type: direct:development update-type: version-update:semver-minor dependency-group: eslint - dependency-name: "@typescript-eslint/parser" dependency-type: direct:development update-type: version-update:semver-minor dependency-group: eslint ... Signed-off-by: dependabot[bot] --- package-lock.json | 86 +++++++++++++++++++++++------------------------ package.json | 2 +- 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/package-lock.json b/package-lock.json index c162318ae..34215c3f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -68,7 +68,7 @@ "@types/node": "^22.0.0", "@types/shortid": "2.2.0", "@types/source-map-support": "^0.5.3", - "@typescript-eslint/eslint-plugin": "8.24.1", + "@typescript-eslint/eslint-plugin": "8.25.0", "@typescript-eslint/parser": "^8.0.0", "coveralls": "^3.0.7", "cypress": "^14.0.0", @@ -5705,17 +5705,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.24.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.24.1.tgz", - "integrity": "sha512-ll1StnKtBigWIGqvYDVuDmXJHVH4zLVot1yQ4fJtLpL7qacwkxJc1T0bptqw+miBQ/QfUbhl1TcQ4accW5KUyA==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.25.0.tgz", + "integrity": "sha512-VM7bpzAe7JO/BFf40pIT1lJqS/z1F8OaSsUB3rpFJucQA4cOSuH2RVVVkFULN+En0Djgr29/jb4EQnedUo95KA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.24.1", - "@typescript-eslint/type-utils": "8.24.1", - "@typescript-eslint/utils": "8.24.1", - "@typescript-eslint/visitor-keys": "8.24.1", + "@typescript-eslint/scope-manager": "8.25.0", + "@typescript-eslint/type-utils": "8.25.0", + "@typescript-eslint/utils": "8.25.0", + "@typescript-eslint/visitor-keys": "8.25.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -5735,14 +5735,14 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { - "version": "8.24.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.24.1.tgz", - "integrity": "sha512-/Do9fmNgCsQ+K4rCz0STI7lYB4phTtEXqqCAs3gZW0pnK7lWNkvWd5iW545GSmApm4AzmQXmSqXPO565B4WVrw==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.25.0.tgz", + "integrity": "sha512-d77dHgHWnxmXOPJuDWO4FDWADmGQkN5+tt6SFRZz/RtCWl4pHgFl3+WdYCn16+3teG09DY6XtEpf3gGD0a186g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.24.1", - "@typescript-eslint/utils": "8.24.1", + "@typescript-eslint/typescript-estree": "8.25.0", + "@typescript-eslint/utils": "8.25.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, @@ -5759,16 +5759,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "8.24.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.24.1.tgz", - "integrity": "sha512-OOcg3PMMQx9EXspId5iktsI3eMaXVwlhC8BvNnX6B5w9a4dVgpkQZuU8Hy67TolKcl+iFWq0XX+jbDGN4xWxjQ==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.25.0.tgz", + "integrity": "sha512-syqRbrEv0J1wywiLsK60XzHnQe/kRViI3zwFALrNEgnntn1l24Ra2KvOAWwWbWZ1lBZxZljPDGOq967dsl6fkA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.24.1", - "@typescript-eslint/types": "8.24.1", - "@typescript-eslint/typescript-estree": "8.24.1" + "@typescript-eslint/scope-manager": "8.25.0", + "@typescript-eslint/types": "8.25.0", + "@typescript-eslint/typescript-estree": "8.25.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5811,16 +5811,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.24.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.24.1.tgz", - "integrity": "sha512-Tqoa05bu+t5s8CTZFaGpCH2ub3QeT9YDkXbPd3uQ4SfsLoh1/vv2GEYAioPoxCWJJNsenXlC88tRjwoHNts1oQ==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.25.0.tgz", + "integrity": "sha512-4gbs64bnbSzu4FpgMiQ1A+D+urxkoJk/kqlDJ2W//5SygaEiAP2B4GoS7TEdxgwol2el03gckFV9lJ4QOMiiHg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.24.1", - "@typescript-eslint/types": "8.24.1", - "@typescript-eslint/typescript-estree": "8.24.1", - "@typescript-eslint/visitor-keys": "8.24.1", + "@typescript-eslint/scope-manager": "8.25.0", + "@typescript-eslint/types": "8.25.0", + "@typescript-eslint/typescript-estree": "8.25.0", + "@typescript-eslint/visitor-keys": "8.25.0", "debug": "^4.3.4" }, "engines": { @@ -5836,14 +5836,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.24.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.24.1.tgz", - "integrity": "sha512-OdQr6BNBzwRjNEXMQyaGyZzgg7wzjYKfX2ZBV3E04hUCBDv3GQCHiz9RpqdUIiVrMgJGkXm3tcEh4vFSHreS2Q==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.25.0.tgz", + "integrity": "sha512-6PPeiKIGbgStEyt4NNXa2ru5pMzQ8OYKO1hX1z53HMomrmiSB+R5FmChgQAP1ro8jMtNawz+TRQo/cSXrauTpg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.24.1", - "@typescript-eslint/visitor-keys": "8.24.1" + "@typescript-eslint/types": "8.25.0", + "@typescript-eslint/visitor-keys": "8.25.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5967,9 +5967,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.24.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.1.tgz", - "integrity": "sha512-9kqJ+2DkUXiuhoiYIUvIYjGcwle8pcPpdlfkemGvTObzgmYfJ5d0Qm6jwb4NBXP9W1I5tss0VIAnWFumz3mC5A==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.25.0.tgz", + "integrity": "sha512-+vUe0Zb4tkNgznQwicsvLUJgZIRs6ITeWSCclX1q85pR1iOiaj+4uZJIUp//Z27QWu5Cseiw3O3AR8hVpax7Aw==", "dev": true, "license": "MIT", "engines": { @@ -5981,14 +5981,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.24.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.1.tgz", - "integrity": "sha512-UPyy4MJ/0RE648DSKQe9g0VDSehPINiejjA6ElqnFaFIhI6ZEiZAkUI0D5MCk0bQcTf/LVqZStvQ6K4lPn/BRg==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.25.0.tgz", + "integrity": "sha512-ZPaiAKEZ6Blt/TPAx5Ot0EIB/yGtLI2EsGoY6F7XKklfMxYQyvtL+gT/UCqkMzO0BVFHLDlzvFqQzurYahxv9Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.24.1", - "@typescript-eslint/visitor-keys": "8.24.1", + "@typescript-eslint/types": "8.25.0", + "@typescript-eslint/visitor-keys": "8.25.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -6205,13 +6205,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.24.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.1.tgz", - "integrity": "sha512-EwVHlp5l+2vp8CoqJm9KikPZgi3gbdZAtabKT9KPShGeOcJhsv4Zdo3oc8T8I0uKEmYoU4ItyxbptjF08enaxg==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.25.0.tgz", + "integrity": "sha512-kCYXKAum9CecGVHGij7muybDfTS2sD3t0L4bJsEZLkyrXUImiCTq1M3LG2SRtOhiHFwMR9wAFplpT6XHYjTkwQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.24.1", + "@typescript-eslint/types": "8.25.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { diff --git a/package.json b/package.json index 9d4bacbf8..490dee712 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "@types/node": "^22.0.0", "@types/shortid": "2.2.0", "@types/source-map-support": "^0.5.3", - "@typescript-eslint/eslint-plugin": "8.24.1", + "@typescript-eslint/eslint-plugin": "8.25.0", "@typescript-eslint/parser": "^8.0.0", "coveralls": "^3.0.7", "cypress": "^14.0.0", From 374de18f9f7344db627a342d8f5447597bbb6803 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Feb 2025 18:54:22 +0000 Subject: [PATCH 09/13] chore(deps-dev): bump jasmine and @types/jasmine Bumps [jasmine](https://github.com/jasmine/jasmine-npm) and [@types/jasmine](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jasmine). These dependencies needed to be updated together. Updates `jasmine` from 5.5.0 to 5.6.0 - [Release notes](https://github.com/jasmine/jasmine-npm/releases) - [Changelog](https://github.com/jasmine/jasmine-npm/blob/main/RELEASE.md) - [Commits](https://github.com/jasmine/jasmine-npm/compare/v5.5.0...v5.6.0) Updates `@types/jasmine` from 5.1.5 to 5.1.7 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jasmine) --- updated-dependencies: - dependency-name: jasmine dependency-type: direct:development update-type: version-update:semver-minor - dependency-name: "@types/jasmine" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index 34215c3f5..b9201c19a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5514,10 +5514,11 @@ } }, "node_modules/@types/jasmine": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.5.tgz", - "integrity": "sha512-SaCZ3kM5NjOiJqMRYwHpLbTfUC2Dyk1KS3QanNFsUYPGTk70CWVK/J9ueun6zNhw/UkgV7xl8V4ZLQZNRbfnNw==", - "dev": true + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.7.tgz", + "integrity": "sha512-DVOfk9FaClQfNFpSfaML15jjB5cjffDMvjtph525sroR5BEAW2uKnTOYUTqTFuZFjNvH0T5XMIydvIctnUKufw==", + "dev": true, + "license": "MIT" }, "node_modules/@types/json-schema": { "version": "7.0.15", @@ -12160,13 +12161,14 @@ } }, "node_modules/jasmine": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-5.5.0.tgz", - "integrity": "sha512-JKlEVCVD5QBPYLsg/VE+IUtjyseDCrW8rMBu8la+9ysYashDgavMLM9Kotls1FhI6dCJLJ40dBCIfQjGLPZI1Q==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-5.6.0.tgz", + "integrity": "sha512-6frlW22jhgRjtlp68QY/DDVCUfrYqmSxDBWM13mrBzYQGx1XITfVcJltnY15bk8B5cRfN5IpKvemkDiDTSRCsA==", "dev": true, + "license": "MIT", "dependencies": { "glob": "^10.2.2", - "jasmine-core": "~5.5.0" + "jasmine-core": "~5.6.0" }, "bin": { "jasmine": "bin/jasmine.js" @@ -12220,13 +12222,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/jasmine/node_modules/jasmine-core": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.5.0.tgz", - "integrity": "sha512-NHOvoPO6o9gVR6pwqEACTEpbgcH+JJ6QDypyymGbSUIFIFsMMbBJ/xsFNud8MSClfnWclXd7RQlAZBz7yVo5TQ==", - "dev": true, - "license": "MIT" - }, "node_modules/jasmine/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", From d0c5388ba5d457cba7b1406ca19b2873ba2df651 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Feb 2025 19:03:18 +0000 Subject: [PATCH 10/13] chore(deps-dev): bump @types/node in the types group Bumps the types group with 1 update: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node). Updates `@types/node` from 22.13.4 to 22.13.5 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-type: direct:development update-type: version-update:semver-patch dependency-group: types ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b9201c19a..73bee6869 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5554,9 +5554,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "22.13.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz", - "integrity": "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==", + "version": "22.13.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.5.tgz", + "integrity": "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==", "dev": true, "license": "MIT", "dependencies": { From f30f4b9b01c71c3458d3f60e333f47d6da3168b5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Feb 2025 19:11:46 +0000 Subject: [PATCH 11/13] chore(deps): bump mathjs from 14.2.0 to 14.2.1 Bumps [mathjs](https://github.com/josdejong/mathjs) from 14.2.0 to 14.2.1. - [Changelog](https://github.com/josdejong/mathjs/blob/develop/HISTORY.md) - [Commits](https://github.com/josdejong/mathjs/compare/v14.2.0...v14.2.1) --- updated-dependencies: - dependency-name: mathjs dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 69 ++++++++++++++--------------------------------- 1 file changed, 20 insertions(+), 49 deletions(-) diff --git a/package-lock.json b/package-lock.json index 73bee6869..ca4cc854e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3785,18 +3785,6 @@ "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", "dev": true }, - "node_modules/@lambdatest/node-tunnel": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@lambdatest/node-tunnel/-/node-tunnel-4.0.8.tgz", - "integrity": "sha512-IY42aDD4Ryqjug9V4wpCjckKpHjC2zrU/XhhorR5ztX088XITRFKUo8U6+gOjy/V8kAB+EgDuIXfK0izXbt9Ow==", - "dependencies": { - "adm-zip": "^0.5.10", - "axios": "^1.6.2", - "get-port": "^1.0.0", - "https-proxy-agent": "^5.0.0", - "split": "^1.0.1" - } - }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -6595,18 +6583,11 @@ "node": ">=8.9.0" } }, - "node_modules/adm-zip": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", - "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", - "engines": { - "node": ">=12.0" - } - }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, "dependencies": { "debug": "4" }, @@ -6955,7 +6936,8 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true }, "node_modules/at-least-node": { "version": "1.0.0", @@ -7040,6 +7022,7 @@ "version": "1.7.9", "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "dev": true, "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -7049,7 +7032,8 @@ "node_modules/axios/node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true }, "node_modules/axobject-query": { "version": "4.0.0", @@ -7911,6 +7895,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -8542,6 +8527,7 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, "dependencies": { "ms": "^2.1.3" }, @@ -8709,6 +8695,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, "engines": { "node": ">=0.4.0" } @@ -10433,6 +10420,7 @@ "version": "1.15.9", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "dev": true, "funding": [ { "type": "individual", @@ -10503,6 +10491,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -10722,17 +10711,6 @@ "node": ">=8.0.0" } }, - "node_modules/get-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-1.0.0.tgz", - "integrity": "sha512-vg59F3kcXBOtcIijwtdAyCxFocyv/fVkGQvw1kVGrxFO1U4SSGkGjrbASg5DN3TVekVle/jltwOjYRnZWc1YdA==", - "bin": { - "get-port": "cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -11298,6 +11276,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, "dependencies": { "agent-base": "6", "debug": "4" @@ -13563,12 +13542,12 @@ } }, "node_modules/mathjs": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-14.2.0.tgz", - "integrity": "sha512-CcJV1cQwRSrQIAAX3sWejFPUvUsQnTZYisEEuoMBw3gMDJDQzvKQlrul/vjKAbdtW7zaDzPCl04h1sf0wh41TA==", + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-14.2.1.tgz", + "integrity": "sha512-vARWETUx75+kR2K9qBV20n6NYtGXCuQKX8Zo4+AhJI5LX+ukSM1NYebv+wLnJG8KMvEe9H01sJUyC5bMciA4Tg==", + "license": "Apache-2.0", "dependencies": { "@babel/runtime": "^7.25.7", - "@lambdatest/node-tunnel": "^4.0.8", "complex.js": "^2.2.5", "decimal.js": "^10.4.3", "escape-latex": "^1.2.0", @@ -13696,6 +13675,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, "engines": { "node": ">= 0.6" } @@ -13704,6 +13684,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -17298,17 +17279,6 @@ "wbuf": "^1.7.3" } }, - "node_modules/split": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", - "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", - "dependencies": { - "through": "2" - }, - "engines": { - "node": "*" - } - }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -17817,7 +17787,8 @@ "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true }, "node_modules/thunky": { "version": "1.1.0", From 4a30f4ef9676e478f2653d2a035791df6cec59b6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Feb 2025 19:20:26 +0000 Subject: [PATCH 12/13] chore(deps-dev): bump cypress from 14.0.1 to 14.0.3 Bumps [cypress](https://github.com/cypress-io/cypress) from 14.0.1 to 14.0.3. - [Release notes](https://github.com/cypress-io/cypress/releases) - [Changelog](https://github.com/cypress-io/cypress/blob/develop/CHANGELOG.md) - [Commits](https://github.com/cypress-io/cypress/compare/v14.0.1...v14.0.3) --- updated-dependencies: - dependency-name: cypress dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index ca4cc854e..eb2226d4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8393,13 +8393,14 @@ "dev": true }, "node_modules/cypress": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-14.0.1.tgz", - "integrity": "sha512-gBAvKZE3f6eBaW1v8OtrwAFP90rjNZjjOO40M2KvOvmwVXk96Ps5Yjyck1EzGkXmNCaC/8kXFOY/1KD/wsaWpQ==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-14.0.3.tgz", + "integrity": "sha512-yIdvobANw3kS+KF/t5vwjjPNufBA8ux7iQHaWxPTkUw2yCKI72m9mKM24eOwE84Wk4ALPsSvEcGbDrwgmhr4RA==", "dev": true, "hasInstallScript": true, + "license": "MIT", "dependencies": { - "@cypress/request": "^3.0.6", + "@cypress/request": "^3.0.7", "@cypress/xvfb": "^1.2.4", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", From d0077b0562a09a797ec18adabc5ca7582d2bca3b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Feb 2025 19:29:29 +0000 Subject: [PATCH 13/13] chore(deps-dev): bump prettier from 3.4.2 to 3.5.2 Bumps [prettier](https://github.com/prettier/prettier) from 3.4.2 to 3.5.2. - [Release notes](https://github.com/prettier/prettier/releases) - [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md) - [Commits](https://github.com/prettier/prettier/compare/3.4.2...3.5.2) --- updated-dependencies: - dependency-name: prettier dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index eb2226d4d..0abe49a04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15451,10 +15451,11 @@ } }, "node_modules/prettier": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", - "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.2.tgz", + "integrity": "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==", "dev": true, + "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" },