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 7df2321d8..0abe49a04 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", @@ -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", @@ -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.25.0", "@typescript-eslint/parser": "^8.0.0", "coveralls": "^3.0.7", "cypress": "^14.0.0", @@ -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", @@ -5179,9 +5167,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" }, @@ -5514,10 +5502,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", @@ -5553,9 +5542,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.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.5.tgz", + "integrity": "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==", "dev": true, "license": "MIT", "dependencies": { @@ -5705,17 +5694,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.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.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.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 +5724,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.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.23.0", - "@typescript-eslint/utils": "8.23.0", + "@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 +5748,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.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.23.0", - "@typescript-eslint/types": "8.23.0", - "@typescript-eslint/typescript-estree": "8.23.0" + "@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 +5800,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.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.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.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 +5825,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.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.23.0", - "@typescript-eslint/visitor-keys": "8.23.0" + "@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 +5956,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.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 +5970,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.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.23.0", - "@typescript-eslint/visitor-keys": "8.23.0", + "@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", @@ -6041,9 +6030,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 +6194,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.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.23.0", + "@typescript-eslint/types": "8.25.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -6594,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" }, @@ -6954,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", @@ -7039,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", @@ -7048,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", @@ -7910,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" }, @@ -8407,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", @@ -8541,6 +8528,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" }, @@ -8708,6 +8696,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" } @@ -10432,6 +10421,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", @@ -10502,6 +10492,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", @@ -10721,17 +10712,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", @@ -11297,6 +11277,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" @@ -12160,23 +12141,25 @@ } }, "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" } }, "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", @@ -13560,12 +13543,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", @@ -13693,6 +13676,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" } @@ -13701,6 +13685,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" }, @@ -15466,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" }, @@ -17295,17 +17281,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", @@ -17814,7 +17789,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", diff --git a/package.json b/package.json index 2783c36a1..490dee712 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", @@ -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.25.0", "@typescript-eslint/parser": "^8.0.0", "coveralls": "^3.0.7", "cypress": "^14.0.0", 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/_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/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 3e802ca3d..e6936d2be 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -31,6 +31,7 @@ import { CookieService } from "ngx-cookie-service"; import { TranslateLoader, TranslateModule } from "@ngx-translate/core"; import { CustomTranslateLoader } from "shared/loaders/custom-translate.loader"; import { RouteTrackerService } from "shared/services/route-tracker.service"; +import { DATE_PIPE_DEFAULT_OPTIONS } from "@angular/common"; const appConfigInitializerFn = (appConfig: AppConfigService) => { return () => appConfig.loadAppConfig(); @@ -120,6 +121,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/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/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/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/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/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/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 }} 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/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) => { 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/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())), - ), - ), + ); + }), ); }); 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, })),