fix(mentor): restore Add Project on mentoring rounds + gate mentor assignment
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m15s
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m15s
Three related bugs around the mentoring-round Projects tab: 1. Add Project to Round was unreachable on MENTORING rounds — the table swap in the prior commit lost the button. Export AddProjectDialog from project-states-table and render it inside MentoringProjectsTable with an "Add" button in the filter row and a CTA in the empty state. 2. The "Assign Projects" quick action on the round overview linked to the global pool with an opaque filter; on MENTORING rounds it now switches to the Projects tab where the new Add Project button + auto-fill + per-team picker all live. Non-mentoring rounds keep the old behavior. 3. mentor.assign and mentor.bulkAssign now refuse projects that aren't enrolled in any MENTORING round (any status). The single-assign throws BAD_REQUEST with a guidance message; the bulk path filters them out and reports ineligibleProjectCount in the result so the UI can warn the admin instead of silently skipping. Tests: the multi-mentor-assignment suite now sets up a MENTORING round + ProjectRoundState for each project it tests against, matching the new gate.
This commit is contained in:
@@ -33,12 +33,32 @@ import {
|
||||
Loader2,
|
||||
Download,
|
||||
X,
|
||||
Plus,
|
||||
} from 'lucide-react'
|
||||
import { CountryDisplay } from '@/components/shared/country-display'
|
||||
import { AddProjectDialog } from '@/components/admin/round/project-states-table'
|
||||
|
||||
type Filter = 'all' | 'unassigned' | 'assigned' | 'wants_only'
|
||||
|
||||
export function MentoringProjectsTable({ roundId }: { roundId: string }) {
|
||||
type CompetitionRound = {
|
||||
id: string
|
||||
name: string
|
||||
sortOrder: number
|
||||
_count: { projectRoundStates: number }
|
||||
}
|
||||
|
||||
export function MentoringProjectsTable({
|
||||
roundId,
|
||||
competitionId,
|
||||
competitionRounds,
|
||||
currentSortOrder,
|
||||
}: {
|
||||
roundId: string
|
||||
competitionId: string
|
||||
competitionRounds?: CompetitionRound[]
|
||||
currentSortOrder?: number
|
||||
}) {
|
||||
const [addProjectOpen, setAddProjectOpen] = useState(false)
|
||||
const [search, setSearch] = useState('')
|
||||
const [filter, setFilter] = useState<Filter>('all')
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set())
|
||||
@@ -67,6 +87,10 @@ export function MentoringProjectsTable({ roundId }: { roundId: string }) {
|
||||
toast.info(
|
||||
`No new assignments — every selected mentor is already on every selected project (${result.totalSkipped} pair${result.totalSkipped === 1 ? '' : 's'} skipped).`,
|
||||
)
|
||||
} else if (result.totalAssigned === 0 && result.ineligibleProjectCount > 0) {
|
||||
toast.warning(
|
||||
`${result.ineligibleProjectCount} project${result.ineligibleProjectCount === 1 ? '' : 's'} aren't in a mentoring round and were skipped.`,
|
||||
)
|
||||
} else {
|
||||
const mentorCount = result.perMentor.filter((m) => m.assigned > 0).length
|
||||
toast.success(
|
||||
@@ -197,18 +221,32 @@ export function MentoringProjectsTable({ roundId }: { roundId: string }) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{importBanner}
|
||||
<div className="rounded-md border bg-muted/30 px-4 py-12 text-center text-sm text-muted-foreground">
|
||||
No projects in this mentoring round yet.
|
||||
{!importBanner && (
|
||||
<>
|
||||
{' '}Use{' '}
|
||||
<span className="font-medium text-foreground">
|
||||
Add Project to Round
|
||||
</span>{' '}
|
||||
to populate it.
|
||||
</>
|
||||
)}
|
||||
<div className="flex items-center justify-end">
|
||||
<Button size="sm" onClick={() => setAddProjectOpen(true)}>
|
||||
<Plus className="mr-1.5 h-4 w-4" />
|
||||
Add Project to Round
|
||||
</Button>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/30 px-4 py-12 text-center text-sm text-muted-foreground">
|
||||
No projects in this mentoring round yet. Click{' '}
|
||||
<span className="font-medium text-foreground">Add Project to Round</span>{' '}
|
||||
above to populate it.
|
||||
</div>
|
||||
|
||||
<AddProjectDialog
|
||||
open={addProjectOpen}
|
||||
onOpenChange={setAddProjectOpen}
|
||||
roundId={roundId}
|
||||
competitionId={competitionId}
|
||||
competitionRounds={competitionRounds}
|
||||
currentSortOrder={currentSortOrder}
|
||||
onAssigned={() => {
|
||||
utils.round.listMentoringProjects.invalidate({ roundId })
|
||||
utils.round.getProjectsNeedingMentor.invalidate({ roundId })
|
||||
utils.round.getMentoringImportCandidates.invalidate({ roundId })
|
||||
utils.mentor.getRoundStats.invalidate({ roundId })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -246,14 +284,24 @@ export function MentoringProjectsTable({ roundId }: { roundId: string }) {
|
||||
<Pill value="assigned" label="Has mentor" count={totals.assigned} />
|
||||
<Pill value="wants_only" label="Wants mentorship" count={totals.wants} />
|
||||
</div>
|
||||
<div className="relative w-full sm:max-w-xs">
|
||||
<Search className="absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Search projects, teams, or mentors…"
|
||||
className="pl-8"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative w-full sm:w-72">
|
||||
<Search className="absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Search projects, teams, or mentors…"
|
||||
className="pl-8"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => setAddProjectOpen(true)}
|
||||
className="shrink-0"
|
||||
>
|
||||
<Plus className="mr-1 h-4 w-4" />
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -670,6 +718,21 @@ export function MentoringProjectsTable({ roundId }: { roundId: string }) {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<AddProjectDialog
|
||||
open={addProjectOpen}
|
||||
onOpenChange={setAddProjectOpen}
|
||||
roundId={roundId}
|
||||
competitionId={competitionId}
|
||||
competitionRounds={competitionRounds}
|
||||
currentSortOrder={currentSortOrder}
|
||||
onAssigned={() => {
|
||||
utils.round.listMentoringProjects.invalidate({ roundId })
|
||||
utils.round.getProjectsNeedingMentor.invalidate({ roundId })
|
||||
utils.round.getMentoringImportCandidates.invalidate({ roundId })
|
||||
utils.mentor.getRoundStats.invalidate({ roundId })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -785,7 +785,7 @@ function QuickAddDialog({
|
||||
* Create New: form to create a project and assign it directly to the round.
|
||||
* From Pool: search existing projects not yet in this round and assign them.
|
||||
*/
|
||||
function AddProjectDialog({
|
||||
export function AddProjectDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
roundId,
|
||||
|
||||
Reference in New Issue
Block a user