Skip to content

Comments

New work app to replace work-manager, and v6 projects API usage in copilots app#1487

Open
jmgasper wants to merge 40 commits intodevfrom
work-manager
Open

New work app to replace work-manager, and v6 projects API usage in copilots app#1487
jmgasper wants to merge 40 commits intodevfrom
work-manager

Conversation

@jmgasper
Copy link
Collaborator

@jmgasper jmgasper commented Feb 20, 2026

@jmgasper jmgasper requested a review from kkartunov as a code owner February 20, 2026 03:14
@@ -0,0 +1,5 @@
REACT_APP_GROUPS_API_URL=https://api.topcoder.com/v6/groups
REACT_APP_TERMS_API_URL=https://api.topcoder.com/v5/terms

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
The API version for REACT_APP_TERMS_API_URL is v5, while others are v6. Ensure this is intentional and that the correct API version is being used.

height: 400,
menubar: false,
plugins: ['table', 'link', 'textcolor', 'contextmenu'],
plugins: ['table', 'link'],

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The removal of the textcolor plugin from the TinyMCE editor configuration might impact users who rely on text color customization. If this change is intentional, ensure that there are no requirements for text color editing. Otherwise, consider re-adding the plugin to maintain existing functionality.

height: 400,
menubar: false,
plugins: ['table', 'link', 'textcolor', 'contextmenu'],
plugins: ['table', 'link'],

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The removal of the contextmenu plugin could affect the user experience by disabling right-click context menus in the editor. Verify if this change aligns with the desired functionality and user requirements.

