From 5b0eb3d72674a6e42d4bd3e96253442538adafc9 Mon Sep 17 00:00:00 2001 From: maxceem Date: Fri, 4 Dec 2020 01:43:55 +0200 Subject: [PATCH 1/3] 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 2/3] 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 3/3] 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)} />
)}