From 8935e60ed03d1316bb77acce49f29d083b6e5d18 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 12 Feb 2026 16:43:10 +0100 Subject: [PATCH 01/11] fix: added work experience description with bullet points support --- .../WorkExperienceCard.module.scss | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/libs/shared/lib/components/work-experience-card/WorkExperienceCard.module.scss b/src/libs/shared/lib/components/work-experience-card/WorkExperienceCard.module.scss index cf99fb2b9..01bb2e621 100644 --- a/src/libs/shared/lib/components/work-experience-card/WorkExperienceCard.module.scss +++ b/src/libs/shared/lib/components/work-experience-card/WorkExperienceCard.module.scss @@ -53,10 +53,18 @@ } } - ul, + ul { + margin: $sp-2 0; + padding-left: $sp-6; + list-style-type: disc; + list-style-position: outside; + } + ol { margin: $sp-2 0; padding-left: $sp-6; + list-style-type: decimal; + list-style-position: outside; } li { From c512bb39e0a188fbe3a692c13f884bfb32088551 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 12 Feb 2026 16:44:17 +0100 Subject: [PATCH 02/11] PM-3642 #time 2h added work experience description with bullet points support --- .../work-experience-card/WorkExperienceCard.module.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/shared/lib/components/work-experience-card/WorkExperienceCard.module.scss b/src/libs/shared/lib/components/work-experience-card/WorkExperienceCard.module.scss index 01bb2e621..517462db9 100644 --- a/src/libs/shared/lib/components/work-experience-card/WorkExperienceCard.module.scss +++ b/src/libs/shared/lib/components/work-experience-card/WorkExperienceCard.module.scss @@ -60,6 +60,7 @@ list-style-position: outside; } + ol { margin: $sp-2 0; padding-left: $sp-6; From 9030c30e5933da005ed60dd60c2a6da37b7b7ba1 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 12 Feb 2026 17:53:32 +0100 Subject: [PATCH 03/11] PM-3642 #time 1.5h added style white listing for list style --- .../work-experience-card/WorkExperienceCard.module.scss | 5 ++--- .../components/work-experience-card/WorkExperienceCard.tsx | 4 +++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/libs/shared/lib/components/work-experience-card/WorkExperienceCard.module.scss b/src/libs/shared/lib/components/work-experience-card/WorkExperienceCard.module.scss index 517462db9..d28975eae 100644 --- a/src/libs/shared/lib/components/work-experience-card/WorkExperienceCard.module.scss +++ b/src/libs/shared/lib/components/work-experience-card/WorkExperienceCard.module.scss @@ -56,15 +56,14 @@ ul { margin: $sp-2 0; padding-left: $sp-6; - list-style-type: disc; + list-style-type: revert; list-style-position: outside; } - ol { margin: $sp-2 0; padding-left: $sp-6; - list-style-type: decimal; + list-style-type: revert; list-style-position: outside; } diff --git a/src/libs/shared/lib/components/work-experience-card/WorkExperienceCard.tsx b/src/libs/shared/lib/components/work-experience-card/WorkExperienceCard.tsx index 48962bc38..9c126fb5a 100644 --- a/src/libs/shared/lib/components/work-experience-card/WorkExperienceCard.tsx +++ b/src/libs/shared/lib/components/work-experience-card/WorkExperienceCard.tsx @@ -98,7 +98,7 @@ const WorkExperienceCard: FC = (props: WorkExperienceCa ALLOWED_ATTR: [ 'href', 'target', 'rel', 'style', 'align', 'border', 'cellpadding', 'cellspacing', 'colspan', - 'rowspan', 'width', 'height', 'class', + 'rowspan', 'width', 'height', 'class', 'type', ], ALLOWED_STYLES: { '*': { @@ -108,6 +108,8 @@ const WorkExperienceCard: FC = (props: WorkExperienceCa 'font-weight': true, 'text-align': true, 'text-decoration': true, + 'list-style-type': true, + 'list-style-position': true, }, }, ALLOWED_TAGS: [ From 87cb1492a0b94957d99dfe3525a37c158265d6d0 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 12 Feb 2026 19:23:48 +0100 Subject: [PATCH 04/11] PM-3689 #time 2h fixed phone card alignment in phone modal --- .../profiles/src/member-profile/phones/PhoneCard/PhoneCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps/profiles/src/member-profile/phones/PhoneCard/PhoneCard.tsx b/src/apps/profiles/src/member-profile/phones/PhoneCard/PhoneCard.tsx index b48e3e53d..e8ae19f33 100644 --- a/src/apps/profiles/src/member-profile/phones/PhoneCard/PhoneCard.tsx +++ b/src/apps/profiles/src/member-profile/phones/PhoneCard/PhoneCard.tsx @@ -27,7 +27,7 @@ const PhoneCard: FC = (props: PhoneCardProps) => {
{ From b1a463cc32e9bab2b6e97b1a307ff4998755ee34 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 12 Feb 2026 19:37:08 +0100 Subject: [PATCH 05/11] fix: lint --- .../components/work-experience-card/WorkExperienceCard.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/shared/lib/components/work-experience-card/WorkExperienceCard.tsx b/src/libs/shared/lib/components/work-experience-card/WorkExperienceCard.tsx index 9c126fb5a..8b3433cf1 100644 --- a/src/libs/shared/lib/components/work-experience-card/WorkExperienceCard.tsx +++ b/src/libs/shared/lib/components/work-experience-card/WorkExperienceCard.tsx @@ -106,10 +106,10 @@ const WorkExperienceCard: FC = (props: WorkExperienceCa color: true, 'font-style': true, 'font-weight': true, + 'list-style-position': true, + 'list-style-type': true, 'text-align': true, 'text-decoration': true, - 'list-style-type': true, - 'list-style-position': true, }, }, ALLOWED_TAGS: [ From ef6c7ae26246b926b7a52ff71a1d64fcccffe6ba Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Fri, 13 Feb 2026 17:56:40 +0200 Subject: [PATCH 06/11] PM-3829 #time 6h ai assisted skill generation for copilot opportunity/request form --- .../src/pages/copilot-request-form/index.tsx | 107 +++++++++- .../copilot-request-form/styles.module.scss | 108 +++++++++- src/config/environments/default.env.ts | 6 + .../environments/global-config.model.ts | 2 + src/libs/shared/lib/services/ai-workflows.tsx | 186 ++++++++++++++++++ src/libs/shared/lib/services/index.ts | 1 + 6 files changed, 397 insertions(+), 13 deletions(-) create mode 100644 src/libs/shared/lib/services/ai-workflows.tsx diff --git a/src/apps/copilots/src/pages/copilot-request-form/index.tsx b/src/apps/copilots/src/pages/copilot-request-form/index.tsx index 87f9f0c0a..130244cf5 100644 --- a/src/apps/copilots/src/pages/copilot-request-form/index.tsx +++ b/src/apps/copilots/src/pages/copilot-request-form/index.tsx @@ -7,7 +7,7 @@ import classNames from 'classnames' import { profileContext, ProfileContextData } from '~/libs/core' import { Button, IconSolid, InputDatePicker, InputMultiselectOption, InputRadio, InputSelect, InputSelectReact, InputText, InputTextarea } from '~/libs/ui' -import { InputSkillSelector } from '~/libs/shared' +import { extractSkillsFromText, InputSkillSelector } from '~/libs/shared' import { getProject, getProjects, ProjectsResponse, useProjects } from '../../services/projects' import { ProjectTypes, ProjectTypeValues } from '../../constants' @@ -17,6 +17,8 @@ import { rootRoute } from '../../copilots.routes' import styles from './styles.module.scss' +const MIN_OVERVIEW_LENGTH = 50 + const editableFields = [ 'projectId', 'opportunityTitle', @@ -46,6 +48,8 @@ const CopilotRequestForm: FC<{}> = () => { const [formErrors, setFormErrors] = useState({}) const [paymentType, setPaymentType] = useState('') const [projectFromQuery, setProjectFromQuery] = useState() + const [isGeneratingSkills, setIsGeneratingSkills] = useState(false) + const [aiGenerationError, setAiGenerationError] = useState() const activeProjectStatuses = ['active', 'approved', 'draft', 'new'] const { data: copilotRequestData }: CopilotRequestResponse = useCopilotRequest(routeParams.requestId) @@ -209,6 +213,61 @@ const CopilotRequestForm: FC<{}> = () => { setIsFormChanged(true) } + async function handleGenerateSkillsWithAI(): Promise { + setIsGeneratingSkills(true) + setAiGenerationError(undefined) + + try { + const result = await extractSkillsFromText(formValues.overview) + + if (!result.matches || result.matches.length === 0) { + setAiGenerationError('No skills were extracted. Try providing more details in the project overview.') + toast.warning( + 'No skills found for the provided project overview. Please add more details about the project.', + ) + return + } + + // Add extracted skills to existing skills (avoid duplicates) + const existingSkillIds = new Set((formValues.skills || []).map((s: any) => s.id)) + const newSkills = result.matches.filter(skill => !existingSkillIds.has(skill.id)) + + if (newSkills.length === 0) { + toast.info('All extracted skills are already in the list.') + return + } + + setFormValues((prevFormValues: any) => ({ + ...prevFormValues, + skills: [...(prevFormValues.skills || []), ...newSkills], + })) + + setFormErrors((prevFormErrors: any) => { + const updatedErrors = { ...prevFormErrors } + delete updatedErrors.skills + return updatedErrors + }) + setIsFormChanged(true) + + toast.success( + `Successfully added ${newSkills.length} skill${newSkills.length > 1 ? 's' : ''} from AI analysis!`, + ) + } catch (error) { + const errorMessage = (error as Error).message + setAiGenerationError(errorMessage) + console.error('AI skills generation failed:', error) + toast.error(`Failed to generate skills: ${errorMessage}`) + } finally { + setIsGeneratingSkills(false) + } + } + + // Check if overview has enough content for AI processing + const canGenerateSkills = useMemo(() => { + const overview = formValues.overview?.trim() || '' + return overview.length >= MIN_OVERVIEW_LENGTH && !isGeneratingSkills + }, [formValues.overview, isGeneratingSkills]) + function handleFormAction(): void { const updatedFormErrors: { [key: string]: string } = {} @@ -478,8 +537,39 @@ const CopilotRequestForm: FC<{}> = () => { error={formErrors.overview} dirty /> -

Any specific skills or technology requirements that come to mind?

-
+
+
+

+ Any specific skills or technology requirements that come to mind? +

+
+ {!canGenerateSkills + && formValues.overview + && formValues.overview.trim().length < MIN_OVERVIEW_LENGTH + && ( +

+ Add at least + {' '} + {MIN_OVERVIEW_LENGTH} + {' '} + characters to the project overview to use AI generation +

+ )} +
+
= () => { onChange={handleSkillsChange} />
+ {isGeneratingSkills && ( +

+ 🤖 AI is analyzing your project overview to extract relevant skills... +

+ )} {formErrors.skills && (

{formErrors.skills}

)} + {aiGenerationError && ( +

+ + {aiGenerationError} +

+ )}

What's the planned start date for the copilot?

( 'https://widget.trolley.com', ) +export const SKILLS_EXTRACTION_WORKFLOW_ID = getReactEnv( + 'AI_SKILLS_EXTRACTION_WORKFLOW_ID', + 'skillExtractionWorkflow', +) + export const ADMIN = { AGREE_ELECTRONICALLY: '5b2798b2-ae82-4210-9b4d-5d6428125ccb', AGREE_FOR_DOCUSIGN_TEMPLATE: '999a26ad-b334-453c-8425-165d4cf496d7', diff --git a/src/config/environments/global-config.model.ts b/src/config/environments/global-config.model.ts index b7812dce8..f56462ad4 100644 --- a/src/config/environments/global-config.model.ts +++ b/src/config/environments/global-config.model.ts @@ -25,6 +25,7 @@ export interface GlobalConfig { }, STANDARDIZED_SKILLS_API: string, TC_FINANCE_API: string, + TC_AI_API: string, AUTH: { ACCOUNTS_APP_CONNECTOR: string } @@ -98,6 +99,7 @@ export interface GlobalConfig { TIMEOUT: number PROGRESS_INTERVAL: number }, + SKILLS_EXTRACTION_WORKFLOW_ID: string ADMIN_SSO_LOGIN_PROVIDERS: SSOLoginProviderConfig[] LOCAL_SERVICE_OVERRIDES?: LocalServiceOverride[] TROLLEY_WIDGET_ORIGIN: string diff --git a/src/libs/shared/lib/services/ai-workflows.tsx b/src/libs/shared/lib/services/ai-workflows.tsx new file mode 100644 index 000000000..9977ea983 --- /dev/null +++ b/src/libs/shared/lib/services/ai-workflows.tsx @@ -0,0 +1,186 @@ +import { EnvironmentConfig } from '~/config' +import { xhrGetAsync, xhrPostAsync } from '~/libs/core' + +// AI Workflow Configuration +const AI_WORKFLOW_POLL_INTERVAL = 2000 // 2 seconds +const AI_WORKFLOW_POLL_TIMEOUT = 120000 // 2 minutes +const API_BASE_URL = EnvironmentConfig.TC_AI_API || `${EnvironmentConfig.API.V6}/ai` + +interface WorkflowRunResponse { + runId: string +} + +interface WorkflowInputData { + inputData: { jobDescription: string } +} + +const sleep = (ms: number): Promise => new Promise(resolve => { + setTimeout(() => resolve(), ms) +}) + +/** + * Start an AI workflow run + * + * @param workflowId - The ID of the workflow to run + * @param input - The input data for the workflow + * @returns The run ID + */ +async function startWorkflowRun(workflowId: string, input: string): Promise { + try { + + // Step 1: Create the run + const runResponse = await xhrPostAsync<{}, WorkflowRunResponse>( + `${API_BASE_URL}/workflows/${workflowId}/create-run`, + {}, + ) + const runId = runResponse.runId + + if (!runId) { + throw new Error('No runId returned from workflow creation') + } + + // Step 2: Start the run with input + await xhrPostAsync( + `${API_BASE_URL}/workflows/${workflowId}/start?runId=${runId}`, + { inputData: { jobDescription: input } }, + ) + + return runId + } catch (error) { + console.error('Failed to start workflow run:', (error as Error).message) + throw error + } +} + +interface WorkflowRunResult { + status: 'success' | 'failed' | 'running' | 'pending' + result?: any + error?: { message: string } +} + +/** + * Poll for workflow run status + * + * @param workflowId - The ID of the workflow + * @param runId - The ID of the run to check + * @param maxAttempts - Maximum polling attempts + * @returns The final run result + */ +async function pollWorkflowRunStatus( + workflowId: string, + runId: string, + _maxAttempts?: number, +): Promise { + const pollInterval = AI_WORKFLOW_POLL_INTERVAL + const pollTimeout = AI_WORKFLOW_POLL_TIMEOUT + let maxAttempts = _maxAttempts + + // Calculate max attempts based on timeout if not provided + if (maxAttempts === undefined) { + maxAttempts = Math.ceil(pollTimeout / pollInterval) + } + + let attempt = 0 + const startTime = Date.now() + + while (attempt < maxAttempts) { + try { + // eslint-disable-next-line no-await-in-loop + const result = await xhrGetAsync( + `${API_BASE_URL}/workflows/${workflowId}/runs/${runId}`, + ) + + const status = result?.status + + if (status === 'success') { + return result + } + + if (status === 'failed') { + const errorMsg = result?.error?.message || 'Workflow execution failed' + throw new Error(`Workflow failed: ${errorMsg}`) + } + + const elapsed = Date.now() - startTime + if (elapsed > pollTimeout) { + throw new Error(`Workflow polling timeout after ${elapsed}ms`) + } + + // Wait before next poll + // eslint-disable-next-line no-await-in-loop + await sleep(pollInterval) + attempt += 1 + } catch (error) { + const errorMessage = (error as Error).message + // If it's a network error or timeout, try again + if (errorMessage.includes('timeout') || (error as any).code === 'ECONNABORTED') { + const elapsed = Date.now() - startTime + if (elapsed > pollTimeout) { + throw new Error(`Workflow polling timeout after ${elapsed}ms`) + } + + // eslint-disable-next-line no-await-in-loop + await sleep(pollInterval) + attempt += 1 + } else { + // For other errors, re-throw immediately + console.error('Error polling workflow status:', errorMessage) + throw error + } + } + } + + throw new Error(`Workflow polling exceeded maximum attempts (${maxAttempts})`) +} + +export interface SkillMatch { + id: string + name: string +} + +export interface SkillsExtractionResult { + matches?: SkillMatch[] +} + +/** + * Extract skills from text using AI workflow + * + * @example + * try { + * const result = await extractSkillsFromText('I have experience with JavaScript, React, and Node.js') + * console.log('Extracted skills:', result.matches) // {id: string; name: string}[] + * } catch (error) { + * console.error('Skills extraction failed:', error.message) + * } + */ +export async function extractSkillsFromText( + description: string, + workflowId?: string, +): Promise { + if (!description || typeof description !== 'string') { + throw new Error('Description must be a non-empty string') + } + + const workflowIdToUse = workflowId || EnvironmentConfig.SKILLS_EXTRACTION_WORKFLOW_ID + + if (!workflowIdToUse) { + throw new Error('AI Skills Extraction Workflow ID is not configured') + } + + try { + // Step 1: Start the workflow run + console.log(`Starting workflow run for: ${workflowIdToUse}`) + const runId = await startWorkflowRun(workflowIdToUse, description) + console.log(`Workflow started with runId: ${runId}`) + + // Step 2: Poll for completion + console.log('Polling for workflow completion...') + const result = await pollWorkflowRunStatus(workflowIdToUse, runId) + console.log('Workflow completed successfully') + + return (result.result as SkillsExtractionResult) || {} + } catch (error) { + console.error('Skills extraction workflow failed:', (error as Error).message) + throw error + } +} diff --git a/src/libs/shared/lib/services/index.ts b/src/libs/shared/lib/services/index.ts index b7999a344..7bb353d7c 100644 --- a/src/libs/shared/lib/services/index.ts +++ b/src/libs/shared/lib/services/index.ts @@ -1 +1,2 @@ +export * from './ai-workflows' export * from './standard-skills' From 32f20a7327c2770b16825b7d79a21e3e15c879f2 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Sun, 15 Feb 2026 20:13:17 +0530 Subject: [PATCH 07/11] PM-3855 Enforce profile completeness for engagement application --- .../EngagementDetailPage.module.scss | 22 ++++++++++ .../EngagementDetailPage.tsx | 41 ++++++++++++++++++- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/apps/engagements/src/pages/engagement-detail/EngagementDetailPage.module.scss b/src/apps/engagements/src/pages/engagement-detail/EngagementDetailPage.module.scss index 6b24588e8..07a6d9440 100644 --- a/src/apps/engagements/src/pages/engagement-detail/EngagementDetailPage.module.scss +++ b/src/apps/engagements/src/pages/engagement-detail/EngagementDetailPage.module.scss @@ -483,3 +483,25 @@ color: $red-120; font-size: 0.9rem; } + +.applyMessage { + display: flex; + flex-direction: column; + gap: $sp-3; + padding: $sp-4; + border-radius: 8px; +} + +.signInLink { + display: inline-block; + width: fit-content; + font-size: 16px; + font-weight: 600; + color: var(--tc-primary); + text-decoration: none; + + &:hover { + text-decoration: underline; + } +} + diff --git a/src/apps/engagements/src/pages/engagement-detail/EngagementDetailPage.tsx b/src/apps/engagements/src/pages/engagement-detail/EngagementDetailPage.tsx index 6f2ca7ae4..318893cee 100644 --- a/src/apps/engagements/src/pages/engagement-detail/EngagementDetailPage.tsx +++ b/src/apps/engagements/src/pages/engagement-detail/EngagementDetailPage.tsx @@ -6,7 +6,7 @@ import remarkFrontmatter from 'remark-frontmatter' import remarkGfm from 'remark-gfm' import { EnvironmentConfig } from '~/config' -import { authUrlLogin, useProfileContext } from '~/libs/core' +import { authUrlLogin, useProfileCompleteness, useProfileContext } from '~/libs/core' import { BaseModal, Button, ContentLayout, IconOutline, IconSolid, LoadingSpinner } from '~/libs/ui' import type { Application, Engagement, TermDetails } from '../../lib/models' @@ -496,6 +496,8 @@ const EngagementDetailPage: FC = () => { const isProfileReady = profileContext.initialized const isLoggedIn = profileContext.isLoggedIn const userId = profileContext.profile?.userId + const profileHandle = profileContext.profile?.handle + const profileCompleteness = useProfileCompleteness(profileHandle) const [engagement, setEngagement] = useState(undefined) const [loading, setLoading] = useState(true) @@ -528,6 +530,8 @@ const EngagementDetailPage: FC = () => { normalizedUserEmail, normalizedUserId, }) + const [profileGateError, setProfileGateError] = useState() + const isPrivateEngagement = Boolean(engagement?.isPrivate) const fetchEngagement = useCallback(async (): Promise => { @@ -652,8 +656,25 @@ const EngagementDetailPage: FC = () => { }, [fetchPendingTerm, navigateToApply]) const handleApplyClick = useCallback(() => { + setProfileGateError(undefined) + + if (profileCompleteness?.isLoading) { + return + } + + if ( + profileCompleteness + && typeof profileCompleteness.percent === 'number' + && profileCompleteness.percent < 100 + ) { + setProfileGateError( + 'Your profile must be 100% complete before applying.', + ) + return + } + openNextPendingTerm() - }, [openNextPendingTerm]) + }, [openNextPendingTerm, profileCompleteness]) const handleBackClick = useCallback(() => navigate(rootRoute || '/'), [navigate]) @@ -933,6 +954,22 @@ const EngagementDetailPage: FC = () => { return termsGate } + if (profileGateError) { + return ( +
+ + {profileGateError} + + + Please update your profile here. + +
+ ) + } + return (
+
+ ) + })} +
+ + {isMobile && openDateKey && (() => { + const selectedDate = paddedDates.find(d => d && getDateKey(d) === openDateKey) + if (!selectedDate) return undefined + + const selectedEntry = teamLeaveDates.find(item => item.date === openDateKey) + const selectedUsers = [...(selectedEntry?.usersOnLeave ?? [])].sort(compareUsersByName) + + return ( +
+
+ +
+
+
+ {format(selectedDate, 'EEE, dd MMM yyyy')} +
+ + +
+ +
+ {selectedUsers.length === 0 ? ( +
No leave
+ ) : ( + selectedUsers.map((user, idx) => { + const isHolidayStatus + = user.status === LeaveStatus.WIPRO_HOLIDAY + || user.status === LeaveStatus.HOLIDAY return (
{getUserDisplayName(user)}
) - })} - {overflowCount > 0 && ( -
- {`+${overflowCount} more`} -
+ }) )}
- ) - })} -
+
+ ) + })()} {isLoading && (
@@ -162,6 +264,7 @@ export const TeamCalendar: FC = (props: TeamCalendarProps) => )}
) + } export default TeamCalendar From 83ee9dda8f5de79e92578c85cf1d127aa0381708 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Mon, 16 Feb 2026 19:30:24 +0530 Subject: [PATCH 10/11] Replace hardcoded colors with vars --- .../src/lib/components/TeamCalendar/TeamCalendar.module.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/apps/calendar/src/lib/components/TeamCalendar/TeamCalendar.module.scss b/src/apps/calendar/src/lib/components/TeamCalendar/TeamCalendar.module.scss index 4c9bac4bc..58ba914d7 100644 --- a/src/apps/calendar/src/lib/components/TeamCalendar/TeamCalendar.module.scss +++ b/src/apps/calendar/src/lib/components/TeamCalendar/TeamCalendar.module.scss @@ -187,7 +187,7 @@ .backdrop { position: absolute; inset: 0; - background: rgba(17, 24, 39, 0.35); + background: var(--text-secondary); } .popover { @@ -202,7 +202,7 @@ background: #fff; border: 1px solid #e5e7eb; border-radius: 16px; - box-shadow: 0 24px 64px rgba(0, 0, 0, 0.22); + box-shadow: 0 24px 64px $tc-black; } .popoverHeader { From aef27e3016e4a5ae90f48b1f84341929c2e4968a Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Mon, 16 Feb 2026 19:10:03 +0100 Subject: [PATCH 11/11] PM-2662 #time 2h allow submissions to be visible in winners tab if submissionsViewable is configured to true --- .../ChallengeDetailContextProvider.tsx | 32 +++++++++++++++++++ .../lib/hooks/useFetchChallengeSubmissions.ts | 27 +++++++++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/apps/review/src/lib/contexts/ChallengeDetailContextProvider.tsx b/src/apps/review/src/lib/contexts/ChallengeDetailContextProvider.tsx index 479f92a5b..98080c350 100644 --- a/src/apps/review/src/lib/contexts/ChallengeDetailContextProvider.tsx +++ b/src/apps/review/src/lib/contexts/ChallengeDetailContextProvider.tsx @@ -18,6 +18,7 @@ import { useFetchChallengeSubmissions, useFetchChallengeSubmissionsProps, } from '../hooks' +import type { ChallengeVisibilityFlags } from '../hooks/useFetchChallengeSubmissions' import { ChallengeDetailContext } from './ChallengeDetailContext' import { ReviewAppContext } from './ReviewAppContext' @@ -51,12 +52,43 @@ export const ChallengeDetailContextProvider: FC = props => { }), [loginUserInfo?.roles, loginUserInfo?.userId, myRoles], ) + const challengeVisibility = useMemo( + () => { + if (!challengeInfo) { + return undefined + } + + const trackName = (challengeInfo.track?.name ?? '') + .toString() + .toLowerCase() + const status = (challengeInfo.status ?? '') + .toString() + .toLowerCase() + const isDesign = trackName === 'design' + const isCompleted = status === 'completed' + const submissionsViewable = Boolean( + challengeInfo.metadata?.some( + m => m.name === 'submissionsViewable' + && String(m.value) + .toLowerCase() === 'true', + ), + ) + + return { isCompleted, isDesign, submissionsViewable } + }, + [ + challengeInfo?.track?.name, + challengeInfo?.status, + challengeInfo?.metadata, + ], + ) const { challengeSubmissions, isLoading: isLoadingChallengeSubmissions, }: useFetchChallengeSubmissionsProps = useFetchChallengeSubmissions( challengeId, submissionViewer, + challengeVisibility, ) const submissionInfos = useMemo( diff --git a/src/apps/review/src/lib/hooks/useFetchChallengeSubmissions.ts b/src/apps/review/src/lib/hooks/useFetchChallengeSubmissions.ts index b7b40b620..172f199f0 100644 --- a/src/apps/review/src/lib/hooks/useFetchChallengeSubmissions.ts +++ b/src/apps/review/src/lib/hooks/useFetchChallengeSubmissions.ts @@ -29,14 +29,23 @@ export interface ChallengeSubmissionsViewer { userId?: string | number | null } +export interface ChallengeVisibilityFlags { + isDesign: boolean + isCompleted: boolean + submissionsViewable: boolean +} + /** * Fetch challenge submissions * @param challengeId challenge id + * @param viewer viewer roles and userId for filtering + * @param challengeVisibility when set and Design + completed + submissionsViewable, submitters see all submissions * @returns challenge submissions */ export function useFetchChallengeSubmissions( challengeId?: string, viewer?: ChallengeSubmissionsViewer, + challengeVisibility?: ChallengeVisibilityFlags, ): useFetchChallengeSubmissionsProps { // Use swr hooks for submissions fetching const { @@ -140,6 +149,20 @@ export function useFetchChallengeSubmissions( [viewer?.userId], ) + const allowViewAllSubmissionsForDesign = useMemo( + () => Boolean( + challengeVisibility + && challengeVisibility.isDesign + && challengeVisibility.isCompleted + && challengeVisibility.submissionsViewable, + ), + [ + challengeVisibility?.isDesign, + challengeVisibility?.isCompleted, + challengeVisibility?.submissionsViewable, + ], + ) + // Show backend error when fetching data fail useEffect(() => { if (error) { @@ -165,7 +188,8 @@ export function useFetchChallengeSubmissions( const activeSubmissions: BackendSubmission[] = [] const shouldRestrictToCurrentMember = Boolean( hasSubmitterRole - && !canViewAllSubmissions, + && !canViewAllSubmissions + && !allowViewAllSubmissionsForDesign, ) const normalizeStatus = (status: unknown): string => { @@ -219,6 +243,7 @@ export function useFetchChallengeSubmissions( canViewAllSubmissions, hasSubmitterRole, viewerMemberId, + allowViewAllSubmissionsForDesign, ]) return {