diff --git a/.circleci/config.yml b/.circleci/config.yml index fbb4b55d3..5f3cac937 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -226,14 +226,7 @@ workflows: branches: only: - dev - - LVT-256 - - CORE-635 - - feat/review - - feat/system-admin - - feat/v6 - - pm-2074_1 - - feat/ai-workflows - - delete_user + - pm-3141_3 - deployQa: context: org-global diff --git a/package.json b/package.json index fab70bf67..cb3cc624d 100644 --- a/package.json +++ b/package.json @@ -19,204 +19,207 @@ "sb:build": "storybook build -o build/storybook" }, "dependencies": { - "@datadog/browser-logs": "^4.21.2", + "@datadog/browser-logs": "^4.50.1", "@hello-pangea/dnd": "^18.0.1", "@heroicons/react": "^1.0.6", - "@hookform/resolvers": "^4.1.2", + "@hookform/resolvers": "^4.1.3", "@popperjs/core": "^2.11.8", - "@sprig-technologies/sprig-browser": "^2.20.1", - "@storybook/addon-actions": "7.6.10", - "@storybook/react": "7.6.10", - "@stripe/react-stripe-js": "1.13.0", - "@stripe/stripe-js": "1.41.0", - "@tinymce/tinymce-react": "^6.2.1", - "@types/codemirror": "5.60.15", + "@sprig-technologies/sprig-browser": "^2.39.0", + "@storybook/addon-actions": "7.6.20", + "@storybook/react": "7.6.20", + "@stripe/react-stripe-js": "1.16.5", + "@stripe/stripe-js": "1.54.2", + "@tinymce/tinymce-react": "^6.3.0", + "@types/codemirror": "5.60.17", "amazon-s3-uri": "^0.1.1", - "apexcharts": "^3.36.0", - "axios": "^1.12.0", + "apexcharts": "^3.54.1", + "axios": "^1.13.2", "browser-cookies": "^1.2.0", - "city-timezones": "^1.2.1", - "classnames": "^2.3.2", + "city-timezones": "^1.3.2", + "classnames": "^2.5.1", "contentful": "^9.3.7", "country-calling-code": "0.0.3", "crypto-js": "^4.2.0", "customize-cra": "^1.0.0", "date-fns": "^2.30.0", - "dompurify": "^2.5.4", - "draft-js": "^0.10.4", - "draft-js-export-html": "^1.2.0", - "draft-js-markdown-shortcuts-plugin": "^0.3.0", - "draft-js-plugins-editor": "^2.0.3", + "dompurify": "^2.5.8", + "draft-js": "^0.11.7", + "draft-js-export-html": "^1.4.1", + "draft-js-markdown-shortcuts-plugin": "^0.6.1", + "draft-js-plugins-editor": "^2.1.1", "easymde": "2.20.0", - "express": "^4.21.2", - "express-fileupload": "^1.4.0", + "express": "^4.22.1", + "express-fileupload": "^1.5.2", "express-interceptor": "^1.2.0", - "filestack-js": "^3.42.0", + "filestack-js": "^3.44.2", "highcharts": "^10.3.3", - "highcharts-react-official": "^3.2.0", - "highlight.js": "^11.6.0", + "highcharts-react-official": "^3.2.3", + "highlight.js": "^11.11.1", "html2canvas": "^1.4.1", "lodash": "^4.17.21", "markdown-it": "^13.0.2", - "marked": "4.1.1", - "moment": "^2.29.4", + "marked": "4.3.0", + "moment": "^2.30.1", "moment-duration-format": "^2.3.2", - "moment-timezone": "^0.5.37", + "moment-timezone": "^0.6.0", "money": "^0.2.0", "prop-types": "^15.8.1", - "qrcode.react": "^3.1.0", - "qs": "^6.11.0", + "qrcode.react": "^3.2.0", + "qs": "^6.14.0", "rc-checkbox": "^2.3.2", - "react": "^18.2.0", - "react-apexcharts": "^1.4.0", - "react-color": "^2.13.8", - "react-contenteditable": "^3.3.6", - "react-css-super-themr": "^2.2.0", - "react-datepicker": "^4.14.1", - "react-dom": "^18.2.0", - "react-dropzone": "^11.3.2", + "react": "^18.3.1", + "react-apexcharts": "^1.9.0", + "react-color": "^2.19.3", + "react-contenteditable": "^3.3.7", + "react-css-super-themr": "^2.3.0", + "react-datepicker": "^4.25.0", + "react-dom": "^18.3.1", + "react-dropzone": "^11.7.1", "react-elastic-carousel": "^0.11.5", "react-gtm-module": "^2.0.11", "react-helmet": "^6.1.0", - "react-hook-form": "^7.54.2", - "react-markdown": "8.0.6", + "react-hook-form": "^7.68.0", + "react-markdown": "8.0.7", "react-otp-input": "^3.1.1", "react-popper": "^2.3.0", - "react-redux": "^8.0.4", - "react-redux-toastr": "^7.6.10", - "react-responsive": "^9.0.0-beta.5", - "react-responsive-modal": "^6.2.0", - "react-router-dom": "^6.4.2", + "react-redux": "^8.1.3", + "react-redux-toastr": "^7.6.13", + "react-responsive": "^9.0.2", + "react-responsive-modal": "^6.4.2", + "react-router-dom": "^6.30.2", "react-scripts": "5.0.1", - "react-select": "^5.8.0", - "react-spinners": "^0.13.6", - "react-toastify": "^9.0.8", - "react-tooltip": "5.11.1", - "redux": "^4.2.0", + "react-select": "^5.10.2", + "react-spinners": "^0.17.0", + "react-toastify": "^9.1.3", + "react-tooltip": "5.30.0", + "redux": "^4.2.1", "redux-actions": "^2.6.5", "redux-logger": "^3.0.6", "redux-promise": "^0.6.0", - "redux-promise-middleware": "^6.1.3", - "redux-thunk": "^2.4.1", + "redux-promise-middleware": "^6.2.0", + "redux-thunk": "^2.4.2", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", - "remark-breaks": "^3.0.2", + "remark-breaks": "^3.0.3", "remark-frontmatter": "^4.0.1", "remark-gfm": "^3.0.1", "remark-parse": "^11.0.0", "remove": "^0.1.5", - "sanitize-html": "^2.12.1", - "sass": "^1.79.0", - "styled-components": "^5.3.6", + "sanitize-html": "^2.17.0", + "sass": "^1.95.0", + "styled-components": "^5.3.11", "swr": "^1.3.0", "tc-auth-lib": "topcoder-platform/tc-auth-lib#v2.0", "tinymce": "^7.9.1", - "typescript": "^4.8.4", - "universal-navigation": "https://github.com/topcoder-platform/universal-navigation#9fc50d938be7182", + "typescript": "^4.9.5", + "universal-navigation": "https://github.com/topcoder-platform/universal-navigation#master", "uuid": "^11.1.0", - "yup": "^1.6.1" + "yup": "^1.7.1" }, "devDependencies": { - "@babel/core": "^7.19.3", - "@babel/plugin-syntax-jsx": "^7.18.6", - "@babel/plugin-transform-runtime": "^7.19.1", - "@babel/preset-env": "^7.19.4", - "@babel/preset-react": "^7.18.6", - "@babel/preset-typescript": "^7.18.6", - "@babel/runtime": "^7.19.4", + "@babel/core": "^7.28.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-runtime": "^7.28.5", + "@babel/preset-env": "^7.28.5", + "@babel/preset-react": "^7.28.5", + "@babel/preset-typescript": "^7.28.5", + "@babel/runtime": "^7.28.4", "@craco/craco": "^7.1.0", "@jackwilsdon/craco-use-babelrc": "1.0.0", - "@storybook/addon-essentials": "7.6.10", - "@storybook/addon-interactions": "7.6.10", - "@storybook/addon-links": "7.6.10", - "@storybook/blocks": "7.6.10", - "@storybook/preset-create-react-app": "7.6.10", - "@storybook/react-webpack5": "7.6.10", - "@storybook/testing-library": "^0.0.14-next.2", - "@testing-library/jest-dom": "^5.16.5", + "@storybook/addon-essentials": "7.6.20", + "@storybook/addon-interactions": "7.6.20", + "@storybook/addon-links": "7.6.20", + "@storybook/blocks": "7.6.20", + "@storybook/preset-create-react-app": "7.6.20", + "@storybook/react-webpack5": "7.6.20", + "@storybook/testing-library": "^0.2.2", + "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", - "@testing-library/user-event": "^14.4.3", - "@types/axios": "^0.14.0", - "@types/dompurify": "^2.3.4", - "@types/highlightjs": "^9.12.2", - "@types/jest": "^29.1.2", - "@types/lodash": "^4.14.186", + "@testing-library/user-event": "^14.6.1", + "@types/axios": "^0.14.4", + "@types/dompurify": "^2.4.0", + "@types/highlightjs": "^9.12.6", + "@types/jest": "^29.5.14", + "@types/lodash": "^4.17.21", "@types/markdown-it": "^12.2.3", - "@types/marked": "4.0.7", - "@types/node": "^18.8.5", - "@types/reach__router": "^1.3.11", - "@types/react": "18.0.35", - "@types/react-datepicker": "^4.11.2", - "@types/react-dom": "^18.0.6", - "@types/react-gtm-module": "^2.0.1", - "@types/react-helmet": "^6.1.6", - "@types/react-redux-toastr": "^7.6.2", + "@types/marked": "4.3.2", + "@types/node": "^18.19.130", + "@types/reach__router": "^1.3.15", + "@types/react": "18.3.27", + "@types/react-datepicker": "^4.19.6", + "@types/react-dom": "^18.3.7", + "@types/react-gtm-module": "^2.0.4", + "@types/react-helmet": "^6.1.11", + "@types/react-redux-toastr": "^7.6.6", "@types/react-router-dom": "^5.3.3", - "@types/redux-actions": "2.6.2", - "@types/redux-logger": "^3.0.9", - "@types/redux-promise": "^0.5.29", - "@types/sanitize-html": "^2.6.2", - "@types/systemjs": "^6.1.1", - "@types/testing-library__jest-dom": "^5.14.5", + "@types/redux-actions": "2.6.5", + "@types/redux-logger": "^3.0.13", + "@types/redux-promise": "^0.5.32", + "@types/sanitize-html": "^2.16.0", + "@types/systemjs": "^6.15.4", + "@types/testing-library__jest-dom": "^5.14.9", "@types/uuid": "^8.3.4", - "@typescript-eslint/eslint-plugin": "^5.30.6", - "@typescript-eslint/parser": "^5.30.6", - "@wdio/junit-reporter": "^7.25.1", - "autoprefixer": "^10.4.12", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", + "@wdio/junit-reporter": "^7.40.0", + "autoprefixer": "^10.4.22", "babel-eslint": "^11.0.0-beta.2", - "babel-jest": "^29.2.0", - "babel-plugin-inline-react-svg": "^2.0.1", + "babel-jest": "^29.7.0", + "babel-plugin-inline-react-svg": "^2.0.2", "babel-plugin-module-resolver": "^4.1.0", "babel-plugin-named-exports-order": "^0.0.2", "babel-plugin-react-css-modules": "^5.2.6", - "concurrently": "^7.4.0", - "craco-css-modules": "^1.0.5", + "concurrently": "^7.6.0", + "craco-css-modules": "^1.0.6", "craco-plugin-env": "^1.0.5", "craco-resolve-url-loader": "^1.0.0", "cross-env": "^7.0.3", - "css-loader": "3.5.3", - "eslint": "^8.25.0", + "css-loader": "3.6.0", + "eslint": "^8.57.1", "eslint-config-airbnb": "^19.0.4", "eslint-config-react-app": "^7.0.1", "eslint-config-react-important-stuff": "^3.0.0", - "eslint-import-resolver-typescript": "^3.2.5", - "eslint-plugin-import": "^2.25.3", + "eslint-import-resolver-typescript": "^3.10.1", + "eslint-plugin-import": "^2.32.0", "eslint-plugin-ordered-imports": "^0.6.0", - "eslint-plugin-react": "^7.28.0", - "eslint-plugin-react-hooks": "^4.3.0", - "eslint-plugin-unicorn": "^46.0.0", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-unicorn": "^46.0.1", "file-loader": "^6.2.0", - "husky": "^8.0.1", + "husky": "^8.0.3", "identity-obj-proxy": "^3.0.0", - "istanbul-lib-coverage": "^3.2.0", - "jest": "^29.2.0", - "jest-cli": "^29.2.0", - "lint-staged": "^13.0.3", + "istanbul-lib-coverage": "^3.2.2", + "jest": "^29.7.0", + "jest-cli": "^29.7.0", + "lint-staged": "^13.3.0", "nyc": "^15.1.0", - "postcss-loader": "^4.0.4", - "postcss-scss": "^3.0.2", - "pretty-quick": "^3.1.3", - "react-docgen-typescript": "^2.2.2", - "react-hot-loader": "^4.3.3", + "postcss-loader": "^4.3.0", + "postcss-scss": "^3.0.5", + "pretty-quick": "^3.3.1", + "react-docgen-typescript": "^2.4.0", + "react-hot-loader": "^4.13.1", "resolve-url-loader": "^5.0.0", - "rimraf": "^6.0.1", + "rimraf": "^6.1.2", "sass-loader": "^13.3.3", - "serve": "^14.0.1", - "start-server-and-test": "^1.14.0", - "storybook": "7.6.10", - "style-loader": "^3.3.1", + "serve": "^14.2.5", + "start-server-and-test": "^2.1.3", + "storybook": "7.6.20", + "style-loader": "^3.3.4", "systemjs-webpack-interop": "^2.3.7", - "tsconfig-paths-webpack-plugin": "^4.0.1", - "typed-scss-modules": "^7.0.1", - "webpack": "^5.79.0", + "tsconfig-paths-webpack-plugin": "^4.2.0", + "typed-scss-modules": "^7.1.4", + "webpack": "^5.103.0", "webpack-cli": "^4.10.0", - "webpack-config-single-spa-react": "^4.0.3", - "webpack-dev-server": "^4.11.1", - "webpack-merge": "^5.8.0" + "webpack-config-single-spa-react": "^4.0.5", + "webpack-dev-server": "^4.15.2", + "webpack-merge": "^5.10.0" }, "resolutions": { "@types/react": "18.0.35", - "string-width": "4.2.0" + "string-width": "4.2.0", + "node-fetch": "2.6.7", + "nth-check": "2.0.1", + "lodash.mergewith": "4.6.2" }, "browserslist": { "production": [ diff --git a/src/apps/profiles/src/components/MemberBadgeModal/MemberBadgeModal.tsx b/src/apps/profiles/src/components/MemberBadgeModal/MemberBadgeModal.tsx index 05834b65e..e567f3379 100644 --- a/src/apps/profiles/src/components/MemberBadgeModal/MemberBadgeModal.tsx +++ b/src/apps/profiles/src/components/MemberBadgeModal/MemberBadgeModal.tsx @@ -36,6 +36,7 @@ const MemberBadgeModal: FC = (props: MemberBadgeModalProp {' '} {format(new Date(props.selectedBadge.awarded_at), 'PPP')} + {/* @ts-expect-error: TS2786: ReactMarkdown cannot be used as a JSX component */} = props => { isSubmissionDownloadRestrictedForMember, getRestrictionMessageForMember, }: UseSubmissionDownloadAccessResult = useSubmissionDownloadAccess() + const { loginUserInfo }: ReviewAppContextModel = useContext(ReviewAppContext) + const { canViewAllSubmissions }: UseRolePermissionsResult = useRolePermissions() const { challengeInfo }: ChallengeDetailContextModel = useContext(ChallengeDetailContext) + const isCompletedDesignChallenge = useMemo(() => { + if (!challengeInfo) return false + const type = challengeInfo.track.name ? String(challengeInfo.track.name) + .toLowerCase() : '' + const status = challengeInfo.status ? String(challengeInfo.status) + .toLowerCase() : '' + return type === 'design' && ( + status === 'completed' + ) + }, [challengeInfo]) + + const isSubmissionsViewable = useMemo(() => { + if (!challengeInfo?.metadata?.length) return false + return challengeInfo.metadata.some(m => m.name === 'submissionsViewable' && String(m.value) + .toLowerCase() === 'true') + }, [challengeInfo]) + + const canViewSubmissions = useMemo(() => { + if (isCompletedDesignChallenge) { + return canViewAllSubmissions || isSubmissionsViewable + } + + return true + }, [isCompletedDesignChallenge, isSubmissionsViewable, canViewAllSubmissions]) + const submissionMetaById = useMemo( () => { const map = new Map() @@ -205,19 +234,32 @@ export const TabContentSubmissions: FC = props => { const filteredSubmissions = useMemo( () => { + + const filterFunc = (submissions: BackendSubmission[]): BackendSubmission[] => submissions + .filter(submission => { + if (!canViewSubmissions) { + return String(submission.memberId) === String(loginUserInfo?.userId) + } + + return true + }) + const filteredByUserId = filterFunc(latestBackendSubmissions) + const filteredByUserIdSubmissions = filterFunc(props.submissions) if (restrictToLatest && hasLatestFlag) { return latestBackendSubmissions.length - ? latestBackendSubmissions - : props.submissions + ? filteredByUserId + : filteredByUserIdSubmissions } - return props.submissions + return filteredByUserIdSubmissions }, [ latestBackendSubmissions, props.submissions, restrictToLatest, hasLatestFlag, + canViewSubmissions, + loginUserInfo?.userId, ], ) diff --git a/src/apps/review/src/lib/components/MarkdownReview/MarkdownReview.tsx b/src/apps/review/src/lib/components/MarkdownReview/MarkdownReview.tsx index 2674e0f1e..34974326a 100644 --- a/src/apps/review/src/lib/components/MarkdownReview/MarkdownReview.tsx +++ b/src/apps/review/src/lib/components/MarkdownReview/MarkdownReview.tsx @@ -20,6 +20,7 @@ interface Props { export const MarkdownReview: FC = (props: Props) => (
+ {/* @ts-expect-error: TS2786: ReactMarkdown cannot be used as a JSX component */} = props => { addAppeal, isSavingAppeal, }: ScorecardViewerContextValue = useScorecardViewerContext() + const { isAdmin, hasReviewerRole }: UseRolePermissionsResult = useRolePermissions() const { challengeInfo }: ChallengeDetailContextModel = useContext( ChallengeDetailContext, @@ -175,7 +177,7 @@ const ReviewComment: FC = props => { appeal={props.appeal} reviewItem={props.reviewItem} scorecardQuestion={props.question} - canRespondToAppeal={isReviewerRole} + canRespondToAppeal={isAdmin || hasReviewerRole} > {isSubmitter && canAddAppeal && (
diff --git a/src/apps/review/src/lib/components/ScorecardDetails/ScorecardDetails.module.scss b/src/apps/review/src/lib/components/ScorecardDetails/ScorecardDetails.module.scss deleted file mode 100644 index fafe94125..000000000 --- a/src/apps/review/src/lib/components/ScorecardDetails/ScorecardDetails.module.scss +++ /dev/null @@ -1,148 +0,0 @@ -@import '@libs/ui/styles/includes'; - -.container { - display: flex; - flex-direction: column; - margin-top: $sp-10; -} - -.blockForm { - display: flex; - flex-direction: column; - width: 100%; -} - -.textTotalScore { - display: flex; - align-items: center; - gap: 9px; - margin-right: 31px; - font-size: 16px; - line-height: 22px; - /* identical to box height, or 138% */ - letter-spacing: -0.02em; - font-weight: $font-weight-semibold; -} - -.textErrorTop, -.textErrorBottom { - align-items: center; - color: var(--RedError); - display: flex; - font-family: 'Nunito Sans', sans-serif; - font-size: 14px; - line-height: 20px; - font-weight: 700; - & > i { - margin-right: $sp-2; - } -} - -.textErrorTop { - margin-bottom: $sp-6; - - &.isEmpty { - height: 2px; - line-height: 2px; - } -} - -.blockTable { - font-size: 16px; - line-height: 22px; - @include ltemd { - display: block; - tbody { - display: block; - } - } -} - -.blockRowGroup { - @include ltemd { - display: block; - } - td { - background-color: var(--Actived); - font-family: 'Nunito Sans', sans-serif; - color: white; - padding: $sp-4; - font-weight: 700; - @include ltemd { - display: block; - } - } -} - -.blockRowSection { - td { - color: var(--FontColor); - font-family: 'Nunito Sans', sans-serif; - font-size: 14px; - padding: $sp-4; - background-color: var(--TableSubHeader); - font-weight: 700; - line-height: 20px; - - @include ltemd { - display: block; - padding: $sp-4; - } - } - @include ltemd { - display: block; - td:nth-child(2), - td:nth-child(3) { - display: none; - } - } -} - -.blockBottomEdit { - display: flex; - align-items: center; - gap: 20px; - justify-content: space-between; - margin-top: 64px; - margin-bottom: 56px; - flex-wrap: wrap; - @include ltemd { - gap: 10px; - margin: $sp-4 0; - } -} - -.blockBtns { - display: flex; - gap: $sp-4; - flex-wrap: wrap; - @include ltemd { - gap: 10px; - } -} - -.blockBottomView { - .textTotalScore { - background-color: var(--TableColumn); - color: var(--FontColor); - font-family: 'Nunito Sans', sans-serif; - font-weight: 700; - padding: $sp-4 0; - margin-right: 0; - justify-content: flex-end; - span:nth-child(1) { - text-align: center; - width: 126px; - } - span:nth-child(2) { - padding: 0 $sp-4; - width: 160px; - } - } - .buttons { - margin-top: 64px; - display: flex; - justify-content: flex-end; - margin-bottom: 56px; - } -} diff --git a/src/apps/review/src/lib/components/ScorecardDetails/ScorecardDetails.tsx b/src/apps/review/src/lib/components/ScorecardDetails/ScorecardDetails.tsx deleted file mode 100644 index 1bd36042c..000000000 --- a/src/apps/review/src/lib/components/ScorecardDetails/ScorecardDetails.tsx +++ /dev/null @@ -1,844 +0,0 @@ -/** - * Scorecard Details. - */ -import { - Dispatch, - ElementType, - FC, - Fragment, - SetStateAction, - useCallback, - useContext, - useEffect, - useMemo, - useState, -} from 'react' -import { useForm, UseFormReturn } from 'react-hook-form' -import { useSWRConfig } from 'swr' -import { NavLink } from 'react-router-dom' -import { filter, forEach, isEmpty, kebabCase, reduce } from 'lodash' -import classNames from 'classnames' - -import { yupResolver } from '@hookform/resolvers/yup' -import { TableLoading } from '~/apps/admin/src/lib' - -import { useAppNavigate } from '../../hooks' -import { - AppealInfo, - ChallengeDetailContextModel, - FormReviews, - MappingAppeal, - ReviewInfo, - ReviewItemInfo, - ScorecardInfo, -} from '../../models' -import { ScorecardDetailsHeader } from '../ScorecardDetailsHeader' -import { ScorecardQuestionEdit } from '../ScorecardQuestionEdit' -import { ScorecardQuestionView } from '../ScorecardQuestionView' -import { formReviewsSchema, roundWith2DecimalPlaces } from '../../utils' -import { ConfirmModal } from '../ConfirmModal' -import { IconError } from '../../assets/icons' -import { ReviewItemComment } from '../../models/ReviewItemComment.model' -import { ChallengeDetailContext } from '../../contexts' -import { - activeReviewAssignmentsRouteId, - rootRoute, -} from '../../../config/routes.config' - -import styles from './ScorecardDetails.module.scss' - -interface Props { - className?: string - isEdit: boolean - isManagerEdit?: boolean - onCancelEdit: () => void - setIsChanged: Dispatch> - scorecardInfo?: ScorecardInfo - isLoading: boolean - reviewInfo?: ReviewInfo - isSavingReview: boolean - isSavingAppeal: boolean - isSavingAppealResponse: boolean - isSavingManagerComment: boolean - saveReviewInfo: ( - updatedReview: FormReviews | undefined, - fullReview: FormReviews | undefined, - committed: boolean, - totalScore: number, - success: () => void, - ) => void - mappingAppeals: MappingAppeal - addAppeal: ( - content: string, - commentItem: ReviewItemComment, - success: () => void, - ) => void - doDeleteAppeal: ( - appealInfo: AppealInfo | undefined, - success: () => void, - ) => void - addAppealResponse: ( - content: string, - updatedResponse: string, - appeal: AppealInfo, - reviewItem: ReviewItemInfo, - success: () => void, - ) => void - addManagerComment: ( - content: string, - updatedResponse: string, - reviewItem: ReviewItemInfo, - success: () => void, - ) => void -} - -export const ScorecardDetails: FC = (props: Props) => { - const className = props.className - const isEdit = props.isEdit - const isManagerEdit = props.isManagerEdit ?? false - const onCancelEdit = props.onCancelEdit - const setIsChanged = props.setIsChanged - const scorecardInfo = props.scorecardInfo - const isLoading = props.isLoading - const reviewInfo = props.reviewInfo - const isSavingReview = props.isSavingReview - const isSavingAppeal = props.isSavingAppeal - const isSavingAppealResponse = props.isSavingAppealResponse - const isSavingManagerComment = props.isSavingManagerComment - const saveReviewInfo = props.saveReviewInfo - const mappingAppeals = props.mappingAppeals - const addAppeal = props.addAppeal - const doDeleteAppeal = props.doDeleteAppeal - const addAppealResponse = props.addAppealResponse - const addManagerComment = props.addManagerComment - const navigate = useAppNavigate() - const { challengeId, challengeInfo }: ChallengeDetailContextModel = useContext( - ChallengeDetailContext, - ) - const { mutate }: { mutate: (key: any, data?: any, opts?: any) => Promise } = useSWRConfig() - const [isExpand, setIsExpand] = useState<{ [key: string]: boolean }>({}) - const [isShowSaveAsDraftModal, setIsShowSaveAsDraftModal] = useState(false) - const [shouldRedirectAfterDraft, setShouldRedirectAfterDraft] = useState(false) - const mappingReviewInfo = useMemo<{ - [key: string]: { - item: ReviewItemInfo - index: number - } - }>(() => { - const result: { - [key: string]: { - item: ReviewItemInfo - index: number - } - } = {} - forEach(reviewInfo?.reviewItems ?? [], (item, index) => { - const normalizedId = normalizeScorecardQuestionId( - item.scorecardQuestionId, - ) - if (!normalizedId) { - return - } - - result[normalizedId] = { - index, - item, - } - }) - return result - }, [reviewInfo]) - const [isTouched, setIsTouched] = useState<{ [key: string]: boolean }>({}) - - const { - control, - handleSubmit, - reset, - formState: { errors, isDirty }, - getValues, - trigger, - }: UseFormReturn = useForm({ - defaultValues: { - reviews: [], - }, - mode: 'onChange', - resolver: yupResolver(formReviewsSchema), - }) - - const changeHandle = setIsChanged - useEffect(() => { - changeHandle(isDirty) - }, [isDirty, changeHandle]) - - const redirectToCurrentPhaseTab = useCallback(async () => { - if (!challengeId) { - return - } - - const challengeDetailsRoute - = `${rootRoute}/${activeReviewAssignmentsRouteId}/${challengeId}/challenge-details` - - try { - await mutate( - (key: unknown) => ( - typeof key === 'string' - && key.startsWith(`reviewBaseUrl/reviews/${challengeId}/`) - ), - ) - await mutate(`reviewBaseUrl/submissions/${challengeId}`) - } catch { - // no-op: navigation should still occur even if revalidation fails - } - - const tabFromPhase = computeTabFromPhase( - (challengeInfo?.phases || []) as Array<{ - id?: string - name?: string - scheduledStartDate?: string - actualStartDate?: string - }>, - reviewInfo?.phaseId, - challengeInfo?.type?.name, - challengeInfo?.type?.abbreviation, - ) - const hasIterativePhase = (challengeInfo?.phases || []) - .some(p => (p?.name || '').toString() - .toLowerCase() - .startsWith('iterative review')) - const tabSlug = tabFromPhase || (hasIterativePhase ? 'iterative-review' : 'review') - - navigate(`${challengeDetailsRoute}?tab=${tabSlug}`) - }, [ - challengeId, - challengeInfo?.phases, - challengeInfo?.type?.abbreviation, - challengeInfo?.type?.name, - mutate, - navigate, - reviewInfo?.phaseId, - ]) - - const errorMessageTop - = isEmpty(errors) || isEmpty(isTouched) - ? '' - : 'There were validation errors. Check below.' - - const errorMessageBottom - = isEmpty(errors) || isEmpty(isTouched) - ? '' - : 'There were validation errors. Check above.' - - const touchedAllField = useCallback(() => { - const formData = getValues() - const isTouchedAll: { [key: string]: boolean } = {} - forEach(formData.reviews, review => { - isTouchedAll[`reviews.${review.index}.initialAnswer.message`] = true - forEach(review.comments, comment => { - isTouchedAll[ - `reviews.${review.index}.comments.${comment.index}.content` - ] = true - }) - }) - setIsTouched(isTouchedAll) - }, [getValues]) - - const ContainerTag = useMemo( - () => (isEdit ? 'form' : 'div'), - [isEdit], - ) - - const [reviewProgress, setReviewProgress] = useState(0) - const [totalScore, setTotalScore] = useState(0) - const displayedTotalScore = useMemo(() => { - const maybeFinalScore = reviewInfo?.finalScore - if ( - !isEdit - && typeof maybeFinalScore === 'number' - && Number.isFinite(maybeFinalScore) - ) { - return roundWith2DecimalPlaces(maybeFinalScore) - } - - return totalScore - }, [isEdit, reviewInfo?.finalScore, totalScore]) - - const onSubmit = useCallback((data: FormReviews) => { - saveReviewInfo( - isDirty ? getValues() : undefined, - getValues(), - true, - totalScore, - () => { - reset(data) - redirectToCurrentPhaseTab() - .catch(() => undefined) - }, - ) - }, [ - getValues, - isDirty, - redirectToCurrentPhaseTab, - reset, - saveReviewInfo, - totalScore, - ]) - - const recalculateReviewProgress = useCallback(() => { - const reviewFormDatas = getValues().reviews - const mapingResult: { - [scorecardQuestionId: string]: string - } = {} - const newReviewProgress - = reviewFormDatas.length > 0 - ? Math.round( - (filter(reviewFormDatas, review => { - const normalizedId = normalizeScorecardQuestionId( - review.scorecardQuestionId, - ) - if (normalizedId) { - mapingResult[normalizedId] = review.initialAnswer - } - - return !!review.initialAnswer - }).length - * 100) - / reviewFormDatas.length, - ) - : 0 - setReviewProgress(newReviewProgress) - - const groupsScore = reduce( - scorecardInfo?.scorecardGroups ?? [], - (groupResult, group) => { - const groupPoint - = (reduce( - group.sections ?? [], - (sectionResult, section) => { - const sectionPoint - = (reduce( - section.questions ?? [], - (questionResult, question) => { - let questionPoint = 0 - const normalizedQuestionId - = normalizeScorecardQuestionId( - question.id as string, - ) - const initialAnswer - = normalizedQuestionId - ? mapingResult[ - normalizedQuestionId - ] - : undefined - if ( - question.type === 'YES_NO' - && initialAnswer === 'Yes' - ) { - questionPoint = 100 - } else if ( - question.type === 'SCALE' - && !!initialAnswer - ) { - const totalPoint - = question.scaleMax - - question.scaleMin - const initialAnswerNumber - = parseInt(initialAnswer, 10) - - question.scaleMin - questionPoint - = totalPoint > 0 - ? (initialAnswerNumber - * 100) - / totalPoint - : 0 - } - - return ( - questionResult - + (questionPoint * question.weight) - / 100 - ) - }, - 0, - ) - * section.weight) - / 100 - return sectionResult + sectionPoint - }, - 0, - ) - * group.weight) / 100 - return groupResult + groupPoint - }, - 0, - ) - setTotalScore(roundWith2DecimalPlaces(groupsScore)) - }, [getValues, scorecardInfo]) - - useEffect(() => { - if (reviewInfo) { - const newFormData = { - reviews: reviewInfo.reviewItems.map( - (reviewItem, reviewItemIndex) => ({ - comments: reviewItem.reviewItemComments.map( - (commentItem, commentIndex) => ({ - content: commentItem.content ?? '', - id: commentItem.id, - index: commentIndex, - type: commentItem.type ?? '', - }), - ), - id: reviewItem.id, - index: reviewItemIndex, - initialAnswer: reviewItem.finalAnswer || reviewItem.initialAnswer, - scorecardQuestionId: reviewItem.scorecardQuestionId, - }), - ), - } - reset(newFormData) - recalculateReviewProgress() - } - }, [reviewInfo, recalculateReviewProgress, reset]) - - const expandAll = useCallback(() => { - setIsExpand( - reduce( - reviewInfo?.reviewItems ?? [], - (result, reviewItem) => ({ ...result, [reviewItem.id]: true }), - {}, - ), - ) - }, [reviewInfo]) - - const back = useCallback(async (e: React.MouseEvent) => { - e.preventDefault() - try { - if (challengeId) { - // Ensure the challenge details reflect the latest data (e.g., active phase) - await mutate(`challengeBaseUrl/challenges/${challengeId}`) - } - } catch { - // no-op: navigation should still occur even if revalidation fails - } - - const pastPrefix = '/past-challenges/' - // eslint-disable-next-line no-restricted-globals - const idx = location.pathname.indexOf(pastPrefix) - const url = idx > -1 - ? `${rootRoute}/past-challenges/${challengeInfo?.id}/challenge-details` - : `${rootRoute}/active-challenges/${challengeInfo?.id}/challenge-details` - navigate(url, { - fallback: './../../../../challenge-details', - }) - }, [challengeId, mutate, navigate]) - - const closeHandel = useCallback(() => { - setIsShowSaveAsDraftModal(false) - if (shouldRedirectAfterDraft) { - setShouldRedirectAfterDraft(false) - redirectToCurrentPhaseTab() - .catch(() => undefined) - } - }, [redirectToCurrentPhaseTab, shouldRedirectAfterDraft]) - - return isLoading ? () : ( -
- - {errorMessageTop && ( -
- - - - {errorMessageTop} -
- )} - - {reviewInfo && ( - - - - {(scorecardInfo?.scorecardGroups || []).map( - (group, groupIndex) => ( - - - - - - {group.sections.map( - (section, sectionIndex) => ( - - - - - - - {section.questions.map( - ( - question, - questionIndex, - ) => { - const normalizedQuestionId - = normalizeScorecardQuestionId( - question.id as string, - ) - const reviewItemInfo - = normalizedQuestionId - ? mappingReviewInfo[ - normalizedQuestionId - ] - : undefined - if ( - !reviewItemInfo - ) { - return undefined - } - - return isEdit ? ( - - ) : ( - - ) - }, - )} - - ), - )} - - ), - )} - -
- {`${groupIndex + 1}. ${group.name} (${group.weight.toFixed(1)})`} -
- {`${section.name} (${section.weight.toFixed(1)})`} - WeightResponse
- - {isEdit ? ( -
-
- {errorMessageBottom && ( -
- - - - {errorMessageBottom} -
- )} -
- -
- - - -
-
- ) : ( -
-
- Total Score: - {displayedTotalScore} -
-
- - Back to Challenge - -
-
- )} -
- )} - -
Your draft has been successfully saved!
-
-
- ) -} - -export default ScorecardDetails - -function normalizeScorecardQuestionId( - id?: string | null, -): string | undefined { - if (id === undefined || id === null) { - return undefined - } - - const normalized = `${id}`.trim() - .toLowerCase() - - return normalized || undefined -} - -// Helpers extracted to tame complexity and satisfy lint rules -function normString(s?: string): string { - return (s || '').trim() - .toLowerCase() -} - -function isExactRegistration(name?: string): boolean { - return normString(name) === 'registration' -} - -function isExactSubmission(name?: string): boolean { - return normString(name) === 'submission' -} - -function isIterativeReview(name?: string): boolean { - return normString(name) - .includes('iterative review') -} - -function sortPhasesForDetails( - phases: Array<{ - id?: string - name?: string - scheduledStartDate?: string - actualStartDate?: string - }>, - typeName?: string, - typeAbbrev?: string, -): Array<{ - id?: string - name?: string - scheduledStartDate?: string - actualStartDate?: string -}> { - let sorted = [...phases].sort((a, b) => { - const aStart = new Date(a.actualStartDate || a.scheduledStartDate || '') - .getTime() - const bStart = new Date(b.actualStartDate || b.scheduledStartDate || '') - .getTime() - if (!Number.isNaN(aStart) && !Number.isNaN(bStart)) { - if (aStart !== bStart) return aStart - bStart - const aReg = isExactRegistration(a.name) - const bReg = isExactRegistration(b.name) - const aSub = isExactSubmission(a.name) - const bSub = isExactSubmission(b.name) - if (aReg && bSub) return -1 - if (aSub && bReg) return 1 - } - - return 0 - }) - - const tn = typeName?.toLowerCase?.() || '' - const ta = typeAbbrev?.toLowerCase?.() || '' - const isF2F = ta === 'f2f' || tn.replace(/\s|-/g, '') === 'first2finish' - if (isF2F) { - const iterative = sorted.filter(p => isIterativeReview(p.name)) - if (iterative.length) { - const remaining = sorted.filter(p => !isIterativeReview(p.name)) - const regIdx = remaining.findIndex(p => isExactRegistration(p.name)) - const subIdx = remaining.findIndex(p => isExactSubmission(p.name)) - const afterIdx = Math.max(regIdx, subIdx) - if (afterIdx >= 0 && afterIdx < remaining.length) { - sorted = [ - ...remaining.slice(0, afterIdx + 1), - ...iterative, - ...remaining.slice(afterIdx + 1), - ] - } else { - sorted = [...remaining, ...iterative] - } - } - } - - return sorted -} - -function computeTabFromPhase( - phases: Array<{ - id?: string - name?: string - scheduledStartDate?: string - actualStartDate?: string - }>, - targetPhaseId?: string, - typeName?: string, - typeAbbrev?: string, -): string | undefined { - if (!phases.length || !targetPhaseId) return undefined - - const sorted = sortPhasesForDetails(phases, typeName, typeAbbrev) - - // Number duplicate labels the same way as ChallengeDetailsPage - const counts = new Map() - for (const p of sorted) { - const raw = (p?.name || '').trim() - if (raw) { - const n = counts.get(raw) || 0 - counts.set(raw, n + 1) - const label = n === 0 ? raw : `${raw} ${n + 1}` - if (p.id === targetPhaseId) { - return kebabCase(label) - } - } - } - - return undefined -} diff --git a/src/apps/review/src/lib/components/ScorecardDetails/index.ts b/src/apps/review/src/lib/components/ScorecardDetails/index.ts deleted file mode 100644 index 51a168df3..000000000 --- a/src/apps/review/src/lib/components/ScorecardDetails/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as ScorecardDetails } from './ScorecardDetails' diff --git a/src/apps/review/src/lib/components/ScorecardDetailsHeader/ScorecardDetailsHeader.module.scss b/src/apps/review/src/lib/components/ScorecardDetailsHeader/ScorecardDetailsHeader.module.scss deleted file mode 100644 index ae1f4f004..000000000 --- a/src/apps/review/src/lib/components/ScorecardDetailsHeader/ScorecardDetailsHeader.module.scss +++ /dev/null @@ -1,55 +0,0 @@ -@import '@libs/ui/styles/includes'; - -.blockTitle { - display: flex; - align-items: center; - justify-content: space-between; - gap: 20px; - flex-wrap: wrap; - position: sticky; - top: 0; - background-color: white; - z-index: 1; - padding-top: $sp-6; - padding-bottom: $sp-6; - @include ltemd { - top: 46px; - } -} - -.blockTitleLeft { - display: flex; - align-items: center; - gap: 18px; - flex-wrap: wrap; -} - -.textTitle { - color: var(--FontColor); - font-family: "Figtree", sans-serif; - font-size: 26px; - line-height: 30px; - font-weight: $font-weight-bold; -} - -.textTotalScore, -.textCompleted { - color: var(--FontColor); - font-family: 'Nunito Sans', sans-serif; - font-size: 16px; - line-height: 22px; - font-weight: $font-weight-bold; -} - -.textTotalScore { - display: flex; - align-items: center; - gap: 9px; - margin-right: 31px; -} - -.blockBtns { - display: flex; - gap: $sp-4; - flex-wrap: wrap; -} diff --git a/src/apps/review/src/lib/components/ScorecardDetailsHeader/ScorecardDetailsHeader.tsx b/src/apps/review/src/lib/components/ScorecardDetailsHeader/ScorecardDetailsHeader.tsx deleted file mode 100644 index 3fa0b0ffb..000000000 --- a/src/apps/review/src/lib/components/ScorecardDetailsHeader/ScorecardDetailsHeader.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Scorecard Details Header. - */ -import { FC } from 'react' -import classNames from 'classnames' - -import { ScorecardInfo } from '../../models' -import { ProgressBar } from '../ProgressBar' - -import styles from './ScorecardDetailsHeader.module.scss' - -interface Props { - isEdit: boolean - scorecardInfo?: ScorecardInfo - reviewProgress?: number - totalScore?: number - expandAll?: () => void - collapseAll?: () => void -} - -export const ScorecardDetailsHeader: FC = (props: Props) => ( -
-
- - {props.scorecardInfo?.name ?? ''} - - - - -
- -
- - -
- Total Score: - {props.totalScore} -
-
-
-) - -export default ScorecardDetailsHeader diff --git a/src/apps/review/src/lib/components/ScorecardDetailsHeader/index.ts b/src/apps/review/src/lib/components/ScorecardDetailsHeader/index.ts deleted file mode 100644 index 03f4d7d9e..000000000 --- a/src/apps/review/src/lib/components/ScorecardDetailsHeader/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as ScorecardDetailsHeader } from './ScorecardDetailsHeader' diff --git a/src/apps/review/src/lib/components/ScorecardQuestionEdit/ScorecardQuestionEdit.module.scss b/src/apps/review/src/lib/components/ScorecardQuestionEdit/ScorecardQuestionEdit.module.scss deleted file mode 100644 index 48601f149..000000000 --- a/src/apps/review/src/lib/components/ScorecardQuestionEdit/ScorecardQuestionEdit.module.scss +++ /dev/null @@ -1,208 +0,0 @@ -@import '@libs/ui/styles/includes'; - -.container { - @include ltemd { - display: flex; - flex-wrap: wrap; - } - td { - vertical-align: middle; - @include ltemd { - &:nth-child(1) { - width: 100%; - } - } - } - :global(.borderButton) { - font-size: 14px; - margin-top: $sp-6; - margin-left: 136px; - margin-bottom: $sp-4; - @include ltemd { - margin-left: 0; - } - } -} - -.blockQuestion { - color: var(--FontColor); - font-family: 'Nunito Sans', sans-serif; - display: flex; - align-items: center; - gap: 16px; - font-weight: 700; -} - -.blockCellWeight { - color: var(--FontColor); - font-family: 'Nunito Sans', sans-serif; - font-size: 14px; - font-weight: 700; - width: 100px; - i { - display: none; - } - - @include ltemd { - align-items: center; - display: flex; - width: 120px; - margin-right: auto; - i { - display: block; - margin-right: $sp-2; - } - } -} - -.blockCellResponse { - width: 160px; - font-size: 14px; - :global(.react-select-container .select__single-value) { - font-size: 14px; - } -} - -.btnExpand { - &.expand { - transform: rotate(180deg); - } -} - -.textQuestion { - align-items: center; - display: flex; - strong { - width: 138px; - } - @include ltemd { - display: inline; - strong { - padding-right: $sp-2; - } - } -} - -.errorMessage { - td { - padding: 0 $sp-4; - text-align: right; - } - &.isError { - border-left: 2px solid var(--RedError); - border-right: 2px solid var(--RedError); - } - :global(.errorMessage) { - margin-bottom: $sp-4; - } -} - -.blockRowQuestionHeader { - @include ltemd { - padding-bottom: $sp-3; - } - td { - padding: $sp-4; - - @include ltemd { - padding: $sp-3 $sp-2 0; - - &:first-child { - padding-left: $sp-2; - } - } - } - - &.isExpand { - td { - padding-bottom: 0; - } - } - - &.isError { - border-top: 2px solid var(--RedError); - border-left: 2px solid var(--RedError); - border-right: 2px solid var(--RedError); - } -} - -.textGuidelines { - color: var(--FontColor); - font-family: "Nunito Sans", sans-serif; - font-size: 14px; - line-height: 20px; -} - -.blockRowGuidelines { - td { - padding: 8px 192px 22px; - - @include ltemd { - padding: 16px 8px; - } - } - - &.isError { - border-left: 2px solid var(--RedError); - border-right: 2px solid var(--RedError); - } -} - -.blockComments { - display: flex; - flex-direction: column; - gap: 30px; -} - -.fieldSelectResponse { - align-items: center; - width: 100%; - display: flex; - font-family: "Nunito Sans", sans-serif; - font-size: 14px; - @include ltemd { - display: block; - } - label { - color: var(--FontColor); - font-family: "Nunito Sans", sans-serif; - font-weight: 700; - font-size: 16px; - margin-right: 16px; - width: 120px; - } - :global(.select__control) { - width: 203px; - @include ltemd { - width: 100%; - } - :global(.select__single-value) { - font-size: 14px; - } - } -} - -.blockMarkdownEditor { - margin-top: $sp-4; - margin-left: 136px; - @include ltemd { - margin-left: 0; - } -} - -.blockRowResponseComment { - td { - padding: $sp-6 $sp-6 $sp-6 54px; - background-color: var(--TableColumn); - - @include ltemd { - padding: $sp-6 $sp-6 $sp-6 10px; - } - } - - &.isError { - border-bottom: 2px solid var(--RedError); - border-left: 2px solid var(--RedError); - border-right: 2px solid var(--RedError); - } -} diff --git a/src/apps/review/src/lib/components/ScorecardQuestionEdit/ScorecardQuestionEdit.tsx b/src/apps/review/src/lib/components/ScorecardQuestionEdit/ScorecardQuestionEdit.tsx deleted file mode 100644 index 66a530116..000000000 --- a/src/apps/review/src/lib/components/ScorecardQuestionEdit/ScorecardQuestionEdit.tsx +++ /dev/null @@ -1,438 +0,0 @@ -/** - * Scorecard Question Edit. - */ -import { Dispatch, FC, SetStateAction, useMemo } from 'react' -import { - Control, - Controller, - ControllerRenderProps, - FieldErrors, - useFieldArray, - UseFieldArrayReturn, - UseFormTrigger, -} from 'react-hook-form' -import _, { capitalize, compact, isEmpty } from 'lodash' -import Select, { SingleValue } from 'react-select' -import classNames from 'classnames' - -import { FieldMarkdownEditor } from '../FieldMarkdownEditor' -import { IconChevronDown } from '../../assets/icons' -import { - QUESTION_RESPONSE_OPTIONS, - QUESTION_YES_NO_OPTIONS, -} from '../../../config/index.config' -import { MarkdownReview } from '../MarkdownReview' -import { - FormReviews, - ReviewItemInfo, - ScorecardQuestion, - SelectOption, -} from '../../models' - -import styles from './ScorecardQuestionEdit.module.scss' - -interface Props { - className?: string - scorecardQuestion: ScorecardQuestion - reviewItem: ReviewItemInfo - groupIndex: number - sectionIndex: number - questionIndex: number - control: Control - fieldIndex: number - errors: FieldErrors - isTouched: { [key: string]: boolean } - disabled?: boolean - setIsTouched: Dispatch< - SetStateAction<{ - [key: string]: boolean - }> - > - trigger: UseFormTrigger - recalculateReviewProgress: () => void - isExpand: { [key: string]: boolean } - setIsExpand: Dispatch< - SetStateAction<{ - [key: string]: boolean - }> - > -} - -export const ScorecardQuestionEdit: FC = (props: Props) => { - const isExpand = props.isExpand[props.reviewItem.id] - const responseOptions = useMemo(() => { - if (props.scorecardQuestion.type === 'SCALE') { - const length - = props.scorecardQuestion.scaleMax - - props.scorecardQuestion.scaleMin - + 1 - return Array.from( - new Array(length), - (x, i) => `${i + props.scorecardQuestion.scaleMin}`, - ) - .map(item => ({ - label: item, - value: item, - })) - } - - if (props.scorecardQuestion.type === 'YES_NO') { - return QUESTION_YES_NO_OPTIONS - } - - return [] - }, [props.scorecardQuestion]) - - const errorMessage = useMemo( - () => { - if (props.isTouched[ - `reviews.${props.fieldIndex}.initialAnswer.message` - ]) { - return _.get( - props.errors, - `reviews.${props.fieldIndex}.initialAnswer.message`, - ) - } - - return '' - }, - [props], - ) - - const initCommentContents = useMemo<{ [key: string]: string }>(() => { - const results: { [key: string]: string } = {} - _.forEach( - props.reviewItem.reviewItemComments, - (commentItem, idx) => { - results[`${idx}.content`] - = commentItem.content ?? '' - }, - ) - return results - }, [props]) - - const { - fields, - append, - }: UseFieldArrayReturn - = useFieldArray({ - control: props.control, - name: `reviews.${props.fieldIndex}.comments` as 'reviews.0.comments', - }) - - const errorCommentsMessage = useMemo<{ [index: number]: string }>(() => { - const result: { [index: number]: string } = {} - _.forEach(fields, (field, idx) => { - result[idx] = props.isTouched[ - `reviews.${props.fieldIndex}.comments.${idx}.content` - ] - ? _.get( - props.errors, - `reviews.${props.fieldIndex}.comments.${idx}.content.message`, - ) ?? '' - : '' - }) - return result - }, [props, fields]) - - return ( - <> - - -
- - - - {/* eslint-disable-next-line max-len */} - {`Question ${props.groupIndex + 1}.${props.sectionIndex + 1}.${props.questionIndex + 1}`} - - {' '} - {props.scorecardQuestion.description} - -
- - - Weight: - {props.scorecardQuestion.weight.toFixed(1)} - - - - }) { - return ( - , - ) { - controlProps.field.onChange( - ( - option as SelectOption - ).value, - ) - props.trigger( - `reviews.${props.fieldIndex}.comments.${idx}.content`, - ) - }} - onBlur={function onBlur() { - controlProps.field.onBlur() - }} - isDisabled={props.disabled} - /> -
- ) - }} - /> - - - }) { - return ( - ({ - ...old, - [`reviews.${props.fieldIndex}.comments.${idx}.content`]: - true, - }), - ) - }} - error={ - errorCommentsMessage[ - idx - ] - } - disabled={props.disabled} - uploadCategory='review-comment' - /> - ) - }} - /> -
- ))} - - - - - - - - ) -} - -export default ScorecardQuestionEdit diff --git a/src/apps/review/src/lib/components/ScorecardQuestionEdit/index.ts b/src/apps/review/src/lib/components/ScorecardQuestionEdit/index.ts deleted file mode 100644 index 4d7c9bec7..000000000 --- a/src/apps/review/src/lib/components/ScorecardQuestionEdit/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as ScorecardQuestionEdit } from './ScorecardQuestionEdit' diff --git a/src/apps/review/src/lib/components/ScorecardQuestionView/ScorecardQuestionView.module.scss b/src/apps/review/src/lib/components/ScorecardQuestionView/ScorecardQuestionView.module.scss deleted file mode 100644 index ea8304f05..000000000 --- a/src/apps/review/src/lib/components/ScorecardQuestionView/ScorecardQuestionView.module.scss +++ /dev/null @@ -1,262 +0,0 @@ -@import '@libs/ui/styles/includes'; - -.container { - @include ltemd { - display: flex; - flex-wrap: wrap; - } - td { - vertical-align: middle; - @include ltemd { - &:nth-child(1) { - width: 100%; - } - } - } -} - -.blockRowQuestionHeader { - @include ltemd { - padding-bottom: $sp-3; - } - td { - padding: 23px $sp-4; - - &:first-child { - padding-left: 18px; - } - - @include ltemd { - padding: $sp-3 $sp-2 0; - - &:first-child { - padding-left: $sp-2; - } - } - } -} - -.textResponse, -.blockCellWeight, -.blockCellResponse, -.textType, -.blockCellQuestion { - color: var(--FontColor); - font-family: "Nunito Sans", sans-serif; - font-weight: 700; - line-height: 20px; -} - -.btnExpand { - margin-right: $sp-4; - height: 24px; - width: 24px; - &.expand { - transform: rotate(180deg); - } -} - -.textQuestion { - align-items: center; - display: flex; - strong { - width: 138px; - } - @include ltemd { - display: inline; - strong { - padding-right: $sp-2; - } - } -} - -.blockCellQuestion { - display: flex; - align-items: center; - gap: 10; -} - -.textType { - font-size: 14px; -} - -.textResponse { - flex-shrink: 0; - width: 118px; - white-space: nowrap; -} - -.blockCellWeight { - color: var(--FontColor); - font-family: 'Nunito Sans', sans-serif; - font-size: 14px; - font-weight: 700; - width: 100px; - i { - display: none; - } - @include ltemd { - align-items: center; - display: flex; - width: 120px; - margin-right: auto; - i { - display: block; - margin-right: $sp-2; - } - } -} - -.blockCellResponse { - font-size: 14px; - width: 160px; - - @include ltemd { - width: 80px; - } -} - -.blockRowResponseComment { - td { - padding: $sp-6 0 30px $sp-14; - @include ltemd { - padding: 0; - } - } - - &.isLast { - border-bottom: 1px solid var(--TableBorderColor); - &:has(+ :global(.question)), - &:last-child { - border-bottom: 0; - } - } - &:has(+ .blockRowAppealComment), - &:has(+ .blockRowManagerComment) { - &.isLast { - border-width: 0; - } - td { - padding-bottom: $sp-4; - } - } -} - -.blockRowAppealComment { - td { - padding: 0 0 40px 194px; - @include ltemd { - padding: 0; - } - &.isEmpty { - padding-bottom: 0; - } - } - - &:has(+ .blockRowManagerComment) { - td { - padding-bottom: $sp-4; - } - } - - &.isLast { - border-bottom: 1px solid var(--TableBorderColor); - &:has(+ :global(.question)), - &:last-child { - border-bottom: 0; - } - } -} - -.blockRowManagerComment { - td { - padding: 0 0 40px 194px; - - @include ltemd { - padding: 0; - } - } - - &.isLast { - border-bottom: 1px solid var(--TableBorderColor); - &:has(+ :global(.question)), - &:last-child { - border-bottom: 0; - } - } -} - -.managerSelect { - max-width: 144px; - width: 144px; -} - -.managerCommentDisplay { - background-color: #ECF4FA; - border-radius: 4px; - display: flex; - flex-direction: column; - gap: $sp-2; - padding: $sp-4; -} - -.managerCommentTitle { - color: var(--FontColor); - font-family: "Nunito Sans", sans-serif; - font-size: 14px; - font-weight: 700; - line-height: 20px; -} - -.managerCommentMarkdown { - width: 100%; -} - -.managerCommentEditor { - background-color: var(--TableColumn); - border-radius: 4px; - display: flex; - flex-direction: column; - gap: $sp-4; - padding: $sp-4; -} - -.managerCommentEditorInput { - width: 100%; -} - -.managerCommentActions { - display: flex; - flex-wrap: wrap; - gap: $sp-3; -} - -.blockResponseComment { - display: flex; - gap: 16px; - @include ltemd { - display: block; - padding: $sp-5 0; - } -} - -.blockCommentContent { - display: flex; - flex-direction: column; - gap: 16px; -} - -.blockCommentList { - display: flex; - flex-direction: column; - gap: 28px; -} - -.blockRowGuidelines { - td { - padding: 8px 192px 22px; - @include ltemd { - padding: 16px 8px; - } - } -} diff --git a/src/apps/review/src/lib/components/ScorecardQuestionView/ScorecardQuestionView.tsx b/src/apps/review/src/lib/components/ScorecardQuestionView/ScorecardQuestionView.tsx deleted file mode 100644 index ee28b7baf..000000000 --- a/src/apps/review/src/lib/components/ScorecardQuestionView/ScorecardQuestionView.tsx +++ /dev/null @@ -1,837 +0,0 @@ -/** - * Scorecard Question View. - */ -import { - Dispatch, - FC, - Fragment, - ReactNode, - SetStateAction, - useCallback, - useContext, - useEffect, - useMemo, - useState, -} from 'react' -import { includes } from 'lodash' -import Select, { SingleValue } from 'react-select' -import classNames from 'classnames' - -import { useWindowSize, WindowSize } from '~/libs/shared' - -import { IconChevronDown } from '../../assets/icons' -import { ReviewAppContext } from '../../contexts' -import { useRole, useRoleProps } from '../../hooks' -import { - AppealInfo, - MappingAppeal, - ReviewAppContextModel, - ReviewInfo, - ReviewItemInfo, - ScorecardQuestion, - SelectOption, -} from '../../models' -import { ReviewItemComment } from '../../models/ReviewItemComment.model' -import { stringIsNumberic } from '../../utils' -import { Appeal } from '../Appeal' -import { AppealComment } from '../AppealComment' -import { FieldMarkdownEditor } from '../FieldMarkdownEditor' -import { ManagerComment } from '../ManagerComment' -import { MarkdownReview } from '../MarkdownReview' -import { - ADMIN, - COPILOT, - FINISHTAB, - QUESTION_YES_NO_OPTIONS, - REVIEWER, - SUBMITTER, - TAB, -} from '../../../config/index.config' - -import styles from './ScorecardQuestionView.module.scss' - -interface Props { - className?: string - scorecardQuestion: ScorecardQuestion - reviewItem: ReviewItemInfo - reviewInfo?: ReviewInfo - groupIndex: number - sectionIndex: number - questionIndex: number - mappingAppeals: MappingAppeal - isExpand: { [key: string]: boolean } - setIsExpand: Dispatch< - SetStateAction<{ - [key: string]: boolean - }> - > - isSavingAppeal: boolean - isSavingAppealResponse: boolean - isSavingManagerComment: boolean - addAppeal: ( - content: string, - commentItem: ReviewItemComment, - success: () => void, - ) => void - doDeleteAppeal: ( - appealInfo: AppealInfo | undefined, - success: () => void, - ) => void - addAppealResponse: ( - content: string, - updatedResponse: string, - appeal: AppealInfo, - reviewItem: ReviewItemInfo, - success: () => void, - ) => void - addManagerComment: ( - content: string, - updatedResponse: string, - reviewItem: ReviewItemInfo, - success: () => void, - ) => void - isManagerEdit?: boolean -} - -function getResponseOptions(scorecardQuestion: ScorecardQuestion): SelectOption[] { - if (scorecardQuestion.type === 'SCALE') { - const length - = scorecardQuestion.scaleMax - - scorecardQuestion.scaleMin - + 1 - - return Array.from({ length }, (_, index) => { - const value = `${index + scorecardQuestion.scaleMin}` - - return { - label: value, - value, - } - }) - } - - if (scorecardQuestion.type === 'YES_NO') { - return QUESTION_YES_NO_OPTIONS - } - - return [] -} - -function getFormattedAnswer(reviewItem: ReviewItemInfo): string | undefined { - // Prefer finalAnswer when it is a non-empty string; otherwise fall back to initialAnswer - // Using logical OR ensures empty strings don't mask a valid initialAnswer - const answer = reviewItem.finalAnswer || reviewItem.initialAnswer || undefined - - if (stringIsNumberic(answer)) { - return `Rating ${answer}` - } - - return answer -} - -interface CommentRowParams { - actionChallengeRole?: string - addAppeal: Props['addAppeal'] - addAppealResponse: Props['addAppealResponse'] - canRespondToAppeal: boolean - className?: string - doDeleteAppeal: Props['doDeleteAppeal'] - isManagerRole: boolean - isMobile: boolean - isSavingAppeal: boolean - isSavingAppealResponse: boolean - isSubmitterRole: boolean - managerCommentContent?: ReactNode - mappingAppeals: MappingAppeal - reviewItem: ReviewItemInfo - scorecardQuestion: ScorecardQuestion - shouldRenderManagerCommentRow: boolean -} - -function buildCommentRows({ - actionChallengeRole, - addAppeal, - addAppealResponse, - canRespondToAppeal, - className, - doDeleteAppeal, - isManagerRole, - isMobile, - isSavingAppeal, - isSavingAppealResponse, - isSubmitterRole, - mappingAppeals, - managerCommentContent, - reviewItem, - scorecardQuestion, - shouldRenderManagerCommentRow, -}: CommentRowParams): JSX.Element[] { - const totalComments = reviewItem.reviewItemComments.length - - return reviewItem.reviewItemComments.map((commentItem, index) => { - const commentAppeal - = mappingAppeals[commentItem.id] - ?? commentItem.appeal - const isLastComment = index === totalComments - 1 - const shouldShowManagerRowHere - = isLastComment && shouldRenderManagerCommentRow - const shouldMarkManagerRowAsLast - = shouldShowManagerRowHere && !isSubmitterRole - - return ( - - {renderResponseCommentRow({ - className, - commentItem, - index, - isMobile, - totalComments, - })} - {renderAppealCommentRow({ - actionChallengeRole, - addAppealResponse, - canRespondToAppeal, - className, - commentAppeal, - index, - isManagerRole, - isSavingAppealResponse, - reviewItem, - scorecardQuestion, - totalComments, - })} - {renderManagerCommentRow({ - className, - managerCommentContent, - shouldMarkManagerRowAsLast, - shouldShowManagerRowHere, - })} - {renderSubmitterAppealRow({ - actionChallengeRole, - addAppeal, - className, - commentAppeal, - commentItem, - doDeleteAppeal, - index, - isSavingAppeal, - totalComments, - })} - - ) - }) -} - -interface ManagerCommentContentParams { - existingManagerComment: string - handleCancelManagerEdit: () => void - handleSubmitManagerComment: () => void - hasManagerComment: boolean - isSavingManagerComment: boolean - isSubmitDisabled: boolean - managerCommentDraft: string - reviewItemId: ReviewItemInfo['id'] - selectedScore: string - setManagerCommentDraft: Dispatch> - showManagerCommentEditor: boolean -} - -function buildManagerCommentContent({ - existingManagerComment, - handleCancelManagerEdit, - handleSubmitManagerComment, - hasManagerComment, - isSavingManagerComment, - isSubmitDisabled, - managerCommentDraft, - reviewItemId, - selectedScore, - setManagerCommentDraft, - showManagerCommentEditor, -}: ManagerCommentContentParams): ReactNode | undefined { - if (showManagerCommentEditor) { - return ( -
- - -
- - -
-
- ) - } - - if (hasManagerComment) { - return ( -
- Manager Comment - -
- ) - } - - return undefined -} - -interface GuidelinesRowParams { - className?: string - isExpanded: boolean - scorecardQuestion: ScorecardQuestion -} - -function buildGuidelinesRow({ - className, - isExpanded, - scorecardQuestion, -}: GuidelinesRowParams): JSX.Element | undefined { - if (!isExpanded) { - return undefined - } - - return ( - - - - - - ) -} - -interface ManagerCommentRowParams { - addManagerComment: Props['addManagerComment'] - className?: string - isManagerRole: boolean - isSavingManagerComment: boolean - reviewInfo?: ReviewInfo - reviewItem: ReviewItemInfo - scorecardQuestion: ScorecardQuestion -} - -function buildManagerCommentRow({ - addManagerComment, - className, - isManagerRole, - isSavingManagerComment, - reviewInfo, - reviewItem, - scorecardQuestion, -}: ManagerCommentRowParams): JSX.Element | undefined { - if (!isManagerRole || reviewInfo?.id) { - return undefined - } - - return ( - - - - - - ) -} - -function renderResponseCommentRow({ - className, - commentItem, - index, - isMobile, - totalComments, -}: { - className?: string - commentItem: ReviewItemComment - index: number - isMobile: boolean - totalComments: number -}): JSX.Element { - return ( - - -
- - {`Response ${index + 1}:`} - -
- - {commentItem.typeDisplay} - - -
')} - className={styles.mardownReview} - /> -
-
- - - ) -} - -function renderAppealCommentRow({ - actionChallengeRole, - addAppealResponse, - canRespondToAppeal, - className, - commentAppeal, - index, - isManagerRole, - isSavingAppealResponse, - reviewItem, - scorecardQuestion, - totalComments, -}: { - actionChallengeRole?: string - addAppealResponse: Props['addAppealResponse'] - canRespondToAppeal: boolean - className?: string - commentAppeal?: AppealInfo - index: number - isManagerRole: boolean - isSavingAppealResponse: boolean - reviewItem: ReviewItemInfo - scorecardQuestion: ScorecardQuestion - totalComments: number -}): JSX.Element | undefined { - if ( - !commentAppeal - || !includes([REVIEWER, COPILOT, ADMIN], actionChallengeRole) - ) { - return undefined - } - - return ( - - - - - - ) -} - -function renderManagerCommentRow({ - className, - managerCommentContent, - shouldMarkManagerRowAsLast, - shouldShowManagerRowHere, -}: { - className?: string - managerCommentContent?: ReactNode - shouldMarkManagerRowAsLast: boolean - shouldShowManagerRowHere: boolean -}): JSX.Element | undefined { - if (!shouldShowManagerRowHere || !managerCommentContent) { - return undefined - } - - return ( - - {managerCommentContent} - - ) -} - -function renderSubmitterAppealRow({ - actionChallengeRole, - addAppeal, - className, - commentAppeal, - commentItem, - doDeleteAppeal, - index, - isSavingAppeal, - totalComments, -}: { - actionChallengeRole?: string - addAppeal: Props['addAppeal'] - className?: string - commentAppeal?: AppealInfo - commentItem: ReviewItemComment - doDeleteAppeal: Props['doDeleteAppeal'] - index: number - isSavingAppeal: boolean - totalComments: number -}): JSX.Element | undefined { - if (!includes([SUBMITTER], actionChallengeRole)) { - return undefined - } - - return ( - - - - - - ) -} - -export const ScorecardQuestionView: FC = (props: Props) => { - const addAppeal = props.addAppeal - const addAppealResponse = props.addAppealResponse - const addManagerComment = props.addManagerComment - const className = props.className - const doDeleteAppeal = props.doDeleteAppeal - const expandState = props.isExpand - const groupIndex = props.groupIndex - const isManagerEdit = props.isManagerEdit - const isSavingAppeal = props.isSavingAppeal - const isSavingAppealResponse = props.isSavingAppealResponse - const isSavingManagerComment = props.isSavingManagerComment - const mappingAppeals = props.mappingAppeals - const questionIndex = props.questionIndex - const reviewInfo = props.reviewInfo - const reviewItem = props.reviewItem - const scorecardQuestion = props.scorecardQuestion - const sectionIndex = props.sectionIndex - const setIsExpand = props.setIsExpand - const { - actionChallengeRole, - myChallengeResources, - }: useRoleProps = useRole() - const { loginUserInfo }: ReviewAppContextModel = useContext(ReviewAppContext) - const { width: screenWidth }: WindowSize = useWindowSize() - const isMobile = useMemo(() => screenWidth <= 745, [screenWidth]) - - const isExpanded = expandState[reviewItem.id] - const reviewerHandle = reviewInfo?.reviewerHandle - const isManagerEditMode = isManagerEdit ?? false - const currentScoreValue = useMemo( - // Show a selected score if finalAnswer is set; otherwise fall back to initialAnswer - () => reviewItem.finalAnswer || reviewItem.initialAnswer || '', - [reviewItem.finalAnswer, reviewItem.initialAnswer], - ) - const existingManagerComment = reviewItem.managerComment ?? '' - - const [selectedScore, setSelectedScore] = useState(currentScoreValue) - const [managerCommentDraft, setManagerCommentDraft] = useState('') - const [showManagerCommentForm, setShowManagerCommentForm] = useState(false) - - useEffect(() => { - setSelectedScore(currentScoreValue) - setShowManagerCommentForm(false) - setManagerCommentDraft('') - }, [currentScoreValue, isManagerEditMode]) - - const responseOptions = useMemo( - () => getResponseOptions(scorecardQuestion), - [scorecardQuestion], - ) - - const selectValue = useMemo( - () => (selectedScore - ? { - label: selectedScore, - value: selectedScore, - } - : undefined), - [selectedScore], - ) - - const handleScoreChange = useCallback((option: SingleValue) => { - const nextValue = (option as SelectOption | null)?.value ?? '' - setSelectedScore(nextValue) - - if (nextValue && nextValue !== currentScoreValue) { - setShowManagerCommentForm(true) - setManagerCommentDraft(prev => (prev || existingManagerComment)) - } else { - setShowManagerCommentForm(false) - setManagerCommentDraft('') - } - }, [currentScoreValue, existingManagerComment]) - - const handleCancelManagerEdit = useCallback(() => { - setSelectedScore(currentScoreValue) - setShowManagerCommentForm(false) - setManagerCommentDraft('') - }, [currentScoreValue]) - - const handleSubmitManagerComment = useCallback(() => { - if ( - !selectedScore - || selectedScore === currentScoreValue - || !managerCommentDraft.trim() - ) { - return - } - - addManagerComment( - managerCommentDraft, - selectedScore, - reviewItem, - () => { - setShowManagerCommentForm(false) - setManagerCommentDraft('') - }, - ) - }, [ - addManagerComment, - currentScoreValue, - managerCommentDraft, - reviewItem, - selectedScore, - ]) - - const reviewerResourceId = reviewInfo?.resourceId - - // Owning the specific reviewer resource proves the current member is assigned this review - const hasReviewerResource = useMemo( - () => Boolean( - reviewerResourceId - && myChallengeResources.some( - resource => resource.id === reviewerResourceId, - ), - ), - [myChallengeResources, reviewerResourceId], - ) - - const canRespondToAppeal = useMemo(() => { - if (!hasReviewerResource) { - return false - } - - const currentHandle = loginUserInfo?.handle - if (!currentHandle || !reviewerHandle) { - return true - } - - return currentHandle.toLowerCase() - === reviewerHandle.toLowerCase() - }, [hasReviewerResource, loginUserInfo?.handle, reviewerHandle]) - - const finalAnswer = useMemo( - () => getFormattedAnswer(reviewItem), - [reviewItem], - ) - - const showManagerCommentEditor = isManagerEditMode && showManagerCommentForm - const hasManagerComment = Boolean(existingManagerComment) - const shouldRenderManagerCommentRow - = showManagerCommentEditor || hasManagerComment - const canEditScore = isManagerEditMode && responseOptions.length > 0 - const isSubmitDisabled = isSavingManagerComment - || !selectedScore - || selectedScore === currentScoreValue - || !managerCommentDraft.trim() - const isSubmitterRole = includes([SUBMITTER], actionChallengeRole) - const isManagerRole = includes([COPILOT, ADMIN], actionChallengeRole) - - const managerCommentContent = buildManagerCommentContent({ - existingManagerComment, - handleCancelManagerEdit, - handleSubmitManagerComment, - hasManagerComment, - isSavingManagerComment, - isSubmitDisabled, - managerCommentDraft, - reviewItemId: reviewItem.id, - selectedScore, - setManagerCommentDraft, - showManagerCommentEditor, - }) - - const commentRows = buildCommentRows({ - actionChallengeRole, - addAppeal, - addAppealResponse, - canRespondToAppeal, - className, - doDeleteAppeal, - isManagerRole, - isMobile, - isSavingAppeal, - isSavingAppealResponse, - isSubmitterRole, - managerCommentContent, - mappingAppeals, - reviewItem, - scorecardQuestion, - shouldRenderManagerCommentRow, - }) - - const questionHeaderRow = ( - - - - - - {`Question ${groupIndex + 1}.${sectionIndex + 1}.${questionIndex + 1}`} - - {' '} - {scorecardQuestion.description} - - - - Weight: - {scorecardQuestion.weight.toFixed(1)} - - - {canEditScore ? ( -