From a9b4b2bf2e332978d9ffa47624e02dc8b42e1850 Mon Sep 17 00:00:00 2001 From: dat Date: Sun, 25 May 2025 20:51:43 +0700 Subject: [PATCH 1/2] Topcoder Admin App - Marathon Match Functionality --- package.json | 2 +- src/apps/admin/src/admin-app.routes.tsx | 9 + .../ManageSubmissionPage.module.scss | 8 + .../ManageSubmissionPage.tsx | 98 +++++++ .../ManageSubmissionPage/index.ts | 1 + src/apps/admin/src/config/busEvent.config.ts | 29 ++ .../ChallengeList/ChallengeList.tsx | 12 + .../SubmissionTable.module.scss | 22 ++ .../SubmissionTable/SubmissionTable.tsx | 258 ++++++++++++++++++ .../SubmissionTableActions.tsx | 111 ++++++++ .../lib/components/SubmissionTable/index.ts | 1 + src/apps/admin/src/lib/components/index.ts | 1 + src/apps/admin/src/lib/hooks/index.ts | 2 + .../admin/src/lib/hooks/useManageBusEvent.ts | 66 +++++ .../hooks/useManageChallengeSubmissions.ts | 198 ++++++++++++++ .../src/lib/models/MemberSubmission.model.ts | 11 + .../src/lib/models/RequestBusAPI.model.ts | 15 + .../admin/src/lib/models/Submission.model.ts | 183 +++++++++++++ .../src/lib/models/SubmissionReview.model.ts | 42 +++ .../models/SubmissionReviewSummation.model.ts | 7 + .../models/challenge-management/Challenge.ts | 1 + src/apps/admin/src/lib/models/index.ts | 3 + .../src/lib/services/bus-event.service.ts | 20 ++ src/apps/admin/src/lib/services/index.js | 2 + .../admin/src/lib/services/reviews.service.ts | 41 +++ .../src/lib/services/submissions.service.ts | 44 +++ src/apps/admin/src/lib/utils/challenge.ts | 84 ++++++ src/apps/admin/src/lib/utils/index.ts | 3 + src/apps/admin/src/lib/utils/number.ts | 56 ++++ src/apps/admin/src/lib/utils/string.ts | 16 ++ src/config/environments/default.env.ts | 1 + .../environments/global-config.model.ts | 1 + src/config/environments/prod.env.ts | 1 + src/libs/ui/lib/components/table/Table.tsx | 3 +- yarn.lock | 25 +- 35 files changed, 1357 insertions(+), 20 deletions(-) create mode 100644 src/apps/admin/src/challenge-management/ManageSubmissionPage/ManageSubmissionPage.module.scss create mode 100644 src/apps/admin/src/challenge-management/ManageSubmissionPage/ManageSubmissionPage.tsx create mode 100644 src/apps/admin/src/challenge-management/ManageSubmissionPage/index.ts create mode 100644 src/apps/admin/src/config/busEvent.config.ts create mode 100644 src/apps/admin/src/lib/components/SubmissionTable/SubmissionTable.module.scss create mode 100644 src/apps/admin/src/lib/components/SubmissionTable/SubmissionTable.tsx create mode 100644 src/apps/admin/src/lib/components/SubmissionTable/SubmissionTableActions.tsx create mode 100644 src/apps/admin/src/lib/components/SubmissionTable/index.ts create mode 100644 src/apps/admin/src/lib/hooks/useManageBusEvent.ts create mode 100644 src/apps/admin/src/lib/hooks/useManageChallengeSubmissions.ts create mode 100644 src/apps/admin/src/lib/models/MemberSubmission.model.ts create mode 100644 src/apps/admin/src/lib/models/RequestBusAPI.model.ts create mode 100644 src/apps/admin/src/lib/models/Submission.model.ts create mode 100644 src/apps/admin/src/lib/models/SubmissionReview.model.ts create mode 100644 src/apps/admin/src/lib/models/SubmissionReviewSummation.model.ts create mode 100644 src/apps/admin/src/lib/services/bus-event.service.ts create mode 100644 src/apps/admin/src/lib/services/reviews.service.ts create mode 100644 src/apps/admin/src/lib/services/submissions.service.ts create mode 100644 src/apps/admin/src/lib/utils/challenge.ts create mode 100644 src/apps/admin/src/lib/utils/number.ts create mode 100644 src/apps/admin/src/lib/utils/string.ts diff --git a/package.json b/package.json index 93a0323e3..4836bfad5 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,7 @@ "tc-auth-lib": "topcoder-platform/tc-auth-lib#1.0.27", "typescript": "^4.8.4", "universal-navigation": "https://github.com/topcoder-platform/universal-navigation#9fc50d938be7182", - "uuid": "^9.0.0", + "uuid": "^11.1.0", "yup": "^1.6.1" }, "devDependencies": { diff --git a/src/apps/admin/src/admin-app.routes.tsx b/src/apps/admin/src/admin-app.routes.tsx index b5251bf65..f58fd2fb3 100644 --- a/src/apps/admin/src/admin-app.routes.tsx +++ b/src/apps/admin/src/admin-app.routes.tsx @@ -33,6 +33,10 @@ const ManageResourcePage: LazyLoadedComponent = lazyLoad( () => import('./challenge-management/ManageResourcePage'), 'ManageResourcePage', ) +const ManageSubmissionPage: LazyLoadedComponent = lazyLoad( + () => import('./challenge-management/ManageSubmissionPage'), + 'ManageSubmissionPage', +) const AddResourcePage: LazyLoadedComponent = lazyLoad( () => import('./challenge-management/AddResourcePage'), 'AddResourcePage', @@ -145,6 +149,11 @@ export const adminRoutes: ReadonlyArray = [ id: 'add-resource', route: ':challengeId/manage-resource/add', }, + { + element: , + id: 'manage-resource', + route: ':challengeId/manage-submission', + }, ], element: , id: manageChallengeRouteId, diff --git a/src/apps/admin/src/challenge-management/ManageSubmissionPage/ManageSubmissionPage.module.scss b/src/apps/admin/src/challenge-management/ManageSubmissionPage/ManageSubmissionPage.module.scss new file mode 100644 index 000000000..06ebfb150 --- /dev/null +++ b/src/apps/admin/src/challenge-management/ManageSubmissionPage/ManageSubmissionPage.module.scss @@ -0,0 +1,8 @@ +.container { + display: flex; + flex-direction: column; +} + +.blockTableContainer { + position: relative; +} diff --git a/src/apps/admin/src/challenge-management/ManageSubmissionPage/ManageSubmissionPage.tsx b/src/apps/admin/src/challenge-management/ManageSubmissionPage/ManageSubmissionPage.tsx new file mode 100644 index 000000000..04584a537 --- /dev/null +++ b/src/apps/admin/src/challenge-management/ManageSubmissionPage/ManageSubmissionPage.tsx @@ -0,0 +1,98 @@ +/** + * Manage Submission Page. + */ +import { FC } from 'react' +import { useParams } from 'react-router-dom' +import classNames from 'classnames' + +import { LinkButton } from '~/libs/ui' + +import { + useManageBusEvent, + useManageBusEventProps, + useManageChallengeSubmissions, + useManageChallengeSubmissionsProps, +} from '../../lib/hooks' +import { + ActionLoading, + PageWrapper, + SubmissionTable, + TableLoading, + TableNoRecord, +} from '../../lib' + +import styles from './ManageSubmissionPage.module.scss' + +interface Props { + className?: string +} + +export const ManageSubmissionPage: FC = (props: Props) => { + const { challengeId = '' }: { challengeId?: string } = useParams<{ + challengeId: string + }>() + const { isRunningTest, isRunningTestBool, doPostBusEvent }: useManageBusEventProps + = useManageBusEvent() + + const { + isLoading, + submissions, + isRemovingSubmission, + isRemovingSubmissionBool, + isRemovingReviewSummations, + isRemovingReviewSummationsBool, + doRemoveSubmission, + doRemoveReviewSummations, + showSubmissionHistory, + setShowSubmissionHistory, + }: useManageChallengeSubmissionsProps + = useManageChallengeSubmissions(challengeId) + + return ( + + Back + + )} + > + {isLoading ? ( + + ) : ( + <> + {submissions.length === 0 ? ( + + ) : ( +
+ + + {(isRemovingSubmissionBool + || isRunningTestBool + || isRemovingReviewSummationsBool) && ( + + )} +
+ )} + + )} +
+ ) +} + +export default ManageSubmissionPage diff --git a/src/apps/admin/src/challenge-management/ManageSubmissionPage/index.ts b/src/apps/admin/src/challenge-management/ManageSubmissionPage/index.ts new file mode 100644 index 000000000..90550558b --- /dev/null +++ b/src/apps/admin/src/challenge-management/ManageSubmissionPage/index.ts @@ -0,0 +1 @@ +export { default as ManageSubmissionPage } from './ManageSubmissionPage' diff --git a/src/apps/admin/src/config/busEvent.config.ts b/src/apps/admin/src/config/busEvent.config.ts new file mode 100644 index 000000000..464b198a0 --- /dev/null +++ b/src/apps/admin/src/config/busEvent.config.ts @@ -0,0 +1,29 @@ +/** + * App config for bus event + */ +import { v4 as uuidv4 } from 'uuid' + +import { RequestBusAPI } from '../lib/models' + +/** + * Create data for bus event + * @param submissionId submission id + * @param testType test type + * @returns data for bus event + */ +export const CREATE_BUS_EVENT_DATA_SUBMISSION_MARATHON_MATCH = ( + submissionId: string, + testType: string, +): RequestBusAPI => ({ + 'mime-type': 'application/json', + originator: 'MMFinalScoreProcessor', + payload: { + id: uuidv4(), + resource: 'score', + submissionId, + testType, + }, + timestamp: new Date() + .toISOString(), + topic: 'submission.notification.score', +}) diff --git a/src/apps/admin/src/lib/components/ChallengeList/ChallengeList.tsx b/src/apps/admin/src/lib/components/ChallengeList/ChallengeList.tsx index 1934a915c..096437b3c 100644 --- a/src/apps/admin/src/lib/components/ChallengeList/ChallengeList.tsx +++ b/src/apps/admin/src/lib/components/ChallengeList/ChallengeList.tsx @@ -21,6 +21,7 @@ import { import { useEventCallback } from '../../hooks' import { Challenge, ChallengeFilterCriteria, ChallengeType } from '../../models' import { Paging } from '../../models/challenge-management/Pagination' +import { checkIsMM } from '../../utils' import { MobileListView } from './MobileListView' import styles from './ChallengeList.module.scss' @@ -134,6 +135,7 @@ const Actions: FC<{ challenge: Challenge currentFilters: ChallengeFilterCriteria }> = props => { + const isMM = useMemo(() => checkIsMM(props.challenge), [props.challenge]) const [openDropdown, setOpenDropdown] = useState(false) const navigate = useNavigate() const goToManageUser = useEventCallback(() => { @@ -208,6 +210,16 @@ const Actions: FC<{ > Resources + {isMM && ( +
  • + Submissions +
  • + )} diff --git a/src/apps/admin/src/lib/components/SubmissionTable/SubmissionTable.module.scss b/src/apps/admin/src/lib/components/SubmissionTable/SubmissionTable.module.scss new file mode 100644 index 000000000..e6e7556f2 --- /dev/null +++ b/src/apps/admin/src/lib/components/SubmissionTable/SubmissionTable.module.scss @@ -0,0 +1,22 @@ +@import '@libs/ui/styles/includes'; + +.container { + display: flex; + flex-direction: column; + padding-bottom: $sp-8; + + @include ltelg { + padding-bottom: $sp-4; + } +} + +.rowActions { + display: flex; + align-items: center; +} + +.desktopTable { + td { + vertical-align: middle; + } +} diff --git a/src/apps/admin/src/lib/components/SubmissionTable/SubmissionTable.tsx b/src/apps/admin/src/lib/components/SubmissionTable/SubmissionTable.tsx new file mode 100644 index 000000000..21fa47798 --- /dev/null +++ b/src/apps/admin/src/lib/components/SubmissionTable/SubmissionTable.tsx @@ -0,0 +1,258 @@ +/** + * Submission Table. + */ +import { Dispatch, FC, SetStateAction, useMemo, useState } from 'react' +import _ from 'lodash' +import classNames from 'classnames' + +import { useWindowSize, WindowSize } from '~/libs/shared' +import { Button, ConfirmModal, Table, TableColumn } from '~/libs/ui' + +import { IsRemovingType, MobileTableColumn, Submission } from '../../models' +import { TableMobile } from '../common/TableMobile' +import { TableWrapper } from '../common/TableWrapper' + +import SubmissionTableActions from './SubmissionTableActions' +import styles from './SubmissionTable.module.scss' + +interface Props { + className?: string + datas: Submission[] + isRemovingSubmission: IsRemovingType + doRemoveSubmission: (item: Submission) => void + isRemovingReviewSummations: IsRemovingType + doRemoveReviewSummations: (item: Submission) => void + isRunningTest: IsRemovingType + doPostBusEvent: (submissionId: string, testType: string) => void + showSubmissionHistory: IsRemovingType + setShowSubmissionHistory: Dispatch> +} + +export const SubmissionTable: FC = (props: Props) => { + const { width: screenWidth }: WindowSize = useWindowSize() + const isTablet = useMemo(() => screenWidth <= 1479, [screenWidth]) + const [ + showConfirmDeleteSubmissionDialog, + setShowConfirmDeleteSubmissionDialog, + ] = useState() + const [showConfirmDeleteReviewsDialog, setShowConfirmDeleteReviewsDialog] + = useState() + + const columns = useMemo[]>( + () => [ + { + label: 'Submitter', + propertyName: 'createdBy', + type: 'text', + }, + { + className: 'blockCellWrap', + label: 'ID', + propertyName: 'id', + type: 'text', + }, + { + label: 'Submission date', + propertyName: 'submittedDateString', + type: 'text', + }, + { + label: 'Example score', + renderer: (data: Submission) => ( + + {data.exampleScore === undefined + ? 'N/A' + : data.exampleScore} + + ), + type: 'element', + }, + { + label: 'Provisional score', + renderer: (data: Submission) => ( + + {data.provisionalScore === undefined + ? 'N/A' + : data.provisionalScore} + + ), + type: 'element', + }, + { + label: 'Final score', + renderer: (data: Submission) => ( + + {data.finalScore === undefined + ? 'N/A' + : data.finalScore} + + ), + type: 'element', + }, + { + label: 'Provisional rank', + renderer: (data: Submission) => ( + + {data.provisionalRank === undefined + ? 'N/A' + : data.provisionalRank} + + ), + type: 'element', + }, + { + label: 'Final rank', + renderer: (data: Submission) => ( + + {data.finalRank === undefined ? 'N/A' : data.finalRank} + + ), + type: 'element', + }, + { + label: '', + renderer: (data: Submission) => ( +
    + + {!data.hideToggleHistory && ( + + )} +
    + ), + type: 'element', + }, + ], + // eslint-disable-next-line react-hooks/exhaustive-deps + [ + props.isRemovingSubmission, + props.isRemovingReviewSummations, + props.isRunningTest, + props.showSubmissionHistory, + ], + ) + + const columnsMobile = useMemo[][]>( + () => columns.map(column => { + if (column.label === '') { + return [ + { + ...column, + colSpan: 2, + mobileType: 'last-value', + }, + ] + } + + return [ + { + ...column, + className: '', + label: `${column.label as string} label`, + mobileType: 'label', + renderer: () => ( +
    + {column.label as string} + : +
    + ), + type: 'element', + }, + { + ...column, + mobileType: 'last-value', + }, + ] + }), + [columns], + ) + + return ( + + {isTablet ? ( + + ) : ( + + )} + + {showConfirmDeleteSubmissionDialog && ( + +
    + Are you sure you want to delete this submission from + {' '} + {showConfirmDeleteSubmissionDialog.createdBy} + ? +
    +
    + )} + {showConfirmDeleteReviewsDialog && ( + +
    + Are you sure you want to delete the review summations? +
    +
    + )} + + ) +} + +export default SubmissionTable diff --git a/src/apps/admin/src/lib/components/SubmissionTable/SubmissionTableActions.tsx b/src/apps/admin/src/lib/components/SubmissionTable/SubmissionTableActions.tsx new file mode 100644 index 000000000..5ae92b42c --- /dev/null +++ b/src/apps/admin/src/lib/components/SubmissionTable/SubmissionTableActions.tsx @@ -0,0 +1,111 @@ +/** + * Submission Table Actions. + */ +import { Dispatch, FC, SetStateAction, useState } from 'react' +import classNames from 'classnames' + +import { ChevronDownIcon } from '@heroicons/react/solid' +import { Button } from '~/libs/ui' + +import { DropdownMenu } from '../common/DropdownMenu' +import { useEventCallback } from '../../hooks' +import { IsRemovingType, Submission } from '../../models' + +interface Props { + data: Submission + isRunningTest: IsRemovingType + isRemovingSubmission: IsRemovingType + isRemovingReviewSummations: IsRemovingType + doPostBusEvent: (submissionId: string, testType: string) => void + setShowConfirmDeleteSubmissionDialog: Dispatch< + SetStateAction + > + setShowConfirmDeleteReviewsDialog: Dispatch< + SetStateAction + > +} + +export const SubmissionTableActions: FC = (props: Props) => { + const [openDropdown, setOpenDropdown] = useState(false) + const manageDropdownMenuTrigger = useEventCallback( + (triggerProps: { + open: boolean + setOpen: Dispatch> + }) => { + const createToggle = () => (): void => triggerProps.setOpen(!triggerProps.open) + return ( + + ) + }, + ) + + return ( + +
      + {props.data.isTheLatestSubmission && ( +
    • + Run System Test +
    • + )} +
    • + Run Provisional Test +
    • +
    • + Delete Submission +
    • +
    • + Delete Review Summations +
    • +
    +
    + ) +} + +export default SubmissionTableActions diff --git a/src/apps/admin/src/lib/components/SubmissionTable/index.ts b/src/apps/admin/src/lib/components/SubmissionTable/index.ts new file mode 100644 index 000000000..2d7f27199 --- /dev/null +++ b/src/apps/admin/src/lib/components/SubmissionTable/index.ts @@ -0,0 +1 @@ +export { default as SubmissionTable } from './SubmissionTable' diff --git a/src/apps/admin/src/lib/components/index.ts b/src/apps/admin/src/lib/components/index.ts index d0e81c24e..f739efa1d 100644 --- a/src/apps/admin/src/lib/components/index.ts +++ b/src/apps/admin/src/lib/components/index.ts @@ -23,3 +23,4 @@ export * from './RejectPendingConfirmDialog' export * from './ResourceTable' export * from './FieldHandleSelect' export * from './FieldSingleSelect' +export * from './SubmissionTable' diff --git a/src/apps/admin/src/lib/hooks/index.ts b/src/apps/admin/src/lib/hooks/index.ts index 67c18e0c3..809a1b375 100644 --- a/src/apps/admin/src/lib/hooks/index.ts +++ b/src/apps/admin/src/lib/hooks/index.ts @@ -25,3 +25,5 @@ export * from './useManagePermissionGroupMembers' export * from './useManageChallengeResources' export * from './useSearchUserInfo' export * from './useManageAddChallengeResource' +export * from './useManageBusEvent' +export * from './useManageChallengeSubmissions' diff --git a/src/apps/admin/src/lib/hooks/useManageBusEvent.ts b/src/apps/admin/src/lib/hooks/useManageBusEvent.ts new file mode 100644 index 000000000..0d32eabd3 --- /dev/null +++ b/src/apps/admin/src/lib/hooks/useManageBusEvent.ts @@ -0,0 +1,66 @@ +/** + * Manage bus event + */ +import { useCallback, useMemo, useState } from 'react' +import { toast } from 'react-toastify' +import _ from 'lodash' + +import { CREATE_BUS_EVENT_DATA_SUBMISSION_MARATHON_MATCH } from '../../config/busEvent.config' +import { reqToBusAPI } from '../services' +import { handleError } from '../utils' +import { IsRemovingType } from '../models' + +export interface useManageBusEventProps { + isRunningTest: IsRemovingType + isRunningTestBool: boolean + doPostBusEvent: (submissionId: string, testType: string) => void +} + +/** + * Manage bus event + */ +export function useManageBusEvent(): useManageBusEventProps { + const [isRunningTest, setIsRunningTest] = useState({}) + const isRunningTestBool = useMemo( + () => _.some(isRunningTest, value => value === true), + [isRunningTest], + ) + + const doPostBusEvent = useCallback( + (submissionId: string, testType: string) => { + setIsRunningTest(previous => ({ + ...previous, + [`${submissionId}_${testType}`]: true, + })) + reqToBusAPI( + CREATE_BUS_EVENT_DATA_SUBMISSION_MARATHON_MATCH( + submissionId, + testType, + ), + ) + .then(() => { + setIsRunningTest(previous => ({ + ...previous, + [`${submissionId}_${testType}`]: false, + })) + toast.success(`Run ${testType} test successfully`, { + toastId: 'Run test', + }) + }) + .catch(e => { + setIsRunningTest(previous => ({ + ...previous, + [`${submissionId}_${testType}`]: false, + })) + handleError(e) + }) + }, + [], + ) + + return { + doPostBusEvent, + isRunningTest, + isRunningTestBool, + } +} diff --git a/src/apps/admin/src/lib/hooks/useManageChallengeSubmissions.ts b/src/apps/admin/src/lib/hooks/useManageChallengeSubmissions.ts new file mode 100644 index 000000000..6bc33d6ae --- /dev/null +++ b/src/apps/admin/src/lib/hooks/useManageChallengeSubmissions.ts @@ -0,0 +1,198 @@ +/** + * Manage Challenge Submissions + */ +import { + Dispatch, + SetStateAction, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react' +import { toast } from 'react-toastify' +import _ from 'lodash' + +import { IsRemovingType, MemberSubmission, recalculateSubmissionRank, Submission } from '../models' +import { fetchSubmissionsOfChallenge, removeSubmission } from '../services' +import { handleError } from '../utils' +import { removeReviewSummations } from '../services/reviews.service' + +export interface useManageChallengeSubmissionsProps { + isLoading: boolean + submissions: Submission[] + isRemovingSubmission: IsRemovingType + isRemovingSubmissionBool: boolean + isRemovingReviewSummations: IsRemovingType + isRemovingReviewSummationsBool: boolean + doRemoveSubmission: (item: Submission) => void + doRemoveReviewSummations: (item: Submission) => void + showSubmissionHistory: IsRemovingType + setShowSubmissionHistory: Dispatch> +} + +/** + * Manage challenge submissions redux state + * @param challengeId challenge id + * @returns state data + */ +export function useManageChallengeSubmissions( + challengeId: string, +): useManageChallengeSubmissionsProps { + const [isLoading, setIsLoading] = useState(false) + const [memberSubmissions, setMemberSubmissions] = useState< + MemberSubmission[] + >([]) + const [showSubmissionHistory, setShowSubmissionHistory] + = useState({}) + + const submissions = useMemo(() => { + const results: Submission[] = [] + _.forEach(memberSubmissions, memberSubmission => { + const theLatestSubmission = memberSubmission.submissions[0] + results.push({ + ...theLatestSubmission, + hideToggleHistory: memberSubmission.submissions.length <= 1, + isTheLatestSubmission: true, + }) + + if (showSubmissionHistory[theLatestSubmission.id]) { + for ( + let index = 1; + index < memberSubmission.submissions.length; + index++ + ) { + const submission = memberSubmission.submissions[index] + submission.hideToggleHistory = true + results.push(submission) + } + } + }) + return results + }, [memberSubmissions, showSubmissionHistory]) + + const isLoadingRef = useRef(false) + const [isRemovingSubmission, setIsRemovingSubmission] + = useState({}) + const isRemovingSubmissionBool = useMemo( + () => _.some(isRemovingSubmission, value => value === true), + [isRemovingSubmission], + ) + const [isRemovingReviewSummations, setIsRemovingReviewSummations] + = useState({}) + const isRemovingReviewSummationsBool = useMemo( + () => _.some(isRemovingReviewSummations, value => value === true), + [isRemovingReviewSummations], + ) + useEffect(() => { + if (isLoadingRef.current) { + return + } + + if (challengeId) { + isLoadingRef.current = true + setIsLoading(isLoadingRef.current) + fetchSubmissionsOfChallenge(challengeId) + .then(result => { + isLoadingRef.current = false + setIsLoading(isLoadingRef.current) + setMemberSubmissions(result) + }) + .catch(e => { + isLoadingRef.current = false + setIsLoading(isLoadingRef.current) + handleError(e) + fail() + }) + } + }, [challengeId]) + + const doRemoveSubmission = useCallback( + (item: Submission) => { + setIsRemovingSubmission(prev => ({ + ...prev, + [item.id]: true, + })) + + removeSubmission(item.id) + .then(() => { + toast.success('Submission removed successfully', { + toastId: 'Remove submission', + }) + setIsRemovingSubmission(prev => ({ + ...prev, + [item.id]: false, + })) + setMemberSubmissions(prev => recalculateSubmissionRank(_.map(prev, member => { + member.submissions = _.filter( + member.submissions, + sub => sub.id !== item.id, + ) + return member + }))) // remove submission in ui + }) + .catch(e => { + handleError(e) + setIsRemovingSubmission(prev => ({ + ...prev, + [item.id]: false, + })) + }) + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [], + ) + const doRemoveReviewSummations = useCallback( + (item: Submission) => { + setIsRemovingReviewSummations(prev => ({ + ...prev, + [item.id]: true, + })) + + removeReviewSummations( + (item.reviewSummation ?? []).map(rS => rS.id), + ) + .then(() => { + toast.success('Review summations removed successfully', { + toastId: 'Remove review summations', + }) + setIsRemovingReviewSummations(prev => ({ + ...prev, + [item.id]: false, + })) + + setMemberSubmissions(prev => recalculateSubmissionRank(_.map(prev, member => { + member.submissions = _.map( + member.submissions, + sub => (sub.id === item.id + ? { ...sub, reviewSummation: [] } + : sub), + ) + return member + }))) // remove review summations in ui + }) + .catch(e => { + handleError(e) + setIsRemovingReviewSummations(prev => ({ + ...prev, + [item.id]: false, + })) + }) + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [], + ) + + return { + doRemoveReviewSummations, + doRemoveSubmission, + isLoading, + isRemovingReviewSummations, + isRemovingReviewSummationsBool, + isRemovingSubmission, + isRemovingSubmissionBool, + setShowSubmissionHistory, + showSubmissionHistory, + submissions, + } +} diff --git a/src/apps/admin/src/lib/models/MemberSubmission.model.ts b/src/apps/admin/src/lib/models/MemberSubmission.model.ts new file mode 100644 index 000000000..f9ada949e --- /dev/null +++ b/src/apps/admin/src/lib/models/MemberSubmission.model.ts @@ -0,0 +1,11 @@ +import { Submission } from './Submission.model' + +/** + * Model for member submissions info + */ +export interface MemberSubmission { + submissions: Submission[] + memberId: number + provisionalRank: number | undefined + finalRank: number | undefined +} diff --git a/src/apps/admin/src/lib/models/RequestBusAPI.model.ts b/src/apps/admin/src/lib/models/RequestBusAPI.model.ts new file mode 100644 index 000000000..bf52c1d22 --- /dev/null +++ b/src/apps/admin/src/lib/models/RequestBusAPI.model.ts @@ -0,0 +1,15 @@ +/** + * Request to bust api + */ +export interface RequestBusAPI { + topic: string + originator: string + timestamp: string + 'mime-type': string + payload: { + id: string + submissionId: string + resource: string + testType: string + } +} diff --git a/src/apps/admin/src/lib/models/Submission.model.ts b/src/apps/admin/src/lib/models/Submission.model.ts new file mode 100644 index 000000000..2b8a9ec77 --- /dev/null +++ b/src/apps/admin/src/lib/models/Submission.model.ts @@ -0,0 +1,183 @@ +import _ from 'lodash' +import moment from 'moment' + +import { EnvironmentConfig } from '~/config' + +import { TABLE_DATE_FORMAT } from '../../config/index.config' +import { isStringObject, processRanks, toFixed } from '../utils' + +import { MemberSubmission } from './MemberSubmission.model' +import { + adjustSubmissionReviewResponse, + SubmissionReview, +} from './SubmissionReview.model' +import { SubmissionReviewSummation } from './SubmissionReviewSummation.model' + +/** + * Model for submission info + */ +export interface Submission { + updatedBy: string + created: Date + legacySubmissionId: number + isFileSubmission: boolean + type: string + submittedDate: Date + submittedDateString?: string // this field is calculated at frontend + url: string + challengeId: number + createdBy: string + legacyChallengeId: number + review: SubmissionReview[] + reviewSummation?: SubmissionReviewSummation[] + id: string + submissionPhaseId: string + updated: Date + fileType: string + memberId: number + v5ChallengeId: string + exampleScore?: number // this field is calculated at frontend + provisionalScore?: number // this field is calculated at frontend + finalScore?: number // this field is calculated at frontend + provisionalRank?: number // this field is calculated at frontend + finalRank?: number // this field is calculated at frontend + hideToggleHistory?: boolean // this field is calculated at frontend + isTheLatestSubmission?: boolean // this field is calculated at frontend +} + +/** + * Recalculate submission rank + * @param memberSubmissions array of member submissions + * @returns array of member submission + */ +export function recalculateSubmissionRank( + memberSubmissions: MemberSubmission[], +): MemberSubmission[] { + _.each(memberSubmissions, memberSubmission => { + memberSubmission.submissions = memberSubmission.submissions.map(adjustSubmissionResponse) + }) + + const { submissions: finalSubmissions, maxFinalScore }: { + maxFinalScore: number; + submissions: MemberSubmission[]; + } + = processRanks(memberSubmissions) + finalSubmissions.sort((a, b) => { + if (maxFinalScore === 0) { + return (a.provisionalRank ?? 0) - (b.provisionalRank ?? 0) + } + + return (a.finalRank ?? 0) - (b.finalRank ?? 0) + }) + let results: Submission[] = [] + + _.forEach(finalSubmissions, finalSubmission => { + finalSubmission.submissions[0].provisionalRank + = finalSubmission.provisionalRank + finalSubmission.submissions[0].finalRank = finalSubmission.finalRank + results = [...results, ...finalSubmission.submissions] + }) + + return finalSubmissions +} + +/** + * Update submissions to show in ui + * @param data data from backend response + * @returns array of member submission + */ +export function adjustSubmissionsResponse( + submissions: Submission[], +): MemberSubmission[] { + const data: { + [key: number]: Submission[] + } = {} + const result: MemberSubmission[] = [] + + _.each(submissions, submission => { + const { memberId }: Submission = submission + if (!data[memberId]) { + data[memberId] = [] + } + + data[memberId].push(submission) + }) + + _.each(data, (value, key) => { + result.push({ + finalRank: undefined, + memberId: key as any, + provisionalRank: undefined, + submissions: [ + ...value.sort( + (a, b) => new Date(b.submittedDate) + .getTime() + - new Date(a.submittedDate) + .getTime(), + ), + ], + }) + }) + + return recalculateSubmissionRank(result) +} + +/** + * Update submission to show in ui + * @param data data from backend response + * @returns updated data + */ +export function adjustSubmissionResponse(data: Submission): Submission { + const validReviews = _.reject(data.review, [ + 'typeId', + EnvironmentConfig.ADMIN.AV_SCAN_SCORER_REVIEW_TYPE_ID, + ]) + const provisionalReviews = _.filter( + validReviews, + item => !item.metadata || item.metadata.testType === 'provisional', + ) + const exampleReviews = _.filter( + validReviews, + item => item.metadata?.testType === 'example', + ) + const finalScore = toFixed( + _.get(data, 'reviewSummation[0].aggregateScore', undefined), + 5, + ) + const provisionalScore = toFixed( + _.get(provisionalReviews, '[0].score', undefined), + 5, + ) + const exampleScore = toFixed( + _.get(exampleReviews, '[0].score', undefined), + 5, + ) + const created + = data.created && isStringObject(data.created) + ? new Date(data.created) + : data.created + const submittedDate + = data.submittedDate && isStringObject(data.submittedDate) + ? new Date(data.submittedDate) + : data.submittedDate + const updated + = data.updated && isStringObject(data.updated) + ? new Date(data.updated) + : data.updated + + return { + ...data, + created, + exampleScore: exampleScore as number, + finalScore: finalScore as number, + provisionalScore: provisionalScore as number, + review: (data.review ?? []).map(adjustSubmissionReviewResponse), + submittedDate, + submittedDateString: data.submittedDate + ? moment(data.submittedDate) + .local() + .format(TABLE_DATE_FORMAT) + : data.submittedDate, + updated, + } +} diff --git a/src/apps/admin/src/lib/models/SubmissionReview.model.ts b/src/apps/admin/src/lib/models/SubmissionReview.model.ts new file mode 100644 index 000000000..ccaaf5250 --- /dev/null +++ b/src/apps/admin/src/lib/models/SubmissionReview.model.ts @@ -0,0 +1,42 @@ +/** + * Model for submission review info + */ +export interface SubmissionReview { + score: number + updatedBy: string + reviewerId: string + submissionId: string + createdBy: string + created: Date + scoreCardId: number + typeId: string + id: string + reviewedDate: Date + updated: Date + status: string + metadata?: { + testType: 'provisional' | 'example' + } +} + +/** + * Update submission review to show in ui + * @param data data from backend response + * @returns updated data + */ +export function adjustSubmissionReviewResponse( + data: SubmissionReview, +): SubmissionReview { + const created = data.created ? new Date(data.created) : data.created + const reviewedDate = data.created + ? new Date(data.reviewedDate) + : data.reviewedDate + const updated = data.created ? new Date(data.updated) : data.updated + + return { + ...data, + created, + reviewedDate, + updated, + } +} diff --git a/src/apps/admin/src/lib/models/SubmissionReviewSummation.model.ts b/src/apps/admin/src/lib/models/SubmissionReviewSummation.model.ts new file mode 100644 index 000000000..f8624ebf0 --- /dev/null +++ b/src/apps/admin/src/lib/models/SubmissionReviewSummation.model.ts @@ -0,0 +1,7 @@ +/** + * Model for submission review summation info + */ +export interface SubmissionReviewSummation { + aggregateScore: number + id: string +} diff --git a/src/apps/admin/src/lib/models/challenge-management/Challenge.ts b/src/apps/admin/src/lib/models/challenge-management/Challenge.ts index c706fd494..416f00e80 100644 --- a/src/apps/admin/src/lib/models/challenge-management/Challenge.ts +++ b/src/apps/admin/src/lib/models/challenge-management/Challenge.ts @@ -45,4 +45,5 @@ export interface Challenge { groups: Array /** Challenge phases. */ phases: Array<{ name: string; isOpen: boolean; scheduledEndDate: string }> + tags: Array } diff --git a/src/apps/admin/src/lib/models/index.ts b/src/apps/admin/src/lib/models/index.ts index 4daa27b2b..1e7960c6a 100644 --- a/src/apps/admin/src/lib/models/index.ts +++ b/src/apps/admin/src/lib/models/index.ts @@ -31,6 +31,9 @@ export * from './FormAddGroup.model' export * from './FormGroupMembersFilters.model' export * from './RoleMemberInfo.model' export * from './FormAddResource.model' +export * from './Submission.model' +export * from './RequestBusAPI.model' +export * from './MemberSubmission.model' export * from './FormAddGroupMembers.type' export * from './TableFilterType.type' export * from './TableRolesFilter.type' diff --git a/src/apps/admin/src/lib/services/bus-event.service.ts b/src/apps/admin/src/lib/services/bus-event.service.ts new file mode 100644 index 000000000..82a352944 --- /dev/null +++ b/src/apps/admin/src/lib/services/bus-event.service.ts @@ -0,0 +1,20 @@ +/** + * Bus event service + */ +import { EnvironmentConfig } from '~/config' +import { xhrPostAsync } from '~/libs/core' + +import { RequestBusAPI } from '../models' + +/** + * Send post event to bus api + * @param data bus event data + * @returns resolve to empty string if success + */ +export const reqToBusAPI = async (data: RequestBusAPI): Promise => { + const resultData = await xhrPostAsync( + `${EnvironmentConfig.API.V5}/bus/events`, + data, + ) + return resultData +} diff --git a/src/apps/admin/src/lib/services/index.js b/src/apps/admin/src/lib/services/index.js index e688beb46..e9b733f54 100644 --- a/src/apps/admin/src/lib/services/index.js +++ b/src/apps/admin/src/lib/services/index.js @@ -6,3 +6,5 @@ export * from './terms.service' export * from './review-management.service' export * from './client.service' export * from './billing-accounts.service' +export * from './submissions.service' +export * from './bus-event.service' diff --git a/src/apps/admin/src/lib/services/reviews.service.ts b/src/apps/admin/src/lib/services/reviews.service.ts new file mode 100644 index 000000000..853708c6c --- /dev/null +++ b/src/apps/admin/src/lib/services/reviews.service.ts @@ -0,0 +1,41 @@ +/** + * Reviews service + */ +import { EnvironmentConfig } from '~/config' +import { xhrDeleteAsync } from '~/libs/core' + +import { ApiV5ResponseSuccess } from '../models' + +/** + * Remove the review summation + * @param reviewSummationId the review summation id + * @returns resolves to success or failure calling api + */ +export const removeReviewSummation = async ( + reviewSummationId: string, +): Promise => { + await xhrDeleteAsync( + `${EnvironmentConfig.API.V5}/reviewSummations/${reviewSummationId}`, + ) + return { + success: true, + } +} + +/** + * Remove the review summations + * @param reviewSummationIds the review summation id list + * @returns resolves to success or failure calling api + */ +export const removeReviewSummations = async ( + reviewSummationIds: string[], +): Promise => { + for (const reviewSummationId of reviewSummationIds) { + // eslint-disable-next-line no-await-in-loop + await removeReviewSummation(reviewSummationId) + } + + return { + success: true, + } +} diff --git a/src/apps/admin/src/lib/services/submissions.service.ts b/src/apps/admin/src/lib/services/submissions.service.ts new file mode 100644 index 000000000..9bec8b654 --- /dev/null +++ b/src/apps/admin/src/lib/services/submissions.service.ts @@ -0,0 +1,44 @@ +/** + * Submissions service + */ +import { EnvironmentConfig } from '~/config' +import { xhrDeleteAsync, xhrGetAsync } from '~/libs/core' + +import { + adjustSubmissionsResponse, + ApiV5ResponseSuccess, + MemberSubmission, + Submission, +} from '../models' + +/** + * Gets all submissions of challenge + * @param challengeId challenge id + * @returns resolves to the submission list + */ +export const fetchSubmissionsOfChallenge = async ( + challengeId: string, +): Promise => { + if (!challengeId) { + return Promise.resolve([]) + } + + const results = await xhrGetAsync( + `${EnvironmentConfig.API.V5}/submissions?challengeId=${challengeId}`, + ) + return adjustSubmissionsResponse(results) +} + +/** + * Remove the submission + * @param submissionId the submission id + * @returns resolves to success or failure calling api + */ +export const removeSubmission = async ( + submissionId: string, +): Promise => { + const result = await xhrDeleteAsync( + `${EnvironmentConfig.API.V5}/submissions/${submissionId}`, + ) + return result +} diff --git a/src/apps/admin/src/lib/utils/challenge.ts b/src/apps/admin/src/lib/utils/challenge.ts new file mode 100644 index 000000000..13007f1be --- /dev/null +++ b/src/apps/admin/src/lib/utils/challenge.ts @@ -0,0 +1,84 @@ +/** + * Util for challenge + */ + +import _ from 'lodash' + +import { Challenge, MemberSubmission } from '../models' + +/** + * Check if the challenge is a marathon match challenge + * @param challenge challenge info + * @returns true if challenge is mm + */ +export function checkIsMM(challenge: Challenge): boolean { + const tags = _.get(challenge, 'tags') || [] + const isMMType = challenge ? challenge.type === 'Marathon Match' : false + return tags.includes('Marathon Match') || isMMType +} + +/** + * Process each submission rank of MM challenge + * @param submissions the array of submissions + * @returns submission after process rank + */ +export function processRanks(submissions: MemberSubmission[]): { + maxFinalScore: number + submissions: MemberSubmission[] +} { + let maxFinalScore + = _.get(submissions[0], 'submissions[0]', { + finalScore: 0, + }).finalScore ?? 0 + submissions.sort((a, b) => { + let pA = _.get(a, 'submissions[0]', { + provisionalScore: 0, + }).provisionalScore + let pB = _.get(b, 'submissions[0]', { + provisionalScore: 0, + }).provisionalScore + if (pA === undefined) pA = 0 + if (pB === undefined) pB = 0 + if (pA === pB) { + const timeA = _.get(a, 'submissions[0].submittedDate') + const timeB = _.get(b, 'submissions[0].submittedDate') + return timeA.getTime() - timeB.getTime() + } + + return pB - pA + }) + _.each(submissions, (submission, i) => { + if (!submission.provisionalRank) { + submission.provisionalRank = 0 + } + + submission.provisionalRank = i + 1 + }) + + submissions.sort((a, b) => { + let pA = _.get(a, 'submissions[0]', { finalScore: 0 }).finalScore + let pB = _.get(b, 'submissions[0]', { finalScore: 0 }).finalScore + if (pA === undefined) pA = 0 + if (pB === undefined) pB = 0 + if (pA > 0) maxFinalScore = pA + if (pB > 0) maxFinalScore = pB + if (pA === pB) { + const timeA = _.get(a, 'submissions[0].submittedDate') + const timeB = _.get(b, 'submissions[0].submittedDate') + return timeA.getTime() - timeB.getTime() + } + + return pB - pA + }) + if (maxFinalScore > 0) { + _.each(submissions, (submission, i) => { + if (!submission.finalRank) { + submission.finalRank = 0 + } + + submission.finalRank = i + 1 + }) + } + + return { maxFinalScore, submissions } +} diff --git a/src/apps/admin/src/lib/utils/index.ts b/src/apps/admin/src/lib/utils/index.ts index 619e1330b..92f28fd78 100644 --- a/src/apps/admin/src/lib/utils/index.ts +++ b/src/apps/admin/src/lib/utils/index.ts @@ -1,2 +1,5 @@ export * from './api' export * from './validation' +export * from './challenge' +export * from './number' +export * from './string' diff --git a/src/apps/admin/src/lib/utils/number.ts b/src/apps/admin/src/lib/utils/number.ts new file mode 100644 index 000000000..08714be08 --- /dev/null +++ b/src/apps/admin/src/lib/utils/number.ts @@ -0,0 +1,56 @@ +/** + * Util for number + */ + +import _ from 'lodash' + +/** + * Remove decimal in number + * @param num number + * @returns number + */ +function removeDecimal(num: number): string | undefined { + // eslint-disable-next-line prefer-regex-literals + const re = new RegExp('^-?\\d+') + return num.toString() + .match(re)?.[0] +} + +/** + * Fixed the input number + * @param num number + * @param decimal number of unit to fix + * @returns number + */ +function toAcurateFixed(num: number, decimal: number): string | undefined { + const re = new RegExp(`^-?\\d+(?:.\\d{0,${decimal}})?`) + return num.toString() + .match(re)?.[0] +} + +/** + * Fix the input number + * @param num number + * @param decimal number of unit to fix + * @returns number + */ +export function toFixed( + num: number | string | undefined, + decimal: number, +): number | undefined { + if (num === undefined) { + return num + } + + if (_.isNaN(parseFloat(num as string))) return num as number + const numFloat = parseFloat(num as string) + + const result = _.toFinite(toAcurateFixed(numFloat, decimal)) + const integerResult = _.toFinite(removeDecimal(numFloat)) + + if (_.isInteger(result)) { + return integerResult + } + + return result +} diff --git a/src/apps/admin/src/lib/utils/string.ts b/src/apps/admin/src/lib/utils/string.ts new file mode 100644 index 000000000..81b19d3b9 --- /dev/null +++ b/src/apps/admin/src/lib/utils/string.ts @@ -0,0 +1,16 @@ +/** + * Util for string + */ + +/** + * Check if variable is string + * @param str input variable + * @returns true if the object is a string, false if otherwise + */ +export function isStringObject(str: any): boolean { + if (typeof str === 'string' || str instanceof String) { + return true + } + + return false +} diff --git a/src/config/environments/default.env.ts b/src/config/environments/default.env.ts index b3f85f0ef..77364ef84 100644 --- a/src/config/environments/default.env.ts +++ b/src/config/environments/default.env.ts @@ -77,6 +77,7 @@ export const USERFLOW_SURVEYS = { } export const ADMIN = { + AV_SCAN_SCORER_REVIEW_TYPE_ID: '68c5a381-c8ab-48af-92a7-7a869a4ee6c3', CHALLENGE_URL: 'https://www.topcoder-dev.com/challenges', CONNECT_URL: 'https://connect.topcoder-dev.com', DIRECT_URL: 'https://www.topcoder-dev.com/direct', diff --git a/src/config/environments/global-config.model.ts b/src/config/environments/global-config.model.ts index d12bf9c09..44ce7f169 100644 --- a/src/config/environments/global-config.model.ts +++ b/src/config/environments/global-config.model.ts @@ -49,5 +49,6 @@ export interface GlobalConfig { WORK_MANAGER_URL: string ONLINE_REVIEW_URL: string CHALLENGE_URL: string + AV_SCAN_SCORER_REVIEW_TYPE_ID: string } } diff --git a/src/config/environments/prod.env.ts b/src/config/environments/prod.env.ts index f2baacba2..5e7d7b74e 100644 --- a/src/config/environments/prod.env.ts +++ b/src/config/environments/prod.env.ts @@ -7,6 +7,7 @@ export const VANILLA_FORUM = { } export const ADMIN = { + AV_SCAN_SCORER_REVIEW_TYPE_ID: '55bbb17d-aac2-45a6-89c3-a8d102863d05', CHALLENGE_URL: 'https://www.topcoder.com/challenges', CONNECT_URL: 'https://connect.topcoder.com', DIRECT_URL: 'https://www.topcoder.com/direct', diff --git a/src/libs/ui/lib/components/table/Table.tsx b/src/libs/ui/lib/components/table/Table.tsx index af302b8b1..0f576ee83 100644 --- a/src/libs/ui/lib/components/table/Table.tsx +++ b/src/libs/ui/lib/components/table/Table.tsx @@ -30,6 +30,7 @@ interface TableProps { readonly onRowClick?: (data: T) => void readonly onToggleSort?: (sort: Sort) => void readonly removeDefaultSort?: boolean + readonly className?: string } interface DefaultSortDirectionMap { @@ -185,7 +186,7 @@ const Table: (props: TableProps) = return ( /* TODO: sticky header */ -
    +
    diff --git a/yarn.lock b/yarn.lock index 0960b4526..6fd04826d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17439,7 +17439,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -17453,13 +17453,6 @@ strip-ansi@^3.0.0: dependencies: ansi-regex "^2.0.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" @@ -18600,6 +18593,11 @@ utrie@^1.0.2: dependencies: base64-arraybuffer "^1.0.2" +uuid@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.0.tgz#9549028be1753bb934fc96e2bca09bb4105ae912" + integrity sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A== + uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" @@ -19293,7 +19291,7 @@ workbox-window@6.5.4: "@types/trusted-types" "^2.0.2" workbox-core "6.5.4" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -19311,15 +19309,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.0.1.tgz#2101e861777fec527d0ea90c57c6b03aac56a5b3" From 21fad4af56007162125e9b4f7781c7531a528042 Mon Sep 17 00:00:00 2001 From: dat Date: Mon, 2 Jun 2025 21:17:00 +0700 Subject: [PATCH 2/2] Marathon match functionality - final fix --- .../ManageSubmissionPage/ManageSubmissionPage.tsx | 2 +- .../src/lib/components/SubmissionTable/SubmissionTable.tsx | 6 +++--- .../components/SubmissionTable/SubmissionTableActions.tsx | 2 +- src/apps/admin/src/lib/models/RequestBusAPI.model.ts | 2 +- src/apps/admin/src/lib/utils/number.ts | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/apps/admin/src/challenge-management/ManageSubmissionPage/ManageSubmissionPage.tsx b/src/apps/admin/src/challenge-management/ManageSubmissionPage/ManageSubmissionPage.tsx index 04584a537..ab9a2abf1 100644 --- a/src/apps/admin/src/challenge-management/ManageSubmissionPage/ManageSubmissionPage.tsx +++ b/src/apps/admin/src/challenge-management/ManageSubmissionPage/ManageSubmissionPage.tsx @@ -67,7 +67,7 @@ export const ManageSubmissionPage: FC = (props: Props) => { ) : (
    void isRemovingReviewSummations: IsRemovingType @@ -196,11 +196,11 @@ export const SubmissionTable: FC = (props: Props) => { return ( {isTablet ? ( - + ) : (
    = (props: Props) => { className={classNames({ disabled: props.isRemovingReviewSummations[props.data.id] - || !(props.data.reviewSummation ?? []).length, + || !props.data.reviewSummation?.length, })} onClick={function onClick() { props.setShowConfirmDeleteReviewsDialog(props.data) diff --git a/src/apps/admin/src/lib/models/RequestBusAPI.model.ts b/src/apps/admin/src/lib/models/RequestBusAPI.model.ts index bf52c1d22..2adeb1c03 100644 --- a/src/apps/admin/src/lib/models/RequestBusAPI.model.ts +++ b/src/apps/admin/src/lib/models/RequestBusAPI.model.ts @@ -1,5 +1,5 @@ /** - * Request to bust api + * Request to bus API */ export interface RequestBusAPI { topic: string diff --git a/src/apps/admin/src/lib/utils/number.ts b/src/apps/admin/src/lib/utils/number.ts index 08714be08..7c8a70048 100644 --- a/src/apps/admin/src/lib/utils/number.ts +++ b/src/apps/admin/src/lib/utils/number.ts @@ -42,7 +42,7 @@ export function toFixed( return num } - if (_.isNaN(parseFloat(num as string))) return num as number + if (Number.isNaN(Number(num))) return num as number const numFloat = parseFloat(num as string) const result = _.toFinite(toAcurateFixed(numFloat, decimal))