From d933676c5de2e90a25e03b9a41b48b9e0b85a5ea Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Sun, 2 Nov 2025 15:59:50 +1100 Subject: [PATCH 1/2] Fix performance issue on scorecard editor page --- .../components/CalculatedWeightsSum.tsx | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/apps/review/src/pages/scorecards/EditScorecardPage/components/CalculatedWeightsSum.tsx b/src/apps/review/src/pages/scorecards/EditScorecardPage/components/CalculatedWeightsSum.tsx index 204425c7b..6f59d1e14 100644 --- a/src/apps/review/src/pages/scorecards/EditScorecardPage/components/CalculatedWeightsSum.tsx +++ b/src/apps/review/src/pages/scorecards/EditScorecardPage/components/CalculatedWeightsSum.tsx @@ -1,5 +1,5 @@ -import { FC } from 'react' -import { useFormContext } from 'react-hook-form' +import { FC, useMemo } from 'react' +import { useFormContext, useWatch } from 'react-hook-form' import classNames from 'classnames' import styles from './CalculatedWeightsSum.module.scss' @@ -13,11 +13,25 @@ interface CalculatedWeightsSumProps { const CalculatedWeightsSum: FC = props => { const form = useFormContext() - const fields = form.watch(props.fieldName) + const watchedFields = useWatch({ + control: form.control, + defaultValue: [], + name: props.fieldName, + }) as { weight?: number | string | null }[] | undefined - const weightsSum = fields.reduce( - (sum: number, field: { weight: string | number | undefined }) => (Number(field.weight) || 0) + sum, - 0, + const fields = useMemo( + () => (Array.isArray(watchedFields) ? watchedFields : []), + [watchedFields], + ) + + const weightsSum = useMemo( + () => fields.reduce( + (sum: number, field: { weight?: string | number | null }) => ( + Number(field?.weight ?? 0) + sum + ), + 0, + ), + [fields], ) return ( From 147b220eec40da180e374dcd11f5d4121bfb26df Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Sun, 2 Nov 2025 16:16:54 +1100 Subject: [PATCH 2/2] Fix for page crashing when editing a scorecard --- .../components/BasicSelect.tsx | 62 +++++--- .../components/ScorecardInfoForm.tsx | 148 ++++++++++++++---- 2 files changed, 157 insertions(+), 53 deletions(-) diff --git a/src/apps/review/src/pages/scorecards/EditScorecardPage/components/BasicSelect.tsx b/src/apps/review/src/pages/scorecards/EditScorecardPage/components/BasicSelect.tsx index 7890278f0..5d91ef812 100644 --- a/src/apps/review/src/pages/scorecards/EditScorecardPage/components/BasicSelect.tsx +++ b/src/apps/review/src/pages/scorecards/EditScorecardPage/components/BasicSelect.tsx @@ -1,37 +1,51 @@ -import { omit } from 'lodash' -import { FC, SelectHTMLAttributes } from 'react' +import { forwardRef, SelectHTMLAttributes } from 'react' import classNames from 'classnames' interface BasicSelectProps extends SelectHTMLAttributes { options: { label: string; value: string|boolean|number }[]; + mapValue?: (value: any) => string; placeholder?: string; } -const BasicSelect: FC> = props => ( - - {props.placeholder} - - {props.options.map(option => ( - ))} - -) + {options.map((option, index) => ( + + ))} + + ) +}) + +BasicSelect.displayName = 'BasicSelect' export default BasicSelect diff --git a/src/apps/review/src/pages/scorecards/EditScorecardPage/components/ScorecardInfoForm.tsx b/src/apps/review/src/pages/scorecards/EditScorecardPage/components/ScorecardInfoForm.tsx index 620322411..c2aa428c5 100644 --- a/src/apps/review/src/pages/scorecards/EditScorecardPage/components/ScorecardInfoForm.tsx +++ b/src/apps/review/src/pages/scorecards/EditScorecardPage/components/ScorecardInfoForm.tsx @@ -1,5 +1,5 @@ import * as yup from 'yup' -import { FC, useEffect, useMemo } from 'react' +import { FC, useCallback, useEffect, useMemo, useRef } from 'react' import { useFormContext } from 'react-hook-form' import classNames from 'classnames' @@ -70,8 +70,30 @@ const toChallengeTrackLabel = (value: string): string => ( ) const legacyChallengeTrackMap: Record = { - DEVELOPMENT: 'DEVELOP', - QUALITY_ASSURANCE: 'QA', + DEVELOPMENT: 'DEVELOPMENT', + DEVELOP: 'DEVELOPMENT', + QUALITY_ASSURANCE: 'QUALITY_ASSURANCE', + QA: 'QUALITY_ASSURANCE', +} + +const normalizeTrackOptionValue = (track: useFetchChallengeTracksProps['challengeTracks'][number]): string => { + if (track.track) { + return track.track + } + + const normalizedName = track.name + ?.replace(/\s+/g, '_') + .toUpperCase() + + if (normalizedName && legacyChallengeTrackMap[normalizedName]) { + return legacyChallengeTrackMap[normalizedName] + } + + if (track.name) { + return normalizedName || track.name + } + + return track.id } const normalizeChallengeTrackValue = ( @@ -82,21 +104,23 @@ const normalizeChallengeTrackValue = ( const directMatch = tracks.find(track => track.track === value && track.isActive) if (directMatch) { - return directMatch.track + return normalizeTrackOptionValue(directMatch) } const mappedValue = legacyChallengeTrackMap[value] if (mappedValue) { - const mappedMatch = tracks.find(track => track.track === mappedValue && track.isActive) + const mappedMatch = tracks.find(track => ( + normalizeTrackOptionValue(track) === mappedValue && track.isActive + )) if (mappedMatch) { - return mappedMatch.track + return normalizeTrackOptionValue(mappedMatch) } } const normalizedLabel = toChallengeTrackLabel(value) const nameMatch = tracks.find(track => track.name === normalizedLabel && track.isActive) if (nameMatch) { - return nameMatch.track + return normalizeTrackOptionValue(nameMatch) } const uppercaseValue = value @@ -109,23 +133,41 @@ const normalizeChallengeTrackValue = ( && track.isActive )) - return fallbackMatch?.track + return fallbackMatch ? normalizeTrackOptionValue(fallbackMatch) : undefined } const ScorecardInfoForm: FC = () => { const form = useFormContext() const { challengeTracks }: useFetchChallengeTracksProps = useFetchChallengeTracks() const { challengeTypes }: useFetchChallengeTypesProps = useFetchChallengeTypes() + const { getValues, setValue } = form + const normalizeValue = useCallback(( + value: string | number | boolean | null | undefined, + ): string | undefined => { + if (value === null || value === undefined) { + return undefined + } + + const normalized = String(value).trim() + + return normalized.length ? normalized : undefined + }, []) const challengeTrackOptions = useMemo(() => ( challengeTracks .filter(track => track.isActive) .map(track => ({ label: track.name, - value: track.track, + value: normalizeTrackOptionValue(track), })) ), [challengeTracks]) + const fallbackChallengeTrack = useMemo( + () => challengeTrackOptions.find(option => option.value)?.value, + [challengeTrackOptions], + ) + const shouldNormalizeTrack = useRef(true) + const challengeTypeOptions = useMemo(() => ( challengeTypes .filter(type => type.isActive) @@ -135,49 +177,97 @@ const ScorecardInfoForm: FC = () => { })) ), [challengeTypes]) + const fallbackChallengeType = useMemo( + () => challengeTypeOptions.find(option => option.value)?.value, + [challengeTypeOptions], + ) + const shouldNormalizeType = useRef(true) + useEffect(() => { + if (!shouldNormalizeTrack.current) { + return + } + if (!challengeTrackOptions.length) { return } - const currentValue: string = form.getValues('challengeTrack') as string + const currentValue = normalizeValue(getValues('challengeTrack') as string) const isCurrentValid: boolean = !!currentValue - && challengeTrackOptions.some(option => option.value === currentValue) + && challengeTrackOptions.some(option => ( + normalizeValue(option.value) === currentValue + )) if (currentValue && !isCurrentValid) { - const normalizedValue = normalizeChallengeTrackValue(currentValue, challengeTracks) - if (normalizedValue) { - form.setValue('challengeTrack', normalizedValue, { + const normalizedValue = normalizeValue( + normalizeChallengeTrackValue(currentValue, challengeTracks), + ) + + if (normalizedValue && normalizedValue !== currentValue) { + setValue('challengeTrack', normalizedValue, { shouldDirty: false, shouldValidate: true, }) - return } + + return } - if (!isCurrentValid) { - form.setValue('challengeTrack', challengeTrackOptions[0].value, { - shouldDirty: false, - shouldValidate: true, - }) + if (!isCurrentValid && fallbackChallengeTrack) { + const normalizedFallback = normalizeValue(fallbackChallengeTrack) + + if (normalizedFallback && normalizedFallback !== currentValue) { + setValue('challengeTrack', normalizedFallback, { + shouldDirty: false, + shouldValidate: true, + }) + } } - }, [challengeTrackOptions, challengeTracks, form]) + + shouldNormalizeTrack.current = false + }, [ + challengeTrackOptions, + challengeTracks, + fallbackChallengeTrack, + getValues, + normalizeValue, + setValue, + ]) useEffect(() => { + if (!shouldNormalizeType.current) { + return + } + if (!challengeTypeOptions.length) { return } - const currentChallengeType: string = form.getValues('challengeType') as string + const currentChallengeType = normalizeValue(getValues('challengeType') as string) const partOfCategories: { label: string; value: string } | undefined - = challengeTypeOptions.find(item => item.value === currentChallengeType) - if ((!partOfCategories || !currentChallengeType) && challengeTypeOptions.length > 0) { - form.setValue('challengeType', challengeTypeOptions[0].value, { - shouldDirty: false, - shouldValidate: true, - }) + = challengeTypeOptions.find(item => ( + normalizeValue(item.value) === currentChallengeType + )) + + if ((!partOfCategories || !currentChallengeType) && fallbackChallengeType) { + const normalizedFallback = normalizeValue(fallbackChallengeType) + + if (normalizedFallback && normalizedFallback !== currentChallengeType) { + setValue('challengeType', normalizedFallback, { + shouldDirty: false, + shouldValidate: true, + }) + } } - }, [challengeTypeOptions, form]) + + shouldNormalizeType.current = false + }, [ + challengeTypeOptions, + fallbackChallengeType, + getValues, + normalizeValue, + setValue, + ]) return (