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 && +
+ { + e.preventDefault() + onAssignSelf() + }}>Assign to me +
+ }
) } @@ -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 }) => {
({ label: template.name, value: template.name, name: template.name }))} placeholder='Select' - labelKey='name' - valueKey='name' - clearable={false} - value={currentTemplate} + isClearable={false} + value={currentTemplate && { label: currentTemplate.name, value: currentTemplate.name }} onChange={(e) => resetPhase(e)} /> )} diff --git a/src/components/ChallengeEditor/ChallengeView/index.js b/src/components/ChallengeEditor/ChallengeView/index.js index 3dbd0de1..99748e74 100644 --- a/src/components/ChallengeEditor/ChallengeView/index.js +++ b/src/components/ChallengeEditor/ChallengeView/index.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' import _ from 'lodash' import { Helmet } from 'react-helmet' import PropTypes from 'prop-types' @@ -21,12 +21,14 @@ import PhaseInput from '../../PhaseInput' import LegacyLinks from '../../LegacyLinks' import AssignedMemberField from '../AssignedMember-Field' import { getResourceRoleByName } from '../../../util/tc' +import { loadGroupDetails } from '../../../actions/challenges' import Tooltip from '../../Tooltip' import { MESSAGE, REVIEW_TYPES } from '../../../config/constants' const ChallengeView = ({ projectDetail, challenge, + attachments, metadata, challengeResources, token, @@ -40,6 +42,18 @@ const ChallengeView = ({ const challengeTrack = _.find(metadata.challengeTracks, { id: challenge.trackId }) const [openAdvanceSettings, setOpenAdvanceSettings] = useState(false) + const [groups, setGroups] = useState('') + + useEffect(() => { + if (challenge.groups && challenge.groups.length > 0) { + loadGroupDetails(challenge.groups).then(res => { + const groups = _.map(res, 'name').join(', ') + setGroups(groups) + }) + } else { + setGroups('') + } + }, [challenge.groups]) const getResourceFromProps = (name) => { const { resourceRoles } = metadata @@ -167,7 +181,7 @@ const ChallengeView = ({
{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 (
- } - { !isDesignChallenge && + { !isDesignChallenge && !isTask && communityOption() }
@@ -77,14 +79,12 @@ const ReviewTypeField = ({ reviewers, challenge, onUpdateOthers, onUpdateSelect isInternal && ( ({ label: tag, value: tag }))} onChange={(value) => onUpdateMultiSelect(value, 'tags')} />)}
diff --git a/src/components/ChallengeEditor/Terms-Field/index.js b/src/components/ChallengeEditor/Terms-Field/index.js index 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 }) => { 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 {