From de6b71da8585ce930949b16f4a048bf8f5475a92 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Tue, 4 Nov 2025 07:50:13 +1100 Subject: [PATCH 1/4] Update for how we display phases if there are registration and submission phases open - prioritise submission. --- .../ChallengePhaseInfo/ChallengePhaseInfo.tsx | 192 ++++++++++++++++-- 1 file changed, 180 insertions(+), 12 deletions(-) diff --git a/src/apps/review/src/lib/components/ChallengePhaseInfo/ChallengePhaseInfo.tsx b/src/apps/review/src/lib/components/ChallengePhaseInfo/ChallengePhaseInfo.tsx index 411492c52..a95c43718 100644 --- a/src/apps/review/src/lib/components/ChallengePhaseInfo/ChallengePhaseInfo.tsx +++ b/src/apps/review/src/lib/components/ChallengePhaseInfo/ChallengePhaseInfo.tsx @@ -8,13 +8,14 @@ import moment from 'moment' import { EnvironmentConfig } from '~/config' -import type { BackendResource, ChallengeInfo, ReviewAppContextModel } from '../../models' +import type { BackendPhase, BackendResource, ChallengeInfo, ReviewAppContextModel } from '../../models' import type { WinningDetailDto } from '../../services' import { ChallengeDetailContext, ReviewAppContext } from '../../contexts' import { useRole, useRoleProps } from '../../hooks' import { fetchWinningsByExternalId } from '../../services' import { ProgressBar } from '../ProgressBar' import { SUBMITTER, TABLE_DATE_FORMAT } from '../../../config/index.config' +import { formatDurationDate } from '../../utils' import styles from './ChallengePhaseInfo.module.scss' @@ -156,6 +157,17 @@ export const ChallengePhaseInfo: FC = (props: Props) => { isLoadingPayment, isTopgearTask, ]) + const phaseDisplayInfo = useMemo( + () => computePhaseDisplayInfo(props.challengeInfo), + [props.challengeInfo], + ) + const { + phaseEndDateString: displayPhaseEndDateString, + phaseLabel: displayPhaseLabel, + timeLeft: displayTimeLeft, + timeLeftColor: displayTimeLeftColor, + timeLeftStatus: displayTimeLeftStatus, + } = phaseDisplayInfo useEffect(() => { const run = async (): Promise => { @@ -219,7 +231,11 @@ export const ChallengePhaseInfo: FC = (props: Props) => { const reviewInProgress = props.reviewInProgress return [ - ...createPhaseItems(variant, data, isTask), + ...createPhaseItems({ + displayPhaseLabel, + isTask, + variant, + }), createRolesItem(myChallengeRoles), ...createTaskItems({ formattedStartDate, @@ -230,6 +246,7 @@ export const ChallengePhaseInfo: FC = (props: Props) => { }), ...createNonTaskItems({ data, + displayPhaseEndDateString, formattedNonTaskPayment, isTask, variant, @@ -237,6 +254,9 @@ export const ChallengePhaseInfo: FC = (props: Props) => { }), ...createActiveItems({ data, + displayTimeLeft, + displayTimeLeftColor, + displayTimeLeftStatus, isTask, progressType: PROGRESS_TYPE, reviewInProgress, @@ -251,6 +271,11 @@ export const ChallengePhaseInfo: FC = (props: Props) => { isTask, myChallengeRoles, props.challengeInfo, + displayPhaseLabel, + displayPhaseEndDateString, + displayTimeLeft, + displayTimeLeftColor, + displayTimeLeftStatus, props.reviewProgress, props.reviewInProgress, props.variant, @@ -311,19 +336,23 @@ export const ChallengePhaseInfo: FC = (props: Props) => { export default ChallengePhaseInfo -function createPhaseItems(variant: ChallengeVariant, data: ChallengeInfo, isTask: boolean): ChallengePhaseItem[] { - if (variant !== 'active') { +function createPhaseItems(config: { + displayPhaseLabel: string + isTask: boolean + variant: ChallengeVariant +}): ChallengePhaseItem[] { + if (config.variant !== 'active') { return [] } - if (isTask) { + if (config.isTask) { return [] } return [{ icon: 'icon-review', title: 'Phase', - value: computeIterativeReviewLabel(data) || data.currentPhase || 'N/A', + value: config.displayPhaseLabel || 'N/A', }] } @@ -385,6 +414,7 @@ function createNonTaskItems(config: { data: ChallengeInfo formattedNonTaskPayment?: string isTask: boolean + displayPhaseEndDateString?: string variant: ChallengeVariant walletUrl: string }): ChallengePhaseItem[] { @@ -397,7 +427,7 @@ function createNonTaskItems(config: { title: config.variant === 'past' ? 'Challenge End Date' : 'Phase End Date', value: config.variant === 'past' ? getChallengeEndDateValue(config.data) - : config.data.currentPhaseEndDateString || '-', + : config.displayPhaseEndDateString || '-', }] if (config.formattedNonTaskPayment) { @@ -421,6 +451,9 @@ function createNonTaskItems(config: { function createActiveItems(config: { data: ChallengeInfo + displayTimeLeft?: string + displayTimeLeftColor?: string + displayTimeLeftStatus?: string progressType: typeof PROGRESS_TYPE reviewInProgress: boolean reviewProgress: number @@ -437,12 +470,12 @@ function createActiveItems(config: { const items: ChallengePhaseItem[] = [{ icon: 'icon-timer', - status: config.data.timeLeftStatus, + status: config.displayTimeLeftStatus, style: { - color: config.data.timeLeftColor, + color: config.displayTimeLeftColor, }, title: 'Time Left', - value: config.data.timeLeft || '-', + value: config.displayTimeLeft || '-', }] if (!config.reviewInProgress) { @@ -456,6 +489,138 @@ function createActiveItems(config: { return items } +interface PhaseDisplayInfo { + phaseEndDateString?: string + phaseLabel: string + timeLeft?: string + timeLeftColor?: string + timeLeftStatus?: string +} + +function computePhaseDisplayInfo(data?: ChallengeInfo): PhaseDisplayInfo { + const fallbackPhaseLabel = (data?.currentPhase || '').trim() || 'N/A' + const fallback: PhaseDisplayInfo = { + phaseEndDateString: data?.currentPhaseEndDateString, + phaseLabel: fallbackPhaseLabel, + timeLeft: data?.timeLeft, + timeLeftColor: data?.timeLeftColor, + timeLeftStatus: data?.timeLeftStatus, + } + + if (!data) { + return fallback + } + + const phaseForDisplay = selectPhaseForDisplay(data) + const phaseLabel = computeDisplayPhaseLabel(data, phaseForDisplay) || fallback.phaseLabel + const timing = computePhaseTiming(phaseForDisplay) + + return { + phaseEndDateString: timing.endDateString ?? fallback.phaseEndDateString, + phaseLabel, + timeLeft: timing.timeLeft ?? fallback.timeLeft, + timeLeftColor: timing.timeLeftColor ?? fallback.timeLeftColor, + timeLeftStatus: timing.timeLeftStatus ?? fallback.timeLeftStatus, + } +} + +function selectPhaseForDisplay(data?: ChallengeInfo): BackendPhase | undefined { + if (!data) return undefined + + const phases = Array.isArray(data.phases) ? data.phases : [] + if (!phases.length) { + return data.currentPhaseObject + } + + const openPhases = phases.filter(phase => phase?.isOpen) + if (!openPhases.length) { + return data.currentPhaseObject + } + + const submissionPhase = openPhases.find(phase => normalizePhaseName(phase?.name) === 'submission') + const registrationPhase = openPhases.find(phase => normalizePhaseName(phase?.name) === 'registration') + + if (submissionPhase && registrationPhase) { + return submissionPhase + } + + if (data.currentPhaseObject && data.currentPhaseObject.isOpen) { + return data.currentPhaseObject + } + + return submissionPhase ?? openPhases[0] +} + +function computeDisplayPhaseLabel( + data?: ChallengeInfo, + phase?: BackendPhase, +): string { + const fallback = (data?.currentPhase || '').trim() || 'N/A' + + if (!data) { + return fallback + } + + if (phase && isIterativePhaseName(phase.name)) { + const iterativeLabel = computeIterativeReviewLabel(data, phase) + if (iterativeLabel) { + return iterativeLabel + } + } else if (!phase) { + const iterativeLabel = computeIterativeReviewLabel(data) + if (iterativeLabel) { + return iterativeLabel + } + } + + const name = (phase?.name || '').trim() + if (name) { + return name + } + + return fallback +} + +function computePhaseTiming(phase?: BackendPhase): { + endDateString?: string + timeLeft?: string + timeLeftColor?: string + timeLeftStatus?: string +} { + if (!phase || !phase.isOpen) { + return {} + } + + const rawEndDate = phase.actualEndDate || phase.scheduledEndDate + if (!rawEndDate) { + return {} + } + + const endDate = new Date(rawEndDate) + if (Number.isNaN(endDate.getTime())) { + return {} + } + + const formattedEndDate = moment(endDate) + .local() + .format(TABLE_DATE_FORMAT) + const duration = formatDurationDate(endDate, new Date()) + + return { + endDateString: formattedEndDate, + timeLeft: duration.durationString, + timeLeftColor: duration.durationColor, + timeLeftStatus: duration.durationStatus, + } +} + +function normalizePhaseName(name?: string): string { + return (name || '') + .toString() + .trim() + .toLowerCase() +} + // Helpers extracted to keep component complexity manageable function isIterativePhaseName(name?: string): boolean { return typeof name === 'string' && name.trim() @@ -463,10 +628,13 @@ function isIterativePhaseName(name?: string): boolean { .includes('iterative review') } -function computeIterativeReviewLabel(data: any): string | undefined { +function computeIterativeReviewLabel( + data: any, + overridePhase?: BackendPhase, +): string | undefined { const phases = Array.isArray(data?.phases) ? data.phases : [] - const current = data?.currentPhaseObject + const current = overridePhase ?? data?.currentPhaseObject const currentIsIterative = isIterativePhaseName(current?.name) const openIterative = currentIsIterative From c213504007913efa6229c7dff2d1f861585c3bfb Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Thu, 6 Nov 2025 16:26:32 +0200 Subject: [PATCH 2/4] Update tc-auth-lib dependency to use master branch --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a9ef6921e..1f2c0c9a0 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,7 @@ "sass": "^1.79.0", "styled-components": "^5.3.6", "swr": "^1.3.0", - "tc-auth-lib": "topcoder-platform/tc-auth-lib#1.0.27", + "tc-auth-lib": "topcoder-platform/tc-auth-lib#master", "tinymce": "^7.9.1", "typescript": "^4.8.4", "universal-navigation": "https://github.com/topcoder-platform/universal-navigation#9fc50d938be7182", From fb1cc20d6eac28a40f4f7c0443c155226d3747bc Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Mon, 10 Nov 2025 13:34:47 +1100 Subject: [PATCH 3/4] Fix for handles not showing with submissions in review app for large challenges and better group searching in system admin app --- .../PermissionGroupsPage.module.scss | 23 ++++++- .../PermissionGroupsPage.tsx | 69 ++++++++++++++----- .../ChallengePhaseInfo/ChallengePhaseInfo.tsx | 25 ++++--- .../src/lib/services/resources.service.ts | 45 ++++++++++-- 4 files changed, 127 insertions(+), 35 deletions(-) diff --git a/src/apps/admin/src/permission-management/PermissionGroupsPage/PermissionGroupsPage.module.scss b/src/apps/admin/src/permission-management/PermissionGroupsPage/PermissionGroupsPage.module.scss index 7862fdfea..74914a33d 100644 --- a/src/apps/admin/src/permission-management/PermissionGroupsPage/PermissionGroupsPage.module.scss +++ b/src/apps/admin/src/permission-management/PermissionGroupsPage/PermissionGroupsPage.module.scss @@ -19,10 +19,29 @@ text-align: center; } -.btnNewGroup { - margin: $sp-8 $sp-8 $sp-4 $sp-8; +.actions { + display: flex; + align-items: flex-end; + gap: $sp-4; + margin: $sp-8 $sp-8 $sp-4; @include ltelg { + flex-direction: column; + align-items: stretch; margin: $sp-4; + gap: $sp-3; + } +} + +.searchField { + flex: 1; +} + +.btnNewGroup { + margin: 0; + min-width: max-content; + + @include ltelg { + width: 100%; } } diff --git a/src/apps/admin/src/permission-management/PermissionGroupsPage/PermissionGroupsPage.tsx b/src/apps/admin/src/permission-management/PermissionGroupsPage/PermissionGroupsPage.tsx index fb94e452a..c0b8b9860 100644 --- a/src/apps/admin/src/permission-management/PermissionGroupsPage/PermissionGroupsPage.tsx +++ b/src/apps/admin/src/permission-management/PermissionGroupsPage/PermissionGroupsPage.tsx @@ -1,10 +1,10 @@ /** * Permission groups page. */ -import { FC, useContext, useState } from 'react' +import { ChangeEvent, FC, useContext, useMemo, useState } from 'react' import classNames from 'classnames' -import { Button, LoadingSpinner, PageTitle } from '~/libs/ui' +import { Button, InputText, LoadingSpinner, PageTitle } from '~/libs/ui' import { PlusIcon } from '@heroicons/react/solid' import { DialogAddGroup } from '../../lib/components/DialogAddGroup' @@ -24,6 +24,7 @@ const pageTitle = 'Groups' export const PermissionGroupsPage: FC = (props: Props) => { const [openDialogAddGroup, setOpenDialogAddGroup] = useState(false) + const [searchTerm, setSearchTerm] = useState('') const { loadUser, cancelLoadUser, usersMapping }: AdminAppContextType = useContext(AdminAppContext) const { @@ -37,6 +38,26 @@ export const PermissionGroupsPage: FC = (props: Props) => { usersMapping, ) + const filteredGroups = useMemo(() => { + const normalized = searchTerm + .trim() + .toLowerCase() + if (!normalized) { + return groups + } + + return groups.filter(group => { + const id = group.id ? group.id.toLowerCase() : '' + const name = group.name ? group.name.toLowerCase() : '' + + return id.includes(normalized) || name.includes(normalized) + }) + }, [groups, searchTerm]) + const hasSearchTerm = useMemo( + () => searchTerm.trim().length > 0, + [searchTerm], + ) + return (
{pageTitle} @@ -51,24 +72,40 @@ export const PermissionGroupsPage: FC = (props: Props) => {
) : ( <> -