diff --git a/CI/e2e/frontend.config.e2e.json b/CI/e2e/frontend.config.e2e.json index 7bdad6c8d..78d33a0c7 100644 --- a/CI/e2e/frontend.config.e2e.json +++ b/CI/e2e/frontend.config.e2e.json @@ -106,17 +106,6 @@ "authorization": ["#datasetAccess", "#datasetPublic"] } ], - "labelMaps": { - "filters": { - "LocationFilter": "Location", - "PidFilter": "Pid", - "GroupFilter": "Group", - "TypeFilter": "Type", - "KeywordFilter": "Keyword", - "DateRangeFilter": "Start Date - End Date", - "TextFilter": "Text" - } - }, "defaultDatasetsListSettings": { "columns": [ { @@ -199,13 +188,48 @@ } ], "filters": [ - { "LocationFilter": true }, - { "PidFilter": true }, - { "GroupFilter": true }, - { "TypeFilter": true }, - { "KeywordFilter": true }, - { "DateRangeFilter": true }, - { "TextFilter": true } + { + "key": "creationLocation", + "label": "Location", + "type": "multiSelect", + "description": "Filter by creation location on the dataset", + "enabled": true + }, + { + "key": "pid", + "label": "Pid", + "type": "text", + "description": "Filter by dataset pid", + "enabled": true + }, + { + "key": "ownerGroup", + "label": "Group", + "type": "multiSelect", + "description": "Filter by owner group of the dataset", + "enabled": true + }, + { + "key": "type", + "label": "Type", + "type": "multiSelect", + "description": "Filter by dataset type", + "enabled": true + }, + { + "key": "keywords", + "label": "Keyword", + "type": "multiSelect", + "description": "Filter by keywords in the dataset", + "enabled": true + }, + { + "key": "creationTime", + "label": "Creation Time", + "type": "dateRange", + "description": "Filter by creation time of the dataset", + "enabled": true + } ], "conditions": [] }, diff --git a/package-lock.json b/package-lock.json index 824b27ad8..57d8c1143 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3371,18 +3371,18 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", - "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", - "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.15" @@ -3486,9 +3486,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.33.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", - "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz", + "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3507,12 +3507,12 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", - "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", "dev": true, "dependencies": { - "@eslint/core": "^0.15.2", + "@eslint/core": "^0.15.1", "levn": "^0.4.1" }, "engines": { @@ -3690,14 +3690,14 @@ } }, "node_modules/@inquirer/editor": { - "version": "4.2.16", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.16.tgz", - "integrity": "sha512-iSzLjT4C6YKp2DU0fr8T7a97FnRRxMO6CushJnW5ktxLNM2iNeuyUuUA5255eOLPORoGYCrVnuDOEBdGkHGkpw==", + "version": "4.2.15", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.15.tgz", + "integrity": "sha512-wst31XT8DnGOSS4nNJDIklGKnf+8shuauVrWzgKegWUe28zfCftcWZ2vktGdzJgcylWSS2SrDnYUb6alZcwnCQ==", "dev": true, "dependencies": { "@inquirer/core": "^10.1.15", - "@inquirer/external-editor": "^1.0.0", - "@inquirer/type": "^3.0.8" + "@inquirer/type": "^3.0.8", + "external-editor": "^3.1.0" }, "engines": { "node": ">=18" @@ -3733,22 +3733,6 @@ } } }, - "node_modules/@inquirer/external-editor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.0.tgz", - "integrity": "sha512-5v3YXc5ZMfL6OJqXPrX9csb4l7NlQA2doO1yynUjpUChT9hg4JcuBVP0RbsEJ/3SL/sxWEyFjT2W69ZhtoBWqg==", - "dev": true, - "dependencies": { - "chardet": "^2.1.0", - "iconv-lite": "^0.6.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - } - }, "node_modules/@inquirer/figures": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.13.tgz", @@ -5176,246 +5160,6 @@ "@parcel/watcher-win32-x64": "2.5.1" } }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", - "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", - "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", - "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", - "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", - "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", - "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", - "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", - "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", - "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", - "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/@parcel/watcher-win32-x64": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", @@ -6132,12 +5876,12 @@ "dev": true }, "node_modules/@types/node": { - "version": "24.2.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz", - "integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==", + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", + "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", "dev": true, "dependencies": { - "undici-types": "~7.10.0" + "undici-types": "~7.8.0" } }, "node_modules/@types/node-forge": { @@ -6149,6 +5893,12 @@ "@types/node": "*" } }, + "node_modules/@types/node/node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "dev": true + }, "node_modules/@types/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", @@ -6314,27 +6064,6 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.1.tgz", - "integrity": "sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==", - "dev": true, - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.39.1", - "@typescript-eslint/types": "^8.39.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 <6.0.0" - } - }, "node_modules/@typescript-eslint/scope-manager": { "version": "8.39.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.1.tgz", @@ -6433,6 +6162,27 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.1.tgz", + "integrity": "sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==", + "dev": true, + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.39.1", + "@typescript-eslint/types": "^8.39.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 <6.0.0" + } + }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -7830,12 +7580,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chardet": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.0.tgz", - "integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==", - "dev": true - }, "node_modules/check-more-types": { "version": "2.24.0", "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", @@ -8578,9 +8322,9 @@ "dev": true }, "node_modules/cypress": { - "version": "14.5.4", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-14.5.4.tgz", - "integrity": "sha512-0Dhm4qc9VatOcI1GiFGVt8osgpPdqJLHzRwcAB5MSD/CAAts3oybvPUPawHyvJZUd8osADqZe/xzMsZ8sDTjXw==", + "version": "14.5.3", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-14.5.3.tgz", + "integrity": "sha512-syLwKjDeMg77FRRx68bytLdlqHXDT4yBVh0/PPkcgesChYDjUZbwxLqMXuryYKzAyJsPsQHUDW1YU74/IYEUIA==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -9568,19 +9312,19 @@ } }, "node_modules/eslint": { - "version": "9.33.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz", - "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz", + "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.1", - "@eslint/core": "^0.15.2", + "@eslint/config-helpers": "^0.3.0", + "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.33.0", - "@eslint/plugin-kit": "^0.3.5", + "@eslint/js": "9.32.0", + "@eslint/plugin-kit": "^0.3.4", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -9643,9 +9387,9 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", - "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.3.tgz", + "integrity": "sha512-NAdMYww51ehKfDyDhv59/eIItUVzU0Io9H2E8nHNGKEeeqlnci+1gCvrHib6EmZdf6GxF+LCV5K7UC65Ezvw7w==", "dev": true, "dependencies": { "prettier-linter-helpers": "^1.0.0", @@ -10072,6 +9816,50 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "dev": true }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/external-editor/node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "node_modules/external-editor/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/external-editor/node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -10540,20 +10328,6 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/fstream": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", @@ -14444,6 +14218,15 @@ "dev": true, "optional": true }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ospath": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz", @@ -17490,12 +17273,6 @@ "node": "*" } }, - "node_modules/undici-types": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", - "dev": true - }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", diff --git a/src/app/app-config.service.spec.ts b/src/app/app-config.service.spec.ts index 3ab11c600..c2a9237f6 100644 --- a/src/app/app-config.service.spec.ts +++ b/src/app/app-config.service.spec.ts @@ -123,17 +123,6 @@ const appConfig: AppConfigInterface = { authorization: ["#datasetAccess", "#datasetPublic"], }, ], - labelMaps: { - filters: { - LocationFilter: "Location", - PidFilter: "Pid", - GroupFilter: "Group", - TypeFilter: "Type", - KeywordFilter: "Keyword", - DateRangeFilter: "Start Date - End Date", - TextFilter: "Text", - }, - }, defaultDatasetsListSettings: { columns: [ { @@ -216,13 +205,48 @@ const appConfig: AppConfigInterface = { }, ], filters: [ - { LocationFilter: true }, - { PidFilter: true }, - { GroupFilter: true }, - { TypeFilter: true }, - { KeywordFilter: true }, - { DateRangeFilter: true }, - { TextFilter: true }, + { + key: "creationLocation", + label: "Location", + type: "multiSelect", + description: "Filter by creation location on the dataset", + enabled: true, + }, + { + key: "pid", + label: "Pid", + type: "text", + description: "Filter by dataset pid", + enabled: true, + }, + { + key: "ownerGroup", + label: "Group", + type: "multiSelect", + description: "Filter by owner group of the dataset", + enabled: true, + }, + { + key: "type", + label: "Type", + type: "multiSelect", + description: "Filter by dataset type", + enabled: true, + }, + { + key: "keywords", + label: "Keyword", + type: "multiSelect", + description: "Filter by keywords in the dataset", + enabled: true, + }, + { + key: "creationTime", + label: "Creation Time", + type: "dateRange", + description: "Filter by creation time of the dataset", + enabled: true, + }, ], conditions: [], }, diff --git a/src/app/app-config.service.ts b/src/app/app-config.service.ts index a31c5e63a..47868d2f7 100644 --- a/src/app/app-config.service.ts +++ b/src/app/app-config.service.ts @@ -4,7 +4,6 @@ import { timeout } from "rxjs/operators"; import { DatasetDetailComponentConfig, DatasetsListSettings, - LabelMaps, LabelsLocalization, TableColumn, } from "state-management/models"; @@ -131,7 +130,6 @@ export interface AppConfigInterface { pidSearchMethod?: string; metadataEditingUnitListDisabled?: boolean; defaultDatasetsListSettings: DatasetsListSettings; - labelMaps: LabelMaps; thumbnailFetchLimitPerPage: number; maxFileUploadSizeInMb?: string; datasetDetailComponent?: DatasetDetailComponentConfig; diff --git a/src/app/datasets/dashboard/dashboard.component.ts b/src/app/datasets/dashboard/dashboard.component.ts index 664c492ce..3975adb83 100644 --- a/src/app/datasets/dashboard/dashboard.component.ts +++ b/src/app/datasets/dashboard/dashboard.component.ts @@ -179,6 +179,7 @@ export class DashboardComponent implements OnInit, OnDestroy { this.store.dispatch(fetchFacetCountsAction()); this.router.navigate(["/datasets"], { queryParams: { args: JSON.stringify(pagination) }, + queryParamsHandling: "merge", }); if (!loggedIn) { this.store.dispatch( diff --git a/src/app/datasets/dataset-detail/dataset-detail/dataset-detail.component.spec.ts b/src/app/datasets/dataset-detail/dataset-detail/dataset-detail.component.spec.ts index 3e72e032f..bd0baf73c 100644 --- a/src/app/datasets/dataset-detail/dataset-detail/dataset-detail.component.spec.ts +++ b/src/app/datasets/dataset-detail/dataset-detail/dataset-detail.component.spec.ts @@ -24,7 +24,7 @@ import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { MockStore } from "@ngrx/store/testing"; import { Store, StoreModule } from "@ngrx/store"; import { - addKeywordFilterAction, + addDatasetFilterAction, clearFacetsAction, updatePropertyAction, } from "state-management/actions/datasets.actions"; @@ -132,7 +132,11 @@ describe("DatasetDetailComponent", () => { expect(dispatchSpy).toHaveBeenCalledTimes(2); expect(dispatchSpy).toHaveBeenCalledWith(clearFacetsAction()); expect(dispatchSpy).toHaveBeenCalledWith( - addKeywordFilterAction({ keyword }), + addDatasetFilterAction({ + filterType: "multiSelect", + key: "keywords", + value: keyword, + }), ); expect(router.navigateByUrl).toHaveBeenCalledWith("/datasets"); }); diff --git a/src/app/datasets/dataset-detail/dataset-detail/dataset-detail.component.ts b/src/app/datasets/dataset-detail/dataset-detail/dataset-detail.component.ts index 14900885d..48f0b6812 100644 --- a/src/app/datasets/dataset-detail/dataset-detail/dataset-detail.component.ts +++ b/src/app/datasets/dataset-detail/dataset-detail/dataset-detail.component.ts @@ -21,7 +21,7 @@ import { } from "state-management/selectors/user.selectors"; import { map } from "rxjs/operators"; import { - addKeywordFilterAction, + addDatasetFilterAction, clearFacetsAction, updatePropertyAction, } from "state-management/actions/datasets.actions"; @@ -161,7 +161,13 @@ export class DatasetDetailComponent implements OnInit, OnDestroy { onClickKeyword(keyword: string) { this.store.dispatch(clearFacetsAction()); - this.store.dispatch(addKeywordFilterAction({ keyword })); + this.store.dispatch( + addDatasetFilterAction({ + filterType: "multiSelect", + key: "keywords", + value: keyword, + }), + ); this.router.navigateByUrl("/datasets"); } diff --git a/src/app/datasets/datasets-filter/datasets-filter.component.html b/src/app/datasets/datasets-filter/datasets-filter.component.html index 697b41838..5c48de017 100644 --- a/src/app/datasets/datasets-filter/datasets-filter.component.html +++ b/src/app/datasets/datasets-filter/datasets-filter.component.html @@ -14,17 +14,22 @@ - - - - - - + +
diff --git a/src/app/datasets/datasets-filter/datasets-filter.component.scss b/src/app/datasets/datasets-filter/datasets-filter.component.scss index e8dc3dc92..ec31bffab 100644 --- a/src/app/datasets/datasets-filter/datasets-filter.component.scss +++ b/src/app/datasets/datasets-filter/datasets-filter.component.scss @@ -58,11 +58,11 @@ mat-card { .datasets-filters-search-button { width: 49%; + margin-left: 2%; } .datasets-filters-clear-all-button { width: 49%; - margin-left: 1%; } } diff --git a/src/app/datasets/datasets-filter/datasets-filter.component.spec.ts b/src/app/datasets/datasets-filter/datasets-filter.component.spec.ts index 5a7422409..8eb42e705 100644 --- a/src/app/datasets/datasets-filter/datasets-filter.component.spec.ts +++ b/src/app/datasets/datasets-filter/datasets-filter.component.spec.ts @@ -7,7 +7,7 @@ import { } from "@angular/core/testing"; import { Store, StoreModule } from "@ngrx/store"; import { DatasetsFilterComponent } from "datasets/datasets-filter/datasets-filter.component"; -import { MockHttp, MockStore } from "shared/MockStubs"; +import { MockActivatedRoute, MockHttp, MockStore } from "shared/MockStubs"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; @@ -17,7 +17,6 @@ import { fetchFacetCountsAction, } from "state-management/actions/datasets.actions"; import { of } from "rxjs"; -import { deselectAllCustomColumnsAction } from "state-management/actions/user.actions"; import { SharedScicatFrontendModule } from "shared/shared.module"; import { MatAutocompleteModule } from "@angular/material/autocomplete"; import { MatDialogModule, MatDialog } from "@angular/material/dialog"; @@ -34,36 +33,59 @@ import { MatButtonModule } from "@angular/material/button"; import { MatIconModule } from "@angular/material/icon"; import { AppConfigService } from "app-config.service"; import { DatasetsFilterSettingsComponent } from "./settings/datasets-filter-settings.component"; -import { FilterConfig } from "../../shared/modules/filters/filters.module"; import { selectConditions, selectFilters, } from "../../state-management/selectors/user.selectors"; import { HttpClient } from "@angular/common/http"; +import { FilterConfig } from "state-management/state/user.store"; +import { ActivatedRoute } from "@angular/router"; const filterConfigs: FilterConfig[] = [ - { LocationFilter: true }, - { PidFilter: true }, - { GroupFilter: true }, - { TypeFilter: true }, - { KeywordFilter: true }, - { DateRangeFilter: true }, - { TextFilter: true }, - { PidFilterContains: false }, - { PidFilterStartsWith: false }, + { + key: "creationLocation", + label: "Location", + type: "multiSelect", + description: "Filter by creation location on the dataset", + enabled: true, + }, + { + key: "pid", + label: "Pid", + type: "text", + description: "Filter by dataset pid", + enabled: true, + }, + { + key: "ownerGroup", + label: "Group", + type: "multiSelect", + description: "Filter by owner group of the dataset", + enabled: true, + }, + { + key: "type", + label: "Type", + type: "multiSelect", + description: "Filter by dataset type", + enabled: true, + }, + { + key: "keywords", + label: "Keyword", + type: "multiSelect", + description: "Filter by keywords in the dataset", + enabled: true, + }, + { + key: "creationTime", + label: "Creation Time", + type: "dateRange", + description: "Filter by creation time of the dataset", + enabled: true, + }, ]; -const labelMaps = { - LocationFilter: "Location Filter", - PidFilter: "Pid Filter", - GroupFilter: "Group Filter", - TypeFilter: "Type Filter", - KeywordFilter: "Keyword Filter", - DateRangeFilter: "Start Date - End Date", - TextFilter: "Text Filter", - PidFilterContains: "PID filter (Contains)- Not implemented", - PidFilterStartsWith: "PID filter (Starts With)- Not implemented", -}; export class MockStoreWithFilters extends MockStore { public select(selector) { if (selector === selectFilters) { @@ -123,6 +145,7 @@ describe("DatasetsFilterComponent", () => { AppConfigService, { provide: HttpClient, useClass: MockHttp }, { provide: Store, useClass: MockStoreWithFilters }, + { provide: ActivatedRoute, useClass: MockActivatedRoute }, ], }); TestBed.overrideComponent(DatasetsFilterComponent, { @@ -161,25 +184,25 @@ describe("DatasetsFilterComponent", () => { it("should contain a date range field", () => { const compiled = fixture.debugElement.nativeElement; - const beamline = compiled.querySelector(".date-input"); + const beamline = compiled.querySelector("#creationTime"); expect(beamline).toBeTruthy(); }); it("should contain a beamline input", () => { const compiled = fixture.debugElement.nativeElement; - const beamline = compiled.querySelector(".location-input"); + const beamline = compiled.querySelector("#creationLocation"); expect(beamline).toBeTruthy(); }); it("should contain a groups input", () => { const compiled = fixture.debugElement.nativeElement; - const group = compiled.querySelector(".group-input"); + const group = compiled.querySelector("#ownerGroup"); expect(group).toBeTruthy(); }); it("should contain a type input", () => { const compiled = fixture.debugElement.nativeElement; - const type = compiled.querySelector(".type-input"); + const type = compiled.querySelector("#type"); expect(type).toBeTruthy(); }); @@ -196,16 +219,13 @@ describe("DatasetsFilterComponent", () => { }); describe("#reset()", () => { - it("should dispatch a ClearFacetsAction and a deselectAllCustomColumnsAction", () => { + it("should dispatch a ClearFacetsAction", () => { dispatchSpy = spyOn(store, "dispatch"); component.reset(); expect(dispatchSpy).toHaveBeenCalledTimes(5); expect(dispatchSpy).toHaveBeenCalledWith(clearFacetsAction()); - expect(dispatchSpy).toHaveBeenCalledWith( - deselectAllCustomColumnsAction(), - ); expect(dispatchSpy).toHaveBeenCalledWith(fetchDatasetsAction()); expect(dispatchSpy).toHaveBeenCalledWith(fetchFacetCountsAction()); }); @@ -224,8 +244,6 @@ describe("DatasetsFilterComponent", () => { { data: { filterConfigs: filterConfigs, - conditionConfigs: [], - labelMaps: labelMaps, }, restoreFocus: false, }, diff --git a/src/app/datasets/datasets-filter/datasets-filter.component.ts b/src/app/datasets/datasets-filter/datasets-filter.component.ts index af9a5ab3c..9b06e39a2 100644 --- a/src/app/datasets/datasets-filter/datasets-filter.component.ts +++ b/src/app/datasets/datasets-filter/datasets-filter.component.ts @@ -1,25 +1,23 @@ -import { - Component, - OnDestroy, - OnInit, - Type, - ViewContainerRef, -} from "@angular/core"; +import { Component, OnDestroy, OnInit } from "@angular/core"; import { MatDialog } from "@angular/material/dialog"; import { Store } from "@ngrx/store"; import { cloneDeep, isEqual } from "lodash-es"; import { + selectFacetCountByKey, + selectFilterByKey, selectHasAppliedFilters, selectScientificConditions, } from "state-management/selectors/datasets.selectors"; import { + addDatasetFilterAction, clearFacetsAction, fetchDatasetsAction, fetchFacetCountsAction, + removeDatasetFilterAction, + setFiltersAction, } from "state-management/actions/datasets.actions"; import { - deselectAllCustomColumnsAction, updateConditionsConfigs, updateUserSettingsAction, } from "state-management/actions/user.actions"; @@ -30,18 +28,6 @@ import { selectFilters, } from "state-management/selectors/user.selectors"; import { AsyncPipe } from "@angular/common"; -import { ConditionFilterComponent } from "../../shared/modules/filters/condition-filter.component"; -import { PidFilterComponent } from "../../shared/modules/filters/pid-filter.component"; -import { PidFilterContainsComponent } from "../../shared/modules/filters/pid-filter-contains.component"; -import { PidFilterStartsWithComponent } from "../../shared/modules/filters/pid-filter-startsWith.component"; -import { LocationFilterComponent } from "../../shared/modules/filters/location-filter.component"; -import { GroupFilterComponent } from "../../shared/modules/filters/group-filter.component"; -import { TypeFilterComponent } from "../../shared/modules/filters/type-filter.component"; -import { KeywordFilterComponent } from "../../shared/modules/filters/keyword-filter.component"; -import { DateRangeFilterComponent } from "../../shared/modules/filters/date-range-filter.component"; -import { TextFilterComponent } from "../../shared/modules/filters/text-filter.component"; -import { Filters, FilterConfig } from "shared/modules/filters/filters.module"; -import { FilterComponentInterface } from "shared/modules/filters/interface/filter-component.interface"; import { Subscription } from "rxjs"; import { take } from "rxjs/operators"; import { SearchParametersDialogComponent } from "../../shared/modules/search-parameters-dialog/search-parameters-dialog.component"; @@ -49,7 +35,6 @@ import { selectMetadataKeys, selectDatasets, } from "state-management/selectors/datasets.selectors"; -import { ConditionConfig } from "shared/modules/filters/filters.module"; import { MatSnackBar } from "@angular/material/snack-bar"; import { addScientificConditionAction, @@ -61,19 +46,14 @@ import { } from "state-management/actions/user.actions"; import { UnitsService } from "shared/services/units.service"; import { ScientificCondition } from "state-management/models"; - -const COMPONENT_MAP: { [K in Filters]: Type } = { - PidFilter: PidFilterComponent, - PidFilterContains: PidFilterContainsComponent, - PidFilterStartsWith: PidFilterStartsWithComponent, - LocationFilter: LocationFilterComponent, - GroupFilter: GroupFilterComponent, - TypeFilter: TypeFilterComponent, - KeywordFilter: KeywordFilterComponent, - DateRangeFilter: DateRangeFilterComponent, - TextFilter: TextFilterComponent, - ConditionFilter: ConditionFilterComponent, -}; +import { + FilterConfig, + ConditionConfig, +} from "state-management/state/user.store"; +import { DateRange } from "state-management/state/proposals.store"; +import { ActivatedRoute, Router } from "@angular/router"; +import { MultiSelectFilterValue } from "shared/modules/filters/multiselect-filter.component"; +import { INumericRange } from "shared/modules/numeric-range/form/model/numeric-range-field.model"; @Component({ selector: "datasets-filter", @@ -83,7 +63,9 @@ const COMPONENT_MAP: { [K in Filters]: Type } = { }) export class DatasetsFilterComponent implements OnInit, OnDestroy { private subscriptions: Subscription[] = []; - protected readonly ConditionFilterComponent = ConditionFilterComponent; + activeFilters: Record = + {}; + filtersList: FilterConfig[]; filterConfigs$ = this.store.select(selectFilters); @@ -97,8 +79,6 @@ export class DatasetsFilterComponent implements OnInit, OnDestroy { hasAppliedFilters$ = this.store.select(selectHasAppliedFilters); - labelMaps: { [key: string]: string } = {}; - metadataKeys$ = this.store.select(selectMetadataKeys); datasets$ = this.store.select(selectDatasets); @@ -112,28 +92,47 @@ export class DatasetsFilterComponent implements OnInit, OnDestroy { public dialog: MatDialog, private store: Store, private asyncPipe: AsyncPipe, - private viewContainerRef: ViewContainerRef, private snackBar: MatSnackBar, private unitsService: UnitsService, + private route: ActivatedRoute, + private router: Router, ) {} ngOnInit() { - this.getAllComponentLabels(); this.applyEnabledConditions(); - } - getAllComponentLabels() { - Object.entries(COMPONENT_MAP).forEach(([key, component]) => { - const componentRef = this.viewContainerRef.createComponent(component); + this.filterConfigs$.subscribe((filterConfigs) => { + if (filterConfigs) { + this.filtersList = filterConfigs; - const instance = componentRef.instance as FilterComponentInterface; + const { queryParams } = this.route.snapshot; - if (instance.label) { - this.labelMaps[key] = instance.label; - } + const searchQuery = JSON.parse(queryParams.searchQuery || "{}"); - componentRef.destroy(); + this.filtersList.forEach((filter) => { + if (!filter.enabled && searchQuery[filter.key]) { + delete searchQuery[filter.key]; + delete this.activeFilters[filter.key]; + } + }); + + this.router.navigate([], { + queryParams: { + searchQuery: JSON.stringify(searchQuery), + }, + queryParamsHandling: "merge", + }); + } }); + + const { queryParams } = this.route.snapshot; + + const searchQuery = JSON.parse(queryParams.searchQuery || "{}"); + this.activeFilters = { ...searchQuery }; + + this.store.dispatch( + setFiltersAction({ datasetFilters: this.activeFilters }), + ); } applyEnabledConditions() { @@ -154,12 +153,18 @@ export class DatasetsFilterComponent implements OnInit, OnDestroy { this.clearSearchBar = true; this.store.dispatch(clearFacetsAction()); - this.store.dispatch(deselectAllCustomColumnsAction()); this.store.dispatch( updateConditionsConfigs({ conditionConfigs: [], }), ); + this.store.dispatch( + updateUserSettingsAction({ + property: { conditions: [] }, + }), + ); + + this.activeFilters = {}; this.applyFilters(); // we need to treat JS event loop here, otherwise this.clearSearchBar is false for the components @@ -173,21 +178,13 @@ export class DatasetsFilterComponent implements OnInit, OnDestroy { // to compare with the updated ones // and dispatch the updated ones if they changed // This is to prevent unnecessary API calls - const initialFilterConfigs = await this.filterConfigs$ - .pipe(take(1)) - .toPromise(); - const initialConditionConfigs = await this.conditionConfigs$ - .pipe(take(1)) - .toPromise(); - - const initialFilterConfigsCopy = cloneDeep(initialFilterConfigs); - const initialConditionConfigsCopy = cloneDeep(initialConditionConfigs); + const initialFilterConfigsCopy = cloneDeep( + this.asyncPipe.transform(this.filterConfigs$), + ); const dialogRef = this.dialog.open(DatasetsFilterSettingsComponent, { data: { - filterConfigs: this.asyncPipe.transform(this.filterConfigs$), - conditionConfigs: this.asyncPipe.transform(this.conditionConfigs$), - labelMaps: this.labelMaps, + filterConfigs: this.filtersList, }, restoreFocus: false, }); @@ -198,36 +195,149 @@ export class DatasetsFilterComponent implements OnInit, OnDestroy { initialFilterConfigsCopy, result.filterConfigs, ); - const conditionsChanged = !isEqual( - initialConditionConfigsCopy, - result.conditionConfigs, - ); - if (filtersChanged || conditionsChanged) { - const updatedProperty = {}; - - if (filtersChanged) { - updatedProperty["filters"] = result.filterConfigs; - } - - if (conditionsChanged) { - updatedProperty["conditions"] = result.conditionConfigs; - } + if (filtersChanged) { this.store.dispatch( updateUserSettingsAction({ - property: updatedProperty, + property: { filters: result.filterConfigs }, }), ); + + this.store.dispatch(fetchFacetCountsAction()); } } }); } applyFilters() { + const { queryParams } = this.route.snapshot; + const searchQuery = JSON.parse(queryParams.searchQuery || "{}"); + + this.router.navigate([], { + queryParams: { + searchQuery: JSON.stringify({ + ...this.activeFilters, + text: searchQuery.text, + }), + }, + queryParamsHandling: "merge", + }); + this.store.dispatch(fetchDatasetsAction()); this.store.dispatch(fetchFacetCountsAction()); } + setDateFilter(filterKey: string, value: DateRange) { + if (value.begin || value.end) { + this.activeFilters[filterKey] = { + begin: value.begin, + end: value.end, + }; + + this.store.dispatch( + addDatasetFilterAction({ + key: filterKey, + value: this.activeFilters[filterKey], + filterType: "dateRange", + }), + ); + } else { + delete this.activeFilters[filterKey]; + + this.store.dispatch( + removeDatasetFilterAction({ + key: filterKey, + filterType: "dateRange", + }), + ); + } + } + + setFilter(filterKey: string, value: string) { + if (value) { + this.activeFilters[filterKey] = value; + + this.store.dispatch( + addDatasetFilterAction({ + key: filterKey, + value: this.activeFilters[filterKey], + filterType: "text", + }), + ); + } else { + delete this.activeFilters[filterKey]; + + this.store.dispatch( + removeDatasetFilterAction({ + key: filterKey, + filterType: "text", + }), + ); + } + } + + addMultiSelectFilterToActiveFilters(key: string, value: string) { + if (this.activeFilters[key] && Array.isArray(this.activeFilters[key])) { + this.activeFilters[key] = [...this.activeFilters[key], value]; + } else { + this.activeFilters[key] = [value]; + } + } + + removeMultiSelectFilterFromActiveFilters(key: string, value: string) { + if (this.activeFilters[key] && Array.isArray(this.activeFilters[key])) { + if (this.activeFilters[key].length > 1) { + this.activeFilters[key] = this.activeFilters[key].filter( + (item: string) => item !== value, + ); + } else { + delete this.activeFilters[key]; + } + } + } + + selectionChange({ event, key, value }: MultiSelectFilterValue) { + if (event === "add") { + this.addMultiSelectFilterToActiveFilters(key, value); + this.store.dispatch( + addDatasetFilterAction({ key, value, filterType: "multiSelect" }), + ); + } else { + this.removeMultiSelectFilterFromActiveFilters(key, value); + this.store.dispatch( + removeDatasetFilterAction({ key, value, filterType: "multiSelect" }), + ); + } + } + + numericRangeChange(filterKey: string, { min, max }: INumericRange) { + if (min !== null && max !== null) { + this.activeFilters[filterKey] = { min, max }; + + this.store.dispatch( + addDatasetFilterAction({ + key: filterKey, + value: this.activeFilters[filterKey], + filterType: "number", + }), + ); + } else { + delete this.activeFilters[filterKey]; + + this.store.dispatch( + removeDatasetFilterAction({ key: filterKey, filterType: "number" }), + ); + } + } + + getFilterFacetCounts$(key: string) { + return this.store.select(selectFacetCountByKey(key)); + } + + getFilterByKey$(key: string) { + return this.store.select(selectFilterByKey(key)); + } + trackByCondition(index: number, conditionConfig: ConditionConfig): string { const condition = conditionConfig.condition; return `${condition.lhs}-${index}`; @@ -555,16 +665,6 @@ export class DatasetsFilterComponent implements OnInit, OnDestroy { ]; } - renderComponent(filterObj: FilterConfig): any { - const key = Object.keys(filterObj)[0]; - const isEnabled = filterObj[key]; - - if (!isEnabled || !COMPONENT_MAP[key]) { - return null; - } - - return COMPONENT_MAP[key]; - } ngOnDestroy() { this.subscriptions.forEach((subscription) => subscription.unsubscribe()); } diff --git a/src/app/datasets/datasets-filter/settings/datasets-filter-settings.component.html b/src/app/datasets/datasets-filter/settings/datasets-filter-settings.component.html index 5b38e0a7e..6a5f603d5 100644 --- a/src/app/datasets/datasets-filter/settings/datasets-filter-settings.component.html +++ b/src/app/datasets/datasets-filter/settings/datasets-filter-settings.component.html @@ -5,13 +5,9 @@

Configure Filters

Configure Filters - {{ - resolveFilterLabel(data.labelMaps, filter) - }} + {{ filter.label || filter.key }} diff --git a/src/app/datasets/datasets-filter/settings/datasets-filter-settings.component.spec.ts b/src/app/datasets/datasets-filter/settings/datasets-filter-settings.component.spec.ts index f09904379..b17804ec5 100644 --- a/src/app/datasets/datasets-filter/settings/datasets-filter-settings.component.spec.ts +++ b/src/app/datasets/datasets-filter/settings/datasets-filter-settings.component.spec.ts @@ -44,6 +44,9 @@ export class MockMatDialog { const getConfig = () => ({ scienceSearchEnabled: false, + defaultDatasetsListSettings: { + filters: [], + }, }); describe("DatasetsFilterSettingsComponent", () => { @@ -92,7 +95,7 @@ describe("DatasetsFilterSettingsComponent", () => { { provide: MAT_DIALOG_DATA, useValue: { - conditionConfigs: [], + filterConfigs: [], }, }, ], diff --git a/src/app/datasets/datasets-filter/settings/datasets-filter-settings.component.ts b/src/app/datasets/datasets-filter/settings/datasets-filter-settings.component.ts index a33794e00..dd611092b 100644 --- a/src/app/datasets/datasets-filter/settings/datasets-filter-settings.component.ts +++ b/src/app/datasets/datasets-filter/settings/datasets-filter-settings.component.ts @@ -8,7 +8,7 @@ import { AppConfigService } from "app-config.service"; import { Store } from "@ngrx/store"; import { selectMetadataKeys } from "../../../state-management/selectors/datasets.selectors"; import { CdkDragDrop, moveItemInArray } from "@angular/cdk/drag-drop"; -import { FilterConfig } from "../../../shared/modules/filters/filters.module"; +import { FilterConfig } from "state-management/state/user.store"; @Component({ selector: "app-type-datasets-filter-settings", @@ -23,6 +23,11 @@ export class DatasetsFilterSettingsComponent { filterValidationStatus = {}; + defaultFilters = []; + userSavedFilters = []; + + mergedFilters = []; + constructor( public dialogRef: MatDialogRef, public dialog: MatDialog, @@ -31,41 +36,41 @@ export class DatasetsFilterSettingsComponent { @Inject(MAT_DIALOG_DATA) public data: any, ) {} - toggleVisibility(filter: FilterConfig): void { - const key = this.getFilterKey(filter); - filter[key] = !filter[key]; + ngOnInit() { + this.defaultFilters = this.appConfig.defaultDatasetsListSettings.filters; + this.userSavedFilters = this.data.filterConfigs || []; + + const newFilters = this.defaultFilters.reduce((filtered, item) => { + if ( + !this.userSavedFilters.some((userFilter) => userFilter.key === item.key) + ) { + filtered.push({ ...item, enabled: false }); + } + return filtered; + }, []); + + this.mergedFilters = [...this.userSavedFilters].concat(newFilters); } - getChecked(filter: FilterConfig): boolean { - const key = this.getFilterKey(filter); - return filter[key]; + toggleVisibility(filter: FilterConfig): void { + filter.enabled = !filter.enabled; } drop(event: CdkDragDrop): void { moveItemInArray( - this.data.filterConfigs, + this.mergedFilters, event.previousIndex, event.currentIndex, ); } onApply() { - this.dialogRef.close(this.data); + this.dialogRef.close({ + filterConfigs: this.mergedFilters, + }); } onCancel() { this.dialogRef.close(); } - - resolveFilterLabel( - labelMaps: Record, - filter: FilterConfig, - ): string { - const key = this.getFilterKey(filter); - return labelMaps[key] || "Unknown filter"; - } - - getFilterKey(filter: FilterConfig): string { - return Object.keys(filter)[0]; - } } diff --git a/src/app/datasets/datasets.module.ts b/src/app/datasets/datasets.module.ts index 29d2f3f08..26f31c5eb 100644 --- a/src/app/datasets/datasets.module.ts +++ b/src/app/datasets/datasets.module.ts @@ -78,7 +78,6 @@ import { DatafilesActionComponent } from "./datafiles-actions/datafiles-action.c import { MatMenuModule } from "@angular/material/menu"; import { DatasetsFilterSettingsComponent } from "./datasets-filter/settings/datasets-filter-settings.component"; import { CdkDrag, CdkDragHandle, CdkDropList } from "@angular/cdk/drag-drop"; -import { FiltersModule } from "shared/modules/filters/filters.module"; 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"; @@ -147,7 +146,7 @@ import { TitleCasePipe } from "shared/pipes/title-case.pipe"; CdkDropList, CdkDrag, CdkDragHandle, - FiltersModule, + // FiltersModule, MatExpansionModule, ], declarations: [ diff --git a/src/app/proposals/proposal-dashboard/proposal-dashboard.component.ts b/src/app/proposals/proposal-dashboard/proposal-dashboard.component.ts index 13810bc6d..8d6aea99e 100644 --- a/src/app/proposals/proposal-dashboard/proposal-dashboard.component.ts +++ b/src/app/proposals/proposal-dashboard/proposal-dashboard.component.ts @@ -7,15 +7,7 @@ import { fetchFacetCountsAction, fetchProposalsAction, } from "state-management/actions/proposals.actions"; - -export type FilterType = "text" | "dateRange"; - -export interface FilterLists { - key: string; - label: string; - description?: string; - type?: FilterType; -} +import { FilterConfig } from "state-management/state/user.store"; @Component({ selector: "app-proposal-dashboard", @@ -29,42 +21,48 @@ export class ProposalDashboardComponent implements OnInit, OnDestroy { params$ = this.route.queryParams; defaultPageSize = 10; - filterLists: FilterLists[] = [ + filterLists: FilterConfig[] = [ { key: "proposalId", label: "Proposal ID", type: "text", description: "Filter by Unique identifier for the proposal", + enabled: true, }, { key: "firstname", label: "First Name", type: "text", description: "Filter by First name of the proposal submitter", + enabled: true, }, { key: "email", label: "Email", type: "text", description: "Filter by Email of the proposal submitter", + enabled: true, }, { key: "pi_firstname", label: "PI First Name", type: "text", description: "Filter by First name of the Principal Investigator", + enabled: true, }, { key: "startTime", label: "Start Time", type: "dateRange", description: "Filter by Start time of the proposal", + enabled: true, }, { key: "endTime", label: "End Time", type: "dateRange", description: "Filter by End time of the proposal", + enabled: true, }, ]; diff --git a/src/app/proposals/proposal-filters/side-bar-filter/proposal-side-filter.component.html b/src/app/proposals/proposal-filters/side-bar-filter/proposal-side-filter.component.html index a7dec3d49..dc4e87dcd 100644 --- a/src/app/proposals/proposal-filters/side-bar-filter/proposal-side-filter.component.html +++ b/src/app/proposals/proposal-filters/side-bar-filter/proposal-side-filter.component.html @@ -13,6 +13,7 @@ (); @Output() dateChange = new EventEmitter>(); diff --git a/src/app/shared/modules/filters/condition-filter.component.html b/src/app/shared/modules/filters/condition-filter.component.html deleted file mode 100644 index b19da68e7..000000000 --- a/src/app/shared/modules/filters/condition-filter.component.html +++ /dev/null @@ -1,9 +0,0 @@ - - Condition - - diff --git a/src/app/shared/modules/filters/condition-filter.component.ts b/src/app/shared/modules/filters/condition-filter.component.ts deleted file mode 100644 index a11f7c806..000000000 --- a/src/app/shared/modules/filters/condition-filter.component.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Component, Input } from "@angular/core"; -import { Store } from "@ngrx/store"; -import { ScientificCondition } from "state-management/models"; - -@Component({ - selector: "app-condition-filter", - templateUrl: "condition-filter.component.html", - styleUrls: ["condition-filter.component.scss"], - standalone: false, -}) -export class ConditionFilterComponent { - @Input() condition: ScientificCondition; - - constructor(private store: Store) {} - - formatCondition() { - const condition = this.condition; - let relationSymbol = ""; - switch (condition.relation) { - case "EQUAL_TO_NUMERIC": - case "EQUAL_TO_STRING": - relationSymbol = "="; - break; - case "LESS_THAN": - relationSymbol = "<"; - break; - case "GREATER_THAN": - relationSymbol = ">"; - break; - default: - relationSymbol = ""; - } - - const rhsValue = - condition.relation === "EQUAL_TO_STRING" - ? `"${condition.rhs}"` - : condition.rhs; - - const unit = condition.unit || ""; - - return `${condition.lhs} ${relationSymbol} ${rhsValue} ${unit}`; - } -} diff --git a/src/app/shared/modules/filters/date-range-filter.component.html b/src/app/shared/modules/filters/date-range-filter.component.html deleted file mode 100644 index b11cd62e4..000000000 --- a/src/app/shared/modules/filters/date-range-filter.component.html +++ /dev/null @@ -1,19 +0,0 @@ - - {{ label }} - - - - - - - diff --git a/src/app/shared/modules/filters/date-range-filter.component.scss b/src/app/shared/modules/filters/date-range-filter.component.scss deleted file mode 100644 index 9c04bc08b..000000000 --- a/src/app/shared/modules/filters/date-range-filter.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -.mat-mdc-form-field { - width: 100%; -} diff --git a/src/app/shared/modules/filters/date-range-filter.component.spec.ts b/src/app/shared/modules/filters/date-range-filter.component.spec.ts deleted file mode 100644 index ee15cb586..000000000 --- a/src/app/shared/modules/filters/date-range-filter.component.spec.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { isDevMode, NO_ERRORS_SCHEMA } from "@angular/core"; -import { - ComponentFixture, - TestBed, - inject, - waitForAsync, -} from "@angular/core/testing"; -import { Store, StoreModule } from "@ngrx/store"; -import { MockHttp, MockStore } from "shared/MockStubs"; - -import { FormsModule, ReactiveFormsModule } from "@angular/forms"; -import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; -import { setDateRangeFilterAction } from "state-management/actions/datasets.actions"; -import { SharedScicatFrontendModule } from "shared/shared.module"; -import { MatAutocompleteModule } from "@angular/material/autocomplete"; -import { MatDialogModule } from "@angular/material/dialog"; -import { MatFormFieldModule } from "@angular/material/form-field"; -import { MatInputModule } from "@angular/material/input"; -import { MatSelectModule } from "@angular/material/select"; -import { SearchParametersDialogComponent } from "shared/modules/search-parameters-dialog/search-parameters-dialog.component"; -import { AsyncPipe } from "@angular/common"; -import { DateTime } from "luxon"; -import { - MatDatepickerInputEvent, - MatDatepickerModule, -} from "@angular/material/datepicker"; -import { MatChipsModule } from "@angular/material/chips"; -import { MatNativeDateModule, MatOptionModule } from "@angular/material/core"; -import { MatCardModule } from "@angular/material/card"; -import { MatButtonModule } from "@angular/material/button"; -import { MatIconModule } from "@angular/material/icon"; -import { DateRangeFilterComponent } from "./date-range-filter.component"; -import { AppConfigService } from "app-config.service"; -import { HttpClient } from "@angular/common/http"; - -describe("DateRangeFilterComponent", () => { - let component: DateRangeFilterComponent; - let fixture: ComponentFixture; - - let store: MockStore; - let dispatchSpy; - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - schemas: [NO_ERRORS_SCHEMA], - imports: [ - BrowserAnimationsModule, - FormsModule, - MatAutocompleteModule, - MatButtonModule, - MatCardModule, - MatChipsModule, - MatDatepickerModule, - MatDialogModule, - MatFormFieldModule, - MatIconModule, - MatInputModule, - MatOptionModule, - MatSelectModule, - MatNativeDateModule, - ReactiveFormsModule, - SharedScicatFrontendModule, - StoreModule.forRoot( - {}, - { - runtimeChecks: { - strictActionImmutability: false, - strictActionSerializability: false, - strictActionTypeUniqueness: false, - strictActionWithinNgZone: false, - strictStateImmutability: false, - strictStateSerializability: false, - }, - }, - ), - ], - declarations: [DateRangeFilterComponent, SearchParametersDialogComponent], - providers: [ - AsyncPipe, - AppConfigService, - { provide: HttpClient, useClass: MockHttp }, - ], - }); - TestBed.compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(DateRangeFilterComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - beforeEach(inject([Store], (mockStore: MockStore) => { - store = mockStore; - })); - - afterEach(() => { - fixture.destroy(); - }); - - describe("#dateChanged()", () => { - it("should dispatch setDateRangeFilterAction with empty string values if event.value is null", () => { - dispatchSpy = spyOn(store, "dispatch"); - - const event = { - targetElement: { - getAttribute: (name: string) => "begin", - }, - value: null, - } as MatDatepickerInputEvent; - - component.dateChanged(event); - - expect(dispatchSpy).toHaveBeenCalledTimes(1); - expect(dispatchSpy).toHaveBeenCalledWith( - setDateRangeFilterAction({ begin: "", end: "" }), - ); - }); - - it("should set dateRange.begin if event has value and event.targetElement name is begin", () => { - dispatchSpy = spyOn(store, "dispatch"); - - const beginDate = DateTime.fromJSDate(new Date("2021-01-01")); - const event = { - targetElement: { - getAttribute: (name: string) => "begin", - }, - value: beginDate, - } as MatDatepickerInputEvent; - - component.dateChanged(event); - - const expected = beginDate.toUTC().toISO(); - expect(component.dateRange.begin).toEqual(expected); - expect(dispatchSpy).not.toHaveBeenCalled(); - }); - - it("should set dateRange.end if event has value and event.targetElement name is end", () => { - dispatchSpy = spyOn(store, "dispatch"); - - const endDate = DateTime.fromJSDate(new Date("2021-07-08")); - const event = { - targetElement: { - getAttribute: (name: string) => "end", - }, - value: endDate, - } as MatDatepickerInputEvent; - - component.dateChanged(event); - - const expected = endDate.toUTC().plus({ days: 1 }).toISO(); - expect(component.dateRange.end).toEqual(expected); - expect(dispatchSpy).not.toHaveBeenCalled(); - }); - - it("should dispatch a setDateRangeFilterAction if dateRange.begin and dateRange.end have values", () => { - dispatchSpy = spyOn(store, "dispatch"); - - const beginDate = DateTime.fromJSDate(new Date("2021-01-01")); - const endDate = DateTime.fromJSDate(new Date("2021-07-08")); - component.dateRange.begin = beginDate.toUTC().toISO(); - const event = { - targetElement: { - getAttribute: (name: string) => "end", - }, - value: endDate, - } as MatDatepickerInputEvent; - - component.dateChanged(event); - - const expected = { - begin: beginDate.toUTC().toISO(), - end: endDate.toUTC().plus({ days: 1 }).toISO(), - }; - expect(dispatchSpy).toHaveBeenCalledOnceWith( - setDateRangeFilterAction(expected), - ); - }); - }); -}); diff --git a/src/app/shared/modules/filters/date-range-filter.component.ts b/src/app/shared/modules/filters/date-range-filter.component.ts deleted file mode 100644 index 762629de9..000000000 --- a/src/app/shared/modules/filters/date-range-filter.component.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Component, Input } from "@angular/core"; -import { ClearableInputComponent } from "./clearable-input.component"; -import { MatDatepickerInputEvent } from "@angular/material/datepicker"; -import { DateTime } from "luxon"; -import { setDateRangeFilterAction } from "state-management/actions/datasets.actions"; -import { selectCreationTimeFilter } from "state-management/selectors/datasets.selectors"; -import { Store } from "@ngrx/store"; -import { FilterComponentInterface } from "./interface/filter-component.interface"; -import { AppConfigService } from "app-config.service"; -import { getFilterLabel } from "./utils"; - -interface DateRange { - begin: string; - end: string; -} - -@Component({ - selector: "app-date-range-filter", - templateUrl: "date-range-filter.component.html", - styleUrls: ["date-range-filter.component.scss"], - standalone: false, -}) -export class DateRangeFilterComponent - extends ClearableInputComponent - implements FilterComponentInterface -{ - readonly componentName: string = "DateRangeFilter"; - readonly label: string = "Start Date - End Date"; - readonly tooltipText: string = - "Filters datasets by creation date, within the specified range"; - - appConfig = this.appConfigService.getConfig(); - creationTimeFilter$ = this.store.select(selectCreationTimeFilter); - - dateRange: DateRange = { - begin: "", - end: "", - }; - - constructor( - private store: Store, - private appConfigService: AppConfigService, - ) { - super(); - - const filters = this.appConfig.labelMaps?.filters; - this.label = getFilterLabel(filters, this.componentName, this.label); - } - - dateChanged(event: MatDatepickerInputEvent) { - if (event.value) { - const name = event.targetElement.getAttribute("name"); - if (name === "begin") { - this.dateRange.begin = event.value.toUTC().toISO(); - this.dateRange.end = ""; - } - if (name === "end") { - this.dateRange.end = event.value.toUTC().plus({ days: 1 }).toISO(); - } - if (this.dateRange.begin.length > 0 && this.dateRange.end.length > 0) { - this.store.dispatch(setDateRangeFilterAction(this.dateRange)); - } - } else { - this.store.dispatch(setDateRangeFilterAction({ begin: "", end: "" })); - } - } - - @Input() - set clear(value: boolean) { - if (value) - this.dateRange = { - begin: "", - end: "", - }; - } -} diff --git a/src/app/shared/modules/filters/filters.module.ts b/src/app/shared/modules/filters/filters.module.ts deleted file mode 100644 index 3d635fc1f..000000000 --- a/src/app/shared/modules/filters/filters.module.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { NgModule } from "@angular/core"; -import { PidFilterContainsComponent } from "./pid-filter-contains.component"; -import { PidFilterComponent } from "./pid-filter.component"; -import { PidFilterStartsWithComponent } from "./pid-filter-startsWith.component"; -import { ClearableInputComponent } from "./clearable-input.component"; -import { LocationFilterComponent } from "./location-filter.component"; -import { GroupFilterComponent } from "./group-filter.component"; -import { ConditionFilterComponent } from "./condition-filter.component"; -import { TypeFilterComponent } from "./type-filter.component"; -import { TextFilterComponent } from "./text-filter.component"; -import { KeywordFilterComponent } from "./keyword-filter.component"; -import { DateRangeFilterComponent } from "./date-range-filter.component"; -import { ScientificCondition } from "state-management/models"; -import { MatInputModule } from "@angular/material/input"; -import { MatDatepickerModule } from "@angular/material/datepicker"; -import { AsyncPipe, NgForOf } from "@angular/common"; -import { MatChipsModule } from "@angular/material/chips"; -import { MatIconModule } from "@angular/material/icon"; -import { MatAutocompleteModule } from "@angular/material/autocomplete"; -import { MatTooltipModule } from "@angular/material/tooltip"; -import { MatSelectModule } from "@angular/material/select"; -import { MatExpansionModule } from "@angular/material/expansion"; -import { MatFormFieldModule } from "@angular/material/form-field"; -import { MatButtonModule } from "@angular/material/button"; - -@NgModule({ - declarations: [ - ClearableInputComponent, - PidFilterComponent, - PidFilterContainsComponent, - PidFilterStartsWithComponent, - LocationFilterComponent, - GroupFilterComponent, - TypeFilterComponent, - KeywordFilterComponent, - DateRangeFilterComponent, - TextFilterComponent, - ConditionFilterComponent, - ], - imports: [ - MatTooltipModule, - MatInputModule, - MatDatepickerModule, - AsyncPipe, - MatChipsModule, - MatIconModule, - MatAutocompleteModule, - NgForOf, - MatSelectModule, - MatExpansionModule, - MatFormFieldModule, - MatButtonModule, - ], - exports: [ - ClearableInputComponent, - PidFilterComponent, - PidFilterContainsComponent, - PidFilterStartsWithComponent, - LocationFilterComponent, - GroupFilterComponent, - TypeFilterComponent, - KeywordFilterComponent, - DateRangeFilterComponent, - TextFilterComponent, - ConditionFilterComponent, - ], -}) -export class FiltersModule {} - -export enum Filters { - PidFilter = "PidFilter", - PidFilterContains = "PidFilterContains", - PidFilterStartsWith = "PidFilterStartsWith", - LocationFilter = "LocationFilter", - GroupFilter = "GroupFilter", - TypeFilter = "TypeFilter", - KeywordFilter = "KeywordFilter", - DateRangeFilter = "DateRangeFilter", - TextFilter = "TextFilter", - ConditionFilter = "ConditionFilter", -} -export type FilterConfig = Partial<{ - [K in Filters]: boolean; -}>; - -export interface ConditionConfig { - condition: ScientificCondition; - enabled: boolean; -} diff --git a/src/app/shared/modules/filters/group-filter.component.html b/src/app/shared/modules/filters/group-filter.component.html deleted file mode 100644 index ced5d9621..000000000 --- a/src/app/shared/modules/filters/group-filter.component.html +++ /dev/null @@ -1,31 +0,0 @@ - - {{ label }} - - {{ group }}cancel - - - - - - {{ getFacetId(fc, "No Group") }} | - {{ getFacetCount(fc) }} - - - diff --git a/src/app/shared/modules/filters/group-filter.component.scss b/src/app/shared/modules/filters/group-filter.component.scss deleted file mode 100644 index 9c04bc08b..000000000 --- a/src/app/shared/modules/filters/group-filter.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -.mat-mdc-form-field { - width: 100%; -} diff --git a/src/app/shared/modules/filters/group-filter.component.spec.ts b/src/app/shared/modules/filters/group-filter.component.spec.ts deleted file mode 100644 index da06448cb..000000000 --- a/src/app/shared/modules/filters/group-filter.component.spec.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { isDevMode, NO_ERRORS_SCHEMA } from "@angular/core"; -import { - ComponentFixture, - TestBed, - inject, - waitForAsync, -} from "@angular/core/testing"; -import { Store, StoreModule } from "@ngrx/store"; -import { MockHttp, MockStore } from "shared/MockStubs"; - -import { FormsModule, ReactiveFormsModule } from "@angular/forms"; -import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; -import { - addGroupFilterAction, - removeGroupFilterAction, -} from "state-management/actions/datasets.actions"; -import { SharedScicatFrontendModule } from "shared/shared.module"; -import { MatAutocompleteModule } from "@angular/material/autocomplete"; -import { MatDialogModule, MatDialog } from "@angular/material/dialog"; -import { MatFormFieldModule } from "@angular/material/form-field"; -import { MatInputModule } from "@angular/material/input"; -import { MatSelectModule } from "@angular/material/select"; -import { SearchParametersDialogComponent } from "shared/modules/search-parameters-dialog/search-parameters-dialog.component"; -import { AsyncPipe } from "@angular/common"; -import { MatDatepickerModule } from "@angular/material/datepicker"; -import { MatChipsModule } from "@angular/material/chips"; -import { MatNativeDateModule, MatOptionModule } from "@angular/material/core"; -import { MatCardModule } from "@angular/material/card"; -import { MatButtonModule } from "@angular/material/button"; -import { MatIconModule } from "@angular/material/icon"; -import { GroupFilterComponent } from "./group-filter.component"; -import { HttpClient } from "@angular/common/http"; -import { AppConfigService } from "app-config.service"; - -describe("GroupFilterComponent", () => { - let component: GroupFilterComponent; - let fixture: ComponentFixture; - - let store: MockStore; - let dispatchSpy; - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - schemas: [NO_ERRORS_SCHEMA], - imports: [ - BrowserAnimationsModule, - FormsModule, - MatAutocompleteModule, - MatButtonModule, - MatCardModule, - MatChipsModule, - MatDatepickerModule, - MatDialogModule, - MatFormFieldModule, - MatIconModule, - MatInputModule, - MatOptionModule, - MatSelectModule, - MatNativeDateModule, - ReactiveFormsModule, - SharedScicatFrontendModule, - StoreModule.forRoot( - {}, - { - runtimeChecks: { - strictActionImmutability: false, - strictActionSerializability: false, - strictActionTypeUniqueness: false, - strictActionWithinNgZone: false, - strictStateImmutability: false, - strictStateSerializability: false, - }, - }, - ), - ], - declarations: [GroupFilterComponent, SearchParametersDialogComponent], - providers: [ - AsyncPipe, - AppConfigService, - { provide: HttpClient, useClass: MockHttp }, - ], - }); - TestBed.compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(GroupFilterComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - beforeEach(inject([Store], (mockStore: MockStore) => { - store = mockStore; - })); - - afterEach(() => { - fixture.destroy(); - }); - - describe("#onGroupInput()", () => { - it("should call next on groupInput$", () => { - const nextSpy = spyOn(component.groupInput$, "next"); - - const event = { - target: { - value: "group", - }, - }; - - component.onGroupInput(event); - - expect(nextSpy).toHaveBeenCalledOnceWith(event.target.value); - }); - }); - - describe("#groupSelected()", () => { - it("should dispatch an AddGroupFilterAction", () => { - dispatchSpy = spyOn(store, "dispatch"); - - const group = "test"; - component.groupSelected(group); - - expect(dispatchSpy).toHaveBeenCalledTimes(1); - expect(dispatchSpy).toHaveBeenCalledWith(addGroupFilterAction({ group })); - }); - }); - - describe("#groupRemoved()", () => { - it("should dispatch a RemoveGroupFilterAction", () => { - dispatchSpy = spyOn(store, "dispatch"); - - const group = "test"; - component.groupRemoved(group); - - expect(dispatchSpy).toHaveBeenCalledTimes(1); - expect(dispatchSpy).toHaveBeenCalledWith( - removeGroupFilterAction({ group }), - ); - }); - }); -}); diff --git a/src/app/shared/modules/filters/group-filter.component.ts b/src/app/shared/modules/filters/group-filter.component.ts deleted file mode 100644 index 9e51d0b7a..000000000 --- a/src/app/shared/modules/filters/group-filter.component.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { Component } from "@angular/core"; -import { - selectGroupFacetCounts, - selectGroupFilter, -} from "state-management/selectors/datasets.selectors"; -import { Store } from "@ngrx/store"; -import { - addGroupFilterAction, - removeGroupFilterAction, -} from "state-management/actions/datasets.actions"; -import { - createSuggestionObserver, - getFacetCount, - getFacetId, - getFilterLabel, -} from "./utils"; -import { BehaviorSubject } from "rxjs"; -import { ClearableInputComponent } from "./clearable-input.component"; -import { FilterComponentInterface } from "./interface/filter-component.interface"; -import { AppConfigService } from "app-config.service"; - -@Component({ - selector: "app-group-filter", - templateUrl: "group-filter.component.html", - styleUrls: ["group-filter.component.scss"], - standalone: false, -}) -export class GroupFilterComponent - extends ClearableInputComponent - implements FilterComponentInterface -{ - protected readonly getFacetId = getFacetId; - protected readonly getFacetCount = getFacetCount; - readonly componentName: string = "GroupFilter"; - readonly label: string = "Group Filter"; - readonly tooltipText: string = "Filters datasets by group"; - - appConfig = this.appConfigService.getConfig(); - - groupFilter$ = this.store.select(selectGroupFilter); - - groupFacetCounts$ = this.store.select(selectGroupFacetCounts); - groupInput$ = new BehaviorSubject(""); - - groupSuggestions$ = createSuggestionObserver( - this.groupFacetCounts$, - this.groupInput$, - this.groupFilter$, - ); - - constructor( - private store: Store, - private appConfigService: AppConfigService, - ) { - super(); - - const filters = this.appConfig.labelMaps?.filters; - this.label = getFilterLabel(filters, this.componentName, this.label); - } - - onGroupInput(event: any) { - const value = (event.target).value; - this.groupInput$.next(value); - } - groupSelected(group: string) { - this.store.dispatch(addGroupFilterAction({ group })); - this.groupInput$.next(""); - } - - groupRemoved(group: string) { - this.store.dispatch(removeGroupFilterAction({ group })); - } -} diff --git a/src/app/shared/modules/filters/keyword-filter.component.html b/src/app/shared/modules/filters/keyword-filter.component.html deleted file mode 100644 index 04798ae2b..000000000 --- a/src/app/shared/modules/filters/keyword-filter.component.html +++ /dev/null @@ -1,31 +0,0 @@ - - {{ label }} - - {{ keyword }}cancel - - - - - - {{ getFacetId(fc, "No Keywords") }} - : {{ getFacetCount(fc) }} - - - diff --git a/src/app/shared/modules/filters/keyword-filter.component.scss b/src/app/shared/modules/filters/keyword-filter.component.scss deleted file mode 100644 index 9c04bc08b..000000000 --- a/src/app/shared/modules/filters/keyword-filter.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -.mat-mdc-form-field { - width: 100%; -} diff --git a/src/app/shared/modules/filters/keyword-filter.component.spec.ts b/src/app/shared/modules/filters/keyword-filter.component.spec.ts deleted file mode 100644 index b7e2c46a7..000000000 --- a/src/app/shared/modules/filters/keyword-filter.component.spec.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { isDevMode, NO_ERRORS_SCHEMA } from "@angular/core"; -import { - ComponentFixture, - TestBed, - inject, - waitForAsync, -} from "@angular/core/testing"; -import { Store, StoreModule } from "@ngrx/store"; -import { MockHttp, MockStore } from "shared/MockStubs"; - -import { FormsModule, ReactiveFormsModule } from "@angular/forms"; -import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; -import { - addKeywordFilterAction, - removeKeywordFilterAction, -} from "state-management/actions/datasets.actions"; -import { SharedScicatFrontendModule } from "shared/shared.module"; -import { MatAutocompleteModule } from "@angular/material/autocomplete"; -import { MatDialogModule, MatDialog } from "@angular/material/dialog"; -import { MatFormFieldModule } from "@angular/material/form-field"; -import { MatInputModule } from "@angular/material/input"; -import { MatSelectModule } from "@angular/material/select"; -import { SearchParametersDialogComponent } from "shared/modules/search-parameters-dialog/search-parameters-dialog.component"; -import { AsyncPipe } from "@angular/common"; -import { MatDatepickerModule } from "@angular/material/datepicker"; -import { MatChipsModule } from "@angular/material/chips"; -import { MatNativeDateModule, MatOptionModule } from "@angular/material/core"; -import { MatCardModule } from "@angular/material/card"; -import { MatButtonModule } from "@angular/material/button"; -import { MatIconModule } from "@angular/material/icon"; -import { KeywordFilterComponent } from "./keyword-filter.component"; -import { HttpClient } from "@angular/common/http"; -import { AppConfigService } from "app-config.service"; - -describe("KeywordFilterComponent", () => { - let component: KeywordFilterComponent; - let fixture: ComponentFixture; - - let store: MockStore; - let dispatchSpy; - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - schemas: [NO_ERRORS_SCHEMA], - imports: [ - BrowserAnimationsModule, - FormsModule, - MatAutocompleteModule, - MatButtonModule, - MatCardModule, - MatChipsModule, - MatDatepickerModule, - MatDialogModule, - MatFormFieldModule, - MatIconModule, - MatInputModule, - MatOptionModule, - MatSelectModule, - MatNativeDateModule, - ReactiveFormsModule, - SharedScicatFrontendModule, - StoreModule.forRoot( - {}, - { - runtimeChecks: { - strictActionImmutability: false, - strictActionSerializability: false, - strictActionTypeUniqueness: false, - strictActionWithinNgZone: false, - strictStateImmutability: false, - strictStateSerializability: false, - }, - }, - ), - ], - declarations: [KeywordFilterComponent, SearchParametersDialogComponent], - providers: [ - AsyncPipe, - AppConfigService, - { provide: HttpClient, useClass: MockHttp }, - ], - }); - TestBed.compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(KeywordFilterComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - beforeEach(inject([Store], (mockStore: MockStore) => { - store = mockStore; - })); - - afterEach(() => { - fixture.destroy(); - }); - - describe("#onKeywordInput()", () => { - it("should call next on keywordsInput$", () => { - const nextSpy = spyOn(component.keywordsInput$, "next"); - - const event = { - target: { - value: "keyword", - }, - }; - - component.onKeywordInput(event); - - expect(nextSpy).toHaveBeenCalledOnceWith(event.target.value); - }); - }); - - describe("#keywordSelected()", () => { - it("should dispatch an AddKeywordFilterAction", () => { - dispatchSpy = spyOn(store, "dispatch"); - - const keyword = "test"; - component.keywordSelected(keyword); - - expect(dispatchSpy).toHaveBeenCalledTimes(1); - expect(dispatchSpy).toHaveBeenCalledWith( - addKeywordFilterAction({ keyword }), - ); - }); - }); - - describe("#keywordRemoved()", () => { - it("should dispatch a RemoveKeywordFilterAction", () => { - dispatchSpy = spyOn(store, "dispatch"); - - const keyword = "test"; - component.keywordRemoved(keyword); - - expect(dispatchSpy).toHaveBeenCalledTimes(1); - expect(dispatchSpy).toHaveBeenCalledWith( - removeKeywordFilterAction({ keyword }), - ); - }); - }); -}); diff --git a/src/app/shared/modules/filters/keyword-filter.component.ts b/src/app/shared/modules/filters/keyword-filter.component.ts deleted file mode 100644 index 811ba8b73..000000000 --- a/src/app/shared/modules/filters/keyword-filter.component.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { Component, OnDestroy } from "@angular/core"; -import { ClearableInputComponent } from "./clearable-input.component"; -import { - createSuggestionObserver, - getFacetCount, - getFacetId, - getFilterLabel, -} from "./utils"; -import { - selectKeywordFacetCounts, - selectKeywordsFilter, - selectKeywordsTerms, -} from "state-management/selectors/datasets.selectors"; -import { Store } from "@ngrx/store"; -import { BehaviorSubject } from "rxjs"; -import { - addKeywordFilterAction, - removeKeywordFilterAction, -} from "state-management/actions/datasets.actions"; -import { debounceTime, distinctUntilChanged, skipWhile } from "rxjs/operators"; -import { AppConfigService } from "app-config.service"; - -@Component({ - selector: "app-keyword-filter", - templateUrl: "keyword-filter.component.html", - styleUrls: ["keyword-filter.component.scss"], - standalone: false, -}) -export class KeywordFilterComponent - extends ClearableInputComponent - implements OnDestroy -{ - protected readonly getFacetCount = getFacetCount; - protected readonly getFacetId = getFacetId; - readonly componentName: string = "KeywordFilter"; - readonly label: string = "Keyword Filter"; - readonly tooltipText: string = "Filters datasets by keyword"; - - appConfig = this.appConfigService.getConfig(); - - keywordsTerms$ = this.store.select(selectKeywordsTerms); - - keywordsFilter$ = this.store.select(selectKeywordsFilter); - - keywordsInput$ = new BehaviorSubject(""); - keywordFacetCounts$ = this.store.select(selectKeywordFacetCounts); - - subscription = undefined; - - keywordsSuggestions$ = createSuggestionObserver( - this.keywordFacetCounts$, - this.keywordsInput$, - this.keywordsFilter$, - ); - - constructor( - private store: Store, - private appConfigService: AppConfigService, - ) { - super(); - - const filters = this.appConfig.labelMaps?.filters; - this.label = getFilterLabel(filters, this.componentName, this.label); - this.subscription = this.keywordsTerms$ - .pipe( - skipWhile((terms) => terms === ""), - debounceTime(500), - distinctUntilChanged(), - ) - .subscribe((terms) => { - this.store.dispatch(addKeywordFilterAction({ keyword: terms })); - }); - } - - onKeywordInput(event: any) { - const value = (event.target).value; - this.keywordsInput$.next(value); - } - - keywordSelected(keyword: string) { - this.store.dispatch(addKeywordFilterAction({ keyword })); - this.keywordsInput$.next(""); - } - - keywordRemoved(keyword: string) { - this.store.dispatch(removeKeywordFilterAction({ keyword })); - } - - ngOnDestroy() { - this.subscription.unsubscribe(); - } -} diff --git a/src/app/shared/modules/filters/location-filter.component.html b/src/app/shared/modules/filters/location-filter.component.html deleted file mode 100644 index c3f2a5016..000000000 --- a/src/app/shared/modules/filters/location-filter.component.html +++ /dev/null @@ -1,33 +0,0 @@ - - {{ label }} - - {{ location || "No Location" }} - cancel - - - - - - - {{ getFacetId(fc, "No Location") }} | - {{ getFacetCount(fc) }} - - - diff --git a/src/app/shared/modules/filters/location-filter.component.scss b/src/app/shared/modules/filters/location-filter.component.scss deleted file mode 100644 index 9c04bc08b..000000000 --- a/src/app/shared/modules/filters/location-filter.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -.mat-mdc-form-field { - width: 100%; -} diff --git a/src/app/shared/modules/filters/location-filter.component.ts b/src/app/shared/modules/filters/location-filter.component.ts deleted file mode 100644 index 6d99d41de..000000000 --- a/src/app/shared/modules/filters/location-filter.component.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Component } from "@angular/core"; -import { - selectLocationFacetCounts, - selectLocationFilter, -} from "state-management/selectors/datasets.selectors"; -import { - createSuggestionObserver, - getFacetCount, - getFacetId, - getFilterLabel, -} from "./utils"; -import { BehaviorSubject } from "rxjs"; -import { - addLocationFilterAction, - removeLocationFilterAction, -} from "state-management/actions/datasets.actions"; -import { ClearableInputComponent } from "./clearable-input.component"; -import { Store } from "@ngrx/store"; -import { FilterComponentInterface } from "./interface/filter-component.interface"; -import { AppConfigService } from "app-config.service"; - -@Component({ - selector: "app-location-filter", - templateUrl: "location-filter.component.html", - styleUrls: ["location-filter.component.scss"], - standalone: false, -}) -export class LocationFilterComponent - extends ClearableInputComponent - implements FilterComponentInterface -{ - protected readonly getFacetId = getFacetId; - protected readonly getFacetCount = getFacetCount; - readonly componentName: string = "LocationFilter"; - readonly label: string = "Location Filter"; - readonly tooltipText: string = "Filters datasets by location"; - - appConfig = this.appConfigService.getConfig(); - - locationFacetCounts$ = this.store.select(selectLocationFacetCounts); - locationFilter$ = this.store.select(selectLocationFilter); - - locationInput$ = new BehaviorSubject(""); - - locationSuggestions$ = createSuggestionObserver( - this.locationFacetCounts$, - this.locationInput$, - this.locationFilter$, - ); - - constructor( - private store: Store, - public appConfigService: AppConfigService, - ) { - super(); - - const filters = this.appConfig.labelMaps?.filters; - this.label = getFilterLabel(filters, this.componentName, this.label); - } - - locationSelected(location: string | null) { - const loc = location || ""; - this.store.dispatch(addLocationFilterAction({ location: loc })); - this.locationInput$.next(""); - } - - locationRemoved(location: string) { - this.store.dispatch(removeLocationFilterAction({ location })); - } - - onLocationInput(event: any) { - const value = (event.target).value; - this.locationInput$.next(value); - } -} diff --git a/src/app/shared/modules/filters/multiselect-filter.component.html b/src/app/shared/modules/filters/multiselect-filter.component.html new file mode 100644 index 000000000..8a39270bc --- /dev/null +++ b/src/app/shared/modules/filters/multiselect-filter.component.html @@ -0,0 +1,33 @@ + + {{ label }} + + {{ item || "No item" }} + cancel + + + + + + + {{ getFacetId(fc, "No items") }} | + {{ getFacetCount(fc) }} + + + diff --git a/src/app/shared/modules/filters/condition-filter.component.scss b/src/app/shared/modules/filters/multiselect-filter.component.scss similarity index 100% rename from src/app/shared/modules/filters/condition-filter.component.scss rename to src/app/shared/modules/filters/multiselect-filter.component.scss diff --git a/src/app/shared/modules/filters/location-filter.component.spec.ts b/src/app/shared/modules/filters/multiselect-filter.component.spec.ts similarity index 68% rename from src/app/shared/modules/filters/location-filter.component.spec.ts rename to src/app/shared/modules/filters/multiselect-filter.component.spec.ts index 7122b0e5d..fb43d4612 100644 --- a/src/app/shared/modules/filters/location-filter.component.spec.ts +++ b/src/app/shared/modules/filters/multiselect-filter.component.spec.ts @@ -1,4 +1,4 @@ -import { isDevMode, NO_ERRORS_SCHEMA } from "@angular/core"; +import { NO_ERRORS_SCHEMA } from "@angular/core"; import { ComponentFixture, TestBed, @@ -10,10 +10,6 @@ import { MockHttp, MockStore } from "shared/MockStubs"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; -import { - addLocationFilterAction, - removeLocationFilterAction, -} from "state-management/actions/datasets.actions"; import { SharedScicatFrontendModule } from "shared/shared.module"; import { MatAutocompleteModule } from "@angular/material/autocomplete"; import { MatDialogModule } from "@angular/material/dialog"; @@ -28,13 +24,13 @@ import { MatNativeDateModule, MatOptionModule } from "@angular/material/core"; import { MatCardModule } from "@angular/material/card"; import { MatButtonModule } from "@angular/material/button"; import { MatIconModule } from "@angular/material/icon"; -import { LocationFilterComponent } from "./location-filter.component"; +import { MultiSelectFilterComponent } from "./multiselect-filter.component"; import { HttpClient } from "@angular/common/http"; import { AppConfigService } from "app-config.service"; -describe("LocationFilterComponent", () => { - let component: LocationFilterComponent; - let fixture: ComponentFixture; +describe("MultiSelectFilterComponent", () => { + let component: MultiSelectFilterComponent; + let fixture: ComponentFixture; let store: MockStore; let dispatchSpy; @@ -73,7 +69,10 @@ describe("LocationFilterComponent", () => { }, ), ], - declarations: [LocationFilterComponent, SearchParametersDialogComponent], + declarations: [ + MultiSelectFilterComponent, + SearchParametersDialogComponent, + ], providers: [ AsyncPipe, AppConfigService, @@ -84,7 +83,7 @@ describe("LocationFilterComponent", () => { })); beforeEach(() => { - fixture = TestBed.createComponent(LocationFilterComponent); + fixture = TestBed.createComponent(MultiSelectFilterComponent); component = fixture.componentInstance; fixture.detectChanges(); }); @@ -97,47 +96,51 @@ describe("LocationFilterComponent", () => { fixture.destroy(); }); - describe("#onLocationInput()", () => { - it("should call next on locationInput$", () => { - const nextSpy = spyOn(component.locationInput$, "next"); + describe("#onInput()", () => { + it("should call next on input$", () => { + const nextSpy = spyOn(component.input$, "next"); const event = { target: { - value: "location", + value: "testValue", }, }; - component.onLocationInput(event); + component.onInput(event); expect(nextSpy).toHaveBeenCalledOnceWith(event.target.value); }); }); - describe("#locationSelected()", () => { - it("should dispatch an AddLocationFilterAction", () => { - dispatchSpy = spyOn(store, "dispatch"); + describe("#itemSelected()", () => { + it("should dispatch an AddMultiSelectFilterAction", () => { + dispatchSpy = spyOn(component.selectionChange, "emit"); - const location = "test"; - component.locationSelected(location); + const value = "test"; + component.itemSelected(value); expect(dispatchSpy).toHaveBeenCalledTimes(1); - expect(dispatchSpy).toHaveBeenCalledWith( - addLocationFilterAction({ location }), - ); + expect(dispatchSpy).toHaveBeenCalledWith({ + key: component.key, + value: value, + event: "add", + }); }); }); - describe("#locationRemoved()", () => { - it("should dispatch a RemoveLocationFilterAction", () => { - dispatchSpy = spyOn(store, "dispatch"); + describe("#itemRemoved()", () => { + it("should dispatch a RemoveMultiSelectFilterAction", () => { + dispatchSpy = spyOn(component.selectionChange, "emit"); - const location = "test"; - component.locationRemoved(location); + const value = "test"; + component.itemRemoved(value); expect(dispatchSpy).toHaveBeenCalledTimes(1); - expect(dispatchSpy).toHaveBeenCalledWith( - removeLocationFilterAction({ location }), - ); + expect(dispatchSpy).toHaveBeenCalledWith({ + key: component.key, + value: value, + event: "remove", + }); }); }); }); diff --git a/src/app/shared/modules/filters/multiselect-filter.component.ts b/src/app/shared/modules/filters/multiselect-filter.component.ts new file mode 100644 index 000000000..aca30325b --- /dev/null +++ b/src/app/shared/modules/filters/multiselect-filter.component.ts @@ -0,0 +1,64 @@ +import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { createSuggestionObserver, getFacetCount, getFacetId } from "./utils"; +import { BehaviorSubject, Observable } from "rxjs"; +import { ClearableInputComponent } from "./clearable-input.component"; +import { AppConfigService } from "app-config.service"; +import { FacetCount } from "state-management/state/datasets.store"; + +export type MultiSelectFilterValue = { + key: string; + value: string; + event: "add" | "remove"; +}; + +@Component({ + selector: "multiselect-filter", + templateUrl: "multiselect-filter.component.html", + styleUrls: ["multiselect-filter.component.scss"], + standalone: false, +}) +export class MultiSelectFilterComponent extends ClearableInputComponent { + protected readonly getFacetId = getFacetId; + protected readonly getFacetCount = getFacetCount; + @Input() key = ""; + @Input() label = ""; + @Input() tooltip = ""; + @Input() facetCounts$: Observable; + @Input() currentFilter$: Observable; + @Output() selectionChange = new EventEmitter(); + + appConfig = this.appConfigService.getConfig(); + + input$ = new BehaviorSubject(""); + suggestions$: Observable; + + constructor(public appConfigService: AppConfigService) { + super(); + } + + ngOnInit() { + this.suggestions$ = createSuggestionObserver( + this.facetCounts$, + this.input$, + this.currentFilter$, + ); + } + + itemSelected(value: string | null) { + this.selectionChange.emit({ key: this.key, value: value, event: "add" }); + this.input$.next(""); + } + + itemRemoved(value: string) { + this.selectionChange.emit({ + key: this.key, + value: value, + event: "remove", + }); + } + + onInput(event: any) { + const value = (event.target).value; + this.input$.next(value); + } +} diff --git a/src/app/shared/modules/filters/pid-filter-contains.component.html b/src/app/shared/modules/filters/pid-filter-contains.component.html deleted file mode 100644 index 7debe93f0..000000000 --- a/src/app/shared/modules/filters/pid-filter-contains.component.html +++ /dev/null @@ -1,11 +0,0 @@ - - {{ label }} - - diff --git a/src/app/shared/modules/filters/pid-filter-contains.component.scss b/src/app/shared/modules/filters/pid-filter-contains.component.scss deleted file mode 100644 index 9c04bc08b..000000000 --- a/src/app/shared/modules/filters/pid-filter-contains.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -.mat-mdc-form-field { - width: 100%; -} diff --git a/src/app/shared/modules/filters/pid-filter-contains.component.spec.ts b/src/app/shared/modules/filters/pid-filter-contains.component.spec.ts deleted file mode 100644 index 40f76ddb0..000000000 --- a/src/app/shared/modules/filters/pid-filter-contains.component.spec.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { NO_ERRORS_SCHEMA } from "@angular/core"; -import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; -import { StoreModule } from "@ngrx/store"; -import { MockHttp } from "shared/MockStubs"; - -import { FormsModule, ReactiveFormsModule } from "@angular/forms"; -import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; -import { SharedScicatFrontendModule } from "shared/shared.module"; -import { MatAutocompleteModule } from "@angular/material/autocomplete"; -import { MatDialogModule } from "@angular/material/dialog"; -import { MatFormFieldModule } from "@angular/material/form-field"; -import { MatInputModule } from "@angular/material/input"; -import { MatSelectModule } from "@angular/material/select"; -import { SearchParametersDialogComponent } from "shared/modules/search-parameters-dialog/search-parameters-dialog.component"; -import { AsyncPipe } from "@angular/common"; -import { MatDatepickerModule } from "@angular/material/datepicker"; -import { MatChipsModule } from "@angular/material/chips"; -import { MatNativeDateModule, MatOptionModule } from "@angular/material/core"; -import { MatCardModule } from "@angular/material/card"; -import { MatButtonModule } from "@angular/material/button"; -import { MatIconModule } from "@angular/material/icon"; -import { AppConfigService } from "app-config.service"; -import { PidFilterContainsComponent } from "./pid-filter-contains.component"; -import { PidFilterComponent } from "./pid-filter.component"; -import { HttpClient } from "@angular/common/http"; - -const getConfig = () => ({ - scienceSearchEnabled: false, -}); - -describe("PidFilterContainsComponent", () => { - let component: PidFilterContainsComponent; - let fixture: ComponentFixture; - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - schemas: [NO_ERRORS_SCHEMA], - imports: [ - BrowserAnimationsModule, - FormsModule, - MatAutocompleteModule, - MatButtonModule, - MatCardModule, - MatChipsModule, - MatDatepickerModule, - MatDialogModule, - MatFormFieldModule, - MatIconModule, - MatInputModule, - MatOptionModule, - MatSelectModule, - MatNativeDateModule, - ReactiveFormsModule, - SharedScicatFrontendModule, - StoreModule.forRoot({}), - ], - declarations: [ - PidFilterContainsComponent, - PidFilterComponent, - SearchParametersDialogComponent, - ], - providers: [ - AsyncPipe, - AppConfigService, - { provide: HttpClient, useClass: MockHttp }, - ], - }); - TestBed.overrideComponent(PidFilterContainsComponent, { - set: { - providers: [ - { - provide: AppConfigService, - useValue: { - getConfig, - }, - }, - ], - }, - }); - TestBed.compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(PidFilterContainsComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - afterEach(() => { - fixture.destroy(); - }); - - describe("#buildPidTermsCondition()", () => { - const tests = [ - { input: "1", method: "contains", expected: { $regex: "1" } }, - ]; - - tests.forEach((test, index) => { - it(`should return correct condition for test case #${index + 1}`, () => { - component.appConfig.pidSearchMethod = test.method; - const condition = component.buildPidTermsCondition(test.input); - expect(condition).toEqual(test.expected); - }); - }); - }); -}); diff --git a/src/app/shared/modules/filters/pid-filter-contains.component.ts b/src/app/shared/modules/filters/pid-filter-contains.component.ts deleted file mode 100644 index cfad3363a..000000000 --- a/src/app/shared/modules/filters/pid-filter-contains.component.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { PidFilterComponent } from "./pid-filter.component"; -import { Component } from "@angular/core"; - -@Component({ - selector: "app-pid-contains-filter", - templateUrl: "./pid-filter-contains.component.html", - styleUrls: ["./pid-filter-contains.component.scss"], - standalone: false, -}) -export class PidFilterContainsComponent extends PidFilterComponent { - readonly componentName: string = "PidFilterContains"; - readonly label: string = "PID filter (Contains)- Not implemented"; - readonly tooltipText: string = "Not implemented"; - - buildPidTermsCondition(terms: string): { $regex: string } { - return { $regex: terms }; - } -} diff --git a/src/app/shared/modules/filters/pid-filter-startsWith.component.html b/src/app/shared/modules/filters/pid-filter-startsWith.component.html deleted file mode 100644 index 7debe93f0..000000000 --- a/src/app/shared/modules/filters/pid-filter-startsWith.component.html +++ /dev/null @@ -1,11 +0,0 @@ - - {{ label }} - - diff --git a/src/app/shared/modules/filters/pid-filter-startsWith.component.scss b/src/app/shared/modules/filters/pid-filter-startsWith.component.scss deleted file mode 100644 index 9c04bc08b..000000000 --- a/src/app/shared/modules/filters/pid-filter-startsWith.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -.mat-mdc-form-field { - width: 100%; -} diff --git a/src/app/shared/modules/filters/pid-filter-startsWith.component.spec.ts b/src/app/shared/modules/filters/pid-filter-startsWith.component.spec.ts deleted file mode 100644 index fc13243bb..000000000 --- a/src/app/shared/modules/filters/pid-filter-startsWith.component.spec.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { NO_ERRORS_SCHEMA } from "@angular/core"; -import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; -import { StoreModule } from "@ngrx/store"; -import { MockHttp } from "shared/MockStubs"; - -import { FormsModule, ReactiveFormsModule } from "@angular/forms"; -import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; -import { SharedScicatFrontendModule } from "shared/shared.module"; -import { MatAutocompleteModule } from "@angular/material/autocomplete"; -import { MatDialogModule } from "@angular/material/dialog"; -import { MatFormFieldModule } from "@angular/material/form-field"; -import { MatInputModule } from "@angular/material/input"; -import { MatSelectModule } from "@angular/material/select"; -import { SearchParametersDialogComponent } from "shared/modules/search-parameters-dialog/search-parameters-dialog.component"; -import { AsyncPipe } from "@angular/common"; -import { MatDatepickerModule } from "@angular/material/datepicker"; -import { MatChipsModule } from "@angular/material/chips"; -import { MatNativeDateModule, MatOptionModule } from "@angular/material/core"; -import { MatCardModule } from "@angular/material/card"; -import { MatButtonModule } from "@angular/material/button"; -import { MatIconModule } from "@angular/material/icon"; -import { AppConfigService } from "app-config.service"; -import { PidFilterComponent } from "./pid-filter.component"; -import { PidFilterStartsWithComponent } from "./pid-filter-startsWith.component"; -import { HttpClient } from "@angular/common/http"; - -const getConfig = () => ({ - scienceSearchEnabled: false, -}); - -describe("PidFilterStartsWithComponent", () => { - let component: PidFilterStartsWithComponent; - let fixture: ComponentFixture; - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - schemas: [NO_ERRORS_SCHEMA], - imports: [ - BrowserAnimationsModule, - FormsModule, - MatAutocompleteModule, - MatButtonModule, - MatCardModule, - MatChipsModule, - MatDatepickerModule, - MatDialogModule, - MatFormFieldModule, - MatIconModule, - MatInputModule, - MatOptionModule, - MatSelectModule, - MatNativeDateModule, - ReactiveFormsModule, - SharedScicatFrontendModule, - StoreModule.forRoot({}), - ], - declarations: [ - PidFilterStartsWithComponent, - PidFilterComponent, - SearchParametersDialogComponent, - ], - providers: [ - AsyncPipe, - AppConfigService, - { provide: HttpClient, useClass: MockHttp }, - ], - }); - TestBed.overrideComponent(PidFilterStartsWithComponent, { - set: { - providers: [ - { - provide: AppConfigService, - useValue: { - getConfig, - }, - }, - ], - }, - }); - TestBed.compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(PidFilterStartsWithComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - afterEach(() => { - fixture.destroy(); - }); - - describe("#buildPidTermsCondition()", () => { - const tests = [ - { input: "1", method: "startsWith", expected: { $regex: "^1" } }, - ]; - - tests.forEach((test, index) => { - it(`should return correct condition for test case #${index + 1}`, () => { - component.appConfig.pidSearchMethod = test.method; - const condition = component["buildPidTermsCondition"](test.input); - expect(condition).toEqual(test.expected); - }); - }); - }); -}); diff --git a/src/app/shared/modules/filters/pid-filter-startsWith.component.ts b/src/app/shared/modules/filters/pid-filter-startsWith.component.ts deleted file mode 100644 index 6b56f5517..000000000 --- a/src/app/shared/modules/filters/pid-filter-startsWith.component.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { PidFilterComponent } from "./pid-filter.component"; -import { Component } from "@angular/core"; - -@Component({ - selector: "app-pid-startsWith-filter", - templateUrl: "./pid-filter-startsWith.component.html", - styleUrls: ["./pid-filter-startsWith.component.scss"], - standalone: false, -}) -export class PidFilterStartsWithComponent extends PidFilterComponent { - readonly componentName: string = "PidFilterStartsWith"; - readonly label: string = "PID filter (Starts With)- Not implemented"; - readonly tooltipText: string = "Not implemented"; - - buildPidTermsCondition(terms: string): { $regex: string } { - return { $regex: `^${terms}` }; - } -} diff --git a/src/app/shared/modules/filters/pid-filter.component.html b/src/app/shared/modules/filters/pid-filter.component.html deleted file mode 100644 index 7debe93f0..000000000 --- a/src/app/shared/modules/filters/pid-filter.component.html +++ /dev/null @@ -1,11 +0,0 @@ - - {{ label }} - - diff --git a/src/app/shared/modules/filters/pid-filter.component.scss b/src/app/shared/modules/filters/pid-filter.component.scss deleted file mode 100644 index 9c04bc08b..000000000 --- a/src/app/shared/modules/filters/pid-filter.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -.mat-mdc-form-field { - width: 100%; -} diff --git a/src/app/shared/modules/filters/pid-filter.component.spec.ts b/src/app/shared/modules/filters/pid-filter.component.spec.ts deleted file mode 100644 index 01d90c646..000000000 --- a/src/app/shared/modules/filters/pid-filter.component.spec.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { NO_ERRORS_SCHEMA } from "@angular/core"; -import { - ComponentFixture, - TestBed, - inject, - waitForAsync, - fakeAsync, - tick, -} from "@angular/core/testing"; -import { Store, StoreModule } from "@ngrx/store"; -import { MockHttp, MockStore } from "shared/MockStubs"; - -import { FormsModule, ReactiveFormsModule } from "@angular/forms"; -import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; -import { setPidTermsFilterAction } from "state-management/actions/datasets.actions"; -import { SharedScicatFrontendModule } from "shared/shared.module"; -import { MatAutocompleteModule } from "@angular/material/autocomplete"; -import { MatDialogModule } from "@angular/material/dialog"; -import { MatFormFieldModule } from "@angular/material/form-field"; -import { MatInputModule } from "@angular/material/input"; -import { MatSelectModule } from "@angular/material/select"; -import { SearchParametersDialogComponent } from "shared/modules/search-parameters-dialog/search-parameters-dialog.component"; -import { AsyncPipe } from "@angular/common"; -import { MatDatepickerModule } from "@angular/material/datepicker"; -import { MatChipsModule } from "@angular/material/chips"; -import { MatNativeDateModule, MatOptionModule } from "@angular/material/core"; -import { MatCardModule } from "@angular/material/card"; -import { MatButtonModule } from "@angular/material/button"; -import { MatIconModule } from "@angular/material/icon"; -import { AppConfigService } from "app-config.service"; -import { PidFilterComponent } from "./pid-filter.component"; -import { HttpClient } from "@angular/common/http"; - -const getConfig = () => ({ - scienceSearchEnabled: false, -}); - -describe("PidFilterComponent", () => { - let component: PidFilterComponent; - let fixture: ComponentFixture; - - let store: MockStore; - let dispatchSpy; - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - schemas: [NO_ERRORS_SCHEMA], - imports: [ - BrowserAnimationsModule, - FormsModule, - MatAutocompleteModule, - MatButtonModule, - MatCardModule, - MatChipsModule, - MatDatepickerModule, - MatDialogModule, - MatFormFieldModule, - MatIconModule, - MatInputModule, - MatOptionModule, - MatSelectModule, - MatNativeDateModule, - ReactiveFormsModule, - SharedScicatFrontendModule, - StoreModule.forRoot({}), - ], - declarations: [PidFilterComponent, SearchParametersDialogComponent], - providers: [ - AsyncPipe, - AppConfigService, - { provide: HttpClient, useClass: MockHttp }, - ], - }); - TestBed.overrideComponent(PidFilterComponent, { - set: { - providers: [ - { - provide: AppConfigService, - useValue: { - getConfig, - }, - }, - ], - }, - }); - TestBed.compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(PidFilterComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - beforeEach(inject([Store], (mockStore: MockStore) => { - store = mockStore; - })); - - afterEach(() => { - fixture.destroy(); - }); - - describe("#onPidInput()", () => { - it("should dispatch a SetSearchTermsAction", fakeAsync(() => { - dispatchSpy = spyOn(store, "dispatch"); - - const pid = "xxxxxx"; - const event = { target: { value: pid } }; - component.onPidInput(event); - - tick(500); //wait for it - - expect(dispatchSpy).toHaveBeenCalledTimes(1); - expect(dispatchSpy).toHaveBeenCalledWith( - setPidTermsFilterAction({ pid }), - ); - })); - }); - - describe("#buildPidTermsCondition()", () => { - const tests = [ - { input: "", method: "", expected: "" }, - { input: "1", method: "equals", expected: "1" }, - { input: "1", method: "", expected: "1" }, - ]; - - tests.forEach((test, index) => { - it(`should return correct condition for test case #${index + 1}`, () => { - component.appConfig.pidSearchMethod = test.method; - const condition = component.buildPidTermsCondition(test.input); - expect(condition).toEqual(test.expected); - }); - }); - }); -}); diff --git a/src/app/shared/modules/filters/pid-filter.component.ts b/src/app/shared/modules/filters/pid-filter.component.ts deleted file mode 100644 index 7d68a00b0..000000000 --- a/src/app/shared/modules/filters/pid-filter.component.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Component, Input, OnDestroy } from "@angular/core"; -import { Store } from "@ngrx/store"; -import { Subject, Subscription } from "rxjs"; -import { - setPidTermsAction, - setPidTermsFilterAction, -} from "state-management/actions/datasets.actions"; -import { debounceTime } from "rxjs/operators"; -import { AppConfigService } from "app-config.service"; -import { ClearableInputComponent } from "./clearable-input.component"; -import { FilterComponentInterface } from "./interface/filter-component.interface"; -import { getFilterLabel } from "./utils"; - -@Component({ - selector: "app-pid-filter", - templateUrl: `./pid-filter.component.html`, - styleUrls: [`./pid-filter.component.scss`], - standalone: false, -}) -export class PidFilterComponent - extends ClearableInputComponent - implements FilterComponentInterface, OnDestroy -{ - private pidSubject = new Subject(); - private subscription: Subscription; - readonly componentName: string = "PidFilter"; - readonly label: string = "Pid Filter"; - readonly tooltipText: string = "Search by dataset's Persistent Identifier"; - - appConfig = this.appConfigService.getConfig(); - - constructor( - public appConfigService: AppConfigService, - private store: Store, - ) { - super(); - - const filters = this.appConfig.labelMaps?.filters; - this.label = getFilterLabel(filters, this.componentName, this.label); - this.subscription = this.pidSubject - .pipe(debounceTime(500)) - .subscribe((pid) => { - const condition = !pid ? "" : this.buildPidTermsCondition(pid); - this.store.dispatch(setPidTermsFilterAction({ pid: condition })); - }); - } - - buildPidTermsCondition(terms: string): string | { $regex: string } { - return terms; - } - - ngOnDestroy() { - // Unsubscribe to avoid memory leaks - this.subscription.unsubscribe(); - this.pidSubject.complete(); - } - - onPidInput(event: any) { - const pid = (event.target as HTMLInputElement).value; - this.pidSubject.next(pid); - } - - @Input() - set clear(value: boolean) { - super.clear = value; - - if (value) this.store.dispatch(setPidTermsAction({ pid: "" })); - } -} diff --git a/src/app/shared/modules/filters/text-filter.component.html b/src/app/shared/modules/filters/text-filter.component.html deleted file mode 100644 index 08d114a82..000000000 --- a/src/app/shared/modules/filters/text-filter.component.html +++ /dev/null @@ -1,12 +0,0 @@ - - {{ label }} - - diff --git a/src/app/shared/modules/filters/text-filter.component.scss b/src/app/shared/modules/filters/text-filter.component.scss deleted file mode 100644 index 9c04bc08b..000000000 --- a/src/app/shared/modules/filters/text-filter.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -.mat-mdc-form-field { - width: 100%; -} diff --git a/src/app/shared/modules/filters/text-filter.component.spec.ts b/src/app/shared/modules/filters/text-filter.component.spec.ts deleted file mode 100644 index 15c0df596..000000000 --- a/src/app/shared/modules/filters/text-filter.component.spec.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { NO_ERRORS_SCHEMA } from "@angular/core"; -import { - ComponentFixture, - TestBed, - inject, - waitForAsync, - fakeAsync, - tick, -} from "@angular/core/testing"; -import { Store, StoreModule } from "@ngrx/store"; -import { MockHttp, MockStore } from "shared/MockStubs"; - -import { FormsModule, ReactiveFormsModule } from "@angular/forms"; -import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; -import { setTextFilterAction } from "state-management/actions/datasets.actions"; -import { SharedScicatFrontendModule } from "shared/shared.module"; -import { MatAutocompleteModule } from "@angular/material/autocomplete"; -import { MatDialogModule } from "@angular/material/dialog"; -import { MatFormFieldModule } from "@angular/material/form-field"; -import { MatInputModule } from "@angular/material/input"; -import { MatSelectModule } from "@angular/material/select"; -import { SearchParametersDialogComponent } from "shared/modules/search-parameters-dialog/search-parameters-dialog.component"; -import { AsyncPipe } from "@angular/common"; -import { MatDatepickerModule } from "@angular/material/datepicker"; -import { MatChipsModule } from "@angular/material/chips"; -import { MatNativeDateModule, MatOptionModule } from "@angular/material/core"; -import { MatCardModule } from "@angular/material/card"; -import { MatButtonModule } from "@angular/material/button"; -import { MatIconModule } from "@angular/material/icon"; -import { TextFilterComponent } from "./text-filter.component"; -import { HttpClient } from "@angular/common/http"; -import { AppConfigService } from "app-config.service"; - -describe("TextFilterComponent", () => { - let component: TextFilterComponent; - let fixture: ComponentFixture; - - let store: MockStore; - let dispatchSpy; - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - schemas: [NO_ERRORS_SCHEMA], - imports: [ - BrowserAnimationsModule, - FormsModule, - MatAutocompleteModule, - MatButtonModule, - MatCardModule, - MatChipsModule, - MatDatepickerModule, - MatDialogModule, - MatFormFieldModule, - MatIconModule, - MatInputModule, - MatOptionModule, - MatSelectModule, - MatNativeDateModule, - ReactiveFormsModule, - SharedScicatFrontendModule, - StoreModule.forRoot( - {}, - { - runtimeChecks: { - strictActionImmutability: false, - strictActionSerializability: false, - strictActionTypeUniqueness: false, - strictActionWithinNgZone: false, - strictStateImmutability: false, - strictStateSerializability: false, - }, - }, - ), - ], - declarations: [TextFilterComponent, SearchParametersDialogComponent], - providers: [ - AsyncPipe, - AppConfigService, - { provide: HttpClient, useClass: MockHttp }, - ], - }); - TestBed.compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(TextFilterComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - beforeEach(inject([Store], (mockStore: MockStore) => { - store = mockStore; - })); - - afterEach(() => { - fixture.destroy(); - }); - - describe("#textSearchChanged()", () => { - it("should dispatch a SetSearchTermsAction", fakeAsync(() => { - dispatchSpy = spyOn(store, "dispatch"); - - const terms = "test"; - const event = { target: { value: terms } }; - component.textSearchChanged(event); - - tick(500); //wait for it - - expect(dispatchSpy).toHaveBeenCalledTimes(1); - expect(dispatchSpy).toHaveBeenCalledWith( - setTextFilterAction({ text: terms }), - ); - })); - }); -}); diff --git a/src/app/shared/modules/filters/text-filter.component.ts b/src/app/shared/modules/filters/text-filter.component.ts deleted file mode 100644 index dda88fd15..000000000 --- a/src/app/shared/modules/filters/text-filter.component.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Component, OnDestroy, OnInit } from "@angular/core"; -import { ClearableInputComponent } from "./clearable-input.component"; -import { Store } from "@ngrx/store"; -import { setTextFilterAction } from "state-management/actions/datasets.actions"; -import { debounceTime, distinctUntilChanged, skipWhile } from "rxjs/operators"; -import { Subject, Subscription } from "rxjs"; -import { AppConfigService } from "app-config.service"; -import { FilterComponentInterface } from "./interface/filter-component.interface"; -import { getFilterLabel } from "./utils"; - -@Component({ - selector: "app-text-filter", - templateUrl: "text-filter.component.html", - styleUrls: ["text-filter.component.scss"], - standalone: false, -}) -export class TextFilterComponent - extends ClearableInputComponent - implements FilterComponentInterface, OnDestroy -{ - private textSubject = new Subject(); - readonly componentName: string = "TextFilter"; - readonly label: string = "Text Filter"; - readonly tooltipText: string = "Search across dataset name and description"; - - appConfig = this.appConfigService.getConfig(); - subscription: Subscription; - - constructor( - private store: Store, - public appConfigService: AppConfigService, - ) { - super(); - - const filters = this.appConfig.labelMaps?.filters; - this.label = getFilterLabel(filters, this.componentName, this.label); - this.subscription = this.textSubject - .pipe( - skipWhile((terms) => terms === ""), - debounceTime(200), - distinctUntilChanged(), - ) - .subscribe((terms) => { - this.store.dispatch(setTextFilterAction({ text: terms })); - }); - } - - textSearchChanged(event: any) { - const pid = (event.target as HTMLInputElement).value; - this.textSubject.next(pid); - } - - ngOnDestroy() { - this.subscription.unsubscribe(); - this.textSubject.complete(); - } -} diff --git a/src/app/shared/modules/filters/type-filter.component.html b/src/app/shared/modules/filters/type-filter.component.html deleted file mode 100644 index 7747d5281..000000000 --- a/src/app/shared/modules/filters/type-filter.component.html +++ /dev/null @@ -1,32 +0,0 @@ - - {{ label }} - - {{ type }}cancel - - - - - - - {{ getFacetId(fc, "No Type") }} | - {{ getFacetCount(fc) }} - - - diff --git a/src/app/shared/modules/filters/type-filter.component.scss b/src/app/shared/modules/filters/type-filter.component.scss deleted file mode 100644 index 9c04bc08b..000000000 --- a/src/app/shared/modules/filters/type-filter.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -.mat-mdc-form-field { - width: 100%; -} diff --git a/src/app/shared/modules/filters/type-filter.component.spec.ts b/src/app/shared/modules/filters/type-filter.component.spec.ts deleted file mode 100644 index c8ded2543..000000000 --- a/src/app/shared/modules/filters/type-filter.component.spec.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { isDevMode, NO_ERRORS_SCHEMA } from "@angular/core"; -import { - ComponentFixture, - TestBed, - inject, - waitForAsync, -} from "@angular/core/testing"; -import { Store, StoreModule } from "@ngrx/store"; -import { MockHttp, MockStore } from "shared/MockStubs"; - -import { FormsModule, ReactiveFormsModule } from "@angular/forms"; -import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; -import { - addTypeFilterAction, - removeTypeFilterAction, -} from "state-management/actions/datasets.actions"; -import { SharedScicatFrontendModule } from "shared/shared.module"; -import { MatAutocompleteModule } from "@angular/material/autocomplete"; -import { MatDialogModule, MatDialog } from "@angular/material/dialog"; -import { MatFormFieldModule } from "@angular/material/form-field"; -import { MatInputModule } from "@angular/material/input"; -import { MatSelectModule } from "@angular/material/select"; -import { SearchParametersDialogComponent } from "shared/modules/search-parameters-dialog/search-parameters-dialog.component"; -import { AsyncPipe } from "@angular/common"; -import { MatDatepickerModule } from "@angular/material/datepicker"; -import { MatChipsModule } from "@angular/material/chips"; -import { MatNativeDateModule, MatOptionModule } from "@angular/material/core"; -import { MatCardModule } from "@angular/material/card"; -import { MatButtonModule } from "@angular/material/button"; -import { MatIconModule } from "@angular/material/icon"; -import { TypeFilterComponent } from "./type-filter.component"; -import { HttpClient } from "@angular/common/http"; -import { AppConfigService } from "app-config.service"; - -describe("TypeFilterComponent", () => { - let component: TypeFilterComponent; - let fixture: ComponentFixture; - - let store: MockStore; - let dispatchSpy; - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - schemas: [NO_ERRORS_SCHEMA], - imports: [ - BrowserAnimationsModule, - FormsModule, - MatAutocompleteModule, - MatButtonModule, - MatCardModule, - MatChipsModule, - MatDatepickerModule, - MatDialogModule, - MatFormFieldModule, - MatIconModule, - MatInputModule, - MatOptionModule, - MatSelectModule, - MatNativeDateModule, - ReactiveFormsModule, - SharedScicatFrontendModule, - StoreModule.forRoot( - {}, - { - runtimeChecks: { - strictActionImmutability: false, - strictActionSerializability: false, - strictActionTypeUniqueness: false, - strictActionWithinNgZone: false, - strictStateImmutability: false, - strictStateSerializability: false, - }, - }, - ), - ], - declarations: [TypeFilterComponent, SearchParametersDialogComponent], - providers: [ - AsyncPipe, - AppConfigService, - { provide: HttpClient, useClass: MockHttp }, - ], - }); - TestBed.compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(TypeFilterComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - beforeEach(inject([Store], (mockStore: MockStore) => { - store = mockStore; - })); - - afterEach(() => { - fixture.destroy(); - }); - - describe("#onTypeInput()", () => { - it("should call next on typeInput$", () => { - const nextSpy = spyOn(component.typeInput$, "next"); - - const event = { - target: { - value: "type", - }, - }; - - component.onTypeInput(event); - - expect(nextSpy).toHaveBeenCalledOnceWith(event.target.value); - }); - }); - - describe("#typeSelected()", () => { - it("should dispatch an AddTypeFilterAction", () => { - dispatchSpy = spyOn(store, "dispatch"); - - const datasetType = "string"; - component.typeSelected(datasetType); - - expect(dispatchSpy).toHaveBeenCalledTimes(1); - expect(dispatchSpy).toHaveBeenCalledWith( - addTypeFilterAction({ datasetType }), - ); - }); - }); - - describe("#typeRemoved()", () => { - it("should dispatch a RemoveTypeFilterAction", () => { - dispatchSpy = spyOn(store, "dispatch"); - - const datasetType = "string"; - component.typeRemoved(datasetType); - - expect(dispatchSpy).toHaveBeenCalledTimes(1); - expect(dispatchSpy).toHaveBeenCalledWith( - removeTypeFilterAction({ datasetType }), - ); - }); - }); -}); diff --git a/src/app/shared/modules/filters/type-filter.component.ts b/src/app/shared/modules/filters/type-filter.component.ts deleted file mode 100644 index ce5e948f1..000000000 --- a/src/app/shared/modules/filters/type-filter.component.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { Component } from "@angular/core"; -import { ClearableInputComponent } from "./clearable-input.component"; -import { - createSuggestionObserver, - getFacetCount, - getFacetId, - getFilterLabel, -} from "./utils"; -import { - selectTypeFacetCounts, - selectTypeFilter, -} from "state-management/selectors/datasets.selectors"; -import { Store } from "@ngrx/store"; -import { BehaviorSubject } from "rxjs"; -import { - addTypeFilterAction, - removeTypeFilterAction, -} from "state-management/actions/datasets.actions"; -import { AppConfigService } from "app-config.service"; -import { FilterComponentInterface } from "./interface/filter-component.interface"; - -@Component({ - selector: "app-type-filter", - templateUrl: "type-filter.component.html", - styleUrls: ["type-filter.component.scss"], - standalone: false, -}) -export class TypeFilterComponent - extends ClearableInputComponent - implements FilterComponentInterface -{ - protected readonly getFacetCount = getFacetCount; - protected readonly getFacetId = getFacetId; - readonly componentName: string = "TypeFilter"; - readonly label: string = "Type Filter"; - readonly tooltipText: string = "Filters datasets by type"; - - appConfig = this.appConfigService.getConfig(); - - typeFacetCounts$ = this.store.select(selectTypeFacetCounts); - - typeFilter$ = this.store.select(selectTypeFilter); - typeInput$ = new BehaviorSubject(""); - - typeSuggestions$ = createSuggestionObserver( - this.typeFacetCounts$, - this.typeInput$, - this.typeFilter$, - ); - - constructor( - private store: Store, - public appConfigService: AppConfigService, - ) { - super(); - - const filters = this.appConfig.labelMaps?.filters; - this.label = getFilterLabel(filters, this.componentName, this.label); - } - onTypeInput(event: any) { - const value = (event.target).value; - this.typeInput$.next(value); - } - - typeSelected(type: string) { - this.store.dispatch(addTypeFilterAction({ datasetType: type })); - this.typeInput$.next(""); - } - - typeRemoved(type: string) { - this.store.dispatch(removeTypeFilterAction({ datasetType: type })); - } -} diff --git a/src/app/shared/modules/filters/utils.ts b/src/app/shared/modules/filters/utils.ts index 92bbeefb4..4fd12c63d 100644 --- a/src/app/shared/modules/filters/utils.ts +++ b/src/app/shared/modules/filters/utils.ts @@ -30,15 +30,3 @@ export function getFacetId(facetCount: FacetCount, fallback = ""): string { export function getFacetCount(facetCount: FacetCount): number { return facetCount.count; } - -export function getFilterLabel( - filters: Record | undefined, - componentName: string, - defaultLabel: string, -): string { - if (!filters || !componentName || !filters[componentName]) { - return defaultLabel; - } - - return filters[componentName]; -} diff --git a/src/app/shared/modules/numeric-range/container/numeric-range-form-field-container.component.html b/src/app/shared/modules/numeric-range/container/numeric-range-form-field-container.component.html new file mode 100644 index 000000000..80585d6b7 --- /dev/null +++ b/src/app/shared/modules/numeric-range/container/numeric-range-form-field-container.component.html @@ -0,0 +1,62 @@ + + {{ label }} + + + close + + + + {{ requiredErrorMessage }} + + + + {{ minErrorMessage }} + + + + {{ maxErrorMessage }} + + + + {{ invalidRangeErrorMessage }} + + diff --git a/src/app/shared/modules/numeric-range/container/numeric-range-form-field-container.component.scss b/src/app/shared/modules/numeric-range/container/numeric-range-form-field-container.component.scss new file mode 100644 index 000000000..8b6787cbd --- /dev/null +++ b/src/app/shared/modules/numeric-range/container/numeric-range-form-field-container.component.scss @@ -0,0 +1,13 @@ +:host { + .numeric-range-field { + width: 100%; + } + + mat-icon { + cursor: context-menu; + } + + .pointer { + cursor: pointer; + } +} diff --git a/src/app/shared/modules/numeric-range/container/numeric-range-form-field-container.component.spec.ts b/src/app/shared/modules/numeric-range/container/numeric-range-form-field-container.component.spec.ts new file mode 100644 index 000000000..5a25ee8d7 --- /dev/null +++ b/src/app/shared/modules/numeric-range/container/numeric-range-form-field-container.component.spec.ts @@ -0,0 +1,255 @@ +import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { + FormControl, + FormControlDirective, + FormGroup, + NgControl, + ReactiveFormsModule, + Validators, +} from "@angular/forms"; +import { By } from "@angular/platform-browser"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { NumericRangeFormFieldControlComponent } from "../control/numeric-range-form-field-control.component"; +import { INumericRange } from "../form/model/numeric-range-field.model"; +import { NumericRangeFormService } from "../form/numeric-range-form.service"; +import { NumericRangeFormFieldContainerComponent } from "./numeric-range-form-field-container.component"; + +@Component({ + template: ` + + + `, + standalone: false, +}) +class HostComponent { + form: FormGroup; + + constructor() { + this.form = new FormGroup({ + numericRange: new FormControl({ min: 0, max: 10 }, [ + Validators.min(0), + Validators.max(10), + ]), + }); + } + + get numericRangeControl() { + return this.form.get("numericRange") as FormControl; + } + + onRangeBlur(): void { + return; + } + + onNumericRangeChanged(value: INumericRange): void { + return; + } + + onNumericRangeEnterPressed(): void { + return; + } + + disableRange(disable: boolean): void { + if (disable) { + this.numericRangeControl.disable(); + } else { + this.numericRangeControl.enable(); + } + } +} + +describe("NumericRangeFormFieldContainerComponent", () => { + let component: HostComponent; + let fixture: ComponentFixture; + let service: NumericRangeFormService; + + function getMinRangeField(): any { + return fixture.debugElement + .query(By.directive(NumericRangeFormFieldContainerComponent)) + .queryAll(By.css("input"))[0].nativeElement; + } + + function getMaxRangeField(): any { + return fixture.debugElement + .query(By.directive(NumericRangeFormFieldContainerComponent)) + .queryAll(By.css("input"))[1].nativeElement; + } + + function getNumericRangeComponent(): NumericRangeFormFieldContainerComponent { + return fixture.debugElement.query( + By.directive(NumericRangeFormFieldContainerComponent), + ).componentInstance; + } + + function getNumericRangeControlComponent(): NumericRangeFormFieldControlComponent { + return fixture.debugElement.query( + By.directive(NumericRangeFormFieldControlComponent), + ).componentInstance; + } + + beforeEach(async () => { + TestBed.overrideComponent(NumericRangeFormFieldContainerComponent, { + set: { + providers: [ + { + provide: NgControl, + useValue: new FormControlDirective([], [], null, null), + }, + NumericRangeFormService, + ], + }, + }); + + await TestBed.configureTestingModule({ + declarations: [ + HostComponent, + NumericRangeFormFieldContainerComponent, + NumericRangeFormFieldControlComponent, + ], + imports: [ReactiveFormsModule, BrowserAnimationsModule], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(HostComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + service = fixture.debugElement + .query(By.directive(NumericRangeFormFieldContainerComponent)) + .injector.get(NumericRangeFormService); + }); + + afterEach(() => { + fixture.destroy(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + expect(getMinRangeField()).toBeTruthy(); + expect(getMaxRangeField()).toBeTruthy(); + }); + + it("should emit on enter pressed and not emit on errors", () => { + const enterSpy = spyOn( + component, + "onNumericRangeEnterPressed", + ).and.callThrough(); + + const rangeField = getMinRangeField(); + rangeField.dispatchEvent(new KeyboardEvent("keyup", { key: "enter" })); + + expect(enterSpy).toHaveBeenCalledTimes(1); + + component.form.get("numericRange").setValue({ min: 10, max: 9 }); + + rangeField.dispatchEvent(new KeyboardEvent("keyup", { key: "enter" })); + + expect(enterSpy).not.toHaveBeenCalledTimes(2); + }); + + it("should emit on blur event", () => { + const blurSpy = spyOn(component, "onRangeBlur").and.callThrough(); + getMinRangeField().dispatchEvent(new Event("blur")); + expect(blurSpy).toHaveBeenCalled(); + }); + + it("should emit on changed date value", () => { + const rangeChangeSpy = spyOn( + component, + "onNumericRangeChanged", + ).and.callThrough(); + + const numericRangeControlComponent = getNumericRangeControlComponent(); + + numericRangeControlComponent.minControl.setValue(8); + numericRangeControlComponent.minControl.updateValueAndValidity(); + + numericRangeControlComponent.onRangeValuesChanged(); + + expect(rangeChangeSpy).toHaveBeenCalledOnceWith({ + min: 8, + max: 10, + }); + expect(numericRangeControlComponent.errorState).toBeFalse(); + }); + + it("should emit null if error happens on changed range values", () => { + const rangeChangeSpy = spyOn( + component, + "onNumericRangeChanged", + ).and.callThrough(); + + const numericRangeControlComponent = getNumericRangeControlComponent(); + + numericRangeControlComponent.minControl.setValue(8); + numericRangeControlComponent.maxControl.setValue(6); + numericRangeControlComponent.formGroup.updateValueAndValidity(); + + numericRangeControlComponent.onRangeValuesChanged(); + + expect(rangeChangeSpy).toHaveBeenCalledWith(null); + expect(numericRangeControlComponent.formGroup.errors).toEqual({ + notValidRange: true, + }); + }); + + it("should reset value", () => { + expect(component.form.value).toEqual({ + numericRange: { min: 0, max: 10 }, + }); + + const resetIcon = fixture.debugElement + .query(By.directive(NumericRangeFormFieldContainerComponent)) + .query(By.css("mat-icon")).nativeElement; + resetIcon.click(); + + expect(component.form.value).toEqual({ + numericRange: { + min: null, + max: null, + }, + }); + }); + + it("should change disabled state of date range component", () => { + const numericRangeComponent = getNumericRangeComponent(); + + component.disableRange(true); + + expect(component.numericRangeControl.disabled).toBeTrue(); + expect(numericRangeComponent.formGroup.disabled).toBeTrue(); + + component.disableRange(false); + + expect(component.numericRangeControl.disabled).toBeFalse(); + expect(numericRangeComponent.formGroup.disabled).toBeFalse(); + }); + + it("should have valid error state of the form", () => { + const numericRangeControlComponent = getNumericRangeControlComponent(); + + expect(numericRangeControlComponent.errorState).toBeFalse(); + }); + + it("should have invalid error state of the form", () => { + const numericRangeControlComponent = getNumericRangeControlComponent(); + + numericRangeControlComponent.minControl.setValue(8); + numericRangeControlComponent.maxControl.setValue(6); + numericRangeControlComponent.minControl.markAsTouched(); + numericRangeControlComponent.minControl.markAsDirty(); + numericRangeControlComponent.maxControl.markAsDirty(); + numericRangeControlComponent.formGroup.updateValueAndValidity(); + + expect(numericRangeControlComponent.errorState).toBeTrue(); + }); +}); diff --git a/src/app/shared/modules/numeric-range/container/numeric-range-form-field-container.component.ts b/src/app/shared/modules/numeric-range/container/numeric-range-form-field-container.component.ts new file mode 100644 index 000000000..fb4b73e1e --- /dev/null +++ b/src/app/shared/modules/numeric-range/container/numeric-range-form-field-container.component.ts @@ -0,0 +1,190 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + EventEmitter, + Host, + Input, + OnChanges, + OnDestroy, + OnInit, + Output, + Self, + SimpleChanges, +} from "@angular/core"; +import { + AbstractControl, + AsyncValidatorFn, + ControlValueAccessor, + FormControl, + NgControl, + ValidationErrors, + Validator, + ValidatorFn, +} from "@angular/forms"; +import { + FloatLabelType, + MatFormFieldAppearance, +} from "@angular/material/form-field"; +import { Subject } from "rxjs"; +import { takeUntil } from "rxjs/operators"; +import { + INumericRange, + NumericRangeFormGroup, +} from "../form/model/numeric-range-field.model"; +import { NumericRangeFormService } from "../form/numeric-range-form.service"; + +@Component({ + selector: "ngx-numeric-range-form-field", + templateUrl: "./numeric-range-form-field-container.component.html", + styleUrls: ["./numeric-range-form-field-container.component.scss"], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [NumericRangeFormService], + standalone: false, +}) +export class NumericRangeFormFieldContainerComponent + implements OnChanges, OnInit, OnDestroy, ControlValueAccessor, Validator +{ + private unsubscribe$ = new Subject(); + + @Input() label: string; + @Input() key: string; + @Input() appearance: MatFormFieldAppearance = "fill"; + @Input() floatLabel: FloatLabelType = "always"; + + @Input() minPlaceholder = "From"; + @Input() maxPlaceholder = "To"; + + @Input() readonly = false; + @Input() minReadonly = false; + @Input() maxReadonly = false; + + @Input() resettable = true; + + @Input() required: boolean; + @Input() requiredErrorMessage = "Field is required!"; + + @Input() minErrorMessage = "min has been reached!"; + @Input() maxErrorMessage = "max has exceeded!"; + @Input() invalidRangeErrorMessage = "Inserted range is not valid!"; + @Input() dynamicSyncValidators: ValidatorFn | ValidatorFn[]; + + @Output() blurred = new EventEmitter(); + @Output() enterPressed = new EventEmitter(); + @Output() numericRangeChanged = new EventEmitter(); + + formGroup: NumericRangeFormGroup = this.formService.formGroup; + control = new FormControl(); + + constructor( + @Self() private controlDirective: NgControl, + @Host() private formService: NumericRangeFormService, + private changeDetectorRef: ChangeDetectorRef, + ) { + this.controlDirective.valueAccessor = this; + } + + private setSyncValidator(validator: ValidatorFn): void { + if (!validator) { + return; + } + + this.control.addValidators(validator); // sets the validators from parent control + this.control.updateValueAndValidity(); + } + + private setAsyncValidator(asyncValidator: AsyncValidatorFn): void { + if (!asyncValidator) { + return; + } + + this.control.addAsyncValidators(asyncValidator); + this.control.updateValueAndValidity(); + } + + onTouched = () => {}; + + get minControl(): FormControl { + return this.formService.minControl; + } + + get maxControl(): FormControl { + return this.formService.maxControl; + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes.dynamicSyncValidators) { + this.control.setErrors(null); + this.control.setValidators(this.dynamicSyncValidators); + this.control.updateValueAndValidity({ emitEvent: false }); + } + } + + ngOnInit(): void { + this.setSyncValidator(this.controlDirective.control.validator); + this.setAsyncValidator(this.controlDirective.control.asyncValidator); + + this.controlDirective.control.setValidators([this.validate.bind(this)]); // overrides the parent control validators by sending out errors from validate() + this.controlDirective.control.updateValueAndValidity({ emitEvent: false }); + + this.changeDetectorRef.detectChanges(); + } + + ngOnDestroy(): void { + this.unsubscribe$.next(); + this.unsubscribe$.complete(); + } + + writeValue(value: INumericRange): void { + if (value === null) { + this.control.reset(); + } else { + this.control.setValue(value, { + emitEvent: false, + }); + } + } + + registerOnChange(fn: any): void { + this.control.valueChanges.pipe(takeUntil(this.unsubscribe$)).subscribe(fn); + } + + registerOnTouched(fn: any): void { + this.onTouched = fn; + } + + setDisabledState(isDisabled: boolean): void { + if (isDisabled) { + this.control.disable(); + } else { + this.control.enable(); + } + } + + validate(control: AbstractControl): ValidationErrors | null { + const errors = { + ...this.minControl.errors, + ...this.maxControl.errors, + }; + + return Object.keys(errors).length ? errors : null; + } + + onEnterPressed(): void { + this.enterPressed.emit(); + } + + onBlur(): void { + this.onTouched(); + this.blurred.emit(); + } + + onRangeValuesChanged(value: INumericRange): void { + this.numericRangeChanged.emit(value); + } + + onReset(): void { + this.formGroup.reset(); + this.numericRangeChanged.emit({ min: null, max: null }); + } +} diff --git a/src/app/shared/modules/numeric-range/control/numeric-range-form-field-control.component.html b/src/app/shared/modules/numeric-range/control/numeric-range-form-field-control.component.html new file mode 100644 index 000000000..42ad728c5 --- /dev/null +++ b/src/app/shared/modules/numeric-range/control/numeric-range-form-field-control.component.html @@ -0,0 +1,21 @@ + + + diff --git a/src/app/shared/modules/numeric-range/control/numeric-range-form-field-control.component.scss b/src/app/shared/modules/numeric-range/control/numeric-range-form-field-control.component.scss new file mode 100644 index 000000000..282d96f14 --- /dev/null +++ b/src/app/shared/modules/numeric-range/control/numeric-range-form-field-control.component.scss @@ -0,0 +1,20 @@ +:host { + display: flex; + align-items: center; + + .spacer { + padding: 0 10px 0 0; + cursor: context-menu; + } + + input::-webkit-outer-spin-button, + input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + + input:read-only { + color: rgba($color: #000000, $alpha: 0.5); + cursor: initial; + } +} diff --git a/src/app/shared/modules/numeric-range/control/numeric-range-form-field-control.component.ts b/src/app/shared/modules/numeric-range/control/numeric-range-form-field-control.component.ts new file mode 100644 index 000000000..bbdc4036d --- /dev/null +++ b/src/app/shared/modules/numeric-range/control/numeric-range-form-field-control.component.ts @@ -0,0 +1,272 @@ +import { + ChangeDetectionStrategy, + Component, + DoCheck, + EventEmitter, + HostBinding, + Input, + OnChanges, + OnDestroy, + OnInit, + Output, + Self, + SimpleChanges, + SkipSelf, +} from "@angular/core"; +import { + AbstractControl, + ControlValueAccessor, + FormControl, + NgControl, + Validator, + ValidatorFn, +} from "@angular/forms"; +import { ErrorStateMatcher } from "@angular/material/core"; +import { MatFormFieldControl } from "@angular/material/form-field"; +import { Subject } from "rxjs"; +import { takeUntil } from "rxjs/operators"; +import { + INumericRange, + NumericRangeFormGroup, +} from "../form/model/numeric-range-field.model"; +import { NumericRangeFormService } from "../form/numeric-range-form.service"; +import { NumericRangeStateMatcher } from "../form/numeric-range-state-matcher"; + +@Component({ + selector: "ngx-numeric-range-form-field-control", + templateUrl: "./numeric-range-form-field-control.component.html", + styleUrls: ["./numeric-range-form-field-control.component.scss"], + providers: [ + { + provide: MatFormFieldControl, + useExisting: NumericRangeFormFieldControlComponent, + }, + { + provide: ErrorStateMatcher, + useClass: NumericRangeStateMatcher, + }, + ], + standalone: false, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class NumericRangeFormFieldControlComponent + implements + OnChanges, + OnInit, + DoCheck, + OnDestroy, + MatFormFieldControl, + ControlValueAccessor, + Validator +{ + static nextId = 0; + private unsubscribe$ = new Subject(); + private _placeholder: string; + + @Input() minPlaceholder: string; + @Input() maxPlaceholder: string; + @Input() readonly = false; + @Input() minReadonly = false; + @Input() maxReadonly = false; + @Input() required: boolean; + @Input() disabled: boolean; + @Input() errorStateMatcher: ErrorStateMatcher; + @Input() autofilled?: boolean; + @Input() dynamicSyncValidators: ValidatorFn | ValidatorFn[]; + + @Output() blurred = new EventEmitter(); + @Output() enterPressed = new EventEmitter(); + @Output() numericRangeChanged = new EventEmitter(); + + formGroup: NumericRangeFormGroup = this.formService.formGroup; + + stateChanges = new Subject(); + + focused = false; + + controlType = "numeric-range-form-control"; + + numericRangeErrorMatcher = new NumericRangeStateMatcher(); + + @HostBinding("attr.aria-describedby") + userAriaDescribedBy = ""; + + @HostBinding() + id = + `numeric-range-form-control-id-${NumericRangeFormFieldControlComponent.nextId++}`; + + constructor( + @Self() public ngControl: NgControl, + @SkipSelf() private formService: NumericRangeFormService, + ) { + this.ngControl.valueAccessor = this; + } + + get value() { + return this.formGroup.getRawValue(); + } + + @Input() + set value(value: INumericRange) { + this.formGroup.patchValue(value); + this.stateChanges.next(); + } + + get placeholder(): string { + return this._placeholder; + } + + @Input() set placeholder(value: string) { + this._placeholder = value; + this.stateChanges.next(); + } + + @HostBinding("class.floated") + get shouldLabelFloat(): boolean { + return true; + } + + get empty(): boolean { + return !this.value.min && !this.value.max; + } + + get errorState() { + return this.numericRangeErrorMatcher.isErrorState( + this.ngControl.control as FormControl, + this.formGroup, + ); + } + + get minControl(): FormControl { + return this.formService.minControl; + } + + get maxControl(): FormControl { + return this.formService.maxControl; + } + + onTouched = () => {}; + + ngOnChanges(changes: SimpleChanges): void { + if (changes.dynamicSyncValidators) { + this.formService.setDynamicValidators(this.dynamicSyncValidators); + } + } + + ngOnInit(): void { + this.formService.setSyncValidators(this.ngControl.control.validator); + this.formService.setAsyncValidators(this.ngControl.control.asyncValidator); + + this.ngControl.control.setValidators([this.validate.bind(this)]); + this.ngControl.control.updateValueAndValidity({ emitEvent: false }); + } + + ngDoCheck(): void { + this.formGroup.markAllAsTouched(); + } + + ngOnDestroy(): void { + this.stateChanges.complete(); + this.unsubscribe$.next(); + this.unsubscribe$.complete(); + } + + writeValue(value: INumericRange): void { + if (value === null) { + this.formGroup.reset(); + } else { + this.formGroup.setValue(value, { emitEvent: false }); + } + } + + registerOnChange(fn: any): void { + this.formGroup.valueChanges + .pipe(takeUntil(this.unsubscribe$)) + .subscribe(fn); + } + + registerOnTouched(fn: any): void { + this.onTouched = fn; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + + if (isDisabled) { + this.formGroup.disable(); + } else { + this.formGroup.enable(); + } + + this.stateChanges.next(); + } + + setDescribedByIds(ids: string[]): void { + this.userAriaDescribedBy = ids.join(" "); + } + + onContainerClick(event: MouseEvent): void {} + + validate(control: AbstractControl) { + return control.errors; + } + + onEnterPressed(): void { + if ( + !this.formGroup.errors && + !this.minControl.errors && + !this.maxControl.errors + ) { + this.enterPressed.emit(); + } + } + + onBlur(): void { + this.onTouched(); + this.blurred.emit(); + } + + onRangeValuesChanged(): void { + if ( + this.formGroup.errors || + this.minControl.errors || + this.maxControl.errors + ) { + this.numericRangeChanged.emit(null); + } else { + this.numericRangeChanged.emit(this.formGroup.getRawValue()); + } + } + + onMinValuesChanged(event: Event): void { + const value = (event.target as HTMLInputElement).valueAsNumber; + if ( + this.formGroup.errors || + this.minControl.errors || + this.maxControl.errors + ) { + this.numericRangeChanged.emit(null); + } else { + this.numericRangeChanged.emit({ + min: value, + max: this.formGroup.get("max")?.value, + }); + } + } + + onMaxValuesChanged(event: Event): void { + const value = (event.target as HTMLInputElement).valueAsNumber; + if ( + this.formGroup.errors || + this.minControl.errors || + this.maxControl.errors + ) { + this.numericRangeChanged.emit(null); + } else { + this.numericRangeChanged.emit({ + min: this.formGroup.get("min")?.value, + max: value, + }); + } + } +} diff --git a/src/app/shared/modules/numeric-range/form/model/numeric-range-field.model.ts b/src/app/shared/modules/numeric-range/form/model/numeric-range-field.model.ts new file mode 100644 index 000000000..3e2f7e3b2 --- /dev/null +++ b/src/app/shared/modules/numeric-range/form/model/numeric-range-field.model.ts @@ -0,0 +1,12 @@ +import { FormControl, FormGroup } from "@angular/forms"; + +export type INumericRange = { + min: number; + max: number; +}; + +type ControlsOf> = { + [K in keyof T]: FormControl; +}; + +export type NumericRangeFormGroup = FormGroup>; diff --git a/src/app/shared/modules/numeric-range/form/numeric-range-form.service.spec.ts b/src/app/shared/modules/numeric-range/form/numeric-range-form.service.spec.ts new file mode 100644 index 000000000..c4ef798a4 --- /dev/null +++ b/src/app/shared/modules/numeric-range/form/numeric-range-form.service.spec.ts @@ -0,0 +1,20 @@ +import { TestBed } from "@angular/core/testing"; +import { ReactiveFormsModule } from "@angular/forms"; + +import { NumericRangeFormService } from "./numeric-range-form.service"; + +describe("NumericRangeFormService", () => { + let service: NumericRangeFormService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ReactiveFormsModule], + providers: [NumericRangeFormService], + }); + service = TestBed.inject(NumericRangeFormService); + }); + + it("should be created", () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/shared/modules/numeric-range/form/numeric-range-form.service.ts b/src/app/shared/modules/numeric-range/form/numeric-range-form.service.ts new file mode 100644 index 000000000..87f478874 --- /dev/null +++ b/src/app/shared/modules/numeric-range/form/numeric-range-form.service.ts @@ -0,0 +1,71 @@ +import { Injectable } from "@angular/core"; +import { + AsyncValidatorFn, + FormControl, + FormGroup, + ValidatorFn, +} from "@angular/forms"; +import { NumericRangeFormGroup } from "./model/numeric-range-field.model"; +import { numericRangeValues } from "./numeric-range.validator"; + +@Injectable() +export class NumericRangeFormService { + private form: NumericRangeFormGroup; + + constructor() { + this.form = new FormGroup( + { + min: new FormControl(null, { updateOn: "blur" }), + max: new FormControl(null, { updateOn: "blur" }), + }, + { validators: numericRangeValues }, + ); + } + + get minControl(): FormControl { + return this.form.get("min") as FormControl; + } + + get maxControl(): FormControl { + return this.form.get("max") as FormControl; + } + + get formGroup(): NumericRangeFormGroup { + return this.form; + } + + setDynamicValidators(validators: ValidatorFn | ValidatorFn[]): void { + if (!validators) { + return; + } + + this.minControl.setErrors(null); + this.maxControl.setErrors(null); + + this.minControl.setValidators(validators); // sets the validators on child control + this.maxControl.setValidators(validators); // sets the validators on child control + + this.minControl.updateValueAndValidity({ emitEvent: false }); + this.maxControl.updateValueAndValidity({ emitEvent: false }); + } + + setSyncValidators(validator: ValidatorFn): void { + if (!validator) { + return; + } + + this.minControl.addValidators(validator); // sets the validators on child control + this.maxControl.addValidators(validator); // sets the validators on child control + this.formGroup.updateValueAndValidity(); + } + + setAsyncValidators(asyncValidator: AsyncValidatorFn): void { + if (!asyncValidator) { + return; + } + + this.minControl.addAsyncValidators(asyncValidator); + this.maxControl.addAsyncValidators(asyncValidator); + this.formGroup.updateValueAndValidity(); + } +} diff --git a/src/app/shared/modules/numeric-range/form/numeric-range-state-matcher.ts b/src/app/shared/modules/numeric-range/form/numeric-range-state-matcher.ts new file mode 100644 index 000000000..91534cc4c --- /dev/null +++ b/src/app/shared/modules/numeric-range/form/numeric-range-state-matcher.ts @@ -0,0 +1,33 @@ +import { + FormControl, + FormGroup, + FormGroupDirective, + NgForm, +} from "@angular/forms"; +import { ErrorStateMatcher } from "@angular/material/core"; + +export class NumericRangeStateMatcher implements ErrorStateMatcher { + private isControlTouchedInvalid(control: FormControl): boolean { + return control.touched && control.invalid; + } + + isErrorState( + control: FormControl | null, + form: FormGroup | FormGroupDirective | NgForm | null, + ): boolean { + if (!control.parent && form instanceof FormGroup) { + const minControl = form.get("min") as FormControl; + const maxControl = form.get("max") as FormControl; + + const isFormInvalid = form.touched && form.invalid; + + const areFormControlsInvalid = + this.isControlTouchedInvalid(minControl) || + this.isControlTouchedInvalid(maxControl); + + return isFormInvalid || areFormControlsInvalid; + } + + return control.touched && control.invalid; + } +} diff --git a/src/app/shared/modules/numeric-range/form/numeric-range.validator.ts b/src/app/shared/modules/numeric-range/form/numeric-range.validator.ts new file mode 100644 index 000000000..a6d50c1c2 --- /dev/null +++ b/src/app/shared/modules/numeric-range/form/numeric-range.validator.ts @@ -0,0 +1,15 @@ +import { AbstractControl, ValidationErrors, ValidatorFn } from "@angular/forms"; + +export const numericRangeValues: ValidatorFn = ( + group: AbstractControl, +): ValidationErrors | null => { + const max = group.get("max").value ? Number(group.get("max").value) : null; + const min = group.get("min").value ? Number(group.get("min").value) : null; + + if (max !== null && min !== null) { + if (max < min) { + return { notValidRange: true }; + } + } + return null; +}; diff --git a/src/app/shared/modules/numeric-range/ngx-numeric-range-form-field.module.ts b/src/app/shared/modules/numeric-range/ngx-numeric-range-form-field.module.ts new file mode 100644 index 000000000..f2bd851a4 --- /dev/null +++ b/src/app/shared/modules/numeric-range/ngx-numeric-range-form-field.module.ts @@ -0,0 +1,24 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { ReactiveFormsModule } from "@angular/forms"; +import { MatFormFieldModule } from "@angular/material/form-field"; +import { MatIconModule } from "@angular/material/icon"; +import { MatInputModule } from "@angular/material/input"; +import { NumericRangeFormFieldContainerComponent } from "./container/numeric-range-form-field-container.component"; +import { NumericRangeFormFieldControlComponent } from "./control/numeric-range-form-field-control.component"; + +@NgModule({ + declarations: [ + NumericRangeFormFieldContainerComponent, + NumericRangeFormFieldControlComponent, + ], + imports: [ + CommonModule, + ReactiveFormsModule, + MatFormFieldModule, + MatInputModule, + MatIconModule, + ], + exports: [NumericRangeFormFieldContainerComponent], +}) +export class NgxNumericRangeFormFieldModule {} diff --git a/src/app/shared/modules/shared-filter/shared-filter.component.html b/src/app/shared/modules/shared-filter/shared-filter.component.html index ebaf0fec7..3ef0e20df 100644 --- a/src/app/shared/modules/shared-filter/shared-filter.component.html +++ b/src/app/shared/modules/shared-filter/shared-filter.component.html @@ -2,7 +2,7 @@
- + {{ label }} - + {{ label }} + + + + + + diff --git a/src/app/shared/modules/shared-filter/shared-filter.component.spec.ts b/src/app/shared/modules/shared-filter/shared-filter.component.spec.ts index 58937f758..122ada858 100644 --- a/src/app/shared/modules/shared-filter/shared-filter.component.spec.ts +++ b/src/app/shared/modules/shared-filter/shared-filter.component.spec.ts @@ -54,6 +54,8 @@ describe("SharedFilterComponent", () => { component.filterForm.setValue({ textField: "test", dateRangeField: { start: new Date(), end: new Date() }, + multiSelectField: [], + numberRange: { min: null, max: null }, }); component.clear = true; const text = component.filterForm.get("textField")!.value; diff --git a/src/app/shared/modules/shared-filter/shared-filter.component.ts b/src/app/shared/modules/shared-filter/shared-filter.component.ts index 007ac82b2..de9b5f039 100644 --- a/src/app/shared/modules/shared-filter/shared-filter.component.ts +++ b/src/app/shared/modules/shared-filter/shared-filter.component.ts @@ -13,6 +13,10 @@ import { MatDatepickerInputEvent } from "@angular/material/datepicker"; import { DateTime } from "luxon"; import { Observable } from "rxjs"; import { DateRange } from "state-management/state/proposals.store"; +import { MultiSelectFilterValue } from "../filters/multiselect-filter.component"; +import { FacetCount } from "state-management/state/datasets.store"; +import { INumericRange } from "../numeric-range/form/model/numeric-range-field.model"; +import { FilterType } from "state-management/state/user.store"; @Component({ selector: "shared-filter", @@ -32,17 +36,20 @@ export class SharedFilterComponent implements OnChanges { start: new FormControl(null), end: new FormControl(null), }), + multiSelectField: new FormControl([]), + numberRange: new FormControl({ min: null, max: null }), }); @ViewChild("input", { static: true }) input!: ElementRef; + @Input() key = ""; @Input() label = "Filter"; @Input() tooltip = ""; - @Input() facetCounts$!: Observable<{ key: string; count: number }[]>; + @Input() facetCounts$!: Observable; @Input() currentFilter$!: Observable; @Input() dispatchAction!: () => void; - @Input() filterType: "text" | "dateRange"; - @Input() prefilled: string | DateRange = undefined; + @Input() filterType: FilterType; + @Input() prefilled: string | DateRange | string[] | INumericRange = undefined; @Input() set clear(value: boolean) { if (value) { @@ -54,6 +61,8 @@ export class SharedFilterComponent implements OnChanges { } @Output() textChange = new EventEmitter(); + @Output() selectionChange = new EventEmitter(); + @Output() numericRangeChange = new EventEmitter(); @Output() dateRangeChange = new EventEmitter<{ begin: string; end: string; @@ -67,6 +76,12 @@ export class SharedFilterComponent implements OnChanges { this.filterForm .get("textField")! .setValue((this.prefilled as string) || ""); + } else if (this.filterType === "number") { + const range = this.prefilled as unknown as INumericRange; + this.filterForm.get("numberRange")!.setValue({ + min: range?.min ?? null, + max: range?.max ?? null, + }); } else { const range = (this.prefilled as DateRange) || { begin: null, @@ -94,4 +109,12 @@ export class SharedFilterComponent implements OnChanges { this.dateRangeChange.emit(this.dateRange); } + + onSelectionChange(value: MultiSelectFilterValue) { + this.selectionChange.emit(value); + } + + onNumericRangeChange(value: INumericRange) { + this.numericRangeChange.emit(value); + } } diff --git a/src/app/shared/modules/shared-filter/shared-filter.module.ts b/src/app/shared/modules/shared-filter/shared-filter.module.ts index 347132415..421dfefea 100644 --- a/src/app/shared/modules/shared-filter/shared-filter.module.ts +++ b/src/app/shared/modules/shared-filter/shared-filter.module.ts @@ -1,6 +1,5 @@ import { NgModule } from "@angular/core"; -import { ScientificCondition } from "state-management/models"; import { MatInputModule } from "@angular/material/input"; import { MatDatepickerModule } from "@angular/material/datepicker"; import { CommonModule } from "@angular/common"; @@ -10,23 +9,13 @@ import { MatTooltipModule } from "@angular/material/tooltip"; import { SharedFilterComponent } from "./shared-filter.component"; import { ReactiveFormsModule } from "@angular/forms"; import { MatFormFieldModule } from "@angular/material/form-field"; - -export type FilterConfig = { - [P in K]?: V; -}; - -export interface FilterComponentInterface { - readonly componentName: string; - readonly label: string; -} - -export interface ConditionConfig { - condition: ScientificCondition; - enabled: boolean; -} +import { MultiSelectFilterComponent } from "../filters/multiselect-filter.component"; +import { MatOptionModule } from "@angular/material/core"; +import { MatChipsModule } from "@angular/material/chips"; +import { NgxNumericRangeFormFieldModule } from "../numeric-range/ngx-numeric-range-form-field.module"; @NgModule({ - declarations: [SharedFilterComponent], + declarations: [SharedFilterComponent, MultiSelectFilterComponent], imports: [ CommonModule, MatTooltipModule, @@ -35,7 +24,11 @@ export interface ConditionConfig { MatIconModule, ReactiveFormsModule, MatFormFieldModule, + MatOptionModule, + MatAutocompleteModule, + MatChipsModule, + NgxNumericRangeFormFieldModule, ], - exports: [SharedFilterComponent], + exports: [SharedFilterComponent, MultiSelectFilterComponent], }) export class SharedFilterModule {} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 1f6e95b88..e6be546f8 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -16,12 +16,12 @@ import { CommonModule } from "@angular/common"; import { SharedTableModule } from "./modules/shared-table/shared-table.module"; import { ScicatDataService } from "./services/scicat-data-service"; import { ScientificMetadataTreeModule } from "./modules/scientific-metadata-tree/scientific-metadata-tree.module"; -import { FiltersModule } from "./modules/filters/filters.module"; import { AttachmentService } from "./services/attachment.service"; import { DynamicMatTableModule } from "./modules/dynamic-material-table/table/dynamic-mat-table.module"; import { TranslateModule } from "@ngx-translate/core"; import { FullTextSearchBarModule } from "./modules/full-text-search-bar/full-text-search-bar.module"; import { SharedFilterModule } from "./modules/shared-filter/shared-filter.module"; +import { NgxNumericRangeFormFieldModule } from "./modules/numeric-range/ngx-numeric-range-form-field.module"; @NgModule({ imports: [ BreadcrumbModule, @@ -43,6 +43,7 @@ import { SharedFilterModule } from "./modules/shared-filter/shared-filter.module ScientificMetadataTreeModule, DynamicMatTableModule.forRoot({}), TranslateModule, + NgxNumericRangeFormFieldModule, ], providers: [ ConfigService, @@ -66,9 +67,9 @@ import { SharedFilterModule } from "./modules/shared-filter/shared-filter.module SharedFilterModule, SharedTableModule, ScientificMetadataTreeModule, - FiltersModule, DynamicMatTableModule, TranslateModule, + NgxNumericRangeFormFieldModule, ], }) export class SharedScicatFrontendModule {} diff --git a/src/app/state-management/actions/datasets.actions.spec.ts b/src/app/state-management/actions/datasets.actions.spec.ts index d5f2f1e81..87cb3b639 100644 --- a/src/app/state-management/actions/datasets.actions.spec.ts +++ b/src/app/state-management/actions/datasets.actions.spec.ts @@ -640,13 +640,19 @@ describe("Dataset Actions", () => { }); }); - describe("addLocationFilterAction", () => { + describe("addDatasetFilterAction", () => { it("should create an action", () => { const location = "test"; - const action = fromActions.addLocationFilterAction({ location }); + const action = fromActions.addDatasetFilterAction({ + filterType: "multiSelect", + key: "creationLocation", + value: location, + }); expect({ ...action }).toEqual({ - type: "[Dataset] Add Location Filter", - location, + type: "[Dataset] Add Dataset Filter", + key: "creationLocation", + value: location, + filterType: "multiSelect", }); }); }); @@ -654,89 +660,16 @@ describe("Dataset Actions", () => { describe("removeLocationFilterAction", () => { it("should create an action", () => { const location = "test"; - const action = fromActions.removeLocationFilterAction({ location }); - expect({ ...action }).toEqual({ - type: "[Dataset] Remove Location Filter", - location, - }); - }); - }); - - describe("addGroupFilterAction", () => { - it("should create an action", () => { - const group = "test"; - const action = fromActions.addGroupFilterAction({ group }); - expect({ ...action }).toEqual({ - type: "[Dataset] Add Group Filter", - group, - }); - }); - }); - - describe("removeGroupFilterAction", () => { - it("should create an action", () => { - const group = "test"; - const action = fromActions.removeGroupFilterAction({ group }); - expect({ ...action }).toEqual({ - type: "[Dataset] Remove Group Filter", - group, - }); - }); - }); - - describe("addTypeFilterAction", () => { - it("should create an action", () => { - const datasetType = "test"; - const action = fromActions.addTypeFilterAction({ datasetType }); - expect({ ...action }).toEqual({ - type: "[Dataset] Add Type Filter", - datasetType, - }); - }); - }); - - describe("removeTypeFilterAction", () => { - it("should create an action", () => { - const datasetType = "test"; - const action = fromActions.removeTypeFilterAction({ datasetType }); - expect({ ...action }).toEqual({ - type: "[Dataset] Remove Type Filter", - datasetType, + const action = fromActions.removeDatasetFilterAction({ + filterType: "multiSelect", + key: "creationLocation", + value: location, }); - }); - }); - - describe("addKeywordFilterAction", () => { - it("should create an action", () => { - const keyword = "test"; - const action = fromActions.addKeywordFilterAction({ keyword }); expect({ ...action }).toEqual({ - type: "[Dataset] Add Keyword Filter", - keyword, - }); - }); - }); - - describe("removeKeywordFilterAction", () => { - it("should create an action", () => { - const keyword = "test"; - const action = fromActions.removeKeywordFilterAction({ keyword }); - expect({ ...action }).toEqual({ - type: "[Dataset] Remove Keyword Filter", - keyword, - }); - }); - }); - - describe("setDateRangeFilterAction", () => { - it("should create an action", () => { - const begin = "testBegin"; - const end = "testEnd"; - const action = fromActions.setDateRangeFilterAction({ begin, end }); - expect({ ...action }).toEqual({ - type: "[Dataset] Set Date Range Filter", - begin, - end, + type: "[Dataset] Remove Dataset Filter", + key: "creationLocation", + value: location, + filterType: "multiSelect", }); }); }); @@ -787,12 +720,4 @@ describe("Dataset Actions", () => { expect({ ...action }).toEqual({ type: "[Dataset] Set Pid Terms", pid }); }); }); - - describe("setPidTermsFilterAction", () => { - it("should create an action", () => { - const pid = { $regex: "1" }; - const action = fromActions.setPidTermsFilterAction({ pid }); - expect({ ...action }).toEqual({ type: "[Dataset] Set Text Filter", pid }); - }); - }); }); diff --git a/src/app/state-management/actions/datasets.actions.ts b/src/app/state-management/actions/datasets.actions.ts index f12a8881a..27d051c4c 100644 --- a/src/app/state-management/actions/datasets.actions.ts +++ b/src/app/state-management/actions/datasets.actions.ts @@ -13,6 +13,8 @@ import { DatasetFilters, ScientificCondition, } from "state-management/models"; +import { DateRange } from "state-management/state/proposals.store"; +import { INumericRange } from "shared/modules/numeric-range/form/model/numeric-range-field.model"; // === Effects === @@ -279,49 +281,30 @@ export const setTextFilterAction = createAction( props<{ text: string }>(), ); -export const setPidTermsFilterAction = createAction( - "[Dataset] Set Text Filter", - props<{ pid: string | { $regex: string } }>(), -); -export const addLocationFilterAction = createAction( - "[Dataset] Add Location Filter", - props<{ location: string }>(), -); -export const removeLocationFilterAction = createAction( - "[Dataset] Remove Location Filter", - props<{ location: string }>(), -); - -export const addGroupFilterAction = createAction( - "[Dataset] Add Group Filter", - props<{ group: string }>(), -); -export const removeGroupFilterAction = createAction( - "[Dataset] Remove Group Filter", - props<{ group: string }>(), -); - -export const addTypeFilterAction = createAction( - "[Dataset] Add Type Filter", - props<{ datasetType: string }>(), -); -export const removeTypeFilterAction = createAction( - "[Dataset] Remove Type Filter", - props<{ datasetType: string }>(), -); - -export const addKeywordFilterAction = createAction( - "[Dataset] Add Keyword Filter", - props<{ keyword: string }>(), +export const setFiltersAction = createAction( + "[Dataset] Set Filters", + props<{ + datasetFilters: Record< + string, + string | DateRange | string[] | INumericRange + >; + }>(), ); -export const removeKeywordFilterAction = createAction( - "[Dataset] Remove Keyword Filter", - props<{ keyword: string }>(), +export const addDatasetFilterAction = createAction( + "[Dataset] Add Dataset Filter", + props<{ + key: string; + value: string | DateRange | string[] | INumericRange; + filterType: "text" | "dateRange" | "number" | "multiSelect"; + }>(), ); - -export const setDateRangeFilterAction = createAction( - "[Dataset] Set Date Range Filter", - props<{ begin: string; end: string }>(), +export const removeDatasetFilterAction = createAction( + "[Dataset] Remove Dataset Filter", + props<{ + key: string; + value?: string; + filterType: "text" | "dateRange" | "number" | "multiSelect"; + }>(), ); export const addScientificConditionAction = createAction( diff --git a/src/app/state-management/actions/user.actions.spec.ts b/src/app/state-management/actions/user.actions.spec.ts index e414ecff1..925146fe2 100644 --- a/src/app/state-management/actions/user.actions.spec.ts +++ b/src/app/state-management/actions/user.actions.spec.ts @@ -387,15 +387,6 @@ describe("User Actions", () => { }); }); - describe("deselectAllCustomColumnsAction", () => { - it("should create an action", () => { - const action = fromActions.deselectAllCustomColumnsAction(); - expect({ ...action }).toEqual({ - type: "[User] Deselect All Custom Columns", - }); - }); - }); - describe("showMessageAction", () => { it("should create an action", () => { const message = new Message("Test", MessageType.Success, 5000); diff --git a/src/app/state-management/actions/user.actions.ts b/src/app/state-management/actions/user.actions.ts index 5f981db53..bc1e9704c 100644 --- a/src/app/state-management/actions/user.actions.ts +++ b/src/app/state-management/actions/user.actions.ts @@ -6,12 +6,12 @@ import { UserSettings, } from "@scicatproject/scicat-sdk-ts-angular"; import { Message, Settings, TableColumn } from "state-management/models"; +import { AppConfigInterface } from "app-config.service"; +import { AccessTokenInterface } from "shared/services/auth/auth.service"; import { ConditionConfig, FilterConfig, -} from "../../shared/modules/filters/filters.module"; -import { AppConfigInterface } from "app-config.service"; -import { AccessTokenInterface } from "shared/services/auth/auth.service"; +} from "state-management/state/user.store"; export const setDatasetTableColumnsAction = createAction( "[User] Set Dataset Table Columns", @@ -153,9 +153,6 @@ export const deselectColumnAction = createAction( "[User] Deselect Column", props<{ name: string; columnType: "standard" | "custom" }>(), ); -export const deselectAllCustomColumnsAction = createAction( - "[User] Deselect All Custom Columns", -); export const showMessageAction = createAction( "[User] Show Message", diff --git a/src/app/state-management/effects/user.effects.spec.ts b/src/app/state-management/effects/user.effects.spec.ts index 6d8b907b8..0fe902df8 100644 --- a/src/app/state-management/effects/user.effects.spec.ts +++ b/src/app/state-management/effects/user.effects.spec.ts @@ -696,18 +696,6 @@ describe("UserEffects", () => { expect(effects.updateUserColumns$).toBeObservable(expected); }); }); - - describe("ofType deselectAllCustomColumnsAction", () => { - it("should dispatch an updateUserSettingsAction", () => { - const action = fromActions.deselectAllCustomColumnsAction(); - const outcome = fromActions.updateUserSettingsAction({ property }); - - actions = hot("-a", { a: action }); - - const expected = cold("-b", { b: outcome }); - expect(effects.updateUserColumns$).toBeObservable(expected); - }); - }); }); describe("updateUserSettings$", () => { diff --git a/src/app/state-management/effects/user.effects.ts b/src/app/state-management/effects/user.effects.ts index efa3a2af0..25bf6835a 100644 --- a/src/app/state-management/effects/user.effects.ts +++ b/src/app/state-management/effects/user.effects.ts @@ -418,11 +418,7 @@ export class UserEffects { updateUserColumns$ = createEffect(() => { return this.actions$.pipe( - ofType( - fromActions.selectColumnAction, - fromActions.deselectColumnAction, - fromActions.deselectAllCustomColumnsAction, - ), + ofType(fromActions.selectColumnAction, fromActions.deselectColumnAction), concatLatestFrom(() => this.columns$), map(([action, columns]) => columns), distinctUntilChanged( diff --git a/src/app/state-management/models/index.ts b/src/app/state-management/models/index.ts index 2c493a623..f6760d348 100644 --- a/src/app/state-management/models/index.ts +++ b/src/app/state-management/models/index.ts @@ -1,7 +1,7 @@ import { - ConditionConfig, FilterConfig, -} from "shared/modules/filters/filters.module"; + ConditionConfig, +} from "state-management/state/user.store"; export interface Settings { tapeCopies: string; @@ -20,10 +20,6 @@ export interface TableColumn { width?: number; } -export interface LabelMaps { - [key: string]: Record; -} - export interface LabelsLocalization { datasetDefault: Record; datasetCustom: Record; diff --git a/src/app/state-management/reducers/datasets.reducer.spec.ts b/src/app/state-management/reducers/datasets.reducer.spec.ts index 074acb055..9a3af0b80 100644 --- a/src/app/state-management/reducers/datasets.reducer.spec.ts +++ b/src/app/state-management/reducers/datasets.reducer.spec.ts @@ -384,11 +384,15 @@ describe("DatasetsReducer", () => { }); }); - describe("on addLocationFilterAction", () => { + describe("on addDatasetFilterAction", () => { it("should set location filter and set skip to 0", () => { const location = "test"; - const action = fromActions.addLocationFilterAction({ location }); + const action = fromActions.addDatasetFilterAction({ + filterType: "multiSelect", + key: "creationLocation", + value: location, + }); const state = fromDatasets.datasetsReducer(initialDatasetState, action); expect(state.filters.creationLocation).toContain(location); @@ -396,11 +400,15 @@ describe("DatasetsReducer", () => { }); }); - describe("on removeLocationFilterAction", () => { + describe("on removeDatasetFilterAction", () => { it("should remove location filter and set skip to 0", () => { const location = "test"; - const action = fromActions.removeLocationFilterAction({ location }); + const action = fromActions.removeDatasetFilterAction({ + filterType: "multiSelect", + key: "creationLocation", + value: location, + }); const state = fromDatasets.datasetsReducer(initialDatasetState, action); expect(state.filters.creationLocation).not.toContain(location); @@ -408,84 +416,16 @@ describe("DatasetsReducer", () => { }); }); - describe("on addGroupFilterAction", () => { - it("should set ownergroup filter and set skip to 0", () => { - const group = "test"; - - const action = fromActions.addGroupFilterAction({ group }); - const state = fromDatasets.datasetsReducer(initialDatasetState, action); - - expect(state.filters.ownerGroup).toContain(group); - expect(state.filters.skip).toEqual(0); - }); - }); - - describe("on removeGroupFilterAction", () => { - it("should remove ownergroup filter and set skip to 0", () => { - const group = "test"; - - const action = fromActions.removeGroupFilterAction({ group }); - const state = fromDatasets.datasetsReducer(initialDatasetState, action); - - expect(state.filters.ownerGroup).not.toContain(group); - expect(state.filters.skip).toEqual(0); - }); - }); - - describe("on addTypeFilterAction", () => { - it("should set type filter and set skip to 0", () => { - const datasetType = "test"; - - const action = fromActions.addTypeFilterAction({ datasetType }); - const state = fromDatasets.datasetsReducer(initialDatasetState, action); - - expect(state.filters.type).toContain(datasetType); - expect(state.filters.skip).toEqual(0); - }); - }); - - describe("on removeTypeFilterAction", () => { - it("should remove type filter and set skip to 0", () => { - const datasetType = "test"; - - const action = fromActions.removeTypeFilterAction({ datasetType }); - const state = fromDatasets.datasetsReducer(initialDatasetState, action); - - expect(state.filters.type).not.toContain(datasetType); - expect(state.filters.skip).toEqual(0); - }); - }); - - describe("on addKeywordFilterAction", () => { - it("should set keyword filter and set skip to 0", () => { - const keyword = "test"; - - const action = fromActions.addKeywordFilterAction({ keyword }); - const state = fromDatasets.datasetsReducer(initialDatasetState, action); - - expect(state.filters.keywords).toContain(keyword); - expect(state.filters.skip).toEqual(0); - }); - }); - - describe("on removeKeywordFilterAction", () => { - it("should remove keyword filter and set skip to 0", () => { - const keyword = "test"; - - const action = fromActions.removeKeywordFilterAction({ keyword }); - const state = fromDatasets.datasetsReducer(initialDatasetState, action); - - expect(state.filters.keywords).not.toContain(keyword); - expect(state.filters.skip).toEqual(0); - }); - }); - describe("on setDateRangeFilterAction", () => { it("should set creationTime filter", () => { const begin = new Date(2018, 1, 2).toISOString(); const end = new Date(2018, 1, 3).toISOString(); - const action = fromActions.setDateRangeFilterAction({ begin, end }); + const action = fromActions.addDatasetFilterAction({ + filterType: "dateRange", + key: "creationTime", + value: { begin, end }, + }); const state = fromDatasets.datasetsReducer(initialDatasetState, action); expect(state.filters.creationTime).toEqual({ begin, end }); @@ -546,14 +486,4 @@ describe("DatasetsReducer", () => { expect(state.pidTerms).toEqual(pid); }); }); - - describe("on setPidTermsFilterAction", () => { - it("should set dataset state to initialDatasetStata", () => { - const pid = { $regex: "1" }; - const action = fromActions.setPidTermsFilterAction({ pid }); - const state = fromDatasets.datasetsReducer(initialDatasetState, action); - - expect(state.filters.pid).toEqual(pid); - }); - }); }); diff --git a/src/app/state-management/reducers/datasets.reducer.ts b/src/app/state-management/reducers/datasets.reducer.ts index 4cb17be1f..31b407db2 100644 --- a/src/app/state-management/reducers/datasets.reducer.ts +++ b/src/app/state-management/reducers/datasets.reducer.ts @@ -358,92 +358,49 @@ const reducer = createReducer( return { ...state, filters }; }), - on(fromActions.setPidTermsFilterAction, (state, { pid }): DatasetState => { - const filters = { ...state.filters, pid, skip: 0 }; - return { ...state, filters }; - }), - on( - fromActions.addLocationFilterAction, - (state, { location }): DatasetState => { - const creationLocation = state.filters.creationLocation - .concat(location) - .filter((val, i, self) => self.indexOf(val) === i); // Unique - const filters = { ...state.filters, creationLocation, skip: 0 }; - return { ...state, filters }; - }, - ), - on( - fromActions.removeLocationFilterAction, - (state, { location }): DatasetState => { - const creationLocation = state.filters.creationLocation.filter( - (existingLocation) => existingLocation !== location, - ); - const filters = { ...state.filters, creationLocation, skip: 0 }; + fromActions.setFiltersAction, + (state, { datasetFilters }): DatasetState => { + const filters = { ...state.filters, ...datasetFilters }; + return { ...state, filters }; }, ), - on(fromActions.addGroupFilterAction, (state, { group }): DatasetState => { - const ownerGroup = state.filters.ownerGroup - .concat(group) - .filter((val, i, self) => self.indexOf(val) === i); // Unique - const filters = { ...state.filters, ownerGroup, skip: 0 }; - return { ...state, filters }; - }), - on(fromActions.removeGroupFilterAction, (state, { group }): DatasetState => { - const ownerGroup = state.filters.ownerGroup.filter( - (existingGroup) => existingGroup !== group, - ); - const filters = { ...state.filters, ownerGroup, skip: 0 }; - return { ...state, filters }; - }), - on( - fromActions.addTypeFilterAction, - (state, { datasetType }): DatasetState => { - const type = state.filters.type - .concat(datasetType) - .filter((val, i, self) => self.indexOf(val) === i); // Unique - const filters = { ...state.filters, type, skip: 0 }; + fromActions.addDatasetFilterAction, + (state, { key, value, filterType }): DatasetState => { + const filters = { + ...state.filters, + }; + if (filterType === "multiSelect") { + const newValue = (state.filters[key] || []) + .concat(value) + .filter((val, i, self) => self.indexOf(val) === i); // Unique + + filters[key] = newValue; + } else { + filters[key] = value; + } + return { ...state, filters }; }, ), on( - fromActions.removeTypeFilterAction, - (state, { datasetType }): DatasetState => { - const type = state.filters.type.filter( - (existingType) => existingType !== datasetType, - ); - const filters = { ...state.filters, type, skip: 0 }; - return { ...state, filters }; - }, - ), + fromActions.removeDatasetFilterAction, + (state, { key, value, filterType }): DatasetState => { + const filters = { ...state.filters }; - on(fromActions.addKeywordFilterAction, (state, { keyword }): DatasetState => { - const keywords = state.filters.keywords - .concat(keyword) - .filter((val, i, self) => self.indexOf(val) === i); // Unique - const filters = { ...state.filters, keywords, skip: 0 }; - return { ...state, filters }; - }), - on( - fromActions.removeKeywordFilterAction, - (state, { keyword }): DatasetState => { - const keywords = state.filters.keywords.filter( - (existingKeyword) => existingKeyword !== keyword, - ); - const filters = { ...state.filters, keywords, skip: 0 }; - return { ...state, filters }; - }, - ), + if (filterType === "multiSelect") { + const newValue = state.filters[key].filter( + (existingValue) => existingValue !== value, + ); + + filters[key] = newValue; + } else { + delete filters[key]; + } - on( - fromActions.setDateRangeFilterAction, - (state, { begin, end }): DatasetState => { - const oldTime = state.filters.creationTime; - const creationTime = begin && end ? { ...oldTime, begin, end } : null; - const filters = { ...state.filters, creationTime }; return { ...state, filters }; }, ), diff --git a/src/app/state-management/reducers/user.reducer.spec.ts b/src/app/state-management/reducers/user.reducer.spec.ts index 0256d5912..790f15f82 100644 --- a/src/app/state-management/reducers/user.reducer.spec.ts +++ b/src/app/state-management/reducers/user.reducer.spec.ts @@ -277,33 +277,6 @@ describe("UserReducer", () => { }); }); - describe("on deselectAllCustomColumnsAction", () => { - it("should set enabled to false for all custom columns", () => { - const names = ["test"]; - const addColumnsAction = fromActions.addCustomColumnsAction({ names }); - const firstState = userReducer(initialUserState, addColumnsAction); - const selectColumnAction = fromActions.selectColumnAction({ - name: "test", - columnType: "custom", - }); - const secondState = userReducer(firstState, selectColumnAction); - secondState.columns.forEach((column) => { - if (column.name === "test") { - expect(column.enabled).toEqual(true); - } - }); - - const action = fromActions.deselectAllCustomColumnsAction(); - const state = userReducer(secondState, action); - - state.columns.forEach((column) => { - if (column.name === "test") { - expect(column.enabled).toEqual(false); - } - }); - }); - }); - describe("on showMessageAction", () => { it("should set message", () => { const message: Message = { diff --git a/src/app/state-management/reducers/user.reducer.ts b/src/app/state-management/reducers/user.reducer.ts index 9dcdff80b..f3928cb0b 100644 --- a/src/app/state-management/reducers/user.reducer.ts +++ b/src/app/state-management/reducers/user.reducer.ts @@ -105,20 +105,24 @@ const reducer = createReducer( jobCount, columns = [], externalSettings, + filters, } = userSettings as any; const settings = { ...state.settings, datasetCount, jobCount }; + if (columns.length > 0) { return { ...state, settings, columns, tablesSettings: externalSettings?.tablesSettings, + filters: filters || state.filters, }; } else { return { ...state, settings, tablesSettings: externalSettings?.tablesSettings, + filters: filters || state.filters, }; } }, @@ -212,18 +216,6 @@ const reducer = createReducer( return { ...state, columns }; }, ), - on(fromActions.deselectAllCustomColumnsAction, (state): UserState => { - const customColumns = [...state.columns].filter( - (column) => column.type !== "standard", - ); - customColumns.forEach((column) => (column.enabled = false)); - const customColumnNames = customColumns.map((column) => column.name); - - const columns = [...state.columns] - .filter((column) => !customColumnNames.includes(column.name)) - .concat(customColumns); - return { ...state, columns }; - }), on( fromActions.showMessageAction, diff --git a/src/app/state-management/selectors/datasets.selectors.spec.ts b/src/app/state-management/selectors/datasets.selectors.spec.ts index 95b8ea6df..a2db2f573 100644 --- a/src/app/state-management/selectors/datasets.selectors.spec.ts +++ b/src/app/state-management/selectors/datasets.selectors.spec.ts @@ -2,6 +2,7 @@ import * as fromDatasetSelectors from "./datasets.selectors"; import { ArchViewMode } from "../models"; import { DatasetState } from "../state/datasets.store"; import { mockDataset as dataset } from "shared/MockStubs"; +import { initialUserState } from "./user.selectors.spec"; const initialDatasetState: DatasetState = { datasets: [], @@ -287,15 +288,15 @@ describe("test dataset selectors", () => { describe("selectFullfacetParams", () => { it("should return the fullfacet params", () => { - const fullfacet = - fromDatasetSelectors.selectFullfacetParams.projector( - initialDatasetState, - ); + const fullfacet = fromDatasetSelectors.selectFullfacetParams.projector( + initialDatasetState, + initialUserState.filters, + ); const fullfacetKeys = Object.keys(fullfacet); expect(fullfacet.facets).toEqual([ - "type", "creationLocation", "ownerGroup", + "type", "keywords", ]); expect(fullfacetKeys).toContain("facets"); diff --git a/src/app/state-management/selectors/datasets.selectors.ts b/src/app/state-management/selectors/datasets.selectors.ts index fbd9a4590..fd1608f77 100644 --- a/src/app/state-management/selectors/datasets.selectors.ts +++ b/src/app/state-management/selectors/datasets.selectors.ts @@ -1,5 +1,6 @@ import { DatasetState } from "state-management/state/datasets.store"; import { createFeatureSelector, createSelector } from "@ngrx/store"; +import { selectFilters as selectUserFilters } from "state-management/selectors/user.selectors"; const selectDatasetState = createFeatureSelector("datasets"); @@ -76,6 +77,9 @@ export const selectTextFilter = createSelector( (filters) => filters.text || "", ); +export const selectFilterByKey = (key: string) => + createSelector(selectFilters, (filters) => filters[key] || []); + export const selectLocationFilter = createSelector( selectFilters, (filters) => filters.creationLocation, @@ -137,6 +141,9 @@ const selectFacetCounts = createSelector( (state) => state.facetCounts || {}, ); +export const selectFacetCountByKey = (key: string) => + createSelector(selectFacetCounts, (counts) => counts[key] || []); + export const selectLocationFacetCounts = createSelector( selectFacetCounts, (counts) => counts.creationLocation || [], @@ -188,7 +195,8 @@ export const selectFullqueryParams = createSelector( export const selectFullfacetParams = createSelector( selectDatasetState, - (state) => { + selectUserFilters, + (state, userFilters) => { const filter = state.filters; const pagination = state.pagination; const { skip, limit, sortField, modeToggle, ...theRest } = { @@ -196,7 +204,10 @@ export const selectFullfacetParams = createSelector( ...pagination, }; const fields = restrictFilter(theRest); - const facets = ["type", "creationLocation", "ownerGroup", "keywords"]; + const facets = userFilters + .filter((f) => f.enabled && f.type === "multiSelect") + .map((f) => f.key); + return { fields, facets }; }, ); diff --git a/src/app/state-management/selectors/user.selectors.spec.ts b/src/app/state-management/selectors/user.selectors.spec.ts index dc9d8bcbb..7c818c9a1 100644 --- a/src/app/state-management/selectors/user.selectors.spec.ts +++ b/src/app/state-management/selectors/user.selectors.spec.ts @@ -73,15 +73,48 @@ export const initialUserState: UserState = { columns: [{ name: "datasetName", order: 1, type: "standard", enabled: true }], filters: [ - { LocationFilter: true }, - { PidFilter: true }, - { GroupFilter: true }, - { TypeFilter: true }, - { KeywordFilter: true }, - { DateRangeFilter: true }, - { TextFilter: true }, - { PidFilterContains: false }, - { PidFilterStartsWith: false }, + { + key: "creationLocation", + label: "Location", + type: "multiSelect", + description: "Filter by creation location on the dataset", + enabled: true, + }, + { + key: "pid", + label: "Pid", + type: "text", + description: "Filter by dataset pid", + enabled: true, + }, + { + key: "ownerGroup", + label: "Group", + type: "multiSelect", + description: "Filter by owner group of the dataset", + enabled: true, + }, + { + key: "type", + label: "Type", + type: "multiSelect", + description: "Filter by dataset type", + enabled: true, + }, + { + key: "keywords", + label: "Keyword", + type: "multiSelect", + description: "Filter by keywords in the dataset", + enabled: true, + }, + { + key: "creationTime", + label: "Creation Time", + type: "dateRange", + description: "Filter by creation time of the dataset", + enabled: true, + }, ], conditions: [], diff --git a/src/app/state-management/state/user.store.ts b/src/app/state-management/state/user.store.ts index d319d709e..a9ae4aaf4 100644 --- a/src/app/state-management/state/user.store.ts +++ b/src/app/state-management/state/user.store.ts @@ -1,10 +1,25 @@ -import { Settings, Message, TableColumn } from "../models"; +import { Settings, Message, TableColumn, ScientificCondition } from "../models"; import { AccessTokenInterface } from "shared/services/auth/auth.service"; import { ReturnedUserDto } from "@scicatproject/scicat-sdk-ts-angular"; -import { - ConditionConfig, - FilterConfig, -} from "../../shared/modules/filters/filters.module"; +import { Observable } from "rxjs"; +import { FacetCount } from "./datasets.store"; + +export type FilterType = "text" | "dateRange" | "multiSelect" | "number"; + +export interface FilterConfig { + key: string; + label: string; + description?: string; + type?: FilterType; + enabled: boolean; + facetCounts$?: Observable; + filter$?: Observable; +} + +export interface ConditionConfig { + condition: ScientificCondition; + enabled: boolean; +} // NOTE It IS ok to make up a state of other sub states export interface UserState { @@ -66,13 +81,48 @@ export const initialUserState: UserState = { columns: [], filters: [ - { LocationFilter: true }, - { PidFilter: true }, - { GroupFilter: true }, - { TypeFilter: true }, - { KeywordFilter: true }, - { DateRangeFilter: true }, - { TextFilter: true }, + { + key: "creationLocation", + label: "Location", + type: "multiSelect", + description: "Filter by creation location on the dataset", + enabled: true, + }, + { + key: "pid", + label: "Pid", + type: "text", + description: "Filter by dataset pid", + enabled: true, + }, + { + key: "ownerGroup", + label: "Group", + type: "multiSelect", + description: "Filter by owner group of the dataset", + enabled: true, + }, + { + key: "type", + label: "Type", + type: "multiSelect", + description: "Filter by dataset type", + enabled: true, + }, + { + key: "keywords", + label: "Keyword", + type: "multiSelect", + description: "Filter by keywords in the dataset", + enabled: true, + }, + { + key: "creationTime", + label: "Creation Time", + type: "dateRange", + description: "Filter by creation time of the dataset", + enabled: true, + }, ], conditions: [], diff --git a/src/assets/config.json b/src/assets/config.json index 42ef255d8..39d9f3c77 100644 --- a/src/assets/config.json +++ b/src/assets/config.json @@ -12,8 +12,8 @@ "externalAuthEndpoint": "/auth/msad", "facility": "Local", "siteIcon": "site-header-logo.png", - "sitetitle" : "", - "siteSciCatLogo" : "", + "sitetitle": "", + "siteSciCatLogo": "", "loginFacilityLabel": "ESS", "loginLdapLabel": "Ldap", "loginLocalLabel": "Local", @@ -126,17 +126,6 @@ "authorization": ["#datasetAccess", "#datasetPublic"] } ], - "labelMaps": { - "filters": { - "LocationFilter": "Location", - "PidFilter": "Pid", - "GroupFilter": "Group", - "TypeFilter": "Type", - "KeywordFilter": "Keyword", - "DateRangeFilter": "Start Date - End Date", - "TextFilter": "Text" - } - }, "defaultDatasetsListSettings": { "columns": [ { @@ -219,13 +208,48 @@ } ], "filters": [ - { "LocationFilter": true }, - { "PidFilter": true }, - { "GroupFilter": true }, - { "TypeFilter": true }, - { "KeywordFilter": true }, - { "DateRangeFilter": true }, - { "TextFilter": true } + { + "key": "creationLocation", + "label": "Location", + "type": "multiSelect", + "description": "Filter by creation location on the dataset", + "enabled": true + }, + { + "key": "pid", + "label": "Pid", + "type": "text", + "description": "Filter by dataset pid", + "enabled": true + }, + { + "key": "ownerGroup", + "label": "Group", + "type": "multiSelect", + "description": "Filter by owner group of the dataset", + "enabled": true + }, + { + "key": "type", + "label": "Type", + "type": "multiSelect", + "description": "Filter by dataset type", + "enabled": true + }, + { + "key": "keywords", + "label": "Keyword", + "type": "multiSelect", + "description": "Filter by keywords in the dataset", + "enabled": true + }, + { + "key": "creationTime", + "label": "Creation Time", + "type": "dateRange", + "description": "Filter by creation time of the dataset", + "enabled": true + } ], "conditions": [] },