const viewRequestDetails = useMemo(() => (
routeParams.requestId && find(requests, { id: +routeParams.requestId }) as CopilotRequest
routeParams.requestId
? find(requests, request => `${request.id}` === routeParams.requestId) as CopilotRequest

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 performance]
The use of string interpolation to compare request.id with routeParams.requestId could be avoided if routeParams.requestId is already a string. Consider ensuring request.id is a string when stored or retrieved, or use a more explicit conversion method if necessary.

import { CopilotRequest } from '../models/CopilotRequest'

const baseUrl = `${EnvironmentConfig.API.V5}/projects`
const baseUrl = `${EnvironmentConfig.API.V6}/projects`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
Ensure that all endpoints and related logic are compatible with the new API version V6. This change might require additional updates elsewhere in the codebase to handle any differences in API responses or behavior.

createdAt: new Date(data.createdAt),
data: undefined,
opportunity: data.copilotOpportunity?.[0],
projectId: String(data?.projectId ?? data?.data?.projectId ?? ''),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The use of String(data?.projectId ?? data?.data?.projectId ?? '') could lead to unexpected results if projectId is 0 or false. Consider explicitly checking for undefined or null to avoid coercing falsy values.


export type ProjectsResponse = SWRResponse<Project[], Project[]>

const sleep = (ms: number): Promise<()=> void> => new Promise(resolve => { setTimeout(resolve, ms) })

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The sleep function returns a Promise<() => void>, which is misleading because the resolved value is actually undefined. Consider changing the return type to Promise<void> for clarity.

const sleep = (ms: number): Promise<()=> void> => new Promise(resolve => { setTimeout(resolve, ms) })
const normalizeProject = (project: Project): Project => ({
...project,
id: String(project.id),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The normalizeProject function converts project.id to a string. Ensure that this conversion is necessary and that all parts of the application expect id to be a string. If id is used as a number elsewhere, this could lead to inconsistencies.

export const PageWrapper: FC<PropsWithChildren<Props>> = props => (
<div className={classNames(styles.container, props.className)}>
<BreadCrumb list={props.breadCrumb} />
{props.breadCrumb.length > 0 && (

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The check props.breadCrumb.length > 0 is a good optimization to avoid rendering an empty BreadCrumb component. However, ensure that props.breadCrumb is always initialized as an array to prevent potential runtime errors.

{props.pageTitle}
</h3>
</PageHeader>
{props.titleAction

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 readability]
The conditional rendering of props.titleAction using a ternary operator is correct, but the use of undefined as the fallback is unnecessary. Consider using a logical AND (&&) operator for cleaner code: {props.titleAction && <div className={styles.blockTitleAction}>{props.titleAction}</div>}.


const WorkApp: FC = () => {
const { getChildRoutes }: RouterContextData = useContext(routerContext)
const childRoutes = useMemo(() => getChildRoutes(toolTitle), [getChildRoutes])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 performance]
Using useMemo here is unnecessary unless getChildRoutes is computationally expensive or has side effects. Consider removing useMemo if it's not needed for performance optimization.

const childRoutes = useMemo(() => getChildRoutes(toolTitle), [getChildRoutes])

useEffect(() => {
document.body.classList.add(WORK_APP_BODY_CLASS)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
Directly manipulating the document.body.classList can lead to unexpected side effects, especially if multiple components are modifying the class list. Consider using a state management solution or a library like classnames to manage body classes more predictably.

export const TABLE_DATE_FORMAT = 'MMM DD YYYY, HH:mm A'

export const PAGINATION_PER_PAGE_OPTIONS = [
{ label: '5', value: '5' },

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The value in PAGINATION_PER_PAGE_OPTIONS is a string, but it might be more appropriate to use a number type for consistency and to avoid potential type-related issues when using these values for pagination logic.

@@ -0,0 +1,28 @@
import { AppSubdomain, EnvironmentConfig } from '~/config'

export const rootRoute: string

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 maintainability]
Consider using a function to determine rootRoute instead of a ternary operator. This could improve readability and maintainability, especially if the logic becomes more complex in the future.

const defaultDate = new Date()
defaultDate.setHours(0, 0, 0, 0)

const daysUntilSaturday = (SATURDAY_DAY_INDEX - defaultDate.getDay() + 7) % 7

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
The calculation for daysUntilSaturday could be simplified by using (SATURDAY_DAY_INDEX - defaultDate.getDay() + 7) % 7. This ensures that the result is always non-negative, which is crucial for correctly setting the date.

const [remarks, setRemarks] = useState<string>('')
const [weekEndingDate, setWeekEndingDate] = useState<Date | null>(() => getDefaultWeekEndingDate())

const paymentTitle = useMemo(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The useMemo hook for paymentTitle depends on weekEndingDate, props.projectName, and props.engagementName. Ensure that these dependencies are correctly updated to prevent stale values from being used.

}, [])

useEffect(() => {
if (!props.open) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ design]
The useEffect hook resets the state whenever props.open changes. Consider whether this is the desired behavior, as it might reset the form unexpectedly if open is toggled.

customInput={<WeekEndingInput />}
dateFormat='MM/dd/yyyy'
disabled={isSubmitting}
filterDate={isSaturday}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 usability]
The filterDate function isSaturday ensures that only Saturdays can be selected. This is a good constraint, but ensure that users are aware of this restriction, perhaps through UI hints or documentation.

<ul className={styles.list}>
{paymentsResult.payments.map((payment, index) => {
const paymentStatus = getPaymentStatus(payment)
const normalizedPaymentStatus = paymentStatus

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
The trim() and toLowerCase() operations on paymentStatus could be moved into getPaymentStatus if this normalization is always required. This would improve maintainability by centralizing the logic.

import { xhrGetAsync } from '~/libs/core'

export interface BillingAccount {
active?: boolean

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The active property is optional, but its type is not specified. Consider specifying a boolean type for clarity and to avoid potential type-related issues.


export interface BillingAccount {
active?: boolean
endDate?: string

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The endDate property is optional and typed as a string. Consider specifying the expected date format or using a Date object to ensure consistent handling of date values.

endDate?: string
id: number | string
name: string
startDate?: string

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The startDate property is optional and typed as a string. Consider specifying the expected date format or using a Date object to ensure consistent handling of date values.

id: number | string
name: string
startDate?: string
status?: string

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
The status property is optional and typed as a string. Consider defining an enumeration for possible status values to improve type safety and maintainability.


await Promise.all(
projectIds.map(async projectId => {
if (projectNameByProjectId.has(projectId)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Consider checking if projectId is a valid string before using it in fetchProjectById. This could prevent unnecessary API calls with invalid IDs.

projectNameByProjectId.set(projectId, projectName)
}
} catch {
// Project lookup failures should not break engagement loading.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
Swallowing all errors in the catch block without logging or handling them might make debugging difficult. Consider logging the error or handling specific error cases.

}

const projectId = getEngagementProjectId(engagement)
const projectName = projectNameByProjectId.get(projectId)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Accessing projectNameByProjectId without checking if projectId is a valid string could lead to unexpected behavior. Ensure projectId is valid before using it as a key.

}
}

if (typedError?.response?.status !== 403) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The check for typedError?.response?.status !== 403 should be more explicit. Consider using typedError?.response?.status !== 403 to ensure the status is exactly 403, as other 4xx errors might also indicate authorization issues.

}

if (
normalizedBillingAccount.active === undefined

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 maintainability]
The condition in the if statement checks for multiple properties being undefined or falsy. Consider using a utility function to improve readability and maintainability.


return extractProjectTypes(response)
} catch (error) {
if (hasAuthorizationMetadataError(error)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The fallback mechanism for fetching project types when an authorization error occurs is a good approach. However, ensure that the fallback URL (PROJECT_METADATA_API_URL) is correct and that it provides the expected data structure.

size='sm'
/>
</Link>
{assignedStatus

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
The conditional rendering of the assignedStatus block is duplicated for both leftActions and rightActions. Consider refactoring this logic to avoid repetition and improve maintainability.


<PaymentFormModal
billingAccountId={projectResult.project?.billingAccountId}
engagementName={engagementResult.engagement?.title}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 correctness]
Ensure that engagementResult.engagement?.title is always defined before passing it to PaymentFormModal. If there's a chance it could be undefined, consider providing a default value or handling the case where it is not available.

onCancel={() => setPaymentMember(undefined)}
onConfirm={handlePaymentSubmit}
open={!!paymentMember}
projectName={projectResult.project?.name}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 correctness]
Ensure that projectResult.project?.name is always defined before passing it to PaymentFormModal. If there's a chance it could be undefined, consider providing a default value or handling the case where it is not available.

return (
<PageWrapper
backUrl={backUrl}
breadCrumb={[]}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
The breadCrumb prop is now set to an empty array, which might lead to a loss of navigation context for users. If the breadcrumb functionality is intended to be removed, ensure that this change is communicated to the team and any dependent components or tests are updated accordingly.

}
}

const currentBillingAccount = params.billingAccounts.find(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Consider using Array.prototype.find with a type guard or a more specific type assertion to ensure currentBillingAccount is of type BillingAccount. This will help prevent runtime errors if billingAccounts contains unexpected types.

const currentBillingAccount = params.billingAccounts.find(
billingAccount => String(billingAccount.id) === params.currentBillingAccountId,
)
const billingAccountWithDetails: ProjectBillingAccount | BillingAccount | undefined

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ design]
The variable billingAccountWithDetails is assigned using a fallback pattern. Ensure that params.projectBillingAccount and currentBillingAccount are mutually exclusive or handle cases where both might be defined to avoid unexpected behavior.

}

return {
endDate: getBillingAccountDate(billingAccountWithDetails, 'endDate'),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The function getBillingAccountDate is called with billingAccountWithDetails which can be either ProjectBillingAccount or BillingAccount. Ensure that both types have the expected properties (endDate and startDate) to avoid potential runtime errors.

return {
endDate: getBillingAccountDate(billingAccountWithDetails, 'endDate'),
id: params.currentBillingAccountId,
name: resolvedBillingAccountName || getBillingAccountName(billingAccountWithDetails),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 maintainability]
The resolvedBillingAccountName is computed using multiple fallbacks. Consider logging or handling cases where all fallbacks result in undefined to improve debugging and ensure data integrity.

const savedChallenge = await createChallenge({
name: formData.name,
projectId: createProjectId,
status: CHALLENGE_STATUS.NEW,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
The removal of roundType from the createChallenge payload may impact the challenge creation logic if roundType is required or used elsewhere in the application. Ensure that this change is intentional and that roundType is not needed for the challenge creation process.

}

downloadProfileAsync(application.handle)
.catch(() => undefined)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
Catching and ignoring all errors with .catch(() => undefined) can make debugging difficult. Consider logging the error or handling specific error cases.

</div>
<div>
<span className={styles.label}>Experience (years)</span>
<span className={styles.value}>{application.yearsOfExperience ?? '-'}</span>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Using the nullish coalescing operator (??) is appropriate here, but ensure that application.yearsOfExperience is not intended to be 0, as 0 will be replaced by '-'. If 0 is a valid value, consider using a different check.

const parsedDuration = Number(duration)
const maxDuration = PHASE_DURATION_MAX_HOURS * 60

if (!Number.isFinite(parsedDuration)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The normalizeDuration function uses Number.isFinite to check if parsedDuration is a valid number. However, Number.isFinite will return false for NaN, Infinity, and -Infinity. If the intention is to only check for NaN, consider using Number.isNaN instead, or clarify the behavior if Infinity values are expected.

const predecessorEndDate = toDate(predecessorPhase?.scheduledEndDate)

if (predecessorEndDate) {
phaseStartDate = isIterativeReviewPhase(phase.name)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
In the recalculatePhases function, when determining phaseStartDate for an iterative review phase, the logic defaults to predecessorStartDate || predecessorEndDate. This could lead to unexpected behavior if predecessorStartDate is undefined but predecessorEndDate is valid. Consider verifying if this logic aligns with the intended scheduling behavior.

? predecessorStartDate || predecessorEndDate
: predecessorEndDate
} else if (shouldScheduleDates && !calculationError) {
const phaseName = phase.name || phase.phaseId || `${index + 1}`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
The error message for an invalid predecessor configuration is set but not immediately returned or logged. Ensure that the error is handled appropriately, either by returning it immediately or by logging it for debugging purposes.

domain: AppSubdomain.work,
element: <WorkApp />,
id: toolTitle,
rolesRequired: WORK_MANAGER_ALLOWED_ROLES,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ security]
Ensure that WORK_MANAGER_ALLOWED_ROLES is correctly defined and includes all necessary roles for accessing this route. If this list is incomplete or incorrect, it could lead to unauthorized access or prevent authorized users from accessing the route.

expect(getChallengeLinkId({
id: '',
legacyId: 654_321,
}))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Consider using String(legacyId) to ensure consistent string conversion, as toBe('654321') expects a string.

id: '',
legacy: { id: 123_456 },
}))
.toBe('123456')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Consider using String(legacy.id) to ensure consistent string conversion, as toBe('123456') expects a string.

&& props.challenge.projectId !== undefined
const hasLegacyId
= 'legacyId' in props.challenge && props.challenge.legacyId !== undefined
= typeof props.challenge.projectId === 'number'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 readability]
The check typeof props.challenge.projectId === 'number' is redundant if props.challenge.projectId > 0 is already being checked, as the greater-than comparison will implicitly ensure it's a number. Consider removing the typeof check for simplicity.

`${EnvironmentConfig.ADMIN.REVIEW_UI_URL}/=${props.challenge.id}` /* eslint-disable-line max-len */
}
href={getReviewUiChallengeUrl(
EnvironmentConfig.ADMIN.REVIEW_UI_URL,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Passing an empty string as a fallback for challengeLinkId in getReviewUiChallengeUrl could lead to unexpected behavior if challengeLinkId is null or undefined. Ensure that getChallengeLinkId always returns a valid string or handle the fallback case more explicitly.

* Returns whether the Review UI link can be opened for a challenge.
*/
export function canOpenReviewUi(challengeId?: string): boolean {
return Boolean(challengeId?.trim())

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 performance]
The use of challengeId?.trim() in Boolean(challengeId?.trim()) is redundant because Boolean() will already handle null or undefined values. Consider simplifying to Boolean(challengeId && challengeId.trim()) to ensure trim() is only called when challengeId is not null or undefined.

COMPLETED_REVIEW_STATUSES.has((value ?? '').toUpperCase())
)

const resolveScreeningReviewDetails = (entry: Screening): ScreeningReviewDetail[] => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The function resolveScreeningReviewDetails returns an array of ScreeningReviewDetail objects. However, if entry.screeningReviews is not defined, it constructs a new array with a single object. This could lead to inconsistent behavior if screeningReviews is expected to always be an array. Consider ensuring screeningReviews is always an array to simplify logic and avoid potential issues.

hasMultipleScreeners,
maxScreenerCount,
}: ScreeningColumnConfig): TableColumn<Screening>[] => {
const screenerColumnCount = Math.max(1, maxScreenerCount)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The screenerColumnCount is calculated using Math.max(1, maxScreenerCount). If maxScreenerCount is less than 1, this will default to 1, which might not be the intended behavior. Ensure that maxScreenerCount is validated or calculated correctly to avoid unexpected UI behavior.

return <span>--</span>
}

const maskScore = shouldMaskScore(data, detail)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
The shouldMaskScore function is called with both data and detail arguments, but the logic inside shouldMaskScore may not account for detail being undefined. Ensure that shouldMaskScore handles cases where detail is not provided to avoid runtime errors.

label: 'Screening Score',
propertyName: 'screening-aggregate-score',
renderer: (data: Screening) => {
if (hasIncompleteScreeningReviews(data)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 readability]
In the renderer function for the 'Screening Score' column, the condition hasIncompleteScreeningReviews(data) is checked. If this returns true, a placeholder -- is shown. Consider providing more context to the user about why the score is not available, as this could improve user experience.


const isSubmissionNotViewable = (submission: Screening): boolean => (
!canViewSubmissions && String(submission.memberId) !== String(loginUserInfo?.userId)
const isSubmissionNotViewable = useCallback(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 performance]
The isSubmissionNotViewable function is wrapped in useCallback, but it does not seem to have any dependencies that would change. Consider removing useCallback if memoization is not necessary, as it adds unnecessary complexity.

? candidateReviews
: (matchedReview ? [matchedReview] : [])

const screeningReviewsWithScore = reviewEntries.map(reviewEntry => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
The logic for determining reviewNumericScore is duplicated from the removed lines. Consider refactoring this logic into a separate function to improve maintainability and reduce code duplication.

const hasPassingResult = screeningReviews.some(reviewDetail => (
(reviewDetail.result ?? '').toUpperCase() === 'PASS'
))
const aggregatedScoreFallback = hasMultipleScreeners

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
The aggregatedScoreFallback logic is repeated multiple times in different forms. Consider extracting this logic into a helper function to improve maintainability and readability.

? '--'
: scoreToDisplay(averageNumericScore, aggregatedScoreFallback)

let aggregatedResult: Screening['result'] = base.result

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ readability]
The logic for determining aggregatedResult is complex and involves multiple conditions. Consider simplifying or breaking it down into smaller functions to enhance readability and maintainability.

reviewStatus?: string
screenerId?: string
screener?: BackendResource
score: string

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The score field is defined as a string, but it might be more appropriate to use a number type if it represents a numerical score. This would prevent potential issues with numerical operations and comparisons.

updated?: string
createdBy?: string
copilot?: string
reviewer?: string

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ design]
The new reviewer field is a string, but there is already a reviewers field of type Reviewer[]. Ensure that the reviewer field is not redundant or conflicting with the reviewers array. Consider whether this should be a single Reviewer object or if it should be integrated with the existing reviewers array.

prizeSets?: PrizeSet[]
privateDescription?: string
reviewers?: Reviewer[]
reviewer?: string

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
The addition of the reviewer field alongside reviewers could lead to confusion. Consider clarifying the distinction between these fields or consolidating them if they serve similar purposes.

tags: string[]
terms?: string[]
legacy?: {
isTask?: boolean

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
The isTask field is added to the legacy object. Ensure that this field is necessary and that its usage is well-documented elsewhere in the codebase to avoid confusion, as legacy fields can often lead to technical debt.

challengeBasicInfoSchema,
} from './challenge-editor.schema'

jest.mock('~/config', () => ({

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
Using jest.mock with a Proxy can lead to unexpected behavior if the get trap is not carefully managed. Consider explicitly handling all expected properties to avoid potential issues with unexpected properties returning the default URL.

prizes?: unknown[]
} | undefined

return Array.isArray(placementPrizeSet?.prizes)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 readability]
The condition (placementPrizeSet?.prizes?.length || 0) > 0 could be simplified to placementPrizeSet?.prizes?.length > 0 for better readability.

isTask: boolean | undefined,
reviewType: string | undefined,
): boolean => (
isTask === true

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 readability]
The condition String(reviewType || '').trim().toUpperCase() === REVIEW_TYPES.INTERNAL could be simplified to reviewType?.trim().toUpperCase() === REVIEW_TYPES.INTERNAL for better readability and to avoid unnecessary string operations.

const status = normalizeOptionalString(formData.status)
?.toUpperCase()
const billingAccountId = normalizeOptionalId(formData.billing?.billingAccountId)
const prizeSets = formData.funChallenge === true

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The logic for setting prizeSets based on formData.funChallenge could lead to unexpected behavior if formData.funChallenge is not explicitly set to true or false. Consider explicitly checking for true to avoid potential issues with falsy values.

])

useEffect(() => {
if (isChallengeCreated || isMarathonMatchChallengeSelected || values.funChallenge !== true) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 maintainability]
The condition values.funChallenge !== true is checked twice in the useEffect dependencies and the if statement. Consider simplifying the logic to avoid redundancy.

<ChallengeNameField />
<ChallengeTrackField disabled={isChallengeCreated} />
<ChallengeTypeField disabled={isChallengeCreated} />
{showFunChallengeField

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 style]
The conditional rendering of FunChallengeField using undefined can be simplified by using null instead, which is more idiomatic in React for rendering nothing.

</div>
</section>

{showPrizesAndBillingSection

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 style]
The conditional rendering of the Prizes & Billing section using undefined can be simplified by using null instead, which is more idiomatic in React for rendering nothing.

? <AssignedMemberField />
: undefined}
<CopilotField projectId={fallbackProjectId} />
{isTaskChallengeSelected

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 style]
The conditional rendering of ReviewTypeField using undefined can be simplified by using null instead, which is more idiomatic in React for rendering nothing.

)
const isReviewerSelectDisabled = useMemo(
(): boolean => {
if (!props.projectId || areProjectMembersLoading) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The condition !props.projectId || areProjectMembersLoading is used to disable the reviewer select field. Consider adding a check for projectMembers.length === 0 to ensure the field is disabled when there are no members, even if loading is complete.

)

useEffect(() => {
if (!props.isTaskChallenge) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The useEffect hook that sets the review type to INTERNAL for task challenges does not handle the case where setValue might not be available or might fail. Consider adding error handling to ensure robustness.

}

return (
<div className={styles.container}>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 design]
The FormRadioGroup is disabled unconditionally. If this is intentional, ensure that it aligns with the intended UX. Otherwise, consider making this behavior conditional based on props or state.

return false
}

const normalizedStatus = toOptionalString(invite.status)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The toOptionalString function is used to normalize the invite.status before checking its presence in OPEN_INVITE_STATUSES. Ensure that toOptionalString correctly handles all possible input types for invite.status to avoid unexpected behavior.

return `At least one member reviewer with a scorecard is required for phase "${uncoveredPhase.name}".`
}

interface ReviewerValidationOptions {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
The renaming of ReviewerLaunchValidationOptions to ReviewerValidationOptions is appropriate given the removal of the launch-specific logic. However, ensure that all references to this interface outside of this file are updated accordingly to prevent any potential type errors.

name: options.challengeTypeName,
})

if (!isMarathonMatch && reviewers.length === 0 && requiredPhases.length > 0) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Replacing the hardcoded error message with options.requiredReviewersErrorMessage improves flexibility. Ensure that this message is appropriately set in all contexts where getReviewerValidationError is called to avoid missing error messages.

}: SaveStatusMetadata = getSaveStatusMetadata(formData.status, {})

if (isSaveAsDraft) {
const reviewerValidationError = getReviewerValidationError(formData, {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 maintainability]
The conditional check for isSaveAsDraft and subsequent validation logic is correct. However, consider extracting this logic into a separate function to improve readability and maintainability, especially if similar validation is needed elsewhere.


import { aggregateSubmissionReviews } from './aggregateSubmissionReviews'

jest.mock('~/libs/core', () => ({

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The mock for getRatingColor returns a hardcoded color #2a2a2a. If the color is expected to vary based on input, consider adding test cases to verify different scenarios.

]

const rows = aggregateSubmissionReviews({
mappingReviewAppeal: {} as MappingReviewAppeal,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The mappingReviewAppeal is cast to MappingReviewAppeal with an empty object. Ensure that this casting does not hide potential issues with the expected structure of MappingReviewAppeal. Consider initializing it with a more realistic mock or verifying its structure in the test.

.toHaveLength(2)

const firstRowScores = new Map(
rows[0].reviews.map(review => [review.reviewerHandle, review.finalScore]),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The use of Map to store scores by reviewerHandle is appropriate here, but ensure that reviewerHandle is always unique within the context of rows[0].reviews. If there is any chance of duplication, consider handling it explicitly.


const normalizedHandle = normalizeStringValue(reviewerHandle)
if (normalizedHandle) {
return `handle:${normalizedHandle.toLowerCase()}`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The use of toLowerCase() on normalizedHandle assumes that all reviewer handles should be case-insensitive. Ensure this aligns with the business logic, as it might lead to unexpected behavior if handles are case-sensitive.

// Prefer the explicit reviewers list; otherwise fall back to discovered resourceIds.
const orderedResourceIds: string[] = (() => {
const compareReviewerIds = (a: string, b: string): number => (
(reviewerHandleByResourceId[a] || a)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The compareReviewerIds function uses localeCompare with sensitivity: 'base', which ignores case differences. Confirm that this is the intended behavior, as it may affect the ordering of reviewer IDs if case sensitivity is required.

})
if (reviewerKey) {
r.reviewerKey = reviewerKey
if (!byReviewerKey[reviewerKey]) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ design]
The logic for assigning reviewerKey to byReviewerKey assumes that the first occurrence of a reviewer key is the correct one to store. Consider if there are scenarios where multiple reviews could have the same reviewerKey but different details, and if so, how those should be handled.

perPage: String(userIdChunk.length),
})
userIdChunk.forEach(userId => {
query.append('userIds[]', userId)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ performance]
Appending userIds[] in a loop can lead to a large query string if userIdChunk is large. Consider checking the maximum URL length supported by the server to avoid potential issues.

perPage: String(chunkUserIds.length),
})
chunkUserIds.forEach(userId => {
searchQuery.append('userIds[]', userId)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The use of searchQuery.append('userIds[]', userId) is correct, but ensure that the backend API correctly interprets this format for array parameters. If the API expects a different format, this could lead to unexpected results.

)
const isChallengeCreated = !!currentChallengeId
const isFunChallengeSelected = values.funChallenge === true
const showFunChallengeField = isMarathonMatchChallengeSelected

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The condition for showFunChallengeField has been changed to always show the field for Marathon Match challenges, even if the challenge is already created. Ensure this change is intentional, as it alters the previous logic where the field was not shown for created challenges.

}

const savedChallenge = await createChallenge({
funChallenge: formData.funChallenge === true,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ design]
Ensure that the funChallenge property is correctly handled by the createChallenge function. If this is a new addition, verify that the backend supports this field and that it is necessary for the challenge creation process.

)

const hasChallengeDetailsAccess = canOpenReviewUi(props.challenge.id)
const hasProjectId

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ performance]
The function canOpenReviewUi is called twice with the same argument props.challenge.id on lines 187 and 192. Consider storing the result in a variable to avoid redundant function calls and improve performance.

resourceId,
reviewerHandle,
}: ReviewerIdentityArgs): string | undefined => {
const normalizedHandle = normalizeStringValue(reviewerHandle)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The order of normalization checks has been changed, prioritizing reviewerHandle over memberId. Ensure this change aligns with the intended logic, as it may affect how reviewer identities are resolved.

return false
}

if (r.reviewerKey && orderedReviewerKeys.has(r.reviewerKey)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 maintainability]
The condition r.reviewerKey && orderedReviewerKeys.has(r.reviewerKey) is redundant because orderedReviewerKeys is derived from ordered, which already includes all reviewerKey values from ordered. Consider removing this condition to simplify the logic.

}

if (typeof value === 'number' && Number.isFinite(value)) {
return String(value)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Converting a number to a string without trimming could lead to unexpected results if the number is very large or has decimal places. Consider if this behavior is intended.

]
.slice(0, reviewerSlots)
.map(memberId => toNormalizedText(memberId))
const missingSlotIndex = normalizedAssignedMemberSlots.findIndex(memberId => !memberId)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Using findIndex to check for missing slots is efficient, but ensure that toNormalizedText correctly handles all potential falsy values that could be present in normalizedAssignedMemberSlots.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant