From 73cf4e17e211df542040cd28d599f4b7bc03c599 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Fri, 4 Jul 2025 14:54:34 +1000 Subject: [PATCH 01/76] V6 --- config/backup-default.js | 1 + config/default.js | 1 + config/production.js | 1 + package.json | 4 ++-- src/server/index.js | 1 + src/shared/actions/page/submission/index.js | 2 +- src/shared/services/reviewTypes.js | 2 +- src/shared/services/submissions.js | 2 +- 8 files changed, 9 insertions(+), 5 deletions(-) diff --git a/config/backup-default.js b/config/backup-default.js index 2ea3ca1ee5..2b85c61351 100644 --- a/config/backup-default.js +++ b/config/backup-default.js @@ -8,6 +8,7 @@ module.exports = { V3: 'https://api.topcoder-dev.com/v3', V4: 'https://api.topcoder-dev.com/v4', V5: 'https://api.topcoder-dev.com/v5', + V6: 'https://api.topcoder-dev.com/v6', MM_BROKER: '/api', }, diff --git a/config/default.js b/config/default.js index 97d6e12835..4fc5508fa4 100644 --- a/config/default.js +++ b/config/default.js @@ -8,6 +8,7 @@ module.exports = { V3: 'https://api.topcoder-dev.com/v3', V4: 'https://api.topcoder-dev.com/v4', V5: 'https://api.topcoder-dev.com/v5', + V6: 'https://api.topcoder-dev.com/v6', MM_BROKER: '/api', }, diff --git a/config/production.js b/config/production.js index a0f0d88b86..9e4664b2e2 100644 --- a/config/production.js +++ b/config/production.js @@ -4,6 +4,7 @@ module.exports = { V3: 'https://api.topcoder.com/v3', V4: 'https://api.topcoder.com/v4', V5: 'https://api.topcoder.com/v5', + V6: 'https://api.topcoder.com/v6', }, AUTH0: { DOMAIN: 'topcoder.auth0.com', diff --git a/package.json b/package.json index 3ed6237f7a..6959e6f8ee 100644 --- a/package.json +++ b/package.json @@ -164,9 +164,9 @@ "supertest": "^3.1.0", "tc-core-library-js": "github:appirio-tech/tc-core-library-js#v2.6.3.1", "tc-ui": "^1.0.12", - "topcoder-react-lib": "1.2.14", + "topcoder-react-lib": "github:topcoder-platform/topcoder-react-lib#v6", "topcoder-react-ui-kit": "2.0.1", - "topcoder-react-utils": "github:topcoder-platform/topcoder-react-utils#v0.9.5", + "topcoder-react-utils": "github:topcoder-platform/topcoder-react-utils#v6", "turndown": "^4.0.2", "uninav-react": "git+https://github.com/topcoder-platform/uninav-react.git#0.0.2", "url-parse": "^1.4.1", diff --git a/src/server/index.js b/src/server/index.js index 7bb01dd9c2..12ca3a7692 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -211,6 +211,7 @@ async function onExpressJsSetup(server) { + ` ${config.API.V3}/` + ` ${config.API.V4}/` + ` ${config.API.V5}/` + + ` ${config.API.V6}/` + ` ${config.CDN.PUBLIC}` + ` ${config.URL.COMMUNITY_APP}` + ' https://api.segment.io' diff --git a/src/shared/actions/page/submission/index.js b/src/shared/actions/page/submission/index.js index bcc0046b98..85e7e6beb4 100644 --- a/src/shared/actions/page/submission/index.js +++ b/src/shared/actions/page/submission/index.js @@ -24,7 +24,7 @@ const Api = services.api.default; * @return Promise */ function submitDone(tokenV3, tokenV2, submissionId, body, track, progress) { - const api = new Api(config.API.V5, tokenV3); + const api = new Api(config.API.V6, tokenV3); const url = '/submissions/'; return api.upload(url, { body, diff --git a/src/shared/services/reviewTypes.js b/src/shared/services/reviewTypes.js index 7f83391f0a..101bd1de45 100644 --- a/src/shared/services/reviewTypes.js +++ b/src/shared/services/reviewTypes.js @@ -1,7 +1,7 @@ import { config } from 'topcoder-react-utils'; const baseUrl = config.URL.REVIEW_API_URL; -const v5ApiUrl = config.API.V5; +const v5ApiUrl = config.API.V6; export default function getReviewTypes(tokenV3) { return fetch(`${v5ApiUrl}${baseUrl}?perPage=500&page=1`, { diff --git a/src/shared/services/submissions.js b/src/shared/services/submissions.js index a6084904ce..aa6351bafe 100644 --- a/src/shared/services/submissions.js +++ b/src/shared/services/submissions.js @@ -1,6 +1,6 @@ import { config } from 'topcoder-react-utils'; -const v5ApiUrl = config.API.V5; +const v5ApiUrl = config.API.V6; export const downloadSubmissions = (tokenV3, submissionId, artifactId) => fetch(`${v5ApiUrl}/submissions/${submissionId}/artifacts/${artifactId}/download`, { method: 'GET', From d1b44c5853860afb28d786cdff9975d1db82843c Mon Sep 17 00:00:00 2001 From: Gunasekar-K Date: Mon, 14 Jul 2025 11:12:38 +0530 Subject: [PATCH 02/76] community app v6 devops config --- .circleci/config.yml | 298 ++++++------------------------------------- build.sh | 2 +- 2 files changed, 41 insertions(+), 259 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0f77a5da75..081a230012 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -28,10 +28,11 @@ install_dependency: &install_dependency install_deploysuite: &install_deploysuite name: Installation of install_deploysuite. command: | - git clone --branch v1.4.14 https://github.com/topcoder-platform/tc-deploy-scripts ../buildscript + git clone --branch v1.4.17 https://github.com/topcoder-platform/tc-deploy-scripts ../buildscript cp ./../buildscript/master_deploy.sh . cp ./../buildscript/buildenv.sh . cp ./../buildscript/awsconfiguration.sh . + cp ./../buildscript/psvar-processor.sh . restore_cache_settings_for_build: &restore_cache_settings_for_build key: docker-node-modules-v4-{{ checksum "package-lock.json" }} @@ -43,14 +44,11 @@ save_cache_settings: &save_cache_settings build_docker_image: &build_docker_image name: Build of Docker image command: | - source buildenvvar + source buildvar_env ./build.sh no_output_timeout: 20m -jobs: - # Build & Deploy against development backend - "build-dev": - <<: *defaults - steps: + +build_steps: &build_steps # Initialization. - checkout - setup_remote_docker @@ -62,8 +60,11 @@ jobs: - run: name: "configuring environment" command: | - ./awsconfiguration.sh DEV - ./buildenv.sh -e DEV -b dev_communityapp_buildvar,dev_communityapp_deployvar -l dev_communityapp_buildvar_ps + ./awsconfiguration.sh ${DEPLOY_ENV} + source awsenvconf + ./psvar-processor.sh -t appenv -p /config/${APPNAME}/buildvar + source buildvar_env + # ./buildenv.sh -e ${DEPLOY_ENV} -b dev_communityapp_buildvar,dev_communityapp_deployvar -l dev_communityapp_buildvar_ps - run: *build_docker_image # Caching node modules. - save_cache: *save_cache_settings @@ -72,218 +73,40 @@ jobs: name: Running MasterScript command: | source awsenvconf - source buildenvvar - ./master_deploy.sh -d ECS -e DEV -t latest -s dev_communityapp_taskvar -i communityapp -p FARGATE - - # Build & Deploy against testing backend - # "build-test": - # <<: *defaults - # steps: - # # Initialization. - # - checkout - # - setup_remote_docker - # - run: *install_dependency - # - run: *install_deploysuite - # # Restoration of node_modules from cache. - # - restore_cache: *restore_cache_settings_for_build - # - run: - # name: "configuring environment" - # command: | - # ./awsconfiguration.sh DEV - # ./buildenv.sh -e DEV -b test_communityapp_buildvar,test_communityapp_deployvar -l dev_communityapp_buildvar_ps - # # Build of Docker image. - # - run: *build_docker_image - # # Caching node modules. - # - save_cache: *save_cache_settings - # # Deployment. - # - deploy: - # name: Running MasterScript - # command: | - # source awsenvconf - # source buildenvvar - # ./master_deploy.sh -d ECS -e DEV -t latest -s test_communityapp_taskvar -i communityapp - - # Build & Deploy against testing backend - # Deprecate this workflow due to beta env shutdown - # https://topcoder.atlassian.net/browse/CORE-250 - # "build-qa": - # <<: *defaults - # steps: - # # Initialization. - # - checkout - # - setup_remote_docker - # - run: *install_dependency - # - run: *install_deploysuite - # # Restoration of node_modules from cache. - # - restore_cache: *restore_cache_settings_for_build - # - run: - # name: "configuring environment" - # command: | - # ./awsconfiguration.sh QA - # ./buildenv.sh -e QA -b qa_communityapp_buildvar,qa_communityapp_deployvar -l qa_communityapp_buildvar_ps - # # Build of Docker image. - # - run: *build_docker_image - # # Caching node modules. - # - save_cache: *save_cache_settings - # # Deployment. - # - deploy: - # name: Running MasterScript - # command: | - # source awsenvconf - # source buildenvvar - # ./master_deploy.sh -d ECS -e QA -t latest -s qa_communityapp_taskvar -i communityapp - - # Build & Deploy against prod api backend - # Deprecate this workflow due to beta env shutdown - # https://topcoder.atlassian.net/browse/CORE-201 - # "build-prod-beta": - # <<: *defaults - # steps: - # # Initialization. - # - checkout - # - setup_remote_docker - # - run: *install_dependency - # - run: *install_deploysuite - # # Restoration of node_modules from cache. - # - restore_cache: *restore_cache_settings_for_build - # - run: - # name: "configuring environment" - # command: | - # ./awsconfiguration.sh PROD - # ./buildenv.sh -e PROD -b beta_communityapp_buildvar,beta_communityapp_deployvar -l prod_communityapp_buildvar_ps - # # Build of Docker image. - # - run: *build_docker_image - # # Caching node modules. - # - save_cache: *save_cache_settings - # # Deployment. - # - deploy: - # name: Running MasterScript - # command: | - # source awsenvconf - # source buildenvvar - # ./master_deploy.sh -d ECS -e PROD -t latest -s beta_communityapp_taskvar, -i communityapp + # source buildenvvar + ./psvar-processor.sh -t appenv -p /config/${APPNAME}/deployvar + source deployvar_env + ./master_deploy.sh -d ECS -e $DEPLOY_ENV -t latest -j /config/${APPNAME}/appvar -i ${APPNAME} -p FARGATE + # ./master_deploy.sh -d ECS -e DEV -t latest -s dev_communityapp_taskvar -i communityapp -p FARGATE + if [ "${DEPLOY_ENV}" = "PROD" ]; + then + # Executing plan + curl --request POST \ + --url https://circleci.com/api/v2/project/github/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/pipeline \ + --header "Circle-Token: ${CIRCLE_TOKEN}" \ + --header 'content-type: application/json' \ + --data '{"branch":"'"$CIRCLE_BRANCH"'","parameters":{"run_smoketesting":true , "run_performancetesting":false, "run_basedeployment": false}}' + fi - # Build & Deploy against prod api backend - # Deprecate this workflow due to beta env shutdown - # https://topcoder.atlassian.net/browse/CORE-202 - # "build-prod-staging": - # <<: *defaults - # steps: - # # Initialization. - # - checkout - # - setup_remote_docker - # - run: *install_dependency - # - run: *install_deploysuite - # # Restoration of node_modules from cache. - # - restore_cache: *restore_cache_settings_for_build - # - run: - # name: "configuring environment" - # command: | - # ./awsconfiguration.sh PROD - # ./buildenv.sh -e PROD -b staging_communityapp_buildvar,staging_communityapp_deployvar -l prod_communityapp_buildvar_ps - # # Build of Docker image. - # - run: *build_docker_image - # # Caching node modules. - # - save_cache: *save_cache_settings - # # Deployment. - # - deploy: - # name: Running MasterScript - # command: | - # source awsenvconf - # source buildenvvar - # ./master_deploy.sh -d ECS -e PROD -t latest -s staging_communityapp_taskvar, -i communityapp - # curl --request POST \ - # --url https://circleci.com/api/v2/project/github/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/pipeline \ - # --header "Circle-Token: ${CIRCLE_TOKEN}" \ - # --header 'content-type: application/json' \ - # --data '{"branch":"'"$CIRCLE_BRANCH"'","parameters":{"run_smoketesting":true , "run_performancetesting":false, "run_basedeployment": false}}' +jobs: + # Build & Deploy against development backend + "build-dev": + <<: *defaults + environment: + DEPLOY_ENV: "DEV" + LOGICAL_ENV: "dev" + APPNAME: "community-app-v6" + steps: *build_steps # Build & Deploy against production backend "build-prod": <<: *defaults - steps: - # Initialization. - - checkout - - setup_remote_docker - - run: *install_dependency - - run: *install_deploysuite - # Restoration of node_modules from cache. - - restore_cache: *restore_cache_settings_for_build - - run: - name: "configuring environment" - command: | - ./awsconfiguration.sh PROD - ./buildenv.sh -e PROD -b prod_communityapp_buildvar,prod_communityapp_deployvar -l prod_communityapp_buildvar_ps - # Build of Docker image. - - run: *build_docker_image - # Caching node modules. - - save_cache: *save_cache_settings - # Deployment. - - deploy: - name: Running MasterScript - command: | - source awsenvconf - source buildenvvar - ./master_deploy.sh -d ECS -e PROD -t latest -s prod_communityapp_taskvar -i communityapp -p FARGATE - curl --request POST \ - --url https://circleci.com/api/v2/project/github/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/pipeline \ - --header "Circle-Token: ${CIRCLE_TOKEN}" \ - --header 'content-type: application/json' \ - --data '{"branch":"'"$CIRCLE_BRANCH"'","parameters":{"run_smoketesting":true , "run_performancetesting":false, "run_basedeployment": false}}' - - # Automated Smoke Testing against Staging - # Deprecate this workflow due to beta env shutdown - # https://topcoder.atlassian.net/browse/CORE-202 - # Smoke-Testing-On-Staging: - # <<: *defaults - # steps: - # # Initialization. - # - checkout - # - setup_remote_docker - # - run: *install_dependency - # - run: *install_deploysuite - # # Restoration of node_modules from cache. - # - restore_cache: *restore_cache_settings_for_build - # - run: - # name: "configuring environment" - # command: | - # ./awsconfiguration.sh PROD - # ./buildenv.sh -e PROD -b staging_communityapp_buildvar,staging_communityapp_deployvar - # - run: - # name: "Run automation" - # no_output_timeout: 20m - # command: | - # source awsenvconf - # source buildenvvar - # ./automated-smoke-test/smoketest.sh automation-config-staging.json prod - # - store_artifacts: - # path: ./automated-smoke-test/test-results + environment: + DEPLOY_ENV: "PROD" + LOGICAL_ENV: "prod" + APPNAME: "community-app-v6" + steps: *build_steps - # Automated Smoke Testing against Production - # Smoke-Testing-On-Production: - # <<: *defaults - # steps: - # # Initialization. - # - checkout - # - setup_remote_docker - # - run: *install_dependency - # - run: *install_deploysuite - # # Restoration of node_modules from cache. - # - restore_cache: *restore_cache_settings_for_build - # - run: - # name: "configuring environment" - # command: | - # ./awsconfiguration.sh PROD - # ./buildenv.sh -e PROD -b prod_communityapp_buildvar,prod_communityapp_deployvar - # - run: - # name: "Run automation" - # no_output_timeout: 20m - # command: | - # source awsenvconf - # source buildenvvar - # ./automated-smoke-test/smoketest.sh automation-config-prod.json prod - # - store_artifacts: - # path: ./automated-smoke-test/test-results # Test job for the cases when we do not need deployment. It just rapidly # installs (updates) app dependencies, and runs tests (ESLint, Stylelint, @@ -358,49 +181,8 @@ workflows: branches: only: - develop - # This is alternate dev env for parallel testing - # Deprecate this workflow due to beta env shutdown - # https://topcoder.atlassian.net/browse/CORE-251 - # - "build-test": - # context : org-global - # filters: - # branches: - # only: - # - metadata-fix - # This is alternate dev env for parallel testing - # Deprecate this workflow due to beta env shutdown - # https://topcoder.atlassian.net/browse/CORE-250 - # - "build-qa": - # context : org-global - # filters: - # branches: - # only: - # - qaenv - # This is beta env for production soft releases - # Deprecate this workflow due to beta env shutdown - # https://topcoder.atlassian.net/browse/CORE-201 - # - "build-prod-beta": - # context : org-global - # filters: - # branches: - # only: - # - develop - # This is stage env for production QA releases - # Deprecate this workflow due to beta env shutdown - # https://topcoder.atlassian.net/browse/CORE-202 - # - "build-prod-staging": - # context : org-global - # filters: &filters-staging - # branches: - # only: - # - develop - # - CORE-201 - # Production builds are exectuted - # when PR is merged to the master - # Don't change anything in this configuration - # That might trigger wrong branch to be - # deployed on the production - # master branch. + - v6 + - "build-prod": context: org-global filters: &filters-prod diff --git a/build.sh b/build.sh index a3650db356..9ab1b6da50 100755 --- a/build.sh +++ b/build.sh @@ -5,7 +5,7 @@ set -eo pipefail # This script expects a single argument: NODE_CONFIG_ENV, which must be either # "development" or "production". # Builds Docker image of the app. -TAG="communityapp:latest" +TAG="community-app-v6:latest" echo "COMMUNITY_APP_URL $COMMUNITY_APP_URL"; echo "NODE_ENV $NODE_CONFIG_ENV"; From a85c6a341407162bf708796d8a17b6c64a344f10 Mon Sep 17 00:00:00 2001 From: Gunasekar-K Date: Mon, 14 Jul 2025 11:47:38 +0530 Subject: [PATCH 03/76] community app v6 devops config --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 081a230012..0eed6eba07 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -197,6 +197,7 @@ workflows: ignore: - develop - submission_delete_button + - v6 Smoke Testing: when: << pipeline.parameters.run_smoketesting >> From 5f272e48a6c4800ee3d597eb25fc3f9ed1e8eee4 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Tue, 15 Jul 2025 13:54:20 +1000 Subject: [PATCH 04/76] Initial commit --- .circleci/config.yml | 4 +- package-lock.json | 288 ++++++++++++++++++++++++++----------------- 2 files changed, 179 insertions(+), 113 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0f77a5da75..034fa48c6c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -57,7 +57,7 @@ jobs: - run: *install_dependency - run: *install_deploysuite # Restoration of node_modules from cache. - - restore_cache: *restore_cache_settings_for_build + # - restore_cache: *restore_cache_settings_for_build # Build of Docker image. - run: name: "configuring environment" @@ -66,7 +66,7 @@ jobs: ./buildenv.sh -e DEV -b dev_communityapp_buildvar,dev_communityapp_deployvar -l dev_communityapp_buildvar_ps - run: *build_docker_image # Caching node modules. - - save_cache: *save_cache_settings + # - save_cache: *save_cache_settings # Deployment. - deploy: name: Running MasterScript diff --git a/package-lock.json b/package-lock.json index 506a1627fd..588c3af504 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1354,6 +1354,11 @@ "glob-to-regexp": "^0.3.0" } }, + "@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==" + }, "@nodelib/fs.stat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", @@ -1420,6 +1425,14 @@ "utility-types": "^2.1.0 || ^3.0.0" } }, + "@paralleldrive/cuid2": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "requires": { + "@noble/hashes": "^1.1.5" + } + }, "@sideway/address": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", @@ -4468,6 +4481,22 @@ "get-intrinsic": "^1.0.2" } }, + "call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "dependencies": { + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + } + } + }, "call-me-maybe": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", @@ -7825,6 +7854,23 @@ "nan": "^2.14.0" } }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "dependencies": { + "gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" + } + } + }, "duplexify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", @@ -8141,6 +8187,16 @@ "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", "dev": true }, + "es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, "es-get-iterator": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", @@ -8166,6 +8222,14 @@ } } }, + "es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "requires": { + "es-errors": "^1.3.0" + } + }, "es-set-tostringtag": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", @@ -10511,6 +10575,15 @@ } } }, + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "requires": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + } + }, "get-stdin": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-7.0.0.tgz", @@ -11517,6 +11590,21 @@ "minimalistic-assert": "^1.0.1" } }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "requires": { + "function-bind": "^1.1.2" + }, + "dependencies": { + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + } + } + }, "hast-to-hyperscript": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/hast-to-hyperscript/-/hast-to-hyperscript-9.0.1.tgz", @@ -15618,6 +15706,11 @@ "integrity": "sha512-4vRUvPyxdO8cWULGTh9dZWL2tZK6LDBvj+OGHBER7poH9Qdt7kXEoj20wiz4lQUbUXQZFjPbe5mVDo9nutizCw==", "dev": true }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, "math-random": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz", @@ -25805,9 +25898,8 @@ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" }, "topcoder-react-lib": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/topcoder-react-lib/-/topcoder-react-lib-1.2.14.tgz", - "integrity": "sha512-QH+OG/oj9i888ho67Oe18zwxyouxye/S+JNF7AS1PBD9k19V/tZH/IGb39qAcn0nZcLsB24u51epGkuY06hSwA==", + "version": "github:topcoder-platform/topcoder-react-lib#0145b54636dcdfee4185baafa95194535f280b37", + "from": "github:topcoder-platform/topcoder-react-lib#v6", "requires": { "@topcoder-platform/tc-auth-lib": "git+https://github.com/topcoder-platform/tc-auth-lib.git#1.0.4", "auth0-js": "^6.8.4", @@ -25826,7 +25918,7 @@ "redux-actions": "^2.4.0", "tc-core-library-js": "github:appirio-tech/tc-core-library-js#v2.6", "to-capital-case": "^1.0.0", - "topcoder-react-utils": "0.7.5" + "topcoder-react-utils": "github:topcoder-platform/topcoder-react-utils#v6" }, "dependencies": { "@topcoder-platform/tc-auth-lib": { @@ -25866,19 +25958,30 @@ } }, "config": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/config/-/config-3.3.11.tgz", - "integrity": "sha512-Dhn63ZoWCW5EMg4P0Sl/XNsj/7RLiUIA1x1npCy+m2cRwRHzLnt3UtYtxRDMZW/6oOMdWhCzaGYkOcajGgrAOA==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/config/-/config-3.3.12.tgz", + "integrity": "sha512-Vmx389R/QVM3foxqBzXO8t2tUikYZP64Q6vQxGrsMpREeJc/aWRnPRERXWsYzOHAumx/AOoILWe6nU3ZJL+6Sw==", "requires": { "json5": "^2.2.3" } }, "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "requires": { - "ms": "2.1.2" + "ms": "^2.1.3" + } + }, + "es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "requires": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" } }, "follow-redirects": { @@ -25906,26 +26009,68 @@ } }, "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "formidable": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz", - "integrity": "sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.5.tgz", + "integrity": "sha512-Oz5Hwvwak/DCaXVVUtPn4oLMLLy1CdclLKO1LFgU7XzDpVMUU5UjlSLpGMocyQNNk8F6IJW9M/YdooSn2MRI+Q==", "requires": { + "@paralleldrive/cuid2": "^2.2.2", "dezalgo": "^1.0.4", - "hexoid": "^1.0.0", "once": "^1.4.0", "qs": "^6.11.0" } }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + }, + "get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + } + }, + "gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" + }, + "has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" + }, + "has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "requires": { + "has-symbols": "^1.0.3" + } + }, "json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -25937,9 +26082,9 @@ "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==" }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "object-keys": { "version": "0.4.0", @@ -25970,22 +26115,14 @@ } }, "semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "requires": { - "lru-cache": "^6.0.0" - } - }, - "serialize-javascript": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.9.1.tgz", - "integrity": "sha512-0Vb/54WJ6k5v8sSWN09S0ora+Hnr+cX40r9F170nT+mSkaxltoE/7R3OrIdBSUv1OoiobH1QoWQbCnAO+e8J1A==" + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==" }, "superagent": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-7.1.5.tgz", - "integrity": "sha512-HQYyGuDRFGmZ6GNC4hq2f37KnsY9Lr0/R1marNZTgMweVDQLTLJJ6DGQ9Tj/xVVs5HEnop9EMmTbywb5P30aqw==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-7.1.6.tgz", + "integrity": "sha512-gZkVCQR1gy/oUXr+kxJMLDjla434KmSOKbx5iGD30Ql+AkJQ/YlPKECJy2nhqOsHLjGHzoDTXNSjhnvWhzKk7g==", "requires": { "component-emitter": "^1.3.0", "cookiejar": "^2.1.3", @@ -25994,7 +26131,7 @@ "form-data": "^4.0.0", "formidable": "^2.0.1", "methods": "^1.1.2", - "mime": "^2.5.0", + "mime": "2.6.0", "qs": "^6.10.3", "readable-stream": "^3.6.0", "semver": "^7.3.7" @@ -26016,9 +26153,9 @@ }, "dependencies": { "auth0-js": { - "version": "9.24.1", - "resolved": "https://registry.npmjs.org/auth0-js/-/auth0-js-9.24.1.tgz", - "integrity": "sha512-Zax2BOEP6ZRLWNnG59JiquIHiWkgePOOfNv3UgA30Vm0jMQY4SyNAI6yG2+xr5rBxjs/L38xZAHLJm5lcwwGog==", + "version": "9.28.0", + "resolved": "https://registry.npmjs.org/auth0-js/-/auth0-js-9.28.0.tgz", + "integrity": "sha512-2xIfQIGM0vX3IdPR91ztLO2+Ar2I5+3iFKcjuZO+LV9vRh4Wje+Ka1hnHjMU9dH892Lm3ZxBAHxRo68YToUhfg==", "requires": { "base64-js": "^1.5.1", "idtoken-verifier": "^2.2.4", @@ -26037,77 +26174,6 @@ } } }, - "topcoder-react-utils": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/topcoder-react-utils/-/topcoder-react-utils-0.7.5.tgz", - "integrity": "sha512-/jolO/UUCC/FL/MniBMFi9d7Wc1KbzwvgT5STGs4T+7u7R26bQugGPpGVISEPuglsmW0Xybh6iRi+pT/muOkbg==", - "requires": { - "babel-runtime": "^6.26.0", - "body-parser": "^1.18.3", - "command-line-args": "^5.0.2", - "command-line-usage": "^5.0.5", - "compression": "^1.7.2", - "config": "^1.30.0", - "cookie-parser": "^1.4.3", - "express": "^4.16.3", - "helmet": "^3.12.1", - "lodash": "^4.17.10", - "moment": "^2.22.2", - "morgan": "^1.9.0", - "node-forge": "^0.7.5", - "prop-types": "^15.6.2", - "raf": "^3.4.0", - "react": "^16.4.1", - "react-css-super-themr": "^2.2.0", - "react-dom": "^16.4.1", - "react-helmet": "^5.2.0", - "react-redux": "^5.0.7", - "react-router-dom": "^4.3.1", - "redux": "^3.7.2", - "redux-actions": "^2.4.0", - "redux-devtools": "^3.4.1", - "redux-devtools-dock-monitor": "^1.1.3", - "redux-devtools-log-monitor": "^1.4.0", - "redux-promise": "^0.6.0", - "request-ip": "^2.0.2", - "serialize-javascript": "^1.5.0", - "serve-favicon": "^2.5.0", - "shortid": "^2.2.8", - "url-parse": "^1.4.1" - }, - "dependencies": { - "config": { - "version": "1.31.0", - "resolved": "https://registry.npmjs.org/config/-/config-1.31.0.tgz", - "integrity": "sha512-Ep/l9Rd1J9IPueztJfpbOqVzuKHQh4ZODMNt9xqTYdBBNRXbV4oTu34kCkkfdRVcDq0ohtpaeXGgb+c0LQxFRA==", - "requires": { - "json5": "^1.0.1" - } - }, - "json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "requires": { - "minimist": "^1.2.0" - } - }, - "react-redux": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-5.1.2.tgz", - "integrity": "sha512-Ns1G0XXc8hDyH/OcBHOxNgQx9ayH3SPxBnFCOidGKSle8pKihysQw2rG/PmciUQRoclhVBO8HMhiRmGXnDja9Q==", - "requires": { - "@babel/runtime": "^7.1.2", - "hoist-non-react-statics": "^3.3.0", - "invariant": "^2.2.4", - "loose-envify": "^1.1.0", - "prop-types": "^15.6.1", - "react-is": "^16.6.0", - "react-lifecycles-compat": "^3.0.0" - } - } - } - }, "winchan": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/winchan/-/winchan-0.1.4.tgz", @@ -26219,7 +26285,7 @@ }, "topcoder-react-utils": { "version": "github:topcoder-platform/topcoder-react-utils#0fcf9a756a371e0ad633636ba050a7881d862cb8", - "from": "github:topcoder-platform/topcoder-react-utils#v0.9.5", + "from": "github:topcoder-platform/topcoder-react-utils#v6", "requires": { "babel-runtime": "^6.26.0", "body-parser": "^1.18.3", From 64baff25cf1577a2a36cabadfc43a8b6cdaf6020 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Tue, 15 Jul 2025 14:12:42 +1000 Subject: [PATCH 05/76] Update build for v6 --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0eed6eba07..27ea031ee3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -55,7 +55,7 @@ build_steps: &build_steps - run: *install_dependency - run: *install_deploysuite # Restoration of node_modules from cache. - - restore_cache: *restore_cache_settings_for_build + # - restore_cache: *restore_cache_settings_for_build # Build of Docker image. - run: name: "configuring environment" @@ -67,7 +67,7 @@ build_steps: &build_steps # ./buildenv.sh -e ${DEPLOY_ENV} -b dev_communityapp_buildvar,dev_communityapp_deployvar -l dev_communityapp_buildvar_ps - run: *build_docker_image # Caching node modules. - - save_cache: *save_cache_settings + # - save_cache: *save_cache_settings # Deployment. - deploy: name: Running MasterScript From 92f2a10ad3da6f534acfec677390bbe21db63b19 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Tue, 15 Jul 2025 15:26:43 +1000 Subject: [PATCH 06/76] Update for v6 challenge status enum values --- src/shared/actions/challenge-listing/index.js | 14 +++++++------- src/shared/actions/contentful.js | 2 +- .../ApplicationsTab/index.jsx | 2 +- .../ReviewOpportunityDetailsPage/Header/index.jsx | 2 +- .../ReviewOpportunityDetailsPage/index.jsx | 2 +- .../SubmissionManagement/index.jsx | 2 +- .../components/challenge-detail/Header/index.jsx | 4 ++-- .../ChallengesCard/index.jsx | 2 +- .../challenge-detail/Submissions/index.jsx | 4 ++-- .../ChallengeCard/Status/index.jsx | 6 +++--- .../challenge-listing/ChallengeCard/index.jsx | 2 +- .../Filters/FiltersPanel/index.jsx | 2 +- src/shared/containers/Dashboard/ChallengesFeed.jsx | 2 +- .../containers/tc-communities/cognitive/home.jsx | 2 +- src/shared/routes/Communities/Veterans/Routes.jsx | 2 +- src/shared/utils/challenge-detail/helper.jsx | 2 +- src/shared/utils/reviewOpportunities.js | 2 +- 17 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/shared/actions/challenge-listing/index.js b/src/shared/actions/challenge-listing/index.js index f7d109ca89..288811bb01 100644 --- a/src/shared/actions/challenge-listing/index.js +++ b/src/shared/actions/challenge-listing/index.js @@ -200,7 +200,7 @@ function getActiveChallengesDone(uuid, page, backendFilter, tokenV3, frontFilter frontFilter: { ...frontFilter, ...extractSearchFilter(frontFilter), - status: 'Active', + status: 'ACTIVE', currentPhaseName: 'Submission', registrationEndDateEnd: new Date().toISOString(), perPage: PAGE_SIZE, @@ -263,7 +263,7 @@ function getOpenForRegistrationChallengesDone(uuid, page, backendFilter, frontFilter: { ...frontFilter, ...extractSearchFilter(frontFilter), - status: 'Active', + status: 'ACTIVE', currentPhaseName: 'Registration', perPage: PAGE_SIZE, page: page + 1, @@ -299,7 +299,7 @@ function getMyChallengesDone(uuid, page, backendFilter, tokenV3, frontFilter = { frontFilter: { ...frontFilter, ...extractSearchFilter(frontFilter), - status: 'Active', + status: 'ACTIVE', memberId: userId, perPage: PAGE_SIZE, page: page + 1, @@ -325,7 +325,7 @@ function getAllChallengesDone(uuid, page, backendFilter, tokenV3, frontFilter = frontFilter: { ...frontFilter, ...extractSearchFilter(frontFilter), - status: 'Active', + status: 'ACTIVE', perPage: PAGE_SIZE, page: page + 1, sortBy: sortObj.field ? sortObj.field : sorts[BUCKETS.ALL], @@ -351,7 +351,7 @@ function getMyPastChallengesDone(uuid, page, backendFilter, tokenV3, frontFilter frontFilter: { ...frontFilter, ...extractSearchFilter(frontFilter), - status: 'Completed', + status: 'COMPLETED', memberId: userId, perPage: PAGE_SIZE, page: page + 1, @@ -379,7 +379,7 @@ function getTotalChallengesCountDone(uuid, tokenV3, frontFilter = {}) { frontFilter: { ...frontFilter, ...extractSearchFilter(frontFilter), - status: 'Active', + status: 'ACTIVE', isLightweight: true, perPage: 1, }, @@ -462,7 +462,7 @@ function getPastChallengesDone(uuid, page, backendFilter, tokenV3, frontFilter = frontFilter: { ...frontFilter, ...extractSearchFilter(frontFilter), - status: 'Completed', + status: 'COMPLETED', perPage: PAGE_SIZE, page: page + 1, sortBy: sortObj.field ? sortObj.field : sorts[BUCKETS.ALL_PAST], diff --git a/src/shared/actions/contentful.js b/src/shared/actions/contentful.js index 28dd6c0f3e..986a2d2afe 100644 --- a/src/shared/actions/contentful.js +++ b/src/shared/actions/contentful.js @@ -342,7 +342,7 @@ async function getChallengesBlockDone(blockProps) { const challengesService = services.challenge.getService(); const filter = {}; if (!block.fields.completedChallenges) { - filter.status = 'Active'; + filter.status = 'ACTIVE'; } if (block.fields.challengeTitleContains) { filter.name = block.fields.challengeTitleContains; diff --git a/src/shared/components/ReviewOpportunityDetailsPage/ApplicationsTab/index.jsx b/src/shared/components/ReviewOpportunityDetailsPage/ApplicationsTab/index.jsx index c57b860022..c45d2d9cbc 100644 --- a/src/shared/components/ReviewOpportunityDetailsPage/ApplicationsTab/index.jsx +++ b/src/shared/components/ReviewOpportunityDetailsPage/ApplicationsTab/index.jsx @@ -30,7 +30,7 @@ const ApplicationsTab = ({ applications }) => (
{ applications - && applications.filter(app => app.status !== 'Cancelled').map(app => ( + && applications.filter(app => app.status !== 'CANCELLED').map(app => (
diff --git a/src/shared/components/ReviewOpportunityDetailsPage/Header/index.jsx b/src/shared/components/ReviewOpportunityDetailsPage/Header/index.jsx index 2ffe5f2382..91709196b1 100644 --- a/src/shared/components/ReviewOpportunityDetailsPage/Header/index.jsx +++ b/src/shared/components/ReviewOpportunityDetailsPage/Header/index.jsx @@ -31,7 +31,7 @@ const Header = ({ _.toString(app.handle) === _.toString(handle) && app.status !== 'Cancelled'))} + hasApplied={Boolean(_.find(details.applications, app => _.toString(app.handle) === _.toString(handle) && app.status !== 'CANCELLED'))} startDate={details.startDate} completed={details.openPositions === 0} /> diff --git a/src/shared/components/ReviewOpportunityDetailsPage/index.jsx b/src/shared/components/ReviewOpportunityDetailsPage/index.jsx index 7e776f7147..a322f105e9 100644 --- a/src/shared/components/ReviewOpportunityDetailsPage/index.jsx +++ b/src/shared/components/ReviewOpportunityDetailsPage/index.jsx @@ -71,7 +71,7 @@ const ReviewOpportunityDetailsPage = ({ > REVIEW APPLICATIONS {' '} - {`(${details.applications ? details.applications.filter(app => app.status !== 'Cancelled').length : 0})`} + {`(${details.applications ? details.applications.filter(app => app.status !== 'CANCELLED').length : 0})`}
diff --git a/src/shared/components/SubmissionManagement/SubmissionManagement/index.jsx b/src/shared/components/SubmissionManagement/SubmissionManagement/index.jsx index 9ab0c2314a..4e1182fafc 100644 --- a/src/shared/components/SubmissionManagement/SubmissionManagement/index.jsx +++ b/src/shared/components/SubmissionManagement/SubmissionManagement/index.jsx @@ -129,7 +129,7 @@ export default function SubmissionManagement(props) { } { - challenge.status !== 'Completed' ? ( + challenge.status !== 'COMPLETED' ? (

Current Deadline Ends: {' '} diff --git a/src/shared/components/challenge-detail/Header/index.jsx b/src/shared/components/challenge-detail/Header/index.jsx index a7426e0d7c..b2e716d09c 100644 --- a/src/shared/components/challenge-detail/Header/index.jsx +++ b/src/shared/components/challenge-detail/Header/index.jsx @@ -124,7 +124,7 @@ export default function ChallengeHeader(props) { let registrationEnded = true; const regPhase = phases && phases.registration; - if (status !== 'Completed' && regPhase) { + if (status !== 'COMPLETED' && regPhase) { registrationEnded = !regPhase.isOpen; } @@ -235,7 +235,7 @@ export default function ChallengeHeader(props) { if (trackLower === 'quality-assurance') { relevantPhases = _.filter(relevantPhases, p => !(p.name.toLowerCase().includes('specification submission') || p.name.toLowerCase().includes('specification review'))); } - if (type === 'First2Finish' && status === 'Completed') { + if (type === 'First2Finish' && status === 'COMPLETED') { const phases2 = allPhases.filter(p => p.name === 'Iterative Review' && !p.isOpen); const endPhaseDate = Math.max(...phases2.map(d => phaseEndDate(d))); relevantPhases = _.filter(relevantPhases, p => (p.name.toLowerCase().includes('registration') diff --git a/src/shared/components/challenge-detail/RecommendedActiveChallenges/ChallengesCard/index.jsx b/src/shared/components/challenge-detail/RecommendedActiveChallenges/ChallengesCard/index.jsx index 3bdeb882f6..b369983490 100644 --- a/src/shared/components/challenge-detail/RecommendedActiveChallenges/ChallengesCard/index.jsx +++ b/src/shared/components/challenge-detail/RecommendedActiveChallenges/ChallengesCard/index.jsx @@ -83,7 +83,7 @@ export default function ChallengesCard({

- {challenge.status === 'Active' ? 'Ends ' : 'Ended '} + {challenge.status === 'ACTIVE' ? 'Ends ' : 'Ended '} {getEndDate(challenge)}
diff --git a/src/shared/components/challenge-detail/Submissions/index.jsx b/src/shared/components/challenge-detail/Submissions/index.jsx index 377259daaa..60c4071320 100644 --- a/src/shared/components/challenge-detail/Submissions/index.jsx +++ b/src/shared/components/challenge-detail/Submissions/index.jsx @@ -120,7 +120,7 @@ class SubmissionsComponent extends React.Component { if (!_.isEmpty(submission.review) && !_.isEmpty(submission.review[0]) && submission.review[0].score - && (challenge.status === 'Completed' + && (challenge.status === 'COMPLETED' || (_.includes(challenge.tags, 'Innovation Challenge') && _.find(challenge.metadata, { name: 'show_data_dashboard' })))) { score = Number(submission.review[0].score).toFixed(2); } @@ -940,7 +940,7 @@ class SubmissionsComponent extends React.Component {
FINAL SCORE

{ - (s.reviewSummation && s.reviewSummation[0].aggregateScore && challenge.status === 'Completed') + (s.reviewSummation && s.reviewSummation[0].aggregateScore && challenge.status === 'COMPLETED') ? s.reviewSummation[0].aggregateScore.toFixed(2) : 'N/A' } diff --git a/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx b/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx index 503718e85e..dd77c03c2e 100644 --- a/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx +++ b/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx @@ -239,7 +239,7 @@ export default function ChallengeStatus(props) { let phaseMessage = STALLED_MSG; if (statusPhase) phaseMessage = statusPhase.name; - else if (status === 'Draft') phaseMessage = DRAFT_MSG; + else if (status === 'DRAFT') phaseMessage = DRAFT_MSG; const showRegisterInfo = false; @@ -287,7 +287,7 @@ export default function ChallengeStatus(props) { { - status === 'Active' && statusPhase ? ( + status === 'ACTIVE' && statusPhase ? (

diff --git a/src/shared/components/challenge-listing/ChallengeCard/index.jsx b/src/shared/components/challenge-listing/ChallengeCard/index.jsx index 3fb2d7b812..076b3598e4 100644 --- a/src/shared/components/challenge-listing/ChallengeCard/index.jsx +++ b/src/shared/components/challenge-listing/ChallengeCard/index.jsx @@ -84,7 +84,7 @@ function ChallengeCard({
- {challenge.status === 'Active' ? 'Ends ' : 'Ended '} + {challenge.status === 'ACTIVE' ? 'Ends ' : 'Ended '} {getEndDate(challenge)} { diff --git a/src/shared/components/challenge-listing/Filters/FiltersPanel/index.jsx b/src/shared/components/challenge-listing/Filters/FiltersPanel/index.jsx index 075eb87c46..d2a0b85fb9 100644 --- a/src/shared/components/challenge-listing/Filters/FiltersPanel/index.jsx +++ b/src/shared/components/challenge-listing/Filters/FiltersPanel/index.jsx @@ -479,7 +479,7 @@ export default function FiltersPanel({ events: [], endDateStart: null, startDateEnd: null, - status: 'Active', + status: 'ACTIVE', reviewOpportunityTypes: _.keys(REVIEW_OPPORTUNITY_TYPES), customDate: false, recommended: false, diff --git a/src/shared/containers/Dashboard/ChallengesFeed.jsx b/src/shared/containers/Dashboard/ChallengesFeed.jsx index 113aa5881c..b8ede649ca 100644 --- a/src/shared/containers/Dashboard/ChallengesFeed.jsx +++ b/src/shared/containers/Dashboard/ChallengesFeed.jsx @@ -23,7 +23,7 @@ class ChallengesFeedContainer extends React.Component { perPage: excludeTags && excludeTags.length ? undefined : itemCount, types: ['CH', 'F2F', 'MM'], tracks, - status: 'Active', + status: 'ACTIVE', sortBy: 'updated', sortOrder: 'desc', isLightweight: true, diff --git a/src/shared/containers/tc-communities/cognitive/home.jsx b/src/shared/containers/tc-communities/cognitive/home.jsx index aa817e8b7a..53ebdc8222 100644 --- a/src/shared/containers/tc-communities/cognitive/home.jsx +++ b/src/shared/containers/tc-communities/cognitive/home.jsx @@ -54,7 +54,7 @@ class HomeContainer extends React.Component { if (filter) { filter = Filter.getFilterFunction(filter.challengeFilter); challenges = activeChallenges - .filter(x => x.status === 'Active') + .filter(x => x.status === 'ACTIVE') .filter(filter) .sort((a, b) => moment(a.registrationStartDate).diff(b.registrationStartDate)); } diff --git a/src/shared/routes/Communities/Veterans/Routes.jsx b/src/shared/routes/Communities/Veterans/Routes.jsx index c15f932bc7..7ebf832deb 100644 --- a/src/shared/routes/Communities/Veterans/Routes.jsx +++ b/src/shared/routes/Communities/Veterans/Routes.jsx @@ -31,7 +31,7 @@ export default function Veterans({ base, member, meta }) { registerBucket(ID, { filter: { ...meta.challengeFilter, - status: 'Active', + status: 'ACTIVE', }, hideCount: false, name: 'Active Veterans Challenges', diff --git a/src/shared/utils/challenge-detail/helper.jsx b/src/shared/utils/challenge-detail/helper.jsx index f331c239a1..cf0b87b92f 100644 --- a/src/shared/utils/challenge-detail/helper.jsx +++ b/src/shared/utils/challenge-detail/helper.jsx @@ -40,7 +40,7 @@ export function getChallengeTypeAbbr(track, challengeTypes) { export function getEndDate(challenge) { const { type } = challenge; let phases = challenge.phases || []; - if (type === 'First2Finish' && challenge.status === 'Completed') { + if (type === 'First2Finish' && challenge.status === 'COMPLETED') { phases = challenge.phases.filter(p => p.phaseType === 'Iterative Review' && p.phaseStatus === 'Closed'); } const endPhaseDate = Math.max(...phases.map(d => phaseEndDate(d))); diff --git a/src/shared/utils/reviewOpportunities.js b/src/shared/utils/reviewOpportunities.js index 206f23bc88..96845c37bf 100644 --- a/src/shared/utils/reviewOpportunities.js +++ b/src/shared/utils/reviewOpportunities.js @@ -45,7 +45,7 @@ export const openPositionsByRole = (details) => { export const activeRoleIds = (details, handle) => { const positions = openPositionsByRole(details); const apps = details.applications - ? details.applications.filter(app => _.toString(app.handle) === _.toString(handle) && app.status !== 'Cancelled') : []; + ? details.applications.filter(app => _.toString(app.handle) === _.toString(handle) && app.status !== 'CANCELLED') : []; return apps.map(app => positions.find(p => p.role === app.role).roleId); }; From f31f4fddce2ca6ef08d76aaaba45b7c23fd6eeeb Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Mon, 11 Aug 2025 15:06:26 +1000 Subject: [PATCH 07/76] CI/CD From 2877409e0f286390d1b2ddd13b79af0835571eba Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Wed, 27 Aug 2025 11:43:08 +1000 Subject: [PATCH 08/76] Empty commit for CI/CD From 51688a9eb570e088a6ce22ab4d0c6a3da3d95934 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Wed, 3 Sep 2025 15:25:28 +1000 Subject: [PATCH 09/76] Match expected enum values --- src/shared/actions/tc-communities/index.js | 2 +- src/shared/components/SubmissionPage/Submit/index.jsx | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/shared/actions/tc-communities/index.js b/src/shared/actions/tc-communities/index.js index 4b14591442..7ba9610cb5 100644 --- a/src/shared/actions/tc-communities/index.js +++ b/src/shared/actions/tc-communities/index.js @@ -14,7 +14,7 @@ const getGroupService = services.groups.getService; /* TODO: Should be updated to add member to all groups related to the community. */ function joinDone(token, groupId, memberId) { - const service = getGroupService(token); + const service = getGroupService(token); return service.addMember(groupId, memberId, 'user'); } diff --git a/src/shared/components/SubmissionPage/Submit/index.jsx b/src/shared/components/SubmissionPage/Submit/index.jsx index 708182ea40..662fef81ed 100644 --- a/src/shared/components/SubmissionPage/Submit/index.jsx +++ b/src/shared/components/SubmissionPage/Submit/index.jsx @@ -78,13 +78,13 @@ class Submit extends React.Component { // Submission type logic if (checkpoint && checkpoint.isOpen) { - subType = 'Checkpoint Submission'; + subType = 'CHECKPOINT_SUBMISSION'; } else if (checkpoint && !checkpoint.isOpen && submission && submission.isOpen) { - subType = 'Contest Submission'; + subType = 'CONTEST_SUBMISSION'; } else if (finalFix && finalFix.isOpen) { - subType = 'Studio Final Fix Submission'; + subType = 'STUDIO_FINAL_FIX_SUBMISSION'; } else { - subType = 'Contest Submission'; + subType = 'CONTEST_SUBMISSION'; } return subType; From aa4b3298f47b3d4a73a92eb114d550bbf12c07a1 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Wed, 3 Sep 2025 15:50:01 +1000 Subject: [PATCH 10/76] Lint --- src/shared/actions/tc-communities/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/actions/tc-communities/index.js b/src/shared/actions/tc-communities/index.js index 7ba9610cb5..4b14591442 100644 --- a/src/shared/actions/tc-communities/index.js +++ b/src/shared/actions/tc-communities/index.js @@ -14,7 +14,7 @@ const getGroupService = services.groups.getService; /* TODO: Should be updated to add member to all groups related to the community. */ function joinDone(token, groupId, memberId) { - const service = getGroupService(token); + const service = getGroupService(token); return service.addMember(groupId, memberId, 'user'); } From 60457f462723694863cc87ca930e43248447b8b1 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Thu, 4 Sep 2025 21:19:02 +0530 Subject: [PATCH 11/76] fix: update the review opportunity list update the review opportunity bucket to work with new review api Fixes #1760 --- src/shared/actions/challenge-listing/index.js | 7 +++--- .../Listing/ReviewOpportunityBucket/index.jsx | 18 ++++++++++----- .../challenge-listing/Listing/index.jsx | 3 +++ .../ReviewOpportunityCard/index.jsx | 12 +++++----- .../challenge-listing/Listing/index.jsx | 6 ++--- .../reducers/challenge-listing/index.js | 4 ++-- src/shared/services/reviewOpportunities.js | 23 +++++++++++++++++++ 7 files changed, 52 insertions(+), 21 deletions(-) create mode 100644 src/shared/services/reviewOpportunities.js diff --git a/src/shared/actions/challenge-listing/index.js b/src/shared/actions/challenge-listing/index.js index 288811bb01..845e21bf62 100644 --- a/src/shared/actions/challenge-listing/index.js +++ b/src/shared/actions/challenge-listing/index.js @@ -10,10 +10,10 @@ import { processSRM } from 'utils/tc'; import { errors, services } from 'topcoder-react-lib'; import { BUCKETS } from 'utils/challenge-listing/buckets'; import SORT from 'utils/challenge-listing/sort'; +import getReviewOpportunities from 'services/reviewOpportunities'; const { fireErrorMessage } = errors; const { getService } = services.challenge; -const { getReviewOpportunitiesService } = services.reviewOpportunities; /** * The maximum number of challenges to fetch in a single API call. @@ -486,9 +486,8 @@ function getPastChallengesDone(uuid, page, backendFilter, tokenV3, frontFilter = * @param {String} tokenV3 Optional. Topcoder auth token v3. * @return {Object} Action object */ -function getReviewOpportunitiesDone(uuid, page, tokenV3) { - return getReviewOpportunitiesService(tokenV3) - .getReviewOpportunities(REVIEW_OPPORTUNITY_PAGE_SIZE, page * REVIEW_OPPORTUNITY_PAGE_SIZE) +function getReviewOpportunitiesDone(uuid, page) { + return getReviewOpportunities(page, REVIEW_OPPORTUNITY_PAGE_SIZE) .then(loaded => ({ uuid, loaded })) .catch((error) => { fireErrorMessage('Error Getting Review Opportunities', error.content || error); diff --git a/src/shared/components/challenge-listing/Listing/ReviewOpportunityBucket/index.jsx b/src/shared/components/challenge-listing/Listing/ReviewOpportunityBucket/index.jsx index 07cc41627d..4f58e3a687 100644 --- a/src/shared/components/challenge-listing/Listing/ReviewOpportunityBucket/index.jsx +++ b/src/shared/components/challenge-listing/Listing/ReviewOpportunityBucket/index.jsx @@ -1,16 +1,18 @@ /** * The bucket for review opportunities. */ +import React from 'react'; import _ from 'lodash'; import PT from 'prop-types'; -import React from 'react'; -import Sort from 'utils/challenge-listing/sort'; -import { BUCKET_DATA } from 'utils/challenge-listing/buckets'; -import SortingSelectBar from 'components/SortingSelectBar'; import Waypoint from 'react-waypoint'; import { challenge as challengeUtils } from 'topcoder-react-lib'; -import CardPlaceholder from '../../placeholders/ChallengeCard'; + +import SortingSelectBar from 'components/SortingSelectBar'; +import { BUCKET_DATA } from 'utils/challenge-listing/buckets'; +import Sort from 'utils/challenge-listing/sort'; + import ReviewOpportunityCard from '../../ReviewOpportunityCard'; +import CardPlaceholder from '../../placeholders/ChallengeCard'; import './style.scss'; @@ -44,6 +46,8 @@ export default function ReviewOpportunityBucket({ const sortedOpportunities = _.clone(opportunities); sortedOpportunities.sort(Sort[activeSort].func); + console.log('sorted opportunities', sortedOpportunities); + /* Filtering for Review Opportunities will be done entirely in the front-end * which means it can be done at render, rather than in the reducer, * which avoids reloading the review opportunities from server every time @@ -56,6 +60,8 @@ export default function ReviewOpportunityBucket({ // }), ); + console.log('filtered opportunities', filteredOpportunities); + const cards = filteredOpportunities.map(item => ( ) : null } diff --git a/src/shared/components/challenge-listing/Listing/index.jsx b/src/shared/components/challenge-listing/Listing/index.jsx index badc681731..8128e546cc 100644 --- a/src/shared/components/challenge-listing/Listing/index.jsx +++ b/src/shared/components/challenge-listing/Listing/index.jsx @@ -56,6 +56,7 @@ function Listing({ loadMoreOnGoing, loadMorePast, loadMoreReviewOpportunities, + allReviewOpportunitiesLoaded, newChallengeDetails, openChallengesInNewTabs, preListingMsg, @@ -164,6 +165,7 @@ function Listing({ challengeTypes={challengeTypes} isLoggedIn={isLoggedIn} setSearchText={setSearchText} + allReviewOpportunitiesLoaded={allReviewOpportunitiesLoaded} /> ) : ( @@ -365,6 +367,7 @@ Listing.propTypes = { isLoggedIn: PT.bool.isRequired, meta: PT.shape().isRequired, setSearchText: PT.func.isRequired, + allReviewOpportunitiesLoaded: PT.bool.isRequired, }; const mapStateToProps = (state) => { diff --git a/src/shared/components/challenge-listing/ReviewOpportunityCard/index.jsx b/src/shared/components/challenge-listing/ReviewOpportunityCard/index.jsx index c71550717e..d880792b3b 100644 --- a/src/shared/components/challenge-listing/ReviewOpportunityCard/index.jsx +++ b/src/shared/components/challenge-listing/ReviewOpportunityCard/index.jsx @@ -3,13 +3,13 @@ * information. Will be contained within a Bucket. */ import _ from 'lodash'; -import { Link } from 'topcoder-react-utils'; import moment from 'moment'; -import React, { useMemo } from 'react'; import PT from 'prop-types'; +import React, { useMemo } from 'react'; +import { Link } from 'topcoder-react-utils'; -import TrackIcon from 'components/TrackIcon'; import Tooltip from 'components/Tooltip'; +import TrackIcon from 'components/TrackIcon'; import { time } from 'topcoder-react-lib'; import { REVIEW_OPPORTUNITY_TYPES } from 'utils/tc'; @@ -18,8 +18,8 @@ import Tags from '../Tags'; import TrackAbbreviationTooltip from '../Tooltips/TrackAbbreviationTooltip'; -import SubmissionsIcon from '../Icons/SubmissionsIcon'; import OpenPositionsIcon from '../Icons/RegistrantsIcon'; +import SubmissionsIcon from '../Icons/SubmissionsIcon'; import './style.scss'; @@ -47,13 +47,13 @@ function ReviewOpportunityCard({ opportunity, challengeType, }) { - const { challenge } = opportunity; + const { challengeData: challenge } = opportunity; let tags = challenge.tags || challenge.technologies; const skills = useMemo(() => _.uniq((challenge.skills || []).map(skill => skill.name)), [ challenge.skills, ]); tags = tags.filter(tag => tag.trim().length); - const { track } = challenge.track; + const { track } = challenge; const start = moment(opportunity.startDate); return (
diff --git a/src/shared/containers/challenge-listing/Listing/index.jsx b/src/shared/containers/challenge-listing/Listing/index.jsx index c37c9d9b0b..f76586e8cf 100644 --- a/src/shared/containers/challenge-listing/Listing/index.jsx +++ b/src/shared/containers/challenge-listing/Listing/index.jsx @@ -597,7 +597,7 @@ export class ListingContainer extends React.Component { let loadMoreReviewOpportunities; if (!allReviewOpportunitiesLoaded) { loadMoreReviewOpportunities = () => getReviewOpportunities( - 1 + lastRequestedPageOfReviewOpportunities, tokenV3, + 1 + lastRequestedPageOfReviewOpportunities, ); } @@ -976,10 +976,10 @@ function mapDispatchToProps(dispatch) { dispatch(a.getPastChallengesInit(uuid, page, frontFilter)); dispatch(a.getPastChallengesDone(uuid, page, filter, token, frontFilter)); }, - getReviewOpportunities: (page, token) => { + getReviewOpportunities: (page) => { const uuid = shortId(); dispatch(a.getReviewOpportunitiesInit(uuid, page)); - dispatch(a.getReviewOpportunitiesDone(uuid, page, token)); + dispatch(a.getReviewOpportunitiesDone(uuid, page)); }, selectBucket: (bucket, expanding) => dispatch(sa.selectBucket(bucket, expanding)), selectBucketDone: () => dispatch(sa.selectBucketDone()), diff --git a/src/shared/reducers/challenge-listing/index.js b/src/shared/reducers/challenge-listing/index.js index add5ae8941..be89e7d1e9 100644 --- a/src/shared/reducers/challenge-listing/index.js +++ b/src/shared/reducers/challenge-listing/index.js @@ -482,9 +482,9 @@ function onGetReviewOpportunitiesDone(state, { payload, error }) { if (uuid !== state.loadingReviewOpportunitiesUUID) return state; const ids = new Set(); - loaded.forEach(item => ids.add(item.id)); + loaded.forEach(item => ids.add(item.challengeId)); const reviewOpportunities = state.reviewOpportunities - .filter(item => !ids.has(item.id)) + .filter(item => !ids.has(item.challengeId)) .concat(loaded); return { diff --git a/src/shared/services/reviewOpportunities.js b/src/shared/services/reviewOpportunities.js new file mode 100644 index 0000000000..ee7a32e63f --- /dev/null +++ b/src/shared/services/reviewOpportunities.js @@ -0,0 +1,23 @@ +import { config } from 'topcoder-react-utils'; + +const v6ApiUrl = config.API.V6; + +/** + * Fetches copilot opportunities. + * + * @param {number} page - Page number (1-based). + * @param {number} pageSize - Number of items per page. + * @param {string} sort - Sort order (e.g., 'createdAt desc'). + * @returns {Promise} The fetched data. + */ +export default function getReviewOpportunities(page, pageSize) { + const offset = page * pageSize; + + const url = new URL(`${v6ApiUrl}/review-opportunities`); + url.searchParams.append('limit', pageSize); + url.searchParams.append('offset', offset); + + return fetch(url.toString(), { + method: 'GET', + }).then(res => res.json()); +} From 213dce4f7287a4a3962a3ebb7f0d40979e8b2ccd Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Thu, 4 Sep 2025 21:27:57 +0530 Subject: [PATCH 12/76] Deploy 1760 --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 27ea031ee3..7512aa664e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -182,6 +182,7 @@ workflows: only: - develop - v6 + - PM-1760 - "build-prod": context: org-global From a298aaf1a183c7b8f8d209289dfdeb5611bc5aad Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Fri, 5 Sep 2025 16:46:49 +0530 Subject: [PATCH 13/76] fix: normalize response normalise response and test Fixes #PM-1760 --- src/shared/services/reviewOpportunities.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/shared/services/reviewOpportunities.js b/src/shared/services/reviewOpportunities.js index ee7a32e63f..b8247ef96e 100644 --- a/src/shared/services/reviewOpportunities.js +++ b/src/shared/services/reviewOpportunities.js @@ -10,14 +10,19 @@ const v6ApiUrl = config.API.V6; * @param {string} sort - Sort order (e.g., 'createdAt desc'). * @returns {Promise} The fetched data. */ -export default function getReviewOpportunities(page, pageSize) { +export default async function getReviewOpportunities(page, pageSize) { const offset = page * pageSize; const url = new URL(`${v6ApiUrl}/review-opportunities`); url.searchParams.append('limit', pageSize); url.searchParams.append('offset', offset); - return fetch(url.toString(), { - method: 'GET', - }).then(res => res.json()); + const res = await fetch(url.toString(), { method: 'GET' }); + const data = await res.json(); + + return data.map(item => ({ + ...item, + challengeData: item.challengeData || item.challenge || {}, + type: item.type || '', + })); } From 59cd4342fbb7802128d53f99dec1086f89bae05f Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Tue, 9 Sep 2025 10:17:23 +1000 Subject: [PATCH 14/76] Fix for new submissions response --- src/shared/containers/challenge-detail/index.jsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/shared/containers/challenge-detail/index.jsx b/src/shared/containers/challenge-detail/index.jsx index 272aebc19e..bdf6ca5c6a 100644 --- a/src/shared/containers/challenge-detail/index.jsx +++ b/src/shared/containers/challenge-detail/index.jsx @@ -837,7 +837,12 @@ function mapStateToProps(state, props) { })); if (challenge.submissions) { - challenge.submissions = challenge.submissions.map(submission => ({ + // Normalize submissions shape: API may return { data, meta } now. + const normalizedSubmissions = Array.isArray(challenge.submissions) + ? challenge.submissions + : (_.get(challenge, 'submissions.data') || []); + + challenge.submissions = normalizedSubmissions.map(submission => ({ ...submission, registrant: _.find(challenge.registrants, r => (`${r.memberId}` === `${submission.memberId}`)), })); From 777182a07fc5d393025d292a41d2ad7aff953843 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Tue, 9 Sep 2025 10:23:14 +1000 Subject: [PATCH 15/76] Remove Husky --- .huskyrc.json | 5 ----- package.json | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) delete mode 100644 .huskyrc.json diff --git a/.huskyrc.json b/.huskyrc.json deleted file mode 100644 index 8fa9b4f133..0000000000 --- a/.huskyrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "hooks": { - "commit-msg": "npm run commitlint" - } -} diff --git a/package.json b/package.json index 6959e6f8ee..7f15ec6c33 100644 --- a/package.json +++ b/package.json @@ -212,7 +212,7 @@ "eslint-plugin-react": "^7.32.2", "file-loader": "^1.1.11", "happypack": "^5.0.1", - "husky": "^4.2.1", + "identity-obj-proxy": "^3.0.0", "jest": "^23.2.0", "jsdoc-to-markdown": "^4.0.1", From 761331c2119f5aff94529b5cb7d380eee25c60ec Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Tue, 9 Sep 2025 13:26:53 +1000 Subject: [PATCH 16/76] Further v6 tweaks --- src/shared/actions/mmLeaderboard.js | 2 +- src/shared/actions/page/profile.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/shared/actions/mmLeaderboard.js b/src/shared/actions/mmLeaderboard.js index 956671d831..08baf1b053 100644 --- a/src/shared/actions/mmLeaderboard.js +++ b/src/shared/actions/mmLeaderboard.js @@ -42,7 +42,7 @@ async function getMMLeaderboardDone(id) { })); // Fetch member photos and rating for top 10 const results = await Promise.all( - _.take(data, 10).map(d => fetch(`${config.API.V5}/members/${d.createdBy}`)), + _.take(data, 10).map(d => fetch(`${config.API.V6}/members/${d.createdBy}`)), ); const memberData = await Promise.all(results.map(r => r.json())); // merge with data diff --git a/src/shared/actions/page/profile.js b/src/shared/actions/page/profile.js index 60e15d117b..57ff1e54a2 100644 --- a/src/shared/actions/page/profile.js +++ b/src/shared/actions/page/profile.js @@ -23,9 +23,9 @@ async function getGamificationBadgesInit(handle) { */ async function getGamificationBadgesDone(handle, limit) { try { - const memberInfo = await fetch(`${config.API.V5}/members/${handle}`) + const memberInfo = await fetch(`${config.API.V6}/members/${handle}`) .then(response => response.json()); - const badges = await fetch(`${config.API.V5}/gamification/badges/assigned/${memberInfo.userId}?organization_id=${config.GAMIFICATION.ORG_ID}&limit=${limit || 4}`) + const badges = await fetch(`${config.API.V6}/gamification/badges/assigned/${memberInfo.userId}?organization_id=${config.GAMIFICATION.ORG_ID}&limit=${limit || 4}`) .then(response => response.json()); return { From 4b48855a46872411843626f76162e94688706cb0 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Wed, 10 Sep 2025 13:40:32 +0530 Subject: [PATCH 17/76] fix: integrate topcoder-react-lib code to CA Previously, the filter function was defined in topcoder-react-lib Fixes #1760 --- .../Listing/ReviewOpportunityBucket/index.jsx | 13 +-- src/shared/services/reviewOpportunities.js | 13 +-- src/shared/utils/challenge-listing/sort.js | 2 +- src/shared/utils/reviewOpportunities.js | 100 ++++++++++++++++++ src/shared/utils/tc.js | 9 +- 5 files changed, 116 insertions(+), 21 deletions(-) diff --git a/src/shared/components/challenge-listing/Listing/ReviewOpportunityBucket/index.jsx b/src/shared/components/challenge-listing/Listing/ReviewOpportunityBucket/index.jsx index 4f58e3a687..b7c8d4912c 100644 --- a/src/shared/components/challenge-listing/Listing/ReviewOpportunityBucket/index.jsx +++ b/src/shared/components/challenge-listing/Listing/ReviewOpportunityBucket/index.jsx @@ -5,19 +5,16 @@ import React from 'react'; import _ from 'lodash'; import PT from 'prop-types'; import Waypoint from 'react-waypoint'; -import { challenge as challengeUtils } from 'topcoder-react-lib'; - import SortingSelectBar from 'components/SortingSelectBar'; import { BUCKET_DATA } from 'utils/challenge-listing/buckets'; import Sort from 'utils/challenge-listing/sort'; +import { getReviewOpportunitiesFilterFunction } from 'utils/reviewOpportunities'; import ReviewOpportunityCard from '../../ReviewOpportunityCard'; import CardPlaceholder from '../../placeholders/ChallengeCard'; import './style.scss'; -const Filter = challengeUtils.filter; - const NO_RESULTS_MESSAGE = 'No challenges found'; const LOADING_MESSAGE = 'Loading Challenges'; @@ -46,26 +43,22 @@ export default function ReviewOpportunityBucket({ const sortedOpportunities = _.clone(opportunities); sortedOpportunities.sort(Sort[activeSort].func); - console.log('sorted opportunities', sortedOpportunities); - /* Filtering for Review Opportunities will be done entirely in the front-end * which means it can be done at render, rather than in the reducer, * which avoids reloading the review opportunities from server every time * a filter is changed. */ const filteredOpportunities = sortedOpportunities.filter( - Filter.getReviewOpportunitiesFilterFunction({ + getReviewOpportunitiesFilterFunction({ ...BUCKET_DATA[bucket].filter, // Default bucket filters from utils/buckets.js ...filterState, // User selected filters }, challengeTypes), // }), ); - console.log('filtered opportunities', filteredOpportunities); - const cards = filteredOpportunities.map(item => ( { diff --git a/src/shared/services/reviewOpportunities.js b/src/shared/services/reviewOpportunities.js index b8247ef96e..ee7a32e63f 100644 --- a/src/shared/services/reviewOpportunities.js +++ b/src/shared/services/reviewOpportunities.js @@ -10,19 +10,14 @@ const v6ApiUrl = config.API.V6; * @param {string} sort - Sort order (e.g., 'createdAt desc'). * @returns {Promise} The fetched data. */ -export default async function getReviewOpportunities(page, pageSize) { +export default function getReviewOpportunities(page, pageSize) { const offset = page * pageSize; const url = new URL(`${v6ApiUrl}/review-opportunities`); url.searchParams.append('limit', pageSize); url.searchParams.append('offset', offset); - const res = await fetch(url.toString(), { method: 'GET' }); - const data = await res.json(); - - return data.map(item => ({ - ...item, - challengeData: item.challengeData || item.challenge || {}, - type: item.type || '', - })); + return fetch(url.toString(), { + method: 'GET', + }).then(res => res.json()); } diff --git a/src/shared/utils/challenge-listing/sort.js b/src/shared/utils/challenge-listing/sort.js index 089cd519a7..1d8b6ba2b4 100644 --- a/src/shared/utils/challenge-listing/sort.js +++ b/src/shared/utils/challenge-listing/sort.js @@ -92,7 +92,7 @@ export default { order: 'asc', }, [SORTS.REVIEW_OPPORTUNITIES_TITLE_A_TO_Z]: { - func: (a, b) => a.challenge.title.localeCompare(b.challenge.title), + func: (a, b) => a.challengeName.localeCompare(b.challengeName), name: 'Title A-Z', }, [SORTS.REVIEW_OPPORTUNITIES_PAYMENT]: { diff --git a/src/shared/utils/reviewOpportunities.js b/src/shared/utils/reviewOpportunities.js index 96845c37bf..95a820d481 100644 --- a/src/shared/utils/reviewOpportunities.js +++ b/src/shared/utils/reviewOpportunities.js @@ -2,6 +2,8 @@ * Utility functions for Review Opportunities */ import _ from 'lodash'; +import moment from 'moment'; +import { REVIEW_OPPORTUNITY_TYPES } from './tc'; /** * Infers open positions using review opportunity details and organizes them by role @@ -50,3 +52,101 @@ export const activeRoleIds = (details, handle) => { }; export default null; + +/** + * Filter function for Review Opportunity Type, will be used internally in filter.js + * @param {Object} opp Review Opportunity object + * @param {Object} state Filter state + * @return {Boolean} True if opp satifies the filter + */ +function filterByReviewOpportunityType(opp, state) { + if (state.reviewOpportunityTypes.length === 0) return false; + return state.reviewOpportunityTypes.includes(opp.type); +} + +function filterByStartDate(challenge, state) { + if (!state.startDate) return true; + const submissionPhase = (challenge.phases || []).filter(d => d.name === 'Submission')[0]; + const submissionEndDate = submissionPhase ? submissionPhase.scheduledEndDate + : challenge.submissionEndDate; + return moment(state.startDate).isBefore(submissionEndDate); +} + +function filterByEndDate(challenge, state) { + if (!state.endDate) return true; + const registrationPhase = (challenge.phases || []).filter(d => d.name === 'Registration')[0]; + const registrationStartDate = registrationPhase ? registrationPhase.scheduledStartDate + : challenge.registrationStartDate; + return moment(state.endDate).isAfter(registrationStartDate); +} + +function filterByTags(challenge, state) { + if (_.isEmpty(state.tags)) return true; + const { platforms, tags } = challenge; + const str = `${platforms.join(' ')} ${tags.join(' ')}`.toLowerCase(); + return state.tags.some(tag => str.includes(tag.toLowerCase())); +} + +function filterByText(challenge, state) { + if (!state.search) return true; + const str = `${challenge.name} ${challenge.tags} ${challenge.platforms} ${challenge.tags}` + .toLowerCase(); + return str.includes(state.search.toLowerCase()); +} + +function filterByTrack(challenge, state) { + // if (!state.tracks) return true; + // eslint-disable-next-line max-len + return state.tracks[challenge.track] === true; +} + +function filterByTypes(challenge, state) { + if (state.types.length === 0) return true; + return state.types.includes(challenge.typeId); +} + +/** + * Generates a Review Opportunities filter function for the provided filter state. + * @param {Object} state + * @return {Function} + */ +export const getReviewOpportunitiesFilterFunction = (state, validTypes) => (opp) => { + const trackAbbr = { + 'Data Science': 'DS', + Development: 'Dev', + Design: 'Des', + 'Quality Assurance': 'QA', + }; + + const { challengeData } = opp; + + // const newType = _.find(validTypes, { name: opp.challenge.type }) || {}; + const newType = _.find(validTypes, { name: challengeData.subTrack === 'FIRST_2_FINISH' ? 'First2Finish' : 'Challenge' }) || {}; + + // Review Opportunity objects have a challenge field which + // is largely compatible with many of the existing filter functions + // especially after a few normalization tweaks + const challenge = { + ...challengeData, + // This allows filterByText to search for Review Types and Challenge Titles + name: `${challengeData.title} ${REVIEW_OPPORTUNITY_TYPES[opp.type]}`, + // registrationStartDate: opp.startDate, // startDate of Review, not Challenge + // submissionEndDate: opp.startDate, // Currently uses startDate for both date comparisons + // communities: new Set([ // Used to filter by Track, and communities at a future date + // opp.challenge.track === 'QA' ? 'Dev' : trackAbbr[opp.challenge.track], + // ]), + track: trackAbbr[challengeData.track], + typeId: newType.abbreviation, + tags: challengeData.technologies || [], + platforms: challengeData.platforms || [], + }; + return ( + filterByTrack(challenge, state) + && filterByText(challenge, state) + && filterByTags(challenge, state) + && filterByTypes(challenge, state) + && filterByEndDate(challenge, state) + && filterByStartDate(challenge, state) + && filterByReviewOpportunityType(opp, state) + ); +}; diff --git a/src/shared/utils/tc.js b/src/shared/utils/tc.js index 27d2fd8368..8dfc4dbdf5 100644 --- a/src/shared/utils/tc.js +++ b/src/shared/utils/tc.js @@ -15,9 +15,16 @@ export const { OLD_COMPETITION_TRACKS, COMPETITION_TRACKS, CHALLENGE_STATUS, - REVIEW_OPPORTUNITY_TYPES, } = tc; +export const REVIEW_OPPORTUNITY_TYPES = { + REGULAR_REVIEW: 'Review', + COMPONENT_DEV_REVIEW: 'Component Dev Review', + SPEC_REVIEW: 'Specification Review', + ITERATIVE_REVIEW: 'Iterative Review', + SCENARIOS_REVIEW: 'Scenarios Review', +}; + /** * Possible phase types (at the moment, this map does not cover all * possibilities). From a62a525cdffa8dd74cb6c81b668acda6095a9768 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Wed, 10 Sep 2025 14:01:35 +0530 Subject: [PATCH 18/76] fix: auto-review feedback remove unnecessary param Fixes #1760 --- src/shared/services/reviewOpportunities.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/shared/services/reviewOpportunities.js b/src/shared/services/reviewOpportunities.js index ee7a32e63f..e1dee08147 100644 --- a/src/shared/services/reviewOpportunities.js +++ b/src/shared/services/reviewOpportunities.js @@ -7,7 +7,6 @@ const v6ApiUrl = config.API.V6; * * @param {number} page - Page number (1-based). * @param {number} pageSize - Number of items per page. - * @param {string} sort - Sort order (e.g., 'createdAt desc'). * @returns {Promise} The fetched data. */ export default function getReviewOpportunities(page, pageSize) { From 16c636a3a0b694b51e8392de5424c04ea0cc7656 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Thu, 11 Sep 2025 12:24:43 +0530 Subject: [PATCH 19/76] fix: add error handling Adds error handling to the API call for fetching review opportunities Fixes #1760 --- src/shared/services/reviewOpportunities.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/shared/services/reviewOpportunities.js b/src/shared/services/reviewOpportunities.js index e1dee08147..ca7748c02f 100644 --- a/src/shared/services/reviewOpportunities.js +++ b/src/shared/services/reviewOpportunities.js @@ -9,14 +9,18 @@ const v6ApiUrl = config.API.V6; * @param {number} pageSize - Number of items per page. * @returns {Promise} The fetched data. */ -export default function getReviewOpportunities(page, pageSize) { +export default async function getReviewOpportunities(page, pageSize) { const offset = page * pageSize; const url = new URL(`${v6ApiUrl}/review-opportunities`); url.searchParams.append('limit', pageSize); url.searchParams.append('offset', offset); - return fetch(url.toString(), { - method: 'GET', - }).then(res => res.json()); + const res = await fetch(url.toString(), { method: 'GET' }); + + if (!res.ok) { + throw new Error(res.statusText); + } + + return res.json(); } From 0009b176abeb2fb10907b3ed00166e219d3a188f Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Thu, 11 Sep 2025 20:11:54 +1000 Subject: [PATCH 20/76] Fixes for v6 --- .gitignore | 3 + .python-version | 1 + config/default.js | 2 +- config/production.js | 2 +- package-lock.json | 224 ------------------ package.json | 1 - src/server/services/mmLeaderboard.js | 3 +- src/shared/actions/mmLeaderboard.js | 20 +- .../MMDashboard/Graph/index.jsx | 6 +- .../challenge-detail/Submissions/index.jsx | 8 +- .../containers/challenge-detail/index.jsx | 19 +- src/shared/services/reviewTypes.js | 6 +- src/shared/utils/submissions.js | 4 +- 13 files changed, 53 insertions(+), 246 deletions(-) create mode 100644 .python-version diff --git a/.gitignore b/.gitignore index ea14c4e5a5..f522e77e8d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ logs *.log npm-debug.log* +# Local certs +*.pem + # Runtime data pids *.pid diff --git a/.python-version b/.python-version new file mode 100644 index 0000000000..1effb00340 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +2.7 diff --git a/config/default.js b/config/default.js index 4fc5508fa4..3190200db7 100644 --- a/config/default.js +++ b/config/default.js @@ -170,7 +170,7 @@ module.exports = { groupId: '20000000', communityId: 'wipro', communityName: 'topgear', userProfile: 'https://topgear-app.wipro.com/user-details', }], TIMELINE_WALL_API: 'https://api.topcoder-dev.com/v5/timeline-wall', - REVIEW_API_URL: '/reviewTypes', + REVIEW_TYPES_API_URL: '/reviewTypes', }, /* Information about Topcoder user groups can be cached in various places. diff --git a/config/production.js b/config/production.js index 9e4664b2e2..82d3c9e745 100644 --- a/config/production.js +++ b/config/production.js @@ -63,7 +63,7 @@ module.exports = { EMAIL_VERIFY_URL: 'http://www.topcoder.com/settings/account/changeEmail', THRIVE_FEED: 'https://topcoder.com/api/feeds/thrive', TIMELINE_WALL_API: 'https://api.topcoder.com/v5/timeline-wall', - REVIEW_API_URL: '/reviewTypes', + REVIEW_TYPES_API_URL: '/reviewTypes', }, /* Filestack configuration for uploading Submissions * These are for the production back end */ diff --git a/package-lock.json b/package-lock.json index 588c3af504..dddafab8aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1614,12 +1614,6 @@ "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", "dev": true }, - "@types/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", - "dev": true - }, "@types/parse5": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-5.0.3.tgz", @@ -4836,12 +4830,6 @@ "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", "dev": true }, - "ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "dev": true - }, "cipher-base": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", @@ -5308,12 +5296,6 @@ "dot-prop": "^3.0.0" } }, - "compare-versions": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz", - "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==", - "dev": true - }, "component-classes": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/component-classes/-/component-classes-1.2.6.tgz", @@ -9718,15 +9700,6 @@ "locate-path": "^2.0.0" } }, - "find-versions": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-4.0.0.tgz", - "integrity": "sha512-wgpWy002tA+wgmO27buH/9KzyEOQnKsG/R0yrcjPT9BOFm0zRBVQbZ95nRGXWMywS8YR5knRbpohio0bcJABxQ==", - "dev": true, - "requires": { - "semver-regex": "^3.1.2" - } - }, "find-with-regex": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/find-with-regex/-/find-with-regex-1.1.3.tgz", @@ -12046,164 +12019,6 @@ } } }, - "husky": { - "version": "4.3.8", - "resolved": "https://registry.npmjs.org/husky/-/husky-4.3.8.tgz", - "integrity": "sha512-LCqqsB0PzJQ/AlCgfrfzRe3e3+NvmefAdKQhRYpxS4u6clblBoDdzzvHi8fmxKRzvMxPY/1WZWzomPZww0Anow==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "ci-info": "^2.0.0", - "compare-versions": "^3.6.0", - "cosmiconfig": "^7.0.0", - "find-versions": "^4.0.0", - "opencollective-postinstall": "^2.0.2", - "pkg-dir": "^5.0.0", - "please-upgrade-node": "^3.2.0", - "slash": "^3.0.0", - "which-pm-runs": "^1.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "dev": true, - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - } - }, - "find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, - "pkg-dir": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz", - "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==", - "dev": true, - "requires": { - "find-up": "^5.0.0" - } - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, "hyphenate-style-name": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", @@ -17851,12 +17666,6 @@ "mimic-fn": "^1.0.0" } }, - "opencollective-postinstall": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", - "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", - "dev": true - }, "opentracing": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/opentracing/-/opentracing-0.14.1.tgz", @@ -18325,15 +18134,6 @@ "find-up": "^2.1.0" } }, - "please-upgrade-node": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", - "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", - "dev": true, - "requires": { - "semver-compare": "^1.0.0" - } - }, "pluralize": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", @@ -23113,18 +22913,6 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" }, - "semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", - "dev": true - }, - "semver-regex": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.4.tgz", - "integrity": "sha512-6IiqeZNgq01qGf0TId0t3NvKzSvUsjcpdEO3AQNeIjR6A2+ckTnQlDpl4qu1bjRv0RzN3FP9hzFmws3lKqRWkA==", - "dev": true - }, "send": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", @@ -28754,12 +28542,6 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, - "yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true - }, "yargs": { "version": "14.2.3", "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz", @@ -28855,12 +28637,6 @@ "decamelize": "^1.2.0" } }, - "yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true - }, "zwitch": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz", diff --git a/package.json b/package.json index 7f15ec6c33..21a134470d 100644 --- a/package.json +++ b/package.json @@ -212,7 +212,6 @@ "eslint-plugin-react": "^7.32.2", "file-loader": "^1.1.11", "happypack": "^5.0.1", - "identity-obj-proxy": "^3.0.0", "jest": "^23.2.0", "jsdoc-to-markdown": "^4.0.1", diff --git a/src/server/services/mmLeaderboard.js b/src/server/services/mmLeaderboard.js index 286e3c14da..24fb315db9 100644 --- a/src/server/services/mmLeaderboard.js +++ b/src/server/services/mmLeaderboard.js @@ -26,7 +26,8 @@ export default class MMLService { const subs = await v5api.get(`/submissions?challengeId=${sanitizedId}&page=1&perPage=500`); return res.send({ id: sanitizedId, - subs: await subs.json(), + // API now returns { data: [...], meta: {...} } + subs: (await subs.json()).data, reviewIds, }); } catch (err) { diff --git a/src/shared/actions/mmLeaderboard.js b/src/shared/actions/mmLeaderboard.js index 08baf1b053..0604f4e7a9 100644 --- a/src/shared/actions/mmLeaderboard.js +++ b/src/shared/actions/mmLeaderboard.js @@ -26,11 +26,25 @@ async function getMMLeaderboardDone(id) { sub.review = _.filter(sub.review, r => res.reviewIds.indexOf(r.typeId) === -1); return sub; }); - const sortedSubs = _.orderBy(filteredSubs, ['updated'], ['desc']); - const scores = _.orderBy(_.compact(sortedSubs[0].review), ['updated'], ['desc']); + // New API uses updatedAt/createdAt; fallback to older fields where needed + const sortedSubs = _.orderBy( + filteredSubs, + [s => s.updatedAt || s.updated || s.createdAt || s.submittedDate || ''], + ['desc'], + ); + const scores = _.orderBy( + _.compact(sortedSubs[0].review), + [r => r.updatedAt || r.updated || r.createdAt || ''], + ['desc'], + ); + const latestDate = sortedSubs[0].submittedDate + || sortedSubs[0].createdAt + || sortedSubs[0].updatedAt + || sortedSubs[0].created + || ''; data.push({ createdBy: handle, - updated: sortedSubs[0].submittedDate, + updated: latestDate, id: sortedSubs[0].id, score: scores && scores.length ? scores[0].score : '...', }); diff --git a/src/shared/components/challenge-detail/MMDashboard/Graph/index.jsx b/src/shared/components/challenge-detail/MMDashboard/Graph/index.jsx index 62bad0f281..9c0a395ebe 100644 --- a/src/shared/components/challenge-detail/MMDashboard/Graph/index.jsx +++ b/src/shared/components/challenge-detail/MMDashboard/Graph/index.jsx @@ -17,7 +17,7 @@ export default function Graph({ statisticsData, baseline, awardLine }) { const dates = []; _.each(statisticsData, (entry) => { _.each(entry.submissions, (sub) => { - dates.push(sub.created || null); + dates.push(sub.created || sub.createdAt || null); flatData.push({ ..._.omit(entry, ['submissions']), submissionCount: _.get(entry, 'submissions.length', 0), @@ -40,7 +40,7 @@ export default function Graph({ statisticsData, baseline, awardLine }) { color = data.ratingColor || getRatingColor(data.rating || 0); } return { - x: moment(data.created).valueOf(), + x: moment(data.created || data.createdAt).valueOf(), y: _.max([0, data.score ? (parseFloat(data.score)) : 0]), name: data.handle, color, @@ -160,7 +160,7 @@ export default function Graph({ statisticsData, baseline, awardLine }) {

${currentPointer.customData.submissionCount} submissions

Score: ${this.y}

-

Submitted: ${moment(currentPointer.customData.created).format('MM/DD/YYYY')}

+

Submitted: ${moment(currentPointer.customData.created || currentPointer.customData.createdAt).format('MM/DD/YYYY')}

`; return str; diff --git a/src/shared/components/challenge-detail/Submissions/index.jsx b/src/shared/components/challenge-detail/Submissions/index.jsx index 60c4071320..a6ed8388d5 100644 --- a/src/shared/components/challenge-detail/Submissions/index.jsx +++ b/src/shared/components/challenge-detail/Submissions/index.jsx @@ -226,8 +226,8 @@ class SubmissionsComponent extends React.Component { valueB = new Date(b.submissions && b.submissions[0].submissionTime); break; case 'Submission Date': { - valueA = new Date(a.created); - valueB = new Date(b.created); + valueA = new Date(a.created || a.createdAt); + valueB = new Date(b.created || b.createdAt); break; } case 'Initial Score': { @@ -902,7 +902,7 @@ class SubmissionsComponent extends React.Component { { !isMM && ( sortedSubmissions.map(s => ( -
+
{ !isF2F && !isBugHunt && ( @@ -927,7 +927,7 @@ class SubmissionsComponent extends React.Component {
SUBMISSION DATE

- {moment(s.created).format('MMM DD, YYYY HH:mm')} + {moment(s.created || s.createdAt).format('MMM DD, YYYY HH:mm')}

diff --git a/src/shared/containers/challenge-detail/index.jsx b/src/shared/containers/challenge-detail/index.jsx index bdf6ca5c6a..5623195763 100644 --- a/src/shared/containers/challenge-detail/index.jsx +++ b/src/shared/containers/challenge-detail/index.jsx @@ -842,10 +842,21 @@ function mapStateToProps(state, props) { ? challenge.submissions : (_.get(challenge, 'submissions.data') || []); - challenge.submissions = normalizedSubmissions.map(submission => ({ - ...submission, - registrant: _.find(challenge.registrants, r => (`${r.memberId}` === `${submission.memberId}`)), - })); + challenge.submissions = normalizedSubmissions.map((submission) => { + const registrant = _.find( + challenge.registrants, + r => (`${r.memberId}` === `${submission.memberId}`), + ); + // Ensure legacy fields used in UI exist + const created = submission.created || submission.createdAt || null; + const updated = submission.updated || submission.updatedAt || null; + return ({ + ...submission, + created, + updated, + registrant, + }); + }); } if (!_.isEmpty(mmSubmissions)) { diff --git a/src/shared/services/reviewTypes.js b/src/shared/services/reviewTypes.js index 101bd1de45..b6d51e731a 100644 --- a/src/shared/services/reviewTypes.js +++ b/src/shared/services/reviewTypes.js @@ -1,10 +1,10 @@ import { config } from 'topcoder-react-utils'; -const baseUrl = config.URL.REVIEW_API_URL; -const v5ApiUrl = config.API.V6; +const baseUrl = config.URL.REVIEW_TYPES_API_URL; +const v6ApiUrl = config.API.V6; export default function getReviewTypes(tokenV3) { - return fetch(`${v5ApiUrl}${baseUrl}?perPage=500&page=1`, { + return fetch(`${v6ApiUrl}${baseUrl}?perPage=500&page=1`, { method: 'GET', headers: new Headers({ Authorization: `Bearer ${tokenV3}`, diff --git a/src/shared/utils/submissions.js b/src/shared/utils/submissions.js index 27cd62e800..548b24022f 100644 --- a/src/shared/utils/submissions.js +++ b/src/shared/utils/submissions.js @@ -8,7 +8,9 @@ */ export function getMMSubmissionId(submissions, handle) { const filterSubmissions = handle ? submissions.filter(s => s.createdBy === handle) : submissions; - const sortedSubmissions = filterSubmissions.sort((a, b) => (a.created < b.created ? 1 : -1)); + const sortedSubmissions = filterSubmissions.sort((a, b) => ( + (a.created || a.createdAt) < (b.created || b.createdAt) ? 1 : -1 + )); return sortedSubmissions.length > 0 ? sortedSubmissions[0].id : null; } From 2cb995aa1ea237fa2dfe70033be6323c43f2db7c Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Mon, 15 Sep 2025 13:51:26 +0530 Subject: [PATCH 21/76] migrate: review opportunity details page to v6 review API migrates review opportunity details page to v6 review api fixes #1761 --- .../page/review-opportunity-details.js | 33 ++++++++++++ .../FailedToLoad/index.jsx | 2 +- .../FailedToLoad/styles.scss | 2 +- .../Header/PhaseList/index.jsx | 4 +- .../ReviewOpportunityDetailsPage/index.jsx | 2 +- .../ReviewOpportunityCard/index.jsx | 4 +- .../containers/ReviewOpportunityDetails.jsx | 46 +++++++++++++--- .../page/review-opportunity-details.js | 48 +++++++++++++++++ src/shared/services/reviewOpportunities.js | 52 +++++++++++++++++++ 9 files changed, 179 insertions(+), 14 deletions(-) diff --git a/src/shared/actions/page/review-opportunity-details.js b/src/shared/actions/page/review-opportunity-details.js index 6b9c75ffa5..4f8f8aa83f 100644 --- a/src/shared/actions/page/review-opportunity-details.js +++ b/src/shared/actions/page/review-opportunity-details.js @@ -4,15 +4,48 @@ import _ from 'lodash'; import { createActions } from 'redux-actions'; +import { getDetails } from '../../services/reviewOpportunities'; + + /* Holds valid values for the tab state. */ export const TABS = { APPLICATIONS: 'APPLICATIONS', CHALLENGE_SPEC: 'CHALLENGE_SPEC', }; +/** + * @static + * @desc Creates an action that signals beginning of loading the review + * opportunity details. + * @return {Action} + */ +function getDetailsInit() {} + +/** + * @static + * @desc Creates an action that gets details of a review opportunity for + * the specified challenge. + * @param {Number} challengeId The ID of the challenge (not the opportunity id) + * @param {String} tokenV3=null Optional. Topcoder auth token v3. + * @default test + * @return {Action} + */ +function getDetailsDone(challengeId, opportunityId, tokenV3) { + return getDetails(challengeId, opportunityId, tokenV3) + .then(details => ({ details })) + .catch((error) => { + if (error.status !== 401) { + console.log('Error Getting Review Opportunity Details', error.content || error); + } + return Promise.reject(error.status); + }); +} + export default createActions({ PAGE: { REVIEW_OPPORTUNITY_DETAILS: { + GET_DETAILS_INIT: getDetailsInit, + GET_DETAILS_DONE: getDetailsDone, SELECT_TAB: _.identity, SET_ROLES: _.identity, TOGGLE_APPLY_MODAL: _.identity, diff --git a/src/shared/components/ReviewOpportunityDetailsPage/FailedToLoad/index.jsx b/src/shared/components/ReviewOpportunityDetailsPage/FailedToLoad/index.jsx index 214a25f1e4..f60f0f30e1 100644 --- a/src/shared/components/ReviewOpportunityDetailsPage/FailedToLoad/index.jsx +++ b/src/shared/components/ReviewOpportunityDetailsPage/FailedToLoad/index.jsx @@ -2,7 +2,7 @@ import React from 'react'; import './styles.scss'; const FailedToLoad = () => ( -
+

diff --git a/src/shared/components/ReviewOpportunityDetailsPage/FailedToLoad/styles.scss b/src/shared/components/ReviewOpportunityDetailsPage/FailedToLoad/styles.scss index 1aa3016218..7c13c67016 100644 --- a/src/shared/components/ReviewOpportunityDetailsPage/FailedToLoad/styles.scss +++ b/src/shared/components/ReviewOpportunityDetailsPage/FailedToLoad/styles.scss @@ -1,6 +1,6 @@ @import "~styles/mixins"; -.comtainer { +.container { background: $tc-gray-neutral-dark; width: 100%; display: flex; diff --git a/src/shared/components/ReviewOpportunityDetailsPage/Header/PhaseList/index.jsx b/src/shared/components/ReviewOpportunityDetailsPage/Header/PhaseList/index.jsx index c0a4db58c7..a258fa12a9 100644 --- a/src/shared/components/ReviewOpportunityDetailsPage/Header/PhaseList/index.jsx +++ b/src/shared/components/ReviewOpportunityDetailsPage/Header/PhaseList/index.jsx @@ -22,9 +22,9 @@ const { formatDuration } = time; * @return {Object} The rendered React element */ const renderPhase = phase => ( -
+
- {phase.type} + {phase.name}
diff --git a/src/shared/components/ReviewOpportunityDetailsPage/index.jsx b/src/shared/components/ReviewOpportunityDetailsPage/index.jsx index a322f105e9..0e3339ead4 100644 --- a/src/shared/components/ReviewOpportunityDetailsPage/index.jsx +++ b/src/shared/components/ReviewOpportunityDetailsPage/index.jsx @@ -42,7 +42,7 @@ const ReviewOpportunityDetailsPage = ({

- {details.challenge.title} + {details.challenge.name}

diff --git a/src/shared/components/challenge-listing/ReviewOpportunityCard/index.jsx b/src/shared/components/challenge-listing/ReviewOpportunityCard/index.jsx index d880792b3b..1f7d0eba0a 100644 --- a/src/shared/components/challenge-listing/ReviewOpportunityCard/index.jsx +++ b/src/shared/components/challenge-listing/ReviewOpportunityCard/index.jsx @@ -78,7 +78,7 @@ function ReviewOpportunityCard({ ) /* END - DISABLED UNTIL REVIEW OPPORTUNITY RECEIVE UPDATE TO API V5 */ }
{challenge.title} @@ -156,7 +156,7 @@ function ReviewOpportunityCard({
diff --git a/src/shared/containers/ReviewOpportunityDetails.jsx b/src/shared/containers/ReviewOpportunityDetails.jsx index bb40d2f79e..158c258e0f 100644 --- a/src/shared/containers/ReviewOpportunityDetails.jsx +++ b/src/shared/containers/ReviewOpportunityDetails.jsx @@ -8,13 +8,14 @@ import React from 'react'; import PT from 'prop-types'; import { connect } from 'react-redux'; -import { actions, errors } from 'topcoder-react-lib'; +import { errors } from 'topcoder-react-lib'; import LoadingIndicator from 'components/LoadingIndicator'; import { activeRoleIds } from 'utils/reviewOpportunities'; import pageActions from 'actions/page/review-opportunity-details'; import ReviewOpportunityDetailsPage from 'components/ReviewOpportunityDetailsPage'; import FailedToLoad from 'components/ReviewOpportunityDetailsPage/FailedToLoad'; import termsActions from 'actions/terms'; +import { goToLogin } from 'utils/tc'; const { fireErrorMessage } = errors; @@ -25,6 +26,7 @@ class ReviewOpportunityDetailsContainer extends React.Component { componentDidMount() { const { challengeId, + opportunityId, details, isLoadingDetails, loadDetails, @@ -32,19 +34,40 @@ class ReviewOpportunityDetailsContainer extends React.Component { } = this.props; if (!isLoadingDetails && !details) { - loadDetails(challengeId, tokenV3); + loadDetails(challengeId, opportunityId, tokenV3); } else if (details.challenge.id !== challengeId) { - loadDetails(challengeId, tokenV3); + loadDetails(challengeId, opportunityId, tokenV3); } } handleOnHeaderApply() { const { + isLoggedIn, + isReviewer, openTermsModal, terms, termsFailure, toggleApplyModal, } = this.props; + + if (!isLoggedIn) { + goToLogin('community-app-main'); + return; + } + + if (!isReviewer) { + fireErrorMessage( + 'Permission Required', + + You must have a reviewer role to apply for this review opportunity.{' '} + + Learn how to become a reviewer + . + , + ); + return; + } + if (termsFailure) { fireErrorMessage('Error Getting Terms Details', ''); return; @@ -130,6 +153,8 @@ ReviewOpportunityDetailsContainer.defaultProps = { termsFailure: false, phasesExpanded: false, tokenV3: null, + isLoggedIn: false, + isReviewer: false, }; /** @@ -140,6 +165,7 @@ ReviewOpportunityDetailsContainer.propTypes = { authError: PT.bool, cancelApplications: PT.func.isRequired, challengeId: PT.string.isRequired, + opportunityId: PT.string.isRequired, details: PT.shape(), handle: PT.string.isRequired, isLoadingDetails: PT.bool, @@ -157,6 +183,8 @@ ReviewOpportunityDetailsContainer.propTypes = { toggleRole: PT.func.isRequired, onPhaseExpand: PT.func.isRequired, tokenV3: PT.string, + isLoggedIn: PT.bool, + isReviewer: PT.bool, }; /** @@ -169,12 +197,14 @@ ReviewOpportunityDetailsContainer.propTypes = { const mapStateToProps = (state, ownProps) => { const api = state.reviewOpportunity; const page = state.page.reviewOpportunityDetails; + const queryParams = new URLSearchParams(ownProps.location.search); const { terms } = state; return { authError: api.authError, applyModalOpened: page.applyModalOpened, challengeId: String(ownProps.match.params.challengeId), - details: api.details, + opportunityId: queryParams.get('opportunityId'), + details: page.details, handle: state.auth.user ? state.auth.user.handle : '', isLoadingDetails: api.isLoadingDetails, phasesExpanded: page.phasesExpanded, @@ -184,6 +214,8 @@ const mapStateToProps = (state, ownProps) => { terms: terms.terms, termsFailure: terms.getTermsFailure, tokenV3: state.auth.tokenV3, + isLoggedIn: Boolean(state.auth.user), + isReviewer: _.includes(state.auth.userRoles || [], 'REVIEWER'), }; }; @@ -194,16 +226,16 @@ const mapStateToProps = (state, ownProps) => { * @return {Object} */ function mapDispatchToProps(dispatch) { - const api = actions.reviewOpportunity; + const api = pageActions.page.reviewOpportunityDetails; const page = pageActions.page.reviewOpportunityDetails; return { cancelApplications: (challengeId, roleIds, tokenV3) => { dispatch(api.cancelApplicationsInit()); dispatch(api.cancelApplicationsDone(challengeId, roleIds, tokenV3)); }, - loadDetails: (challengeId, tokenV3) => { + loadDetails: (challengeId, opportunityId, tokenV3) => { dispatch(api.getDetailsInit()); - dispatch(api.getDetailsDone(challengeId, tokenV3)); + dispatch(api.getDetailsDone(challengeId, opportunityId, tokenV3)); }, onPhaseExpand: () => dispatch(page.togglePhasesExpand()), openTermsModal: () => { diff --git a/src/shared/reducers/page/review-opportunity-details.js b/src/shared/reducers/page/review-opportunity-details.js index 39b8127e44..fe9fa08a49 100644 --- a/src/shared/reducers/page/review-opportunity-details.js +++ b/src/shared/reducers/page/review-opportunity-details.js @@ -3,6 +3,52 @@ import { handleActions } from 'redux-actions'; import actions, { TABS } from 'actions/page/review-opportunity-details'; +/** + * Generates a list of unique terms ids required for the open review roles + * with an agreed field + * + * @param {Object} details Review Opportuny details from API + * @return {Array} List of unique terms + */ +function buildRequiredTermsList(details) { + const roles = details.payments.map(payment => payment.role); + + const requiredTerms = _.uniqBy( + details.challenge.terms + // Sometimes roles such as Primary Reviewer have no directly equal + // terms entry. Include the plain Reviewer terms when present as a back-up. + .filter(term => term.role === 'Reviewer' || _.includes(roles, term.role)) + .map(term => _.pick(term, ['id', 'agreed', 'title'])), + term => term.id, + ); + + return requiredTerms || []; +} + + +/** + * Handles REVIEW_OPPORTUNITY/GET__DETAILS_DONE action. + * @param {Object} state + * @param {Object} action Payload will be JSON from api call + * @return {Object} New state + */ +function onGetDetailsDone(state, { payload, error }) { + if (error) { + return { + ...state, + authError: true, + isLoadingDetails: false, + }; + } + + return { + ...state, + details: payload.details, + isLoadingDetails: false, + requiredTerms: buildRequiredTermsList(payload.details), + }; +} + /** * Creates a new reducer. * @param {Object} state Optional. Initial state. @@ -11,6 +57,8 @@ import actions, { TABS } from 'actions/page/review-opportunity-details'; function create(defaultState = {}) { const a = actions.page.reviewOpportunityDetails; return handleActions({ + [a.getDetailsInit]: state => ({ ...state, isLoadingDetails: true }), + [a.getDetailsDone]: onGetDetailsDone, [a.selectTab]: (state, { payload }) => ({ ...state, selectedTab: payload }), [a.setRoles]: (state, { payload }) => ({ ...state, selectedRoles: payload }), [a.toggleApplyModal]: state => ({ ...state, applyModalOpened: !state.applyModalOpened }), diff --git a/src/shared/services/reviewOpportunities.js b/src/shared/services/reviewOpportunities.js index ca7748c02f..6bc119e0bb 100644 --- a/src/shared/services/reviewOpportunities.js +++ b/src/shared/services/reviewOpportunities.js @@ -1,4 +1,6 @@ import { config } from 'topcoder-react-utils'; +import _ from 'lodash'; + const v6ApiUrl = config.API.V6; @@ -24,3 +26,53 @@ export default async function getReviewOpportunities(page, pageSize) { return res.json(); } + +/** + * Sync the fields of V3 and V5 for front-end to process successfully + * @param challenge - challenge to normalize + */ +function normalizeChallengePhases(challenge) { + return { + ...challenge, + phases: _.map(challenge.phases, phase => ({ + ...phase, + scheduledStartDate: phase.scheduledStartTime, + scheduledEndDate: phase.scheduledEndTime, + })), + }; +} + + +/** + * Gets the details of the review opportunity for the corresponding challenge + * @param {Number} challengeId The ID of the challenge (not the opportunity id) + * @return {Promise} Resolves to the api response in JSON. + */ +export async function getDetails(challengeId, opportunityId) { + const getReviewOpportunityUrl = new URL(`${v6ApiUrl}/review-opportunities/${opportunityId}`); + const getChallengeUrl = new URL(`${v6ApiUrl}/challenges/${challengeId}`); + + try { + const [opportunityRes, challengeRes] = await Promise.all([ + fetch(getReviewOpportunityUrl.toString(), { method: 'GET' }), + fetch(getChallengeUrl.toString(), { method: 'GET' }), + ]); + + if (!opportunityRes.ok) { + throw new Error(`Failed to load review opportunity: ${opportunityRes.statusText}`); + } + if (!challengeRes.ok) { + throw new Error(`Failed to load challenge details: ${challengeRes.statusText}`); + } + + const opportunityData = await opportunityRes.json(); + const challengeData = await challengeRes.json(); + + return { + ...opportunityData.result.content, + challenge: normalizeChallengePhases(challengeData), + }; + } catch (err) { + return Promise.reject(err); + } +} From 608b14d7f685bbac2ece0992488f6b0a84af3255 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Mon, 15 Sep 2025 14:23:33 +0530 Subject: [PATCH 22/76] fix: ai review feedbacks ai review feedbacks on return types and logging Fixes #1760 --- src/shared/actions/page/review-opportunity-details.js | 9 +++++---- src/shared/services/reviewOpportunities.js | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/shared/actions/page/review-opportunity-details.js b/src/shared/actions/page/review-opportunity-details.js index 4f8f8aa83f..7bc3a15208 100644 --- a/src/shared/actions/page/review-opportunity-details.js +++ b/src/shared/actions/page/review-opportunity-details.js @@ -3,6 +3,7 @@ */ import _ from 'lodash'; import { createActions } from 'redux-actions'; +import { logger } from 'topcoder-react-lib'; import { getDetails } from '../../services/reviewOpportunities'; @@ -26,16 +27,16 @@ function getDetailsInit() {} * @desc Creates an action that gets details of a review opportunity for * the specified challenge. * @param {Number} challengeId The ID of the challenge (not the opportunity id) + * @param {Number} opportunityId The ID of the review opportunity * @param {String} tokenV3=null Optional. Topcoder auth token v3. - * @default test * @return {Action} */ -function getDetailsDone(challengeId, opportunityId, tokenV3) { - return getDetails(challengeId, opportunityId, tokenV3) +function getDetailsDone(challengeId, opportunityId) { + return getDetails(challengeId, opportunityId) .then(details => ({ details })) .catch((error) => { if (error.status !== 401) { - console.log('Error Getting Review Opportunity Details', error.content || error); + logger.error('Error Getting Review Opportunity Details', error.content || error); } return Promise.reject(error.status); }); diff --git a/src/shared/services/reviewOpportunities.js b/src/shared/services/reviewOpportunities.js index 6bc119e0bb..3374cb02c1 100644 --- a/src/shared/services/reviewOpportunities.js +++ b/src/shared/services/reviewOpportunities.js @@ -46,7 +46,7 @@ function normalizeChallengePhases(challenge) { /** * Gets the details of the review opportunity for the corresponding challenge * @param {Number} challengeId The ID of the challenge (not the opportunity id) - * @return {Promise} Resolves to the api response in JSON. + * @return {Object} The combined data of the review opportunity and challenge details */ export async function getDetails(challengeId, opportunityId) { const getReviewOpportunityUrl = new URL(`${v6ApiUrl}/review-opportunities/${opportunityId}`); From 08b27d4987abacae653a6f65cf57245fbc4365cb Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Tue, 16 Sep 2025 12:51:31 +0530 Subject: [PATCH 23/76] fix: remove link removes link for how to be a reviewer, we can add it later when process is in place fixes #1761 --- src/shared/containers/ReviewOpportunityDetails.jsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/shared/containers/ReviewOpportunityDetails.jsx b/src/shared/containers/ReviewOpportunityDetails.jsx index 158c258e0f..5f01554ae2 100644 --- a/src/shared/containers/ReviewOpportunityDetails.jsx +++ b/src/shared/containers/ReviewOpportunityDetails.jsx @@ -59,10 +59,7 @@ class ReviewOpportunityDetailsContainer extends React.Component { fireErrorMessage( 'Permission Required', - You must have a reviewer role to apply for this review opportunity.{' '} - - Learn how to become a reviewer - . + You must have a reviewer role to apply for this review opportunity. , ); return; From e9f3c3385339bef28d5ce05bbf5cf1d366fad265 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Thu, 18 Sep 2025 07:16:07 +1000 Subject: [PATCH 24/76] Fix registrants and submissions tab --- config/webpack/production.js | 3 +++ config/webpack/qa.js | 3 +++ src/shared/components/challenge-detail/Registrants/index.jsx | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/config/webpack/production.js b/config/webpack/production.js index 89010e8953..09f4fd1c0c 100644 --- a/config/webpack/production.js +++ b/config/webpack/production.js @@ -34,4 +34,7 @@ standardDevelopmentConfig.plugins.push(new webpack.DefinePlugin({ PUBLIC_PATH: JSON.stringify(publicPath), })); +// Ensure production bundles emit debuggable source maps that can be hosted alongside the assets +standardDevelopmentConfig.devtool = 'source-map'; + module.exports = webpackMerge.smart(standardDevelopmentConfig, defaultConfig); diff --git a/config/webpack/qa.js b/config/webpack/qa.js index b04db2dcaf..ced236a528 100644 --- a/config/webpack/qa.js +++ b/config/webpack/qa.js @@ -39,4 +39,7 @@ standardDevelopmentConfig.plugins.push(new webpack.DefinePlugin({ PUBLIC_PATH: JSON.stringify('/api/cdn/public/static-assets'), })); +// Align QA bundles with production by emitting source maps for debugging in browsers +standardDevelopmentConfig.devtool = 'source-map'; + module.exports = webpackMerge.smart(standardDevelopmentConfig, defaultConfig); diff --git a/src/shared/components/challenge-detail/Registrants/index.jsx b/src/shared/components/challenge-detail/Registrants/index.jsx index 5c21256bb1..77c1466bf5 100644 --- a/src/shared/components/challenge-detail/Registrants/index.jsx +++ b/src/shared/components/challenge-detail/Registrants/index.jsx @@ -288,7 +288,7 @@ export default class Registrants extends React.Component { const revertSort = (sort === 'desc') ? 'asc' : 'desc'; const isDesign = track.toLowerCase() === 'design'; - const placementPrizes = _.find(prizeSets, { type: 'placement' }); + const placementPrizes = _.find(prizeSets, { type: 'PLACEMENT' }); const { prizes } = placementPrizes || []; const checkpoints = challenge.checkpoints || []; From 644678b91d27a820539899442b971da309834779 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Fri, 19 Sep 2025 11:51:43 +0530 Subject: [PATCH 25/76] PM-1761 QA feedback --- .../ChallengeSpecTab/index.jsx | 4 ++-- .../containers/ReviewOpportunityDetails.jsx | 2 +- src/shared/services/reviewOpportunities.js | 20 +------------------ 3 files changed, 4 insertions(+), 22 deletions(-) diff --git a/src/shared/components/ReviewOpportunityDetailsPage/ChallengeSpecTab/index.jsx b/src/shared/components/ReviewOpportunityDetailsPage/ChallengeSpecTab/index.jsx index 37c0d832c5..d501a707a4 100644 --- a/src/shared/components/ReviewOpportunityDetailsPage/ChallengeSpecTab/index.jsx +++ b/src/shared/components/ReviewOpportunityDetailsPage/ChallengeSpecTab/index.jsx @@ -12,7 +12,7 @@ import './styles.scss'; const ChallengeSpecTab = ({ challenge }) => (
{ - challenge.detailedRequirements + challenge.description && (

@@ -21,7 +21,7 @@ const ChallengeSpecTab = ({ challenge }) => (
{ termsFailure: terms.getTermsFailure, tokenV3: state.auth.tokenV3, isLoggedIn: Boolean(state.auth.user), - isReviewer: _.includes(state.auth.userRoles || [], 'REVIEWER'), + isReviewer: _.includes(state.auth.user.roles || [], 'reviewer'), }; }; diff --git a/src/shared/services/reviewOpportunities.js b/src/shared/services/reviewOpportunities.js index 3374cb02c1..f2a3237562 100644 --- a/src/shared/services/reviewOpportunities.js +++ b/src/shared/services/reviewOpportunities.js @@ -1,6 +1,4 @@ import { config } from 'topcoder-react-utils'; -import _ from 'lodash'; - const v6ApiUrl = config.API.V6; @@ -27,22 +25,6 @@ export default async function getReviewOpportunities(page, pageSize) { return res.json(); } -/** - * Sync the fields of V3 and V5 for front-end to process successfully - * @param challenge - challenge to normalize - */ -function normalizeChallengePhases(challenge) { - return { - ...challenge, - phases: _.map(challenge.phases, phase => ({ - ...phase, - scheduledStartDate: phase.scheduledStartTime, - scheduledEndDate: phase.scheduledEndTime, - })), - }; -} - - /** * Gets the details of the review opportunity for the corresponding challenge * @param {Number} challengeId The ID of the challenge (not the opportunity id) @@ -70,7 +52,7 @@ export async function getDetails(challengeId, opportunityId) { return { ...opportunityData.result.content, - challenge: normalizeChallengePhases(challengeData), + challenge: challengeData, }; } catch (err) { return Promise.reject(err); From 1cadcd234262fefefa49fb23b6cdaf3978956966 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Sun, 21 Sep 2025 17:39:25 +1000 Subject: [PATCH 26/76] Fix submissions tab --- .../challenge-detail/Submissions/index.jsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/shared/components/challenge-detail/Submissions/index.jsx b/src/shared/components/challenge-detail/Submissions/index.jsx index a6ed8388d5..1f8a76a583 100644 --- a/src/shared/components/challenge-detail/Submissions/index.jsx +++ b/src/shared/components/challenge-detail/Submissions/index.jsx @@ -119,7 +119,7 @@ class SubmissionsComponent extends React.Component { const { challenge } = this.props; if (!_.isEmpty(submission.review) && !_.isEmpty(submission.review[0]) - && submission.review[0].score + && submission.review[0].initialScore && (challenge.status === 'COMPLETED' || (_.includes(challenge.tags, 'Innovation Challenge') && _.find(challenge.metadata, { name: 'show_data_dashboard' })))) { score = Number(submission.review[0].score).toFixed(2); @@ -191,7 +191,7 @@ class SubmissionsComponent extends React.Component { let isHaveFinalScore = false; if (field === 'Initial Score' || 'Final Score') { isHaveFinalScore = _.some(submissions, s => !_.isNil( - s.reviewSummation && s.reviewSummation[0].aggregateScore, + s.review && s.review[0].finalScore, )); } return sortList(submissions, field, sort, (a, b) => { @@ -232,15 +232,15 @@ class SubmissionsComponent extends React.Component { } case 'Initial Score': { if (isHaveFinalScore) { - valueA = getFinalScore(a); - valueB = getFinalScore(b); + valueA = !_.isEmpty(a.review) && a.review[0].finalScore; + valueB = !_.isEmpty(b.review) && b.review[0].finalScore; } else if (valueA.score || valueB.score) { // Handle MM formatted scores in a code challenge (PS-295) valueA = Number(valueA.score); valueB = Number(valueB.score); } else { - valueA = !_.isEmpty(a.review) && a.review[0].score; - valueB = !_.isEmpty(b.review) && b.review[0].score; + valueA = !_.isEmpty(a.review) && a.review[0].initialScore; + valueB = !_.isEmpty(b.review) && b.review[0].initialScore; } break; } @@ -940,8 +940,8 @@ class SubmissionsComponent extends React.Component {
FINAL SCORE

{ - (s.reviewSummation && s.reviewSummation[0].aggregateScore && challenge.status === 'COMPLETED') - ? s.reviewSummation[0].aggregateScore.toFixed(2) + (s.review && s.review[0].finalScore && challenge.status === 'COMPLETED') + ? s.review[0].finalScore.toFixed(2) : 'N/A' }

From 87dc25e437b207b727ce70e257a9402963159c38 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Tue, 23 Sep 2025 11:32:37 +1000 Subject: [PATCH 27/76] Score cleanup --- .../challenge-detail/Submissions/index.jsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/shared/components/challenge-detail/Submissions/index.jsx b/src/shared/components/challenge-detail/Submissions/index.jsx index 1f8a76a583..effaf70fa9 100644 --- a/src/shared/components/challenge-detail/Submissions/index.jsx +++ b/src/shared/components/challenge-detail/Submissions/index.jsx @@ -118,11 +118,11 @@ class SubmissionsComponent extends React.Component { let score = 'N/A'; const { challenge } = this.props; if (!_.isEmpty(submission.review) - && !_.isEmpty(submission.review[0]) - && submission.review[0].initialScore + && !_.isEmpty(submission) + && submission.initialScore && (challenge.status === 'COMPLETED' || (_.includes(challenge.tags, 'Innovation Challenge') && _.find(challenge.metadata, { name: 'show_data_dashboard' })))) { - score = Number(submission.review[0].score).toFixed(2); + score = Number(submission.initialScore).toFixed(2); } return score; } @@ -191,7 +191,7 @@ class SubmissionsComponent extends React.Component { let isHaveFinalScore = false; if (field === 'Initial Score' || 'Final Score') { isHaveFinalScore = _.some(submissions, s => !_.isNil( - s.review && s.review[0].finalScore, + s.review && s.finalScore, )); } return sortList(submissions, field, sort, (a, b) => { @@ -232,15 +232,15 @@ class SubmissionsComponent extends React.Component { } case 'Initial Score': { if (isHaveFinalScore) { - valueA = !_.isEmpty(a.review) && a.review[0].finalScore; - valueB = !_.isEmpty(b.review) && b.review[0].finalScore; + valueA = !_.isEmpty(a.review) && a.finalScore; + valueB = !_.isEmpty(b.review) && b.finalScore; } else if (valueA.score || valueB.score) { // Handle MM formatted scores in a code challenge (PS-295) valueA = Number(valueA.score); valueB = Number(valueB.score); } else { - valueA = !_.isEmpty(a.review) && a.review[0].initialScore; - valueB = !_.isEmpty(b.review) && b.review[0].initialScore; + valueA = !_.isEmpty(a.review) && a.initialScore; + valueB = !_.isEmpty(b.review) && b.initialScore; } break; } @@ -940,8 +940,8 @@ class SubmissionsComponent extends React.Component {
FINAL SCORE

{ - (s.review && s.review[0].finalScore && challenge.status === 'COMPLETED') - ? s.review[0].finalScore.toFixed(2) + (s.review && s.finalScore && challenge.status === 'COMPLETED') + ? s.finalScore.toFixed(2) : 'N/A' }

From 38bed307669e7caf44bb5369ff569d4cc3cd0ac7 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Thu, 25 Sep 2025 13:37:34 +0530 Subject: [PATCH 28/76] fix: apply for review modal updates the apply for review modal to work with only single role fixes #1761 --- .../page/review-opportunity-details.js | 23 ++++- .../ApplyModal/index.jsx | 84 ++++++++----------- .../Header/ApplyTime/index.jsx | 2 +- .../containers/ReviewOpportunityDetails.jsx | 57 +++++-------- src/shared/services/reviewOpportunities.js | 30 +++++++ 5 files changed, 109 insertions(+), 87 deletions(-) diff --git a/src/shared/actions/page/review-opportunity-details.js b/src/shared/actions/page/review-opportunity-details.js index 7bc3a15208..f21f4769a0 100644 --- a/src/shared/actions/page/review-opportunity-details.js +++ b/src/shared/actions/page/review-opportunity-details.js @@ -5,7 +5,7 @@ import _ from 'lodash'; import { createActions } from 'redux-actions'; import { logger } from 'topcoder-react-lib'; -import { getDetails } from '../../services/reviewOpportunities'; +import { getDetails, submitApplications } from '../../services/reviewOpportunities'; /* Holds valid values for the tab state. */ @@ -42,11 +42,32 @@ function getDetailsDone(challengeId, opportunityId) { }); } +/** + * @static + * @desc Creates an action that signals beginning of review application process. + * @return {Action} + */ +function submitAppliationInit() {} + +/** + * @static + * @desc Creates an action that submits application for a review opportunity. + * @param {Number} challengeId The ID of the challenge (not the opportunity id) + * @param {Array} roleIds Array of roleId Numbers to cancel applications for + * @param {String} tokenV3 Required. Topcoder auth token v3. + * @return {Action} + */ +function submitApplicationsDone(challengeId, tokenV3) { + return submitApplications(challengeId, tokenV3); +} + export default createActions({ PAGE: { REVIEW_OPPORTUNITY_DETAILS: { GET_DETAILS_INIT: getDetailsInit, GET_DETAILS_DONE: getDetailsDone, + SUBMIT_APPLICATIONS_INIT: submitAppliationInit, + SUBMIT_APPLICATIONS_DONE: submitApplicationsDone, SELECT_TAB: _.identity, SET_ROLES: _.identity, TOGGLE_APPLY_MODAL: _.identity, diff --git a/src/shared/components/ReviewOpportunityDetailsPage/ApplyModal/index.jsx b/src/shared/components/ReviewOpportunityDetailsPage/ApplyModal/index.jsx index c09fa5159e..f1d67a269c 100644 --- a/src/shared/components/ReviewOpportunityDetailsPage/ApplyModal/index.jsx +++ b/src/shared/components/ReviewOpportunityDetailsPage/ApplyModal/index.jsx @@ -1,7 +1,6 @@ /** * Modal that displays a list of available reviewer roles and allows user to select and apply */ -import _ from 'lodash'; import React from 'react'; import PT from 'prop-types'; import { Modal, PrimaryButton, Button } from 'topcoder-react-ui-kit'; @@ -26,23 +25,29 @@ class ApplyModal extends React.Component { render() { const { - details, handle, onApply, onCancel, toggleRole, selectedRoles, + details, handle, onApply, onCancel, selectedRoles, } = this.props; const positions = openPositionsByRole(details); + const position = positions[0]; const previousRoles = activeRoleIds(details, handle); const hasApplied = Boolean(previousRoles.length); - const hasChanged = !_.isEqual(new Set(selectedRoles), new Set(previousRoles)); + + const onHandleApply = () => { + const updatedRoles = [...selectedRoles]; + if (position.role === 'Reviewer') { + updatedRoles.push(position.roleId); + } + onApply(updatedRoles); + }; return (

- {hasApplied ? 'Manage Applications' : 'Apply Reviewer Positions'} + {hasApplied ? 'Reviewer Application' : 'Apply Reviewer Position'}

- Select the review roles you would like to apply for and click the button. - The system will assign members that best meet the review requirements for this contest. - Although you will be assigned to at most one review position, - applying for multiple roles increases your chances of being selected. + Click Apply Now to apply as a Reviewer for this challenge. + Payment and positions are shown below.

@@ -54,44 +59,23 @@ class ApplyModal extends React.Component {
Payment
-
- { - positions.map(position => ( -
-
- - {position.role} - -
-
- {position.openPositions} -
-
- $ - {position.payment} - .00* -
-
-
- toggleRole(position.roleId)} - type="checkbox" - /> - {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} -
-
- )) - } +
+
+ + {position.role} + +
+
+ {position.openPositions} +
+
+ $ + {position.payment} + .00* +
+

*Depends on the number of submissions, the actual payment may differ. @@ -100,12 +84,17 @@ class ApplyModal extends React.Component { + {!hasApplied + && ( - {hasApplied ? 'Update' : 'Apply Now'} + Apply Now + ) + } +

); @@ -128,7 +117,6 @@ ApplyModal.propTypes = { onApply: PT.func.isRequired, onCancel: PT.func.isRequired, setRoles: PT.func.isRequired, - toggleRole: PT.func.isRequired, selectedRoles: PT.arrayOf(PT.number), }; diff --git a/src/shared/components/ReviewOpportunityDetailsPage/Header/ApplyTime/index.jsx b/src/shared/components/ReviewOpportunityDetailsPage/Header/ApplyTime/index.jsx index cc48ef7541..0013a94009 100644 --- a/src/shared/components/ReviewOpportunityDetailsPage/Header/ApplyTime/index.jsx +++ b/src/shared/components/ReviewOpportunityDetailsPage/Header/ApplyTime/index.jsx @@ -51,7 +51,7 @@ const ApplyTime = ({ disabled={!timeLeft || !openPositions} onClick={() => onApply()} > - {hasApplied ? 'Manage Applications' : 'Apply for review'} + {hasApplied ? 'View Application' : 'Apply for review'}
diff --git a/src/shared/containers/ReviewOpportunityDetails.jsx b/src/shared/containers/ReviewOpportunityDetails.jsx index 146b286b5b..3b6ae8b17c 100644 --- a/src/shared/containers/ReviewOpportunityDetails.jsx +++ b/src/shared/containers/ReviewOpportunityDetails.jsx @@ -8,14 +8,14 @@ import React from 'react'; import PT from 'prop-types'; import { connect } from 'react-redux'; -import { errors } from 'topcoder-react-lib'; +import { actions, errors } from 'topcoder-react-lib'; import LoadingIndicator from 'components/LoadingIndicator'; -import { activeRoleIds } from 'utils/reviewOpportunities'; import pageActions from 'actions/page/review-opportunity-details'; import ReviewOpportunityDetailsPage from 'components/ReviewOpportunityDetailsPage'; import FailedToLoad from 'components/ReviewOpportunityDetailsPage/FailedToLoad'; import termsActions from 'actions/terms'; import { goToLogin } from 'utils/tc'; +import { logger } from 'tc-core-library-js'; const { fireErrorMessage } = errors; @@ -76,45 +76,28 @@ class ReviewOpportunityDetailsContainer extends React.Component { } } - handleOnModalApply() { + async handleOnModalApply() { const { - cancelApplications, challengeId, - details, - handle, + opportunityId, loadDetails, - selectedRoles, submitApplications, toggleApplyModal, tokenV3, } = this.props; - const rolesToApply = []; - const rolesToCancel = []; - - const previousRoles = activeRoleIds(details, handle); - - previousRoles.forEach((id) => { - if (!_.includes(selectedRoles, id)) { - rolesToCancel.push(id); - } - }); + try { + // Wait for the submit to finish (and succeed) + await submitApplications(opportunityId, tokenV3); - selectedRoles.forEach((id) => { - if (!_.includes(previousRoles, id)) { - rolesToApply.push(id); - } - }); + toggleApplyModal(); - if (rolesToApply.length) { - submitApplications(challengeId, rolesToApply, tokenV3); - } - if (rolesToCancel.length) { - cancelApplications(challengeId, rolesToCancel, tokenV3); + await loadDetails(challengeId, opportunityId, tokenV3); + } catch (err) { + logger.error('submitApplications failed', err); + toggleApplyModal(); } - - toggleApplyModal(); - loadDetails(challengeId, tokenV3); + loadDetails(challengeId, opportunityId, tokenV3); } render() { @@ -212,7 +195,7 @@ const mapStateToProps = (state, ownProps) => { termsFailure: terms.getTermsFailure, tokenV3: state.auth.tokenV3, isLoggedIn: Boolean(state.auth.user), - isReviewer: _.includes(state.auth.user.roles || [], 'reviewer'), + isReviewer: _.includes((state.auth.user && state.auth.user.roles) || [], 'reviewer'), }; }; @@ -223,7 +206,7 @@ const mapStateToProps = (state, ownProps) => { * @return {Object} */ function mapDispatchToProps(dispatch) { - const api = pageActions.page.reviewOpportunityDetails; + const api = actions.reviewOpportunity; const page = pageActions.page.reviewOpportunityDetails; return { cancelApplications: (challengeId, roleIds, tokenV3) => { @@ -231,8 +214,8 @@ function mapDispatchToProps(dispatch) { dispatch(api.cancelApplicationsDone(challengeId, roleIds, tokenV3)); }, loadDetails: (challengeId, opportunityId, tokenV3) => { - dispatch(api.getDetailsInit()); - dispatch(api.getDetailsDone(challengeId, opportunityId, tokenV3)); + dispatch(page.getDetailsInit()); + return dispatch(page.getDetailsDone(challengeId, opportunityId, tokenV3)); }, onPhaseExpand: () => dispatch(page.togglePhasesExpand()), openTermsModal: () => { @@ -240,9 +223,9 @@ function mapDispatchToProps(dispatch) { }, selectTab: tab => dispatch(page.selectTab(tab)), setRoles: roles => dispatch(page.setRoles(roles)), - submitApplications: (challengeId, roleIds, tokenV3) => { - dispatch(api.submitApplicationsInit()); - dispatch(api.submitApplicationsDone(challengeId, roleIds, tokenV3)); + submitApplications: (challengeId, tokenV3) => { + dispatch(page.submitApplicationsInit()); + return dispatch(page.submitApplicationsDone(challengeId, tokenV3)); }, toggleApplyModal: () => { dispatch(page.toggleApplyModal()); diff --git a/src/shared/services/reviewOpportunities.js b/src/shared/services/reviewOpportunities.js index f2a3237562..d9e525e654 100644 --- a/src/shared/services/reviewOpportunities.js +++ b/src/shared/services/reviewOpportunities.js @@ -58,3 +58,33 @@ export async function getDetails(challengeId, opportunityId) { return Promise.reject(err); } } + +/** + * Submits review opportunity application for the specified challenge + * @param {Number} challengeId The ID of the challenge (not the opportunity id) + * @param {Array} roleIds List of review role IDs to apply for + * @return {Promise} Resolves to the api response in JSON. + */ +export async function submitApplications(opportunityId, tokenV3) { + const payload = { + opportunityId, + role: 'REVIEWER', + }; + try { + const res = await fetch(`${v6ApiUrl}/review-applications`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${tokenV3}`, + }, + body: JSON.stringify(payload), + }); + if (!res.ok) { + throw new Error(`Request failed: ${res.status}`); + } + + return res.json(); + } catch (error) { + return 'There was an error while submitting the application.'; + } +} From 7bdfc522b3a6ad3e1579694e61eb18169a966028 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Thu, 25 Sep 2025 16:29:28 +0530 Subject: [PATCH 29/76] fix: time left to apply the open for review challenges shows late by text even when there is time left Fixes #2081 --- .../challenge-listing/ReviewOpportunityCard/index.jsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/shared/components/challenge-listing/ReviewOpportunityCard/index.jsx b/src/shared/components/challenge-listing/ReviewOpportunityCard/index.jsx index 1f7d0eba0a..b0b9b86aee 100644 --- a/src/shared/components/challenge-listing/ReviewOpportunityCard/index.jsx +++ b/src/shared/components/challenge-listing/ReviewOpportunityCard/index.jsx @@ -55,6 +55,8 @@ function ReviewOpportunityCard({ tags = tags.filter(tag => tag.trim().length); const { track } = challenge; const start = moment(opportunity.startDate); + const now = moment(); + const isLate = now.isAfter(start); return (
@@ -160,8 +162,11 @@ function ReviewOpportunityCard({ styleName="register-button" > - Late by
- { start.isAfter() ? formatDuration(start.diff()) : ` ${formatDuration(-start.diff())}` } + {isLate ? 'Late by' : 'Time left'}
+ {isLate + ? formatDuration(now.diff(start)) + : formatDuration(start.diff(now)) + }
to Apply From d4ae0f49bbc05784a54a283a21e39c65f7b81c9b Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Thu, 25 Sep 2025 16:38:25 +0530 Subject: [PATCH 30/76] fix: update link for review help page update link for review help page Fixes #2079 --- src/shared/components/ReviewOpportunityDetailsPage/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/components/ReviewOpportunityDetailsPage/index.jsx b/src/shared/components/ReviewOpportunityDetailsPage/index.jsx index 0e3339ead4..34d4cc7f7a 100644 --- a/src/shared/components/ReviewOpportunityDetailsPage/index.jsx +++ b/src/shared/components/ReviewOpportunityDetailsPage/index.jsx @@ -85,7 +85,7 @@ const ReviewOpportunityDetailsPage = ({
From bc8d0f80cbb414b37040baf2751dc31bf616020e Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Fri, 26 Sep 2025 14:07:53 +1000 Subject: [PATCH 31/76] Various QA fixes PM-1971, PM-1972 --- .../SubmissionManagement/index.jsx | 3 ++- .../components/challenge-detail/Header/index.jsx | 10 ++++++++-- .../components/challenge-detail/Registrants/index.jsx | 5 ++++- src/shared/containers/challenge-detail/index.jsx | 5 ++++- src/shared/utils/challenge-detail/helper.jsx | 5 ++++- 5 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/shared/components/SubmissionManagement/SubmissionManagement/index.jsx b/src/shared/components/SubmissionManagement/SubmissionManagement/index.jsx index 4e1182fafc..b800f7c75b 100644 --- a/src/shared/components/SubmissionManagement/SubmissionManagement/index.jsx +++ b/src/shared/components/SubmissionManagement/SubmissionManagement/index.jsx @@ -57,6 +57,7 @@ export default function SubmissionManagement(props) { .sort((a, b) => moment(a.scheduledEndDate).diff(b.scheduledEndDate))[0]; const submissionPhase = challenge.phases.filter(p => p.name === 'Submission')[0]; const submissionEndDate = submissionPhase && phaseEndDate(submissionPhase); + const isSubmissionPhaseOpen = Boolean(submissionPhase && submissionPhase.isOpen); const now = moment(); const end = moment(currentPhase && currentPhase.scheduledEndDate); @@ -192,7 +193,7 @@ export default function SubmissionManagement(props) { ) }
- {now.isBefore(submissionEndDate) && ( + {isSubmissionPhaseOpen && now.isBefore(submissionEndDate) && (
moment(phaseEndDate(a)).diff(phaseEndDate(b))); - const placementPrizes = _.find(prizeSets, { type: 'placement' }); + const placementPrizes = _.find( + prizeSets, + prizeSet => ((prizeSet && prizeSet.type) || '').toLowerCase() === 'placement', + ); const { prizes } = placementPrizes || []; - const checkpointPrizes = _.find(prizeSets, { type: 'checkpoint' }); + const checkpointPrizes = _.find( + prizeSets, + prizeSet => ((prizeSet && prizeSet.type) || '').toLowerCase() === 'checkpoint', + ); let numberOfCheckpointsPrizes = 0; let topCheckPointPrize = 0; if (!_.isEmpty(checkpointPrizes)) { diff --git a/src/shared/components/challenge-detail/Registrants/index.jsx b/src/shared/components/challenge-detail/Registrants/index.jsx index 77c1466bf5..b7727f4f2e 100644 --- a/src/shared/components/challenge-detail/Registrants/index.jsx +++ b/src/shared/components/challenge-detail/Registrants/index.jsx @@ -288,7 +288,10 @@ export default class Registrants extends React.Component { const revertSort = (sort === 'desc') ? 'asc' : 'desc'; const isDesign = track.toLowerCase() === 'design'; - const placementPrizes = _.find(prizeSets, { type: 'PLACEMENT' }); + const placementPrizes = _.find( + prizeSets, + prizeSet => ((prizeSet && prizeSet.type) || '').toLowerCase() === 'placement', + ); const { prizes } = placementPrizes || []; const checkpoints = challenge.checkpoints || []; diff --git a/src/shared/containers/challenge-detail/index.jsx b/src/shared/containers/challenge-detail/index.jsx index 5623195763..28d57c778d 100644 --- a/src/shared/containers/challenge-detail/index.jsx +++ b/src/shared/containers/challenge-detail/index.jsx @@ -423,7 +423,10 @@ class ChallengeDetailPageContainer extends React.Component { const { prizeSets } = challenge; let challengePrizes = []; - const placementPrizes = _.find(prizeSets, { type: 'placement' }); + const placementPrizes = _.find( + prizeSets, + prizeSet => ((prizeSet && prizeSet.type) || '').toLowerCase() === 'placement', + ); if (placementPrizes) { challengePrizes = _.filter(placementPrizes.prizes, p => p.value > 0); } diff --git a/src/shared/utils/challenge-detail/helper.jsx b/src/shared/utils/challenge-detail/helper.jsx index cf0b87b92f..5572f1a80e 100644 --- a/src/shared/utils/challenge-detail/helper.jsx +++ b/src/shared/utils/challenge-detail/helper.jsx @@ -159,7 +159,10 @@ export function getPrizePurseUI( * @param {Object} challenge challenge info */ export function getPrizePointsUI(challenge) { - const placementPrizes = _.find(challenge.prizeSets, { type: 'placement' }); + const placementPrizes = _.find( + challenge.prizeSets, + prizeSet => ((prizeSet && prizeSet.type) || '').toLowerCase() === 'placement', + ); if (placementPrizes) { const { prizes } = placementPrizes || []; From 7d98926dcda83200dbe7b611a9f00526660a6c0d Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Fri, 3 Oct 2025 12:13:51 +1000 Subject: [PATCH 32/76] Terms updates --- .../containers/challenge-detail/index.jsx | 58 ++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/src/shared/containers/challenge-detail/index.jsx b/src/shared/containers/challenge-detail/index.jsx index 28d57c778d..4ebdd8ca76 100644 --- a/src/shared/containers/challenge-detail/index.jsx +++ b/src/shared/containers/challenge-detail/index.jsx @@ -45,7 +45,8 @@ import { } from 'utils/tc'; import { config } from 'topcoder-react-utils'; import MetaTags from 'components/MetaTags'; -import { actions } from 'topcoder-react-lib'; +import { decodeToken } from '@topcoder-platform/tc-auth-lib'; +import { actions, services } from 'topcoder-react-lib'; import { getService } from 'services/contentful'; import { getSubmissionArtifacts as getSubmissionArtifactsService } from 'services/submissions'; // import { @@ -87,6 +88,7 @@ import './styles.scss'; /* Holds various time ranges in milliseconds. */ const MIN = 60 * 1000; const DAY = 24 * 60 * MIN; +const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); /** * Given challenge details object, it returns the URL of the image to be used in @@ -1005,7 +1007,59 @@ const mapDispatchToProps = (dispatch) => { registerForChallenge: (auth, challengeId) => { const a = actions.challenge; dispatch(a.registerInit()); - dispatch(a.registerDone(auth, challengeId)); + + const challengeService = services.challenge.getService(auth.tokenV3, auth.tokenV2); + const apiV6 = services.api.getApi('V6', auth.tokenV3); + const actionType = a.registerDone.toString(); + + const payload = (async () => { + try { + if (!auth.tokenV3) { + throw new Error('Authentication token is required to register.'); + } + + const roleId = await challengeService.getRoleId('Submitter'); + const user = decodeToken(auth.tokenV3); + const requestBody = { + challengeId, + memberHandle: encodeURIComponent(user.handle), + roleId, + }; + + const response = await apiV6.postJson('/resources', requestBody); + let responseData = null; + + if (!response.ok) { + try { + responseData = await response.json(); + } catch (parseError) { + responseData = null; + } + const error = new Error((responseData && responseData.message) + || response.statusText + || 'Failed to register for the challenge.'); + error.status = response.status; + if (responseData && responseData.metadata && responseData.metadata.missingTerms) { + error.missingTerms = responseData.metadata.missingTerms; + } + throw error; + } + + await response.json().catch(() => null); + await wait(config.CHALLENGE_DETAILS_REFRESH_DELAY); + return challengeService.getChallengeDetails(challengeId); + } catch (err) { + if (err.status === 403 && err.missingTerms) { + dispatch(termsActions.terms.openTermsModal('ANY')); + } + throw err; + } + })(); + + return dispatch({ + type: actionType, + payload, + }); }, reloadChallengeDetails: (tokens, challengeId) => { const a = actions.challenge; From b404479312ee89fe5106c6eed5f763d7b8a5cc71 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Mon, 6 Oct 2025 16:23:11 +1100 Subject: [PATCH 33/76] Use V6 APIs for the dashboard / home page --- src/shared/services/dashboard.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/shared/services/dashboard.js b/src/shared/services/dashboard.js index cbcd3cc60a..8fb874bc9d 100644 --- a/src/shared/services/dashboard.js +++ b/src/shared/services/dashboard.js @@ -5,12 +5,12 @@ const { getApi } = services.api; class DashboardService { /** - * @param {String} tokenV5 Optional. Auth token for Topcoder API v5. + * @param {String} tokenV6 Optional. Auth token for Topcoder API v6. */ - constructor(tokenV5) { + constructor(tokenV6) { this.private = { - api: getApi('V5', tokenV5), - tokenV5, + api: getApi('V6', tokenV6), + tokenV6, }; } @@ -27,13 +27,13 @@ class DashboardService { /** * Returns a new or existing challenges service. - * @param {String} tokenV5 Optional. Auth token for Topcoder API v5. + * @param {String} tokenV6 Optional. Auth token for Topcoder API v6. * @return {DashboardService} Dashboard service object */ let lastInstance = null; -export function getService(tokenV5) { - if (!lastInstance || tokenV5 !== lastInstance.private.tokenV5) { - lastInstance = new DashboardService(tokenV5); +export function getService(tokenV6) { + if (!lastInstance || tokenV6 !== lastInstance.private.tokenV6) { + lastInstance = new DashboardService(tokenV6); } return lastInstance; } From 885c34129a82fa091d793de58c53e8b19325cab5 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Wed, 8 Oct 2025 10:07:56 +1100 Subject: [PATCH 34/76] Updates for challenge track and type objects instead of strings. --- src/shared/components/ChallengeTile/index.jsx | 8 +++-- .../Contentful/ChallengesBlock/Card/index.jsx | 3 +- .../ReviewOpportunityDetailsPage/index.jsx | 2 +- .../SubmissionManagement/index.jsx | 6 ++-- src/shared/components/TrackIcon/index.jsx | 3 +- .../Header/TabSelector/index.jsx | 3 +- .../challenge-detail/Header/index.jsx | 10 +++--- .../challenge-detail/Registrants/index.jsx | 3 +- .../challenge-detail/Specification/index.jsx | 15 ++++---- .../challenge-detail/Submissions/index.jsx | 11 +++--- .../ChallengeCard/Status/index.jsx | 3 +- .../challenge-listing/ChallengeCard/index.jsx | 5 +-- .../Listing/Bucket/index.jsx | 6 ++-- .../ChallengesBlock/Card/index.jsx | 3 +- src/shared/containers/SubmissionPage.jsx | 2 +- .../containers/challenge-detail/index.jsx | 34 +++++++++++-------- src/shared/utils/challenge-detail/helper.jsx | 3 +- src/shared/utils/challenge.js | 34 +++++++++++++++++-- 18 files changed, 105 insertions(+), 49 deletions(-) diff --git a/src/shared/components/ChallengeTile/index.jsx b/src/shared/components/ChallengeTile/index.jsx index 0dbc7b7a22..0f0b47234d 100644 --- a/src/shared/components/ChallengeTile/index.jsx +++ b/src/shared/components/ChallengeTile/index.jsx @@ -7,7 +7,7 @@ import React from 'react'; import PT from 'prop-types'; import { Link } from 'react-router-dom'; import { formatDate, COMPETITION_TRACKS } from 'utils/tc'; -import { isMM } from 'utils/challenge'; +import { isMM, getTrackName, getTypeName } from 'utils/challenge'; import ReactImageFallback from 'react-image-fallback'; import InviteOnly from 'assets/images/ico-invite-only-prj.svg'; import WinnerRibbon from 'assets/images/ico-winner-ribbon.svg'; @@ -75,9 +75,11 @@ class ChallengeTile extends React.Component { } = this.props; const { track, type } = challenge; + const trackName = getTrackName(track); + const typeName = getTypeName(type); const roles = _.get(challenge, 'userDetails.roles'); - const outStyleName = `challenge tile-view ${track.replace(' ', '-').toLowerCase()}`; + const outStyleName = `challenge tile-view ${(trackName || '').replace(' ', '-').toLowerCase()}`; const extraStyle = { width: '285px', padding: '15px', @@ -107,7 +109,7 @@ class ChallengeTile extends React.Component { )}

- {underscoreReplace(type)} + {underscoreReplace(typeName)}

{challenge.submissionEndDate && formatDate(challenge.submissionEndDate)} diff --git a/src/shared/components/Contentful/ChallengesBlock/Card/index.jsx b/src/shared/components/Contentful/ChallengesBlock/Card/index.jsx index 565036305b..27254b2a30 100644 --- a/src/shared/components/Contentful/ChallengesBlock/Card/index.jsx +++ b/src/shared/components/Contentful/ChallengesBlock/Card/index.jsx @@ -13,6 +13,7 @@ import { import { Link } from 'topcoder-react-utils'; import { COMPETITION_TRACKS } from 'utils/tc'; +import { getTrackName } from 'utils/challenge'; import './style.scss'; @@ -29,7 +30,7 @@ export default function Card({ } = challenge; let TrackTag; - switch (track.toLowerCase()) { + switch ((getTrackName(track) || '').toLowerCase()) { case COMPETITION_TRACKS.DS: TrackTag = DataScienceTrackTag; break; diff --git a/src/shared/components/ReviewOpportunityDetailsPage/index.jsx b/src/shared/components/ReviewOpportunityDetailsPage/index.jsx index 34d4cc7f7a..8e63b3005c 100644 --- a/src/shared/components/ReviewOpportunityDetailsPage/index.jsx +++ b/src/shared/components/ReviewOpportunityDetailsPage/index.jsx @@ -49,7 +49,7 @@ const ReviewOpportunityDetailsPage = ({ Review Opportunities

- {details.challenge.type} + {typeof details.challenge.type === 'object' ? details.challenge.type.name : details.challenge.type}
diff --git a/src/shared/components/SubmissionManagement/SubmissionManagement/index.jsx b/src/shared/components/SubmissionManagement/SubmissionManagement/index.jsx index b800f7c75b..6ff4ff8202 100644 --- a/src/shared/components/SubmissionManagement/SubmissionManagement/index.jsx +++ b/src/shared/components/SubmissionManagement/SubmissionManagement/index.jsx @@ -22,6 +22,7 @@ import { Link } from 'topcoder-react-utils'; import LeftArrow from 'assets/images/arrow-prev-green.svg'; import { PrimaryButton } from 'topcoder-react-ui-kit'; import { phaseEndDate } from 'utils/challenge-listing/helper'; +import { getTrackName } from 'utils/challenge'; import SubmissionsTable from '../SubmissionsTable'; import style from './styles.scss'; @@ -47,8 +48,9 @@ export default function SubmissionManagement(props) { } = props; const { track } = challenge; + const trackName = getTrackName(track); - const challengeType = track.toLowerCase(); + const challengeType = (trackName || '').toLowerCase(); const isDesign = challengeType === 'design'; const isDevelop = challengeType === 'development'; @@ -185,7 +187,7 @@ export default function SubmissionManagement(props) { challenge={challenge} submissionObjects={submissions} showDetails={showDetails} - track={track} + track={trackName} status={challenge.status} submissionPhaseStartDate={submissionPhaseStartDate} {...componentConfig} diff --git a/src/shared/components/TrackIcon/index.jsx b/src/shared/components/TrackIcon/index.jsx index 33ccf15e72..fcf37779c9 100644 --- a/src/shared/components/TrackIcon/index.jsx +++ b/src/shared/components/TrackIcon/index.jsx @@ -13,7 +13,8 @@ export default function TrackIcon({ challengesUrl, }) { const TCO_URL = `${MAIN_URL}/tco`; - const trackStyle = track.replace(' ', '-').toLowerCase(); + const trackName = (track && typeof track === 'object') ? (track.name || '') : (track || ''); + const trackStyle = trackName.replace(' ', '-').toLowerCase(); let abbreviationStyle = type.abbreviation; if (['CH', 'F2F', 'TSK', 'MM'].indexOf(abbreviationStyle) < 0) { abbreviationStyle = ''; diff --git a/src/shared/components/challenge-detail/Header/TabSelector/index.jsx b/src/shared/components/challenge-detail/Header/TabSelector/index.jsx index 9c3e7cb300..e2e7567d80 100644 --- a/src/shared/components/challenge-detail/Header/TabSelector/index.jsx +++ b/src/shared/components/challenge-detail/Header/TabSelector/index.jsx @@ -17,6 +17,7 @@ import CloseIcon from 'assets/images/icon-close-green.svg'; import SortIcon from 'assets/images/icon-sort-mobile.svg'; import style from './style.scss'; +import { getTypeName } from 'utils/challenge'; function getSelectorStyle(selectedView, currentView) { return `challenge-selector-common ${(selectedView === currentView @@ -57,7 +58,7 @@ export default function ChallengeViewSelector(props) { const [isTabClosed, setIsTabClosed] = useState(true); const [expanded, setExpanded] = useState(false); const [selectedSortOption, setSelectedSortOption] = useState(); - const isF2F = type === 'First2Finish'; + const isF2F = getTypeName({ type }) === 'First2Finish'; const isBugHunt = _.includes(tags, 'Bug Hunt'); const isDesign = trackLower === 'design'; diff --git a/src/shared/components/challenge-detail/Header/index.jsx b/src/shared/components/challenge-detail/Header/index.jsx index 90436dcf77..0d1e061e17 100644 --- a/src/shared/components/challenge-detail/Header/index.jsx +++ b/src/shared/components/challenge-detail/Header/index.jsx @@ -8,7 +8,7 @@ import _ from 'lodash'; import moment from 'moment'; import 'moment-duration-format'; -import { isMM } from 'utils/challenge'; +import { isMM, getTrackName, getTypeName } from 'utils/challenge'; import PT from 'prop-types'; import React, { useMemo } from 'react'; @@ -141,7 +141,9 @@ export default function ChallengeHeader(props) { const currentPhases = allOpenPhases .filter(p => !isRegistrationPhase(p))[0]; - const trackLower = track ? track.replace(' ', '-').toLowerCase() : 'design'; + const trackName = getTrackName(track); + const typeName = getTypeName(type); + const trackLower = trackName ? trackName.replace(' ', '-').toLowerCase() : 'design'; const eventNames = (events || []).map((event => (event.eventName || '').toUpperCase())); @@ -241,7 +243,7 @@ export default function ChallengeHeader(props) { if (trackLower === 'quality-assurance') { relevantPhases = _.filter(relevantPhases, p => !(p.name.toLowerCase().includes('specification submission') || p.name.toLowerCase().includes('specification review'))); } - if (type === 'First2Finish' && status === 'COMPLETED') { + if (typeName === 'First2Finish' && status === 'COMPLETED') { const phases2 = allPhases.filter(p => p.name === 'Iterative Review' && !p.isOpen); const endPhaseDate = Math.max(...phases2.map(d => phaseEndDate(d))); relevantPhases = _.filter(relevantPhases, p => (p.name.toLowerCase().includes('registration') @@ -325,7 +327,7 @@ export default function ChallengeHeader(props) { 0) { - forumLink = track.toLowerCase() === 'design' + const trackName = (getTrackName(track) || '').toLowerCase(); + forumLink = trackName === 'design' ? `/?module=ThreadList&forumID=${forumId}` : `/?module=Category&categoryID=${forumId}`; } @@ -108,7 +109,7 @@ export default function ChallengeDetailsView(props) { } let accentedStyle = ''; - switch (track.toLowerCase()) { + switch ((getTrackName(track) || '').toLowerCase()) { case 'design': accentedStyle = 'challenge-specs-design'; break; @@ -187,7 +188,7 @@ export default function ChallengeDetailsView(props) {
{ - track.toLowerCase() !== 'design' + (getTrackName(track) || '').toLowerCase() !== 'design' ? (
{ @@ -366,8 +367,8 @@ export default function ChallengeDetailsView(props) { legacyId={legacyId} forumLink={forumLink} discuss={discuss} - isDesign={track.toLowerCase() === 'design'} - isDevelop={track.toLowerCase() === 'development'} + isDesign={(getTrackName(track) || '').toLowerCase() === 'design'} + isDevelop={(getTrackName(track) || '').toLowerCase() === 'development'} eventDetail={_.isEmpty(events) ? null : events[0]} isMM={isMM(challenge)} terms={terms} @@ -417,7 +418,7 @@ ChallengeDetailsView.propTypes = { forumId: PT.number, selfService: PT.bool, }), - track: PT.string.isRequired, + track: PT.oneOfType([PT.string, PT.shape()]).isRequired, legacyId: PT.oneOfType([PT.string, PT.number]), groups: PT.any, reviewType: PT.string, diff --git a/src/shared/components/challenge-detail/Submissions/index.jsx b/src/shared/components/challenge-detail/Submissions/index.jsx index effaf70fa9..0716c09d1e 100644 --- a/src/shared/components/challenge-detail/Submissions/index.jsx +++ b/src/shared/components/challenge-detail/Submissions/index.jsx @@ -6,7 +6,7 @@ import React from 'react'; import PT from 'prop-types'; import moment from 'moment'; -import { isMM as checkIsMM, isRDM as checkIsRDM } from 'utils/challenge'; +import { isMM as checkIsMM, isRDM as checkIsRDM, getTrackName, getTypeName } from 'utils/challenge'; import _ from 'lodash'; import { connect } from 'react-redux'; import { config } from 'topcoder-react-utils'; @@ -284,7 +284,8 @@ class SubmissionsComponent extends React.Component { isMM() { const { challenge } = this.props; - return challenge.track.toLowerCase() === 'data science' || checkIsMM(challenge); + const trackName = getTrackName(challenge); + return (trackName || '').toLowerCase() === 'data science' || checkIsMM(challenge); } /** @@ -336,6 +337,8 @@ class SubmissionsComponent extends React.Component { type, tags, } = challenge; + const trackName = getTrackName(track); + const typeName = getTypeName(type); // todo: hide download button until update submissions API const hideDownloadForMMRDM = true; @@ -414,7 +417,7 @@ class SubmissionsComponent extends React.Component {
); - const isF2F = type === 'First2Finish'; + const isF2F = typeName === 'First2Finish'; const isBugHunt = _.includes(tags, 'Bug Hunt'); // copy colorStyle from registrants to submissions @@ -434,7 +437,7 @@ class SubmissionsComponent extends React.Component { } }); - if (track.toLowerCase() === 'design') { + if ((trackName || '').toLowerCase() === 'design') { return challenge.submissionViewable === 'true' ? (
diff --git a/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx b/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx index dd77c03c2e..dd081a9313 100644 --- a/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx +++ b/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx @@ -16,6 +16,7 @@ import ProgressBarTooltip from '../../Tooltips/ProgressBarTooltip'; import UserAvatarTooltip from '../../Tooltips/UserAvatarTooltip'; import ForumIcon from '../../Icons/forum.svg'; import './style.scss'; +import { getTypeName } from 'utils/challenge'; import NumRegistrants from '../NumRegistrants'; import NumSubmissions from '../NumSubmissions'; @@ -232,7 +233,7 @@ export default function ChallengeStatus(props) { .filter(p => p.name !== 'Registration' && p.isOpen) .sort((a, b) => moment(a.scheduledEndDate).diff(b.scheduledEndDate))[0]; - if (!statusPhase && type === 'First2Finish' && allPhases.length) { + if (!statusPhase && getTypeName({ type }) === 'First2Finish' && allPhases.length) { statusPhase = _.clone(allPhases[0]); statusPhase.name = 'Submission'; } diff --git a/src/shared/components/challenge-listing/ChallengeCard/index.jsx b/src/shared/components/challenge-listing/ChallengeCard/index.jsx index 076b3598e4..80803a7713 100644 --- a/src/shared/components/challenge-listing/ChallengeCard/index.jsx +++ b/src/shared/components/challenge-listing/ChallengeCard/index.jsx @@ -42,6 +42,7 @@ function ChallengeCard({ id, track, } = challenge; + const trackName = (track && typeof track === 'object') ? (track.name || '') : track; challenge.prize = challenge.prizes || []; @@ -59,12 +60,12 @@ function ChallengeCard({
0 ? challenge.events[0].key : ''} /> diff --git a/src/shared/components/challenge-listing/Listing/Bucket/index.jsx b/src/shared/components/challenge-listing/Listing/Bucket/index.jsx index 22a21d39bc..dfb3996cb9 100644 --- a/src/shared/components/challenge-listing/Listing/Bucket/index.jsx +++ b/src/shared/components/challenge-listing/Listing/Bucket/index.jsx @@ -20,6 +20,7 @@ import CardPlaceholder from '../../placeholders/ChallengeCard'; import ChallengeCard from '../../ChallengeCard'; import NoRecommenderChallengeCard from '../../NoRecommenderChallengeCard'; import './style.scss'; +import { getTypeName } from 'utils/challenge'; // const COLLAPSED_SIZE = 10; @@ -85,7 +86,8 @@ export default function Bucket({ if (!_.includes(roles, 'administrator')) { filteredChallenges = sortedChallenges.filter((ch) => { - if (ch.type === 'Task' + const typeName = getTypeName(ch); + if (typeName === 'Task' && ch.task && ch.task.isTask && ch.task.isAssigned @@ -158,7 +160,7 @@ export default function Bucket({ const cards = filteredChallenges.map(challenge => ( { diff --git a/src/shared/components/tc-communities/ChallengesBlock/Card/index.jsx b/src/shared/components/tc-communities/ChallengesBlock/Card/index.jsx index d79b811a07..a33c1943a4 100644 --- a/src/shared/components/tc-communities/ChallengesBlock/Card/index.jsx +++ b/src/shared/components/tc-communities/ChallengesBlock/Card/index.jsx @@ -13,6 +13,7 @@ import { import { Link } from 'topcoder-react-utils'; import { COMPETITION_TRACKS } from 'utils/tc'; +import { getTrackName } from 'utils/challenge'; import './style.scss'; @@ -29,7 +30,7 @@ export default function Card({ } = challenge; let TrackTag; - switch (track.toLowerCase()) { + switch ((getTrackName(track) || '').toLowerCase()) { case 'datasci': case COMPETITION_TRACKS.DS: TrackTag = DataScienceTrackTag; diff --git a/src/shared/containers/SubmissionPage.jsx b/src/shared/containers/SubmissionPage.jsx index 80579b39ab..88be1deed9 100644 --- a/src/shared/containers/SubmissionPage.jsx +++ b/src/shared/containers/SubmissionPage.jsx @@ -188,7 +188,7 @@ const mapStateToProps = (state, ownProps) => { challengesUrl: ownProps.challengesUrl, tokenV2: state.auth.tokenV2, tokenV3: state.auth.tokenV3, - track: details.track, + track: (details && details.track && details.track.name) ? details.track.name : details.track, challenge: state.challenge, status: details.status, isRegistered: details.isRegistered, diff --git a/src/shared/containers/challenge-detail/index.jsx b/src/shared/containers/challenge-detail/index.jsx index 4ebdd8ca76..271d98e950 100644 --- a/src/shared/containers/challenge-detail/index.jsx +++ b/src/shared/containers/challenge-detail/index.jsx @@ -8,7 +8,7 @@ import _ from 'lodash'; import communityActions from 'actions/tc-communities'; -import { isMM as checkIsMM, isRDM as checkIsRDM } from 'utils/challenge'; +import { isMM as checkIsMM, isRDM as checkIsRDM, getTrackName, getTypeName } from 'utils/challenge'; import LoadingPagePlaceholder from 'components/LoadingPagePlaceholder'; import pageActions from 'actions/page'; import ChallengeHeader from 'components/challenge-detail/Header'; @@ -99,6 +99,8 @@ const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); function getOgImage(challenge) { const { legacy } = challenge; const { subTrack } = legacy; + const trackName = getTrackName(challenge); + const typeName = getTypeName(challenge); if (challenge.name.startsWith('LUX -')) return ogBigPrizesChallenge; if (challenge.name.startsWith('RUX -')) return ogBigPrizesChallenge; if (challenge.prizes) { @@ -108,15 +110,15 @@ function getOgImage(challenge) { switch (subTrack) { case SUBTRACKS.FIRST_2_FINISH: - switch (challenge.track) { - case COMPETITION_TRACKS_V3.DEVELOP: return challenge.type === 'Task' ? ogDEVTask : ogFirst2FinishDEV; - case COMPETITION_TRACKS_V3.QA: return challenge.type === 'Task' ? ogQATask : ogFirst2FinishQA; + switch (trackName) { + case COMPETITION_TRACKS_V3.DEVELOP: return typeName === 'Task' ? ogDEVTask : ogFirst2FinishDEV; + case COMPETITION_TRACKS_V3.QA: return typeName === 'Task' ? ogQATask : ogFirst2FinishQA; default: return ogFirst2FinishDEV; } case SUBTRACKS.DESIGN_FIRST_2_FINISH: - switch (challenge.track) { - case COMPETITION_TRACKS_V3.DESIGN: return challenge.type === 'Task' ? ogDESIGNTask : ogFirst2FinishDESIGN; + switch (trackName) { + case COMPETITION_TRACKS_V3.DESIGN: return typeName === 'Task' ? ogDESIGNTask : ogFirst2FinishDESIGN; default: return ogUiDesign; } @@ -141,11 +143,11 @@ function getOgImage(challenge) { default: } - switch (challenge.track) { + switch (trackName) { case COMPETITION_TRACKS_V3.DEVELOP: return ogDevelopment; - case COMPETITION_TRACKS_V3.DESIGN: return challenge.type === 'Task' ? ogDESIGNTask : ogUiDesign; + case COMPETITION_TRACKS_V3.DESIGN: return typeName === 'Task' ? ogDESIGNTask : ogUiDesign; case COMPETITION_TRACKS_V3.DS: return ogDSChallenge; - case COMPETITION_TRACKS_V3.QA: return challenge.type === 'Task' ? ogQATask : ogQAChallenge; + case COMPETITION_TRACKS_V3.QA: return typeName === 'Task' ? ogQATask : ogQAChallenge; default: return ogImage; } } @@ -261,7 +263,7 @@ class ChallengeDetailPageContainer extends React.Component { history.push(history.location.pathname, history.state); } - if (!checkIsMM(challenge) && COMPETITION_TRACKS_V3.DS !== challenge.track + if (!checkIsMM(challenge) && COMPETITION_TRACKS_V3.DS !== getTrackName(challenge) && selectedTab === DETAIL_TABS.MM_DASHBOARD) { onSelectorClicked(DETAIL_TABS.DETAILS); } @@ -414,11 +416,12 @@ class ChallengeDetailPageContainer extends React.Component { } = challenge; let { track } = legacy || {}; - if (!track) { /* eslint-disable prefer-destructuring */ track = challenge.track || ''; } + // Normalize track to a string if needed + track = getTrackName(track); const submissionsViewable = _.find(metadata, { type: 'submissionsViewable' }); const isLoggedIn = !_.isEmpty(auth.tokenV3); @@ -462,7 +465,7 @@ class ChallengeDetailPageContainer extends React.Component { } let winners = challenge.winners || []; - if (challenge.type !== 'Task') { + if (getTypeName(challenge) !== 'Task') { winners = winners.filter(w => !w.type || w.type === 'final'); } @@ -989,7 +992,8 @@ const mapDispatchToProps = (dispatch) => { dispatch(a.getDetailsDone(challengeId, tokens.tokenV3, tokens.tokenV2)) .then((res) => { const ch = res.payload; - if (ch.track === COMPETITION_TRACKS.DES) { + const chTrack = (ch && ch.track && ch.track.name) ? ch.track.name : ch.track; + if (chTrack === COMPETITION_TRACKS.DES) { const p = ch.phases || [] .filter(x => x.name === 'Checkpoint Review'); if (p.length && !p[0].isOpen) { @@ -1065,7 +1069,9 @@ const mapDispatchToProps = (dispatch) => { const a = actions.challenge; dispatch(a.getDetailsDone(challengeId, tokens.tokenV3, tokens.tokenV2)) .then((challengeDetails) => { - if (challengeDetails.track === COMPETITION_TRACKS.DES) { + const trackName = (challengeDetails && challengeDetails.track && challengeDetails.track.name) + ? challengeDetails.track.name : challengeDetails.track; + if (trackName === COMPETITION_TRACKS.DES) { const p = challengeDetails.phases || [] .filter(x => x.name === 'Checkpoint Review'); if (p.length && !p[0].isOpen) { diff --git a/src/shared/utils/challenge-detail/helper.jsx b/src/shared/utils/challenge-detail/helper.jsx index 5572f1a80e..b0d7d73985 100644 --- a/src/shared/utils/challenge-detail/helper.jsx +++ b/src/shared/utils/challenge-detail/helper.jsx @@ -8,6 +8,7 @@ import { convertNow as convertMoney } from 'services/money'; import { challenge as challengeUtils } from 'topcoder-react-lib'; import { config } from 'topcoder-react-utils'; import Prize from 'components/challenge-listing/ChallengeCard/Prize'; +import { getTypeName } from 'utils/challenge'; import { BUCKETS, getBuckets } from 'utils/challenge-listing/buckets'; import { phaseEndDate } from 'utils/challenge-listing/helper'; @@ -38,7 +39,7 @@ export function getChallengeTypeAbbr(track, challengeTypes) { * @param {Object} challenge challenge info */ export function getEndDate(challenge) { - const { type } = challenge; + const type = getTypeName(challenge); let phases = challenge.phases || []; if (type === 'First2Finish' && challenge.status === 'COMPLETED') { phases = challenge.phases.filter(p => p.phaseType === 'Iterative Review' && p.phaseStatus === 'Closed'); diff --git a/src/shared/utils/challenge.js b/src/shared/utils/challenge.js index 7cda66df6c..1e559908ec 100644 --- a/src/shared/utils/challenge.js +++ b/src/shared/utils/challenge.js @@ -3,13 +3,40 @@ */ import _ from 'lodash'; +/** + * Safely returns the string name for a provided track value which may be + * either a string or an object like { name: 'Development' }. + * @param {Object|String} trackOrChallenge Either a track object/string or a challenge object + * @returns {String} + */ +export function getTrackName(trackOrChallenge) { + const value = (trackOrChallenge && trackOrChallenge.track !== undefined) + ? trackOrChallenge.track : trackOrChallenge; + if (_.isObject(value)) return value.name || ''; + return value || ''; +} + +/** + * Safely returns the string name for a provided type value which may be + * either a string or an object like { name: 'Task' }. + * @param {Object|String} typeOrChallenge Either a type object/string or a challenge object + * @returns {String} + */ +export function getTypeName(typeOrChallenge) { + const value = (typeOrChallenge && typeOrChallenge.type !== undefined) + ? typeOrChallenge.type : typeOrChallenge; + if (_.isObject(value)) return value.name || ''; + return value || ''; +} + /** * check if is marathon match challenge * @param {Object} challenge challenge object */ export function isMM(challenge) { const tags = _.get(challenge, 'tags') || []; - const isMMType = challenge ? challenge.type === 'Marathon Match' : false; + const typeName = getTypeName(challenge); + const isMMType = challenge ? typeName === 'Marathon Match' : false; return tags.includes('Marathon Match') || isMMType; } @@ -19,7 +46,8 @@ export function isMM(challenge) { */ export function isRDM(challenge) { const tags = _.get(challenge, 'tags') || []; - const isMMType = challenge ? challenge.type === 'Rapid Development Match' : false; + const typeName = getTypeName(challenge); + const isMMType = challenge ? typeName === 'Rapid Development Match' : false; return tags.includes('Rapid Development Match') || tags.includes('RDM') || isMMType; } @@ -40,4 +68,6 @@ export function updateChallengeType(challenges, challengeTypeMap) { export default { isMM, updateChallengeType, + getTrackName, + getTypeName, }; From c40ccb8e5bed77fcd0eeb405e71d6737546f5624 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Wed, 8 Oct 2025 10:51:54 +1100 Subject: [PATCH 35/76] Lint and test cleanup --- .../challenge-detail/Header/TabSelector/index.jsx | 2 +- .../components/challenge-detail/Submissions/index.jsx | 7 ++++++- .../challenge-listing/ChallengeCard/Status/index.jsx | 2 +- .../challenge-listing/Listing/Bucket/index.jsx | 2 +- src/shared/containers/challenge-detail/index.jsx | 10 +++++++--- 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/shared/components/challenge-detail/Header/TabSelector/index.jsx b/src/shared/components/challenge-detail/Header/TabSelector/index.jsx index e2e7567d80..6223dc353c 100644 --- a/src/shared/components/challenge-detail/Header/TabSelector/index.jsx +++ b/src/shared/components/challenge-detail/Header/TabSelector/index.jsx @@ -16,8 +16,8 @@ import ArrowIcon from 'assets/images/ico-arrow-down.svg'; import CloseIcon from 'assets/images/icon-close-green.svg'; import SortIcon from 'assets/images/icon-sort-mobile.svg'; -import style from './style.scss'; import { getTypeName } from 'utils/challenge'; +import style from './style.scss'; function getSelectorStyle(selectedView, currentView) { return `challenge-selector-common ${(selectedView === currentView diff --git a/src/shared/components/challenge-detail/Submissions/index.jsx b/src/shared/components/challenge-detail/Submissions/index.jsx index 0716c09d1e..c6e9625300 100644 --- a/src/shared/components/challenge-detail/Submissions/index.jsx +++ b/src/shared/components/challenge-detail/Submissions/index.jsx @@ -6,7 +6,12 @@ import React from 'react'; import PT from 'prop-types'; import moment from 'moment'; -import { isMM as checkIsMM, isRDM as checkIsRDM, getTrackName, getTypeName } from 'utils/challenge'; +import { + isMM as checkIsMM, + isRDM as checkIsRDM, + getTrackName, + getTypeName, +} from 'utils/challenge'; import _ from 'lodash'; import { connect } from 'react-redux'; import { config } from 'topcoder-react-utils'; diff --git a/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx b/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx index dd081a9313..73189a5914 100644 --- a/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx +++ b/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx @@ -11,12 +11,12 @@ import { getTimeLeft, } from 'utils/challenge-detail/helper'; +import { getTypeName } from 'utils/challenge'; import ChallengeProgressBar from '../../ChallengeProgressBar'; import ProgressBarTooltip from '../../Tooltips/ProgressBarTooltip'; import UserAvatarTooltip from '../../Tooltips/UserAvatarTooltip'; import ForumIcon from '../../Icons/forum.svg'; import './style.scss'; -import { getTypeName } from 'utils/challenge'; import NumRegistrants from '../NumRegistrants'; import NumSubmissions from '../NumSubmissions'; diff --git a/src/shared/components/challenge-listing/Listing/Bucket/index.jsx b/src/shared/components/challenge-listing/Listing/Bucket/index.jsx index dfb3996cb9..7021ad7994 100644 --- a/src/shared/components/challenge-listing/Listing/Bucket/index.jsx +++ b/src/shared/components/challenge-listing/Listing/Bucket/index.jsx @@ -16,11 +16,11 @@ import { import SortingSelectBar from 'components/SortingSelectBar'; import Waypoint from 'react-waypoint'; // import { challenge as challengeUtils } from 'topcoder-react-lib'; +import { getTypeName } from 'utils/challenge'; import CardPlaceholder from '../../placeholders/ChallengeCard'; import ChallengeCard from '../../ChallengeCard'; import NoRecommenderChallengeCard from '../../NoRecommenderChallengeCard'; import './style.scss'; -import { getTypeName } from 'utils/challenge'; // const COLLAPSED_SIZE = 10; diff --git a/src/shared/containers/challenge-detail/index.jsx b/src/shared/containers/challenge-detail/index.jsx index 271d98e950..89c30e0c70 100644 --- a/src/shared/containers/challenge-detail/index.jsx +++ b/src/shared/containers/challenge-detail/index.jsx @@ -8,7 +8,12 @@ import _ from 'lodash'; import communityActions from 'actions/tc-communities'; -import { isMM as checkIsMM, isRDM as checkIsRDM, getTrackName, getTypeName } from 'utils/challenge'; +import { + isMM as checkIsMM, + isRDM as checkIsRDM, + getTrackName, + getTypeName, +} from 'utils/challenge'; import LoadingPagePlaceholder from 'components/LoadingPagePlaceholder'; import pageActions from 'actions/page'; import ChallengeHeader from 'components/challenge-detail/Header'; @@ -1069,8 +1074,7 @@ const mapDispatchToProps = (dispatch) => { const a = actions.challenge; dispatch(a.getDetailsDone(challengeId, tokens.tokenV3, tokens.tokenV2)) .then((challengeDetails) => { - const trackName = (challengeDetails && challengeDetails.track && challengeDetails.track.name) - ? challengeDetails.track.name : challengeDetails.track; + const trackName = _.get(challengeDetails, 'track.name', challengeDetails.track); if (trackName === COMPETITION_TRACKS.DES) { const p = challengeDetails.phases || [] .filter(x => x.name === 'Checkpoint Review'); From 21dcd3e700679e685d93da096f84a1f596ce984f Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Wed, 8 Oct 2025 15:09:05 +0530 Subject: [PATCH 36/76] API response has final score as string --- src/shared/components/challenge-detail/Submissions/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/components/challenge-detail/Submissions/index.jsx b/src/shared/components/challenge-detail/Submissions/index.jsx index c6e9625300..ca09aad07b 100644 --- a/src/shared/components/challenge-detail/Submissions/index.jsx +++ b/src/shared/components/challenge-detail/Submissions/index.jsx @@ -949,7 +949,7 @@ class SubmissionsComponent extends React.Component {

{ (s.review && s.finalScore && challenge.status === 'COMPLETED') - ? s.finalScore.toFixed(2) + ? Number(s.finalScore).toFixed(2) : 'N/A' }

From 57a6df5e90ab4455eb115b23a936e1ef5efecb79 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Wed, 8 Oct 2025 15:11:30 +0530 Subject: [PATCH 37/76] deploy PM-2233 --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 7512aa664e..77bd840cb0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -182,7 +182,7 @@ workflows: only: - develop - v6 - - PM-1760 + - PM-2233 - "build-prod": context: org-global From 1a4b810f7b9e83a9fbf619e357e5da2290c5721f Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Thu, 9 Oct 2025 09:06:23 +1100 Subject: [PATCH 38/76] MM leaderboard updates for v6 --- src/server/services/mmLeaderboard.js | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/server/services/mmLeaderboard.js b/src/server/services/mmLeaderboard.js index 24fb315db9..ff1209c808 100644 --- a/src/server/services/mmLeaderboard.js +++ b/src/server/services/mmLeaderboard.js @@ -5,7 +5,7 @@ import { services } from 'topcoder-react-lib'; import xss from 'xss'; -const { api, submissions } = services; +const { api } = services; /** * Auxiliary class that handles communication with MMLeaderboard @@ -20,15 +20,27 @@ export default class MMLService { try { const sanitizedId = xss(req.params.id); const m2mToken = await api.getTcM2mToken(); - const subSrv = submissions.getService(m2mToken); - const reviewIds = await subSrv.getScanReviewIds(); - const v5api = api.getApiV5(m2mToken); - const subs = await v5api.get(`/submissions?challengeId=${sanitizedId}&page=1&perPage=500`); + const v6api = api.getApi('V6', m2mToken); + const subsRes = await v6api.get(`/submissions?challengeId=${sanitizedId}&page=1&perPage=500`); + const json = await subsRes.json(); + const data = (json && json.data) ? json.data : []; + + // Map v6 reviewSummations into a "review" array compatible with existing client code + const mapped = data.map(s => ({ + ...s, + createdBy: s.submitterHandle || s.createdBy, + review: (s.reviewSummation || []).map(rs => ({ + score: rs.aggregateScore, + updatedAt: rs.updatedAt || rs.reviewedDate || s.updatedAt || s.submittedDate, + createdAt: rs.createdAt || rs.reviewedDate || s.createdAt || s.submittedDate, + createdBy: rs.createdBy, + })), + })); + return res.send({ id: sanitizedId, - // API now returns { data: [...], meta: {...} } - subs: (await subs.json()).data, - reviewIds, + subs: mapped, + reviewIds: [], }); } catch (err) { return next(err); From 98c187d036f586c30592a1209530e656bf3c6357 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Thu, 9 Oct 2025 09:27:41 +1100 Subject: [PATCH 39/76] Updates for new statistics endpoint in reports --- src/shared/containers/SmartLooker.jsx | 73 +++++++++++++++++++++++++++ src/shared/utils/markdown.js | 2 +- 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 src/shared/containers/SmartLooker.jsx diff --git a/src/shared/containers/SmartLooker.jsx b/src/shared/containers/SmartLooker.jsx new file mode 100644 index 0000000000..d1de75e7d1 --- /dev/null +++ b/src/shared/containers/SmartLooker.jsx @@ -0,0 +1,73 @@ +/** + * SmartLooker bridges legacy usages to new + * reports-api-v6 endpoints for specific Looker IDs used by the + * /community/statistics page. Unknown IDs fall back to the original Looker + * container (which fetches from the v4 Looks API). That fallback is a "just in case" + * item. + */ +import React from 'react'; +import PT from 'prop-types'; +import { config } from 'topcoder-react-utils'; +import Looker from 'components/Looker'; +import OriginalLookerContainer from 'containers/Looker'; + +const LOOKER_TO_REPORTS_MAP = { + // SRM datasets (static JSON served by reports-api-v6) + 1653: '/statistics/srm/top-rated', + 1657: '/statistics/srm/country-ratings', + 1654: '/statistics/srm/competitions-count', +}; + +export default function SmartLooker(props) { + const { lookerId } = props; + const reportsPath = LOOKER_TO_REPORTS_MAP[lookerId]; + + const [state, setState] = React.useState({ + loading: !!reportsPath, + error: null, + lookerInfo: null, + }); + + React.useEffect(() => { + let cancelled = false; + async function load() { + if (!reportsPath) return; + setState(s => ({ ...s, loading: true, error: null })); + try { + const res = await fetch(`${config.API.V6}/reports${reportsPath}`); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const data = await res.json(); + const lookerData = Array.isArray(data) ? data : [data]; + if (!cancelled) setState({ loading: false, error: null, lookerInfo: { lookerData } }); + } catch (e) { + if (!cancelled) { + setState({ + loading: false, + error: e.message, + lookerInfo: { lookerData: [] }, + }); + } + } + } + load(); + return () => { cancelled = true; }; + }, [lookerId]); + + if (!reportsPath) { + // Fall back to legacy behavior for non-mapped lookerIds + return ( + + ); + } + + if (state.loading && !state.lookerInfo) return 'loading...'; + if (state.error) return state.error; + + return ( + + ); +} + +SmartLooker.propTypes = { + lookerId: PT.string.isRequired, +}; diff --git a/src/shared/utils/markdown.js b/src/shared/utils/markdown.js index bc24c03989..7791519842 100644 --- a/src/shared/utils/markdown.js +++ b/src/shared/utils/markdown.js @@ -20,7 +20,7 @@ import 'highlight.js/styles/github.css'; import JoinCommunity from 'containers/tc-communities/JoinCommunity'; import VideoModalButton from 'components/VideoModalButton'; -import Looker from 'containers/Looker'; +import Looker from 'containers/SmartLooker'; import AnchorLink from 'react-anchor-link-smooth-scroll'; import Modal from 'components/Contentful/Modal'; import MMLeaderboard from 'containers/MMLeaderboard'; From 3c0dbadd8f3b37f7e2f4f690473dd73733855a68 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Thu, 9 Oct 2025 10:33:11 +1100 Subject: [PATCH 40/76] More statistics updates --- src/shared/containers/SmartLooker.jsx | 134 +++++++++++++++++++++++++- 1 file changed, 131 insertions(+), 3 deletions(-) diff --git a/src/shared/containers/SmartLooker.jsx b/src/shared/containers/SmartLooker.jsx index d1de75e7d1..2e19eef8ea 100644 --- a/src/shared/containers/SmartLooker.jsx +++ b/src/shared/containers/SmartLooker.jsx @@ -11,16 +11,141 @@ import { config } from 'topcoder-react-utils'; import Looker from 'components/Looker'; import OriginalLookerContainer from 'containers/Looker'; +// Direct mappings from known Looker IDs to reports-api-v6 endpoints. +// These are the specific Looker tiles used by the /community/statistics page +// that we want to source from the new reports service instead of legacy Looker. const LOOKER_TO_REPORTS_MAP = { // SRM datasets (static JSON served by reports-api-v6) 1653: '/statistics/srm/top-rated', 1657: '/statistics/srm/country-ratings', 1654: '/statistics/srm/competitions-count', + // NOTE: Additional general statistics Looker IDs can be added here as they + // are identified in content (e.g. header metrics and general tab tables). }; +// Safely parse a possible JSON string table definition into an array of columns. +function parseTableDef(table) { + if (!table && table !== '') return null; + if (Array.isArray(table)) return table; + if (table === '') return []; + try { + // Contentful markdown encodes quotes as &q; in some places; normalize first + const normalized = String(table).replace(/&q;/g, '"').replace(/'/g, '"'); + const cols = JSON.parse(normalized); + return Array.isArray(cols) ? cols : null; + } catch (e) { + return null; + } +} + +// Infer a reports-api-v6 endpoint and a transformer based on Looker props. +// This allows us to support the community/statistics page without knowing all +// Looker IDs ahead of time, by recognizing common property/table patterns. +function inferFromProps(props) { + const { property, table } = props; + const cols = parseTableDef(table); + + // Header metrics (single value) + if (property && !cols) { + const prop = String(property).toLowerCase(); + if (prop === 'user.count') { + return { + path: '/statistics/general/member-count', + transform: data => [{ 'user.count': Number(data.count) || 0 }], + }; + } + if (prop === 'challenge.count') { + return { + path: '/statistics/general/completed-challenges', + transform: data => [{ 'challenge.count': Number(data.count) || 0 }], + }; + } + // Total prizes: common property names are often 'total' or contain 'total' + if (prop === 'total' || prop.includes('total')) { + return { + path: '/statistics/general/total-prizes', + transform: data => [{ [property]: data.total }], + }; + } + } + + // General tab tables + if (cols && cols.length) { + const propList = cols.map(c => String(c.property || '')); + const hasCountry = propList.some(p => p.toLowerCase().includes('country')); + const hasHandle = propList.some(p => p.toLowerCase().includes('handle')); + const hasCopilot = propList.some(p => p.toLowerCase().includes('copilot')); + const hasReview = propList.some(p => p.toLowerCase().includes('review')); + const hasChallengeCount = propList.some(p => p.toLowerCase() === 'challenge.count'); + const hasUserCount = propList.some(p => p.toLowerCase() === 'user.count'); + const hasFirstPlace = propList.some(p => p.toLowerCase().includes('first_place')); + + // Countries represented: expect country + user.count + if (hasCountry && hasUserCount) { + return { + path: '/statistics/general/countries-represented', + transform: rows => rows.map(r => ({ + // Use whatever property the table asked for to label country + [propList.find(p => p.toLowerCase().includes('country')) || 'country.country_name']: + r.country_code, + [propList.find(p => p.toLowerCase() === 'user.count') || 'user.count']: + Number(r.members_count) || 0, + })), + }; + } + + // First place by country: expect country + a first_place count field + if (hasCountry && (hasFirstPlace || hasUserCount)) { + // Count column property: prefer one mentioning first_place, else user.count + const valueProp = propList.find(p => p.toLowerCase().includes('first_place')) + || propList.find(p => p.toLowerCase() === 'user.count') + || 'first_place_count'; + const nameProp = propList.find(p => p.toLowerCase().includes('country')) || 'country.country_name'; + return { + path: '/statistics/general/first-place-by-country', + transform: rows => rows.map(r => ({ + [nameProp]: r.country_code, + [valueProp]: Number(r.first_place_count) || 0, + })), + }; + } + + // Copiloted challenges: expect copilot handle + challenge.count + if ((hasCopilot || hasHandle) && hasChallengeCount) { + const nameProp = propList.find(p => p.toLowerCase().includes('copilot')) + || propList.find(p => p.toLowerCase().includes('handle')) + || 'copilot.handle'; + return { + path: '/statistics/general/copiloted-challenges', + transform: rows => rows.map(r => ({ + [nameProp]: r.handle, + 'challenge.count': Number(r.copiloted_challenges) || 0, + })), + }; + } + + // Reviews by member: expect handle + review count + if ((hasHandle || hasCopilot) && hasReview) { + const nameProp = propList.find(p => p.toLowerCase().includes('handle')) || 'handle'; + const valueProp = propList.find(p => p.toLowerCase().includes('review')) || 'review.count'; + return { + path: '/statistics/general/reviews-by-member', + transform: rows => rows.map(r => ({ + [nameProp]: r.handle, + [valueProp]: Number(r.review_count) || 0, + })), + }; + } + } + + return null; +} + export default function SmartLooker(props) { const { lookerId } = props; - const reportsPath = LOOKER_TO_REPORTS_MAP[lookerId]; + const direct = LOOKER_TO_REPORTS_MAP[lookerId]; + const inferred = inferFromProps(props); + const reportsPath = direct || (inferred && inferred.path); const [state, setState] = React.useState({ loading: !!reportsPath, @@ -37,7 +162,10 @@ export default function SmartLooker(props) { const res = await fetch(`${config.API.V6}/reports${reportsPath}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); - const lookerData = Array.isArray(data) ? data : [data]; + let lookerData = Array.isArray(data) ? data : [data]; + if (inferred && typeof inferred.transform === 'function') { + lookerData = inferred.transform(Array.isArray(data) ? data : data); + } if (!cancelled) setState({ loading: false, error: null, lookerInfo: { lookerData } }); } catch (e) { if (!cancelled) { @@ -51,7 +179,7 @@ export default function SmartLooker(props) { } load(); return () => { cancelled = true; }; - }, [lookerId]); + }, [lookerId, reportsPath]); if (!reportsPath) { // Fall back to legacy behavior for non-mapped lookerIds From 734408c960e12d11c07bc13442a2bd0eb6a5a4dd Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Thu, 9 Oct 2025 11:38:45 +1100 Subject: [PATCH 41/76] Further statistics page fixes for v6 reporting API --- src/server/index.js | 2 +- src/server/routes/contentful.js | 8 +- src/shared/components/Looker/index.jsx | 17 +++- .../components/ProfileBadgesPage/index.jsx | 2 +- src/shared/containers/SmartLooker.jsx | 82 +++++++++++++++---- 5 files changed, 85 insertions(+), 26 deletions(-) diff --git a/src/server/index.js b/src/server/index.js index 12ca3a7692..795b51d485 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -69,7 +69,7 @@ const getTimestamp = async () => { timestamp = moment(tsData.timestamp).valueOf(); } catch (err) { - console.error('Error:', err.message); + logger.error('Error:', err.message); } return timestamp; diff --git a/src/server/routes/contentful.js b/src/server/routes/contentful.js index 38c8249ec4..8189c525c5 100644 --- a/src/server/routes/contentful.js +++ b/src/server/routes/contentful.js @@ -29,7 +29,7 @@ routes.options('*', cors()); /* Gets non-image asset file. */ routes.use( '/:spaceName/:environment/assets/:id/:version/:name', - (req, res) => { + (req, res, next) => { try { const { environment, @@ -45,7 +45,7 @@ routes.use( const url = new URL(`https://${ASSETS_DOMAIN}/spaces/${spaceId}/environments/${environment}/${id}/${version}/${name}`); res.redirect(url.href); } catch (e) { - console.log('error in getting asset', e); + next(e); } }, ); @@ -53,7 +53,7 @@ routes.use( /* Gets image file. */ routes.use( '/:spaceName/:environment/images/:id/:version/:name', - (req, res) => { + (req, res, next) => { try { const { environment, @@ -69,7 +69,7 @@ routes.use( const url = new URL(`https://${IMAGES_DOMAIN}/spaces/${spaceId}/environments/${environment}/${id}/${version}/${name}`); res.redirect(url.href); } catch (e) { - console.log('error in getting image', e); + next(e); } }, ); diff --git a/src/shared/components/Looker/index.jsx b/src/shared/components/Looker/index.jsx index 773e7f6cfb..c24c9710e3 100644 --- a/src/shared/components/Looker/index.jsx +++ b/src/shared/components/Looker/index.jsx @@ -158,16 +158,25 @@ export default class Looker extends Component { } const { styles } = c; let value = ''; + let cellKey; if (limit <= 0 || i < limit) { - if (typeof record[prop] === 'string') { + // Special-case a column with property "rank" to display + // the current row index (1-based). This mirrors the old + // Looker behavior and ensures alignment even when the + // underlying data does not include a rank field. + if ((prop || '').toString().toLowerCase() === 'rank') { + value = i + 1; + cellKey = `${prop}-index-${i}`; + } else if (typeof record[prop] === 'string') { value = record[prop]; - } - if (typeof record[prop] === 'number') { + cellKey = record[prop]; + } else if (typeof record[prop] === 'number') { value = record[prop].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); + cellKey = record[prop]; } } return value ? ( - + {memberLinks ? ( {value} diff --git a/src/shared/components/ProfileBadgesPage/index.jsx b/src/shared/components/ProfileBadgesPage/index.jsx index fd69254b3a..e1015a16d0 100644 --- a/src/shared/components/ProfileBadgesPage/index.jsx +++ b/src/shared/components/ProfileBadgesPage/index.jsx @@ -76,7 +76,7 @@ const ProfileBadges = ({ badges, handleParam }) => { }
-
+
{title}
diff --git a/src/shared/containers/SmartLooker.jsx b/src/shared/containers/SmartLooker.jsx index 2e19eef8ea..b1845e1d84 100644 --- a/src/shared/containers/SmartLooker.jsx +++ b/src/shared/containers/SmartLooker.jsx @@ -42,7 +42,7 @@ function parseTableDef(table) { // This allows us to support the community/statistics page without knowing all // Looker IDs ahead of time, by recognizing common property/table patterns. function inferFromProps(props) { - const { property, table } = props; + const { property, table, render } = props; const cols = parseTableDef(table); // Header metrics (single value) @@ -60,8 +60,14 @@ function inferFromProps(props) { transform: data => [{ 'challenge.count': Number(data.count) || 0 }], }; } - // Total prizes: common property names are often 'total' or contain 'total' - if (prop === 'total' || prop.includes('total')) { + // Total prizes: property names may vary. Cover common cases. + if ( + prop === 'total' + || prop.includes('total') + || prop.includes('prize') + || prop.includes('payment') + || prop.includes('amount') + ) { return { path: '/statistics/general/total-prizes', transform: data => [{ [property]: data.total }], @@ -94,20 +100,39 @@ function inferFromProps(props) { }; } - // First place by country: expect country + a first_place count field - if (hasCountry && (hasFirstPlace || hasUserCount)) { - // Count column property: prefer one mentioning first_place, else user.count - const valueProp = propList.find(p => p.toLowerCase().includes('first_place')) - || propList.find(p => p.toLowerCase() === 'user.count') - || 'first_place_count'; + // First place by country: expect country + a numeric count field. + // In Contentful, the numeric column property has varied, so fall back to + // the first non-country, non-rank property when we don't explicitly see + // `first_place` or `user.count`. + if (hasCountry && (hasFirstPlace || (!hasUserCount && !hasCopilot && !hasReview))) { + // Prefer an explicit first_place column, else user.count, else the first + // non-country/non-rank column as the value property. + let valueProp = propList.find(p => p.toLowerCase().includes('first_place')) + || propList.find(p => p.toLowerCase() === 'user.count'); + if (!valueProp) { + valueProp = propList.find(p => ( + p + && !p.toLowerCase().includes('country') + && p.toLowerCase() !== 'rank' + )) || 'first_place_count'; + } + const nameProp = propList.find(p => p.toLowerCase().includes('country')) || 'country.country_name'; - return { - path: '/statistics/general/first-place-by-country', - transform: rows => rows.map(r => ({ - [nameProp]: r.country_code, - [valueProp]: Number(r.first_place_count) || 0, - })), - }; + + // Heuristic guard: if this looks like the Countries Represented table + // (i.e. valueProp resolved to user.count) let the dedicated block above + // handle it instead of mapping to first-place. + if (valueProp && valueProp.toLowerCase() === 'user.count') { + // Do nothing; countries-represented case above will match. + } else { + return { + path: '/statistics/general/first-place-by-country', + transform: rows => rows.map(r => ({ + [nameProp]: r.country_code, + [valueProp]: Number(r.first_place_count) || 0, + })), + }; + } } // Copiloted challenges: expect copilot handle + challenge.count @@ -138,6 +163,31 @@ function inferFromProps(props) { } } + // Render-based single value (e.g. used to prefix with currency symbol) + // Try to infer the data source by inspecting referenced fields in the render function. + if (!property && !cols && render) { + try { + const r = String(render).replace(/&q;/g, '"').replace(/'/g, '"'); + // Find all occurrences of data[0]["prop"] + const matches = Array.from(r.matchAll(/data\s*\[\s*0\s*\]\s*\[\s*"([^"]+)"\s*\]/g)); + const referenced = matches.map(m => (m && m[1] ? m[1] : '')); + const hasTotalPrizes = referenced.some((p) => { + const pl = String(p).toLowerCase(); + return pl.includes('total') || pl.includes('prize') || pl.includes('payment') || pl.includes('amount'); + }); + if (hasTotalPrizes) { + // Use the first referenced property as the key expected by the render function + const key = referenced.find(p => p) || 'total'; + return { + path: '/statistics/general/total-prizes', + transform: data => [{ [key]: data.total }], + }; + } + } catch (e) { + // swallow and fall back to legacy looker + } + } + return null; } From e553b93969fc2d6e1d92dae7b67cc5ea317d4901 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Thu, 9 Oct 2025 11:59:41 +1100 Subject: [PATCH 42/76] Fix dollar formatting to match past Looker report --- src/shared/components/Looker/index.jsx | 36 +++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/src/shared/components/Looker/index.jsx b/src/shared/components/Looker/index.jsx index c24c9710e3..9aac606e27 100644 --- a/src/shared/components/Looker/index.jsx +++ b/src/shared/components/Looker/index.jsx @@ -26,6 +26,36 @@ import cn from 'classnames'; import { Scrollbars } from 'react-custom-scrollbars'; import './style.scss'; +// Format strings that represent numeric values (optionally with a leading +// '$' and/or a leading '-') by inserting thousands separators while +// preserving any decimal part. Non-numeric strings are returned as-is. +function formatMaybeNumericString(s) { + if (typeof s !== 'string') return s; + const str = s.trim(); + if (!str) return s; + if (str.includes(',')) return s; // assume already formatted + + let prefix = ''; + let sign = ''; + let numberPart = str; + + if (numberPart[0] === '$') { + prefix = '$'; + numberPart = numberPart.slice(1); + } + if (numberPart[0] === '-') { + sign = '-'; + numberPart = numberPart.slice(1); + } + + if (!/^\d+(\.\d+)?$/.test(numberPart)) return s; + + const [intPart, decPart] = numberPart.split('.'); + const intWithCommas = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ','); + const dec = decPart ? `.${decPart}` : ''; + return `${prefix}${sign}${intWithCommas}${dec}`; +} + export default class Looker extends Component { constructor(props) { super(props); @@ -77,7 +107,7 @@ export default class Looker extends Component { if (property) { if (lookerData.length > 0 && lookerData[0][property]) { if (typeof lookerData[0][property] === 'string') { - return lookerData[0][property]; + return formatMaybeNumericString(lookerData[0][property]); } if (typeof lookerData[0][property] === 'number') { return lookerData[0][property].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); @@ -168,7 +198,7 @@ export default class Looker extends Component { value = i + 1; cellKey = `${prop}-index-${i}`; } else if (typeof record[prop] === 'string') { - value = record[prop]; + value = formatMaybeNumericString(record[prop]); cellKey = record[prop]; } else if (typeof record[prop] === 'number') { value = record[prop].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); @@ -219,7 +249,7 @@ export default class Looker extends Component { try { const retValue = f(lookerData); if (typeof retValue === 'string') { - return retValue; + return formatMaybeNumericString(retValue); } if (typeof retValue === 'number') { return retValue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); From daa231882b14ee18fda25fe4976ee65683630a02 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Thu, 9 Oct 2025 12:27:17 +1100 Subject: [PATCH 43/76] More statistics reports convert to reports-api-v6 --- src/shared/containers/SmartLooker.jsx | 142 +++++++++++++++++++++++++- 1 file changed, 141 insertions(+), 1 deletion(-) diff --git a/src/shared/containers/SmartLooker.jsx b/src/shared/containers/SmartLooker.jsx index b1845e1d84..83fc80cd06 100644 --- a/src/shared/containers/SmartLooker.jsx +++ b/src/shared/containers/SmartLooker.jsx @@ -44,6 +44,9 @@ function parseTableDef(table) { function inferFromProps(props) { const { property, table, render } = props; const cols = parseTableDef(table); + const headers = (cols || []).map(c => String(c.headerName || '')); + const lowerHeaders = headers.map(h => h.toLowerCase()); + const lowerProps = (cols || []).map(c => String(c.property || '').toLowerCase()); // Header metrics (single value) if (property && !cols) { @@ -86,6 +89,127 @@ function inferFromProps(props) { const hasUserCount = propList.some(p => p.toLowerCase() === 'user.count'); const hasFirstPlace = propList.some(p => p.toLowerCase().includes('first_place')); + // Deprecated/removed options: ignore Wireframe/LUX/RUX tiles by returning + // an empty dataset mapping (no fetch, just empty rows rendered if any). + const removedOption = lowerHeaders.some(h => ( + h.includes('wireframe wins') + || h.includes('lux 1st place wins') + || h.includes('lux placements') + || h.includes('rux 1st place wins') + || h.includes('rux placements') + )); + if (removedOption) { + return { + path: null, + transform: () => ([]), + }; + } + + // Design tab: UI Design Wins (by member) + const looksLikeUiDesignWins = ( + (hasHandle) + && ( + lowerHeaders.some(h => (h.includes('ui') && h.includes('win'))) // header mentions UI + wins + || lowerProps.some(p => (p.includes('ui') && p.includes('win'))) + || lowerHeaders.some(h => (h.includes('design') && h.includes('wins'))) + ) + ); + if (looksLikeUiDesignWins) { + const nameProp = propList.find(p => p.toLowerCase().includes('handle')) || 'handle'; + // choose a numeric/value column property name from the table definition + const valueProp = propList.find(p => p.toLowerCase().includes('win')) + || propList.find(p => (p && p.toLowerCase() !== 'rank' && !p.toLowerCase().includes('handle'))) + || 'wins'; + return { + path: '/statistics/design/ui-design-wins', + transform: rows => rows.map(r => ({ + [nameProp]: r.handle, + [valueProp]: Number(r.wins_count) || 0, + })), + }; + } + + // Design tab: F2F Wins (by member) + const looksLikeF2FWins = ( + hasHandle && ( + lowerHeaders.some(h => (h.includes('f2f') && h.includes('win'))) + || lowerProps.some(p => (p.includes('f2f') && p.includes('win'))) + ) + ); + if (looksLikeF2FWins) { + const nameProp = propList.find(p => p.toLowerCase().includes('handle')) || 'handle'; + const valueProp = propList.find(p => p.toLowerCase().includes('win')) + || propList.find(p => (p && p.toLowerCase() !== 'rank' && !p.toLowerCase().includes('handle'))) + || 'wins'; + return { + path: '/statistics/design/f2f-wins', + transform: rows => rows.map(r => ({ + [nameProp]: r.handle, + [valueProp]: Number(r.wins_count) || 0, + })), + }; + } + + // Design tab: First Time Submitters (by date) + const looksLikeFirstTimeSubmitters = ( + hasHandle && ( + lowerHeaders.some(h => (h.includes('first time') || h.includes('first-time'))) + || lowerProps.some(p => p.includes('first') && p.includes('submission')) + || lowerProps.some(p => p.includes('date')) + ) + ); + if (looksLikeFirstTimeSubmitters) { + const nameProp = propList.find(p => p.toLowerCase().includes('handle')) || 'handle'; + const dateProp = propList.find(p => p.toLowerCase().includes('date')) + || propList.find(p => (p && p.toLowerCase() !== 'rank' && !p.toLowerCase().includes('handle'))) + || 'first_submission_date'; + return { + path: '/statistics/design/first-time-submitters', + transform: rows => rows.map(r => ({ + [nameProp]: r.handle, + [dateProp]: r.first_submission_date, + })), + }; + } + + // Design tab: Countries Represented (Design submitters) + const looksLikeDesignCountries = ( + lowerHeaders.some(h => h.includes('countries represented')) + || (hasCountry && hasUserCount && lowerHeaders.some(h => h.includes('design'))) + ); + if (looksLikeDesignCountries) { + return { + path: '/statistics/design/countries-represented', + transform: rows => rows.map(r => ({ + [propList.find(p => p.toLowerCase().includes('country')) || 'country.country_name']: + r.country_code, + [propList.find(p => p.toLowerCase() === 'user.count') || 'user.count']: + Number(r.members_count) || 0, + })), + }; + } + + // Design tab: 1st Place Finishes by Country + const looksLikeDesignFirstPlace = ( + hasCountry && ( + lowerHeaders.some(h => (h.includes('1st place') || h.includes('first place'))) + || hasFirstPlace + ) + ); + if (looksLikeDesignFirstPlace) { + const nameProp = propList.find(p => p.toLowerCase().includes('country')) || 'country.country_name'; + const valueProp = propList.find(p => p.toLowerCase().includes('first_place')) + || propList.find(p => (p && p.toLowerCase() !== 'rank' && !p.toLowerCase().includes('country'))) + || 'first_place_count'; + return { + path: '/statistics/design/first-place-by-country', + transform: rows => rows.map(r => ({ + [nameProp]: r.country_code, + [valueProp]: Number(r.first_place_count) || 0, + })), + }; + } + // Countries represented: expect country + user.count if (hasCountry && hasUserCount) { return { @@ -196,6 +320,7 @@ export default function SmartLooker(props) { const direct = LOOKER_TO_REPORTS_MAP[lookerId]; const inferred = inferFromProps(props); const reportsPath = direct || (inferred && inferred.path); + const blocked = Boolean(inferred && Object.prototype.hasOwnProperty.call(inferred, 'path') && inferred.path === null); const [state, setState] = React.useState({ loading: !!reportsPath, @@ -206,6 +331,21 @@ export default function SmartLooker(props) { React.useEffect(() => { let cancelled = false; async function load() { + if (blocked) { + // Intentionally block legacy Looker usage and render empty data + // for removed/unsupported tiles. + const transformed = (inferred && typeof inferred.transform === 'function') + ? inferred.transform([]) + : []; + if (!cancelled) { + setState({ + loading: false, + error: null, + lookerInfo: { lookerData: transformed }, + }); + } + return; + } if (!reportsPath) return; setState(s => ({ ...s, loading: true, error: null })); try { @@ -231,7 +371,7 @@ export default function SmartLooker(props) { return () => { cancelled = true; }; }, [lookerId, reportsPath]); - if (!reportsPath) { + if (!reportsPath && !blocked) { // Fall back to legacy behavior for non-mapped lookerIds return ( From 7502ff75eca80682022dd0a3a0dcf065b06d73e6 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Thu, 9 Oct 2025 15:14:53 +1100 Subject: [PATCH 44/76] More reports-api-v6 integration --- src/shared/containers/SmartLooker.jsx | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/shared/containers/SmartLooker.jsx b/src/shared/containers/SmartLooker.jsx index 83fc80cd06..b58061dd05 100644 --- a/src/shared/containers/SmartLooker.jsx +++ b/src/shared/containers/SmartLooker.jsx @@ -331,21 +331,7 @@ export default function SmartLooker(props) { React.useEffect(() => { let cancelled = false; async function load() { - if (blocked) { - // Intentionally block legacy Looker usage and render empty data - // for removed/unsupported tiles. - const transformed = (inferred && typeof inferred.transform === 'function') - ? inferred.transform([]) - : []; - if (!cancelled) { - setState({ - loading: false, - error: null, - lookerInfo: { lookerData: transformed }, - }); - } - return; - } + if (blocked) return; // Hide removed/unsupported tiles entirely if (!reportsPath) return; setState(s => ({ ...s, loading: true, error: null })); try { @@ -371,6 +357,8 @@ export default function SmartLooker(props) { return () => { cancelled = true; }; }, [lookerId, reportsPath]); + if (blocked) return null; // Do not render removed options + if (!reportsPath && !blocked) { // Fall back to legacy behavior for non-mapped lookerIds return ( From 1d9f3d688f6a6b5f8bc544566aa9e9e91a5fd015 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Thu, 9 Oct 2025 16:52:25 +1100 Subject: [PATCH 45/76] Better mapping of looker report ID to reports API v6 endpoints --- src/shared/containers/SmartLooker.jsx | 204 +++++++++++++++++++++++--- 1 file changed, 184 insertions(+), 20 deletions(-) diff --git a/src/shared/containers/SmartLooker.jsx b/src/shared/containers/SmartLooker.jsx index b58061dd05..4682087397 100644 --- a/src/shared/containers/SmartLooker.jsx +++ b/src/shared/containers/SmartLooker.jsx @@ -11,18 +11,6 @@ import { config } from 'topcoder-react-utils'; import Looker from 'components/Looker'; import OriginalLookerContainer from 'containers/Looker'; -// Direct mappings from known Looker IDs to reports-api-v6 endpoints. -// These are the specific Looker tiles used by the /community/statistics page -// that we want to source from the new reports service instead of legacy Looker. -const LOOKER_TO_REPORTS_MAP = { - // SRM datasets (static JSON served by reports-api-v6) - 1653: '/statistics/srm/top-rated', - 1657: '/statistics/srm/country-ratings', - 1654: '/statistics/srm/competitions-count', - // NOTE: Additional general statistics Looker IDs can be added here as they - // are identified in content (e.g. header metrics and general tab tables). -}; - // Safely parse a possible JSON string table definition into an array of columns. function parseTableDef(table) { if (!table && table !== '') return null; @@ -38,6 +26,164 @@ function parseTableDef(table) { } } +// Direct mappings from known Looker IDs to reports-api-v6 endpoints. +// These are the specific Looker tiles used by the /community/statistics page +// that we want to source from the new reports service instead of legacy Looker. +const LOOKER_TO_REPORTS_MAP = { + // SRM datasets (static JSON served by reports-api-v6) + 1653: '/statistics/srm/top-rated', + 1657: '/statistics/srm/country-ratings', + 1654: '/statistics/srm/competitions-count', + // Marathon Match datasets (static JSON served by reports-api-v6) + 1652: '/statistics/mm/top-rated', + 1658: '/statistics/mm/country-ratings', + 1656: '/statistics/mm/top-10-finishes', + 1655: '/statistics/mm/competitions-count', + // General tab datasets (fetched from DB via reports-api-v6) + // Countries Represented + 1127: (props) => { + const cols = parseTableDef(props.table) || []; + const propList = cols.map(c => String(c.property || '')); + const countryProp = propList.find(p => p.toLowerCase().includes('country')) || 'country.country_name'; + const countProp = propList.find(p => p.toLowerCase() === 'user.count') || 'user.count'; + return { + path: '/statistics/general/countries-represented', + transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ + [countryProp]: r.country_code, + [countProp]: Number(r.members_count) || 0, + })), + }; + }, + // 1st place by country + 1149: (props) => { + const cols = parseTableDef(props.table) || []; + const propList = cols.map(c => String(c.property || '')); + const countryProp = propList.find(p => p.toLowerCase().includes('country')) || 'country.country_name'; + let valueProp = propList.find(p => p.toLowerCase().includes('first_place')) + || propList.find(p => p.toLowerCase() === 'user.count'); + if (!valueProp) { + valueProp = propList.find(p => (p && !p.toLowerCase().includes('country') && p.toLowerCase() !== 'rank')) + || 'first_place_count'; + } + return { + path: '/statistics/general/first-place-by-country', + transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ + [countryProp]: r.country_code, + [valueProp]: Number(r.first_place_count) || 0, + })), + }; + }, + // Copiloted challenges (by member) + 1146: (props) => { + const cols = parseTableDef(props.table) || []; + const propList = cols.map(c => String(c.property || '')); + const handleProp = propList.find(p => p.toLowerCase().includes('copilot')) + || propList.find(p => p.toLowerCase().includes('handle')) + || 'copilot.handle'; + const countProp = 'challenge.count'; + return { + path: '/statistics/general/copiloted-challenges', + transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ + [handleProp]: r.handle, + [countProp]: Number(r.copiloted_challenges) || 0, + })), + }; + }, + // Number of Reviews (by member) + 1150: (props) => { + const cols = parseTableDef(props.table) || []; + const propList = cols.map(c => String(c.property || '')); + const handleProp = propList.find(p => p.toLowerCase().includes('handle')) || 'handle'; + const valueProp = propList.find(p => p.toLowerCase().includes('review')) || 'review.count'; + return { + path: '/statistics/general/reviews-by-member', + transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ + [handleProp]: r.handle, + [valueProp]: Number(r.review_count) || 0, + })), + }; + }, + // DESIGN tab datasets (fetched from DB via reports-api-v6) + // UI Design Wins (by member) + 1138: (props) => { + const cols = parseTableDef(props.table) || []; + const propList = cols.map(c => String(c.property || '')); + const nameProp = propList.find(p => p.toLowerCase().includes('handle')) || 'handle'; + const valueProp = propList.find(p => p.toLowerCase().includes('win')) + || propList.find(p => (p && p.toLowerCase() !== 'rank' && !p.toLowerCase().includes('handle'))) + || 'wins'; + return { + path: '/statistics/design/ui-design-wins', + transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ + [nameProp]: r.handle, + [valueProp]: Number(r.wins_count) || 0, + })), + }; + }, + // Design F2F Wins (by member) + 1141: (props) => { + const cols = parseTableDef(props.table) || []; + const propList = cols.map(c => String(c.property || '')); + const nameProp = propList.find(p => p.toLowerCase().includes('handle')) || 'handle'; + const valueProp = propList.find(p => p.toLowerCase().includes('win')) + || propList.find(p => (p && p.toLowerCase() !== 'rank' && !p.toLowerCase().includes('handle'))) + || 'wins'; + return { + path: '/statistics/design/f2f-wins', + transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ + [nameProp]: r.handle, + [valueProp]: Number(r.wins_count) || 0, + })), + }; + }, + // 1st Place Finishes (by country) + 1135: (props) => { + const cols = parseTableDef(props.table) || []; + const propList = cols.map(c => String(c.property || '')); + const nameProp = propList.find(p => p.toLowerCase().includes('country')) || 'country.country_name'; + const valueProp = propList.find(p => p.toLowerCase().includes('first_place')) + || propList.find(p => (p && p.toLowerCase() !== 'rank' && !p.toLowerCase().includes('country'))) + || 'first_place_count'; + return { + path: '/statistics/design/first-place-by-country', + transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ + [nameProp]: r.country_code, + [valueProp]: Number(r.first_place_count) || 0, + })), + }; + }, + // First Time Submitters (Design) + 1178: (props) => { + const cols = parseTableDef(props.table) || []; + const propList = cols.map(c => String(c.property || '')); + const nameProp = propList.find(p => p.toLowerCase().includes('handle')) || 'handle'; + const dateProp = propList.find(p => p.toLowerCase().includes('date')) + || propList.find(p => (p && p.toLowerCase() !== 'rank' && !p.toLowerCase().includes('handle'))) + || 'first_submission_date'; + return { + path: '/statistics/design/first-time-submitters', + transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ + [nameProp]: r.handle, + [dateProp]: r.first_submission_date, + })), + }; + }, + // Countries Represented (Design) + 1136: (props) => { + const cols = parseTableDef(props.table) || []; + const propList = cols.map(c => String(c.property || '')); + return { + path: '/statistics/design/countries-represented', + transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ + [propList.find(p => p.toLowerCase().includes('country')) || 'country.country_name']: + r.country_code, + [propList.find(p => p.toLowerCase() === 'user.count') || 'user.count']: + Number(r.members_count) || 0, + })), + }; + }, +}; + // Infer a reports-api-v6 endpoint and a transformer based on Looker props. // This allows us to support the community/statistics page without knowing all // Looker IDs ahead of time, by recognizing common property/table patterns. @@ -89,14 +235,17 @@ function inferFromProps(props) { const hasUserCount = propList.some(p => p.toLowerCase() === 'user.count'); const hasFirstPlace = propList.some(p => p.toLowerCase().includes('first_place')); - // Deprecated/removed options: ignore Wireframe/LUX/RUX tiles by returning - // an empty dataset mapping (no fetch, just empty rows rendered if any). const removedOption = lowerHeaders.some(h => ( h.includes('wireframe wins') || h.includes('lux 1st place wins') || h.includes('lux placements') || h.includes('rux 1st place wins') || h.includes('rux placements') + || (h.includes('wireframe') && h.includes('win')) + || (h.includes('lux') && (h.includes('1st') || h.includes('first')) && (h.includes('win'))) + || (h.includes('lux') && h.includes('placement')) + || (h.includes('rux') && (h.includes('1st') || h.includes('first')) && (h.includes('win'))) + || (h.includes('rux') && h.includes('placement')) )); if (removedOption) { return { @@ -105,6 +254,8 @@ function inferFromProps(props) { }; } + // MM tiles are handled via direct Looker ID mappings. + // Design tab: UI Design Wins (by member) const looksLikeUiDesignWins = ( (hasHandle) @@ -130,10 +281,17 @@ function inferFromProps(props) { } // Design tab: F2F Wins (by member) + const containsF2FSynonym = s => ( + s.includes('f2f') + || s.includes('first2finish') + || s.includes('first 2 finish') + || s.includes('first-to-finish') + || s.includes('first to finish') + ); const looksLikeF2FWins = ( hasHandle && ( - lowerHeaders.some(h => (h.includes('f2f') && h.includes('win'))) - || lowerProps.some(p => (p.includes('f2f') && p.includes('win'))) + lowerHeaders.some(h => (containsF2FSynonym(h) && (h.includes('win') || h.includes('wins') || true))) + || lowerProps.some(p => (containsF2FSynonym(p) && (p.includes('win') || p.includes('wins') || true))) ) ); if (looksLikeF2FWins) { @@ -317,9 +475,15 @@ function inferFromProps(props) { export default function SmartLooker(props) { const { lookerId } = props; - const direct = LOOKER_TO_REPORTS_MAP[lookerId]; + const directEntry = LOOKER_TO_REPORTS_MAP[lookerId]; + let directConfig = null; + if (typeof directEntry === 'function') directConfig = directEntry(props); + else if (typeof directEntry === 'string') directConfig = { path: directEntry }; + else if (directEntry && typeof directEntry === 'object') directConfig = directEntry; + const inferred = inferFromProps(props); - const reportsPath = direct || (inferred && inferred.path); + const reportsPath = (directConfig && directConfig.path) || (inferred && inferred.path); + const transformer = (directConfig && directConfig.transform) || (inferred && inferred.transform); const blocked = Boolean(inferred && Object.prototype.hasOwnProperty.call(inferred, 'path') && inferred.path === null); const [state, setState] = React.useState({ @@ -339,8 +503,8 @@ export default function SmartLooker(props) { if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); let lookerData = Array.isArray(data) ? data : [data]; - if (inferred && typeof inferred.transform === 'function') { - lookerData = inferred.transform(Array.isArray(data) ? data : data); + if (typeof transformer === 'function') { + lookerData = transformer(Array.isArray(data) ? data : data); } if (!cancelled) setState({ loading: false, error: null, lookerInfo: { lookerData } }); } catch (e) { From ab6a4058a52df5aa871a4caf7da6365612f5b50a Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Thu, 9 Oct 2025 18:42:48 +1100 Subject: [PATCH 46/76] Updates for community/statistics page --- .../components/Contentful/Tabs/Tabs.jsx | 45 ++++++++++++++----- src/shared/containers/SmartLooker.jsx | 33 +++++++++----- 2 files changed, 56 insertions(+), 22 deletions(-) diff --git a/src/shared/components/Contentful/Tabs/Tabs.jsx b/src/shared/components/Contentful/Tabs/Tabs.jsx index 4eb0541cdf..a5ac160313 100644 --- a/src/shared/components/Contentful/Tabs/Tabs.jsx +++ b/src/shared/components/Contentful/Tabs/Tabs.jsx @@ -93,27 +93,51 @@ export default class TabsItemsLoader extends Component { } = this.props; const { tabIndex, mobileTabsShow } = this.state; + // Helper to hide deprecated/unsupported statistics options + const shouldHideTab = (label) => { + if (!label) return false; + const t = String(label).toLowerCase(); + return ( + t.includes('wireframe') + || t.includes(' lux') || t.startsWith('lux') || t.includes('lux ') + || t.includes(' rux') || t.startsWith('rux') || t.includes('rux ') + ); + }; + return ( ( - { + // Convert to array to safely filter/map in a stable order + const allTabItems = _.toArray(data.entries.items); + const tabItems = allTabItems.filter(ti => !shouldHideTab(_.get(ti, 'fields.tab', ''))); + // Ensure selected index is within bounds after filtering + const safeTabIndex = Math.min(tabIndex, Math.max(0, tabItems.length - 1)); + + if (!tabItems.length) return null; + + return ( + this.setState({ tabIndex: tIndx, mobileTabsShow: false })} + onSelect={(tIndx) => this.setState({ tabIndex: tIndx, mobileTabsShow: false })} forceRenderTabPanel={forceRenderTabPanel} >
{ themeName === 'Underline box' ? (
{ - _.map(data.entries.items, tabItem => ( + _.map(tabItems, tabItem => ( - )} + ); + }} renderPlaceholder={LoadingIndicator} /> ); diff --git a/src/shared/containers/SmartLooker.jsx b/src/shared/containers/SmartLooker.jsx index 4682087397..b87213e853 100644 --- a/src/shared/containers/SmartLooker.jsx +++ b/src/shared/containers/SmartLooker.jsx @@ -10,6 +10,7 @@ import PT from 'prop-types'; import { config } from 'topcoder-react-utils'; import Looker from 'components/Looker'; import OriginalLookerContainer from 'containers/Looker'; +import { getCountryObjFromAlpha3 } from 'utils/countries'; // Safely parse a possible JSON string table definition into an array of columns. function parseTableDef(table) { @@ -26,6 +27,14 @@ function parseTableDef(table) { } } +// Convert a 3-letter country code to its English full name. +// Falls back to the original code if unknown. +function countryNameFromCode(code) { + if (!code) return code; + const obj = getCountryObjFromAlpha3(String(code).toUpperCase()); + return (obj && obj.name) ? obj.name : code; +} + // Direct mappings from known Looker IDs to reports-api-v6 endpoints. // These are the specific Looker tiles used by the /community/statistics page // that we want to source from the new reports service instead of legacy Looker. @@ -49,7 +58,7 @@ const LOOKER_TO_REPORTS_MAP = { return { path: '/statistics/general/countries-represented', transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ - [countryProp]: r.country_code, + [countryProp]: countryNameFromCode(r.country_code), [countProp]: Number(r.members_count) || 0, })), }; @@ -68,7 +77,7 @@ const LOOKER_TO_REPORTS_MAP = { return { path: '/statistics/general/first-place-by-country', transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ - [countryProp]: r.country_code, + [countryProp]: countryNameFromCode(r.country_code), [valueProp]: Number(r.first_place_count) || 0, })), }; @@ -116,7 +125,7 @@ const LOOKER_TO_REPORTS_MAP = { path: '/statistics/design/ui-design-wins', transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ [nameProp]: r.handle, - [valueProp]: Number(r.wins_count) || 0, + [valueProp]: Number((r && (r.wins_count ?? r.count)) || 0), })), }; }, @@ -132,7 +141,7 @@ const LOOKER_TO_REPORTS_MAP = { path: '/statistics/design/f2f-wins', transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ [nameProp]: r.handle, - [valueProp]: Number(r.wins_count) || 0, + [valueProp]: Number((r && (r.wins_count ?? r.count)) || 0), })), }; }, @@ -147,7 +156,7 @@ const LOOKER_TO_REPORTS_MAP = { return { path: '/statistics/design/first-place-by-country', transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ - [nameProp]: r.country_code, + [nameProp]: countryNameFromCode(r.country_code), [valueProp]: Number(r.first_place_count) || 0, })), }; @@ -176,7 +185,7 @@ const LOOKER_TO_REPORTS_MAP = { path: '/statistics/design/countries-represented', transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ [propList.find(p => p.toLowerCase().includes('country')) || 'country.country_name']: - r.country_code, + countryNameFromCode(r.country_code), [propList.find(p => p.toLowerCase() === 'user.count') || 'user.count']: Number(r.members_count) || 0, })), @@ -275,7 +284,7 @@ function inferFromProps(props) { path: '/statistics/design/ui-design-wins', transform: rows => rows.map(r => ({ [nameProp]: r.handle, - [valueProp]: Number(r.wins_count) || 0, + [valueProp]: Number((r && (r.wins_count ?? r.count)) || 0), })), }; } @@ -303,7 +312,7 @@ function inferFromProps(props) { path: '/statistics/design/f2f-wins', transform: rows => rows.map(r => ({ [nameProp]: r.handle, - [valueProp]: Number(r.wins_count) || 0, + [valueProp]: Number((r && (r.wins_count ?? r.count)) || 0), })), }; } @@ -340,7 +349,7 @@ function inferFromProps(props) { path: '/statistics/design/countries-represented', transform: rows => rows.map(r => ({ [propList.find(p => p.toLowerCase().includes('country')) || 'country.country_name']: - r.country_code, + countryNameFromCode(r.country_code), [propList.find(p => p.toLowerCase() === 'user.count') || 'user.count']: Number(r.members_count) || 0, })), @@ -362,7 +371,7 @@ function inferFromProps(props) { return { path: '/statistics/design/first-place-by-country', transform: rows => rows.map(r => ({ - [nameProp]: r.country_code, + [nameProp]: countryNameFromCode(r.country_code), [valueProp]: Number(r.first_place_count) || 0, })), }; @@ -375,7 +384,7 @@ function inferFromProps(props) { transform: rows => rows.map(r => ({ // Use whatever property the table asked for to label country [propList.find(p => p.toLowerCase().includes('country')) || 'country.country_name']: - r.country_code, + countryNameFromCode(r.country_code), [propList.find(p => p.toLowerCase() === 'user.count') || 'user.count']: Number(r.members_count) || 0, })), @@ -410,7 +419,7 @@ function inferFromProps(props) { return { path: '/statistics/general/first-place-by-country', transform: rows => rows.map(r => ({ - [nameProp]: r.country_code, + [nameProp]: countryNameFromCode(r.country_code), [valueProp]: Number(r.first_place_count) || 0, })), }; From c338035c3a0be1b60b94c93846ab9b06cbd93769 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Thu, 9 Oct 2025 19:22:48 +1100 Subject: [PATCH 47/76] Additional statistics integrations on /community/statistics --- .../components/Contentful/Tabs/Tabs.jsx | 262 +++++++++--------- src/shared/containers/SmartLooker.jsx | 227 ++++++++------- 2 files changed, 262 insertions(+), 227 deletions(-) diff --git a/src/shared/components/Contentful/Tabs/Tabs.jsx b/src/shared/components/Contentful/Tabs/Tabs.jsx index a5ac160313..a21609964a 100644 --- a/src/shared/components/Contentful/Tabs/Tabs.jsx +++ b/src/shared/components/Contentful/Tabs/Tabs.jsx @@ -93,17 +93,6 @@ export default class TabsItemsLoader extends Component { } = this.props; const { tabIndex, mobileTabsShow } = this.state; - // Helper to hide deprecated/unsupported statistics options - const shouldHideTab = (label) => { - if (!label) return false; - const t = String(label).toLowerCase(); - return ( - t.includes('wireframe') - || t.includes(' lux') || t.startsWith('lux') || t.includes('lux ') - || t.includes(' rux') || t.startsWith('rux') || t.includes('rux ') - ); - }; - return ( { - // Convert to array to safely filter/map in a stable order - const allTabItems = _.toArray(data.entries.items); - const tabItems = allTabItems.filter(ti => !shouldHideTab(_.get(ti, 'fields.tab', ''))); - // Ensure selected index is within bounds after filtering + // Convert to array to safely map in a stable order + const tabItems = _.toArray(data.entries.items); + // Ensure selected index is within bounds const safeTabIndex = Math.min(tabIndex, Math.max(0, tabItems.length - 1)); + const mobileToggleIconClass = mobileTabsShow + ? theme.tabListMobileTriggerSVGOpen + : theme.tabListMobileTriggerSVG; if (!tabItems.length) return null; return ( this.setState({ tabIndex: tIndx, mobileTabsShow: false })} - forceRenderTabPanel={forceRenderTabPanel} - > -
- { - themeName === 'Underline box' ? ( - - ) : null - } - - { - _.map(tabItems, tabItem => ( - - - - )) + + + ) : null } - -
- { - _.map(tabItems, tabItem => ( - + { - tabItem.fields.panelDescription ? ( -
- -
- ) : null - } - { - _.map(tabItem.fields.panel, panelItemLink => ( - { - const { id } = panelItemLink.sys; - const entryType = panelItem.entries.items[id].sys.contentType.sys.id; - if (entryType === 'appComponent') { - return AppComponentSwitch(panelItem.entries.items[id]); - } - if (entryType === 'contentBlock') { - return ( - - ); - } - if (entryType === 'tabs') { - const { fields } = panelItem.entries.items[id]; - return ( - - ); - } - - if (entryType === 'viewport') { - return ( - - ); - } - return null; - }} + _.map(tabItems, tabItem => ( + + > + + )) } -
- )) - } -
+ +
+ { + _.map(tabItems, tabItem => ( + + { + tabItem.fields.panelDescription ? ( +
+ +
+ ) : null + } + { + _.map(tabItem.fields.panel, panelItemLink => ( + { + const { id } = panelItemLink.sys; + const entryType = panelItem.entries.items[id].sys.contentType.sys.id; + if (entryType === 'appComponent') { + return AppComponentSwitch(panelItem.entries.items[id]); + } + if (entryType === 'contentBlock') { + return ( + + ); + } + if (entryType === 'tabs') { + const { fields } = panelItem.entries.items[id]; + return ( + + ); + } + + if (entryType === 'viewport') { + return ( + + ); + } + return null; + }} + key={tabItem.sys.id} + /> + )) + } +
+ )) + } + ); }} renderPlaceholder={LoadingIndicator} diff --git a/src/shared/containers/SmartLooker.jsx b/src/shared/containers/SmartLooker.jsx index b87213e853..afc7a46946 100644 --- a/src/shared/containers/SmartLooker.jsx +++ b/src/shared/containers/SmartLooker.jsx @@ -35,6 +35,14 @@ function countryNameFromCode(code) { return (obj && obj.name) ? obj.name : code; } +function pickDefined(...values) { + for (let i = 0; i < values.length; i += 1) { + const value = values[i]; + if (value !== undefined && value !== null) return value; + } + return undefined; +} + // Direct mappings from known Looker IDs to reports-api-v6 endpoints. // These are the specific Looker tiles used by the /community/statistics page // that we want to source from the new reports service instead of legacy Looker. @@ -63,6 +71,52 @@ const LOOKER_TO_REPORTS_MAP = { })), }; }, + // Countries Represented (Design) + 1136: (props) => { + const cols = parseTableDef(props.table) || []; + const propList = cols.map(c => String(c.property || '')); + return { + path: '/statistics/design/countries-represented', + transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ + [propList.find(p => p.toLowerCase().includes('country')) || 'country.country_name']: + countryNameFromCode(r.country_code), + [propList.find(p => p.toLowerCase() === 'user.count') || 'user.count']: + Number(r.members_count) || 0, + })), + }; + }, + // First Time Submitters (Design) + 1178: (props) => { + const cols = parseTableDef(props.table) || []; + const propList = cols.map(c => String(c.property || '')); + const nameProp = propList.find(p => p.toLowerCase().includes('handle')) || 'handle'; + const dateProp = propList.find(p => p.toLowerCase().includes('date')) + || propList.find(p => (p && p.toLowerCase() !== 'rank' && !p.toLowerCase().includes('handle'))) + || 'first_submission_date'; + return { + path: '/statistics/design/first-time-submitters', + transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ + [nameProp]: r.handle, + [dateProp]: r.first_submission_date, + })), + }; + }, + // 1st Place Finishes (by country) + 1135: (props) => { + const cols = parseTableDef(props.table) || []; + const propList = cols.map(c => String(c.property || '')); + const nameProp = propList.find(p => p.toLowerCase().includes('country')) || 'country.country_name'; + const valueProp = propList.find(p => p.toLowerCase().includes('first_place')) + || propList.find(p => (p && p.toLowerCase() !== 'rank' && !p.toLowerCase().includes('country'))) + || 'first_place_count'; + return { + path: '/statistics/design/first-place-by-country', + transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ + [nameProp]: countryNameFromCode(r.country_code), + [valueProp]: Number(r.first_place_count) || 0, + })), + }; + }, // 1st place by country 1149: (props) => { const cols = parseTableDef(props.table) || []; @@ -125,7 +179,7 @@ const LOOKER_TO_REPORTS_MAP = { path: '/statistics/design/ui-design-wins', transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ [nameProp]: r.handle, - [valueProp]: Number((r && (r.wins_count ?? r.count)) || 0), + [valueProp]: Number(pickDefined(r && r.wins_count, r && r.count, 0)) || 0, })), }; }, @@ -141,54 +195,68 @@ const LOOKER_TO_REPORTS_MAP = { path: '/statistics/design/f2f-wins', transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ [nameProp]: r.handle, - [valueProp]: Number((r && (r.wins_count ?? r.count)) || 0), + [valueProp]: Number(pickDefined(r && r.wins_count, r && r.count, 0)) || 0, })), }; }, - // 1st Place Finishes (by country) - 1135: (props) => { - const cols = parseTableDef(props.table) || []; - const propList = cols.map(c => String(c.property || '')); - const nameProp = propList.find(p => p.toLowerCase().includes('country')) || 'country.country_name'; - const valueProp = propList.find(p => p.toLowerCase().includes('first_place')) - || propList.find(p => (p && p.toLowerCase() !== 'rank' && !p.toLowerCase().includes('country'))) - || 'first_place_count'; - return { - path: '/statistics/design/first-place-by-country', - transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ - [nameProp]: countryNameFromCode(r.country_code), - [valueProp]: Number(r.first_place_count) || 0, - })), - }; - }, - // First Time Submitters (Design) - 1178: (props) => { + // LUX 1st Place Wins (Design) + 1571: (props) => { const cols = parseTableDef(props.table) || []; const propList = cols.map(c => String(c.property || '')); - const nameProp = propList.find(p => p.toLowerCase().includes('handle')) || 'handle'; - const dateProp = propList.find(p => p.toLowerCase().includes('date')) - || propList.find(p => (p && p.toLowerCase() !== 'rank' && !p.toLowerCase().includes('handle'))) - || 'first_submission_date'; + const winnerHandleProp = propList.find(p => p.toLowerCase().includes('winner_handle')) + || 'challenge_stats.winner_handle'; + const handleProp = propList.find(p => p.toLowerCase() === 'handle') || 'handle'; + const challengeCountProp = propList.find(p => p.toLowerCase().includes('challenge_stats.count')) + || 'challenge_stats.count'; + const countProp = propList.find(p => p.toLowerCase() === 'count') || 'count'; + const maxRatingProp = propList.find(p => p.toLowerCase().includes('max_rating')) + || 'member_profile_advanced.max_rating'; + const rankProp = propList.find(p => p.toLowerCase() === 'rank') || 'rank'; return { - path: '/statistics/design/first-time-submitters', - transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ - [nameProp]: r.handle, - [dateProp]: r.first_submission_date, - })), + path: '/statistics/design/lux-first-place-wins', + transform: rows => (Array.isArray(rows) ? rows : [rows]).map((r) => { + const wins = Number(pickDefined(r && r.wins_count, r && r.count, 0)) || 0; + const maxRating = pickDefined(r && r.max_rating, null); + const mapped = { + [winnerHandleProp]: r.handle, + [handleProp]: r.handle, + [challengeCountProp]: wins, + [countProp]: wins, + [maxRatingProp]: maxRating, + [rankProp]: Number(r && r.rank) || 0, + }; + return mapped; + }), }; }, - // Countries Represented (Design) - 1136: (props) => { + // LUX Placements (Design) + 1573: (props) => { const cols = parseTableDef(props.table) || []; const propList = cols.map(c => String(c.property || '')); + const winnerHandleProp = propList.find(p => p.toLowerCase().includes('winner_handle')) + || 'challenge_stats.winner_handle'; + const handleProp = propList.find(p => p.toLowerCase() === 'handle') || 'handle'; + const challengeCountProp = propList.find(p => p.toLowerCase().includes('challenge_stats.count')) + || 'challenge_stats.count'; + const countProp = propList.find(p => p.toLowerCase() === 'count') || 'count'; + const maxRatingProp = propList.find(p => p.toLowerCase().includes('max_rating')) + || 'member_profile_advanced.max_rating'; + const rankProp = propList.find(p => p.toLowerCase() === 'rank') || 'rank'; return { - path: '/statistics/design/countries-represented', - transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ - [propList.find(p => p.toLowerCase().includes('country')) || 'country.country_name']: - countryNameFromCode(r.country_code), - [propList.find(p => p.toLowerCase() === 'user.count') || 'user.count']: - Number(r.members_count) || 0, - })), + path: '/statistics/design/lux-placements', + transform: rows => (Array.isArray(rows) ? rows : [rows]).map((r) => { + const placements = Number(pickDefined(r && r.placements_count, r && r.count, 0)) || 0; + const maxRating = pickDefined(r && r.max_rating, null); + const mapped = { + [winnerHandleProp]: r.handle, + [handleProp]: r.handle, + [challengeCountProp]: placements, + [countProp]: placements, + [maxRatingProp]: maxRating, + [rankProp]: Number(r && r.rank) || 0, + }; + return mapped; + }), }; }, }; @@ -218,7 +286,6 @@ function inferFromProps(props) { transform: data => [{ 'challenge.count': Number(data.count) || 0 }], }; } - // Total prizes: property names may vary. Cover common cases. if ( prop === 'total' || prop.includes('total') @@ -244,47 +311,25 @@ function inferFromProps(props) { const hasUserCount = propList.some(p => p.toLowerCase() === 'user.count'); const hasFirstPlace = propList.some(p => p.toLowerCase().includes('first_place')); - const removedOption = lowerHeaders.some(h => ( - h.includes('wireframe wins') - || h.includes('lux 1st place wins') - || h.includes('lux placements') - || h.includes('rux 1st place wins') - || h.includes('rux placements') - || (h.includes('wireframe') && h.includes('win')) - || (h.includes('lux') && (h.includes('1st') || h.includes('first')) && (h.includes('win'))) - || (h.includes('lux') && h.includes('placement')) - || (h.includes('rux') && (h.includes('1st') || h.includes('first')) && (h.includes('win'))) - || (h.includes('rux') && h.includes('placement')) - )); - if (removedOption) { - return { - path: null, - transform: () => ([]), - }; - } - - // MM tiles are handled via direct Looker ID mappings. - // Design tab: UI Design Wins (by member) const looksLikeUiDesignWins = ( - (hasHandle) + hasHandle && ( - lowerHeaders.some(h => (h.includes('ui') && h.includes('win'))) // header mentions UI + wins + lowerHeaders.some(h => (h.includes('ui') && h.includes('win'))) || lowerProps.some(p => (p.includes('ui') && p.includes('win'))) || lowerHeaders.some(h => (h.includes('design') && h.includes('wins'))) ) ); if (looksLikeUiDesignWins) { const nameProp = propList.find(p => p.toLowerCase().includes('handle')) || 'handle'; - // choose a numeric/value column property name from the table definition const valueProp = propList.find(p => p.toLowerCase().includes('win')) || propList.find(p => (p && p.toLowerCase() !== 'rank' && !p.toLowerCase().includes('handle'))) || 'wins'; return { path: '/statistics/design/ui-design-wins', - transform: rows => rows.map(r => ({ + transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ [nameProp]: r.handle, - [valueProp]: Number((r && (r.wins_count ?? r.count)) || 0), + [valueProp]: Number(pickDefined(r && r.wins_count, r && r.count, 0)) || 0, })), }; } @@ -297,11 +342,17 @@ function inferFromProps(props) { || s.includes('first-to-finish') || s.includes('first to finish') ); + const headersMentionF2F = lowerHeaders.some(h => containsF2FSynonym(h)); + const propsMentionF2F = lowerProps.some(p => containsF2FSynonym(p)); + const headersMentionF2FWins = lowerHeaders.some( + h => containsF2FSynonym(h) && (h.includes('win') || h.includes('wins')), + ); + const propsMentionF2FWins = lowerProps.some( + p => containsF2FSynonym(p) && (p.includes('win') || p.includes('wins')), + ); const looksLikeF2FWins = ( - hasHandle && ( - lowerHeaders.some(h => (containsF2FSynonym(h) && (h.includes('win') || h.includes('wins') || true))) - || lowerProps.some(p => (containsF2FSynonym(p) && (p.includes('win') || p.includes('wins') || true))) - ) + hasHandle + && (headersMentionF2FWins || propsMentionF2FWins || headersMentionF2F || propsMentionF2F) ); if (looksLikeF2FWins) { const nameProp = propList.find(p => p.toLowerCase().includes('handle')) || 'handle'; @@ -310,9 +361,9 @@ function inferFromProps(props) { || 'wins'; return { path: '/statistics/design/f2f-wins', - transform: rows => rows.map(r => ({ + transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ [nameProp]: r.handle, - [valueProp]: Number((r && (r.wins_count ?? r.count)) || 0), + [valueProp]: Number(pickDefined(r && r.wins_count, r && r.count, 0)) || 0, })), }; } @@ -332,7 +383,7 @@ function inferFromProps(props) { || 'first_submission_date'; return { path: '/statistics/design/first-time-submitters', - transform: rows => rows.map(r => ({ + transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ [nameProp]: r.handle, [dateProp]: r.first_submission_date, })), @@ -347,7 +398,7 @@ function inferFromProps(props) { if (looksLikeDesignCountries) { return { path: '/statistics/design/countries-represented', - transform: rows => rows.map(r => ({ + transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ [propList.find(p => p.toLowerCase().includes('country')) || 'country.country_name']: countryNameFromCode(r.country_code), [propList.find(p => p.toLowerCase() === 'user.count') || 'user.count']: @@ -370,7 +421,7 @@ function inferFromProps(props) { || 'first_place_count'; return { path: '/statistics/design/first-place-by-country', - transform: rows => rows.map(r => ({ + transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ [nameProp]: countryNameFromCode(r.country_code), [valueProp]: Number(r.first_place_count) || 0, })), @@ -381,8 +432,7 @@ function inferFromProps(props) { if (hasCountry && hasUserCount) { return { path: '/statistics/general/countries-represented', - transform: rows => rows.map(r => ({ - // Use whatever property the table asked for to label country + transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ [propList.find(p => p.toLowerCase().includes('country')) || 'country.country_name']: countryNameFromCode(r.country_code), [propList.find(p => p.toLowerCase() === 'user.count') || 'user.count']: @@ -392,12 +442,7 @@ function inferFromProps(props) { } // First place by country: expect country + a numeric count field. - // In Contentful, the numeric column property has varied, so fall back to - // the first non-country, non-rank property when we don't explicitly see - // `first_place` or `user.count`. if (hasCountry && (hasFirstPlace || (!hasUserCount && !hasCopilot && !hasReview))) { - // Prefer an explicit first_place column, else user.count, else the first - // non-country/non-rank column as the value property. let valueProp = propList.find(p => p.toLowerCase().includes('first_place')) || propList.find(p => p.toLowerCase() === 'user.count'); if (!valueProp) { @@ -410,15 +455,12 @@ function inferFromProps(props) { const nameProp = propList.find(p => p.toLowerCase().includes('country')) || 'country.country_name'; - // Heuristic guard: if this looks like the Countries Represented table - // (i.e. valueProp resolved to user.count) let the dedicated block above - // handle it instead of mapping to first-place. if (valueProp && valueProp.toLowerCase() === 'user.count') { // Do nothing; countries-represented case above will match. } else { return { path: '/statistics/general/first-place-by-country', - transform: rows => rows.map(r => ({ + transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ [nameProp]: countryNameFromCode(r.country_code), [valueProp]: Number(r.first_place_count) || 0, })), @@ -433,7 +475,7 @@ function inferFromProps(props) { || 'copilot.handle'; return { path: '/statistics/general/copiloted-challenges', - transform: rows => rows.map(r => ({ + transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ [nameProp]: r.handle, 'challenge.count': Number(r.copiloted_challenges) || 0, })), @@ -446,7 +488,7 @@ function inferFromProps(props) { const valueProp = propList.find(p => p.toLowerCase().includes('review')) || 'review.count'; return { path: '/statistics/general/reviews-by-member', - transform: rows => rows.map(r => ({ + transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ [nameProp]: r.handle, [valueProp]: Number(r.review_count) || 0, })), @@ -455,11 +497,9 @@ function inferFromProps(props) { } // Render-based single value (e.g. used to prefix with currency symbol) - // Try to infer the data source by inspecting referenced fields in the render function. if (!property && !cols && render) { try { const r = String(render).replace(/&q;/g, '"').replace(/'/g, '"'); - // Find all occurrences of data[0]["prop"] const matches = Array.from(r.matchAll(/data\s*\[\s*0\s*\]\s*\[\s*"([^"]+)"\s*\]/g)); const referenced = matches.map(m => (m && m[1] ? m[1] : '')); const hasTotalPrizes = referenced.some((p) => { @@ -467,7 +507,6 @@ function inferFromProps(props) { return pl.includes('total') || pl.includes('prize') || pl.includes('payment') || pl.includes('amount'); }); if (hasTotalPrizes) { - // Use the first referenced property as the key expected by the render function const key = referenced.find(p => p) || 'total'; return { path: '/statistics/general/total-prizes', @@ -493,7 +532,6 @@ export default function SmartLooker(props) { const inferred = inferFromProps(props); const reportsPath = (directConfig && directConfig.path) || (inferred && inferred.path); const transformer = (directConfig && directConfig.transform) || (inferred && inferred.transform); - const blocked = Boolean(inferred && Object.prototype.hasOwnProperty.call(inferred, 'path') && inferred.path === null); const [state, setState] = React.useState({ loading: !!reportsPath, @@ -504,7 +542,6 @@ export default function SmartLooker(props) { React.useEffect(() => { let cancelled = false; async function load() { - if (blocked) return; // Hide removed/unsupported tiles entirely if (!reportsPath) return; setState(s => ({ ...s, loading: true, error: null })); try { @@ -513,7 +550,7 @@ export default function SmartLooker(props) { const data = await res.json(); let lookerData = Array.isArray(data) ? data : [data]; if (typeof transformer === 'function') { - lookerData = transformer(Array.isArray(data) ? data : data); + lookerData = transformer(Array.isArray(data) ? data : [data]); } if (!cancelled) setState({ loading: false, error: null, lookerInfo: { lookerData } }); } catch (e) { @@ -530,9 +567,7 @@ export default function SmartLooker(props) { return () => { cancelled = true; }; }, [lookerId, reportsPath]); - if (blocked) return null; // Do not render removed options - - if (!reportsPath && !blocked) { + if (!reportsPath) { // Fall back to legacy behavior for non-mapped lookerIds return ( From 0bfc094f5ebdbf73c79ebc0c42c49b6b1585f7fe Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Thu, 9 Oct 2025 19:49:20 +0530 Subject: [PATCH 48/76] Show terms for challenge even if legacyId absent --- .../containers/challenge-detail/index.jsx | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/shared/containers/challenge-detail/index.jsx b/src/shared/containers/challenge-detail/index.jsx index 89c30e0c70..280639a4ff 100644 --- a/src/shared/containers/challenge-detail/index.jsx +++ b/src/shared/containers/challenge-detail/index.jsx @@ -414,7 +414,6 @@ class ChallengeDetailPageContainer extends React.Component { const { legacy, - legacyId, status, phases, metadata, @@ -695,17 +694,17 @@ class ChallengeDetailPageContainer extends React.Component { ) }
- {legacyId && ( - { - registerForChallenge(auth, challengeId); - }} - /> - )} + + { + registerForChallenge(auth, challengeId); + }} + /> + {showSecurityReminder && ( this.setState({ showSecurityReminder: false })} From c34e63e12c7290c35f9d642ea0b534fce7f8472e Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Fri, 10 Oct 2025 07:41:35 +1100 Subject: [PATCH 49/76] Minor fixes for design options on statistics page --- src/shared/containers/SmartLooker.jsx | 62 ++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/src/shared/containers/SmartLooker.jsx b/src/shared/containers/SmartLooker.jsx index afc7a46946..4536bd98fd 100644 --- a/src/shared/containers/SmartLooker.jsx +++ b/src/shared/containers/SmartLooker.jsx @@ -230,7 +230,7 @@ const LOOKER_TO_REPORTS_MAP = { }; }, // LUX Placements (Design) - 1573: (props) => { + 1572: (props) => { const cols = parseTableDef(props.table) || []; const propList = cols.map(c => String(c.property || '')); const winnerHandleProp = propList.find(p => p.toLowerCase().includes('winner_handle')) @@ -259,6 +259,66 @@ const LOOKER_TO_REPORTS_MAP = { }), }; }, + // RUX 1st Place Wins (Design) + 1573: (props) => { + const cols = parseTableDef(props.table) || []; + const propList = cols.map(c => String(c.property || '')); + const winnerHandleProp = propList.find(p => p.toLowerCase().includes('winner_handle')) + || 'challenge_stats.winner_handle'; + const handleProp = propList.find(p => p.toLowerCase() === 'handle') || 'handle'; + const challengeCountProp = propList.find(p => p.toLowerCase().includes('challenge_stats.count')) + || 'challenge_stats.count'; + const countProp = propList.find(p => p.toLowerCase() === 'count') || 'count'; + const maxRatingProp = propList.find(p => p.toLowerCase().includes('max_rating')) + || 'member_profile_advanced.max_rating'; + const rankProp = propList.find(p => p.toLowerCase() === 'rank') || 'rank'; + return { + path: '/statistics/design/rux-first-place-wins', + transform: rows => (Array.isArray(rows) ? rows : [rows]).map((r) => { + const wins = Number(pickDefined(r && r.wins_count, r && r.count, 0)) || 0; + const maxRating = pickDefined(r && r.max_rating, null); + const mapped = { + [winnerHandleProp]: r.handle, + [handleProp]: r.handle, + [challengeCountProp]: wins, + [countProp]: wins, + [maxRatingProp]: maxRating, + [rankProp]: Number(r && r.rank) || 0, + }; + return mapped; + }), + }; + }, + // RUX Placements (Design) + 1574: (props) => { + const cols = parseTableDef(props.table) || []; + const propList = cols.map(c => String(c.property || '')); + const winnerHandleProp = propList.find(p => p.toLowerCase().includes('winner_handle')) + || 'challenge_stats.winner_handle'; + const handleProp = propList.find(p => p.toLowerCase() === 'handle') || 'handle'; + const challengeCountProp = propList.find(p => p.toLowerCase().includes('challenge_stats.count')) + || 'challenge_stats.count'; + const countProp = propList.find(p => p.toLowerCase() === 'count') || 'count'; + const maxRatingProp = propList.find(p => p.toLowerCase().includes('max_rating')) + || 'member_profile_advanced.max_rating'; + const rankProp = propList.find(p => p.toLowerCase() === 'rank') || 'rank'; + return { + path: '/statistics/design/rux-placements', + transform: rows => (Array.isArray(rows) ? rows : [rows]).map((r) => { + const placements = Number(pickDefined(r && r.placements_count, r && r.count, 0)) || 0; + const maxRating = pickDefined(r && r.max_rating, null); + const mapped = { + [winnerHandleProp]: r.handle, + [handleProp]: r.handle, + [challengeCountProp]: placements, + [countProp]: placements, + [maxRatingProp]: maxRating, + [rankProp]: Number(r && r.rank) || 0, + }; + return mapped; + }), + }; + }, }; // Infer a reports-api-v6 endpoint and a transformer based on Looker props. From 030b8ea38a8f3ad3783f87ef1cbb4e7b3c08728b Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Fri, 10 Oct 2025 09:01:24 +1100 Subject: [PATCH 50/76] Integration of final statistics / v6 report endpoints --- src/shared/containers/SmartLooker.jsx | 278 +++++++++++++++++++++++++- 1 file changed, 276 insertions(+), 2 deletions(-) diff --git a/src/shared/containers/SmartLooker.jsx b/src/shared/containers/SmartLooker.jsx index 4536bd98fd..bb957476d0 100644 --- a/src/shared/containers/SmartLooker.jsx +++ b/src/shared/containers/SmartLooker.jsx @@ -57,10 +57,68 @@ const LOOKER_TO_REPORTS_MAP = { 1656: '/statistics/mm/top-10-finishes', 1655: '/statistics/mm/competitions-count', // General tab datasets (fetched from DB via reports-api-v6) - // Countries Represented + // Development Challenges by Technology (fallback to countries represented) 1127: (props) => { const cols = parseTableDef(props.table) || []; const propList = cols.map(c => String(c.property || '')); + const lowerProps = propList.map(p => p.toLowerCase()); + const looksLikeCountries = lowerProps.some(p => p.includes('country')); + const looksLikeTechnologyBreakdown = lowerProps.some(p => p.includes('challenge_technology')) + || lowerProps.some(p => p.includes('technology')) + || (!propList.length && !looksLikeCountries); + + if (looksLikeTechnologyBreakdown) { + const technologyProp = propList.find(p => p.toLowerCase().includes('challenge_technology')) + || propList.find(p => p.toLowerCase().includes('technology')) + || 'challenge_technology.name'; + const challengeCountProp = propList.find(p => p.toLowerCase().includes('challenge_stats.count')) + || propList.find(p => (p && p.toLowerCase().includes('challenge') && p.toLowerCase().includes('count'))) + || propList.find(p => (p && p.toLowerCase().endsWith('.count'))) + || 'challenge_stats.count'; + const rankProp = propList.find(p => p.toLowerCase() === 'rank') || 'rank'; + + return { + path: '/statistics/development/challenges-technology', + transform: rows => (Array.isArray(rows) ? rows : [rows]).map((r) => { + const technologyName = pickDefined( + r && r['challenge_technology.name'], + r && r.challenge_technology_name, + r && r.challenge_technology, + r && r.technology, + r && r.skill_name, + null, + ); + const challengeCount = Number(pickDefined( + r && r['challenge_stats.count'], + r && r.challenge_stats_count, + r && r.challenge_count, + r && r.count, + r && r.challenge_stats && r.challenge_stats.count, + 0, + )) || 0; + const technologyNameValue = ( + technologyName === undefined || technologyName === null + ) ? technologyName : String(technologyName); + const rankValue = Number(pickDefined( + r && r.rank, + r && rankProp && r[rankProp], + 0, + )) || 0; + + const mapped = { + [technologyProp]: technologyNameValue, + [challengeCountProp]: challengeCount, + }; + + if (rankProp && rankProp !== technologyProp && rankProp !== challengeCountProp) { + mapped[rankProp] = rankValue; + } + + return mapped; + }), + }; + } + const countryProp = propList.find(p => p.toLowerCase().includes('country')) || 'country.country_name'; const countProp = propList.find(p => p.toLowerCase() === 'user.count') || 'user.count'; return { @@ -71,6 +129,20 @@ const LOOKER_TO_REPORTS_MAP = { })), }; }, + // Countries Represented (Development) + 1129: (props) => { + const cols = parseTableDef(props.table) || []; + const propList = cols.map(c => String(c.property || '')); + const countryProp = propList.find(p => p.toLowerCase().includes('country')) || 'country.country_name'; + const countProp = propList.find(p => p.toLowerCase() === 'user.count') || 'user.count'; + return { + path: '/statistics/development/countries-represented', + transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ + [countryProp]: countryNameFromCode(r.country_code), + [countProp]: Number(r.members_count) || 0, + })), + }; + }, // Countries Represented (Design) 1136: (props) => { const cols = parseTableDef(props.table) || []; @@ -166,6 +238,204 @@ const LOOKER_TO_REPORTS_MAP = { })), }; }, + // DEVELOPMENT tab datasets (fetched from DB via reports-api-v6) + // Development First Place Wins (by member) + 1130: (props) => { + const cols = parseTableDef(props.table) || []; + const propList = cols.map(c => String(c.property || '')); + const registrantHandleProp = propList.find(p => p.toLowerCase().includes('registrant_handle')) + || propList.find(p => p.toLowerCase().includes('winner_handle')) + || propList.find(p => p.toLowerCase().includes('handle')) + || 'challenge_stats.registrant_handle'; + const handleProp = propList.find(p => p.toLowerCase() === 'handle') || null; + const challengeCountProp = propList.find(p => p.toLowerCase().includes('challenge_stats.count')) + || propList.find(p => p.toLowerCase().includes('win')) + || 'challenge_stats.count'; + const countProp = propList.find(p => p.toLowerCase() === 'count') || null; + const maxRatingProp = propList.find(p => p.toLowerCase().includes('max_rating')) + || 'member_profile_advanced.max_rating'; + const rankProp = propList.find(p => p.toLowerCase() === 'rank') || 'rank'; + return { + path: '/statistics/development/first-place-wins', + transform: rows => (Array.isArray(rows) ? rows : [rows]).map((r) => { + const wins = Number(pickDefined( + r && r['challenge_stats.count'], + r && r.count, + r && r.wins, + 0, + )) || 0; + const registrantHandle = pickDefined( + r && r['challenge_stats.registrant_handle'], + r && r['challenge_stats.winner_handle'], + r && r.handle, + null, + ); + const maxRating = pickDefined( + r && r['member_profile_advanced.max_rating'], + r && r.max_rating, + null, + ); + const rankValue = Number(pickDefined(r && r.rank, 0)) || 0; + + const mapped = { + [registrantHandleProp]: registrantHandle, + [challengeCountProp]: wins, + [maxRatingProp]: maxRating, + [rankProp]: rankValue, + }; + + if (handleProp && handleProp !== registrantHandleProp) { + mapped[handleProp] = registrantHandle; + } + if (countProp && countProp !== challengeCountProp) mapped[countProp] = wins; + + return mapped; + }), + }; + }, + // Development Prototype Wins (by member) + 1131: (props) => { + const cols = parseTableDef(props.table) || []; + const propList = cols.map(c => String(c.property || '')); + const winnerHandleProp = propList.find(p => p.toLowerCase().includes('winner_handle')) + || propList.find(p => p.toLowerCase().includes('handle')) + || 'challenge_stats.winner_handle'; + const handleProp = propList.find(p => p.toLowerCase() === 'handle') || null; + const challengeCountProp = propList.find(p => p.toLowerCase().includes('challenge_stats.count')) + || propList.find(p => p.toLowerCase().includes('win')) + || 'challenge_stats.count'; + const countProp = propList.find(p => p.toLowerCase() === 'count') || null; + const maxRatingProp = propList.find(p => p.toLowerCase().includes('max_rating')) + || 'member_profile_advanced.max_rating'; + const rankProp = propList.find(p => p.toLowerCase() === 'rank') || 'rank'; + return { + path: '/statistics/development/prototype-wins', + transform: rows => (Array.isArray(rows) ? rows : [rows]).map((r) => { + const wins = Number(pickDefined( + r && r['challenge_stats.count'], + r && r.count, + r && r.wins, + 0, + )) || 0; + const winnerHandle = pickDefined( + r && r['challenge_stats.winner_handle'], + r && r.handle, + null, + ); + const maxRating = pickDefined(r && r['member_profile_advanced.max_rating'], null); + const rankValue = Number(pickDefined(r && r.rank, 0)) || 0; + + const mapped = { + [winnerHandleProp]: winnerHandle, + [challengeCountProp]: wins, + [maxRatingProp]: maxRating, + [rankProp]: rankValue, + }; + + if (handleProp && handleProp !== winnerHandleProp) mapped[handleProp] = winnerHandle; + if (countProp && countProp !== challengeCountProp) mapped[countProp] = wins; + + return mapped; + }), + }; + }, + // Development Code Wins (by member) + 1132: (props) => { + const cols = parseTableDef(props.table) || []; + const propList = cols.map(c => String(c.property || '')); + const winnerHandleProp = propList.find(p => p.toLowerCase().includes('winner_handle')) + || propList.find(p => p.toLowerCase().includes('handle')) + || 'challenge_stats.winner_handle'; + const handleProp = propList.find(p => p.toLowerCase() === 'handle') || null; + const challengeCountProp = propList.find(p => p.toLowerCase().includes('challenge_stats.count')) + || propList.find(p => p.toLowerCase().includes('win')) + || 'challenge_stats.count'; + const countProp = propList.find(p => p.toLowerCase() === 'count') || null; + const maxRatingProp = propList.find(p => p.toLowerCase().includes('max_rating')) + || 'member_profile_advanced.max_rating'; + const rankProp = propList.find(p => p.toLowerCase() === 'rank') || 'rank'; + return { + path: '/statistics/development/code-wins', + transform: rows => (Array.isArray(rows) ? rows : [rows]).map((r) => { + const wins = Number(pickDefined( + r && r['challenge_stats.count'], + r && r.count, + r && r.wins, + 0, + )) || 0; + const winnerHandle = pickDefined( + r && r['challenge_stats.winner_handle'], + r && r.handle, + null, + ); + const maxRating = pickDefined(r && r['member_profile_advanced.max_rating'], null); + const rankValue = Number(pickDefined(r && r.rank, 0)) || 0; + + const mapped = { + [winnerHandleProp]: winnerHandle, + [challengeCountProp]: wins, + [maxRatingProp]: maxRating, + [rankProp]: rankValue, + }; + + if (handleProp && handleProp !== winnerHandleProp) mapped[handleProp] = winnerHandle; + if (countProp && countProp !== challengeCountProp) mapped[countProp] = wins; + + return mapped; + }), + }; + }, + // Development First Time Submitters + 1172: (props) => { + const cols = parseTableDef(props.table) || []; + const propList = cols.map(c => String(c.property || '')); + const handleProp = propList.find(p => p.toLowerCase().includes('handle')) || 'user.handle'; + const challengeProp = propList.find(p => p.toLowerCase().includes('challenge')) || 'challenge.challenge_name'; + const dateProp = propList.find(p => p.toLowerCase().includes('submit_date')) || 'newest_submitters.submit_date_date'; + const ratingProp = propList.find(p => p.toLowerCase().includes('max_rating')) || null; + return { + path: '/statistics/development/first-time-submitters', + transform: rows => (Array.isArray(rows) ? rows : [rows]).map((r) => { + const handleValue = pickDefined( + r && r['user.handle'], + r && r.handle, + r && r.member_handle, + null, + ); + const challengeValue = pickDefined( + r && r['challenge.challenge_name'], + r && r.challenge_name, + null, + ); + const dateValueRaw = pickDefined( + r && r['newest_submitters.submit_date_date'], + r && r.submit_date, + r && r.first_submission_date, + null, + ); + const dateValue = dateValueRaw ? String(dateValueRaw).split('T')[0] : dateValueRaw; + const ratingValue = pickDefined( + r && r['submitter_profile.max_rating'], + r && r.max_rating, + null, + ); + + const mapped = { + [handleProp]: handleValue, + [challengeProp]: challengeValue, + [dateProp]: dateValue, + }; + if (ratingProp) mapped[ratingProp] = ratingValue; + return mapped; + }), + }; + }, + // DEVELOPMENT tab datasets (fetched from DB via reports-api-v6) + // Development F2F Wins (by member) + 1133: '/statistics/development/f2f-wins', + // QA tab datasets (fetched from DB via reports-api-v6) + // QA Wins (by member) + 1700: '/statistics/qa/wins', // DESIGN tab datasets (fetched from DB via reports-api-v6) // UI Design Wins (by member) 1138: (props) => { @@ -610,7 +880,11 @@ export default function SmartLooker(props) { const data = await res.json(); let lookerData = Array.isArray(data) ? data : [data]; if (typeof transformer === 'function') { - lookerData = transformer(Array.isArray(data) ? data : [data]); + const transformed = transformer(data); + if (Array.isArray(transformed)) lookerData = transformed; + else if (transformed !== undefined && transformed !== null) { + lookerData = [transformed]; + } else lookerData = []; } if (!cancelled) setState({ loading: false, error: null, lookerInfo: { lookerData } }); } catch (e) { From 0d0a03ea99f0a0852c6a3a3debbe8b3b7466b58d Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Fri, 10 Oct 2025 10:24:12 +1100 Subject: [PATCH 51/76] Statistics fix --- src/shared/containers/SmartLooker.jsx | 84 ++++++++++++++++++++++++--- 1 file changed, 76 insertions(+), 8 deletions(-) diff --git a/src/shared/containers/SmartLooker.jsx b/src/shared/containers/SmartLooker.jsx index bb957476d0..be7ac9d480 100644 --- a/src/shared/containers/SmartLooker.jsx +++ b/src/shared/containers/SmartLooker.jsx @@ -232,10 +232,44 @@ const LOOKER_TO_REPORTS_MAP = { const valueProp = propList.find(p => p.toLowerCase().includes('review')) || 'review.count'; return { path: '/statistics/general/reviews-by-member', - transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ - [handleProp]: r.handle, - [valueProp]: Number(r.review_count) || 0, - })), + transform: (rows) => { + const list = Array.isArray(rows) ? rows : [rows]; + return list.map((r) => { + const handleValue = pickDefined( + r && r.handle, + r && r.member_handle, + r && r.copilot_handle, + r && r['user.handle'], + null, + ); + const reviewCountValue = Number(pickDefined( + r && r.review_count, + r && r['review.count'], + r && r.count, + 0, + )) || 0; + const mapped = { + handle: handleValue, + 'review.count': reviewCountValue, + review_count: reviewCountValue, + count: reviewCountValue, + }; + if (handleProp && handleProp !== 'handle') mapped[handleProp] = handleValue; + if ( + valueProp + && valueProp !== 'review.count' + && valueProp !== 'review_count' + && valueProp !== 'count' + ) { + mapped[valueProp] = reviewCountValue; + } + if (r && r.rank !== undefined && r.rank !== null) { + mapped.rank = Number(r.rank) || 0; + } + if (r && r.member_id !== undefined) mapped.member_id = r.member_id; + return mapped; + }); + }, }; }, // DEVELOPMENT tab datasets (fetched from DB via reports-api-v6) @@ -818,10 +852,44 @@ function inferFromProps(props) { const valueProp = propList.find(p => p.toLowerCase().includes('review')) || 'review.count'; return { path: '/statistics/general/reviews-by-member', - transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ - [nameProp]: r.handle, - [valueProp]: Number(r.review_count) || 0, - })), + transform: (rows) => { + const list = Array.isArray(rows) ? rows : [rows]; + return list.map((r) => { + const handleValue = pickDefined( + r && r.handle, + r && r.member_handle, + r && r.copilot_handle, + r && r['user.handle'], + null, + ); + const reviewCountValue = Number(pickDefined( + r && r.review_count, + r && r['review.count'], + r && r.count, + 0, + )) || 0; + const mapped = { + handle: handleValue, + 'review.count': reviewCountValue, + review_count: reviewCountValue, + count: reviewCountValue, + }; + if (nameProp && nameProp !== 'handle') mapped[nameProp] = handleValue; + if ( + valueProp + && valueProp !== 'review.count' + && valueProp !== 'review_count' + && valueProp !== 'count' + ) { + mapped[valueProp] = reviewCountValue; + } + if (r && r.rank !== undefined && r.rank !== null) { + mapped.rank = Number(r.rank) || 0; + } + if (r && r.member_id !== undefined) mapped.member_id = r.member_id; + return mapped; + }); + }, }; } } From 5e9d6e06c154005d01e69d37204fb4f65d3fded9 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Fri, 10 Oct 2025 11:08:16 +1100 Subject: [PATCH 52/76] Statistics updates --- src/shared/containers/SmartLooker.jsx | 269 +++++++++++++++++++------- 1 file changed, 198 insertions(+), 71 deletions(-) diff --git a/src/shared/containers/SmartLooker.jsx b/src/shared/containers/SmartLooker.jsx index be7ac9d480..dbbab47c7e 100644 --- a/src/shared/containers/SmartLooker.jsx +++ b/src/shared/containers/SmartLooker.jsx @@ -43,6 +43,65 @@ function pickDefined(...values) { return undefined; } +function toBoolean(value) { + if (value === true) return true; + if (value === false) return false; + if (typeof value === 'string') { + const normalized = value.trim().toLowerCase(); + if (normalized === 'true') return true; + if (normalized === 'false') return false; + } + return false; +} + +function collectHandleProps(columns, fallback = 'handle') { + const props = new Set(); + if (fallback) props.add(fallback); + if (!Array.isArray(columns)) return props; + columns.forEach((col) => { + if (!col) return; + const property = String(col.property || '').trim(); + if (!property || property.toLowerCase() === 'rank') return; + const header = String(col.headerName || '').toLowerCase(); + const memberLinks = toBoolean(col.memberLinks); + if ( + property.toLowerCase().includes('handle') + || header.includes('handle') + || memberLinks + ) { + props.add(property); + } + }); + return props; +} + +function assignHandleProps(target, handleValue, props) { + if ( + handleValue === undefined + || handleValue === null + || !target + || typeof target !== 'object' + ) { + return; + } + const list = []; + if (props && typeof props.forEach === 'function') { + props.forEach((prop) => { + if (prop) list.push(prop); + }); + } else if (Array.isArray(props)) { + props.forEach((prop) => { + if (prop) list.push(prop); + }); + } + if (!list.length) list.push('handle'); + const updates = {}; + list.forEach((prop) => { + updates[prop] = handleValue; + }); + Object.assign(target, updates); +} + // Direct mappings from known Looker IDs to reports-api-v6 endpoints. // These are the specific Looker tiles used by the /community/statistics page // that we want to source from the new reports service instead of legacy Looker. @@ -228,7 +287,7 @@ const LOOKER_TO_REPORTS_MAP = { 1150: (props) => { const cols = parseTableDef(props.table) || []; const propList = cols.map(c => String(c.property || '')); - const handleProp = propList.find(p => p.toLowerCase().includes('handle')) || 'handle'; + const handleProps = collectHandleProps(cols); const valueProp = propList.find(p => p.toLowerCase().includes('review')) || 'review.count'; return { path: '/statistics/general/reviews-by-member', @@ -249,12 +308,11 @@ const LOOKER_TO_REPORTS_MAP = { 0, )) || 0; const mapped = { - handle: handleValue, 'review.count': reviewCountValue, review_count: reviewCountValue, count: reviewCountValue, }; - if (handleProp && handleProp !== 'handle') mapped[handleProp] = handleValue; + assignHandleProps(mapped, handleValue, handleProps); if ( valueProp && valueProp !== 'review.count' @@ -475,41 +533,61 @@ const LOOKER_TO_REPORTS_MAP = { 1138: (props) => { const cols = parseTableDef(props.table) || []; const propList = cols.map(c => String(c.property || '')); - const nameProp = propList.find(p => p.toLowerCase().includes('handle')) || 'handle'; + const handleProps = collectHandleProps(cols); const valueProp = propList.find(p => p.toLowerCase().includes('win')) || propList.find(p => (p && p.toLowerCase() !== 'rank' && !p.toLowerCase().includes('handle'))) || 'wins'; return { path: '/statistics/design/ui-design-wins', - transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ - [nameProp]: r.handle, - [valueProp]: Number(pickDefined(r && r.wins_count, r && r.count, 0)) || 0, - })), + transform: rows => (Array.isArray(rows) ? rows : [rows]).map((r) => { + const handleValue = pickDefined( + r && r.handle, + r && r.member_handle, + r && r.winner_handle, + null, + ); + const winsValue = Number(pickDefined(r && r.wins_count, r && r.count, 0)) || 0; + const mapped = {}; + if (valueProp) mapped[valueProp] = winsValue; + if (valueProp !== 'wins') mapped.wins = winsValue; + if (valueProp !== 'count') mapped.count = winsValue; + assignHandleProps(mapped, handleValue, handleProps); + return mapped; + }), }; }, // Design F2F Wins (by member) 1141: (props) => { const cols = parseTableDef(props.table) || []; const propList = cols.map(c => String(c.property || '')); - const nameProp = propList.find(p => p.toLowerCase().includes('handle')) || 'handle'; + const handleProps = collectHandleProps(cols); const valueProp = propList.find(p => p.toLowerCase().includes('win')) || propList.find(p => (p && p.toLowerCase() !== 'rank' && !p.toLowerCase().includes('handle'))) || 'wins'; return { path: '/statistics/design/f2f-wins', - transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ - [nameProp]: r.handle, - [valueProp]: Number(pickDefined(r && r.wins_count, r && r.count, 0)) || 0, - })), + transform: rows => (Array.isArray(rows) ? rows : [rows]).map((r) => { + const handleValue = pickDefined( + r && r.handle, + r && r.member_handle, + r && r.winner_handle, + null, + ); + const winsValue = Number(pickDefined(r && r.wins_count, r && r.count, 0)) || 0; + const mapped = {}; + if (valueProp) mapped[valueProp] = winsValue; + if (valueProp !== 'wins') mapped.wins = winsValue; + if (valueProp !== 'count') mapped.count = winsValue; + assignHandleProps(mapped, handleValue, handleProps); + return mapped; + }), }; }, // LUX 1st Place Wins (Design) 1571: (props) => { const cols = parseTableDef(props.table) || []; const propList = cols.map(c => String(c.property || '')); - const winnerHandleProp = propList.find(p => p.toLowerCase().includes('winner_handle')) - || 'challenge_stats.winner_handle'; - const handleProp = propList.find(p => p.toLowerCase() === 'handle') || 'handle'; + const handleProps = collectHandleProps(cols); const challengeCountProp = propList.find(p => p.toLowerCase().includes('challenge_stats.count')) || 'challenge_stats.count'; const countProp = propList.find(p => p.toLowerCase() === 'count') || 'count'; @@ -521,14 +599,22 @@ const LOOKER_TO_REPORTS_MAP = { transform: rows => (Array.isArray(rows) ? rows : [rows]).map((r) => { const wins = Number(pickDefined(r && r.wins_count, r && r.count, 0)) || 0; const maxRating = pickDefined(r && r.max_rating, null); - const mapped = { - [winnerHandleProp]: r.handle, - [handleProp]: r.handle, - [challengeCountProp]: wins, - [countProp]: wins, - [maxRatingProp]: maxRating, - [rankProp]: Number(r && r.rank) || 0, - }; + const rankValue = Number(pickDefined(r && r.rank, 0)) || 0; + const handleValue = pickDefined( + r && r.handle, + r && r.winner_handle, + r && r['challenge_stats.winner_handle'], + r && r.member_handle, + null, + ); + const mapped = {}; + if (challengeCountProp) mapped[challengeCountProp] = wins; + if (countProp && countProp !== challengeCountProp) mapped[countProp] = wins; + if (challengeCountProp !== 'wins') mapped.wins = wins; + if (challengeCountProp !== 'count' && countProp !== 'count') mapped.count = wins; + if (maxRatingProp) mapped[maxRatingProp] = maxRating; + if (rankProp) mapped[rankProp] = rankValue; + assignHandleProps(mapped, handleValue, handleProps); return mapped; }), }; @@ -537,9 +623,7 @@ const LOOKER_TO_REPORTS_MAP = { 1572: (props) => { const cols = parseTableDef(props.table) || []; const propList = cols.map(c => String(c.property || '')); - const winnerHandleProp = propList.find(p => p.toLowerCase().includes('winner_handle')) - || 'challenge_stats.winner_handle'; - const handleProp = propList.find(p => p.toLowerCase() === 'handle') || 'handle'; + const handleProps = collectHandleProps(cols); const challengeCountProp = propList.find(p => p.toLowerCase().includes('challenge_stats.count')) || 'challenge_stats.count'; const countProp = propList.find(p => p.toLowerCase() === 'count') || 'count'; @@ -551,14 +635,22 @@ const LOOKER_TO_REPORTS_MAP = { transform: rows => (Array.isArray(rows) ? rows : [rows]).map((r) => { const placements = Number(pickDefined(r && r.placements_count, r && r.count, 0)) || 0; const maxRating = pickDefined(r && r.max_rating, null); - const mapped = { - [winnerHandleProp]: r.handle, - [handleProp]: r.handle, - [challengeCountProp]: placements, - [countProp]: placements, - [maxRatingProp]: maxRating, - [rankProp]: Number(r && r.rank) || 0, - }; + const rankValue = Number(pickDefined(r && r.rank, 0)) || 0; + const handleValue = pickDefined( + r && r.handle, + r && r.winner_handle, + r && r['challenge_stats.winner_handle'], + r && r.member_handle, + null, + ); + const mapped = {}; + if (challengeCountProp) mapped[challengeCountProp] = placements; + if (countProp && countProp !== challengeCountProp) mapped[countProp] = placements; + if (challengeCountProp !== 'wins') mapped.wins = placements; + if (challengeCountProp !== 'count' && countProp !== 'count') mapped.count = placements; + if (maxRatingProp) mapped[maxRatingProp] = maxRating; + if (rankProp) mapped[rankProp] = rankValue; + assignHandleProps(mapped, handleValue, handleProps); return mapped; }), }; @@ -567,9 +659,7 @@ const LOOKER_TO_REPORTS_MAP = { 1573: (props) => { const cols = parseTableDef(props.table) || []; const propList = cols.map(c => String(c.property || '')); - const winnerHandleProp = propList.find(p => p.toLowerCase().includes('winner_handle')) - || 'challenge_stats.winner_handle'; - const handleProp = propList.find(p => p.toLowerCase() === 'handle') || 'handle'; + const handleProps = collectHandleProps(cols); const challengeCountProp = propList.find(p => p.toLowerCase().includes('challenge_stats.count')) || 'challenge_stats.count'; const countProp = propList.find(p => p.toLowerCase() === 'count') || 'count'; @@ -581,14 +671,22 @@ const LOOKER_TO_REPORTS_MAP = { transform: rows => (Array.isArray(rows) ? rows : [rows]).map((r) => { const wins = Number(pickDefined(r && r.wins_count, r && r.count, 0)) || 0; const maxRating = pickDefined(r && r.max_rating, null); - const mapped = { - [winnerHandleProp]: r.handle, - [handleProp]: r.handle, - [challengeCountProp]: wins, - [countProp]: wins, - [maxRatingProp]: maxRating, - [rankProp]: Number(r && r.rank) || 0, - }; + const rankValue = Number(pickDefined(r && r.rank, 0)) || 0; + const handleValue = pickDefined( + r && r.handle, + r && r.winner_handle, + r && r['challenge_stats.winner_handle'], + r && r.member_handle, + null, + ); + const mapped = {}; + if (challengeCountProp) mapped[challengeCountProp] = wins; + if (countProp && countProp !== challengeCountProp) mapped[countProp] = wins; + if (challengeCountProp !== 'wins') mapped.wins = wins; + if (challengeCountProp !== 'count' && countProp !== 'count') mapped.count = wins; + if (maxRatingProp) mapped[maxRatingProp] = maxRating; + if (rankProp) mapped[rankProp] = rankValue; + assignHandleProps(mapped, handleValue, handleProps); return mapped; }), }; @@ -597,9 +695,7 @@ const LOOKER_TO_REPORTS_MAP = { 1574: (props) => { const cols = parseTableDef(props.table) || []; const propList = cols.map(c => String(c.property || '')); - const winnerHandleProp = propList.find(p => p.toLowerCase().includes('winner_handle')) - || 'challenge_stats.winner_handle'; - const handleProp = propList.find(p => p.toLowerCase() === 'handle') || 'handle'; + const handleProps = collectHandleProps(cols); const challengeCountProp = propList.find(p => p.toLowerCase().includes('challenge_stats.count')) || 'challenge_stats.count'; const countProp = propList.find(p => p.toLowerCase() === 'count') || 'count'; @@ -611,14 +707,22 @@ const LOOKER_TO_REPORTS_MAP = { transform: rows => (Array.isArray(rows) ? rows : [rows]).map((r) => { const placements = Number(pickDefined(r && r.placements_count, r && r.count, 0)) || 0; const maxRating = pickDefined(r && r.max_rating, null); - const mapped = { - [winnerHandleProp]: r.handle, - [handleProp]: r.handle, - [challengeCountProp]: placements, - [countProp]: placements, - [maxRatingProp]: maxRating, - [rankProp]: Number(r && r.rank) || 0, - }; + const rankValue = Number(pickDefined(r && r.rank, 0)) || 0; + const handleValue = pickDefined( + r && r.handle, + r && r.winner_handle, + r && r['challenge_stats.winner_handle'], + r && r.member_handle, + null, + ); + const mapped = {}; + if (challengeCountProp) mapped[challengeCountProp] = placements; + if (countProp && countProp !== challengeCountProp) mapped[countProp] = placements; + if (challengeCountProp !== 'wins') mapped.wins = placements; + if (challengeCountProp !== 'count' && countProp !== 'count') mapped.count = placements; + if (maxRatingProp) mapped[maxRatingProp] = maxRating; + if (rankProp) mapped[rankProp] = rankValue; + assignHandleProps(mapped, handleValue, handleProps); return mapped; }), }; @@ -667,10 +771,15 @@ function inferFromProps(props) { // General tab tables if (cols && cols.length) { const propList = cols.map(c => String(c.property || '')); + const handleProps = collectHandleProps(cols); const hasCountry = propList.some(p => p.toLowerCase().includes('country')); const hasHandle = propList.some(p => p.toLowerCase().includes('handle')); const hasCopilot = propList.some(p => p.toLowerCase().includes('copilot')); const hasReview = propList.some(p => p.toLowerCase().includes('review')); + const hasHandleHeader = lowerHeaders.some(h => h.includes('handle')); + const hasHandleMemberLink = cols.some(c => toBoolean(c && c.memberLinks)); + const hasReviewHeader = lowerHeaders.some(h => h.includes('review')); + const reviewIndicator = hasReview || hasReviewHeader; const hasChallengeCount = propList.some(p => p.toLowerCase() === 'challenge.count'); const hasUserCount = propList.some(p => p.toLowerCase() === 'user.count'); const hasFirstPlace = propList.some(p => p.toLowerCase().includes('first_place')); @@ -685,16 +794,26 @@ function inferFromProps(props) { ) ); if (looksLikeUiDesignWins) { - const nameProp = propList.find(p => p.toLowerCase().includes('handle')) || 'handle'; const valueProp = propList.find(p => p.toLowerCase().includes('win')) || propList.find(p => (p && p.toLowerCase() !== 'rank' && !p.toLowerCase().includes('handle'))) || 'wins'; return { path: '/statistics/design/ui-design-wins', - transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ - [nameProp]: r.handle, - [valueProp]: Number(pickDefined(r && r.wins_count, r && r.count, 0)) || 0, - })), + transform: rows => (Array.isArray(rows) ? rows : [rows]).map((r) => { + const handleValue = pickDefined( + r && r.handle, + r && r.member_handle, + r && r.winner_handle, + null, + ); + const winsValue = Number(pickDefined(r && r.wins_count, r && r.count, 0)) || 0; + const mapped = {}; + if (valueProp) mapped[valueProp] = winsValue; + if (valueProp !== 'wins') mapped.wins = winsValue; + if (valueProp !== 'count') mapped.count = winsValue; + assignHandleProps(mapped, handleValue, handleProps); + return mapped; + }), }; } @@ -719,16 +838,26 @@ function inferFromProps(props) { && (headersMentionF2FWins || propsMentionF2FWins || headersMentionF2F || propsMentionF2F) ); if (looksLikeF2FWins) { - const nameProp = propList.find(p => p.toLowerCase().includes('handle')) || 'handle'; const valueProp = propList.find(p => p.toLowerCase().includes('win')) || propList.find(p => (p && p.toLowerCase() !== 'rank' && !p.toLowerCase().includes('handle'))) || 'wins'; return { path: '/statistics/design/f2f-wins', - transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ - [nameProp]: r.handle, - [valueProp]: Number(pickDefined(r && r.wins_count, r && r.count, 0)) || 0, - })), + transform: rows => (Array.isArray(rows) ? rows : [rows]).map((r) => { + const handleValue = pickDefined( + r && r.handle, + r && r.member_handle, + r && r.winner_handle, + null, + ); + const winsValue = Number(pickDefined(r && r.wins_count, r && r.count, 0)) || 0; + const mapped = {}; + if (valueProp) mapped[valueProp] = winsValue; + if (valueProp !== 'wins') mapped.wins = winsValue; + if (valueProp !== 'count') mapped.count = winsValue; + assignHandleProps(mapped, handleValue, handleProps); + return mapped; + }), }; } @@ -847,8 +976,7 @@ function inferFromProps(props) { } // Reviews by member: expect handle + review count - if ((hasHandle || hasCopilot) && hasReview) { - const nameProp = propList.find(p => p.toLowerCase().includes('handle')) || 'handle'; + if ((hasHandle || hasCopilot || hasHandleHeader || hasHandleMemberLink) && reviewIndicator) { const valueProp = propList.find(p => p.toLowerCase().includes('review')) || 'review.count'; return { path: '/statistics/general/reviews-by-member', @@ -869,12 +997,11 @@ function inferFromProps(props) { 0, )) || 0; const mapped = { - handle: handleValue, 'review.count': reviewCountValue, review_count: reviewCountValue, count: reviewCountValue, }; - if (nameProp && nameProp !== 'handle') mapped[nameProp] = handleValue; + assignHandleProps(mapped, handleValue, handleProps); if ( valueProp && valueProp !== 'review.count' From fd10b54389afc83ec3e328c6e899434460b99aaf Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Fri, 10 Oct 2025 12:53:37 +1100 Subject: [PATCH 53/76] Statistics page simplification and cleanup --- src/shared/containers/SmartLooker.jsx | 1120 ++----------------------- 1 file changed, 88 insertions(+), 1032 deletions(-) diff --git a/src/shared/containers/SmartLooker.jsx b/src/shared/containers/SmartLooker.jsx index dbbab47c7e..55d419a532 100644 --- a/src/shared/containers/SmartLooker.jsx +++ b/src/shared/containers/SmartLooker.jsx @@ -1,1045 +1,94 @@ /** - * SmartLooker bridges legacy usages to new - * reports-api-v6 endpoints for specific Looker IDs used by the - * /community/statistics page. Unknown IDs fall back to the original Looker - * container (which fetches from the v4 Looks API). That fallback is a "just in case" - * item. + * SmartLooker bridges legacy usages to the new + * reports-api-v6 endpoints for the /community/statistics page. The reports + * service now mirrors the original Looker schemas, so we simply proxy data + * from the API to the existing Looker component. */ import React from 'react'; import PT from 'prop-types'; import { config } from 'topcoder-react-utils'; import Looker from 'components/Looker'; -import OriginalLookerContainer from 'containers/Looker'; -import { getCountryObjFromAlpha3 } from 'utils/countries'; -// Safely parse a possible JSON string table definition into an array of columns. -function parseTableDef(table) { - if (!table && table !== '') return null; - if (Array.isArray(table)) return table; - if (table === '') return []; - try { - // Contentful markdown encodes quotes as &q; in some places; normalize first - const normalized = String(table).replace(/&q;/g, '"').replace(/'/g, '"'); - const cols = JSON.parse(normalized); - return Array.isArray(cols) ? cols : null; - } catch (e) { - return null; - } -} - -// Convert a 3-letter country code to its English full name. -// Falls back to the original code if unknown. -function countryNameFromCode(code) { - if (!code) return code; - const obj = getCountryObjFromAlpha3(String(code).toUpperCase()); - return (obj && obj.name) ? obj.name : code; -} - -function pickDefined(...values) { - for (let i = 0; i < values.length; i += 1) { - const value = values[i]; - if (value !== undefined && value !== null) return value; - } - return undefined; -} - -function toBoolean(value) { - if (value === true) return true; - if (value === false) return false; - if (typeof value === 'string') { - const normalized = value.trim().toLowerCase(); - if (normalized === 'true') return true; - if (normalized === 'false') return false; - } - return false; -} - -function collectHandleProps(columns, fallback = 'handle') { - const props = new Set(); - if (fallback) props.add(fallback); - if (!Array.isArray(columns)) return props; - columns.forEach((col) => { - if (!col) return; - const property = String(col.property || '').trim(); - if (!property || property.toLowerCase() === 'rank') return; - const header = String(col.headerName || '').toLowerCase(); - const memberLinks = toBoolean(col.memberLinks); - if ( - property.toLowerCase().includes('handle') - || header.includes('handle') - || memberLinks - ) { - props.add(property); - } - }); - return props; -} - -function assignHandleProps(target, handleValue, props) { - if ( - handleValue === undefined - || handleValue === null - || !target - || typeof target !== 'object' - ) { - return; - } - const list = []; - if (props && typeof props.forEach === 'function') { - props.forEach((prop) => { - if (prop) list.push(prop); - }); - } else if (Array.isArray(props)) { - props.forEach((prop) => { - if (prop) list.push(prop); - }); - } - if (!list.length) list.push('handle'); - const updates = {}; - list.forEach((prop) => { - updates[prop] = handleValue; - }); - Object.assign(target, updates); -} - -// Direct mappings from known Looker IDs to reports-api-v6 endpoints. -// These are the specific Looker tiles used by the /community/statistics page -// that we want to source from the new reports service instead of legacy Looker. -const LOOKER_TO_REPORTS_MAP = { - // SRM datasets (static JSON served by reports-api-v6) +const LOOKER_TO_REPORTS_PATH = { + 1127: '/statistics/development/challenges-technology', + 1129: '/statistics/development/countries-represented', + 1130: '/statistics/development/first-place-wins', + 1131: '/statistics/development/prototype-wins', + 1132: '/statistics/development/code-wins', + 1133: '/statistics/development/f2f-wins', + 1135: '/statistics/design/first-place-by-country', + 1136: '/statistics/design/countries-represented', + 1138: '/statistics/design/ui-design-wins', + 1141: '/statistics/design/f2f-wins', + 1146: '/statistics/general/copiloted-challenges', + 1149: '/statistics/general/first-place-by-country', + 1150: '/statistics/general/reviews-by-member', + 1172: '/statistics/development/first-time-submitters', + 1178: '/statistics/design/first-time-submitters', + 1571: '/statistics/design/lux-first-place-wins', + 1572: '/statistics/design/lux-placements', + 1573: '/statistics/design/rux-first-place-wins', + 1574: '/statistics/design/rux-placements', + 1652: '/statistics/mm/top-rated', 1653: '/statistics/srm/top-rated', - 1657: '/statistics/srm/country-ratings', 1654: '/statistics/srm/competitions-count', - // Marathon Match datasets (static JSON served by reports-api-v6) - 1652: '/statistics/mm/top-rated', - 1658: '/statistics/mm/country-ratings', - 1656: '/statistics/mm/top-10-finishes', 1655: '/statistics/mm/competitions-count', - // General tab datasets (fetched from DB via reports-api-v6) - // Development Challenges by Technology (fallback to countries represented) - 1127: (props) => { - const cols = parseTableDef(props.table) || []; - const propList = cols.map(c => String(c.property || '')); - const lowerProps = propList.map(p => p.toLowerCase()); - const looksLikeCountries = lowerProps.some(p => p.includes('country')); - const looksLikeTechnologyBreakdown = lowerProps.some(p => p.includes('challenge_technology')) - || lowerProps.some(p => p.includes('technology')) - || (!propList.length && !looksLikeCountries); - - if (looksLikeTechnologyBreakdown) { - const technologyProp = propList.find(p => p.toLowerCase().includes('challenge_technology')) - || propList.find(p => p.toLowerCase().includes('technology')) - || 'challenge_technology.name'; - const challengeCountProp = propList.find(p => p.toLowerCase().includes('challenge_stats.count')) - || propList.find(p => (p && p.toLowerCase().includes('challenge') && p.toLowerCase().includes('count'))) - || propList.find(p => (p && p.toLowerCase().endsWith('.count'))) - || 'challenge_stats.count'; - const rankProp = propList.find(p => p.toLowerCase() === 'rank') || 'rank'; - - return { - path: '/statistics/development/challenges-technology', - transform: rows => (Array.isArray(rows) ? rows : [rows]).map((r) => { - const technologyName = pickDefined( - r && r['challenge_technology.name'], - r && r.challenge_technology_name, - r && r.challenge_technology, - r && r.technology, - r && r.skill_name, - null, - ); - const challengeCount = Number(pickDefined( - r && r['challenge_stats.count'], - r && r.challenge_stats_count, - r && r.challenge_count, - r && r.count, - r && r.challenge_stats && r.challenge_stats.count, - 0, - )) || 0; - const technologyNameValue = ( - technologyName === undefined || technologyName === null - ) ? technologyName : String(technologyName); - const rankValue = Number(pickDefined( - r && r.rank, - r && rankProp && r[rankProp], - 0, - )) || 0; - - const mapped = { - [technologyProp]: technologyNameValue, - [challengeCountProp]: challengeCount, - }; - - if (rankProp && rankProp !== technologyProp && rankProp !== challengeCountProp) { - mapped[rankProp] = rankValue; - } - - return mapped; - }), - }; - } - - const countryProp = propList.find(p => p.toLowerCase().includes('country')) || 'country.country_name'; - const countProp = propList.find(p => p.toLowerCase() === 'user.count') || 'user.count'; - return { - path: '/statistics/general/countries-represented', - transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ - [countryProp]: countryNameFromCode(r.country_code), - [countProp]: Number(r.members_count) || 0, - })), - }; - }, - // Countries Represented (Development) - 1129: (props) => { - const cols = parseTableDef(props.table) || []; - const propList = cols.map(c => String(c.property || '')); - const countryProp = propList.find(p => p.toLowerCase().includes('country')) || 'country.country_name'; - const countProp = propList.find(p => p.toLowerCase() === 'user.count') || 'user.count'; - return { - path: '/statistics/development/countries-represented', - transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ - [countryProp]: countryNameFromCode(r.country_code), - [countProp]: Number(r.members_count) || 0, - })), - }; - }, - // Countries Represented (Design) - 1136: (props) => { - const cols = parseTableDef(props.table) || []; - const propList = cols.map(c => String(c.property || '')); - return { - path: '/statistics/design/countries-represented', - transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ - [propList.find(p => p.toLowerCase().includes('country')) || 'country.country_name']: - countryNameFromCode(r.country_code), - [propList.find(p => p.toLowerCase() === 'user.count') || 'user.count']: - Number(r.members_count) || 0, - })), - }; - }, - // First Time Submitters (Design) - 1178: (props) => { - const cols = parseTableDef(props.table) || []; - const propList = cols.map(c => String(c.property || '')); - const nameProp = propList.find(p => p.toLowerCase().includes('handle')) || 'handle'; - const dateProp = propList.find(p => p.toLowerCase().includes('date')) - || propList.find(p => (p && p.toLowerCase() !== 'rank' && !p.toLowerCase().includes('handle'))) - || 'first_submission_date'; - return { - path: '/statistics/design/first-time-submitters', - transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ - [nameProp]: r.handle, - [dateProp]: r.first_submission_date, - })), - }; - }, - // 1st Place Finishes (by country) - 1135: (props) => { - const cols = parseTableDef(props.table) || []; - const propList = cols.map(c => String(c.property || '')); - const nameProp = propList.find(p => p.toLowerCase().includes('country')) || 'country.country_name'; - const valueProp = propList.find(p => p.toLowerCase().includes('first_place')) - || propList.find(p => (p && p.toLowerCase() !== 'rank' && !p.toLowerCase().includes('country'))) - || 'first_place_count'; - return { - path: '/statistics/design/first-place-by-country', - transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ - [nameProp]: countryNameFromCode(r.country_code), - [valueProp]: Number(r.first_place_count) || 0, - })), - }; - }, - // 1st place by country - 1149: (props) => { - const cols = parseTableDef(props.table) || []; - const propList = cols.map(c => String(c.property || '')); - const countryProp = propList.find(p => p.toLowerCase().includes('country')) || 'country.country_name'; - let valueProp = propList.find(p => p.toLowerCase().includes('first_place')) - || propList.find(p => p.toLowerCase() === 'user.count'); - if (!valueProp) { - valueProp = propList.find(p => (p && !p.toLowerCase().includes('country') && p.toLowerCase() !== 'rank')) - || 'first_place_count'; - } - return { - path: '/statistics/general/first-place-by-country', - transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ - [countryProp]: countryNameFromCode(r.country_code), - [valueProp]: Number(r.first_place_count) || 0, - })), - }; - }, - // Copiloted challenges (by member) - 1146: (props) => { - const cols = parseTableDef(props.table) || []; - const propList = cols.map(c => String(c.property || '')); - const handleProp = propList.find(p => p.toLowerCase().includes('copilot')) - || propList.find(p => p.toLowerCase().includes('handle')) - || 'copilot.handle'; - const countProp = 'challenge.count'; - return { - path: '/statistics/general/copiloted-challenges', - transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ - [handleProp]: r.handle, - [countProp]: Number(r.copiloted_challenges) || 0, - })), - }; - }, - // Number of Reviews (by member) - 1150: (props) => { - const cols = parseTableDef(props.table) || []; - const propList = cols.map(c => String(c.property || '')); - const handleProps = collectHandleProps(cols); - const valueProp = propList.find(p => p.toLowerCase().includes('review')) || 'review.count'; - return { - path: '/statistics/general/reviews-by-member', - transform: (rows) => { - const list = Array.isArray(rows) ? rows : [rows]; - return list.map((r) => { - const handleValue = pickDefined( - r && r.handle, - r && r.member_handle, - r && r.copilot_handle, - r && r['user.handle'], - null, - ); - const reviewCountValue = Number(pickDefined( - r && r.review_count, - r && r['review.count'], - r && r.count, - 0, - )) || 0; - const mapped = { - 'review.count': reviewCountValue, - review_count: reviewCountValue, - count: reviewCountValue, - }; - assignHandleProps(mapped, handleValue, handleProps); - if ( - valueProp - && valueProp !== 'review.count' - && valueProp !== 'review_count' - && valueProp !== 'count' - ) { - mapped[valueProp] = reviewCountValue; - } - if (r && r.rank !== undefined && r.rank !== null) { - mapped.rank = Number(r.rank) || 0; - } - if (r && r.member_id !== undefined) mapped.member_id = r.member_id; - return mapped; - }); - }, - }; - }, - // DEVELOPMENT tab datasets (fetched from DB via reports-api-v6) - // Development First Place Wins (by member) - 1130: (props) => { - const cols = parseTableDef(props.table) || []; - const propList = cols.map(c => String(c.property || '')); - const registrantHandleProp = propList.find(p => p.toLowerCase().includes('registrant_handle')) - || propList.find(p => p.toLowerCase().includes('winner_handle')) - || propList.find(p => p.toLowerCase().includes('handle')) - || 'challenge_stats.registrant_handle'; - const handleProp = propList.find(p => p.toLowerCase() === 'handle') || null; - const challengeCountProp = propList.find(p => p.toLowerCase().includes('challenge_stats.count')) - || propList.find(p => p.toLowerCase().includes('win')) - || 'challenge_stats.count'; - const countProp = propList.find(p => p.toLowerCase() === 'count') || null; - const maxRatingProp = propList.find(p => p.toLowerCase().includes('max_rating')) - || 'member_profile_advanced.max_rating'; - const rankProp = propList.find(p => p.toLowerCase() === 'rank') || 'rank'; - return { - path: '/statistics/development/first-place-wins', - transform: rows => (Array.isArray(rows) ? rows : [rows]).map((r) => { - const wins = Number(pickDefined( - r && r['challenge_stats.count'], - r && r.count, - r && r.wins, - 0, - )) || 0; - const registrantHandle = pickDefined( - r && r['challenge_stats.registrant_handle'], - r && r['challenge_stats.winner_handle'], - r && r.handle, - null, - ); - const maxRating = pickDefined( - r && r['member_profile_advanced.max_rating'], - r && r.max_rating, - null, - ); - const rankValue = Number(pickDefined(r && r.rank, 0)) || 0; - - const mapped = { - [registrantHandleProp]: registrantHandle, - [challengeCountProp]: wins, - [maxRatingProp]: maxRating, - [rankProp]: rankValue, - }; - - if (handleProp && handleProp !== registrantHandleProp) { - mapped[handleProp] = registrantHandle; - } - if (countProp && countProp !== challengeCountProp) mapped[countProp] = wins; - - return mapped; - }), - }; - }, - // Development Prototype Wins (by member) - 1131: (props) => { - const cols = parseTableDef(props.table) || []; - const propList = cols.map(c => String(c.property || '')); - const winnerHandleProp = propList.find(p => p.toLowerCase().includes('winner_handle')) - || propList.find(p => p.toLowerCase().includes('handle')) - || 'challenge_stats.winner_handle'; - const handleProp = propList.find(p => p.toLowerCase() === 'handle') || null; - const challengeCountProp = propList.find(p => p.toLowerCase().includes('challenge_stats.count')) - || propList.find(p => p.toLowerCase().includes('win')) - || 'challenge_stats.count'; - const countProp = propList.find(p => p.toLowerCase() === 'count') || null; - const maxRatingProp = propList.find(p => p.toLowerCase().includes('max_rating')) - || 'member_profile_advanced.max_rating'; - const rankProp = propList.find(p => p.toLowerCase() === 'rank') || 'rank'; - return { - path: '/statistics/development/prototype-wins', - transform: rows => (Array.isArray(rows) ? rows : [rows]).map((r) => { - const wins = Number(pickDefined( - r && r['challenge_stats.count'], - r && r.count, - r && r.wins, - 0, - )) || 0; - const winnerHandle = pickDefined( - r && r['challenge_stats.winner_handle'], - r && r.handle, - null, - ); - const maxRating = pickDefined(r && r['member_profile_advanced.max_rating'], null); - const rankValue = Number(pickDefined(r && r.rank, 0)) || 0; - - const mapped = { - [winnerHandleProp]: winnerHandle, - [challengeCountProp]: wins, - [maxRatingProp]: maxRating, - [rankProp]: rankValue, - }; - - if (handleProp && handleProp !== winnerHandleProp) mapped[handleProp] = winnerHandle; - if (countProp && countProp !== challengeCountProp) mapped[countProp] = wins; - - return mapped; - }), - }; - }, - // Development Code Wins (by member) - 1132: (props) => { - const cols = parseTableDef(props.table) || []; - const propList = cols.map(c => String(c.property || '')); - const winnerHandleProp = propList.find(p => p.toLowerCase().includes('winner_handle')) - || propList.find(p => p.toLowerCase().includes('handle')) - || 'challenge_stats.winner_handle'; - const handleProp = propList.find(p => p.toLowerCase() === 'handle') || null; - const challengeCountProp = propList.find(p => p.toLowerCase().includes('challenge_stats.count')) - || propList.find(p => p.toLowerCase().includes('win')) - || 'challenge_stats.count'; - const countProp = propList.find(p => p.toLowerCase() === 'count') || null; - const maxRatingProp = propList.find(p => p.toLowerCase().includes('max_rating')) - || 'member_profile_advanced.max_rating'; - const rankProp = propList.find(p => p.toLowerCase() === 'rank') || 'rank'; - return { - path: '/statistics/development/code-wins', - transform: rows => (Array.isArray(rows) ? rows : [rows]).map((r) => { - const wins = Number(pickDefined( - r && r['challenge_stats.count'], - r && r.count, - r && r.wins, - 0, - )) || 0; - const winnerHandle = pickDefined( - r && r['challenge_stats.winner_handle'], - r && r.handle, - null, - ); - const maxRating = pickDefined(r && r['member_profile_advanced.max_rating'], null); - const rankValue = Number(pickDefined(r && r.rank, 0)) || 0; - - const mapped = { - [winnerHandleProp]: winnerHandle, - [challengeCountProp]: wins, - [maxRatingProp]: maxRating, - [rankProp]: rankValue, - }; - - if (handleProp && handleProp !== winnerHandleProp) mapped[handleProp] = winnerHandle; - if (countProp && countProp !== challengeCountProp) mapped[countProp] = wins; - - return mapped; - }), - }; - }, - // Development First Time Submitters - 1172: (props) => { - const cols = parseTableDef(props.table) || []; - const propList = cols.map(c => String(c.property || '')); - const handleProp = propList.find(p => p.toLowerCase().includes('handle')) || 'user.handle'; - const challengeProp = propList.find(p => p.toLowerCase().includes('challenge')) || 'challenge.challenge_name'; - const dateProp = propList.find(p => p.toLowerCase().includes('submit_date')) || 'newest_submitters.submit_date_date'; - const ratingProp = propList.find(p => p.toLowerCase().includes('max_rating')) || null; - return { - path: '/statistics/development/first-time-submitters', - transform: rows => (Array.isArray(rows) ? rows : [rows]).map((r) => { - const handleValue = pickDefined( - r && r['user.handle'], - r && r.handle, - r && r.member_handle, - null, - ); - const challengeValue = pickDefined( - r && r['challenge.challenge_name'], - r && r.challenge_name, - null, - ); - const dateValueRaw = pickDefined( - r && r['newest_submitters.submit_date_date'], - r && r.submit_date, - r && r.first_submission_date, - null, - ); - const dateValue = dateValueRaw ? String(dateValueRaw).split('T')[0] : dateValueRaw; - const ratingValue = pickDefined( - r && r['submitter_profile.max_rating'], - r && r.max_rating, - null, - ); - - const mapped = { - [handleProp]: handleValue, - [challengeProp]: challengeValue, - [dateProp]: dateValue, - }; - if (ratingProp) mapped[ratingProp] = ratingValue; - return mapped; - }), - }; - }, - // DEVELOPMENT tab datasets (fetched from DB via reports-api-v6) - // Development F2F Wins (by member) - 1133: '/statistics/development/f2f-wins', - // QA tab datasets (fetched from DB via reports-api-v6) - // QA Wins (by member) + 1656: '/statistics/mm/top-10-finishes', + 1657: '/statistics/srm/country-ratings', + 1658: '/statistics/mm/country-ratings', 1700: '/statistics/qa/wins', - // DESIGN tab datasets (fetched from DB via reports-api-v6) - // UI Design Wins (by member) - 1138: (props) => { - const cols = parseTableDef(props.table) || []; - const propList = cols.map(c => String(c.property || '')); - const handleProps = collectHandleProps(cols); - const valueProp = propList.find(p => p.toLowerCase().includes('win')) - || propList.find(p => (p && p.toLowerCase() !== 'rank' && !p.toLowerCase().includes('handle'))) - || 'wins'; - return { - path: '/statistics/design/ui-design-wins', - transform: rows => (Array.isArray(rows) ? rows : [rows]).map((r) => { - const handleValue = pickDefined( - r && r.handle, - r && r.member_handle, - r && r.winner_handle, - null, - ); - const winsValue = Number(pickDefined(r && r.wins_count, r && r.count, 0)) || 0; - const mapped = {}; - if (valueProp) mapped[valueProp] = winsValue; - if (valueProp !== 'wins') mapped.wins = winsValue; - if (valueProp !== 'count') mapped.count = winsValue; - assignHandleProps(mapped, handleValue, handleProps); - return mapped; - }), - }; - }, - // Design F2F Wins (by member) - 1141: (props) => { - const cols = parseTableDef(props.table) || []; - const propList = cols.map(c => String(c.property || '')); - const handleProps = collectHandleProps(cols); - const valueProp = propList.find(p => p.toLowerCase().includes('win')) - || propList.find(p => (p && p.toLowerCase() !== 'rank' && !p.toLowerCase().includes('handle'))) - || 'wins'; - return { - path: '/statistics/design/f2f-wins', - transform: rows => (Array.isArray(rows) ? rows : [rows]).map((r) => { - const handleValue = pickDefined( - r && r.handle, - r && r.member_handle, - r && r.winner_handle, - null, - ); - const winsValue = Number(pickDefined(r && r.wins_count, r && r.count, 0)) || 0; - const mapped = {}; - if (valueProp) mapped[valueProp] = winsValue; - if (valueProp !== 'wins') mapped.wins = winsValue; - if (valueProp !== 'count') mapped.count = winsValue; - assignHandleProps(mapped, handleValue, handleProps); - return mapped; - }), - }; - }, - // LUX 1st Place Wins (Design) - 1571: (props) => { - const cols = parseTableDef(props.table) || []; - const propList = cols.map(c => String(c.property || '')); - const handleProps = collectHandleProps(cols); - const challengeCountProp = propList.find(p => p.toLowerCase().includes('challenge_stats.count')) - || 'challenge_stats.count'; - const countProp = propList.find(p => p.toLowerCase() === 'count') || 'count'; - const maxRatingProp = propList.find(p => p.toLowerCase().includes('max_rating')) - || 'member_profile_advanced.max_rating'; - const rankProp = propList.find(p => p.toLowerCase() === 'rank') || 'rank'; - return { - path: '/statistics/design/lux-first-place-wins', - transform: rows => (Array.isArray(rows) ? rows : [rows]).map((r) => { - const wins = Number(pickDefined(r && r.wins_count, r && r.count, 0)) || 0; - const maxRating = pickDefined(r && r.max_rating, null); - const rankValue = Number(pickDefined(r && r.rank, 0)) || 0; - const handleValue = pickDefined( - r && r.handle, - r && r.winner_handle, - r && r['challenge_stats.winner_handle'], - r && r.member_handle, - null, - ); - const mapped = {}; - if (challengeCountProp) mapped[challengeCountProp] = wins; - if (countProp && countProp !== challengeCountProp) mapped[countProp] = wins; - if (challengeCountProp !== 'wins') mapped.wins = wins; - if (challengeCountProp !== 'count' && countProp !== 'count') mapped.count = wins; - if (maxRatingProp) mapped[maxRatingProp] = maxRating; - if (rankProp) mapped[rankProp] = rankValue; - assignHandleProps(mapped, handleValue, handleProps); - return mapped; - }), - }; - }, - // LUX Placements (Design) - 1572: (props) => { - const cols = parseTableDef(props.table) || []; - const propList = cols.map(c => String(c.property || '')); - const handleProps = collectHandleProps(cols); - const challengeCountProp = propList.find(p => p.toLowerCase().includes('challenge_stats.count')) - || 'challenge_stats.count'; - const countProp = propList.find(p => p.toLowerCase() === 'count') || 'count'; - const maxRatingProp = propList.find(p => p.toLowerCase().includes('max_rating')) - || 'member_profile_advanced.max_rating'; - const rankProp = propList.find(p => p.toLowerCase() === 'rank') || 'rank'; - return { - path: '/statistics/design/lux-placements', - transform: rows => (Array.isArray(rows) ? rows : [rows]).map((r) => { - const placements = Number(pickDefined(r && r.placements_count, r && r.count, 0)) || 0; - const maxRating = pickDefined(r && r.max_rating, null); - const rankValue = Number(pickDefined(r && r.rank, 0)) || 0; - const handleValue = pickDefined( - r && r.handle, - r && r.winner_handle, - r && r['challenge_stats.winner_handle'], - r && r.member_handle, - null, - ); - const mapped = {}; - if (challengeCountProp) mapped[challengeCountProp] = placements; - if (countProp && countProp !== challengeCountProp) mapped[countProp] = placements; - if (challengeCountProp !== 'wins') mapped.wins = placements; - if (challengeCountProp !== 'count' && countProp !== 'count') mapped.count = placements; - if (maxRatingProp) mapped[maxRatingProp] = maxRating; - if (rankProp) mapped[rankProp] = rankValue; - assignHandleProps(mapped, handleValue, handleProps); - return mapped; - }), - }; - }, - // RUX 1st Place Wins (Design) - 1573: (props) => { - const cols = parseTableDef(props.table) || []; - const propList = cols.map(c => String(c.property || '')); - const handleProps = collectHandleProps(cols); - const challengeCountProp = propList.find(p => p.toLowerCase().includes('challenge_stats.count')) - || 'challenge_stats.count'; - const countProp = propList.find(p => p.toLowerCase() === 'count') || 'count'; - const maxRatingProp = propList.find(p => p.toLowerCase().includes('max_rating')) - || 'member_profile_advanced.max_rating'; - const rankProp = propList.find(p => p.toLowerCase() === 'rank') || 'rank'; - return { - path: '/statistics/design/rux-first-place-wins', - transform: rows => (Array.isArray(rows) ? rows : [rows]).map((r) => { - const wins = Number(pickDefined(r && r.wins_count, r && r.count, 0)) || 0; - const maxRating = pickDefined(r && r.max_rating, null); - const rankValue = Number(pickDefined(r && r.rank, 0)) || 0; - const handleValue = pickDefined( - r && r.handle, - r && r.winner_handle, - r && r['challenge_stats.winner_handle'], - r && r.member_handle, - null, - ); - const mapped = {}; - if (challengeCountProp) mapped[challengeCountProp] = wins; - if (countProp && countProp !== challengeCountProp) mapped[countProp] = wins; - if (challengeCountProp !== 'wins') mapped.wins = wins; - if (challengeCountProp !== 'count' && countProp !== 'count') mapped.count = wins; - if (maxRatingProp) mapped[maxRatingProp] = maxRating; - if (rankProp) mapped[rankProp] = rankValue; - assignHandleProps(mapped, handleValue, handleProps); - return mapped; - }), - }; - }, - // RUX Placements (Design) - 1574: (props) => { - const cols = parseTableDef(props.table) || []; - const propList = cols.map(c => String(c.property || '')); - const handleProps = collectHandleProps(cols); - const challengeCountProp = propList.find(p => p.toLowerCase().includes('challenge_stats.count')) - || 'challenge_stats.count'; - const countProp = propList.find(p => p.toLowerCase() === 'count') || 'count'; - const maxRatingProp = propList.find(p => p.toLowerCase().includes('max_rating')) - || 'member_profile_advanced.max_rating'; - const rankProp = propList.find(p => p.toLowerCase() === 'rank') || 'rank'; - return { - path: '/statistics/design/rux-placements', - transform: rows => (Array.isArray(rows) ? rows : [rows]).map((r) => { - const placements = Number(pickDefined(r && r.placements_count, r && r.count, 0)) || 0; - const maxRating = pickDefined(r && r.max_rating, null); - const rankValue = Number(pickDefined(r && r.rank, 0)) || 0; - const handleValue = pickDefined( - r && r.handle, - r && r.winner_handle, - r && r['challenge_stats.winner_handle'], - r && r.member_handle, - null, - ); - const mapped = {}; - if (challengeCountProp) mapped[challengeCountProp] = placements; - if (countProp && countProp !== challengeCountProp) mapped[countProp] = placements; - if (challengeCountProp !== 'wins') mapped.wins = placements; - if (challengeCountProp !== 'count' && countProp !== 'count') mapped.count = placements; - if (maxRatingProp) mapped[maxRatingProp] = maxRating; - if (rankProp) mapped[rankProp] = rankValue; - assignHandleProps(mapped, handleValue, handleProps); - return mapped; - }), - }; - }, }; -// Infer a reports-api-v6 endpoint and a transformer based on Looker props. -// This allows us to support the community/statistics page without knowing all -// Looker IDs ahead of time, by recognizing common property/table patterns. function inferFromProps(props) { - const { property, table, render } = props; - const cols = parseTableDef(table); - const headers = (cols || []).map(c => String(c.headerName || '')); - const lowerHeaders = headers.map(h => h.toLowerCase()); - const lowerProps = (cols || []).map(c => String(c.property || '').toLowerCase()); + const { property, render } = props; - // Header metrics (single value) - if (property && !cols) { - const prop = String(property).toLowerCase(); - if (prop === 'user.count') { - return { - path: '/statistics/general/member-count', - transform: data => [{ 'user.count': Number(data.count) || 0 }], - }; + if (property) { + const normalized = String(property).toLowerCase(); + if (normalized === 'user.count') { + return { path: '/statistics/general/member-count' }; } - if (prop === 'challenge.count') { - return { - path: '/statistics/general/completed-challenges', - transform: data => [{ 'challenge.count': Number(data.count) || 0 }], - }; + if (normalized === 'challenge.count') { + return { path: '/statistics/general/completed-challenges' }; } if ( - prop === 'total' - || prop.includes('total') - || prop.includes('prize') - || prop.includes('payment') - || prop.includes('amount') + normalized === 'total' + || normalized.includes('total') + || normalized.includes('prize') + || normalized.includes('payment') + || normalized.includes('amount') ) { return { path: '/statistics/general/total-prizes', - transform: data => [{ [property]: data.total }], - }; - } - } - - // General tab tables - if (cols && cols.length) { - const propList = cols.map(c => String(c.property || '')); - const handleProps = collectHandleProps(cols); - const hasCountry = propList.some(p => p.toLowerCase().includes('country')); - const hasHandle = propList.some(p => p.toLowerCase().includes('handle')); - const hasCopilot = propList.some(p => p.toLowerCase().includes('copilot')); - const hasReview = propList.some(p => p.toLowerCase().includes('review')); - const hasHandleHeader = lowerHeaders.some(h => h.includes('handle')); - const hasHandleMemberLink = cols.some(c => toBoolean(c && c.memberLinks)); - const hasReviewHeader = lowerHeaders.some(h => h.includes('review')); - const reviewIndicator = hasReview || hasReviewHeader; - const hasChallengeCount = propList.some(p => p.toLowerCase() === 'challenge.count'); - const hasUserCount = propList.some(p => p.toLowerCase() === 'user.count'); - const hasFirstPlace = propList.some(p => p.toLowerCase().includes('first_place')); - - // Design tab: UI Design Wins (by member) - const looksLikeUiDesignWins = ( - hasHandle - && ( - lowerHeaders.some(h => (h.includes('ui') && h.includes('win'))) - || lowerProps.some(p => (p.includes('ui') && p.includes('win'))) - || lowerHeaders.some(h => (h.includes('design') && h.includes('wins'))) - ) - ); - if (looksLikeUiDesignWins) { - const valueProp = propList.find(p => p.toLowerCase().includes('win')) - || propList.find(p => (p && p.toLowerCase() !== 'rank' && !p.toLowerCase().includes('handle'))) - || 'wins'; - return { - path: '/statistics/design/ui-design-wins', - transform: rows => (Array.isArray(rows) ? rows : [rows]).map((r) => { - const handleValue = pickDefined( - r && r.handle, - r && r.member_handle, - r && r.winner_handle, - null, - ); - const winsValue = Number(pickDefined(r && r.wins_count, r && r.count, 0)) || 0; - const mapped = {}; - if (valueProp) mapped[valueProp] = winsValue; - if (valueProp !== 'wins') mapped.wins = winsValue; - if (valueProp !== 'count') mapped.count = winsValue; - assignHandleProps(mapped, handleValue, handleProps); - return mapped; - }), - }; - } - - // Design tab: F2F Wins (by member) - const containsF2FSynonym = s => ( - s.includes('f2f') - || s.includes('first2finish') - || s.includes('first 2 finish') - || s.includes('first-to-finish') - || s.includes('first to finish') - ); - const headersMentionF2F = lowerHeaders.some(h => containsF2FSynonym(h)); - const propsMentionF2F = lowerProps.some(p => containsF2FSynonym(p)); - const headersMentionF2FWins = lowerHeaders.some( - h => containsF2FSynonym(h) && (h.includes('win') || h.includes('wins')), - ); - const propsMentionF2FWins = lowerProps.some( - p => containsF2FSynonym(p) && (p.includes('win') || p.includes('wins')), - ); - const looksLikeF2FWins = ( - hasHandle - && (headersMentionF2FWins || propsMentionF2FWins || headersMentionF2F || propsMentionF2F) - ); - if (looksLikeF2FWins) { - const valueProp = propList.find(p => p.toLowerCase().includes('win')) - || propList.find(p => (p && p.toLowerCase() !== 'rank' && !p.toLowerCase().includes('handle'))) - || 'wins'; - return { - path: '/statistics/design/f2f-wins', - transform: rows => (Array.isArray(rows) ? rows : [rows]).map((r) => { - const handleValue = pickDefined( - r && r.handle, - r && r.member_handle, - r && r.winner_handle, - null, - ); - const winsValue = Number(pickDefined(r && r.wins_count, r && r.count, 0)) || 0; - const mapped = {}; - if (valueProp) mapped[valueProp] = winsValue; - if (valueProp !== 'wins') mapped.wins = winsValue; - if (valueProp !== 'count') mapped.count = winsValue; - assignHandleProps(mapped, handleValue, handleProps); - return mapped; - }), - }; - } - - // Design tab: First Time Submitters (by date) - const looksLikeFirstTimeSubmitters = ( - hasHandle && ( - lowerHeaders.some(h => (h.includes('first time') || h.includes('first-time'))) - || lowerProps.some(p => p.includes('first') && p.includes('submission')) - || lowerProps.some(p => p.includes('date')) - ) - ); - if (looksLikeFirstTimeSubmitters) { - const nameProp = propList.find(p => p.toLowerCase().includes('handle')) || 'handle'; - const dateProp = propList.find(p => p.toLowerCase().includes('date')) - || propList.find(p => (p && p.toLowerCase() !== 'rank' && !p.toLowerCase().includes('handle'))) - || 'first_submission_date'; - return { - path: '/statistics/design/first-time-submitters', - transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ - [nameProp]: r.handle, - [dateProp]: r.first_submission_date, - })), - }; - } - - // Design tab: Countries Represented (Design submitters) - const looksLikeDesignCountries = ( - lowerHeaders.some(h => h.includes('countries represented')) - || (hasCountry && hasUserCount && lowerHeaders.some(h => h.includes('design'))) - ); - if (looksLikeDesignCountries) { - return { - path: '/statistics/design/countries-represented', - transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ - [propList.find(p => p.toLowerCase().includes('country')) || 'country.country_name']: - countryNameFromCode(r.country_code), - [propList.find(p => p.toLowerCase() === 'user.count') || 'user.count']: - Number(r.members_count) || 0, - })), - }; - } - - // Design tab: 1st Place Finishes by Country - const looksLikeDesignFirstPlace = ( - hasCountry && ( - lowerHeaders.some(h => (h.includes('1st place') || h.includes('first place'))) - || hasFirstPlace - ) - ); - if (looksLikeDesignFirstPlace) { - const nameProp = propList.find(p => p.toLowerCase().includes('country')) || 'country.country_name'; - const valueProp = propList.find(p => p.toLowerCase().includes('first_place')) - || propList.find(p => (p && p.toLowerCase() !== 'rank' && !p.toLowerCase().includes('country'))) - || 'first_place_count'; - return { - path: '/statistics/design/first-place-by-country', - transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ - [nameProp]: countryNameFromCode(r.country_code), - [valueProp]: Number(r.first_place_count) || 0, - })), - }; - } - - // Countries represented: expect country + user.count - if (hasCountry && hasUserCount) { - return { - path: '/statistics/general/countries-represented', - transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ - [propList.find(p => p.toLowerCase().includes('country')) || 'country.country_name']: - countryNameFromCode(r.country_code), - [propList.find(p => p.toLowerCase() === 'user.count') || 'user.count']: - Number(r.members_count) || 0, - })), - }; - } - - // First place by country: expect country + a numeric count field. - if (hasCountry && (hasFirstPlace || (!hasUserCount && !hasCopilot && !hasReview))) { - let valueProp = propList.find(p => p.toLowerCase().includes('first_place')) - || propList.find(p => p.toLowerCase() === 'user.count'); - if (!valueProp) { - valueProp = propList.find(p => ( - p - && !p.toLowerCase().includes('country') - && p.toLowerCase() !== 'rank' - )) || 'first_place_count'; - } - - const nameProp = propList.find(p => p.toLowerCase().includes('country')) || 'country.country_name'; - - if (valueProp && valueProp.toLowerCase() === 'user.count') { - // Do nothing; countries-represented case above will match. - } else { - return { - path: '/statistics/general/first-place-by-country', - transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ - [nameProp]: countryNameFromCode(r.country_code), - [valueProp]: Number(r.first_place_count) || 0, - })), - }; - } - } - - // Copiloted challenges: expect copilot handle + challenge.count - if ((hasCopilot || hasHandle) && hasChallengeCount) { - const nameProp = propList.find(p => p.toLowerCase().includes('copilot')) - || propList.find(p => p.toLowerCase().includes('handle')) - || 'copilot.handle'; - return { - path: '/statistics/general/copiloted-challenges', - transform: rows => (Array.isArray(rows) ? rows : [rows]).map(r => ({ - [nameProp]: r.handle, - 'challenge.count': Number(r.copiloted_challenges) || 0, - })), - }; - } - - // Reviews by member: expect handle + review count - if ((hasHandle || hasCopilot || hasHandleHeader || hasHandleMemberLink) && reviewIndicator) { - const valueProp = propList.find(p => p.toLowerCase().includes('review')) || 'review.count'; - return { - path: '/statistics/general/reviews-by-member', - transform: (rows) => { - const list = Array.isArray(rows) ? rows : [rows]; - return list.map((r) => { - const handleValue = pickDefined( - r && r.handle, - r && r.member_handle, - r && r.copilot_handle, - r && r['user.handle'], - null, - ); - const reviewCountValue = Number(pickDefined( - r && r.review_count, - r && r['review.count'], - r && r.count, - 0, - )) || 0; - const mapped = { - 'review.count': reviewCountValue, - review_count: reviewCountValue, - count: reviewCountValue, - }; - assignHandleProps(mapped, handleValue, handleProps); - if ( - valueProp - && valueProp !== 'review.count' - && valueProp !== 'review_count' - && valueProp !== 'count' - ) { - mapped[valueProp] = reviewCountValue; - } - if (r && r.rank !== undefined && r.rank !== null) { - mapped.rank = Number(r.rank) || 0; - } - if (r && r.member_id !== undefined) mapped.member_id = r.member_id; - return mapped; - }); - }, + transform: data => ([{ [property]: data.total }]), }; } } - // Render-based single value (e.g. used to prefix with currency symbol) - if (!property && !cols && render) { + if (!property && render) { try { - const r = String(render).replace(/&q;/g, '"').replace(/'/g, '"'); - const matches = Array.from(r.matchAll(/data\s*\[\s*0\s*\]\s*\[\s*"([^"]+)"\s*\]/g)); - const referenced = matches.map(m => (m && m[1] ? m[1] : '')); - const hasTotalPrizes = referenced.some((p) => { - const pl = String(p).toLowerCase(); - return pl.includes('total') || pl.includes('prize') || pl.includes('payment') || pl.includes('amount'); + const source = String(render).replace(/&q;/g, '"').replace(/'/g, '"'); + const matches = Array.from( + source.matchAll(/data\s*\[\s*0\s*]\s*\[\s*"([^"]+)"\s*]/g), + ); + const referenced = matches.map(match => (match && match[1] ? match[1] : '')); + const hasTotalPrizes = referenced.some((ref) => { + const lower = ref.toLowerCase(); + return ( + lower.includes('total') + || lower.includes('prize') + || lower.includes('payment') + || lower.includes('amount') + ); }); if (hasTotalPrizes) { - const key = referenced.find(p => p) || 'total'; + const key = referenced.find(ref => ref) || 'total'; return { path: '/statistics/general/total-prizes', - transform: data => [{ [key]: data.total }], + transform: data => ([{ [key]: data.total }]), }; } - } catch (e) { - // swallow and fall back to legacy looker + } catch (err) { + // swallow and fall through to unsupported handling } } @@ -1048,15 +97,10 @@ function inferFromProps(props) { export default function SmartLooker(props) { const { lookerId } = props; - const directEntry = LOOKER_TO_REPORTS_MAP[lookerId]; - let directConfig = null; - if (typeof directEntry === 'function') directConfig = directEntry(props); - else if (typeof directEntry === 'string') directConfig = { path: directEntry }; - else if (directEntry && typeof directEntry === 'object') directConfig = directEntry; - - const inferred = inferFromProps(props); - const reportsPath = (directConfig && directConfig.path) || (inferred && inferred.path); - const transformer = (directConfig && directConfig.transform) || (inferred && inferred.transform); + const directPath = LOOKER_TO_REPORTS_PATH[lookerId]; + const inferred = directPath ? null : inferFromProps(props); + const reportsPath = directPath || (inferred && inferred.path); + const transformer = inferred && inferred.transform; const [state, setState] = React.useState({ loading: !!reportsPath, @@ -1066,9 +110,11 @@ export default function SmartLooker(props) { React.useEffect(() => { let cancelled = false; + async function load() { if (!reportsPath) return; setState(s => ({ ...s, loading: true, error: null })); + try { const res = await fetch(`${config.API.V6}/reports${reportsPath}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); @@ -1079,36 +125,46 @@ export default function SmartLooker(props) { if (Array.isArray(transformed)) lookerData = transformed; else if (transformed !== undefined && transformed !== null) { lookerData = [transformed]; - } else lookerData = []; + } else { + lookerData = []; + } } - if (!cancelled) setState({ loading: false, error: null, lookerInfo: { lookerData } }); - } catch (e) { if (!cancelled) { setState({ loading: false, - error: e.message, + error: null, + lookerInfo: { lookerData }, + }); + } + } catch (err) { + if (!cancelled) { + setState({ + loading: false, + error: err.message, lookerInfo: { lookerData: [] }, }); } } } + load(); - return () => { cancelled = true; }; - }, [lookerId, reportsPath]); + return () => { + cancelled = true; + }; + }, [lookerId, reportsPath, transformer]); if (!reportsPath) { - // Fall back to legacy behavior for non-mapped lookerIds - return ( - - ); + if (process.env.NODE_ENV !== 'production') { + // eslint-disable-next-line no-console + console.error(`SmartLooker: no reports mapping for Looker ID ${lookerId}`); + } + return 'Statistics report mapping missing.'; } if (state.loading && !state.lookerInfo) return 'loading...'; if (state.error) return state.error; - return ( - - ); + return ; } SmartLooker.propTypes = { From c6a5aa326082701c7183dd822cbe0cac757477ac Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Fri, 10 Oct 2025 15:15:45 +1100 Subject: [PATCH 54/76] Statistics finalization --- src/shared/containers/SmartLooker.jsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/shared/containers/SmartLooker.jsx b/src/shared/containers/SmartLooker.jsx index 55d419a532..c9606e502b 100644 --- a/src/shared/containers/SmartLooker.jsx +++ b/src/shared/containers/SmartLooker.jsx @@ -19,8 +19,10 @@ const LOOKER_TO_REPORTS_PATH = { 1135: '/statistics/design/first-place-by-country', 1136: '/statistics/design/countries-represented', 1138: '/statistics/design/ui-design-wins', + 1140: '/statistics/design/wireframe-wins', 1141: '/statistics/design/f2f-wins', 1146: '/statistics/general/copiloted-challenges', + 1148: '/statistics/general/countries-represented', 1149: '/statistics/general/first-place-by-country', 1150: '/statistics/general/reviews-by-member', 1172: '/statistics/development/first-time-submitters', @@ -39,9 +41,7 @@ const LOOKER_TO_REPORTS_PATH = { 1700: '/statistics/qa/wins', }; -function inferFromProps(props) { - const { property, render } = props; - +function inferFromProps(property, render) { if (property) { const normalized = String(property).toLowerCase(); if (normalized === 'user.count') { @@ -96,9 +96,12 @@ function inferFromProps(props) { } export default function SmartLooker(props) { - const { lookerId } = props; + const { lookerId, property, render } = props; const directPath = LOOKER_TO_REPORTS_PATH[lookerId]; - const inferred = directPath ? null : inferFromProps(props); + const inferred = React.useMemo( + () => (directPath ? null : inferFromProps(property, render)), + [directPath, property, render], + ); const reportsPath = directPath || (inferred && inferred.path); const transformer = inferred && inferred.transform; @@ -169,4 +172,6 @@ export default function SmartLooker(props) { SmartLooker.propTypes = { lookerId: PT.string.isRequired, + property: PT.string, + render: PT.func, }; From 3f5855c8509f55c342e824dc9f5ab4e68792887f Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Fri, 10 Oct 2025 15:31:44 +1100 Subject: [PATCH 55/76] Lint --- src/shared/containers/SmartLooker.jsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/shared/containers/SmartLooker.jsx b/src/shared/containers/SmartLooker.jsx index c9606e502b..7770912fa0 100644 --- a/src/shared/containers/SmartLooker.jsx +++ b/src/shared/containers/SmartLooker.jsx @@ -175,3 +175,8 @@ SmartLooker.propTypes = { property: PT.string, render: PT.func, }; + +SmartLooker.defaultProps = { + property: undefined, + render: undefined, +}; From 8bc283143b31ae01df478e3d91c6087753ee7e40 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Tue, 21 Oct 2025 07:58:06 +1100 Subject: [PATCH 56/76] Hide "Topgear Task" from the filter menu on the challenge list --- .../challenge-listing/FilterPanel.jsx | 38 +++++++++++++++++-- .../reducers/challenge-listing/index.js | 18 +++++++-- .../utils/challenge-listing/constants.js | 2 + 3 files changed, 50 insertions(+), 8 deletions(-) create mode 100644 src/shared/utils/challenge-listing/constants.js diff --git a/src/shared/containers/challenge-listing/FilterPanel.jsx b/src/shared/containers/challenge-listing/FilterPanel.jsx index 36a342925f..367e70f732 100644 --- a/src/shared/containers/challenge-listing/FilterPanel.jsx +++ b/src/shared/containers/challenge-listing/FilterPanel.jsx @@ -17,10 +17,10 @@ import { connect } from 'react-redux'; import qs from 'qs'; import _ from 'lodash'; import { createStaticRanges } from 'utils/challenge-listing/date-range'; +import { EXCLUDED_CHALLENGE_TYPE_NAMES } from 'utils/challenge-listing/constants'; const MIN = 60 * 1000; - export class Container extends React.Component { constructor(props) { super(props); @@ -96,13 +96,26 @@ export class Container extends React.Component { setFilterState, validTypes, } = this.props; + const currentTypes = filterState.types || []; - if (!filterState.types.length && validTypes.length && !this.initialDefaultChallengeTypes) { + if (!currentTypes.length && validTypes.length && !this.initialDefaultChallengeTypes) { setFilterState({ ..._.clone(filterState), types: validTypes.map(item => item.abbreviation), }); this.initialDefaultChallengeTypes = true; + } else if (validTypes.length && currentTypes.length) { + const validAbbreviations = validTypes.map(item => item.abbreviation); + const sanitizedTypes = currentTypes.filter(type => validAbbreviations.includes(type)); + if (sanitizedTypes.length !== currentTypes.length) { + if (!sanitizedTypes.length) { + this.initialDefaultChallengeTypes = false; + } + setFilterState({ + ..._.clone(filterState), + types: sanitizedTypes, + }); + } } } @@ -222,6 +235,23 @@ function mapDispatchToProps(dispatch) { function mapStateToProps(state, ownProps) { const cl = state.challengeListing; const tc = state.tcCommunities; + const filteredChallengeTypes = cl.challengeTypes + .filter(type => !EXCLUDED_CHALLENGE_TYPE_NAMES.includes(type.name)); + const excludedTypeAbbreviations = cl.challengeTypes + .filter(type => EXCLUDED_CHALLENGE_TYPE_NAMES.includes(type.name)) + .map(type => type.abbreviation); + let filterState = cl.filter; + const existingTypes = Array.isArray(cl.filter.types) ? cl.filter.types : []; + if (excludedTypeAbbreviations.length && existingTypes.length) { + const sanitizedTypes = existingTypes + .filter(type => !excludedTypeAbbreviations.includes(type)); + if (sanitizedTypes.length !== existingTypes.length) { + filterState = { + ...cl.filter, + types: sanitizedTypes, + }; + } + } return { ...ownProps, ...state.challengeListing.filterPanel, @@ -229,9 +259,9 @@ function mapStateToProps(state, ownProps) { communityFilters: tc.list.data, communityList: tc.list, defaultCommunityId: ownProps.defaultCommunityId, - filterState: cl.filter, + filterState, loadingTypes: cl.loadingChallengeTypes, - validTypes: cl.challengeTypes, + validTypes: filteredChallengeTypes, selectedCommunityId: cl.selectedCommunityId, auth: state.auth, tokenV2: state.auth.tokenV2, diff --git a/src/shared/reducers/challenge-listing/index.js b/src/shared/reducers/challenge-listing/index.js index 71acf75ebb..a35a06d046 100644 --- a/src/shared/reducers/challenge-listing/index.js +++ b/src/shared/reducers/challenge-listing/index.js @@ -15,6 +15,7 @@ import { actions as actionsUtils, } from 'topcoder-react-lib'; import { REVIEW_OPPORTUNITY_TYPES } from 'utils/tc'; +import { EXCLUDED_CHALLENGE_TYPE_NAMES } from 'utils/challenge-listing/constants'; import filterPanel from './filter-panel'; import sidebar, { factory as sidebarFactory } from './sidebar'; @@ -405,15 +406,24 @@ function onSetFilter(state, { payload }) { /* Validation of filter parameters: they may come from URL query, thus * validation is not a bad idea. As you may note, at the moment we do not * do it very carefuly (many params are not validated). */ + const basePayload = _.isPlainObject(payload) ? payload : {}; + const sanitizedPayload = { ...basePayload }; + const excludedTypeAbbreviations = state.challengeTypes + .filter(type => EXCLUDED_CHALLENGE_TYPE_NAMES.includes(type.name)) + .map(type => type.abbreviation); + if (excludedTypeAbbreviations.length && Array.isArray(basePayload.types)) { + sanitizedPayload.types = basePayload.types + .filter(type => !excludedTypeAbbreviations.includes(type)); + } const filter = _.pickBy(_.pick( - payload, + sanitizedPayload, ['tags', 'types', 'search', 'startDateEnd', 'endDateStart', 'groups', 'events', 'tracks', 'tco', 'isInnovationChallenge'], ), value => (!_.isArray(value) && value && value !== '') || (_.isArray(value) && value.length > 0)); const emptyArrayAllowedFields = ['types']; emptyArrayAllowedFields.forEach((field) => { - if (_.isEqual(payload[field], [])) { - filter[field] = payload[field]; + if (_.isEqual(sanitizedPayload[field], [])) { + filter[field] = sanitizedPayload[field]; } }); @@ -437,7 +447,7 @@ function onSetFilter(state, { payload }) { // console.log(`======`); return { ...state, - filter: _.assign({}, state.filter, payload), + filter: _.assign({}, state.filter, sanitizedPayload), /* Page numbers of past/upcoming challenges depend on the filters. To keep * the code simple we just reset them each time a filter is modified. */ diff --git a/src/shared/utils/challenge-listing/constants.js b/src/shared/utils/challenge-listing/constants.js new file mode 100644 index 0000000000..1dff854baf --- /dev/null +++ b/src/shared/utils/challenge-listing/constants.js @@ -0,0 +1,2 @@ +export const EXCLUDED_CHALLENGE_TYPE_NAMES = ['Topgear Task']; + From 9537e53b6da35e1e8bcfd601d097fb32508f31c9 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Wed, 22 Oct 2025 12:34:05 +1100 Subject: [PATCH 57/76] Bunch of updates to support MMs in v6 on the challenge details screen, including submissions, my submissions and dashboard tabs --- config/default.js | 1 + config/production.js | 1 + .../MySubmissions/SubmissionsDetail/index.jsx | 8 +- .../MySubmissions/SubmissionsList/index.jsx | 74 ++- .../challenge-detail/MySubmissions/index.jsx | 5 +- .../SubmissionHistoryRow/index.jsx | 47 +- .../Submissions/SubmissionRow/index.jsx | 158 +++--- .../challenge-detail/Submissions/index.jsx | 61 ++- .../containers/challenge-detail/index.jsx | 293 +++++++++-- src/shared/services/reviewSummations.js | 73 +++ .../challenge-detail/submission-status.js | 71 +++ .../utils/challenge-listing/constants.js | 4 +- src/shared/utils/mm-review-summations.js | 459 ++++++++++++++++++ 13 files changed, 1116 insertions(+), 139 deletions(-) create mode 100644 src/shared/services/reviewSummations.js create mode 100644 src/shared/utils/challenge-detail/submission-status.js create mode 100644 src/shared/utils/mm-review-summations.js diff --git a/config/default.js b/config/default.js index f8c2f708d4..7491d03ff9 100644 --- a/config/default.js +++ b/config/default.js @@ -172,6 +172,7 @@ module.exports = { }], TIMELINE_WALL_API: 'https://api.topcoder-dev.com/v5/timeline-wall', REVIEW_TYPES_API_URL: '/reviewTypes', + REVIEW_SUMMATIONS_API_URL: '/reviewSummations', }, /* Information about Topcoder user groups can be cached in various places. diff --git a/config/production.js b/config/production.js index 792611648b..c37c4fd63b 100644 --- a/config/production.js +++ b/config/production.js @@ -65,6 +65,7 @@ module.exports = { THRIVE_FEED: 'https://topcoder.com/api/feeds/thrive', TIMELINE_WALL_API: 'https://api.topcoder.com/v5/timeline-wall', REVIEW_TYPES_API_URL: '/reviewTypes', + REVIEW_SUMMATIONS_API_URL: '/reviewSummations', }, /* Filestack configuration for uploading Submissions * These are for the production back end */ diff --git a/src/shared/components/challenge-detail/MySubmissions/SubmissionsDetail/index.jsx b/src/shared/components/challenge-detail/MySubmissions/SubmissionsDetail/index.jsx index 71ca8c6fea..821ad64351 100644 --- a/src/shared/components/challenge-detail/MySubmissions/SubmissionsDetail/index.jsx +++ b/src/shared/components/challenge-detail/MySubmissions/SubmissionsDetail/index.jsx @@ -7,6 +7,7 @@ import PT from 'prop-types'; import _ from 'lodash'; import cn from 'classnames'; import sortList from 'utils/challenge-detail/sort'; +import { getSubmissionStatus } from 'utils/challenge-detail/submission-status'; import Tooltip from 'components/Tooltip'; import IconClose from 'assets/images/icon-close-green.svg'; @@ -145,6 +146,9 @@ class SubmissionsDetailView extends React.Component { const { onCancel, submission, onSortChange } = this.props; let { finalScore } = submission; const { sortedSubmissions } = this.state; + const { isAccepted } = getSubmissionStatus(submission); + const finalStatusStyleName = isAccepted ? 'status-complete' : 'status-in-queue'; + const finalStatusLabel = isAccepted ? 'Complete' : 'In Queue'; const { field, sort } = this.getSubmissionsSortParam(); const revertSort = (sort === 'desc') ? 'asc' : 'desc'; @@ -373,9 +377,7 @@ class SubmissionsDetailView extends React.Component {
Status
- {submission.provisionalScoringIsCompleted ? ( - Complete - ) : (In Queue)} + {finalStatusLabel}
diff --git a/src/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx b/src/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx index 368e00cf1e..3a86b25541 100644 --- a/src/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx +++ b/src/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx @@ -10,6 +10,7 @@ import { PrimaryButton, Modal } from 'topcoder-react-ui-kit'; import PT from 'prop-types'; import { services } from 'topcoder-react-lib'; import sortList from 'utils/challenge-detail/sort'; +import { getSubmissionStatus } from 'utils/challenge-detail/submission-status'; import IconClose from 'assets/images/icon-close-green.svg'; import DateSortIcon from 'assets/images/icon-date-sort.svg'; @@ -25,6 +26,43 @@ import style from './styles.scss'; const { getService } = services.submissions; +const collectReviewSummations = (submission) => { + const summations = []; + if (!submission) { + return summations; + } + if (Array.isArray(submission.reviewSummations)) { + summations.push(...submission.reviewSummations); + } + if (Array.isArray(submission.reviewSummation)) { + summations.push(...submission.reviewSummation); + } + return summations; +}; + +const getReviewSummationSubmissionId = (submission) => { + const summations = collectReviewSummations(submission); + const match = _.find(summations, s => s && !_.isNil(s.submissionId)); + if (!match) { + return null; + } + return `${match.submissionId}`; +}; + +const getDisplaySubmissionId = (submission) => { + const fromSummation = getReviewSummationSubmissionId(submission); + if (fromSummation) { + return fromSummation; + } + if (submission && !_.isNil(submission.submissionId)) { + return `${submission.submissionId}`; + } + if (submission && !_.isNil(submission.id)) { + return `${submission.id}`; + } + return ''; +}; + class SubmissionsListView extends React.Component { constructor(props) { @@ -100,16 +138,27 @@ class SubmissionsListView extends React.Component { return sortList(submissions, field, sort, (a, b) => { let valueA = 0; let valueB = 0; - const valueIsString = false; + let valueIsString = false; switch (field) { case 'Submission ID': { - valueA = a.id; - valueB = b.id; + const idA = getDisplaySubmissionId(a); + const idB = getDisplaySubmissionId(b); + const numericA = Number(idA); + const numericB = Number(idB); + const useNumericSort = idA !== '' && idB !== '' && _.isFinite(numericA) && _.isFinite(numericB); + if (useNumericSort) { + valueA = numericA; + valueB = numericB; + } else { + valueA = idA; + valueB = idB; + valueIsString = true; + } break; } case 'Status': { - valueA = a.provisionalScoringIsCompleted; - valueB = b.provisionalScoringIsCompleted; + valueA = getSubmissionStatus(a).isAccepted ? 1 : 0; + valueB = getSubmissionStatus(b).isAccepted ? 1 : 0; break; } case 'Final': { @@ -391,8 +440,15 @@ class SubmissionsListView extends React.Component { } else { provisionalScore = 'N/A'; } + const { isAccepted } = getSubmissionStatus(mySubmission); + const statusStyleName = isAccepted ? 'accepted' : 'queue'; + const statusLabel = isAccepted ? 'Accepted' : 'In Queue'; + const displaySubmissionId = getDisplaySubmissionId(mySubmission); return ( -
+
Submission Id
- {mySubmission.id} + {displaySubmissionId}
Status
- {mySubmission.provisionalScoringIsCompleted ? ( - Accepted - ) : In Queue} + {statusLabel}
diff --git a/src/shared/components/challenge-detail/MySubmissions/index.jsx b/src/shared/components/challenge-detail/MySubmissions/index.jsx index 1cf44d994e..d8ba171f95 100644 --- a/src/shared/components/challenge-detail/MySubmissions/index.jsx +++ b/src/shared/components/challenge-detail/MySubmissions/index.jsx @@ -197,7 +197,10 @@ MySubmissionsView.propTypes = { submissionEnded: PT.bool.isRequired, isMM: PT.bool.isRequired, isLegacyMM: PT.bool.isRequired, - loadingMMSubmissionsForChallengeId: PT.string.isRequired, + loadingMMSubmissionsForChallengeId: PT.oneOfType([ + PT.string, + PT.oneOf([null]), + ]).isRequired, auth: PT.shape().isRequired, loadMMSubmissions: PT.func.isRequired, mySubmissions: PT.arrayOf(PT.shape()).isRequired, diff --git a/src/shared/components/challenge-detail/Submissions/SubmissionRow/SubmissionHistoryRow/index.jsx b/src/shared/components/challenge-detail/Submissions/SubmissionRow/SubmissionHistoryRow/index.jsx index 45d244cac8..54b7289dda 100644 --- a/src/shared/components/challenge-detail/Submissions/SubmissionRow/SubmissionHistoryRow/index.jsx +++ b/src/shared/components/challenge-detail/Submissions/SubmissionRow/SubmissionHistoryRow/index.jsx @@ -36,26 +36,40 @@ export default function SubmissionHistoryRow({ }) { // todo: hide download button until update submissions API const hideDownloadForMMRDM = true; + const parseScore = (value) => { + const numeric = Number(value); + return Number.isFinite(numeric) ? numeric : null; + }; + const provisionalScoreValue = parseScore(provisionalScore); + const finalScoreValue = parseScore(finalScore); + const submissionMoment = submissionTime ? moment(submissionTime) : null; + const submissionTimeDisplay = submissionMoment + ? `${submissionMoment.format('DD MMM YYYY')} ${submissionMoment.format('HH:mm:ss')}` + : 'N/A'; const getInitialReviewResult = () => { - if (provisionalScore && provisionalScore < 0) return ; + if (status === 'failed') return ; + if (status === 'in-review') return ; + if (status === 'queued') return ; + if (provisionalScoreValue === null) return 'N/A'; + if (provisionalScoreValue < 0) return ; switch (status) { case 'completed': - return provisionalScore; - case 'in-review': - return ; - case 'queued': - return ; - case 'failed': - return ; + return provisionalScoreValue; default: - return provisionalScore === '-' ? 'N/A' : provisionalScore; + return provisionalScoreValue; } }; const getFinalScore = () => { - if (isMM && finalScore && finalScore > -1 && isReviewPhaseComplete) { - return finalScore; + if (!isReviewPhaseComplete) { + return 'N/A'; + } + if (finalScoreValue === null) { + return 'N/A'; } - return 'N/A'; + if (finalScoreValue < 0) { + return 0; + } + return finalScoreValue; }; return ( @@ -80,7 +94,7 @@ export default function SubmissionHistoryRow({
TIME
- {moment(submissionTime).format('DD MMM YYYY')} {moment(submissionTime).format('HH:mm:ss')} + {submissionTimeDisplay}
{ @@ -129,13 +143,18 @@ SubmissionHistoryRow.propTypes = { finalScore: PT.oneOfType([ PT.number, PT.string, + PT.oneOf([null]), ]), status: PT.string.isRequired, provisionalScore: PT.oneOfType([ PT.number, PT.string, + PT.oneOf([null]), ]), - submissionTime: PT.string.isRequired, + submissionTime: PT.oneOfType([ + PT.string, + PT.oneOf([null]), + ]).isRequired, challengeStatus: PT.string.isRequired, isReviewPhaseComplete: PT.bool, auth: PT.shape().isRequired, diff --git a/src/shared/components/challenge-detail/Submissions/SubmissionRow/index.jsx b/src/shared/components/challenge-detail/Submissions/SubmissionRow/index.jsx index b2fc9cca9b..9ee00ea4e1 100644 --- a/src/shared/components/challenge-detail/Submissions/SubmissionRow/index.jsx +++ b/src/shared/components/challenge-detail/Submissions/SubmissionRow/index.jsx @@ -19,46 +19,78 @@ import SubmissionHistoryRow from './SubmissionHistoryRow'; import style from './style.scss'; export default function SubmissionRow({ - isMM, isRDM, openHistory, member, submissions, score, toggleHistory, challengeStatus, + isMM, isRDM, openHistory, member, submissions, toggleHistory, challengeStatus, isReviewPhaseComplete, finalRank, provisionalRank, onShowPopup, rating, viewAsTable, numWinners, auth, isLoggedIn, }) { + const submissionList = Array.isArray(submissions) ? submissions : []; + const latestSubmission = submissionList[0] || {}; const { - submissionTime, provisionalScore, status, submissionId, - } = submissions[0]; - let { finalScore } = submissions[0]; - finalScore = (!finalScore && finalScore < 0) || !isReviewPhaseComplete ? '-' : finalScore; - let initialScore; - if (provisionalScore >= 0 || provisionalScore === -1) { - initialScore = provisionalScore; - } + status, + submissionId, + submissionTime, + } = latestSubmission; + + const parseScore = (value) => { + const numeric = Number(value); + return Number.isFinite(numeric) ? numeric : null; + }; + + const provisionalScore = parseScore(_.get(latestSubmission, 'provisionalScore')); + const finalScore = parseScore(_.get(latestSubmission, 'finalScore')); const getInitialReviewResult = () => { - const s = isMM ? _.get(score, 'provisional', initialScore) : initialScore; - if (s && s < 0) return ; - switch (status) { - case 'completed': - return s; - case 'in-review': - return ; - case 'queued': - return ; - case 'failed': - return ; - default: - return s; + if (status === 'failed') { + return ; + } + if (status === 'in-review') { + return ; + } + if (status === 'queued') { + return ; + } + if (_.isNil(provisionalScore)) { + return 'N/A'; + } + if (provisionalScore < 0) { + return ; } + return provisionalScore; }; const getFinalReviewResult = () => { - const s = isMM && isReviewPhaseComplete ? _.get(score, 'final', finalScore) : finalScore; - if (isReviewPhaseComplete) { - if (s && s < 0) return 0; - return s; + if (!isReviewPhaseComplete) { + return 'N/A'; } - return 'N/A'; + if (_.isNil(finalScore)) { + return 'N/A'; + } + if (finalScore < 0) { + return 0; + } + return finalScore; }; + const initialReviewResult = getInitialReviewResult(); + const finalReviewResult = getFinalReviewResult(); + + const submissionMoment = submissionTime ? moment(submissionTime) : null; + const submissionDateDisplay = submissionMoment + ? `${submissionMoment.format('DD MMM YYYY')} ${submissionMoment.format('HH:mm:ss')}` + : 'N/A'; + + const finalRankDisplay = (isReviewPhaseComplete && _.isFinite(finalRank)) ? finalRank : 'N/A'; + const provisionalRankDisplay = _.isFinite(provisionalRank) ? provisionalRank : 'N/A'; + const ratingDisplay = _.isFinite(rating) ? rating : '-'; + const ratingLevelStyle = `col level-${getRatingLevel(rating)}`; + const memberHandle = member || ''; + const memberDisplay = memberHandle || '-'; + const memberProfileUrl = memberHandle ? `${window.origin}/members/${memberHandle}` : null; + const memberLinkTarget = `${_.includes(window.origin, 'www') ? '_self' : '_blank'}`; + const memberForHistory = memberHandle || memberDisplay; + const latestSubmissionId = submissionId || 'N/A'; + const submissionCount = submissionList.length; + return (
@@ -67,14 +99,12 @@ export default function SubmissionRow({
FINAL RANK
- { - isReviewPhaseComplete ? finalRank || 'N/A' : 'N/A' - } + {finalRankDisplay}
PROVISIONAL RANK
- { provisionalRank || 'N/A' } + { provisionalRankDisplay }
@@ -82,37 +112,43 @@ export default function SubmissionRow({ }
RATING
- - {rating || '-'} + + {ratingDisplay}
USERNAME
-
- {member || '-'} - + { + memberProfileUrl ? ( + + {memberDisplay} + + ) : ( + {memberDisplay} + ) + }
FINAL SCORE
- {getFinalReviewResult()} + {finalReviewResult}
PROVISIONAL SCORE
- {getInitialReviewResult() ? getInitialReviewResult() : 'N/A'} + {initialReviewResult}
SUBMISSION DATE
- {moment(submissionTime).format('DD MMM YYYY')} {moment(submissionTime).format('HH:mm:ss')} + {submissionDateDisplay}
@@ -123,7 +159,7 @@ export default function SubmissionRow({ > History ( - {submissions.length} + {submissionCount} ) @@ -143,7 +179,7 @@ export default function SubmissionRow({

- Latest Submission: {submissionId} + Latest Submission: {latestSubmissionId}
@@ -182,17 +218,17 @@ export default function SubmissionRow({
{ - submissions.map((submissionHistory, index) => ( + submissionList.map((submissionHistory, index) => ( {}, - score: {}, isReviewPhaseComplete: false, finalRank: null, provisionalRank: null, @@ -234,29 +269,20 @@ SubmissionRow.propTypes = { provisionalScore: PT.oneOfType([ PT.number, PT.string, + PT.oneOf([null]), ]), finalScore: PT.oneOfType([ PT.number, PT.string, - ]), - initialScore: PT.oneOfType([ - PT.number, - PT.string, + PT.oneOf([null]), ]), status: PT.string.isRequired, submissionId: PT.string.isRequired, - submissionTime: PT.string.isRequired, - })).isRequired, - score: PT.shape({ - final: PT.oneOfType([ - PT.number, - PT.string, - ]), - provisional: PT.oneOfType([ - PT.number, + submissionTime: PT.oneOfType([ PT.string, - ]), - }), + PT.oneOf([null]), + ]).isRequired, + })).isRequired, rating: PT.number, toggleHistory: PT.func, isReviewPhaseComplete: PT.bool, diff --git a/src/shared/components/challenge-detail/Submissions/index.jsx b/src/shared/components/challenge-detail/Submissions/index.jsx index ca09aad07b..796dd3a0db 100644 --- a/src/shared/components/challenge-detail/Submissions/index.jsx +++ b/src/shared/components/challenge-detail/Submissions/index.jsx @@ -180,7 +180,9 @@ class SubmissionsComponent extends React.Component { updateSortedSubmissions() { const isMM = this.isMM(); const { submissions, mmSubmissions } = this.props; - const sortedSubmissions = _.cloneDeep(isMM ? mmSubmissions : submissions); + const source = isMM ? mmSubmissions : submissions; + const sourceList = Array.isArray(source) ? source : []; + const sortedSubmissions = _.cloneDeep(sourceList); this.sortSubmissions(sortedSubmissions); this.setState({ sortedSubmissions }); } @@ -190,16 +192,36 @@ class SubmissionsComponent extends React.Component { * @param {Array} submissions array of submission */ sortSubmissions(submissions) { + if (!Array.isArray(submissions) || !submissions.length) { + return; + } const isMM = this.isMM(); const isReviewPhaseComplete = this.checkIsReviewPhaseComplete(); const { field, sort } = this.getSubmissionsSortParam(isMM, isReviewPhaseComplete); let isHaveFinalScore = false; - if (field === 'Initial Score' || 'Final Score') { + if (field === 'Initial Score' || field === 'Final Score') { isHaveFinalScore = _.some(submissions, s => !_.isNil( s.review && s.finalScore, )); } - return sortList(submissions, field, sort, (a, b) => { + const toSubmissionTime = (entry) => { + const latest = _.get(entry, ['submissions', 0]); + if (!latest) { + return null; + } + const { submissionTime } = latest; + if (!submissionTime) { + return null; + } + const timestamp = new Date(submissionTime).getTime(); + return Number.isFinite(timestamp) ? timestamp : null; + }; + const toRankValue = rank => (_.isFinite(rank) ? rank : Number.MAX_SAFE_INTEGER); + const toScoreValue = (score) => { + const numeric = Number(score); + return Number.isFinite(numeric) ? numeric : null; + }; + sortList(submissions, field, sort, (a, b) => { let valueA = 0; let valueB = 0; let valueIsString = false; @@ -227,12 +249,14 @@ class SubmissionsComponent extends React.Component { break; } case 'Time': - valueA = new Date(a.submissions && a.submissions[0].submissionTime); - valueB = new Date(b.submissions && b.submissions[0].submissionTime); + valueA = toSubmissionTime(a); + valueB = toSubmissionTime(b); break; case 'Submission Date': { - valueA = new Date(a.created || a.createdAt); - valueB = new Date(b.created || b.createdAt); + const createdA = a.created || a.createdAt; + const createdB = b.created || b.createdAt; + valueA = createdA ? new Date(createdA).getTime() : null; + valueB = createdB ? new Date(createdB).getTime() : null; break; } case 'Initial Score': { @@ -250,25 +274,25 @@ class SubmissionsComponent extends React.Component { break; } case 'Final Rank': { - if (this.checkIsReviewPhaseComplete()) { - valueA = a.finalRank ? a.finalRank : 0; - valueB = b.finalRank ? b.finalRank : 0; + if (isReviewPhaseComplete) { + valueA = toRankValue(_.get(a, 'finalRank')); + valueB = toRankValue(_.get(b, 'finalRank')); } break; } case 'Provisional Rank': { - valueA = a.provisionalRank ? a.provisionalRank : 0; - valueB = b.provisionalRank ? b.provisionalRank : 0; + valueA = toRankValue(_.get(a, 'provisionalRank')); + valueB = toRankValue(_.get(b, 'provisionalRank')); break; } case 'Final Score': { - valueA = getFinalScore(a); - valueB = getFinalScore(b); + valueA = toScoreValue(getFinalScore(a)); + valueB = toScoreValue(getFinalScore(b)); break; } case 'Provisional Score': { - valueA = getProvisionalScore(a); - valueB = getProvisionalScore(b); + valueA = toScoreValue(getProvisionalScore(a)); + valueB = toScoreValue(getProvisionalScore(b)); break; } default: @@ -1020,7 +1044,10 @@ SubmissionsComponent.propTypes = { submissionHistoryOpen: PT.shape({}).isRequired, loadMMSubmissions: PT.func.isRequired, mmSubmissions: PT.arrayOf(PT.shape()).isRequired, - loadingMMSubmissionsForChallengeId: PT.string.isRequired, + loadingMMSubmissionsForChallengeId: PT.oneOfType([ + PT.string, + PT.oneOf([null]), + ]).isRequired, isLoadingSubmissionInformation: PT.bool, submissionInformation: PT.shape(), loadSubmissionInformation: PT.func.isRequired, diff --git a/src/shared/containers/challenge-detail/index.jsx b/src/shared/containers/challenge-detail/index.jsx index 280639a4ff..eeebbe21b5 100644 --- a/src/shared/containers/challenge-detail/index.jsx +++ b/src/shared/containers/challenge-detail/index.jsx @@ -54,6 +54,8 @@ import { decodeToken } from '@topcoder-platform/tc-auth-lib'; import { actions, services } from 'topcoder-react-lib'; import { getService } from 'services/contentful'; import { getSubmissionArtifacts as getSubmissionArtifactsService } from 'services/submissions'; +import getReviewSummationsService from 'services/reviewSummations'; +import { buildMmSubmissionData, buildStatisticsData } from 'utils/mm-review-summations'; // import { // getDisplayRecommendedChallenges, // getRecommendedTags, @@ -232,7 +234,7 @@ class ChallengeDetailPageContainer extends React.Component { loadChallengeDetails(auth, challengeId); } - fetchChallengeStatistics(auth, challengeId); + fetchChallengeStatistics(auth, challenge); if (!allCountries.length) { getAllCountries(auth.tokenV3); @@ -263,6 +265,15 @@ class ChallengeDetailPageContainer extends React.Component { onSelectorClicked, } = this.props; + const previousChallengeId = _.get(challenge, 'id'); + const nextChallengeId = _.get(nextProps.challenge, 'id'); + + if (nextChallengeId + && nextChallengeId !== previousChallengeId + && checkIsMM(nextProps.challenge)) { + nextProps.fetchChallengeStatistics(nextProps.auth, nextProps.challenge); + } + if (challenge.isLegacyChallenge && !history.location.pathname.includes(challenge.id)) { history.location.pathname = `/challenges/${challenge.id}`; // eslint-disable-line no-param-reassign history.push(history.location.pathname, history.state); @@ -759,13 +770,13 @@ ChallengeDetailPageContainer.defaultProps = { allCountries: [], reviewTypes: [], isMenuOpened: false, - loadingMMSubmissionsForChallengeId: '', + loadingMMSubmissionsForChallengeId: null, mmSubmissions: [], mySubmissions: [], isLoadingSubmissionInformation: false, submissionInformation: null, // prizeMode: 'money-usd', - statisticsData: null, + statisticsData: [], getSubmissionArtifacts: () => {}, }; @@ -817,7 +828,10 @@ ChallengeDetailPageContainer.propTypes = { unregistering: PT.bool.isRequired, updateChallenge: PT.func.isRequired, isMenuOpened: PT.bool, - loadingMMSubmissionsForChallengeId: PT.string, + loadingMMSubmissionsForChallengeId: PT.oneOfType([ + PT.string, + PT.oneOf([null]), + ]), mmSubmissions: PT.arrayOf(PT.shape()), loadMMSubmissions: PT.func.isRequired, isLoadingSubmissionInformation: PT.bool, @@ -871,40 +885,190 @@ function mapStateToProps(state, props) { }); } + const loggedInUserId = _.get(auth, 'user.userId'); + const loggedInUserHandle = _.get(auth, 'user.handle'); if (!_.isEmpty(mmSubmissions)) { mmSubmissions = mmSubmissions.map((submission) => { - let registrant; - const { memberId } = submission; - let member = memberId; - if (`${auth.user.userId}` === `${memberId}`) { - mySubmissions = submission.submissions || []; - mySubmissions.forEach((mySubmission, index) => { - mySubmissions[index].id = mySubmissions.length - index; - }); + let memberId = submission.memberId || submission.submitterId; + let registrant = submission.registrant; + let memberHandle = submission.member || submission.submitterHandle || ''; + let ratingValue = null; + if (_.isFinite(submission.rating)) { + ratingValue = submission.rating; + } else if (_.isFinite(submission.submitterMaxRating)) { + ratingValue = submission.submitterMaxRating; + } + + const normalizedAttempts = Array.isArray(submission.submissions) + ? submission.submissions.map((attempt, attemptIndex) => { + const normalizedAttempt = { ...attempt }; + if (!normalizedAttempt.submissionTime) { + normalizedAttempt.submissionTime = normalizedAttempt.reviewedDate + || normalizedAttempt.created + || normalizedAttempt.createdAt + || null; + } + if (!normalizedAttempt.submissionId && normalizedAttempt.id) { + normalizedAttempt.submissionId = `${normalizedAttempt.id}`; + } + + const attemptSummations = []; + if (Array.isArray(normalizedAttempt.reviewSummations)) { + attemptSummations.push(...normalizedAttempt.reviewSummations); + } + if (Array.isArray(normalizedAttempt.reviewSummation)) { + attemptSummations.push(...normalizedAttempt.reviewSummation); + } + + let effectiveSummations = attemptSummations; + if (!effectiveSummations.length) { + const submissionLevelSummations = []; + if (Array.isArray(submission.reviewSummations)) { + submissionLevelSummations.push(...submission.reviewSummations); + } + if (Array.isArray(submission.reviewSummation)) { + submissionLevelSummations.push(...submission.reviewSummation); + } + // Fall back to submission-level scores for the latest attempt when + // per-attempt review summaries are not available. + effectiveSummations = submissionLevelSummations.length && attemptIndex === 0 + ? submissionLevelSummations + : attemptSummations; + } + + const toNumericScore = (value) => { + const numeric = Number(value); + return Number.isFinite(numeric) ? numeric : null; + }; + + const findScore = (summations, predicate) => { + if (!Array.isArray(summations) || !summations.length) { + return null; + } + const match = _.find(summations, predicate); + if (!match) { + return null; + } + return toNumericScore(match.aggregateScore); + }; + + const hasProvisionalScore = !_.isNil(normalizedAttempt.provisionalScore); + const hasFinalScore = !_.isNil(normalizedAttempt.finalScore); + + if (!hasProvisionalScore) { + const provisionalScore = findScore( + effectiveSummations, + s => s && (s.isProvisional || _.get(s, 'type', '').toLowerCase() === 'provisional'), + ); + if (!_.isNil(provisionalScore)) { + normalizedAttempt.provisionalScore = provisionalScore; + } + } + + if (!hasFinalScore) { + const finalScore = findScore( + effectiveSummations, + s => s && (s.isFinal || _.get(s, 'type', '').toLowerCase() === 'final'), + ); + if (!_.isNil(finalScore)) { + normalizedAttempt.finalScore = finalScore; + } + } + if (process.env.NODE_ENV !== 'production' + && _.isNil(normalizedAttempt.provisionalScore) + && _.isNil(normalizedAttempt.finalScore) + && effectiveSummations.length) { + // eslint-disable-next-line no-console + console.warn('Submission attempt missing review scores despite summations', { + attemptId: normalizedAttempt.submissionId || normalizedAttempt.id, + }); + } + + return normalizedAttempt; + }) + : []; + + const normalizedHandle = _.toLower(memberHandle || ''); + let submissionDetail = null; + if (memberId) { + submissionDetail = _.find( + challenge.submissions, + s => (`${s.memberId}` === `${memberId}`), + ); + } + if (!submissionDetail && normalizedHandle) { + submissionDetail = _.find( + challenge.submissions, + (s) => { + const submissionHandle = _.toLower(_.get(s, 'registrant.memberHandle') + || _.get(s, 'memberHandle') + || _.get(s, 'createdBy') + || ''); + return submissionHandle && submissionHandle === normalizedHandle; + }, + ); } - const submissionDetail = _.find(challenge.submissions, s => (`${s.memberId}` === `${submission.memberId}`)); if (submissionDetail) { - member = submissionDetail.createdBy; - ({ registrant } = submissionDetail); + registrant = registrant || submissionDetail.registrant; + memberHandle = memberHandle || submissionDetail.createdBy; + if (!memberId && submissionDetail.memberId) { + memberId = `${submissionDetail.memberId}`; + } } - if (!registrant) { + if (!registrant && memberId) { registrant = _.find(challenge.registrants, r => `${r.memberId}` === `${memberId}`); } + if (!registrant && normalizedHandle) { + registrant = _.find( + challenge.registrants, + (r) => { + const registrantHandle = _.toLower(r.memberHandle || r.handle || ''); + return registrantHandle && registrantHandle === normalizedHandle; + }, + ); + } if (registrant) { - member = registrant.memberHandle; + memberHandle = memberHandle || registrant.memberHandle; + if (!_.isFinite(ratingValue) && _.isFinite(registrant.rating)) { + ratingValue = registrant.rating; + } + if (!memberId && registrant.memberId) { + memberId = `${registrant.memberId}`; + } + } + + if (!memberHandle && !_.isNil(memberId)) { + memberHandle = `${memberId}`; + } + + const isLoggedInSubmitter = ( + memberId && loggedInUserId && `${loggedInUserId}` === `${memberId}` + ) || ( + normalizedHandle + && loggedInUserHandle + && normalizedHandle === _.toLower(loggedInUserHandle) + ); + + if (isLoggedInSubmitter) { + mySubmissions = normalizedAttempts.map((attempt, index) => ({ + ...attempt, + id: normalizedAttempts.length - index, + })); } return ({ ...submission, + submissions: normalizedAttempts, registrant, - member, + member: memberHandle, + rating: ratingValue, }); }); - } else { - mySubmissions = _.filter(challenge.submissions, s => (`${s.memberId}` === `${auth.user.userId}`)); + } else if (loggedInUserId) { + mySubmissions = _.filter(challenge.submissions, s => (`${s.memberId}` === `${loggedInUserId}`)); } } const { page: { challengeDetails: { feedbackOpen } } } = state; @@ -966,6 +1130,76 @@ function mapStateToProps(state, props) { const mapDispatchToProps = (dispatch) => { const ca = communityActions.tcCommunity; const lookupActions = actions.lookup; + const challengeActions = actions.challenge || {}; + const hasReviewSummationsActions = ( + typeof challengeActions.getReviewSummationsInit === 'function' + && typeof challengeActions.getReviewSummationsDone === 'function' + ); + + const dispatchReviewSummations = (challengeId, tokenV3) => { + const challengeIdStr = _.toString(challengeId); + if (!challengeIdStr) { + return; + } + + if (hasReviewSummationsActions) { + dispatch(challengeActions.getReviewSummationsInit(challengeIdStr)); + dispatch(challengeActions.getReviewSummationsDone(challengeIdStr, tokenV3)); + return; + } + + dispatch({ + type: 'CHALLENGE/GET_REVIEW_SUMMATIONS_INIT', + payload: challengeIdStr, + }); + dispatch({ + type: 'CHALLENGE/GET_MM_SUBMISSIONS_INIT', + payload: challengeIdStr, + }); + + getReviewSummationsService(tokenV3, challengeIdStr) + .then(({ data }) => { + const reviewSummations = Array.isArray(data) ? data : []; + const mmSubmissions = buildMmSubmissionData(reviewSummations); + const statisticsData = buildStatisticsData(reviewSummations); + + dispatch({ + type: 'CHALLENGE/GET_REVIEW_SUMMATIONS_DONE', + payload: reviewSummations, + meta: { challengeId: challengeIdStr }, + }); + dispatch({ + type: 'CHALLENGE/GET_MM_SUBMISSIONS_DONE', + payload: { + challengeId: challengeIdStr, + submissions: mmSubmissions, + }, + }); + dispatch({ + type: 'CHALLENGE/FETCH_CHALLENGE_STATISTICS_DONE', + payload: statisticsData, + }); + }) + .catch((error) => { + dispatch({ + type: 'CHALLENGE/GET_REVIEW_SUMMATIONS_DONE', + error: true, + payload: { challengeId: challengeIdStr, error }, + meta: { challengeId: challengeIdStr }, + }); + dispatch({ + type: 'CHALLENGE/GET_MM_SUBMISSIONS_DONE', + error: true, + payload: { challengeId: challengeIdStr, error }, + }); + dispatch({ + type: 'CHALLENGE/FETCH_CHALLENGE_STATISTICS_DONE', + error: true, + payload: error, + }); + }); + }; + return { // getAllRecommendedChallenges: (tokenV3, recommendedTechnology) => { // const uuid = shortId(); @@ -1135,9 +1369,7 @@ const mapDispatchToProps = (dispatch) => { dispatch(a.updateChallengeDone(uuid, challenge, tokenV3)); }, loadMMSubmissions: (challengeId, tokenV3) => { - const a = actions.challenge; - dispatch(a.getMmSubmissionsInit(challengeId)); - dispatch(a.getMmSubmissionsDone(challengeId, tokenV3)); + dispatchReviewSummations(challengeId, tokenV3); }, getSubmissionArtifacts: (submissionId, tokenV3) => getSubmissionArtifactsService(tokenV3, submissionId), @@ -1150,10 +1382,17 @@ const mapDispatchToProps = (dispatch) => { const a = challengeListingActions.challengeListing; dispatch(a.expandTag(id)); }, - fetchChallengeStatistics: (tokens, challengeId) => { - const a = actions.challenge; - dispatch(a.fetchChallengeStatisticsInit()); - dispatch(a.fetchChallengeStatisticsDone(challengeId, tokens.tokenV3)); + fetchChallengeStatistics: (tokens, challengeDetails) => { + if (!tokens || !tokens.tokenV3 || !challengeDetails || !checkIsMM(challengeDetails)) { + return; + } + + const challengeId = _.toString(challengeDetails.id || challengeDetails.legacyId); + if (!challengeId) { + return; + } + + dispatchReviewSummations(challengeId, tokens.tokenV3); }, }; }; diff --git a/src/shared/services/reviewSummations.js b/src/shared/services/reviewSummations.js new file mode 100644 index 0000000000..02ae2a40f7 --- /dev/null +++ b/src/shared/services/reviewSummations.js @@ -0,0 +1,73 @@ +import _ from 'lodash'; +import { config } from 'topcoder-react-utils'; + +const baseUrl = config.URL.REVIEW_SUMMATIONS_API_URL; +const v6ApiUrl = config.API.V6; +const DEFAULT_PER_PAGE = 500; + +async function fetchReviewSummationsPage({ + challengeId, + headers, + page, + aggregated, + meta, +}) { + const url = `${v6ApiUrl}${baseUrl}?challengeId=${encodeURIComponent(challengeId)}&perPage=${DEFAULT_PER_PAGE}&page=${page}`; + const response = await fetch(url, { + method: 'GET', + headers, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch review summations: ${response.status} ${response.statusText}`); + } + + const payload = await response.json(); + const data = payload.data || []; + const combined = [...aggregated, ...data]; + const latestMeta = payload.meta || meta; + + const totalPages = _.get(payload, 'meta.totalPages') + || _.get(payload, 'meta.total_pages'); + const reachedEnd = !data.length + || (totalPages && page >= totalPages) + || data.length < DEFAULT_PER_PAGE; + + if (reachedEnd) { + return { + aggregated: combined, + meta: latestMeta, + }; + } + + return fetchReviewSummationsPage({ + challengeId, + headers, + page: page + 1, + aggregated: combined, + meta: latestMeta, + }); +} + +export default async function getReviewSummations(tokenV3, challengeId) { + const headers = new Headers({ + Authorization: `Bearer ${tokenV3}`, + }); + + const { aggregated, meta } = await fetchReviewSummationsPage({ + challengeId, + headers, + page: 1, + aggregated: [], + meta: null, + }); + + return { + data: aggregated, + meta: { + ...(meta || {}), + totalItems: aggregated.length, + perPage: DEFAULT_PER_PAGE, + }, + }; +} diff --git a/src/shared/utils/challenge-detail/submission-status.js b/src/shared/utils/challenge-detail/submission-status.js new file mode 100644 index 0000000000..1094d66067 --- /dev/null +++ b/src/shared/utils/challenge-detail/submission-status.js @@ -0,0 +1,71 @@ +import _ from 'lodash'; + +function toArray(value) { + if (!value) { + return []; + } + if (Array.isArray(value)) { + return value.filter(Boolean); + } + if (_.isObject(value)) { + return [value]; + } + return []; +} + +function collectReviewSummations(submission) { + if (!submission) { + return []; + } + + const combined = []; + toArray(_.get(submission, 'reviewSummations')).forEach(item => combined.push(item)); + toArray(_.get(submission, 'reviewSummation')).forEach(item => combined.push(item)); + + return combined; +} + +export function getSubmissionReviewSummations(submission) { + return collectReviewSummations(submission); +} + +export function getSubmissionStatus(submission) { + const targetIdRaw = _.get(submission, 'submissionId', _.get(submission, 'id', null)); + const targetId = _.toString(targetIdRaw || '').trim(); + const shouldMatchId = Boolean(targetId); + + const reviewSummations = collectReviewSummations(submission).filter((summation) => { + if (!summation) { + return false; + } + if (!shouldMatchId) { + return true; + } + const summationIdRaw = _.get(summation, 'submissionId', _.get(summation, 'id', null)); + const summationId = _.toString(summationIdRaw || '').trim(); + return summationId && summationId === targetId; + }); + + const hasReviewSummation = reviewSummations.length > 0; + + const isAccepted = reviewSummations.some((summation) => { + const type = _.toLower(_.toString(_.get(summation, 'type', '') || '').trim()); + const hasFlag = Boolean( + _.get(summation, 'isProvisional') + || _.get(summation, 'isFinal') + || _.get(summation, 'is_provisional') + || _.get(summation, 'is_final'), + ); + return hasFlag || type === 'provisional' || type === 'final'; + }); + + return { + hasReviewSummation, + isAccepted, + }; +} + +export default { + getSubmissionStatus, + getSubmissionReviewSummations, +}; diff --git a/src/shared/utils/challenge-listing/constants.js b/src/shared/utils/challenge-listing/constants.js index 1dff854baf..fbddb9fdca 100644 --- a/src/shared/utils/challenge-listing/constants.js +++ b/src/shared/utils/challenge-listing/constants.js @@ -1,2 +1,4 @@ -export const EXCLUDED_CHALLENGE_TYPE_NAMES = ['Topgear Task']; +const EXCLUDED_CHALLENGE_TYPE_NAMES = ['Topgear Task']; +export { EXCLUDED_CHALLENGE_TYPE_NAMES }; +export default EXCLUDED_CHALLENGE_TYPE_NAMES; diff --git a/src/shared/utils/mm-review-summations.js b/src/shared/utils/mm-review-summations.js new file mode 100644 index 0000000000..10aa837646 --- /dev/null +++ b/src/shared/utils/mm-review-summations.js @@ -0,0 +1,459 @@ +import _ from 'lodash'; + +function normalizeScoreValue(score) { + if (_.isNil(score) || score === '' || score === '-') { + return null; + } + const parsed = Number(score); + if (_.isNaN(parsed)) { + return null; + } + return parsed; +} + +function getSummationTimestamp(summation) { + const candidates = [ + _.get(summation, 'reviewedDate'), + _.get(summation, 'createdAt'), + _.get(summation, 'updatedAt'), + _.get(summation, 'created'), + ]; + return _.find(candidates, value => !!value) || null; +} + +function toTimestampValue(value) { + if (!value) { + return 0; + } + const timestamp = new Date(value).getTime(); + if (_.isNaN(timestamp)) { + return 0; + } + return timestamp; +} + +function getSummationHandle(summation) { + const handle = _.get(summation, 'submitterHandle'); + if (!handle || !_.isString(handle) || !handle.trim()) { + return 'unknown'; + } + return handle; +} + +function getSummationMemberId(summation) { + const memberId = _.get(summation, 'submitterId'); + return _.isNil(memberId) ? null : _.toString(memberId); +} + +function getSummationRating(summation) { + const rating = _.get(summation, 'submitterMaxRating'); + return _.isNil(rating) ? null : rating; +} + +function ensureSubmissionEntry(existingEntry, { submissionId, timestamp, timestampValue }) { + const baseEntry = { + submissionId, + submissionTime: timestamp || null, + provisionalScore: null, + finalScore: null, + status: 'completed', + reviewSummations: [], + reviewSummation: [], + latestTimestamp: timestampValue, + provisionalMeta: { timestamp: -Infinity, score: null }, + finalMeta: { timestamp: -Infinity, score: null }, + }; + + if (!existingEntry) { + return baseEntry; + } + + const reviewSummations = Array.isArray(existingEntry.reviewSummations) + ? [...existingEntry.reviewSummations] + : []; + const reviewSummation = Array.isArray(existingEntry.reviewSummation) + ? [...existingEntry.reviewSummation] + : [...reviewSummations]; + + return { + ...baseEntry, + ...existingEntry, + submissionId: existingEntry.submissionId || submissionId, + submissionTime: existingEntry.submissionTime || baseEntry.submissionTime, + status: existingEntry.status || 'completed', + reviewSummations, + reviewSummation, + latestTimestamp: _.isFinite(existingEntry.latestTimestamp) + ? existingEntry.latestTimestamp + : baseEntry.latestTimestamp, + provisionalMeta: existingEntry.provisionalMeta + ? { ...existingEntry.provisionalMeta } + : baseEntry.provisionalMeta, + finalMeta: existingEntry.finalMeta + ? { ...existingEntry.finalMeta } + : baseEntry.finalMeta, + }; +} + +function mergeScoreData(meta, currentValue, score, timestampValue, options = {}) { + const { allowOlderTimestampUpdate = true } = options; + const nextMeta = { ...meta }; + let nextValue = currentValue; + + if (timestampValue > nextMeta.timestamp) { + nextMeta.timestamp = timestampValue; + nextMeta.score = score; + nextValue = _.isNil(score) ? null : score; + } else if (timestampValue === nextMeta.timestamp) { + if (!_.isNil(score) && (_.isNil(nextMeta.score) || score > nextMeta.score)) { + nextMeta.score = score; + nextValue = score; + } + } else if (allowOlderTimestampUpdate && _.isNil(nextValue) && !_.isNil(score)) { + nextMeta.timestamp = timestampValue; + nextMeta.score = score; + nextValue = score; + } + + return { + meta: nextMeta, + value: nextValue, + }; +} + +function updateSubmissionEntry(existingEntry, { + submissionId, + timestamp, + timestampValue, + normalizedScore, + summation, + isProvisional, +}) { + const baseEntry = ensureSubmissionEntry(existingEntry, { + submissionId, + timestamp, + timestampValue, + }); + const { + submissionTime: baseSubmissionTime, + latestTimestamp: baseLatestTimestamp, + } = baseEntry; + + let submissionTime = baseSubmissionTime; + let latestTimestamp = baseLatestTimestamp; + + if (timestampValue > latestTimestamp) { + latestTimestamp = timestampValue; + submissionTime = timestamp || submissionTime; + } else if (!submissionTime && timestamp) { + submissionTime = timestamp; + } + + const provisionalResult = isProvisional + ? mergeScoreData( + baseEntry.provisionalMeta, + baseEntry.provisionalScore, + normalizedScore, + timestampValue, + ) + : { meta: baseEntry.provisionalMeta, value: baseEntry.provisionalScore }; + + const finalResult = isProvisional + ? { meta: baseEntry.finalMeta, value: baseEntry.finalScore } + : mergeScoreData( + baseEntry.finalMeta, + baseEntry.finalScore, + normalizedScore, + timestampValue, + ); + + const reviewSummations = [...baseEntry.reviewSummations, summation]; + + return { + ...baseEntry, + submissionTime, + latestTimestamp, + provisionalMeta: provisionalResult.meta, + provisionalScore: provisionalResult.value, + finalMeta: finalResult.meta, + finalScore: finalResult.value, + reviewSummations, + reviewSummation: [...reviewSummations], + }; +} + +function assignRanks(members, scoreKey, rankKey) { + const rankedEntries = members + .map(member => ({ + key: `${member.memberId || member.member || ''}`, + score: member[scoreKey], + })) + .filter(entry => !_.isNil(entry.score)) + .sort((a, b) => b.score - a.score); + + let processed = 0; + let previousScore = null; + let currentRank = 0; + const rankByKey = new Map(); + + rankedEntries.forEach((entry) => { + processed += 1; + if (previousScore === null || entry.score !== previousScore) { + currentRank = processed; + previousScore = entry.score; + } + rankByKey.set(entry.key, currentRank); + }); + + return members.map((member) => { + const key = `${member.memberId || member.member || ''}`; + const rank = rankByKey.get(key); + return { + ...member, + [rankKey]: _.isNil(rank) ? null : rank, + }; + }); +} + +function createStatisticsSubmission({ + submissionId, + timestamp, + timestampValue, + score, +}) { + return { + submissionId, + created: timestamp || null, + createdAt: timestamp || null, + score, + meta: { + timestamp: timestampValue, + score, + }, + }; +} + +function updateStatisticsSubmission(submission, { + timestamp, + timestampValue, + score, +}) { + const base = { + ...submission, + meta: submission.meta ? { ...submission.meta } : { timestamp: -Infinity, score: null }, + }; + + const previousMeta = base.meta; + const { meta, value } = mergeScoreData(previousMeta, base.score, score, timestampValue, { + allowOlderTimestampUpdate: false, + }); + + const hasNewerTimestamp = meta.timestamp > previousMeta.timestamp; + + return { + ...base, + created: hasNewerTimestamp ? (timestamp || base.created) : base.created, + createdAt: hasNewerTimestamp ? (timestamp || base.createdAt) : base.createdAt, + score: value, + meta, + }; +} + +export function buildMmSubmissionData(reviewSummations = []) { + if (!Array.isArray(reviewSummations) || !reviewSummations.length) { + return []; + } + + const membersByHandle = new Map(); + + reviewSummations.forEach((summation, index) => { + if (!summation) { + return; + } + + const handle = getSummationHandle(summation); + if (!membersByHandle.has(handle)) { + membersByHandle.set(handle, { + handle, + memberId: null, + rating: null, + submissionsMap: new Map(), + bestProvisionalScore: null, + bestFinalScore: null, + }); + } + + const memberEntry = membersByHandle.get(handle); + + const memberId = getSummationMemberId(summation); + if (!memberEntry.memberId && memberId) { + memberEntry.memberId = memberId; + } + + const rating = getSummationRating(summation); + if (_.isNil(memberEntry.rating) && !_.isNil(rating)) { + memberEntry.rating = rating; + } + + const rawSubmissionId = _.get(summation, 'submissionId', _.get(summation, 'id')); + const submissionId = rawSubmissionId ? _.toString(rawSubmissionId) : `unknown-${handle}-${index}`; + const timestamp = getSummationTimestamp(summation); + const timestampValue = toTimestampValue(timestamp); + + const normalizedScore = normalizeScoreValue(_.get(summation, 'aggregateScore')); + const isProvisional = Boolean(summation.isProvisional); + + const updatedEntry = updateSubmissionEntry(memberEntry.submissionsMap.get(submissionId), { + submissionId, + timestamp, + timestampValue, + normalizedScore, + summation, + isProvisional, + }); + + memberEntry.submissionsMap.set(submissionId, updatedEntry); + + if (!_.isNil(normalizedScore)) { + if (isProvisional) { + memberEntry.bestProvisionalScore = _.isNil(memberEntry.bestProvisionalScore) + ? normalizedScore + : Math.max(memberEntry.bestProvisionalScore, normalizedScore); + } else { + memberEntry.bestFinalScore = _.isNil(memberEntry.bestFinalScore) + ? normalizedScore + : Math.max(memberEntry.bestFinalScore, normalizedScore); + } + } + }); + + const members = Array.from(membersByHandle.values()).map((memberEntry) => { + const submissions = Array.from(memberEntry.submissionsMap.values()) + .map(submission => ({ + submissionId: submission.submissionId, + submissionTime: submission.submissionTime, + provisionalScore: _.isNil(submission.provisionalScore) ? null : submission.provisionalScore, + finalScore: _.isNil(submission.finalScore) ? null : submission.finalScore, + status: submission.status || 'completed', + reviewSummations: [...submission.reviewSummations], + reviewSummation: [...submission.reviewSummations], + })) + .sort((a, b) => toTimestampValue(b.submissionTime) - toTimestampValue(a.submissionTime)); + + const rating = _.isNil(memberEntry.rating) ? null : memberEntry.rating; + const memberId = memberEntry.memberId ? _.toString(memberEntry.memberId) : null; + + const registrant = memberId ? { + userId: memberId, + memberHandle: memberEntry.handle === 'unknown' ? null : memberEntry.handle, + rating, + } : null; + + return { + member: memberEntry.handle, + memberId, + registrant, + rating, + provisionalRank: null, + finalRank: null, + submissions, + bestProvisionalScore: memberEntry.bestProvisionalScore, + bestFinalScore: memberEntry.bestFinalScore, + }; + }); + + const withProvisionalRanks = assignRanks(members, 'bestProvisionalScore', 'provisionalRank'); + const withFinalRanks = assignRanks(withProvisionalRanks, 'bestFinalScore', 'finalRank'); + + return withFinalRanks + .map(({ bestProvisionalScore, bestFinalScore, ...rest }) => rest) + .sort((a, b) => { + if (!_.isNil(a.finalRank) && !_.isNil(b.finalRank)) { + return a.finalRank - b.finalRank; + } + if (!_.isNil(a.finalRank)) { + return -1; + } + if (!_.isNil(b.finalRank)) { + return 1; + } + if (!_.isNil(a.provisionalRank) && !_.isNil(b.provisionalRank)) { + return a.provisionalRank - b.provisionalRank; + } + if (!_.isNil(a.provisionalRank)) { + return -1; + } + if (!_.isNil(b.provisionalRank)) { + return 1; + } + return 0; + }); +} + +export function buildStatisticsData(reviewSummations = []) { + if (!Array.isArray(reviewSummations) || !reviewSummations.length) { + return []; + } + + const grouped = new Map(); + + reviewSummations.forEach((summation, index) => { + if (!summation) { + return; + } + + const handle = getSummationHandle(summation); + if (!grouped.has(handle)) { + grouped.set(handle, { + handle, + rating: null, + submissionsMap: new Map(), + }); + } + + const entry = grouped.get(handle); + + const rating = getSummationRating(summation); + if (_.isNil(entry.rating) && !_.isNil(rating)) { + entry.rating = rating; + } + + const timestamp = getSummationTimestamp(summation); + const timestampValue = toTimestampValue(timestamp); + const score = normalizeScoreValue(_.get(summation, 'aggregateScore')); + + const rawSubmissionId = _.get(summation, 'submissionId', _.get(summation, 'id')); + const submissionId = rawSubmissionId ? _.toString(rawSubmissionId) : `unknown-${handle}-${index}`; + + const existingSubmission = entry.submissionsMap.get(submissionId); + + const updatedSubmission = existingSubmission + ? updateStatisticsSubmission(existingSubmission, { timestamp, timestampValue, score }) + : createStatisticsSubmission({ + submissionId, + timestamp, + timestampValue, + score, + }); + + entry.submissionsMap.set(submissionId, updatedSubmission); + }); + + return Array.from(grouped.values()).map(entry => ({ + handle: entry.handle, + rating: entry.rating, + submissions: Array.from(entry.submissionsMap.values()) + .map(submission => ({ + submissionId: submission.submissionId, + created: submission.created, + createdAt: submission.createdAt, + score: submission.score, + })) + .sort((a, b) => toTimestampValue(b.createdAt) - toTimestampValue(a.createdAt)), + })); +} + +export default { + buildMmSubmissionData, + buildStatisticsData, +}; From 194e238e7df9fcad0c01907a16fc678f30c81d1a Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Thu, 23 Oct 2025 11:21:02 +1100 Subject: [PATCH 58/76] Use review summations on 'my-submissions' page under challenge details --- .../RatingsListModal/index.jsx | 82 +++--- .../SubmissionManagement/index.jsx | 16 +- .../SubmissionsTable/index.jsx | 16 +- .../containers/SubmissionManagement/index.jsx | 251 ++++++++++++++++-- 4 files changed, 286 insertions(+), 79 deletions(-) diff --git a/src/shared/components/SubmissionManagement/RatingsListModal/index.jsx b/src/shared/components/SubmissionManagement/RatingsListModal/index.jsx index da0db39153..fcf827215a 100644 --- a/src/shared/components/SubmissionManagement/RatingsListModal/index.jsx +++ b/src/shared/components/SubmissionManagement/RatingsListModal/index.jsx @@ -27,46 +27,52 @@ const RatingsListModal = ({ onCancel, submissionId, challengeId, - getReviewTypesList, - getChallengeResources, - getSubmissionInformation, + getSubmissionScores, }) => { const [reviews, setReviews] = useState([]); const [loading, setLoading] = useState(false); - const enrichSources = useCallback(async (submissionReviews, reviewSummation) => { - const reviewTypes = await getReviewTypesList(); - const resources = await getChallengeResources(challengeId); - - const finalReview = { - reviewType: 'Final score', - reviewer: '', - score: reviewSummation ? reviewSummation.aggregateScore : 'N/A', - isPassing: reviewSummation ? reviewSummation.isPassing : undefined, - }; - - return [...submissionReviews.map((review) => { - const reviewType = reviewTypes.find(rt => rt.id === review.typeId); - const reviewer = resources - .find(resource => resource.memberHandle === review.reviewerId) || SystemReviewers.Default; - return { - ...review, - reviewType: reviewType ? reviewType.name : '', - reviewer, - }; - }), finalReview]; - }, [challengeId, getReviewTypesList, getChallengeResources]); - - const getSubmission = useCallback(async () => { - const submissionInfo = await getSubmissionInformation(submissionId); - setReviews(await enrichSources(submissionInfo.review, submissionInfo.reviewSummation[0])); - setLoading(false); - }, [submissionId, getSubmissionInformation, enrichSources]); + const formatScoreValue = useCallback((value) => { + if (_.isNil(value)) { + return 'N/A'; + } + if (Number.isFinite(value)) { + return Number.isInteger(value) ? `${value}` : value.toFixed(2); + } + const numeric = Number(value); + if (Number.isFinite(numeric)) { + return Number.isInteger(numeric) ? `${numeric}` : numeric.toFixed(2); + } + return `${value}`; + }, []); + + const loadScores = useCallback(async () => { + if (!submissionId) { + setReviews([]); + return; + } + setLoading(true); + try { + const response = await getSubmissionScores(submissionId, challengeId); + const normalized = Array.isArray(response) ? response : []; + const decorated = normalized.map((entry, index) => ({ + id: entry.id || `${submissionId}-${index}`, + reviewType: entry.label || entry.reviewType || 'Score', + reviewer: entry.reviewer || SystemReviewers.Default, + score: formatScoreValue(entry.score), + isPassing: typeof entry.isPassing === 'boolean' ? entry.isPassing : null, + })); + setReviews(decorated); + } catch (error) { + setReviews([]); + } finally { + setLoading(false); + } + }, [submissionId, challengeId, getSubmissionScores, formatScoreValue]); useEffect(() => { - setLoading(true); - getSubmission(); - }, [submissionId, getSubmission]); + loadScores(); + }, [loadScores]); return ( onCancel()} theme={theme}> @@ -131,18 +137,14 @@ RatingsListModal.defaultProps = { onCancel: () => {}, submissionId: '', challengeId: '', - getReviewTypesList: _.noop, - getChallengeResources: _.noop, - getSubmissionInformation: _.noop, + getSubmissionScores: _.noop, }; RatingsListModal.propTypes = { onCancel: PropTypes.func, submissionId: PropTypes.string, challengeId: PropTypes.string, - getReviewTypesList: PropTypes.func, - getChallengeResources: PropTypes.func, - getSubmissionInformation: PropTypes.func, + getSubmissionScores: PropTypes.func, }; export default RatingsListModal; diff --git a/src/shared/components/SubmissionManagement/SubmissionManagement/index.jsx b/src/shared/components/SubmissionManagement/SubmissionManagement/index.jsx index 6ff4ff8202..6cbecd938e 100644 --- a/src/shared/components/SubmissionManagement/SubmissionManagement/index.jsx +++ b/src/shared/components/SubmissionManagement/SubmissionManagement/index.jsx @@ -42,9 +42,7 @@ export default function SubmissionManagement(props) { submissionPhaseStartDate, onDownloadArtifacts, getSubmissionArtifacts, - getSubmissionInformation, - getReviewTypesList, - getChallengeResources, + getSubmissionScores, } = props; const { track } = challenge; @@ -80,9 +78,7 @@ export default function SubmissionManagement(props) { onShowDetails, onDownloadArtifacts, getSubmissionArtifacts, - getSubmissionInformation, - getReviewTypesList, - getChallengeResources, + getSubmissionScores, }; return (
@@ -220,9 +216,7 @@ SubmissionManagement.defaultProps = { onDownload: _.noop, onDownloadArtifacts: _.noop, getSubmissionArtifacts: _.noop, - getSubmissionInformation: _.noop, - getReviewTypesList: _.noop, - getChallengeResources: _.noop, + getSubmissionScores: _.noop, onlineReviewUrl: '', helpPageUrl: '', loadingSubmissions: false, @@ -240,9 +234,7 @@ SubmissionManagement.propTypes = { onShowDetails: PT.func, onDownloadArtifacts: PT.func, getSubmissionArtifacts: PT.func, - getSubmissionInformation: PT.func, - getReviewTypesList: PT.func, - getChallengeResources: PT.func, + getSubmissionScores: PT.func, submissions: PT.arrayOf(PT.shape()), loadingSubmissions: PT.bool, challengeUrl: PT.string, diff --git a/src/shared/components/SubmissionManagement/SubmissionsTable/index.jsx b/src/shared/components/SubmissionManagement/SubmissionsTable/index.jsx index 8396d88166..14137693c2 100644 --- a/src/shared/components/SubmissionManagement/SubmissionsTable/index.jsx +++ b/src/shared/components/SubmissionManagement/SubmissionsTable/index.jsx @@ -46,9 +46,7 @@ export default function SubmissionsTable(props) { submissionPhaseStartDate, onDownloadArtifacts, getSubmissionArtifacts, - getReviewTypesList, - getChallengeResources, - getSubmissionInformation, + getSubmissionScores, } = props; const onOpenDownloadArtifactsModal = useCallback((id) => { @@ -159,11 +157,9 @@ export default function SubmissionsTable(props) { setSubmissionId(''); setShowRatingsListModal(false); }} - getReviewTypesList={getReviewTypesList} - getChallengeResources={getChallengeResources} submissionId={submissionId} challengeId={challenge.id} - getSubmissionInformation={getSubmissionInformation} + getSubmissionScores={getSubmissionScores} /> )}
@@ -189,9 +185,7 @@ SubmissionsTable.defaultProps = { getSubmissionArtifacts: _.noop, onlineReviewUrl: '', helpPageUrl: '', - getReviewTypesList: _.noop, - getChallengeResources: _.noop, - getSubmissionInformation: _.noop, + getSubmissionScores: _.noop, }; SubmissionsTable.propTypes = { @@ -208,7 +202,5 @@ SubmissionsTable.propTypes = { getSubmissionArtifacts: PT.func, status: PT.string.isRequired, submissionPhaseStartDate: PT.string.isRequired, - getReviewTypesList: PT.func, - getChallengeResources: PT.func, - getSubmissionInformation: PT.func, + getSubmissionScores: PT.func, }; diff --git a/src/shared/containers/SubmissionManagement/index.jsx b/src/shared/containers/SubmissionManagement/index.jsx index a6e962c60e..0b0676b186 100644 --- a/src/shared/containers/SubmissionManagement/index.jsx +++ b/src/shared/containers/SubmissionManagement/index.jsx @@ -16,7 +16,7 @@ import { connect } from 'react-redux'; import { Modal, PrimaryButton } from 'topcoder-react-ui-kit'; import { config } from 'topcoder-react-utils'; import { actions, services } from 'topcoder-react-lib'; -import getReviewTypes from 'services/reviewTypes'; +import getReviewSummationsService from 'services/reviewSummations'; import { getSubmissionArtifacts, downloadSubmissions } from 'services/submissions'; import style from './styles.scss'; @@ -24,7 +24,121 @@ import smpActions from '../../actions/page/submission_management'; const { getService } = services.submissions; -const { getService: getMemberService } = services.members; +const SUMMATION_TYPE_PRIORITY = { + example: 0, + provisional: 1, + final: 2, + other: 3, +}; + +const normalizeTimestamp = (value) => { + if (!value) { + return 0; + } + const parsed = new Date(value).getTime(); + return Number.isFinite(parsed) ? parsed : 0; +}; + +const ensureMetadataObject = metadata => ((metadata && typeof metadata === 'object') ? metadata : {}); + +const deriveSummationType = (summation) => { + const metadata = ensureMetadataObject(_.get(summation, 'metadata')); + const metaStage = _.toLower(_.get(metadata, 'stage', '')); + const metaTestType = _.toLower(_.get(metadata, 'testType', '')); + const isExample = summation.isExample === true || metaTestType === 'example'; + const isProvisional = summation.isProvisional === true || metaTestType === 'provisional'; + const isFinal = summation.isFinal === true || metaStage === 'final'; + + if (isExample) { + return { key: 'example', label: 'Example' }; + } + if (isProvisional) { + return { key: 'provisional', label: 'Provisional' }; + } + if (isFinal) { + return { key: 'final', label: 'Final' }; + } + return { key: 'other', label: 'Score' }; +}; + +const mergeSubmissionWithSummations = (submission, reviewSummationsBySubmission) => { + if (!submission) { + return submission; + } + const submissionId = _.toString(submission.id || submission.submissionId); + if (!submissionId) { + return submission; + } + + const summations = reviewSummationsBySubmission[submissionId]; + if (!Array.isArray(summations) || !summations.length) { + return submission; + } + + return { + ...submission, + reviewSummations: summations, + reviewSummation: summations, + }; +}; + +const buildScoreEntries = (summations = []) => { + if (!Array.isArray(summations) || !summations.length) { + return []; + } + + const latestByType = new Map(); + + summations.forEach((summation, index) => { + if (!summation) { + return; + } + + const { key, label } = deriveSummationType(summation); + const timestampRaw = summation.reviewedDate + || summation.updatedAt + || summation.createdAt + || null; + const timestampValue = normalizeTimestamp(timestampRaw); + const reviewer = summation.updatedBy || summation.createdBy || 'System'; + const aggregateScore = _.get(summation, 'aggregateScore'); + let normalizedScore = null; + if (!_.isNil(aggregateScore) && aggregateScore !== '') { + if (Number.isFinite(aggregateScore)) { + normalizedScore = aggregateScore; + } else { + const parsedScore = Number(aggregateScore); + normalizedScore = Number.isFinite(parsedScore) ? parsedScore : aggregateScore; + } + } + + const entry = { + id: summation.id || `${summation.submissionId || 'submission'}-${key}-${index}`, + label, + reviewer: reviewer || 'System', + score: _.isNil(normalizedScore) ? null : normalizedScore, + isPassing: typeof summation.isPassing === 'boolean' ? summation.isPassing : null, + reviewedOn: timestampRaw, + orderKey: key, + orderValue: SUMMATION_TYPE_PRIORITY[key] || SUMMATION_TYPE_PRIORITY.other, + timestampValue, + }; + + const existing = latestByType.get(key); + if (!existing || entry.timestampValue > existing.timestampValue) { + latestByType.set(key, entry); + } + }); + + return Array.from(latestByType.values()) + .sort((a, b) => { + if (a.orderValue !== b.orderValue) { + return a.orderValue - b.orderValue; + } + return b.timestampValue - a.timestampValue; + }) + .map(entry => _.omit(entry, ['orderKey', 'orderValue', 'timestampValue'])); +}; const theme = { container: style.modalContainer, @@ -35,14 +149,19 @@ class SubmissionManagementPageContainer extends React.Component { constructor(props) { super(props); + this.pendingReviewSummationChallengeId = null; + this.isComponentMounted = false; + this.state = { needReload: false, initialState: true, submissions: [], + reviewSummationsBySubmission: {}, }; } componentDidMount() { + this.isComponentMounted = true; const { authTokens, challenge, @@ -60,6 +179,8 @@ class SubmissionManagementPageContainer extends React.Component { if (challengeId !== loadingSubmissionsForChallengeId) { loadMySubmissions(authTokens, challengeId); } + + this.loadReviewSummations(_.get(authTokens, 'tokenV3'), challengeId); } componentWillReceiveProps(nextProps) { @@ -73,6 +194,7 @@ class SubmissionManagementPageContainer extends React.Component { this.setState({ needReload: true }); setTimeout(() => { loadMySubmissions(authTokens, challengeId); + this.loadReviewSummations(_.get(authTokens, 'tokenV3'), challengeId); this.setState({ needReload: false }); }, 2000); } @@ -84,17 +206,32 @@ class SubmissionManagementPageContainer extends React.Component { deletionSucceed, toBeDeletedId, mySubmissions, + authTokens, + challengeId, } = this.props; const { initialState } = this.state; if (initialState && mySubmissions) { // eslint-disable-next-line react/no-did-update-set-state this.setState({ - submissions: [...mySubmissions], + submissions: this.buildSubmissionsArray(mySubmissions), initialState: false, }); return; } + + if (challengeId !== prevProps.challengeId + || _.get(authTokens, 'tokenV3') !== _.get(prevProps.authTokens, 'tokenV3')) { + this.loadReviewSummations(_.get(authTokens, 'tokenV3'), challengeId); + } + + if (mySubmissions !== prevProps.mySubmissions && !initialState && mySubmissions) { + // eslint-disable-next-line react/no-did-update-set-state + this.setState({ + submissions: this.buildSubmissionsArray(mySubmissions), + }); + } + const { submissions } = this.state; if (deletionSucceed !== prevProps.deletionSucceed) { @@ -108,6 +245,101 @@ class SubmissionManagementPageContainer extends React.Component { } } + componentWillUnmount() { + this.isComponentMounted = false; + this.pendingReviewSummationChallengeId = null; + } + + buildSubmissionsArray = (source) => { + const { reviewSummationsBySubmission } = this.state; + const base = Array.isArray(source) ? source : []; + return base.map(submission => ( + mergeSubmissionWithSummations(submission, reviewSummationsBySubmission) + )); + }; + + refreshSubmissionScores = () => { + if (!this.isComponentMounted) { + return; + } + this.setState((prevState) => { + if (!Array.isArray(prevState.submissions) || !prevState.submissions.length) { + return null; + } + const updated = prevState.submissions.map(submission => ( + mergeSubmissionWithSummations(submission, prevState.reviewSummationsBySubmission) + )); + if (_.isEqual(updated, prevState.submissions)) { + return null; + } + return { submissions: updated }; + }); + }; + + loadReviewSummations = async (tokenV3, challengeId) => { + const challengeIdStr = _.toString(challengeId); + if (!this.isComponentMounted || !tokenV3 || !challengeIdStr) { + if (this.isComponentMounted) { + this.setState({ reviewSummationsBySubmission: {} }, () => { + this.refreshSubmissionScores(); + }); + } + return; + } + + this.pendingReviewSummationChallengeId = challengeIdStr; + + try { + const { data } = await getReviewSummationsService(tokenV3, challengeIdStr); + if (!this.isComponentMounted || this.pendingReviewSummationChallengeId !== challengeIdStr) { + return; + } + + const grouped = {}; + (Array.isArray(data) ? data : []).forEach((summation) => { + if (!summation) { + return; + } + const submissionId = _.toString(_.get(summation, 'submissionId') || _.get(summation, 'id')); + if (!submissionId) { + return; + } + if (!grouped[submissionId]) { + grouped[submissionId] = []; + } + grouped[submissionId].push(summation); + }); + + Object.keys(grouped).forEach((key) => { + grouped[key].sort((a, b) => ( + normalizeTimestamp(_.get(b, 'reviewedDate') || _.get(b, 'updatedAt') || _.get(b, 'createdAt')) + - normalizeTimestamp(_.get(a, 'reviewedDate') || _.get(a, 'updatedAt') || _.get(a, 'createdAt')) + )); + }); + + this.setState({ reviewSummationsBySubmission: grouped }, () => { + this.refreshSubmissionScores(); + }); + } catch (error) { + if (!this.isComponentMounted || this.pendingReviewSummationChallengeId !== challengeIdStr) { + return; + } + this.setState({ reviewSummationsBySubmission: {} }, () => { + this.refreshSubmissionScores(); + }); + } + }; + + getSubmissionScores = async (submissionId) => { + const submissionKey = _.toString(submissionId); + if (!submissionKey) { + return []; + } + const { reviewSummationsBySubmission } = this.state; + const summations = reviewSummationsBySubmission[submissionKey] || []; + return buildScoreEntries(summations); + }; + render() { const { authTokens, @@ -175,18 +407,7 @@ class SubmissionManagementPageContainer extends React.Component { }, getSubmissionArtifacts: submissionId => getSubmissionArtifacts(authTokens.tokenV3, submissionId), - getReviewTypesList: () => { - const reviewTypes = getReviewTypes(authTokens.tokenV3); - return reviewTypes; - }, - getChallengeResources: (cId) => { - const membersService = getMemberService(authTokens.tokenV3); - return membersService.getChallengeResources(cId); - }, - getSubmissionInformation: (submissionId) => { - const submissionsService = getService(authTokens.tokenV3); - return submissionsService.getSubmissionInformation(submissionId); - }, + getSubmissionScores: submissionId => this.getSubmissionScores(submissionId), onlineReviewUrl: `${config.URL.ONLINE_REVIEW}/review/actions/ViewProjectDetails?pid=${challengeId}`, challengeUrl: `${challengesUrl}/${challengeId}`, addSumissionUrl: `${config.URL.BASE}/challenges/${challengeId}/submit`, From 94f59287b670c0f7386980467093608e1a01c1eb Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Thu, 23 Oct 2025 11:11:58 +0300 Subject: [PATCH 59/76] Update CODEOWNERS to reflect default reviewers --- CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 35b5ce982d..bbed3eaae8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1 +1 @@ -* @ajefts @luizrrodrigues +* @jmgasper @kkartunov From 0514140088cb19fecdf7c63c4c4449f798195cca Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Thu, 23 Oct 2025 18:54:22 +0530 Subject: [PATCH 60/76] PM-2479 Fix delete option for design challenges --- src/shared/components/SubmissionManagement/Submission/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/components/SubmissionManagement/Submission/index.jsx b/src/shared/components/SubmissionManagement/Submission/index.jsx index 9f3b771603..16ee9da580 100644 --- a/src/shared/components/SubmissionManagement/Submission/index.jsx +++ b/src/shared/components/SubmissionManagement/Submission/index.jsx @@ -74,7 +74,7 @@ export default function Submission(props) { {formatDate(submissionObject.created)} { - track === COMPETITION_TRACKS.DES && ( + track === COMPETITION_TRACKS.COMPETITION_TRACKS.DES && ( Screening Status {safeForDownloadCheck !== true ? safeForDownloadCheck : submissionObject.screening From 3fbfb5727f7d6e07cd504e7164ccc02964fc2490 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Thu, 23 Oct 2025 18:55:50 +0530 Subject: [PATCH 61/76] deploy branch PM-2479 --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 77bd840cb0..fea4505934 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -182,7 +182,7 @@ workflows: only: - develop - v6 - - PM-2233 + - PM-2479 - "build-prod": context: org-global From af06ba3566e012a0c39a4c337c10457f33ce38a7 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Thu, 23 Oct 2025 19:33:27 +0530 Subject: [PATCH 62/76] test logs --- src/shared/components/SubmissionManagement/Submission/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/components/SubmissionManagement/Submission/index.jsx b/src/shared/components/SubmissionManagement/Submission/index.jsx index 16ee9da580..8393698d61 100644 --- a/src/shared/components/SubmissionManagement/Submission/index.jsx +++ b/src/shared/components/SubmissionManagement/Submission/index.jsx @@ -139,7 +139,7 @@ export default function Submission(props) { > */ } {status !== CHALLENGE_STATUS.COMPLETED - && track === COMPETITION_TRACKS.DES + && track === 'DESIGN' && safeForDownloadCheck === true && ( */ } {status !== CHALLENGE_STATUS.COMPLETED - && track === 'DESIGN' + && track === CHALLENGE_TRACKS.DES && safeForDownloadCheck === true && ( */ } {status !== CHALLENGE_STATUS.COMPLETED - && track === CHALLENGE_TRACKS.DES + && track === COMPETITION_TRACKS.DES && safeForDownloadCheck === true && (