Skip to content

Commit 45f9452

Browse files
authored
Merge pull request #1287 from topcoder-platform/feat/v6
Feat/ Fix for page crashing when editing a scorecard
2 parents 2410959 + 147b220 commit 45f9452

File tree

3 files changed

+177
-59
lines changed

3 files changed

+177
-59
lines changed
Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,51 @@
1-
import { omit } from 'lodash'
2-
import { FC, SelectHTMLAttributes } from 'react'
1+
import { forwardRef, SelectHTMLAttributes } from 'react'
32
import classNames from 'classnames'
43

54
interface BasicSelectProps<T> extends SelectHTMLAttributes<T> {
65
options: { label: string; value: string|boolean|number }[];
6+
mapValue?: (value: any) => string;
77
placeholder?: string;
88
}
99

10-
const BasicSelect: FC<BasicSelectProps<any>> = props => (
11-
<select
12-
{...omit(props, 'options')}
13-
className={
14-
classNames(
15-
props.className,
16-
!props.value && `${props.value}` !== 'false' && 'empty',
17-
)
18-
}
19-
>
20-
<option
21-
disabled
22-
value=''
10+
const BasicSelect = forwardRef<HTMLSelectElement, BasicSelectProps<any>>((
11+
props,
12+
ref,
13+
) => {
14+
const { className, options, placeholder, value, mapValue: _mapValue, ...rest } = props
15+
16+
const normalizedValue = value === null || value === undefined ? '' : value
17+
18+
return (
19+
<select
20+
ref={ref}
21+
{...rest}
22+
className={
23+
classNames(
24+
className,
25+
!normalizedValue && `${normalizedValue}` !== 'false' && 'empty',
26+
)
27+
}
28+
value={normalizedValue as any}
2329
>
24-
{props.placeholder}
25-
</option>
26-
{props.options.map(option => (
2730
<option
28-
key={`${option.value}`}
29-
value={`${option.value}`}
31+
key='placeholder-option'
32+
disabled
33+
value=''
3034
>
31-
{option.label}
35+
{placeholder}
3236
</option>
33-
))}
34-
</select>
35-
)
37+
{options.map((option, index) => (
38+
<option
39+
key={`${option.value ?? option.label ?? index}-${index}`}
40+
value={`${option.value ?? ''}`}
41+
>
42+
{option.label}
43+
</option>
44+
))}
45+
</select>
46+
)
47+
})
48+
49+
BasicSelect.displayName = 'BasicSelect'
3650

3751
export default BasicSelect

src/apps/review/src/pages/scorecards/EditScorecardPage/components/CalculatedWeightsSum.tsx

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { FC } from 'react'
2-
import { useFormContext } from 'react-hook-form'
1+
import { FC, useMemo } from 'react'
2+
import { useFormContext, useWatch } from 'react-hook-form'
33
import classNames from 'classnames'
44

