Skip to content

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

Open
jmgasper wants to merge 63 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 63 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.

phaseId: string | undefined,
phaseNameById: Map<string, string>,
): Scorecard[] {
const reviewerPhaseId = normalizeText(phaseId)

Choose a reason for hiding this comment

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

[💡 performance]
The normalizeText function is used to normalize phaseId, but it might return an empty string if phaseId is undefined. Consider handling the case where phaseId is undefined before calling normalizeText to avoid unnecessary operations.

const reviewerPhaseName = reviewerPhaseId
? phaseNameById.get(reviewerPhaseId)
: undefined
const normalizedReviewerPhase = normalizePhaseToken(reviewerPhaseName)

Choose a reason for hiding this comment

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

[⚠️ correctness]
The normalizePhaseToken function is called with reviewerPhaseName, which can be undefined. Ensure that normalizePhaseToken can handle undefined inputs gracefully to avoid potential runtime errors.

&& scorecardPhaseId
&& scorecardPhaseId === reviewerPhaseId
const matchesPhaseType = normalizedReviewerPhase
&& normalizedScorecardType

Choose a reason for hiding this comment

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

[💡 readability]
The condition normalizedReviewerPhase && normalizedScorecardType && normalizedScorecardType === normalizedReviewerPhase could be simplified by using a single comparison, as both variables are already normalized strings.

const normalizedNextPhaseId = normalizeText(nextPhaseId)
if (
reviewer.isMemberReview === false
|| !normalizedNextPhaseId

Choose a reason for hiding this comment

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

[💡 maintainability]
The condition reviewer.isMemberReview === false is checked multiple times in the function. Consider extracting this condition into a variable for better readability and maintainability.

const hasDefaultScorecard = defaultScorecardId
? matchingScorecards.some(scorecard => normalizeText(scorecard.id) === defaultScorecardId)
: false
const fallbackScorecardId = hasDefaultScorecard

Choose a reason for hiding this comment

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

[⚠️ correctness]
Accessing matchingScorecards[0]?.id without checking if matchingScorecards is empty could lead to unexpected behavior. Consider adding a check to ensure matchingScorecards is not empty before accessing its elements.


export interface CopilotApplication {
id: number,
id?: number | string,

Choose a reason for hiding this comment

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

[❗❗ correctness]
Changing the type of id from number to number | string introduces potential issues with type consistency. Ensure that all parts of the application that use id can handle both types, or consider maintaining a single type for consistency.

notes?: string,
createdAt: Date,
opportunityId: string,
opportunityId?: string,

Choose a reason for hiding this comment

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

[⚠️ correctness]
Making opportunityId optional may lead to scenarios where this field is missing. Ensure that the application logic correctly handles cases where opportunityId is undefined.

handle?: string,
userId: number,
userHandle?: string,
userId: number | string,

Choose a reason for hiding this comment

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

[❗❗ correctness]
Changing the type of userId from number to number | string could lead to type inconsistencies. Verify that all usages of userId can handle both types, or consider maintaining a single type for consistency.

const { data: copilotApplications }: { data?: CopilotApplication[] } = useCopilotApplications(opportunityId)
const appliedCopilotApplications = useMemo(
() => copilotApplications?.filter(item => item.userId === profile?.userId),
() => copilotApplications?.filter(

Choose a reason for hiding this comment

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

[⚠️ correctness]
The conversion of userId and profile?.userId to strings before comparison ensures type consistency, which is good. However, ensure that both userId and profile?.userId are not null or undefined before this conversion to avoid unexpected results.


await assignCopilotOpportunity(opportunityId, copilotApplication.id)
const applicationId = Number(copilotApplication.id)
if (!Number.isFinite(applicationId)) {

Choose a reason for hiding this comment

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

[⚠️ correctness]
Consider using Number.isInteger() instead of Number.isFinite() to validate applicationId as an integer, since IDs are typically expected to be integers.

const getData = (): CopilotApplication[] => (props.copilotApplications ? props.copilotApplications.map(item => {
const member = props.members && props.members.find(each => each.userId === item.userId)
const member = props.members && props.members.find(
each => String(each.userId) === String(item.userId),

Choose a reason for hiding this comment

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

[⚠️ correctness]
Converting userId to a string for comparison may mask potential type issues. Ensure that userId is consistently typed as a string throughout the application to avoid unexpected behavior.

activeProjects: member?.activeProjects || 0,
fulfilment: member?.copilotFulfillment || 0,
handle: member?.handle,
handle: member?.handle || item.userHandle || item.handle,

Choose a reason for hiding this comment

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

[⚠️ correctness]
The fallback logic for handle could lead to unexpected results if item.userHandle or item.handle are not defined or incorrect. Consider validating these fields before using them as fallbacks.

* @returns {MembersResponse} - The response containing the list of members data
*/
export const useMembers = (userIds: number[]): MembersResponse => {
export const useMembers = (userIds: Array<number | string>): MembersResponse => {

Choose a reason for hiding this comment

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

[⚠️ correctness]
The change to allow userIds to be an array of number | string can lead to inconsistent behavior if not handled properly. Consider converting all userId values to strings before appending them to the query string to ensure uniformity and prevent potential issues with type coercion.

return extractProjectTypes(response)
} catch (projectTypesError) {
if (hasAuthorizationMetadataError(projectTypesError)) {
throw normalizeError(metadataError, 'Failed to fetch project types')
Copy link

Choose a reason for hiding this comment

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

[⚠️ maintainability]
The error handling logic here might lead to confusion. If hasAuthorizationMetadataError returns true, the original metadataError is thrown, which could be misleading since the fallback attempt also failed. Consider throwing a more informative error that includes details from both errors.

const isProjectActive = String(projectResult.project?.status || '')
.trim()
.toLowerCase() === PROJECT_STATUS.ACTIVE
const isCreateActionDisabled = !!projectIdFromRoute && !isProjectActive
Copy link

Choose a reason for hiding this comment

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

[⚠️ correctness]
The isCreateActionDisabled logic relies on projectIdFromRoute being truthy and isProjectActive being false. Consider explicitly checking for projectIdFromRoute to avoid potential issues if projectIdFromRoute is an empty string or a falsy value that should be treated differently.

: 'Challenges'
const isProjectActive = String(projectResult.project?.status || '')
.trim()
.toLowerCase() === PROJECT_STATUS.ACTIVE
Copy link

Choose a reason for hiding this comment

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

[⚠️ correctness]
The comparison to determine if a project is active uses PROJECT_STATUS.ACTIVE. Ensure that PROJECT_STATUS.ACTIVE is defined and matches the expected format (lowercase) to avoid potential mismatches.

const isProjectActive = String(projectResult.project?.status || '')
.trim()
.toLowerCase() === PROJECT_STATUS.ACTIVE
const isCreateActionDisabled = !!projectId && !isProjectActive
Copy link

Choose a reason for hiding this comment

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

[⚠️ correctness]
The isProjectActive variable is determined by comparing the project status to PROJECT_STATUS.ACTIVE. Ensure that PROJECT_STATUS.ACTIVE is defined and used consistently across the application to avoid potential mismatches or typos.

const isProjectActive = String(projectResult.project?.status || '')
.trim()
.toLowerCase() === PROJECT_STATUS.ACTIVE
const isCreateActionDisabled = !!projectId && !isProjectActive
Copy link

Choose a reason for hiding this comment

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

[❗❗ correctness]
The logic for determining isCreateActionDisabled relies on projectId and isProjectActive. Consider adding a check to ensure projectResult.project is defined before accessing its properties to prevent potential runtime errors.

}

const payload: UpdateProjectPayload = {
billingAccountId: normalizedBillingAccountId || '',
Copy link

Choose a reason for hiding this comment

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

[⚠️ correctness]
Using normalizedBillingAccountId || '' could lead to unexpected behavior if normalizedBillingAccountId is undefined. Consider ensuring that normalizedBillingAccountId is always a valid string before this assignment.

.object({
billingAccountId: yup
.string()
.required('Billing account is required'),
Copy 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 conditional requirement for billingAccountId based on isEdit means that this field is now always required. Ensure that this change aligns with the intended business logic, as it could impact scenarios where a billing account is not needed during editing.

const titleAction = projectIdFromRoute
? (
<div className={styles.projectTitleActions}>
{projectResult.project?.status
Copy link

Choose a reason for hiding this comment

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

[💡 readability]
Consider using a more explicit conditional rendering approach for ProjectStatus to improve readability. The current ternary operator with undefined as a fallback can be less clear to readers.

: 'Billing Account'}
name='billingAccountId'
placeholder='Search billing account by name'
required
Copy link

Choose a reason for hiding this comment

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

[❗❗ correctness]
Changing required={!props.isEdit} to required unconditionally makes the billing account field always required, even in edit mode. Ensure this change aligns with the intended behavior of the form, as it alters the form's validation logic.

export interface AiReviewConfig {
autoFinalize: boolean
challengeId: string
createdAt?: string | Date
Copy link

Choose a reason for hiding this comment

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

[⚠️ maintainability]
Consider using a consistent type for createdAt and updatedAt fields across interfaces. Currently, they are typed as string | Date, which can lead to inconsistencies in how dates are handled. It's generally better to use Date to ensure consistent date handling and avoid potential parsing issues.

autoFinalize: boolean
challengeId: string
createdAt?: string | Date
decisions?: Record<string, unknown>[]
Copy link

Choose a reason for hiding this comment

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

[⚠️ maintainability]
The decisions field is typed as Record<string, unknown>[], which might be too generic. If possible, specify a more detailed type to improve type safety and maintainability.

challengeId: string
createdAt?: string | Date
decisions?: Record<string, unknown>[]
formula?: Record<string, unknown>
Copy link

Choose a reason for hiding this comment

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

[⚠️ maintainability]
The formula field is typed as Record<string, unknown>, which is quite generic. Consider defining a more specific type for formula to enhance type safety and maintainability.

autoFinalize: boolean
challengeTrack: string
challengeType: string
createdAt?: string | Date
Copy link

Choose a reason for hiding this comment

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

[⚠️ maintainability]
Similar to AiReviewConfig, consider using a consistent type for createdAt and updatedAt fields. Using Date consistently can help avoid potential issues with date parsing and manipulation.

AiReviewMode,
} from '../models'

const AI_REVIEW_CONFIGS_API_URL = `${EnvironmentConfig.API.V6}/ai-review/configs`
Copy link

Choose a reason for hiding this comment

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

[⚠️ correctness]
Consider validating EnvironmentConfig.API.V6 to ensure it is a valid URL string before using it to construct AI_REVIEW_CONFIGS_API_URL. This can prevent potential runtime errors if the configuration is incorrect.

return normalizedValue || undefined
}

function normalizeNumber(value: unknown): number | undefined {
Copy link

Choose a reason for hiding this comment

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

[❗❗ correctness]
The normalizeNumber function should handle cases where value is a non-numeric string that results in NaN when converted to a number. Consider adding a check for NaN to return undefined in such cases.

challengeId: string,
): Promise<AiReviewConfig | undefined> {
try {
const response = await xhrGetAsync<unknown>(
Copy link

Choose a reason for hiding this comment

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

[⚠️ correctness]
Ensure that xhrGetAsync handles network errors and unexpected response formats gracefully. Consider adding error handling for network-related issues to improve robustness.

input: SaveAiReviewConfigInput,
): Promise<AiReviewConfig> {
try {
const response = await xhrPostAsync<SaveAiReviewConfigInput, unknown>(
Copy link

Choose a reason for hiding this comment

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

[⚠️ correctness]
Ensure that xhrPostAsync handles server errors and unexpected response formats gracefully. Consider adding error handling for server-related issues to improve robustness.

input: SaveAiReviewConfigInput,
): Promise<AiReviewConfig> {
try {
const response = await xhrPutAsync<SaveAiReviewConfigInput, unknown>(
Copy link

Choose a reason for hiding this comment

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

[⚠️ correctness]
Ensure that xhrPutAsync handles server errors and unexpected response formats gracefully. Consider adding error handling for server-related issues to improve robustness.

configId: string,
): Promise<void> {
try {
await xhrDeleteAsync<unknown>(
Copy link

Choose a reason for hiding this comment

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

[⚠️ correctness]
Ensure that xhrDeleteAsync handles server errors and unexpected response formats gracefully. Consider adding error handling for server-related issues to improve robustness.

return normalizedValue || undefined
}

function normalizeNumber(value: unknown): number | undefined {
Copy link

Choose a reason for hiding this comment

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

[⚠️ correctness]
The normalizeNumber function currently returns undefined for empty strings. Consider handling cases where the input is a string representation of a number (e.g., '42') to ensure it is parsed correctly.

return undefined
}

function normalizeError(error: unknown, fallbackMessage: string): Error {
Copy link

Choose a reason for hiding this comment

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

[⚠️ correctness]
The normalizeError function assumes a specific error structure. Consider adding a check to ensure error is an object before accessing its properties to avoid potential runtime errors.

)
}

function normalizeWorkflow(
Copy link

Choose a reason for hiding this comment

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

[⚠️ correctness]
The normalizeWorkflow function does not handle cases where workflowId or weightPercent are invalid or missing. Consider adding validation to ensure these fields are present and valid before proceeding.

}
}

function normalizeTemplate(
Copy link

Choose a reason for hiding this comment

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

[⚠️ correctness]
The normalizeTemplate function does not handle cases where id, title, challengeTrack, challengeType, mode, or minPassingThreshold are invalid or missing. Consider adding validation to ensure these fields are present and valid before proceeding.

}
}

function normalizeTemplatesResponse(response: unknown): AiReviewTemplate[] {
Copy link

Choose a reason for hiding this comment

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

[⚠️ correctness]
In normalizeTemplatesResponse, consider adding a default case to handle unexpected response structures to improve robustness.

color: #b04337;
}

@media (max-width: 767px) {
Copy link

Choose a reason for hiding this comment

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

[💡 design]
Consider using a more specific media query breakpoint for better responsiveness. The current breakpoint of 767px might not cover all device sizes effectively, especially for tablets. Evaluate if a more precise breakpoint could enhance the layout on various devices.

}

saveTimerRef.current = setTimeout(() => {
setIsSaving(true)
Copy link

Choose a reason for hiding this comment

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

[⚠️ maintainability]
The setTimeout function is used here for debouncing the save operation. Consider using a custom hook for debouncing to improve code reusability and readability.

validationErrors,
])

if (isConfigLoading || isWorkflowsLoading) {
Copy link

Choose a reason for hiding this comment

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

[💡 maintainability]
The component returns loading and error states directly in the render method. Consider extracting these into separate components or hooks to improve readability and maintainability.

>
<div>
Total workflow weight:
{totalWorkflowWeight.toFixed(2)}
Copy link

Choose a reason for hiding this comment

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

[💡 readability]
The total workflow weight is displayed with two decimal places. Consider rounding to the nearest integer if precision is not necessary, to improve readability.

setIsScorecardsLoading(true)
setLoadError(undefined)

const scorecardFilters = {
Copy link

Choose a reason for hiding this comment

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

[⚠️ performance]
Consider adding a debounce or throttle mechanism to the fetchScorecards call to prevent excessive API requests when selectedScorecardTrack or selectedScorecardType changes rapidly. This can help improve performance and reduce server load.

}
}, [selectedScorecardTrack, selectedScorecardType])

useEffect(() => {
Copy link

Choose a reason for hiding this comment

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

[💡 maintainability]
The useEffect for fetching default reviewers does not handle errors in a way that informs the user. Consider setting a load error state or providing feedback to the user if fetching default reviewers fails.

}
}, [trackId, typeId])

useEffect(() => {
Copy link

Choose a reason for hiding this comment

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

[⚠️ correctness]
The useEffect for assigning member IDs to reviewers does not handle cases where getReviewerFieldIndex returns undefined. Consider adding error handling or logging to capture these cases for debugging purposes.

normalizedChallengeId,
])

useEffect(() => {
Copy link

Choose a reason for hiding this comment

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

[⚠️ maintainability]
The useEffect for managing additional member IDs does not handle errors when updating reviewer assignments. Consider adding error handling to provide feedback to the user or log errors for debugging.

],
)

const handleMemberSelectionChange = useCallback(
Copy link

Choose a reason for hiding this comment

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

[❗❗ correctness]
The handleMemberSelectionChange function uses an async function syncAssignedMember but does not await its execution. Consider using await to ensure that the promise is handled correctly and any errors are caught.

currentReviewers: Reviewer[] | undefined,
nextReviewers: Reviewer[],
): boolean {
return JSON.stringify(currentReviewers || []) !== JSON.stringify(nextReviewers)
Copy link

Choose a reason for hiding this comment

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

[⚠️ performance]
Using JSON.stringify for deep comparison of objects can be inefficient and may lead to unexpected results if the order of properties changes. Consider using a utility function like lodash.isEqual for a more robust deep comparison.

setActiveTab('ai')
}, [])

const handleAiConfigPersisted = useCallback(
Copy link

Choose a reason for hiding this comment

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

[⚠️ correctness]
The handleAiConfigPersisted function directly modifies the reviewers field in the form context. Ensure that this update logic is correct and that it does not inadvertently overwrite other form state. Consider isolating this logic or adding validation to prevent potential data loss.

isMemberReview: false,
memberId: undefined,
memberReviewerCount: undefined,
phaseId: 'old-phase-id',
Copy link

Choose a reason for hiding this comment

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

[⚠️ correctness]
The phaseId for AI reviewers is set to 'old-phase-id', which might not be intended. Consider verifying if this should be updated to match the current phase configuration.

isMemberReview: false,
memberId: undefined,
memberReviewerCount: undefined,
phaseId: 'old-phase-id',
Copy link

Choose a reason for hiding this comment

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

[⚠️ correctness]
The phaseId for AI reviewers is set to 'old-phase-id', which might not be intended. Consider verifying if this should be updated to match the current phase configuration.

return value === true
}

function toNumber(value: unknown): number | undefined {
Copy link

Choose a reason for hiding this comment

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

[⚠️ correctness]
The toNumber function does not handle cases where value is a non-numeric string that cannot be converted to a number. Consider adding a check to return undefined for such cases to avoid unexpected behavior.

}

const totalWeight = workflows.reduce((sum, workflow) => sum + Number(workflow.weightPercent || 0), 0)
if (Math.abs(totalWeight - 100) > 0.01) {
Copy link

Choose a reason for hiding this comment

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

[⚠️ correctness]
The total weight calculation in validateAiReviewConfiguration uses a tolerance of 0.01 to check if the total is 100. Consider using a stricter comparison or documenting the reason for this tolerance to ensure clarity and correctness.

? phases
: []
const reviewPhase = phaseRows.find(phase => {
const phaseName = normalizeReviewerText(phase.name)
Copy link

Choose a reason for hiding this comment

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

[❗❗ correctness]
The getAiReviewerPhaseId function uses toLowerCase() on phaseName but does not handle potential null or undefined values for phase.name. Consider adding a check to ensure phase.name is defined before calling toLowerCase().


const existingAiReviewer = existingAiReviewers.find(
reviewer => normalizeReviewerText(reviewer.aiWorkflowId) === workflowId,
)
Copy link

Choose a reason for hiding this comment

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

[⚠️ correctness]
In syncAiConfigReviewers, the existingAiReviewer is found using normalizeReviewerText(reviewer.aiWorkflowId) === workflowId. Ensure that normalizeReviewerText is appropriate for this comparison, as it may introduce subtle bugs if aiWorkflowId is not normalized consistently.

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