From 8a256a013ed13aab7763a2f281703adaa271de58 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Wed, 12 Feb 2025 11:20:04 +0200 Subject: [PATCH 01/18] PM-690 - assets library management --- config/constants/development.js | 5 +- config/constants/production.js | 5 +- package-lock.json | 134 +++++++ package.json | 1 + src/actions/projects.js | 44 ++- src/assets/images/files/aac.svg | 17 + src/assets/images/files/ai.svg | 10 + src/assets/images/files/ase.svg | 18 + src/assets/images/files/asp.svg | 20 + src/assets/images/files/aspx.svg | 22 ++ src/assets/images/files/avi.svg | 12 + src/assets/images/files/bmp.svg | 16 + src/assets/images/files/c++.svg | 13 + src/assets/images/files/cad.svg | 18 + src/assets/images/files/cfm.svg | 15 + src/assets/images/files/cgi.svg | 17 + src/assets/images/files/csh.svg | 20 + src/assets/images/files/css.svg | 27 ++ src/assets/images/files/csv.svg | 21 ++ src/assets/images/files/default.svg | 4 + src/assets/images/files/dmg.svg | 17 + src/assets/images/files/doc.svg | 19 + src/assets/images/files/docx.svg | 21 ++ src/assets/images/files/eps.svg | 18 + src/assets/images/files/epub.svg | 19 + src/assets/images/files/exe.svg | 10 + src/assets/images/files/flash.svg | 21 ++ src/assets/images/files/flv.svg | 10 + src/assets/images/files/font.svg | 15 + src/assets/images/files/gif.svg | 13 + src/assets/images/files/gpx.svg | 16 + src/assets/images/files/gzip.svg | 16 + src/assets/images/files/html.svg | 12 + src/assets/images/files/ics.svg | 21 ++ src/assets/images/files/iso.svg | 20 + src/assets/images/files/jar.svg | 15 + src/assets/images/files/java.svg | 17 + src/assets/images/files/jpg.svg | 17 + src/assets/images/files/js.svg | 17 + src/assets/images/files/jsp.svg | 21 ++ src/assets/images/files/link-12.svg | 11 + src/assets/images/files/log.svg | 17 + src/assets/images/files/max.svg | 14 + src/assets/images/files/md.svg | 13 + src/assets/images/files/mkv.svg | 13 + src/assets/images/files/mov.svg | 16 + src/assets/images/files/mp3.svg | 20 + src/assets/images/files/mp4.svg | 15 + src/assets/images/files/mpg.svg | 16 + src/assets/images/files/obj.svg | 19 + src/assets/images/files/otf.svg | 13 + src/assets/images/files/pdf.svg | 13 + src/assets/images/files/php.svg | 13 + src/assets/images/files/png.svg | 16 + src/assets/images/files/pptx.svg | 15 + src/assets/images/files/psd.svg | 20 + src/assets/images/files/py.svg | 10 + src/assets/images/files/rar.svg | 15 + src/assets/images/files/raw.svg | 17 + src/assets/images/files/rb.svg | 14 + src/assets/images/files/rss.svg | 25 ++ src/assets/images/files/rtf.svg | 11 + src/assets/images/files/sketch.svg | 24 ++ src/assets/images/files/sql.svg | 1 + src/assets/images/files/srt.svg | 18 + src/assets/images/files/svg.svg | 21 ++ src/assets/images/files/tif.svg | 9 + src/assets/images/files/tiff.svg | 10 + src/assets/images/files/ttf.svg | 9 + src/assets/images/files/txt.svg | 10 + src/assets/images/files/wav.svg | 16 + src/assets/images/files/xlsx.svg | 19 + src/assets/images/files/xml.svg | 12 + src/assets/images/files/zip.svg | 11 + .../AssetsLibrary/DownloadFile/index.js | 63 ++++ .../DownloadFile/styles.module.scss | 35 ++ .../AssetsLibrary/ModalAddLink/index.js | 178 +++++++++ .../ModalAddLink/styles.module.scss | 51 +++ .../ModalAttachmentOptions/index.js | 284 ++++++++++++++ .../ModalAttachmentOptions/styles.module.scss | 97 +++++ .../AssetsLibrary/ProjectMember/index.js | 33 ++ .../ProjectMember/styles.module.scss | 5 + .../AssetsLibrary/ProjectMembers/index.js | 60 +++ .../ProjectMembers/styles.module.scss | 17 + .../AssetsLibrary/TabCommon/index.js | 49 +++ .../TabCommon/styles.module.scss | 50 +++ .../AssetsLibrary/TableAssets/index.js | 158 ++++++++ .../TableAssets/styles.module.scss | 34 ++ src/components/AssetsLibrary/index.js | 0 .../Submissions/Submissions.module.scss | 1 - src/components/ChallengesComponent/index.js | 12 +- src/components/DropdownMenu/index.js | 50 +++ .../DropdownMenu/styles.module.scss | 37 ++ src/components/FieldInput/index.js | 12 +- src/components/FieldLabelDynamic/index.js | 14 +- src/components/FieldUserAutoComplete/index.js | 61 +++ .../FieldUserAutoComplete/styles.module.scss | 22 ++ src/components/Icons/IconFile/index.js | 242 ++++++++++++ .../Icons/IconFile/styles.module.scss | 6 + src/components/Icons/IconThreeDot/index.js | 24 ++ .../Icons/IconThreeDot/styles.module.scss | 20 + src/components/Loader/index.js | 10 +- src/components/Modal/ConfirmationModal.js | 32 +- .../Modal/ConfirmationModal.module.scss | 11 +- src/components/Select/styles.js | 1 - src/components/Table/Table.module.scss | 63 ++-- src/components/Table/index.js | 42 ++- src/config/constants.js | 24 +- src/containers/ProjectAssets/index.jsx | 353 ++++++++++++++++++ .../ProjectAssets/styles.module.scss | 65 ++++ .../ProjectEditor/ProjectEditor.module.scss | 8 + src/reducers/projects.js | 33 +- src/routes.js | 15 + src/services/projects.js | 81 ++++ src/styles/_colors.scss | 3 + src/util/tc.js | 20 + src/util/validation.js | 27 ++ 117 files changed, 3599 insertions(+), 74 deletions(-) create mode 100644 src/assets/images/files/aac.svg create mode 100644 src/assets/images/files/ai.svg create mode 100644 src/assets/images/files/ase.svg create mode 100644 src/assets/images/files/asp.svg create mode 100644 src/assets/images/files/aspx.svg create mode 100644 src/assets/images/files/avi.svg create mode 100644 src/assets/images/files/bmp.svg create mode 100644 src/assets/images/files/c++.svg create mode 100644 src/assets/images/files/cad.svg create mode 100644 src/assets/images/files/cfm.svg create mode 100644 src/assets/images/files/cgi.svg create mode 100644 src/assets/images/files/csh.svg create mode 100644 src/assets/images/files/css.svg create mode 100644 src/assets/images/files/csv.svg create mode 100644 src/assets/images/files/default.svg create mode 100644 src/assets/images/files/dmg.svg create mode 100644 src/assets/images/files/doc.svg create mode 100644 src/assets/images/files/docx.svg create mode 100644 src/assets/images/files/eps.svg create mode 100644 src/assets/images/files/epub.svg create mode 100644 src/assets/images/files/exe.svg create mode 100644 src/assets/images/files/flash.svg create mode 100644 src/assets/images/files/flv.svg create mode 100644 src/assets/images/files/font.svg create mode 100644 src/assets/images/files/gif.svg create mode 100644 src/assets/images/files/gpx.svg create mode 100644 src/assets/images/files/gzip.svg create mode 100644 src/assets/images/files/html.svg create mode 100644 src/assets/images/files/ics.svg create mode 100644 src/assets/images/files/iso.svg create mode 100644 src/assets/images/files/jar.svg create mode 100644 src/assets/images/files/java.svg create mode 100644 src/assets/images/files/jpg.svg create mode 100644 src/assets/images/files/js.svg create mode 100644 src/assets/images/files/jsp.svg create mode 100644 src/assets/images/files/link-12.svg create mode 100644 src/assets/images/files/log.svg create mode 100644 src/assets/images/files/max.svg create mode 100644 src/assets/images/files/md.svg create mode 100644 src/assets/images/files/mkv.svg create mode 100644 src/assets/images/files/mov.svg create mode 100644 src/assets/images/files/mp3.svg create mode 100644 src/assets/images/files/mp4.svg create mode 100644 src/assets/images/files/mpg.svg create mode 100644 src/assets/images/files/obj.svg create mode 100644 src/assets/images/files/otf.svg create mode 100644 src/assets/images/files/pdf.svg create mode 100644 src/assets/images/files/php.svg create mode 100644 src/assets/images/files/png.svg create mode 100644 src/assets/images/files/pptx.svg create mode 100644 src/assets/images/files/psd.svg create mode 100644 src/assets/images/files/py.svg create mode 100644 src/assets/images/files/rar.svg create mode 100644 src/assets/images/files/raw.svg create mode 100644 src/assets/images/files/rb.svg create mode 100644 src/assets/images/files/rss.svg create mode 100644 src/assets/images/files/rtf.svg create mode 100644 src/assets/images/files/sketch.svg create mode 100644 src/assets/images/files/sql.svg create mode 100644 src/assets/images/files/srt.svg create mode 100644 src/assets/images/files/svg.svg create mode 100644 src/assets/images/files/tif.svg create mode 100644 src/assets/images/files/tiff.svg create mode 100644 src/assets/images/files/ttf.svg create mode 100644 src/assets/images/files/txt.svg create mode 100644 src/assets/images/files/wav.svg create mode 100644 src/assets/images/files/xlsx.svg create mode 100644 src/assets/images/files/xml.svg create mode 100644 src/assets/images/files/zip.svg create mode 100644 src/components/AssetsLibrary/DownloadFile/index.js create mode 100644 src/components/AssetsLibrary/DownloadFile/styles.module.scss create mode 100644 src/components/AssetsLibrary/ModalAddLink/index.js create mode 100644 src/components/AssetsLibrary/ModalAddLink/styles.module.scss create mode 100644 src/components/AssetsLibrary/ModalAttachmentOptions/index.js create mode 100644 src/components/AssetsLibrary/ModalAttachmentOptions/styles.module.scss create mode 100644 src/components/AssetsLibrary/ProjectMember/index.js create mode 100644 src/components/AssetsLibrary/ProjectMember/styles.module.scss create mode 100644 src/components/AssetsLibrary/ProjectMembers/index.js create mode 100644 src/components/AssetsLibrary/ProjectMembers/styles.module.scss create mode 100644 src/components/AssetsLibrary/TabCommon/index.js create mode 100644 src/components/AssetsLibrary/TabCommon/styles.module.scss create mode 100644 src/components/AssetsLibrary/TableAssets/index.js create mode 100644 src/components/AssetsLibrary/TableAssets/styles.module.scss create mode 100644 src/components/AssetsLibrary/index.js create mode 100644 src/components/DropdownMenu/index.js create mode 100644 src/components/DropdownMenu/styles.module.scss create mode 100644 src/components/FieldUserAutoComplete/index.js create mode 100644 src/components/FieldUserAutoComplete/styles.module.scss create mode 100644 src/components/Icons/IconFile/index.js create mode 100644 src/components/Icons/IconFile/styles.module.scss create mode 100644 src/components/Icons/IconThreeDot/index.js create mode 100644 src/components/Icons/IconThreeDot/styles.module.scss create mode 100644 src/containers/ProjectAssets/index.jsx create mode 100644 src/containers/ProjectAssets/styles.module.scss diff --git a/config/constants/development.js b/config/constants/development.js index fd7938dc..a728f4ef 100644 --- a/config/constants/development.js +++ b/config/constants/development.js @@ -44,8 +44,10 @@ module.exports = { CREATE_FORUM_TYPE_IDS: ['927abff4-7af9-4145-8ba1-577c16e64e2e', 'dc876fa4-ef2d-4eee-b701-b555fcc6544c', 'ecd58c69-238f-43a4-a4bb-d172719b9f31', '78b37a69-92d5-4ad7-bf85-c79b65420c79', '929bc408-9cf2-4b3e-ba71-adfbf693046c'], FILE_PICKER_API_KEY: process.env.FILE_PICKER_API_KEY, FILE_PICKER_CONTAINER_NAME: 'tc-challenge-v5-dev', + FILE_PICKER_SUBMISSION_CONTAINER_NAME: process.env.FILE_PICKER_SUBMISSION_CONTAINER_NAME || 'submission-staging-dev', FILE_PICKER_REGION: 'us-east-1', FILE_PICKER_CNAME: 'fs.topcoder.com', + FILE_PICKER_LOCATION: 's3', // if idle for this many minutes, show user a prompt saying they'll be logged out IDLE_TIMEOUT_MINUTES: 10, // duration to show the prompt saying user will be logged out, before actually logging out the user @@ -57,5 +59,6 @@ module.exports = { SKILLS_V5_API_URL: `${API_V5}/standardized-skills/skills/autocomplete`, UPDATE_SKILLS_V5_API_URL: `${API_V5}/standardized-skills/challenge-skills`, SALESFORCE_BILLING_ACCOUNT_LINK: 'https://c.cs18.visual.force.com/apex/baredirect?id=', - TYPEFORM_URL: 'https://topcoder.typeform.com/to/YJ7AL4p8' + TYPEFORM_URL: 'https://topcoder.typeform.com/to/YJ7AL4p8', + PROFILE_URL: 'https://profiles.topcoder-dev.com/' } diff --git a/config/constants/production.js b/config/constants/production.js index 56a94c63..bbebe026 100644 --- a/config/constants/production.js +++ b/config/constants/production.js @@ -43,8 +43,10 @@ module.exports = { CREATE_FORUM_TYPE_IDS: ['927abff4-7af9-4145-8ba1-577c16e64e2e', 'dc876fa4-ef2d-4eee-b701-b555fcc6544c', 'ecd58c69-238f-43a4-a4bb-d172719b9f31', '78b37a69-92d5-4ad7-bf85-c79b65420c79', '929bc408-9cf2-4b3e-ba71-adfbf693046c'], FILE_PICKER_API_KEY: process.env.FILE_PICKER_API_KEY, FILE_PICKER_CONTAINER_NAME: 'tc-challenge-v5-prod', + FILE_PICKER_SUBMISSION_CONTAINER_NAME: process.env.FILE_PICKER_SUBMISSION_CONTAINER_NAME || 'submission-staging-prod', FILE_PICKER_REGION: 'us-east-1', FILE_PICKER_CNAME: 'fs.topcoder.com', + FILE_PICKER_LOCATION: 's3', IDLE_TIMEOUT_MINUTES: 10, IDLE_TIMEOUT_GRACE_MINUTES: 5, MULTI_ROUND_CHALLENGE_TEMPLATE_ID: 'd4201ca4-8437-4d63-9957-3f7708184b07', @@ -54,5 +56,6 @@ module.exports = { SKILLS_V5_API_URL: `${API_V5}/standardized-skills/skills/autocomplete`, UPDATE_SKILLS_V5_API_URL: `${API_V5}/standardized-skills/challenge-skills`, SALESFORCE_BILLING_ACCOUNT_LINK: 'https://topcoder.my.salesforce.com/apex/baredirect?id=', - TYPEFORM_URL: 'https://topcoder.typeform.com/to/YJ7AL4p8' + TYPEFORM_URL: 'https://topcoder.typeform.com/to/YJ7AL4p8', + PROFILE_URL: 'https://profiles.topcoder.com/' } diff --git a/package-lock.json b/package-lock.json index 26029628..b7ab6daa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2931,6 +2931,11 @@ "prop-types": "^15.7.2" } }, + "@hookform/resolvers": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz", + "integrity": "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==" + }, "@jridgewell/gen-mapping": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", @@ -3137,6 +3142,25 @@ "resolved": "https://registry.npmjs.org/@tanem/svg-injector/-/svg-injector-1.2.1.tgz", "integrity": "sha512-mA5Q5ulPoGQ+e08Vts1R6xw2QU0BKEnMH/KcqoYoS7Gk6imvMTpyFPeu1g+NOZObSIoAzA3/kRzY8m96cEBA2A==" }, + "@toast-ui/editor": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/@toast-ui/editor/-/editor-2.5.4.tgz", + "integrity": "sha512-XsuYlPQxhec9dHQREFAigjE4enHSuGMF7D0YQ6wW7phmusvAu0FnJfZUPjJBoU/GKz7WP5U6fKU9/P+8j65D8A==", + "requires": { + "@types/codemirror": "0.0.71", + "codemirror": "^5.48.4" + }, + "dependencies": { + "@types/codemirror": { + "version": "0.0.71", + "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.71.tgz", + "integrity": "sha512-b2oEEnno1LIGKMR7uBEsr40al1UijF1HEpRn0+Yf1xOLl24iQgB7DBpZVMM7y54G5wCNoclDrRO65E6KHPNO2w==", + "requires": { + "@types/tern": "*" + } + } + } + }, "@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -3255,6 +3279,25 @@ "@types/unist": "*" } }, + "@types/hoist-non-react-statics": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz", + "integrity": "sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==", + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + }, + "dependencies": { + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "requires": { + "react-is": "^16.7.0" + } + } + } + }, "@types/katex": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.11.1.tgz", @@ -3318,6 +3361,21 @@ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" }, + "@types/react": { + "version": "19.0.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.8.tgz", + "integrity": "sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw==", + "requires": { + "csstype": "^3.0.2" + }, + "dependencies": { + "csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + } + } + }, "@types/serve-static": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", @@ -5819,6 +5877,11 @@ "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, + "codemirror": { + "version": "5.65.18", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.18.tgz", + "integrity": "sha512-Gaz4gHnkbHMGgahNt3CA5HBk5lLQBqmD/pBgeB4kQU6OedZmqMBjlRF0LSrp2tJ4wlLNPm2FfaUd1pDy0mdlpA==" + }, "codemirror-spell-checker": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/codemirror-spell-checker/-/codemirror-spell-checker-1.1.2.tgz", @@ -6821,6 +6884,11 @@ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" }, + "deepmerge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", + "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==" + }, "default-gateway": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz", @@ -8795,6 +8863,41 @@ "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz", "integrity": "sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==" }, + "formik": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/formik/-/formik-2.4.6.tgz", + "integrity": "sha512-A+2EI7U7aG296q2TLGvNapDNTZp1khVt5Vk0Q/fyfSROss0V/V6+txt2aJnwEos44IxTCW/LYAi/zgWzlevj+g==", + "requires": { + "@types/hoist-non-react-statics": "^3.3.1", + "deepmerge": "^2.1.1", + "hoist-non-react-statics": "^3.3.0", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "react-fast-compare": "^2.0.1", + "tiny-warning": "^1.0.2", + "tslib": "^2.0.0" + }, + "dependencies": { + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "requires": { + "react-is": "^16.7.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + } + } + }, "forwarded": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", @@ -17495,6 +17598,11 @@ "react-is": "^16.8.1" } }, + "property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==" + }, "property-information": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", @@ -21553,6 +21661,11 @@ "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=" }, + "tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==" + }, "tiny-invariant": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz", @@ -21799,6 +21912,11 @@ } } }, + "toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + }, "tough-cookie": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", @@ -21914,6 +22032,11 @@ "prelude-ls": "~1.1.2" } }, + "type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==" + }, "type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -24272,6 +24395,17 @@ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==" }, + "yup": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.6.1.tgz", + "integrity": "sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA==", + "requires": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } + }, "zwitch": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz", diff --git a/package.json b/package.json index dbd288db..c0b81e02 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "@fortawesome/fontawesome-svg-core": "^1.2.14", "@fortawesome/free-solid-svg-icons": "^5.7.1", "@fortawesome/react-fontawesome": "^0.1.4", + "@hookform/resolvers": "^3.10.0", "@nateradebaugh/react-datetime": "^4.4.11", "@popperjs/core": "^2.5.4", "@svgr/webpack": "2.4.1", diff --git a/src/actions/projects.js b/src/actions/projects.js index 45d18a58..5f38015c 100644 --- a/src/actions/projects.js +++ b/src/actions/projects.js @@ -9,7 +9,10 @@ import { LOAD_PROJECT_BILLING_ACCOUNTS, UPDATE_PROJECT_PENDING, UPDATE_PROJECT_SUCCESS, - UPDATE_PROJECT_FAILURE + UPDATE_PROJECT_FAILURE, + ADD_PROJECT_ATTACHMENT_SUCCESS, + UPDATE_PROJECT_ATTACHMENT_SUCCESS, + REMOVE_PROJECT_ATTACHMENT_SUCCESS } from '../config/constants' import { fetchProjectById, @@ -84,6 +87,45 @@ export function createProject (project) { } } +/** + * Add attachment to project + * @param {Object} newAttachment new attachment data + */ +export function addAttachment (newAttachment) { + return (dispatch) => { + return dispatch({ + type: ADD_PROJECT_ATTACHMENT_SUCCESS, + payload: newAttachment + }) + } +} + +/** + * Update project attachment + * @param {Object} newAttachment new attachment data + */ +export function updateAttachment (newAttachment) { + return (dispatch) => { + return dispatch({ + type: UPDATE_PROJECT_ATTACHMENT_SUCCESS, + payload: newAttachment + }) + } +} + +/** + * Remove project attachment + * @param {number} attachmentId attachment id + */ +export function removeAttachment (attachmentId) { + return (dispatch) => { + return dispatch({ + type: REMOVE_PROJECT_ATTACHMENT_SUCCESS, + payload: attachmentId + }) + } +} + /** * Only loads project details * @param {String} projectId Id of the project diff --git a/src/assets/images/files/aac.svg b/src/assets/images/files/aac.svg new file mode 100644 index 00000000..38d95bbf --- /dev/null +++ b/src/assets/images/files/aac.svg @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/ai.svg b/src/assets/images/files/ai.svg new file mode 100644 index 00000000..33d19427 --- /dev/null +++ b/src/assets/images/files/ai.svg @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/ase.svg b/src/assets/images/files/ase.svg new file mode 100644 index 00000000..ad573707 --- /dev/null +++ b/src/assets/images/files/ase.svg @@ -0,0 +1,18 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/asp.svg b/src/assets/images/files/asp.svg new file mode 100644 index 00000000..0a254722 --- /dev/null +++ b/src/assets/images/files/asp.svg @@ -0,0 +1,20 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/aspx.svg b/src/assets/images/files/aspx.svg new file mode 100644 index 00000000..de4dd224 --- /dev/null +++ b/src/assets/images/files/aspx.svg @@ -0,0 +1,22 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/avi.svg b/src/assets/images/files/avi.svg new file mode 100644 index 00000000..9da8a8c3 --- /dev/null +++ b/src/assets/images/files/avi.svg @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/bmp.svg b/src/assets/images/files/bmp.svg new file mode 100644 index 00000000..d966f646 --- /dev/null +++ b/src/assets/images/files/bmp.svg @@ -0,0 +1,16 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/c++.svg b/src/assets/images/files/c++.svg new file mode 100644 index 00000000..bbe352c4 --- /dev/null +++ b/src/assets/images/files/c++.svg @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/cad.svg b/src/assets/images/files/cad.svg new file mode 100644 index 00000000..16ee1953 --- /dev/null +++ b/src/assets/images/files/cad.svg @@ -0,0 +1,18 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/cfm.svg b/src/assets/images/files/cfm.svg new file mode 100644 index 00000000..79870942 --- /dev/null +++ b/src/assets/images/files/cfm.svg @@ -0,0 +1,15 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/cgi.svg b/src/assets/images/files/cgi.svg new file mode 100644 index 00000000..e8811aa6 --- /dev/null +++ b/src/assets/images/files/cgi.svg @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/csh.svg b/src/assets/images/files/csh.svg new file mode 100644 index 00000000..eb5cca5d --- /dev/null +++ b/src/assets/images/files/csh.svg @@ -0,0 +1,20 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/css.svg b/src/assets/images/files/css.svg new file mode 100644 index 00000000..59692551 --- /dev/null +++ b/src/assets/images/files/css.svg @@ -0,0 +1,27 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/csv.svg b/src/assets/images/files/csv.svg new file mode 100644 index 00000000..e1dd6611 --- /dev/null +++ b/src/assets/images/files/csv.svg @@ -0,0 +1,21 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/default.svg b/src/assets/images/files/default.svg new file mode 100644 index 00000000..2676714d --- /dev/null +++ b/src/assets/images/files/default.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/assets/images/files/dmg.svg b/src/assets/images/files/dmg.svg new file mode 100644 index 00000000..1e55afb8 --- /dev/null +++ b/src/assets/images/files/dmg.svg @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/doc.svg b/src/assets/images/files/doc.svg new file mode 100644 index 00000000..dee20735 --- /dev/null +++ b/src/assets/images/files/doc.svg @@ -0,0 +1,19 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/docx.svg b/src/assets/images/files/docx.svg new file mode 100644 index 00000000..298c8c0e --- /dev/null +++ b/src/assets/images/files/docx.svg @@ -0,0 +1,21 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/eps.svg b/src/assets/images/files/eps.svg new file mode 100644 index 00000000..35879b7a --- /dev/null +++ b/src/assets/images/files/eps.svg @@ -0,0 +1,18 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/epub.svg b/src/assets/images/files/epub.svg new file mode 100644 index 00000000..7e18a411 --- /dev/null +++ b/src/assets/images/files/epub.svg @@ -0,0 +1,19 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/exe.svg b/src/assets/images/files/exe.svg new file mode 100644 index 00000000..6d9238cc --- /dev/null +++ b/src/assets/images/files/exe.svg @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/flash.svg b/src/assets/images/files/flash.svg new file mode 100644 index 00000000..7ead3253 --- /dev/null +++ b/src/assets/images/files/flash.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/flv.svg b/src/assets/images/files/flv.svg new file mode 100644 index 00000000..375ae6d4 --- /dev/null +++ b/src/assets/images/files/flv.svg @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/font.svg b/src/assets/images/files/font.svg new file mode 100644 index 00000000..841cf5a9 --- /dev/null +++ b/src/assets/images/files/font.svg @@ -0,0 +1,15 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/gif.svg b/src/assets/images/files/gif.svg new file mode 100644 index 00000000..eefd4a67 --- /dev/null +++ b/src/assets/images/files/gif.svg @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/gpx.svg b/src/assets/images/files/gpx.svg new file mode 100644 index 00000000..1052c35c --- /dev/null +++ b/src/assets/images/files/gpx.svg @@ -0,0 +1,16 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/gzip.svg b/src/assets/images/files/gzip.svg new file mode 100644 index 00000000..3547af13 --- /dev/null +++ b/src/assets/images/files/gzip.svg @@ -0,0 +1,16 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/html.svg b/src/assets/images/files/html.svg new file mode 100644 index 00000000..5400dc37 --- /dev/null +++ b/src/assets/images/files/html.svg @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/ics.svg b/src/assets/images/files/ics.svg new file mode 100644 index 00000000..c5bc30f8 --- /dev/null +++ b/src/assets/images/files/ics.svg @@ -0,0 +1,21 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/iso.svg b/src/assets/images/files/iso.svg new file mode 100644 index 00000000..a3154bd5 --- /dev/null +++ b/src/assets/images/files/iso.svg @@ -0,0 +1,20 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/jar.svg b/src/assets/images/files/jar.svg new file mode 100644 index 00000000..73320066 --- /dev/null +++ b/src/assets/images/files/jar.svg @@ -0,0 +1,15 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/java.svg b/src/assets/images/files/java.svg new file mode 100644 index 00000000..b4e93e49 --- /dev/null +++ b/src/assets/images/files/java.svg @@ -0,0 +1,17 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/jpg.svg b/src/assets/images/files/jpg.svg new file mode 100644 index 00000000..d2c71c95 --- /dev/null +++ b/src/assets/images/files/jpg.svg @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/js.svg b/src/assets/images/files/js.svg new file mode 100644 index 00000000..d3830b5c --- /dev/null +++ b/src/assets/images/files/js.svg @@ -0,0 +1,17 @@ + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/jsp.svg b/src/assets/images/files/jsp.svg new file mode 100644 index 00000000..7c73e070 --- /dev/null +++ b/src/assets/images/files/jsp.svg @@ -0,0 +1,21 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/link-12.svg b/src/assets/images/files/link-12.svg new file mode 100644 index 00000000..c7a68910 --- /dev/null +++ b/src/assets/images/files/link-12.svg @@ -0,0 +1,11 @@ + + + + DEA252EB-7149-42DE-B34C-229261299CDE + Created with sketchtool. + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/log.svg b/src/assets/images/files/log.svg new file mode 100644 index 00000000..ddef4c49 --- /dev/null +++ b/src/assets/images/files/log.svg @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/max.svg b/src/assets/images/files/max.svg new file mode 100644 index 00000000..cda62f32 --- /dev/null +++ b/src/assets/images/files/max.svg @@ -0,0 +1,14 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/md.svg b/src/assets/images/files/md.svg new file mode 100644 index 00000000..d94d0e8e --- /dev/null +++ b/src/assets/images/files/md.svg @@ -0,0 +1,13 @@ + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/mkv.svg b/src/assets/images/files/mkv.svg new file mode 100644 index 00000000..78ac9de0 --- /dev/null +++ b/src/assets/images/files/mkv.svg @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/mov.svg b/src/assets/images/files/mov.svg new file mode 100644 index 00000000..63fdcceb --- /dev/null +++ b/src/assets/images/files/mov.svg @@ -0,0 +1,16 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/mp3.svg b/src/assets/images/files/mp3.svg new file mode 100644 index 00000000..cd0004d7 --- /dev/null +++ b/src/assets/images/files/mp3.svg @@ -0,0 +1,20 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/mp4.svg b/src/assets/images/files/mp4.svg new file mode 100644 index 00000000..07efa4fb --- /dev/null +++ b/src/assets/images/files/mp4.svg @@ -0,0 +1,15 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/mpg.svg b/src/assets/images/files/mpg.svg new file mode 100644 index 00000000..8f0063bf --- /dev/null +++ b/src/assets/images/files/mpg.svg @@ -0,0 +1,16 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/obj.svg b/src/assets/images/files/obj.svg new file mode 100644 index 00000000..b10608e5 --- /dev/null +++ b/src/assets/images/files/obj.svg @@ -0,0 +1,19 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/otf.svg b/src/assets/images/files/otf.svg new file mode 100644 index 00000000..ec9e8a84 --- /dev/null +++ b/src/assets/images/files/otf.svg @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/pdf.svg b/src/assets/images/files/pdf.svg new file mode 100644 index 00000000..e51db6af --- /dev/null +++ b/src/assets/images/files/pdf.svg @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/php.svg b/src/assets/images/files/php.svg new file mode 100644 index 00000000..4fa31bf5 --- /dev/null +++ b/src/assets/images/files/php.svg @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/png.svg b/src/assets/images/files/png.svg new file mode 100644 index 00000000..dc79b7b7 --- /dev/null +++ b/src/assets/images/files/png.svg @@ -0,0 +1,16 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/pptx.svg b/src/assets/images/files/pptx.svg new file mode 100644 index 00000000..d6b420ee --- /dev/null +++ b/src/assets/images/files/pptx.svg @@ -0,0 +1,15 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/psd.svg b/src/assets/images/files/psd.svg new file mode 100644 index 00000000..c9505839 --- /dev/null +++ b/src/assets/images/files/psd.svg @@ -0,0 +1,20 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/py.svg b/src/assets/images/files/py.svg new file mode 100644 index 00000000..7ad94154 --- /dev/null +++ b/src/assets/images/files/py.svg @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/rar.svg b/src/assets/images/files/rar.svg new file mode 100644 index 00000000..c11a7ea1 --- /dev/null +++ b/src/assets/images/files/rar.svg @@ -0,0 +1,15 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/raw.svg b/src/assets/images/files/raw.svg new file mode 100644 index 00000000..f636dace --- /dev/null +++ b/src/assets/images/files/raw.svg @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/rb.svg b/src/assets/images/files/rb.svg new file mode 100644 index 00000000..4bec9d8d --- /dev/null +++ b/src/assets/images/files/rb.svg @@ -0,0 +1,14 @@ + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/rss.svg b/src/assets/images/files/rss.svg new file mode 100644 index 00000000..ee5e4420 --- /dev/null +++ b/src/assets/images/files/rss.svg @@ -0,0 +1,25 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/rtf.svg b/src/assets/images/files/rtf.svg new file mode 100644 index 00000000..d6833570 --- /dev/null +++ b/src/assets/images/files/rtf.svg @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/sketch.svg b/src/assets/images/files/sketch.svg new file mode 100644 index 00000000..ebe192e7 --- /dev/null +++ b/src/assets/images/files/sketch.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/sql.svg b/src/assets/images/files/sql.svg new file mode 100644 index 00000000..75b8f3bc --- /dev/null +++ b/src/assets/images/files/sql.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/files/srt.svg b/src/assets/images/files/srt.svg new file mode 100644 index 00000000..ab7e8658 --- /dev/null +++ b/src/assets/images/files/srt.svg @@ -0,0 +1,18 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/svg.svg b/src/assets/images/files/svg.svg new file mode 100644 index 00000000..9923ee36 --- /dev/null +++ b/src/assets/images/files/svg.svg @@ -0,0 +1,21 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/tif.svg b/src/assets/images/files/tif.svg new file mode 100644 index 00000000..ca24a164 --- /dev/null +++ b/src/assets/images/files/tif.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/tiff.svg b/src/assets/images/files/tiff.svg new file mode 100644 index 00000000..928ce965 --- /dev/null +++ b/src/assets/images/files/tiff.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/ttf.svg b/src/assets/images/files/ttf.svg new file mode 100644 index 00000000..33800659 --- /dev/null +++ b/src/assets/images/files/ttf.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/txt.svg b/src/assets/images/files/txt.svg new file mode 100644 index 00000000..992f5d5e --- /dev/null +++ b/src/assets/images/files/txt.svg @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/wav.svg b/src/assets/images/files/wav.svg new file mode 100644 index 00000000..989a380a --- /dev/null +++ b/src/assets/images/files/wav.svg @@ -0,0 +1,16 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/xlsx.svg b/src/assets/images/files/xlsx.svg new file mode 100644 index 00000000..1555d7b3 --- /dev/null +++ b/src/assets/images/files/xlsx.svg @@ -0,0 +1,19 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/xml.svg b/src/assets/images/files/xml.svg new file mode 100644 index 00000000..f57f8428 --- /dev/null +++ b/src/assets/images/files/xml.svg @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/files/zip.svg b/src/assets/images/files/zip.svg new file mode 100644 index 00000000..ed8c4a95 --- /dev/null +++ b/src/assets/images/files/zip.svg @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/components/AssetsLibrary/DownloadFile/index.js b/src/components/AssetsLibrary/DownloadFile/index.js new file mode 100644 index 00000000..a76a95ba --- /dev/null +++ b/src/components/AssetsLibrary/DownloadFile/index.js @@ -0,0 +1,63 @@ +/* Component to render button to download project attachment file */ + +import React, { useState } from 'react' +import PropTypes from 'prop-types' +import _ from 'lodash' +import styles from './styles.module.scss' +import Loader from '../../Loader' +import { toastr } from 'react-redux-toastr' +import cn from 'classnames' +import { getProjectAttachment } from '../../../services/projects' +import ReactSVG from 'react-svg' +const Download = './IconSquareDownload.svg' +const assets = require.context('../../../assets/images', false, /svg/) + +const DownloadFile = ({ classsName, file, projectId }) => { + const [isLoading, setIsLoading] = useState(false) + + return ( + + ) +} + +DownloadFile.defaultProps = { + file: {} +} + +DownloadFile.propTypes = { + classsName: PropTypes.string, + projectId: PropTypes.string, + file: PropTypes.shape() +} + +export default DownloadFile diff --git a/src/components/AssetsLibrary/DownloadFile/styles.module.scss b/src/components/AssetsLibrary/DownloadFile/styles.module.scss new file mode 100644 index 00000000..fccd9fd5 --- /dev/null +++ b/src/components/AssetsLibrary/DownloadFile/styles.module.scss @@ -0,0 +1,35 @@ +@import '../../../styles/includes'; + +.container { + display: flex; + padding: 0; + margin: 0; + border: none; + outline: none !important; + background-color: transparent; + color: $tc-gray-90 !important; + &:hover .downloadIcon { + opacity: 1; + } +} + +.loader { + width: auto; + height: auto; + margin-left: 5px; + + svg { + width: 20px; + height: 20px; + } +} + +.downloadIcon { + opacity: 0; + margin-left: 5px; + transition: 0.15s ease; + svg { + width: 12px; + height: 12px; + } +} \ No newline at end of file diff --git a/src/components/AssetsLibrary/ModalAddLink/index.js b/src/components/AssetsLibrary/ModalAddLink/index.js new file mode 100644 index 00000000..420f287a --- /dev/null +++ b/src/components/AssetsLibrary/ModalAddLink/index.js @@ -0,0 +1,178 @@ +/* Component to render add link modal */ + +import React, { useMemo, useState, useEffect, useCallback } from 'react' +import { useForm } from 'react-hook-form' +import { yupResolver } from '@hookform/resolvers/yup' +import { connect } from 'react-redux' +import _ from 'lodash' +import PropTypes from 'prop-types' +import styles from './styles.module.scss' +import cn from 'classnames' +import { toastr } from 'react-redux-toastr' +import Modal from '../../Modal' +import PrimaryButton from '../../Buttons/PrimaryButton' +import OutlineButton from '../../Buttons/OutlineButton' +import FieldLabelDynamic from '../../FieldLabelDynamic' +import FieldInput from '../../FieldInput' +import { assetsLibraryAddLinkSchema } from '../../../util/validation' +import { addAttachment, updateAttachment } from '../../../actions/projects' +import { + addProjectAttachmentApi, + updateProjectAttachmentApi +} from '../../../services/projects' +import { ATTACHMENT_TYPE_LINK } from '../../../config/constants' + +const ModalAddLink = ({ + classsName, + theme, + onCancel, + link, + addAttachment, + updateAttachment, + projectId +}) => { + const [isProcessing, setIsProcessing] = useState(false) + const buttonText = useMemo(() => (link ? 'Edit Link' : 'Add Link'), [link]) + + const { + register, + handleSubmit, + reset, + formState: { errors, isValid, isDirty } + } = useForm({ + defaultValues: { + title: '', + path: '' + }, + resolver: yupResolver(assetsLibraryAddLinkSchema), + mode: 'all' + }) + + useEffect(() => { + if (link) { + reset({ + title: link.title, + path: link.path + }) + } + }, [link]) + + const onSubmit = useCallback( + data => { + if (!link) { + setIsProcessing(true) + addProjectAttachmentApi(projectId, { + ...data, + tags: [], + type: ATTACHMENT_TYPE_LINK + }) + .then(result => { + toastr.success('Success', 'Added link to the project successfully.') + setIsProcessing(false) + addAttachment(result) + onCancel() + }) + .catch(e => { + setIsProcessing(false) + const errorMessage = _.get( + e, + 'response.data.message', + 'Failed to add link.' + ) + toastr.error('Error', errorMessage) + }) + } else { + setIsProcessing(true) + updateProjectAttachmentApi(projectId, link.id, data) + .then(result => { + toastr.success('Success', 'Updated link successfully.') + setIsProcessing(false) + updateAttachment(result) + onCancel() + }) + .catch(e => { + setIsProcessing(false) + const errorMessage = _.get( + e, + 'response.data.message', + 'Failed to update link.' + ) + toastr.error('Error', errorMessage) + }) + } + }, + [link, projectId] + ) + + return ( + +
+
{link ? 'EDIT LINK' : 'ADD A LINK'}
+ +
+ + + + + + +
+
+ +
+
+ +
+
+
+
+
+ ) +} + +ModalAddLink.defaultProps = { + isProcessing: false, + projectId: '', + onCancel: () => {} +} + +ModalAddLink.propTypes = { + classsName: PropTypes.string, + theme: PropTypes.shape(), + onCancel: PropTypes.func, + addAttachment: PropTypes.func.isRequired, + updateAttachment: PropTypes.func.isRequired, + link: PropTypes.shape(), + projectId: PropTypes.string +} + +const mapStateToProps = () => { + return {} +} + +const mapDispatchToProps = { + addAttachment, + updateAttachment +} + +export default connect(mapStateToProps, mapDispatchToProps)(ModalAddLink) diff --git a/src/components/AssetsLibrary/ModalAddLink/styles.module.scss b/src/components/AssetsLibrary/ModalAddLink/styles.module.scss new file mode 100644 index 00000000..8ff2a99d --- /dev/null +++ b/src/components/AssetsLibrary/ModalAddLink/styles.module.scss @@ -0,0 +1,51 @@ +@import '../../../styles/includes'; + +.container { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + box-sizing: border-box; + background: $white; + opacity: 1; + position: relative; + border-radius: 6px; + margin: 0 auto; + padding: 15px 20px; + width: 600px; + max-width: 95vw; +} + +.button { + height: 40px; + + span, + button { + padding: 0 20px; + } +} + +.title { + @include roboto-medium(); + + color: grey; + font-size: 16px; +} + +.blockRow { + width: 100%; +} + +.blockForm { + display: flex; + flex-direction: column; + gap: 30px; + align-items: center; + margin-top: 30px; + margin-bottom: 10px; +} + +.blockBtns { + display: flex; + gap: 30px; +} diff --git a/src/components/AssetsLibrary/ModalAttachmentOptions/index.js b/src/components/AssetsLibrary/ModalAttachmentOptions/index.js new file mode 100644 index 00000000..e879746a --- /dev/null +++ b/src/components/AssetsLibrary/ModalAttachmentOptions/index.js @@ -0,0 +1,284 @@ +/* Component to render attachment options modal */ + +import React, { useEffect, useState, useRef, useCallback } from 'react' +import _ from 'lodash' +import PropTypes from 'prop-types' +import styles from './styles.module.scss' +import { useForm, Controller } from 'react-hook-form' +import { yupResolver } from '@hookform/resolvers/yup' +import { toastr } from 'react-redux-toastr' +import { connect } from 'react-redux' +import cn from 'classnames' +import Modal from '../../Modal' +import FieldUserAutoComplete from '../../FieldUserAutoComplete' +import FieldLabelDynamic from '../../FieldLabelDynamic' +import PrimaryButton from '../../Buttons/PrimaryButton' +import OutlineButton from '../../Buttons/OutlineButton' +import FieldInput from '../../FieldInput' +import { assetsLibraryEditFileSchema } from '../../../util/validation' +import { + addProjectAttachmentApi, + updateProjectAttachmentApi +} from '../../../services/projects' +import { addAttachment, updateAttachment } from '../../../actions/projects' + +const ModalAttachmentOptions = ({ + classsName, + theme, + onCancel, + attachment, + members, + addAttachment, + updateAttachment, + projectId, + loggedInUser, + newAttachments +}) => { + const [isProcessing, setIsProcessing] = useState(false) + const shareType = useRef('') + + const { + control, + register, + handleSubmit, + reset, + watch, + formState: { errors, isValid, isDirty } + } = useForm({ + defaultValues: { + allowedUsers: [] + }, + resolver: attachment ? yupResolver(assetsLibraryEditFileSchema) : null, + mode: 'all' + }) + + const allowedUsers = watch('allowedUsers') + + useEffect(() => { + if (attachment) { + reset({ + title: attachment.title, + allowedUsers: attachment.allowedUsers || [] + }) + } + }, [attachment]) + + const onEditSubmit = useCallback( + data => { + setIsProcessing(true) + updateProjectAttachmentApi(projectId, attachment.id, data) + .then(result => { + toastr.success('Success', 'Updated file successfully.') + setIsProcessing(false) + updateAttachment(result) + onCancel() + }) + .catch(e => { + setIsProcessing(false) + const errorMessage = _.get( + e, + 'response.data.message', + 'Failed to update file.' + ) + toastr.error('Error', errorMessage) + }) + }, + [attachment, projectId] + ) + + const onNewSubmit = useCallback( + allowedUsers => { + let count = newAttachments.length + let errorMessage = '' + const checkToFinish = () => { + count = count - 1 + if (count === 0) { + setIsProcessing(false) + if (errorMessage) { + toastr.error('Error', errorMessage) + } else { + toastr.success('Success', 'Added file to the project successfully.') + onCancel() + } + } + } + setIsProcessing(true) + _.forEach(newAttachments, newAttachment => { + addProjectAttachmentApi(projectId, { + ...newAttachment, + allowedUsers + }) + .then(result => { + addAttachment(result) + checkToFinish() + }) + .catch(e => { + errorMessage = _.get( + e, + 'response.data.message', + 'Failed to add file.' + ) + checkToFinish() + }) + }) + }, + [newAttachments, projectId] + ) + + return ( + +
+
+ {attachment ? 'EDIT FILE' : 'ATTACHMENT OPTIONS'} +
+
+ {!!attachment && ( + + + + )} +
+ {!attachment && ( + <> + + Who do you want to share this file with? + +
+ { + shareType.current = 'all' + onNewSubmit(null) + }} + disabled={isProcessing} + /> +
+ + )} +
+ {!attachment && ( + + OR ONLY SPECIFIC PEOPLE + + )} + + ( + + )} + name='allowedUsers' + /> + + {!attachment && ( +
+ { + shareType.current = 'selected' + onNewSubmit(allowedUsers) + }} + disabled={ + isProcessing || !allowedUsers || !allowedUsers.length + } + /> +
+ )} +
+
+ + {!!attachment && ( +
+
+ +
+
+ +
+
+ )} +
+
+
+ ) +} + +ModalAttachmentOptions.defaultProps = { + members: [], + newAttachments: [], + projectId: '' +} + +ModalAttachmentOptions.propTypes = { + classsName: PropTypes.string, + theme: PropTypes.shape(), + onCancel: PropTypes.func, + attachment: PropTypes.shape(), + newAttachments: PropTypes.arrayOf(PropTypes.shape()), + members: PropTypes.arrayOf(PropTypes.shape()), + addAttachment: PropTypes.func.isRequired, + updateAttachment: PropTypes.func.isRequired, + projectId: PropTypes.string, + loggedInUser: PropTypes.object +} + +const mapStateToProps = () => { + return {} +} + +const mapDispatchToProps = { + addAttachment, + updateAttachment +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(ModalAttachmentOptions) diff --git a/src/components/AssetsLibrary/ModalAttachmentOptions/styles.module.scss b/src/components/AssetsLibrary/ModalAttachmentOptions/styles.module.scss new file mode 100644 index 00000000..cef8ec1e --- /dev/null +++ b/src/components/AssetsLibrary/ModalAttachmentOptions/styles.module.scss @@ -0,0 +1,97 @@ +@import '../../../styles/includes'; + +.container { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + box-sizing: border-box; + background: $white; + opacity: 1; + position: relative; + border-radius: 6px; + margin: 0 auto; + padding: 15px 20px; + width: 600px; + max-width: 95vw; +} + +.button { + height: 40px; + + span, + button { + padding: 0 20px; + } +} + +.title { + @include roboto-medium(); + + color: grey; + font-size: 16px; +} + +.blockRow { + width: 100%; +} + +.blockAddAttachment { + border-top: 1px solid $tc-gray-30; + display: flex; + flex-direction: column; + align-items: center; + margin-top: 20px; +} + +.textWhoYouWant { + @include roboto; + + width: 100%; + margin-top: 30px; + margin-bottom: 10px; + font-size: 16px; +} + +.blockForm { + display: flex; + flex-direction: column; + gap: 30px; + align-items: center; +} + +.blockFormEdit { + margin-top: 30px; + margin-bottom: 10px; +} + +.blockSelectMember { + width: calc(100% + 40px); + display: flex; + flex-direction: column; + align-items: center; + box-sizing: border-box; + padding: 10px; + background: $tc-gray-20; + border-top: 1px solid $tc-gray-30; + margin: 10px -20px -15px -20px; + border-radius: 0 0 10px 10px; +} + +.textOrOnly { + @include roboto-medium(); + + font-size: 11px; + color: grey; + text-transform: uppercase; +} + +.btnShareWith { + margin-top: 10px; + margin-bottom: 30px; +} + +.blockBtns { + display: flex; + gap: 30px; +} diff --git a/src/components/AssetsLibrary/ProjectMember/index.js b/src/components/AssetsLibrary/ProjectMember/index.js new file mode 100644 index 00000000..f129b17f --- /dev/null +++ b/src/components/AssetsLibrary/ProjectMember/index.js @@ -0,0 +1,33 @@ +/* Component to render project member */ + +import React, { useMemo } from 'react' +import PropTypes from 'prop-types' +import styles from './styles.module.scss' +import cn from 'classnames' +import { PROFILE_URL } from '../../../config/constants' +import { getFullNameWithFallback } from '../../../util/tc' + +const ProjectMember = ({ classsName, memberInfo }) => { + const fullName = useMemo(() => getFullNameWithFallback(memberInfo), [ + memberInfo + ]) + return ( + + {fullName} + + ) +} + +ProjectMember.defaultProps = {} + +ProjectMember.propTypes = { + classsName: PropTypes.string, + memberInfo: PropTypes.shape() +} + +export default ProjectMember diff --git a/src/components/AssetsLibrary/ProjectMember/styles.module.scss b/src/components/AssetsLibrary/ProjectMember/styles.module.scss new file mode 100644 index 00000000..4ac6ceeb --- /dev/null +++ b/src/components/AssetsLibrary/ProjectMember/styles.module.scss @@ -0,0 +1,5 @@ +@import '../../../styles/includes'; + +.container { + display: flex; +} \ No newline at end of file diff --git a/src/components/AssetsLibrary/ProjectMembers/index.js b/src/components/AssetsLibrary/ProjectMembers/index.js new file mode 100644 index 00000000..ad9eeb6c --- /dev/null +++ b/src/components/AssetsLibrary/ProjectMembers/index.js @@ -0,0 +1,60 @@ +/* Component to render list of project member */ + +import React, { useMemo, useState } from 'react' +import PropTypes from 'prop-types' +import _ from 'lodash' +import styles from './styles.module.scss' +import ProjectMember from '../ProjectMember' +import cn from 'classnames' + +const ProjectMembers = ({ classsName, members, allowedUsers, maxShownNum }) => { + const [showAll, setShowAll] = useState(false) + const allowedUserInfos = useMemo(() => { + const results = _.uniqBy( + _.compact(allowedUsers.map(userId => _.find(members, { userId }))), + 'userId' + ) + let extra = 0 + const maxUsers = [...results] + if (maxUsers.length > maxShownNum) { + extra = results.length - maxShownNum + maxUsers.length = maxShownNum + } + + return { + all: results, + maxUsers, + extra + } + }, [members, allowedUsers, maxShownNum]) + + return ( +
+ {(showAll ? allowedUserInfos.all : allowedUserInfos.maxUsers).map( + item => ( + + ) + )} + {!showAll && allowedUserInfos.extra !== 0 && ( + + )} +
+ ) +} + +ProjectMembers.defaultProps = { + maxShownNum: 3, + allowedUsers: [], + members: [] +} + +ProjectMembers.propTypes = { + classsName: PropTypes.string, + maxShownNum: PropTypes.number, + allowedUsers: PropTypes.arrayOf(PropTypes.number), + members: PropTypes.arrayOf(PropTypes.shape()) +} + +export default ProjectMembers diff --git a/src/components/AssetsLibrary/ProjectMembers/styles.module.scss b/src/components/AssetsLibrary/ProjectMembers/styles.module.scss new file mode 100644 index 00000000..16cefcce --- /dev/null +++ b/src/components/AssetsLibrary/ProjectMembers/styles.module.scss @@ -0,0 +1,17 @@ +@import '../../../styles/includes'; + +.container { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.btn { + border: none; + background-color: transparent; + margin: 0; + outline: none !important; + border: 1px solid $tc-gray-30; + border-radius: 6px; + padding: 0 3px; +} \ No newline at end of file diff --git a/src/components/AssetsLibrary/TabCommon/index.js b/src/components/AssetsLibrary/TabCommon/index.js new file mode 100644 index 00000000..2b319c04 --- /dev/null +++ b/src/components/AssetsLibrary/TabCommon/index.js @@ -0,0 +1,49 @@ +/* Component to render tab ui */ + +import React from 'react' +import PropTypes from 'prop-types' +import styles from './styles.module.scss' +import cn from 'classnames' + +const TabCommon = ({ items, classsName, selectedIndex, onSelect }) => { + return ( +
+ {items.map((item, index) => ( + + ))} +
+ ) +} + +TabCommon.defaultProps = { + items: [], + classsName: '', + onSelect: () => {} +} + +TabCommon.propTypes = { + items: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.string, + count: PropTypes.number + }) + ), + classsName: PropTypes.string, + selectedIndex: PropTypes.number, + onSelect: PropTypes.func +} + +export default TabCommon diff --git a/src/components/AssetsLibrary/TabCommon/styles.module.scss b/src/components/AssetsLibrary/TabCommon/styles.module.scss new file mode 100644 index 00000000..191cee55 --- /dev/null +++ b/src/components/AssetsLibrary/TabCommon/styles.module.scss @@ -0,0 +1,50 @@ +@import '../../../styles/includes'; + +.container { + display: flex; + gap: 20px; +} + +.blockTab { + padding: 0 15px; + margin: 0; + border: none; + background: transparent; + display: flex; + border-radius: 20px; + height: 30px; + align-items: flex-end; + gap: 5px; + outline: none !important; + + &.selected { + background-color: $tc-gray-30; + + * { + font-weight: 700; + } + .textCount { + background-color: $tc-gray-80; + } + } +} + +.textLabel { + @include roboto; + + font-size: 16px; + line-height: 30px; + color: $tc-gray-90; +} + +.textCount { + @include roboto; + + background-color: grey; + border-radius: 9px; + color: #fff; + font-size: 10px; + line-height: 12px; + padding: 0 4px; + margin-bottom: 7px; +} diff --git a/src/components/AssetsLibrary/TableAssets/index.js b/src/components/AssetsLibrary/TableAssets/index.js new file mode 100644 index 00000000..e5dc40c4 --- /dev/null +++ b/src/components/AssetsLibrary/TableAssets/index.js @@ -0,0 +1,158 @@ +/* Component to render assets table */ + +import React, { useMemo } from 'react' +import PropTypes from 'prop-types' +import _ from 'lodash' +import moment from 'moment' +import styles from './styles.module.scss' +import Table from '../../Table' +import IconThreeDot from '../../Icons/IconThreeDot' +import DropdownMenu from '../../DropdownMenu' +import DownloadFile from '../DownloadFile' +import IconFile from '../../Icons/IconFile' +import cn from 'classnames' +import { + PROJECT_ASSETS_SHARED_WITH_ALL_MEMBERS, + PROJECT_ASSETS_SHARED_WITH_ADMIN +} from '../../../config/constants' +import ProjectMembers from '../ProjectMembers' +import ProjectMember from '../ProjectMember' + +const TableAssets = ({ + classsName, + title, + onEdit, + onRemove, + datas, + isLink, + projectId, + members, + loggedInUser, + isAdmin +}) => { + const displayAssets = useMemo( + () => + datas.map(item => { + const titles = item.title.split('.') + const owner = _.find(members, { userId: item.createdBy }) + const canEdit = + `${item.createdBy}` === `${loggedInUser.userId}` || isAdmin + return { + ...item, + fileType: titles[titles.length - 1], + owner, + updatedAtString: item.updatedAt + ? moment(item.updatedAt).format('MM/DD/YYYY h:mm A') + : '—', + canEdit + } + }), + [datas, members, loggedInUser, isAdmin] + ) + return ( +
+ {title} + + ( + + + + + + {isLink ? ( + + {item.title} + + ) : ( + + )} + + + {!item.allowedUsers && PROJECT_ASSETS_SHARED_WITH_ALL_MEMBERS} + {item.allowedUsers && + item.allowedUsers === 0 && + PROJECT_ASSETS_SHARED_WITH_ADMIN} + {item.allowedUsers && item.allowedUsers !== 0 && ( + + )} + + + {!item.owner && !item.createdBy && '—'} + {!item.owner && item.createdBy !== 'CoderBot' && 'Unknown'} + {!item.owner && item.createdBy === 'CoderBot' && 'CoderBot'} + {!!item.owner && } + + + {item.updatedAtString} + + + {item.canEdit && ( + { + if (menu === 'Edit') { + onEdit(item) + } else if (menu === 'Remove') { + onRemove(item) + } + }} + options={['Edit', 'Remove']} + > + + + )} + + + ))} + /> + + ) +} + +TableAssets.defaultProps = { + title: '', + onEdit: () => {}, + onRemove: () => {}, + datas: [], + isLink: false, + isAdmin: false, + members: [] +} + +TableAssets.propTypes = { + classsName: PropTypes.string, + projectId: PropTypes.string, + title: PropTypes.string, + isAdmin: PropTypes.bool, + onEdit: PropTypes.func, + onRemove: PropTypes.func, + datas: PropTypes.arrayOf(PropTypes.shape()), + members: PropTypes.arrayOf(PropTypes.shape()), + isLink: PropTypes.bool, + loggedInUser: PropTypes.object +} + +export default TableAssets diff --git a/src/components/AssetsLibrary/TableAssets/styles.module.scss b/src/components/AssetsLibrary/TableAssets/styles.module.scss new file mode 100644 index 00000000..f719e5c7 --- /dev/null +++ b/src/components/AssetsLibrary/TableAssets/styles.module.scss @@ -0,0 +1,34 @@ +@import '../../../styles/includes'; + +.container { + display: flex; + flex-direction: column; + gap: 16px; +} + +.textTitle { + color: $tc-gray-90; + @include roboto-bold(); + + font-size: 16px; + line-height: 20px; +} + +.colMenu { + width: 50px; +} + +.blockItem { + @include roboto; + + font-size: 14px; + color: $tc-gray-90; + + a { + color: $tc-gray-90; + + &:hover { + text-decoration: none; + } + } +} diff --git a/src/components/AssetsLibrary/index.js b/src/components/AssetsLibrary/index.js new file mode 100644 index 00000000..e69de29b diff --git a/src/components/ChallengeEditor/Submissions/Submissions.module.scss b/src/components/ChallengeEditor/Submissions/Submissions.module.scss index 59149d21..b741a9ba 100644 --- a/src/components/ChallengeEditor/Submissions/Submissions.module.scss +++ b/src/components/ChallengeEditor/Submissions/Submissions.module.scss @@ -3,7 +3,6 @@ $tc-black : #151516; $tc-gray-10: #d5d5d5; $tc-gray-50: #808080; -$tc-gray-90: #2a2a2b; $tc-light-blue: #15acec; $tc-dark-blue: #0681ff; $tc-dark-blue-110: #006ad7; diff --git a/src/components/ChallengesComponent/index.js b/src/components/ChallengesComponent/index.js index 46166fc2..a0ffab02 100644 --- a/src/components/ChallengesComponent/index.js +++ b/src/components/ChallengesComponent/index.js @@ -50,6 +50,7 @@ const ChallengesComponent = ({ }) => { const [loginUserRoleInProject, setLoginUserRoleInProject] = useState('') const isReadOnly = checkReadOnlyRoles(auth.token) || loginUserRoleInProject === PROJECT_ROLES.READ + const isAdminOrCopilot = checkAdminOrCopilot(auth.token) useEffect(() => { const loggedInUser = auth.user @@ -69,7 +70,7 @@ const ChallengesComponent = ({ {activeProject ? activeProject.name : ''} {activeProject && activeProject.status && } - {activeProject && activeProject.id && checkAdminOrCopilot(auth.token) && ( + {activeProject && activeProject.id && isAdminOrCopilot && ( ( {activeProject && activeProject.id && !isReadOnly ? (
+ {isAdminOrCopilot && ( + + )} {checkAdmin(auth.token) && ( { + const menu = ( +
+ {_.map(options, r => { + return ( +
{ + onSelectMenu(r) + }} + > + {r} +
+ ) + })} +
+ ) + + return ( + + + + ) +} + +DropdownMenu.defaultProps = { + onSelectMenu: () => {}, + options: [] +} + +DropdownMenu.propTypes = { + onSelectMenu: PropTypes.func.isRequired, + children: PropTypes.node, + options: PropTypes.arrayOf(PropTypes.string) +} + +export default DropdownMenu diff --git a/src/components/DropdownMenu/styles.module.scss b/src/components/DropdownMenu/styles.module.scss new file mode 100644 index 00000000..a504c35b --- /dev/null +++ b/src/components/DropdownMenu/styles.module.scss @@ -0,0 +1,37 @@ +@import "../../styles/includes"; + +.menus { + outline: none; + position: relative; + list-style-type: none; + padding: 10px 0; + margin: 2px 0 0 0; + text-align: left; + background-color: #fff; + border-radius: 5px; + box-shadow: 0 0 10px rgba(21, 21, 22, .2); + border: none; + background-clip: padding-box; + cursor: pointer; + min-width: 150px; + min-height: 45px; + + .menu { + height: 30px; + line-height: 30px; + padding: 0 10px 0 20px; + font-size: 13px; + + &:hover { + background-color: rgba($tc-dark-blue-10, .5); + } + } +} + +.btn { + border: none; + background-color: transparent; + outline: none !important; + padding: 0; + margin: 0; +} \ No newline at end of file diff --git a/src/components/FieldInput/index.js b/src/components/FieldInput/index.js index 926778f1..a75cb1ba 100644 --- a/src/components/FieldInput/index.js +++ b/src/components/FieldInput/index.js @@ -1,3 +1,5 @@ +/* Component to render input field */ + import React from 'react' import PropTypes from 'prop-types' import styles from './styles.module.scss' @@ -6,7 +8,8 @@ const FieldInput = ({ onChangeValue, placeholder, value, - type + type, + inputControl }) => { return ( ) } FieldInput.defaultProps = { type: 'text', - onChangeValue: () => {} + onChangeValue: () => {}, + inputControl: {} } FieldInput.propTypes = { onChangeValue: PropTypes.func, placeholder: PropTypes.string, value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - type: PropTypes.string + type: PropTypes.string, + inputControl: PropTypes.any } export default FieldInput diff --git a/src/components/FieldLabelDynamic/index.js b/src/components/FieldLabelDynamic/index.js index 7e40b76b..03c0eb36 100644 --- a/src/components/FieldLabelDynamic/index.js +++ b/src/components/FieldLabelDynamic/index.js @@ -1,3 +1,5 @@ +/* Component to render field label */ + import React from 'react' import PropTypes from 'prop-types' import styles from './styles.module.scss' @@ -23,11 +25,13 @@ const FieldLabelDynamic = ({ className )} > -
- -
+ {!!title && ( +
+ +
+ )}
{info &&
{info}
} diff --git a/src/components/FieldUserAutoComplete/index.js b/src/components/FieldUserAutoComplete/index.js new file mode 100644 index 00000000..44fbcff0 --- /dev/null +++ b/src/components/FieldUserAutoComplete/index.js @@ -0,0 +1,61 @@ +/* Component to render select user field */ + +import React, { useMemo } from 'react' +import _ from 'lodash' +import PropTypes from 'prop-types' +import Select from '../Select' + +const FieldUserAutoComplete = ({ + value, + onChangeValue, + id, + projectMembers, + loggedInUser +}) => { + const selectedUsers = useMemo(() => { + return value.map(item => { + const selectedUser = _.find(projectMembers, { userId: item }) + return { + label: selectedUser.handle, + value: selectedUser.userId + } + }) + }, [value, projectMembers]) + + return ( +
) + const headers = options.map(o => ) // If the table is expandable it uses multiple tbodys return (
{o.name}{o.name}
@@ -16,7 +16,7 @@ const Table = (props) => { {headers} {props.expandable && rows} - {!props.expandable && ({rows})} + {!props.expandable && {rows}}
) } @@ -32,9 +32,11 @@ Table.propTypes = { className: PropTypes.string } -Table.Row = (props) => { +Table.Row = props => { return ( - {props.children} + + {props.children} + ) } @@ -44,10 +46,8 @@ Table.Row.propTypes = { onClick: PropTypes.func } -Table.Col = (props) => { - return ( - {props.children} - ) +Table.Col = props => { + return {props.children} } Table.Col.propTypes = { @@ -55,16 +55,22 @@ Table.Col.propTypes = { width: PropTypes.number } -Table.ExpandableRow = (props) => { +Table.ExpandableRow = props => { return ( - <> - - - - - {props.expandRows} - - + <> + + + + + {props.expandRows} + + ) } diff --git a/src/config/constants.js b/src/config/constants.js index 2ee6e0ee..d0d5a283 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -30,7 +30,8 @@ export const { SKILLS_V5_API_URL, UPDATE_SKILLS_V5_API_URL, SALESFORCE_BILLING_ACCOUNT_LINK, - TYPEFORM_URL + TYPEFORM_URL, + PROFILE_URL } = process.env export const CREATE_FORUM_TYPE_IDS = typeof process.env.CREATE_FORUM_TYPE_IDS === 'string' ? process.env.CREATE_FORUM_TYPE_IDS.split(',') : process.env.CREATE_FORUM_TYPE_IDS @@ -41,9 +42,14 @@ export const CREATE_FORUM_TYPE_IDS = typeof process.env.CREATE_FORUM_TYPE_IDS == // but if we want to test file uploading we should provide the real value in `FILE_PICKER_API_KEY` env variable export const FILE_PICKER_API_KEY = process.env.FILE_PICKER_API_KEY || 'DUMMY' export const FILE_PICKER_CONTAINER_NAME = process.env.FILE_PICKER_CONTAINER_NAME || 'tc-challenge-v5-dev' +export const FILE_PICKER_SUBMISSION_CONTAINER_NAME = process.env.FILE_PICKER_SUBMISSION_CONTAINER_NAME || 'submission-staging-dev' +export const PROJECT_ATTACHMENTS_FOLDER = process.env.PROJECT_ATTACHMENTS_FOLDER || 'PROJECT_ATTACHMENTS' export const FILE_PICKER_REGION = process.env.FILE_PICKER_REGION || 'us-east-1' +export const FILE_PICKER_LOCATION = process.env.FILE_PICKER_LOCATION || 's3' export const FILE_PICKER_CNAME = process.env.FILE_PICKER_CNAME || 'fs.topcoder.com' export const FILE_PICKER_FROM_SOURCES = ['local_file_system', 'googledrive', 'dropbox'] +export const ASSETS_FILE_PICKER_FROM_SOURCES = ['local_file_system'] +export const ASSETS_FILE_PICKER_MAX_FILES = 4 export const FILE_PICKER_ACCEPT = ['.bmp', '.gif', '.jpg', '.tex', '.xls', '.xlsx', '.doc', '.docx', '.zip', '.txt', '.pdf', '.png', '.ppt', '.pptx', '.rtf', '.csv'] export const FILE_PICKER_MAX_FILES = 10 export const FILE_PICKER_MAX_SIZE = 500 * 1024 * 1024 // 500Mb @@ -178,6 +184,10 @@ export const CREATE_PROJECT_PENDING = 'CREATE_PROJECT_PENDING' export const CREATE_PROJECT_SUCCESS = 'CREATE_PROJECT_SUCCESS' export const CREATE_PROJECT_FAILURE = 'CREATE_PROJECT_FAILURE' +export const ADD_PROJECT_ATTACHMENT_SUCCESS = 'ADD_PROJECT_ATTACHMENT_SUCCESS' +export const UPDATE_PROJECT_ATTACHMENT_SUCCESS = 'UPDATE_PROJECT_ATTACHMENT_SUCCESS' +export const REMOVE_PROJECT_ATTACHMENT_SUCCESS = 'REMOVE_PROJECT_ATTACHMENT_SUCCESS' + export const UPDATE_PROJECT = 'UPDATE_PROJECT' export const UPDATE_PROJECT_PENDING = 'UPDATE_PROJECT_PENDING' export const UPDATE_PROJECT_SUCCESS = 'UPDATE_PROJECT_SUCCESS' @@ -418,3 +428,15 @@ export const JOB_WORKLOAD_OPTIONS = [ { value: 'fulltime', label: 'Full-Time' }, { value: 'fractional', label: 'Fractional' } ] + +/* +* Project Attachment types +*/ +export const ATTACHMENT_TYPE_FILE = 'file' +export const ATTACHMENT_TYPE_LINK = 'link' + +/** + * Project assets shared with type text + */ +export const PROJECT_ASSETS_SHARED_WITH_ALL_MEMBERS = 'All Project Members' +export const PROJECT_ASSETS_SHARED_WITH_ADMIN = 'Only Admins' diff --git a/src/containers/ProjectAssets/index.jsx b/src/containers/ProjectAssets/index.jsx new file mode 100644 index 00000000..05017ef7 --- /dev/null +++ b/src/containers/ProjectAssets/index.jsx @@ -0,0 +1,353 @@ +/* Component to render project assets page */ + +import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react' +import { connect } from 'react-redux' +import PropTypes from 'prop-types' +import _ from 'lodash' +import * as filepicker from 'filestack-js' +import { toastr } from 'react-redux-toastr' +import PrimaryButton from '../../components/Buttons/PrimaryButton' +import OutlineButton from '../../components/Buttons/OutlineButton' +import TabCommon from '../../components/AssetsLibrary/TabCommon' +import TableAssets from '../../components/AssetsLibrary/TableAssets' +import ModalAttachmentOptions from '../../components/AssetsLibrary/ModalAttachmentOptions' +import ModalAddLink from '../../components/AssetsLibrary/ModalAddLink' +import ConfirmationModal from '../../components/Modal/ConfirmationModal' +import { loadOnlyProjectInfo, removeAttachment } from '../../actions/projects' + +import styles from './styles.module.scss' +import Loader from '../../components/Loader' +import { + ASSETS_FILE_PICKER_FROM_SOURCES, + ASSETS_FILE_PICKER_MAX_FILES, + ATTACHMENT_TYPE_FILE, + ATTACHMENT_TYPE_LINK, + FILE_PICKER_ACCEPT, + FILE_PICKER_API_KEY, + FILE_PICKER_CNAME, + FILE_PICKER_LOCATION, + FILE_PICKER_REGION, + FILE_PICKER_SUBMISSION_CONTAINER_NAME, + PROJECT_ATTACHMENTS_FOLDER +} from '../../config/constants' +import { removeProjectAttachmentApi } from '../../services/projects' +import { checkAdmin } from '../../util/tc' + +const theme = { + container: styles.modalContainer +} + +const ProjectAssets = ({ + projectId, + projectDetail, + loadOnlyProjectInfo, + isLoading, + removeAttachment, + loggedInUser, + token +}) => { + const [isProcessing, setIsProcessing] = useState(false) + const [selectedTab, setSelectedTab] = useState(0) + const [showDeleteFile, setShowDeleteFile] = useState(null) + const [showDeleteLink, setShowDeleteLink] = useState(null) + const [showAttachmentOptions, setShowAttachmentOptions] = useState(false) + const [pendingUploadFiles, setPendingUploadFiles] = useState([]) + const uploadedFiles = useRef([]) + const [showAddLink, setShowAddLink] = useState(false) + const hasProjectAccess = useMemo( + () => (projectDetail ? `${projectDetail.id}` === projectId : false), + [projectDetail, projectId] + ) + const isAdmin = useMemo(() => checkAdmin(token), [token]) + const fileUploadClient = useMemo(() => { + return filepicker.init(FILE_PICKER_API_KEY, { + cname: FILE_PICKER_CNAME + }) + }, []) + + const openFileUpload = useCallback(() => { + if (fileUploadClient && projectId) { + const attachmentsStorePath = `${PROJECT_ATTACHMENTS_FOLDER}/${projectId}/` + const picker = fileUploadClient.picker({ + storeTo: { + location: FILE_PICKER_LOCATION, + path: attachmentsStorePath, + container: FILE_PICKER_SUBMISSION_CONTAINER_NAME, + region: FILE_PICKER_REGION + }, + maxFiles: ASSETS_FILE_PICKER_MAX_FILES, + fromSources: ASSETS_FILE_PICKER_FROM_SOURCES, + accept: FILE_PICKER_ACCEPT, + uploadInBackground: false, + onFileUploadFinished: files => { + const attachments = [] + const fpFiles = _.isArray(files) ? files : [files] + _.forEach(fpFiles, f => { + const attachment = { + title: f.filename, + description: '', + size: f.size, + path: f.key, + type: ATTACHMENT_TYPE_FILE, + contentType: f.mimetype || 'application/unknown' + } + attachments.push(attachment) + }) + uploadedFiles.current = [...uploadedFiles.current, ...attachments] + }, + onOpen: () => { + uploadedFiles.current = [] + }, + onClose: () => { + if (uploadedFiles.current.length) { + setPendingUploadFiles([...uploadedFiles.current]) + setShowAttachmentOptions(true) + } + } + }) + + picker.open() + } + }, [fileUploadClient, projectId]) + + const files = useMemo(() => { + if (!hasProjectAccess) { + return [] + } + + let results = _.filter( + projectDetail.attachments, + a => a.type === ATTACHMENT_TYPE_FILE + ) + results = _.sortBy(results, file => -new Date(file.updatedAt).getTime()) + return results + }, [projectDetail, hasProjectAccess]) + + const links = useMemo(() => { + if (!hasProjectAccess) { + return [] + } + + let results = _.filter( + projectDetail.attachments, + a => a.type === ATTACHMENT_TYPE_LINK + ) + results = _.sortBy(results, file => -new Date(file.updatedAt).getTime()) + return results + }, [projectDetail, hasProjectAccess]) + + const { tableTitle, tableDatas, isLink } = useMemo(() => { + if (selectedTab === 0) { + return { + tableTitle: 'All Files', + tableDatas: files, + isLink: false + } + } else if (selectedTab === 1) { + return { + tableTitle: 'All Links', + tableDatas: links, + isLink: true + } + } + return { + tableTitle: '', + tableDatas: [], + isLink: false + } + }, [files, links, selectedTab]) + + useEffect(() => { + if (projectId) { + loadOnlyProjectInfo(projectId) + } + }, [projectId]) + + if (isLoading) { + return + } + + return ( +
+
+
Assets Library
+
+ {hasProjectAccess && ( +
+ { + if (selectedTab === 0) { + openFileUpload() + } else if (selectedTab === 1) { + setShowAddLink(true) + } + }} + /> +
+ )} +
+ +
+
+
+ {hasProjectAccess && ( + <> + + { + if (selectedTab === 0) { + setShowAttachmentOptions(item) + } else if (selectedTab === 1) { + setShowAddLink(item) + } + }} + onRemove={item => { + if (selectedTab === 0) { + setShowDeleteFile(item) + } else if (selectedTab === 1) { + setShowDeleteLink(item) + } + }} + datas={tableDatas} + isLink={isLink} + members={projectDetail.members} + loggedInUser={loggedInUser} + isAdmin={isAdmin} + /> + + )} + + {showAttachmentOptions && ( + setShowAttachmentOptions(false)} + attachment={ + showAttachmentOptions === true ? null : showAttachmentOptions + } + members={projectDetail.members} + projectId={projectId} + loggedInUser={loggedInUser} + newAttachments={pendingUploadFiles} + /> + )} + {showAddLink && ( + setShowAddLink(false)} + projectId={projectId} + link={showAddLink === true ? null : showAddLink} + /> + )} + {showDeleteFile && ( + setShowDeleteFile(null)} + theme={theme} + confirmText='Delete file' + confirmType='danger' + cancelType='info' + isProcessing={isProcessing} + onConfirm={() => { + setIsProcessing(true) + removeProjectAttachmentApi(projectId, showDeleteFile.id) + .then(() => { + toastr.success('Success', 'Removed file successfully.') + removeAttachment(showDeleteFile.id) + setIsProcessing(false) + setShowDeleteFile(null) + }) + .catch(e => { + setIsProcessing(false) + const errorMessage = _.get( + e, + 'response.data.message', + 'Failed to remove file.' + ) + toastr.error('Error', errorMessage) + }) + }} + /> + )} + {showDeleteLink && ( + setShowDeleteLink(null)} + onConfirm={() => { + setIsProcessing(true) + removeProjectAttachmentApi(projectId, showDeleteLink.id) + .then(() => { + toastr.success('Success', 'Removed link successfully.') + removeAttachment(showDeleteLink.id) + setIsProcessing(false) + setShowDeleteLink(null) + }) + .catch(e => { + setIsProcessing(false) + const errorMessage = _.get( + e, + 'response.data.message', + 'Failed to remove link.' + ) + toastr.error('Error', errorMessage) + }) + }} + theme={theme} + confirmText='Delete link' + confirmType='danger' + cancelType='info' + isProcessing={isProcessing} + /> + )} +
+ ) +} + +ProjectAssets.propTypes = { + projectId: PropTypes.string.isRequired, + token: PropTypes.string, + loadOnlyProjectInfo: PropTypes.func.isRequired, + removeAttachment: PropTypes.func.isRequired, + projectDetail: PropTypes.object, + isLoading: PropTypes.bool, + loggedInUser: PropTypes.object +} + +const mapStateToProps = ({ projects, auth }) => { + return { + projectDetail: projects.projectDetail, + isLoading: projects.isLoading, + loggedInUser: auth.user, + token: auth.token + } +} + +const mapDispatchToProps = { + loadOnlyProjectInfo, + removeAttachment +} + +export default connect(mapStateToProps, mapDispatchToProps)(ProjectAssets) diff --git a/src/containers/ProjectAssets/styles.module.scss b/src/containers/ProjectAssets/styles.module.scss new file mode 100644 index 00000000..6acf18b7 --- /dev/null +++ b/src/containers/ProjectAssets/styles.module.scss @@ -0,0 +1,65 @@ +@import '../../styles/includes'; + +.container { + padding: 30px 20px; +} + +.btn { + height: 40px; + + span, + button { + padding: 0 20px; + white-space: nowrap; + } +} + +.blockHeader { + display: flex; + justify-content: space-between; + align-items: center; +} + +.title { + font-size: 24px; + font-weight: 700; + line-height: 29px; + color: $challenges-title; +} + +.modalContainer { + padding: 0; + position: fixed; + overflow: auto; + z-index: 10000; + top: 0; + right: 0; + bottom: 0; + left: 0; + box-sizing: border-box; + width: auto; + max-width: none; + transform: none; + background: transparent; + color: $text-color; + opacity: 1; + display: flex; + justify-content: center; + align-items: center; + + @include xs-to-lg { + max-width: 100vw; + } +} + +.btns { + display: flex; + align-items: center; + gap: 20px; + flex-wrap: wrap; +} + +.blockTabs { + margin-top: 20px; + margin-bottom: 30px; +} diff --git a/src/containers/ProjectEditor/ProjectEditor.module.scss b/src/containers/ProjectEditor/ProjectEditor.module.scss index aa73035e..1a9587c2 100644 --- a/src/containers/ProjectEditor/ProjectEditor.module.scss +++ b/src/containers/ProjectEditor/ProjectEditor.module.scss @@ -37,6 +37,9 @@ .actionButtons { top: 30px; + display: flex; + gap: 20px; + a,button { height: 40px; } @@ -251,3 +254,8 @@ } } } + +.btnOutline { + padding: 0 40px; + white-space: nowrap; +} \ No newline at end of file diff --git a/src/reducers/projects.js b/src/reducers/projects.js index a6b94b9c..7621865b 100644 --- a/src/reducers/projects.js +++ b/src/reducers/projects.js @@ -23,7 +23,10 @@ import { UPDATE_PROJECT_SUCCESS, UPDATE_PROJECT_DETAILS_FAILURE, UPDATE_PROJECT_DETAILS_PENDING, - UPDATE_PROJECT_DETAILS_SUCCESS + UPDATE_PROJECT_DETAILS_SUCCESS, + ADD_PROJECT_ATTACHMENT_SUCCESS, + UPDATE_PROJECT_ATTACHMENT_SUCCESS, + REMOVE_PROJECT_ATTACHMENT_SUCCESS } from '../config/constants' import { toastSuccess, toastFailure } from '../util/toaster' import moment from 'moment-timezone' @@ -244,6 +247,34 @@ export default function (state = initialState, action) { ...state, isUpdatingProject: false } + case ADD_PROJECT_ATTACHMENT_SUCCESS: + return { + ...state, + projectDetail: { + ...state.projectDetail, + attachments: [...state.projectDetail.attachments, action.payload] + } + } + case UPDATE_PROJECT_ATTACHMENT_SUCCESS: + return { + ...state, + projectDetail: { + ...state.projectDetail, + attachments: state.projectDetail.attachments.map(item => + item.id !== action.payload.id ? item : action.payload + ) + } + } + case REMOVE_PROJECT_ATTACHMENT_SUCCESS: + return { + ...state, + projectDetail: { + ...state.projectDetail, + attachments: state.projectDetail.attachments.filter( + item => item.id !== action.payload + ) + } + } default: return state } diff --git a/src/routes.js b/src/routes.js index c26549d7..fee5af10 100644 --- a/src/routes.js +++ b/src/routes.js @@ -12,6 +12,7 @@ import FooterContainer from './containers/FooterContainer' import Tab from './containers/Tab' import Challenges from './containers/Challenges' import TaaSList from './containers/TaaSList' +import ProjectAssets from './containers/ProjectAssets' import TaaSProjectForm from './containers/TaaSProjectForm' import ChallengeEditor from './containers/ChallengeEditor' import { getFreshToken, decodeToken } from 'tc-auth-lib' @@ -216,6 +217,20 @@ class Routes extends React.Component { )()} /> + {(isCopilot || isAdmin) && ( + + renderApp( + , + , + , + + )() + } + /> + )} { !isReadOnly && ( } + */ +export async function getProjectAttachment (projectId, attachmentId) { + const response = await axiosInstance.get( + `${PROJECT_API_URL}/${projectId}/attachments/${attachmentId}` + ) + return _.get(response, 'data') +} + +/** + * Add attachment to project + * @param projectId project id + * @param data attachment data + * @returns {Promise<*>} + */ +export async function addProjectAttachmentApi (projectId, data) { + if (data.type === ATTACHMENT_TYPE_FILE) { + // add s3 bucket prop + data.s3Bucket = FILE_PICKER_SUBMISSION_CONTAINER_NAME + } + + // The api takes only arrays + if (!data.tags) { + data.tags = [] + } + + const response = await axiosInstance.post( + `${PROJECT_API_URL}/${projectId}/attachments`, + data + ) + return _.get(response, 'data') +} + +/** + * Update project attachment + * @param projectId project id + * @param attachmentId attachment id + * @param attachment attachment data + * @returns {Promise<*>} + */ +export async function updateProjectAttachmentApi ( + projectId, + attachmentId, + attachment +) { + let data = { + ...attachment + } + if (data && (!data.allowedUsers || data.allowedUsers.length === 0)) { + data.allowedUsers = null + } + + // The api takes only arrays + if (data && !data.tags) { + data.tags = [] + } + + const response = await axiosInstance.patch( + `${PROJECT_API_URL}/${projectId}/attachments/${attachmentId}`, + data + ) + return _.get(response, 'data') +} + +/** + * Remove project attachment + * @param projectId project id + * @param attachmentId attachment id + */ +export async function removeProjectAttachmentApi (projectId, attachmentId) { + await axiosInstance.delete( + `${PROJECT_API_URL}/${projectId}/attachments/${attachmentId}` + ) +} diff --git a/src/styles/_colors.scss b/src/styles/_colors.scss index b640d949..266755ae 100644 --- a/src/styles/_colors.scss +++ b/src/styles/_colors.scss @@ -39,6 +39,8 @@ $tc-blue-20: #2C95D7; $tc-blue-30: #2984BD; $tc-blue-40: #16679A; +$tc-dark-blue-10: #f4f9ff; + $tc-green-20: #43D7B0; $tc-green-30: #60C602; $tc-green-40: #35AC35; @@ -57,6 +59,7 @@ $tc-gray-50: #AAAAAA; $tc-gray-60: #979797; $tc-gray-70: #555555; $tc-gray-80: #2A2A2A; +$tc-gray-90: #2a2a2b; $tc-black: #000000; $tc-handle-yellow: #FDD615; diff --git a/src/util/tc.js b/src/util/tc.js index a52abddd..46ea1405 100644 --- a/src/util/tc.js +++ b/src/util/tc.js @@ -324,3 +324,23 @@ export function getChallengeTypeAbbr (track, challengeTypes) { export function is2RoundsChallenge (challenge) { return !!_.find(challenge.phases, { name: 'Checkpoint Submission' }) } + +/** + * Get full name of user + * @param {Object} user user info + * @returns + */ +export function getFullNameWithFallback (user) { + if (!user) return '' + let userFullName = user.firstName + if (userFullName && user.lastName) { + userFullName += ' ' + user.lastName + } + userFullName = + userFullName && userFullName.trim().length > 0 ? userFullName : user.handle + userFullName = + userFullName && userFullName.trim().length > 0 + ? userFullName + : 'Connect user' + return userFullName +} diff --git a/src/util/validation.js b/src/util/validation.js index c0398531..26a10b25 100644 --- a/src/util/validation.js +++ b/src/util/validation.js @@ -54,3 +54,30 @@ export const taaSProjectFormValidationSchema = Yup.object({ }) ) }) + +/** + * regex for url validation + */ +const urlRegex = /((https?):\/\/)?(www.)?[a-z0-9]+(\.[a-z]{2,}){1,3}(#?\/?[a-zA-Z0-9#]+)*\/?(\?[a-zA-Z0-9-_]+=[a-zA-Z0-9-%]+&?)?$/ + +/** + * validation schema for add link form in assets library + */ +export const assetsLibraryAddLinkSchema = Yup.object({ + title: Yup.string() + .trim() + .required('Name is required'), + path: Yup.string() + .trim() + .required('URL is required') + .matches(urlRegex, 'Please enter a valid URL') +}) + +/** + * validation schema for edit file form in assets library + */ +export const assetsLibraryEditFileSchema = Yup.object({ + title: Yup.string() + .trim() + .required('Title is required') +}) From 2cc9ba2be1be4d0053f8acaba9944eeeb0e4ee5d Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Wed, 12 Feb 2025 11:46:27 +0200 Subject: [PATCH 02/18] deploy branch --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index c266fd14..10ffea36 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -152,7 +152,7 @@ workflows: context: org-global filters: &filters-dev branches: - only: ["develop", "multiround", "release_0.20.9", "metadata-fix"] + only: ["develop", "PM-690_asset-library-management"] # Production builds are exectuted only on tagged commits to the # master branch. From 356f008cbc2d3e56cd3e24dd1156a0879d522eac Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Thu, 13 Feb 2025 10:18:40 +0200 Subject: [PATCH 03/18] Fix assets library upload first attachment --- src/containers/ProjectAssets/index.jsx | 2 +- src/reducers/projects.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/containers/ProjectAssets/index.jsx b/src/containers/ProjectAssets/index.jsx index 05017ef7..002ce66b 100644 --- a/src/containers/ProjectAssets/index.jsx +++ b/src/containers/ProjectAssets/index.jsx @@ -191,7 +191,7 @@ const ProjectAssets = ({
diff --git a/src/reducers/projects.js b/src/reducers/projects.js index 7621865b..a2e9a938 100644 --- a/src/reducers/projects.js +++ b/src/reducers/projects.js @@ -252,7 +252,7 @@ export default function (state = initialState, action) { ...state, projectDetail: { ...state.projectDetail, - attachments: [...state.projectDetail.attachments, action.payload] + attachments: [...(state.projectDetail.attachments || []), action.payload] } } case UPDATE_PROJECT_ATTACHMENT_SUCCESS: From 2c40ccdde9c891ee28fc84f7ec00abc2853f09ae Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 18 Feb 2025 09:52:45 +0200 Subject: [PATCH 04/18] PM-804 - restrict user access based on project role --- src/components/ChallengesComponent/index.js | 3 ++- src/containers/ProjectEditor/index.js | 2 +- src/util/tc.js | 19 +++++++++++++------ 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/components/ChallengesComponent/index.js b/src/components/ChallengesComponent/index.js index 46166fc2..9c9682c5 100644 --- a/src/components/ChallengesComponent/index.js +++ b/src/components/ChallengesComponent/index.js @@ -50,6 +50,7 @@ const ChallengesComponent = ({ }) => { const [loginUserRoleInProject, setLoginUserRoleInProject] = useState('') const isReadOnly = checkReadOnlyRoles(auth.token) || loginUserRoleInProject === PROJECT_ROLES.READ + const isAdminOrCopilot = checkAdminOrCopilot(auth.token, activeProject) useEffect(() => { const loggedInUser = auth.user @@ -69,7 +70,7 @@ const ChallengesComponent = ({ {activeProject ? activeProject.name : ''} {activeProject && activeProject.status && } - {activeProject && activeProject.id && checkAdminOrCopilot(auth.token) && ( + {activeProject && activeProject.id && isAdminOrCopilot && ( ( { * Checks if token has any of the copilot roles * @param token */ -export const checkCopilot = token => { - const roles = _.get(decodeToken(token), 'roles') - return roles.some(val => COPILOT_ROLES.indexOf(val.toLowerCase()) > -1) +export const checkCopilot = (token, project) => { + const tokenData = decodeToken(token) + const roles = _.get(tokenData, 'roles') + const isCopilot = roles.some(val => COPILOT_ROLES.indexOf(val.toLowerCase()) > -1) + const canManageProject = !project || _.isEmpty(project) || ALLOWED_EDIT_RESOURCE_ROLES.includes(_.get(_.find(project.members, { userId: tokenData.userId }), 'role')) + return isCopilot && canManageProject } /** * Checks if token has any of the admin or copilot roles * @param token */ -export const checkAdminOrCopilot = token => { - const roles = _.get(decodeToken(token), 'roles') +export const checkAdminOrCopilot = (token, project) => { + const tokenData = decodeToken(token) + const roles = _.get(tokenData, 'roles') const allowedRoles = [...ADMIN_ROLES, ...COPILOT_ROLES] - return roles.some(val => allowedRoles.indexOf(val.toLowerCase()) > -1) + const isAdminOrCopilot = roles.some(val => allowedRoles.indexOf(val.toLowerCase()) > -1) + const canManageProject = !project || _.isEmpty(project) || ALLOWED_EDIT_RESOURCE_ROLES.includes(_.get(_.find(project.members, { userId: tokenData.userId }), 'role')) + + return isAdminOrCopilot && canManageProject } /** From 4bb611fafa8fdb985d7728f1688a7e7cbc0cb115 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 18 Feb 2025 09:55:08 +0200 Subject: [PATCH 05/18] PM-805 - send status when editing project --- src/components/ProjectForm/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/ProjectForm/index.js b/src/components/ProjectForm/index.js index 6fcef40e..d66ecb82 100644 --- a/src/components/ProjectForm/index.js +++ b/src/components/ProjectForm/index.js @@ -50,6 +50,7 @@ const ProjectForm = ({ name: data.projectName, description: data.description, type: data.projectType.value, + status: data.status.value, groups: data.groups, terms: data.terms ? [data.terms] : [] } From 9d68f4fa30b883019475a75a7ad30783f4684e62 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 18 Feb 2025 10:53:26 +0200 Subject: [PATCH 06/18] PM-802 - load more projects on scroll --- src/actions/sidebar.js | 26 +++++++++--- src/components/InfiniteLoadTrigger/index.js | 45 +++++++++++++++++++++ src/config/constants.js | 2 + src/containers/Challenges/index.js | 7 ++++ 4 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 src/components/InfiniteLoadTrigger/index.js diff --git a/src/actions/sidebar.js b/src/actions/sidebar.js index 4a843747..28ad0154 100644 --- a/src/actions/sidebar.js +++ b/src/actions/sidebar.js @@ -8,7 +8,8 @@ import { LOAD_PROJECTS_PENDING, LOAD_PROJECTS_SUCCESS, RESET_SIDEBAR_ACTIVE_PARAMS, - UNLOAD_PROJECTS_SUCCESS + UNLOAD_PROJECTS_SUCCESS, + PROJECTS_PAGE_SIZE } from '../config/constants' import _ from 'lodash' @@ -29,7 +30,7 @@ export function setActiveProject (projectId) { * Loads projects of the authenticated user */ export function loadProjects (filterProjectName = '', myProjects = true, paramFilters = {}) { - return (dispatch) => { + return (dispatch, getState) => { dispatch({ type: LOAD_PROJECTS_PENDING }) @@ -37,6 +38,7 @@ export function loadProjects (filterProjectName = '', myProjects = true, paramFi const filters = { status: 'active', sort: 'lastActivityAt desc', + perPage: PROJECTS_PAGE_SIZE, ...paramFilters } if (!_.isEmpty(filterProjectName)) { @@ -47,21 +49,35 @@ export function loadProjects (filterProjectName = '', myProjects = true, paramFi } } - // filters['perPage'] = 20 - // filters['page'] = 1 if (myProjects) { filters['memberOnly'] = true } + const state = getState().sidebar fetchMemberProjects(filters).then(projects => dispatch({ type: LOAD_PROJECTS_SUCCESS, - projects + projects: _.uniqBy((state.projects || []).concat(projects), 'id') })).catch(() => dispatch({ type: LOAD_PROJECTS_FAILURE })) } } +/** + * Load more projects for the authenticated user + */ +export function loadMoreProjects (filterProjectName = '', myProjects = true, paramFilters = {}) { + return (dispatch, getState) => { + const state = getState().sidebar + const projects = state.projects || [] + + loadProjects(filterProjectName, myProjects, _.assignIn({}, paramFilters, { + perPage: PROJECTS_PAGE_SIZE, + page: Math.ceil(projects.length / PROJECTS_PAGE_SIZE) + 1 + }))(dispatch, getState) + } +} + /** * Unloads projects of the authenticated user */ diff --git a/src/components/InfiniteLoadTrigger/index.js b/src/components/InfiniteLoadTrigger/index.js new file mode 100644 index 00000000..7b848710 --- /dev/null +++ b/src/components/InfiniteLoadTrigger/index.js @@ -0,0 +1,45 @@ +import React, { useEffect, useRef, useCallback } from 'react' +import PropTypes from 'prop-types' + +const InfiniteScrollTrigger = ({ onLoadMore, rootMargin = '100px', threshold = 0.1 }) => { + const triggerRef = useRef(null) + + const observerCallback = useCallback( + (entries) => { + const [entry] = entries + if (entry.isIntersecting) { + onLoadMore() + } + }, + [onLoadMore] + ) + + useEffect(() => { + // eslint-disable-next-line no-undef + const observer = new IntersectionObserver(observerCallback, { + root: null, // Observe relative to viewport + rootMargin, + threshold + }) + + if (triggerRef.current) { + observer.observe(triggerRef.current) + } + + return () => { + if (triggerRef.current) { + observer.unobserve(triggerRef.current) + } + } + }, [observerCallback, rootMargin, threshold]) + + return
+} + +InfiniteScrollTrigger.propTypes = { + onLoadMore: PropTypes.func.isRequired, + rootMargin: PropTypes.string, + threshold: PropTypes.number +} + +export default InfiniteScrollTrigger diff --git a/src/config/constants.js b/src/config/constants.js index 2ee6e0ee..6b223759 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -303,6 +303,8 @@ export const downloadAttachmentURL = (challengeId, attachmentId, token) => export const PAGE_SIZE = 10 +export const PROJECTS_PAGE_SIZE = 20 + /** * The minimal number of characters to enter before starting showing autocomplete suggestions */ diff --git a/src/containers/Challenges/index.js b/src/containers/Challenges/index.js index 4b56402a..ebc2a156 100644 --- a/src/containers/Challenges/index.js +++ b/src/containers/Challenges/index.js @@ -18,6 +18,7 @@ import { } from '../../actions/challenges' import { loadProject, updateProject } from '../../actions/projects' import { + loadMoreProjects, loadProjects, setActiveProject, resetSidebarActiveParams @@ -25,6 +26,7 @@ import { import styles from './Challenges.module.scss' import { checkAdmin, checkAdminOrCopilot } from '../../util/tc' import { PrimaryButton } from '../../components/Buttons' +import InfiniteScrollTrigger from '../../components/InfiniteLoadTrigger' class Challenges extends Component { constructor (props) { @@ -173,6 +175,9 @@ class Challenges extends Component {
)}
    {projectComponents}
+ {projects && !!projects.length && ( + + )} ) : null} {(dashboard || activeProjectId !== -1 || selfService) && ( @@ -264,6 +269,7 @@ Challenges.propTypes = { dashboard: PropTypes.bool, auth: PropTypes.object.isRequired, loadChallengeTypes: PropTypes.func, + loadMoreProjects: PropTypes.func, metadata: PropTypes.shape({ challengeTypes: PropTypes.array }) @@ -291,6 +297,7 @@ const mapStateToProps = ({ challenges, sidebar, projects, auth }) => ({ const mapDispatchToProps = { loadChallengesByPage, resetSidebarActiveParams, + loadMoreProjects, loadProject, loadProjects, updateProject, From 973049f4bc30aa78a7e537c21a9486bb7d15387d Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 18 Feb 2025 10:57:37 +0200 Subject: [PATCH 07/18] PM-806 - remove "manage milestone" buttons --- src/components/ChallengeEditor/ChallengeView/index.js | 8 +------- src/components/ChallengeEditor/Milestone-Field/index.js | 6 ------ 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/src/components/ChallengeEditor/ChallengeView/index.js b/src/components/ChallengeEditor/ChallengeView/index.js index 8b658549..b0c3de96 100644 --- a/src/components/ChallengeEditor/ChallengeView/index.js +++ b/src/components/ChallengeEditor/ChallengeView/index.js @@ -20,7 +20,6 @@ import { getResourceRoleByName } from '../../../util/tc' import { loadGroupDetails } from '../../../actions/challenges' import { REVIEW_TYPES, - CONNECT_APP_URL, PHASE_PRODUCT_CHALLENGE_ID_FIELD, MULTI_ROUND_CHALLENGE_TEMPLATE_ID, DS_TRACK_ID @@ -118,12 +117,7 @@ const ChallengeView = ({ {selectedMilestone &&
- Milestone: {selectedMilestone ? ( - - {selectedMilestone.name} - - ) : ''} + Milestone: {selectedMilestone ? selectedMilestone.name : ''}
}
diff --git a/src/components/ChallengeEditor/Milestone-Field/index.js b/src/components/ChallengeEditor/Milestone-Field/index.js index c6e077d6..346981ba 100644 --- a/src/components/ChallengeEditor/Milestone-Field/index.js +++ b/src/components/ChallengeEditor/Milestone-Field/index.js @@ -4,8 +4,6 @@ import _ from 'lodash' import Select from '../../Select' import cn from 'classnames' import styles from './Milestone-Field.module.scss' -import { CONNECT_APP_URL } from '../../../config/constants' -import PrimaryButton from '../../Buttons/PrimaryButton' const MilestoneField = ({ milestones, onUpdateSelect, disabled, projectId, selectedMilestoneId }) => { const options = milestones.map(type => ({ label: type.name, value: type.id })) @@ -28,10 +26,6 @@ const MilestoneField = ({ milestones, onUpdateSelect, disabled, projectId, selec isDisabled={disabled} />
- - - ) From ab9bb074905181e89a11a5610e8021bfa0a765f3 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 18 Feb 2025 11:00:17 +0200 Subject: [PATCH 08/18] update regex --- src/util/validation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/validation.js b/src/util/validation.js index 26a10b25..7f7e7c70 100644 --- a/src/util/validation.js +++ b/src/util/validation.js @@ -58,7 +58,7 @@ export const taaSProjectFormValidationSchema = Yup.object({ /** * regex for url validation */ -const urlRegex = /((https?):\/\/)?(www.)?[a-z0-9]+(\.[a-z]{2,}){1,3}(#?\/?[a-zA-Z0-9#]+)*\/?(\?[a-zA-Z0-9-_]+=[a-zA-Z0-9-%]+&?)?$/ +const urlRegex = /((https?):\/\/)?(www.)?[a-z0-9]+(\.[a-z]{2,}){1,3}(#?\/?(?:[a-zA-Z0-9#]+))*\/?(\?[a-zA-Z0-9-_]+=[a-zA-Z0-9-%]+&?)?$/ /** * validation schema for add link form in assets library From 49c655e31c85ae50883b439ee962ea9a3a36ae7f Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 18 Feb 2025 11:05:49 +0200 Subject: [PATCH 09/18] deploy to dev --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 10ffea36..7ef89ba5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -152,7 +152,7 @@ workflows: context: org-global filters: &filters-dev branches: - only: ["develop", "PM-690_asset-library-management"] + only: ["develop", "PM-803_wm-regression-fixes"] # Production builds are exectuted only on tagged commits to the # master branch. From a745f7b3d655746833640403a05f86b1a3ad7d7a Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 18 Feb 2025 12:51:13 +0200 Subject: [PATCH 10/18] PM-813 - fix project creation --- src/components/ProjectForm/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ProjectForm/index.js b/src/components/ProjectForm/index.js index d66ecb82..8933928b 100644 --- a/src/components/ProjectForm/index.js +++ b/src/components/ProjectForm/index.js @@ -50,7 +50,7 @@ const ProjectForm = ({ name: data.projectName, description: data.description, type: data.projectType.value, - status: data.status.value, + status: (data.status || {}).value, groups: data.groups, terms: data.terms ? [data.terms] : [] } From 86e5ca315b19e99ba3f4f91b60043284818ce727 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Wed, 19 Feb 2025 11:48:17 +0200 Subject: [PATCH 11/18] PM-802 - show all projects --- src/actions/sidebar.js | 19 +++++++++++++------ src/actions/users.js | 2 +- .../InfiniteLoadTrigger.module.scss | 11 +++++++++++ src/components/InfiniteLoadTrigger/index.js | 15 +++++++++++---- .../ProjectCard/ProjectCard.module.scss | 10 +++++++--- src/components/ProjectCard/index.js | 11 +++++++++-- src/components/ProjectForm/index.js | 5 +++-- src/config/constants.js | 5 +++++ src/containers/Challenges/index.js | 5 +++-- src/containers/Tab/index.js | 5 ++++- src/reducers/sidebar.js | 6 +++++- src/services/projects.js | 16 +++++++++++++--- src/util/pagination.js | 6 ++++++ src/util/tc.js | 12 +++++++++--- 14 files changed, 100 insertions(+), 28 deletions(-) create mode 100644 src/components/InfiniteLoadTrigger/InfiniteLoadTrigger.module.scss create mode 100644 src/util/pagination.js diff --git a/src/actions/sidebar.js b/src/actions/sidebar.js index 28ad0154..9005f5cc 100644 --- a/src/actions/sidebar.js +++ b/src/actions/sidebar.js @@ -36,7 +36,6 @@ export function loadProjects (filterProjectName = '', myProjects = true, paramFi }) const filters = { - status: 'active', sort: 'lastActivityAt desc', perPage: PROJECTS_PAGE_SIZE, ...paramFilters @@ -54,10 +53,13 @@ export function loadProjects (filterProjectName = '', myProjects = true, paramFi } const state = getState().sidebar - fetchMemberProjects(filters).then(projects => dispatch({ + // eslint-disable-next-line no-sequences + fetchMemberProjects(filters).then(({ projects, pagination }) => (console.log('here', pagination), dispatch({ type: LOAD_PROJECTS_SUCCESS, - projects: _.uniqBy((state.projects || []).concat(projects), 'id') - })).catch(() => dispatch({ + projects: _.uniqBy((state.projects || []).concat(projects), 'id'), + total: pagination.xTotal, + page: pagination.xPage + }))).catch(() => dispatch({ type: LOAD_PROJECTS_FAILURE })) } @@ -69,15 +71,20 @@ export function loadProjects (filterProjectName = '', myProjects = true, paramFi export function loadMoreProjects (filterProjectName = '', myProjects = true, paramFilters = {}) { return (dispatch, getState) => { const state = getState().sidebar - const projects = state.projects || [] loadProjects(filterProjectName, myProjects, _.assignIn({}, paramFilters, { perPage: PROJECTS_PAGE_SIZE, - page: Math.ceil(projects.length / PROJECTS_PAGE_SIZE) + 1 + page: state.page + 1 }))(dispatch, getState) } } +export function loadTaasProjects (filterProjectName = '', myProjects = true, paramFilters = {}) { + return loadProjects(filterProjectName, myProjects, Object.assign({ + type: 'talent-as-a-service' + }, paramFilters)) +} + /** * Unloads projects of the authenticated user */ diff --git a/src/actions/users.js b/src/actions/users.js index 92e54cc1..e02ef1ee 100644 --- a/src/actions/users.js +++ b/src/actions/users.js @@ -66,7 +66,7 @@ export function searchUserProjects (isAdmin = true, keyword) { filters['memberOnly'] = true } - fetchMemberProjects(filters).then(projects => dispatch({ + fetchMemberProjects(filters).then(({ projects }) => dispatch({ type: SEARCH_USER_PROJECTS_SUCCESS, projects })).catch(() => dispatch({ diff --git a/src/components/InfiniteLoadTrigger/InfiniteLoadTrigger.module.scss b/src/components/InfiniteLoadTrigger/InfiniteLoadTrigger.module.scss new file mode 100644 index 00000000..5bd73022 --- /dev/null +++ b/src/components/InfiniteLoadTrigger/InfiniteLoadTrigger.module.scss @@ -0,0 +1,11 @@ +.loader { + width: 100%; + display: flex; + justify-content: center; + margin-bottom: 20px; + + > * { + width: auto; + min-width: 130px; + } +} \ No newline at end of file diff --git a/src/components/InfiniteLoadTrigger/index.js b/src/components/InfiniteLoadTrigger/index.js index 7b848710..faf17852 100644 --- a/src/components/InfiniteLoadTrigger/index.js +++ b/src/components/InfiniteLoadTrigger/index.js @@ -1,7 +1,10 @@ import React, { useEffect, useRef, useCallback } from 'react' import PropTypes from 'prop-types' -const InfiniteScrollTrigger = ({ onLoadMore, rootMargin = '100px', threshold = 0.1 }) => { +import styles from './InfiniteLoadTrigger.module.scss' +import { OutlineButton } from '../Buttons' + +const InfiniteLoadTrigger = ({ onLoadMore, rootMargin = '100px', threshold = 0.1 }) => { const triggerRef = useRef(null) const observerCallback = useCallback( @@ -33,13 +36,17 @@ const InfiniteScrollTrigger = ({ onLoadMore, rootMargin = '100px', threshold = 0 } }, [observerCallback, rootMargin, threshold]) - return
+ return ( +
+ onLoadMore()} /> +
+ ) } -InfiniteScrollTrigger.propTypes = { +InfiniteLoadTrigger.propTypes = { onLoadMore: PropTypes.func.isRequired, rootMargin: PropTypes.string, threshold: PropTypes.number } -export default InfiniteScrollTrigger +export default InfiniteLoadTrigger diff --git a/src/components/ProjectCard/ProjectCard.module.scss b/src/components/ProjectCard/ProjectCard.module.scss index dab1a372..168de868 100644 --- a/src/components/ProjectCard/ProjectCard.module.scss +++ b/src/components/ProjectCard/ProjectCard.module.scss @@ -44,9 +44,13 @@ overflow: hidden; white-space: nowrap; text-overflow: ellipsis; - //display: flex; - //justify-content: space-between; - //align-items: center; + display: flex; + justify-content: space-between; + align-items: center; + + .status { + opacity: 0.7; + } } .icon { diff --git a/src/components/ProjectCard/index.js b/src/components/ProjectCard/index.js index 39016cb2..706fe4df 100644 --- a/src/components/ProjectCard/index.js +++ b/src/components/ProjectCard/index.js @@ -2,10 +2,13 @@ import React from 'react' import PT from 'prop-types' import { Link } from 'react-router-dom' import cn from 'classnames' +import { find } from 'lodash' + +import { PROJECT_STATUS } from '../../config/constants' import styles from './ProjectCard.module.scss' -const ProjectCard = ({ projectName, projectId, selected, setActiveProject }) => { +const ProjectCard = ({ projectName, projectStatus, projectId, selected, setActiveProject }) => { return (
className={cn(styles.projectName, { [styles.selected]: selected })} onClick={() => setActiveProject(parseInt(projectId))} > -
{projectName}
+
+ {projectName} + {find(PROJECT_STATUS, { value: projectStatus }).label} +
) } ProjectCard.propTypes = { + projectStatus: PT.string.isRequired, projectId: PT.number.isRequired, projectName: PT.string.isRequired, selected: PT.bool.isRequired, diff --git a/src/components/ProjectForm/index.js b/src/components/ProjectForm/index.js index 8933928b..0de82681 100644 --- a/src/components/ProjectForm/index.js +++ b/src/components/ProjectForm/index.js @@ -15,6 +15,7 @@ const ProjectForm = ({ setActiveProject, history, isEdit, + canManage, projectDetail }) => { const [isSaving, setIsSaving] = useState(false) @@ -50,7 +51,7 @@ const ProjectForm = ({ name: data.projectName, description: data.description, type: data.projectType.value, - status: (data.status || {}).value, + status: canManage ? (data.status || {}).value : undefined, groups: data.groups, terms: data.terms ? [data.terms] : [] } @@ -112,7 +113,7 @@ const ProjectForm = ({ )}
- {isEdit && ( + {isEdit && canManage && (
) : null} diff --git a/src/containers/Tab/index.js b/src/containers/Tab/index.js index bf72d6df..d9845f37 100644 --- a/src/containers/Tab/index.js +++ b/src/containers/Tab/index.js @@ -5,6 +5,7 @@ import { connect } from 'react-redux' import Tab from '../../components/Tab' import { loadProjects, + loadTaasProjects, setActiveProject, resetSidebarActiveParams, unloadProjects @@ -96,7 +97,7 @@ class TabContainer extends Component { const { history } = props if (history.location.pathname === '/taas') { - this.props.loadProjects('', false, { type: 'talent-as-a-service', status: undefined }) + this.props.loadTaasProjects() } else { this.props.loadProjects() } @@ -138,6 +139,7 @@ TabContainer.propTypes = { isLoading: PropTypes.bool, isLoadProjectsSuccess: PropTypes.bool, loadProjects: PropTypes.func, + loadTaasProjects: PropTypes.func, unloadProjects: PropTypes.func, activeProjectId: PropTypes.number, history: PropTypes.any.isRequired, @@ -153,6 +155,7 @@ const mapStateToProps = ({ sidebar }) => ({ const mapDispatchToProps = { loadProjects, + loadTaasProjects, unloadProjects, setActiveProject, resetSidebarActiveParams diff --git a/src/reducers/sidebar.js b/src/reducers/sidebar.js index abb6b049..be25da00 100644 --- a/src/reducers/sidebar.js +++ b/src/reducers/sidebar.js @@ -17,6 +17,8 @@ const initialState = { isLoading: false, projects: [], taasProjects: [], + total: 0, + page: 0, isLoadProjectsSuccess: false } @@ -31,12 +33,14 @@ export default function (state = initialState, action) { taasProjects: _.filter(action.projects, { type: 'talent-as-a-service' }), + total: action.total, + page: action.page, isLoading: false, isLoggedIn: true, isLoadProjectsSuccess: true } case UNLOAD_PROJECTS_SUCCESS: - return { ...state, projects: [], isLoading: false, isLoggedIn: true, isLoadProjectsSuccess: false } + return { ...state, total: 0, page: 0, projects: [], isLoading: false, isLoggedIn: true, isLoadProjectsSuccess: false } case LOAD_PROJECTS_PENDING: return { ...state, isLoading: true } case LOAD_PROJECTS_FAILURE: { diff --git a/src/services/projects.js b/src/services/projects.js index 2f3c7d0a..e749ea11 100644 --- a/src/services/projects.js +++ b/src/services/projects.js @@ -9,6 +9,8 @@ import { PHASE_PRODUCT_CHALLENGE_ID_FIELD, PHASE_PRODUCT_TEMPLATE_ID } from '../config/constants' +import { paginationHeaders } from '../util/pagination' + const { PROJECT_API_URL } = process.env /** @@ -39,13 +41,21 @@ export async function fetchBillingAccount (projectId) { * Api request for fetching member's projects * @returns {Promise<*>} */ -export async function fetchMemberProjects (filters) { +export function fetchMemberProjects (filters) { const params = { ...filters } - const response = await axiosInstance.get(`${PROJECT_API_URL}?${queryString.stringify(params)}`) - return _.get(response, 'data') + for (let param in params) { + if (params[param] && Array.isArray(params[param])) { + params[`${param}[$in]`] = params[param] + params[param] = undefined + } + } + + return axiosInstance.get(`${PROJECT_API_URL}?${queryString.stringify(params)}`).then(response => { + return { projects: _.get(response, 'data'), pagination: paginationHeaders(response) } + }) } /** diff --git a/src/util/pagination.js b/src/util/pagination.js new file mode 100644 index 00000000..83361379 --- /dev/null +++ b/src/util/pagination.js @@ -0,0 +1,6 @@ +import { get, pick, camelCase } from 'lodash' + +export function paginationHeaders (response) { + const headers = pick(get(response, 'headers'), 'x-page', 'x-per-page', 'x-total', 'x-total-pages') + return Object.fromEntries(Object.entries(headers).map(([key, value]) => [camelCase(key), +value])) +} diff --git a/src/util/tc.js b/src/util/tc.js index f77de62e..518a230b 100644 --- a/src/util/tc.js +++ b/src/util/tc.js @@ -10,6 +10,7 @@ import { SUBMITTER_ROLE_UUID, READ_ONLY_ROLES, ALLOWED_DOWNLOAD_SUBMISSIONS_ROLES, + ALLOWED_ACCEPT_PROJECT_ROLES, ALLOWED_EDIT_RESOURCE_ROLES } from '../config/constants' import _ from 'lodash' @@ -194,9 +195,13 @@ export const checkEditResourceRoles = resourceRoles => { * Checks if token has any of the admin roles * @param token */ -export const checkAdmin = token => { - const roles = _.get(decodeToken(token), 'roles') - return roles.some(val => ADMIN_ROLES.indexOf(val.toLowerCase()) > -1) +export const checkAdmin = (token, project) => { + const tokenData = decodeToken(token) + const roles = _.get(tokenData, 'roles') + const isAdmin = roles.some(val => ADMIN_ROLES.indexOf(val.toLowerCase()) > -1) + const canManageProject = !project || _.isEmpty(project) || ALLOWED_ACCEPT_PROJECT_ROLES.includes(_.get(_.find(project.members, { userId: tokenData.userId }), 'role')) + + return isAdmin && canManageProject } /** @@ -208,6 +213,7 @@ export const checkCopilot = (token, project) => { const roles = _.get(tokenData, 'roles') const isCopilot = roles.some(val => COPILOT_ROLES.indexOf(val.toLowerCase()) > -1) const canManageProject = !project || _.isEmpty(project) || ALLOWED_EDIT_RESOURCE_ROLES.includes(_.get(_.find(project.members, { userId: tokenData.userId }), 'role')) + return isCopilot && canManageProject } From 22cd966a2dbe6d2ed6673cca3ee8d51055958da5 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Thu, 20 Feb 2025 10:05:37 +0200 Subject: [PATCH 12/18] For admin, fetch all projects --- src/actions/sidebar.js | 19 ++++++++++--------- src/actions/users.js | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/actions/sidebar.js b/src/actions/sidebar.js index 9005f5cc..dd521d25 100644 --- a/src/actions/sidebar.js +++ b/src/actions/sidebar.js @@ -11,6 +11,7 @@ import { UNLOAD_PROJECTS_SUCCESS, PROJECTS_PAGE_SIZE } from '../config/constants' +import { checkAdmin } from '../util/tc' import _ from 'lodash' /** @@ -29,7 +30,7 @@ export function setActiveProject (projectId) { /** * Loads projects of the authenticated user */ -export function loadProjects (filterProjectName = '', myProjects = true, paramFilters = {}) { +export function loadProjects (filterProjectName = '', paramFilters = {}) { return (dispatch, getState) => { dispatch({ type: LOAD_PROJECTS_PENDING @@ -48,18 +49,18 @@ export function loadProjects (filterProjectName = '', myProjects = true, paramFi } } - if (myProjects) { + if (!checkAdmin(getState().auth.token)) { filters['memberOnly'] = true } + // eslint-disable-next-line no-debugger const state = getState().sidebar - // eslint-disable-next-line no-sequences - fetchMemberProjects(filters).then(({ projects, pagination }) => (console.log('here', pagination), dispatch({ + fetchMemberProjects(filters).then(({ projects, pagination }) => dispatch({ type: LOAD_PROJECTS_SUCCESS, projects: _.uniqBy((state.projects || []).concat(projects), 'id'), total: pagination.xTotal, page: pagination.xPage - }))).catch(() => dispatch({ + })).catch(() => dispatch({ type: LOAD_PROJECTS_FAILURE })) } @@ -68,19 +69,19 @@ export function loadProjects (filterProjectName = '', myProjects = true, paramFi /** * Load more projects for the authenticated user */ -export function loadMoreProjects (filterProjectName = '', myProjects = true, paramFilters = {}) { +export function loadMoreProjects (filterProjectName = '', paramFilters = {}) { return (dispatch, getState) => { const state = getState().sidebar - loadProjects(filterProjectName, myProjects, _.assignIn({}, paramFilters, { + loadProjects(filterProjectName, _.assignIn({}, paramFilters, { perPage: PROJECTS_PAGE_SIZE, page: state.page + 1 }))(dispatch, getState) } } -export function loadTaasProjects (filterProjectName = '', myProjects = true, paramFilters = {}) { - return loadProjects(filterProjectName, myProjects, Object.assign({ +export function loadTaasProjects (filterProjectName = '', paramFilters = {}) { + return loadProjects(filterProjectName, Object.assign({ type: 'talent-as-a-service' }, paramFilters)) } diff --git a/src/actions/users.js b/src/actions/users.js index e02ef1ee..46a9a56f 100644 --- a/src/actions/users.js +++ b/src/actions/users.js @@ -28,7 +28,7 @@ export function loadAllUserProjects (isAdmin = true) { filters['memberOnly'] = true } - fetchMemberProjects(filters).then(projects => dispatch({ + fetchMemberProjects(filters).then(({ projects }) => dispatch({ type: LOAD_ALL_USER_PROJECTS_SUCCESS, projects })).catch(() => dispatch({ From 4e47782b79cac80769c296552e2b0a5122bbe834 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Mon, 24 Feb 2025 08:31:01 +0200 Subject: [PATCH 13/18] PM-846 - show status field for managers --- src/containers/ProjectEditor/index.js | 29 ++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/containers/ProjectEditor/index.js b/src/containers/ProjectEditor/index.js index 7c96f588..b7291d36 100644 --- a/src/containers/ProjectEditor/index.js +++ b/src/containers/ProjectEditor/index.js @@ -15,8 +15,10 @@ import { updateProject } from '../../actions/projects' import { setActiveProject } from '../../actions/sidebar' -import { checkAdminOrCopilot } from '../../util/tc' +import { checkAdminOrCopilot, checkAdmin } from '../../util/tc' +import { PROJECT_ROLES } from '../../config/constants' import Loader from '../../components/Loader' + class ProjectEditor extends Component { constructor (props) { super(props) @@ -53,6 +55,25 @@ class ProjectEditor extends Component { } } + getMemberRole (members, handle) { + if (!handle) { return null } + + const found = _.find(members, (m) => { + return m.handle === handle + }) + + return _.get(found, 'role') + } + + checkIsCopilotOrManager (projectMembers, handle) { + if (projectMembers && projectMembers.length > 0) { + const role = this.getMemberRole(projectMembers, handle) + return role === PROJECT_ROLES.COPILOT || role === PROJECT_ROLES.MANAGER + } else { + return false + } + } + render () { const { match, @@ -66,8 +87,13 @@ class ProjectEditor extends Component { isProjectLoading, projectDetail } = this.props + if (isProjectTypesLoading || (isEdit && isProjectLoading)) return + const isAdmin = checkAdmin(this.props.auth.token) + const isCopilotOrManager = this.checkIsCopilotOrManager(_.get(this.state.project, 'members', []), _.get(this.props.auth, 'user.handle', null)) + const canManage = isAdmin || isCopilotOrManager + const projectId = this.getProjectId(match) return (
@@ -97,6 +123,7 @@ class ProjectEditor extends Component { setActiveProject={setActiveProject} history={history} isEdit={isEdit} + canManage={canManage} projectDetail={projectDetail} />
From 5f7bbad3bde2f7429278d28fa3cf55914380129a Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Mon, 24 Feb 2025 08:38:14 +0200 Subject: [PATCH 14/18] PM-845 - allow admin to edit project even if not part of the project --- src/config/constants.js | 5 ----- src/util/tc.js | 14 +++++--------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/config/constants.js b/src/config/constants.js index 2540b72e..43e69a56 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -247,11 +247,6 @@ export const PROJECT_ROLES = { COPILOT: 'copilot' } -export const ALLOWED_ACCEPT_PROJECT_ROLES = [ - 'administrator', - PROJECT_ROLES.MANAGER -] - export const ALLOWED_DOWNLOAD_SUBMISSIONS_ROLES = [ 'administrator', PROJECT_ROLES.MANAGER, diff --git a/src/util/tc.js b/src/util/tc.js index 518a230b..576e9df4 100644 --- a/src/util/tc.js +++ b/src/util/tc.js @@ -10,7 +10,6 @@ import { SUBMITTER_ROLE_UUID, READ_ONLY_ROLES, ALLOWED_DOWNLOAD_SUBMISSIONS_ROLES, - ALLOWED_ACCEPT_PROJECT_ROLES, ALLOWED_EDIT_RESOURCE_ROLES } from '../config/constants' import _ from 'lodash' @@ -195,13 +194,10 @@ export const checkEditResourceRoles = resourceRoles => { * Checks if token has any of the admin roles * @param token */ -export const checkAdmin = (token, project) => { +export const checkAdmin = (token) => { const tokenData = decodeToken(token) const roles = _.get(tokenData, 'roles') - const isAdmin = roles.some(val => ADMIN_ROLES.indexOf(val.toLowerCase()) > -1) - const canManageProject = !project || _.isEmpty(project) || ALLOWED_ACCEPT_PROJECT_ROLES.includes(_.get(_.find(project.members, { userId: tokenData.userId }), 'role')) - - return isAdmin && canManageProject + return roles.some(val => ADMIN_ROLES.indexOf(val.toLowerCase()) > -1) } /** @@ -224,11 +220,11 @@ export const checkCopilot = (token, project) => { export const checkAdminOrCopilot = (token, project) => { const tokenData = decodeToken(token) const roles = _.get(tokenData, 'roles') - const allowedRoles = [...ADMIN_ROLES, ...COPILOT_ROLES] - const isAdminOrCopilot = roles.some(val => allowedRoles.indexOf(val.toLowerCase()) > -1) + const isAdmin = roles.some(val => ADMIN_ROLES.indexOf(val.toLowerCase()) > -1) + const isCopilot = roles.some(val => COPILOT_ROLES.indexOf(val.toLowerCase()) > -1) const canManageProject = !project || _.isEmpty(project) || ALLOWED_EDIT_RESOURCE_ROLES.includes(_.get(_.find(project.members, { userId: tokenData.userId }), 'role')) - return isAdminOrCopilot && canManageProject + return isAdmin || (isCopilot && canManageProject) } /** From ab260d41d5f34a0aa529ce6d4f68128d8ccc22d8 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Mon, 24 Feb 2025 08:42:57 +0200 Subject: [PATCH 15/18] PM-839 - disable "launch new" challenge button when project is not active --- .../ChallengesComponent/ProjectStatus/index.js | 4 ++-- src/components/ChallengesComponent/index.js | 16 ++++++++++------ src/components/ProjectCard/index.js | 4 ++-- src/components/ProjectForm/index.js | 6 +++--- src/config/constants.js | 11 ++++++++++- 5 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/components/ChallengesComponent/ProjectStatus/index.js b/src/components/ChallengesComponent/ProjectStatus/index.js index 1fa9b837..9592c53f 100644 --- a/src/components/ChallengesComponent/ProjectStatus/index.js +++ b/src/components/ChallengesComponent/ProjectStatus/index.js @@ -1,13 +1,13 @@ import React from 'react' import PropTypes from 'prop-types' import cn from 'classnames' -import { PROJECT_STATUS } from '../../../config/constants' +import { PROJECT_STATUSES } from '../../../config/constants' import styles from './ProjectStatus.module.scss' const ProjectStatus = ({ status }) => { return (
-
{PROJECT_STATUS.find(item => item.value === status).label}
+
{PROJECT_STATUSES.find(item => item.value === status).label}
) } diff --git a/src/components/ChallengesComponent/index.js b/src/components/ChallengesComponent/index.js index 5321e4fd..5a978a54 100644 --- a/src/components/ChallengesComponent/index.js +++ b/src/components/ChallengesComponent/index.js @@ -7,7 +7,7 @@ import PropTypes from 'prop-types' import { Helmet } from 'react-helmet' import { Link } from 'react-router-dom' import ProjectStatus from './ProjectStatus' -import { PROJECT_ROLES, TYPEFORM_URL } from '../../config/constants' +import { PROJECT_ROLES, TYPEFORM_URL, PROJECT_STATUS } from '../../config/constants' import { PrimaryButton, OutlineButton } from '../Buttons' import ChallengeList from './ChallengeList' import styles from './ChallengesComponent.module.scss' @@ -101,11 +101,15 @@ const ChallengesComponent = ({ target={'_blank'} /> )} - - - + {activeProject.status === PROJECT_STATUS.ACTIVE ? ( + + + + ) : ( + + )}
) : ( diff --git a/src/components/ProjectCard/index.js b/src/components/ProjectCard/index.js index 706fe4df..f639259b 100644 --- a/src/components/ProjectCard/index.js +++ b/src/components/ProjectCard/index.js @@ -4,7 +4,7 @@ import { Link } from 'react-router-dom' import cn from 'classnames' import { find } from 'lodash' -import { PROJECT_STATUS } from '../../config/constants' +import { PROJECT_STATUSES } from '../../config/constants' import styles from './ProjectCard.module.scss' @@ -18,7 +18,7 @@ const ProjectCard = ({ projectName, projectStatus, projectId, selected, setActiv >
{projectName} - {find(PROJECT_STATUS, { value: projectStatus }).label} + {find(PROJECT_STATUSES, { value: projectStatus }).label}
diff --git a/src/components/ProjectForm/index.js b/src/components/ProjectForm/index.js index 0de82681..dc5d9380 100644 --- a/src/components/ProjectForm/index.js +++ b/src/components/ProjectForm/index.js @@ -5,7 +5,7 @@ import { get } from 'lodash' import styles from './ProjectForm.module.scss' import { PrimaryButton } from '../Buttons' import Select from '../Select' -import { PROJECT_STATUS, DEFAULT_NDA_UUID } from '../../config/constants' +import { PROJECT_STATUSES, DEFAULT_NDA_UUID } from '../../config/constants' import GroupsFormField from './GroupsFormField' const ProjectForm = ({ @@ -30,7 +30,7 @@ const ProjectForm = ({ projectName: isEdit ? projectDetail.name : '', description: isEdit ? projectDetail.description : '', status: isEdit - ? PROJECT_STATUS.find((item) => item.value === projectDetail.status) || + ? PROJECT_STATUSES.find((item) => item.value === projectDetail.status) || null : null, projectType: isEdit @@ -127,7 +127,7 @@ const ProjectForm = ({ rules={{ required: 'Please select a status' }} render={({ field }) => ( + {errors.projectStatus && ( +
+ {errors.projectStatus.message} +
+ )} + + + )} + /> + )} {!isEdit && (