55
import styles from './CalculatedWeightsSum.module.scss'
@@ -13,11 +13,25 @@ interface CalculatedWeightsSumProps {
1313

1414
const CalculatedWeightsSum: FC<CalculatedWeightsSumProps> = props => {
1515
const form = useFormContext()
16-
const fields = form.watch(props.fieldName)
16+
const watchedFields = useWatch({
17+
control: form.control,
18+
defaultValue: [],
19+
name: props.fieldName,
20+
}) as { weight?: number | string | null }[] | undefined
1721

18-
const weightsSum = fields.reduce(
19-
(sum: number, field: { weight: string | number | undefined }) => (Number(field.weight) || 0) + sum,
20-
0,
22+
const fields = useMemo(
23+
() => (Array.isArray(watchedFields) ? watchedFields : []),
24+
[watchedFields],
25+
)
26+
27+
const weightsSum = useMemo(
28+
() => fields.reduce(
29+
(sum: number, field: { weight?: string | number | null }) => (
30+
Number(field?.weight ?? 0) + sum
31+
),
32+
0,
33+
),
34+
[fields],
2135
)
2236

2337
return (

src/apps/review/src/pages/scorecards/EditScorecardPage/components/ScorecardInfoForm.tsx

Lines changed: 119 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as yup from 'yup'
2-
import { FC, useEffect, useMemo } from 'react'
2+
import { FC, useCallback, useEffect, useMemo, useRef } from 'react'
33
import { useFormContext } from 'react-hook-form'
44
import classNames from 'classnames'
55

@@ -70,8 +70,30 @@ const toChallengeTrackLabel = (value: string): string => (
7070
)
7171

7272
const legacyChallengeTrackMap: Record<string, string> = {
73-
DEVELOPMENT: 'DEVELOP',
74-
QUALITY_ASSURANCE: 'QA',
73+
DEVELOPMENT: 'DEVELOPMENT',
74+
DEVELOP: 'DEVELOPMENT',
75+
QUALITY_ASSURANCE: 'QUALITY_ASSURANCE',
76+
QA: 'QUALITY_ASSURANCE',
77+
}
78+
79+
const normalizeTrackOptionValue = (track: useFetchChallengeTracksProps['challengeTracks'][number]): string => {
80+
if (track.track) {
81+
return track.track
82+
}
83+
84+
const normalizedName = track.name
85+
?.replace(/\s+/g, '_')
86+
.toUpperCase()
87+
88+
if (normalizedName && legacyChallengeTrackMap[normalizedName]) {
89+
return legacyChallengeTrackMap[normalizedName]
90+
}
91+
92+
if (track.name) {
93+
return normalizedName || track.name
94+
}
95+
96+
return track.id
7597
}
7698

7799
const normalizeChallengeTrackValue = (
@@ -82,21 +104,23 @@ const normalizeChallengeTrackValue = (
82104

83105
const directMatch = tracks.find(track => track.track === value && track.isActive)
84106
if (directMatch) {
85-
return directMatch.track
107+
return normalizeTrackOptionValue(directMatch)
86108
}
87109

88110
const mappedValue = legacyChallengeTrackMap[value]
89111
if (mappedValue) {
90-
const mappedMatch = tracks.find(track => track.track === mappedValue && track.isActive)
112+
const mappedMatch = tracks.find(track => (
113+
normalizeTrackOptionValue(track) === mappedValue && track.isActive
114+
))
91115
if (mappedMatch) {
92-
return mappedMatch.track
116+
return normalizeTrackOptionValue(mappedMatch)
93117
}
94118
}
95119

96120
const normalizedLabel = toChallengeTrackLabel(value)
97121
const nameMatch = tracks.find(track => track.name === normalizedLabel && track.isActive)
98122
if (nameMatch) {
99-
return nameMatch.track
123+
return normalizeTrackOptionValue(nameMatch)
100124
}
101125

102126
const uppercaseValue = value
@@ -109,23 +133,41 @@ const normalizeChallengeTrackValue = (
109133
&& track.isActive
110134
))
111135

112-
return fallbackMatch?.track
136+
return fallbackMatch ? normalizeTrackOptionValue(fallbackMatch) : undefined
113137
}
114138

115139
const ScorecardInfoForm: FC = () => {
116140
const form = useFormContext()
117141
const { challengeTracks }: useFetchChallengeTracksProps = useFetchChallengeTracks()
118142
const { challengeTypes }: useFetchChallengeTypesProps = useFetchChallengeTypes()
143+
const { getValues, setValue } = form
144+
const normalizeValue = useCallback((
145+
value: string | number | boolean | null | undefined,
146+
): string | undefined => {
147+
if (value === null || value === undefined) {
148+
return undefined
149+
}
150+
151+
const normalized = String(value).trim()
152+
153+
return normalized.length ? normalized : undefined
154+
}, [])
119155

120156
const challengeTrackOptions = useMemo(() => (
121157
challengeTracks
122158
.filter(track => track.isActive)
123159
.map(track => ({
124160
label: track.name,
125-
value: track.track,
161+
value: normalizeTrackOptionValue(track),
126162
}))
127163
), [challengeTracks])
128164

165+
const fallbackChallengeTrack = useMemo(
166+
() => challengeTrackOptions.find(option => option.value)?.value,
167+
[challengeTrackOptions],
168+
)
169+
const shouldNormalizeTrack = useRef(true)
170+
129171
const challengeTypeOptions = useMemo(() => (
130172
challengeTypes
131173
.filter(type => type.isActive)
@@ -135,49 +177,97 @@ const ScorecardInfoForm: FC = () => {
135177
}))
136178
), [challengeTypes])
137179

180+
const fallbackChallengeType = useMemo(
181+
() => challengeTypeOptions.find(option => option.value)?.value,
182+
[challengeTypeOptions],
183+
)
184+
const shouldNormalizeType = useRef(true)
185+
138186
useEffect(() => {
187+
if (!shouldNormalizeTrack.current) {
188+
return
189+
}
190+
139191
if (!challengeTrackOptions.length) {
140192
return
141193
}
142194

143-
const currentValue: string = form.getValues('challengeTrack') as string
195+
const currentValue = normalizeValue(getValues('challengeTrack') as string)
144196
const isCurrentValid: boolean = !!currentValue
145-
&& challengeTrackOptions.some(option => option.value === currentValue)
197+
&& challengeTrackOptions.some(option => (
198+
normalizeValue(option.value) === currentValue
199+
))
146200

147201
if (currentValue && !isCurrentValid) {
148-
const normalizedValue = normalizeChallengeTrackValue(currentValue, challengeTracks)
149-
if (normalizedValue) {
150-
form.setValue('challengeTrack', normalizedValue, {
202+
const normalizedValue = normalizeValue(
203+
normalizeChallengeTrackValue(currentValue, challengeTracks),
204+
)
205+
206+
if (normalizedValue && normalizedValue !== currentValue) {
207+
setValue('challengeTrack', normalizedValue, {
151208
shouldDirty: false,
152209
shouldValidate: true,
153210
})
154-
return
155211
}
212+
213+
return
156214
}
157215

158-
if (!isCurrentValid) {
159-
form.setValue('challengeTrack', challengeTrackOptions[0].value, {
160-
shouldDirty: false,
161-
shouldValidate: true,
162-
})
216+
if (!isCurrentValid && fallbackChallengeTrack) {
217+
const normalizedFallback = normalizeValue(fallbackChallengeTrack)
218+
219+
if (normalizedFallback && normalizedFallback !== currentValue) {
220+
setValue('challengeTrack', normalizedFallback, {
221+
shouldDirty: false,
222+
shouldValidate: true,
223+
})
224+
}
163225
}
164-
}, [challengeTrackOptions, challengeTracks, form])
226+
227+
shouldNormalizeTrack.current = false
228+
}, [
229+
challengeTrackOptions,
230+
challengeTracks,
231+
fallbackChallengeTrack,
232+
getValues,
233+
normalizeValue,
234+
setValue,
235+
])
165236

166237
useEffect(() => {
238+
if (!shouldNormalizeType.current) {
239+
return
240+
}
241+
167242
if (!challengeTypeOptions.length) {
168243
return
169244
}
170245

171-
const currentChallengeType: string = form.getValues('challengeType') as string
246+
const currentChallengeType = normalizeValue(getValues('challengeType') as string)
172247
const partOfCategories: { label: string; value: string } | undefined
173-
= challengeTypeOptions.find(item => item.value === currentChallengeType)
174-
if ((!partOfCategories || !currentChallengeType) && challengeTypeOptions.length > 0) {
175-
form.setValue('challengeType', challengeTypeOptions[0].value, {
176-
shouldDirty: false,
177-
shouldValidate: true,
178-
})
248+
= challengeTypeOptions.find(item => (
249+
normalizeValue(item.value) === currentChallengeType
250+
))
251+
252+
if ((!partOfCategories || !currentChallengeType) && fallbackChallengeType) {
253+
const normalizedFallback = normalizeValue(fallbackChallengeType)
254+
255+
if (normalizedFallback && normalizedFallback !== currentChallengeType) {
256+
setValue('challengeType', normalizedFallback, {
257+
shouldDirty: false,
258+
shouldValidate: true,
259+
})
260+
}
179261
}
180-
}, [challengeTypeOptions, form])
262+
263+
shouldNormalizeType.current = false
264+
}, [
265+
challengeTypeOptions,
266+
fallbackChallengeType,
267+
getValues,
268+
normalizeValue,
269+
setValue,
270+
])
181271

182272
return (
183273
<div className={classNames(styles.grayWrapper, styles.scorecardInfo)}>

0 commit comments

Comments
 (0)