diff --git a/CI/e2e/frontend.config.e2e.json b/CI/e2e/frontend.config.e2e.json index 53eb05838..bdaf97730 100644 --- a/CI/e2e/frontend.config.e2e.json +++ b/CI/e2e/frontend.config.e2e.json @@ -129,7 +129,7 @@ "name": "pid", "order": 1, "type": "standard", - "enabled": true + "enabled": false }, { "name": "datasetName", diff --git a/cypress/e2e/datasets/datasets-datafiles.cy.js b/cypress/e2e/datasets/datasets-datafiles.cy.js index ec3ea63bb..4adbbf8fe 100644 --- a/cypress/e2e/datasets/datasets-datafiles.cy.js +++ b/cypress/e2e/datasets/datasets-datafiles.cy.js @@ -44,7 +44,7 @@ describe("Dataset datafiles", () => { cy.isLoading(); - cy.contains("mat-row", "Cypress Dataset").first().click(); + cy.get("mat-row").contains("Cypress Dataset").first().click(); cy.wait("@fetch"); @@ -101,7 +101,7 @@ describe("Dataset datafiles", () => { cy.isLoading(); - cy.contains("mat-row", "Cypress Dataset").first().click(); + cy.get("mat-row").contains("Cypress Dataset").first().click(); cy.wait("@fetch"); diff --git a/cypress/e2e/datasets/datasets-publish.cy.js b/cypress/e2e/datasets/datasets-publish.cy.js index 8921ecdae..d55a09442 100644 --- a/cypress/e2e/datasets/datasets-publish.cy.js +++ b/cypress/e2e/datasets/datasets-publish.cy.js @@ -23,7 +23,7 @@ describe("Datasets", () => { cy.isLoading(); - cy.get("[data-cy=checkboxInput]").first().click(); + cy.get(".dataset-table input[type='checkbox']").first().click(); cy.get("#addToBatchButton").click(); diff --git a/cypress/e2e/datasets/datasets-share.cy.js b/cypress/e2e/datasets/datasets-share.cy.js index eb16ea908..842ee60b2 100644 --- a/cypress/e2e/datasets/datasets-share.cy.js +++ b/cypress/e2e/datasets/datasets-share.cy.js @@ -23,7 +23,7 @@ describe("Datasets", () => { cy.isLoading(); - cy.get("[data-cy=checkboxInput]").first().click(); + cy.get(".dataset-table input[type='checkbox']").first().click(); cy.get("#addToBatchButton").click(); diff --git a/package-lock.json b/package-lock.json index 90faa7342..9cb2936dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "@ngrx/store": "^19.1.0", "@ngx-translate/core": "^16.0.4", "@ngxmc/datetime-picker": "^19.3.1", - "@scicatproject/scicat-sdk-ts-angular": "^4.17.0", + "@scicatproject/scicat-sdk-ts-angular": "^4.17.1", "autolinker": "^4.0.0", "deep-equal": "^2.0.5", "exceljs": "^4.3.0", @@ -68,7 +68,7 @@ "@types/jasmine": "^5.1.0", "@types/lodash-es": "^4.17.12", "@types/luxon": "^3.3.0", - "@types/node": "^22.0.0", + "@types/node": "^24.0.3", "@types/shortid": "2.2.0", "@types/source-map-support": "^0.5.3", "@typescript-eslint/eslint-plugin": "^8.32.0", @@ -110,13 +110,13 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1902.13", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1902.13.tgz", - "integrity": "sha512-ZMj+PjK22Ph2U8usG6L7LqEfvWlbaOvmiWXSrEt9YiC9QJt6rsumCkOgUIsmHQtucm/lK+9CMtyYdwH2fYycjg==", + "version": "0.1902.15", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1902.15.tgz", + "integrity": "sha512-RbqhStc6ZoRv57ZqLB36VOkBkAdU3nNezCvIs0AJV5V4+vLPMrb0hpIB0sF+9yMlMjWsolnRsj0/Fil+zQG3bw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.2.13", + "@angular-devkit/core": "19.2.15", "rxjs": "7.8.1" }, "engines": { @@ -136,17 +136,17 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "19.2.13", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-19.2.13.tgz", - "integrity": "sha512-MrNpwrCq6COszhxyD/u2LE0yygTEjIAlaKaIvvDi9nurzUoKRc1vIJWeB2VkGgmUEjj6OTEeM/6zbo02s88EzA==", + "version": "19.2.15", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-19.2.15.tgz", + "integrity": "sha512-mqudAcyrSp/E7ZQdQoHfys0/nvQuwyJDaAzj3qL3HUStuUzb5ULNOj2f6sFBo+xYo+/WT8IzmzDN9DCqDgvFaA==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1902.13", - "@angular-devkit/build-webpack": "0.1902.13", - "@angular-devkit/core": "19.2.13", - "@angular/build": "19.2.13", + "@angular-devkit/architect": "0.1902.15", + "@angular-devkit/build-webpack": "0.1902.15", + "@angular-devkit/core": "19.2.15", + "@angular/build": "19.2.15", "@babel/core": "7.26.10", "@babel/generator": "7.26.10", "@babel/helper-annotate-as-pure": "7.25.9", @@ -157,7 +157,7 @@ "@babel/preset-env": "7.26.9", "@babel/runtime": "7.26.10", "@discoveryjs/json-ext": "0.6.3", - "@ngtools/webpack": "19.2.13", + "@ngtools/webpack": "19.2.15", "@vitejs/plugin-basic-ssl": "1.2.0", "ansi-colors": "4.1.3", "autoprefixer": "10.4.20", @@ -194,7 +194,7 @@ "tslib": "2.8.1", "webpack": "5.98.0", "webpack-dev-middleware": "7.4.2", - "webpack-dev-server": "5.2.0", + "webpack-dev-server": "5.2.2", "webpack-merge": "6.0.1", "webpack-subresource-integrity": "5.1.0" }, @@ -211,7 +211,7 @@ "@angular/localize": "^19.0.0 || ^19.2.0-next.0", "@angular/platform-server": "^19.0.0 || ^19.2.0-next.0", "@angular/service-worker": "^19.0.0 || ^19.2.0-next.0", - "@angular/ssr": "^19.2.13", + "@angular/ssr": "^19.2.15", "@web/test-runner": "^0.20.0", "browser-sync": "^3.0.2", "jest": "^29.5.0", @@ -271,13 +271,13 @@ } }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1902.13", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1902.13.tgz", - "integrity": "sha512-upb+cKWkuXwmKyppSwZf3ryHWPm4aS6sJkQu0TWh4RoMRp1WCYVxUfgZ28fTMqcBF3eoFy2XPjdOfkJDRb6Hrg==", + "version": "0.1902.15", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1902.15.tgz", + "integrity": "sha512-pIfZeizWsViXx8bsMoBLZw7Tl7uFf7bM7hAfmNwk0bb0QGzx5k1BiW6IKWyaG+Dg6U4UCrlNpIiut2b78HwQZw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1902.13", + "@angular-devkit/architect": "0.1902.15", "rxjs": "7.8.1" }, "engines": { @@ -301,9 +301,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "19.2.13", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.13.tgz", - "integrity": "sha512-iq73hE5Uvms1w3uMUSk4i4NDXDMQ863VAifX8LOTadhG6U0xISjNJ11763egVCxQmaKmg7zbG4rda88wHJATzA==", + "version": "19.2.15", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.15.tgz", + "integrity": "sha512-pU2RZYX6vhd7uLSdLwPnuBcr0mXJSjp3EgOXKsrlQFQZevc+Qs+2JdXgIElnOT/aDqtRtriDmLlSbtdE8n3ZbA==", "dev": true, "license": "MIT", "dependencies": { @@ -338,13 +338,13 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "19.2.13", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.13.tgz", - "integrity": "sha512-NhSPz3lI9njEo8eMUlZVGtlXl12UcNZv5lWTBZY/FGWUu6P5ciD/9iJINbc1jiaDH5E/DLEicUNuai0Q91X4Nw==", + "version": "19.2.15", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.15.tgz", + "integrity": "sha512-kNOJ+3vekJJCQKWihNmxBkarJzNW09kP5a9E1SRNiQVNOUEeSwcRR0qYotM65nx821gNzjjhJXnAZ8OazWldrg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.2.13", + "@angular-devkit/core": "19.2.15", "jsonc-parser": "3.3.1", "magic-string": "0.30.17", "ora": "5.4.1", @@ -367,9 +367,9 @@ } }, "node_modules/@angular-eslint/builder": { - "version": "19.5.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-19.5.0.tgz", - "integrity": "sha512-alYhEnZcA4RMfgC8AMMl29q77KhvfH9D0x80UPgd0XBUua94iYJky/Ng/LKq7gcIDWPRvEYCd5Tbmk4xfsmJVw==", + "version": "19.8.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-19.8.0.tgz", + "integrity": "sha512-+sDe92BpzlxNZWFuBbKD1L8xsW/dyOU+acPn4V84Vn55XMdhrBWOwDX7oxmBCOwuVTrS3mgHz7d22J1sdNwySw==", "dev": true, "license": "MIT", "dependencies": { @@ -382,21 +382,21 @@ } }, "node_modules/@angular-eslint/bundled-angular-compiler": { - "version": "19.5.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-19.5.0.tgz", - "integrity": "sha512-k75d3oQaF4F4a3Rk3JLBGtcmCNlaI5TlMSleKTMhKRrsD0nqDc+b5iFc/+JUzB9I5E6SovgMueU13ZdZfXZGZg==", + "version": "19.8.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-19.8.0.tgz", + "integrity": "sha512-nschDOyrAZPwS2mdC63pAf6vtVRZJ81imnosziOTx5jh1TTEwYdmFpfLA3LBvZUMkwDxkFOjxrMyl/k+c+oLBw==", "dev": true, "license": "MIT" }, "node_modules/@angular-eslint/eslint-plugin": { - "version": "19.5.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-19.5.0.tgz", - "integrity": "sha512-9hxPnX5rWCCH2Qga30Plym2eDXXetS0luPuOl0kHqdXQ/MB6j6tuSWcLKmqKlCRFe7/G9qEJoiblUvex6gwb9g==", + "version": "19.8.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-19.8.0.tgz", + "integrity": "sha512-4wmMopW9mEum3MI865WkWiVvQ7/Ia691LO006zr7Hp6VB+4+zzxZW4CH7X6tuxE2DMVILlU5fsSjR04ex9vuew==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "19.5.0", - "@angular-eslint/utils": "19.5.0" + "@angular-eslint/bundled-angular-compiler": "19.8.0", + "@angular-eslint/utils": "19.8.0" }, "peerDependencies": { "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", @@ -405,18 +405,19 @@ } }, "node_modules/@angular-eslint/eslint-plugin-template": { - "version": "19.5.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-19.5.0.tgz", - "integrity": "sha512-xDlxptxYelH6MuP48PE10cKDU+EHNYxDvsRmcUmP84MHP69VMAlmAAIS1j9y0UtfHrB1JxlKdsTU7cWH2YeWqA==", + "version": "19.8.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-19.8.0.tgz", + "integrity": "sha512-LXruJuwmRwJR6wfCq1wRxtGeF8lZQFNnS4GHNaRB7BRqYisPQXHsHT8EAnNf2eDwIkkq4TzZC0EfC6eaLlsh0A==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "19.5.0", - "@angular-eslint/utils": "19.5.0", + "@angular-eslint/bundled-angular-compiler": "19.8.0", + "@angular-eslint/utils": "19.8.0", "aria-query": "5.3.2", "axobject-query": "4.1.0" }, "peerDependencies": { + "@angular-eslint/template-parser": "19.8.0", "@typescript-eslint/types": "^7.11.0 || ^8.0.0", "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", "eslint": "^8.57.0 || ^9.0.0", @@ -424,17 +425,17 @@ } }, "node_modules/@angular-eslint/schematics": { - "version": "19.5.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-19.5.0.tgz", - "integrity": "sha512-1IgJPJDwlYdMWaY4L5amO83G8aHy5PXFkoL2ijlHIBxJFA2ltn4aMai1LdsntQTViJ3zsbK6L3xLHM26cDzeRw==", + "version": "19.8.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-19.8.0.tgz", + "integrity": "sha512-jBzRioyfFb8ikhtvQHeSmtc48YhNw+Kb4LN/DqTvPm0dYeWqz8qpIVlUdKUT7wcC46i0vfILjMCSbWEnkRjHiA==", "dev": true, "license": "MIT", "dependencies": { "@angular-devkit/core": ">= 19.0.0 < 20.0.0", "@angular-devkit/schematics": ">= 19.0.0 < 20.0.0", - "@angular-eslint/eslint-plugin": "19.5.0", - "@angular-eslint/eslint-plugin-template": "19.5.0", - "ignore": "7.0.4", + "@angular-eslint/eslint-plugin": "19.8.0", + "@angular-eslint/eslint-plugin-template": "19.8.0", + "ignore": "7.0.5", "semver": "7.7.2", "strip-json-comments": "3.1.1" } @@ -453,13 +454,13 @@ } }, "node_modules/@angular-eslint/template-parser": { - "version": "19.5.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-19.5.0.tgz", - "integrity": "sha512-Cgv0FXnJW0y+metp5a6QhGhWPKcuWAPZjza5KqjrRM3AtANqX4lH3mLplQ2DlN7On1zQEVQ3l8IQ2C6lxBHshA==", + "version": "19.8.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-19.8.0.tgz", + "integrity": "sha512-43hWc14pMc0LkqBD+ui7uF6NTUVpNrVnUSQqNCYsn3aoXeOdXKgKQEeBUhotKlxAOyAQjAet0tU24+IAW0xwkw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "19.5.0", + "@angular-eslint/bundled-angular-compiler": "19.8.0", "eslint-scope": "^8.0.2" }, "peerDependencies": { @@ -468,13 +469,13 @@ } }, "node_modules/@angular-eslint/utils": { - "version": "19.5.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-19.5.0.tgz", - "integrity": "sha512-s9ec5WAGppuqkYCU6yXg2jsz95Cbqo8eY5xNftI+4AiMP9YEwNwe212U1Z7OxHDvS1TbQGb6Vp0dJcgAmqs5bw==", + "version": "19.8.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-19.8.0.tgz", + "integrity": "sha512-ue3seSy4b+H5MN/m1m0SgyWr0XpDjfkGwyYo+uz2bhxpCyhZNzTBKeNRPkTTs+yeq9NhSKEhjvgUSA2nK5LqmA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "19.5.0" + "@angular-eslint/bundled-angular-compiler": "19.8.0" }, "peerDependencies": { "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", @@ -483,9 +484,9 @@ } }, "node_modules/@angular/animations": { - "version": "19.2.13", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-19.2.13.tgz", - "integrity": "sha512-x9LYcSndY9BdwuRxTx0gXvlLrvJyzjnWoaIoVLrAZWZbKfQh2+HK4XkclbzNvn8RMeoBpZZatcC3ZBC1TffjtA==", + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-19.2.14.tgz", + "integrity": "sha512-xhl8fLto5HHJdVj8Nb6EoBEiTAcXuWDYn1q5uHcGxyVH3kiwENWy/2OQXgCr2CuWo2e6hNUGzSLf/cjbsMNqEA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -494,19 +495,19 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "19.2.13", - "@angular/core": "19.2.13" + "@angular/common": "19.2.14", + "@angular/core": "19.2.14" } }, "node_modules/@angular/build": { - "version": "19.2.13", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-19.2.13.tgz", - "integrity": "sha512-ABcwhAB9DpsvXY7joRFSKiQCHJmCokVJK1Liuz0/AI9Xlp7spqaWqJcC1DVWO0645tUk4HhYmUh5a68REK1Q1A==", + "version": "19.2.15", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-19.2.15.tgz", + "integrity": "sha512-iE4fp4d5ALu702uoL6/YkjM2JlGEXZ5G+RVzq3W2jg/Ft6ISAQnRKB6mymtetDD6oD7i87e8uSu9kFVNBauX2w==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1902.13", + "@angular-devkit/architect": "0.1902.15", "@babel/core": "7.26.10", "@babel/helper-annotate-as-pure": "7.25.9", "@babel/helper-split-export-declaration": "7.24.7", @@ -546,7 +547,7 @@ "@angular/localize": "^19.0.0 || ^19.2.0-next.0", "@angular/platform-server": "^19.0.0 || ^19.2.0-next.0", "@angular/service-worker": "^19.0.0 || ^19.2.0-next.0", - "@angular/ssr": "^19.2.13", + "@angular/ssr": "^19.2.15", "karma": "^6.4.0", "less": "^4.2.0", "ng-packagr": "^19.0.0 || ^19.2.0-next.0", @@ -657,9 +658,9 @@ } }, "node_modules/@angular/build/node_modules/vite/node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.5", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.5.tgz", + "integrity": "sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg==", "dev": true, "funding": [ { @@ -677,7 +678,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -686,9 +687,9 @@ } }, "node_modules/@angular/cdk": { - "version": "19.2.17", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-19.2.17.tgz", - "integrity": "sha512-3jG33S+5+kqymCRwQlcSEWlY5rYwkKxe0onln+NXxT0/kteR02vWvv1+Li4/QqSr5JvsGHEhAFsZaR9QtOzbdA==", + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-19.2.18.tgz", + "integrity": "sha512-aGMHOYK/VV9PhxGTUDwiu/4ozoR/RKz8cimI+QjRxEBhzn4EPqjUDSganvlhmgS7cTN3+aqozdvF/GopMRJjLg==", "license": "MIT", "dependencies": { "parse5": "^7.1.2", @@ -701,18 +702,18 @@ } }, "node_modules/@angular/cli": { - "version": "19.2.13", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.2.13.tgz", - "integrity": "sha512-dDRCS73/lrItWx9j4SmwHR56GiZsW8ObNi2q9l/1ny813CG9K43STYFG/wJvGS7ZF3y5hvjIiJOwBx2YIouOIw==", + "version": "19.2.15", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.2.15.tgz", + "integrity": "sha512-YRIpARHWSOnWkHusUWTQgeUrPWMjWvtQrOkjWc6stF36z2KUzKMEng6EzUvH6sZolNSwVwOFpODEP0ut4aBkvQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1902.13", - "@angular-devkit/core": "19.2.13", - "@angular-devkit/schematics": "19.2.13", + "@angular-devkit/architect": "0.1902.15", + "@angular-devkit/core": "19.2.15", + "@angular-devkit/schematics": "19.2.15", "@inquirer/prompts": "7.3.2", "@listr2/prompt-adapter-inquirer": "2.0.18", - "@schematics/angular": "19.2.13", + "@schematics/angular": "19.2.15", "@yarnpkg/lockfile": "1.1.0", "ini": "5.0.0", "jsonc-parser": "3.3.1", @@ -735,9 +736,9 @@ } }, "node_modules/@angular/common": { - "version": "19.2.13", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-19.2.13.tgz", - "integrity": "sha512-k7I4bLH+bgI02VL81MaL0NcZPfVl153KAiARwk+ZlkmQjMnWlmsAHQ6054SWoNEXwP855ATR6YYDVqJh8TZaqw==", + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-19.2.14.tgz", + "integrity": "sha512-NcNklcuyqaTjOVGf7aru8APX9mjsnZ01gFZrn47BxHozhaR0EMRrotYQTdi8YdVjPkeYFYanVntSLfhyobq/jg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -746,14 +747,14 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "19.2.13", + "@angular/core": "19.2.14", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "19.2.13", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-19.2.13.tgz", - "integrity": "sha512-xAj1peVrQtb65NsULmz8ocH4QZ4ESG5YiiVzJ0tLz8t280xY+QhJiM6C0+jaCVHLXvZp0c7GEzsYjL6x1HmabQ==", + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-19.2.14.tgz", + "integrity": "sha512-ZqJDYOdhgKpVGNq3+n/Gbxma8DVYElDsoRe0tvNtjkWBVdaOxdZZUqmJ3kdCBsqD/aqTRvRBu0KGo9s2fCChkA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -763,9 +764,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "19.2.13", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-19.2.13.tgz", - "integrity": "sha512-SSuzKMcktvd6VexivDwhP7ctQBD6yyoo5E91I7Frn5nrvYNM+TIyYcXmJ4dgby5/GrPZGfm2sWl3ARr2vbCgtA==", + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-19.2.14.tgz", + "integrity": "sha512-e9/h86ETjoIK2yTLE9aUeMCKujdg/du2pq7run/aINjop4RtnNOw+ZlSTUa6R65lP5CVwDup1kPytpAoifw8cA==", "dev": true, "license": "MIT", "dependencies": { @@ -787,7 +788,7 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/compiler": "19.2.13", + "@angular/compiler": "19.2.14", "typescript": ">=5.5 <5.9" } }, @@ -837,9 +838,9 @@ } }, "node_modules/@angular/core": { - "version": "19.2.13", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-19.2.13.tgz", - "integrity": "sha512-HpzDI3TSQzVV2mmQ8KwH0JSLNlYNemNrEo3L3hcqqYwTzqFgAK4y1Q2Xym3yiRSLTenYhW5D4CQqOHUQ26HxwQ==", + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-19.2.14.tgz", + "integrity": "sha512-EVErpW9tGqJ/wNcAN3G/ErH8pHCJ8mM1E6bsJ8UJIpDTZkpqqYjBMtZS9YWH5n3KwUd1tAkAB2w8FK125AjDUQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -853,9 +854,9 @@ } }, "node_modules/@angular/forms": { - "version": "19.2.13", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-19.2.13.tgz", - "integrity": "sha512-g46KQFrBJhmknczlGEYvWVsPhk7ZI8WOuWkzWEl81Lf3ojEVA/OF8w4VwKZL7wOMKRxOUhuYq6tNPm8tBjtryw==", + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-19.2.14.tgz", + "integrity": "sha512-hWtDOj2B0AuRTf+nkMJeodnFpDpmEK9OIhIv1YxcRe73ooaxrIdjgugkElO8I9Tj0E4/7m117ezhWDUkbqm1zA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -864,22 +865,22 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "19.2.13", - "@angular/core": "19.2.13", - "@angular/platform-browser": "19.2.13", + "@angular/common": "19.2.14", + "@angular/core": "19.2.14", + "@angular/platform-browser": "19.2.14", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/material": { - "version": "19.2.17", - "resolved": "https://registry.npmjs.org/@angular/material/-/material-19.2.17.tgz", - "integrity": "sha512-IyA+KP+uUj3r9loqGJrj7qAiEBckj7EVIdV0jlYwqWIUyKWeJ3R88GmLPMH2BgtBU3R/WkS2blXDI0yvRhKfww==", + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-19.2.18.tgz", + "integrity": "sha512-xxedRQ9u7aiUYVrHAxASLUxnofN29xaqEGhBcHLAfOsFXdDMwDe/2ly79iKufwEs5BFBm3nfhJoarXZ3+8pucQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { - "@angular/cdk": "19.2.17", + "@angular/cdk": "19.2.18", "@angular/common": "^19.0.0 || ^20.0.0", "@angular/core": "^19.0.0 || ^20.0.0", "@angular/forms": "^19.0.0 || ^20.0.0", @@ -888,23 +889,23 @@ } }, "node_modules/@angular/material-luxon-adapter": { - "version": "19.2.17", - "resolved": "https://registry.npmjs.org/@angular/material-luxon-adapter/-/material-luxon-adapter-19.2.17.tgz", - "integrity": "sha512-pDtF10LjsFa1Tv0YSlHHSC+VK9edRihPkZ4CK3i5/RA6qJgeWdGGkC9gfCR8b1fkwinSpTaGSwI+L4u5d7acJg==", + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@angular/material-luxon-adapter/-/material-luxon-adapter-19.2.18.tgz", + "integrity": "sha512-OLoiRqowWlrCJth1b3bSpD2BOS/Rfn74fgZ78WZAPerlz9CL71XBrzNR7uEAdheM6U7XfO9+cBVCN6dCbGxyPQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/core": "^19.0.0 || ^20.0.0", - "@angular/material": "19.2.17", + "@angular/material": "19.2.18", "luxon": "^3.0.0" } }, "node_modules/@angular/platform-browser": { - "version": "19.2.13", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.2.13.tgz", - "integrity": "sha512-YeuRfGbo8qFepoAUoubk/1079wOown5Qgr9eAhgCXxoXb2rt87xbJF3YCSSim38SP3kK1rJQqP+Sr8n7ef+n5Q==", + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.2.14.tgz", + "integrity": "sha512-hzkT5nmA64oVBQl6PRjdL4dIFT1n7lfM9rm5cAoS+6LUUKRgiE2d421Kpn/Hz3jaCJfo+calMIdtSMIfUJBmww==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -913,9 +914,9 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/animations": "19.2.13", - "@angular/common": "19.2.13", - "@angular/core": "19.2.13" + "@angular/animations": "19.2.14", + "@angular/common": "19.2.14", + "@angular/core": "19.2.14" }, "peerDependenciesMeta": { "@angular/animations": { @@ -924,9 +925,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "19.2.13", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-19.2.13.tgz", - "integrity": "sha512-qbIPwnqkqQZ1sK56cbb2k/qtg+BKYicU6aS/YKfRrEfM9zFNyxfSCdKOwL7hogKGZKJulFfFKpi44wJcdW13rg==", + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-19.2.14.tgz", + "integrity": "sha512-Hfz0z1KDQmIdnFXVFCwCPykuIsHPkr1uW2aY396eARwZ6PK8i0Aadcm1ZOnpd3MR1bMyDrJo30VRS5kx89QWvA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -935,16 +936,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "19.2.13", - "@angular/compiler": "19.2.13", - "@angular/core": "19.2.13", - "@angular/platform-browser": "19.2.13" + "@angular/common": "19.2.14", + "@angular/compiler": "19.2.14", + "@angular/core": "19.2.14", + "@angular/platform-browser": "19.2.14" } }, "node_modules/@angular/platform-server": { - "version": "19.2.13", - "resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-19.2.13.tgz", - "integrity": "sha512-hv5bLiPNaSDmbcOfayGEsAzvl4RSz0Ps79uHzVgskQvN+cDnvtdUSkKoE6z/nDOxPvOSSmjnNG7DuCeP7UuHyA==", + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-19.2.14.tgz", + "integrity": "sha512-vmnRTDhlhahna6HbmzJh+qelXkyy1wBiJrOhnLR3UVeoBMBOTTjnTKtInfVrgZTMYcV9H8us480cvtSWzYsddA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0", @@ -954,17 +955,17 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "19.2.13", - "@angular/compiler": "19.2.13", - "@angular/core": "19.2.13", - "@angular/platform-browser": "19.2.13", + "@angular/common": "19.2.14", + "@angular/compiler": "19.2.14", + "@angular/core": "19.2.14", + "@angular/platform-browser": "19.2.14", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/router": { - "version": "19.2.13", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-19.2.13.tgz", - "integrity": "sha512-BZObWQtGkDv2WHyLVRRecGbLwalbI8kOXKaVgN5dqP4z/t5bpzYXZixPO9e0E1Ff0+m4tQalhTc84j8X7XZuTw==", + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-19.2.14.tgz", + "integrity": "sha512-cBTWY9Jx7YhbmDYDb7Hqz4Q7UNIMlKTkdKToJd2pbhIXyoS+kHVQrySmyca+jgvYMjWnIjsAEa3dpje12D4mFw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -973,16 +974,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "19.2.13", - "@angular/core": "19.2.13", - "@angular/platform-browser": "19.2.13", + "@angular/common": "19.2.14", + "@angular/core": "19.2.14", + "@angular/platform-browser": "19.2.14", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/service-worker": { - "version": "19.2.13", - "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-19.2.13.tgz", - "integrity": "sha512-6go8iP2kb/dYiOT6MMaZHUsUaijMTb2cc8Spnvge91gNr48QwiDbP7Tukg0EVTGbXfRX8oU3AflMVOQqDY2v1w==", + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-19.2.14.tgz", + "integrity": "sha512-ajH4kjsuzDvJNxnG18y8N47R0avXFKwOeLszoiirlr5160C+k4HmQvIbzcCjD5liW0OkmxJN1cMW6KdilP8/2w==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -994,7 +995,7 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "19.2.13", + "@angular/core": "19.2.14", "rxjs": "^6.5.3 || ^7.4.0" } }, @@ -3323,10 +3324,11 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.1.tgz", + "integrity": "sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", @@ -3337,10 +3339,11 @@ } }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3351,6 +3354,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -3426,10 +3430,11 @@ "dev": true }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3487,9 +3492,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.27.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz", - "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", + "version": "9.29.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.29.0.tgz", + "integrity": "sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==", "dev": true, "license": "MIT", "engines": { @@ -3504,6 +3509,7 @@ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } @@ -4703,23 +4709,23 @@ } }, "node_modules/@ngrx/effects": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-19.2.0.tgz", - "integrity": "sha512-DIoFdEdSehAMHUNTWIdl94HjhSh1ZRx0Rgtgp1TjHHyjLiS+vbMmDgPjrCkBv5lT/pEaKbHKnYxjY3CQiW2Hsg==", + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-19.2.1.tgz", + "integrity": "sha512-RZmTPOIC/h4JtySxh4Oa0ReQomxv4/+2er9vJ2IiuPDgUo7oE83iKZvB8uZUW/8y9dcu+MB6u0VjWM6rcbpCcA==", "license": "MIT", "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@angular/core": "^19.0.0", - "@ngrx/store": "19.2.0", + "@ngrx/store": "19.2.1", "rxjs": "^6.5.3 || ^7.5.0" } }, "node_modules/@ngrx/eslint-plugin": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/@ngrx/eslint-plugin/-/eslint-plugin-19.2.0.tgz", - "integrity": "sha512-dOHjJCAsv8aI53EwFKh33wCgrx8W58Y3cvlt/+pu8WCIjVkRwZKXZH05ufEPdqp6+xKp0NZuP7fe/F82vaNF4g==", + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/@ngrx/eslint-plugin/-/eslint-plugin-19.2.1.tgz", + "integrity": "sha512-6NquOZRc6y6uWdct7ygYh83KMenTCy4dz3+W3+g5wdohtvH2uHmdOnEuA4FW7PHSP0DJKwSN2GgmS6PYs4TEvg==", "dev": true, "license": "MIT", "dependencies": { @@ -4734,9 +4740,9 @@ } }, "node_modules/@ngrx/operators": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/@ngrx/operators/-/operators-19.2.0.tgz", - "integrity": "sha512-kv3hFlpWbZxfyILvQAJT2JNbsRGauUIj67U6zOUd8psD7qoJdtdUAZmr/LUgu/6/tweYDUj1mcQJfvaudik0ZQ==", + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/@ngrx/operators/-/operators-19.2.1.tgz", + "integrity": "sha512-umjSny5nWe7+a3XPeyMfE8vjhXD4ec6nA/KSV7bQA43Yt3eW8cQQr5ng7UZOkC0rbqcBGpSsJPt5thTeXiMXQg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -4746,9 +4752,9 @@ } }, "node_modules/@ngrx/router-store": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/@ngrx/router-store/-/router-store-19.2.0.tgz", - "integrity": "sha512-emR6Y+NIcFxFt1QsyDdMIVhkuGEzawGZM5yOo8A6kUZljzf88S/7tHXQRKLz1Vy2fpDRZDO6r/0eagW0JDMfLA==", + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/@ngrx/router-store/-/router-store-19.2.1.tgz", + "integrity": "sha512-4gI9A5Mnl52UEHskLKb2A6QXdHDdGr6eyBM940t16mI5RCXfkoSJNb5mQ/jXh2OZjhx9ponRVNmCaLwkxBMB5g==", "license": "MIT", "dependencies": { "tslib": "^2.0.0" @@ -4757,21 +4763,21 @@ "@angular/common": "^19.0.0", "@angular/core": "^19.0.0", "@angular/router": "^19.0.0", - "@ngrx/store": "19.2.0", + "@ngrx/store": "19.2.1", "rxjs": "^6.5.3 || ^7.5.0" } }, "node_modules/@ngrx/schematics": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/@ngrx/schematics/-/schematics-19.2.0.tgz", - "integrity": "sha512-nhgBHVqQgvJsqHK4j0zvi7ZETFXqNXWtNZLmCRaprKWNOwZga2LxkmVi93ei/1YHdK7KPdbUA6jNw9w2r8VQ6g==", + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/@ngrx/schematics/-/schematics-19.2.1.tgz", + "integrity": "sha512-6N7nyQ5QkHnFVAHPEHGiIKTgBqbwv/lS/vqbbuY4H4Gd4DYH/wAHcN97/EU+wVzlsrK7I1cCUEL2zwQ7WsbPdg==", "dev": true, "license": "MIT" }, "node_modules/@ngrx/store": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-19.2.0.tgz", - "integrity": "sha512-k2n/jLJZ75Z5rd5vPa2mXPYG/On2rFLiNdrccs9Dw2r+oJosORMlN5TbdsGHhVDFfjzbY9a7JbHUE3YOa69gqw==", + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-19.2.1.tgz", + "integrity": "sha512-c5vQId7YoAhM0y4HASrz9mtLju+28vJspd6OBlhPbBlSae8GN8m9S/oav+8LaSY19yh95cZ5B/nMcLNNWgL/jA==", "license": "MIT", "dependencies": { "tslib": "^2.0.0" @@ -4782,9 +4788,9 @@ } }, "node_modules/@ngrx/store-devtools": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/@ngrx/store-devtools/-/store-devtools-19.2.0.tgz", - "integrity": "sha512-AKlXHsuSRJgYYxmrXZ8WWnDxqgKMG0+HP+IIDmk5h5Z5RIkOLHk6ZGKbakhIiFlL8d16N+GcJ76rqUajaHT+0w==", + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/@ngrx/store-devtools/-/store-devtools-19.2.1.tgz", + "integrity": "sha512-gj1YO+4yl6D0l9vzLWdw07TQSu5UPKgsSLsNJfDLXraaLCUcB8voAp4J7zohN8qR5ixDuHeMoiSSVuklQ75u2w==", "dev": true, "license": "MIT", "dependencies": { @@ -4792,14 +4798,14 @@ }, "peerDependencies": { "@angular/core": "^19.0.0", - "@ngrx/store": "19.2.0", + "@ngrx/store": "19.2.1", "rxjs": "^6.5.3 || ^7.5.0" } }, "node_modules/@ngtools/webpack": { - "version": "19.2.13", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-19.2.13.tgz", - "integrity": "sha512-9dYfLsqWFTn1YVUiWydSp2bboaSW+byeZRFx8qeR7lsOkDGbm/idG68IXFHybHtZ3ptJ5fEeuw89RL47SQ61oA==", + "version": "19.2.15", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-19.2.15.tgz", + "integrity": "sha512-H37nop/wWMkSgoU2VvrMzanHePdLRRrX52nC5tT2ZhH3qP25+PrnMyw11PoLDLv3iWXC68uB1AiKNIT+jiQbuQ==", "dev": true, "license": "MIT", "engines": { @@ -5485,10 +5491,11 @@ } }, "node_modules/@pkgr/core": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.4.tgz", - "integrity": "sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz", + "integrity": "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==", "dev": true, + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.18.0 || >=16.0.0" }, @@ -5679,9 +5686,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.1.tgz", - "integrity": "sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.43.0.tgz", + "integrity": "sha512-ruf3hPWhjw6uDFsOAzmbNIvlXFXlBQ4nk57Sec8E8rUxs/AI4HD6xmiiasOOx/3QxS2f5eQMKTAwk7KHwpzr/Q==", "cpu": [ "riscv64" ], @@ -5778,14 +5785,14 @@ ] }, "node_modules/@schematics/angular": { - "version": "19.2.13", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-19.2.13.tgz", - "integrity": "sha512-SOpK4AwH0isXo7Y2SkgXLyGLMw4GxWPAun6sCLiprmop4KlqKGGALn4xIW0yjq0s5GS0Vx0FFjz8bBfPkgnawA==", + "version": "19.2.15", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-19.2.15.tgz", + "integrity": "sha512-dz/eoFQKG09POSygpEDdlCehFIMo35HUM2rVV8lx9PfQEibpbGwl1NNQYEbqwVjTyCyD/ILyIXCWPE+EfTnG4g==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.2.13", - "@angular-devkit/schematics": "19.2.13", + "@angular-devkit/core": "19.2.15", + "@angular-devkit/schematics": "19.2.15", "jsonc-parser": "3.3.1" }, "engines": { @@ -5795,9 +5802,10 @@ } }, "node_modules/@scicatproject/scicat-sdk-ts-angular": { - "version": "4.17.0", - "resolved": "https://registry.npmjs.org/@scicatproject/scicat-sdk-ts-angular/-/scicat-sdk-ts-angular-4.17.0.tgz", - "integrity": "sha512-f9wgZab5kDCr76nCgKF7Eoq5rQd588xDAuxiPl7qs1oNsY3zh9Tgk5t3GYwGOcBT+da+9kV/I4Ws6f+lqK3tJw==", + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/@scicatproject/scicat-sdk-ts-angular/-/scicat-sdk-ts-angular-4.17.1.tgz", + "integrity": "sha512-9eeHsMF7ck9r5XHJGrC1Oqw6qSV8QDIZB9BnD/co7JIpnuqy1PtBnBmssukbp44gm0zH/skxtg3A+syd/rJmQw==", + "license": "Unlicense", "dependencies": { "tslib": "^2.3.0" }, @@ -5959,9 +5967,9 @@ } }, "node_modules/@types/body-parser": { - "version": "1.19.5", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", - "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", "dev": true, "license": "MIT", "dependencies": { @@ -6042,9 +6050,9 @@ "dev": true }, "node_modules/@types/express": { - "version": "4.17.22", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.22.tgz", - "integrity": "sha512-eZUmSnhRX9YRSkplpz0N+k6NljUUn5l3EWZIKZvYzhvMphEuNiyyy1viH/ejgt66JWgALwC/gtSUAeQKtSwW/w==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", + "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6055,19 +6063,6 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", - "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/express/node_modules/@types/express-serve-static-core": { "version": "4.19.6", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", @@ -6087,9 +6082,9 @@ "dev": true }, "node_modules/@types/http-errors": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", "dev": true, "license": "MIT" }, @@ -6144,13 +6139,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.15.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", - "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", + "version": "24.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.3.tgz", + "integrity": "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.8.0" } }, "node_modules/@types/node-forge": { @@ -6185,9 +6180,9 @@ "license": "MIT" }, "node_modules/@types/send": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", - "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", "dev": true, "license": "MIT", "dependencies": { @@ -6206,9 +6201,9 @@ } }, "node_modules/@types/serve-static": { - "version": "1.15.7", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", - "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", "dev": true, "license": "MIT", "dependencies": { @@ -6284,17 +6279,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", - "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz", + "integrity": "sha512-STXcN6ebF6li4PxwNeFnqF8/2BNDvBupf2OPx2yWNzr6mKNGF7q49VM00Pz5FaomJyqvbXpY6PhO+T9w139YEQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/type-utils": "8.32.1", - "@typescript-eslint/utils": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/scope-manager": "8.34.1", + "@typescript-eslint/type-utils": "8.34.1", + "@typescript-eslint/utils": "8.34.1", + "@typescript-eslint/visitor-keys": "8.34.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -6308,20 +6303,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "@typescript-eslint/parser": "^8.34.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", - "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.1.tgz", + "integrity": "sha512-beu6o6QY4hJAgL1E8RaXNC071G4Kso2MGmJskCFQhRhg8VOH/FDbC8soP8NHN7e/Hdphwp8G8cE6OBzC8o41ZA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1" + "@typescript-eslint/types": "8.34.1", + "@typescript-eslint/visitor-keys": "8.34.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6332,14 +6327,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", - "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.1.tgz", + "integrity": "sha512-rjCNqqYPuMUF5ODD+hWBNmOitjBWghkGKJg6hiCHzUvXRy6rK22Jd3rwbP2Xi+R7oYVvIKhokHVhH41BxPV5mA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/project-service": "8.34.1", + "@typescript-eslint/tsconfig-utils": "8.34.1", + "@typescript-eslint/types": "8.34.1", + "@typescript-eslint/visitor-keys": "8.34.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -6359,16 +6356,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", - "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.1.tgz", + "integrity": "sha512-mqOwUdZ3KjtGk7xJJnLbHxTuWVn3GO2WZZuM+Slhkun4+qthLdXx32C8xIXbO1kfCECb3jIs3eoxK3eryk7aoQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1" + "@typescript-eslint/scope-manager": "8.34.1", + "@typescript-eslint/types": "8.34.1", + "@typescript-eslint/typescript-estree": "8.34.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6383,14 +6380,14 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", - "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.1.tgz", + "integrity": "sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "eslint-visitor-keys": "^4.2.0" + "@typescript-eslint/types": "8.34.1", + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6401,9 +6398,9 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -6430,16 +6427,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", - "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.34.1.tgz", + "integrity": "sha512-4O3idHxhyzjClSMJ0a29AcoK0+YwnEqzI6oz3vlRf3xw0zbzt15MzXwItOlnr5nIth6zlY2RENLsOPvhyrKAQA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/scope-manager": "8.34.1", + "@typescript-eslint/types": "8.34.1", + "@typescript-eslint/typescript-estree": "8.34.1", + "@typescript-eslint/visitor-keys": "8.34.1", "debug": "^4.3.4" }, "engines": { @@ -6455,14 +6452,14 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", - "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.1.tgz", + "integrity": "sha512-beu6o6QY4hJAgL1E8RaXNC071G4Kso2MGmJskCFQhRhg8VOH/FDbC8soP8NHN7e/Hdphwp8G8cE6OBzC8o41ZA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1" + "@typescript-eslint/types": "8.34.1", + "@typescript-eslint/visitor-keys": "8.34.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6473,14 +6470,16 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", - "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.1.tgz", + "integrity": "sha512-rjCNqqYPuMUF5ODD+hWBNmOitjBWghkGKJg6hiCHzUvXRy6rK22Jd3rwbP2Xi+R7oYVvIKhokHVhH41BxPV5mA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/project-service": "8.34.1", + "@typescript-eslint/tsconfig-utils": "8.34.1", + "@typescript-eslint/types": "8.34.1", + "@typescript-eslint/visitor-keys": "8.34.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -6500,14 +6499,14 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", - "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.1.tgz", + "integrity": "sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "eslint-visitor-keys": "^4.2.0" + "@typescript-eslint/types": "8.34.1", + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6518,9 +6517,9 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -6546,6 +6545,28 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.1.tgz", + "integrity": "sha512-nuHlOmFZfuRwLJKDGQOVc0xnQrAmuq1Mj/ISou5044y1ajGNp2BNliIqp7F2LPQ5sForz8lempMFCovfeS1XoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.34.1", + "@typescript-eslint/types": "^8.34.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, "node_modules/@typescript-eslint/scope-manager": { "version": "8.31.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.31.1.tgz", @@ -6578,15 +6599,32 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.1.tgz", + "integrity": "sha512-K4Sjdo4/xF9NEeA2khOb7Y5nY6NSXBnod87uniVYW9kHP+hNlDV8trUSFeynA2uxWam4gIWgWoygPrv9VMWrYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", - "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.34.1.tgz", + "integrity": "sha512-Tv7tCCr6e5m8hP4+xFugcrwTOucB8lshffJ6zf1mF1TbU67R+ntCc6DzLNKM+s/uzDyv8gLq7tufaAhIBYeV8g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/utils": "8.32.1", + "@typescript-eslint/typescript-estree": "8.34.1", + "@typescript-eslint/utils": "8.34.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -6603,14 +6641,14 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", - "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.1.tgz", + "integrity": "sha512-beu6o6QY4hJAgL1E8RaXNC071G4Kso2MGmJskCFQhRhg8VOH/FDbC8soP8NHN7e/Hdphwp8G8cE6OBzC8o41ZA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1" + "@typescript-eslint/types": "8.34.1", + "@typescript-eslint/visitor-keys": "8.34.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6621,14 +6659,16 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", - "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.1.tgz", + "integrity": "sha512-rjCNqqYPuMUF5ODD+hWBNmOitjBWghkGKJg6hiCHzUvXRy6rK22Jd3rwbP2Xi+R7oYVvIKhokHVhH41BxPV5mA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/project-service": "8.34.1", + "@typescript-eslint/tsconfig-utils": "8.34.1", + "@typescript-eslint/types": "8.34.1", + "@typescript-eslint/visitor-keys": "8.34.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -6648,16 +6688,16 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", - "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.1.tgz", + "integrity": "sha512-mqOwUdZ3KjtGk7xJJnLbHxTuWVn3GO2WZZuM+Slhkun4+qthLdXx32C8xIXbO1kfCECb3jIs3eoxK3eryk7aoQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1" + "@typescript-eslint/scope-manager": "8.34.1", + "@typescript-eslint/types": "8.34.1", + "@typescript-eslint/typescript-estree": "8.34.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6672,14 +6712,14 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", - "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.1.tgz", + "integrity": "sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "eslint-visitor-keys": "^4.2.0" + "@typescript-eslint/types": "8.34.1", + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6690,9 +6730,9 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -6719,9 +6759,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", - "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", + "version": "8.34.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.1.tgz", + "integrity": "sha512-rjLVbmE7HR18kDsjNIZQHxmv9RZwlgzavryL5Lnj2ujIRTeXlKtILHgRNmQ3j4daw7zd+mQgy+uyt6Zo6I0IGA==", "dev": true, "license": "MIT", "engines": { @@ -7081,10 +7121,11 @@ } }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -7832,9 +7873,10 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -8958,9 +9000,9 @@ "dev": true }, "node_modules/cypress": { - "version": "14.4.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-14.4.0.tgz", - "integrity": "sha512-/I59Fqxo7fqdiDi3IM2QKA65gZ7+PVejXg404/I8ZSq+NOnrmw+2pnMUJzpoNyg7KABcEBmgpkfAqhV98p7wJA==", + "version": "14.4.1", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-14.4.1.tgz", + "integrity": "sha512-YSGvVXtTqSGRTyHbaxHI5dHU/9xc5ymaTIM4BU85GKhj980y6XgA3fShSpj5DatS8knXMsAvYItQxVQFHGpUtw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -9980,19 +10022,19 @@ } }, "node_modules/eslint": { - "version": "9.27.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", - "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", + "version": "9.29.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.29.0.tgz", + "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.20.0", + "@eslint/config-array": "^0.20.1", "@eslint/config-helpers": "^0.2.1", "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.27.0", + "@eslint/js": "9.29.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -10004,9 +10046,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -10057,13 +10099,14 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.4.0.tgz", - "integrity": "sha512-BvQOvUhkVQM1i63iMETK9Hjud9QhqBnbtT1Zc642p9ynzBuCe5pybkOnvqZIBypXmMlsGcnU4HZ8sCTPfpAexA==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.4.1.tgz", + "integrity": "sha512-9dF+KuU/Ilkq27A8idRP7N2DH8iUR6qXcjF3FR2wETY21PZdBrIjwCau8oboyGj9b7etWmTGEeM8e7oOed6ZWg==", "dev": true, + "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.11.0" + "synckit": "^0.11.7" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -10087,10 +10130,11 @@ } }, "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -10131,20 +10175,22 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -10180,14 +10226,15 @@ } }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -10197,10 +10244,11 @@ } }, "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -11185,9 +11233,10 @@ "dev": true }, "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -11496,9 +11545,9 @@ } }, "node_modules/htmlparser2/node_modules/entities": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz", - "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -11687,9 +11736,9 @@ ] }, "node_modules/ignore": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", - "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { @@ -12419,23 +12468,23 @@ } }, "node_modules/jasmine": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-5.7.1.tgz", - "integrity": "sha512-E/4fkRNy/9ALz6z3Z3/tYXFAohoznVy7In9FWutG2fqBSkILJHFzbgZtHJUw5UrL3jgUQ4sdGYOVZ5KpSXYjGw==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-5.8.0.tgz", + "integrity": "sha512-1V6HGa0+TMoMY20+/vp++RqLlL1noupV8awzV6CiPuICC0g7iKZ9z87zV2KyelRyoig0G1lHn7ueElXVMGVagg==", "dev": true, "license": "MIT", "dependencies": { "glob": "^10.2.2", - "jasmine-core": "~5.7.0" + "jasmine-core": "~5.8.0" }, "bin": { "jasmine": "bin/jasmine.js" } }, "node_modules/jasmine-core": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.7.1.tgz", - "integrity": "sha512-QnurrtpKsPoixxG2R3d1xP0St/2kcX5oTZyDyQJMY+Vzi/HUlu1kGm+2V8Tz+9lV991leB1l0xcsyz40s9xOOw==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.8.0.tgz", + "integrity": "sha512-Q9dqmpUAfptwyueW3+HqBOkSuYd9I/clZSSfN97wXE/Nr2ROFNCwIBEC1F6kb3QXS9Fcz0LjFYSDQT+BiwjuhA==", "dev": true, "license": "MIT" }, @@ -12812,10 +12861,11 @@ } }, "node_modules/karma-coverage/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -12928,10 +12978,11 @@ } }, "node_modules/karma/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -13890,9 +13941,9 @@ } }, "node_modules/mathjs": { - "version": "14.5.0", - "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-14.5.0.tgz", - "integrity": "sha512-/zg7ikD0zfb9Qogqr0EXIMwyHD9GKnkrAz7EGXvOD2e+4yQdKwQ4xqj5Mhk0TWT39IWhu3Pen3GXiUufkGHgOg==", + "version": "14.5.2", + "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-14.5.2.tgz", + "integrity": "sha512-51U6hp7j4M4Rj+l+q2KbmXAV9EhQVQzUdw1wE67RnUkKKq5ibxdrl9Ky2YkSUEIc2+VU8/IsThZNu6QSHUoyTA==", "license": "Apache-2.0", "dependencies": { "@babel/runtime": "^7.26.10", @@ -16761,9 +16812,9 @@ } }, "node_modules/shell-quote": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz", - "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", "dev": true, "license": "MIT", "engines": { @@ -17487,13 +17538,13 @@ } }, "node_modules/synckit": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.4.tgz", - "integrity": "sha512-Q/XQKRaJiLiFIBNN+mndW7S/RHxvwzuZS6ZwmRzUBqJBv/5QIKCEwkBC8GBf8EQJKYnaFs0wOZbKTXBPj8L9oQ==", + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz", + "integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==", "dev": true, + "license": "MIT", "dependencies": { - "@pkgr/core": "^0.2.3", - "tslib": "^2.8.1" + "@pkgr/core": "^0.2.4" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -18137,10 +18188,11 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "dev": true, + "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", @@ -18494,9 +18546,9 @@ } }, "node_modules/vite/node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.1.tgz", - "integrity": "sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.43.0.tgz", + "integrity": "sha512-Krjy9awJl6rKbruhQDgivNbD1WuLb8xAclM4IR4cN5pHGAs2oIMMQJEiC3IC/9TZJ+QZkmZhlMO/6MBGxPidpw==", "cpu": [ "arm" ], @@ -18509,9 +18561,9 @@ "peer": true }, "node_modules/vite/node_modules/@rollup/rollup-android-arm64": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.1.tgz", - "integrity": "sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.43.0.tgz", + "integrity": "sha512-ss4YJwRt5I63454Rpj+mXCXicakdFmKnUNxr1dLK+5rv5FJgAxnN7s31a5VchRYxCFWdmnDWKd0wbAdTr0J5EA==", "cpu": [ "arm64" ], @@ -18524,9 +18576,9 @@ "peer": true }, "node_modules/vite/node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.1.tgz", - "integrity": "sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.43.0.tgz", + "integrity": "sha512-eKoL8ykZ7zz8MjgBenEF2OoTNFAPFz1/lyJ5UmmFSz5jW+7XbH1+MAgCVHy72aG59rbuQLcJeiMrP8qP5d/N0A==", "cpu": [ "arm64" ], @@ -18539,9 +18591,9 @@ "peer": true }, "node_modules/vite/node_modules/@rollup/rollup-darwin-x64": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.1.tgz", - "integrity": "sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.43.0.tgz", + "integrity": "sha512-SYwXJgaBYW33Wi/q4ubN+ldWC4DzQY62S4Ll2dgfr/dbPoF50dlQwEaEHSKrQdSjC6oIe1WgzosoaNoHCdNuMg==", "cpu": [ "x64" ], @@ -18554,9 +18606,9 @@ "peer": true }, "node_modules/vite/node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.1.tgz", - "integrity": "sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.43.0.tgz", + "integrity": "sha512-SV+U5sSo0yujrjzBF7/YidieK2iF6E7MdF6EbYxNz94lA+R0wKl3SiixGyG/9Klab6uNBIqsN7j4Y/Fya7wAjQ==", "cpu": [ "arm64" ], @@ -18569,9 +18621,9 @@ "peer": true }, "node_modules/vite/node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.1.tgz", - "integrity": "sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.43.0.tgz", + "integrity": "sha512-J7uCsiV13L/VOeHJBo5SjasKiGxJ0g+nQTrBkAsmQBIdil3KhPnSE9GnRon4ejX1XDdsmK/l30IYLiAaQEO0Cg==", "cpu": [ "x64" ], @@ -18584,9 +18636,9 @@ "peer": true }, "node_modules/vite/node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.1.tgz", - "integrity": "sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.43.0.tgz", + "integrity": "sha512-gTJ/JnnjCMc15uwB10TTATBEhK9meBIY+gXP4s0sHD1zHOaIh4Dmy1X9wup18IiY9tTNk5gJc4yx9ctj/fjrIw==", "cpu": [ "arm" ], @@ -18599,9 +18651,9 @@ "peer": true }, "node_modules/vite/node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.1.tgz", - "integrity": "sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.43.0.tgz", + "integrity": "sha512-ZJ3gZynL1LDSIvRfz0qXtTNs56n5DI2Mq+WACWZ7yGHFUEirHBRt7fyIk0NsCKhmRhn7WAcjgSkSVVxKlPNFFw==", "cpu": [ "arm" ], @@ -18614,9 +18666,9 @@ "peer": true }, "node_modules/vite/node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.1.tgz", - "integrity": "sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.43.0.tgz", + "integrity": "sha512-8FnkipasmOOSSlfucGYEu58U8cxEdhziKjPD2FIa0ONVMxvl/hmONtX/7y4vGjdUhjcTHlKlDhw3H9t98fPvyA==", "cpu": [ "arm64" ], @@ -18629,9 +18681,9 @@ "peer": true }, "node_modules/vite/node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.1.tgz", - "integrity": "sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.43.0.tgz", + "integrity": "sha512-KPPyAdlcIZ6S9C3S2cndXDkV0Bb1OSMsX0Eelr2Bay4EsF9yi9u9uzc9RniK3mcUGCLhWY9oLr6er80P5DE6XA==", "cpu": [ "arm64" ], @@ -18644,9 +18696,9 @@ "peer": true }, "node_modules/vite/node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.1.tgz", - "integrity": "sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.43.0.tgz", + "integrity": "sha512-HPGDIH0/ZzAZjvtlXj6g+KDQ9ZMHfSP553za7o2Odegb/BEfwJcR0Sw0RLNpQ9nC6Gy8s+3mSS9xjZ0n3rhcYg==", "cpu": [ "loong64" ], @@ -18659,9 +18711,9 @@ "peer": true }, "node_modules/vite/node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.1.tgz", - "integrity": "sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.43.0.tgz", + "integrity": "sha512-gEmwbOws4U4GLAJDhhtSPWPXUzDfMRedT3hFMyRAvM9Mrnj+dJIFIeL7otsv2WF3D7GrV0GIewW0y28dOYWkmw==", "cpu": [ "ppc64" ], @@ -18674,9 +18726,9 @@ "peer": true }, "node_modules/vite/node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.1.tgz", - "integrity": "sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.43.0.tgz", + "integrity": "sha512-XXKvo2e+wFtXZF/9xoWohHg+MuRnvO29TI5Hqe9xwN5uN8NKUYy7tXUG3EZAlfchufNCTHNGjEx7uN78KsBo0g==", "cpu": [ "riscv64" ], @@ -18689,9 +18741,9 @@ "peer": true }, "node_modules/vite/node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.1.tgz", - "integrity": "sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.43.0.tgz", + "integrity": "sha512-QmNIAqDiEMEvFV15rsSnjoSmO0+eJLoKRD9EAa9rrYNwO/XRCtOGM3A5A0X+wmG+XRrw9Fxdsw+LnyYiZWWcVw==", "cpu": [ "s390x" ], @@ -18704,9 +18756,9 @@ "peer": true }, "node_modules/vite/node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.1.tgz", - "integrity": "sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.43.0.tgz", + "integrity": "sha512-jAHr/S0iiBtFyzjhOkAics/2SrXE092qyqEg96e90L3t9Op8OTzS6+IX0Fy5wCt2+KqeHAkti+eitV0wvblEoQ==", "cpu": [ "x64" ], @@ -18719,9 +18771,9 @@ "peer": true }, "node_modules/vite/node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.1.tgz", - "integrity": "sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.43.0.tgz", + "integrity": "sha512-3yATWgdeXyuHtBhrLt98w+5fKurdqvs8B53LaoKD7P7H7FKOONLsBVMNl9ghPQZQuYcceV5CDyPfyfGpMWD9mQ==", "cpu": [ "x64" ], @@ -18734,9 +18786,9 @@ "peer": true }, "node_modules/vite/node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.1.tgz", - "integrity": "sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.43.0.tgz", + "integrity": "sha512-wVzXp2qDSCOpcBCT5WRWLmpJRIzv23valvcTwMHEobkjippNf+C3ys/+wf07poPkeNix0paTNemB2XrHr2TnGw==", "cpu": [ "arm64" ], @@ -18749,9 +18801,9 @@ "peer": true }, "node_modules/vite/node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.1.tgz", - "integrity": "sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.43.0.tgz", + "integrity": "sha512-fYCTEyzf8d+7diCw8b+asvWDCLMjsCEA8alvtAutqJOJp/wL5hs1rWSqJ1vkjgW0L2NB4bsYJrpKkiIPRR9dvw==", "cpu": [ "ia32" ], @@ -18764,9 +18816,9 @@ "peer": true }, "node_modules/vite/node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.1.tgz", - "integrity": "sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.43.0.tgz", + "integrity": "sha512-SnGhLiE5rlK0ofq8kzuDkM0g7FN1s5VYY+YSMTibP7CqShxCQvqtNxTARS4xX4PFJfHjG0ZQYX9iGzI3FQh5Aw==", "cpu": [ "x64" ], @@ -18779,9 +18831,9 @@ "peer": true }, "node_modules/vite/node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.5", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.5.tgz", + "integrity": "sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg==", "dev": true, "funding": [ { @@ -18800,7 +18852,7 @@ "license": "MIT", "peer": true, "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -18809,9 +18861,9 @@ } }, "node_modules/vite/node_modules/rollup": { - "version": "4.41.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.1.tgz", - "integrity": "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.43.0.tgz", + "integrity": "sha512-wdN2Kd3Twh8MAEOEJZsuxuLKCsBEo4PVNLK6tQWAn10VhsVewQLzcucMgLolRlhFybGxfclbPeEYBaP6RvUFGg==", "dev": true, "license": "MIT", "peer": true, @@ -18826,26 +18878,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.41.1", - "@rollup/rollup-android-arm64": "4.41.1", - "@rollup/rollup-darwin-arm64": "4.41.1", - "@rollup/rollup-darwin-x64": "4.41.1", - "@rollup/rollup-freebsd-arm64": "4.41.1", - "@rollup/rollup-freebsd-x64": "4.41.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.41.1", - "@rollup/rollup-linux-arm-musleabihf": "4.41.1", - "@rollup/rollup-linux-arm64-gnu": "4.41.1", - "@rollup/rollup-linux-arm64-musl": "4.41.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.41.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1", - "@rollup/rollup-linux-riscv64-gnu": "4.41.1", - "@rollup/rollup-linux-riscv64-musl": "4.41.1", - "@rollup/rollup-linux-s390x-gnu": "4.41.1", - "@rollup/rollup-linux-x64-gnu": "4.41.1", - "@rollup/rollup-linux-x64-musl": "4.41.1", - "@rollup/rollup-win32-arm64-msvc": "4.41.1", - "@rollup/rollup-win32-ia32-msvc": "4.41.1", - "@rollup/rollup-win32-x64-msvc": "4.41.1", + "@rollup/rollup-android-arm-eabi": "4.43.0", + "@rollup/rollup-android-arm64": "4.43.0", + "@rollup/rollup-darwin-arm64": "4.43.0", + "@rollup/rollup-darwin-x64": "4.43.0", + "@rollup/rollup-freebsd-arm64": "4.43.0", + "@rollup/rollup-freebsd-x64": "4.43.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.43.0", + "@rollup/rollup-linux-arm-musleabihf": "4.43.0", + "@rollup/rollup-linux-arm64-gnu": "4.43.0", + "@rollup/rollup-linux-arm64-musl": "4.43.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.43.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.43.0", + "@rollup/rollup-linux-riscv64-gnu": "4.43.0", + "@rollup/rollup-linux-riscv64-musl": "4.43.0", + "@rollup/rollup-linux-s390x-gnu": "4.43.0", + "@rollup/rollup-linux-x64-gnu": "4.43.0", + "@rollup/rollup-linux-x64-musl": "4.43.0", + "@rollup/rollup-win32-arm64-msvc": "4.43.0", + "@rollup/rollup-win32-ia32-msvc": "4.43.0", + "@rollup/rollup-win32-x64-msvc": "4.43.0", "fsevents": "~2.3.2" } }, @@ -18975,15 +19027,16 @@ } }, "node_modules/webpack-dev-server": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.0.tgz", - "integrity": "sha512-90SqqYXA2SK36KcT6o1bvwvZfJFcmoamqeJY7+boioffX9g9C0wjjJRGUrQIuh43pb0ttX7+ssavmj/WN2RHtA==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.2.tgz", + "integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==", "dev": true, "license": "MIT", "dependencies": { "@types/bonjour": "^3.5.13", "@types/connect-history-api-fallback": "^1.5.4", "@types/express": "^4.17.21", + "@types/express-serve-static-core": "^4.17.21", "@types/serve-index": "^1.9.4", "@types/serve-static": "^1.15.5", "@types/sockjs": "^0.3.36", @@ -18996,7 +19049,7 @@ "connect-history-api-fallback": "^2.0.0", "express": "^4.21.2", "graceful-fs": "^4.2.6", - "http-proxy-middleware": "^2.0.7", + "http-proxy-middleware": "^2.0.9", "ipaddr.js": "^2.1.0", "launch-editor": "^2.6.1", "open": "^10.0.3", diff --git a/package.json b/package.json index f023331d0..246c1d445 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "@ngrx/store": "^19.1.0", "@ngx-translate/core": "^16.0.4", "@ngxmc/datetime-picker": "^19.3.1", - "@scicatproject/scicat-sdk-ts-angular": "^4.17.0", + "@scicatproject/scicat-sdk-ts-angular": "^4.17.1", "autolinker": "^4.0.0", "deep-equal": "^2.0.5", "exceljs": "^4.3.0", @@ -76,7 +76,7 @@ "@types/jasmine": "^5.1.0", "@types/lodash-es": "^4.17.12", "@types/luxon": "^3.3.0", - "@types/node": "^22.0.0", + "@types/node": "^24.0.3", "@types/shortid": "2.2.0", "@types/source-map-support": "^0.5.3", "@typescript-eslint/eslint-plugin": "^8.32.0", diff --git a/src/app/_layout/app-header/app-header.component.ts b/src/app/_layout/app-header/app-header.component.ts index dd1db860f..543e4a6de 100644 --- a/src/app/_layout/app-header/app-header.component.ts +++ b/src/app/_layout/app-header/app-header.component.ts @@ -47,9 +47,9 @@ export class AppHeaderComponent implements OnInit { login(): void { if (this.config.skipSciCatLoginPageEnabled) { - const returnURL = encodeURIComponent(this.router.url); + const returnUrl = encodeURIComponent(this.router.url); for (const endpoint of this.oAuth2Endpoints) { - this.document.location.href = `${this.config.lbBaseURL}/${endpoint.authURL}?returnURL=${returnURL}`; + this.document.location.href = `${this.config.lbBaseURL}/${endpoint.authURL}?returnUrl=${returnUrl}`; } } else { this.router.navigateByUrl("/login"); diff --git a/src/app/app-config.service.spec.ts b/src/app/app-config.service.spec.ts index e276c8dec..1e60a28c6 100644 --- a/src/app/app-config.service.spec.ts +++ b/src/app/app-config.service.spec.ts @@ -1,10 +1,14 @@ import { HttpClient } from "@angular/common/http"; import { TestBed } from "@angular/core/testing"; -import { AppConfig, AppConfigService, HelpMessages } from "app-config.service"; +import { + AppConfigInterface, + AppConfigService, + HelpMessages, +} from "app-config.service"; import { of } from "rxjs"; import { MockHttp } from "shared/MockStubs"; -const appConfig: AppConfig = { +const appConfig: AppConfigInterface = { skipSciCatLoginPageEnabled: false, accessTokenPrefix: "", addDatasetEnabled: true, @@ -128,7 +132,7 @@ const appConfig: AppConfig = { name: "select", order: 0, type: "standard", - enabled: true, + enabled: false, }, { name: "pid", diff --git a/src/app/app-config.service.ts b/src/app/app-config.service.ts index c906f8a98..285d15e32 100644 --- a/src/app/app-config.service.ts +++ b/src/app/app-config.service.ts @@ -35,7 +35,7 @@ export class HelpMessages { } } -export interface AppConfig { +export interface AppConfigInterface { skipSciCatLoginPageEnabled?: boolean; accessTokenPrefix: string; addDatasetEnabled: boolean; @@ -108,7 +108,9 @@ export interface AppConfig { dateFormat?: string; } -@Injectable() +@Injectable({ + providedIn: "root", +}) export class AppConfigService { private appConfig: object = {}; @@ -132,11 +134,11 @@ export class AppConfigService { } } - getConfig(): AppConfig { + getConfig(): AppConfigInterface { if (!this.appConfig) { console.error("AppConfigService: Configuration not loaded!"); } - return this.appConfig as AppConfig; + return this.appConfig as AppConfigInterface; } } diff --git a/src/app/app.component.ts b/src/app/app.component.ts index da51bf273..6da07ec4b 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -23,7 +23,7 @@ import { selectUserMessage, } from "state-management/selectors/user.selectors"; import { MessageType } from "state-management/models"; -import { AppConfigService, AppConfig as Config } from "app-config.service"; +import { AppConfigService, AppConfigInterface } from "app-config.service"; import { Configuration } from "@scicatproject/scicat-sdk-ts-angular"; @Component({ @@ -36,7 +36,7 @@ import { Configuration } from "@scicatproject/scicat-sdk-ts-angular"; export class AppComponent implements OnDestroy, OnInit, AfterViewChecked { loading$ = this.store.select(selectIsLoading); - config: Config; + config: AppConfigInterface; title: string; facility: string; diff --git a/src/app/datasets/dashboard/dashboard.component.html b/src/app/datasets/dashboard/dashboard.component.html index 53d82af5c..af17fa48f 100644 --- a/src/app/datasets/dashboard/dashboard.component.html +++ b/src/app/datasets/dashboard/dashboard.component.html @@ -1,21 +1,15 @@ - - + +
- + add_circle Create Dataset @@ -31,27 +25,13 @@
+ [selectedSets]="selectedSets$ | async"> - + (pageChange)="onPageChange($event)">
- - - - - + \ No newline at end of file diff --git a/src/app/datasets/dashboard/dashboard.component.spec.ts b/src/app/datasets/dashboard/dashboard.component.spec.ts index be1c837c4..dc7f23a4c 100644 --- a/src/app/datasets/dashboard/dashboard.component.spec.ts +++ b/src/app/datasets/dashboard/dashboard.component.spec.ts @@ -20,22 +20,15 @@ import { addDatasetAction, changePageAction, } from "state-management/actions/datasets.actions"; -import { - selectColumnAction, - deselectColumnAction, -} from "state-management/actions/user.actions"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; -import { SelectColumnEvent } from "datasets/dataset-table-settings/dataset-table-settings.component"; import { provideMockStore } from "@ngrx/store/testing"; import { selectSelectedDatasets } from "state-management/selectors/datasets.selectors"; -import { TableColumn } from "state-management/models"; import { selectColumns, selectIsLoggedIn, } from "state-management/selectors/user.selectors"; import { MatDialog, MatDialogModule } from "@angular/material/dialog"; -import { MatSidenav, MatSidenavModule } from "@angular/material/sidenav"; -import { MatCheckboxChange } from "@angular/material/checkbox"; +import { MatSidenavModule } from "@angular/material/sidenav"; import { MatCardModule } from "@angular/material/card"; import { MatIconModule } from "@angular/material/icon"; import { AppConfigService } from "app-config.service"; @@ -129,91 +122,6 @@ describe("DashboardComponent", () => { expect(component).toBeTruthy(); }); - describe("#onSettingsClick()", () => { - it("should toggle the sideNav", () => { - const toggleSpy = spyOn(component.sideNav, "toggle"); - - component.onSettingsClick(); - - expect(toggleSpy).toHaveBeenCalled(); - }); - - it("should not clear the search column if sidenav is open", () => { - component.sideNav.opened = false; - // The opened status is toggled when onSettingsClick is called - component.onSettingsClick(); - - expect(component.clearColumnSearch).toEqual(false); - }); - - it("should clear the search column if sidenav is closed", () => { - component.sideNav.opened = true; - // The opened status is toggled when onSettingsClick is called - component.onSettingsClick(); - - expect(component.clearColumnSearch).toEqual(true); - }); - }); - - describe("#onCloseClick()", () => { - it("should close the sideNav", () => { - const closeSpy = spyOn(component.sideNav, "close"); - - component.onCloseClick(); - - expect(closeSpy).toHaveBeenCalled(); - }); - }); - - describe("#onSelectColumn()", () => { - const column: TableColumn = { - name: "test", - order: 0, - type: "standard", - enabled: false, - }; - - it("should dispatch a selectColumnAction if checkBoxChange.checked is true", () => { - dispatchSpy = spyOn(store, "dispatch"); - - const checkBoxChange = { - checked: true, - } as MatCheckboxChange; - - const event: SelectColumnEvent = { - checkBoxChange, - column, - }; - - component.onSelectColumn(event); - - expect(dispatchSpy).toHaveBeenCalledTimes(1); - expect(dispatchSpy).toHaveBeenCalledWith( - selectColumnAction({ name: column.name, columnType: column.type }), - ); - }); - - it("should dispatch a deselectColumnAction if checkBoxChange.checked is false", () => { - dispatchSpy = spyOn(store, "dispatch"); - - const checkBoxChange = { - checked: false, - } as MatCheckboxChange; - - const event: SelectColumnEvent = { - checkBoxChange, - column, - }; - - component.onSelectColumn(event); - - expect(dispatchSpy).toHaveBeenCalledTimes(1); - expect(dispatchSpy).toHaveBeenCalledWith( - deselectColumnAction({ name: column.name, columnType: column.type }), - ); - }); - }); - describe("#onRowClick()", () => { it("should navigate to a dataset", () => { component.onRowClick(dataset); @@ -296,32 +204,4 @@ describe("DashboardComponent", () => { ); }); }); - - describe("#tableColumn$ observable", () => { - it("should show 'select' column when user is logged in", () => { - const testColumn: TableColumn = { - name: "test", - order: 0, - type: "standard", - enabled: false, - }; - const selectColumn: TableColumn = { - name: "select", - order: 1, - type: "standard", - enabled: true, - }; - selectColumns.setResult([testColumn, selectColumn]); - selectIsLoggedIn.setResult(true); - - component.tableColumns$.subscribe((result) => { - expect(result.length).toEqual(2); - }); - - selectIsLoggedIn.setResult(false); - component.tableColumns$.subscribe((result) => - expect(result).toEqual([testColumn]), - ); - }); - }); }); diff --git a/src/app/datasets/dashboard/dashboard.component.ts b/src/app/datasets/dashboard/dashboard.component.ts index 3e7413ad6..5c1c6bb8d 100644 --- a/src/app/datasets/dashboard/dashboard.component.ts +++ b/src/app/datasets/dashboard/dashboard.component.ts @@ -28,23 +28,19 @@ import { distinctUntilChanged, filter, map, take } from "rxjs/operators"; import { MatDialog } from "@angular/material/dialog"; import { MatSidenav } from "@angular/material/sidenav"; import { AddDatasetDialogComponent } from "datasets/add-dataset-dialog/add-dataset-dialog.component"; -import { combineLatest, Subscription } from "rxjs"; +import { combineLatest, Subscription, lastValueFrom } from "rxjs"; import { selectProfile, selectCurrentUser, selectColumns, selectIsLoggedIn, + selectHasFetchedSettings, } from "state-management/selectors/user.selectors"; import { OutputDatasetObsoleteDto, ReturnedUserDto, } from "@scicatproject/scicat-sdk-ts-angular"; -import { - selectColumnAction, - deselectColumnAction, - loadDefaultSettings, -} from "state-management/actions/user.actions"; -import { SelectColumnEvent } from "datasets/dataset-table-settings/dataset-table-settings.component"; +import { loadDefaultSettings } from "state-management/actions/user.actions"; import { AppConfigService } from "app-config.service"; @Component({ @@ -61,15 +57,8 @@ export class DashboardComponent implements OnInit, OnDestroy { loggedIn$ = this.store.select(selectIsLoggedIn); selectedSets$ = this.store.select(selectSelectedDatasets); selectColumns$ = this.store.select(selectColumns); + selectHasFetchedSettings$ = this.store.select(selectHasFetchedSettings); - tableColumns$ = combineLatest([this.selectColumns$, this.loggedIn$]).pipe( - map(([columns, loggedIn]) => - columns.filter((column) => loggedIn || column.name !== "select"), - ), - ); - selectableColumns$ = this.selectColumns$.pipe( - map((columns) => columns.filter((column) => column.name !== "select")), - ); public nonEmpty$ = this.store.select(selectIsBatchNonEmpty); subscriptions: Subscription[] = []; @@ -97,33 +86,6 @@ export class DashboardComponent implements OnInit, OnDestroy { ); } - onSettingsClick(): void { - this.sideNav.toggle(); - if (this.sideNav.opened) { - this.clearColumnSearch = false; - } else { - this.clearColumnSearch = true; - } - } - - onCloseClick(): void { - this.clearColumnSearch = true; - this.sideNav.close(); - } - - onSelectColumn(event: SelectColumnEvent): void { - const { checkBoxChange, column } = event; - if (checkBoxChange.checked) { - this.store.dispatch( - selectColumnAction({ name: column.name, columnType: column.type }), - ); - } else if (!checkBoxChange.checked) { - this.store.dispatch( - deselectColumnAction({ name: column.name, columnType: column.type }), - ); - } - } - onRowClick(dataset: OutputDatasetObsoleteDto): void { const pid = encodeURIComponent(dataset.pid); this.router.navigateByUrl("/datasets/" + pid); @@ -178,12 +140,29 @@ export class DashboardComponent implements OnInit, OnDestroy { this.store.dispatch(fetchMetadataKeysAction()); this.subscriptions.push( - combineLatest([this.pagination$, this.readyToFetch$, this.loggedIn$]) + combineLatest([ + this.pagination$, + this.readyToFetch$, + this.loggedIn$, + this.selectHasFetchedSettings$, + ]) .pipe( - map(([pagination, , loggedIn]) => [pagination, loggedIn]), + map(([pagination, , loggedIn, hasFetchedSettings]) => [ + pagination, + loggedIn, + hasFetchedSettings, + ]), distinctUntilChanged(deepEqual), ) - .subscribe(([pagination, loggedIn]) => { + .subscribe(async ([pagination, loggedIn]) => { + const hasFetchedSettings = await lastValueFrom( + this.selectHasFetchedSettings$.pipe(take(1)), + ); + + if (!hasFetchedSettings) { + return; + } + this.store.dispatch(fetchDatasetsAction()); this.store.dispatch(fetchFacetCountsAction()); this.router.navigate(["/datasets"], { diff --git a/src/app/datasets/dataset-detail/dataset-detail-dynamic/dataset-detail-dynamic.component.html b/src/app/datasets/dataset-detail/dataset-detail-dynamic/dataset-detail-dynamic.component.html index a840501b2..0910b29dd 100644 --- a/src/app/datasets/dataset-detail/dataset-detail-dynamic/dataset-detail-dynamic.component.html +++ b/src/app/datasets/dataset-detail/dataset-detail-dynamic/dataset-detail-dynamic.component.html @@ -76,7 +76,12 @@ {{ value ?? "-" }} diff --git a/src/app/datasets/dataset-detail/dataset-detail-dynamic/dataset-detail-dynamic.component.spec.ts b/src/app/datasets/dataset-detail/dataset-detail-dynamic/dataset-detail-dynamic.component.spec.ts index 98fe7ad01..85e5b957a 100644 --- a/src/app/datasets/dataset-detail/dataset-detail-dynamic/dataset-detail-dynamic.component.spec.ts +++ b/src/app/datasets/dataset-detail/dataset-detail-dynamic/dataset-detail-dynamic.component.spec.ts @@ -16,6 +16,7 @@ import { TranslationObject, } from "@ngx-translate/core"; import { DatasetDetailDynamicComponent } from "./dataset-detail-dynamic.component"; +import { InternalLinkType } from "state-management/models"; class MockTranslateLoader implements TranslateLoader { getTranslation(): Observable { return of({}); @@ -75,6 +76,176 @@ describe("DatasetDetailDynamicComponent", () => { expect(component).toBeTruthy(); }); + describe("getNestedValue with instrument name resolution", () => { + it("should return instrument name when path is 'instrumentName' and instrument exists", () => { + component.instrument = { + pid: "instrument1", + name: "Test Instrument", + } as any; + const dataset = {} as any; + const result = component.getNestedValue(dataset, "instrumentName"); + expect(result).toBe("Test Instrument"); + }); + + it("should return '-' when path is 'instrumentName' but instrument has no name", () => { + component.instrument = { pid: "instrument1" } as any; + const dataset = {} as any; + const result = component.getNestedValue(dataset, "instrumentName"); + expect(result).toBe("-"); + }); + + it("should return undefined when path is 'instrumentName' but no instrument", () => { + component.instrument = undefined; + const dataset = {} as any; + const result = component.getNestedValue(dataset, "instrumentName"); + expect(result).toBeUndefined(); + }); + + it("should work normally for non-instrumentName paths", () => { + component.instrument = { + pid: "instrument1", + name: "Test Instrument", + } as any; + const dataset = { pid: "test-pid" } as any; + const result = component.getNestedValue(dataset, "pid"); + expect(result).toBe("test-pid"); + }); + + it("should handle nested property paths", () => { + component.instrument = undefined; + const dataset = { nested: { property: "nested-value" } } as any; + const result = component.getNestedValue(dataset, "nested.property"); + expect(result).toBe("nested-value"); + }); + + it("should return undefined for non-existent paths", () => { + component.instrument = undefined; + const dataset = { pid: "test-pid" } as any; + const result = component.getNestedValue(dataset, "nonexistent.path"); + expect(result).toBeUndefined(); + }); + + it("should return error message when path is missing", () => { + component.instrument = undefined; + const dataset = {} as any; + const result = component.getNestedValue(dataset, ""); + expect(result).toBe("field source is missing"); + }); + + it("should return null when dataset is null", () => { + component.instrument = undefined; + const result = component.getNestedValue(null, "any.path"); + expect(result).toBeNull(); + }); + }); + + describe("getInternalLinkValue", () => { + it("should return instrument pid when path is 'instrumentName' and instrument exists", () => { + component.instrument = { + pid: "instrument1", + name: "Test Instrument", + } as any; + const dataset = {} as any; + const result = component.getInternalLinkValue(dataset, "instrumentName"); + expect(result).toBe("instrument1"); + }); + + it("should return empty string when path is 'instrumentName' but instrument has no pid", () => { + component.instrument = { name: "Test Instrument" } as any; + const dataset = {} as any; + const result = component.getInternalLinkValue(dataset, "instrumentName"); + expect(result).toBe(""); + }); + + it("should return empty string when path is 'instrumentName' but no instrument", () => { + component.instrument = undefined; + const dataset = {} as any; + const result = component.getInternalLinkValue(dataset, "instrumentName"); + expect(result).toBe(""); + }); + + it("should use getNestedValue for non-instrumentName paths", () => { + component.instrument = { + pid: "instrument1", + name: "Test Instrument", + } as any; + const dataset = { pid: "test-pid" } as any; + const result = component.getInternalLinkValue(dataset, "pid"); + expect(result).toBe("test-pid"); + }); + + it("should handle nested paths correctly", () => { + component.instrument = undefined; + const dataset = { + nested: { value: "test-value" }, + } as any; + const result = component.getInternalLinkValue(dataset, "nested.value"); + expect(result).toBe("test-value"); + }); + + it("should return empty string for null/undefined values", () => { + component.instrument = undefined; + const dataset = {} as any; + const result = component.getInternalLinkValue(dataset, "nonexistent"); + expect(result).toBe(""); + }); + }); + + describe("onClickInternalLink with instrument support", () => { + beforeEach(() => { + (component["router"].navigateByUrl as jasmine.Spy).calls.reset(); + spyOn(component["snackBar"], "open"); + }); + + it("should navigate to instruments page when internalLinkType is 'instruments'", () => { + component.onClickInternalLink( + InternalLinkType.INSTRUMENTS, + "instrument123", + ); + expect(component["router"].navigateByUrl).toHaveBeenCalledWith( + "/instruments/instrument123", + ); + }); + + it("should navigate to instruments page when internalLinkType is 'instrumentsName'", () => { + component.onClickInternalLink( + InternalLinkType.INSTRUMENTS_NAME, + "instrument123", + ); + expect(component["router"].navigateByUrl).toHaveBeenCalledWith( + "/instruments/instrument123", + ); + }); + + it("should encode special characters in instrument ID", () => { + component.onClickInternalLink( + InternalLinkType.INSTRUMENTS, + "instrument with spaces", + ); + expect(component["router"].navigateByUrl).toHaveBeenCalledWith( + "/instruments/instrument%20with%20spaces", + ); + }); + + it("should navigate to datasets page for dataset links", () => { + component.onClickInternalLink(InternalLinkType.DATASETS, "dataset123"); + expect(component["router"].navigateByUrl).toHaveBeenCalledWith( + "/datasets/dataset123", + ); + }); + + it("should show error message for invalid link types", () => { + component.onClickInternalLink("invalid", "test123"); + expect(component["snackBar"].open).toHaveBeenCalledWith( + "The URL is not valid", + "Close", + { + duration: 2000, + }, + ); + }); + }); + describe("getScientificMetadata", () => { type TestCase = { desc: string; diff --git a/src/app/datasets/dataset-detail/dataset-detail-dynamic/dataset-detail-dynamic.component.ts b/src/app/datasets/dataset-detail/dataset-detail-dynamic/dataset-detail-dynamic.component.ts index e317cf1c9..394ec88a5 100644 --- a/src/app/datasets/dataset-detail/dataset-detail-dynamic/dataset-detail-dynamic.component.ts +++ b/src/app/datasets/dataset-detail/dataset-detail-dynamic/dataset-detail-dynamic.component.ts @@ -1,7 +1,8 @@ -import { Component, OnInit } from "@angular/core"; +import { Component, OnInit, OnDestroy } from "@angular/core"; import { MatDialog } from "@angular/material/dialog"; import { Store } from "@ngrx/store"; +import { Subscription } from "rxjs"; import { showMessageAction } from "state-management/actions/user.actions"; import { @@ -10,6 +11,7 @@ import { selectCurrentDatasetWithoutFileInfo, } from "state-management/selectors/datasets.selectors"; import { selectIsLoading } from "state-management/selectors/user.selectors"; +import { selectCurrentInstrument } from "state-management/selectors/instruments.selectors"; import { AppConfigService } from "app-config.service"; @@ -26,6 +28,7 @@ import { AttachmentService } from "shared/services/attachment.service"; import { TranslateService } from "@ngx-translate/core"; import { DatePipe } from "@angular/common"; import { OutputDatasetObsoleteDto } from "@scicatproject/scicat-sdk-ts-angular/model/outputDatasetObsoleteDto"; +import { Instrument } from "@scicatproject/scicat-sdk-ts-angular"; import { Router } from "@angular/router"; import { MatSnackBar } from "@angular/material/snack-bar"; @@ -41,7 +44,9 @@ import { MatSnackBar } from "@angular/material/snack-bar"; styleUrls: ["./dataset-detail-dynamic.component.scss"], standalone: false, }) -export class DatasetDetailDynamicComponent implements OnInit { +export class DatasetDetailDynamicComponent implements OnInit, OnDestroy { + private subscriptions: Subscription[] = []; + datasetView: CustomizationItem[]; form: FormGroup; cols = 10; @@ -55,6 +60,8 @@ export class DatasetDetailDynamicComponent implements OnInit { loading$ = this.store.select(selectIsLoading); show = false; + instrument: Instrument | undefined; + constructor( public appConfigService: AppConfigService, public dialog: MatDialog, @@ -82,6 +89,12 @@ export class DatasetDetailDynamicComponent implements OnInit { }); this.datasetView = sortedDatasetView; + + this.subscriptions.push( + this.store.select(selectCurrentInstrument).subscribe((instrument) => { + this.instrument = instrument; + }), + ); } onCopy(value: string) { @@ -175,9 +188,23 @@ export class DatasetDetailDynamicComponent implements OnInit { return null; } + if (path === "instrumentName" && this.instrument) { + return this.instrument.name || "-"; + } + return path .split(".") - .reduce((prev, curr) => (prev ? prev[curr] : undefined), obj); + .reduce((prev, curr) => (prev != null ? prev[curr] : undefined), obj); + } + + getInternalLinkValue(obj: OutputDatasetObsoleteDto, path: string): string { + // For instrumentName internal links, return the instrument ID instead of the name + if (path === "instrumentName" && this.instrument) { + return this.instrument.pid || ""; + } + + const value = this.getNestedValue(obj, path); + return Array.isArray(value) ? value[0] || "" : (value as string) || ""; } onClickInternalLink(internalLinkType: string, id: string): void { @@ -194,6 +221,7 @@ export class DatasetDetailDynamicComponent implements OnInit { this.router.navigateByUrl("/proposals/" + encodedId); break; case InternalLinkType.INSTRUMENTS: + case InternalLinkType.INSTRUMENTS_NAME: this.router.navigateByUrl("/instruments/" + encodedId); break; default: @@ -221,4 +249,10 @@ export class DatasetDetailDynamicComponent implements OnInit { // Ensure the result is a valid object for metadata display return result && typeof result === "object" ? result : null; } + + ngOnDestroy() { + this.subscriptions.forEach((subscription) => { + subscription.unsubscribe(); + }); + } } diff --git a/src/app/datasets/dataset-detail/dataset-detail-wrapper.component.spec.ts b/src/app/datasets/dataset-detail/dataset-detail-wrapper.component.spec.ts index 35af74dc5..c2273f935 100644 --- a/src/app/datasets/dataset-detail/dataset-detail-wrapper.component.spec.ts +++ b/src/app/datasets/dataset-detail/dataset-detail-wrapper.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { NO_ERRORS_SCHEMA } from "@angular/core"; -import { AppConfig, AppConfigService } from "app-config.service"; +import { AppConfigInterface, AppConfigService } from "app-config.service"; import { DatasetDetailWrapperComponent } from "./dataset-detail-wrapper.component"; import { DatasetDetailComponent } from "./dataset-detail/dataset-detail.component"; import { DatasetDetailDynamicComponent } from "./dataset-detail-dynamic/dataset-detail-dynamic.component"; @@ -69,7 +69,7 @@ describe("DatasetDetailWrapperComponent", () => { datasetDetailComponent: { enableCustomizedComponent: true, }, - } as AppConfig); + } as AppConfigInterface); fixture.detectChanges(); @@ -82,7 +82,7 @@ describe("DatasetDetailWrapperComponent", () => { datasetDetailComponent: { enableCustomizedComponent: false, }, - } as AppConfig); + } as AppConfigInterface); fixture.detectChanges(); diff --git a/src/app/datasets/dataset-table-actions/dataset-table-actions.component.html b/src/app/datasets/dataset-table-actions/dataset-table-actions.component.html index 3151afb81..3d2a9802e 100644 --- a/src/app/datasets/dataset-table-actions/dataset-table-actions.component.html +++ b/src/app/datasets/dataset-table-actions/dataset-table-actions.component.html @@ -1,61 +1,36 @@
- + My Data All Public Data - - + + {{ mode | titlecase }}
- - -
-
+
\ No newline at end of file diff --git a/src/app/datasets/dataset-table-settings/_dataset-table-settings-theme.scss b/src/app/datasets/dataset-table-settings/_dataset-table-settings-theme.scss deleted file mode 100644 index 2507c89d8..000000000 --- a/src/app/datasets/dataset-table-settings/_dataset-table-settings-theme.scss +++ /dev/null @@ -1,18 +0,0 @@ -@use "@angular/material" as mat; -@use "sass:map"; - -@mixin color($theme) { - $color-config: map.get($theme, "color"); - $hover: map.get($color-config, "hover"); - - .close-button { - color: mat.m2-get-color-from-palette($hover, "default"); - } -} - -@mixin theme($theme) { - $color-config: mat.m2-get-color-config($theme); - @if $color-config != null { - @include color($theme); - } -} diff --git a/src/app/datasets/dataset-table-settings/dataset-table-settings.component.html b/src/app/datasets/dataset-table-settings/dataset-table-settings.component.html deleted file mode 100644 index 40aa3eb44..000000000 --- a/src/app/datasets/dataset-table-settings/dataset-table-settings.component.html +++ /dev/null @@ -1,32 +0,0 @@ -

Columns

- - - -
- - - - - - - -
- - {{ column.name }} - -
diff --git a/src/app/datasets/dataset-table-settings/dataset-table-settings.component.scss b/src/app/datasets/dataset-table-settings/dataset-table-settings.component.scss deleted file mode 100644 index 833352ce7..000000000 --- a/src/app/datasets/dataset-table-settings/dataset-table-settings.component.scss +++ /dev/null @@ -1,8 +0,0 @@ -.title { - float: left; - transform: translateY(25%); -} - -.close-button { - float: right; -} diff --git a/src/app/datasets/dataset-table-settings/dataset-table-settings.component.spec.ts b/src/app/datasets/dataset-table-settings/dataset-table-settings.component.spec.ts deleted file mode 100644 index a2bab6472..000000000 --- a/src/app/datasets/dataset-table-settings/dataset-table-settings.component.spec.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; - -import { - DatasetTableSettingsComponent, - SelectColumnEvent, -} from "./dataset-table-settings.component"; -import { NO_ERRORS_SCHEMA } from "@angular/core"; - -import { SearchBarModule } from "shared/modules/search-bar/search-bar.module"; -import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; -import { TableColumn } from "state-management/models"; -import { MatButtonModule } from "@angular/material/button"; -import { - MatCheckboxModule, - MatCheckboxChange, -} from "@angular/material/checkbox"; -import { MatIconModule } from "@angular/material/icon"; - -describe("DatasetTableSettingsComponent", () => { - let component: DatasetTableSettingsComponent; - let fixture: ComponentFixture; - - let emitSpy; - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - schemas: [NO_ERRORS_SCHEMA], - declarations: [DatasetTableSettingsComponent], - imports: [ - BrowserAnimationsModule, - MatButtonModule, - MatCheckboxModule, - MatIconModule, - SearchBarModule, - ], - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(DatasetTableSettingsComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it("should create", () => { - expect(component).toBeTruthy(); - }); - - describe("#doCloseClick()", () => { - it("should emit a MouseEvent", () => { - emitSpy = spyOn(component.closeClick, "emit"); - - const event = {} as MouseEvent; - - component.doCloseClick(event); - - expect(emitSpy).toHaveBeenCalledTimes(1); - expect(emitSpy).toHaveBeenCalledWith(event); - }); - }); - - describe("#doSelectColumn()", () => { - it("should emit a SelectColumnEvent", () => { - emitSpy = spyOn(component.selectColumn, "emit"); - - const event = {} as MatCheckboxChange; - const column: TableColumn = { - name: "test", - order: 0, - type: "standard", - enabled: true, - }; - - const emittedEvent: SelectColumnEvent = { - checkBoxChange: event, - column, - }; - - component.doSelectColumn(event, column); - - expect(emitSpy).toHaveBeenCalledTimes(1); - expect(emitSpy).toHaveBeenCalledWith(emittedEvent); - }); - }); - - describe("#doSearch()", () => { - it("should set filteredColumns based on the input value", () => { - component.selectableColumns = [ - { name: "test", order: 0, type: "standard", enabled: true }, - { name: "filter", order: 1, type: "custom", enabled: true }, - ]; - - component.doSearch("test"); - - expect(component.filteredColumns.length).toEqual(1); - }); - }); -}); diff --git a/src/app/datasets/dataset-table-settings/dataset-table-settings.component.ts b/src/app/datasets/dataset-table-settings/dataset-table-settings.component.ts deleted file mode 100644 index 75e460f2c..000000000 --- a/src/app/datasets/dataset-table-settings/dataset-table-settings.component.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Component, Input, Output, EventEmitter } from "@angular/core"; -import { TableColumn } from "state-management/models"; -import { MatCheckboxChange } from "@angular/material/checkbox"; - -export interface SelectColumnEvent { - checkBoxChange: MatCheckboxChange; - column: TableColumn; -} - -@Component({ - selector: "dataset-table-settings", - templateUrl: "./dataset-table-settings.component.html", - styleUrls: ["./dataset-table-settings.component.scss"], - standalone: false, -}) -export class DatasetTableSettingsComponent { - @Input() clearSearchBar = false; - @Input() selectableColumns: TableColumn[] | null = null; - filteredColumns: TableColumn[] = []; - - @Output() closeClick = new EventEmitter(); - @Output() selectColumn = new EventEmitter(); - - doCloseClick(event: MouseEvent): void { - this.closeClick.emit(event); - } - - doSelectColumn(event: MatCheckboxChange, column: TableColumn): void { - const selectColumnEvent: SelectColumnEvent = { - checkBoxChange: event, - column, - }; - this.selectColumn.emit(selectColumnEvent); - } - - doSearch(value: string): void { - const filterValue = value.toLowerCase(); - this.filteredColumns = this.selectableColumns!.filter(({ name }) => - name.toLowerCase().includes(filterValue), - ); - } -} diff --git a/src/app/datasets/dataset-table/dataset-table.component.html b/src/app/datasets/dataset-table/dataset-table.component.html index fe8299015..9a7ce2e99 100644 --- a/src/app/datasets/dataset-table/dataset-table.component.html +++ b/src/app/datasets/dataset-table/dataset-table.component.html @@ -1,406 +1,10 @@ -
- -
-
- - - - - -
- - - - - - - - - - - - - - - - shopping_cart - - - - - - - -
-
- perm_identity -
-
Pid
-
-
- - {{ dataset.pid }} - -
- - - - -
-
- fingerprint -
-
Name
-
-
- - {{ dataset.datasetName }} - -
- - - - -
-
- explore -
-
Source Folder
-
-
- - ...{{ dataset.sourceFolder | slice: -10 }} - -
- - - - -
-
- directions_run -
-
Run No.
-
-
- -
- {{ dataset.scientificMetadata.runNumber.value }} -
-
-
- - - - -
-
- save -
-
Size
-
-
- -
-
{{ dataset.size | filesize }}
-
-   -
-
-
-
- - - - -
-
- calendar_today -
-
Start Time
-
-
- -
-
{{ dataset.creationTime | date }}
-
-
-
- - - - -
-
- bubble_chart -
-
Type
-
-
- {{ dataset.type }} -
- - - - -
-
- camera_alt -
-
Image
-
-
- - - -
- - - - -
-
- assessment -
-
Science Metadata
-
-
- -
- {{ - dataset.scientificMetadata | jsonHead - }} - {{ - dataset.scientificMetadata | json | slice: 0 : 100 - }}... -
-
- - - - -
-
- spa -
-
Proposal ID
-
-
- {{ - dataset.proposalId | StripProposalPrefix - }} -
- - - - -
-
- group -
-
Group
-
-
- {{ dataset.ownerGroup }} -
- - - - -
-
- archive/ cloud_upload -
-
Data Status
-
-
- - - - - hourglass_empty  work in - progress - - - - - archive -  archivable - - - - - cloud_upload -  retrievable - - - - - error -  system error - - - - - build -  user error - -
- - - - - - - - - -
-
- star -
-
{{ column.name | replaceUnderscore | titlecase }}
-
-
- -
- - {{ value.value || value.v || value }} {{ - value.unit || value.u | prettyUnit - }} -
-
-
-
-
- - - - - - - - -
-
+ + \ No newline at end of file diff --git a/src/app/datasets/dataset-table/dataset-table.component.scss b/src/app/datasets/dataset-table/dataset-table.component.scss index 3b81e2ad9..fd0a35218 100644 --- a/src/app/datasets/dataset-table/dataset-table.component.scss +++ b/src/app/datasets/dataset-table/dataset-table.component.scss @@ -1,91 +1,7 @@ .dataset-table { mat-table { - overflow-x: scroll; - - mat-header-row { - min-width: 1200px; - } - - mat-header-cell { - &.mat-column-standard_select { - justify-content: center; - } - - mat-icon { - display: flex; - padding-top: 0.25rem; - } - } - - mat-row { - min-width: 1200px; - } - - mat-row:hover { - background-color: rgba(0, 0, 0, 0.1); - cursor: pointer; - } - mat-cell { - &.mat-column-standard_select { - justify-content: center; - } - - mat-icon { - display: inline-table; - } - } - - .mat-column-standard_select { - flex: 0 0 40px; - } - - .mat-column-standard_datasetName { - flex: 1 0 200px; - justify-content: left; - } - - .mat-column-standard_runNumber { - flex: 0.5 0 60px; - } - - .mat-column-standard_sourceFolder { - flex: 0 1 100px; - } - - .mat-column-standard_size { - flex: 0.5 0 70px; - } - - .mat-column-standard_creationTime { - flex: 0 1 90px; - } - - .mat-column-standard_type { - flex: 0.5 0 70px; - } - - .mat-column-standard_image { - flex: 0 0 60px; - } - - .mat-column-standard_metadata { - flex: 1 1 200px; - } - - .mat-column-standard_proposalId { - flex: 0 1 90px; - } - - .mat-column-standard_ownerGroup { - flex: 0 1 80px; - } - - .mat-column-standard_dataStatus { - flex: 1 1 120px; - } - - .mat-column-standard_derivedDatasetsNum { - flex: 0 1 80px; + mat-icon { + display: inline-table; } } } diff --git a/src/app/datasets/dataset-table/dataset-table.component.spec.ts b/src/app/datasets/dataset-table/dataset-table.component.spec.ts index 894665cc2..42d484279 100644 --- a/src/app/datasets/dataset-table/dataset-table.component.spec.ts +++ b/src/app/datasets/dataset-table/dataset-table.component.spec.ts @@ -9,6 +9,7 @@ import { MockDatasetApi, mockDataset, createMock, + MockActivatedRoute, } from "shared/MockStubs"; import { NO_ERRORS_SCHEMA } from "@angular/core"; import { @@ -28,6 +29,7 @@ import { } from "state-management/actions/datasets.actions"; import { provideMockStore } from "@ngrx/store/testing"; import { selectDatasets } from "state-management/selectors/datasets.selectors"; +import { selectInstruments } from "state-management/selectors/instruments.selectors"; import { MatTableModule } from "@angular/material/table"; import { MatCheckboxChange, @@ -42,6 +44,11 @@ import { DatasetClass, DatasetsService, } from "@scicatproject/scicat-sdk-ts-angular"; +import { RowEventType } from "shared/modules/dynamic-material-table/models/table-row.model"; +import { ActivatedRoute } from "@angular/router"; +import { JsonHeadPipe } from "shared/pipes/json-head.pipe"; +import { DatePipe } from "@angular/common"; +import { FileSizePipe } from "shared/pipes/filesize.pipe"; const getConfig = () => ({}); @@ -67,8 +74,14 @@ describe("DatasetTableComponent", () => { ], providers: [ provideMockStore({ - selectors: [{ selector: selectDatasets, value: [] }], + selectors: [ + { selector: selectDatasets, value: [] }, + { selector: selectInstruments, value: [] }, + ], }), + JsonHeadPipe, + DatePipe, + FileSizePipe, ], declarations: [DatasetTableComponent], }); @@ -80,6 +93,7 @@ describe("DatasetTableComponent", () => { useValue: { getConfig }, }, { provide: DatasetsService, useClass: MockDatasetApi }, + { provide: ActivatedRoute, useClass: MockActivatedRoute }, ], }, }); @@ -89,7 +103,6 @@ describe("DatasetTableComponent", () => { beforeEach(() => { fixture = TestBed.createComponent(DatasetTableComponent); component = fixture.componentInstance; - component.tableColumns = []; fixture.detectChanges(); }); @@ -105,24 +118,15 @@ describe("DatasetTableComponent", () => { expect(component).toBeTruthy(); }); - describe("#doSettingsClick()", () => { - it("should emit a MouseEvent on click", () => { - const emitSpy = spyOn(component.settingsClick, "emit"); - - const event = {} as MouseEvent; - component.doSettingsClick(event); - - expect(emitSpy).toHaveBeenCalledTimes(1); - expect(emitSpy).toHaveBeenCalledWith(event); - }); - }); - describe("#doRowClick()", () => { it("should emit the dataset clicked", () => { const emitSpy = spyOn(component.rowClick, "emit"); const dataset = mockDataset; - component.doRowClick(dataset); + component.onRowEvent({ + event: RowEventType.RowClick, + sender: { row: dataset }, + }); expect(emitSpy).toHaveBeenCalledTimes(1); expect(emitSpy).toHaveBeenCalledWith(dataset); @@ -317,40 +321,6 @@ describe("DatasetTableComponent", () => { }); }); - describe("#isSelected()", () => { - it("should return false if dataset is not selected", () => { - const dataset = createMock({}); - const selected = component.isSelected(dataset); - - expect(selected).toEqual(false); - }); - }); - - describe("#isAllSelected()", () => { - it("should return false if length of datasets and length of selectedSets are not equal", () => { - component.datasets = [mockDataset]; - - const allSelected = component.isAllSelected(); - - expect(allSelected).toEqual(false); - }); - - it("should return true if length of datasets and length of selectedSets are equal", () => { - const allSelected = component.isAllSelected(); - - expect(allSelected).toEqual(true); - }); - }); - - describe("#isInBatch()", () => { - it("should return false if dataset is not in batch", () => { - const dataset = createMock({}); - const inBatch = component.isInBatch(dataset); - - expect(inBatch).toEqual(false); - }); - }); - describe("#onSelect()", () => { it("should dispatch a selectDatasetAction if checked is true", () => { dispatchSpy = spyOn(store, "dispatch"); @@ -423,11 +393,283 @@ describe("DatasetTableComponent", () => { }); }); - describe("#countDerivedDatasets()", () => { - xit("should return the number of derived datasets for a dataset", () => { - // const dataset = mockDataset; - // const numberOfDerivedDataset = component.countDerivedDatasets(dataset); - // expect(numberOfDerivedDataset).toEqual(0); + describe("#convertSavedColumns() with instrumentName", () => { + beforeEach(() => { + component.instruments = [ + { + pid: "instrument1", + uniqueName: "unique1", + name: "Test Instrument 1", + }, + { + pid: "instrument2", + uniqueName: "unique2", + name: "Test Instrument 2", + }, + { pid: "instrument3", uniqueName: "unique3", name: "" }, + ] as any[]; + + component.instrumentMap = new Map( + component.instruments.map((instrument) => [instrument.pid, instrument]), + ); + }); + + it("should render instrument name when instrument is found", () => { + const columns = [ + { + name: "instrumentName", + order: 0, + enabled: true, + width: 200, + type: "standard" as const, + }, + ]; + + const convertedColumns = component.convertSavedColumns(columns); + const instrumentColumn = convertedColumns[0]; + + const mockRow = { instrumentId: "instrument1" }; + const result = instrumentColumn.customRender(instrumentColumn, mockRow); + + expect(result).toBe("Test Instrument 1"); + }); + + it("should render instrumentId when instrument is not found", () => { + const columns = [ + { + name: "instrumentName", + order: 0, + enabled: true, + width: 200, + type: "standard" as const, + }, + ]; + + const convertedColumns = component.convertSavedColumns(columns); + const instrumentColumn = convertedColumns[0]; + + const mockRow = { instrumentId: "nonexistent" }; + const result = instrumentColumn.customRender(instrumentColumn, mockRow); + + expect(result).toBe("nonexistent"); + }); + + it("should render '-' when instrumentId is not present", () => { + const columns = [ + { + name: "instrumentName", + order: 0, + enabled: true, + width: 200, + type: "standard" as const, + }, + ]; + + const convertedColumns = component.convertSavedColumns(columns); + const instrumentColumn = convertedColumns[0]; + + const mockRow = {}; + const result = instrumentColumn.customRender(instrumentColumn, mockRow); + + expect(result).toBe("-"); + }); + + it("should render instrumentId when instrument has empty name", () => { + const columns = [ + { + name: "instrumentName", + order: 0, + enabled: true, + width: 200, + type: "standard" as const, + }, + ]; + + const convertedColumns = component.convertSavedColumns(columns); + const instrumentColumn = convertedColumns[0]; + + const mockRow = { instrumentId: "instrument3" }; + const result = instrumentColumn.customRender(instrumentColumn, mockRow); + + expect(result).toBe("instrument3"); + }); + + it("should export instrument name when instrument is found", () => { + const columns = [ + { + name: "instrumentName", + order: 0, + enabled: true, + width: 200, + type: "standard" as const, + }, + ]; + + const convertedColumns = component.convertSavedColumns(columns); + const instrumentColumn = convertedColumns[0]; + + const mockRow = { instrumentId: "instrument2" }; + const result = instrumentColumn.toExport(mockRow, instrumentColumn); + + expect(result).toBe("Test Instrument 2"); + }); + + it("should export instrumentId when instrument is not found", () => { + const columns = [ + { + name: "instrumentName", + order: 0, + enabled: true, + width: 200, + type: "standard" as const, + }, + ]; + + const convertedColumns = component.convertSavedColumns(columns); + const instrumentColumn = convertedColumns[0]; + + const mockRow = { instrumentId: "unknown-instrument" }; + const result = instrumentColumn.toExport(mockRow, instrumentColumn); + + expect(result).toBe("unknown-instrument"); + }); + + it("should not affect other column types", () => { + const columns = [ + { + name: "datasetName", + order: 0, + enabled: true, + width: 200, + type: "standard" as const, + }, + { + name: "instrumentName", + order: 1, + enabled: true, + width: 200, + type: "standard" as const, + }, + ]; + + const convertedColumns = component.convertSavedColumns(columns); + + expect(convertedColumns.length).toBe(2); + expect(convertedColumns[0].name).toBe("datasetName"); + expect(convertedColumns[0].customRender).toBeUndefined(); + expect(convertedColumns[1].name).toBe("instrumentName"); + expect(convertedColumns[1].customRender).toBeDefined(); + }); + }); + + describe("instruments subscription with Map optimization", () => { + it("should update both instruments array and instrumentMap when instruments observable changes", () => { + const mockInstruments = [ + { pid: "inst1", uniqueName: "unique1", name: "Instrument 1" }, + { pid: "inst2", uniqueName: "unique2", name: "Instrument 2" }, + ]; + + component.instruments = mockInstruments; + component.instrumentMap = new Map( + mockInstruments.map((instrument) => [instrument.pid, instrument]), + ); + + expect(component.instruments).toEqual(mockInstruments); + expect(component.instrumentMap.size).toBe(2); + expect(component.instrumentMap.get("inst1")).toEqual(mockInstruments[0]); + expect(component.instrumentMap.get("inst2")).toEqual(mockInstruments[1]); + }); + + it("should handle empty instruments array and clear instrumentMap", () => { + component.instruments = []; + component.instrumentMap = new Map(); + + expect(component.instruments).toEqual([]); + expect(component.instrumentMap.size).toBe(0); + }); + + it("should provide O(1) lookup performance for instrument retrieval", () => { + const mockInstruments = [ + { pid: "fast-lookup", uniqueName: "unique1", name: "Fast Instrument" }, + ]; + + component.instrumentMap = new Map( + mockInstruments.map((instrument) => [instrument.pid, instrument]), + ); + + const foundInstrument = component.instrumentMap.get("fast-lookup"); + expect(foundInstrument).toEqual(mockInstruments[0]); + + const notFoundInstrument = component.instrumentMap.get("nonexistent"); + expect(notFoundInstrument).toBeUndefined(); + }); + }); + + describe("#getInstrumentName() private method", () => { + beforeEach(() => { + const mockInstruments = [ + { pid: "inst1", uniqueName: "unique1", name: "Test Instrument 1" }, + { pid: "inst2", uniqueName: "unique2", name: "Test Instrument 2" }, + { pid: "inst3", uniqueName: "unique3", name: "" }, + ]; + + component.instrumentMap = new Map( + mockInstruments.map((instrument) => [instrument.pid, instrument]), + ); + }); + + it("should return instrument name when instrument is found", () => { + const mockRow = { instrumentId: "inst1" } as any; + const result = component["getInstrumentName"](mockRow); + expect(result).toBe("Test Instrument 1"); + }); + + it("should return instrumentId when instrument is not found", () => { + const mockRow = { instrumentId: "nonexistent" } as any; + const result = component["getInstrumentName"](mockRow); + expect(result).toBe("nonexistent"); + }); + + it("should return '-' when instrumentId is not present", () => { + const mockRow = {} as any; + const result = component["getInstrumentName"](mockRow); + expect(result).toBe("-"); + }); + + it("should return instrumentId when instrument has empty name", () => { + const mockRow = { instrumentId: "inst3" } as any; + const result = component["getInstrumentName"](mockRow); + expect(result).toBe("inst3"); + }); + + it("should handle undefined instrumentId gracefully", () => { + const mockRow = { instrumentId: undefined } as any; + const result = component["getInstrumentName"](mockRow); + expect(result).toBe("-"); + }); + + it("should handle null instrumentId gracefully", () => { + const mockRow = { instrumentId: null } as any; + const result = component["getInstrumentName"](mockRow); + expect(result).toBe("-"); + }); + + it("should handle empty string instrumentId gracefully", () => { + const mockRow = { instrumentId: "" } as any; + const result = component["getInstrumentName"](mockRow); + expect(result).toBe("-"); + }); + + it("should return instrument name even when instrumentId is empty but instrument exists", () => { + // Add an instrument with empty string pid to test edge case + component.instrumentMap.set("", { + pid: "", + name: "Empty PID Instrument", + } as any); + + const mockRow = { instrumentId: "" } as any; + const result = component["getInstrumentName"](mockRow); + expect(result).toBe("Empty PID Instrument"); }); }); }); diff --git a/src/app/datasets/dataset-table/dataset-table.component.ts b/src/app/datasets/dataset-table/dataset-table.component.ts index 5ea06844f..35d1d5431 100644 --- a/src/app/datasets/dataset-table/dataset-table.component.ts +++ b/src/app/datasets/dataset-table/dataset-table.component.ts @@ -5,13 +5,11 @@ import { Output, EventEmitter, Input, - OnChanges, - SimpleChange, ViewEncapsulation, } from "@angular/core"; import { TableColumn } from "state-management/models"; import { MatCheckboxChange } from "@angular/material/checkbox"; -import { Subscription } from "rxjs"; +import { BehaviorSubject, Subscription, lastValueFrom, take } from "rxjs"; import { Store } from "@ngrx/store"; import { clearSelectionAction, @@ -20,6 +18,7 @@ import { selectAllDatasetsAction, sortByColumnAction, } from "state-management/actions/datasets.actions"; +import { fetchInstrumentsAction } from "state-management/actions/instruments.actions"; import { selectDatasets, @@ -28,14 +27,43 @@ import { selectTotalSets, selectDatasetsInBatch, } from "state-management/selectors/datasets.selectors"; -import { get } from "lodash-es"; +import { get as lodashGet } from "lodash-es"; import { AppConfigService } from "app-config.service"; -import { selectCurrentUser } from "state-management/selectors/user.selectors"; +import { + selectColumnsWithHasFetchedSettings, + selectCurrentUser, +} from "state-management/selectors/user.selectors"; import { DatasetClass, OutputDatasetObsoleteDto, + Instrument, } from "@scicatproject/scicat-sdk-ts-angular"; -import { PageEvent } from "@angular/material/paginator"; +import { TableField } from "shared/modules/dynamic-material-table/models/table-field.model"; +import { + ITableSetting, + TableSettingEventType, +} from "shared/modules/dynamic-material-table/models/table-setting.model"; +import { + TablePagination, + TablePaginationMode, +} from "shared/modules/dynamic-material-table/models/table-pagination.model"; +import { + IRowEvent, + ITableEvent, + RowEventType, + TableEventType, + TableSelectionMode, +} from "shared/modules/dynamic-material-table/models/table-row.model"; +import { updateUserSettingsAction } from "state-management/actions/user.actions"; +import { Sort } from "@angular/material/sort"; +import { ActivatedRoute } from "@angular/router"; +import { JsonHeadPipe } from "shared/pipes/json-head.pipe"; +import { DatePipe } from "@angular/common"; +import { FileSizePipe } from "shared/pipes/filesize.pipe"; +import { actionMenu } from "shared/modules/dynamic-material-table/utilizes/default-table-settings"; +import { TableConfigService } from "shared/services/table-config.service"; +import { selectInstruments } from "state-management/selectors/instruments.selectors"; + export interface SortChangeEvent { active: string; direction: "asc" | "desc" | ""; @@ -48,19 +76,22 @@ export interface SortChangeEvent { encapsulation: ViewEncapsulation.None, standalone: false, }) -export class DatasetTableComponent implements OnInit, OnDestroy, OnChanges { - private inBatchPids: string[] = []; +export class DatasetTableComponent implements OnInit, OnDestroy { private subscriptions: Subscription[] = []; + selectionIds: string[] = []; appConfig = this.appConfigService.getConfig(); - - lodashGet = get; currentPage$ = this.store.select(selectPage); datasetsPerPage$ = this.store.select(selectDatasetsPerPage); datasetCount$ = this.store.select(selectTotalSets); + currentUser$ = this.store.select(selectCurrentUser); + datasets$ = this.store.select(selectDatasets); + selectedDatasets$ = this.store.select(selectDatasetsInBatch); + selectColumnsWithFetchedSettings$ = this.store.select( + selectColumnsWithHasFetchedSettings, + ); + instruments$ = this.store.select(selectInstruments); - @Input() tableColumns: TableColumn[] | null = null; - displayedColumns: string[] = []; @Input() selectedSets: OutputDatasetObsoleteDto[] | null = null; @Output() pageChange = new EventEmitter<{ pageIndex: number; @@ -68,27 +99,190 @@ export class DatasetTableComponent implements OnInit, OnDestroy, OnChanges { }>(); datasets: OutputDatasetObsoleteDto[] = []; + instruments: Instrument[] = []; + instrumentMap: Map = new Map(); - @Output() settingsClick = new EventEmitter(); @Output() rowClick = new EventEmitter(); + tableDefaultSettingsConfig: ITableSetting = { + visibleActionMenu: actionMenu, + settingList: [ + { + visibleActionMenu: actionMenu, + isDefaultSetting: true, + isCurrentSetting: true, + columnSetting: [], + }, + ], + rowStyle: { + "border-bottom": "1px solid #d2d2d2", + }, + }; + + tableName = "datasetsTable"; + + columns: TableField[]; + + pending = true; + + setting: ITableSetting = {}; + + paginationMode: TablePaginationMode = "server-side"; + + dataSource: BehaviorSubject = new BehaviorSubject< + OutputDatasetObsoleteDto[] + >([]); + + pagination: TablePagination = {}; + + rowSelectionMode: TableSelectionMode = "multi"; + + showGlobalTextSearch = false; + + defaultPageSize = 10; + + defaultPageSizeOptions = [5, 10, 25, 100]; + + tablesSettings: object; + constructor( public appConfigService: AppConfigService, private store: Store, + private route: ActivatedRoute, + private jsonHeadPipe: JsonHeadPipe, + private datePipe: DatePipe, + private fileSize: FileSizePipe, + private tableConfigService: TableConfigService, ) {} - onPageChange(event: PageEvent) { - this.pageChange.emit({ - pageIndex: event.pageIndex, - pageSize: event.pageSize, + private getInstrumentName(row: OutputDatasetObsoleteDto): string { + const instrument = this.instrumentMap.get(row.instrumentId); + if (instrument?.name) { + return instrument.name; + } + if (row.instrumentId != null) { + return row.instrumentId === "" ? "-" : row.instrumentId; + } + return "-"; + } + + getTableSort(): ITableSetting["tableSort"] { + const { queryParams } = this.route.snapshot; + + if (queryParams.sortDirection && queryParams.sortColumn) { + return { + sortColumn: queryParams.sortColumn, + sortDirection: queryParams.sortDirection, + }; + } + + return null; + } + + getTablePaginationConfig(dataCount = 0): TablePagination { + const { queryParams } = this.route.snapshot; + + const { skip = 0, limit = 25 } = JSON.parse(queryParams.args ?? "{}"); + + return { + pageSizeOptions: this.defaultPageSizeOptions, + pageIndex: skip / limit || 0, + pageSize: limit || this.defaultPageSize, + length: dataCount, + }; + } + + initTable( + settingConfig: ITableSetting, + paginationConfig: TablePagination, + ): void { + let currentColumnSetting = settingConfig.settingList.find( + (s) => s.isCurrentSetting, + )?.columnSetting; + + if (!currentColumnSetting && settingConfig.settingList.length > 0) { + currentColumnSetting = settingConfig.settingList[0].columnSetting; + } + + this.columns = currentColumnSetting; + this.setting = settingConfig; + this.pagination = paginationConfig; + } + + saveTableSettings(setting: ITableSetting) { + this.pending = true; + const columnsSetting = setting.columnSetting.map((column, index) => { + const { name, display, width, type } = column; + + return { + name, + enabled: !!(display === "visible"), + order: index, + width, + type, + }; }); + this.store.dispatch( + updateUserSettingsAction({ + property: { + columns: columnsSetting, + }, + }), + ); + + this.pending = false; + } + + onSettingChange(event: { + type: TableSettingEventType; + setting: ITableSetting; + }) { + if ( + event.type === TableSettingEventType.save || + event.type === TableSettingEventType.create + ) { + this.saveTableSettings(event.setting); + } + } + + onRowEvent({ event, sender }: IRowEvent) { + if (event === RowEventType.RowClick) { + const dataset = sender.row; + this.rowClick.emit(dataset); + } else if (event === RowEventType.RowSelectionChange) { + const dataset = sender.row; + if (sender.checked) { + this.store.dispatch(selectDatasetAction({ dataset })); + } else { + this.store.dispatch(deselectDatasetAction({ dataset })); + } + } else if (event === RowEventType.MasterSelectionChange) { + if (sender.checked) { + this.store.dispatch(selectAllDatasetsAction()); + } else { + this.store.dispatch(clearSelectionAction()); + } + } } - doSettingsClick(event: MouseEvent) { - this.settingsClick.emit(event); + + onTableEvent({ event, sender }: ITableEvent) { + if (event === TableEventType.SortChanged) { + const { active, direction } = sender as Sort; + + let column = active; + if (column === "runNumber") { + column = "scientificMetadata.runNumber.value"; + } + + this.store.dispatch(sortByColumnAction({ column, direction })); + } } - doRowClick(dataset: OutputDatasetObsoleteDto): void { - this.rowClick.emit(dataset); + onPageChange({ pageIndex, pageSize }: TablePagination) { + this.pageChange.emit({ + pageIndex, + pageSize, + }); } // conditional to asses dataset status and assign correct icon ArchViewMode.work_in_progress @@ -149,22 +343,9 @@ export class DatasetTableComponent implements OnInit, OnDestroy, OnChanges { return false; } - isSelected(dataset: DatasetClass): boolean { - if (!this.selectedSets) { - return false; - } - return this.selectedSets.map((set) => set.pid).indexOf(dataset.pid) !== -1; - } - - isAllSelected(): boolean { - const numSelected = this.selectedSets ? this.selectedSets.length : 0; - const numRows = this.datasets ? this.datasets.length : 0; - return numSelected === numRows; - } - - isInBatch(dataset: DatasetClass): boolean { - return this.inBatchPids.indexOf(dataset.pid) !== -1; - } + // isInBatch(dataset: DatasetClass): boolean { + // return this.inBatchPids.indexOf(dataset.pid) !== -1; + // } onSelect(event: MatCheckboxChange, dataset: OutputDatasetObsoleteDto): void { if (event.checked) { @@ -189,71 +370,197 @@ export class DatasetTableComponent implements OnInit, OnDestroy, OnChanges { this.store.dispatch(sortByColumnAction({ column, direction })); } - // countDerivedDatasets(dataset: Dataset): number { - // let derivedDatasetsNum = 0; - // if (dataset.history) { - // dataset.history.forEach(item => { - // if ( - // item.hasOwnProperty("derivedDataset") && - // this.datasets.map(set => set.pid).includes(item.derivedDataset.pid) - // ) { - // derivedDatasetsNum++; - // } - // }); - // } - // return derivedDatasetsNum; - // } + convertSavedColumns(columns: TableColumn[]): TableField[] { + return columns + .filter((column) => column.name !== "select") + .map((column) => { + const convertedColumn: TableField = { + name: column.name, + header: column.header, + index: column.order, + display: column.enabled ? "visible" : "hidden", + width: column.width, + type: column.type as any, + }; + + if (column.name === "runNumber" && column.type !== "custom") { + // NOTE: This is for the saved columns in the database or the old config. + convertedColumn.customRender = (c, row) => + lodashGet(row, "scientificMetadata.runNumber.value"); + convertedColumn.toExport = (row) => + lodashGet(row, "scientificMetadata.runNumber.value"); + } + // NOTE: This is how we render the custom columns if new config is used. + if (column.type === "custom") { + convertedColumn.customRender = (c, row) => + lodashGet(row, column.path || column.name); + convertedColumn.toExport = (row) => + lodashGet(row, column.path || column.name); + } + + if (column.name === "size") { + convertedColumn.customRender = (column, row) => + this.fileSize.transform(row[column.name]); + convertedColumn.toExport = (row) => + this.fileSize.transform(row[column.name]); + } + + if (column.name === "creationTime") { + convertedColumn.customRender = (column, row) => + this.datePipe.transform(row[column.name]); + convertedColumn.toExport = (row) => + this.datePipe.transform(row[column.name]); + } + + if ( + column.name === "metadata" || + column.name === "scientificMetadata" + ) { + convertedColumn.customRender = (column, row) => { + // NOTE: Maybe here we should use the "scientificMetadata" as field name and not "metadata". This should be changed in the backend config. + return this.jsonHeadPipe.transform(row["scientificMetadata"]); + }; + convertedColumn.toExport = (row) => { + return this.jsonHeadPipe.transform(row["scientificMetadata"]); + }; + } + + if (column.name === "dataStatus") { + convertedColumn.renderContentIcon = (column, row) => { + if (this.wipCondition(row)) { + return "hourglass_empty"; + } else if (this.archivableCondition(row)) { + return "archive"; + } else if (this.retrievableCondition(row)) { + return "archive"; + } else if (this.systemErrorCondition(row)) { + return "error_outline"; + } else if (this.userErrorCondition(row)) { + return "error_outline"; + } + + return ""; + }; + + convertedColumn.customRender = (column, row) => { + if (this.wipCondition(row)) { + return "Work in progress"; + } else if (this.archivableCondition(row)) { + return "Archivable"; + } else if (this.retrievableCondition(row)) { + return "Retrievable"; + } else if (this.systemErrorCondition(row)) { + return "System error"; + } else if (this.userErrorCondition(row)) { + return "User error"; + } + + return ""; + }; + + convertedColumn.toExport = (row) => { + if (this.wipCondition(row)) { + return "Work in progress"; + } else if (this.archivableCondition(row)) { + return "Archivable"; + } else if (this.retrievableCondition(row)) { + return "Retrievable"; + } else if (this.systemErrorCondition(row)) { + return "System error"; + } else if (this.userErrorCondition(row)) { + return "User error"; + } + + return ""; + }; + } + + if (column.name === "image") { + convertedColumn.renderImage = true; + } + + if (column.name === "instrumentName") { + convertedColumn.customRender = (column, row) => + this.getInstrumentName(row); + convertedColumn.toExport = (row, column) => + this.getInstrumentName(row); + } + + return convertedColumn; + }); + } ngOnInit() { + this.store.dispatch(fetchInstrumentsAction({ limit: 1000, skip: 0 })); + this.subscriptions.push( - this.store.select(selectDatasetsInBatch).subscribe((datasets) => { - this.inBatchPids = datasets.map((dataset) => { + this.selectedDatasets$.subscribe((datasets) => { + // NOTE: In the selectionIds we are storing either _id or pid. Dynamic material table works only with these two. + this.selectionIds = datasets.map((dataset) => { return dataset.pid; }); }), ); - if (this.tableColumns) { - this.displayedColumns = this.tableColumns - .filter((column) => column.enabled) - .map((column) => { - return column.type + "_" + column.name; - }); - } + this.subscriptions.push( + this.instruments$.subscribe((instruments) => { + this.instruments = instruments; + this.instrumentMap = new Map( + instruments.map((instrument) => [instrument.pid, instrument]), + ); + }), + ); this.subscriptions.push( - this.store.select(selectDatasets).subscribe((datasets) => { - this.store.select(selectCurrentUser).subscribe((currentUser) => { - const publishedDatasets = datasets.filter( - (dataset) => dataset.isPublished, - ); - this.datasets = currentUser ? datasets : publishedDatasets; - }); + this.datasets$.subscribe((datasets) => { + this.currentUser$.subscribe((currentUser) => { + this.datasetCount$.subscribe(async (count) => { + const defaultTableColumns = await lastValueFrom( + this.selectColumnsWithFetchedSettings$.pipe(take(1)), + ); - // this.derivationMapPids = this.datasetDerivationsMaps.map( - // datasetderivationMap => datasetderivationMap.datasetPid - // ); - // this.datasetDerivationsMaps = datasets - // .filter(({ pid }) => !this.derivationMapPids.includes(pid)) - // .map(dataset => ({ - // datasetPid: dataset.pid, - // derivedDatasetsNum: this.countDerivedDatasets(dataset) - // })); + if ( + defaultTableColumns.hasFetchedSettings && + defaultTableColumns.columns.length + ) { + const tableColumns = defaultTableColumns.columns; + + if (!currentUser) { + this.rowSelectionMode = "none"; + } + + if (tableColumns) { + this.dataSource.next(datasets); + this.pending = false; + + const savedTableConfigColumns = + this.convertSavedColumns(tableColumns); + + const tableSort = this.getTableSort(); + const paginationConfig = this.getTablePaginationConfig(count); + + this.tableDefaultSettingsConfig.settingList[0].columnSetting = + savedTableConfigColumns; + + const tableSettingsConfig = + this.tableConfigService.getTableSettingsConfig( + this.tableName, + this.tableDefaultSettingsConfig, + savedTableConfigColumns, + tableSort, + ); + + if (tableSettingsConfig?.settingList.length) { + this.initTable(tableSettingsConfig, paginationConfig); + } + } + } + }); + }); }), ); } - ngOnChanges(changes: { [propKey: string]: SimpleChange }) { - for (const propName in changes) { - if (propName === "tableColumns") { - this.tableColumns = changes[propName].currentValue; - this.displayedColumns = changes[propName].currentValue - .filter((column: TableColumn) => column.enabled) - .map((column: TableColumn) => column.type + "_" + column.name); - } - } - } - ngOnDestroy() { this.subscriptions.forEach((subscription) => subscription.unsubscribe()); } diff --git a/src/app/datasets/datasets-filter/datasets-filter.component.html b/src/app/datasets/datasets-filter/datasets-filter.component.html index 7886cfd33..38853e286 100644 --- a/src/app/datasets/datasets-filter/datasets-filter.component.html +++ b/src/app/datasets/datasets-filter/datasets-filter.component.html @@ -7,58 +7,176 @@ - + ">
-
- - - -
+ +
+ + + + + + Outgassing values after 1h + + + {{ getFilterDisplayText(outgassingForm1h) }} + + + +
+ + Operator + + is greater than + is less than + is equal to + + + + + Value + + + + + Unit + + +
+
+ + + + + + + Outgassing values after 10h + + + {{ getFilterDisplayText(outgassingForm10h) }} + + + +
+ + Operator + + is greater than + is less than + is equal to + + + + + Value + + + + + Unit + + +
+
+ + + + + Outgassing values after 100h + + + {{ getFilterDisplayText(outgassingForm100h) }} + + + +
+ + Operator + + is greater than + is less than + is equal to + + + + + Value + + + + + Unit + + +
+
+ + + + + + Outgassing values after >100h + + + {{ getFilterDisplayText(outgassingFormGreater100h) }} + + + +
+ + Operator + + is greater than + is less than + is equal to + + + + + Value + + + + + Unit + + +
+
+
+
+ +
- -
- + \ No newline at end of file diff --git a/src/app/datasets/datasets-filter/datasets-filter.component.scss b/src/app/datasets/datasets-filter/datasets-filter.component.scss index 92dbd784e..127630a8c 100644 --- a/src/app/datasets/datasets-filter/datasets-filter.component.scss +++ b/src/app/datasets/datasets-filter/datasets-filter.component.scss @@ -69,4 +69,73 @@ mat-card { .section-container:first-child { margin-top: unset; } + + .outgassing-filters { + padding: 0.5rem; + border-radius: 4px; + background-color: rgba(0, 0, 0, 0.02); + margin-bottom: 1rem; + + h4 { + margin-top: 0; + margin-bottom: 0.5rem; + color: rgba(0, 0, 0, 0.7); + font-size: 1rem; + font-weight: 500; + } + + // Accordion styling + mat-expansion-panel { + margin-bottom: 0.5rem; + border-radius: 4px; + overflow: hidden; + + &:last-child { + margin-bottom: 0; + } + } + + .custom-panel-header { + height: auto !important; + padding: 8px 16px; + + .panel-header-content { + display: flex; + flex-direction: column; + width: 100%; + } + + ::ng-deep .mat-content { + flex-direction: column; + align-items: flex-start; + overflow: visible; + } + + ::ng-deep .mat-expansion-panel-header-title { + font-size: 0.9rem; + font-weight: 500; + color: rgba(0, 0, 0, 0.8); + margin-bottom: 4px; + white-space: normal; + } + + ::ng-deep .mat-expansion-panel-header-description { + font-size: 0.85rem; + color: rgba(0, 0, 0, 0.6); + margin: 0; + white-space: normal; + margin-top: 2px; + } + } .outgassing-form { + display: flex; + flex-direction: column; + gap: 0.5rem; + + // Make the form fields stack vertically + .form-field-full-width { + width: 100%; + margin-bottom: 0.5rem; + } + } + } } diff --git a/src/app/datasets/datasets-filter/datasets-filter.component.ts b/src/app/datasets/datasets-filter/datasets-filter.component.ts index fde7399fa..37cd5e136 100644 --- a/src/app/datasets/datasets-filter/datasets-filter.component.ts +++ b/src/app/datasets/datasets-filter/datasets-filter.component.ts @@ -5,18 +5,22 @@ import { Type, ViewContainerRef, } from "@angular/core"; +import { FormControl, FormGroup, Validators } from "@angular/forms"; import { MatDialog } from "@angular/material/dialog"; +import { MatSnackBar } from "@angular/material/snack-bar"; import { Store } from "@ngrx/store"; import { cloneDeep, isEqual } from "lodash-es"; import { selectHasAppliedFilters, selectScientificConditions, } from "state-management/selectors/datasets.selectors"; +import { ScientificCondition } from "state-management/models"; import { clearFacetsAction, fetchDatasetsAction, fetchFacetCountsAction, + addScientificConditionAction, } from "state-management/actions/datasets.actions"; import { deselectAllCustomColumnsAction, @@ -74,19 +78,67 @@ export class DatasetsFilterComponent implements OnInit, OnDestroy { scientificConditions$ = this.store.select(selectScientificConditions); appConfig = this.appConfigService.getConfig(); - + unitsEnabled = this.appConfig.scienceSearchUnitsEnabled; clearSearchBar = false; + // Track the expanded state of each outgassing filter panel + expandedPanels: { [key: string]: boolean } = { + typeOfCleaning: false, + outgassing1h: false, + outgassing10h: false, + outgassing100h: false, + outgassingGreater100h: false, + }; + hasAppliedFilters$ = this.store.select(selectHasAppliedFilters); labelMaps: { [key: string]: string } = {}; + // Add form groups for the outgassing filters + typeOfCleaningForm = new FormGroup({ + lhs: new FormControl("Type of Cleaning", [Validators.required]), + relation: new FormControl("EQUAL_TO_STRING", [Validators.required]), + rhs: new FormControl("", [Validators.required, Validators.minLength(1)]), + unit: new FormControl(""), + }); + + outgassingForm1h = new FormGroup({ + lhs: new FormControl("Outgassing values after 1h", [Validators.required]), + relation: new FormControl("GREATER_THAN", [Validators.required]), + rhs: new FormControl("", [Validators.required, Validators.minLength(1)]), + unit: new FormControl(""), + }); + + outgassingForm10h = new FormGroup({ + lhs: new FormControl("Outgassing values after 10h", [Validators.required]), + relation: new FormControl("GREATER_THAN", [Validators.required]), + rhs: new FormControl("", [Validators.required, Validators.minLength(1)]), + unit: new FormControl(""), + }); + + outgassingForm100h = new FormGroup({ + lhs: new FormControl("Outgassing values after 100h", [Validators.required]), + relation: new FormControl("GREATER_THAN", [Validators.required]), + rhs: new FormControl("", [Validators.required, Validators.minLength(1)]), + unit: new FormControl(""), + }); + + outgassingFormGreater100h = new FormGroup({ + lhs: new FormControl("Outgassing values after >100h", [ + Validators.required, + ]), + relation: new FormControl("GREATER_THAN", [Validators.required]), + rhs: new FormControl("", [Validators.required, Validators.minLength(1)]), + unit: new FormControl(""), + }); + constructor( public appConfigService: AppConfigService, public dialog: MatDialog, private store: Store, private asyncPipe: AsyncPipe, private viewContainerRef: ViewContainerRef, + private snackBar: MatSnackBar, ) {} ngOnInit() { @@ -175,10 +227,80 @@ export class DatasetsFilterComponent implements OnInit, OnDestroy { } applyFilters() { + // Apply all outgassing filters that have values + this.applyOutgassingFilters(); + + // Fetch datasets with applied filters this.store.dispatch(fetchDatasetsAction()); this.store.dispatch(fetchFacetCountsAction()); } + // Apply all outgassing filters at once + private applyOutgassingFilters() { + const forms = [ + { type: "typeOfCleaning", form: this.typeOfCleaningForm }, + { type: "1h", form: this.outgassingForm1h }, + { type: "10h", form: this.outgassingForm10h }, + { type: "100h", form: this.outgassingForm100h }, + { type: "greater100h", form: this.outgassingFormGreater100h }, + ]; + // Process each form that has a value + forms.forEach(({ type, form }) => { + // Only process if the form has a value + if (form.get("rhs")?.value) { + this.processOutgassingFilter( + type as "1h" | "10h" | "100h" | "greater100h", + ); + } + }); + } + + // Process a single outgassing filter + private processOutgassingFilter( + filterType: "typeOfCleaning" | "1h" | "10h" | "100h" | "greater100h", + ): void { + let form: FormGroup; + + // Select the appropriate form based on the filter type + switch (filterType) { + case "typeOfCleaning": + form = this.typeOfCleaningForm; + break; + case "1h": + form = this.outgassingForm1h; + break; + case "10h": + form = this.outgassingForm10h; + break; + case "100h": + form = this.outgassingForm100h; + break; + case "greater100h": + form = this.outgassingFormGreater100h; + break; + default: + return; + } + + // Check if form is valid + if (form.invalid || !form.get("rhs")?.value) { + return; + } + + const { lhs, relation, unit } = form.value; + const rawRhs = form.get("rhs")?.value; + + // Parse the value based on the relation + const rhs = + relation === "EQUAL_TO_STRING" ? String(rawRhs) : Number(rawRhs); + + // Create the condition + const condition = { lhs, relation, rhs, unit }; + console.log({ condition }); + // Dispatch the action to add the scientific condition + this.store.dispatch(addScientificConditionAction({ condition })); + } + renderComponent(filterObj: FilterConfig): any { const key = Object.keys(filterObj)[0]; const isEnabled = filterObj[key]; @@ -189,6 +311,98 @@ export class DatasetsFilterComponent implements OnInit, OnDestroy { return COMPONENT_MAP[key]; } + + // Method to apply outgassing filters + applyOutgassingFilter( + filterType: "typeOfCleaning" | "1h" | "10h" | "100h" | "greater100h", + ): void { + let form: FormGroup; + + // Select the appropriate form based on the filter type + switch (filterType) { + case "typeOfCleaning": + form = this.typeOfCleaningForm; + break; + case "1h": + form = this.outgassingForm1h; + break; + case "10h": + form = this.outgassingForm10h; + break; + case "100h": + form = this.outgassingForm100h; + break; + case "greater100h": + form = this.outgassingFormGreater100h; + break; + default: + return; + } + + // Check if form is valid + if (form.invalid) { + return; + } + + const { lhs, relation, unit } = form.value; + const rawRhs = form.get("rhs")?.value; + + // Parse the value based on the relation + const rhs = + relation === "EQUAL_TO_STRING" ? String(rawRhs) : Number(rawRhs); + + // Create the condition + const condition = { lhs, relation, rhs, unit }; + + // Dispatch the action to add the scientific condition + this.store.dispatch(addScientificConditionAction({ condition })); + + // Show success message + this.snackBar.open( + `Added filter: ${lhs} ${this.formatRelation(relation)} ${rhs} ${unit || ""}`, + "Close", + { + duration: 2000, + }, + ); + + // Reset the value field after applying + form.get("rhs")?.reset(""); + } + + // Helper method to format relation for display + private formatRelation(relation: string): string { + switch (relation) { + case "GREATER_THAN": + return ">"; + case "LESS_THAN": + return "<"; + case "EQUAL_TO_NUMERIC": + case "EQUAL_TO_STRING": + return "="; + default: + return relation; + } + } + + // Helper method to get filter display text + getFilterDisplayText(formGroup: FormGroup): string { + const relation = formGroup.get("relation")?.value; + const rhs = formGroup.get("rhs")?.value; + const unit = formGroup.get("unit")?.value; + + if (!rhs) { + return "No value set"; + } + + return `${this.formatRelation(relation)} ${rhs} ${unit || ""}`.trim(); + } + + // Toggle panel expansion + togglePanel(panel: string): void { + this.expandedPanels[panel] = !this.expandedPanels[panel]; + } + ngOnDestroy() { this.subscriptions.forEach((subscription) => subscription.unsubscribe()); } diff --git a/src/app/datasets/datasets.module.ts b/src/app/datasets/datasets.module.ts index 9bbefaf23..3e132a2e4 100644 --- a/src/app/datasets/datasets.module.ts +++ b/src/app/datasets/datasets.module.ts @@ -20,6 +20,7 @@ import { MatCardModule } from "@angular/material/card"; import { MatCheckboxModule } from "@angular/material/checkbox"; import { MatOptionModule } from "@angular/material/core"; import { MatDialogModule } from "@angular/material/dialog"; +import { MatExpansionModule } from "@angular/material/expansion"; import { MatFormFieldModule } from "@angular/material/form-field"; import { MatGridListModule } from "@angular/material/grid-list"; import { MatIconModule } from "@angular/material/icon"; @@ -49,7 +50,6 @@ import { DatasetDetailComponent } from "./dataset-detail/dataset-detail/dataset- import { DatasetTableComponent } from "./dataset-table/dataset-table.component"; import { DatasetsFilterComponent } from "./datasets-filter/datasets-filter.component"; import { AddDatasetDialogComponent } from "./add-dataset-dialog/add-dataset-dialog.component"; -import { DatasetTableSettingsComponent } from "./dataset-table-settings/dataset-table-settings.component"; import { DatasetTableActionsComponent } from "./dataset-table-actions/dataset-table-actions.component"; import { DatasetLifecycleComponent } from "./dataset-lifecycle/dataset-lifecycle.component"; import { SampleEditComponent } from "./sample-edit/sample-edit.component"; @@ -85,6 +85,8 @@ import { userReducer } from "state-management/reducers/user.reducer"; import { MatSnackBarModule } from "@angular/material/snack-bar"; import { DatasetDetailDynamicComponent } from "./dataset-detail/dataset-detail-dynamic/dataset-detail-dynamic.component"; import { DatasetDetailWrapperComponent } from "./dataset-detail/dataset-detail-wrapper.component"; +import { JsonHeadPipe } from "shared/pipes/json-head.pipe"; +import { ThumbnailPipe } from "shared/pipes/thumbnail.pipe"; @NgModule({ imports: [ CommonModule, @@ -100,6 +102,7 @@ import { DatasetDetailWrapperComponent } from "./dataset-detail/dataset-detail-w MatChipsModule, MatDatepickerModule, MatDialogModule, + MatExpansionModule, MatFormFieldModule, MatGridListModule, MatIconModule, @@ -162,7 +165,6 @@ import { DatasetDetailWrapperComponent } from "./dataset-detail/dataset-detail-w ReduceComponent, DatasetDetailsDashboardComponent, AddDatasetDialogComponent, - DatasetTableSettingsComponent, DatasetTableActionsComponent, DatasetLifecycleComponent, SampleEditComponent, @@ -178,6 +180,8 @@ import { DatasetDetailWrapperComponent } from "./dataset-detail/dataset-detail-w providers: [ ArchivingService, AsyncPipe, + JsonHeadPipe, + ThumbnailPipe, ADAuthService, SharedScicatFrontendModule, FileSizePipe, diff --git a/src/app/files/files-dashboard/files-dashboard.component.html b/src/app/files/files-dashboard/files-dashboard.component.html index b61a861b2..aec823181 100644 --- a/src/app/files/files-dashboard/files-dashboard.component.html +++ b/src/app/files/files-dashboard/files-dashboard.component.html @@ -1,8 +1,20 @@ - - + diff --git a/src/app/files/files-dashboard/files-dashboard.component.spec.ts b/src/app/files/files-dashboard/files-dashboard.component.spec.ts index 72207c6a9..e8b70bc7e 100644 --- a/src/app/files/files-dashboard/files-dashboard.component.spec.ts +++ b/src/app/files/files-dashboard/files-dashboard.component.spec.ts @@ -1,28 +1,24 @@ import { NO_ERRORS_SCHEMA } from "@angular/core"; import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; import { ActivatedRoute, Router } from "@angular/router"; -import { AppConfigService } from "app-config.service"; -import { MockActivatedRoute, MockRouter } from "shared/MockStubs"; -import { ExportExcelService } from "shared/services/export-excel.service"; -import { ScicatDataService } from "shared/services/scicat-data-service"; +import { MockActivatedRoute, MockRouter, MockStore } from "shared/MockStubs"; import { FilesDashboardComponent } from "./files-dashboard.component"; +import { DatePipe } from "@angular/common"; +import { Store } from "@ngrx/store"; describe("FilesDashboardComponent", () => { let component: FilesDashboardComponent; let fixture: ComponentFixture; - const getConfig = () => ({}); - beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ schemas: [NO_ERRORS_SCHEMA], declarations: [FilesDashboardComponent], providers: [ { provide: ActivatedRoute, useClass: MockActivatedRoute }, - { provide: AppConfigService, useValue: { getConfig } }, - { provide: ExportExcelService, useValue: {} }, { provide: Router, useClass: MockRouter }, - { provide: ScicatDataService, useValue: {} }, + { provide: Store, useClass: MockStore }, + DatePipe, ], }).compileComponents(); })); diff --git a/src/app/files/files-dashboard/files-dashboard.component.ts b/src/app/files/files-dashboard/files-dashboard.component.ts index 7eddc0fe3..5473fc45e 100644 --- a/src/app/files/files-dashboard/files-dashboard.component.ts +++ b/src/app/files/files-dashboard/files-dashboard.component.ts @@ -1,9 +1,30 @@ -import { Component, OnDestroy } from "@angular/core"; -import { SciCatDataSource } from "../../shared/services/scicat.datasource"; -import { ScicatDataService } from "../../shared/services/scicat-data-service"; -import { ExportExcelService } from "../../shared/services/export-excel.service"; -import { Column } from "shared/modules/shared-table/shared-table.module"; -import { AppConfigService } from "app-config.service"; +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { BehaviorSubject, Subscription } from "rxjs"; +import { TableField } from "shared/modules/dynamic-material-table/models/table-field.model"; +import { + ITableSetting, + TableSettingEventType, +} from "shared/modules/dynamic-material-table/models/table-setting.model"; +import { + TablePagination, + TablePaginationMode, +} from "shared/modules/dynamic-material-table/models/table-pagination.model"; +import { + IRowEvent, + ITableEvent, + TableEventType, + TableSelectionMode, +} from "shared/modules/dynamic-material-table/models/table-row.model"; +import { Store } from "@ngrx/store"; +import { ActivatedRoute, Router } from "@angular/router"; +import { updateUserSettingsAction } from "state-management/actions/user.actions"; +import { Sort } from "@angular/material/sort"; +import { selectFilesWithCountAndTableSettings } from "state-management/selectors/files.selectors"; +import { fetchAllOrigDatablocksAction } from "state-management/actions/files.actions"; +import { get } from "lodash-es"; +import { DatePipe } from "@angular/common"; +import { actionMenu } from "shared/modules/dynamic-material-table/utilizes/default-table-settings"; +import { TableConfigService } from "shared/services/table-config.service"; @Component({ selector: "app-files-dashboard", @@ -11,92 +32,278 @@ import { AppConfigService } from "app-config.service"; styleUrls: ["./files-dashboard.component.scss"], standalone: false, }) -export class FilesDashboardComponent implements OnDestroy { - columns: Column[] = [ - { - id: "dataFileList.path", - icon: "text_snippet", - label: "Filename", - canSort: true, - matchMode: "contains", - hideOrder: 1, - }, - { - id: "dataFileList.size", - icon: "save", - label: "Size", - canSort: true, - matchMode: "greaterThan", - hideOrder: 2, - }, - { - id: "dataFileList.time", - icon: "access_time", - label: "Created at", - format: "date medium", - canSort: true, - matchMode: "between", - sortDefault: "desc", - hideOrder: 3, - }, - { - id: "dataFileList.uid", - icon: "person", - label: "UID", - canSort: true, - matchMode: "contains", - hideOrder: 4, - }, - { - id: "dataFileList.gid", - icon: "group", - label: "GID", - canSort: true, - matchMode: "contains", - hideOrder: 5, - }, - { - id: "ownerGroup", - icon: "group", - label: "Owner Group", - canSort: true, - matchMode: "contains", - hideOrder: 6, - }, - { - id: "datasetId", - icon: "list", - label: "Dataset PID", - type: "dataseturl", - canSort: true, - matchMode: "contains", - hideOrder: 7, - }, - ]; +export class FilesDashboardComponent implements OnInit, OnDestroy { + filesWithCountAndTableSettings$ = this.store.select( + selectFilesWithCountAndTableSettings, + ); - tableDefinition = { - collection: "Origdatablocks", - columns: this.columns, - }; + subscriptions: Subscription[] = []; + + tableName = "filesTable"; + + columns: TableField[]; + + pending = true; + + setting: ITableSetting = {}; + + paginationMode: TablePaginationMode = "server-side"; + + dataSource: BehaviorSubject = new BehaviorSubject([]); - dataSource: SciCatDataSource; + pagination: TablePagination = {}; + + rowSelectionMode: TableSelectionMode = "none"; + + globalTextSearch = ""; + + defaultPageSize = 10; + + defaultPageSizeOptions = [5, 10, 25, 100]; + + tablesSettings: object; + + tableDefaultSettingsConfig: ITableSetting = { + visibleActionMenu: actionMenu, + settingList: [ + { + visibleActionMenu: actionMenu, + isDefaultSetting: true, + isCurrentSetting: true, + columnSetting: [ + { + name: "dataFileList.path", + icon: "text_snippet", + header: "Filename", + customRender(column, row) { + return get(row, column.name); + }, + }, + { + name: "dataFileList.size", + icon: "save", + header: "Size", + customRender(column, row) { + return get(row, column.name); + }, + }, + { + name: "dataFileList.time", + icon: "access_time", + header: "Created at", + customRender: (column, row) => { + return this.datePipe.transform(get(row, column.name)); + }, + }, + { + name: "dataFileList.uid", + icon: "person", + header: "UID", + customRender: (column, row) => { + return get(row, column.name); + }, + }, + { + name: "dataFileList.gid", + icon: "group", + header: "GID", + customRender: (column, row) => { + return get(row, column.name); + }, + }, + { + name: "ownerGroup", + icon: "group", + header: "Owner Group", + }, + { + name: "datasetId", + icon: "list", + header: "Dataset PID", + customRender: (column, row) => + `
${row[column.name]}`, + }, + ], + }, + ], + rowStyle: { + "border-bottom": "1px solid #d2d2d2", + }, + }; constructor( - private appConfigService: AppConfigService, - private dataService: ScicatDataService, - private exportService: ExportExcelService, - ) { - this.dataSource = new SciCatDataSource( - this.appConfigService, - this.dataService, - this.exportService, - this.tableDefinition, + private store: Store, + private router: Router, + private route: ActivatedRoute, + private datePipe: DatePipe, + private tableConfigService: TableConfigService, + ) {} + + ngOnInit(): void { + this.subscriptions.push( + this.filesWithCountAndTableSettings$.subscribe( + ({ origDatablocks, count, tablesSettings }) => { + this.tablesSettings = tablesSettings; + this.dataSource.next(origDatablocks); + this.pending = false; + + const savedTableConfigColumns = + tablesSettings?.[this.tableName]?.columns; + const tableSort = this.getTableSort(); + const paginationConfig = this.getTablePaginationConfig(count); + + const tableSettingsConfig = + this.tableConfigService.getTableSettingsConfig( + this.tableName, + this.tableDefaultSettingsConfig, + savedTableConfigColumns, + tableSort, + ); + + if (tableSettingsConfig?.settingList.length) { + this.initTable(tableSettingsConfig, paginationConfig); + } + }, + ), + ); + + this.subscriptions.push( + this.route.queryParams.subscribe((queryParams) => { + this.pending = true; + const limit = queryParams.pageSize + ? +queryParams.pageSize + : this.defaultPageSize; + const skip = queryParams.pageIndex ? +queryParams.pageIndex * limit : 0; + if (queryParams.textSearch) { + this.globalTextSearch = queryParams.textSearch; + } + + this.store.dispatch( + fetchAllOrigDatablocksAction({ + limit: limit, + skip: skip, + search: queryParams.textSearch, + sortColumn: queryParams.sortColumn, + sortDirection: queryParams.sortDirection, + }), + ); + }), ); } - ngOnDestroy() { - this.dataSource.disconnectExportData(); + getTableSort(): ITableSetting["tableSort"] { + const { queryParams } = this.route.snapshot; + + if (queryParams.sortDirection && queryParams.sortColumn) { + return { + sortColumn: queryParams.sortColumn, + sortDirection: queryParams.sortDirection, + }; + } + + return null; + } + + getTablePaginationConfig(dataCount = 0): TablePagination { + const { queryParams } = this.route.snapshot; + + return { + pageSizeOptions: this.defaultPageSizeOptions, + pageIndex: queryParams.pageIndex, + pageSize: queryParams.pageSize || this.defaultPageSize, + length: dataCount, + }; + } + + initTable( + settingConfig: ITableSetting, + paginationConfig: TablePagination, + ): void { + const currentColumnSetting = settingConfig.settingList.find( + (s) => s.isCurrentSetting, + )?.columnSetting; + + this.columns = currentColumnSetting; + this.setting = settingConfig; + this.pagination = paginationConfig; + } + + onPaginationChange(pagination: TablePagination) { + this.router.navigate([], { + queryParams: { + pageIndex: pagination.pageIndex, + pageSize: pagination.pageSize, + }, + queryParamsHandling: "merge", + }); + } + + onGlobalTextSearchChange(text: string) { + this.router.navigate([], { + queryParams: { + textSearch: text || undefined, + pageIndex: 0, + }, + queryParamsHandling: "merge", + }); } - onRowClick(file: any) {} + saveTableSettings(setting: ITableSetting) { + this.pending = true; + const columnsSetting = setting.columnSetting.map((column) => { + const { name, display, index, width } = column; + + return { name, display, index, width }; + }); + + const tablesSettings = { + ...this.tablesSettings, + [setting.settingName || this.tableName]: { + columns: columnsSetting, + }, + }; + + this.store.dispatch( + updateUserSettingsAction({ + property: { + tablesSettings, + }, + }), + ); + } + + onSettingChange(event: { + type: TableSettingEventType; + setting: ITableSetting; + }) { + if ( + event.type === TableSettingEventType.save || + event.type === TableSettingEventType.create + ) { + this.saveTableSettings(event.setting); + } + } + + onRowClick(event: IRowEvent) {} + + onTableEvent({ event, sender }: ITableEvent) { + if (event === TableEventType.SortChanged) { + const { active: sortColumn, direction: sortDirection } = sender as Sort; + + this.router.navigate([], { + queryParams: { + pageIndex: 0, + sortDirection: sortDirection || undefined, + sortColumn: sortDirection ? sortColumn : undefined, + }, + queryParamsHandling: "merge", + }); + } + } + + ngOnDestroy() { + this.subscriptions.forEach((sub) => { + sub.unsubscribe(); + }); + } } diff --git a/src/app/files/files.module.ts b/src/app/files/files.module.ts index 40a9055b5..4adebb54a 100644 --- a/src/app/files/files.module.ts +++ b/src/app/files/files.module.ts @@ -6,18 +6,22 @@ import { MatIconModule } from "@angular/material/icon"; import { FlexLayoutModule } from "@ngbracket/ngx-layout"; import { SharedScicatFrontendModule } from "shared/shared.module"; import { FilesDashboardComponent } from "./files-dashboard/files-dashboard.component"; - -// TODO remove unneeded "store" structures in new componnets +import { StoreModule } from "@ngrx/store"; +import { filesReducer } from "state-management/reducers/files.reducer"; +import { EffectsModule } from "@ngrx/effects"; +import { FilesEffects } from "state-management/effects/files.effects"; @NgModule({ declarations: [FilesDashboardComponent], imports: [ CommonModule, + EffectsModule.forFeature([FilesEffects]), FlexLayoutModule, MatButtonToggleModule, MatCardModule, MatIconModule, SharedScicatFrontendModule, + StoreModule.forFeature("files", filesReducer), ], exports: [FilesDashboardComponent], }) diff --git a/src/app/samples/sample-dashboard/sample-dashboard.component.ts b/src/app/samples/sample-dashboard/sample-dashboard.component.ts index 95ead5466..600dd454e 100644 --- a/src/app/samples/sample-dashboard/sample-dashboard.component.ts +++ b/src/app/samples/sample-dashboard/sample-dashboard.component.ts @@ -84,6 +84,7 @@ export class SampleDashboardComponent implements OnInit, OnDestroy { }, { name: "ownerGroup", + header: "Owner group", icon: "group", }, ], diff --git a/src/app/shared/MockStubs.ts b/src/app/shared/MockStubs.ts index ad2189322..2c82ad519 100644 --- a/src/app/shared/MockStubs.ts +++ b/src/app/shared/MockStubs.ts @@ -20,6 +20,7 @@ import { Logbook, Policy, ReturnedUserDto, + OrigDatablock, OutputAttachmentV3Dto, } from "@scicatproject/scicat-sdk-ts-angular"; import { SDKToken } from "./services/auth/auth.service"; @@ -326,6 +327,7 @@ export const mockAttachment = createMock({}); export const mockSample = createMock({}); export const mockProposal = createMock({}); export const mockInstrument = createMock({}); +export const mockOrigDatablock = createMock({}); export const mockJob = createMock({}); export const mockLogbook = createMock({}); export const mockPolicy = createMock({}); 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 index 64ff419a9..a20367f86 100644 --- 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 @@ -39,6 +39,7 @@ 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"; +import { MatCheckboxChange } from "@angular/material/checkbox"; @Directive({ // eslint-disable-next-line @angular-eslint/directive-selector @@ -72,6 +73,7 @@ export class TableCoreDirective { @Input() showGlobalTextSearch = true; @Input() globalTextSearch = ""; @Input() globalTextSearchPlaceholder = "Search"; + @Input() selectionIds = []; // 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 @@ -438,7 +440,7 @@ export class TableCoreDirective { } /** Selects all rows if they are not all selected; otherwise clear selection. */ - masterToggle() { + masterToggle(e: MatCheckboxChange) { const isAllSelected = this.isAllSelected(); if (isAllSelected === false) { this.tvsDataSource.filteredData.forEach((row) => @@ -449,11 +451,14 @@ export class TableCoreDirective { } this.onRowEvent.emit({ event: RowEventType.MasterSelectionChange, - sender: { selectionModel: this._rowSelectionModel }, + sender: { + selectionModel: this._rowSelectionModel, + checked: e.checked, + }, }); } - onRowSelectionChange(e: any, row: T) { + onRowSelectionChange(e: MatCheckboxChange, row: T) { if (e) { this._rowSelectionModel.toggle(row); this.onRowEvent.emit({ @@ -461,8 +466,15 @@ export class TableCoreDirective { sender: { selectionModel: this._rowSelectionModel, row: row, + checked: e.checked, }, }); } } + + isInSelection(row: T) { + const id = row._id || row.pid; + + return this.selectionIds.indexOf(id) !== -1; + } } 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 index fa0c8bcfb..874d9ab39 100644 --- 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 @@ -73,8 +73,8 @@ export interface AbstractField { toString?: (column: TableField, row: TableRow) => string; customSort?: (column: TableField, row: any) => string; customRender?: (column: TableField, row: any) => string; - renderContentIcon?: (column: TableField, row: any) => boolean; - contentIcon?: string; + renderImage?: boolean; + renderContentIcon?: (column: TableField, row: any) => string; contentIconTooltip?: string; contentIconClass?: string; contentIconLink?: (column: TableField, row: any) => string; 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 index 40f793661..f8305923a 100644 --- 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 @@ -6,6 +6,8 @@ import { TableField } from "./table-field.model"; // this fields are for each row data export interface TableRow { id?: number; + _id?: string; + pid?: string; rowActionMenu?: { [key: string]: ContextMenuItem }; option?: RowOption; } 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 index 1d9645832..54ff342dd 100644 --- 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 @@ -1,12 +1,12 @@ import { TableScrollStrategy } from "../cores/fixed-size-table-virtual-scroll-strategy"; -import { AbstractField } from "./table-field.model"; +import { TableField } from "./table-field.model"; export type Direction = "rtl" | "ltr"; export type DisplayMode = "visible" | "hidden" | "none"; export interface ITableSetting { pageSize?: number; direction?: Direction; - columnSetting?: AbstractField[] | null; + columnSetting?: TableField[] | null; visibleActionMenu?: VisibleActionMenu | null; visibleTableMenu?: boolean; alternativeRowStyle?: any; 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 index 3428cba56..439393126 100644 --- 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 @@ -31,7 +31,7 @@ > @@ -129,28 +130,38 @@ [ngStyle]="cellStyle(row?.option, column)" (contextmenu)="onContextMenu($event, column, row)" > - - + + + - - + + + + + + + @@ -164,16 +175,16 @@ {{ column.contentIcon }}{{ column.renderContentIcon(column, row) }} {{ column.contentIcon }}{{ column.renderContentIcon(column, row) }} 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 index 7a452ce63..aa059b2b9 100644 --- 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 @@ -31,6 +31,7 @@ .label-cell { width: 100%; + cursor: inherit; } mat-cell .label-cell { 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 index 70254fd23..e9c30d689 100644 --- 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 @@ -519,14 +519,6 @@ export class DynamicMatTableComponent return row[column.name]; } - shouldRenderContentIcon(row: any, column: TableField) { - if (column.renderContentIcon) { - return column.renderContentIcon(column, row); - } - - return false; - } - renderContentIconLink(row: any, column: TableField) { if (column.contentIconLink) { return column.contentIconLink(column, row); 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 index f54a12eef..b7fdc4ddf 100644 --- 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 @@ -36,6 +36,7 @@ import { TooltipDirective } from "../tooltip/tooltip.directive"; import { TemplateOrStringDirective } from "../tooltip/template-or-string.directive"; import { FormsModule } from "@angular/forms"; import { ITableSetting, TableSetting } from "../models/table-setting.model"; +import { PipesModule } from "shared/pipes/pipes.module"; // eslint-disable-next-line @typescript-eslint/naming-convention const ExtensionsModule = [HeaderFilterModule, RowMenuModule]; @@ -64,6 +65,7 @@ const ExtensionsModule = [HeaderFilterModule, RowMenuModule]; MatRippleModule, OverlayModule, ExtensionsModule, + PipesModule, ], exports: [DynamicMatTableComponent], declarations: [ diff --git a/src/app/shared/modules/filters/condition-filter.component.html b/src/app/shared/modules/filters/condition-filter.component.html index b19da68e7..c5e2a2009 100644 --- a/src/app/shared/modules/filters/condition-filter.component.html +++ b/src/app/shared/modules/filters/condition-filter.component.html @@ -1,9 +1,5 @@ - + Condition - + \ No newline at end of file diff --git a/src/app/shared/modules/scientific-metadata/metadata-view/metadata-view.component.ts b/src/app/shared/modules/scientific-metadata/metadata-view/metadata-view.component.ts index 1aaf76ce6..fd1eeb996 100644 --- a/src/app/shared/modules/scientific-metadata/metadata-view/metadata-view.component.ts +++ b/src/app/shared/modules/scientific-metadata/metadata-view/metadata-view.component.ts @@ -112,9 +112,24 @@ export class MetadataViewComponent implements OnInit, OnChanges { return row[column.name]; }, - contentIcon: "hub", + toExport: (column, row) => { + if (row.type === "date" || this.isDate(row)) { + return this.datePipe.transform(row[column.name]); + } + + if (row.type === "link") { + return this.linkyPipe.transform(row[column.name] || "", { + urls: true, + newWindow: true, + stripPrefix: false, + sanitizeHtml: true, + }); + } + + return row[column.name]; + }, renderContentIcon: (column, row) => { - return !!row.ontology_reference; + return row.ontology_reference ? "hub" : ""; }, contentIconLink: (column, row) => { return row.ontology_reference; @@ -128,10 +143,12 @@ export class MetadataViewComponent implements OnInit, OnChanges { ? this.prettyUnit.transform(row[column.name]) : "--"; }, + toExport: (row) => { + return row.unit ? this.prettyUnit.transform(row.unit) : "--"; + }, renderContentIcon: (column, row) => { - return row.validUnit === false; + return row.validUnit === false ? "error" : ""; }, - contentIcon: "error", contentIconTooltip: "Unrecognized unit, conversion disabled", contentIconClass: "general-warning", cellClass: "unit-input", diff --git a/src/app/shared/services/scicat.datasource.ts b/src/app/shared/services/scicat.datasource.ts index 17584c6e7..bd43dbc40 100644 --- a/src/app/shared/services/scicat.datasource.ts +++ b/src/app/shared/services/scicat.datasource.ts @@ -4,7 +4,7 @@ import { ScicatDataService } from "./scicat-data-service"; import { catchError, finalize } from "rxjs/operators"; import { ExportExcelService } from "./export-excel.service"; import { Column } from "shared/modules/shared-table/shared-table.module"; -import { AppConfig, AppConfigService } from "app-config.service"; +import { AppConfigInterface, AppConfigService } from "app-config.service"; // For each different table type one instance of this class should be created @@ -12,7 +12,7 @@ const resolvePath = (object: any, path: string, defaultValue: unknown) => path.split(".").reduce((o, p) => (o ? o[p] : defaultValue), object); export class SciCatDataSource implements DataSource { - private appConfig: AppConfig; + private appConfig: AppConfigInterface; private exportSubscription: Subscription; private dataForExcel: unknown[] = []; private columnsdef: Column[] = []; diff --git a/src/app/state-management/actions/files.actions.spec.ts b/src/app/state-management/actions/files.actions.spec.ts new file mode 100644 index 000000000..15d4d0a02 --- /dev/null +++ b/src/app/state-management/actions/files.actions.spec.ts @@ -0,0 +1,114 @@ +import { mockOrigDatablock as origDatablock } from "shared/MockStubs"; +import * as fromActions from "./files.actions"; + +describe("File Actions", () => { + const origDatablocks = [origDatablock]; + + describe("fetchAllOrigDatablocksAction", () => { + it("should create an action", () => { + const action = fromActions.fetchAllOrigDatablocksAction({}); + + expect({ ...action }).toEqual({ + type: "[OrigDatablock] Fetch All Orig Datablocks", + }); + }); + }); + + describe("fetchAllOrigDatablocksCompleteAction", () => { + it("should create an action", () => { + const action = fromActions.fetchAllOrigDatablocksCompleteAction({ + origDatablocks: origDatablocks, + }); + + expect({ ...action }).toEqual({ + type: "[OrigDatablock] Fetch All Orig Datablocks Complete", + origDatablocks, + }); + }); + }); + + describe("fetchAllOrigDatablocksFailedAction", () => { + it("should create an action", () => { + const action = fromActions.fetchAllOrigDatablocksFailedAction(); + + expect({ ...action }).toEqual({ + type: "[OrigDatablock] Fetch All Orig Datablocks Failed", + }); + }); + }); + + describe("fetchCountAction", () => { + it("should create an action", () => { + const action = fromActions.fetchCountAction({}); + + expect({ ...action }).toEqual({ + type: "[OrigDatablock] Fetch Count", + }); + }); + }); + + describe("fetchCountCompleteAction", () => { + it("should create an action", () => { + const count = 100; + const action = fromActions.fetchCountCompleteAction({ count }); + + expect({ ...action }).toEqual({ + type: "[OrigDatablock] Fetch Count Complete", + count, + }); + }); + }); + + describe("fetchCountFailedAction", () => { + it("should create an action", () => { + const action = fromActions.fetchCountFailedAction(); + + expect({ ...action }).toEqual({ + type: "[OrigDatablock] Fetch Count Failed", + }); + }); + }); + + describe("fetchOrigDatablockAction", () => { + it("should create an action", () => { + const id = "testId"; + const action = fromActions.fetchOrigDatablockAction({ id }); + + expect({ ...action }).toEqual({ + type: "[OrigDatablock] Fetch Orig Datablock", + id, + }); + }); + }); + + describe("fetchOrigDatablockCompleteAction", () => { + it("should create an action", () => { + const action = fromActions.fetchOrigDatablockCompleteAction({ + origDatablock, + }); + + expect({ ...action }).toEqual({ + type: "[OrigDatablock] Fetch Orig Datablock Complete", + origDatablock, + }); + }); + }); + + describe("fetchOrigDatablockFailedAction", () => { + it("should create an action", () => { + const action = fromActions.fetchOrigDatablockFailedAction(); + + expect({ ...action }).toEqual({ + type: "[OrigDatablock] Fetch Orig Datablock Failed", + }); + }); + }); + + describe("clearFilesStateAction", () => { + it("should create an action", () => { + const action = fromActions.clearOrigDatablockStateAction(); + + expect({ ...action }).toEqual({ type: "[OrigDatablock] Clear State" }); + }); + }); +}); diff --git a/src/app/state-management/actions/files.actions.ts b/src/app/state-management/actions/files.actions.ts new file mode 100644 index 000000000..f93892087 --- /dev/null +++ b/src/app/state-management/actions/files.actions.ts @@ -0,0 +1,48 @@ +import { createAction, props } from "@ngrx/store"; +import { OrigDatablock } from "@scicatproject/scicat-sdk-ts-angular"; + +export const fetchAllOrigDatablocksAction = createAction( + "[OrigDatablock] Fetch All Orig Datablocks", + props<{ + skip?: number; + limit?: number; + search?: string; + sortDirection?: string; + sortColumn?: string; + }>(), +); +export const fetchAllOrigDatablocksCompleteAction = createAction( + "[OrigDatablock] Fetch All Orig Datablocks Complete", + props<{ origDatablocks: object[] }>(), +); +export const fetchAllOrigDatablocksFailedAction = createAction( + "[OrigDatablock] Fetch All Orig Datablocks Failed", +); + +export const fetchCountAction = createAction( + "[OrigDatablock] Fetch Count", + props<{ fields?: Record }>(), +); +export const fetchCountCompleteAction = createAction( + "[OrigDatablock] Fetch Count Complete", + props<{ count: number }>(), +); +export const fetchCountFailedAction = createAction( + "[OrigDatablock] Fetch Count Failed", +); + +export const fetchOrigDatablockAction = createAction( + "[OrigDatablock] Fetch Orig Datablock", + props<{ id: string }>(), +); +export const fetchOrigDatablockCompleteAction = createAction( + "[OrigDatablock] Fetch Orig Datablock Complete", + props<{ origDatablock: OrigDatablock }>(), +); +export const fetchOrigDatablockFailedAction = createAction( + "[OrigDatablock] Fetch Orig Datablock Failed", +); + +export const clearOrigDatablockStateAction = createAction( + "[OrigDatablock] Clear State", +); diff --git a/src/app/state-management/actions/user.actions.ts b/src/app/state-management/actions/user.actions.ts index 25aefeecf..5f981db53 100644 --- a/src/app/state-management/actions/user.actions.ts +++ b/src/app/state-management/actions/user.actions.ts @@ -10,7 +10,7 @@ import { ConditionConfig, FilterConfig, } from "../../shared/modules/filters/filters.module"; -import { AppConfig } from "app-config.service"; +import { AppConfigInterface } from "app-config.service"; import { AccessTokenInterface } from "shared/services/auth/auth.service"; export const setDatasetTableColumnsAction = createAction( @@ -176,6 +176,16 @@ export const updateFilterConfigs = createAction( props<{ filterConfigs: FilterConfig[] }>(), ); +export const updateHasFetchedSettings = createAction( + "[User] Update Has Fetched User Settings", + props<{ hasFetchedSettings: boolean }>(), +); + +export const updateIsPublishedAction = createAction( + "[User] Update Is Published", + props<{ isPublished: boolean }>(), +); + export const updateConditionsConfigs = createAction( "[User] Update Conditions Configs", props<{ conditionConfigs: ConditionConfig[] }>(), @@ -183,5 +193,5 @@ export const updateConditionsConfigs = createAction( export const loadDefaultSettings = createAction( "[User] Load Default Settings", - props<{ config: AppConfig }>(), + props<{ config: AppConfigInterface }>(), ); diff --git a/src/app/state-management/effects/datasets.effects.ts b/src/app/state-management/effects/datasets.effects.ts index 35a390bcb..45e2d8ede 100644 --- a/src/app/state-management/effects/datasets.effects.ts +++ b/src/app/state-management/effects/datasets.effects.ts @@ -403,6 +403,7 @@ export class DatasetEffects { fromActions.addAttachmentAction, fromActions.updateAttachmentCaptionAction, fromActions.removeAttachmentAction, + fromActions.setPublicViewModeAction, ), switchMap(() => of(loadingAction())), ); diff --git a/src/app/state-management/effects/files.effects.spec.ts b/src/app/state-management/effects/files.effects.spec.ts new file mode 100644 index 000000000..964a9ed81 --- /dev/null +++ b/src/app/state-management/effects/files.effects.spec.ts @@ -0,0 +1,222 @@ +import { OrigdatablocksService } from "@scicatproject/scicat-sdk-ts-angular"; +import { TestBed } from "@angular/core/testing"; +import { provideMockActions } from "@ngrx/effects/testing"; +import * as fromActions from "state-management/actions/files.actions"; +import { hot, cold } from "jasmine-marbles"; +import { provideMockStore } from "@ngrx/store/testing"; +import { + loadingAction, + loadingCompleteAction, +} from "state-management/actions/user.actions"; +import { Type } from "@angular/core"; +import { TestObservable } from "jasmine-marbles/src/test-observables"; +import { createMock } from "shared/MockStubs"; +import { FilesEffects } from "./files.effects"; + +describe("FileEffects", () => { + let actions: TestObservable; + let effects: FilesEffects; + let origDatablocksApi: jasmine.SpyObj; + + const mockOrigDatablockFile = createMock({ + _id: "2b568c4d-7b0f-47f5-b4af-b94098d6b98f", + size: 913818, + dataFileList: { + path: "V20_ESSIntegration_2018-12-11_0952.nxs", + size: 913818, + time: "2018-12-11T08:53:29.000Z", + chk: "string", + uid: "10095", + gid: "4064", + perm: "755", + }, + ownerGroup: "ess", + accessGroups: ["brightness", "ess"], + createdBy: "ingestor", + updatedBy: "admin", + datasetId: "20.500.12269/BRIGHTNESS/V200022", + rawDatasetId: "20.500.12269/BRIGHTNESS/V200022", + derivedDatasetId: "20.500.12269/BRIGHTNESS/V200022", + createdAt: "2018-12-11T08:53:29.000Z", + updatedAt: "2020-09-11T07:36:53.857Z", + datasetExist: true, + }); + + const origDatablockFiles = [mockOrigDatablockFile]; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + FilesEffects, + provideMockActions(() => actions), + provideMockStore({ + selectors: [], + }), + { + provide: OrigdatablocksService, + useValue: jasmine.createSpyObj("origDatablocksApi", [ + "origDatablocksControllerFullqueryFilesV3", + "origDatablocksControllerFullfacetFilesV3", + ]), + }, + ], + }); + + effects = TestBed.inject(FilesEffects); + origDatablocksApi = injectedStub(OrigdatablocksService); + }); + + const injectedStub = (service: Type): jasmine.SpyObj => + TestBed.inject(service) as jasmine.SpyObj; + + describe("fetchAllOrigDatablocks$", () => { + describe("ofType fetchAllOrigDatablocksAction", () => { + it("should result in a fetchAllOrigDatablocksCompleteAction and a fetchCountAction", () => { + const action = fromActions.fetchAllOrigDatablocksAction({}); + const outcome1 = fromActions.fetchAllOrigDatablocksCompleteAction({ + origDatablocks: origDatablockFiles, + }); + const outcome2 = fromActions.fetchCountAction({ + fields: { text: undefined }, + }); + + actions = hot("-a", { a: action }); + const response = cold("-a|", { a: origDatablockFiles }); + origDatablocksApi.origDatablocksControllerFullqueryFilesV3.and.returnValue( + response, + ); + + const expected = cold("--(bc)", { b: outcome1, c: outcome2 }); + expect(effects.fetchAllOrigDatablocks$).toBeObservable(expected); + }); + + it("should result in a fetchAllOrigDatablocksFailedAction", () => { + const action = fromActions.fetchAllOrigDatablocksAction({}); + const outcome = fromActions.fetchAllOrigDatablocksFailedAction(); + + actions = hot("-a", { a: action }); + const response = cold("-#", {}); + origDatablocksApi.origDatablocksControllerFullqueryFilesV3.and.returnValue( + response, + ); + + const expected = cold("--b", { b: outcome }); + expect(effects.fetchAllOrigDatablocks$).toBeObservable(expected); + }); + }); + }); + + describe("fetchCount$", () => { + it("should result in a fetchCountCompleteAction", () => { + const count = origDatablockFiles.length; + const action = fromActions.fetchCountAction({}); + const outcome = fromActions.fetchCountCompleteAction({ + count, + }); + + const responseArray = [{ all: [{ totalSets: count }] }]; + + actions = hot("-a", { a: action }); + const response = cold("-a|", { a: responseArray }); + origDatablocksApi.origDatablocksControllerFullfacetFilesV3.and.returnValue( + response, + ); + + const expected = cold("--b", { b: outcome }); + expect(effects.fetchCount$).toBeObservable(expected); + }); + + it("should result in a fetchCountFailedAction", () => { + const action = fromActions.fetchCountAction({}); + const outcome = fromActions.fetchCountFailedAction(); + + actions = hot("-a", { a: action }); + const response = cold("-#", {}); + origDatablocksApi.origDatablocksControllerFullfacetFilesV3.and.returnValue( + response, + ); + + const expected = cold("--b", { b: outcome }); + expect(effects.fetchCount$).toBeObservable(expected); + }); + }); + + describe("loading$", () => { + describe("ofType fetchAllOrigDatablocksAction", () => { + it("should dispatch a loadingAction", () => { + const action = fromActions.fetchAllOrigDatablocksAction({}); + const outcome = loadingAction(); + + actions = hot("-a", { a: action }); + + const expected = cold("-b", { b: outcome }); + expect(effects.loading$).toBeObservable(expected); + }); + }); + + describe("ofType fetchCountAction", () => { + it("should dispatch a loadingAction", () => { + const action = fromActions.fetchCountAction({}); + const outcome = loadingAction(); + + actions = hot("-a", { a: action }); + + const expected = cold("-b", { b: outcome }); + expect(effects.loading$).toBeObservable(expected); + }); + }); + }); + + describe("loadingComplete$", () => { + describe("ofType fetchAllOrigDatablocksCompleteAction", () => { + it("should dispatch a loadingCompleteAction", () => { + const action = fromActions.fetchAllOrigDatablocksCompleteAction({ + origDatablocks: origDatablockFiles, + }); + const outcome = loadingCompleteAction(); + + actions = hot("-a", { a: action }); + + const expected = cold("-b", { b: outcome }); + expect(effects.loadingComplete$).toBeObservable(expected); + }); + }); + + describe("ofType fetchAllOrigDatablocksFailedAction", () => { + it("should dispatch a loadingCompleteAction", () => { + const action = fromActions.fetchAllOrigDatablocksFailedAction(); + const outcome = loadingCompleteAction(); + + actions = hot("-a", { a: action }); + + const expected = cold("-b", { b: outcome }); + expect(effects.loadingComplete$).toBeObservable(expected); + }); + }); + + describe("ofType fetchCountCompleteAction", () => { + it("should dispatch a loadingCompleteAction", () => { + const count = 100; + const action = fromActions.fetchCountCompleteAction({ count }); + const outcome = loadingCompleteAction(); + + actions = hot("-a", { a: action }); + + const expected = cold("-b", { b: outcome }); + expect(effects.loadingComplete$).toBeObservable(expected); + }); + }); + + describe("ofType fetchCountFailedAction", () => { + it("should dispatch a loadingCompleteAction", () => { + const action = fromActions.fetchCountFailedAction(); + const outcome = loadingCompleteAction(); + + actions = hot("-a", { a: action }); + + const expected = cold("-b", { b: outcome }); + expect(effects.loadingComplete$).toBeObservable(expected); + }); + }); + }); +}); diff --git a/src/app/state-management/effects/files.effects.ts b/src/app/state-management/effects/files.effects.ts new file mode 100644 index 000000000..1697e7007 --- /dev/null +++ b/src/app/state-management/effects/files.effects.ts @@ -0,0 +1,102 @@ +import { Injectable } from "@angular/core"; +import { Actions, createEffect, ofType } from "@ngrx/effects"; +import { + OrigDatablock, + OrigdatablocksService, +} from "@scicatproject/scicat-sdk-ts-angular"; +import * as fromActions from "state-management/actions/files.actions"; +import { mergeMap, map, catchError, switchMap } from "rxjs/operators"; +import { of } from "rxjs"; +import { + loadingAction, + loadingCompleteAction, +} from "state-management/actions/user.actions"; + +@Injectable() +export class FilesEffects { + fetchAllOrigDatablocks$ = createEffect(() => { + return this.actions$.pipe( + ofType(fromActions.fetchAllOrigDatablocksAction), + switchMap(({ limit, search, skip, sortColumn, sortDirection }) => { + const limitsParam = { + skip: skip, + limit: limit, + order: undefined, + }; + + if (sortColumn && sortDirection) { + limitsParam.order = `${sortColumn}:${sortDirection}`; + } + + const queryParams = { text: search || undefined }; + + return this.origDataBlocksService + .origDatablocksControllerFullqueryFilesV3( + JSON.stringify(limitsParam), + JSON.stringify(queryParams), + ) + .pipe( + mergeMap((origDatablocks: OrigDatablock[]) => [ + fromActions.fetchAllOrigDatablocksCompleteAction({ + origDatablocks, + }), + fromActions.fetchCountAction({ + fields: queryParams, + }), + ]), + catchError(() => + of(fromActions.fetchAllOrigDatablocksFailedAction()), + ), + ); + }), + ); + }); + + fetchCount$ = createEffect(() => { + return this.actions$.pipe( + ofType(fromActions.fetchCountAction), + switchMap(({ fields }) => + this.origDataBlocksService + .origDatablocksControllerFullfacetFilesV3({ + fields: JSON.stringify(fields), + }) + .pipe( + map((res) => { + const { all } = res[0] as any; + const count = all && all.length > 0 ? all[0].totalSets : 0; + + return fromActions.fetchCountCompleteAction({ count }); + }), + catchError(() => of(fromActions.fetchCountFailedAction())), + ), + ), + ); + }); + + loading$ = createEffect(() => { + return this.actions$.pipe( + ofType( + fromActions.fetchAllOrigDatablocksAction, + fromActions.fetchCountAction, + ), + switchMap(() => of(loadingAction())), + ); + }); + + loadingComplete$ = createEffect(() => { + return this.actions$.pipe( + ofType( + fromActions.fetchAllOrigDatablocksCompleteAction, + fromActions.fetchAllOrigDatablocksFailedAction, + fromActions.fetchCountCompleteAction, + fromActions.fetchCountFailedAction, + ), + switchMap(() => of(loadingCompleteAction())), + ); + }); + + constructor( + private actions$: Actions, + private origDataBlocksService: OrigdatablocksService, + ) {} +} diff --git a/src/app/state-management/effects/jobs.effects.spec.ts b/src/app/state-management/effects/jobs.effects.spec.ts index 0f227123c..e9c4729c7 100644 --- a/src/app/state-management/effects/jobs.effects.spec.ts +++ b/src/app/state-management/effects/jobs.effects.spec.ts @@ -59,9 +59,9 @@ describe("JobEffects", () => { { provide: JobsService, useValue: jasmine.createSpyObj("jobApi", [ - "jobsControllerFindAllV3V3", - "jobsControllerFindOneV3V3", - "jobsControllerCreateV3V3", + "jobsControllerFindAllV3", + "jobsControllerFindOneV3", + "jobsControllerCreateV3", ]), }, ], @@ -84,7 +84,7 @@ describe("JobEffects", () => { actions = hot("-a", { a: action }); const response = cold("-a|", { a: jobs }); - jobApi.jobsControllerFindAllV3V3.and.returnValue(response); + jobApi.jobsControllerFindAllV3.and.returnValue(response); const expected = cold("--(bc)", { b: outcome1, c: outcome2 }); expect(effects.fetchJobs$).toBeObservable(expected); @@ -96,7 +96,7 @@ describe("JobEffects", () => { actions = hot("-a", { a: action }); const response = cold("-#", {}); - jobApi.jobsControllerFindAllV3V3.and.returnValue(response); + jobApi.jobsControllerFindAllV3.and.returnValue(response); const expected = cold("--b", { b: outcome }); expect(effects.fetchJobs$).toBeObservable(expected); @@ -115,7 +115,7 @@ describe("JobEffects", () => { actions = hot("-a", { a: action }); const response = cold("-a|", { a: jobs }); - jobApi.jobsControllerFindAllV3V3.and.returnValue(response); + jobApi.jobsControllerFindAllV3.and.returnValue(response); const expected = cold("--(bc)", { b: outcome1, c: outcome2 }); expect(effects.fetchJobs$).toBeObservable(expected); @@ -127,7 +127,7 @@ describe("JobEffects", () => { actions = hot("-a", { a: action }); const response = cold("-#", {}); - jobApi.jobsControllerFindAllV3V3.and.returnValue(response); + jobApi.jobsControllerFindAllV3.and.returnValue(response); const expected = cold("--b", { b: outcome }); expect(effects.fetchJobs$).toBeObservable(expected); @@ -146,7 +146,7 @@ describe("JobEffects", () => { actions = hot("-a", { a: action }); const response = cold("-a|", { a: jobs }); - jobApi.jobsControllerFindAllV3V3.and.returnValue(response); + jobApi.jobsControllerFindAllV3.and.returnValue(response); const expected = cold("--(bc)", { b: outcome1, c: outcome2 }); expect(effects.fetchJobs$).toBeObservable(expected); @@ -158,7 +158,7 @@ describe("JobEffects", () => { actions = hot("-a", { a: action }); const response = cold("-#", {}); - jobApi.jobsControllerFindAllV3V3.and.returnValue(response); + jobApi.jobsControllerFindAllV3.and.returnValue(response); const expected = cold("--b", { b: outcome }); expect(effects.fetchJobs$).toBeObservable(expected); @@ -176,7 +176,7 @@ describe("JobEffects", () => { actions = hot("-a", { a: action }); const response = cold("-a|", { a: jobs }); - jobApi.jobsControllerFindAllV3V3.and.returnValue(response); + jobApi.jobsControllerFindAllV3.and.returnValue(response); const expected = cold("--(bc)", { b: outcome1, c: outcome2 }); expect(effects.fetchJobs$).toBeObservable(expected); @@ -188,7 +188,7 @@ describe("JobEffects", () => { actions = hot("-a", { a: action }); const response = cold("-#", {}); - jobApi.jobsControllerFindAllV3V3.and.returnValue(response); + jobApi.jobsControllerFindAllV3.and.returnValue(response); const expected = cold("--b", { b: outcome }); expect(effects.fetchJobs$).toBeObservable(expected); @@ -220,7 +220,7 @@ describe("JobEffects", () => { actions = hot("-a", { a: action }); const response = cold("-a|", { a: outputJob }); - jobApi.jobsControllerFindOneV3V3.and.returnValue(response); + jobApi.jobsControllerFindOneV3.and.returnValue(response); const expected = cold("--b", { b: outcome }); expect(effects.fetchJob$).toBeObservable(expected); @@ -232,7 +232,7 @@ describe("JobEffects", () => { actions = hot("-a", { a: action }); const response = cold("-#", {}); - jobApi.jobsControllerFindOneV3V3.and.returnValue(response); + jobApi.jobsControllerFindOneV3.and.returnValue(response); const expected = cold("--b", { b: outcome }); expect(effects.fetchJob$).toBeObservable(expected); @@ -246,7 +246,7 @@ describe("JobEffects", () => { actions = hot("-a", { a: action }); const response = cold("-a|", { a: outputJob }); - jobApi.jobsControllerCreateV3V3.and.returnValue(response); + jobApi.jobsControllerCreateV3.and.returnValue(response); const expected = cold("--b", { b: outcome }); expect(effects.submitJob$).toBeObservable(expected); diff --git a/src/app/state-management/effects/jobs.effects.ts b/src/app/state-management/effects/jobs.effects.ts index 4bd123c9a..f69093ff4 100644 --- a/src/app/state-management/effects/jobs.effects.ts +++ b/src/app/state-management/effects/jobs.effects.ts @@ -30,7 +30,7 @@ export class JobEffects { concatLatestFrom(() => this.queryParams$), map(([action, params]) => params), switchMap((params) => - this.jobsService.jobsControllerFindAllV3V3(JSON.stringify(params)).pipe( + this.jobsService.jobsControllerFindAllV3(JSON.stringify(params)).pipe( switchMap((jobs) => [ fromActions.fetchJobsCompleteAction({ jobs }), fromActions.fetchCountAction(), @@ -54,7 +54,7 @@ export class JobEffects { return this.actions$.pipe( ofType(fromActions.fetchJobAction), switchMap(({ jobId }) => - this.jobsService.jobsControllerFindOneV3V3(jobId).pipe( + this.jobsService.jobsControllerFindOneV3(jobId).pipe( map((job) => fromActions.fetchJobCompleteAction({ job })), catchError(() => of(fromActions.fetchJobFailedAction())), ), @@ -66,7 +66,7 @@ export class JobEffects { return this.actions$.pipe( ofType(fromActions.submitJobAction), switchMap(({ job }) => - this.jobsService.jobsControllerCreateV3V3(job).pipe( + this.jobsService.jobsControllerCreateV3(job).pipe( map((res) => fromActions.submitJobCompleteAction({ job: res })), catchError((err) => of(fromActions.submitJobFailedAction({ err }))), ), diff --git a/src/app/state-management/effects/user.effects.ts b/src/app/state-management/effects/user.effects.ts index ab4d72789..cb0bf2e6c 100644 --- a/src/app/state-management/effects/user.effects.ts +++ b/src/app/state-management/effects/user.effects.ts @@ -95,6 +95,7 @@ export class UserEffects { }); this.authService.setToken(token); this.apiConfigService.accessToken = token.id; + this.apiConfigService.credentials.bearer = token.id; return this.usersService .usersControllerFindByIdV3(oidcLoginResponse.userId) .pipe( @@ -125,6 +126,7 @@ export class UserEffects { }); this.authService.setToken(token); this.apiConfigService.accessToken = token.id; + this.apiConfigService.credentials.bearer = token.id; return this.usersService .usersControllerFindByIdV3(adLoginResponse.userId) .pipe( @@ -155,6 +157,8 @@ export class UserEffects { .pipe( switchMap((loginResponse) => { this.apiConfigService.accessToken = loginResponse.access_token; + this.apiConfigService.credentials.bearer = + loginResponse.access_token; this.authService.setToken({ ...loginResponse, created: new Date(loginResponse.created), @@ -244,6 +248,8 @@ export class UserEffects { return this.actions$.pipe( ofType(fromActions.logoutCompleteAction), tap(({ logoutURL }) => { + this.apiConfigService.accessToken = null; + this.apiConfigService.credentials.bearer = null; if (logoutURL) { window.location.href = logoutURL; @@ -515,15 +521,18 @@ export class UserEffects { config.defaultDatasetsListSettings.columns || config.localColumns || initialUserState.columns; + const isAuthenticated = this.authService.isAuthenticated(); return [ fromActions.updateConditionsConfigs({ conditionConfigs: defaultConditions, }), + fromActions.updateHasFetchedSettings({ + hasFetchedSettings: !isAuthenticated, + }), fromActions.updateFilterConfigs({ filterConfigs: defaultFilters }), - fromActions.setDatasetTableColumnsAction({ - columns: columns, + columns, }), ]; }), diff --git a/src/app/state-management/models/index.ts b/src/app/state-management/models/index.ts index 1c1425300..2376baf4c 100644 --- a/src/app/state-management/models/index.ts +++ b/src/app/state-management/models/index.ts @@ -12,9 +12,12 @@ export interface Settings { export interface TableColumn { name: string; + header?: string; + path?: string; order: number; type: "standard" | "custom"; enabled: boolean; + width?: number; } export interface LabelMaps { @@ -44,6 +47,7 @@ export enum InternalLinkType { DATASETS = "inputDatasets", SAMPLES = "sampleIds", INSTRUMENTS = "instrumentIds", + INSTRUMENTS_NAME = "instrumentName", PROPOSALS = "proposalIds", } diff --git a/src/app/state-management/reducers/files.reducer.spec.ts b/src/app/state-management/reducers/files.reducer.spec.ts new file mode 100644 index 000000000..915e7764d --- /dev/null +++ b/src/app/state-management/reducers/files.reducer.spec.ts @@ -0,0 +1,48 @@ +import * as fromActions from "state-management/actions/files.actions"; +import { initialFilesState } from "state-management/state/files.store"; +import { mockOrigDatablock as origDatablock } from "shared/MockStubs"; +import { filesReducer } from "./files.reducer"; + +describe("FilesReducer", () => { + describe("on fetchAllOrigDatablocksCompleteAction", () => { + it("should set origDatablocks property", () => { + const origDatablocks = [origDatablock]; + const action = fromActions.fetchAllOrigDatablocksCompleteAction({ + origDatablocks, + }); + const state = filesReducer(initialFilesState, action); + + expect(state.origDatablocks).toEqual(origDatablocks); + }); + }); + + describe("on fetchCountCompleteAction", () => { + it("should set count property", () => { + const count = 100; + const action = fromActions.fetchCountCompleteAction({ count }); + const state = filesReducer(initialFilesState, action); + + expect(state.totalCount).toEqual(count); + }); + }); + + describe("on fetchOrigDatablockCompleteAction", () => { + it("should set currentOrigDatablock property", () => { + const action = fromActions.fetchOrigDatablockCompleteAction({ + origDatablock, + }); + const state = filesReducer(initialFilesState, action); + + expect(state.currentOrigDatablock).toEqual(origDatablock); + }); + }); + + describe("on clearOrigDatablockStateAction", () => { + it("should set files state to initialFilesState", () => { + const action = fromActions.clearOrigDatablockStateAction(); + const state = filesReducer(initialFilesState, action); + + expect(state).toEqual(initialFilesState); + }); + }); +}); diff --git a/src/app/state-management/reducers/files.reducer.ts b/src/app/state-management/reducers/files.reducer.ts new file mode 100644 index 000000000..855520249 --- /dev/null +++ b/src/app/state-management/reducers/files.reducer.ts @@ -0,0 +1,47 @@ +import { createReducer, Action, on } from "@ngrx/store"; +import * as fromActions from "state-management/actions/files.actions"; +import { + FilesState, + initialFilesState, +} from "state-management/state/files.store"; + +const reducer = createReducer( + initialFilesState, + on( + fromActions.fetchAllOrigDatablocksCompleteAction, + (state, { origDatablocks }): FilesState => ({ + ...state, + origDatablocks, + }), + ), + + on( + fromActions.fetchCountCompleteAction, + (state, { count }): FilesState => ({ + ...state, + totalCount: count, + }), + ), + + on( + fromActions.fetchOrigDatablockCompleteAction, + (state, { origDatablock }): FilesState => ({ + ...state, + currentOrigDatablock: origDatablock, + }), + ), + + on( + fromActions.clearOrigDatablockStateAction, + (): FilesState => ({ + ...initialFilesState, + }), + ), +); + +export const filesReducer = (state: FilesState | undefined, action: Action) => { + if (action.type.indexOf("[Orig]") !== -1) { + console.log("Action came in! " + action.type); + } + return reducer(state, action); +}; diff --git a/src/app/state-management/reducers/user.reducer.spec.ts b/src/app/state-management/reducers/user.reducer.spec.ts index 11358aa48..0b2f3f359 100644 --- a/src/app/state-management/reducers/user.reducer.spec.ts +++ b/src/app/state-management/reducers/user.reducer.spec.ts @@ -15,7 +15,9 @@ describe("UserReducer", () => { const columns: TableColumn[] = [ { name: "testColumn", order: 0, type: "standard", enabled: true }, ]; - const action = fromActions.setDatasetTableColumnsAction({ columns }); + const action = fromActions.setDatasetTableColumnsAction({ + columns, + }); const state = userReducer(initialUserState, action); expect(state.columns.length).toEqual(1); diff --git a/src/app/state-management/reducers/user.reducer.ts b/src/app/state-management/reducers/user.reducer.ts index 51b2613cf..9dcdff80b 100644 --- a/src/app/state-management/reducers/user.reducer.ts +++ b/src/app/state-management/reducers/user.reducer.ts @@ -13,12 +13,21 @@ const reducer = createReducer( }), ), + on( + fromActions.updateHasFetchedSettings, + (state, { hasFetchedSettings }): UserState => ({ + ...state, + hasFetchedSettings, + }), + ), + on( fromActions.loginAction, (state): UserState => ({ ...state, isLoggingIn: true, isLoggedIn: false, + hasFetchedSettings: false, }), ), @@ -30,6 +39,7 @@ const reducer = createReducer( accountType, isLoggingIn: false, isLoggedIn: true, + hasFetchedSettings: false, }), ), on( @@ -74,12 +84,14 @@ const reducer = createReducer( settings, columns, tablesSettings: externalSettings?.tablesSettings, + hasFetchedSettings: true, }; } else { return { ...state, settings, tablesSettings: externalSettings?.tablesSettings, + hasFetchedSettings: true, }; } }, diff --git a/src/app/state-management/selectors/files.selectors.spec.ts b/src/app/state-management/selectors/files.selectors.spec.ts new file mode 100644 index 000000000..d391e6c96 --- /dev/null +++ b/src/app/state-management/selectors/files.selectors.spec.ts @@ -0,0 +1,62 @@ +import * as fromSelectors from "./files.selectors"; +import { selectTablesSettings } from "./user.selectors"; +import { GenericFilters } from "state-management/models"; +import { mockOrigDatablock as origDatablock } from "shared/MockStubs"; +import { FilesState } from "state-management/state/files.store"; +import { initialUserState } from "state-management/state/user.store"; + +const filesFilters: GenericFilters = { + sortField: "name desc", + skip: 0, + limit: 25, +}; + +const initialFilesState: FilesState = { + origDatablocks: [], + currentOrigDatablock: origDatablock, + totalCount: 0, + + filters: filesFilters, +}; + +describe("Files Selectors", () => { + describe("selectAllOrigDatablocks", () => { + it("should select origDatablocks", () => { + expect( + fromSelectors.selectAllOrigDatablocks.projector(initialFilesState), + ).toEqual([]); + }); + }); + + describe("selectCurrentOrigDatablock", () => { + it("should select current origDatablock", () => { + expect( + fromSelectors.selectCurrentOrigDatablock.projector(initialFilesState), + ).toEqual(origDatablock); + }); + }); + + describe("selectOrigDatablocksCount", () => { + it("should select the total origDatablocks count", () => { + expect( + fromSelectors.selectOrigDatablocksCount.projector(initialFilesState), + ).toEqual(0); + }); + }); + + describe("selectFilesWithCountAndTableSettings", () => { + it("should select the origDatablocks with count and table settings", () => { + expect( + fromSelectors.selectFilesWithCountAndTableSettings.projector( + fromSelectors.selectAllOrigDatablocks.projector(initialFilesState), + fromSelectors.selectOrigDatablocksCount.projector(initialFilesState), + selectTablesSettings.projector(initialUserState), + ), + ).toEqual({ + origDatablocks: [], + count: 0, + tablesSettings: {}, + }); + }); + }); +}); diff --git a/src/app/state-management/selectors/files.selectors.ts b/src/app/state-management/selectors/files.selectors.ts new file mode 100644 index 000000000..280563cac --- /dev/null +++ b/src/app/state-management/selectors/files.selectors.ts @@ -0,0 +1,31 @@ +import { createFeatureSelector, createSelector } from "@ngrx/store"; +import { FilesState } from "state-management/state/files.store"; +import { selectTablesSettings } from "./user.selectors"; + +const selectFilesState = createFeatureSelector("files"); + +export const selectAllOrigDatablocks = createSelector( + selectFilesState, + (state) => state.origDatablocks, +); + +export const selectCurrentOrigDatablock = createSelector( + selectFilesState, + (state) => state.currentOrigDatablock, +); + +export const selectOrigDatablocksCount = createSelector( + selectFilesState, + (state) => state.totalCount, +); + +export const selectFilesWithCountAndTableSettings = createSelector( + selectAllOrigDatablocks, + selectOrigDatablocksCount, + selectTablesSettings, + (origDatablocks, count, tablesSettings) => ({ + origDatablocks, + count, + tablesSettings, + }), +); diff --git a/src/app/state-management/selectors/user.selectors.spec.ts b/src/app/state-management/selectors/user.selectors.spec.ts index e9f528728..bfcf7dcdd 100644 --- a/src/app/state-management/selectors/user.selectors.spec.ts +++ b/src/app/state-management/selectors/user.selectors.spec.ts @@ -86,6 +86,7 @@ export const initialUserState: UserState = { conditions: [], tablesSettings: {}, + hasFetchedSettings: false, }; describe("User Selectors", () => { diff --git a/src/app/state-management/selectors/user.selectors.ts b/src/app/state-management/selectors/user.selectors.ts index 422ebb8d8..4ec3c4702 100644 --- a/src/app/state-management/selectors/user.selectors.ts +++ b/src/app/state-management/selectors/user.selectors.ts @@ -133,3 +133,16 @@ export const selectUserSettingsPageViewModel = createSelector( settings, }), ); + +export const selectHasFetchedSettings = createSelector( + selectUserState, + (state) => state.hasFetchedSettings, +); + +export const selectColumnsWithHasFetchedSettings = createSelector( + selectUserState, + (state) => ({ + columns: state.columns, + hasFetchedSettings: state.hasFetchedSettings, + }), +); diff --git a/src/app/state-management/state/files.store.ts b/src/app/state-management/state/files.store.ts new file mode 100644 index 000000000..896ed4a4e --- /dev/null +++ b/src/app/state-management/state/files.store.ts @@ -0,0 +1,23 @@ +import { GenericFilters } from "state-management/models"; + +export interface FilesState { + origDatablocks: object[]; + currentOrigDatablock: object | undefined; + + totalCount: number; + + filters: GenericFilters; +} + +export const initialFilesState: FilesState = { + origDatablocks: [], + currentOrigDatablock: undefined, + + totalCount: 0, + + filters: { + sortField: "createdAt desc", + skip: 0, + limit: 25, + }, +}; diff --git a/src/app/state-management/state/user.store.ts b/src/app/state-management/state/user.store.ts index d8a78b94b..492eed453 100644 --- a/src/app/state-management/state/user.store.ts +++ b/src/app/state-management/state/user.store.ts @@ -21,6 +21,8 @@ export interface UserState { isLoggingIn: boolean; isLoggedIn: boolean; + hasFetchedSettings: boolean; + isLoading: boolean; columns: TableColumn[]; @@ -59,6 +61,8 @@ export const initialUserState: UserState = { isLoading: false, + hasFetchedSettings: false, + columns: [], filters: [ diff --git a/src/app/users/auth-callback/auth-callback.component.ts b/src/app/users/auth-callback/auth-callback.component.ts index cde600422..a6a2496f8 100644 --- a/src/app/users/auth-callback/auth-callback.component.ts +++ b/src/app/users/auth-callback/auth-callback.component.ts @@ -39,11 +39,13 @@ export class AuthCallbackComponent implements OnInit { // External authentication will redirect to this component with a access-token and user-id query parameter const accessToken = params["access-token"]; const userId = params["user-id"]; - const parsedToken = this.parseJwt(params["access-token"]); - const ttl = parsedToken.exp - parsedToken.iat; - const created = new Date(parsedToken.iat * 1000); + const returnUrl: string = params["returnUrl"]; if (accessToken && userId) { + const parsedToken = this.parseJwt(accessToken); + const ttl = parsedToken.exp - parsedToken.iat; + const created = new Date(parsedToken.iat * 1000); + // If the user is authenticated, we will store the access token and user id in the store this.store.dispatch( loginOIDCAction({ @@ -65,7 +67,6 @@ export class AuthCallbackComponent implements OnInit { // After the user is authenticated, we will redirect to the home page // or the value of returnUrl query param - const returnUrl: string = params["returnUrl"]; this.router.navigateByUrl(returnUrl || "/"); } }); diff --git a/src/app/users/login/login.component.ts b/src/app/users/login/login.component.ts index be34fbcc1..d4167ac7f 100644 --- a/src/app/users/login/login.component.ts +++ b/src/app/users/login/login.component.ts @@ -9,6 +9,7 @@ import { loginAction, funcLoginAction, loginOIDCAction, + updateHasFetchedSettings, } from "state-management/actions/user.actions"; import { Subscription } from "rxjs"; import { filter } from "rxjs/operators"; @@ -16,7 +17,7 @@ import { selectLoginPageViewModel } from "state-management/selectors/user.select import { MatDialog } from "@angular/material/dialog"; import { PrivacyDialogComponent } from "users/privacy-dialog/privacy-dialog.component"; import { - AppConfig, + AppConfigInterface, AppConfigService, OAuth2Endpoint, } from "app-config.service"; @@ -45,7 +46,7 @@ export class LoginComponent implements OnInit, OnDestroy { private proceedSubscription = new Subscription(); vm$ = this.store.select(selectLoginPageViewModel); - appConfig: AppConfig = this.appConfigService.getConfig(); + appConfig: AppConfigInterface = this.appConfigService.getConfig(); facility: string | null = null; loginFormEnabled = false; loginFacilityEnabled = false; @@ -83,10 +84,10 @@ export class LoginComponent implements OnInit, OnDestroy { } redirectOIDC(authURL: string) { - const returnURL = this.returnUrl + const returnUrl = this.returnUrl ? encodeURIComponent(this.returnUrl) : "/datasets"; - this.document.location.href = `${this.appConfig.lbBaseURL}/${authURL}?returnURL=${returnURL}`; + this.document.location.href = `${this.appConfig.lbBaseURL}/${authURL}?returnUrl=${returnUrl}`; } openPrivacyDialog() { @@ -134,16 +135,22 @@ export class LoginComponent implements OnInit, OnDestroy { this.route.queryParams.subscribe((params) => { // OIDC logins eventually redirect to this componenet, adding information about user // which are parsed here. - if (params.returnUrl) { + if (params["returnUrl"]) { // dispatching to the loginOIDCAction passes information to eventually be added to Loopback AccessToken let accessToken = params["access-token"]; let userId = params["user-id"]; + // Required for backend v3 compatibility (access-token and user-id are encoded in returnUrl) if (!accessToken && !userId) { const urlqp = new URLSearchParams(params.returnUrl.split("?")[1]); accessToken = urlqp.get("access-token"); userId = urlqp.get("user-id"); + } else { + // A returnUrl coming from v4 should be respected as the destination redirect + // after login and user info fetching. + this.returnUrl = params["returnUrl"]; } + this.store.dispatch( loginOIDCAction({ oidcLoginResponse: { accessToken, userId } }), ); @@ -154,6 +161,13 @@ export class LoginComponent implements OnInit, OnDestroy { ); } }); + + // If user is not logged in, hasFetchedSettings is set to true to allow fettcing of the public datasets + this.store.dispatch( + updateHasFetchedSettings({ + hasFetchedSettings: true, + }), + ); } ngOnDestroy() { diff --git a/src/assets/config.json b/src/assets/config.json index bdfae34cc..e81343130 100644 --- a/src/assets/config.json +++ b/src/assets/config.json @@ -139,7 +139,7 @@ "name": "pid", "order": 1, "type": "standard", - "enabled": true + "enabled": false }, { "name": "datasetName", @@ -224,7 +224,7 @@ "labelSets": { "ess": { "dataset-default": {}, - "dataset-custom" : { + "dataset-custom": { "pid": "PID", "description": "Description", "principalInvestigator": "Principal Investigator", @@ -233,17 +233,17 @@ "scientificMetadata": "Scientific Metadata", "metadataJsonView": "Metadata JsonView" }, - "proposal" : { + "proposal": { "General Information": "Proposal Information", - "Abstract" : "Abstract", - "Proposal Id" : "Proposal Id", - "Proposal Type" : "Proposal Type", - "Parent Proposal" : "Parent Proposal", - "Start Time" : "Start Time", - "End Time" : "End Time", - "Creator Information" : "People", + "Abstract": "Abstract", + "Proposal Id": "Proposal Id", + "Proposal Type": "Proposal Type", + "Parent Proposal": "Parent Proposal", + "Start Time": "Start Time", + "End Time": "End Time", + "Creator Information": "People", "Main Proposer": "Proposal Submitted By", - "Principal Investigator" : "Principal Investigator", + "Principal Investigator": "Principal Investigator", "Metadata": "Additional Information" } } diff --git a/src/styles.scss b/src/styles.scss index 890086281..629ad33d3 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -16,8 +16,6 @@ @use "./app/datasets/dataset-lifecycle/dataset-lifecycle-theme" as dataset-lifecycle; @use "./app/datasets/dataset-table/dataset-table-theme" as dataset-table; -@use "./app/datasets/dataset-table-settings/dataset-table-settings-theme" as - dataset-table-settings; @use "./app/datasets/reduce/reduce-theme" as reduce; @use "./app/instruments/instrument-details/instrument-details-theme" as instrument-details; @@ -239,7 +237,6 @@ $theme: map.merge( @include dataset-details-dashboard.theme($theme); @include dataset-lifecycle.theme($theme); @include dataset-table.theme($theme); -@include dataset-table-settings.theme($theme); @include reduce.theme($theme); @include instrument-details.theme($theme); @include logbooks-detail.theme($theme);