diff --git a/docs/plugins/events.md b/docs/plugins/events.md index 06af2a70cc0bfb3fffd7e997c267c8072a022e4b..b996c0c5e5ed98aa9551e8fe2e4d1d2f56b87f4e 100644 --- a/docs/plugins/events.md +++ b/docs/plugins/events.md @@ -9,18 +9,17 @@ To listen for specific events, plugins can use the `addListener` method in `even - onAddDataOverlay - triggered after successfully adding an overlay; the created overlay is passed as an argument. Example argument: -```javascript +```json { - "name": "Example Overlay", - "googleLicenseConsent": false, - "creator": "appu-admin", - "description": "Different", - "genomeType": null, - "genomeVersion": null, - "idObject": 149, - "publicOverlay": false, - "type": "GENERIC", - "order": 9 + "name": "Example Overlay", + "creator": "appu-admin", + "description": "Different", + "genomeType": null, + "genomeVersion": null, + "idObject": 149, + "publicOverlay": false, + "type": "GENERIC", + "order": 9 } ``` @@ -32,10 +31,9 @@ To listen for specific events, plugins can use the `addListener` method in `even - onShowOverlay - triggered after displaying an overlay on the map; the displayed overlay is passed as an argument. Example argument: -```javascript +```json { "name": "Generic advanced format overlay", - "googleLicenseConsent": false, "creator": "appu-admin", "description": "Data set provided by a user", "genomeType": null, @@ -49,10 +47,9 @@ To listen for specific events, plugins can use the `addListener` method in `even - onHideOverlay - triggered after disabling an overlay on the map; the disabled overlay is passed as an argument. Example argument: -```javascript +```json { "name": "colored overlay", - "googleLicenseConsent": false, "creator": "appu-admin", "description": "", "genomeType": null, diff --git a/package-lock.json b/package-lock.json index ff30a5f424e87ddc41de6f6b36b62cd92e0cd417..f01c2eceb3119c16c2f36825d84407d1df93e80f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,87 +9,89 @@ "version": "0.1.0", "hasInstallScript": true, "dependencies": { - "@next/font": "^13.5.2", - "@reduxjs/toolkit": "^1.9.6", - "@tanstack/react-table": "^8.11.7", + "@next/font": "13.5.6", + "@reduxjs/toolkit": "1.9.7", + "@tanstack/react-table": "8.11.7", "@types/node": "20.6.2", - "@types/openlayers": "^4.6.20", + "@types/openlayers": "4.6.23", "@types/react": "18.2.21", "@types/react-dom": "18.2.7", "autoprefixer": "10.4.15", - "axios": "^1.5.1", - "axios-hooks": "^5.0.0", - "crypto-js": "^4.2.0", - "downshift": "^8.2.3", + "axios": "1.6.3", + "axios-hooks": "5.0.2", + "crypto-js": "4.2.0", + "downshift": "8.3.1", "eslint-config-next": "13.4.19", - "is-uuid": "^1.0.2", + "is-uuid": "1.0.2", "molart": "github:davidhoksza/MolArt", "next": "13.4.19", - "ol": "^8.1.0", - "polished": "^4.3.1", + "ol": "8.2.0", + "polished": "4.3.1", "postcss": "8.4.29", "query-string": "7.1.3", "react": "18.2.0", - "react-accessible-accordion": "^5.0.0", - "react-dnd": "^16.0.1", - "react-dnd-html5-backend": "^16.0.1", + "react-accessible-accordion": "5.0.0", + "react-autosuggest": "^10.1.0", + "react-dnd": "16.0.1", + "react-dnd-html5-backend": "16.0.1", "react-dom": "18.2.0", - "react-dropzone": "^14.2.3", - "react-redux": "^8.1.2", - "sonner": "^1.4.3", - "tailwind-merge": "^1.14.0", + "react-dropzone": "14.2.3", + "react-redux": "8.1.3", + "sonner": "1.4.3", + "tailwind-merge": "1.14.0", "tailwindcss": "3.3.3", - "ts-deepmerge": "^6.2.0", - "use-debounce": "^9.0.4", - "uuid": "^9.0.1", - "zod": "^3.22.2", - "zod-to-json-schema": "^3.22.4" + "ts-deepmerge": "6.2.0", + "use-debounce": "9.0.4", + "uuid": "9.0.1", + "zod": "3.22.4", + "zod-to-json-schema": "3.22.4" }, "devDependencies": { - "@commitlint/cli": "^17.7.1", - "@commitlint/config-conventional": "^17.7.0", - "@testing-library/jest-dom": "^6.1.3", - "@testing-library/react": "^14.0.0", - "@testing-library/user-event": "^14.5.2", - "@types/crypto-js": "^4.2.2", - "@types/is-uuid": "^1.0.2", - "@types/jest": "^29.5.5", - "@types/react-redux": "^7.1.26", - "@types/redux-mock-store": "^1.0.6", - "@types/uuid": "^9.0.8", - "@typescript-eslint/eslint-plugin": "^6.7.0", - "@typescript-eslint/parser": "^6.7.0", - "axios-mock-adapter": "^1.22.0", - "cypress": "^13.2.0", - "cz-conventional-changelog": "^3.3.0", - "eslint": "^8.49.0", - "eslint-config-airbnb": "^19.0.4", - "eslint-config-prettier": "^9.0.0", - "eslint-config-standard-with-typescript": "^39.0.0", - "eslint-plugin-import": "^2.28.1", - "eslint-plugin-jsx-a11y": "^6.7.1", - "eslint-plugin-n": "^16.1.0", - "eslint-plugin-prettier": "^5.0.1", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-react": "^7.33.2", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-tailwindcss": "^3.13.0", - "eslint-plugin-testing-library": "^6.0.1", - "husky": "^8.0.0", - "jest": "^29.7.0", - "jest-canvas-mock": "^2.5.2", - "jest-environment-jsdom": "^29.7.0", - "jest-junit": "^16.0.0", - "jest-watch-typeahead": "^2.2.2", - "lint-staged": "^14.0.1", - "next-router-mock": "^0.9.10", - "prettier": "^3.0.3", - "prettier-2": "npm:prettier@^2", - "prettier-plugin-tailwindcss": "^0.5.6", - "redux-mock-store": "^1.5.4", - "redux-thunk": "^2.4.2", - "typescript": "^5.2.2", - "zod-fixture": "^2.5.0" + "@commitlint/cli": "17.8.1", + "@commitlint/config-conventional": "17.8.1", + "@testing-library/jest-dom": "6.1.6", + "@testing-library/react": "14.1.2", + "@testing-library/user-event": "14.5.2", + "@types/crypto-js": "4.2.2", + "@types/is-uuid": "1.0.2", + "@types/jest": "29.5.11", + "@types/react-autosuggest": "^10.1.11", + "@types/react-redux": "7.1.33", + "@types/redux-mock-store": "1.0.6", + "@types/uuid": "9.0.8", + "@typescript-eslint/eslint-plugin": "6.17.0", + "@typescript-eslint/parser": "6.17.0", + "axios-mock-adapter": "1.22.0", + "cypress": "13.6.2", + "cz-conventional-changelog": "3.3.0", + "eslint": "8.56.0", + "eslint-config-airbnb": "19.0.4", + "eslint-config-prettier": "9.1.0", + "eslint-config-standard-with-typescript": "39.1.1", + "eslint-plugin-import": "2.29.1", + "eslint-plugin-jsx-a11y": "6.8.0", + "eslint-plugin-n": "16.6.1", + "eslint-plugin-prettier": "5.1.2", + "eslint-plugin-promise": "6.1.1", + "eslint-plugin-react": "7.33.2", + "eslint-plugin-react-hooks": "4.6.0", + "eslint-plugin-tailwindcss": "3.13.1", + "eslint-plugin-testing-library": "6.2.0", + "husky": "8.0.3", + "jest": "29.7.0", + "jest-canvas-mock": "2.5.2", + "jest-environment-jsdom": "29.7.0", + "jest-junit": "16.0.0", + "jest-watch-typeahead": "2.2.2", + "lint-staged": "14.0.1", + "next-router-mock": "0.9.11", + "prettier": "3.1.1", + "prettier-2": "npm:prettier@2.8.8", + "prettier-plugin-tailwindcss": "0.5.6", + "redux-mock-store": "1.5.4", + "redux-thunk": "2.4.2", + "typescript": "5.3.3", + "zod-fixture": "2.5.1" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -2448,6 +2450,15 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-autosuggest": { + "version": "10.1.11", + "resolved": "https://registry.npmjs.org/@types/react-autosuggest/-/react-autosuggest-10.1.11.tgz", + "integrity": "sha512-lneJrX/5TZJzKHPJ6UuUjsh9OfeyQHKYEVHyBh5Y7LeRbCZxyIsjBmpxdPy1iH++Ger0qcyW+phPpYH+g3naLA==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-dom": { "version": "18.2.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.7.tgz", @@ -5330,6 +5341,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -11666,6 +11682,21 @@ "react-dom": "^16.3.3 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-autosuggest": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-autosuggest/-/react-autosuggest-10.1.0.tgz", + "integrity": "sha512-/azBHmc6z/31s/lBf6irxPf/7eejQdR0IqnZUzjdSibtlS8+Rw/R79pgDAo6Ft5QqCUTyEQ+f0FhL+1olDQ8OA==", + "dependencies": { + "es6-promise": "^4.2.8", + "prop-types": "^15.7.2", + "react-themeable": "^1.1.0", + "section-iterator": "^2.0.0", + "shallow-equal": "^1.2.1" + }, + "peerDependencies": { + "react": ">=16.3.0" + } + }, "node_modules/react-dnd": { "version": "16.0.1", "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", @@ -11774,6 +11805,22 @@ } } }, + "node_modules/react-themeable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/react-themeable/-/react-themeable-1.1.0.tgz", + "integrity": "sha512-kl5tQ8K+r9IdQXZd8WLa+xxYN04lLnJXRVhHfdgwsUJr/SlKJxIejoc9z9obEkx1mdqbTw1ry43fxEUwyD9u7w==", + "dependencies": { + "object-assign": "^3.0.0" + } + }, + "node_modules/react-themeable/node_modules/object-assign": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", + "integrity": "sha512-jHP15vXVGeVh1HuaA2wY6lxk+whK/x4KBG88VXeRma7CCun7iGD5qPc4eYykQ9sdQvg8jkwFKsSxHln2ybW3xQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -12260,6 +12307,11 @@ "loose-envify": "^1.1.0" } }, + "node_modules/section-iterator": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/section-iterator/-/section-iterator-2.0.0.tgz", + "integrity": "sha512-xvTNwcbeDayXotnV32zLb3duQsP+4XosHpb/F+tu6VzEZFmIjzPdNk6/O+QOOx5XTh08KL2ufdXeCO33p380pQ==" + }, "node_modules/semantic-ui-button": { "version": "2.2.12", "resolved": "https://registry.npmjs.org/semantic-ui-button/-/semantic-ui-button-2.2.12.tgz", @@ -12337,6 +12389,11 @@ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" }, + "node_modules/shallow-equal": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz", + "integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -15877,6 +15934,15 @@ "csstype": "^3.0.2" } }, + "@types/react-autosuggest": { + "version": "10.1.11", + "resolved": "https://registry.npmjs.org/@types/react-autosuggest/-/react-autosuggest-10.1.11.tgz", + "integrity": "sha512-lneJrX/5TZJzKHPJ6UuUjsh9OfeyQHKYEVHyBh5Y7LeRbCZxyIsjBmpxdPy1iH++Ger0qcyW+phPpYH+g3naLA==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/react-dom": { "version": "18.2.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.7.tgz", @@ -18012,6 +18078,11 @@ "is-symbol": "^1.0.2" } }, + "es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -22449,6 +22520,18 @@ "integrity": "sha512-MT2obYpTgLIIfPr9d7hEyvPB5rg8uJcHpgA83JSRlEUHvzH48+8HJPvzSs+nM+XprTugDgLfhozO5qyJpBvYRQ==", "requires": {} }, + "react-autosuggest": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-autosuggest/-/react-autosuggest-10.1.0.tgz", + "integrity": "sha512-/azBHmc6z/31s/lBf6irxPf/7eejQdR0IqnZUzjdSibtlS8+Rw/R79pgDAo6Ft5QqCUTyEQ+f0FhL+1olDQ8OA==", + "requires": { + "es6-promise": "^4.2.8", + "prop-types": "^15.7.2", + "react-themeable": "^1.1.0", + "section-iterator": "^2.0.0", + "shallow-equal": "^1.2.1" + } + }, "react-dnd": { "version": "16.0.1", "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", @@ -22506,6 +22589,21 @@ "use-sync-external-store": "^1.0.0" } }, + "react-themeable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/react-themeable/-/react-themeable-1.1.0.tgz", + "integrity": "sha512-kl5tQ8K+r9IdQXZd8WLa+xxYN04lLnJXRVhHfdgwsUJr/SlKJxIejoc9z9obEkx1mdqbTw1ry43fxEUwyD9u7w==", + "requires": { + "object-assign": "^3.0.0" + }, + "dependencies": { + "object-assign": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", + "integrity": "sha512-jHP15vXVGeVh1HuaA2wY6lxk+whK/x4KBG88VXeRma7CCun7iGD5qPc4eYykQ9sdQvg8jkwFKsSxHln2ybW3xQ==" + } + } + }, "read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -22868,6 +22966,11 @@ "loose-envify": "^1.1.0" } }, + "section-iterator": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/section-iterator/-/section-iterator-2.0.0.tgz", + "integrity": "sha512-xvTNwcbeDayXotnV32zLb3duQsP+4XosHpb/F+tu6VzEZFmIjzPdNk6/O+QOOx5XTh08KL2ufdXeCO33p380pQ==" + }, "semantic-ui-button": { "version": "2.2.12", "resolved": "https://registry.npmjs.org/semantic-ui-button/-/semantic-ui-button-2.2.12.tgz", @@ -22932,6 +23035,11 @@ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" }, + "shallow-equal": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz", + "integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==" + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/package.json b/package.json index 7446c416ffab49ba8b230715f99607f328e93cce..e6b1adc41339ca81db3eea08ca1f05331a9ddc19 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "query-string": "7.1.3", "react": "18.2.0", "react-accessible-accordion": "5.0.0", + "react-autosuggest": "^10.1.0", "react-dnd": "16.0.1", "react-dnd-html5-backend": "16.0.1", "react-dom": "18.2.0", @@ -68,6 +69,7 @@ "@types/crypto-js": "4.2.2", "@types/is-uuid": "1.0.2", "@types/jest": "29.5.11", + "@types/react-autosuggest": "^10.1.11", "@types/react-redux": "7.1.33", "@types/redux-mock-store": "1.0.6", "@types/uuid": "9.0.8", diff --git a/src/components/FunctionalArea/Modal/AddCommentModal/AddCommentModal.component.tsx b/src/components/FunctionalArea/Modal/AddCommentModal/AddCommentModal.component.tsx index 9c4fde54b754948a099b53bbaf8626b78f2bbb2c..d8469a9461e0c85e6735801885b53e129a5851f6 100644 --- a/src/components/FunctionalArea/Modal/AddCommentModal/AddCommentModal.component.tsx +++ b/src/components/FunctionalArea/Modal/AddCommentModal/AddCommentModal.component.tsx @@ -8,6 +8,7 @@ import { useAppSelector } from '@/redux/hooks/useAppSelector'; import { lastRightClickSelector } from '@/redux/models/models.selectors'; import { closeModal } from '@/redux/modal/modal.slice'; import { getComments } from '@/redux/comment/thunks/getComments'; +import { showToast } from '@/utils/showToast'; export const AddCommentModal: React.FC = () => { const dispatch = useAppDispatch(); @@ -30,6 +31,11 @@ export const AddCommentModal: React.FC = () => { await dispatch(addComment(data)); dispatch(closeModal()); dispatch(getComments()); + showToast({ + type: 'success', + message: + 'Thank you for your feedback, your comment has been placed on the map. To see all comments, use the “show comments†button in the upper right', + }); }; return ( diff --git a/src/components/FunctionalArea/Modal/LicenseModal/LicenseModal.component.tsx b/src/components/FunctionalArea/Modal/LicenseModal/LicenseModal.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..bdb79d2d85249e45549562b2d4a2d7bbef9856d8 --- /dev/null +++ b/src/components/FunctionalArea/Modal/LicenseModal/LicenseModal.component.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { useAppSelector } from '@/redux/hooks/useAppSelector'; +import { projectSelector } from '@/redux/project/project.selectors'; + +export const LicenseModal = (): React.ReactNode => { + const project = useAppSelector(projectSelector).data; + + let licenseDescription = ''; + if (project) { + licenseDescription = project.license + ? `<a href="${project.license.url}" target="_blank">Link</a><br/><br/>${project.license.content}` + : `<a href="${project.customLicenseUrl}" target="_blank">Link: ${project.customLicenseUrl}</a>`; + } + return ( + <div className="w-full overflow-auto border border-t-[#E1E0E6] bg-white p-[24px]"> + <div dangerouslySetInnerHTML={{ __html: licenseDescription }} /> + </div> + ); +}; diff --git a/src/components/FunctionalArea/Modal/LicenseModal/index.ts b/src/components/FunctionalArea/Modal/LicenseModal/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..b05258c696c04adae86a585264c23777a9b95356 --- /dev/null +++ b/src/components/FunctionalArea/Modal/LicenseModal/index.ts @@ -0,0 +1 @@ +export { LicenseModal } from './LicenseModal.component'; diff --git a/src/components/FunctionalArea/Modal/LoginModal/LoginModal.component.test.tsx b/src/components/FunctionalArea/Modal/LoginModal/LoginModal.component.test.tsx index 3887053892dda1e0ff1344fbb2705fb8662cf58b..c60246b26b343b9b84f7d27022dac2ae7547fa3d 100644 --- a/src/components/FunctionalArea/Modal/LoginModal/LoginModal.component.test.tsx +++ b/src/components/FunctionalArea/Modal/LoginModal/LoginModal.component.test.tsx @@ -31,6 +31,13 @@ const renderComponent = (initialStoreState: InitialStoreState = {}): { store: St ); }; +const testOverlaysFixture = overlaysFixture.map(overlay => { + return { + ...overlay, + publicOverlay: false, + }; +}); + describe('LoginModal - component', () => { test('renders LoginModal component', () => { renderComponent(); @@ -79,7 +86,7 @@ describe('LoginModal - component', () => { publicOverlay: false, }), ) - .reply(HttpStatusCode.Ok, overlaysFixture); + .reply(HttpStatusCode.Ok, testOverlaysFixture); const { store } = renderComponent(); const loginInput = screen.getByLabelText(/login/i); @@ -97,7 +104,7 @@ describe('LoginModal - component', () => { }); expect(store.getState().overlays.userOverlays.loading).toBe('succeeded'); - expect(store.getState().overlays.userOverlays.data).toEqual(overlaysFixture); + expect(store.getState().overlays.userOverlays.data).toEqual(testOverlaysFixture); }); it('should display loggedInMenuModal after successful login as admin', async () => { mockedAxiosClient.onPost(apiPath.postLogin()).reply(HttpStatusCode.Ok, loginFixture); @@ -117,7 +124,7 @@ describe('LoginModal - component', () => { publicOverlay: false, }), ) - .reply(HttpStatusCode.Ok, overlaysFixture); + .reply(HttpStatusCode.Ok, testOverlaysFixture); const { store } = renderComponent({ modal: MODAL_INITIAL_STATE_MOCK, @@ -156,7 +163,7 @@ describe('LoginModal - component', () => { publicOverlay: false, }), ) - .reply(HttpStatusCode.Ok, overlaysFixture); + .reply(HttpStatusCode.Ok, testOverlaysFixture); const { store } = renderComponent({ modal: MODAL_INITIAL_STATE_MOCK, @@ -187,7 +194,7 @@ describe('LoginModal - component', () => { publicOverlay: false, }), ) - .reply(HttpStatusCode.Ok, overlaysFixture); + .reply(HttpStatusCode.Ok, testOverlaysFixture); const { store } = renderComponent({ modal: MODAL_INITIAL_STATE_MOCK, diff --git a/src/components/FunctionalArea/Modal/Modal.component.tsx b/src/components/FunctionalArea/Modal/Modal.component.tsx index 8e2a02b528cf172eebb868fd183008da565dde12..8b97ece7a85b1931420cb95c9c781dd9a72d6067 100644 --- a/src/components/FunctionalArea/Modal/Modal.component.tsx +++ b/src/components/FunctionalArea/Modal/Modal.component.tsx @@ -3,6 +3,7 @@ import { modalSelector } from '@/redux/modal/modal.selector'; import dynamic from 'next/dynamic'; import { AccessDeniedModal } from '@/components/FunctionalArea/Modal/AccessDeniedModal/AccessDeniedModal.component'; import { AddCommentModal } from '@/components/FunctionalArea/Modal/AddCommentModal/AddCommentModal.component'; +import { LicenseModal } from '@/components/FunctionalArea/Modal/LicenseModal'; import { EditOverlayModal } from './EditOverlayModal'; import { LoginModal } from './LoginModal'; import { ErrorReportModal } from './ErrorReportModal'; @@ -41,6 +42,11 @@ export const Modal = (): React.ReactNode => { <ErrorReportModal /> </ModalLayout> )} + {isOpen && modalName === 'license' && ( + <ModalLayout> + <LicenseModal /> + </ModalLayout> + )} {isOpen && modalName === 'publications' && <PublicationsModal />} {isOpen && modalName === 'edit-overlay' && ( <ModalLayout> @@ -57,6 +63,11 @@ export const Modal = (): React.ReactNode => { <AccessDeniedModal /> </ModalLayout> )} + {isOpen && modalName === 'select-project' && ( + <ModalLayout> + <AccessDeniedModal /> + </ModalLayout> + )} {isOpen && modalName === 'add-comment' && ( <ModalLayout> <AddCommentModal /> diff --git a/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx b/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx index 3afcf1f8cfc442b64f8ced8934545a33337b4d2c..56e1991ab095814ea1a03a1856b76e4ada43c07d 100644 --- a/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx +++ b/src/components/FunctionalArea/Modal/ModalLayout/ModalLayout.component.tsx @@ -29,6 +29,7 @@ export const ModalLayout = ({ children }: ModalLayoutProps): JSX.Element => { 'flex h-5/6 w-10/12 flex-col overflow-hidden rounded-lg', modalName === 'login' && 'h-auto w-[400px]', modalName === 'access-denied' && 'h-auto w-[400px]', + modalName === 'select-project' && 'h-auto w-[400px]', modalName === 'add-comment' && 'h-auto w-[400px]', modalName === 'error-report' && 'h-auto w-[800px]', ['edit-overlay', 'logged-in-menu'].includes(modalName) && 'h-auto w-[432px]', diff --git a/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.component.tsx b/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.component.tsx index 128d5b2365f02947d0190760654c736b8dbc0c50..fc28ddc3b711bc72190ee2aa0be5538c74f6165f 100644 --- a/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.component.tsx +++ b/src/components/FunctionalArea/TopBar/SearchBar/SearchBar.component.tsx @@ -1,3 +1,8 @@ +import { + autocompleteChemicalSelector, + autocompleteDrugSelector, + autocompleteSearchSelector, +} from '@/redux/autocomplete/autocomplete.selectors'; import { currentSelectedSearchElement, searchDrawerOpenSelector, @@ -14,23 +19,33 @@ import { import { getSearchData } from '@/redux/search/search.thunks'; import Image from 'next/image'; import { useRouter } from 'next/router'; -import { ChangeEvent, KeyboardEvent, useCallback, useEffect, useState } from 'react'; +import { useCallback, KeyboardEvent, useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; -import { ONE, ZERO } from '@/constants/common'; +import { FIVE, ONE, ZERO } from '@/constants/common'; +import Autosuggest from 'react-autosuggest'; import { clearEntityNumberData } from '@/redux/entityNumber/entityNumber.slice'; import { getDefaultSearchTab, getSearchValuesArrayAndTrimToSeven } from './SearchBar.utils'; +import './autocomplete.css'; + +type Suggestion = { + name: string; +}; const ENTER_KEY_CODE = 'Enter'; export const SearchBar = (): JSX.Element => { const [searchValue, setSearchValue] = useState<string>(''); + const [filteredSuggestions, setFilteredSuggestions] = useState<Suggestion[]>([]); const isPendingSearchStatus = useSelector(isPendingSearchStatusSelector); const isSearchDrawerOpen = useSelector(searchDrawerOpenSelector); const isPerfectMatch = useSelector(perfectMatchSelector); const searchValueState = useSelector(searchValueSelector); - const currentTab = useSelector(currentSelectedSearchElement); + const searchAutocompleteState = useSelector(autocompleteSearchSelector); + const drugAutocompleteState = useSelector(autocompleteDrugSelector); + const chemicalAutocompleteState = useSelector(autocompleteChemicalSelector); const dispatch = useAppDispatch(); const router = useRouter(); + const currentTab = useSelector(currentSelectedSearchElement); const updateSearchValueFromQueryParam = useCallback((): void => { const { searchValue: searchValueQueryParam } = router.query; @@ -53,10 +68,6 @@ export const SearchBar = (): JSX.Element => { } }; - const onSearchChange = (event: ChangeEvent<HTMLInputElement>): void => { - setSearchValue(event.target.value); - }; - const onSearchClick = (): void => { const searchValues = getSearchValuesArrayAndTrimToSeven(searchValue); @@ -82,6 +93,60 @@ export const SearchBar = (): JSX.Element => { openSearchDrawerIfClosed(currentTab); }; + const suggestions = searchAutocompleteState.searchValues + .concat(drugAutocompleteState.searchValues, chemicalAutocompleteState.searchValues) + .map(entry => { + return { name: entry }; + }) + .sort((a: Suggestion, b: Suggestion) => a.name.localeCompare(b.name)); + + const getSuggestions = (value: string): Suggestion[] => { + const inputValue = value.trim().toLowerCase(); + const inputLength = inputValue.length; + if (inputLength === ZERO) { + return []; + } + return suggestions + .filter(lang => lang.name.toLowerCase().slice(ZERO, inputLength) === inputValue) + .slice(ZERO, FIVE); + }; + + const renderSuggestion = (suggestion: Suggestion): JSX.Element => { + return <div>{suggestion.name}</div>; + }; + + // Autosuggest will call this function every time you need to update suggestions. + // You already implemented this logic above, so just use it. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const onSuggestionsFetchRequested = ({ value }): void => { + setFilteredSuggestions(getSuggestions(value)); + }; + + // Autosuggest will call this function every time you need to clear suggestions. + const onSuggestionsClearRequested = (): void => { + setFilteredSuggestions([]); + }; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const onChange = (event, { newValue }): void => { + setSearchValue(newValue); + }; + // Autosuggest will pass through all these props to the input. + const inputProps = { + placeholder: '', + value: searchValue, + name: 'search-input', + onChange, + onKeyDown: handleKeyPress, + onClick: handleSearchClick, + 'data-testid': 'search-input', + disabled: isPendingSearchStatus, + }; + + const getSuggestionValue = (suggestion: Suggestion): string => suggestion.name; + useEffect(() => { updateSearchValueFromQueryParam(); }, [updateSearchValueFromQueryParam]); @@ -89,19 +154,34 @@ export const SearchBar = (): JSX.Element => { clearSearchValueFromClearedState(); }, [clearSearchValueFromClearedState]); + const theme = { + input: + 'h-9 w-72 rounded-[64px] border border-transparent bg-cultured px-4 py-2.5 text-xs font-medium text-font-400 outline-none hover:border-greyscale-600 focus:border-greyscale-600', + container: 'react-autosuggest__container', + inputFocused: 'react-autosuggest__input--focused', + suggestionsContainer: 'react-autosuggest__suggestions-container', + suggestionsContainerOpen: 'react-autosuggest__suggestions-container--open', + suggestionsList: 'react-autosuggest__suggestions-list', + suggestion: 'react-autosuggest__suggestion', + suggestionFirst: 'react-autosuggest__suggestion--first', + suggestionHighlighted: 'bg-primary-100', + sectionContainer: 'react-autosuggest__section-container', + sectionContainerFirst: 'react-autosuggest__section-container--first', + sectionTitle: 'react-autosuggest__section-title', + }; + return ( - <div className="relative" data-testid="search-bar"> - <input - value={searchValue} - name="search-input" - aria-label="search-input" - data-testid="search-input" - onKeyDown={handleKeyPress} - onChange={onSearchChange} - disabled={isPendingSearchStatus} - onClick={handleSearchClick} - className="h-9 w-72 rounded-[64px] border border-transparent bg-cultured px-4 py-2.5 text-xs font-medium text-font-400 outline-none hover:border-greyscale-600 focus:border-greyscale-600" + <div className="relative mt-5" data-testid="search-bar"> + <Autosuggest + suggestions={filteredSuggestions} + onSuggestionsFetchRequested={onSuggestionsFetchRequested} + onSuggestionsClearRequested={onSuggestionsClearRequested} + getSuggestionValue={getSuggestionValue} + renderSuggestion={renderSuggestion} + inputProps={inputProps} + theme={theme} /> + <button disabled={isPendingSearchStatus} type="button" diff --git a/src/components/FunctionalArea/TopBar/SearchBar/autocomplete.css b/src/components/FunctionalArea/TopBar/SearchBar/autocomplete.css new file mode 100644 index 0000000000000000000000000000000000000000..294dbea4fb31ff166e36bb14b7123095686dab79 --- /dev/null +++ b/src/components/FunctionalArea/TopBar/SearchBar/autocomplete.css @@ -0,0 +1,55 @@ +.react-autosuggest__container { + position: relative; +} + +.react-autosuggest__input { + width: 100%; + height: 36px; + padding: 10px; + border: 1px solid; + border-radius: 4px; + border-bottom-right-radius: 0; + border-top-right-radius: 0; +} + +.react-autosuggest__input--focused { + outline: none; +} + +.react-autosuggest__input--open { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +.react-autosuggest__suggestions-container { + display: none; +} + +.react-autosuggest__suggestions-container--open { + display: block; + position: absolute; + top: 33px; + width: 100%; + min-width: 160px; + margin-left: 1px; + background-color: #ffffff; + border-radius: 0 0 4px 4px; + z-index: 2; + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + background-clip: padding-box; +} + +.react-autosuggest__suggestions-list { + margin: 0; + padding: 0; + list-style-type: none; +} + +.react-autosuggest__suggestion { + cursor: pointer; + padding: 10px 20px; +} + +.react-autosuggest__suggestion--highlighted { + background-color: blue; +} diff --git a/src/components/FunctionalArea/TopBar/TopBar.component.test.tsx b/src/components/FunctionalArea/TopBar/TopBar.component.test.tsx index 09a15505096ac668987969dc11ba86e21dda3107..0fb6117915a0eae1c6631f65133822a9bf64fc12 100644 --- a/src/components/FunctionalArea/TopBar/TopBar.component.test.tsx +++ b/src/components/FunctionalArea/TopBar/TopBar.component.test.tsx @@ -20,6 +20,7 @@ import { SEARCH_STATE_INITIAL_MOCK } from '@/redux/search/search.mock'; import { ZOD_SEED } from '@/constants'; import { createFixture } from 'zod-fixture'; import { overviewImageView } from '@/models/overviewImageView'; +import { AUTOCOMPLETE_INITIAL_STATE } from '@/redux/autocomplete/autocomplete.constants'; import { TopBar } from './TopBar.component'; const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => { @@ -117,6 +118,9 @@ describe('TopBar - component', () => { user: USER_INITIAL_STATE_MOCK, map: initialMapStateFixture, search: SEARCH_STATE_INITIAL_MOCK, + autocompleteSearch: AUTOCOMPLETE_INITIAL_STATE, + autocompleteDrug: AUTOCOMPLETE_INITIAL_STATE, + autocompleteChemical: AUTOCOMPLETE_INITIAL_STATE, backgrounds: { ...BACKGROUND_INITIAL_STATE_MOCK, data: BACKGROUNDS_MOCK }, }); @@ -133,6 +137,9 @@ describe('TopBar - component', () => { renderComponentWithActionListener({ user: USER_INITIAL_STATE_MOCK, search: SEARCH_STATE_INITIAL_MOCK, + autocompleteSearch: AUTOCOMPLETE_INITIAL_STATE, + autocompleteDrug: AUTOCOMPLETE_INITIAL_STATE, + autocompleteChemical: AUTOCOMPLETE_INITIAL_STATE, drawer: initialStateFixture, project: { ...PROJECT_STATE_INITIAL_MOCK, diff --git a/src/components/Map/Drawer/OverlaysDrawer/UserOverlayForm/UserOverlayForm.component.test.tsx b/src/components/Map/Drawer/OverlaysDrawer/UserOverlayForm/UserOverlayForm.component.test.tsx index 5248abdf5b3c4fb6247b99aeca83ce6f81876517..d800a3a2a27b75bc29fca7f30aa1a528dda3161f 100644 --- a/src/components/Map/Drawer/OverlaysDrawer/UserOverlayForm/UserOverlayForm.component.test.tsx +++ b/src/components/Map/Drawer/OverlaysDrawer/UserOverlayForm/UserOverlayForm.component.test.tsx @@ -59,6 +59,13 @@ const renderComponentWithActionListener = ( ); }; +const testOverlaysFixture = overlaysFixture.map(overlay => { + return { + ...overlay, + publicOverlay: false, + }; +}); + describe('UserOverlayForm - Component', () => { beforeEach(() => { jest.clearAllMocks(); @@ -215,7 +222,7 @@ describe('UserOverlayForm - Component', () => { mockedAxiosClient .onGet(apiPath.getAllUserOverlaysByCreatorQuery({ creator: 'test', publicOverlay: false })) - .reply(HttpStatusCode.Ok, overlaysFixture); + .reply(HttpStatusCode.Ok, testOverlaysFixture); const { store } = renderComponent({ user: { @@ -258,7 +265,7 @@ describe('UserOverlayForm - Component', () => { const refetchedUserOverlays = store.getState().overlays.userOverlays.data; await waitFor(() => { - expect(refetchedUserOverlays).toEqual(overlaysFixture); + expect(refetchedUserOverlays).toEqual(testOverlaysFixture); }); }); it('should show toast after successful creating user overlays', async () => { @@ -276,7 +283,7 @@ describe('UserOverlayForm - Component', () => { mockedAxiosClient .onGet(apiPath.getAllUserOverlaysByCreatorQuery({ creator: 'test', publicOverlay: false })) - .reply(HttpStatusCode.Ok, overlaysFixture); + .reply(HttpStatusCode.Ok, testOverlaysFixture); const { store } = renderComponent({ user: { diff --git a/src/components/Map/Drawer/OverlaysDrawer/UserOverlays/UserOverlaysWithoutGroup/UserOverlaysWithoutGroup.utils.test.ts b/src/components/Map/Drawer/OverlaysDrawer/UserOverlays/UserOverlaysWithoutGroup/UserOverlaysWithoutGroup.utils.test.ts index 8c07bab617c8bcb8d8d65ce44031e06f8f6fab6e..fd5bf60f17cf343871ea8eab11e33daf740c2a09 100644 --- a/src/components/Map/Drawer/OverlaysDrawer/UserOverlays/UserOverlaysWithoutGroup/UserOverlaysWithoutGroup.utils.test.ts +++ b/src/components/Map/Drawer/OverlaysDrawer/UserOverlays/UserOverlaysWithoutGroup/UserOverlaysWithoutGroup.utils.test.ts @@ -7,7 +7,6 @@ const INPUT_ARRAY: MapOverlay[] = [ name: 'Overlay1', description: 'Description1', type: 'Type1', - googleLicenseConsent: true, creator: 'Creator1', genomeType: 'GenomeType1', genomeVersion: 'GenomeVersion1', @@ -19,7 +18,6 @@ const INPUT_ARRAY: MapOverlay[] = [ name: 'Overlay2', description: 'Description2', type: 'Type2', - googleLicenseConsent: false, creator: 'Creator2', genomeType: 'GenomeType2', genomeVersion: 'GenomeVersion2', @@ -31,7 +29,6 @@ const INPUT_ARRAY: MapOverlay[] = [ name: 'Overlay3', description: 'Description3', type: 'Type3', - googleLicenseConsent: true, creator: 'Creator3', genomeType: 'GenomeType3', genomeVersion: 'GenomeVersion3', @@ -48,7 +45,6 @@ describe('moveArrayElement', () => { name: 'Overlay1', description: 'Description1', type: 'Type1', - googleLicenseConsent: true, creator: 'Creator1', genomeType: 'GenomeType1', genomeVersion: 'GenomeVersion1', @@ -60,7 +56,6 @@ describe('moveArrayElement', () => { name: 'Overlay3', description: 'Description3', type: 'Type3', - googleLicenseConsent: true, creator: 'Creator3', genomeType: 'GenomeType3', genomeVersion: 'GenomeVersion3', @@ -72,7 +67,6 @@ describe('moveArrayElement', () => { name: 'Overlay2', description: 'Description2', type: 'Type2', - googleLicenseConsent: false, creator: 'Creator2', genomeType: 'GenomeType2', genomeVersion: 'GenomeVersion2', @@ -93,7 +87,6 @@ describe('moveArrayElement', () => { name: 'Overlay1', description: 'Description1', type: 'Type1', - googleLicenseConsent: true, creator: 'Creator1', genomeType: 'GenomeType1', genomeVersion: 'GenomeVersion1', @@ -105,7 +98,6 @@ describe('moveArrayElement', () => { name: 'Overlay3', description: 'Description3', type: 'Type3', - googleLicenseConsent: true, creator: 'Creator3', genomeType: 'GenomeType3', genomeVersion: 'GenomeVersion3', @@ -117,7 +109,6 @@ describe('moveArrayElement', () => { name: 'Overlay2', description: 'Description2', type: 'Type2', - googleLicenseConsent: false, creator: 'Creator2', genomeType: 'GenomeType2', genomeVersion: 'GenomeVersion2', @@ -138,7 +129,6 @@ describe('moveArrayElement', () => { name: 'Overlay3', description: 'Description3', type: 'Type3', - googleLicenseConsent: true, creator: 'Creator3', genomeType: 'GenomeType3', genomeVersion: 'GenomeVersion3', @@ -150,7 +140,6 @@ describe('moveArrayElement', () => { name: 'Overlay1', description: 'Description1', type: 'Type1', - googleLicenseConsent: true, creator: 'Creator1', genomeType: 'GenomeType1', genomeVersion: 'GenomeVersion1', @@ -162,7 +151,6 @@ describe('moveArrayElement', () => { name: 'Overlay2', description: 'Description2', type: 'Type2', - googleLicenseConsent: false, creator: 'Creator2', genomeType: 'GenomeType2', genomeVersion: 'GenomeVersion2', @@ -183,7 +171,6 @@ describe('moveArrayElement', () => { name: 'Overlay2', description: 'Description2', type: 'Type2', - googleLicenseConsent: false, creator: 'Creator2', genomeType: 'GenomeType2', genomeVersion: 'GenomeVersion2', @@ -195,7 +182,6 @@ describe('moveArrayElement', () => { name: 'Overlay3', description: 'Description3', type: 'Type3', - googleLicenseConsent: true, creator: 'Creator3', genomeType: 'GenomeType3', genomeVersion: 'GenomeVersion3', @@ -207,7 +193,6 @@ describe('moveArrayElement', () => { name: 'Overlay1', description: 'Description1', type: 'Type1', - googleLicenseConsent: true, creator: 'Creator1', genomeType: 'GenomeType1', genomeVersion: 'GenomeVersion1', diff --git a/src/components/Map/Drawer/ProjectInfoDrawer/ProjectInfoDrawer.component.tsx b/src/components/Map/Drawer/ProjectInfoDrawer/ProjectInfoDrawer.component.tsx index 5a04b6fabcedce598ad481a6059e5ab30ee8c502..9bf8267cdfde579330b19eb9cb221e79b0c2ad30 100644 --- a/src/components/Map/Drawer/ProjectInfoDrawer/ProjectInfoDrawer.component.tsx +++ b/src/components/Map/Drawer/ProjectInfoDrawer/ProjectInfoDrawer.component.tsx @@ -1,7 +1,7 @@ import { apiPath } from '@/redux/apiPath'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { useAppSelector } from '@/redux/hooks/useAppSelector'; -import { openPublicationsModal } from '@/redux/modal/modal.slice'; +import { openLicenseModal, openPublicationsModal } from '@/redux/modal/modal.slice'; import { mainMapModelDescriptionSelector } from '@/redux/models/models.selectors'; import { diseaseLinkSelector, @@ -9,6 +9,7 @@ import { organismLinkSelector, organismNameSelector, projectNameSelector, + projectSelector, versionSelector, } from '@/redux/project/project.selectors'; import { DrawerHeading } from '@/shared/DrawerHeading'; @@ -23,11 +24,18 @@ export const ProjectInfoDrawer = (): JSX.Element => { const organismLink = useAppSelector(organismLinkSelector); const organismName = useAppSelector(organismNameSelector); const projectName = useAppSelector(projectNameSelector); + const project = useAppSelector(projectSelector).data; const version = useAppSelector(versionSelector); const description = useAppSelector(mainMapModelDescriptionSelector); const sourceDownloadLink = window.location.hostname + apiPath.getSourceFile(); + let licenseName: string = ''; + if (project) { + licenseName = project.license ? project.license.name : project.customLicenseName; + } + const licenseExists = licenseName !== ''; + useEffect(() => { // dispatch(getPublications()); }, [dispatch]); @@ -36,6 +44,10 @@ export const ProjectInfoDrawer = (): JSX.Element => { dispatch(openPublicationsModal()); }; + const onLicenseClick = (): void => { + dispatch(openLicenseModal(licenseName)); + }; + return ( <div data-testid="export-drawer" className="h-full max-h-full"> <DrawerHeading title="Project info" /> @@ -76,6 +88,13 @@ export const ProjectInfoDrawer = (): JSX.Element => { </a> </li> )} + {licenseExists && ( + <li className="mt-2 text-hyperlink-blue"> + <button type="button" onClick={onLicenseClick} className="text-base font-semibold"> + License: {licenseName} + </button> + </li> + )} {organismName && ( <li className="mt-2 text-hyperlink-blue"> <span className="text-black">Organism: </span> diff --git a/src/constants/common.ts b/src/constants/common.ts index 9bdcd6487cb3cd51ff146c5a2d60796ddccb4c99..3480fb272b2a221814efbb1d0d04ff70ada7dbd9 100644 --- a/src/constants/common.ts +++ b/src/constants/common.ts @@ -12,6 +12,8 @@ export const SECOND_ARRAY_ELEMENT = 1; export const TWO = 2; +export const FIVE = 5; + export const THIRD_ARRAY_ELEMENT = 2; export const NOOP = (): void => {}; diff --git a/src/models/autocompleteSchema.ts b/src/models/autocompleteSchema.ts new file mode 100644 index 0000000000000000000000000000000000000000..1f8e1095d21fb7906b4f5f57f8c76628b1bd8de4 --- /dev/null +++ b/src/models/autocompleteSchema.ts @@ -0,0 +1,3 @@ +import { z } from 'zod'; + +export const autocompleteSchema = z.array(z.string()); diff --git a/src/models/configurationSchema.ts b/src/models/configurationSchema.ts index b3ec41d84ddccc28ed1e1fbc0628508a316e51b9..05ee948c2351a74075d62fec38a4a9392b322207 100644 --- a/src/models/configurationSchema.ts +++ b/src/models/configurationSchema.ts @@ -69,8 +69,6 @@ export const privilegeTypeSchema = z.record( export const mapTypeSchema = z.object({ name: z.string(), id: z.string() }); -export const mapCanvasTypeSchema = z.object({ name: z.string(), id: z.string() }); - export const unitTypeSchema = z.object({ name: z.string(), id: z.string() }); export const modificationStateTypeSchema = z.record( @@ -93,7 +91,6 @@ export const configurationSchema = z.object({ annotators: z.array(annotatorSchema), privilegeTypes: privilegeTypeSchema, mapTypes: z.array(mapTypeSchema), - mapCanvasTypes: z.array(mapCanvasTypeSchema), unitTypes: z.array(unitTypeSchema), modificationStateTypes: modificationStateTypeSchema, }); diff --git a/src/models/disease.ts b/src/models/disease.ts index 65079e66740c8eb2f4488a9f5798339cc9a4abe4..7152a107591c97797fb840aae640a4f49ac191d0 100644 --- a/src/models/disease.ts +++ b/src/models/disease.ts @@ -1,9 +1,9 @@ import { z } from 'zod'; export const disease = z.object({ - link: z.string(), + id: z.number().int().positive(), + link: z.string().optional(), type: z.string(), resource: z.string(), - id: z.number(), annotatorClassName: z.string(), }); diff --git a/src/models/licenseSchema.ts b/src/models/licenseSchema.ts new file mode 100644 index 0000000000000000000000000000000000000000..3111314e390f0b65e684f8fdd1b83178de6b0d47 --- /dev/null +++ b/src/models/licenseSchema.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; + +export const licenseSchema = z.object({ + id: z.number().int().positive(), + name: z.string(), + url: z.string(), + content: z.string(), +}); diff --git a/src/models/mapOverlay.ts b/src/models/mapOverlay.ts index 16da571736fd815bb63a9c6bef833c5ab2102c3a..d9b645eb5e3a637c9c50e738a0b27fa193a120e2 100644 --- a/src/models/mapOverlay.ts +++ b/src/models/mapOverlay.ts @@ -1,16 +1,16 @@ import { z } from 'zod'; +import { ZERO } from '@/constants/common'; export const mapOverlay = z.object({ + idObject: z.number(), name: z.string(), - googleLicenseConsent: z.boolean(), + order: z.number().int().gte(ZERO), creator: z.string(), description: z.string(), genomeType: z.string().nullable(), genomeVersion: z.string().nullable(), - idObject: z.number(), publicOverlay: z.boolean(), type: z.string(), - order: z.number(), }); export const createdOverlayFileSchema = z.object({ @@ -25,7 +25,6 @@ export const uploadedOverlayFileContentSchema = createdOverlayFileSchema.extend( export const createdOverlaySchema = z.object({ name: z.string(), - googleLicenseConsent: z.boolean(), creator: z.string(), description: z.string(), genomeType: z.string().nullable(), diff --git a/src/models/organism.ts b/src/models/organism.ts index 4b003eefff187e85f7b27831e55d5c09ea3b5c62..f583456293d3886270327f55285aaa67d8cd90a6 100644 --- a/src/models/organism.ts +++ b/src/models/organism.ts @@ -1,9 +1,9 @@ import { z } from 'zod'; export const organism = z.object({ - link: z.string(), + id: z.number().int().positive(), + link: z.string().optional(), type: z.string(), resource: z.string(), - id: z.number(), annotatorClassName: z.string(), }); diff --git a/src/models/projectSchema.ts b/src/models/projectSchema.ts index d3eeeb2a7a6ddc75c825c3bdb34e8f6caf99e74d..39862947ce2adb12652bc9c91eca96ee77ad35a8 100644 --- a/src/models/projectSchema.ts +++ b/src/models/projectSchema.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import { licenseSchema } from '@/models/licenseSchema'; import { disease } from './disease'; import { organism } from './organism'; import { overviewImageView } from './overviewImageView'; @@ -21,7 +22,9 @@ export const projectSchema = z.object({ }), projectId: z.string(), creationDate: z.string(), - mapCanvasType: z.string(), overviewImageViews: z.array(overviewImageView), topOverviewImage: overviewImageView.nullable(), + license: z.optional(licenseSchema).nullable(), + customLicenseName: z.string(), + customLicenseUrl: z.string(), }); diff --git a/src/redux/apiPath.ts b/src/redux/apiPath.ts index 29aa41741fa7d60ed7c1d72aa6c67709749fd7aa..944cfa00adaa176e99eb468559a8e841fac84832 100644 --- a/src/redux/apiPath.ts +++ b/src/redux/apiPath.ts @@ -63,7 +63,7 @@ export const apiPath = { getConfigurationOptions: (): string => 'configuration/options/', getConfiguration: (): string => 'configuration/', getOverlayBioEntity: ({ overlayId, modelId }: { overlayId: number; modelId: number }): string => - `projects/${PROJECT_ID}/overlays/${overlayId}/models/${modelId}/bioEntities/?includeIndirect=true`, + `projects/${PROJECT_ID}/overlays/${overlayId}/models/${modelId}/bioEntities/`, createOverlay: (projectId: string): string => `projects/${projectId}/overlays/`, createOverlayFile: (): string => `files/`, uploadOverlayFileContent: (fileId: number): string => `files/${fileId}:uploadContent`, @@ -109,4 +109,9 @@ export const apiPath = { getComments: (): string => `projects/${PROJECT_ID}/comments/models/*/`, addComment: (modelId: number, x: number, y: number): string => `projects/${PROJECT_ID}/comments/models/${modelId}/points/${x},${y}/`, + + getSearchAutocomplete: (): string => + `projects/${PROJECT_ID}/models/*/bioEntities/suggestedQueryList`, + getDrugAutocomplete: (): string => `projects/${PROJECT_ID}/drugs/suggestedQueryList`, + getChemicalAutocomplete: (): string => `projects/${PROJECT_ID}/chemicals/suggestedQueryList`, }; diff --git a/src/redux/autocomplete/autocomplete.constants.ts b/src/redux/autocomplete/autocomplete.constants.ts new file mode 100644 index 0000000000000000000000000000000000000000..486a494b599f548981dd4978f2099fc76e5cb445 --- /dev/null +++ b/src/redux/autocomplete/autocomplete.constants.ts @@ -0,0 +1,6 @@ +import { AutocompleteState } from './autocomplete.types'; + +export const AUTOCOMPLETE_INITIAL_STATE: AutocompleteState = { + searchValues: [''], + loading: 'idle', +}; diff --git a/src/redux/autocomplete/autocomplete.reducers.ts b/src/redux/autocomplete/autocomplete.reducers.ts new file mode 100644 index 0000000000000000000000000000000000000000..a36fa9a370add7af40898ef2cac8c441aac40072 --- /dev/null +++ b/src/redux/autocomplete/autocomplete.reducers.ts @@ -0,0 +1,55 @@ +import { AutocompleteState } from '@/redux/autocomplete/autocomplete.types'; +import { ActionReducerMapBuilder } from '@reduxjs/toolkit'; +import { + getChemicalAutocomplete, + getDrugAutocomplete, + getSearchAutocomplete, +} from '@/redux/autocomplete/autocomplete.thunks'; + +export const getSearchAutocompleteReducer = ( + builder: ActionReducerMapBuilder<AutocompleteState>, +): void => { + builder.addCase(getSearchAutocomplete.pending, state => { + state.loading = 'pending'; + }); + builder.addCase(getSearchAutocomplete.fulfilled, (state, action) => { + state.searchValues = action.payload ? action.payload : []; + state.loading = 'succeeded'; + }); + builder.addCase(getSearchAutocomplete.rejected, state => { + state.loading = 'failed'; + // TODO: error management to be discussed in the team + }); +}; + +export const getDrugAutocompleteReducer = ( + builder: ActionReducerMapBuilder<AutocompleteState>, +): void => { + builder.addCase(getDrugAutocomplete.pending, state => { + state.loading = 'pending'; + }); + builder.addCase(getDrugAutocomplete.fulfilled, (state, action) => { + state.searchValues = action.payload ? action.payload : []; + state.loading = 'succeeded'; + }); + builder.addCase(getDrugAutocomplete.rejected, state => { + state.loading = 'failed'; + // TODO: error management to be discussed in the team + }); +}; + +export const getChemicalAutocompleteReducer = ( + builder: ActionReducerMapBuilder<AutocompleteState>, +): void => { + builder.addCase(getChemicalAutocomplete.pending, state => { + state.loading = 'pending'; + }); + builder.addCase(getChemicalAutocomplete.fulfilled, (state, action) => { + state.searchValues = action.payload ? action.payload : []; + state.loading = 'succeeded'; + }); + builder.addCase(getChemicalAutocomplete.rejected, state => { + state.loading = 'failed'; + // TODO: error management to be discussed in the team + }); +}; diff --git a/src/redux/autocomplete/autocomplete.selectors.ts b/src/redux/autocomplete/autocomplete.selectors.ts new file mode 100644 index 0000000000000000000000000000000000000000..d1ee002ae613cfa11a49029ca8c5b12c9bb092d1 --- /dev/null +++ b/src/redux/autocomplete/autocomplete.selectors.ts @@ -0,0 +1,17 @@ +import { rootSelector } from '@/redux/root/root.selectors'; +import { createSelector } from '@reduxjs/toolkit'; + +export const autocompleteSearchSelector = createSelector( + rootSelector, + state => state.autocompleteSearch, +); + +export const autocompleteDrugSelector = createSelector( + rootSelector, + state => state.autocompleteDrug, +); + +export const autocompleteChemicalSelector = createSelector( + rootSelector, + state => state.autocompleteChemical, +); diff --git a/src/redux/autocomplete/autocomplete.slice.ts b/src/redux/autocomplete/autocomplete.slice.ts new file mode 100644 index 0000000000000000000000000000000000000000..6d042065c4d7edd5ceaeb5a6c3f4059df4e9aa8a --- /dev/null +++ b/src/redux/autocomplete/autocomplete.slice.ts @@ -0,0 +1,40 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { AUTOCOMPLETE_INITIAL_STATE } from '@/redux/autocomplete/autocomplete.constants'; +import { + getChemicalAutocompleteReducer, + getDrugAutocompleteReducer, + getSearchAutocompleteReducer, +} from '@/redux/autocomplete/autocomplete.reducers'; + +export const autocompleteSearchSlice = createSlice({ + name: 'autocompleteSearch', + initialState: AUTOCOMPLETE_INITIAL_STATE, + reducers: {}, + extraReducers(builder) { + getSearchAutocompleteReducer(builder); + }, +}); + +export const autocompleteSearchReducer = autocompleteSearchSlice.reducer; + +export const autocompleteDrugSlice = createSlice({ + name: 'autocompleteDrug', + initialState: AUTOCOMPLETE_INITIAL_STATE, + reducers: {}, + extraReducers(builder) { + getDrugAutocompleteReducer(builder); + }, +}); + +export const autocompleteDrugReducer = autocompleteDrugSlice.reducer; + +export const autocompleteChemicalSlice = createSlice({ + name: 'autocompleteChemical', + initialState: AUTOCOMPLETE_INITIAL_STATE, + reducers: {}, + extraReducers(builder) { + getChemicalAutocompleteReducer(builder); + }, +}); + +export const autocompleteChemicalReducer = autocompleteChemicalSlice.reducer; diff --git a/src/redux/autocomplete/autocomplete.thunks.ts b/src/redux/autocomplete/autocomplete.thunks.ts new file mode 100644 index 0000000000000000000000000000000000000000..d9204b9e906e38e46c6d2478dec8ce706db734ef --- /dev/null +++ b/src/redux/autocomplete/autocomplete.thunks.ts @@ -0,0 +1,68 @@ +import { ThunkConfig } from '@/types/store'; +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { getError } from '@/utils/error-report/getError'; +import { axiosInstance } from '@/services/api/utils/axiosInstance'; +import { apiPath } from '@/redux/apiPath'; +import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema'; +import { autocompleteSchema } from '@/models/autocompleteSchema'; +import type { RootState } from '../store'; + +export const getSearchAutocomplete = createAsyncThunk< + string[] | undefined, + void, + { state: RootState } & ThunkConfig +>( + 'project/getSearchAutocomplete', + // eslint-disable-next-line consistent-return + async () => { + try { + const response = await axiosInstance.get<string[]>(apiPath.getSearchAutocomplete()); + + const isDataValid = validateDataUsingZodSchema(response.data, autocompleteSchema); + + return isDataValid ? response.data : undefined; + } catch (error) { + return Promise.reject(getError({ error })); + } + }, +); + +export const getDrugAutocomplete = createAsyncThunk< + string[] | undefined, + void, + { state: RootState } & ThunkConfig +>( + 'project/getDrugAutocomplete', + // eslint-disable-next-line consistent-return + async () => { + try { + const response = await axiosInstance.get<string[]>(apiPath.getDrugAutocomplete()); + + const isDataValid = validateDataUsingZodSchema(response.data, autocompleteSchema); + + return isDataValid ? response.data : undefined; + } catch (error) { + return Promise.reject(getError({ error })); + } + }, +); + +export const getChemicalAutocomplete = createAsyncThunk< + string[] | undefined, + void, + { state: RootState } & ThunkConfig +>( + 'project/getChemicalAutocomplete', + // eslint-disable-next-line consistent-return + async () => { + try { + const response = await axiosInstance.get<string[]>(apiPath.getChemicalAutocomplete()); + + const isDataValid = validateDataUsingZodSchema(response.data, autocompleteSchema); + + return isDataValid ? response.data : undefined; + } catch (error) { + return Promise.reject(getError({ error })); + } + }, +); diff --git a/src/redux/autocomplete/autocomplete.types.ts b/src/redux/autocomplete/autocomplete.types.ts new file mode 100644 index 0000000000000000000000000000000000000000..4d359029df20f73f4ad603d96c5b0fcbaf756bee --- /dev/null +++ b/src/redux/autocomplete/autocomplete.types.ts @@ -0,0 +1,6 @@ +import { Loading } from '@/types/loadingState'; + +export interface AutocompleteState { + searchValues: string[]; + loading: Loading; +} diff --git a/src/redux/bioEntity/bioEntity.constants.ts b/src/redux/bioEntity/bioEntity.constants.ts index b267180f2c4cc9dbc46f9436eda4d6547da8796a..70b92fc4ec9863719d389c994002db131975e998 100644 --- a/src/redux/bioEntity/bioEntity.constants.ts +++ b/src/redux/bioEntity/bioEntity.constants.ts @@ -1,5 +1,5 @@ import { FetchDataState } from '@/types/fetchDataState'; -import { BioEntityContent } from '@/types/models'; +import { BioEntity } from '@/types/models'; import { BioEntityContentsState } from './bioEntity.types'; export const DEFAULT_BIOENTITY_PARAMS = { @@ -9,7 +9,7 @@ export const DEFAULT_BIOENTITY_PARAMS = { export const BIO_ENTITY_FETCHING_ERROR_PREFIX = 'Failed to fetch bio entity'; export const MULTI_BIO_ENTITY_FETCHING_ERROR_PREFIX = 'Failed to fetch multi bio entity'; -export const BIOENTITY_SUBMAP_CONNECTIONS_INITIAL_STATE: FetchDataState<BioEntityContent[]> = { +export const BIOENTITY_SUBMAP_CONNECTIONS_INITIAL_STATE: FetchDataState<BioEntity[]> = { data: [], loading: 'idle', error: { name: '', message: '' }, diff --git a/src/redux/bioEntity/bioEntity.selectors.ts b/src/redux/bioEntity/bioEntity.selectors.ts index ee3b893c71f2dfa198c35186f7cd541a8bb45332..e5b70204d4717e7271abf7add212bc70664611a6 100644 --- a/src/redux/bioEntity/bioEntity.selectors.ts +++ b/src/redux/bioEntity/bioEntity.selectors.ts @@ -51,8 +51,7 @@ export const bioEntityDataListSelector = createSelector(bioEntityDataSelector, b export const allSubmapConnectionsBioEntitySelector = createSelector( bioEntitySelector, - (bioEntityData): BioEntity[] => - (bioEntityData?.submapConnections?.data || []).map(({ bioEntity }) => bioEntity), + (bioEntityData): BioEntity[] => bioEntityData?.submapConnections?.data || [], ); export const allSubmapConnectionsBioEntityOfCurrentSubmapSelector = createSelector( diff --git a/src/redux/bioEntity/bioEntity.types.ts b/src/redux/bioEntity/bioEntity.types.ts index 11c3418fac7c70bd76ce4c2554fad6cfb27722e5..9540daab08abcbe0efede2f8113e75ed81d2709c 100644 --- a/src/redux/bioEntity/bioEntity.types.ts +++ b/src/redux/bioEntity/bioEntity.types.ts @@ -1,9 +1,9 @@ import { FetchDataState, MultiFetchDataState } from '@/types/fetchDataState'; -import { BioEntityContent } from '@/types/models'; +import { BioEntity, BioEntityContent } from '@/types/models'; import { PayloadAction } from '@reduxjs/toolkit'; export type BioEntityContentsState = MultiFetchDataState<BioEntityContent[]> & { - submapConnections?: FetchDataState<BioEntityContent[]>; + submapConnections?: FetchDataState<BioEntity[]>; isContentTabOpened?: boolean; }; diff --git a/src/redux/bioEntity/thunks/getSubmapConnectionsBioEntity.ts b/src/redux/bioEntity/thunks/getSubmapConnectionsBioEntity.ts index e50189938bbe9b45e5fa38601d799c327785df2f..554dfde559edfe0b7700893b7764b26c69d9bc6c 100644 --- a/src/redux/bioEntity/thunks/getSubmapConnectionsBioEntity.ts +++ b/src/redux/bioEntity/thunks/getSubmapConnectionsBioEntity.ts @@ -1,15 +1,15 @@ -import { bioEntityContentSchema } from '@/models/bioEntityContentSchema'; import { submapConnection } from '@/models/submapConnection'; import { apiPath } from '@/redux/apiPath'; import { axiosInstance, axiosInstanceNewAPI } from '@/services/api/utils/axiosInstance'; -import { BioEntityContent, BioEntityResponse, SubmapConnection } from '@/types/models'; +import { BioEntity, SubmapConnection } from '@/types/models'; import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema'; import { createAsyncThunk } from '@reduxjs/toolkit'; import { z } from 'zod'; import { getError } from '@/utils/error-report/getError'; +import { bioEntitySchema } from '@/models/bioEntitySchema'; import { MULTI_BIO_ENTITY_FETCHING_ERROR_PREFIX } from '../bioEntity.constants'; -export const getSubmapConnectionsBioEntity = createAsyncThunk<BioEntityContent[]>( +export const getSubmapConnectionsBioEntity = createAsyncThunk<BioEntity[]>( 'project/getSubmapConnectionsBioEntity', async () => { try { @@ -20,21 +20,20 @@ export const getSubmapConnectionsBioEntity = createAsyncThunk<BioEntityContent[] throw new Error('Submap connections validation error'); } - const searchQueries = response.data.map(({ from }) => `${from.id}`); + const targetElements = response.data.map(({ from }) => from); - const asyncFetchBioEntityFunctions = searchQueries.map(searchQuery => - axiosInstanceNewAPI.get<BioEntityResponse>( - apiPath.getBioEntityContentsStringWithQuery({ searchQuery, isPerfectMatch: true }), + const asyncFetchBioEntityFunctions = targetElements.map(targetElement => + axiosInstanceNewAPI.get<BioEntity>( + apiPath.getElementById(targetElement.id, targetElement.modelId), ), ); const bioEntityContentResponse = await Promise.all(asyncFetchBioEntityFunctions); const bioEntityContents = bioEntityContentResponse - .map(contentResponse => contentResponse?.data?.content || []) + .map(contentResponse => contentResponse.data) .flat() - .filter(content => bioEntityContentSchema.safeParse(content).success) - .filter(payload => 'bioEntity' in payload || {}); + .filter(content => bioEntitySchema.safeParse(content).success); return bioEntityContents; } catch (error) { diff --git a/src/redux/modal/modal.reducers.ts b/src/redux/modal/modal.reducers.ts index f9d88800610efc49275e3cdad86a19a2396a4d97..4556c17cc91238adaa87cb538d3d8548ea4376e4 100644 --- a/src/redux/modal/modal.reducers.ts +++ b/src/redux/modal/modal.reducers.ts @@ -73,6 +73,12 @@ export const openAccessDeniedModalReducer = (state: ModalState): void => { state.modalTitle = 'Access denied!'; }; +export const openSelectProjectModalReducer = (state: ModalState): void => { + state.isOpen = true; + state.modalName = 'select-project'; + state.modalTitle = 'Select project!'; +}; + export const setOverviewImageIdReducer = ( state: ModalState, action: PayloadAction<number>, @@ -97,3 +103,9 @@ export const openEditOverlayModalReducer = ( state.modalTitle = action.payload.name; state.editOverlayState = action.payload; }; + +export const openLicenseModalReducer = (state: ModalState, action: PayloadAction<string>): void => { + state.isOpen = true; + state.modalName = 'license'; + state.modalTitle = `License: ${action.payload}`; +}; diff --git a/src/redux/modal/modal.slice.ts b/src/redux/modal/modal.slice.ts index 57d852cd19596f39ccb2476e0aae599d4927be4f..3e945e4a1e01b8d6cba771860700e4e2e9177927 100644 --- a/src/redux/modal/modal.slice.ts +++ b/src/redux/modal/modal.slice.ts @@ -13,6 +13,8 @@ import { openAddCommentModalReducer, openErrorReportModalReducer, openAccessDeniedModalReducer, + openSelectProjectModalReducer, + openLicenseModalReducer, } from './modal.reducers'; const modalSlice = createSlice({ @@ -31,6 +33,8 @@ const modalSlice = createSlice({ openLoggedInMenuModal: openLoggedInMenuModalReducer, openErrorReportModal: openErrorReportModalReducer, openAccessDeniedModal: openAccessDeniedModalReducer, + openSelectProjectModal: openSelectProjectModalReducer, + openLicenseModal: openLicenseModalReducer, }, }); @@ -47,6 +51,8 @@ export const { openLoggedInMenuModal, openErrorReportModal, openAccessDeniedModal, + openSelectProjectModal, + openLicenseModal, } = modalSlice.actions; export default modalSlice.reducer; diff --git a/src/redux/overlayBioEntity/overlayBioEntity.selector.ts b/src/redux/overlayBioEntity/overlayBioEntity.selector.ts index f6ad669d69dad1723460c82f8764112db4ee87cd..2b96a5389302e6acce4f654a67fb7ed4d23abe98 100644 --- a/src/redux/overlayBioEntity/overlayBioEntity.selector.ts +++ b/src/redux/overlayBioEntity/overlayBioEntity.selector.ts @@ -1,5 +1,6 @@ import { OverlayBioEntityRender } from '@/types/OLrendering'; import { createSelector } from '@reduxjs/toolkit'; +import { allSubmapConnectionsBioEntitySelector } from '@/redux/bioEntity/bioEntity.selectors'; import { currentSearchedBioEntityId } from '../drawer/drawer.selectors'; import { currentModelIdSelector } from '../models/models.selectors'; import { @@ -34,7 +35,8 @@ export const overlayBioEntitiesForCurrentModelSelector = createSelector( overlayBioEntityDataSelector, activeOverlaysIdSelector, currentModelIdSelector, - (data, activeOverlaysIds, currentModelId) => { + allSubmapConnectionsBioEntitySelector, + (data, activeOverlaysIds, currentModelId, submapConnections) => { const result: OverlayBioEntityRender[] = []; activeOverlaysIds.forEach(overlayId => { @@ -43,6 +45,37 @@ export const overlayBioEntitiesForCurrentModelSelector = createSelector( } }); + submapConnections.forEach(submapConnection => { + if (submapConnection.model === currentModelId) { + const submapId = submapConnection?.submodel?.mapId; + if (submapId) { + activeOverlaysIds.forEach(overlayId => { + if (data[overlayId]?.[submapId]) { + data[overlayId][submapId].forEach(overlayBioEntityRender => { + const newOverlayBioEntityRender = { + id: submapConnection.id, + modelId: submapConnection.model, + x1: submapConnection.x, + y2: submapConnection.y, + x2: submapConnection.x + submapConnection.width, + y1: submapConnection.y + submapConnection.height, + width: submapConnection.width, + height: submapConnection.height, + value: overlayBioEntityRender.value, + overlayId: overlayBioEntityRender.overlayId, + color: overlayBioEntityRender.color, + hexColor: overlayBioEntityRender.hexColor, + type: overlayBioEntityRender.type, + geneVariants: overlayBioEntityRender.geneVariants, + name: overlayBioEntityRender.name, + }; + result.push(newOverlayBioEntityRender); + }); + } + }); + } + } + }); return result; }, ); diff --git a/src/redux/overlays/overlays.mock.ts b/src/redux/overlays/overlays.mock.ts index 4f7d8687ba2205a06193fb67dec32cc049a280d2..d3acf0dcb392d87e03cd11917be0176c6ae3fd16 100644 --- a/src/redux/overlays/overlays.mock.ts +++ b/src/redux/overlays/overlays.mock.ts @@ -28,7 +28,6 @@ export const OVERLAYS_INITIAL_STATE_MOCK: OverlaysState = { export const PUBLIC_OVERLAYS_MOCK: MapOverlay[] = [ { name: 'PD substantia nigra', - googleLicenseConsent: false, creator: 'appu-admin', description: 'Differential transcriptome expression from post mortem tissue. Meta-analysis from 8 published datasets, FDR = 0.05, see PMIDs 23832570 and 25447234.', @@ -41,7 +40,6 @@ export const PUBLIC_OVERLAYS_MOCK: MapOverlay[] = [ }, { name: 'Ageing brain', - googleLicenseConsent: false, creator: 'appu-admin', description: 'Differential transcriptome expression from post mortem tissue. Source: Allen Brain Atlas datasets, see PMID 25447234.', @@ -54,7 +52,6 @@ export const PUBLIC_OVERLAYS_MOCK: MapOverlay[] = [ }, { name: 'PRKN variants example', - googleLicenseConsent: false, creator: 'appu-admin', description: 'PRKN variants', genomeType: 'UCSC', @@ -66,7 +63,6 @@ export const PUBLIC_OVERLAYS_MOCK: MapOverlay[] = [ }, { name: 'PRKN variants doubled', - googleLicenseConsent: false, creator: 'appu-admin', description: 'PRKN variants', genomeType: 'UCSC', @@ -78,7 +74,6 @@ export const PUBLIC_OVERLAYS_MOCK: MapOverlay[] = [ }, { name: 'Generic advanced format overlay', - googleLicenseConsent: false, creator: 'appu-admin', description: 'Data set provided by a user', genomeType: null, @@ -125,7 +120,6 @@ export const ADD_OVERLAY_MOCK = { export const USER_OVERLAYS_MOCK: MapOverlay[] = [ { name: 'PD substantia nigra', - googleLicenseConsent: false, creator: 'appu-admin', description: 'Differential transcriptome expression from post mortem tissue. Meta-analysis from 8 published datasets, FDR = 0.05, see PMIDs 23832570 and 25447234.', @@ -138,7 +132,6 @@ export const USER_OVERLAYS_MOCK: MapOverlay[] = [ }, { name: 'Ageing brain', - googleLicenseConsent: false, creator: 'appu-admin', description: 'Differential transcriptome expression from post mortem tissue. Source: Allen Brain Atlas datasets, see PMID 25447234.', diff --git a/src/redux/overlays/overlays.thunks.ts b/src/redux/overlays/overlays.thunks.ts index 700eeb58024c66facd3888536301f49959165498..5a685bc8f5f5ab1140d28411895fd7f9a7cb3d8c 100644 --- a/src/redux/overlays/overlays.thunks.ts +++ b/src/redux/overlays/overlays.thunks.ts @@ -72,7 +72,9 @@ export const getAllUserOverlaysByCreator = createAsyncThunk<MapOverlay[], void, return -1; }; - const sortedUserOverlays = response.data.sort(sortByOrder); + const sortedUserOverlays = response.data + .sort(sortByOrder) + .filter(overlay => !overlay.publicOverlay); return isDataValid ? sortedUserOverlays : []; } catch (error) { @@ -159,7 +161,6 @@ const creteOverlay = async ({ name, description, filename: createdFile.filename, - googleLicenseConsent: false.toString(), type, fileId: createdFile.id.toString(), }; diff --git a/src/redux/root/init.thunks.ts b/src/redux/root/init.thunks.ts index 18dabadf7d56d2deb7dc92b21d8ccbedce474aed..20a722afb23f2b73870dea3ee777170a060f1ac9 100644 --- a/src/redux/root/init.thunks.ts +++ b/src/redux/root/init.thunks.ts @@ -7,6 +7,14 @@ import { PluginsManager } from '@/services/pluginsManager'; import { createAsyncThunk } from '@reduxjs/toolkit'; import { ZERO } from '@/constants/common'; import { getConstant } from '@/redux/constant/constant.thunks'; +import { + getChemicalAutocomplete, + getDrugAutocomplete, + getSearchAutocomplete, +} from '@/redux/autocomplete/autocomplete.thunks'; +import { openSelectProjectModal } from '@/redux/modal/modal.slice'; +import { getProjects } from '@/redux/projects/projects.thunks'; +import { getSubmapConnectionsBioEntity } from '@/redux/bioEntity/thunks/getSubmapConnectionsBioEntity'; import { getAllBackgroundsByProjectId } from '../backgrounds/backgrounds.thunks'; import { getConfiguration, getConfigurationOptions } from '../configuration/configuration.thunks'; import { @@ -86,6 +94,13 @@ export const fetchInitialAppData = createAsyncThunk< // Fetch plugins list dispatch(getAllPlugins()); + // autocomplete + dispatch(getSearchAutocomplete()); + dispatch(getDrugAutocomplete()); + dispatch(getChemicalAutocomplete()); + + dispatch(getSubmapConnectionsBioEntity()); + /** Trigger search */ if (queryData.searchValue) { dispatch(setPerfectMatch(queryData.perfectMatch)); @@ -107,4 +122,8 @@ export const fetchInitialAppData = createAsyncThunk< dispatch(openOverlaysDrawer()); } } + if (queryData.oauthLogin === 'success') { + await dispatch(getProjects()); + dispatch(openSelectProjectModal()); + } }); diff --git a/src/redux/root/root.fixtures.ts b/src/redux/root/root.fixtures.ts index 421151bd9d870052cc5137a3f66f7c3451b494e8..e260f230ea8bd4a0060fe58399cb9ef2b9965087 100644 --- a/src/redux/root/root.fixtures.ts +++ b/src/redux/root/root.fixtures.ts @@ -2,6 +2,7 @@ import { CONSTANT_INITIAL_STATE } from '@/redux/constant/constant.adapter'; import { PROJECTS_STATE_INITIAL_MOCK } from '@/redux/projects/projects.mock'; import { OAUTH_INITIAL_STATE_MOCK } from '@/redux/oauth/oauth.mock'; import { COMMENT_INITIAL_STATE_MOCK } from '@/redux/comment/comment.mock'; +import { AUTOCOMPLETE_INITIAL_STATE } from '@/redux/autocomplete/autocomplete.constants'; import { BACKGROUND_INITIAL_STATE_MOCK } from '../backgrounds/background.mock'; import { BIOENTITY_INITIAL_STATE_MOCK } from '../bioEntity/bioEntity.mock'; import { CHEMICALS_INITIAL_STATE_MOCK } from '../chemicals/chemicals.mock'; @@ -30,6 +31,9 @@ import { RootState } from '../store'; import { USER_INITIAL_STATE_MOCK } from '../user/user.mock'; export const INITIAL_STORE_STATE_MOCK: RootState = { + autocompleteSearch: AUTOCOMPLETE_INITIAL_STATE, + autocompleteDrug: AUTOCOMPLETE_INITIAL_STATE, + autocompleteChemical: AUTOCOMPLETE_INITIAL_STATE, search: SEARCH_STATE_INITIAL_MOCK, project: PROJECT_STATE_INITIAL_MOCK, projects: PROJECTS_STATE_INITIAL_MOCK, diff --git a/src/redux/store.ts b/src/redux/store.ts index ebc65642cab5db9e0a830989779bdaaceec80450..f11f6e57248b0a405241aa54ee29d321e1b29ffc 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -18,6 +18,11 @@ import projectsReducer from '@/redux/projects/projects.slice'; import reactionsReducer from '@/redux/reactions/reactions.slice'; import searchReducer from '@/redux/search/search.slice'; import userReducer from '@/redux/user/user.slice'; +import { + autocompleteChemicalReducer, + autocompleteDrugReducer, + autocompleteSearchReducer, +} from '@/redux/autocomplete/autocomplete.slice'; import { AnyAction, ListenerEffectAPI, @@ -38,6 +43,9 @@ import publicationsReducer from './publications/publications.slice'; import statisticsReducer from './statistics/statistics.slice'; export const reducers = { + autocompleteSearch: autocompleteSearchReducer, + autocompleteDrug: autocompleteDrugReducer, + autocompleteChemical: autocompleteChemicalReducer, search: searchReducer, project: projectReducer, projects: projectsReducer, diff --git a/src/redux/user/user.thunks.ts b/src/redux/user/user.thunks.ts index 6abb07c34200d96522c4c69c59586f950b44b160..3e1f7bc1079e8524940facf371cd28bf14511fb1 100644 --- a/src/redux/user/user.thunks.ts +++ b/src/redux/user/user.thunks.ts @@ -8,6 +8,7 @@ import { USER_ROLE } from '@/constants/user'; import { getError } from '@/utils/error-report/getError'; import axios, { HttpStatusCode } from 'axios'; import { showToast } from '@/utils/showToast'; +import { setLoginForOldMinerva } from '@/utils/setLoginForOldMinerva'; import { apiPath } from '../apiPath'; import { closeModal, openLoggedInMenuModal } from '../modal/modal.slice'; import { hasPrivilege } from './user.utils'; @@ -44,6 +45,8 @@ export const login = createAsyncThunk( const loginName = response.data.login; if (isDataValid) { + setLoginForOldMinerva(loginName); + const userData = await getUserData(loginName); const role = getUserRole(userData.privileges); @@ -91,6 +94,8 @@ export const getSessionValid = createAsyncThunk('user/getSessionValid', async () const role = getUserRole(userData.privileges); if (isDataValid) { + setLoginForOldMinerva(loginName); + return { login: loginName, userData, @@ -107,6 +112,7 @@ export const logout = createAsyncThunk('user/logout', async () => { withCredentials: true, }); + setLoginForOldMinerva(undefined); return undefined; } catch (error) { return Promise.reject(getError({ error, prefix: 'Log out' })); diff --git a/src/types/OLrendering.ts b/src/types/OLrendering.ts index a117dea712eac055fab31a6b96d1965bac0cdc5e..bb96d0c1619680cf7ada55150ee2ca59e7aa971d 100644 --- a/src/types/OLrendering.ts +++ b/src/types/OLrendering.ts @@ -5,9 +5,9 @@ export type OverlayBioEntityRenderType = 'line' | 'rectangle' | 'submap-link'; export type OverlayBioEntityRender = { id: number | string; modelId: number; - /** bottom left corner of whole element, Xmin */ + /** bottom left corner of whole element, xMin */ x1: number; - /** bottom left corner of whole element, Ymin */ + /** bottom left corner of whole element, yMin */ y1: number; /** top right corner of whole element, xMax */ x2: number; diff --git a/src/types/modal.ts b/src/types/modal.ts index 90e7c20d501aa6c7cb50642df1eea295c6406816..1030c46b8b181cbc81f54e7e74054064825b6e16 100644 --- a/src/types/modal.ts +++ b/src/types/modal.ts @@ -4,8 +4,10 @@ export type ModalName = | 'overview-images' | 'mol-art' | 'login' + | 'license' | 'publications' | 'edit-overlay' | 'error-report' | 'access-denied' + | 'select-project' | 'logged-in-menu'; diff --git a/src/types/query.ts b/src/types/query.ts index e41e2520df7ff6550b80c93d0749a43c73fbcaa2..5ac42d9c81468344c80657d83a79373ae3574f91 100644 --- a/src/types/query.ts +++ b/src/types/query.ts @@ -9,6 +9,7 @@ export interface QueryData { initialPosition?: Partial<Point>; overlaysId?: number[]; pluginsId?: string[]; + oauthLogin?: string; } export interface QueryDataParams { @@ -35,4 +36,5 @@ export interface QueryDataRouterParams { z?: string; overlaysId?: string; pluginsId?: string; + oauthLogin?: string; } diff --git a/src/utils/parseQueryToTypes.ts b/src/utils/parseQueryToTypes.ts index 5aebfe4aab60a8069093b693861d08f2460eee17..91e4b5f49aa4b41d54bdd9bc258be9e03131f183 100644 --- a/src/utils/parseQueryToTypes.ts +++ b/src/utils/parseQueryToTypes.ts @@ -13,4 +13,5 @@ export const parseQueryToTypes = (query: QueryDataRouterParams): QueryData => ({ }, overlaysId: query.overlaysId?.split(',').map(Number), pluginsId: query.pluginsId?.split(',').map(String), + oauthLogin: query.oauthLogin, }); diff --git a/src/utils/setLoginForOldMinerva.ts b/src/utils/setLoginForOldMinerva.ts new file mode 100644 index 0000000000000000000000000000000000000000..db31cda390acbaec5925981afb26788bebf144ce --- /dev/null +++ b/src/utils/setLoginForOldMinerva.ts @@ -0,0 +1,8 @@ +export const setLoginForOldMinerva = (loginName: string | undefined): void => { + // old minerva require this information + if (loginName) { + window.localStorage.setItem('LOGIN', loginName); + } else { + window.localStorage.removeItem('LOGIN'); + } +};