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/README.md b/README.md
index 6a9716bc..47540cab 100644
--- a/README.md
+++ b/README.md
@@ -39,8 +39,6 @@ npm install
1. copy the environment file in docs/dev.env to /.env
-1. add `127.0.0.1 local.topcoder-dev.com` to your /etc/hosts file
-
1. If you are using local instances of the API's, change the DEV_API_HOSTNAME in configs/constants/development.js to match your local api endpoint.
- For example change it to 'http://localhost:3000/',
@@ -50,7 +48,7 @@ npm install
npm run dev
```
-You can access the app from [http://local.topcoder-dev.com:3001/](http://local.topcoder-dev.com:3001/)
+You can access the app from [http://localhost:3000/](http://localhost:3000/)
The page will reload if you make edits.
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/config/constants/development.js b/config/constants/development.js
index 7043e5b0..2f660373 100644
--- a/config/constants/development.js
+++ b/config/constants/development.js
@@ -7,7 +7,6 @@ module.exports = {
COMMUNITY_APP_URL: `https://www.${DOMAIN}`,
MEMBER_API_URL: `${DEV_API_HOSTNAME}/v4/members`,
MEMBER_API_V3_URL: `${DEV_API_HOSTNAME}/v3/members`,
- DEV_APP_URL: `http://local.${DOMAIN}`,
CHALLENGE_API_URL: `${DEV_API_HOSTNAME}/v5/challenges`,
CHALLENGE_TIMELINE_TEMPLATES_URL: `${DEV_API_HOSTNAME}/v5/timeline-templates`,
CHALLENGE_TYPES_URL: `${DEV_API_HOSTNAME}/v5/challenge-types`,
@@ -32,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 e690824d..a84e7373 100644
--- a/config/constants/production.js
+++ b/config/constants/production.js
@@ -7,7 +7,6 @@ module.exports = {
COMMUNITY_APP_URL: `https://www.${DOMAIN}`,
MEMBER_API_URL: `${PROD_API_HOSTNAME}/v4/members`,
MEMBER_API_V3_URL: `${PROD_API_HOSTNAME}/v3/members`,
- DEV_APP_URL: `https://submission-review.${DOMAIN}`,
CHALLENGE_API_URL: `${PROD_API_HOSTNAME}/v5/challenges`,
CHALLENGE_TIMELINE_TEMPLATES_URL: `${PROD_API_HOSTNAME}/v5/timeline-templates`,
CHALLENGE_TYPES_URL: `${PROD_API_HOSTNAME}/v5/challenge-types`,
@@ -32,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/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
diff --git a/package-lock.json b/package-lock.json
index b0c37594..2c61a7d0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1018,6 +1018,92 @@
"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=="
+ },
+ "@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 +1152,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 +1855,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 +2074,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",
@@ -2157,6 +2277,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 +2324,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 +4307,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 +4689,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",
@@ -5774,6 +5937,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 +5994,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 +6024,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",
@@ -5923,8 +6122,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",
@@ -6024,16 +6222,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 +8361,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 +9147,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 +9366,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 +9464,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",
@@ -9342,6 +9550,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",
@@ -10233,6 +10446,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 +14773,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",
@@ -14723,13 +14934,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 +14984,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",
@@ -16287,6 +16514,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",
@@ -17127,8 +17359,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"
},
@@ -17461,6 +17693,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 +19902,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 1f0c041e..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",
@@ -79,7 +78,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/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')
diff --git a/scripts/start.js b/scripts/start.js
index b7aaf362..71d668f7 100644
--- a/scripts/start.js
+++ b/scripts/start.js
@@ -99,7 +99,7 @@ checkBrowsers(paths.appPath, isInteractive)
clearConsole()
}
console.log(chalk.cyan('Starting the development server...\n'))
- openBrowser(constants.DEV_APP_URL ? `${constants.DEV_APP_URL}:${process.env.PORT || 3000}` : urls.localUrlForBrowser)
+ openBrowser(urls.localUrlForBrowser)
})
const SIGNALS = ['SIGINT', 'SIGTERM']
diff --git a/src/actions/challenges.js b/src/actions/challenges.js
index eca362f6..6314ecbe 100644
--- a/src/actions/challenges.js
+++ b/src/actions/challenges.js
@@ -5,7 +5,8 @@ import {
fetchGroups,
fetchTimelineTemplates,
fetchChallengePhases,
- uploadAttachment,
+ createAttachments as createAttachmentsAPI,
+ removeAttachment as removeAttachmentAPI,
fetchChallenge,
fetchChallenges,
fetchChallengeTerms,
@@ -13,8 +14,10 @@ import {
fetchResourceRoles,
fetchChallengeTimelines,
fetchChallengeTracks,
+ fetchGroupDetail,
updateChallenge,
patchChallenge,
+ deleteChallenge as deleteChallengeAPI,
createChallenge as createChallengeAPI,
createResource as createResourceAPI,
deleteResource as deleteResourceAPI
@@ -25,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,
@@ -38,6 +43,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'
@@ -182,6 +190,14 @@ export function loadChallengeDetails (projectId, challengeId) {
}
}
+/**
+ * Loads group details
+ */
+export function loadGroupDetails (groupIds) {
+ const promiseAll = groupIds.map(id => fetchGroupDetail(id))
+ return Promise.all(promiseAll)
+}
+
/**
* Update challenge details
*
@@ -267,6 +283,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()
@@ -347,39 +383,51 @@ export function loadGroups () {
}
}
-export function createAttachment (challengeId, file) {
- return async (dispatch, getState) => {
- const getUploadingId = () => _.get(getState(), 'challenge.uploadingId')
+export function createAttachments (challengeId, files) {
+ return async (dispatch) => {
+ dispatch({
+ type: CREATE_ATTACHMENT_PENDING,
+ challengeId,
+ files
+ })
- if (challengeId !== getUploadingId()) {
+ try {
+ const attachment = await createAttachmentsAPI(challengeId, files)
dispatch({
- type: UPLOAD_ATTACHMENT_PENDING,
- challengeId
+ type: CREATE_ATTACHMENT_SUCCESS,
+ attachments: attachment.data
+ })
+ } catch (error) {
+ dispatch({
+ type: CREATE_ATTACHMENT_FAILURE,
+ files
})
-
- 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/AssignedMember-Field/AssignedMember-Field.module.scss b/src/components/ChallengeEditor/AssignedMember-Field/AssignedMember-Field.module.scss
index 4f22fc27..33eea080 100644
--- a/src/components/ChallengeEditor/AssignedMember-Field/AssignedMember-Field.module.scss
+++ b/src/components/ChallengeEditor/AssignedMember-Field/AssignedMember-Field.module.scss
@@ -45,5 +45,10 @@
.readOnlyValue {
margin-bottom: 0.5rem; // the same like `label` to be aligned
}
+
+ .assignSelfField {
+ margin-left: 20px;
+ padding-top: 6px;
+ }
}
diff --git a/src/components/ChallengeEditor/AssignedMember-Field/index.js b/src/components/ChallengeEditor/AssignedMember-Field/index.js
index 4731518d..cf52873f 100644
--- a/src/components/ChallengeEditor/AssignedMember-Field/index.js
+++ b/src/components/ChallengeEditor/AssignedMember-Field/index.js
@@ -7,12 +7,13 @@ import cn from 'classnames'
import styles from './AssignedMember-Field.module.scss'
import SelectUserAutocomplete from '../../SelectUserAutocomplete'
-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 +29,15 @@ const AssignedMemberField = ({ challenge, onChange, assignedMemberDetails, readO
/>
)}
+ {
+ !readOnly &&
+
+ }
)
}
@@ -41,7 +51,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/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..9e4c099b 100644
--- a/src/components/ChallengeEditor/Attachment-Field/index.js
+++ b/src/components/ChallengeEditor/Attachment-Field/index.js
@@ -1,34 +1,43 @@
import _ from 'lodash'
import React, { useCallback } 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'
+import Loader from '../../Loader'
-const AttachmentField = ({ challenge, removeAttachment, onUploadFile, token, readOnly }) => {
- const onDrop = useCallback(acceptedFiles => {
- _.forEach(acceptedFiles, item => {
- onUploadFile(challenge.id, item)
- })
- }, [])
-
- const {
- getRootProps,
- getInputProps,
- isDragActive
- } = useDropzone({ onDrop })
+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.fileName}
+
+
{att.name}
{formatBytes(att.fileSize)}
- {!readOnly && (
removeAttachment(att.id)}>
-
-
)}
+ {!readOnly && (
+
+ {!att.isDeleting && att.id && (
+
removeAttachment(challengeId, att.id)} className={styles.removeIcon} />
+ )}
+ {(att.isDeleting || !att.id) && (
+
+ )}
+
+ )}
))
)
@@ -41,45 +50,31 @@ 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 && (
+
+
-
)}
+ )}
{
- _.has(challenge, 'attachments') && challenge.attachments.length > 0 && (
-
-
-
-
-
File Name
-
Size
-
Action
-
- { renderAttachments(challenge.attachments) }
+ attachments && attachments.length > 0 && (
+
+
+
File Name
+
Size
+ {!readOnly &&
Action
}
-
+ { renderAttachments(attachments) }
+
)
}
@@ -88,14 +83,16 @@ const AttachmentField = ({ challenge, removeAttachment, onUploadFile, token, rea
AttachmentField.defaultProps = {
removeAttachment: () => {},
- onUploadFile: () => {},
- readOnly: false
+ onUploadFiles: () => {},
+ readOnly: false,
+ attachments: []
}
AttachmentField.propTypes = {
- challenge: PropTypes.shape().isRequired,
+ 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/BillingAccount-Field/index.js b/src/components/ChallengeEditor/BillingAccount-Field/index.js
index 6b376899..80576bad 100644
--- a/src/components/ChallengeEditor/BillingAccount-Field/index.js
+++ b/src/components/ChallengeEditor/BillingAccount-Field/index.js
@@ -13,14 +13,12 @@ const BillingAccountField = ({ accounts, onUpdateSelect, challenge }) => {
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/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 {
) : (
{openAdvanceSettings && (
- Groups: {challenge.groups ? challenge.groups.join(', ') : ''}
+ Groups: {groups}
)}
{
@@ -209,13 +223,12 @@ const ChallengeView = ({
challenge={challenge}
readOnly
/>
- { false && (
-
- )}
+
@@ -244,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/Groups-Field/index.js b/src/components/ChallengeEditor/Groups-Field/index.js
index f9624450..d8f2b16f 100644
--- a/src/components/ChallengeEditor/Groups-Field/index.js
+++ b/src/components/ChallengeEditor/Groups-Field/index.js
@@ -1,38 +1,69 @@
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 { fetchGroups } from '../../../services/challenges'
+import { AUTOCOMPLETE_MIN_LENGTH, AUTOCOMPLETE_DEBOUNCE_TIME_MS } from '../../../config/constants'
+
+const GroupsField = ({ onUpdateMultiSelect, challenge }) => {
+ const [groups, setGroups] = React.useState([])
+
+ 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({ name: inputValue })
+ const suggestions = data.map(suggestion => ({
+ label: suggestion.name,
+ value: suggestion.id
+ }))
+ 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)
+ }, [])
-const GroupsField = ({ groups, onUpdateMultiSelect, challenge }) => {
return (
-
)
}
-GroupsField.defaultProps = {
- groups: []
-}
-
GroupsField.propTypes = {
onUpdateMultiSelect: PropTypes.func.isRequired,
- challenge: PropTypes.shape().isRequired,
- groups: PropTypes.arrayOf(PropTypes.shape()).isRequired
+ challenge: PropTypes.shape().isRequired
}
export default GroupsField
diff --git a/src/components/ChallengeEditor/ReviewType-Field/index.js b/src/components/ChallengeEditor/ReviewType-Field/index.js
index 139ea0a3..e726dc1c 100644
--- a/src/components/ChallengeEditor/ReviewType-Field/index.js
+++ b/src/components/ChallengeEditor/ReviewType-Field/index.js
@@ -1,3 +1,4 @@
+import _ from 'lodash'
import React from 'react'
import PropTypes from 'prop-types'
import Select from '../../Select'
@@ -9,10 +10,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 = _.get(challenge, 'task.isTask', false)
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()
}
@@ -77,14 +79,12 @@ const ReviewTypeField = ({ reviewers, challenge, onUpdateOthers, onUpdateSelect
isInternal && (
diff --git a/src/components/ChallengeEditor/Terms-Field/index.js b/src/components/ChallengeEditor/Terms-Field/index.js
index 2faf8643..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 (
@@ -15,11 +26,14 @@ const TermsField = ({ terms, challenge, onUpdateMultiSelect }) => {
onUpdateMultiSelect(value, 'terms')}
+ value={currTerms}
+ onChange={(value) => {
+ onUpdateMultiSelect(value, 'terms')
+ setCurrTerms(setCurrTerms(terms))
+ }}
/>
diff --git a/src/components/ChallengeEditor/Type-Field/index.js b/src/components/ChallengeEditor/Type-Field/index.js
index 0e9d9209..e92ae5c0 100644
--- a/src/components/ChallengeEditor/Type-Field/index.js
+++ b/src/components/ChallengeEditor/Type-Field/index.js
@@ -15,14 +15,11 @@ const TypeField = ({ types, onUpdateSelect, challenge, disabled }) => {
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 935b65ad..36d7ff7f 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,
@@ -90,12 +91,12 @@ 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)
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)
@@ -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,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
*
@@ -155,7 +179,9 @@ class ChallengeEditor extends Component {
try {
const copilotResource = this.getResourceFromProps('Copilot')
const copilotFromResources = copilotResource ? copilotResource.memberHandle : ''
- const reviewerResource = this.getResourceFromProps('Reviewer')
+ const reviewerResource =
+ (challengeDetails.type === 'First2Finish' || challengeDetails.type === 'Task')
+ ? this.getResourceFromProps('Iterative Reviewer') : this.getResourceFromProps('Reviewer')
const reviewerFromResources = reviewerResource ? reviewerResource.memberHandle : ''
setState({ isConfirm: false, isLaunch: false })
const challengeData = this.updateAttachmentlist(challengeDetails, attachments)
@@ -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 })
}
/**
@@ -335,6 +361,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
@@ -551,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
@@ -708,7 +741,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()
@@ -722,20 +755,8 @@ 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 } = this.props
+ const { attachments, metadata } = this.props
const challenge = pick([
'phases',
'typeId',
@@ -752,6 +773,7 @@ class ChallengeEditor extends Component {
'prizeSets',
'winners'
], this.state.challenge)
+ const isTask = _.find(metadata.challengeTypes, { id: challenge.typeId, isTask: true })
challenge.legacy = _.assign(this.state.challenge.legacy, {
reviewType: challenge.reviewType
})
@@ -762,6 +784,10 @@ class ChallengeEditor extends Component {
return { ...p, prizes }
})
challenge.status = status
+ if (status === 'Active' && isTask) {
+ challenge.startDate = moment().format()
+ }
+
if (this.state.challenge.id) {
challenge.attachmentIds = _.map(attachments, item => item.id)
}
@@ -796,7 +822,7 @@ class ChallengeEditor extends Component {
const avlTemplates = this.getAvailableTimelineTemplates()
// chooses first available timeline template or fallback template for the new challenge
const defaultTemplate = avlTemplates && avlTemplates.length > 0 ? avlTemplates[0] : STD_DEV_TIMELINE_TEMPLATE
-
+ const isTask = _.find(metadata.challengeTypes, { id: typeId, isTask: true })
const newChallenge = {
status: 'New',
projectId: this.props.projectId,
@@ -805,7 +831,7 @@ class ChallengeEditor extends Component {
trackId,
startDate: moment().add(1, 'days').format(),
legacy: {
- reviewType: isDesignChallenge ? REVIEW_TYPES.INTERNAL : REVIEW_TYPES.COMMUNITY
+ reviewType: isTask || isDesignChallenge ? REVIEW_TYPES.INTERNAL : REVIEW_TYPES.COMMUNITY
},
descriptionFormat: 'markdown',
timelineTemplateId: defaultTemplate.id,
@@ -818,6 +844,10 @@ class ChallengeEditor extends Component {
}
try {
const action = await createChallenge(newChallenge)
+ if (isTask) {
+ await this.updateResource(action.challengeDetails.id, 'Iterative Reviewer', action.challengeDetails.createdBy, action.challengeDetails.reviewer)
+ action.challengeDetails.reviewer = action.challengeDetails.createdBy
+ }
const draftChallenge = {
data: action.challengeDetails
}
@@ -880,9 +910,12 @@ class ChallengeEditor extends Component {
case 'copilot':
await this.updateResource(challengeId, 'Copilot', this.state.challenge.copilot, prevValue)
break
- case 'reviewer':
- await this.updateResource(challengeId, 'Reviewer', this.state.challenge.reviewer, prevValue)
+ case 'reviewer': {
+ const { type } = this.state.challenge
+ const useIterativeReview = type === 'First2Finish' || type === 'Task'
+ await this.updateResource(challengeId, useIterativeReview ? 'Iterative Review' : 'Reviewer', this.state.challenge.reviewer, prevValue)
break
+ }
}
} else {
let patchObject = (changedField === 'reviewType')
@@ -955,7 +988,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 +998,13 @@ 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') {
+ const iterativeReviewer = this.getResourceFromProps('Iterative Reviewer')
+ const previousIterativeReviewer = iterativeReviewer && iterativeReviewer.memberHandle
+ if (reviewer !== previousIterativeReviewer) await this.updateResource(challengeId, 'Iterative Reviewer', reviewer, previousIterativeReviewer)
+ } else {
+ if (reviewer !== previousReviewer) await this.updateResource(challengeId, 'Reviewer', reviewer, previousReviewer)
+ }
const draftChallenge = { data: action.challengeDetails }
draftChallenge.data.copilot = copilot
draftChallenge.data.reviewer = reviewer
@@ -1091,11 +1129,12 @@ class ChallengeEditor extends Component {
isNew,
isLoading,
metadata,
- uploadAttachment,
+ uploadAttachments,
token,
removeAttachment,
failedToLoad,
- projectDetail
+ projectDetail,
+ attachments
} = this.props
if (_.isEmpty(challenge)) {
return Error loading challenge
@@ -1326,6 +1365,7 @@ class ChallengeEditor extends Component {
challenge={challenge}
onChange={this.onUpdateAssignedMember}
assignedMemberDetails={assignedMemberDetails}
+ onAssignSelf={this.onAssignSelf}
/>
)}
@@ -1354,10 +1394,10 @@ class ChallengeEditor extends Component {
{/* remove terms field and use default term */}
{false && ()}
-
+
)}
- {
+ {!isTask && (
+ )}
+ {
+ this.state.isDeleteLaunch && !this.state.isConfirm && (
+
+ )
}
{ showTimeline && (
- { false && (
-
- )}
+
@@ -1426,6 +1479,7 @@ class ChallengeEditor extends Component {
{getTitle(isNew)}
+ {!isNew && this.props.challengeDetails.status === 'New' &&
}
* Required
@@ -1459,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,
@@ -1469,7 +1523,9 @@ ChallengeEditor.propTypes = {
updateChallengeDetails: PropTypes.func.isRequired,
createChallenge: PropTypes.func,
replaceResourceInRole: PropTypes.func,
- partiallyUpdateChallengeDetails: PropTypes.func.isRequired
+ partiallyUpdateChallengeDetails: PropTypes.func.isRequired,
+ deleteChallenge: PropTypes.func.isRequired,
+ loggedInUser: PropTypes.shape().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..67b15a9d 100644
--- a/src/components/ChallengesComponent/ChallengeCard/index.js
+++ b/src/components/ChallengesComponent/ChallengeCard/index.js
@@ -96,14 +96,18 @@ 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
+ return (
+
+ )
}
return challenge.legacyId ? (
@@ -177,10 +181,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 +202,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 () {
@@ -205,10 +218,15 @@ class ChallengeCard extends React.Component {
const { challenge } = this.props
try {
this.setState({ isSaving: true })
- // call action to update the challenge with a new status
- await partiallyUpdateChallengeDetails(challenge.id, {
+ const isTask = _.get(challenge, 'task.isTask', false)
+ const payload = {
status: 'Active'
- })
+ }
+ if (isTask) {
+ payload.startDate = moment().format()
+ }
+ // call action to update the challenge with a new status
+ await partiallyUpdateChallengeDetails(challenge.id, payload)
this.setState({ isLaunch: true, isConfirm: challenge.id, isSaving: false })
} catch (e) {
const error = _.get(e, 'response.data.message', 'Unable to activate the challenge')
@@ -216,12 +234,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 +327,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/components/FilestackFilePicker/FilestackFilePicker.module.scss b/src/components/FilestackFilePicker/FilestackFilePicker.module.scss
new file mode 100644
index 00000000..99cb0b13
--- /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;
+ min-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..53459896
--- /dev/null
+++ b/src/components/FilestackFilePicker/index.jsx
@@ -0,0 +1,369 @@
+/**
+ * FilestackFilePicker Component
+ *
+ * Component for uploading files using Filestack Picker and Drag & Drop.
+ * - Supports multiple file uploading.
+ */
+import _ from 'lodash'
+import React, { useEffect, useLayoutEffect, useReducer, 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,
+ 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
+ */
+const FilestackFilePicker = ({
+ path,
+ onFileUploadFinished,
+ onFileUploadFailed,
+ onUploadDone
+}) => {
+ // the list of filenames which are currently being uploaded
+ // 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)
+
+ // init Filestack (without waiting for rendering)
+ useLayoutEffect(() => {
+ filestackRef.current = filestack.init(FILE_PICKER_API_KEY, {
+ cname: FILE_PICKER_CNAME
+ })
+ }, [])
+
+ useEffect(() => {
+ // 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, 'error'),
+ filesUploaded: _.map(filesUploaded, 'file')
+ })
+ }
+ }
+
+ // 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) => {
+ 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)
+ }
+
+ /**
+ * Handle for error during 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} error error during file uploading
+ */
+ const handleFileUploadError = (error) => {
+ dispatch({
+ type: ACTION.UPDATE_FILE,
+ payload: {
+ filename: error.originalFile.name,
+ updated: {
+ error: error
+ }
+ }
+ })
+ onFileUploadFailed && onFileUploadFailed(error)
+ }
+
+ /**
+ * 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,
+ uploadConfig: {
+ retry: FILE_PICKER_UPLOAD_RETRY,
+ timeout: FILE_PICKER_UPLOAD_TIMEOUT
+ },
+ onUploadStarted: (files) => {
+ 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)
+ },
+ onFileUploadFinished: handleFileUploadSuccess,
+ onFileUploadProgress: (file, event) => {
+ dispatch({
+ type: ACTION.UPDATE_FILE,
+ payload: {
+ filename: file.filename,
+ updated: {
+ progress: event.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 = new Error(`Not allowed file type "${fileExt}".`)
+ error.originalFile = _.pick(file, ['name', 'type', 'size'])
+ }
+
+ if (index + 1 > FILE_PICKER_MAX_FILES) {
+ 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,
+ error
+ }
+ })
+
+ const filesToUpload = _.map(_.reject(files, 'error'), 'file')
+
+ dispatch({
+ type: ACTION.SET_FILES,
+ payload: files.map((file) => ({ ...file, file: null }))
+ })
+
+ filesToUpload.map((file) =>
+ filestackRef.current
+ .upload(
+ file,
+ {
+ onProgress: ({ totalPercent }) => {
+ dispatch({
+ type: ACTION.UPDATE_FILE,
+ payload: {
+ filename: file.name,
+ updated: {
+ progress: totalPercent
+ }
+ }
+ })
+ },
+ progressInterval: FILE_PICKER_PROGRESS_INTERVAL,
+ retry: FILE_PICKER_UPLOAD_RETRY,
+ timeout: FILE_PICKER_UPLOAD_TIMEOUT
+ },
+ {
+ container: FILE_PICKER_CONTAINER_NAME,
+ path,
+ region: FILE_PICKER_REGION
+ }
+ )
+ .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)
+ })
+ )
+ }
+
+ 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.toString()}
+ ) : (
+ `${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/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 {
({ label: name, value: name, name }))}
placeholder='Select Scorecard'
- labelKey='name'
- valueKey='name'
- clearable={false}
+ isClearable={false}
value={phase.scorecard}
onChange={(e) => onUpdateSelect(e, true, 'phases')}
/>
diff --git a/src/components/Select/AsyncSelect.js b/src/components/Select/AsyncSelect.js
new file mode 100644
index 00000000..f1c35dad
--- /dev/null
+++ b/src/components/Select/AsyncSelect.js
@@ -0,0 +1,25 @@
+import React from 'react'
+import _ from 'lodash'
+import ReactSelect from 'react-select/async'
+import PT from 'prop-types'
+import styles from './styles'
+
+export default function Select (props) {
+ const { selectRef } = props
+ return (
+
+ )
+}
+
+Select.defaultProps = {
+ selectRef: _.noop
+}
+
+Select.propTypes = {
+ selectRef: PT.func
+}
diff --git a/src/components/Select/Select.module.scss b/src/components/Select/Select.module.scss
deleted file mode 100644
index e8ac207a..00000000
--- a/src/components/Select/Select.module.scss
+++ /dev/null
@@ -1,118 +0,0 @@
-@import "../../styles/includes";
-
-.select {
- :global {
- $select-input-height: 40px;
- @import '~react-select/scss/default';
-
- width: 100%;
-
- .Select-control {
- border-radius: 2px !important;
- }
- .Select.is-focused:not(.is-open) > .Select-control {
- border: 1px solid $tc-blue-20 !important;
- box-shadow: none !important;
- input {
- border: none;
- }
- }
- .Select.is-focused.is-open.is-searchable.Select--single >.Select-control {
- border: 1px solid $tc-blue-20 !important;
- border-bottom: none;
-
- input {
- border: none;
- }
-
- }
-
- .Select-menu-outer {
- box-sizing: border-box;
- @include roboto-light();
-
- font-size: 15px;
- font-weight: 300;
- line-height: 18px;
- color: $tc-gray-80;
- border: 1px solid $tc-blue-20;
- z-index: 4;
- }
-
- .Select-option {
- padding-left: 20px;
- }
-
- .Select-placeholder {
- @include roboto-light();
-
- font-size: 15px;
- font-weight: 300;
- padding-left: 20px;
- color: $tc-gray-80;
- }
-
- .Select--multi > .Select-control > .Select-multi-value-wrapper {
-
- .Select-value {
- background-color: $tc-blue-20;
- color: $white;
- padding-left: 0 !important;
- padding-right: 24px;
- position: relative;
-
- .Select-value-icon {
- position: absolute;
- right: 0;
- margin: 4px 4px;
- border-radius: 50%;
- width: 16px;
- height: 16px;
- display: flex;
- justify-content: center;
- align-items: center;
- font-size: 14px;
- background-color: $tc-multi-select-icon-bg-color;
- color: $tc-blue-20;
- padding-top: 2px;
- padding-left: 6px;
- }
-
- .Select-value-label {
- font-size: 14px;
- }
- }
-
- input {
- border-width: 0;
- }
- }
-
- input.Select-input,
- input.Select-input:focus {
- background-color: transparent !important;
- margin-left: 0 !important;
- padding-right: 6px !important;
- padding-left: 10px !important;
- border: none !important;
- }
-
- .Select--single > .Select-control > .Select-multi-value-wrapper {
- width: 100% !important;
-
- .Select-value {
- padding-left: 20px !important;
-
- @include roboto-light();
- font-size: 15px;
- font-weight: 300;
- }
-
- .Select-input {
- input {
- padding-left: 10px;
- }
- }
- }
- }
-}
diff --git a/src/components/Select/index.js b/src/components/Select/index.js
index f680aebd..a121be62 100644
--- a/src/components/Select/index.js
+++ b/src/components/Select/index.js
@@ -2,18 +2,17 @@ import React from 'react'
import _ from 'lodash'
import ReactSelect from 'react-select'
import PT from 'prop-types'
-import styles from './Select.module.scss'
+import styles from './styles'
export default function Select (props) {
const { selectRef } = props
return (
-
-
-
+
)
}
diff --git a/src/components/Select/styles.js b/src/components/Select/styles.js
new file mode 100644
index 00000000..b52249ad
--- /dev/null
+++ b/src/components/Select/styles.js
@@ -0,0 +1,84 @@
+export default {
+ container: (provided) => ({
+ ...provided,
+ width: '100%'
+ }),
+ control: (provided, state) => {
+ let styles = {
+ ...provided,
+ borderRadius: '2px !important'
+ }
+ if (state.isFocused) {
+ styles = {
+ ...styles,
+ border: '1px solid #2C95D7',
+ boxShadow: 'none'
+ }
+ }
+ return styles
+ },
+ menu: (provided) => ({
+ ...provided,
+ boxSizing: 'border-box',
+ fontFamily: 'Roboto, Helvetica, Arial, sans-serif',
+ fontSize: '15px',
+ fontWeight: 300,
+ lineHeight: '18px',
+ color: '#2a2a2a',
+ border: '1px solid #2C95D7',
+ borderRadius: 0,
+ zIndex: 4,
+ margin: 0,
+ padding: 0
+ }),
+ menuList: (provided) => ({
+ ...provided,
+ padding: 0
+ }),
+ option: (provided) => ({
+ ...provided,
+ paddingLeft: '20px'
+ }),
+ placeholder: (provided) => ({
+ ...provided,
+ fontFamily: 'Roboto, Helvetica, Arial, sans-serif',
+ fontSize: '15px',
+ fontWeight: 300,
+ paddingLeft: '10px',
+ color: '#2a2a2a'
+ }),
+ input: (provided) => ({
+ ...provided,
+ backgroundColor: 'transparent',
+ marginLeft: 0,
+ paddingRight: '6px',
+ paddingLeft: '10px',
+ border: 'none',
+ input: {
+ width: 'auto !important',
+ height: 'auto !important',
+ lineHeight: 'normal !important'
+ }
+ }),
+ singleValue: (provided) => ({
+ ...provided,
+ paddingLeft: '10px'
+ }),
+ multiValue: (provided) => ({
+ ...provided,
+ backgroundColor: '#2c95d7'
+ }),
+ multiValueLabel: (provided) => ({
+ ...provided,
+ color: 'white'
+ }),
+ multiValueRemove: (provided) => ({
+ ...provided,
+ margin: '4px 4px',
+ height: '16px',
+ width: '16px',
+ backgroundColor: '#c6def1',
+ color: '#2C95D7',
+ borderRadius: '50%'
+ })
+}
diff --git a/src/config/constants.js b/src/config/constants.js
index d1e4aef0..e8eab683 100644
--- a/src/config/constants.js
+++ b/src/config/constants.js
@@ -18,6 +18,26 @@ 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'
+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 // 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'
+
+export const getAWSContainerFileURL = (key) => `https://${FILE_PICKER_CONTAINER_NAME}.s3.amazonaws.com/${key}`
+
// Actions
export const LOAD_PROJECTS_SUCCESS = 'LOAD_PROJECTS_SUCCESS'
export const LOAD_PROJECTS_PENDING = 'LOAD_PROJECTS_PENDING'
@@ -48,6 +68,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'
@@ -62,9 +86,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 +109,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'
@@ -158,7 +184,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 ab0c4796..51ffaa26 100644
--- a/src/containers/ChallengeEditor/index.js
+++ b/src/containers/ChallengeEditor/index.js
@@ -2,6 +2,7 @@ import _ from 'lodash'
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { withRouter, Route } from 'react-router-dom'
+import moment from 'moment'
import ChallengeEditorComponent from '../../components/ChallengeEditor'
import ChallengeViewComponent from '../../components/ChallengeEditor/ChallengeView'
import Loader from '../../components/Loader'
@@ -18,12 +19,13 @@ import {
// loadChallengeTerms,
loadGroups,
loadChallengeDetails,
- createAttachment,
+ createAttachments,
removeAttachment,
loadResources,
loadResourceRoles,
updateChallengeDetails,
partiallyUpdateChallengeDetails,
+ deleteChallenge,
createChallenge,
replaceResourceInRole
} from '../../actions/challenges'
@@ -155,13 +157,18 @@ class ChallengeEditor extends Component {
async activateChallenge () {
const { partiallyUpdateChallengeDetails } = this.props
if (this.state.isLaunching) return
- const { challengeDetails } = this.props
+ const { challengeDetails, metadata } = this.props
+ const isTask = _.find(metadata.challengeTypes, { id: challengeDetails.typeId, isTask: true })
try {
this.setState({ isLaunching: true })
- // call action to update the challenge status
- const action = await partiallyUpdateChallengeDetails(challengeDetails.id, {
+ const payload = {
status: 'Active'
- })
+ }
+ if (isTask) {
+ payload.startDate = moment().format()
+ }
+ // call action to update the challenge status
+ const action = await partiallyUpdateChallengeDetails(challengeDetails.id, payload)
this.setState({
isLaunching: false,
showLaunchModal: false,
@@ -220,7 +227,7 @@ class ChallengeEditor extends Component {
// challengeDetails,
challengeResources,
metadata,
- createAttachment,
+ createAttachments,
attachments,
token,
removeAttachment,
@@ -229,7 +236,9 @@ class ChallengeEditor extends Component {
updateChallengeDetails,
partiallyUpdateChallengeDetails,
createChallenge,
- replaceResourceInRole
+ replaceResourceInRole,
+ deleteChallenge,
+ loggedInUser
// members
} = this.props
const {
@@ -298,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}
@@ -325,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}
@@ -335,6 +344,8 @@ class ChallengeEditor extends Component {
updateChallengeDetails={updateChallengeDetails}
replaceResourceInRole={replaceResourceInRole}
partiallyUpdateChallengeDetails={partiallyUpdateChallengeDetails}
+ deleteChallenge={deleteChallenge}
+ loggedInUser={loggedInUser}
/>
))
} />
@@ -348,6 +359,7 @@ class ChallengeEditor extends Component {
metadata={metadata}
projectDetail={projectDetail}
challenge={challengeDetails}
+ attachments={attachments}
challengeResources={challengeResources}
token={token}
challengeId={challengeId}
@@ -391,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,
@@ -400,6 +412,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())
}
@@ -428,13 +441,14 @@ const mapDispatchToProps = {
loadChallengeTimelines,
loadChallengeTags,
loadGroups,
- createAttachment,
+ createAttachments,
removeAttachment,
// loadChallengeTerms,
loadResources,
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..0019229b 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,
@@ -27,7 +29,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 +47,7 @@ const initialState = {
attachments: [],
challenge: null,
filterChallengeName: '',
+ failedToDelete: false,
status: '',
perPage: 0,
page: 1,
@@ -49,20 +55,19 @@ const initialState = {
projectId: -1
}
-function toastrSuccess (title, message) {
+function toastrFailure (title, message) {
setImmediate(() => {
- toastr.success(title, message)
+ toastr.error(title, message)
})
}
-function toastrFailure (title, message) {
+function toastrSuccess (title, message) {
setImmediate(() => {
- toastr.error(title, message)
+ toastr.success(title, message)
})
}
export default function (state = initialState, action) {
- let attachments
switch (action.type) {
case LOAD_CHALLENGES_SUCCESS:
return {
@@ -144,6 +149,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
@@ -218,23 +244,70 @@ 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 || []),
+ // 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 => {
+ // 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, (attachment) =>
+ _.find(action.files, { url: attachment.url })
+ )
+ 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..321b6ee1 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,
@@ -56,12 +55,25 @@ 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', [])
+}
+
+/**
+ * Api request for fetching Group Detail
+ *
+ * @param groupId
+ * @returns {Promise<*>}
+ */
+export async function fetchGroupDetail (id) {
+ const response = await axiosInstance.get(`${GROUPS_API_URL}/${id}`)
return _.get(response, 'data', [])
}
@@ -126,10 +138,28 @@ export function updateChallenge (challengeId, challenge) {
})
}
-export function uploadAttachment (challengeId, file) {
- const data = new FormData()
- data.append('attachment', file)
- return axiosInstance.post(`${CHALLENGE_API_URL}/${challengeId}/attachments`, data)
+/**
+ * Create attachments
+ *
+ * @param {String|Number} challengeId challenge id
+ * @param {Object[]} attachments list of attachments
+ *
+ * @returns {Promise<*>} attachments data
+ */
+export function createAttachments (challengeId, attachments) {
+ return axiosInstance.post(`${CHALLENGE_API_URL}/${challengeId}/attachments`, attachments)
+}
+
+/**
+ * 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}`)
}
/**
@@ -160,6 +190,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<*>}