diff --git a/config/constants/development.js b/config/constants/development.js
index ad9b6b81..2c8bc0bc 100644
--- a/config/constants/development.js
+++ b/config/constants/development.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: '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 f1606001..042b391f 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..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 802ff8ae..6f8c507f 100644
--- a/src/components/ChallengeEditor/ChallengeView/index.js
+++ b/src/components/ChallengeEditor/ChallengeView/index.js
@@ -27,6 +27,7 @@ import { MESSAGE } from '../../../config/constants'
const ChallengeView = ({
projectDetail,
challenge,
+ attachments,
metadata,
challengeResources,
token,
@@ -209,13 +210,12 @@ const ChallengeView = ({
challenge={challenge}
readOnly
/>
- { false && (
-
- )}
+
@@ -244,6 +244,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 adedeeda..02e3f2c3 100644
--- a/src/components/ChallengeEditor/index.js
+++ b/src/components/ChallengeEditor/index.js
@@ -92,7 +92,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)
@@ -536,15 +535,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
@@ -1037,7 +1027,8 @@ class ChallengeEditor extends Component {
token,
removeAttachment,
failedToLoad,
- projectDetail
+ projectDetail,
+ attachments
} = this.props
if (_.isEmpty(challenge)) {
return Error loading challenge
@@ -1344,14 +1335,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 581b9eb0..6a013193 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 c4033093..a7641308 100644
--- a/src/containers/ChallengeEditor/index.js
+++ b/src/containers/ChallengeEditor/index.js
@@ -344,6 +344,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