From 5b0eb3d72674a6e42d4bd3e96253442538adafc9 Mon Sep 17 00:00:00 2001 From: maxceem Date: Fri, 4 Dec 2020 01:43:55 +0200 Subject: [PATCH 01/32] feat: challenge attachments - Support drag & drop for multiple attachments ref issue #917 --- config/constants/development.js | 10 +- config/constants/production.js | 6 +- package-lock.json | 190 ++++++++++-- package.json | 3 +- src/actions/challenges.js | 76 +++-- .../Attachment-Field.module.scss | 135 ++------- .../ChallengeEditor/Attachment-Field/index.js | 102 +++---- .../ChallengeEditor/ChallengeView/index.js | 15 +- src/components/ChallengeEditor/index.js | 29 +- .../FilestackFilePicker.module.scss | 60 ++++ src/components/FilestackFilePicker/index.jsx | 283 ++++++++++++++++++ src/config/constants.js | 36 ++- src/containers/ChallengeEditor/index.js | 1 + src/reducers/challenges.js | 86 ++++-- src/services/challenges.js | 25 +- 15 files changed, 762 insertions(+), 295 deletions(-) create mode 100644 src/components/FilestackFilePicker/FilestackFilePicker.module.scss create mode 100644 src/components/FilestackFilePicker/index.jsx diff --git a/config/constants/development.js b/config/constants/development.js index e311459d..16c2f8e4 100644 --- a/config/constants/development.js +++ b/config/constants/development.js @@ -2,8 +2,8 @@ const DOMAIN = 'topcoder-dev.com' const DEV_API_HOSTNAME = `https://api.${DOMAIN}` module.exports = { - ACCOUNTS_APP_CONNECTOR_URL: `https://accounts-auth0.${DOMAIN}`, - ACCOUNTS_APP_LOGIN_URL: `https://accounts-auth0.${DOMAIN}`, + ACCOUNTS_APP_CONNECTOR_URL: `http://localhost:5000`, + ACCOUNTS_APP_LOGIN_URL: `http://localhost:5000`, COMMUNITY_APP_URL: `https://www.${DOMAIN}`, MEMBER_API_URL: `${DEV_API_HOSTNAME}/v4/members`, MEMBER_API_V3_URL: `${DEV_API_HOSTNAME}/v3/members`, @@ -32,5 +32,9 @@ module.exports = { DS_TRACK_ID: 'c0f5d461-8219-4c14-878a-c3a3f356466d', QA_TRACK_ID: '36e6a8d0-7e1e-4608-a673-64279d99c115', SEGMENT_API_KEY: 'QBtLgV8vCiuRX1lDikbMjcoe9aCHkF6n', - CREATE_FORUM_TYPE_IDS: ['927abff4-7af9-4145-8ba1-577c16e64e2e', 'dc876fa4-ef2d-4eee-b701-b555fcc6544c'] + CREATE_FORUM_TYPE_IDS: ['927abff4-7af9-4145-8ba1-577c16e64e2e', 'dc876fa4-ef2d-4eee-b701-b555fcc6544c'], + FILE_PICKER_API_KEY: process.env.FILE_PICKER_API_KEY, + FILE_PICKER_CONTAINER_NAME: 'tc-challenge-v5-dev', + FILE_PICKER_REGION: 'us-east-1', + FILE_PICKER_CNAME: 'fs.topcoder.com' } diff --git a/config/constants/production.js b/config/constants/production.js index e690824d..2822083a 100644 --- a/config/constants/production.js +++ b/config/constants/production.js @@ -32,5 +32,9 @@ module.exports = { DS_TRACK_ID: 'c0f5d461-8219-4c14-878a-c3a3f356466d', QA_TRACK_ID: '36e6a8d0-7e1e-4608-a673-64279d99c115', SEGMENT_API_KEY: 'QSQAW5BWmZfLoKFNRgNKaqHvLDLJoGqF', - CREATE_FORUM_TYPE_IDS: ['927abff4-7af9-4145-8ba1-577c16e64e2e', 'dc876fa4-ef2d-4eee-b701-b555fcc6544c'] + CREATE_FORUM_TYPE_IDS: ['927abff4-7af9-4145-8ba1-577c16e64e2e', 'dc876fa4-ef2d-4eee-b701-b555fcc6544c'], + FILE_PICKER_API_KEY: process.env.FILE_PICKER_API_KEY, + FILE_PICKER_CONTAINER_NAME: 'tc-challenge-v5-prod', + FILE_PICKER_REGION: 'us-east-1', + FILE_PICKER_CNAME: 'fs.topcoder.com' } diff --git a/package-lock.json b/package-lock.json index b0c37594..c179f235 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1018,6 +1018,11 @@ "resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz", "integrity": "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==" }, + "@filestack/loader": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@filestack/loader/-/loader-1.0.8.tgz", + "integrity": "sha512-dqgvVy5zULZJVnaiFkhXFNmK/U1JWNR2HD1DBz7tW9xDxjR/nccGQJPaTd5M3eTm7jLZ7uO870Dq17UOLatR/Q==" + }, "@fortawesome/fontawesome-common-types": { "version": "0.2.28", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.28.tgz", @@ -1066,6 +1071,40 @@ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.5.4.tgz", "integrity": "sha512-ZpKr+WTb8zsajqgDkvCEWgp6d5eJT6Q63Ng2neTbzBO76Lbe91vX/iVIW9dikq+Fs3yEo+ls4cxeXABD2LtcbQ==" }, + "@sentry/hub": { + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.28.0.tgz", + "integrity": "sha512-1k19yJJcKoHbw12FET35t0m86lx/X6eJ6r4qM13eb2WN/OpoFtsgs1IjQOhGFL3OfVMcfh800Lc57ga04RLjLA==", + "requires": { + "@sentry/types": "5.28.0", + "@sentry/utils": "5.28.0", + "tslib": "^1.9.3" + } + }, + "@sentry/minimal": { + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.28.0.tgz", + "integrity": "sha512-HzFrJx0xe5KETEZc7RxlH+1TfmH3q8w35ILOP5HGvk3+lG1DR25wHbMFmuUqNqVXrl26t0z32UBI30G1MxmTfA==", + "requires": { + "@sentry/hub": "5.28.0", + "@sentry/types": "5.28.0", + "tslib": "^1.9.3" + } + }, + "@sentry/types": { + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.28.0.tgz", + "integrity": "sha512-nNhoZEXdqM2xivxJBrLhxtJ2+s6FfKXUw5yBf0Jf/RBrBnH5fggPNImmyfpOoysl72igWcMWk4nnfyP5iDrriQ==" + }, + "@sentry/utils": { + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.28.0.tgz", + "integrity": "sha512-LW+ReVw9JG6g8Bvp2I1ThMDPATlisvkde+1WykxGqRhu2YIO+PvWhnoFhr9RD0ia3rYVlJkgkuTshMbPJ8HVwA==", + "requires": { + "@sentry/types": "5.28.0", + "tslib": "^1.9.3" + } + }, "@svgr/core": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@svgr/core/-/core-2.4.1.tgz", @@ -1735,6 +1774,11 @@ } } }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -1949,11 +1993,6 @@ "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" }, - "attr-accept": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.1.0.tgz", - "integrity": "sha512-sLzVM3zCCmmDtDNhI0i96k6PUztkotSOXqE4kDGQt/6iDi5M+H0srjeF+QC6jN581l4X/Zq3Zu/tgcErEssavg==" - }, "autoprefixer": { "version": "9.7.6", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.7.6.tgz", @@ -5774,6 +5813,11 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" }, + "fast-xml-parser": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-3.17.4.tgz", + "integrity": "sha512-qudnQuyYBgnvzf5Lj/yxMcf4L9NcVWihXJg7CiU1L+oUCq8MUnFEfH2/nXR/W5uq+yvUN1h7z6s7vs2v1WkL1A==" + }, "fastparse": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", @@ -5826,13 +5870,10 @@ "schema-utils": "^1.0.0" } }, - "file-selector": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.1.12.tgz", - "integrity": "sha512-Kx7RTzxyQipHuiqyZGf+Nz4vY9R1XGxuQl/hLoJwq+J4avk/9wxxgZyHKtbyIPJmbD4A66DWGYfyykWNpcYutQ==", - "requires": { - "tslib": "^1.9.0" - } + "file-type": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-10.11.0.tgz", + "integrity": "sha512-uzk64HRpUZyTGZtVuvrjP0FYxzQrBf4rojot6J65YMEbwBLB0CWm0CLojVpwpmFmxcE/lkvYICgfcGozbBq6rw==" }, "file-uri-to-path": { "version": "1.0.0", @@ -5859,6 +5900,40 @@ "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz", "integrity": "sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==" }, + "filestack-js": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/filestack-js/-/filestack-js-3.20.0.tgz", + "integrity": "sha512-aPFVi/sA7bBGsL4uh69WgEDYYrkdMLnb2iW2gAGpY7Yd2I848ffckvlVXCI6rwRYnkLdioWQc3sXn5snzA/HBQ==", + "requires": { + "@babel/runtime": "^7.8.4", + "@filestack/loader": "^1.0.4", + "@sentry/minimal": "^5.12.0", + "abab": "^2.0.3", + "debug": "^4.1.1", + "eventemitter3": "^4.0.0", + "fast-xml-parser": "^3.16.0", + "file-type": "^10.11.0", + "follow-redirects": "^1.10.0", + "isutf8": "^2.1.0", + "jsonschema": "^1.2.5", + "lodash.clonedeep": "^4.5.0", + "p-queue": "^4.0.0", + "spark-md5": "^3.0.0", + "ts-node": "^8.10.1" + }, + "dependencies": { + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, + "follow-redirects": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz", + "integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==" + } + } + }, "fill-range": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", @@ -6024,16 +6099,6 @@ "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" }, - "form-data": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", - "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, "forwarded": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", @@ -8173,6 +8238,11 @@ "handlebars": "^4.0.3" } }, + "isutf8": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isutf8/-/isutf8-2.1.0.tgz", + "integrity": "sha512-rEMU6f82evtJNtYMrtVODUbf+C654mos4l+9noOueesUMipSWK6x3tpt8DiXhcZh/ZOBWYzJ9h9cNAlcQQnMiQ==" + }, "jest": { "version": "23.6.0", "resolved": "https://registry.npmjs.org/jest/-/jest-23.6.0.tgz", @@ -8954,6 +9024,11 @@ "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" }, + "jsonschema": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.4.0.tgz", + "integrity": "sha512-/YgW6pRMr6M7C+4o8kS+B/2myEpHCrxO4PEWnqJNBFMjn7EWXqlQ4tGwL6xTHeRplwuZmcAncdvfOad1nT2yMw==" + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -9168,6 +9243,11 @@ "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" + }, "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -9261,6 +9341,11 @@ } } }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, "makeerror": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", @@ -10233,6 +10318,14 @@ "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==" }, + "p-queue": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-4.0.0.tgz", + "integrity": "sha512-3cRXXn3/O0o3+eVmUroJPSj/esxoEFIm0ZOno/T+NzG/VZgPOqQ8WKmlNqubSEpZmCIngEy34unkHGg83ZIBmg==", + "requires": { + "eventemitter3": "^3.1.0" + } + }, "p-retry": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-3.0.1.tgz", @@ -14552,16 +14645,6 @@ "scheduler": "^0.19.1" } }, - "react-dropzone": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-10.2.2.tgz", - "integrity": "sha512-U5EKckXVt6IrEyhMMsgmHQiWTGLudhajPPG77KFSvgsMqNEHSyGpqWvOMc5+DhEah/vH4E1n+J5weBNLd5VtyA==", - "requires": { - "attr-accept": "^2.0.0", - "file-selector": "^0.1.12", - "prop-types": "^15.7.2" - } - }, "react-error-overlay": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-5.1.6.tgz", @@ -16287,6 +16370,11 @@ "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" }, + "spark-md5": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spark-md5/-/spark-md5-3.0.1.tgz", + "integrity": "sha512-0tF3AGSD1ppQeuffsLDIOWlKUd3lS92tFxcsrh5Pe3ZphhnoK+oXIBTzOAThZCiuINZLvpiLH/1VS1/ANEJVig==" + }, "spdx-correct": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", @@ -17461,6 +17549,39 @@ "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==" }, + "ts-node": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz", + "integrity": "sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==", + "requires": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + }, + "dependencies": { + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + } + } + }, "tslib": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", @@ -19637,6 +19758,11 @@ "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" } } + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==" } } } diff --git a/package.json b/package.json index 3540d5a1..55f3c73e 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "eslint-plugin-standard": "^4.0.0", "express": "^4.16.4", "file-loader": "2.0.0", - "form-data": "^2.4.0", + "filestack-js": "^3.20.0", "fs-extra": "7.0.0", "html-webpack-plugin": "4.0.0-alpha.2", "identity-obj-proxy": "3.0.0", @@ -71,7 +71,6 @@ "react-debounce-input": "^3.2.0", "react-dev-utils": "^7.0.1", "react-dom": "^16.7.0", - "react-dropzone": "^10.1.5", "react-google-charts": "^3.0.13", "react-helmet": "^5.2.0", "react-js-pagination": "^3.0.3", diff --git a/src/actions/challenges.js b/src/actions/challenges.js index 641b9542..1d323904 100644 --- a/src/actions/challenges.js +++ b/src/actions/challenges.js @@ -5,7 +5,8 @@ import { fetchGroups, fetchTimelineTemplates, fetchChallengePhases, - uploadAttachment, + createAttachment as createAttachmentAPI, + removeAttachment as removeAttachmentAPI, fetchChallenge, fetchChallenges, fetchChallengeTerms, @@ -25,12 +26,14 @@ import { LOAD_CHALLENGES_FAILURE, LOAD_CHALLENGES_PENDING, LOAD_CHALLENGES_SUCCESS, - UPLOAD_ATTACHMENT_FAILURE, - UPLOAD_ATTACHMENT_PENDING, - UPLOAD_ATTACHMENT_SUCCESS, + CREATE_ATTACHMENT_FAILURE, + CREATE_ATTACHMENT_PENDING, + CREATE_ATTACHMENT_SUCCESS, + REMOVE_ATTACHMENT_FAILURE, + REMOVE_ATTACHMENT_PENDING, + REMOVE_ATTACHMENT_SUCCESS, CREATE_CHALLENGE_RESOURCE, DELETE_CHALLENGE_RESOURCE, - REMOVE_ATTACHMENT, PAGE_SIZE, UPDATE_CHALLENGE_DETAILS_PENDING, UPDATE_CHALLENGE_DETAILS_SUCCESS, @@ -347,38 +350,57 @@ export function loadGroups () { } export function createAttachment (challengeId, file) { - return async (dispatch, getState) => { - const getUploadingId = () => _.get(getState(), 'challenge.uploadingId') + return async (dispatch) => { + // create a temporary uploading id for each attachment + // so we can identify them for various actions (names theoretically can duplicate) + const uploadingId = _.uniqueId('uploadingId_') + + dispatch({ + type: CREATE_ATTACHMENT_PENDING, + challengeId, + file, + uploadingId + }) - if (challengeId !== getUploadingId()) { + try { + const attachment = await createAttachmentAPI(challengeId, file) dispatch({ - type: UPLOAD_ATTACHMENT_PENDING, - challengeId + type: CREATE_ATTACHMENT_SUCCESS, + attachment: attachment.data, + uploadingId + }) + } catch (error) { + dispatch({ + type: CREATE_ATTACHMENT_FAILURE, + file, + uploadingId }) - - try { - const attachment = await uploadAttachment(challengeId, file) - dispatch({ - type: UPLOAD_ATTACHMENT_SUCCESS, - attachment: attachment.data, - filename: file.name - }) - } catch (error) { - dispatch({ - type: UPLOAD_ATTACHMENT_FAILURE, - filename: file.name - }) - } } } } -export function removeAttachment (attachmentId) { - return (dispatch) => { +export function removeAttachment (challengeId, attachmentId) { + return async (dispatch) => { dispatch({ - type: REMOVE_ATTACHMENT, + type: REMOVE_ATTACHMENT_PENDING, + challengeId, attachmentId }) + + try { + await removeAttachmentAPI(challengeId, attachmentId) + dispatch({ + type: REMOVE_ATTACHMENT_SUCCESS, + challengeId, + attachmentId + }) + } catch (error) { + dispatch({ + type: REMOVE_ATTACHMENT_FAILURE, + challengeId, + attachmentId + }) + } } } diff --git a/src/components/ChallengeEditor/Attachment-Field/Attachment-Field.module.scss b/src/components/ChallengeEditor/Attachment-Field/Attachment-Field.module.scss index 20ae10aa..aca43d8d 100644 --- a/src/components/ChallengeEditor/Attachment-Field/Attachment-Field.module.scss +++ b/src/components/ChallengeEditor/Attachment-Field/Attachment-Field.module.scss @@ -1,112 +1,18 @@ @import "../../../styles/includes"; .container { - display: flex; - flex-direction: column; + margin-top: 30px; .row { - box-sizing: border-box; - display: flex; - flex-direction: row; - align-content: space-between; - justify-content: flex-start; - margin-top: 30px; + margin: 0 30px; - .field { - @include upto-sm { - display: block; - padding-bottom: 10px; - } - - label { - @include roboto-bold(); - - font-size: 16px; - line-height: 19px; - font-weight: 500; - color: $tc-gray-80; - } - - &.col1 { - max-width: 185px; - min-width: 185px; - margin-left: 30px; - margin-right: 14px; - margin-bottom: auto; - margin-top: auto; - padding-top: 10px; - white-space: nowrap; - display: flex; - align-items: center; - } - - &.col2 { - align-self: flex-end; - margin-bottom: auto; - margin-top: auto; - display: flex; - flex-direction: row; - align-items: center; - - input { - margin-right: 30px; - width: 271px; - } - input:last-of-type { - width: 187px; - margin-right: 10px; - } - } - } - - .uploadPanel { - cursor: pointer; - margin: 0 30px; - width: 100%; - align-self: center; - - - border: 1px solid $tc-gray-40; - border-radius: 6px; - height: 227px; - - &.isActive { - outline: auto 5px -webkit-focus-ring-color; - } - - label { - height: 100%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - } + label { + @include roboto-bold(); - .icon { - color: $tc-blue-20; - margin-bottom: 30px; - } - - .info { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - @include roboto; - - font-size: 16px; - font-weight: 400; - line-height: 19px; - color: $tc-gray-80; - - span { - color: $tc-blue-20; - } - } - - input { - display: none; - } + font-size: 16px; + line-height: 19px; + font-weight: 500; + color: $tc-gray-80; } .header { @@ -127,6 +33,7 @@ line-height: 19px; color: $tc-gray-80; padding: 0 30px; + margin-top: 30px; .col1 { flex: 4; @@ -171,25 +78,25 @@ justify-content: center; } - .icon { - color: $tc-red; + .actions { flex: 4; display: flex; justify-content: flex-end; padding-right: 15px; + } + + .removeIcon { + color: $tc-red; cursor: pointer; } - } - } - .row:nth-of-type(4) { - flex-direction: column; - padding: 0 30px; - } - .icon { - color: $tc-red; - cursor: pointer; + .loader { + > div { + margin-right: -7px; /* to center along with icons */ + width: 32px; + } + } + } } - } diff --git a/src/components/ChallengeEditor/Attachment-Field/index.js b/src/components/ChallengeEditor/Attachment-Field/index.js index 7224d2d7..28edb7bd 100644 --- a/src/components/ChallengeEditor/Attachment-Field/index.js +++ b/src/components/ChallengeEditor/Attachment-Field/index.js @@ -1,34 +1,29 @@ import _ from 'lodash' -import React, { useCallback } from 'react' +import React from 'react' import PropTypes from 'prop-types' -import { useDropzone } from 'react-dropzone' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { downloadAttachmentURL } from '../../../config/constants' -import { faCloudUploadAlt, faTrash } from '@fortawesome/free-solid-svg-icons' +import { downloadAttachmentURL, SPECIFICATION_ATTACHMENTS_FOLDER, getAWSContainerFileURL } from '../../../config/constants' +import { faTrash } from '@fortawesome/free-solid-svg-icons' +import FilestackFilePicker from '../../FilestackFilePicker' import styles from './Attachment-Field.module.scss' -import cn from 'classnames' - -const AttachmentField = ({ challenge, removeAttachment, onUploadFile, token, readOnly }) => { - const onDrop = useCallback(acceptedFiles => { - _.forEach(acceptedFiles, item => { - onUploadFile(challenge.id, item) - }) - }, []) - - const { - getRootProps, - getInputProps, - isDragActive - } = useDropzone({ onDrop }) +import Loader from '../../Loader' +const AttachmentField = ({ challengeId, attachments, removeAttachment, onUploadFile, token, readOnly }) => { const renderAttachments = (attachments) => ( _.map(attachments, (att, index) => ( -
- {att.fileName} +
+ {att.name}
{formatBytes(att.fileSize)}
- {!readOnly && (
removeAttachment(att.id)}> - -
)} + {!readOnly && ( +
+ {!att.isDeleting && !att.isUploading && ( + removeAttachment(challengeId, att.id)} className={styles.removeIcon} /> + )} + {(att.isDeleting || att.isUploading) && ( +
+ )} +
+ )}
)) ) @@ -41,45 +36,36 @@ const AttachmentField = ({ challenge, removeAttachment, onUploadFile, token, rea const i = Math.floor(Math.log(bytes) / Math.log(k)) return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i] } + return (
-
- -
+
- {!readOnly && (
-
- - + + {!readOnly && ( +
+ onUploadFile(challengeId, { + name: file.filename, + fileSize: file.size, + url: getAWSContainerFileURL(file.key) + })} + onUploadDone={(files) => console.log('onUploadDone', files)} + />
-
)} + )} { - _.has(challenge, 'attachments') && challenge.attachments.length > 0 && ( - -
-
- -
-
-
-
-
File Name
-
Size
-
Action
-
- { renderAttachments(challenge.attachments) } + attachments && attachments.length > 0 && ( +
+
+
File Name
+
Size
+
Action
- + { renderAttachments(attachments) } +
) }
@@ -89,11 +75,13 @@ const AttachmentField = ({ challenge, removeAttachment, onUploadFile, token, rea AttachmentField.defaultProps = { removeAttachment: () => {}, onUploadFile: () => {}, - readOnly: false + readOnly: false, + attachments: [] } AttachmentField.propTypes = { - challenge: PropTypes.shape().isRequired, + challengeId: PropTypes.string.isRequired, + attachments: PropTypes.array, removeAttachment: PropTypes.func, onUploadFile: PropTypes.func, token: PropTypes.string.isRequired, diff --git a/src/components/ChallengeEditor/ChallengeView/index.js b/src/components/ChallengeEditor/ChallengeView/index.js index 2a543acf..8eb1aa2d 100644 --- a/src/components/ChallengeEditor/ChallengeView/index.js +++ b/src/components/ChallengeEditor/ChallengeView/index.js @@ -24,6 +24,7 @@ import AssignedMemberField from '../AssignedMember-Field' const ChallengeView = ({ projectDetail, challenge, + attachments, metadata, challengeResources, token, @@ -180,13 +181,12 @@ const ChallengeView = ({ challenge={challenge} readOnly /> - { false && ( - - )} + @@ -215,6 +215,7 @@ ChallengeView.propTypes = { }).isRequired, projectDetail: PropTypes.object, challenge: PropTypes.object, + attachments: PropTypes.array, metadata: PropTypes.object, token: PropTypes.string, isLoading: PropTypes.bool.isRequired, diff --git a/src/components/ChallengeEditor/index.js b/src/components/ChallengeEditor/index.js index a9681444..939e8288 100644 --- a/src/components/ChallengeEditor/index.js +++ b/src/components/ChallengeEditor/index.js @@ -89,7 +89,6 @@ class ChallengeEditor extends Component { this.updateFileTypesMetadata = this.updateFileTypesMetadata.bind(this) this.toggleAdvanceSettings = this.toggleAdvanceSettings.bind(this) this.toggleNdaRequire = this.toggleNdaRequire.bind(this) - this.removeAttachment = this.removeAttachment.bind(this) this.removePhase = this.removePhase.bind(this) this.resetPhase = this.resetPhase.bind(this) this.savePhases = this.savePhases.bind(this) @@ -529,15 +528,6 @@ class ChallengeEditor extends Component { this.setState({ challenge: newChallenge }) } - removeAttachment (file) { - const { challenge } = this.state - const newChallenge = { ...challenge } - const { attachments: oldAttachments } = challenge - const newAttachments = _.remove(oldAttachments, att => att.fileName !== file) - newChallenge.attachments = _.clone(newAttachments) - this.setState({ challenge: newChallenge }) - } - /** * Remove Phase from challenge Phases list * @param index @@ -1025,7 +1015,8 @@ class ChallengeEditor extends Component { token, removeAttachment, failedToLoad, - projectDetail + projectDetail, + attachments } = this.props if (_.isEmpty(challenge)) { return
Error loading challenge
@@ -1323,14 +1314,14 @@ class ChallengeEditor extends Component { onUpdateMultiSelect={this.onUpdateMultiSelect} onUpdateMetadata={this.onUpdateMetadata} /> - { false && ( - - )} + diff --git a/src/components/FilestackFilePicker/FilestackFilePicker.module.scss b/src/components/FilestackFilePicker/FilestackFilePicker.module.scss new file mode 100644 index 00000000..1a9de2b0 --- /dev/null +++ b/src/components/FilestackFilePicker/FilestackFilePicker.module.scss @@ -0,0 +1,60 @@ +@import "../../styles/includes"; + +.container { + .file-picker { + display: flex; + flex-direction: column; + align-items: center; + padding: 20px; + border: 1px solid $tc-gray-40; + border-radius: 6px; + height: 227px; + position: relative; + font-size: 16px; + font-weight: 400; + line-height: 19px; + color: $tc-gray-80; + + .icon { + color: $tc-blue-20; + margin-bottom: 30px; + } + + .pseudo-link { + color: $tc-blue-20; + } + } + + .file-picker.error { + border-color: #f22f24; + } + + .file-picker.drag { + background-color: rgba(0, 0, 0, 0.1); + border-color: rgba(0, 0, 0, 0.4); + } + + .uploading-files .file-error { + color: #f22f24; + } + + .error-container { + margin-top: 5px; + padding: 5px 13px; + background: #fff4f4; + border: 1px solid #ffd4d1; + color: #f22f24; + font-size: 13px; + border-radius: 2px; + font-style: italic; + } +} + +.drop-zone-mask { + bottom: 0; + cursor: pointer; + position: absolute; + left: 0; + right: 0; + top: 0; +} diff --git a/src/components/FilestackFilePicker/index.jsx b/src/components/FilestackFilePicker/index.jsx new file mode 100644 index 00000000..e6c872e8 --- /dev/null +++ b/src/components/FilestackFilePicker/index.jsx @@ -0,0 +1,283 @@ +/** + * FilestackFilePicker Component + * + * Component for uploading files using Filestack Picker and Drag & Drop. + * - Supports multiple file uploading. + */ +import _ from 'lodash' +import React, { useEffect, useLayoutEffect, useRef, useState } from 'react' +import PT from 'prop-types' +import * as filestack from 'filestack-js' +import cn from 'classnames' +import { + FILE_PICKER_API_KEY, + FILE_PICKER_CNAME, + FILE_PICKER_FROM_SOURCES, + FILE_PICKER_REGION, + FILE_PICKER_CONTAINER_NAME, + FILE_PICKER_ACCEPT, + FILE_PICKER_MAX_SIZE, + FILE_PICKER_MAX_FILES, + FILE_PICKER_PROGRESS_INTERVAL +} from '../../config/constants' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons' +import styles from './FilestackFilePicker.module.scss' + +/** + * FilestackFilePicker component + */ +const FilestackFilePicker = ({ + path, + onFileUploadFinished, + onFileUploadFailed, + onUploadDone +}) => { + // the list of filenames which are currently being uploaded + const [uploadingFiles, setUploadingFiles] = useState([]) + // if something is currently dragged over the area + const [dragged, setDragged] = useState(false) + // Filestack client instance + const filestackRef = useRef(null) + // we have to use ref for this method, because filestack would be initialized once with a callback using this method + const updateUploadingFile = useRef() + + // init Filestack (without waiting for rendering) + useLayoutEffect(() => { + filestackRef.current = filestack.init(FILE_PICKER_API_KEY, { + cname: FILE_PICKER_CNAME + }) + }, []) + + // update the ref to `updateUploadingFile` to keep referencing fresh state data + useEffect(() => { + updateUploadingFile.current = (filename, updated) => { + const uploadingFileIndex = _.findIndex(uploadingFiles, { filename }) + + if (uploadingFileIndex > -1) { + const updatedFile = { + ...uploadingFiles[uploadingFileIndex], + ...updated + } + + setUploadingFiles([ + ...uploadingFiles.slice(0, uploadingFileIndex), + updatedFile, + ...uploadingFiles.slice(uploadingFileIndex + 1) + ]) + + return updatedFile + } + } + }, [uploadingFiles, setUploadingFiles]) + + useEffect(() => { + // if all files have been uploaded successfully, clean uploading file list + if (uploadingFiles.length > 0 && _.every(uploadingFiles, 'file')) { + setUploadingFiles([]) + } + + // if all files are fully loaded or error happens for them call `onUploadDone` callback + if ( + uploadingFiles.length > 0 && + _.every(uploadingFiles, (file) => file.file || file.error) + ) { + if (onUploadDone) { + const filesFailed = _.filter(uploadingFiles, 'error') + const filesUploaded = _.filter(uploadingFiles, 'file') + + onUploadDone({ + filesFailed: _.map(filesFailed, 'file'), + filesUploaded: _.map(filesUploaded, 'file') + }) + } + } + }, [uploadingFiles, setUploadingFiles, onUploadDone]) + + /** + * Handle for success file(s) uploading + * + * @param {Object} file upload file info + */ + const handleFileUploadSuccess = (file) => { + console.log('handleFileUploadSuccess', file) + updateUploadingFile.current(file.name, { + file, // set `file` to indicate that file uploaded + progress: 100 // make sure that progress is set to 100 when uploading is complete + }) + onFileUploadFinished && onFileUploadFinished(file) + } + + /** + * Handle for error during file(s) uploading + * + * @param {Object|String} error error during file uploading + */ + const handleFileUploadError = (file) => { + updateUploadingFile.current(file.name, { + file, // set `file` to indicate that file uploaded + progress: 100 // make sure that progress is set to 100 when uploading is complete + }) + onFileUploadFailed && onFileUploadFailed(file) + } + + /** + * Open Filestack picker + */ + const openFilePicker = () => { + filestackRef.current + .picker({ + accept: FILE_PICKER_ACCEPT, + fromSources: FILE_PICKER_FROM_SOURCES, + maxSize: FILE_PICKER_MAX_SIZE, + maxFiles: FILE_PICKER_MAX_FILES, + onUploadStarted: (files) => { + setUploadingFiles( + files.map((file) => ({ + filename: file.filename, + progress: 0, + file: null, + error: null + })) + ) + }, + onFileUploadFailed: handleFileUploadError, + onFileUploadFinished: handleFileUploadSuccess, + onFileUploadProgress: (file, progressInfo) => { + updateUploadingFile.current(file.filename, { + progress: progressInfo.totalPercent + }) + }, + startUploadingWhenMaxFilesReached: true, + storeTo: { + container: FILE_PICKER_CONTAINER_NAME, + path, + region: FILE_PICKER_REGION + } + }) + .open() + } + + /** + * Handle file(s) uploading when dropping them on the area + * + * @param {Event} e event + */ + const handleFileDrop = (e) => { + e.preventDefault() + + setDragged(false) + + const files = Array.from(e.dataTransfer.files).map((file, index) => { + const fileExt = '.' + file.name.split('.').pop() + let error = null + + if (!_.includes(FILE_PICKER_ACCEPT, fileExt)) { + error = `Not allowed file type "${fileExt}".` + } + + if (index + 1 > FILE_PICKER_MAX_FILES) { + error = `File skipped, because can upload maximum ${FILE_PICKER_MAX_FILES} files at once.` + } + + return { + filename: file.name, + progress: 0, + file, + error + } + }) + + const filesToUpload = _.map(_.reject(files, 'error'), 'file') + + setUploadingFiles(files.map((file) => ({ ...file, file: null }))) + + filesToUpload.map((file) => + filestackRef.current + .upload( + file, + { + onProgress: ({ totalPercent }) => { + updateUploadingFile.current(file.name, { + progress: totalPercent + }) + }, + progressInterval: FILE_PICKER_PROGRESS_INTERVAL + }, + { + container: FILE_PICKER_CONTAINER_NAME, + path, + region: FILE_PICKER_REGION + } + ) + .then(handleFileUploadSuccess) + .catch(handleFileUploadError) + ) + } + + const hasErrors = _.some(uploadingFiles, 'error') + + return ( +
+
+
+ +
+ + {uploadingFiles.length === 0 ? ( + <> +
Drag & Drop files here
+
or
+
+ click here to + browse +
+ + ) : ( +
+ {uploadingFiles.map((uploadingFile) => ( +
+ {uploadingFile.filename} ( + {uploadingFile.error ? ( + {uploadingFile.error} + ) : ( + `${uploadingFile.progress}%` + )} + ) +
+ ))} +
+ )} + +
setDragged(true)} + onDragLeave={() => setDragged(false)} + onDragOver={(e) => e.preventDefault()} + onDrop={handleFileDrop} + role='tab' + tabIndex={0} + aria-label='Select file to upload' + /> +
+
+ ) +} + +FilestackFilePicker.defaultProps = {} + +FilestackFilePicker.propTypes = { + path: PT.string.isRequired, + onFileUploadFinished: PT.func, + onFileUploadFailed: PT.func, + onUploadDone: PT.func +} + +export default FilestackFilePicker diff --git a/src/config/constants.js b/src/config/constants.js index 344e91fa..a3eebf43 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -18,6 +18,28 @@ export const { } = 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 +/** + * Filepicker config + */ +// to be able to start the Connect App we should pass at least the dummy value for `FILE_PICKER_API_KEY` +// 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' +// TODO uncomment this line to use correct `tc-challenge-v5-dev` bucket for DEV +// export const FILE_PICKER_CONTAINER_NAME = prcess.env.FILE_PICKER_CONTAINER_NAME || 'tc-challenge-v5-dev' +export const FILE_PICKER_CONTAINER_NAME = 'submission-staging-dev' +export const FILE_PICKER_REGION = process.env.FILE_PICKER_REGION || 'us-east-1' +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 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 +export const FILE_PICKER_PROGRESS_INTERVAL = 100 +export const SPECIFICATION_ATTACHMENTS_FOLDER = 'SPECIFICATION_ATTACHMENTS' + +// TODO uncomment this line to use the same bucket as we during FileStack uploading +// export const getAWSContainerFileURL = (key) => `https://${FILE_PICKER_CONTAINER_NAME}.s3.amazonaws.com/${key}` +export const getAWSContainerFileURL = (key) => `https://tc-challenge-v5-dev.s3.amazonaws.com/${key}` + // Actions export const LOAD_PROJECTS_SUCCESS = 'LOAD_PROJECTS_SUCCESS' export const LOAD_PROJECTS_PENDING = 'LOAD_PROJECTS_PENDING' @@ -62,9 +84,13 @@ export const LOAD_CHALLENGE_METADATA_SUCCESS = 'LOAD_CHALLENGE_METADATA_SUCCESS' export const SAVE_AUTH_TOKEN = 'SAVE_AUTH_TOKEN' -export const UPLOAD_ATTACHMENT_PENDING = 'UPLOAD_ATTACHMENT_PENDING' -export const UPLOAD_ATTACHMENT_FAILURE = 'UPLOAD_ATTACHMENT_FAILURE' -export const UPLOAD_ATTACHMENT_SUCCESS = 'UPLOAD_ATTACHMENT_SUCCESS' +export const CREATE_ATTACHMENT_PENDING = 'CREATE_ATTACHMENT_PENDING' +export const CREATE_ATTACHMENT_FAILURE = 'CREATE_ATTACHMENT_FAILURE' +export const CREATE_ATTACHMENT_SUCCESS = 'CREATE_ATTACHMENT_SUCCESS' + +export const REMOVE_ATTACHMENT_PENDING = 'REMOVE_ATTACHMENT_PENDING' +export const REMOVE_ATTACHMENT_FAILURE = 'REMOVE_ATTACHMENT_FAILURE' +export const REMOVE_ATTACHMENT_SUCCESS = 'REMOVE_ATTACHMENT_SUCCESS' export const LOAD_CHALLENGE_RESOURCES = 'LOAD_CHALLENGE_RESOURCES' export const LOAD_CHALLENGE_RESOURCES_SUCCESS = 'LOAD_CHALLENGE_RESOURCES_SUCCESS' @@ -81,8 +107,6 @@ export const DELETE_CHALLENGE_RESOURCE_SUCCESS = 'DELETE_CHALLENGE_RESOURCE_SUCC export const DELETE_CHALLENGE_RESOURCE_PENDING = 'DELETE_CHALLENGE_RESOURCE_PENDING' export const DELETE_CHALLENGE_RESOURCE_FAILURE = 'DELETE_CHALLENGE_RESOURCE_FAILURE' -export const REMOVE_ATTACHMENT = 'REMOVE_ATTACHMENT' - export const SET_FILTER_CHALLENGE_VALUE = 'SET_FILTER_CHALLENGE_VALUE' export const RESET_SIDEBAR_ACTIVE_PARAMS = 'RESET_SIDEBAR_ACTIVE_PARAMS' @@ -153,7 +177,7 @@ export const ADMIN_ROLES = [ ] export const downloadAttachmentURL = (challengeId, attachmentId, token) => - `${CHALLENGE_API_URL}/${challengeId}/attachments/${attachmentId}?token=${token}` + `${CHALLENGE_API_URL}/${challengeId}/attachments/${attachmentId}/download?token=${token}` export const PAGE_SIZE = 50 diff --git a/src/containers/ChallengeEditor/index.js b/src/containers/ChallengeEditor/index.js index 109e42ad..e540248e 100644 --- a/src/containers/ChallengeEditor/index.js +++ b/src/containers/ChallengeEditor/index.js @@ -233,6 +233,7 @@ class ChallengeEditor extends Component { metadata={metadata} projectDetail={projectDetail} challenge={challengeDetails} + attachments={attachments} challengeResources={challengeResources} token={token} challengeId={challengeId} diff --git a/src/reducers/challenges.js b/src/reducers/challenges.js index df398975..1a1c0ac1 100644 --- a/src/reducers/challenges.js +++ b/src/reducers/challenges.js @@ -15,10 +15,12 @@ import { LOAD_CHALLENGES_FAILURE, LOAD_CHALLENGES_PENDING, LOAD_CHALLENGES_SUCCESS, - UPLOAD_ATTACHMENT_FAILURE, - UPLOAD_ATTACHMENT_SUCCESS, - UPLOAD_ATTACHMENT_PENDING, - REMOVE_ATTACHMENT, + CREATE_ATTACHMENT_FAILURE, + CREATE_ATTACHMENT_SUCCESS, + CREATE_ATTACHMENT_PENDING, + REMOVE_ATTACHMENT_FAILURE, + REMOVE_ATTACHMENT_SUCCESS, + REMOVE_ATTACHMENT_PENDING, SET_FILTER_CHALLENGE_VALUE, UPDATE_CHALLENGE_DETAILS_FAILURE, UPDATE_CHALLENGE_DETAILS_SUCCESS, @@ -49,12 +51,6 @@ const initialState = { projectId: -1 } -function toastrSuccess (title, message) { - setImmediate(() => { - toastr.success(title, message) - }) -} - function toastrFailure (title, message) { setImmediate(() => { toastr.error(title, message) @@ -62,7 +58,6 @@ function toastrFailure (title, message) { } export default function (state = initialState, action) { - let attachments switch (action.type) { case LOAD_CHALLENGES_SUCCESS: return { @@ -218,23 +213,68 @@ export default function (state = initialState, action) { case LOAD_CHALLENGE_MEMBERS_SUCCESS: { return { ...state, metadata: { ...state.metadata, members: action.members } } } - case UPLOAD_ATTACHMENT_PENDING: - return { ...state, isUploading: true, isSuccess: false, uploadingId: action.challengeId } - case UPLOAD_ATTACHMENT_SUCCESS: - toastrSuccess('Success', `${action.filename} uploaded successfully. Save the challenge to reflect the changes!`) - attachments = _.cloneDeep(state.attachments) - attachments.push(action.attachment) - return { ...state, isUploading: false, isSuccess: true, uploadingId: null, attachments } - case UPLOAD_ATTACHMENT_FAILURE: - toastrFailure('Upload failure', `Failed to upload ${action.filename}`) - return { ...state, isUploading: false, isSuccess: false, uploadingId: null } - case REMOVE_ATTACHMENT: - attachments = _.filter(state.attachments, item => { + case CREATE_ATTACHMENT_PENDING: { + const attachments = [ + ...(state.attachments || []), + { + uploadingId: action.uploadingId, + name: action.file.name, + fileSize: action.file.fileSize, + isUploading: true + } + ] + return { ...state, attachments } + } + case CREATE_ATTACHMENT_SUCCESS: { + const attachments = _.map(state.attachments, item => { + if (item.uploadingId !== action.uploadingId) { + return item + } else { + return action.attachment + } + }) + return { ...state, attachments } + } + case CREATE_ATTACHMENT_FAILURE: { + toastrFailure('Upload failure', `Failed to upload ${action.file.name}`) + const attachments = _.reject(state.attachments, { + uploadingId: action.uploadingId + }) + return { ...state, attachments } + } + case REMOVE_ATTACHMENT_PENDING: { + const attachments = _.map(state.attachments, item => { + if (item.id !== action.attachmentId) { + return item + } else { + return { + ...item, + isDeleting: true + } + } + }) + return { ...state, attachments } + } + case REMOVE_ATTACHMENT_SUCCESS: { + const attachments = _.reject(state.attachments, { + id: action.attachmentId + }) + return { ...state, attachments } + } + case REMOVE_ATTACHMENT_FAILURE: { + toastrFailure('Removing failure', `Failed to remove attachment`) + const attachments = _.map(state.attachments, item => { if (item.id !== action.attachmentId) { return item + } else { + return { + ...item, + isDeleting: false + } } }) return { ...state, attachments } + } case SET_FILTER_CHALLENGE_VALUE: return { ...state, filterChallengeName: action.value.name, status: action.value.status } default: diff --git a/src/services/challenges.js b/src/services/challenges.js index b6a57c98..1eef1036 100644 --- a/src/services/challenges.js +++ b/src/services/challenges.js @@ -2,7 +2,6 @@ import _ from 'lodash' import qs from 'qs' import { axiosInstance } from './axiosWithAuth' import { updateChallengePhaseBeforeSendRequest, convertChallengePhaseFromSecondsToHours, normalizeChallengeDataFromAPI } from '../util/date' -import FormData from 'form-data' import { GROUPS_DROPDOWN_PER_PAGE } from '../config/constants' const { CHALLENGE_API_URL, @@ -126,12 +125,30 @@ export function updateChallenge (challengeId, challenge) { }) } -export function uploadAttachment (challengeId, file) { - const data = new FormData() - data.append('attachment', file) +/** + * Create attachment + * + * @param {String|Number} challengeId challenge id + * @param {String|Number} attachmentId attachment id + * + * @returns {Promise<*>} attachment data + */ +export function createAttachment (challengeId, data) { return axiosInstance.post(`${CHALLENGE_API_URL}/${challengeId}/attachments`, data) } +/** + * Remove attachment + * + * @param {String|Number} challengeId challenge id + * @param {String|Number} attachmentId attachment id + * + * @returns {Promise} + */ +export function removeAttachment (challengeId, attachmentId) { + return axiosInstance.delete(`${CHALLENGE_API_URL}/${challengeId}/attachments/${attachmentId}`) +} + /** * Fetch challenges from v5 API * @param filters From f113938cd8b75d5fdf8e8fff3f53a05341c5a31e Mon Sep 17 00:00:00 2001 From: maxceem Date: Fri, 4 Dec 2020 01:46:04 +0200 Subject: [PATCH 02/32] fix: revert back accounts URLS ref issue #917 --- config/constants/development.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/constants/development.js b/config/constants/development.js index 16c2f8e4..6a2aa92f 100644 --- a/config/constants/development.js +++ b/config/constants/development.js @@ -2,8 +2,8 @@ const DOMAIN = 'topcoder-dev.com' const DEV_API_HOSTNAME = `https://api.${DOMAIN}` module.exports = { - ACCOUNTS_APP_CONNECTOR_URL: `http://localhost:5000`, - ACCOUNTS_APP_LOGIN_URL: `http://localhost:5000`, + ACCOUNTS_APP_CONNECTOR_URL: `https://accounts-auth0.${DOMAIN}`, + ACCOUNTS_APP_LOGIN_URL: `https://accounts-auth0.${DOMAIN}`, COMMUNITY_APP_URL: `https://www.${DOMAIN}`, MEMBER_API_URL: `${DEV_API_HOSTNAME}/v4/members`, MEMBER_API_V3_URL: `${DEV_API_HOSTNAME}/v3/members`, From 0e8c3e3bfd19e57df80646bb2e7d919fa07885bf Mon Sep 17 00:00:00 2001 From: maxceem Date: Fri, 4 Dec 2020 01:48:03 +0200 Subject: [PATCH 03/32] chore: remove console.log --- src/components/ChallengeEditor/Attachment-Field/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/ChallengeEditor/Attachment-Field/index.js b/src/components/ChallengeEditor/Attachment-Field/index.js index 28edb7bd..18cae96a 100644 --- a/src/components/ChallengeEditor/Attachment-Field/index.js +++ b/src/components/ChallengeEditor/Attachment-Field/index.js @@ -52,7 +52,6 @@ const AttachmentField = ({ challengeId, attachments, removeAttachment, onUploadF fileSize: file.size, url: getAWSContainerFileURL(file.key) })} - onUploadDone={(files) => console.log('onUploadDone', files)} />
)} From f5f00627b772013e0f04b901330ee05516ecc04b Mon Sep 17 00:00:00 2001 From: nursoltan-s Date: Sat, 9 Jan 2021 19:00:37 +0800 Subject: [PATCH 04/32] add favicon --- public/favicon.ico | Bin 0 -> 15767 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 public/favicon.ico diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..74a5210073f9e140b0ee3e4e95ee37f9a8634441 GIT binary patch literal 15767 zcmeIZWl)^KvoB0Y@DNCFcXwMH0tC0IEyWdTX1)G_r+cB{?GeX z-BY*fez~7d?K3s?%=~(KdY+!?>E4-NUsYr=(TUL!5D+ls<-VvRAiM$mC!r$4OO9IF zO5oW$YYAlu1cd5Xj3*!x{QZrax~wEZ#VF|k{Dsn7PF)!R!IvHZ;aeyI!UMeM+dcw< z2PXo;p(z4_&>sW@0_Th-H4%6PvYDdn7lhaU{Bl~065u6h&T@Kg@LcY{lFWd;7y^PY zy8IUjP47imtBxC~PE*BwIdn4VI|`U_O+6jrm4UNoWfr*dRMGh7S1RNd1l1}FP-Kgc zq4mcb)LG;sV1bk(sDF|}{6cjv`VkL7B1cJk*C6|K+B*ArnHn|ZE6tUM_!|lA&>@*n zW$e&Js!n=TNf8Bi<3QPa=15)pU0 z71`b?3R0;PC)VYBE;+Qn|B)n60=d-+a8mo4cx$u@QhO=aRj<2vNtAr2+JH?B>WtMu zA22K*JWZ;$Y5)8KpG-H=dZIT#YcY>EXDmXE&U6+rtaBGVT`DeVttgFJ3-_0+ILmYo ztGI*!Tv@A39WwLf>0uI=Q@VP+7X#Sr{XXO#!ILC(Yyx z&{FAFP#dC81C`7F2nX%QeGsXsrq3q6_yW|py#<)K8jZEHZ)AQ6g&CPIFtco#je^~i zQ`_S{p8Wk%uigp94Pxu!&G?pD+`*gi+qNB7GJw6X5CQ6}XYzns3i=M79t~)B)>4dQ z*V?Y8&L(DX#Zd-u(+0$D&s%#Q<7D4i&`5n`0aaw0;t%#bvQj&qGE#$15w#45O2hp0 z^en)nL2NYh{ip!fr;$X{spln=q0HYtp1={Gz2%{cm%Zfp=isXO_K!!TC5PIxIupT$tgSh!~KHuYy)sMfkck4uqXhs7} z!NnmX-ZalXFzz^}q!hc6R}rm=qe>?R9U7W!tVo1{D-yNS)N@24*GADi)$>*o_f;gf zJS7{FHlUYbWkk4)P8;f_DM_}5T}Q>GO1LM2dYD30>wEjyEmjg$aT6^R2{G^G$7sYp z{2{v={(xvDD|S#KD>ij$PCaYT`Vm-$F+^1`IEZFcB|hK(%M&M0c*lXCZh81j;Rtkp zI@XX_rf%m_n0&&j@EsjP=^bc0R8JvLHsQmkc%1A53EKe}N;iYp!T4`gOzBc%0V~M> zY+%{P3e(vUGf<0Y|^c!pxsmpOk5gq=9E)k8~n#H$K ziCqx|QE>YZbNS%M-x!g`7~1U|{zP5E>}5xSL7mR^1vfl*4M&>Mq7BeCVib|RVbaI% zdnCv0yw%FoYtIcoR@>sgI}+S*RJfEFKBly2BH&!Pl~-V?_}QZj#-RrB6BZZ^g#AJf zxsKZRR5bElXz*coE!(y0eq`O{e`Nd1U2FtAQ@y%8d z)zqm};bR4eHd}z;T1hy?ndT#~`B!2v6&dckbRzI07Z}T2|8(r9Bg37&pb#Vetll+0 zU+pNndwD4bx{h26@H|a0S{QAhNP~XtZSNW%p*UJ9`MYhL?;{_FV8}F`o;bSrv@kQd zf4-jd;j=A!%PN&g`NMkr*O|DrmZKgtjBA@n_?0{50zou1%Y&$1IK8UWg?{W4zhkfS zOtcKANy1QI}WUO`f0!GAxhq0|&Hba_JUFP-orp zmQKE#-Blvz|G+F9L#%N*$kJDXriY>~F+*?}E*6QR4VYlDR<&Y(CK}SQ9gDm(5n%)s zzde%_cpb5|=mnSSMu}I4ZB|v`Vi1W({5e6P4H!%MdDaCs>pZ0Ftc%dx{OclddL6af z`sb#PM<{#LW;xo3bcBPg){BnG8IRS#sf|u|%!upK3NXfq5=-4Xh6qdrc5R;_>ORh09CHex?*gG36RC!%^Y-vx z&pR_?_L0k&fuSjh`)<)v)%u5+^o%J72AdAz$2$AJP1r4kM@OI_f&9yb#MvJ_c`MU= z#VHU%>FJ8NQmh$WeoQ*%vr;`zaK?leW})Ak1DgR~9GVk-xCY83BDZHwHBdlaBDs#* z|BN@JuS18@YQ3NardoQR8cc_xWw8LCttwVrs3_$`64VvZ~5aolYH;MA)|dfPbx8U%=@q4p8e*Z z?)%-!gA%Lk)F2(@q-$RMAhx1=W2HUo|1h0w$%fBS$K~tEQ*?eTOIG8dNlqqC7szQy z#>Cdo?e0D+v&L5CFY^^?u6v)WvL<}7AumqmLgML@4e6#KW?~G=4GWrKO+t(|tFc_b zVB3a`EU7R>W?FTOi#lvzvm|IEIVLo{#7*=+!!t0fyu~Q3TwY78Hrv89_6bx}?z2-d z>p6O0Voo;GH3u^ z93=OwH9|(-!wmTQzkUG+4*&S34>x=;gL-pGH7ljJ7Y8$#H5n$-H$BIPINH{T462>!`9~UD>r&*d-76CK3+n+AdMic z!M(t}0G$WKu?60?&=n<%r5it-aI}k6X#=U}Tp=%tI_|;*d}D2^dBBZbvq7%y+vUaz zk;ogitv;%QaksrLtZbFY%DrU8CAXMpHB;U@#rn_RE7`J;RfDy2T#9P7@e6Q?Z(G?_ zoxu3F_Q9RZ5s0owj<#>~?)lg#2k;=Io1`@bZU!YiN0HDR6l|i-dV2LOv=Xp8!aNsYE!k>i)EAlkM`J z(wD!wc0RPL!q@oorFdFw1cTlcPoCLVho}oS=?X$|%z^OUXYm3ps5=k3r1`iWsK?gv&!!nr{A822Z?inLQ}uk`7mSKo(fNKg*8U8HE$a_4vw*+S zHqx=1CJRcWTX8DkwGzfyM|3ffs;sp4mAF!SIr`0OMJ(;08|)yKs`rP4sdir@v3-(ka%XW<#5k z5s_4SpXv`#H@SxCBeVNvZh7R2iv2^{xeD)XRW3E8-BDVsg1K)%^P94DX&BeQg=oVV zjnteR50;*xqIbWn*7f48sRBVZj5d$sS2+g;w=>_9oopY+&ogtN0X9CfdX}q_9W8W< zqlvSrdY=X?F`V1dWip>%Kq5^~v)Ya&I1QI(DM{Lkto83d?e|0^5V@Q83@EwqLP(YI z`grykB3yRM+m^EeR8j{1BQQNOrj1j_1KWHPRx_6A?c4bl%n=U))%3pc!TE)u(f3f? z%cirZqk{QA`rJvZC+Eg@#l@xOk#3thtjlA}fD|R5&^MR075V_eYoI?mguUEr;~&%g z;x}6%>g=eq5TdN?98J6~man)w_BbA8EPG=%;N>2CMrrI2FNQ8;b9zk0#cE!^H|b$O z;cp}3H6P($(dHgIvARZ~JlSaQzT^W(mb&6z6O>~cdAIIgEZ$EJ>(9}~pQ3r%PZ!@n z5V`H;-Q>V#e>zun&uxnD9CN&d>W-Che`xdwc(e!M=>$ISd*R=87UJA0wO|Bc4le+LyDQm*#e^jnpC^3DMH3F3J7R)|O$T z4D2!Zy!a9?oAd;Zt>u#wZ0Z9=NCUS2j=STP1JLn8YAigz3qY0BF_>i&@=`ZaKT6DV zIEf+#NaB)?gG7p;HyJW}*y5z7!?<9L+JTAEKjMY)H)Zvs7+IM{@76_eD)|^*|oRuD-#%s>@0Af)1JFPNg@}Yey+~82lpM<8{js ztl1Szfr6U;KIQip?_PXL3F$2t`bn6V>`~K=y2}>RolmV*##dkIK;q8gmmwEqEOtJ0 zi-;H9pRg!Tq~M-cV_`(OF7CYRK!u0h-+s9owoO(ERh+0);56#m6USFA399Iuj}p^;JUF+H#!-NKYO1JVhh`;X)Vq{ag)wIT9R+$~nF(Yeh3efd84JBj zRyGU=u%W5=xAC{CdLo0w+?e|*Eo$hf=+jo=%GUE=Bql-=5bPRuRa59#BoN~E%IB}E zOzU&!qj7^yn6qMy8vD~*uI8`-J7T^CFArb zP(}fRs zOL3hnIKLioq@R#uR?E=Rp`+^d;4YN*kg->O?^hl6e+TGpoc4WFMmgJWfarZ*o@qoE1%nBCR)`c~}8 zOJ{NeB{FZGSOfRBG9~$EZboTX(&n4U{{2Yp-qlY1@xP;%nOA7CqMcQW zgrpzuA8QxKY-(@Ox^NI*am5L-L#3i9nlIv0j+@Kx4X)Hj`+V^wf>6uzat(KD%vtS` zXM@2Ab=3RbdVPTkBD!D<962S zmLGZL*nHlM^&LWqJ=y6&v+On5>0TRCuLoZow(2135pzEK>0!{{eFcw#RThw*_Z5o1 zq4#q>8y5Gw7}Dqm{qa|92lMI{POtKe7X=ofotp^+Z+s?@ zn@4yDCZ=!$gnH(Th+l4N=d#9)gCuDdY3z=tY{)+XknjHCI#}5xfcc1XHwa!&*((KI|$g<=}As7J%L;;DbQ>o?_e zX{8qI+U1TWXR)CBvyw|sP|7=U9X_L1ig)V#;LWqHog<&J6U$$C%Cfp7ioT%&`u&`} zkFI-HT2v7{&OD30^M&=BL;n1YGgu)y#MAU#i>R2Lq-(63N=IV3Z1J}0n*o|q;ruTb zmwMvlZy7-c`i*~Nx-()sGv!_6?MppZ+KgBC8uD7^kIvGeJFSoGWv)5a zH_ChUJQKK@@|n(YDR-wU2B)F**N%7L?#4^@CPS!lV*;gJt{ zSMx5)-+lHh&D`H2X)4tW;O^F_cA0L01Ty2F#@FG-3BhWIn~=NR*HYPZ&$ zS5@XtadO36O{8s&nl1(Ci`CNKdtw*QNS6n+iS^$yXHlM~XkaJ=2@X#9 z#MtAo3R?$)Rpa>NEZ-_DzQGOBcauQ%7QkecoLrLLp>E~DwDG$}0>6?+MXdAm*^7AMw%p@Iy69PTcJ^gp zu~&*1hLdNiVDQg@g8bc?@jEY?20XpAwxf66KoMn8)%v+2A##Z~_C$v&$NCkeVv}iH z*{-?`&xJiK8CV21CCnqKtS*~YPe*~RDY8kNa%HT=OPve^(^%Clp?#U2j@;j+@zUmf zw-bVes94D%$Eg~8J?gP0$8VP6U0Ka&)@nQ6 z`3789BK!AHhxmF{z*zH45W!<}B$`25LuQ)KEJLYUv#==IkZ|e5ow;zg#Xtz53D{&SJ{0$q`Gard!fzx5`GDb}wK&eQ| zeHGaC#YR*oW$Jp?T8a`egVy`*$Dyo5dGX}*CuW-!I8_$kRAh@m$K9;yIS?jXv_bsK z-TU`Ly0-L#vbK?2$&59%X+5b1&01A;$q;BNq&c&?c`MY(dcCTb%)00v@Lv@GH6jkOPncGc%90GAAT$R^CUq9#}ccw|3hLX$t*^ zxrTkW$l4kUki~0sOHv~VTH?$nJ%|=k4@0jDOhVMI4 zEm85SQX!-9Z&od^oG#uw)$n_9D7!`|{3r&6Gk;1>{My*a{|0yP42Bj~hvydu=T}A>-B`9>S`#0=G#pQ@+!qA{O$oM=(N{BXry#}J zF|ESi<4VS?Sav=kZz)Yg;^kk>cvc&+`urX?bw`JMw6GPNKHOFRTVcgUrdPAj9M7pc zXtTK7(^11DM3%S3Zsi%3N-iXkeqwGVJ32FLRcsc{Jb53FQkJaHVN%fWlj0>|XF4z$ z-Zz+)lPzit=9;Wnbg?N|qpCH@Wly_zMObC|WYr~kWmemE1POnPWvOB{?6}lPZHeg$ zM*zJ#^)w!hUvsUrC=-uUVVyP{s2`5cQF)1=bI3*3aX0%1fR>ws=n<7J z1|-!?=vqMN=I+F$qt|y7-j~xAWuI4x&L5`?A`L07_Wb4ntvMg@?p75Kw;Y8Qm0zps z%>>Rjra4mBDn}o^)yh3A$yNxnqVZPdM}Sk6>qaJ^CRw!x#FZ&2ft4TI8rIO2zmr&6 z9)yYMh4D>%jb#ODjeQ8-fee)da4VI zR#kgSF*(f(ItKs!*7|F6<1^QwF`ElQWsOd4zp3*(hA|-bw_V?Z7;RNcXi4E_A&sIK zgf9)EJvyBK84rz_$gEKIZtFBN3=kMWB<@Bu>K-~cMCDdWN_s{7?^Vk@%K`e^GJ;l3 z$(obYHl{heH6XiB6_H#F{qC+SG*&#y=QMB-oSC7*!0^7@pwM{=2eUUX53xD30qt#9 zO$7`&*Lil4_Iqpf=EPU7fap~RNNek5km2!1#UN3;f1_`0oEm;Ll?56V_u$`J*!=Qy z{I;zeCE9{(uLh7Vq8YwI2s&d^yHyR9ZKo2tr+Ha7@rKaod^Sk5ze#j4(|xOEAd^G| z1h0(f%Y7QL0SO%Vh&+p}AMj)+RP=e9^>gnee`Wcqz*cx}e2d`&dt2o`gnPs5?4Zrx zPc}11hxvg%!i0jkO~e!5;QE4nu!pryVY*OCK-BqU|0dw)BLC)QRaj(60nM`J@+MyF z=;ur8VBr~;=!Dj1&c)sU zwJ3;A`k|W3xesW4P)tTtzC7=%u>m={*E$IC;;Dgbvsaz7Mu_+laAyZP=UA=XUH`?u?! z=ioz{{ix0mO~2F6$Wnid%wfKMtikuTE1cXDrnpm9EF#k<8ZtrCnIMi$79cVGj2?6bW+`Pe8;v3O z#GGuT^^JEX<3{wu2hl7s4DSb#^}k1Pd+1}3&|sE1f1trrX?~gi&yZg(M3eqdbB88( z^uChKqqhHqj@CZ#<|j(`Ot73qP7U~_+r{NcBV z#t*>1Zum4Sy6cj?`+mx~RrF^eR}md|K#)cGjwzxO@6{?52++9tk=8{M+ou{3cq8Ar zC!T(O(1&(=^0yp}=iSyygoYex4_##B`HMmX&~Ti%h&VJz;+@a#tbg0Hm239k>k|6%+ z>NS{ug(CrR=SM_oK=gA#4XAY|j5>zzIT-jlzIE20KgT~@xJXCz^|AsLtP4X=J%o&` z>zC16aow9st6~+H#&??drmd0Ly#ziy%o4Z6u-FZ$Z76&v=)4iNajMWxc3{FGzFpbe zqTUGap2Y`m3K4C_W`FW*rxtn&+260fd*&WW9mB`#JuJLeDC==$BmD-A7|?X{KR&9`clo@%2~#B0Nfu5!-Z!4uzBFd#5cgt9yrbN^-WB-^jqcy=zm8_C_p>Pu@ap=L|Q zzi*+gWC!8ZIluWiBCIms*-PCQxKdm&Thazu++8tYxeCwn>AXzij#QELPOyGg6vQBM zPqQ(g^X|ElD`9Wny&>@cxBjxGIGJ518%uAO_#8!w;Kz}tsLEsbzwt!O2lTc2ib$Xj z-#W8eCb{;%aTBGNsxpmQ1F~BQM0nKb^|V9Jehb7@b3xn^c#%VFI=kr1=KvdVJ>5q~ zC9wuW%hT#=shxHetJ%3sC&gaQs&Q)b*RZpmJ_cR&CpqEvOwQB~a_sJEkXhN|P3h_F z`>jLjPp4o$-M?@E<{dnqt2a=)(+FNhy$Y>yk$VREwg6d%qPzUQFS6|nLJxc! z0~GMpgVV){iL2_0I;>vuTF?kLf_5fKCT*(%9wQ0JKJU&vRkJ~R8(Cquu8jn~jbU52LeJNgDDj%h|>Sh~E_;g%icosRk1OKXRJM$y7iq);?DTk*x6Vt+b0HJppGf-qxwdD?L!^s3-sw?$KGiSfpA&c7I z9Lw)uahv=oX->##dVzv{$Jb%ty#7D8S?+Z}@Uct-eaf^I_U+32=}h(P^zxxPpo>ZOe-2>$QZLr%rmCAt|QIw72;}$ zgbuc1vp(It$ar7S0dHBJHl&7+|7JvM%k_Q66yR{|ip%vnoDja=zr{=_p;un%IXYbP zZXO65#GUL=GM6P+66s%c4A*2~uv3+ZcoiCdI+1e|@R(gZ!ry9(8v0mw*kM_X(@=~w z!e;|+hem4yez+fV=Pr0)#n<9%b^mEl0t6@pYHuJ&}Id)||~o8Csx zR5>%`Pn<#nn(0a1t7rrFXRza^EGMK%|3Va-d;ul~#d!!uvK@a~kwj$eDfLa=N@VY6!3H&}cYbdT3@59ox&-9Uq(;vU2p50-Eu8417Q8dqZ z`aD@>wn#eY)~!xzaz*lW<>vCA?O(tD9t^~9-j>As-pqVEjaKC9pslfGIQe8ZIx%^< zV^etNrgKi=m)4%mEK{=d(eHaG*dGNljahJo$Y5u0z^>?<8ljx$H=ZHD2cXns5DKGDRbw?Z z9rZ`mUh8lx&XsBxGCtE5rW0pa-1KpiTUdxEWtG%v(0H7IriTEf!V0#aJ7P3m)*@M< zGM{ry%#K3+kXzqdxtKg zNh^>V;&^cGgvDn<+Qhm~%ObU|rji8gY3PB?wrWF!^O zj7ZVhvFVdCpH}dujFB5D0E_R6y@yQ8(zZ{&9BwVog90n>4Rmp9IeU7pc?#^k>WiHFlV9gawo#X_>ml*% z-T?MTzKxOM-Gs&kr%_QD*84uAdUj`#2}UHK&96>=FV!-Sf0vW7aX%E+K%EupUv&xT zBg8WqU))U2`^-mN1Y?SQGj&F0Fbtpw^wCsoVL7b_%jz8dFeb{`raOHRXZ)pfr<}Ag zHP6<}1MJFDdExu`{cXjNsrD_npHbs-&tJyvu2_s=op$8bV{;q|yX$rqa!&BW!#8J@ z;rR5v(gU4+2D(uFFGggVL}gwJJ)B8|xCzgyOSL9<;i1*hg^zGo zFO*@2c4^h*OeU5~-m3&?#LBw?Q)?{J)1GaMeqR(Ibvn&SNL0uZExqAW(P-ShX_jhv zUdsbZW?>+{j;IFYFBg{V!A)n3A=L?gfan_LLtW*cdKS=nHxS^Oi59qy?%jo7X`#wO zprQDqg0WuC_;vu-G2TJQbz$^H+=lcyeE-f1zHN0&pSdJr|4upZX2bUn|EvyVtv7rN zC)?W3v05pH?}R$oeZ4P(>h0jGSVeHe;=MLj-e2gtr43VT zf-gP89I}{3mT7-m=;NQ&W!fL!6D+zGz|Z?XB)eZQ(?sApHiscc+^>$VUsb~Qiyq%8 z9$&kauqGC!VI;bNt0B(OjLw+H_%+L56cJHAQql)fFUR@Y!UJir} z=uQcHK-N@H!q2}X(5B^mwb@A?{4*#DX8)d&If+bApVdGcLhdt47BFziuG$#wCXmti zem{uJCNSu)w^(2^(RhlBXs5RJSwaCPQHT3>OdxiW<3X(I1_b|tsrzkx*dfZ%Txq`3 zkdigruakA8(QN!oCvuPTvM=mznB?^H4;ugXd+R!_sG_t4oa{ZnH-0FNjS$HVzxAZ* z`or3nT*V%J>9nm-Fh!2>q`3ZbH~jCtHyNwhgHsrV>@w(!6_vg3HZ{KA5|2aGVfrhoJ z*Z@tRaKz&!34@R^i`UdvxSO8y^ISwnwZ!FfhYgIy9J80(Cmi-f{CL}A(i~B#f@^Y% zhk_&hATxuO(szxv^dx7=dE5y?+|nS->iBN*4~Dh!^PKsn@Jb}a4uY7Ttl^4b$dUkM zG-xo$%DF%jIxycPUv;N&y6d+TU>IBJ=wlvY;+YI3&bc;o?U$b$ry$G{!MNMwXQBxD zb9P_EMG^4Z`PQ>{)U$U{KZYVX{dh=UAi9Uw?czGholQy^6QG4_IpLt+A)rM%wm$Lo za;JK8j3EMk`iar94QPi4`hJ8{7jejF!9^+_CQlKQ)j9e&>bled1DHVhsqBEtKcg{@ zhb8mA68$dN_YYB#E%@WX1u-AKLgf+d82&IJzjp10&sDl;CLSA8(IHHQ>eOG8vyBAz zNUZ(dv}(jwzT(w1C!DXgCf=SDEVm}qKr$hXagfyk+jogY#6F$JB>||aO=kWM(VhBJ|3ykJ=1PWV4l4w9=-T6&sI1_}c z2bgoK1Mt+Zi+IuzFU+$kGO1Sty=GCClPfgZ zQjPRa+Tqvg3#w@*D&OEc;Poga+=@^fIh`z<4`tazX~(u zkydMf9jAKC>S^`n>aopTij*;dk_boo$hLRt?!%Dk3>6`FzaDd2FLKXAoG+b&pL_T; z=}6*ns~$iOEr^Mt8q|4-_L)s+<%Lyj`O}kg6mD!z{T~TJ@ql9cpNv<}^Nk;1(Wcj7 zu=o71GxW|wjXogXCgguE6#mcM$^Z9TrLW>fh#Y;vIUeWm>#=n1(z@;zKzB31GWT$>baWRI`U;1-A#fGFHgo(R znE&D_BxGZ0Yi;9>z$M6yh2ReI3*<$^Tdwf zRoefpN=scH!O9k72^XKk!O057wh}Po pwgm850(pRfW`b5gUVaO9I0ZYre}IYJO*jVxd1;j|6_TbQ{|nT>KLG#$ literal 0 HcmV?d00001 From 20aff2a32afcdd5315410129e64aae2b21908c07 Mon Sep 17 00:00:00 2001 From: nursoltan-s Date: Sat, 9 Jan 2021 19:42:29 +0800 Subject: [PATCH 05/32] added assign to me button --- .../AssignedMember-Field.module.scss | 4 ++++ .../AssignedMember-Field/index.js | 14 +++++++++++-- src/components/ChallengeEditor/index.js | 21 ++++++++++++++++++- src/containers/ChallengeEditor/index.js | 4 +++- 4 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/components/ChallengeEditor/AssignedMember-Field/AssignedMember-Field.module.scss b/src/components/ChallengeEditor/AssignedMember-Field/AssignedMember-Field.module.scss index 4f22fc27..d31dff9e 100644 --- a/src/components/ChallengeEditor/AssignedMember-Field/AssignedMember-Field.module.scss +++ b/src/components/ChallengeEditor/AssignedMember-Field/AssignedMember-Field.module.scss @@ -45,5 +45,9 @@ .readOnlyValue { margin-bottom: 0.5rem; // the same like `label` to be aligned } + + .assignSelfField { + margin-left: 20px; + } } diff --git a/src/components/ChallengeEditor/AssignedMember-Field/index.js b/src/components/ChallengeEditor/AssignedMember-Field/index.js index 4731518d..5a15b8f6 100644 --- a/src/components/ChallengeEditor/AssignedMember-Field/index.js +++ b/src/components/ChallengeEditor/AssignedMember-Field/index.js @@ -6,13 +6,15 @@ import PropTypes from 'prop-types' import cn from 'classnames' import styles from './AssignedMember-Field.module.scss' import SelectUserAutocomplete from '../../SelectUserAutocomplete' +import { PrimaryButton } from '../../Buttons' -const AssignedMemberField = ({ challenge, onChange, assignedMemberDetails, readOnly }) => { +const AssignedMemberField = ({ challenge, onAssignSelf, onChange, assignedMemberDetails, readOnly }) => { const value = assignedMemberDetails ? { // if we know assigned member details, then show user `handle`, otherwise fallback to `userId` label: assignedMemberDetails.handle, value: assignedMemberDetails.userId + '' } : null + return (
@@ -28,6 +30,13 @@ const AssignedMemberField = ({ challenge, onChange, assignedMemberDetails, readO /> )}
+
+ +
) } @@ -41,7 +50,8 @@ AssignedMemberField.propTypes = { challenge: PropTypes.shape().isRequired, onChange: PropTypes.func, assignedMemberDetails: PropTypes.shape(), - readOnly: PropTypes.bool + readOnly: PropTypes.bool, + onAssignSelf: PropTypes.func } export default AssignedMemberField diff --git a/src/components/ChallengeEditor/index.js b/src/components/ChallengeEditor/index.js index 935b65ad..82281cc0 100644 --- a/src/components/ChallengeEditor/index.js +++ b/src/components/ChallengeEditor/index.js @@ -90,6 +90,7 @@ class ChallengeEditor extends Component { this.onUpdateOthers = this.onUpdateOthers.bind(this) this.onUpdateCheckbox = this.onUpdateCheckbox.bind(this) this.onUpdateAssignedMember = this.onUpdateAssignedMember.bind(this) + this.onAssignSelf = this.onAssignSelf.bind(this) this.addFileType = this.addFileType.bind(this) this.removeFileType = this.removeFileType.bind(this) this.updateFileTypesMetadata = this.updateFileTypesMetadata.bind(this) @@ -335,6 +336,22 @@ class ChallengeEditor extends Component { }) } + /** + * Update Assigned Member to Current User + */ + onAssignSelf () { + const { loggedInUser } = this.props + + const assignedMemberDetails = { + handle: loggedInUser.handle, + userId: loggedInUser.userId + } + + this.setState({ + assignedMemberDetails + }) + } + /** * Update Single Select * @param option The select option @@ -1326,6 +1343,7 @@ class ChallengeEditor extends Component { challenge={challenge} onChange={this.onUpdateAssignedMember} assignedMemberDetails={assignedMemberDetails} + onAssignSelf={this.onAssignSelf} /> )} @@ -1469,7 +1487,8 @@ ChallengeEditor.propTypes = { updateChallengeDetails: PropTypes.func.isRequired, createChallenge: PropTypes.func, replaceResourceInRole: PropTypes.func, - partiallyUpdateChallengeDetails: PropTypes.func.isRequired + partiallyUpdateChallengeDetails: PropTypes.func.isRequired, + loggedInUser: PropTypes.shape().isRequired } export default withRouter(ChallengeEditor) diff --git a/src/containers/ChallengeEditor/index.js b/src/containers/ChallengeEditor/index.js index ab0c4796..a7bba88a 100644 --- a/src/containers/ChallengeEditor/index.js +++ b/src/containers/ChallengeEditor/index.js @@ -229,7 +229,8 @@ class ChallengeEditor extends Component { updateChallengeDetails, partiallyUpdateChallengeDetails, createChallenge, - replaceResourceInRole + replaceResourceInRole, + loggedInUser // members } = this.props const { @@ -335,6 +336,7 @@ class ChallengeEditor extends Component { updateChallengeDetails={updateChallengeDetails} replaceResourceInRole={replaceResourceInRole} partiallyUpdateChallengeDetails={partiallyUpdateChallengeDetails} + loggedInUser={loggedInUser} /> )) } /> From b9ae916114b39811d9b51ca492fdb310da70db3d Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Sat, 9 Jan 2021 18:42:23 +0530 Subject: [PATCH 06/32] add Iterative Reviewer for First2Finish and Task type challenges --- src/components/ChallengeEditor/index.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/ChallengeEditor/index.js b/src/components/ChallengeEditor/index.js index 935b65ad..4ba5274c 100644 --- a/src/components/ChallengeEditor/index.js +++ b/src/components/ChallengeEditor/index.js @@ -955,7 +955,7 @@ class ChallengeEditor extends Component { try { const challengeId = this.getCurrentChallengeId() // state can have updated assigned member (in cases where user changes assignments without refreshing the page) - const { challenge: { copilot, reviewer }, assignedMemberDetails: assignedMember } = this.state + const { challenge: { copilot, reviewer, type }, assignedMemberDetails: assignedMember } = this.state const oldMemberHandle = _.get(oldAssignedMember, 'handle') const assignedMemberHandle = _.get(assignedMember, 'handle') // assigned member has been updated @@ -965,8 +965,11 @@ class ChallengeEditor extends Component { const action = await updateChallengeDetails(challengeId, challenge) const { copilot: previousCopilot, reviewer: previousReviewer } = this.state.draftChallenge.data if (copilot !== previousCopilot) await this.updateResource(challengeId, 'Copilot', copilot, previousCopilot) - if (reviewer !== previousReviewer) await this.updateResource(challengeId, 'Reviewer', reviewer, previousReviewer) - + if (type === 'First2Finish' || type === 'Task') { + await this.updateResource(challengeId, 'Iterative Reviewer', reviewer) + } else { + if (reviewer !== previousReviewer) await this.updateResource(challengeId, 'Reviewer', reviewer, previousReviewer) + } const draftChallenge = { data: action.challengeDetails } draftChallenge.data.copilot = copilot draftChallenge.data.reviewer = reviewer From ba34ea00c313be505760448d4c50c49757d55d22 Mon Sep 17 00:00:00 2001 From: mark-nakachon Date: Sat, 9 Jan 2021 19:34:23 +0530 Subject: [PATCH 07/32] fixed-#927 --- src/actions/challenges.js | 24 ++++++++ .../ChallengeEditor.module.scss | 8 ++- src/components/ChallengeEditor/index.js | 45 +++++++++++++- .../ChallengeCard/ChallengeCard.module.scss | 25 ++++++++ .../ChallengeCard/index.js | 59 ++++++++++++++++--- .../ChallengeList/index.js | 7 ++- src/components/ChallengesComponent/index.js | 7 ++- src/config/constants.js | 4 ++ src/containers/ChallengeEditor/index.js | 7 ++- src/containers/Challenges/index.js | 12 ++-- src/reducers/challenges.js | 27 ++++++++- src/services/challenges.js | 8 +++ 12 files changed, 212 insertions(+), 21 deletions(-) diff --git a/src/actions/challenges.js b/src/actions/challenges.js index eca362f6..5817bac8 100644 --- a/src/actions/challenges.js +++ b/src/actions/challenges.js @@ -15,6 +15,7 @@ import { fetchChallengeTracks, updateChallenge, patchChallenge, + deleteChallenge as deleteChallengeAPI, createChallenge as createChallengeAPI, createResource as createResourceAPI, deleteResource as deleteResourceAPI @@ -38,6 +39,9 @@ import { CREATE_CHALLENGE_PENDING, CREATE_CHALLENGE_SUCCESS, CREATE_CHALLENGE_FAILURE, + DELETE_CHALLENGE_PENDING, + DELETE_CHALLENGE_SUCCESS, + DELETE_CHALLENGE_FAILURE, LOAD_CHALLENGE_RESOURCES } from '../config/constants' import { loadProject } from './projects' @@ -267,6 +271,26 @@ export function partiallyUpdateChallengeDetails (challengeId, partialChallengeDe } } +export function deleteChallenge (challengeId) { + return async (dispatch) => { + dispatch({ + type: DELETE_CHALLENGE_PENDING + }) + + return deleteChallengeAPI(challengeId).then((challenge) => { + return dispatch({ + type: DELETE_CHALLENGE_SUCCESS, + challengeDetails: challenge + }) + }).catch((error) => { + dispatch({ + type: DELETE_CHALLENGE_FAILURE + }) + throw error + }) + } +} + export function loadTimelineTemplates () { return async (dispatch) => { const timelineTemplates = await fetchTimelineTemplates() diff --git a/src/components/ChallengeEditor/ChallengeEditor.module.scss b/src/components/ChallengeEditor/ChallengeEditor.module.scss index 4d7f5b24..cac38dd5 100644 --- a/src/components/ChallengeEditor/ChallengeEditor.module.scss +++ b/src/components/ChallengeEditor/ChallengeEditor.module.scss @@ -241,7 +241,7 @@ .actionButtons { position: absolute; top: 30px; - a { + a,button { height: 40px; } } @@ -251,7 +251,13 @@ } .actionButtonsRight { + display: flex; + align-items: center; right: 20px; + + button { + margin-right: 20px; + } } .buttonContainer { diff --git a/src/components/ChallengeEditor/index.js b/src/components/ChallengeEditor/index.js index 935b65ad..941b2e66 100644 --- a/src/components/ChallengeEditor/index.js +++ b/src/components/ChallengeEditor/index.js @@ -68,6 +68,7 @@ class ChallengeEditor extends Component { super(props) this.state = { isLaunch: false, + isDeleteLaunch: false, isConfirm: false, isClose: false, isOpenAdvanceSettings: false, @@ -121,6 +122,8 @@ class ChallengeEditor extends Component { this.getAvailableTimelineTemplates = this.getAvailableTimelineTemplates.bind(this) this.autoUpdateChallengeThrottled = _.throttle(this.validateAndAutoUpdateChallenge.bind(this), 3000) // 3s this.updateResource = this.updateResource.bind(this) + this.onDeleteChallenge = this.onDeleteChallenge.bind(this) + this.deleteModalLaunch = this.deleteModalLaunch.bind(this) } componentDidMount () { @@ -131,6 +134,29 @@ class ChallengeEditor extends Component { this.resetChallengeData(this.setState.bind(this)) } + deleteModalLaunch () { + if (!this.state.isDeleteLaunch) { + this.setState({ isDeleteLaunch: true }) + } + } + + async onDeleteChallenge () { + const { deleteChallenge, challengeDetails, history } = this.props + console.log(challengeDetails, deleteChallenge) + try { + this.setState({ isSaving: true }) + // Call action to delete the challenge + await deleteChallenge(challengeDetails.id) + this.setState({ isSaving: false }) + this.resetModal() + history.push(`/projects/${challengeDetails.projectId}/challenges`) + } catch (e) { + const error = _.get(e, 'response.data.message', 'Unable to Delete the challenge') + console.log(error) + this.setState({ isSaving: false, error }) + } + } + /** * Validates challenge and if its valid calling an autosave method * @@ -206,7 +232,7 @@ class ChallengeEditor extends Component { } resetModal () { - this.setState({ isLoading: false, isConfirm: false, isLaunch: false, error: null, isCloseTask: false }) + this.setState({ isLoading: false, isConfirm: false, isLaunch: false, error: null, isCloseTask: false, isDeleteLaunch: false }) } /** @@ -1373,6 +1399,19 @@ class ChallengeEditor extends Component { />
} + { + this.state.isDeleteLaunch && !this.state.isConfirm && ( + + ) + } { showTimeline && (
{getTitle(isNew)}
+ {this.props.challengeDetails.status === 'New' && }
* Required
@@ -1469,7 +1509,8 @@ ChallengeEditor.propTypes = { updateChallengeDetails: PropTypes.func.isRequired, createChallenge: PropTypes.func, replaceResourceInRole: PropTypes.func, - partiallyUpdateChallengeDetails: PropTypes.func.isRequired + partiallyUpdateChallengeDetails: PropTypes.func.isRequired, + deleteChallenge: PropTypes.func.isRequired } export default withRouter(ChallengeEditor) diff --git a/src/components/ChallengesComponent/ChallengeCard/ChallengeCard.module.scss b/src/components/ChallengesComponent/ChallengeCard/ChallengeCard.module.scss index bac62146..89614ec3 100644 --- a/src/components/ChallengesComponent/ChallengeCard/ChallengeCard.module.scss +++ b/src/components/ChallengesComponent/ChallengeCard/ChallengeCard.module.scss @@ -257,6 +257,31 @@ } } +.deleteButton { + height: 22px; + width: 86px; + border-radius: 11.5px; + display: flex; + justify-content: center; + align-items: center; + background-color: $tc-red; + border-color: $tc-red; + cursor: pointer; + + span { + @include roboto; + + font-size: 14px; + font-weight: 400; + line-height: 17px; + color: $white; + text-transform: capitalize; + display: flex; + justify-content: center; + align-items: center; + } +} + .icon { vertical-align: bottom; } diff --git a/src/components/ChallengesComponent/ChallengeCard/index.js b/src/components/ChallengesComponent/ChallengeCard/index.js index 9da8123f..85270886 100644 --- a/src/components/ChallengesComponent/ChallengeCard/index.js +++ b/src/components/ChallengesComponent/ChallengeCard/index.js @@ -96,14 +96,20 @@ const getPhaseInfo = (c) => { * @param onUpdateLaunch * @returns {*} */ -const hoverComponents = (challenge, onUpdateLaunch) => { +const hoverComponents = (challenge, onUpdateLaunch, deleteModalLaunch) => { const communityAppUrl = `${COMMUNITY_APP_URL}/challenges/${challenge.id}` const directUrl = `${DIRECT_PROJECT_URL}/contest/detail?projectId=${challenge.legacyId}` const orUrl = `${ONLINE_REVIEW_URL}/review/actions/ViewProjectDetails?pid=${challenge.legacyId}` // NEW projects never have Legacy challenge created, so don't show links and "Activate" button for them at all if (challenge.status.toUpperCase() === CHALLENGE_STATUS.NEW) { - return null + if (challenge.status.toUpperCase() === CHALLENGE_STATUS.NEW) { + return ( + + ) + } } return challenge.legacyId ? ( @@ -177,10 +183,13 @@ class ChallengeCard extends React.Component { this.state = { isConfirm: false, isLaunch: false, + isDeleteLaunch: false, isSaving: false } this.onUpdateConfirm = this.onUpdateConfirm.bind(this) this.onUpdateLaunch = this.onUpdateLaunch.bind(this) + this.onDeleteChallenge = this.onDeleteChallenge.bind(this) + this.deleteModalLaunch = this.deleteModalLaunch.bind(this) this.resetModal = this.resetModal.bind(this) this.onLaunchChallenge = this.onLaunchChallenge.bind(this) } @@ -195,8 +204,14 @@ class ChallengeCard extends React.Component { } } + deleteModalLaunch () { + if (!this.state.isDeleteLaunch) { + this.setState({ isDeleteLaunch: true }) + } + } + resetModal () { - this.setState({ isConfirm: false, isLaunch: false }) + this.setState({ isConfirm: false, isLaunch: false, isDeleteLaunch: false }) } async onLaunchChallenge () { @@ -216,12 +231,39 @@ class ChallengeCard extends React.Component { } } + async onDeleteChallenge () { + const { deleteChallenge, challenge } = this.props + try { + this.setState({ isSaving: true }) + // Call action to delete the challenge + await deleteChallenge(challenge.id) + this.setState({ isSaving: false }) + this.resetModal() + } catch (e) { + const error = _.get(e, 'response.data.message', 'Unable to Delete the challenge') + this.setState({ isSaving: false, error }) + } + } + render () { - const { isLaunch, isConfirm, isSaving } = this.state + const { isLaunch, isConfirm, isSaving, isDeleteLaunch } = this.state const { challenge, shouldShowCurrentPhase, reloadChallengeList } = this.props const { phaseMessage, endTime } = getPhaseInfo(challenge) return (
+ { + isDeleteLaunch && !isConfirm && ( + + ) + } { isLaunch && !isConfirm && ( {endTime} )}
- {hoverComponents(challenge, this.onUpdateLaunch, this.props.showError)} + {hoverComponents(challenge, this.onUpdateLaunch, this.deleteModalLaunch)}
@@ -282,16 +324,17 @@ class ChallengeCard extends React.Component { ChallengeCard.defaultPrps = { shouldShowCurrentPhase: true, - showError: () => {}, + // showError: () => {}, reloadChallengeList: () => {} } ChallengeCard.propTypes = { challenge: PropTypes.object, shouldShowCurrentPhase: PropTypes.bool, - showError: PropTypes.func, + // showError: PropTypes.func, reloadChallengeList: PropTypes.func, - partiallyUpdateChallengeDetails: PropTypes.func.isRequired + partiallyUpdateChallengeDetails: PropTypes.func.isRequired, + deleteChallenge: PropTypes.func.isRequired } export default withRouter(ChallengeCard) diff --git a/src/components/ChallengesComponent/ChallengeList/index.js b/src/components/ChallengesComponent/ChallengeList/index.js index fe33891d..08a70266 100644 --- a/src/components/ChallengesComponent/ChallengeList/index.js +++ b/src/components/ChallengesComponent/ChallengeList/index.js @@ -102,7 +102,8 @@ class ChallengeList extends Component { page, perPage, totalChallenges, - partiallyUpdateChallengeDetails + partiallyUpdateChallengeDetails, + deleteChallenge } = this.props if (warnMessage) { return @@ -214,6 +215,7 @@ class ChallengeList extends Component { showError={this.showError} reloadChallengeList={this.reloadChallengeList} partiallyUpdateChallengeDetails={partiallyUpdateChallengeDetails} + deleteChallenge={deleteChallenge} /> ) @@ -256,7 +258,8 @@ ChallengeList.propTypes = { page: PropTypes.number.isRequired, perPage: PropTypes.number.isRequired, totalChallenges: PropTypes.number.isRequired, - partiallyUpdateChallengeDetails: PropTypes.func.isRequired + partiallyUpdateChallengeDetails: PropTypes.func.isRequired, + deleteChallenge: PropTypes.func.isRequired } export default ChallengeList diff --git a/src/components/ChallengesComponent/index.js b/src/components/ChallengesComponent/index.js index c173492f..eb386d49 100644 --- a/src/components/ChallengesComponent/index.js +++ b/src/components/ChallengesComponent/index.js @@ -26,7 +26,8 @@ const ChallengesComponent = ({ page, perPage, totalChallenges, - partiallyUpdateChallengeDetails + partiallyUpdateChallengeDetails, + deleteChallenge }) => { return ( @@ -86,6 +87,7 @@ const ChallengesComponent = ({ perPage={perPage} totalChallenges={totalChallenges} partiallyUpdateChallengeDetails={partiallyUpdateChallengeDetails} + deleteChallenge={deleteChallenge} /> )}
@@ -109,7 +111,8 @@ ChallengesComponent.propTypes = { page: PropTypes.number.isRequired, perPage: PropTypes.number.isRequired, totalChallenges: PropTypes.number.isRequired, - partiallyUpdateChallengeDetails: PropTypes.func.isRequired + partiallyUpdateChallengeDetails: PropTypes.func.isRequired, + deleteChallenge: PropTypes.func.isRequired } ChallengesComponent.defaultProps = { diff --git a/src/config/constants.js b/src/config/constants.js index d1e4aef0..2b26b0f9 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -48,6 +48,10 @@ export const CREATE_CHALLENGE_SUCCESS = 'CREATE_CHALLENGE_SUCCESS' export const CREATE_CHALLENGE_PENDING = 'CREATE_CHALLENGE_PENDING' export const CREATE_CHALLENGE_FAILURE = 'CREATE_CHALLENGE_FAILURE' +export const DELETE_CHALLENGE_SUCCESS = 'DELETE_CHALLENGE_SUCCESS' +export const DELETE_CHALLENGE_PENDING = 'DELETE_CHALLENGE_PENDING' +export const DELETE_CHALLENGE_FAILURE = 'DELETE_CHALLENGE_FAILURE' + export const LOAD_PROJECT_DETAILS = 'LOAD_PROJECT_DETAILS' export const LOAD_PROJECT_DETAILS_SUCCESS = 'LOAD_PROJECT_DETAILS_SUCCESS' export const LOAD_PROJECT_DETAILS_PENDING = 'LOAD_PROJECT_DETAILS_PENDING' diff --git a/src/containers/ChallengeEditor/index.js b/src/containers/ChallengeEditor/index.js index ab0c4796..e7c96db2 100644 --- a/src/containers/ChallengeEditor/index.js +++ b/src/containers/ChallengeEditor/index.js @@ -24,6 +24,7 @@ import { loadResourceRoles, updateChallengeDetails, partiallyUpdateChallengeDetails, + deleteChallenge, createChallenge, replaceResourceInRole } from '../../actions/challenges' @@ -229,7 +230,8 @@ class ChallengeEditor extends Component { updateChallengeDetails, partiallyUpdateChallengeDetails, createChallenge, - replaceResourceInRole + replaceResourceInRole, + deleteChallenge // members } = this.props const { @@ -335,6 +337,7 @@ class ChallengeEditor extends Component { updateChallengeDetails={updateChallengeDetails} replaceResourceInRole={replaceResourceInRole} partiallyUpdateChallengeDetails={partiallyUpdateChallengeDetails} + deleteChallenge={deleteChallenge} /> )) } /> @@ -400,6 +403,7 @@ ChallengeEditor.propTypes = { updateChallengeDetails: PropTypes.func.isRequired, partiallyUpdateChallengeDetails: PropTypes.func.isRequired, createChallenge: PropTypes.func.isRequired, + deleteChallenge: PropTypes.func.isRequired, replaceResourceInRole: PropTypes.func // members: PropTypes.arrayOf(PropTypes.shape()) } @@ -435,6 +439,7 @@ const mapDispatchToProps = { loadResourceRoles, updateChallengeDetails, partiallyUpdateChallengeDetails, + deleteChallenge, createChallenge, replaceResourceInRole } diff --git a/src/containers/Challenges/index.js b/src/containers/Challenges/index.js index e0b9ef5d..7632bb3f 100644 --- a/src/containers/Challenges/index.js +++ b/src/containers/Challenges/index.js @@ -10,7 +10,7 @@ import { DebounceInput } from 'react-debounce-input' import ChallengesComponent from '../../components/ChallengesComponent' import ProjectCard from '../../components/ProjectCard' import Loader from '../../components/Loader' -import { loadChallengesByPage, partiallyUpdateChallengeDetails } from '../../actions/challenges' +import { loadChallengesByPage, partiallyUpdateChallengeDetails, deleteChallenge } from '../../actions/challenges' import { loadProject } from '../../actions/projects' import { loadProjects, setActiveProject, resetSidebarActiveParams } from '../../actions/sidebar' import { @@ -86,7 +86,8 @@ class Challenges extends Component { perPage, totalChallenges, setActiveProject, - partiallyUpdateChallengeDetails + partiallyUpdateChallengeDetails, + deleteChallenge } = this.props const { searchProjectName, onlyMyProjects } = this.state const projectInfo = _.find(projects, { id: activeProjectId }) || {} @@ -147,6 +148,7 @@ class Challenges extends Component { perPage={perPage} totalChallenges={totalChallenges} partiallyUpdateChallengeDetails={partiallyUpdateChallengeDetails} + deleteChallenge={deleteChallenge} /> } @@ -173,7 +175,8 @@ Challenges.propTypes = { totalChallenges: PropTypes.number.isRequired, loadProjects: PropTypes.func.isRequired, setActiveProject: PropTypes.func.isRequired, - partiallyUpdateChallengeDetails: PropTypes.func.isRequired + partiallyUpdateChallengeDetails: PropTypes.func.isRequired, + deleteChallenge: PropTypes.func.isRequired } const mapStateToProps = ({ challenges, sidebar, projects }) => ({ @@ -191,7 +194,8 @@ const mapDispatchToProps = { loadProject, loadProjects, setActiveProject, - partiallyUpdateChallengeDetails + partiallyUpdateChallengeDetails, + deleteChallenge } export default connect(mapStateToProps, mapDispatchToProps)(Challenges) diff --git a/src/reducers/challenges.js b/src/reducers/challenges.js index df398975..20698d89 100644 --- a/src/reducers/challenges.js +++ b/src/reducers/challenges.js @@ -27,7 +27,10 @@ import { CREATE_CHALLENGE_RESOURCE_SUCCESS, DELETE_CHALLENGE_RESOURCE_SUCCESS, DELETE_CHALLENGE_RESOURCE_FAILURE, - CREATE_CHALLENGE_RESOURCE_FAILURE + CREATE_CHALLENGE_RESOURCE_FAILURE, + DELETE_CHALLENGE_SUCCESS, + DELETE_CHALLENGE_FAILURE, + DELETE_CHALLENGE_PENDING } from '../config/constants' const initialState = { @@ -42,6 +45,7 @@ const initialState = { attachments: [], challenge: null, filterChallengeName: '', + failedToDelete: false, status: '', perPage: 0, page: 1, @@ -144,6 +148,27 @@ export default function (state = initialState, action) { } case UPDATE_CHALLENGE_DETAILS_FAILURE: return { ...state, isLoading: false, attachments: [], challenge: null, failedToLoad: false, failedToUpdate: true } + + case DELETE_CHALLENGE_PENDING: + return { ...state, failedToLoad: false } + + case DELETE_CHALLENGE_SUCCESS: { + const deletedChallengeDetails = action.challengeDetails.data + const updatedChallenges = state.challenges.filter((challenge) => challenge.id !== deletedChallengeDetails.id) + toastrSuccess('Success', `Challenge deleted successfully.`) + return { + ...state, + challenges: updatedChallenges + } + } + + case DELETE_CHALLENGE_FAILURE: { + return { + ...state, + failedToDelete: true + } + } + case CREATE_CHALLENGE_SUCCESS: { // if we are showing the list of challenges with the same status as we just created, // then add the new challenge to the beginning of the current challenge list diff --git a/src/services/challenges.js b/src/services/challenges.js index b6a57c98..2b73f9f0 100644 --- a/src/services/challenges.js +++ b/src/services/challenges.js @@ -160,6 +160,14 @@ export function patchChallenge (challengeId, params) { }) } +/* +* Deletes the challenge with the provided id. +* @param challengeId +*/ +export function deleteChallenge (challengeId) { + return axiosInstance.delete(`${CHALLENGE_API_URL}/${challengeId}`).then(rs => rs) +} + /** * Api request for fetching challenge terms * @returns {Promise<*>} From 2b5f8c2d337df4b279db8545b8747523a687534e Mon Sep 17 00:00:00 2001 From: Nowshad Date: Sat, 9 Jan 2021 20:07:02 +0600 Subject: [PATCH 08/32] Fixed issue #1007 --- .../ChallengeEditor/ReviewType-Field/index.js | 5 +++-- src/components/ChallengeEditor/index.js | 10 +++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/components/ChallengeEditor/ReviewType-Field/index.js b/src/components/ChallengeEditor/ReviewType-Field/index.js index 139ea0a3..01fffd30 100644 --- a/src/components/ChallengeEditor/ReviewType-Field/index.js +++ b/src/components/ChallengeEditor/ReviewType-Field/index.js @@ -9,10 +9,11 @@ import { DES_TRACK_ID, REVIEW_TYPES, MESSAGE, QA_TRACK_ID } from '../../../confi const ReviewTypeField = ({ reviewers, challenge, onUpdateOthers, onUpdateSelect }) => { const isDesignChallenge = challenge.trackId === DES_TRACK_ID const isQAChallenge = challenge.trackId === QA_TRACK_ID + const isTask = challenge.type === 'Task' const defaultReviewType = isDesignChallenge ? REVIEW_TYPES.INTERNAL : REVIEW_TYPES.COMMUNITY const reviewType = challenge.reviewType ? challenge.reviewType.toUpperCase() : defaultReviewType const isCommunity = reviewType === REVIEW_TYPES.COMMUNITY - const isInternal = reviewType === REVIEW_TYPES.INTERNAL + const isInternal = reviewType === REVIEW_TYPES.INTERNAL || isTask const communityOption = (disabled) => (
} - { !isDesignChallenge && + { !isDesignChallenge && !isTask && communityOption() }
diff --git a/src/components/ChallengeEditor/index.js b/src/components/ChallengeEditor/index.js index 935b65ad..56165486 100644 --- a/src/components/ChallengeEditor/index.js +++ b/src/components/ChallengeEditor/index.js @@ -63,6 +63,8 @@ const getTitle = (isNew) => { return 'Set-Up Work' } +const checkTaskType = (type) => type === 'Task' + class ChallengeEditor extends Component { constructor (props) { super(props) @@ -167,7 +169,8 @@ class ChallengeEditor extends Component { reviewer = challenge.reviewer } challengeData.copilot = copilot || copilotFromResources - challengeData.reviewer = reviewer || reviewerFromResources + challengeData.reviewer = reviewer || reviewerFromResources || (checkTaskType(challengeData.type) ? challengeData.createdBy : '') + challengeData.startDate = (checkTaskType(challengeData.type) ? moment().format() : challengeData.startDate) const challengeDetail = { ...challengeData } const isOpenAdvanceSettings = challengeDetail.groups.length > 0 setState({ @@ -1100,6 +1103,7 @@ class ChallengeEditor extends Component { if (_.isEmpty(challenge)) { return
Error loading challenge
} + const isTaskType = checkTaskType(challenge.type) const isTask = _.get(challenge, 'task.isTask', false) const { assignedMemberDetails, error } = this.state let isActive = false @@ -1357,7 +1361,7 @@ class ChallengeEditor extends Component { )} - { + {!isTaskType && (
- } + )} { showTimeline && ( Date: Sat, 9 Jan 2021 22:06:57 +0530 Subject: [PATCH 09/32] groups filter from api --- package-lock.json | 166 ++++++++++++++++-- package.json | 2 +- .../ChallengeEditor/Groups-Field/index.js | 42 +++-- src/components/ChallengeEditor/index.js | 2 +- src/components/Select/AsyncSelect.js | 71 ++++++++ src/components/Select/Select.module.scss | 118 ------------- src/components/Select/index.js | 61 ++++++- src/config/constants.js | 3 +- 8 files changed, 315 insertions(+), 150 deletions(-) create mode 100644 src/components/Select/AsyncSelect.js delete mode 100644 src/components/Select/Select.module.scss diff --git a/package-lock.json b/package-lock.json index b0c37594..57be0c21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1018,6 +1018,87 @@ "resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz", "integrity": "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==" }, + "@emotion/cache": { + "version": "10.0.29", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.29.tgz", + "integrity": "sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ==", + "requires": { + "@emotion/sheet": "0.9.4", + "@emotion/stylis": "0.8.5", + "@emotion/utils": "0.11.3", + "@emotion/weak-memoize": "0.2.5" + } + }, + "@emotion/core": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@emotion/core/-/core-10.1.1.tgz", + "integrity": "sha512-ZMLG6qpXR8x031NXD8HJqugy/AZSkAuMxxqB46pmAR7ze47MhNJ56cdoX243QPZdGctrdfo+s08yZTiwaUcRKA==", + "requires": { + "@babel/runtime": "^7.5.5", + "@emotion/cache": "^10.0.27", + "@emotion/css": "^10.0.27", + "@emotion/serialize": "^0.11.15", + "@emotion/sheet": "0.9.4", + "@emotion/utils": "0.11.3" + } + }, + "@emotion/css": { + "version": "10.0.27", + "resolved": "https://registry.npmjs.org/@emotion/css/-/css-10.0.27.tgz", + "integrity": "sha512-6wZjsvYeBhyZQYNrGoR5yPMYbMBNEnanDrqmsqS1mzDm1cOTu12shvl2j4QHNS36UaTE0USIJawCH9C8oW34Zw==", + "requires": { + "@emotion/serialize": "^0.11.15", + "@emotion/utils": "0.11.3", + "babel-plugin-emotion": "^10.0.27" + } + }, + "@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" + }, + "@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==" + }, + "@emotion/serialize": { + "version": "0.11.16", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.11.16.tgz", + "integrity": "sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg==", + "requires": { + "@emotion/hash": "0.8.0", + "@emotion/memoize": "0.7.4", + "@emotion/unitless": "0.7.5", + "@emotion/utils": "0.11.3", + "csstype": "^2.5.7" + } + }, + "@emotion/sheet": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-0.9.4.tgz", + "integrity": "sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA==" + }, + "@emotion/stylis": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", + "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" + }, + "@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + }, + "@emotion/utils": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.3.tgz", + "integrity": "sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==" + }, + "@emotion/weak-memoize": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz", + "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==" + }, "@fortawesome/fontawesome-common-types": { "version": "0.2.28", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.28.tgz", @@ -2157,6 +2238,23 @@ "object.assign": "^4.1.0" } }, + "babel-plugin-emotion": { + "version": "10.0.33", + "resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-10.0.33.tgz", + "integrity": "sha512-bxZbTTGz0AJQDHm8k6Rf3RQJ8tX2scsfsRyKVgAbiUPUNIRtlK+7JxP+TAd1kRLABFxe0CFm2VdK4ePkoA9FxQ==", + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@emotion/hash": "0.8.0", + "@emotion/memoize": "0.7.4", + "@emotion/serialize": "^0.11.16", + "babel-plugin-macros": "^2.0.0", + "babel-plugin-syntax-jsx": "^6.18.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^1.0.5", + "find-root": "^1.1.0", + "source-map": "^0.5.7" + } + }, "babel-plugin-istanbul": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz", @@ -2187,6 +2285,11 @@ "resolved": "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.6.tgz", "integrity": "sha512-1aGDUfL1qOOIoqk9QKGIo2lANk+C7ko/fqH0uIyC71x3PEGz0uVP8ISgfEsFuG+FKmjHTvFK/nNM8dowpmUxLA==" }, + "babel-plugin-syntax-jsx": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", + "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=" + }, "babel-plugin-syntax-object-rest-spread": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", @@ -4165,6 +4268,11 @@ } } }, + "csstype": { + "version": "2.6.14", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.14.tgz", + "integrity": "sha512-2mSc+VEpGPblzAxyeR+vZhJKgYg0Og0nnRi7pmRXFYYxSfnOnW8A5wwQb4n4cE2nIOzqKOAzLCaEX6aBmNEv8A==" + }, "currently-unhandled": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", @@ -4542,6 +4650,22 @@ "utila": "~0.4" } }, + "dom-helpers": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.0.tgz", + "integrity": "sha512-Ru5o9+V8CpunKnz5LGgWXkmrH/20cGKwcHwS4m73zIvs54CN9epEmT/HLqFJW3kXpakAFkEdzgy1hzlJe3E4OQ==", + "requires": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + }, + "dependencies": { + "csstype": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.6.tgz", + "integrity": "sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw==" + } + } + }, "dom-serializer": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", @@ -5923,8 +6047,7 @@ "find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", - "dev": true + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" }, "find-up": { "version": "2.1.0", @@ -9342,6 +9465,11 @@ } } }, + "memoize-one": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz", + "integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==" + }, "memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", @@ -14723,13 +14851,18 @@ } }, "react-select": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/react-select/-/react-select-1.3.0.tgz", - "integrity": "sha512-g/QAU1HZrzSfxkwMAo/wzi6/ezdWye302RGZevsATec07hI/iSxcpB1hejFIp7V63DJ8mwuign6KmB3VjdlinQ==", - "requires": { - "classnames": "^2.2.4", - "prop-types": "^15.5.8", - "react-input-autosize": "^2.1.2" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-3.1.1.tgz", + "integrity": "sha512-HjC6jT2BhUxbIbxMZWqVcDibrEpdUJCfGicN0MMV+BQyKtCaPTgFekKWiOizSCy4jdsLMGjLqcFGJMhVGWB0Dg==", + "requires": { + "@babel/runtime": "^7.4.4", + "@emotion/cache": "^10.0.9", + "@emotion/core": "^10.0.9", + "@emotion/css": "^10.0.9", + "memoize-one": "^5.0.0", + "prop-types": "^15.6.0", + "react-input-autosize": "^2.2.2", + "react-transition-group": "^4.3.0" } }, "react-side-effect": { @@ -14768,6 +14901,17 @@ "prop-types": "^15.5.0" } }, + "react-transition-group": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz", + "integrity": "sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==", + "requires": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + } + }, "read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", @@ -17127,8 +17271,8 @@ } }, "tc-auth-lib": { - "version": "github:topcoder-platform/tc-auth-lib#fbd62f7c65f0e7eecccf2c131b07e84104505754", - "from": "github:topcoder-platform/tc-auth-lib#1.0.1", + "version": "github:topcoder-platform/tc-auth-lib#68fdc22464810c51b703a33e529cdbd6d09437de", + "from": "github:topcoder-platform/tc-auth-lib#1.0.4", "requires": { "lodash": "^4.17.19" }, diff --git a/package.json b/package.json index 1f0c041e..a4b659bd 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "react-redux": "^6.0.0", "react-redux-toastr": "^7.5.1", "react-router-dom": "^4.3.1", - "react-select": "^1.2.0", + "react-select": "^3.1.1", "react-stickynode": "^2.1.1", "react-svg": "^4.1.1", "react-tabs": "^3.0.0", diff --git a/src/components/ChallengeEditor/Groups-Field/index.js b/src/components/ChallengeEditor/Groups-Field/index.js index f9624450..4e4f3264 100644 --- a/src/components/ChallengeEditor/Groups-Field/index.js +++ b/src/components/ChallengeEditor/Groups-Field/index.js @@ -1,20 +1,47 @@ import React from 'react' import PropTypes from 'prop-types' -import Select from '../../Select' +import AsyncSelect from '../../Select/AsyncSelect' import cn from 'classnames' import styles from './Groups-Field.module.scss' +import _ from 'lodash' +import { axiosInstance } from '../../../services/axiosWithAuth' +import { AUTOCOMPLETE_MIN_LENGTH, AUTOCOMPLETE_DEBOUNCE_TIME_MS, GROUPS_API_URL } from '../../../config/constants' + +const GroupsField = ({ onUpdateMultiSelect, challenge }) => { + async function fetchGroups (name) { + if (!name) return [] + console.log('url') + console.log(GROUPS_API_URL) + const url = `${GROUPS_API_URL}?name=${name}` + return axiosInstance.get(url) + } + + const onInputChange = React.useCallback(_.debounce(async (inputValue, callback) => { + if (!inputValue) return + const preparedValue = inputValue.trim() + if (preparedValue.length < AUTOCOMPLETE_MIN_LENGTH) { + return [] + } + const { data } = await fetchGroups(inputValue) + const suggestions = data.map(suggestion => ({ + label: suggestion.name, + value: suggestion.id + })) + callback && callback(suggestions) + }, AUTOCOMPLETE_DEBOUNCE_TIME_MS), []) -const GroupsField = ({ groups, onUpdateMultiSelect, challenge }) => { return (
- ({ label: account.name, value: account.name, name: account.name }))} + value={{ label: challenge.billingAccount, value: challenge.billingAccount }} placeholder='Select an existing account' - labelKey='name' - valueKey='name' - clearable={false} + isClearable={false} onChange={(e) => onUpdateSelect(e)} - disabled={false} + isDisabled={false} />
diff --git a/src/components/ChallengeEditor/ChallengeSchedule-Field/index.js b/src/components/ChallengeEditor/ChallengeSchedule-Field/index.js index d0a4e5c8..8fca8678 100644 --- a/src/components/ChallengeEditor/ChallengeSchedule-Field/index.js +++ b/src/components/ChallengeEditor/ChallengeSchedule-Field/index.js @@ -331,12 +331,10 @@ class ChallengeScheduleField extends Component { ) : ( ({ label: handle, value: handle }))} placeholder='Select Reviewer' - labelKey='handle' - valueKey='handle' - clearable={false} - value={challenge.reviewer} - onChange={(e) => onUpdateSelect(e.handle, false, 'reviewer')} - disabled={false} + value={{ label: challenge.reviewer, value: challenge.reviewer }} + isClearable={false} + onChange={(e) => onUpdateSelect(e.value, false, 'reviewer')} + isDisabled={false} /> ) } diff --git a/src/components/ChallengeEditor/TagsField/index.js b/src/components/ChallengeEditor/TagsField/index.js index 68ff0910..9678cefc 100644 --- a/src/components/ChallengeEditor/TagsField/index.js +++ b/src/components/ChallengeEditor/TagsField/index.js @@ -19,10 +19,10 @@ const TagsField = ({ challengeTags, challenge, onUpdateMultiSelect, readOnly }) {existingTags} ) : ( t.isActive)} - value={challenge.typeId} + options={_.filter(types, t => t.isActive).map(type => ({ label: type.name, value: type.id }))} placeholder='Work Format' - labelKey='name' - valueKey='id' - clearable={false} - onChange={(e) => onUpdateSelect(e.id, false, 'typeId')} - disabled={disabled} + isClearable={false} + onChange={(e) => onUpdateSelect(e.value, false, 'typeId')} + isDisabled={disabled} />
diff --git a/src/components/ChallengeEditor/index.js b/src/components/ChallengeEditor/index.js index c398ccad..3febcf69 100644 --- a/src/components/ChallengeEditor/index.js +++ b/src/components/ChallengeEditor/index.js @@ -708,7 +708,7 @@ class ChallengeEditor extends Component { onUpdateMultiSelect (options, field) { const { challenge } = this.state let newChallenge = { ...challenge } - newChallenge[field] = options ? options.split(',') : [] + newChallenge[field] = options ? options.map(option => option.value) : [] this.setState({ challenge: newChallenge }, () => { this.validateChallenge() diff --git a/src/components/PhaseInput/index.js b/src/components/PhaseInput/index.js index 1e4b4e4a..2a87c6a6 100644 --- a/src/components/PhaseInput/index.js +++ b/src/components/PhaseInput/index.js @@ -77,11 +77,9 @@ class PhaseInput extends Component {
- )} +
)} { - attachments && attachments.length > 0 && ( -
-
-
File Name
-
Size
-
Action
+ _.has(challenge, 'attachments') && challenge.attachments.length > 0 && ( + +
+
+ +
+
+
+
+
File Name
+
Size
+
Action
+
+ { renderAttachments(challenge.attachments) }
- { renderAttachments(attachments) } -
+ ) }
@@ -74,13 +89,11 @@ const AttachmentField = ({ challengeId, attachments, removeAttachment, onUploadF AttachmentField.defaultProps = { removeAttachment: () => {}, onUploadFile: () => {}, - readOnly: false, - attachments: [] + readOnly: false } AttachmentField.propTypes = { - challengeId: PropTypes.string.isRequired, - attachments: PropTypes.array, + challenge: PropTypes.shape().isRequired, removeAttachment: PropTypes.func, onUploadFile: PropTypes.func, token: PropTypes.string.isRequired, diff --git a/src/components/ChallengeEditor/ChallengeView/index.js b/src/components/ChallengeEditor/ChallengeView/index.js index 29dedff7..3dbd0de1 100644 --- a/src/components/ChallengeEditor/ChallengeView/index.js +++ b/src/components/ChallengeEditor/ChallengeView/index.js @@ -27,7 +27,6 @@ import { MESSAGE, REVIEW_TYPES } from '../../../config/constants' const ChallengeView = ({ projectDetail, challenge, - attachments, metadata, challengeResources, token, @@ -210,12 +209,13 @@ const ChallengeView = ({ challenge={challenge} readOnly /> - + { false && ( + + )} @@ -244,7 +244,6 @@ ChallengeView.propTypes = { }).isRequired, projectDetail: PropTypes.object, challenge: PropTypes.object, - attachments: PropTypes.array, metadata: PropTypes.object, token: PropTypes.string, isLoading: PropTypes.bool.isRequired, diff --git a/src/components/ChallengeEditor/index.js b/src/components/ChallengeEditor/index.js index 6b2a8da2..935b65ad 100644 --- a/src/components/ChallengeEditor/index.js +++ b/src/components/ChallengeEditor/index.js @@ -95,6 +95,7 @@ class ChallengeEditor extends Component { this.updateFileTypesMetadata = this.updateFileTypesMetadata.bind(this) this.toggleAdvanceSettings = this.toggleAdvanceSettings.bind(this) this.toggleNdaRequire = this.toggleNdaRequire.bind(this) + this.removeAttachment = this.removeAttachment.bind(this) this.removePhase = this.removePhase.bind(this) this.resetPhase = this.resetPhase.bind(this) this.savePhases = this.savePhases.bind(this) @@ -550,6 +551,15 @@ class ChallengeEditor extends Component { this.setState({ challenge: newChallenge }) } + removeAttachment (file) { + const { challenge } = this.state + const newChallenge = { ...challenge } + const { attachments: oldAttachments } = challenge + const newAttachments = _.remove(oldAttachments, att => att.fileName !== file) + newChallenge.attachments = _.clone(newAttachments) + this.setState({ challenge: newChallenge }) + } + /** * Remove Phase from challenge Phases list * @param index @@ -1085,8 +1095,7 @@ class ChallengeEditor extends Component { token, removeAttachment, failedToLoad, - projectDetail, - attachments + projectDetail } = this.props if (_.isEmpty(challenge)) { return
Error loading challenge
@@ -1392,14 +1401,14 @@ class ChallengeEditor extends Component { onUpdateMultiSelect={this.onUpdateMultiSelect} onUpdateMetadata={this.onUpdateMetadata} /> - + { false && ( + + )} diff --git a/src/components/FilestackFilePicker/FilestackFilePicker.module.scss b/src/components/FilestackFilePicker/FilestackFilePicker.module.scss deleted file mode 100644 index 1a9de2b0..00000000 --- a/src/components/FilestackFilePicker/FilestackFilePicker.module.scss +++ /dev/null @@ -1,60 +0,0 @@ -@import "../../styles/includes"; - -.container { - .file-picker { - display: flex; - flex-direction: column; - align-items: center; - padding: 20px; - border: 1px solid $tc-gray-40; - border-radius: 6px; - height: 227px; - position: relative; - font-size: 16px; - font-weight: 400; - line-height: 19px; - color: $tc-gray-80; - - .icon { - color: $tc-blue-20; - margin-bottom: 30px; - } - - .pseudo-link { - color: $tc-blue-20; - } - } - - .file-picker.error { - border-color: #f22f24; - } - - .file-picker.drag { - background-color: rgba(0, 0, 0, 0.1); - border-color: rgba(0, 0, 0, 0.4); - } - - .uploading-files .file-error { - color: #f22f24; - } - - .error-container { - margin-top: 5px; - padding: 5px 13px; - background: #fff4f4; - border: 1px solid #ffd4d1; - color: #f22f24; - font-size: 13px; - border-radius: 2px; - font-style: italic; - } -} - -.drop-zone-mask { - bottom: 0; - cursor: pointer; - position: absolute; - left: 0; - right: 0; - top: 0; -} diff --git a/src/components/FilestackFilePicker/index.jsx b/src/components/FilestackFilePicker/index.jsx deleted file mode 100644 index e6c872e8..00000000 --- a/src/components/FilestackFilePicker/index.jsx +++ /dev/null @@ -1,283 +0,0 @@ -/** - * FilestackFilePicker Component - * - * Component for uploading files using Filestack Picker and Drag & Drop. - * - Supports multiple file uploading. - */ -import _ from 'lodash' -import React, { useEffect, useLayoutEffect, useRef, useState } from 'react' -import PT from 'prop-types' -import * as filestack from 'filestack-js' -import cn from 'classnames' -import { - FILE_PICKER_API_KEY, - FILE_PICKER_CNAME, - FILE_PICKER_FROM_SOURCES, - FILE_PICKER_REGION, - FILE_PICKER_CONTAINER_NAME, - FILE_PICKER_ACCEPT, - FILE_PICKER_MAX_SIZE, - FILE_PICKER_MAX_FILES, - FILE_PICKER_PROGRESS_INTERVAL -} from '../../config/constants' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons' -import styles from './FilestackFilePicker.module.scss' - -/** - * FilestackFilePicker component - */ -const FilestackFilePicker = ({ - path, - onFileUploadFinished, - onFileUploadFailed, - onUploadDone -}) => { - // the list of filenames which are currently being uploaded - const [uploadingFiles, setUploadingFiles] = useState([]) - // if something is currently dragged over the area - const [dragged, setDragged] = useState(false) - // Filestack client instance - const filestackRef = useRef(null) - // we have to use ref for this method, because filestack would be initialized once with a callback using this method - const updateUploadingFile = useRef() - - // init Filestack (without waiting for rendering) - useLayoutEffect(() => { - filestackRef.current = filestack.init(FILE_PICKER_API_KEY, { - cname: FILE_PICKER_CNAME - }) - }, []) - - // update the ref to `updateUploadingFile` to keep referencing fresh state data - useEffect(() => { - updateUploadingFile.current = (filename, updated) => { - const uploadingFileIndex = _.findIndex(uploadingFiles, { filename }) - - if (uploadingFileIndex > -1) { - const updatedFile = { - ...uploadingFiles[uploadingFileIndex], - ...updated - } - - setUploadingFiles([ - ...uploadingFiles.slice(0, uploadingFileIndex), - updatedFile, - ...uploadingFiles.slice(uploadingFileIndex + 1) - ]) - - return updatedFile - } - } - }, [uploadingFiles, setUploadingFiles]) - - useEffect(() => { - // if all files have been uploaded successfully, clean uploading file list - if (uploadingFiles.length > 0 && _.every(uploadingFiles, 'file')) { - setUploadingFiles([]) - } - - // if all files are fully loaded or error happens for them call `onUploadDone` callback - if ( - uploadingFiles.length > 0 && - _.every(uploadingFiles, (file) => file.file || file.error) - ) { - if (onUploadDone) { - const filesFailed = _.filter(uploadingFiles, 'error') - const filesUploaded = _.filter(uploadingFiles, 'file') - - onUploadDone({ - filesFailed: _.map(filesFailed, 'file'), - filesUploaded: _.map(filesUploaded, 'file') - }) - } - } - }, [uploadingFiles, setUploadingFiles, onUploadDone]) - - /** - * Handle for success file(s) uploading - * - * @param {Object} file upload file info - */ - const handleFileUploadSuccess = (file) => { - console.log('handleFileUploadSuccess', file) - updateUploadingFile.current(file.name, { - file, // set `file` to indicate that file uploaded - progress: 100 // make sure that progress is set to 100 when uploading is complete - }) - onFileUploadFinished && onFileUploadFinished(file) - } - - /** - * Handle for error during file(s) uploading - * - * @param {Object|String} error error during file uploading - */ - const handleFileUploadError = (file) => { - updateUploadingFile.current(file.name, { - file, // set `file` to indicate that file uploaded - progress: 100 // make sure that progress is set to 100 when uploading is complete - }) - onFileUploadFailed && onFileUploadFailed(file) - } - - /** - * Open Filestack picker - */ - const openFilePicker = () => { - filestackRef.current - .picker({ - accept: FILE_PICKER_ACCEPT, - fromSources: FILE_PICKER_FROM_SOURCES, - maxSize: FILE_PICKER_MAX_SIZE, - maxFiles: FILE_PICKER_MAX_FILES, - onUploadStarted: (files) => { - setUploadingFiles( - files.map((file) => ({ - filename: file.filename, - progress: 0, - file: null, - error: null - })) - ) - }, - onFileUploadFailed: handleFileUploadError, - onFileUploadFinished: handleFileUploadSuccess, - onFileUploadProgress: (file, progressInfo) => { - updateUploadingFile.current(file.filename, { - progress: progressInfo.totalPercent - }) - }, - startUploadingWhenMaxFilesReached: true, - storeTo: { - container: FILE_PICKER_CONTAINER_NAME, - path, - region: FILE_PICKER_REGION - } - }) - .open() - } - - /** - * Handle file(s) uploading when dropping them on the area - * - * @param {Event} e event - */ - const handleFileDrop = (e) => { - e.preventDefault() - - setDragged(false) - - const files = Array.from(e.dataTransfer.files).map((file, index) => { - const fileExt = '.' + file.name.split('.').pop() - let error = null - - if (!_.includes(FILE_PICKER_ACCEPT, fileExt)) { - error = `Not allowed file type "${fileExt}".` - } - - if (index + 1 > FILE_PICKER_MAX_FILES) { - error = `File skipped, because can upload maximum ${FILE_PICKER_MAX_FILES} files at once.` - } - - return { - filename: file.name, - progress: 0, - file, - error - } - }) - - const filesToUpload = _.map(_.reject(files, 'error'), 'file') - - setUploadingFiles(files.map((file) => ({ ...file, file: null }))) - - filesToUpload.map((file) => - filestackRef.current - .upload( - file, - { - onProgress: ({ totalPercent }) => { - updateUploadingFile.current(file.name, { - progress: totalPercent - }) - }, - progressInterval: FILE_PICKER_PROGRESS_INTERVAL - }, - { - container: FILE_PICKER_CONTAINER_NAME, - path, - region: FILE_PICKER_REGION - } - ) - .then(handleFileUploadSuccess) - .catch(handleFileUploadError) - ) - } - - const hasErrors = _.some(uploadingFiles, 'error') - - return ( -
-
-
- -
- - {uploadingFiles.length === 0 ? ( - <> -
Drag & Drop files here
-
or
-
- click here to - browse -
- - ) : ( -
- {uploadingFiles.map((uploadingFile) => ( -
- {uploadingFile.filename} ( - {uploadingFile.error ? ( - {uploadingFile.error} - ) : ( - `${uploadingFile.progress}%` - )} - ) -
- ))} -
- )} - -
setDragged(true)} - onDragLeave={() => setDragged(false)} - onDragOver={(e) => e.preventDefault()} - onDrop={handleFileDrop} - role='tab' - tabIndex={0} - aria-label='Select file to upload' - /> -
-
- ) -} - -FilestackFilePicker.defaultProps = {} - -FilestackFilePicker.propTypes = { - path: PT.string.isRequired, - onFileUploadFinished: PT.func, - onFileUploadFailed: PT.func, - onUploadDone: PT.func -} - -export default FilestackFilePicker diff --git a/src/config/constants.js b/src/config/constants.js index 7371e24c..d1e4aef0 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -18,28 +18,6 @@ export const { } = 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 -/** - * Filepicker config - */ -// to be able to start the Connect App we should pass at least the dummy value for `FILE_PICKER_API_KEY` -// 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' -// TODO uncomment this line to use correct `tc-challenge-v5-dev` bucket for DEV -// export const FILE_PICKER_CONTAINER_NAME = prcess.env.FILE_PICKER_CONTAINER_NAME || 'tc-challenge-v5-dev' -export const FILE_PICKER_CONTAINER_NAME = 'submission-staging-dev' -export const FILE_PICKER_REGION = process.env.FILE_PICKER_REGION || 'us-east-1' -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 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 -export const FILE_PICKER_PROGRESS_INTERVAL = 100 -export const SPECIFICATION_ATTACHMENTS_FOLDER = 'SPECIFICATION_ATTACHMENTS' - -// TODO uncomment this line to use the same bucket as we during FileStack uploading -// export const getAWSContainerFileURL = (key) => `https://${FILE_PICKER_CONTAINER_NAME}.s3.amazonaws.com/${key}` -export const getAWSContainerFileURL = (key) => `https://tc-challenge-v5-dev.s3.amazonaws.com/${key}` - // Actions export const LOAD_PROJECTS_SUCCESS = 'LOAD_PROJECTS_SUCCESS' export const LOAD_PROJECTS_PENDING = 'LOAD_PROJECTS_PENDING' @@ -84,13 +62,9 @@ export const LOAD_CHALLENGE_METADATA_SUCCESS = 'LOAD_CHALLENGE_METADATA_SUCCESS' export const SAVE_AUTH_TOKEN = 'SAVE_AUTH_TOKEN' -export const CREATE_ATTACHMENT_PENDING = 'CREATE_ATTACHMENT_PENDING' -export const CREATE_ATTACHMENT_FAILURE = 'CREATE_ATTACHMENT_FAILURE' -export const CREATE_ATTACHMENT_SUCCESS = 'CREATE_ATTACHMENT_SUCCESS' - -export const REMOVE_ATTACHMENT_PENDING = 'REMOVE_ATTACHMENT_PENDING' -export const REMOVE_ATTACHMENT_FAILURE = 'REMOVE_ATTACHMENT_FAILURE' -export const REMOVE_ATTACHMENT_SUCCESS = 'REMOVE_ATTACHMENT_SUCCESS' +export const UPLOAD_ATTACHMENT_PENDING = 'UPLOAD_ATTACHMENT_PENDING' +export const UPLOAD_ATTACHMENT_FAILURE = 'UPLOAD_ATTACHMENT_FAILURE' +export const UPLOAD_ATTACHMENT_SUCCESS = 'UPLOAD_ATTACHMENT_SUCCESS' export const LOAD_CHALLENGE_RESOURCES = 'LOAD_CHALLENGE_RESOURCES' export const LOAD_CHALLENGE_RESOURCES_SUCCESS = 'LOAD_CHALLENGE_RESOURCES_SUCCESS' @@ -107,6 +81,8 @@ export const DELETE_CHALLENGE_RESOURCE_SUCCESS = 'DELETE_CHALLENGE_RESOURCE_SUCC export const DELETE_CHALLENGE_RESOURCE_PENDING = 'DELETE_CHALLENGE_RESOURCE_PENDING' export const DELETE_CHALLENGE_RESOURCE_FAILURE = 'DELETE_CHALLENGE_RESOURCE_FAILURE' +export const REMOVE_ATTACHMENT = 'REMOVE_ATTACHMENT' + export const SET_FILTER_CHALLENGE_VALUE = 'SET_FILTER_CHALLENGE_VALUE' export const RESET_SIDEBAR_ACTIVE_PARAMS = 'RESET_SIDEBAR_ACTIVE_PARAMS' @@ -182,7 +158,7 @@ export const ADMIN_ROLES = [ ] export const downloadAttachmentURL = (challengeId, attachmentId, token) => - `${CHALLENGE_API_URL}/${challengeId}/attachments/${attachmentId}/download?token=${token}` + `${CHALLENGE_API_URL}/${challengeId}/attachments/${attachmentId}?token=${token}` export const PAGE_SIZE = 50 diff --git a/src/containers/ChallengeEditor/index.js b/src/containers/ChallengeEditor/index.js index d5cbe397..ab0c4796 100644 --- a/src/containers/ChallengeEditor/index.js +++ b/src/containers/ChallengeEditor/index.js @@ -348,7 +348,6 @@ class ChallengeEditor extends Component { metadata={metadata} projectDetail={projectDetail} challenge={challengeDetails} - attachments={attachments} challengeResources={challengeResources} token={token} challengeId={challengeId} diff --git a/src/reducers/challenges.js b/src/reducers/challenges.js index 1a1c0ac1..df398975 100644 --- a/src/reducers/challenges.js +++ b/src/reducers/challenges.js @@ -15,12 +15,10 @@ import { LOAD_CHALLENGES_FAILURE, LOAD_CHALLENGES_PENDING, LOAD_CHALLENGES_SUCCESS, - CREATE_ATTACHMENT_FAILURE, - CREATE_ATTACHMENT_SUCCESS, - CREATE_ATTACHMENT_PENDING, - REMOVE_ATTACHMENT_FAILURE, - REMOVE_ATTACHMENT_SUCCESS, - REMOVE_ATTACHMENT_PENDING, + UPLOAD_ATTACHMENT_FAILURE, + UPLOAD_ATTACHMENT_SUCCESS, + UPLOAD_ATTACHMENT_PENDING, + REMOVE_ATTACHMENT, SET_FILTER_CHALLENGE_VALUE, UPDATE_CHALLENGE_DETAILS_FAILURE, UPDATE_CHALLENGE_DETAILS_SUCCESS, @@ -51,6 +49,12 @@ const initialState = { projectId: -1 } +function toastrSuccess (title, message) { + setImmediate(() => { + toastr.success(title, message) + }) +} + function toastrFailure (title, message) { setImmediate(() => { toastr.error(title, message) @@ -58,6 +62,7 @@ function toastrFailure (title, message) { } export default function (state = initialState, action) { + let attachments switch (action.type) { case LOAD_CHALLENGES_SUCCESS: return { @@ -213,68 +218,23 @@ export default function (state = initialState, action) { case LOAD_CHALLENGE_MEMBERS_SUCCESS: { return { ...state, metadata: { ...state.metadata, members: action.members } } } - case CREATE_ATTACHMENT_PENDING: { - const attachments = [ - ...(state.attachments || []), - { - uploadingId: action.uploadingId, - name: action.file.name, - fileSize: action.file.fileSize, - isUploading: true - } - ] - return { ...state, attachments } - } - case CREATE_ATTACHMENT_SUCCESS: { - const attachments = _.map(state.attachments, item => { - if (item.uploadingId !== action.uploadingId) { - return item - } else { - return action.attachment - } - }) - return { ...state, attachments } - } - case CREATE_ATTACHMENT_FAILURE: { - toastrFailure('Upload failure', `Failed to upload ${action.file.name}`) - const attachments = _.reject(state.attachments, { - uploadingId: action.uploadingId - }) - return { ...state, attachments } - } - case REMOVE_ATTACHMENT_PENDING: { - const attachments = _.map(state.attachments, item => { - if (item.id !== action.attachmentId) { - return item - } else { - return { - ...item, - isDeleting: true - } - } - }) - return { ...state, attachments } - } - case REMOVE_ATTACHMENT_SUCCESS: { - const attachments = _.reject(state.attachments, { - id: action.attachmentId - }) - return { ...state, attachments } - } - case REMOVE_ATTACHMENT_FAILURE: { - toastrFailure('Removing failure', `Failed to remove attachment`) - const attachments = _.map(state.attachments, item => { + case UPLOAD_ATTACHMENT_PENDING: + return { ...state, isUploading: true, isSuccess: false, uploadingId: action.challengeId } + case UPLOAD_ATTACHMENT_SUCCESS: + toastrSuccess('Success', `${action.filename} uploaded successfully. Save the challenge to reflect the changes!`) + attachments = _.cloneDeep(state.attachments) + attachments.push(action.attachment) + return { ...state, isUploading: false, isSuccess: true, uploadingId: null, attachments } + case UPLOAD_ATTACHMENT_FAILURE: + toastrFailure('Upload failure', `Failed to upload ${action.filename}`) + return { ...state, isUploading: false, isSuccess: false, uploadingId: null } + case REMOVE_ATTACHMENT: + attachments = _.filter(state.attachments, item => { if (item.id !== action.attachmentId) { return item - } else { - return { - ...item, - isDeleting: false - } } }) return { ...state, attachments } - } case SET_FILTER_CHALLENGE_VALUE: return { ...state, filterChallengeName: action.value.name, status: action.value.status } default: diff --git a/src/services/challenges.js b/src/services/challenges.js index 1eef1036..b6a57c98 100644 --- a/src/services/challenges.js +++ b/src/services/challenges.js @@ -2,6 +2,7 @@ import _ from 'lodash' import qs from 'qs' import { axiosInstance } from './axiosWithAuth' import { updateChallengePhaseBeforeSendRequest, convertChallengePhaseFromSecondsToHours, normalizeChallengeDataFromAPI } from '../util/date' +import FormData from 'form-data' import { GROUPS_DROPDOWN_PER_PAGE } from '../config/constants' const { CHALLENGE_API_URL, @@ -125,30 +126,12 @@ export function updateChallenge (challengeId, challenge) { }) } -/** - * Create attachment - * - * @param {String|Number} challengeId challenge id - * @param {String|Number} attachmentId attachment id - * - * @returns {Promise<*>} attachment data - */ -export function createAttachment (challengeId, data) { +export function uploadAttachment (challengeId, file) { + const data = new FormData() + data.append('attachment', file) return axiosInstance.post(`${CHALLENGE_API_URL}/${challengeId}/attachments`, data) } -/** - * Remove attachment - * - * @param {String|Number} challengeId challenge id - * @param {String|Number} attachmentId attachment id - * - * @returns {Promise} - */ -export function removeAttachment (challengeId, attachmentId) { - return axiosInstance.delete(`${CHALLENGE_API_URL}/${challengeId}/attachments/${attachmentId}`) -} - /** * Fetch challenges from v5 API * @param filters From 311bcb1b6e3c8dc40dab850fb29fa7697e1e9400 Mon Sep 17 00:00:00 2001 From: C Dharmateja Date: Wed, 13 Jan 2021 12:00:00 +0530 Subject: [PATCH 24/32] requested changes in pull request --- .../ChallengeEditor/Groups-Field/index.js | 31 +++++++++++++------ .../ChallengeEditor/TagsField/index.js | 2 +- .../ChallengeEditor/Terms-Field/index.js | 18 +++++++++-- src/components/Select/styles.js | 8 +++-- src/config/constants.js | 3 +- src/services/challenges.js | 14 +++++---- 6 files changed, 52 insertions(+), 24 deletions(-) diff --git a/src/components/ChallengeEditor/Groups-Field/index.js b/src/components/ChallengeEditor/Groups-Field/index.js index 47410496..a0c014a2 100644 --- a/src/components/ChallengeEditor/Groups-Field/index.js +++ b/src/components/ChallengeEditor/Groups-Field/index.js @@ -4,15 +4,11 @@ import AsyncSelect from '../../Select/AsyncSelect' import cn from 'classnames' import styles from './Groups-Field.module.scss' import _ from 'lodash' -import { axiosInstance } from '../../../services/axiosWithAuth' -import { AUTOCOMPLETE_MIN_LENGTH, AUTOCOMPLETE_DEBOUNCE_TIME_MS, GROUPS_API_URL } from '../../../config/constants' +import { fetchGroups } from '../../../services/challenges' +import { AUTOCOMPLETE_MIN_LENGTH, AUTOCOMPLETE_DEBOUNCE_TIME_MS } from '../../../config/constants' const GroupsField = ({ onUpdateMultiSelect, challenge }) => { - async function fetchGroups (name) { - if (!name) return [] - const url = `${GROUPS_API_URL}?name=${name}` - return axiosInstance.get(url) - } + const [groups, setGroups] = React.useState([]) const onInputChange = React.useCallback(_.debounce(async (inputValue, callback) => { if (!inputValue) return @@ -20,7 +16,7 @@ const GroupsField = ({ onUpdateMultiSelect, challenge }) => { if (preparedValue.length < AUTOCOMPLETE_MIN_LENGTH) { return [] } - const { data } = await fetchGroups(inputValue) + const data = await fetchGroups({ name: inputValue }) const suggestions = data.map(suggestion => ({ label: suggestion.name, value: suggestion.id @@ -28,6 +24,18 @@ const GroupsField = ({ onUpdateMultiSelect, challenge }) => { callback && callback(suggestions) }, AUTOCOMPLETE_DEBOUNCE_TIME_MS), []) + React.useEffect(() => { + Promise.all( + challenge.groups + .map(group => fetchGroups({}, `/${group}`)) + ).then(groups => { + setGroups(groups.map(group => ({ + label: group.name, + value: group.id + }))) + }).catch(console.error) + }, []) + return (
@@ -41,9 +49,12 @@ const GroupsField = ({ onUpdateMultiSelect, challenge }) => { onInputChange(inputValue, callback) }} simpleValue - multivalue={challenge.groups} + value={groups} placeholder='Select groups' - onChange={(e) => onUpdateMultiSelect(e, 'groups')} + onChange={(e) => { + onUpdateMultiSelect(e, 'groups') + setGroups(e) + }} />
diff --git a/src/components/ChallengeEditor/TagsField/index.js b/src/components/ChallengeEditor/TagsField/index.js index 9678cefc..7dcdfb88 100644 --- a/src/components/ChallengeEditor/TagsField/index.js +++ b/src/components/ChallengeEditor/TagsField/index.js @@ -22,7 +22,7 @@ const TagsField = ({ challengeTags, challenge, onUpdateMultiSelect, readOnly }) isMulti options={challengeTags.map(mapOps)} simpleValue - multivalue={challenge.tags && challenge.tags.map(tag => ({ label: tag, value: tag }))} + value={challenge.tags && challenge.tags.map(tag => ({ label: tag, value: tag }))} onChange={(value) => onUpdateMultiSelect(value, 'tags')} />)}
diff --git a/src/components/ChallengeEditor/Terms-Field/index.js b/src/components/ChallengeEditor/Terms-Field/index.js index 7b5bc2b9..4d05e086 100644 --- a/src/components/ChallengeEditor/Terms-Field/index.js +++ b/src/components/ChallengeEditor/Terms-Field/index.js @@ -6,6 +6,17 @@ import styles from './Terms-Field.module.scss' const TermsField = ({ terms, challenge, onUpdateMultiSelect }) => { const mapOps = item => ({ label: item.title, value: item.id }) + + const [currTerms, setCurrTerms] = React.useState([]) + + React.useEffect(() => { + const challengeTerms = new Set(challenge.terms) + const defaultValue = terms + .filter(term => challengeTerms.has(term.id)) + .map(mapOps) + setCurrTerms(defaultValue) + }, []) + return (
@@ -18,8 +29,11 @@ const TermsField = ({ terms, challenge, onUpdateMultiSelect }) => { isMulti options={terms.map(mapOps)} simpleValue - multivalue={challenge.terms} - onChange={(value) => onUpdateMultiSelect(value, 'terms')} + value={currTerms} + onChange={(value) => { + onUpdateMultiSelect(value, 'terms') + setCurrTerms(setCurrTerms(terms)) + }} />
diff --git a/src/components/Select/styles.js b/src/components/Select/styles.js index 9ca893d0..b52249ad 100644 --- a/src/components/Select/styles.js +++ b/src/components/Select/styles.js @@ -4,8 +4,6 @@ export default { width: '100%' }), control: (provided, state) => { - console.log('state') - console.log(state) let styles = { ...provided, borderRadius: '2px !important' @@ -46,7 +44,7 @@ export default { fontFamily: 'Roboto, Helvetica, Arial, sans-serif', fontSize: '15px', fontWeight: 300, - paddingLeft: '20px', + paddingLeft: '10px', color: '#2a2a2a' }), input: (provided) => ({ @@ -62,6 +60,10 @@ export default { lineHeight: 'normal !important' } }), + singleValue: (provided) => ({ + ...provided, + paddingLeft: '10px' + }), multiValue: (provided) => ({ ...provided, backgroundColor: '#2c95d7' diff --git a/src/config/constants.js b/src/config/constants.js index 5903535e..d1e4aef0 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -14,8 +14,7 @@ export const { DES_TRACK_ID, DS_TRACK_ID, QA_TRACK_ID, - SEGMENT_API_KEY, - GROUPS_API_URL + SEGMENT_API_KEY } = 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 diff --git a/src/services/challenges.js b/src/services/challenges.js index b6a57c98..7203f81c 100644 --- a/src/services/challenges.js +++ b/src/services/challenges.js @@ -56,12 +56,14 @@ export async function fetchChallengeTags () { * @param filters * @returns {Promise<*>} */ -export async function fetchGroups (filters) { - const finalFilters = { - ...filters, - perPage: GROUPS_DROPDOWN_PER_PAGE // make sure that we are retrieving all the groups - } - const response = await axiosInstance.get(`${GROUPS_API_URL}?${qs.stringify(finalFilters, { encode: false })}`) +export async function fetchGroups (filters, params = '') { + const finalFilters = filters && Object.keys(filters).length > 0 + ? { + ...filters, + perPage: GROUPS_DROPDOWN_PER_PAGE // make sure that we are retrieving all the groups + } + : {} + const response = await axiosInstance.get(`${GROUPS_API_URL}${params}?${qs.stringify(finalFilters, { encode: false })}`) return _.get(response, 'data', []) } From ab2d4cffeb7d33297e044e8f537eaa091bfd3a2b Mon Sep 17 00:00:00 2001 From: maxceem Date: Wed, 13 Jan 2021 12:29:26 +0200 Subject: [PATCH 25/32] Revert "Merge branch 'develop' into cf-jan-2021" This reverts commit ce7f7cfd3340cd1eaeb76077adde5fc7af30dc9c, reversing changes made to ca7394667209a4d943da55b0f682932dc77b96a4. --- src/actions/challenges.js | 24 ++++++++ .../ChallengeEditor.module.scss | 8 ++- src/components/ChallengeEditor/index.js | 27 ++++++++- .../ChallengeCard/ChallengeCard.module.scss | 25 ++++++++ .../ChallengeCard/index.js | 57 ++++++++++++++++--- .../ChallengeList/index.js | 8 ++- src/components/ChallengesComponent/index.js | 7 ++- src/config/constants.js | 4 ++ src/containers/ChallengeEditor/index.js | 3 + src/containers/Challenges/index.js | 12 ++-- src/reducers/challenges.js | 27 ++++++++- src/services/challenges.js | 8 +++ 12 files changed, 190 insertions(+), 20 deletions(-) diff --git a/src/actions/challenges.js b/src/actions/challenges.js index 0cc3373c..d9aafda0 100644 --- a/src/actions/challenges.js +++ b/src/actions/challenges.js @@ -16,6 +16,7 @@ import { fetchGroupDetail, updateChallenge, patchChallenge, + deleteChallenge as deleteChallengeAPI, createChallenge as createChallengeAPI, createResource as createResourceAPI, deleteResource as deleteResourceAPI @@ -39,6 +40,9 @@ import { CREATE_CHALLENGE_PENDING, CREATE_CHALLENGE_SUCCESS, CREATE_CHALLENGE_FAILURE, + DELETE_CHALLENGE_PENDING, + DELETE_CHALLENGE_SUCCESS, + DELETE_CHALLENGE_FAILURE, LOAD_CHALLENGE_RESOURCES } from '../config/constants' import { loadProject } from './projects' @@ -276,6 +280,26 @@ export function partiallyUpdateChallengeDetails (challengeId, partialChallengeDe } } +export function deleteChallenge (challengeId) { + return async (dispatch) => { + dispatch({ + type: DELETE_CHALLENGE_PENDING + }) + + return deleteChallengeAPI(challengeId).then((challenge) => { + return dispatch({ + type: DELETE_CHALLENGE_SUCCESS, + challengeDetails: challenge + }) + }).catch((error) => { + dispatch({ + type: DELETE_CHALLENGE_FAILURE + }) + throw error + }) + } +} + export function loadTimelineTemplates () { return async (dispatch) => { const timelineTemplates = await fetchTimelineTemplates() diff --git a/src/components/ChallengeEditor/ChallengeEditor.module.scss b/src/components/ChallengeEditor/ChallengeEditor.module.scss index 4d7f5b24..cac38dd5 100644 --- a/src/components/ChallengeEditor/ChallengeEditor.module.scss +++ b/src/components/ChallengeEditor/ChallengeEditor.module.scss @@ -241,7 +241,7 @@ .actionButtons { position: absolute; top: 30px; - a { + a,button { height: 40px; } } @@ -251,7 +251,13 @@ } .actionButtonsRight { + display: flex; + align-items: center; right: 20px; + + button { + margin-right: 20px; + } } .buttonContainer { diff --git a/src/components/ChallengeEditor/index.js b/src/components/ChallengeEditor/index.js index f6d5810b..d8c485bd 100644 --- a/src/components/ChallengeEditor/index.js +++ b/src/components/ChallengeEditor/index.js @@ -68,6 +68,7 @@ class ChallengeEditor extends Component { super(props) this.state = { isLaunch: false, + isDeleteLaunch: false, isConfirm: false, isClose: false, isOpenAdvanceSettings: false, @@ -122,6 +123,8 @@ class ChallengeEditor extends Component { this.getAvailableTimelineTemplates = this.getAvailableTimelineTemplates.bind(this) this.autoUpdateChallengeThrottled = _.throttle(this.validateAndAutoUpdateChallenge.bind(this), 3000) // 3s this.updateResource = this.updateResource.bind(this) + this.onDeleteChallenge = this.onDeleteChallenge.bind(this) + this.deleteModalLaunch = this.deleteModalLaunch.bind(this) } componentDidMount () { @@ -132,6 +135,27 @@ class ChallengeEditor extends Component { this.resetChallengeData(this.setState.bind(this)) } + deleteModalLaunch () { + if (!this.state.isDeleteLaunch) { + this.setState({ isDeleteLaunch: true }) + } + } + + async onDeleteChallenge () { + const { deleteChallenge, challengeDetails, history } = this.props + try { + this.setState({ isSaving: true }) + // Call action to delete the challenge + await deleteChallenge(challengeDetails.id) + this.setState({ isSaving: false }) + this.resetModal() + history.push(`/projects/${challengeDetails.projectId}/challenges`) + } catch (e) { + const error = _.get(e, 'response.data.message', 'Unable to Delete the challenge') + this.setState({ isSaving: false, error }) + } + } + /** * Validates challenge and if its valid calling an autosave method * @@ -209,7 +233,7 @@ class ChallengeEditor extends Component { } resetModal () { - this.setState({ isLoading: false, isConfirm: false, isLaunch: false, error: null, isCloseTask: false }) + this.setState({ isLoading: false, isConfirm: false, isLaunch: false, error: null, isCloseTask: false, isDeleteLaunch: false }) } /** @@ -1476,6 +1500,7 @@ class ChallengeEditor extends Component {
{getTitle(isNew)}
+ {this.props.challengeDetails.status === 'New' && }
* Required
diff --git a/src/components/ChallengesComponent/ChallengeCard/ChallengeCard.module.scss b/src/components/ChallengesComponent/ChallengeCard/ChallengeCard.module.scss index bac62146..89614ec3 100644 --- a/src/components/ChallengesComponent/ChallengeCard/ChallengeCard.module.scss +++ b/src/components/ChallengesComponent/ChallengeCard/ChallengeCard.module.scss @@ -257,6 +257,31 @@ } } +.deleteButton { + height: 22px; + width: 86px; + border-radius: 11.5px; + display: flex; + justify-content: center; + align-items: center; + background-color: $tc-red; + border-color: $tc-red; + cursor: pointer; + + span { + @include roboto; + + font-size: 14px; + font-weight: 400; + line-height: 17px; + color: $white; + text-transform: capitalize; + display: flex; + justify-content: center; + align-items: center; + } +} + .icon { vertical-align: bottom; } diff --git a/src/components/ChallengesComponent/ChallengeCard/index.js b/src/components/ChallengesComponent/ChallengeCard/index.js index f4485a79..50adec5f 100644 --- a/src/components/ChallengesComponent/ChallengeCard/index.js +++ b/src/components/ChallengesComponent/ChallengeCard/index.js @@ -96,14 +96,20 @@ const getPhaseInfo = (c) => { * @param onUpdateLaunch * @returns {*} */ -const hoverComponents = (challenge, onUpdateLaunch) => { +const hoverComponents = (challenge, onUpdateLaunch, deleteModalLaunch) => { const communityAppUrl = `${COMMUNITY_APP_URL}/challenges/${challenge.id}` const directUrl = `${DIRECT_PROJECT_URL}/contest/detail?projectId=${challenge.legacyId}` const orUrl = `${ONLINE_REVIEW_URL}/review/actions/ViewProjectDetails?pid=${challenge.legacyId}` // NEW projects never have Legacy challenge created, so don't show links and "Activate" button for them at all if (challenge.status.toUpperCase() === CHALLENGE_STATUS.NEW) { - return null + if (challenge.status.toUpperCase() === CHALLENGE_STATUS.NEW) { + return ( + + ) + } } return challenge.legacyId ? ( @@ -177,10 +183,13 @@ class ChallengeCard extends React.Component { this.state = { isConfirm: false, isLaunch: false, + isDeleteLaunch: false, isSaving: false } this.onUpdateConfirm = this.onUpdateConfirm.bind(this) this.onUpdateLaunch = this.onUpdateLaunch.bind(this) + this.onDeleteChallenge = this.onDeleteChallenge.bind(this) + this.deleteModalLaunch = this.deleteModalLaunch.bind(this) this.resetModal = this.resetModal.bind(this) this.onLaunchChallenge = this.onLaunchChallenge.bind(this) } @@ -195,8 +204,14 @@ class ChallengeCard extends React.Component { } } + deleteModalLaunch () { + if (!this.state.isDeleteLaunch) { + this.setState({ isDeleteLaunch: true }) + } + } + resetModal () { - this.setState({ isConfirm: false, isLaunch: false }) + this.setState({ isConfirm: false, isLaunch: false, isDeleteLaunch: false }) } async onLaunchChallenge () { @@ -221,12 +236,39 @@ class ChallengeCard extends React.Component { } } + async onDeleteChallenge () { + const { deleteChallenge, challenge } = this.props + try { + this.setState({ isSaving: true }) + // Call action to delete the challenge + await deleteChallenge(challenge.id) + this.setState({ isSaving: false }) + this.resetModal() + } catch (e) { + const error = _.get(e, 'response.data.message', 'Unable to Delete the challenge') + this.setState({ isSaving: false, error }) + } + } + render () { - const { isLaunch, isConfirm, isSaving } = this.state + const { isLaunch, isConfirm, isSaving, isDeleteLaunch } = this.state const { challenge, shouldShowCurrentPhase, reloadChallengeList } = this.props const { phaseMessage, endTime } = getPhaseInfo(challenge) return (
+ { + isDeleteLaunch && !isConfirm && ( + + ) + } { isLaunch && !isConfirm && ( {endTime} )}
- {hoverComponents(challenge, this.onUpdateLaunch, this.props.showError)} + {hoverComponents(challenge, this.onUpdateLaunch, this.deleteModalLaunch)}
@@ -287,16 +329,15 @@ class ChallengeCard extends React.Component { ChallengeCard.defaultPrps = { shouldShowCurrentPhase: true, - showError: () => {}, reloadChallengeList: () => {} } ChallengeCard.propTypes = { challenge: PropTypes.object, shouldShowCurrentPhase: PropTypes.bool, - showError: PropTypes.func, reloadChallengeList: PropTypes.func, - partiallyUpdateChallengeDetails: PropTypes.func.isRequired + partiallyUpdateChallengeDetails: PropTypes.func.isRequired, + deleteChallenge: PropTypes.func.isRequired } export default withRouter(ChallengeCard) diff --git a/src/components/ChallengesComponent/ChallengeList/index.js b/src/components/ChallengesComponent/ChallengeList/index.js index fe33891d..941c27eb 100644 --- a/src/components/ChallengesComponent/ChallengeList/index.js +++ b/src/components/ChallengesComponent/ChallengeList/index.js @@ -102,7 +102,8 @@ class ChallengeList extends Component { page, perPage, totalChallenges, - partiallyUpdateChallengeDetails + partiallyUpdateChallengeDetails, + deleteChallenge } = this.props if (warnMessage) { return @@ -211,9 +212,9 @@ class ChallengeList extends Component { ) @@ -256,7 +257,8 @@ ChallengeList.propTypes = { page: PropTypes.number.isRequired, perPage: PropTypes.number.isRequired, totalChallenges: PropTypes.number.isRequired, - partiallyUpdateChallengeDetails: PropTypes.func.isRequired + partiallyUpdateChallengeDetails: PropTypes.func.isRequired, + deleteChallenge: PropTypes.func.isRequired } export default ChallengeList diff --git a/src/components/ChallengesComponent/index.js b/src/components/ChallengesComponent/index.js index c173492f..eb386d49 100644 --- a/src/components/ChallengesComponent/index.js +++ b/src/components/ChallengesComponent/index.js @@ -26,7 +26,8 @@ const ChallengesComponent = ({ page, perPage, totalChallenges, - partiallyUpdateChallengeDetails + partiallyUpdateChallengeDetails, + deleteChallenge }) => { return ( @@ -86,6 +87,7 @@ const ChallengesComponent = ({ perPage={perPage} totalChallenges={totalChallenges} partiallyUpdateChallengeDetails={partiallyUpdateChallengeDetails} + deleteChallenge={deleteChallenge} /> )}
@@ -109,7 +111,8 @@ ChallengesComponent.propTypes = { page: PropTypes.number.isRequired, perPage: PropTypes.number.isRequired, totalChallenges: PropTypes.number.isRequired, - partiallyUpdateChallengeDetails: PropTypes.func.isRequired + partiallyUpdateChallengeDetails: PropTypes.func.isRequired, + deleteChallenge: PropTypes.func.isRequired } ChallengesComponent.defaultProps = { diff --git a/src/config/constants.js b/src/config/constants.js index d1e4aef0..2b26b0f9 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -48,6 +48,10 @@ export const CREATE_CHALLENGE_SUCCESS = 'CREATE_CHALLENGE_SUCCESS' export const CREATE_CHALLENGE_PENDING = 'CREATE_CHALLENGE_PENDING' export const CREATE_CHALLENGE_FAILURE = 'CREATE_CHALLENGE_FAILURE' +export const DELETE_CHALLENGE_SUCCESS = 'DELETE_CHALLENGE_SUCCESS' +export const DELETE_CHALLENGE_PENDING = 'DELETE_CHALLENGE_PENDING' +export const DELETE_CHALLENGE_FAILURE = 'DELETE_CHALLENGE_FAILURE' + export const LOAD_PROJECT_DETAILS = 'LOAD_PROJECT_DETAILS' export const LOAD_PROJECT_DETAILS_SUCCESS = 'LOAD_PROJECT_DETAILS_SUCCESS' export const LOAD_PROJECT_DETAILS_PENDING = 'LOAD_PROJECT_DETAILS_PENDING' diff --git a/src/containers/ChallengeEditor/index.js b/src/containers/ChallengeEditor/index.js index 346892c1..7d264a8d 100644 --- a/src/containers/ChallengeEditor/index.js +++ b/src/containers/ChallengeEditor/index.js @@ -25,6 +25,7 @@ import { loadResourceRoles, updateChallengeDetails, partiallyUpdateChallengeDetails, + deleteChallenge, createChallenge, replaceResourceInRole } from '../../actions/challenges' @@ -410,6 +411,7 @@ ChallengeEditor.propTypes = { updateChallengeDetails: PropTypes.func.isRequired, partiallyUpdateChallengeDetails: PropTypes.func.isRequired, createChallenge: PropTypes.func.isRequired, + deleteChallenge: PropTypes.func.isRequired, replaceResourceInRole: PropTypes.func // members: PropTypes.arrayOf(PropTypes.shape()) } @@ -445,6 +447,7 @@ const mapDispatchToProps = { loadResourceRoles, updateChallengeDetails, partiallyUpdateChallengeDetails, + deleteChallenge, createChallenge, replaceResourceInRole } diff --git a/src/containers/Challenges/index.js b/src/containers/Challenges/index.js index e0b9ef5d..7632bb3f 100644 --- a/src/containers/Challenges/index.js +++ b/src/containers/Challenges/index.js @@ -10,7 +10,7 @@ import { DebounceInput } from 'react-debounce-input' import ChallengesComponent from '../../components/ChallengesComponent' import ProjectCard from '../../components/ProjectCard' import Loader from '../../components/Loader' -import { loadChallengesByPage, partiallyUpdateChallengeDetails } from '../../actions/challenges' +import { loadChallengesByPage, partiallyUpdateChallengeDetails, deleteChallenge } from '../../actions/challenges' import { loadProject } from '../../actions/projects' import { loadProjects, setActiveProject, resetSidebarActiveParams } from '../../actions/sidebar' import { @@ -86,7 +86,8 @@ class Challenges extends Component { perPage, totalChallenges, setActiveProject, - partiallyUpdateChallengeDetails + partiallyUpdateChallengeDetails, + deleteChallenge } = this.props const { searchProjectName, onlyMyProjects } = this.state const projectInfo = _.find(projects, { id: activeProjectId }) || {} @@ -147,6 +148,7 @@ class Challenges extends Component { perPage={perPage} totalChallenges={totalChallenges} partiallyUpdateChallengeDetails={partiallyUpdateChallengeDetails} + deleteChallenge={deleteChallenge} /> } @@ -173,7 +175,8 @@ Challenges.propTypes = { totalChallenges: PropTypes.number.isRequired, loadProjects: PropTypes.func.isRequired, setActiveProject: PropTypes.func.isRequired, - partiallyUpdateChallengeDetails: PropTypes.func.isRequired + partiallyUpdateChallengeDetails: PropTypes.func.isRequired, + deleteChallenge: PropTypes.func.isRequired } const mapStateToProps = ({ challenges, sidebar, projects }) => ({ @@ -191,7 +194,8 @@ const mapDispatchToProps = { loadProject, loadProjects, setActiveProject, - partiallyUpdateChallengeDetails + partiallyUpdateChallengeDetails, + deleteChallenge } export default connect(mapStateToProps, mapDispatchToProps)(Challenges) diff --git a/src/reducers/challenges.js b/src/reducers/challenges.js index df398975..20698d89 100644 --- a/src/reducers/challenges.js +++ b/src/reducers/challenges.js @@ -27,7 +27,10 @@ import { CREATE_CHALLENGE_RESOURCE_SUCCESS, DELETE_CHALLENGE_RESOURCE_SUCCESS, DELETE_CHALLENGE_RESOURCE_FAILURE, - CREATE_CHALLENGE_RESOURCE_FAILURE + CREATE_CHALLENGE_RESOURCE_FAILURE, + DELETE_CHALLENGE_SUCCESS, + DELETE_CHALLENGE_FAILURE, + DELETE_CHALLENGE_PENDING } from '../config/constants' const initialState = { @@ -42,6 +45,7 @@ const initialState = { attachments: [], challenge: null, filterChallengeName: '', + failedToDelete: false, status: '', perPage: 0, page: 1, @@ -144,6 +148,27 @@ export default function (state = initialState, action) { } case UPDATE_CHALLENGE_DETAILS_FAILURE: return { ...state, isLoading: false, attachments: [], challenge: null, failedToLoad: false, failedToUpdate: true } + + case DELETE_CHALLENGE_PENDING: + return { ...state, failedToLoad: false } + + case DELETE_CHALLENGE_SUCCESS: { + const deletedChallengeDetails = action.challengeDetails.data + const updatedChallenges = state.challenges.filter((challenge) => challenge.id !== deletedChallengeDetails.id) + toastrSuccess('Success', `Challenge deleted successfully.`) + return { + ...state, + challenges: updatedChallenges + } + } + + case DELETE_CHALLENGE_FAILURE: { + return { + ...state, + failedToDelete: true + } + } + case CREATE_CHALLENGE_SUCCESS: { // if we are showing the list of challenges with the same status as we just created, // then add the new challenge to the beginning of the current challenge list diff --git a/src/services/challenges.js b/src/services/challenges.js index 7afa6e12..49bce2dc 100644 --- a/src/services/challenges.js +++ b/src/services/challenges.js @@ -173,6 +173,14 @@ export function patchChallenge (challengeId, params) { }) } +/* +* Deletes the challenge with the provided id. +* @param challengeId +*/ +export function deleteChallenge (challengeId) { + return axiosInstance.delete(`${CHALLENGE_API_URL}/${challengeId}`) +} + /** * Api request for fetching challenge terms * @returns {Promise<*>} From b2a15b40a0e98243b533a7c775bd5cce1472dbfc Mon Sep 17 00:00:00 2001 From: maxceem Date: Wed, 13 Jan 2021 12:35:48 +0200 Subject: [PATCH 26/32] fix: delete button small fixes --- src/components/ChallengeEditor/index.js | 2 +- .../ChallengesComponent/ChallengeCard/index.js | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/components/ChallengeEditor/index.js b/src/components/ChallengeEditor/index.js index d8c485bd..35eee6a8 100644 --- a/src/components/ChallengeEditor/index.js +++ b/src/components/ChallengeEditor/index.js @@ -1500,7 +1500,7 @@ class ChallengeEditor extends Component {
{getTitle(isNew)}
- {this.props.challengeDetails.status === 'New' && } + {!isNew && this.props.challengeDetails.status === 'New' && }
* Required
diff --git a/src/components/ChallengesComponent/ChallengeCard/index.js b/src/components/ChallengesComponent/ChallengeCard/index.js index 50adec5f..67b15a9d 100644 --- a/src/components/ChallengesComponent/ChallengeCard/index.js +++ b/src/components/ChallengesComponent/ChallengeCard/index.js @@ -103,13 +103,11 @@ const hoverComponents = (challenge, onUpdateLaunch, deleteModalLaunch) => { // NEW projects never have Legacy challenge created, so don't show links and "Activate" button for them at all if (challenge.status.toUpperCase() === CHALLENGE_STATUS.NEW) { - if (challenge.status.toUpperCase() === CHALLENGE_STATUS.NEW) { - return ( - - ) - } + return ( + + ) } return challenge.legacyId ? ( From 6dc4760665a5a0ca4d4220431ea220160978a2fe Mon Sep 17 00:00:00 2001 From: maxceem Date: Wed, 13 Jan 2021 12:49:45 +0200 Subject: [PATCH 27/32] Revert "add favicon" This reverts commit f5f00627b772013e0f04b901330ee05516ecc04b. --- public/favicon.ico | Bin 15767 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 public/favicon.ico diff --git a/public/favicon.ico b/public/favicon.ico deleted file mode 100644 index 74a5210073f9e140b0ee3e4e95ee37f9a8634441..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15767 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 From 81485ee57b4f39dbd17c7d775dd659f3a77e7beb Mon Sep 17 00:00:00 2001 From: C Dharmateja Date: Wed, 13 Jan 2021 19:35:21 +0530 Subject: [PATCH 28/32] fix search group placeholder --- src/components/ChallengeEditor/Groups-Field/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ChallengeEditor/Groups-Field/index.js b/src/components/ChallengeEditor/Groups-Field/index.js index a0c014a2..d8f2b16f 100644 --- a/src/components/ChallengeEditor/Groups-Field/index.js +++ b/src/components/ChallengeEditor/Groups-Field/index.js @@ -50,7 +50,7 @@ const GroupsField = ({ onUpdateMultiSelect, challenge }) => { }} simpleValue value={groups} - placeholder='Select groups' + placeholder='Search groups' onChange={(e) => { onUpdateMultiSelect(e, 'groups') setGroups(e) From c1415633bd702b84889f52b01a8eb16368d3bedb Mon Sep 17 00:00:00 2001 From: maxceem Date: Thu, 14 Jan 2021 10:00:54 +0200 Subject: [PATCH 29/32] Revert "Merge pull request #1035 from topcoder-platform/revert-931-issue-917" This reverts commit 39671b8489b22b3b3493083390478cafeb4c6920, reversing changes made to 61c4705de8776075352aec6bdff2d2ed192e6beb. # Conflicts: # package-lock.json --- config/constants/development.js | 6 +- config/constants/production.js | 6 +- package-lock.json | 185 ++++++++++-- package.json | 3 +- src/actions/challenges.js | 76 +++-- .../Attachment-Field.module.scss | 135 ++------- .../ChallengeEditor/Attachment-Field/index.js | 101 +++---- .../ChallengeEditor/ChallengeView/index.js | 15 +- src/components/ChallengeEditor/index.js | 29 +- .../FilestackFilePicker.module.scss | 60 ++++ src/components/FilestackFilePicker/index.jsx | 283 ++++++++++++++++++ src/config/constants.js | 36 ++- src/containers/ChallengeEditor/index.js | 1 + src/reducers/challenges.js | 86 ++++-- src/services/challenges.js | 25 +- 15 files changed, 754 insertions(+), 293 deletions(-) create mode 100644 src/components/FilestackFilePicker/FilestackFilePicker.module.scss create mode 100644 src/components/FilestackFilePicker/index.jsx diff --git a/config/constants/development.js b/config/constants/development.js index 2a79c4c3..2f660373 100644 --- a/config/constants/development.js +++ b/config/constants/development.js @@ -31,5 +31,9 @@ module.exports = { DS_TRACK_ID: 'c0f5d461-8219-4c14-878a-c3a3f356466d', QA_TRACK_ID: '36e6a8d0-7e1e-4608-a673-64279d99c115', SEGMENT_API_KEY: 'QBtLgV8vCiuRX1lDikbMjcoe9aCHkF6n', - CREATE_FORUM_TYPE_IDS: ['927abff4-7af9-4145-8ba1-577c16e64e2e', 'dc876fa4-ef2d-4eee-b701-b555fcc6544c'] + CREATE_FORUM_TYPE_IDS: ['927abff4-7af9-4145-8ba1-577c16e64e2e', 'dc876fa4-ef2d-4eee-b701-b555fcc6544c'], + FILE_PICKER_API_KEY: process.env.FILE_PICKER_API_KEY, + FILE_PICKER_CONTAINER_NAME: 'tc-challenge-v5-dev', + FILE_PICKER_REGION: 'us-east-1', + FILE_PICKER_CNAME: 'fs.topcoder.com' } diff --git a/config/constants/production.js b/config/constants/production.js index b5bd162e..a84e7373 100644 --- a/config/constants/production.js +++ b/config/constants/production.js @@ -31,5 +31,9 @@ module.exports = { DS_TRACK_ID: 'c0f5d461-8219-4c14-878a-c3a3f356466d', QA_TRACK_ID: '36e6a8d0-7e1e-4608-a673-64279d99c115', SEGMENT_API_KEY: 'QSQAW5BWmZfLoKFNRgNKaqHvLDLJoGqF', - CREATE_FORUM_TYPE_IDS: ['927abff4-7af9-4145-8ba1-577c16e64e2e', 'dc876fa4-ef2d-4eee-b701-b555fcc6544c'] + CREATE_FORUM_TYPE_IDS: ['927abff4-7af9-4145-8ba1-577c16e64e2e', 'dc876fa4-ef2d-4eee-b701-b555fcc6544c'], + FILE_PICKER_API_KEY: process.env.FILE_PICKER_API_KEY, + FILE_PICKER_CONTAINER_NAME: 'tc-challenge-v5-prod', + FILE_PICKER_REGION: 'us-east-1', + FILE_PICKER_CNAME: 'fs.topcoder.com' } diff --git a/package-lock.json b/package-lock.json index 57be0c21..17bf2acd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1147,6 +1147,40 @@ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.5.4.tgz", "integrity": "sha512-ZpKr+WTb8zsajqgDkvCEWgp6d5eJT6Q63Ng2neTbzBO76Lbe91vX/iVIW9dikq+Fs3yEo+ls4cxeXABD2LtcbQ==" }, + "@sentry/hub": { + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.28.0.tgz", + "integrity": "sha512-1k19yJJcKoHbw12FET35t0m86lx/X6eJ6r4qM13eb2WN/OpoFtsgs1IjQOhGFL3OfVMcfh800Lc57ga04RLjLA==", + "requires": { + "@sentry/types": "5.28.0", + "@sentry/utils": "5.28.0", + "tslib": "^1.9.3" + } + }, + "@sentry/minimal": { + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.28.0.tgz", + "integrity": "sha512-HzFrJx0xe5KETEZc7RxlH+1TfmH3q8w35ILOP5HGvk3+lG1DR25wHbMFmuUqNqVXrl26t0z32UBI30G1MxmTfA==", + "requires": { + "@sentry/hub": "5.28.0", + "@sentry/types": "5.28.0", + "tslib": "^1.9.3" + } + }, + "@sentry/types": { + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.28.0.tgz", + "integrity": "sha512-nNhoZEXdqM2xivxJBrLhxtJ2+s6FfKXUw5yBf0Jf/RBrBnH5fggPNImmyfpOoysl72igWcMWk4nnfyP5iDrriQ==" + }, + "@sentry/utils": { + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.28.0.tgz", + "integrity": "sha512-LW+ReVw9JG6g8Bvp2I1ThMDPATlisvkde+1WykxGqRhu2YIO+PvWhnoFhr9RD0ia3rYVlJkgkuTshMbPJ8HVwA==", + "requires": { + "@sentry/types": "5.28.0", + "tslib": "^1.9.3" + } + }, "@svgr/core": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@svgr/core/-/core-2.4.1.tgz", @@ -1816,6 +1850,11 @@ } } }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -2030,11 +2069,6 @@ "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" }, - "attr-accept": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.1.0.tgz", - "integrity": "sha512-sLzVM3zCCmmDtDNhI0i96k6PUztkotSOXqE4kDGQt/6iDi5M+H0srjeF+QC6jN581l4X/Zq3Zu/tgcErEssavg==" - }, "autoprefixer": { "version": "9.7.6", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.7.6.tgz", @@ -5898,6 +5932,11 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" }, + "fast-xml-parser": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-3.17.4.tgz", + "integrity": "sha512-qudnQuyYBgnvzf5Lj/yxMcf4L9NcVWihXJg7CiU1L+oUCq8MUnFEfH2/nXR/W5uq+yvUN1h7z6s7vs2v1WkL1A==" + }, "fastparse": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", @@ -5950,13 +5989,10 @@ "schema-utils": "^1.0.0" } }, - "file-selector": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.1.12.tgz", - "integrity": "sha512-Kx7RTzxyQipHuiqyZGf+Nz4vY9R1XGxuQl/hLoJwq+J4avk/9wxxgZyHKtbyIPJmbD4A66DWGYfyykWNpcYutQ==", - "requires": { - "tslib": "^1.9.0" - } + "file-type": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-10.11.0.tgz", + "integrity": "sha512-uzk64HRpUZyTGZtVuvrjP0FYxzQrBf4rojot6J65YMEbwBLB0CWm0CLojVpwpmFmxcE/lkvYICgfcGozbBq6rw==" }, "file-uri-to-path": { "version": "1.0.0", @@ -5983,6 +6019,40 @@ "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz", "integrity": "sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==" }, + "filestack-js": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/filestack-js/-/filestack-js-3.20.0.tgz", + "integrity": "sha512-aPFVi/sA7bBGsL4uh69WgEDYYrkdMLnb2iW2gAGpY7Yd2I848ffckvlVXCI6rwRYnkLdioWQc3sXn5snzA/HBQ==", + "requires": { + "@babel/runtime": "^7.8.4", + "@filestack/loader": "^1.0.4", + "@sentry/minimal": "^5.12.0", + "abab": "^2.0.3", + "debug": "^4.1.1", + "eventemitter3": "^4.0.0", + "fast-xml-parser": "^3.16.0", + "file-type": "^10.11.0", + "follow-redirects": "^1.10.0", + "isutf8": "^2.1.0", + "jsonschema": "^1.2.5", + "lodash.clonedeep": "^4.5.0", + "p-queue": "^4.0.0", + "spark-md5": "^3.0.0", + "ts-node": "^8.10.1" + }, + "dependencies": { + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, + "follow-redirects": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz", + "integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==" + } + } + }, "fill-range": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", @@ -6147,16 +6217,6 @@ "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" }, - "form-data": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", - "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, "forwarded": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", @@ -8296,6 +8356,11 @@ "handlebars": "^4.0.3" } }, + "isutf8": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isutf8/-/isutf8-2.1.0.tgz", + "integrity": "sha512-rEMU6f82evtJNtYMrtVODUbf+C654mos4l+9noOueesUMipSWK6x3tpt8DiXhcZh/ZOBWYzJ9h9cNAlcQQnMiQ==" + }, "jest": { "version": "23.6.0", "resolved": "https://registry.npmjs.org/jest/-/jest-23.6.0.tgz", @@ -9077,6 +9142,11 @@ "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" }, + "jsonschema": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.4.0.tgz", + "integrity": "sha512-/YgW6pRMr6M7C+4o8kS+B/2myEpHCrxO4PEWnqJNBFMjn7EWXqlQ4tGwL6xTHeRplwuZmcAncdvfOad1nT2yMw==" + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -9291,6 +9361,11 @@ "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" + }, "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -9384,6 +9459,11 @@ } } }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, "makeerror": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", @@ -10361,6 +10441,14 @@ "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==" }, + "p-queue": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-4.0.0.tgz", + "integrity": "sha512-3cRXXn3/O0o3+eVmUroJPSj/esxoEFIm0ZOno/T+NzG/VZgPOqQ8WKmlNqubSEpZmCIngEy34unkHGg83ZIBmg==", + "requires": { + "eventemitter3": "^3.1.0" + } + }, "p-retry": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-3.0.1.tgz", @@ -14680,16 +14768,6 @@ "scheduler": "^0.19.1" } }, - "react-dropzone": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-10.2.2.tgz", - "integrity": "sha512-U5EKckXVt6IrEyhMMsgmHQiWTGLudhajPPG77KFSvgsMqNEHSyGpqWvOMc5+DhEah/vH4E1n+J5weBNLd5VtyA==", - "requires": { - "attr-accept": "^2.0.0", - "file-selector": "^0.1.12", - "prop-types": "^15.7.2" - } - }, "react-error-overlay": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-5.1.6.tgz", @@ -16431,6 +16509,11 @@ "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" }, + "spark-md5": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spark-md5/-/spark-md5-3.0.1.tgz", + "integrity": "sha512-0tF3AGSD1ppQeuffsLDIOWlKUd3lS92tFxcsrh5Pe3ZphhnoK+oXIBTzOAThZCiuINZLvpiLH/1VS1/ANEJVig==" + }, "spdx-correct": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", @@ -17605,6 +17688,39 @@ "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==" }, + "ts-node": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz", + "integrity": "sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==", + "requires": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + }, + "dependencies": { + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + } + } + }, "tslib": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", @@ -19781,6 +19897,11 @@ "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" } } + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==" } } } diff --git a/package.json b/package.json index a4b659bd..8084faf8 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "eslint-plugin-standard": "^4.0.0", "express": "^4.16.4", "file-loader": "2.0.0", - "form-data": "^2.4.0", + "filestack-js": "^3.20.0", "fs-extra": "7.0.0", "html-webpack-plugin": "4.0.0-alpha.2", "identity-obj-proxy": "3.0.0", @@ -71,7 +71,6 @@ "react-debounce-input": "^3.2.0", "react-dev-utils": "^7.0.1", "react-dom": "^16.7.0", - "react-dropzone": "^10.1.5", "react-google-charts": "^3.0.13", "react-helmet": "^5.2.0", "react-js-pagination": "^3.0.3", diff --git a/src/actions/challenges.js b/src/actions/challenges.js index d9aafda0..8ead9d1d 100644 --- a/src/actions/challenges.js +++ b/src/actions/challenges.js @@ -5,7 +5,8 @@ import { fetchGroups, fetchTimelineTemplates, fetchChallengePhases, - uploadAttachment, + createAttachment as createAttachmentAPI, + removeAttachment as removeAttachmentAPI, fetchChallenge, fetchChallenges, fetchChallengeTerms, @@ -27,12 +28,14 @@ import { LOAD_CHALLENGES_FAILURE, LOAD_CHALLENGES_PENDING, LOAD_CHALLENGES_SUCCESS, - UPLOAD_ATTACHMENT_FAILURE, - UPLOAD_ATTACHMENT_PENDING, - UPLOAD_ATTACHMENT_SUCCESS, + CREATE_ATTACHMENT_FAILURE, + CREATE_ATTACHMENT_PENDING, + CREATE_ATTACHMENT_SUCCESS, + REMOVE_ATTACHMENT_FAILURE, + REMOVE_ATTACHMENT_PENDING, + REMOVE_ATTACHMENT_SUCCESS, CREATE_CHALLENGE_RESOURCE, DELETE_CHALLENGE_RESOURCE, - REMOVE_ATTACHMENT, PAGE_SIZE, UPDATE_CHALLENGE_DETAILS_PENDING, UPDATE_CHALLENGE_DETAILS_SUCCESS, @@ -381,38 +384,57 @@ export function loadGroups () { } export function createAttachment (challengeId, file) { - return async (dispatch, getState) => { - const getUploadingId = () => _.get(getState(), 'challenge.uploadingId') + return async (dispatch) => { + // create a temporary uploading id for each attachment + // so we can identify them for various actions (names theoretically can duplicate) + const uploadingId = _.uniqueId('uploadingId_') + + dispatch({ + type: CREATE_ATTACHMENT_PENDING, + challengeId, + file, + uploadingId + }) - if (challengeId !== getUploadingId()) { + try { + const attachment = await createAttachmentAPI(challengeId, file) dispatch({ - type: UPLOAD_ATTACHMENT_PENDING, - challengeId + type: CREATE_ATTACHMENT_SUCCESS, + attachment: attachment.data, + uploadingId + }) + } catch (error) { + dispatch({ + type: CREATE_ATTACHMENT_FAILURE, + file, + uploadingId }) - - try { - const attachment = await uploadAttachment(challengeId, file) - dispatch({ - type: UPLOAD_ATTACHMENT_SUCCESS, - attachment: attachment.data, - filename: file.name - }) - } catch (error) { - dispatch({ - type: UPLOAD_ATTACHMENT_FAILURE, - filename: file.name - }) - } } } } -export function removeAttachment (attachmentId) { - return (dispatch) => { +export function removeAttachment (challengeId, attachmentId) { + return async (dispatch) => { dispatch({ - type: REMOVE_ATTACHMENT, + type: REMOVE_ATTACHMENT_PENDING, + challengeId, attachmentId }) + + try { + await removeAttachmentAPI(challengeId, attachmentId) + dispatch({ + type: REMOVE_ATTACHMENT_SUCCESS, + challengeId, + attachmentId + }) + } catch (error) { + dispatch({ + type: REMOVE_ATTACHMENT_FAILURE, + challengeId, + attachmentId + }) + } } } diff --git a/src/components/ChallengeEditor/Attachment-Field/Attachment-Field.module.scss b/src/components/ChallengeEditor/Attachment-Field/Attachment-Field.module.scss index 20ae10aa..aca43d8d 100644 --- a/src/components/ChallengeEditor/Attachment-Field/Attachment-Field.module.scss +++ b/src/components/ChallengeEditor/Attachment-Field/Attachment-Field.module.scss @@ -1,112 +1,18 @@ @import "../../../styles/includes"; .container { - display: flex; - flex-direction: column; + margin-top: 30px; .row { - box-sizing: border-box; - display: flex; - flex-direction: row; - align-content: space-between; - justify-content: flex-start; - margin-top: 30px; + margin: 0 30px; - .field { - @include upto-sm { - display: block; - padding-bottom: 10px; - } - - label { - @include roboto-bold(); - - font-size: 16px; - line-height: 19px; - font-weight: 500; - color: $tc-gray-80; - } - - &.col1 { - max-width: 185px; - min-width: 185px; - margin-left: 30px; - margin-right: 14px; - margin-bottom: auto; - margin-top: auto; - padding-top: 10px; - white-space: nowrap; - display: flex; - align-items: center; - } - - &.col2 { - align-self: flex-end; - margin-bottom: auto; - margin-top: auto; - display: flex; - flex-direction: row; - align-items: center; - - input { - margin-right: 30px; - width: 271px; - } - input:last-of-type { - width: 187px; - margin-right: 10px; - } - } - } - - .uploadPanel { - cursor: pointer; - margin: 0 30px; - width: 100%; - align-self: center; - - - border: 1px solid $tc-gray-40; - border-radius: 6px; - height: 227px; - - &.isActive { - outline: auto 5px -webkit-focus-ring-color; - } - - label { - height: 100%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - } + label { + @include roboto-bold(); - .icon { - color: $tc-blue-20; - margin-bottom: 30px; - } - - .info { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - @include roboto; - - font-size: 16px; - font-weight: 400; - line-height: 19px; - color: $tc-gray-80; - - span { - color: $tc-blue-20; - } - } - - input { - display: none; - } + font-size: 16px; + line-height: 19px; + font-weight: 500; + color: $tc-gray-80; } .header { @@ -127,6 +33,7 @@ line-height: 19px; color: $tc-gray-80; padding: 0 30px; + margin-top: 30px; .col1 { flex: 4; @@ -171,25 +78,25 @@ justify-content: center; } - .icon { - color: $tc-red; + .actions { flex: 4; display: flex; justify-content: flex-end; padding-right: 15px; + } + + .removeIcon { + color: $tc-red; cursor: pointer; } - } - } - .row:nth-of-type(4) { - flex-direction: column; - padding: 0 30px; - } - .icon { - color: $tc-red; - cursor: pointer; + .loader { + > div { + margin-right: -7px; /* to center along with icons */ + width: 32px; + } + } + } } - } diff --git a/src/components/ChallengeEditor/Attachment-Field/index.js b/src/components/ChallengeEditor/Attachment-Field/index.js index 7224d2d7..18cae96a 100644 --- a/src/components/ChallengeEditor/Attachment-Field/index.js +++ b/src/components/ChallengeEditor/Attachment-Field/index.js @@ -1,34 +1,29 @@ import _ from 'lodash' -import React, { useCallback } from 'react' +import React from 'react' import PropTypes from 'prop-types' -import { useDropzone } from 'react-dropzone' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { downloadAttachmentURL } from '../../../config/constants' -import { faCloudUploadAlt, faTrash } from '@fortawesome/free-solid-svg-icons' +import { downloadAttachmentURL, SPECIFICATION_ATTACHMENTS_FOLDER, getAWSContainerFileURL } from '../../../config/constants' +import { faTrash } from '@fortawesome/free-solid-svg-icons' +import FilestackFilePicker from '../../FilestackFilePicker' import styles from './Attachment-Field.module.scss' -import cn from 'classnames' - -const AttachmentField = ({ challenge, removeAttachment, onUploadFile, token, readOnly }) => { - const onDrop = useCallback(acceptedFiles => { - _.forEach(acceptedFiles, item => { - onUploadFile(challenge.id, item) - }) - }, []) - - const { - getRootProps, - getInputProps, - isDragActive - } = useDropzone({ onDrop }) +import Loader from '../../Loader' +const AttachmentField = ({ challengeId, attachments, removeAttachment, onUploadFile, token, readOnly }) => { const renderAttachments = (attachments) => ( _.map(attachments, (att, index) => ( -
- {att.fileName} +
+ {att.name}
{formatBytes(att.fileSize)}
- {!readOnly && (
removeAttachment(att.id)}> - -
)} + {!readOnly && ( +
+ {!att.isDeleting && !att.isUploading && ( + removeAttachment(challengeId, att.id)} className={styles.removeIcon} /> + )} + {(att.isDeleting || att.isUploading) && ( +
+ )} +
+ )}
)) ) @@ -41,45 +36,35 @@ const AttachmentField = ({ challenge, removeAttachment, onUploadFile, token, rea const i = Math.floor(Math.log(bytes) / Math.log(k)) return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i] } + return (
-
- -
+
- {!readOnly && (
-
- - + + {!readOnly && ( +
+ onUploadFile(challengeId, { + name: file.filename, + fileSize: file.size, + url: getAWSContainerFileURL(file.key) + })} + />
-
)} + )} { - _.has(challenge, 'attachments') && challenge.attachments.length > 0 && ( - -
-
- -
-
-
-
-
File Name
-
Size
-
Action
-
- { renderAttachments(challenge.attachments) } + attachments && attachments.length > 0 && ( +
+
+
File Name
+
Size
+
Action
- + { renderAttachments(attachments) } +
) }
@@ -89,11 +74,13 @@ const AttachmentField = ({ challenge, removeAttachment, onUploadFile, token, rea AttachmentField.defaultProps = { removeAttachment: () => {}, onUploadFile: () => {}, - readOnly: false + readOnly: false, + attachments: [] } AttachmentField.propTypes = { - challenge: PropTypes.shape().isRequired, + challengeId: PropTypes.string.isRequired, + attachments: PropTypes.array, removeAttachment: PropTypes.func, onUploadFile: PropTypes.func, token: PropTypes.string.isRequired, diff --git a/src/components/ChallengeEditor/ChallengeView/index.js b/src/components/ChallengeEditor/ChallengeView/index.js index b2316299..99748e74 100644 --- a/src/components/ChallengeEditor/ChallengeView/index.js +++ b/src/components/ChallengeEditor/ChallengeView/index.js @@ -28,6 +28,7 @@ import { MESSAGE, REVIEW_TYPES } from '../../../config/constants' const ChallengeView = ({ projectDetail, challenge, + attachments, metadata, challengeResources, token, @@ -222,13 +223,12 @@ const ChallengeView = ({ challenge={challenge} readOnly /> - { false && ( - - )} + @@ -257,6 +257,7 @@ ChallengeView.propTypes = { }).isRequired, projectDetail: PropTypes.object, challenge: PropTypes.object, + attachments: PropTypes.array, metadata: PropTypes.object, token: PropTypes.string, isLoading: PropTypes.bool.isRequired, diff --git a/src/components/ChallengeEditor/index.js b/src/components/ChallengeEditor/index.js index 35eee6a8..c05fff24 100644 --- a/src/components/ChallengeEditor/index.js +++ b/src/components/ChallengeEditor/index.js @@ -97,7 +97,6 @@ class ChallengeEditor extends Component { this.updateFileTypesMetadata = this.updateFileTypesMetadata.bind(this) this.toggleAdvanceSettings = this.toggleAdvanceSettings.bind(this) this.toggleNdaRequire = this.toggleNdaRequire.bind(this) - this.removeAttachment = this.removeAttachment.bind(this) this.removePhase = this.removePhase.bind(this) this.resetPhase = this.resetPhase.bind(this) this.savePhases = this.savePhases.bind(this) @@ -594,15 +593,6 @@ class ChallengeEditor extends Component { this.setState({ challenge: newChallenge }) } - removeAttachment (file) { - const { challenge } = this.state - const newChallenge = { ...challenge } - const { attachments: oldAttachments } = challenge - const newAttachments = _.remove(oldAttachments, att => att.fileName !== file) - newChallenge.attachments = _.clone(newAttachments) - this.setState({ challenge: newChallenge }) - } - /** * Remove Phase from challenge Phases list * @param index @@ -1155,7 +1145,8 @@ class ChallengeEditor extends Component { token, removeAttachment, failedToLoad, - projectDetail + projectDetail, + attachments } = this.props if (_.isEmpty(challenge)) { return
Error loading challenge
@@ -1475,14 +1466,14 @@ class ChallengeEditor extends Component { onUpdateMultiSelect={this.onUpdateMultiSelect} onUpdateMetadata={this.onUpdateMetadata} /> - { false && ( - - )} + diff --git a/src/components/FilestackFilePicker/FilestackFilePicker.module.scss b/src/components/FilestackFilePicker/FilestackFilePicker.module.scss new file mode 100644 index 00000000..1a9de2b0 --- /dev/null +++ b/src/components/FilestackFilePicker/FilestackFilePicker.module.scss @@ -0,0 +1,60 @@ +@import "../../styles/includes"; + +.container { + .file-picker { + display: flex; + flex-direction: column; + align-items: center; + padding: 20px; + border: 1px solid $tc-gray-40; + border-radius: 6px; + height: 227px; + position: relative; + font-size: 16px; + font-weight: 400; + line-height: 19px; + color: $tc-gray-80; + + .icon { + color: $tc-blue-20; + margin-bottom: 30px; + } + + .pseudo-link { + color: $tc-blue-20; + } + } + + .file-picker.error { + border-color: #f22f24; + } + + .file-picker.drag { + background-color: rgba(0, 0, 0, 0.1); + border-color: rgba(0, 0, 0, 0.4); + } + + .uploading-files .file-error { + color: #f22f24; + } + + .error-container { + margin-top: 5px; + padding: 5px 13px; + background: #fff4f4; + border: 1px solid #ffd4d1; + color: #f22f24; + font-size: 13px; + border-radius: 2px; + font-style: italic; + } +} + +.drop-zone-mask { + bottom: 0; + cursor: pointer; + position: absolute; + left: 0; + right: 0; + top: 0; +} diff --git a/src/components/FilestackFilePicker/index.jsx b/src/components/FilestackFilePicker/index.jsx new file mode 100644 index 00000000..e6c872e8 --- /dev/null +++ b/src/components/FilestackFilePicker/index.jsx @@ -0,0 +1,283 @@ +/** + * FilestackFilePicker Component + * + * Component for uploading files using Filestack Picker and Drag & Drop. + * - Supports multiple file uploading. + */ +import _ from 'lodash' +import React, { useEffect, useLayoutEffect, useRef, useState } from 'react' +import PT from 'prop-types' +import * as filestack from 'filestack-js' +import cn from 'classnames' +import { + FILE_PICKER_API_KEY, + FILE_PICKER_CNAME, + FILE_PICKER_FROM_SOURCES, + FILE_PICKER_REGION, + FILE_PICKER_CONTAINER_NAME, + FILE_PICKER_ACCEPT, + FILE_PICKER_MAX_SIZE, + FILE_PICKER_MAX_FILES, + FILE_PICKER_PROGRESS_INTERVAL +} from '../../config/constants' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons' +import styles from './FilestackFilePicker.module.scss' + +/** + * FilestackFilePicker component + */ +const FilestackFilePicker = ({ + path, + onFileUploadFinished, + onFileUploadFailed, + onUploadDone +}) => { + // the list of filenames which are currently being uploaded + const [uploadingFiles, setUploadingFiles] = useState([]) + // if something is currently dragged over the area + const [dragged, setDragged] = useState(false) + // Filestack client instance + const filestackRef = useRef(null) + // we have to use ref for this method, because filestack would be initialized once with a callback using this method + const updateUploadingFile = useRef() + + // init Filestack (without waiting for rendering) + useLayoutEffect(() => { + filestackRef.current = filestack.init(FILE_PICKER_API_KEY, { + cname: FILE_PICKER_CNAME + }) + }, []) + + // update the ref to `updateUploadingFile` to keep referencing fresh state data + useEffect(() => { + updateUploadingFile.current = (filename, updated) => { + const uploadingFileIndex = _.findIndex(uploadingFiles, { filename }) + + if (uploadingFileIndex > -1) { + const updatedFile = { + ...uploadingFiles[uploadingFileIndex], + ...updated + } + + setUploadingFiles([ + ...uploadingFiles.slice(0, uploadingFileIndex), + updatedFile, + ...uploadingFiles.slice(uploadingFileIndex + 1) + ]) + + return updatedFile + } + } + }, [uploadingFiles, setUploadingFiles]) + + useEffect(() => { + // if all files have been uploaded successfully, clean uploading file list + if (uploadingFiles.length > 0 && _.every(uploadingFiles, 'file')) { + setUploadingFiles([]) + } + + // if all files are fully loaded or error happens for them call `onUploadDone` callback + if ( + uploadingFiles.length > 0 && + _.every(uploadingFiles, (file) => file.file || file.error) + ) { + if (onUploadDone) { + const filesFailed = _.filter(uploadingFiles, 'error') + const filesUploaded = _.filter(uploadingFiles, 'file') + + onUploadDone({ + filesFailed: _.map(filesFailed, 'file'), + filesUploaded: _.map(filesUploaded, 'file') + }) + } + } + }, [uploadingFiles, setUploadingFiles, onUploadDone]) + + /** + * Handle for success file(s) uploading + * + * @param {Object} file upload file info + */ + const handleFileUploadSuccess = (file) => { + console.log('handleFileUploadSuccess', file) + updateUploadingFile.current(file.name, { + file, // set `file` to indicate that file uploaded + progress: 100 // make sure that progress is set to 100 when uploading is complete + }) + onFileUploadFinished && onFileUploadFinished(file) + } + + /** + * Handle for error during file(s) uploading + * + * @param {Object|String} error error during file uploading + */ + const handleFileUploadError = (file) => { + updateUploadingFile.current(file.name, { + file, // set `file` to indicate that file uploaded + progress: 100 // make sure that progress is set to 100 when uploading is complete + }) + onFileUploadFailed && onFileUploadFailed(file) + } + + /** + * Open Filestack picker + */ + const openFilePicker = () => { + filestackRef.current + .picker({ + accept: FILE_PICKER_ACCEPT, + fromSources: FILE_PICKER_FROM_SOURCES, + maxSize: FILE_PICKER_MAX_SIZE, + maxFiles: FILE_PICKER_MAX_FILES, + onUploadStarted: (files) => { + setUploadingFiles( + files.map((file) => ({ + filename: file.filename, + progress: 0, + file: null, + error: null + })) + ) + }, + onFileUploadFailed: handleFileUploadError, + onFileUploadFinished: handleFileUploadSuccess, + onFileUploadProgress: (file, progressInfo) => { + updateUploadingFile.current(file.filename, { + progress: progressInfo.totalPercent + }) + }, + startUploadingWhenMaxFilesReached: true, + storeTo: { + container: FILE_PICKER_CONTAINER_NAME, + path, + region: FILE_PICKER_REGION + } + }) + .open() + } + + /** + * Handle file(s) uploading when dropping them on the area + * + * @param {Event} e event + */ + const handleFileDrop = (e) => { + e.preventDefault() + + setDragged(false) + + const files = Array.from(e.dataTransfer.files).map((file, index) => { + const fileExt = '.' + file.name.split('.').pop() + let error = null + + if (!_.includes(FILE_PICKER_ACCEPT, fileExt)) { + error = `Not allowed file type "${fileExt}".` + } + + if (index + 1 > FILE_PICKER_MAX_FILES) { + error = `File skipped, because can upload maximum ${FILE_PICKER_MAX_FILES} files at once.` + } + + return { + filename: file.name, + progress: 0, + file, + error + } + }) + + const filesToUpload = _.map(_.reject(files, 'error'), 'file') + + setUploadingFiles(files.map((file) => ({ ...file, file: null }))) + + filesToUpload.map((file) => + filestackRef.current + .upload( + file, + { + onProgress: ({ totalPercent }) => { + updateUploadingFile.current(file.name, { + progress: totalPercent + }) + }, + progressInterval: FILE_PICKER_PROGRESS_INTERVAL + }, + { + container: FILE_PICKER_CONTAINER_NAME, + path, + region: FILE_PICKER_REGION + } + ) + .then(handleFileUploadSuccess) + .catch(handleFileUploadError) + ) + } + + const hasErrors = _.some(uploadingFiles, 'error') + + return ( +
+
+
+ +
+ + {uploadingFiles.length === 0 ? ( + <> +
Drag & Drop files here
+
or
+
+ click here to + browse +
+ + ) : ( +
+ {uploadingFiles.map((uploadingFile) => ( +
+ {uploadingFile.filename} ( + {uploadingFile.error ? ( + {uploadingFile.error} + ) : ( + `${uploadingFile.progress}%` + )} + ) +
+ ))} +
+ )} + +
setDragged(true)} + onDragLeave={() => setDragged(false)} + onDragOver={(e) => e.preventDefault()} + onDrop={handleFileDrop} + role='tab' + tabIndex={0} + aria-label='Select file to upload' + /> +
+
+ ) +} + +FilestackFilePicker.defaultProps = {} + +FilestackFilePicker.propTypes = { + path: PT.string.isRequired, + onFileUploadFinished: PT.func, + onFileUploadFailed: PT.func, + onUploadDone: PT.func +} + +export default FilestackFilePicker diff --git a/src/config/constants.js b/src/config/constants.js index 2b26b0f9..52f406a3 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -18,6 +18,28 @@ export const { } = 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 +/** + * Filepicker config + */ +// to be able to start the Connect App we should pass at least the dummy value for `FILE_PICKER_API_KEY` +// 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' +// TODO uncomment this line to use correct `tc-challenge-v5-dev` bucket for DEV +// export const FILE_PICKER_CONTAINER_NAME = prcess.env.FILE_PICKER_CONTAINER_NAME || 'tc-challenge-v5-dev' +export const FILE_PICKER_CONTAINER_NAME = 'submission-staging-dev' +export const FILE_PICKER_REGION = process.env.FILE_PICKER_REGION || 'us-east-1' +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 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 +export const FILE_PICKER_PROGRESS_INTERVAL = 100 +export const SPECIFICATION_ATTACHMENTS_FOLDER = 'SPECIFICATION_ATTACHMENTS' + +// TODO uncomment this line to use the same bucket as we during FileStack uploading +// export const getAWSContainerFileURL = (key) => `https://${FILE_PICKER_CONTAINER_NAME}.s3.amazonaws.com/${key}` +export const getAWSContainerFileURL = (key) => `https://tc-challenge-v5-dev.s3.amazonaws.com/${key}` + // Actions export const LOAD_PROJECTS_SUCCESS = 'LOAD_PROJECTS_SUCCESS' export const LOAD_PROJECTS_PENDING = 'LOAD_PROJECTS_PENDING' @@ -66,9 +88,13 @@ export const LOAD_CHALLENGE_METADATA_SUCCESS = 'LOAD_CHALLENGE_METADATA_SUCCESS' export const SAVE_AUTH_TOKEN = 'SAVE_AUTH_TOKEN' -export const UPLOAD_ATTACHMENT_PENDING = 'UPLOAD_ATTACHMENT_PENDING' -export const UPLOAD_ATTACHMENT_FAILURE = 'UPLOAD_ATTACHMENT_FAILURE' -export const UPLOAD_ATTACHMENT_SUCCESS = 'UPLOAD_ATTACHMENT_SUCCESS' +export const CREATE_ATTACHMENT_PENDING = 'CREATE_ATTACHMENT_PENDING' +export const CREATE_ATTACHMENT_FAILURE = 'CREATE_ATTACHMENT_FAILURE' +export const CREATE_ATTACHMENT_SUCCESS = 'CREATE_ATTACHMENT_SUCCESS' + +export const REMOVE_ATTACHMENT_PENDING = 'REMOVE_ATTACHMENT_PENDING' +export const REMOVE_ATTACHMENT_FAILURE = 'REMOVE_ATTACHMENT_FAILURE' +export const REMOVE_ATTACHMENT_SUCCESS = 'REMOVE_ATTACHMENT_SUCCESS' export const LOAD_CHALLENGE_RESOURCES = 'LOAD_CHALLENGE_RESOURCES' export const LOAD_CHALLENGE_RESOURCES_SUCCESS = 'LOAD_CHALLENGE_RESOURCES_SUCCESS' @@ -85,8 +111,6 @@ export const DELETE_CHALLENGE_RESOURCE_SUCCESS = 'DELETE_CHALLENGE_RESOURCE_SUCC export const DELETE_CHALLENGE_RESOURCE_PENDING = 'DELETE_CHALLENGE_RESOURCE_PENDING' export const DELETE_CHALLENGE_RESOURCE_FAILURE = 'DELETE_CHALLENGE_RESOURCE_FAILURE' -export const REMOVE_ATTACHMENT = 'REMOVE_ATTACHMENT' - export const SET_FILTER_CHALLENGE_VALUE = 'SET_FILTER_CHALLENGE_VALUE' export const RESET_SIDEBAR_ACTIVE_PARAMS = 'RESET_SIDEBAR_ACTIVE_PARAMS' @@ -162,7 +186,7 @@ export const ADMIN_ROLES = [ ] export const downloadAttachmentURL = (challengeId, attachmentId, token) => - `${CHALLENGE_API_URL}/${challengeId}/attachments/${attachmentId}?token=${token}` + `${CHALLENGE_API_URL}/${challengeId}/attachments/${attachmentId}/download?token=${token}` export const PAGE_SIZE = 50 diff --git a/src/containers/ChallengeEditor/index.js b/src/containers/ChallengeEditor/index.js index 7d264a8d..8b3ea2da 100644 --- a/src/containers/ChallengeEditor/index.js +++ b/src/containers/ChallengeEditor/index.js @@ -359,6 +359,7 @@ class ChallengeEditor extends Component { metadata={metadata} projectDetail={projectDetail} challenge={challengeDetails} + attachments={attachments} challengeResources={challengeResources} token={token} challengeId={challengeId} diff --git a/src/reducers/challenges.js b/src/reducers/challenges.js index 20698d89..8bffe831 100644 --- a/src/reducers/challenges.js +++ b/src/reducers/challenges.js @@ -15,10 +15,12 @@ import { LOAD_CHALLENGES_FAILURE, LOAD_CHALLENGES_PENDING, LOAD_CHALLENGES_SUCCESS, - UPLOAD_ATTACHMENT_FAILURE, - UPLOAD_ATTACHMENT_SUCCESS, - UPLOAD_ATTACHMENT_PENDING, - REMOVE_ATTACHMENT, + CREATE_ATTACHMENT_FAILURE, + CREATE_ATTACHMENT_SUCCESS, + CREATE_ATTACHMENT_PENDING, + REMOVE_ATTACHMENT_FAILURE, + REMOVE_ATTACHMENT_SUCCESS, + REMOVE_ATTACHMENT_PENDING, SET_FILTER_CHALLENGE_VALUE, UPDATE_CHALLENGE_DETAILS_FAILURE, UPDATE_CHALLENGE_DETAILS_SUCCESS, @@ -53,12 +55,6 @@ const initialState = { projectId: -1 } -function toastrSuccess (title, message) { - setImmediate(() => { - toastr.success(title, message) - }) -} - function toastrFailure (title, message) { setImmediate(() => { toastr.error(title, message) @@ -66,7 +62,6 @@ function toastrFailure (title, message) { } export default function (state = initialState, action) { - let attachments switch (action.type) { case LOAD_CHALLENGES_SUCCESS: return { @@ -243,23 +238,68 @@ export default function (state = initialState, action) { case LOAD_CHALLENGE_MEMBERS_SUCCESS: { return { ...state, metadata: { ...state.metadata, members: action.members } } } - case UPLOAD_ATTACHMENT_PENDING: - return { ...state, isUploading: true, isSuccess: false, uploadingId: action.challengeId } - case UPLOAD_ATTACHMENT_SUCCESS: - toastrSuccess('Success', `${action.filename} uploaded successfully. Save the challenge to reflect the changes!`) - attachments = _.cloneDeep(state.attachments) - attachments.push(action.attachment) - return { ...state, isUploading: false, isSuccess: true, uploadingId: null, attachments } - case UPLOAD_ATTACHMENT_FAILURE: - toastrFailure('Upload failure', `Failed to upload ${action.filename}`) - return { ...state, isUploading: false, isSuccess: false, uploadingId: null } - case REMOVE_ATTACHMENT: - attachments = _.filter(state.attachments, item => { + case CREATE_ATTACHMENT_PENDING: { + const attachments = [ + ...(state.attachments || []), + { + uploadingId: action.uploadingId, + name: action.file.name, + fileSize: action.file.fileSize, + isUploading: true + } + ] + return { ...state, attachments } + } + case CREATE_ATTACHMENT_SUCCESS: { + const attachments = _.map(state.attachments, item => { + if (item.uploadingId !== action.uploadingId) { + return item + } else { + return action.attachment + } + }) + return { ...state, attachments } + } + case CREATE_ATTACHMENT_FAILURE: { + toastrFailure('Upload failure', `Failed to upload ${action.file.name}`) + const attachments = _.reject(state.attachments, { + uploadingId: action.uploadingId + }) + return { ...state, attachments } + } + case REMOVE_ATTACHMENT_PENDING: { + const attachments = _.map(state.attachments, item => { + if (item.id !== action.attachmentId) { + return item + } else { + return { + ...item, + isDeleting: true + } + } + }) + return { ...state, attachments } + } + case REMOVE_ATTACHMENT_SUCCESS: { + const attachments = _.reject(state.attachments, { + id: action.attachmentId + }) + return { ...state, attachments } + } + case REMOVE_ATTACHMENT_FAILURE: { + toastrFailure('Removing failure', `Failed to remove attachment`) + const attachments = _.map(state.attachments, item => { if (item.id !== action.attachmentId) { return item + } else { + return { + ...item, + isDeleting: false + } } }) return { ...state, attachments } + } case SET_FILTER_CHALLENGE_VALUE: return { ...state, filterChallengeName: action.value.name, status: action.value.status } default: diff --git a/src/services/challenges.js b/src/services/challenges.js index 49bce2dc..95202881 100644 --- a/src/services/challenges.js +++ b/src/services/challenges.js @@ -2,7 +2,6 @@ import _ from 'lodash' import qs from 'qs' import { axiosInstance } from './axiosWithAuth' import { updateChallengePhaseBeforeSendRequest, convertChallengePhaseFromSecondsToHours, normalizeChallengeDataFromAPI } from '../util/date' -import FormData from 'form-data' import { GROUPS_DROPDOWN_PER_PAGE } from '../config/constants' const { CHALLENGE_API_URL, @@ -139,12 +138,30 @@ export function updateChallenge (challengeId, challenge) { }) } -export function uploadAttachment (challengeId, file) { - const data = new FormData() - data.append('attachment', file) +/** + * Create attachment + * + * @param {String|Number} challengeId challenge id + * @param {String|Number} attachmentId attachment id + * + * @returns {Promise<*>} attachment data + */ +export function createAttachment (challengeId, data) { return axiosInstance.post(`${CHALLENGE_API_URL}/${challengeId}/attachments`, data) } +/** + * Remove attachment + * + * @param {String|Number} challengeId challenge id + * @param {String|Number} attachmentId attachment id + * + * @returns {Promise} + */ +export function removeAttachment (challengeId, attachmentId) { + return axiosInstance.delete(`${CHALLENGE_API_URL}/${challengeId}/attachments/${attachmentId}`) +} + /** * Fetch challenges from v5 API * @param filters From 8130075f1d3da68b111aa245ba3c8a80b1642c44 Mon Sep 17 00:00:00 2001 From: maxceem Date: Thu, 14 Jan 2021 17:03:39 +0200 Subject: [PATCH 30/32] fix: attachment fixes and implevements - updated code to use changed POST endpoint which accepts multiple attachments at once - fix callback which is called after all attachments are loaded - fix internal FilePicker logic, which had issue when uploading many attachments in the same time (replaced useState with useReducer) - fix view attachments mode - improve error handling - fix issue caused by reverting PR for attachments --- package-lock.json | 5 + src/actions/challenges.js | 19 +- .../ChallengeEditor/Attachment-Field/index.js | 36 +-- src/components/ChallengeEditor/index.js | 18 +- .../FilestackFilePicker.module.scss | 2 +- src/components/FilestackFilePicker/index.jsx | 208 +++++++++++++----- src/config/constants.js | 12 +- src/containers/ChallengeEditor/index.js | 12 +- src/reducers/challenges.js | 34 +-- src/services/challenges.js | 10 +- 10 files changed, 222 insertions(+), 134 deletions(-) diff --git a/package-lock.json b/package-lock.json index 17bf2acd..2c61a7d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1099,6 +1099,11 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz", "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==" }, + "@filestack/loader": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@filestack/loader/-/loader-1.0.8.tgz", + "integrity": "sha512-dqgvVy5zULZJVnaiFkhXFNmK/U1JWNR2HD1DBz7tW9xDxjR/nccGQJPaTd5M3eTm7jLZ7uO870Dq17UOLatR/Q==" + }, "@fortawesome/fontawesome-common-types": { "version": "0.2.28", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.28.tgz", diff --git a/src/actions/challenges.js b/src/actions/challenges.js index 8ead9d1d..6314ecbe 100644 --- a/src/actions/challenges.js +++ b/src/actions/challenges.js @@ -5,7 +5,7 @@ import { fetchGroups, fetchTimelineTemplates, fetchChallengePhases, - createAttachment as createAttachmentAPI, + createAttachments as createAttachmentsAPI, removeAttachment as removeAttachmentAPI, fetchChallenge, fetchChallenges, @@ -383,31 +383,24 @@ export function loadGroups () { } } -export function createAttachment (challengeId, file) { +export function createAttachments (challengeId, files) { return async (dispatch) => { - // create a temporary uploading id for each attachment - // so we can identify them for various actions (names theoretically can duplicate) - const uploadingId = _.uniqueId('uploadingId_') - dispatch({ type: CREATE_ATTACHMENT_PENDING, challengeId, - file, - uploadingId + files }) try { - const attachment = await createAttachmentAPI(challengeId, file) + const attachment = await createAttachmentsAPI(challengeId, files) dispatch({ type: CREATE_ATTACHMENT_SUCCESS, - attachment: attachment.data, - uploadingId + attachments: attachment.data }) } catch (error) { dispatch({ type: CREATE_ATTACHMENT_FAILURE, - file, - uploadingId + files }) } } diff --git a/src/components/ChallengeEditor/Attachment-Field/index.js b/src/components/ChallengeEditor/Attachment-Field/index.js index 18cae96a..9e4c099b 100644 --- a/src/components/ChallengeEditor/Attachment-Field/index.js +++ b/src/components/ChallengeEditor/Attachment-Field/index.js @@ -1,5 +1,5 @@ import _ from 'lodash' -import React from 'react' +import React, { useCallback } from 'react' import PropTypes from 'prop-types' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { downloadAttachmentURL, SPECIFICATION_ATTACHMENTS_FOLDER, getAWSContainerFileURL } from '../../../config/constants' @@ -8,18 +8,32 @@ import FilestackFilePicker from '../../FilestackFilePicker' import styles from './Attachment-Field.module.scss' import Loader from '../../Loader' -const AttachmentField = ({ challengeId, attachments, removeAttachment, onUploadFile, token, readOnly }) => { +const AttachmentField = ({ challengeId, attachments, removeAttachment, onUploadFiles, token, readOnly }) => { + // when all files are upload to the S3 this method would be called to create attachments via Challenge API + const onUploadDone = useCallback(({ filesUploaded }) => { + if (filesUploaded && filesUploaded.length > 0) { + onUploadFiles( + challengeId, + filesUploaded.map(file => ({ + name: file.originalFile.name, + fileSize: file.originalFile.size, + url: encodeURI(getAWSContainerFileURL(file.key)) + })) + ) + } + }, [challengeId, onUploadFiles]) + const renderAttachments = (attachments) => ( _.map(attachments, (att, index) => ( -
+
{att.name}
{formatBytes(att.fileSize)}
{!readOnly && (
- {!att.isDeleting && !att.isUploading && ( + {!att.isDeleting && att.id && ( removeAttachment(challengeId, att.id)} className={styles.removeIcon} /> )} - {(att.isDeleting || att.isUploading) && ( + {(att.isDeleting || !att.id) && (
)}
@@ -47,11 +61,7 @@ const AttachmentField = ({ challengeId, attachments, removeAttachment, onUploadF
onUploadFile(challengeId, { - name: file.filename, - fileSize: file.size, - url: getAWSContainerFileURL(file.key) - })} + onUploadDone={onUploadDone} />
)} @@ -61,7 +71,7 @@ const AttachmentField = ({ challengeId, attachments, removeAttachment, onUploadF
File Name
Size
-
Action
+ {!readOnly &&
Action
}
{ renderAttachments(attachments) }
@@ -73,7 +83,7 @@ const AttachmentField = ({ challengeId, attachments, removeAttachment, onUploadF AttachmentField.defaultProps = { removeAttachment: () => {}, - onUploadFile: () => {}, + onUploadFiles: () => {}, readOnly: false, attachments: [] } @@ -82,7 +92,7 @@ AttachmentField.propTypes = { challengeId: PropTypes.string.isRequired, attachments: PropTypes.array, removeAttachment: PropTypes.func, - onUploadFile: PropTypes.func, + onUploadFiles: PropTypes.func, token: PropTypes.string.isRequired, readOnly: PropTypes.bool } diff --git a/src/components/ChallengeEditor/index.js b/src/components/ChallengeEditor/index.js index c05fff24..36d7ff7f 100644 --- a/src/components/ChallengeEditor/index.js +++ b/src/components/ChallengeEditor/index.js @@ -755,18 +755,6 @@ class ChallengeEditor extends Component { this.setState({ challenge: newChallenge }) } - onUploadFile (files) { - const { challenge: oldChallenge } = this.state - const newChallenge = { ...oldChallenge } - _.forEach(files, (file) => { - newChallenge.attachments.push({ - fileName: file.name, - size: file.size - }) - }) - this.setState({ challenge: newChallenge }) - } - collectChallengeData (status) { const { attachments, metadata } = this.props const challenge = pick([ @@ -1141,7 +1129,7 @@ class ChallengeEditor extends Component { isNew, isLoading, metadata, - uploadAttachment, + uploadAttachments, token, removeAttachment, failedToLoad, @@ -1470,7 +1458,7 @@ class ChallengeEditor extends Component { challenge={{ ...challenge, id: currentChallengeId }} challengeId={currentChallengeId} attachments={attachments} - onUploadFile={uploadAttachment} + onUploadFiles={uploadAttachments} token={token} removeAttachment={removeAttachment} /> @@ -1525,7 +1513,7 @@ ChallengeEditor.propTypes = { challengeId: PropTypes.string, metadata: PropTypes.object.isRequired, isLoading: PropTypes.bool.isRequired, - uploadAttachment: PropTypes.func.isRequired, + uploadAttachments: PropTypes.func.isRequired, removeAttachment: PropTypes.func.isRequired, attachments: PropTypes.arrayOf(PropTypes.shape()), token: PropTypes.string.isRequired, diff --git a/src/components/FilestackFilePicker/FilestackFilePicker.module.scss b/src/components/FilestackFilePicker/FilestackFilePicker.module.scss index 1a9de2b0..99cb0b13 100644 --- a/src/components/FilestackFilePicker/FilestackFilePicker.module.scss +++ b/src/components/FilestackFilePicker/FilestackFilePicker.module.scss @@ -8,7 +8,7 @@ padding: 20px; border: 1px solid $tc-gray-40; border-radius: 6px; - height: 227px; + min-height: 227px; position: relative; font-size: 16px; font-weight: 400; diff --git a/src/components/FilestackFilePicker/index.jsx b/src/components/FilestackFilePicker/index.jsx index e6c872e8..53459896 100644 --- a/src/components/FilestackFilePicker/index.jsx +++ b/src/components/FilestackFilePicker/index.jsx @@ -5,7 +5,7 @@ * - Supports multiple file uploading. */ import _ from 'lodash' -import React, { useEffect, useLayoutEffect, useRef, useState } from 'react' +import React, { useEffect, useLayoutEffect, useReducer, useRef, useState } from 'react' import PT from 'prop-types' import * as filestack from 'filestack-js' import cn from 'classnames' @@ -18,12 +18,59 @@ import { FILE_PICKER_ACCEPT, FILE_PICKER_MAX_SIZE, FILE_PICKER_MAX_FILES, - FILE_PICKER_PROGRESS_INTERVAL + FILE_PICKER_PROGRESS_INTERVAL, + FILE_PICKER_UPLOAD_RETRY, + FILE_PICKER_UPLOAD_TIMEOUT } from '../../config/constants' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons' import styles from './FilestackFilePicker.module.scss' +const initialState = [] + +const ACTION = { + UPDATE_FILE: 'UPDATE_FILE', + SET_FILES: 'SET_FILES', + CLEAR_FILES: 'CLEAR_FILES' +} + +const reducer = (state, action) => { + switch (action.type) { + case ACTION.UPDATE_FILE: { + const { filename, updated } = action.payload + const uploadingFileIndex = _.findIndex(state, { filename }) + + if (uploadingFileIndex > -1) { + const updatedFile = { + ...state[uploadingFileIndex], + ...updated + } + + const newState = [ + ...state.slice(0, uploadingFileIndex), + updatedFile, + ...state.slice(uploadingFileIndex + 1) + ] + + return newState + } + + return state + } + + case ACTION.SET_FILES: { + return action.payload + } + + case ACTION.CLEAR_FILES: { + return initialState + } + + default: + throw new Error() + } +} + /** * FilestackFilePicker component */ @@ -34,13 +81,13 @@ const FilestackFilePicker = ({ onUploadDone }) => { // the list of filenames which are currently being uploaded - const [uploadingFiles, setUploadingFiles] = useState([]) + // we cannot utilize `useState` here, because we need to update the items in the uploading files array at random points of time + // if we use state, then it could happen, that 2 updates happen at the same time overriding results of each other. + const [uploadingFiles, dispatch] = useReducer(reducer, initialState) // if something is currently dragged over the area const [dragged, setDragged] = useState(false) // Filestack client instance const filestackRef = useRef(null) - // we have to use ref for this method, because filestack would be initialized once with a callback using this method - const updateUploadingFile = useRef() // init Filestack (without waiting for rendering) useLayoutEffect(() => { @@ -49,34 +96,7 @@ const FilestackFilePicker = ({ }) }, []) - // update the ref to `updateUploadingFile` to keep referencing fresh state data - useEffect(() => { - updateUploadingFile.current = (filename, updated) => { - const uploadingFileIndex = _.findIndex(uploadingFiles, { filename }) - - if (uploadingFileIndex > -1) { - const updatedFile = { - ...uploadingFiles[uploadingFileIndex], - ...updated - } - - setUploadingFiles([ - ...uploadingFiles.slice(0, uploadingFileIndex), - updatedFile, - ...uploadingFiles.slice(uploadingFileIndex + 1) - ]) - - return updatedFile - } - } - }, [uploadingFiles, setUploadingFiles]) - useEffect(() => { - // if all files have been uploaded successfully, clean uploading file list - if (uploadingFiles.length > 0 && _.every(uploadingFiles, 'file')) { - setUploadingFiles([]) - } - // if all files are fully loaded or error happens for them call `onUploadDone` callback if ( uploadingFiles.length > 0 && @@ -87,23 +107,40 @@ const FilestackFilePicker = ({ const filesUploaded = _.filter(uploadingFiles, 'file') onUploadDone({ - filesFailed: _.map(filesFailed, 'file'), + filesFailed: _.map(filesFailed, 'error'), filesUploaded: _.map(filesUploaded, 'file') }) } } - }, [uploadingFiles, setUploadingFiles, onUploadDone]) + + // if all files have been uploaded successfully, clean uploading file list + if (uploadingFiles.length > 0 && _.every(uploadingFiles, 'file')) { + dispatch({ type: ACTION.CLEAR_FILES }) + } + }, [uploadingFiles]) /** * Handle for success file(s) uploading * + * NOTE: this method used as callback in two different methods: + * `filestackRef.current.picker` and `filestackRef.current.upload` + * They call this method with slightly different arguments data. + * I've partially normalized the argument this method is called with, + * but not completely. So if you make any changes, test it using both + * methods of uploading: Drag & Drop and FileStack Picker (on click) + * * @param {Object} file upload file info */ const handleFileUploadSuccess = (file) => { - console.log('handleFileUploadSuccess', file) - updateUploadingFile.current(file.name, { - file, // set `file` to indicate that file uploaded - progress: 100 // make sure that progress is set to 100 when uploading is complete + dispatch({ + type: ACTION.UPDATE_FILE, + payload: { + filename: file.originalFile.name, + updated: { + file, // set `file` to indicate that file uploaded + progress: 100 // make sure that progress is set to 100 when uploading is complete + } + } }) onFileUploadFinished && onFileUploadFinished(file) } @@ -111,14 +148,26 @@ const FilestackFilePicker = ({ /** * Handle for error during file(s) uploading * - * @param {Object|String} error error during file uploading + * NOTE: this method used as callback in two different methods: + * `filestackRef.current.picker` and `filestackRef.current.upload` + * They call this method with slightly different arguments data. + * I've partially normalized the argument this method is called with, + * but not completely. So if you make any changes, test it using both + * methods of uploading: Drag & Drop and FileStack Picker (on click) + * + * @param {Object} error error during file uploading */ - const handleFileUploadError = (file) => { - updateUploadingFile.current(file.name, { - file, // set `file` to indicate that file uploaded - progress: 100 // make sure that progress is set to 100 when uploading is complete + const handleFileUploadError = (error) => { + dispatch({ + type: ACTION.UPDATE_FILE, + payload: { + filename: error.originalFile.name, + updated: { + error: error + } + } }) - onFileUploadFailed && onFileUploadFailed(file) + onFileUploadFailed && onFileUploadFailed(error) } /** @@ -131,21 +180,37 @@ const FilestackFilePicker = ({ fromSources: FILE_PICKER_FROM_SOURCES, maxSize: FILE_PICKER_MAX_SIZE, maxFiles: FILE_PICKER_MAX_FILES, + uploadConfig: { + retry: FILE_PICKER_UPLOAD_RETRY, + timeout: FILE_PICKER_UPLOAD_TIMEOUT + }, onUploadStarted: (files) => { - setUploadingFiles( - files.map((file) => ({ + dispatch({ + type: ACTION.SET_FILES, + payload: files.map((file) => ({ filename: file.filename, progress: 0, file: null, error: null })) - ) + }) + }, + onFileUploadFailed: (file, event) => { + const error = new Error(event.status) + error.originalFile = file.originalFile + + handleFileUploadError(error) }, - onFileUploadFailed: handleFileUploadError, onFileUploadFinished: handleFileUploadSuccess, - onFileUploadProgress: (file, progressInfo) => { - updateUploadingFile.current(file.filename, { - progress: progressInfo.totalPercent + onFileUploadProgress: (file, event) => { + dispatch({ + type: ACTION.UPDATE_FILE, + payload: { + filename: file.filename, + updated: { + progress: event.totalPercent + } + } }) }, startUploadingWhenMaxFilesReached: true, @@ -173,24 +238,29 @@ const FilestackFilePicker = ({ let error = null if (!_.includes(FILE_PICKER_ACCEPT, fileExt)) { - error = `Not allowed file type "${fileExt}".` + error = new Error(`Not allowed file type "${fileExt}".`) + error.originalFile = _.pick(file, ['name', 'type', 'size']) } if (index + 1 > FILE_PICKER_MAX_FILES) { - error = `File skipped, because can upload maximum ${FILE_PICKER_MAX_FILES} files at once.` + error = new Error(`File skipped, because can upload maximum ${FILE_PICKER_MAX_FILES} files at once.`) + error.originalFile = _.pick(file, ['name', 'type', 'size']) } return { filename: file.name, progress: 0, - file, + file: file, error } }) const filesToUpload = _.map(_.reject(files, 'error'), 'file') - setUploadingFiles(files.map((file) => ({ ...file, file: null }))) + dispatch({ + type: ACTION.SET_FILES, + payload: files.map((file) => ({ ...file, file: null })) + }) filesToUpload.map((file) => filestackRef.current @@ -198,11 +268,19 @@ const FilestackFilePicker = ({ file, { onProgress: ({ totalPercent }) => { - updateUploadingFile.current(file.name, { - progress: totalPercent + dispatch({ + type: ACTION.UPDATE_FILE, + payload: { + filename: file.name, + updated: { + progress: totalPercent + } + } }) }, - progressInterval: FILE_PICKER_PROGRESS_INTERVAL + progressInterval: FILE_PICKER_PROGRESS_INTERVAL, + retry: FILE_PICKER_UPLOAD_RETRY, + timeout: FILE_PICKER_UPLOAD_TIMEOUT }, { container: FILE_PICKER_CONTAINER_NAME, @@ -210,8 +288,16 @@ const FilestackFilePicker = ({ region: FILE_PICKER_REGION } ) - .then(handleFileUploadSuccess) - .catch(handleFileUploadError) + .then((event) => handleFileUploadSuccess({ + ...event, + originalFile: _.pick(file, ['name', 'type', 'size']) + })) + .catch((event) => { + const error = new Error(event.status) + error.originalFile = _.pick(file, ['name', 'type', 'size']) + + handleFileUploadError(error) + }) ) } @@ -244,7 +330,7 @@ const FilestackFilePicker = ({
{uploadingFile.filename} ( {uploadingFile.error ? ( - {uploadingFile.error} + {uploadingFile.error.toString()} ) : ( `${uploadingFile.progress}%` )} diff --git a/src/config/constants.js b/src/config/constants.js index 52f406a3..e8eab683 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -24,21 +24,19 @@ export const CREATE_FORUM_TYPE_IDS = typeof process.env.CREATE_FORUM_TYPE_IDS == // to be able to start the Connect App we should pass at least the dummy value for `FILE_PICKER_API_KEY` // 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' -// TODO uncomment this line to use correct `tc-challenge-v5-dev` bucket for DEV -// export const FILE_PICKER_CONTAINER_NAME = prcess.env.FILE_PICKER_CONTAINER_NAME || 'tc-challenge-v5-dev' -export const FILE_PICKER_CONTAINER_NAME = 'submission-staging-dev' +export const FILE_PICKER_CONTAINER_NAME = process.env.FILE_PICKER_CONTAINER_NAME || 'tc-challenge-v5-dev' export const FILE_PICKER_REGION = process.env.FILE_PICKER_REGION || 'us-east-1' 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 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 +export const FILE_PICKER_MAX_SIZE = 500 * 1024 * 1024 // 500Mb export const FILE_PICKER_PROGRESS_INTERVAL = 100 +export const FILE_PICKER_UPLOAD_RETRY = 2 +export const FILE_PICKER_UPLOAD_TIMEOUT = 30 * 60 * 1000 // 30 minutes export const SPECIFICATION_ATTACHMENTS_FOLDER = 'SPECIFICATION_ATTACHMENTS' -// TODO uncomment this line to use the same bucket as we during FileStack uploading -// export const getAWSContainerFileURL = (key) => `https://${FILE_PICKER_CONTAINER_NAME}.s3.amazonaws.com/${key}` -export const getAWSContainerFileURL = (key) => `https://tc-challenge-v5-dev.s3.amazonaws.com/${key}` +export const getAWSContainerFileURL = (key) => `https://${FILE_PICKER_CONTAINER_NAME}.s3.amazonaws.com/${key}` // Actions export const LOAD_PROJECTS_SUCCESS = 'LOAD_PROJECTS_SUCCESS' diff --git a/src/containers/ChallengeEditor/index.js b/src/containers/ChallengeEditor/index.js index 8b3ea2da..51ffaa26 100644 --- a/src/containers/ChallengeEditor/index.js +++ b/src/containers/ChallengeEditor/index.js @@ -19,7 +19,7 @@ import { // loadChallengeTerms, loadGroups, loadChallengeDetails, - createAttachment, + createAttachments, removeAttachment, loadResources, loadResourceRoles, @@ -227,7 +227,7 @@ class ChallengeEditor extends Component { // challengeDetails, challengeResources, metadata, - createAttachment, + createAttachments, attachments, token, removeAttachment, @@ -307,7 +307,7 @@ class ChallengeEditor extends Component { projectId={_.get(match.params, 'projectId', null)} challengeId={challengeId} isNew={!_.has(match.params, 'challengeId')} - uploadAttachment={createAttachment} + uploadAttachments={createAttachments} attachments={attachments} token={token} removeAttachment={removeAttachment} @@ -334,7 +334,7 @@ class ChallengeEditor extends Component { projectId={_.get(match.params, 'projectId', null)} challengeId={challengeId} isNew={!_.has(match.params, 'challengeId')} - uploadAttachment={createAttachment} + uploadAttachments={createAttachments} attachments={attachments} token={token} removeAttachment={removeAttachment} @@ -403,7 +403,7 @@ ChallengeEditor.propTypes = { challengeTypes: PropTypes.array }), isLoading: PropTypes.bool, - createAttachment: PropTypes.func, + createAttachments: PropTypes.func, attachments: PropTypes.arrayOf(PropTypes.shape()), token: PropTypes.string, loggedInUser: PropTypes.object, @@ -441,7 +441,7 @@ const mapDispatchToProps = { loadChallengeTimelines, loadChallengeTags, loadGroups, - createAttachment, + createAttachments, removeAttachment, // loadChallengeTerms, loadResources, diff --git a/src/reducers/challenges.js b/src/reducers/challenges.js index 8bffe831..0019229b 100644 --- a/src/reducers/challenges.js +++ b/src/reducers/challenges.js @@ -61,6 +61,12 @@ function toastrFailure (title, message) { }) } +function toastrSuccess (title, message) { + setImmediate(() => { + toastr.success(title, message) + }) +} + export default function (state = initialState, action) { switch (action.type) { case LOAD_CHALLENGES_SUCCESS: @@ -241,30 +247,32 @@ export default function (state = initialState, action) { case CREATE_ATTACHMENT_PENDING: { const attachments = [ ...(state.attachments || []), - { - uploadingId: action.uploadingId, - name: action.file.name, - fileSize: action.file.fileSize, - isUploading: true - } + // file that we are uploading at the moment + // they are different from attachments, because they don't have `id` + ...action.files ] return { ...state, attachments } } case CREATE_ATTACHMENT_SUCCESS: { const attachments = _.map(state.attachments, item => { - if (item.uploadingId !== action.uploadingId) { - return item - } else { - return action.attachment + // as `url` is unique we can use to replace files which were uploading with uploaded attachments + const createdAttachment = _.find(action.attachments, { + url: item.url + }) + + if (createdAttachment) { + return createdAttachment } + + return item }) return { ...state, attachments } } case CREATE_ATTACHMENT_FAILURE: { toastrFailure('Upload failure', `Failed to upload ${action.file.name}`) - const attachments = _.reject(state.attachments, { - uploadingId: action.uploadingId - }) + const attachments = _.reject(state.attachments, (attachment) => + _.find(action.files, { url: attachment.url }) + ) return { ...state, attachments } } case REMOVE_ATTACHMENT_PENDING: { diff --git a/src/services/challenges.js b/src/services/challenges.js index 95202881..321b6ee1 100644 --- a/src/services/challenges.js +++ b/src/services/challenges.js @@ -139,15 +139,15 @@ export function updateChallenge (challengeId, challenge) { } /** - * Create attachment + * Create attachments * * @param {String|Number} challengeId challenge id - * @param {String|Number} attachmentId attachment id + * @param {Object[]} attachments list of attachments * - * @returns {Promise<*>} attachment data + * @returns {Promise<*>} attachments data */ -export function createAttachment (challengeId, data) { - return axiosInstance.post(`${CHALLENGE_API_URL}/${challengeId}/attachments`, data) +export function createAttachments (challengeId, attachments) { + return axiosInstance.post(`${CHALLENGE_API_URL}/${challengeId}/attachments`, attachments) } /** From 918759c89d8ca28764c00907e24b508ca0419e8c Mon Sep 17 00:00:00 2001 From: maxceem Date: Sat, 16 Jan 2021 10:42:15 +0200 Subject: [PATCH 31/32] chore: debug build script --- scripts/build.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/build.js b/scripts/build.js index 6d429742..eef55b65 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -10,6 +10,8 @@ process.on('unhandledRejection', err => { // Ensure environment variables are read. require('../config/env') +console.log(`Build script is run with FILE_PICKER_API_KEY=${process.env.FILE_PICKER_API_KEY}`) + const path = require('path') const chalk = require('chalk') const fs = require('fs-extra') From b13ec4e7e028aa2bf0ac2080d4a5c348604bfa3d Mon Sep 17 00:00:00 2001 From: Gunasekar-K Date: Tue, 19 Jan 2021 12:24:40 +0530 Subject: [PATCH 32/32] build var integration --- .circleci/config.yml | 13 +++++++++++-- build.sh | 2 +- docker/Dockerfile | 5 +++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index ed9f79f1..8a204949 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -28,11 +28,20 @@ builddeploy_steps: &builddeploy_steps - setup_remote_docker - run: *install_dependency - run: *install_deploysuite - - run: ./build.sh ${APPNAME} + - run: + name: "configuring environment" + command: | + ./awsconfiguration.sh $DEPLOY_ENV + ./buildenv.sh -e $DEPLOY_ENV -b ${LOGICAL_ENV}-${APPNAME}-buildvar + - run: + name: "building image" + command: | + source buildenvvar + ./build.sh ${APPNAME} - deploy: name: Running MasterScript. command: | - ./awsconfiguration.sh $DEPLOY_ENV + #./awsconfiguration.sh $DEPLOY_ENV source awsenvconf ./buildenv.sh -e $DEPLOY_ENV -b ${LOGICAL_ENV}-${APPNAME}-deployvar source buildenvvar diff --git a/build.sh b/build.sh index 37024115..1e833a31 100755 --- a/build.sh +++ b/build.sh @@ -4,7 +4,7 @@ APP_NAME=$1 UPDATE_CACHE="" echo "NODE ENV: $NODE_ENV" echo "BABEL ENV: $BABEL_ENV" -docker-compose -f docker/docker-compose.yml build --build-arg NODE_ENV=$NODE_ENV --build-arg BABEL_ENV=$BABEL_ENV $APP_NAME +docker-compose -f docker/docker-compose.yml build --build-arg NODE_ENV=$NODE_ENV --build-arg BABEL_ENV=$BABEL_ENV --build-arg FILE_PICKER_API_KEY=$FILE_PICKER_API_KEY --build-arg FORCE_DEV=$FORCE_DEV $APP_NAME docker create --name app $APP_NAME:latest if [ -d node_modules ] diff --git a/docker/Dockerfile b/docker/Dockerfile index d645cae9..c99ad38b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -3,8 +3,13 @@ FROM node:latest ARG NODE_ENV ARG BABEL_ENV +ARG FILE_PICKER_API_KEY +ARG FORCE_DEV + ENV NODE_ENV=$NODE_ENV ENV BABEL_ENV=$BABEL_ENV +ENV FILE_PICKER_API_KEY=$FILE_PICKER_API_KEY +ENV FORCE_DEV=$FORCE_DEV # Copy the current directory into the Docker image COPY . /challenge-engine-ui