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()"
/>
- {{ 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 @@
0">
- {{
- 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ search
+
+
+
+
+
+
+
+
+
+
+
+ 0" matColumnDef="expandedDetail">
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+ 0">
+
+
+
+
+
+ 0">
+
+
+
+
+
+
+
+
+
+ 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 @@
+
+
+
+
+
+ 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 }}
+
+
+
+
+ {{ column.header }} |
+ {{ row[column.name] }} |
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ setting.settingName
+ }}
+ save
+ delete
+
+
+ lightbulb
+ No setting
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0; else noColumns"
+ >
+
+
+
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,
})),