diff --git a/src/biomarkers/serializers.py b/src/biomarkers/serializers.py index b2db9182..a0f0a6b8 100644 --- a/src/biomarkers/serializers.py +++ b/src/biomarkers/serializers.py @@ -107,12 +107,12 @@ def get_has_fs_experiment(ins: Biomarker) -> bool: class BiomarkerSimpleUpdateSerializer(serializers.ModelSerializer): """ - This serializer is used to update only the name and description of the Biomarker model to prevent updating all + This serializer is used to update simple Biomarker fields to prevent updating all the molecules when it's not necessary. """ class Meta: model = Biomarker - fields = ['name', 'description', 'tissue'] + fields = ['name', 'description', 'tag', 'tissue'] class BiomarkerSerializer(WritableNestedModelSerializer): diff --git a/src/frontend/static/frontend/package-lock.json b/src/frontend/static/frontend/package-lock.json index 82b5881d..5a417f5d 100644 --- a/src/frontend/static/frontend/package-lock.json +++ b/src/frontend/static/frontend/package-lock.json @@ -131,7 +131,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -2128,7 +2127,6 @@ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", "license": "MIT", - "peer": true, "engines": { "node": ">=6.9.0" } @@ -4236,7 +4234,6 @@ "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -4555,7 +4552,6 @@ "resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.3.2.tgz", "integrity": "sha512-QbEn1SkNW3b89KTlSkp6OHdvw3DhpL6tSdDhsOlldw3LoRBy4fx80Z9W9lmg+g+8DjTAs1Z1ysElEFtAN69AZg==", "dev": true, - "peer": true, "dependencies": { "@module-federation/runtime-tools": "0.11.2", "@rspack/binding": "1.3.2", @@ -4606,7 +4602,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -5880,7 +5875,6 @@ "version": "12.2.3", "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", - "peer": true, "dependencies": { "@types/linkify-it": "*", "@types/mdurl": "*" @@ -5983,7 +5977,6 @@ "version": "18.3.20", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.20.tgz", "integrity": "sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -6121,7 +6114,6 @@ "version": "4.41.32", "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.32.tgz", "integrity": "sha512-cb+0ioil/7oz5//7tZUSwbrSAN/NWHrQylz5cW8G0dWTcF/g+/dSdMlKVZspBYuMAN1+WnwHrkxiRrLcwd0Heg==", - "peer": true, "dependencies": { "@types/node": "*", "@types/tapable": "^1", @@ -6242,7 +6234,6 @@ "integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/types": "8.59.3", @@ -7024,6 +7015,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", @@ -7034,13 +7026,15 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-numbers/node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { "version": "1.9.0", @@ -7195,7 +7189,6 @@ "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7275,7 +7268,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -7435,7 +7427,6 @@ "version": "3.52.0", "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.52.0.tgz", "integrity": "sha512-7dg0ADKs8AA89iYMZMe2sFDG0XK5PfqllKV9N+i3hKHm3vEtdhwz8AlXGm+/b0nJ6jKiaXsqci5LfVxNhtB+dA==", - "peer": true, "dependencies": { "@yr/monotone-cubic-spline": "^1.0.3", "svg.draggable.js": "^2.2.2", @@ -7929,7 +7920,6 @@ "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", "deprecated": "babel-eslint is now @babel/eslint-parser. This package will no longer receive updates.", - "peer": true, "dependencies": { "@babel/code-frame": "^7.0.0", "@babel/parser": "^7.7.0", @@ -8755,7 +8745,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -10087,7 +10076,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", @@ -10839,7 +10827,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", - "peer": true, "engines": { "node": ">=12" } @@ -12230,7 +12217,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -12421,7 +12407,6 @@ "version": "2.31.0", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.8", @@ -12455,7 +12440,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.9.3.tgz", "integrity": "sha512-NrPUarxpFzGpQVXdVWkGttDD8WIxBuM/dRNw5kKFxrlGdjAJ3l8ma0LK5hsK5Qp79GBGM+HY1zYVbHqateTklA==", "dev": true, - "peer": true, "dependencies": { "@types/doctrine": "^0.0.9", "@typescript-eslint/utils": "^8.28.0", @@ -12626,7 +12610,6 @@ "version": "6.10.2", "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", - "peer": true, "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", @@ -12660,7 +12643,6 @@ "version": "7.37.4", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz", "integrity": "sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==", - "peer": true, "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", @@ -14478,7 +14460,8 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "peer": true }, "node_modules/global-modules": { "version": "2.0.0", @@ -16765,7 +16748,6 @@ "version": "26.6.0", "resolved": "https://registry.npmjs.org/jest/-/jest-26.6.0.tgz", "integrity": "sha512-jxTmrvuecVISvKFFhOkjsWRZV7sFqdSUAd1ajOKY+/QE/aLBVstsJ/dX8GczLzwiT6ZEwwmZqtCUHLHHQVzcfA==", - "peer": true, "dependencies": { "@jest/core": "^26.6.0", "import-local": "^3.0.2", @@ -19828,7 +19810,6 @@ "version": "12.3.2", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", - "peer": true, "dependencies": { "argparse": "^2.0.1", "entities": "~2.1.0", @@ -20312,8 +20293,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "peer": true + "dev": true }, "node_modules/memory-fs": { "version": "0.4.1", @@ -21506,7 +21486,6 @@ "version": "4.33.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz", "integrity": "sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg==", - "peer": true, "dependencies": { "@typescript-eslint/experimental-utils": "4.33.0", "@typescript-eslint/scope-manager": "4.33.0", @@ -21578,7 +21557,6 @@ "version": "4.33.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.33.0.tgz", "integrity": "sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA==", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "4.33.0", "@typescript-eslint/types": "4.33.0", @@ -21973,7 +21951,6 @@ "version": "7.32.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", - "peer": true, "dependencies": { "@babel/code-frame": "7.12.11", "@eslint/eslintrc": "^0.4.3", @@ -22062,7 +22039,6 @@ "version": "5.10.0", "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-5.10.0.tgz", "integrity": "sha512-vcz32f+7TP+kvTUyMXZmCnNujBQZDNmcqPImw8b9PZ+16w1Qdm6ryRuYZYVaG9xRqqmAPr2Cs9FAX5gN+x/bjw==", - "peer": true, "dependencies": { "lodash": "^4.17.15", "string-natural-compare": "^3.0.1" @@ -22078,7 +22054,6 @@ "version": "24.7.0", "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-24.7.0.tgz", "integrity": "sha512-wUxdF2bAZiYSKBclsUMrYHH6WxiBreNjyDxbRv345TIvPeoCEgPNEn3Sa+ZrSqsf1Dl9SqqSREXMHExlMMu1DA==", - "peer": true, "dependencies": { "@typescript-eslint/experimental-utils": "^4.0.1" }, @@ -22099,7 +22074,6 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", - "peer": true, "engines": { "node": ">=10" }, @@ -22111,7 +22085,6 @@ "version": "3.10.2", "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-3.10.2.tgz", "integrity": "sha512-WAmOCt7EbF1XM8XfbCKAEzAPnShkNSwcIsAD2jHdsMUT9mZJPjLCG7pMzbcC8kK366NOuGip8HKLDC+Xk4yIdA==", - "peer": true, "dependencies": { "@typescript-eslint/experimental-utils": "^3.10.1" }, @@ -22611,7 +22584,6 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -22905,6 +22877,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/neo-react-semantic-ui-range/node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/neo-react-semantic-ui-range/node_modules/universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -22917,7 +22904,6 @@ "version": "4.44.2", "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.44.2.tgz", "integrity": "sha512-6KJVGlCxYdISyurpQ0IPTklv+DULv05rs2hseIXer6D7KrUicRDLFb4IUM1S6LUAKypPM/nSiVSuv8jHu1m3/Q==", - "peer": true, "dependencies": { "@webassemblyjs/ast": "1.9.0", "@webassemblyjs/helper-module-context": "1.9.0", @@ -26035,7 +26021,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "peer": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -26291,7 +26276,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -26586,7 +26570,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -26712,7 +26695,6 @@ "version": "0.8.3", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz", "integrity": "sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg==", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -27624,7 +27606,6 @@ "version": "1.32.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-1.32.1.tgz", "integrity": "sha512-/2HA0Ec70TvQnXdzynFffkjA6XN+1e2pEv/uKS5Ulca40g2L7KuOE3riasHoNVHOsFD5KKZgDsMk1CP3Tw9s+A==", - "peer": true, "dependencies": { "@types/estree": "*", "@types/node": "*", @@ -28783,7 +28764,6 @@ "version": "1.6.1", "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.6.1.tgz", "integrity": "sha512-2g0tjOR+fRs0amxENLi/q5TiJTqY+WXFOzb5UwXndlK6TO3U/mirZznpx6w34HVMoc3g7cY24yC/ZMIYnDlfkw==", - "peer": true, "dependencies": { "debug": "^3.2.7", "eventsource": "^2.0.2", @@ -29916,6 +29896,7 @@ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", @@ -29976,6 +29957,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -30005,6 +29987,7 @@ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -30016,13 +29999,15 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/terser-webpack-plugin/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -30032,6 +30017,7 @@ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -30045,13 +30031,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/terser-webpack-plugin/node_modules/schema-utils": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "license": "MIT", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -30071,6 +30059,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "license": "MIT", + "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -30086,6 +30075,7 @@ "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz", "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==", "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -30217,7 +30207,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -30796,7 +30785,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -32059,7 +32047,6 @@ "version": "3.11.1", "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.1.tgz", "integrity": "sha512-u4R3mRzZkbxQVa+MBWi2uVpB5W59H3ekZAJsQlKUTdl7Elcah2EhygTPLmeFXybQkf9i2+L0kn7ik9SnXa6ihQ==", - "peer": true, "dependencies": { "ansi-html": "0.0.7", "bonjour": "^3.5.0", @@ -32755,6 +32742,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" @@ -32764,25 +32752,29 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/webpack/node_modules/@webassemblyjs/helper-buffer": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/webpack/node_modules/@webassemblyjs/helper-wasm-bytecode": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/webpack/node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -32795,6 +32787,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "license": "MIT", + "peer": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } @@ -32804,6 +32797,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@xtuc/long": "4.2.2" } @@ -32812,13 +32806,15 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/webpack/node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -32835,6 +32831,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", @@ -32848,6 +32845,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -32860,6 +32858,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", @@ -32874,6 +32873,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" @@ -32897,6 +32897,7 @@ "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.13.0" }, @@ -32926,6 +32927,7 @@ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -32938,6 +32940,7 @@ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.0.tgz", "integrity": "sha512-SkE2t82KlkkxQRVMVLAGKxLfORGQfrkx5dkj+vlgXRVNEdPc4eZcR+J/Fvj8C+yKSFH5L0q3NFlyufOVQnCcYQ==", "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" @@ -32950,19 +32953,22 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/webpack/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/webpack/node_modules/loader-runner": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", "license": "MIT", + "peer": true, "engines": { "node": ">=6.11.5" }, @@ -32976,6 +32982,7 @@ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -32985,6 +32992,7 @@ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "license": "MIT", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -33004,6 +33012,7 @@ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "license": "MIT", + "peer": true, "engines": { "node": ">=6" }, @@ -33017,6 +33026,7 @@ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.1.2" }, @@ -33029,6 +33039,7 @@ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.0.tgz", "integrity": "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.13.0" } @@ -33659,7 +33670,6 @@ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx b/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx index 7fec916a..df86fa7d 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx +++ b/src/frontend/static/frontend/src/components/biomarkers/BiomarkersPanel.tsx @@ -3,7 +3,7 @@ import { Base } from '../Base' import { Header, Button, Modal, Table, DropdownItemProps, Icon, Confirm, Form, Grid } from 'semantic-ui-react' import { DjangoCGDSStudy, DjangoTag, DjangoUserFile, TagType } from '../../utils/django_interfaces' import ky, { Options } from 'ky' -import { getDjangoHeader, alertGeneralError, formatDateLocale, cleanRef, getFilenameFromSource, makeSourceAndAppend, getDefaultSource, getDefaultNewTag, copyObject } from '../../utils/util_functions' +import { getDjangoHeader, alertGeneralError, formatDateLocale, cleanRef, getFilenameFromSource, makeSourceAndAppend, getDefaultSource, getDefaultNewTag, copyObject, getUserTagsByType } from '../../utils/util_functions' import { Nullable, CustomAlert, CustomAlertTypes, SourceType, OkResponse, ConfirmModal } from '../../utils/interfaces' import { Biomarker, BiomarkerType, BiomarkerOrigin, FormBiomarkerData, MoleculesSectionData, MoleculesTypeOfSelection, SaveBiomarkerStructure, SaveMoleculeStructure, FeatureSelectionPanelData, SourceStateBiomarker, FeatureSelectionAlgorithm, FitnessFunction, FitnessFunctionParameters, BiomarkerState, AdvancedAlgorithm as AdvancedAlgorithmParameters, BBHAVersion, BiomarkerSimple } from './types' import { ManualForm } from './modalContentBiomarker/manualForm/ManualForm' @@ -26,7 +26,7 @@ import { EditBiomarkerIcon } from './EditBiomarkerIcon' import { SwitchPublicButton } from '../common/SwitchPublicButton' import { PopupIcons } from '../common/PopupIcons' import { useIntl, IntlShape } from 'react-intl' -import { TagsPanel } from '../files-manager/TagsPanel' +import { TagDropdown } from '../common/TagDropdown' // URLs defined in biomarkers.html declare const urlBiomarkersCRUD: string @@ -57,6 +57,11 @@ type BiomarkerNameAndDesc = { tag?: { id: number } | null } +/** Extremely simple struct of a Biomarker (useful for simple updates). */ +type BiomarkerTag = { + tag: Nullable +} + /** Some flags to validate the Biomarkers form. */ type ValidationForm = { haveAmbiguous: boolean, @@ -1685,17 +1690,8 @@ class BiomarkersPanel extends React.Component { - response.json().then((tags) => { - this.setState({ tags }) - }).catch((err) => { - console.log('Error parsing JSON ->', err) - }) + getUserTagsByType(TagType.EXPERIMENT, this.abortController.signal).then((tags) => { + this.setState({ tags }) }).catch((err) => { console.log("Error getting user's tags ->", err) }) @@ -1809,6 +1805,44 @@ class BiomarkersPanel extends React.Component + ) => { + const { intl } = this.props + + const biomarkerTagToSend: BiomarkerTag = { + tag: tagId + } + + ky.patch( + `${urlBiomarkersSimpleUpdate}/${biomarker.id}/`, + { + headers: getDjangoHeader(), + json: biomarkerTagToSend, + timeout: REQUEST_TIMEOUT + } + ).catch((err) => { + console.error('Error updating Biomarker tag ->', err) + + this.setState(prevState => ({ + alert: { + ...prevState.alert, + isOpen: true, + type: CustomAlertTypes.ERROR, + message: intl.formatMessage({ + id: 'biomarkersPanel.alert.errorEditing' + }) + } + })) + }) + } + /** * Does a request to add a new Tag */ @@ -1832,7 +1866,7 @@ class BiomarkersPanel extends React.Component { - requestMethod(addOrEditURL, { headers: myHeaders, json: this.state.newTag }).then((response) => { + requestMethod(addOrEditURL, { headers: myHeaders, json: { ...this.state.newTag, type: TagType.EXPERIMENT } }).then((response) => { this.setState({ addingTag: false }) response.json().then((responseJSON: DjangoTag) => { if (responseJSON && responseJSON.id) { @@ -1866,14 +1900,6 @@ class BiomarkersPanel extends React.Component { - const id = tag.id as number - return { key: id, value: id, text: tag.name } - }) - - tagOptions.unshift({ key: 'no_tag', text: 'No tag' }) return ( {/* Biomarker deletion modal */} @@ -1882,39 +1908,11 @@ class BiomarkersPanel extends React.Component - - - - - - this.handleCancelConfirmModalState()} - onConfirm={() => { - this.handleCancelConfirmModalState() - this.state.confirmModal.onConfirm() - }} - /> - - - {/* Files overview panel */} + + {/* Biomarkers overview panel */} @@ -1963,7 +1961,17 @@ class BiomarkersPanel extends React.Component - + + } + tagType={TagType.EXPERIMENT} + onTagSelect={(tagId) => this.updateBiomarkerTag(biomarker, tagId)} + onTagCreated={() => this.getUserTags()} + onTagEdited={() => this.getUserTags()} + onTagDeleted={() => this.getUserTags()} + /> + @@ -2158,13 +2166,6 @@ class BiomarkersPanel extends React.Component { - const id = tag.id as number - return { key: id, value: id, text: tag.name } - }) - ]} /> )} diff --git a/src/frontend/static/frontend/src/components/biomarkers/modalContentBiomarker/manualForm/ManualForm.tsx b/src/frontend/static/frontend/src/components/biomarkers/modalContentBiomarker/manualForm/ManualForm.tsx index 6fe5765c..46aac2af 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/modalContentBiomarker/manualForm/ManualForm.tsx +++ b/src/frontend/static/frontend/src/components/biomarkers/modalContentBiomarker/manualForm/ManualForm.tsx @@ -1,14 +1,13 @@ -import React from 'react' -import { DropdownItemProps, Grid } from 'semantic-ui-react' +import React from 'react' +import { Grid } from 'semantic-ui-react' import { BiomarkerType, FormBiomarkerData, MoleculesSectionData, MoleculesTypeOfSelection } from './../../types' import { NewBiomarkerForm } from './newBiomarkerForm/NewBiomarkerForm' import { MoleculesSectionsContainer } from './MoleculeSectionContainer' import { DjangoTag } from '../../../../utils/django_interfaces' -/** ManualForm's props. */ -interface ManualFormProps { - tagOptions: DropdownItemProps[] - tags: DjangoTag[] +/** ManualForm's props. */ +interface ManualFormProps { + tags: DjangoTag[] biomarkerForm: FormBiomarkerData, /** Value for Checkbox. */ checkedIgnoreProposedAlias: boolean, @@ -49,12 +48,11 @@ export const ManualForm = (props: ManualFormProps) => { handleGenesSymbolsFinder={props.handleGenesSymbolsFinder} handleGenesSymbols={props.handleGenesSymbols} handleChangeConfirmModalState={props.handleChangeConfirmModalState} - handleValidateForm={props.handleValidateForm} - handleSendForm={props.handleSendForm} - handleChangeCheckBox={props.handleChangeCheckBox} - tagOptions={props.tagOptions} - tags={props.tags} - /> + handleValidateForm={props.handleValidateForm} + handleSendForm={props.handleSendForm} + handleChangeCheckBox={props.handleChangeCheckBox} + tags={props.tags} + /> void, @@ -61,8 +61,16 @@ interface NewBiomarkerFormProps { * @returns Component */ export const NewBiomarkerForm = (props: NewBiomarkerFormProps) => { + const intl = useIntl() const { haveInvalid, haveAmbiguous } = props.handleValidateForm() const canEditMolecules = props.biomarkerForm.canEditMolecules + const tagOptions: DropdownItemProps[] = [ + { key: 'no_tag', value: '', text: intl.formatMessage({ id: 'biomarkersPanel.tags.noTag' }) }, + ...props.tags.map((tag) => { + const id = tag.id as number + return { key: id, value: id, text: tag.name } + }) + ] return ( @@ -116,7 +124,7 @@ export const NewBiomarkerForm = (props: NewBiomarkerFormProps) => { , + /** + * Element on which the Dropdown is rendered when clicked. In a table it'll be the Tag + * that's shown in the column, in a form it'll be a Button + */ + trigger: React.ReactNode, + /** + * Type of Tag to fetch/create (Tags are different for Files and Experiments, see `TagType`). + * If not specified, every Tag of the user is fetched + */ + tagType?: TagType, + /** + * Executed when the user confirms a (possibly new) Tag selection by clicking "Save". + * Receives `null` when the user clears the assigned Tag. + * NOTE: this callback is not part of the two params described in the ticket, but it's needed + * so the parent component (which owns the File/Experiment being tagged) can persist the change. + * It's optional so the component still satisfies the original contract on its own. + */ + onTagSelect?: (tagId: Nullable) => void + /** + * Executed after a new Tag is successfully created from this Dropdown. + * Lets parent components refresh their own Tag lists, such as table filters. + */ + onTagCreated?: (newTag: DjangoTag) => void + /** + * Executed after an existing Tag is successfully edited from this Dropdown. + * Lets parent components refresh their own Tag lists, such as table filters. + */ + onTagEdited?: (editedTag: DjangoTag) => void + /** + * Executed after an existing Tag is successfully deleted from this Dropdown. + * Lets parent components refresh their own Tag lists, such as table filters. + */ + onTagDeleted?: (deletedTagId: number) => void +} + +/** + * Renders a Dropdown to select a Tag (with search) and to add a new one, replacing the old + * Tag's CRUD UX (TagsPanel/ExperimentTagInfo/LastExperimentTagInfo). + * @param props Component's props + * @returns Component + */ +export const TagDropdown = (props: TagDropdownProps) => { + const [open, setOpen] = useState(false) + const [view, setView] = useState<'list' | 'new'>('list') + const [tags, setTags] = useState([]) + const [loadingTags, setLoadingTags] = useState(false) + const [searchTerm, setSearchTerm] = useState('') + const [selectedId, setSelectedId] = useState>(props.selectedTagId) + const [newTag, setNewTag] = useState(getDefaultNewTag()) + const [addingTag, setAddingTag] = useState(false) + const [menuPosition, setMenuPosition] = useState<{ top: number, left: number }>({ top: 0, left: 0 }) + + const triggerRef = useRef(null) + const abortController = useRef(new AbortController()) + + /** Fetches every Tag available for this TagType from the backend */ + const getTags = () => { + setLoadingTags(true) + + getUserTagsByType(props.tagType, abortController.current.signal) + .then((tagsResponse) => { + setTags(tagsResponse) + }) + .catch((err) => { + if (!abortController.current.signal.aborted) { + console.log('Error getting Tags ->', err) + } + }) + .finally(() => setLoadingTags(false)) + } + + /** Fetches all the Tags as soon as the component is mounted. */ + useEffect(() => { + getTags() + + return () => abortController.current.abort() + }, []) + + /** Discards any unsaved "New tag" draft when the panel is closed */ + const handleClose = () => { + setOpen(false) + setView('list') + setNewTag(getDefaultNewTag()) + } + + /** + * Keeps the internal selection in sync if the parent updates the Tag from outside + * while the panel is closed (e.g. after a refresh of the underlying data). + */ + useEffect(() => { + if (!open) { + setSelectedId(props.selectedTagId) + } + }, [open, props.selectedTagId]) + + /** Resets the panel's internal state every time it's opened */ + const handleOpen = () => { + if (triggerRef.current) { + const rect = triggerRef.current.getBoundingClientRect() + setMenuPosition({ top: rect.bottom, left: rect.left }) + } + + setSelectedId(props.selectedTagId) + setSearchTerm('') + setView('list') + setOpen(true) + } + + /** + * Confirms the current selection and closes the panel. + * @param e Click event from the Save button. + */ + const handleSave = (e?: React.MouseEvent) => { + e?.preventDefault() + e?.stopPropagation() + + if (props.onTagSelect) { + props.onTagSelect(selectedId) + } + + handleClose() + } + + /** + * Discards any pending change made during this opening of the Dropdown. + */ + const handleResetSelection = () => { + setSelectedId(props.selectedTagId) + } + + /** + * Handles changes for the "New tag" form fields (name/description) + * @param name Field name to update. + * @param value New field value. + */ + const handleNewTagInputsChange = (name: string, value: any) => { + setNewTag((prevNewTag) => ({ ...prevNewTag, [name]: value })) + } + + /** + * Goes back to the Tags list discarding the current "New tag" draft. + */ + const goBackToList = () => { + setNewTag(getDefaultNewTag()) + setView('list') + } + + /** + * Submits the "New tag" form on Enter, cancels it on Escape + * @param e Keyboard event from the Tag form. + */ + const handleNewTagKeyDown = (e: React.KeyboardEvent) => { + e.stopPropagation() + + if (e.key === 'Enter') { + createTag() + } else if (e.key === 'Escape') { + goBackToList() + } + } + + /** + * Opens the Tag form with the selected Tag's data. + * @param e Click event from the edit icon. + * @param tag Tag to edit. + */ + const editTag = (e: React.MouseEvent, tag: DjangoTag) => { + e.preventDefault() + e.stopPropagation() + setNewTag(copyObject(tag)) + setView('new') + } + + /** + * Deletes a Tag and removes it from the local list. + * @param e Click event from the delete icon. + * @param tag Tag to delete. + */ + const deleteTag = (e: React.MouseEvent, tag: DjangoTag) => { + // Keeps the icon click from selecting the row or closing the Semantic UI Dropdown. + e.preventDefault() + e.stopPropagation() + + if (!tag.id) { + return + } + + ky.delete(`${urlTagsCRUD}${tag.id}/`, { headers: getDjangoHeader() }) + .then(() => { + setTags((prevTags) => prevTags.filter((currentTag) => currentTag.id !== tag.id)) + + if (selectedId === tag.id) { + setSelectedId(null) + } + + if (props.onTagDeleted) { + props.onTagDeleted(tag.id as number) + } + }) + .catch((err) => { + alertGeneralError() + console.log('Error deleting Tag ->', err) + }) + } + + /** + * Sends the request to create or update a Tag and selects it as soon as it's saved. + */ + const createTag = () => { + console.log('Creating tag with data ->', newTag) + + if (addingTag || !newTag.name.trim()) { + return + } + + const tagToCreate: DjangoTag = { ...newTag, type: props.tagType ?? newTag.type } + const isEditing = newTag.id !== null + const request = isEditing + ? ky.patch(`${urlTagsCRUD}${newTag.id}/`, { headers: getDjangoHeader(), json: tagToCreate }) + : ky.post(urlTagsCRUD, { headers: getDjangoHeader(), json: tagToCreate }) + + setAddingTag(true) + + request + .then((response) => { + response.json().then((savedTag) => { + if (savedTag && savedTag.id) { + setTags((prevTags) => { + if (isEditing) { + return prevTags.map((currentTag) => currentTag.id === savedTag.id ? savedTag : currentTag) + } + + return [...prevTags, savedTag] + }) + setSelectedId(savedTag.id) + setNewTag(getDefaultNewTag()) + setView('list') + + if (isEditing && props.onTagEdited) { + props.onTagEdited(savedTag) + } else if (!isEditing && props.onTagCreated) { + props.onTagCreated(savedTag) + } + } + }).catch((err) => { + alertGeneralError() + console.log('Error parsing the saved Tag JSON ->', err) + }) + }) + .catch((err) => { + alertGeneralError() + console.log('Error saving Tag ->', err) + }) + .finally(() => setAddingTag(false)) + } + + const filteredTags = tags.filter((tag) => tag.name.toLowerCase().includes(searchTerm.toLowerCase())) + const hasPendingChanges = selectedId !== props.selectedTagId + const dropdownMenuStyle = { + position: 'fixed' as const, + top: menuPosition.top, + left: menuPosition.left, + width: 320, + maxWidth: 320, + zIndex: 9999 + } + + /** + * Handles key presses inside the list search input. + * @param e Keyboard event from the search input. + */ + const handleSearchKeyDown = (e: React.KeyboardEvent) => { + e.stopPropagation() + + if (e.key === 'Enter') { + handleSave() + } + } + + /** + * Handles keyboard shortcuts bubbling to the menu. + * @param e Keyboard event from the menu. + */ + const handleMenuKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Tab') { + e.stopPropagation() + + return + } + + if (e.key !== 'Enter') { + e.stopPropagation() + + return + } + + e.stopPropagation() + + if (e.target instanceof HTMLElement && e.target.tagName === 'BUTTON') { + return + } + + if (view === 'list') { + handleSave() + } else { + createTag() + } + } + + /** + * Renders the "Tags" list/search view. + * @returns Tags list/search view. + */ + const renderTagsListView = () => ( + <> +
+ Tags + + setView('new')}> + New tag + +
+ +
+ setSearchTerm(value)} + onKeyDown={handleSearchKeyDown} + /> +
+ +
+ {loadingTags && tags.length === 0 + ? + : ( + + { + e.stopPropagation() + setSelectedId(null) + }} + > +
+ + + No tag + +
+
+ + + {filteredTags.map((tag) => { + const isSelected = tag.id === selectedId + console.log('Rendering tag ->', tag, 'isSelected ->', isSelected) + return ( + { + e.stopPropagation() + setSelectedId(tag.id) + }} + > +
+ + + {tag.name} + +
+ editTag(e, tag)} + /> + deleteTag(e, tag)} + /> +
+
+
+ ) + })} + + {(!loadingTags && filteredTags.length === 0) && + No Tags found} + + )} +
+ +
+ + +
+ + ) + + /** + * Renders the "New tag" form view. + * @returns New Tag form view. + */ + const newTagView = ( + <> +
+ {newTag.id === null ? 'New tag' : 'Edit tag'} + +
+ +
+ +
+ +
+ +
+ + ) + + return ( + {props.trigger}} + icon={null} + open={open} + closeOnBlur + onOpen={handleOpen} + onClose={handleClose} + onKeyDown={(e) => { + if (e.key === 'Tab') { + e.stopPropagation() + } + }} + className='tag-dropdown' + > + e.stopPropagation()} + onKeyDown={handleMenuKeyDown} + > + {view === 'list' ? renderTagsListView() : newTagView} + + + ) +} diff --git a/src/frontend/static/frontend/src/components/common/TagForm.tsx b/src/frontend/static/frontend/src/components/common/TagForm.tsx index 1bee84d1..7cde6223 100644 --- a/src/frontend/static/frontend/src/components/common/TagForm.tsx +++ b/src/frontend/static/frontend/src/components/common/TagForm.tsx @@ -12,7 +12,8 @@ interface TagFormProps { disableInputs: boolean, loading: boolean, onHandleKeyDown: (any) => void, - onHandleAddTagInputsChange: (name: string, value: any) => void + onHandleAddTagInputsChange: (name: string, value: any) => void, + tabIndexStart?: number } /** @@ -38,6 +39,7 @@ export const TagForm = (props: TagFormProps) => { disabled={props.disableInputs} placeholder={intl.formatMessage({ id: 'tagForm.newTag' })} maxLength={20} + tabIndex={props.tabIndexStart} /> { disabled={props.disableInputs} placeholder={intl.formatMessage({ id: 'common.descriptionOptional' })} maxLength={60} + tabIndex={props.tabIndexStart !== undefined ? props.tabIndexStart + 1 : undefined} />
) diff --git a/src/frontend/static/frontend/src/components/files-manager/FilesManager.tsx b/src/frontend/static/frontend/src/components/files-manager/FilesManager.tsx index 04760807..9b3827dc 100644 --- a/src/frontend/static/frontend/src/components/files-manager/FilesManager.tsx +++ b/src/frontend/static/frontend/src/components/files-manager/FilesManager.tsx @@ -1,22 +1,22 @@ import React from 'react' import { Base } from '../Base' import { Grid, Header, Button, Modal, DropdownItemProps, Table, Icon } from 'semantic-ui-react' -import { DjangoTag, DjangoUserFile, TagType, DjangoInstitution, DjangoInstitutionSimple, DjangoMethylationPlatform, DjangoResponseUploadUserFileError, DjangoUserFileUploadErrorInternalCode, DjangoSurvivalColumnsTupleSimple, RowHeader, DjangoTissue } from '../../utils/django_interfaces' +import { DjangoTag, DjangoUserFile, TagType, DjangoInstitution, DjangoMethylationPlatform, DjangoResponseUploadUserFileError, DjangoUserFileUploadErrorInternalCode, DjangoSurvivalColumnsTupleSimple, RowHeader, DjangoTissue, DjangoInstitutionSimple } from '../../utils/django_interfaces' import ky, { HTTPError } from 'ky' -import { getDjangoHeader, alertGeneralError, getFileTypeSelectOptions, getDefaultNewTag, copyObject, formatDateLocale, getFileTypeName, getInputFileCSVColumns } from '../../utils/util_functions' -import { TagsPanel } from './TagsPanel' +import { getDjangoHeader, alertGeneralError, getFileTypeSelectOptions, formatDateLocale, getFileTypeName, getInputFileCSVColumns, getUserTagsByType, getDefaultNewTag } from '../../utils/util_functions' import { FileType, Nullable } from '../../utils/interfaces' import { NewFileForm } from './NewFileForm' import { startUpload, UploadState } from '../../utils/file_uploader' import { PaginatedTable, PaginationCustomFilter } from '../common/PaginatedTable' import { TableCellWithTitle } from '../common/TableCellWithTitle' import { TagLabel } from '../common/TagLabel' +import { TagDropdown } from '../common/TagDropdown' import { PopupIcons } from '../common/PopupIcons' import { SwitchPublicButton } from '../common/SwitchPublicButton' import { useIntl, IntlShape } from 'react-intl' import { DeleteButton } from '../common/DeleteButton' -import { getTissueDropdownOptions, getTissueIds, TissueLabels } from '../common/TissueLabels' -import { SharedInstitutionsUserFile } from './SharedInstitutionsUserFile' +import { getTissueDropdownOptions, getTissueIds, TissueLabels } from '../common/TissueLabels' +import { SharedInstitutionsUserFile } from './SharedInstitutionsUserFile' /** Structure returned from the chunk upload service. */ type UploadResponse = { @@ -27,15 +27,14 @@ type UploadResponse = { } // URLs defined in files.html -declare const urlTagsCRUD: string declare const urlUserFilesCRUD: string declare const urlUserInstitutions: string declare const urlTissuesCRUD: string declare const urlChunkUpload: string declare const urlChunkUploadComplete: string -declare const downloadFileURL: string -declare const downloadFileHeaders: string -declare const currentUserId: string +declare const downloadFileURL: string +declare const downloadFileHeaders: string +declare const currentUserId: string /** * New File Form fields @@ -47,12 +46,12 @@ interface NewFile { newFileDescription: string, newFileType: FileType, isCpGSiteId: boolean, - platform: DjangoMethylationPlatform, - newTag: Nullable, - institutions: number[], - tissue: Nullable, - survivalColumns: DjangoSurvivalColumnsTupleSimple[] -} + platform: DjangoMethylationPlatform, + newTag: Nullable, + institutions: number[], + tissue: Nullable, + survivalColumns: DjangoSurvivalColumnsTupleSimple[] +} /** * Component's state @@ -61,24 +60,24 @@ interface FilesManagerState { tags: DjangoTag[], files: DjangoUserFile[], userInstitutions: DjangoInstitution[], - tissues: DjangoTissue[], - newTag: DjangoTag, - showDeleteTagModal: boolean, showDeleteFileModal: boolean, - selectedTagToDelete: Nullable, selectedFileToDelete: Nullable, - deletingTag: boolean, deletingFile: boolean, uploadingFile: boolean, newFile: NewFile, - addingTag: boolean, uploadPercentage: number, uploadState: Nullable - /** posibles values for survival tuple */ - survivalTuplesPossiblesValues: string[], - selectedFileToShare: Nullable, - showShareInstitutionsModal: boolean, - sharedInstitutionsByFile: { [fileId: number]: DjangoInstitutionSimple[] }, + /** Possible values for survival tuple */ + survivalTuplesPossiblesValues: string[], + tissues: DjangoTissue[], + newTag: DjangoTag, + showDeleteTagModal: boolean, + selectedTagToDelete: Nullable, + deletingTag: boolean, + addingTag: boolean, + selectedFileToShare: Nullable, + showShareInstitutionsModal: boolean, + sharedInstitutionsByFile: { [fileId: number]: DjangoInstitutionSimple[] }, } /** @@ -102,23 +101,23 @@ class FilesManager extends React.Component tags: [], files: [], userInstitutions: [], - tissues: [], - newTag: getDefaultNewTag(), - showDeleteTagModal: false, showDeleteFileModal: false, - selectedTagToDelete: null, selectedFileToDelete: null, - deletingTag: false, deletingFile: false, uploadingFile: false, newFile: this.getDefaultNewFile(), - addingTag: false, uploadPercentage: 0, - uploadState: null, - survivalTuplesPossiblesValues: [], - selectedFileToShare: null, - showShareInstitutionsModal: false, - sharedInstitutionsByFile: {} + uploadState: null, + survivalTuplesPossiblesValues: [], + tissues: [], + newTag: getDefaultNewTag(), + showDeleteTagModal: false, + selectedTagToDelete: null, + deletingTag: false, + addingTag: false, + selectedFileToShare: null, + showShareInstitutionsModal: false, + sharedInstitutionsByFile: {} } } @@ -140,12 +139,12 @@ class FilesManager extends React.Component newFileName: this.getFileInputLabelTranslated(), newFileNameUser: '', newFileDescription: '', - newFileType: FileType.MRNA, - newTag: null, - institutions: [], - tissue: null, - isCpGSiteId: false, - platform: DjangoMethylationPlatform.PLATFORM_450, + newFileType: FileType.MRNA, + newTag: null, + institutions: [], + tissue: null, + isCpGSiteId: false, + platform: DjangoMethylationPlatform.PLATFORM_450, survivalColumns: [] } } @@ -242,100 +241,13 @@ class FilesManager extends React.Component * Fetches the User's defined tags */ getUserTags () { - // Gets only File's Tags - const searchParams = { - type: TagType.FILE - } - - ky.get(urlTagsCRUD, { searchParams, signal: this.abortController.signal }).then((response) => { - response.json().then((tags) => { - this.setState({ tags }) - }).catch((err) => { - console.log('Error parsing JSON ->', err) - }) + getUserTagsByType(TagType.FILE, this.abortController.signal).then((tags) => { + this.setState({ tags }) }).catch((err) => { console.log("Error getting user's tags ->", err) }) } - /** - * Selects a new Tag to edit - * @param selectedTag Tag to edit - */ - editTag = (selectedTag: DjangoTag) => { this.setState({ newTag: copyObject(selectedTag) }) } - - /** - * Does a request to add a new Tag - */ - addOrEditTag () { - if (this.state.addingTag) { - return - } - - // Sets the Request's Headers - const myHeaders = getDjangoHeader() - - // If exists an id then we are editing, otherwise It's a new Tag - let addOrEditURL, requestMethod: typeof ky.post | typeof ky.patch - - if (this.state.newTag.id !== null) { - addOrEditURL = `${urlTagsCRUD}${this.state.newTag.id}/` - requestMethod = ky.patch - } else { - addOrEditURL = urlTagsCRUD - requestMethod = ky.post - } - - this.setState({ addingTag: true }, () => { - requestMethod(addOrEditURL, { headers: myHeaders, json: this.state.newTag }).then((response) => { - this.setState({ addingTag: false }) - response.json().then((responseJSON) => { - if (responseJSON && responseJSON.id) { - // If all is OK, resets the form and gets the User's tag to refresh the list - this.setState({ newTag: getDefaultNewTag() }) - this.getUserTags() - } - }).catch((err) => { - alertGeneralError() - console.log('Error parsing JSON ->', err) - }) - }).catch((err) => { - this.setState({ addingTag: false }) - alertGeneralError() - console.log('Error adding new Tag ->', err) - }) - }) - } - - /** - * Makes a request to delete a Tag - */ - deleteTag = () => { - if (this.state.selectedTagToDelete === null) { - return - } - - // Sets the Request's Headers - const myHeaders = getDjangoHeader() - const deleteURL = `${urlTagsCRUD}${this.state.selectedTagToDelete.id}` - this.setState({ deletingTag: true }, () => { - ky.delete(deleteURL, { headers: myHeaders }).then((response) => { - // If OK is returned refresh the tags - if (response.ok) { - this.setState({ - deletingTag: false, - showDeleteTagModal: false - }) - this.getUserTags() - } - }).catch((err) => { - this.setState({ deletingTag: false }) - alertGeneralError() - console.log('Error deleting Tag ->', err) - }) - }) - } - /** * Makes a request to delete a File */ @@ -370,90 +282,86 @@ class FilesManager extends React.Component } /** - * Handles New Tag Input changes - * @param name State field to change - * @param value Value to assign to the specified field + * Show a modal to confirm a File deletion + * @param file Selected Tag to delete */ - handleAddTagInputsChange = (name: string, value) => { - const newTag = this.state.newTag - newTag[name] = value - this.setState(prevState => ({ - newTag: { - ...prevState.newTag, - [name]: value, - } - })) + confirmFileDeletion = (file: DjangoUserFile) => { + this.setState({ + selectedFileToDelete: file, + showDeleteFileModal: true + }) } /** - * Handles New Tag Input Key Press - * @param e Event of change + * Opens the institution-sharing modal for a dataset. + * @param file Dataset whose sharing configuration will be managed. */ - handleKeyDown = (e) => { - // If pressed Enter key submits the new Tag - if (e.which === 13 || e.keyCode === 13) { - this.addOrEditTag() - } else { - if (e.which === 27 || e.keyCode === 27) { - this.setState({ newTag: getDefaultNewTag() }) - } - } + openShareInstitutions = (file: DjangoUserFile) => { + this.setState({ + selectedFileToShare: file, + showShareInstitutionsModal: true + }) } - /** - * Show a modal to confirm a Tag deletion - * @param tag Selected Tag to delete - */ - confirmTagDeletion = (tag: DjangoTag) => { + /** Closes the institution-sharing modal and clears the selected dataset. */ + closeShareInstitutions = () => { this.setState({ - selectedTagToDelete: tag, - showDeleteTagModal: true + selectedFileToShare: null, + showShareInstitutionsModal: false }) } /** - * Show a modal to confirm a File deletion - * @param file Selected Tag to delete + * Persists a Tag change for an existing UserFile. + * @param userFile UserFile to update. + * @param tagId New Tag id, or null to clear it. */ - confirmFileDeletion = (file: DjangoUserFile) => { - this.setState({ - selectedFileToDelete: file, - showDeleteFileModal: true + updateUserFileTag = (userFile: DjangoUserFile, tagId: Nullable) => { + const formData = new FormData() + formData.append('name', userFile.name) + formData.append('description', userFile.description ?? '') + formData.append('file_type', userFile.file_type.toString()) + formData.append('is_cpg_site_id', userFile.is_cpg_site_id.toString()) + + if (userFile.is_cpg_site_id && userFile.platform) { + formData.append('platform', userFile.platform.toString()) + } + + userFile.institutions.forEach((institution) => { + formData.append('institutions', institution.id.toString()) }) - } - - /** - * Opens the institution-sharing modal for a dataset. - * @param file Dataset whose sharing configuration will be managed. - */ - openShareInstitutions = (file: DjangoUserFile) => { - this.setState({ - selectedFileToShare: file, - showShareInstitutionsModal: true - }) - } - - /** Closes the institution-sharing modal and clears the selected dataset. */ - closeShareInstitutions = () => { - this.setState({ - selectedFileToShare: null, - showShareInstitutionsModal: false - }) - } - - /** - * Updates the table cache after the shared institutions of a dataset change. - * @param fileId Dataset identifier. - * @param institutions Current institutions shared with the dataset. - */ - updateSharedInstitutions = (fileId: number, institutions: DjangoInstitutionSimple[]) => { - this.setState(prevState => ({ - sharedInstitutionsByFile: { - ...prevState.sharedInstitutionsByFile, - [fileId]: institutions - } - })) - } + + if (userFile.survival_columns && userFile.survival_columns.length > 0) { + formData.append('survival_columns', JSON.stringify(userFile.survival_columns)) + } + + if (tagId !== null) { + formData.append('tag', tagId.toString()) + } + + ky.patch(`${urlUserFilesCRUD}${userFile.id}/`, { + headers: getDjangoHeader(), + body: formData, + timeout: false + }).catch((err) => { + alertGeneralError() + console.log('Error updating UserFile Tag ->', err) + }) + } + + /** + * Updates the table cache after the shared institutions of a dataset change. + * @param fileId Dataset identifier. + * @param institutions Current institutions shared with the dataset. + */ + updateSharedInstitutions = (fileId: number, institutions: DjangoInstitutionSimple[]) => { + this.setState(prevState => ({ + sharedInstitutionsByFile: { + ...prevState.sharedInstitutionsByFile, + [fileId]: institutions + } + })) + } /** * Closes the deletion confirm modals @@ -581,10 +489,10 @@ class FilesManager extends React.Component formData.append('institutions', institutionId.toString()) }) - // Adds the Tissue's id, if selected - if (newFileForm.tissue) { - formData.append('tissue', newFileForm.tissue.toString()) - } + // Adds the Tissue's id, if selected + if (newFileForm.tissue) { + formData.append('tissue', newFileForm.tissue.toString()) + } // Adds the survival columns tuples, if needed if (newFileForm.survivalColumns.length > 0) { @@ -640,35 +548,6 @@ class FilesManager extends React.Component }) } - /** - * Generates the modal to confirm a Tag deletion - * @returns Modal component. Null if no Tag was selected to delete - */ - getTagDeletionConfirmModals () { - const { intl } = this.props - - if (!this.state.selectedTagToDelete) { - return null - } - - return ( - -
- -

{intl.formatMessage({ id: 'files.manager.delete.tag.confirm' }, { tagName: this.state.selectedTagToDelete.name })}

-
- - - - - - ) - } - /** * Generates the modal to confirm a File deletion * @returns Modal component. Null if no File was selected to delete @@ -722,11 +601,11 @@ class FilesManager extends React.Component newFileDescription: fileToEdit.description ?? '', newTag: fileToEdit.tag ? fileToEdit.tag.id : null, isCpGSiteId: fileToEdit.is_cpg_site_id, - platform: fileToEdit.platform ? fileToEdit.platform : DjangoMethylationPlatform.PLATFORM_450, - institutions: fileToEdit.institutions.map((institution) => institution.id), - tissue: getTissueIds(fileToEdit.tissue)[0] ?? null, - survivalColumns: fileToEdit.survival_columns ?? [] - } + platform: fileToEdit.platform ? fileToEdit.platform : DjangoMethylationPlatform.PLATFORM_450, + institutions: fileToEdit.institutions.map((institution) => institution.id), + tissue: getTissueIds(fileToEdit.tissue)[0] ?? null, + survivalColumns: fileToEdit.survival_columns ?? [] + } }) }).catch((err) => { console.log('Error parsing JSON ->', err) @@ -745,11 +624,11 @@ class FilesManager extends React.Component newFileDescription: fileToEdit.description ?? '', newTag: fileToEdit.tag ? fileToEdit.tag.id : null, isCpGSiteId: fileToEdit.is_cpg_site_id, - platform: fileToEdit.platform ? fileToEdit.platform : DjangoMethylationPlatform.PLATFORM_450, - institutions: fileToEdit.institutions.map((institution) => institution.id), - tissue: getTissueIds(fileToEdit.tissue)[0] ?? null, - survivalColumns: fileToEdit.survival_columns ?? [] - } + platform: fileToEdit.platform ? fileToEdit.platform : DjangoMethylationPlatform.PLATFORM_450, + institutions: fileToEdit.institutions.map((institution) => institution.id), + tissue: getTissueIds(fileToEdit.tissue)[0] ?? null, + survivalColumns: fileToEdit.survival_columns ?? [] + } }) } } @@ -835,12 +714,12 @@ class FilesManager extends React.Component { key: 'private', text: intl.formatMessage({ id: 'files.manager.filter.visibility.private' }), value: 'private' } ] - const institutionsOptions: DropdownItemProps[] = this.state.userInstitutions.map((institution) => { - return { key: institution.id, value: institution.id, text: institution.name } - }) - const tissueOptions = getTissueDropdownOptions(this.state.tissues) - - return [ + const institutionsOptions: DropdownItemProps[] = this.state.userInstitutions.map((institution) => { + return { key: institution.id, value: institution.id, text: institution.name } + }) + const tissueOptions = getTissueDropdownOptions(this.state.tissues) + + return [ { label: intl.formatMessage({ id: 'files.manager.table.tag' }), keyForServer: 'tag', @@ -857,24 +736,24 @@ class FilesManager extends React.Component clearable: false, width: 2 }, - { - label: intl.formatMessage({ id: 'files.manager.filter.institutions.label' }), - keyForServer: 'institutions', - defaultValue: '', - options: institutionsOptions, - disabledFunction: (actualValues) => actualValues.visibility === 'private', - width: 3 - }, - { - label: 'Tissue', - keyForServer: 'tissue', - defaultValue: '', - placeholder: 'Select tissue', - options: tissueOptions, - width: 3 - }, - { - label: intl.formatMessage({ id: 'files.manager.filter.fileType.label' }), + { + label: intl.formatMessage({ id: 'files.manager.filter.institutions.label' }), + keyForServer: 'institutions', + defaultValue: '', + options: institutionsOptions, + disabledFunction: (actualValues) => actualValues.visibility === 'private', + width: 3 + }, + { + label: 'Tissue', + keyForServer: 'tissue', + defaultValue: '', + placeholder: 'Select tissue', + options: tissueOptions, + width: 3 + }, + { + label: intl.formatMessage({ id: 'files.manager.filter.fileType.label' }), keyForServer: 'file_type', defaultValue: FileType.ALL, options: getFileTypeSelectOptions(), @@ -885,12 +764,11 @@ class FilesManager extends React.Component } render () { - // Tag and File deletion modals + // File deletion modal const { intl } = this.props - const tagDeletionConfirmModal = this.getTagDeletionConfirmModals() - const fileDeletionConfirmModal = this.getFileDeletionConfirmModals() - const selectedFileToShare = this.state.selectedFileToShare + const fileDeletionConfirmModal = this.getFileDeletionConfirmModals() const fileTypeOptions = getFileTypeSelectOptions(false) + const selectedFileToShare = this.state.selectedFileToShare const tagOptions: DropdownItemProps[] = this.state.tags.map((tag) => { const id = tag.id as number return { key: id, value: id, text: tag.name } @@ -901,26 +779,23 @@ class FilesManager extends React.Component const institutionsOptions: DropdownItemProps[] = this.state.userInstitutions.map((institution) => { return { key: institution.id, value: institution.id, text: institution.name } }) - const tissueFormOptions = getTissueDropdownOptions(this.state.tissues, true) + const tissueFormOptions = getTissueDropdownOptions(this.state.tissues, true) return ( - {/* Tag deletion modal */} - {tagDeletionConfirmModal} - - {/* File deletion modal */} - {fileDeletionConfirmModal} - - {selectedFileToShare && ( - this.updateSharedInstitutions(selectedFileToShare.id as number, institutions)} - /> - )} + {/* File deletion modal */} + {fileDeletionConfirmModal} + + {selectedFileToShare && ( + this.updateSharedInstitutions(selectedFileToShare.id as number, institutions)} + /> + )} @@ -931,7 +806,7 @@ class FilesManager extends React.Component fileTypeOptions={fileTypeOptions} tagOptions={tagOptions} institutionsOptions={institutionsOptions} - tissueOptions={tissueFormOptions} + tissueOptions={tissueFormOptions} uploadingFile={this.state.uploadingFile} uploadPercentage={this.state.uploadPercentage} uploadState={this.state.uploadState} @@ -945,16 +820,6 @@ class FilesManager extends React.Component removeSurvivalFormTuple={this.removeSurvivalFormTuple} survivalTuplesPossiblesValues={this.state.survivalTuplesPossiblesValues} /> - - {/* Files overview panel */} @@ -972,27 +837,56 @@ class FilesManager extends React.Component searchPlaceholder={intl.formatMessage({ id: 'files.manager.search.placeholder' })} urlToRetrieveData={urlUserFilesCRUD} updateWSKey='update_user_files' - mapFunction={(userFileRow: DjangoUserFile) => ( - + mapFunction={(userFileRow: DjangoUserFile) => ( + {getFileTypeName(userFileRow.file_type)} - - {(this.state.sharedInstitutionsByFile[userFileRow.id as number] ?? userFileRow.institutions).length > 0 - ? ( - i.name).join(', ') } - )} - /> - ) - : '-'} - - + + {userFileRow.institutions.length > 0 && ( + i.name).join(', ') } + )} + /> + )} + + + } + tagType={TagType.FILE} + onTagSelect={(tagId) => this.updateUserFileTag(userFileRow, tagId)} + onTagCreated={() => this.getUserTags()} + onTagEdited={() => this.getUserTags()} + onTagDeleted={(deletedTagId) => { + if (this.state.newFile.newTag === deletedTagId) { + this.handleAddFileInputsChange('newTag', null) + } + + this.getUserTags() + }} + /> + + + {(this.state.sharedInstitutionsByFile[userFileRow.id as number] ?? userFileRow.institutions).length > 0 + ? ( + i.name).join(', ') } + )} + /> + ) + : '-'} + + { @@ -1015,25 +909,25 @@ class FilesManager extends React.Component {/* Extra information: */} - - {Number(currentUserId) === userFileRow.user.id && ( - this.openShareInstitutions(userFileRow)} - /> - )} - {/* Users can modify or delete own files or the ones which belongs to an + /> + {Number(currentUserId) === userFileRow.user.id && ( + this.openShareInstitutions(userFileRow)} + /> + )} + {/* Users can modify or delete own files or the ones which belongs to an Institution which the user is admin of */} {userFileRow.is_private_or_institution_admin && ( <> diff --git a/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx b/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx index fe1c8f62..92e482e2 100644 --- a/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx +++ b/src/frontend/static/frontend/src/components/pipeline/experiment-result/BiomarkerFromCorrelationModal.tsx @@ -1,9 +1,9 @@ import React from 'react' // Update the import path to the correct location of Base component import { Modal, DropdownItemProps, Icon, Form, Button, Confirm } from 'semantic-ui-react' -import { DjangoCGDSStudy, DjangoMRNAxGEMResultRow, DjangoSurvivalColumnsTupleSimple, DjangoTag, DjangoUserFile } from '../../../utils/django_interfaces' +import { DjangoCGDSStudy, DjangoMRNAxGEMResultRow, DjangoSurvivalColumnsTupleSimple, DjangoTag, DjangoUserFile, TagType } from '../../../utils/django_interfaces' import ky, { Options } from 'ky' -import { getDjangoHeader, cleanRef, getFilenameFromSource, getDefaultSource } from '../../../utils/util_functions' +import { getDjangoHeader, cleanRef, getFilenameFromSource, getDefaultSource, getUserTagsByType } from '../../../utils/util_functions' import { NameOfCGDSDataset, Nullable, CustomAlert, CustomAlertTypes, SourceType, ConfirmModal, ExperimentInfo, ExperimentResultTableControl } from '../../../utils/interfaces' import { Biomarker, BiomarkerType, BiomarkerOrigin, FormBiomarkerData, MoleculesSectionData, MoleculesTypeOfSelection, SaveBiomarkerStructure, SaveMoleculeStructure, FeatureSelectionPanelData, SourceStateBiomarker, FeatureSelectionAlgorithm, FitnessFunction, FitnessFunctionParameters, BiomarkerState, AdvancedAlgorithm as AdvancedAlgorithmParameters, BBHAVersion, BiomarkerSimple, CrossValidationParameters } from '../../biomarkers/types' import { ManualForm } from '../../biomarkers/modalContentBiomarker/manualForm/ManualForm' @@ -134,11 +134,25 @@ class BiomarkerFromCorrelationModal extends React.Component { + this.setState({ tags }) + }).catch((err) => { + console.log("Error getting user's tags ->", err) + }) + } + /** * Generates default feature selection creation structure * @returns Default the default Alert @@ -1436,6 +1450,7 @@ class BiomarkerFromCorrelationModal extends React.Component diff --git a/src/frontend/static/frontend/src/utils/util_functions.ts b/src/frontend/static/frontend/src/utils/util_functions.ts index b46d4f1a..e0cfe463 100644 --- a/src/frontend/static/frontend/src/utils/util_functions.ts +++ b/src/frontend/static/frontend/src/utils/util_functions.ts @@ -4,6 +4,7 @@ import { DropdownItemProps, InputOnChangeData } from 'semantic-ui-react' import { TagType, DjangoTag, ExperimentState, ExperimentType, CorrelationMethod, PValuesAdjustmentMethod, DjangoMRNAxGEMResultRow } from './django_interfaces' import dayjs from 'dayjs' import countBy from 'lodash/countBy' +import ky from 'ky' import { MAX_FILE_SIZE_IN_MB_ERROR } from './constants' import { DifferentialExpressionAnalysisExperimentState } from '../components/differential-expression/types' @@ -13,6 +14,7 @@ const USER_LOCALE: readonly string[] | string = navigator.languages !== undefine : navigator.language declare const CSRFToken: string +declare const urlTagsCRUD: string const DEFAULT_FILENAME = 'Choose File' @@ -26,6 +28,18 @@ const getDjangoHeader = (): Headers => { return headers } +/** + * Fetches the current user's Tags, optionally filtering by Tag type. + * @param tagType Tag type to filter by. If omitted, every Tag of the user is fetched. + * @param signal Optional abort signal to cancel the request. + * @returns Promise with the user's Tags. + */ +const getUserTagsByType = (tagType?: TagType, signal?: AbortSignal): Promise => { + const searchParams = tagType !== undefined ? { type: tagType } : undefined + + return ky.get(urlTagsCRUD, { searchParams, signal }).json() +} + /** * Alerts a generic error message in screen */ @@ -892,6 +906,7 @@ const getDefaultAlertProps = (): CustomAlert => { export { getDjangoHeader, + getUserTagsByType, alertGeneralError, getFileTypeSelectOptions, getDefaultNewTag,