UI/UX audit fixes: clickable pipelines, broken links, isActive locking
Build and Push Docker Image / build (push) Successful in 10m55s
Details
Build and Push Docker Image / build (push) Successful in 10m55s
Details
- Make pipeline cards clickable on list page (navigate to detail view) - Fix broken nav link: applicant /messages → /mentor - Fix broken nav link: mentor /messages → /projects - Add isActive field locking to all 7 wizard sections (intake, main-track, filtering, assignment, awards, live-finals, notifications) - Add minLoad ≤ maxLoad cross-field validation in assignment section - Add duplicate stage slug detection in main track section - Add active pipeline warning banners in intake and main track sections Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
31225b099e
commit
7d1c87e938
|
|
@ -309,6 +309,7 @@ export default function EditPipelinePage() {
|
||||||
onChange={(c) =>
|
onChange={(c) =>
|
||||||
updateStageConfig('INTAKE', c as unknown as Record<string, unknown>)
|
updateStageConfig('INTAKE', c as unknown as Record<string, unknown>)
|
||||||
}
|
}
|
||||||
|
isActive={isActive}
|
||||||
/>
|
/>
|
||||||
</WizardSection>
|
</WizardSection>
|
||||||
|
|
||||||
|
|
@ -323,6 +324,7 @@ export default function EditPipelinePage() {
|
||||||
<MainTrackSection
|
<MainTrackSection
|
||||||
stages={mainTrack?.stages ?? []}
|
stages={mainTrack?.stages ?? []}
|
||||||
onChange={updateMainTrackStages}
|
onChange={updateMainTrackStages}
|
||||||
|
isActive={isActive}
|
||||||
/>
|
/>
|
||||||
</WizardSection>
|
</WizardSection>
|
||||||
|
|
||||||
|
|
@ -339,6 +341,7 @@ export default function EditPipelinePage() {
|
||||||
onChange={(c) =>
|
onChange={(c) =>
|
||||||
updateStageConfig('FILTER', c as unknown as Record<string, unknown>)
|
updateStageConfig('FILTER', c as unknown as Record<string, unknown>)
|
||||||
}
|
}
|
||||||
|
isActive={isActive}
|
||||||
/>
|
/>
|
||||||
</WizardSection>
|
</WizardSection>
|
||||||
|
|
||||||
|
|
@ -355,6 +358,7 @@ export default function EditPipelinePage() {
|
||||||
onChange={(c) =>
|
onChange={(c) =>
|
||||||
updateStageConfig('EVALUATION', c as unknown as Record<string, unknown>)
|
updateStageConfig('EVALUATION', c as unknown as Record<string, unknown>)
|
||||||
}
|
}
|
||||||
|
isActive={isActive}
|
||||||
/>
|
/>
|
||||||
</WizardSection>
|
</WizardSection>
|
||||||
|
|
||||||
|
|
@ -369,6 +373,7 @@ export default function EditPipelinePage() {
|
||||||
<AwardsSection
|
<AwardsSection
|
||||||
tracks={state.tracks}
|
tracks={state.tracks}
|
||||||
onChange={(tracks) => updateState({ tracks })}
|
onChange={(tracks) => updateState({ tracks })}
|
||||||
|
isActive={isActive}
|
||||||
/>
|
/>
|
||||||
</WizardSection>
|
</WizardSection>
|
||||||
|
|
||||||
|
|
@ -385,6 +390,7 @@ export default function EditPipelinePage() {
|
||||||
onChange={(c) =>
|
onChange={(c) =>
|
||||||
updateStageConfig('LIVE_FINAL', c as unknown as Record<string, unknown>)
|
updateStageConfig('LIVE_FINAL', c as unknown as Record<string, unknown>)
|
||||||
}
|
}
|
||||||
|
isActive={isActive}
|
||||||
/>
|
/>
|
||||||
</WizardSection>
|
</WizardSection>
|
||||||
|
|
||||||
|
|
@ -403,6 +409,7 @@ export default function EditPipelinePage() {
|
||||||
onOverridePolicyChange={(overridePolicy) =>
|
onOverridePolicyChange={(overridePolicy) =>
|
||||||
updateState({ overridePolicy })
|
updateState({ overridePolicy })
|
||||||
}
|
}
|
||||||
|
isActive={isActive}
|
||||||
/>
|
/>
|
||||||
</WizardSection>
|
</WizardSection>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,12 @@ export default function PipelineListPage() {
|
||||||
{pipelines && pipelines.length > 0 && (
|
{pipelines && pipelines.length > 0 && (
|
||||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{pipelines.map((pipeline) => (
|
{pipelines.map((pipeline) => (
|
||||||
<Card key={pipeline.id} className="group hover:shadow-md transition-shadow">
|
<Link
|
||||||
|
key={pipeline.id}
|
||||||
|
href={`/admin/rounds/pipeline/${pipeline.id}` as Route}
|
||||||
|
className="block"
|
||||||
|
>
|
||||||
|
<Card className="group hover:shadow-md transition-shadow cursor-pointer">
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
|
|
@ -160,6 +165,7 @@ export default function PipelineListPage() {
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-7 w-7 opacity-0 group-hover:opacity-100 transition-opacity"
|
className="h-7 w-7 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
onClick={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -198,6 +204,7 @@ export default function PipelineListPage() {
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -250,7 +250,7 @@ export default function ApplicantPipelinePage() {
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href={"/applicant/messages" as Route}
|
href={"/applicant/mentor" as Route}
|
||||||
className="group flex items-center gap-3 rounded-xl border p-4 transition-all hover:border-amber-500/30 hover:bg-amber-50/50 hover:-translate-y-0.5 hover:shadow-md"
|
className="group flex items-center gap-3 rounded-xl border p-4 transition-all hover:border-amber-500/30 hover:bg-amber-50/50 hover:-translate-y-0.5 hover:shadow-md"
|
||||||
>
|
>
|
||||||
<div className="rounded-lg bg-amber-50 p-2 dark:bg-amber-950/40">
|
<div className="rounded-lg bg-amber-50 p-2 dark:bg-amber-950/40">
|
||||||
|
|
|
||||||
|
|
@ -189,7 +189,7 @@ export default function MentorDashboard() {
|
||||||
{/* Quick Actions */}
|
{/* Quick Actions */}
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<Button variant="outline" size="sm" asChild>
|
<Button variant="outline" size="sm" asChild>
|
||||||
<Link href={'/mentor/messages' as Route}>
|
<Link href={'/mentor/projects' as Route}>
|
||||||
<Mail className="mr-2 h-4 w-4" />
|
<Mail className="mr-2 h-4 w-4" />
|
||||||
Messages
|
Messages
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,10 @@ import type { EvaluationConfig } from '@/types/pipeline-wizard'
|
||||||
type AssignmentSectionProps = {
|
type AssignmentSectionProps = {
|
||||||
config: EvaluationConfig
|
config: EvaluationConfig
|
||||||
onChange: (config: EvaluationConfig) => void
|
onChange: (config: EvaluationConfig) => void
|
||||||
|
isActive?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AssignmentSection({ config, onChange }: AssignmentSectionProps) {
|
export function AssignmentSection({ config, onChange, isActive }: AssignmentSectionProps) {
|
||||||
const updateConfig = (updates: Partial<EvaluationConfig>) => {
|
const updateConfig = (updates: Partial<EvaluationConfig>) => {
|
||||||
onChange({ ...config, ...updates })
|
onChange({ ...config, ...updates })
|
||||||
}
|
}
|
||||||
|
|
@ -32,6 +33,7 @@ export function AssignmentSection({ config, onChange }: AssignmentSectionProps)
|
||||||
min={1}
|
min={1}
|
||||||
max={20}
|
max={20}
|
||||||
value={config.requiredReviews}
|
value={config.requiredReviews}
|
||||||
|
disabled={isActive}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateConfig({ requiredReviews: parseInt(e.target.value) || 3 })
|
updateConfig({ requiredReviews: parseInt(e.target.value) || 3 })
|
||||||
}
|
}
|
||||||
|
|
@ -48,6 +50,7 @@ export function AssignmentSection({ config, onChange }: AssignmentSectionProps)
|
||||||
min={1}
|
min={1}
|
||||||
max={100}
|
max={100}
|
||||||
value={config.maxLoadPerJuror}
|
value={config.maxLoadPerJuror}
|
||||||
|
disabled={isActive}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateConfig({ maxLoadPerJuror: parseInt(e.target.value) || 20 })
|
updateConfig({ maxLoadPerJuror: parseInt(e.target.value) || 20 })
|
||||||
}
|
}
|
||||||
|
|
@ -64,6 +67,7 @@ export function AssignmentSection({ config, onChange }: AssignmentSectionProps)
|
||||||
min={0}
|
min={0}
|
||||||
max={50}
|
max={50}
|
||||||
value={config.minLoadPerJuror}
|
value={config.minLoadPerJuror}
|
||||||
|
disabled={isActive}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateConfig({ minLoadPerJuror: parseInt(e.target.value) || 5 })
|
updateConfig({ minLoadPerJuror: parseInt(e.target.value) || 5 })
|
||||||
}
|
}
|
||||||
|
|
@ -74,6 +78,12 @@ export function AssignmentSection({ config, onChange }: AssignmentSectionProps)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{config.minLoadPerJuror > config.maxLoadPerJuror && (
|
||||||
|
<p className="text-sm text-destructive">
|
||||||
|
Min load per juror cannot exceed max load per juror.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<Label>Availability Weighting</Label>
|
<Label>Availability Weighting</Label>
|
||||||
|
|
@ -86,6 +96,7 @@ export function AssignmentSection({ config, onChange }: AssignmentSectionProps)
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
updateConfig({ availabilityWeighting: checked })
|
updateConfig({ availabilityWeighting: checked })
|
||||||
}
|
}
|
||||||
|
disabled={isActive}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -98,6 +109,7 @@ export function AssignmentSection({ config, onChange }: AssignmentSectionProps)
|
||||||
overflowPolicy: value as EvaluationConfig['overflowPolicy'],
|
overflowPolicy: value as EvaluationConfig['overflowPolicy'],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
disabled={isActive}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import type { RoutingMode, DecisionMode, AwardScoringMode } from '@prisma/client
|
||||||
type AwardsSectionProps = {
|
type AwardsSectionProps = {
|
||||||
tracks: WizardTrackConfig[]
|
tracks: WizardTrackConfig[]
|
||||||
onChange: (tracks: WizardTrackConfig[]) => void
|
onChange: (tracks: WizardTrackConfig[]) => void
|
||||||
|
isActive?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
function slugify(name: string): string {
|
function slugify(name: string): string {
|
||||||
|
|
@ -40,7 +41,7 @@ function slugify(name: string): string {
|
||||||
.replace(/^-|-$/g, '')
|
.replace(/^-|-$/g, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AwardsSection({ tracks, onChange }: AwardsSectionProps) {
|
export function AwardsSection({ tracks, onChange, isActive }: AwardsSectionProps) {
|
||||||
const awardTracks = tracks.filter((t) => t.kind === 'AWARD')
|
const awardTracks = tracks.filter((t) => t.kind === 'AWARD')
|
||||||
const nonAwardTracks = tracks.filter((t) => t.kind !== 'AWARD')
|
const nonAwardTracks = tracks.filter((t) => t.kind !== 'AWARD')
|
||||||
|
|
||||||
|
|
@ -72,7 +73,7 @@ export function AwardsSection({ tracks, onChange }: AwardsSectionProps) {
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Configure special award tracks that run alongside the main competition.
|
Configure special award tracks that run alongside the main competition.
|
||||||
</p>
|
</p>
|
||||||
<Button type="button" variant="outline" size="sm" onClick={addAward}>
|
<Button type="button" variant="outline" size="sm" onClick={addAward} disabled={isActive}>
|
||||||
<Plus className="h-3.5 w-3.5 mr-1" />
|
<Plus className="h-3.5 w-3.5 mr-1" />
|
||||||
Add Award Track
|
Add Award Track
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -100,6 +101,7 @@ export function AwardsSection({ tracks, onChange }: AwardsSectionProps) {
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-7 w-7 text-muted-foreground hover:text-destructive"
|
className="h-7 w-7 text-muted-foreground hover:text-destructive"
|
||||||
|
disabled={isActive}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -129,6 +131,7 @@ export function AwardsSection({ tracks, onChange }: AwardsSectionProps) {
|
||||||
<Input
|
<Input
|
||||||
placeholder="e.g., Innovation Award"
|
placeholder="e.g., Innovation Award"
|
||||||
value={track.awardConfig?.name ?? track.name}
|
value={track.awardConfig?.name ?? track.name}
|
||||||
|
disabled={isActive}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const name = e.target.value
|
const name = e.target.value
|
||||||
updateAward(index, {
|
updateAward(index, {
|
||||||
|
|
@ -151,6 +154,7 @@ export function AwardsSection({ tracks, onChange }: AwardsSectionProps) {
|
||||||
routingModeDefault: value as RoutingMode,
|
routingModeDefault: value as RoutingMode,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
disabled={isActive}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="text-sm">
|
<SelectTrigger className="text-sm">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
|
|
@ -178,6 +182,7 @@ export function AwardsSection({ tracks, onChange }: AwardsSectionProps) {
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
updateAward(index, { decisionMode: value as DecisionMode })
|
updateAward(index, { decisionMode: value as DecisionMode })
|
||||||
}
|
}
|
||||||
|
disabled={isActive}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="text-sm">
|
<SelectTrigger className="text-sm">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
|
|
@ -203,6 +208,7 @@ export function AwardsSection({ tracks, onChange }: AwardsSectionProps) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
disabled={isActive}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="text-sm">
|
<SelectTrigger className="text-sm">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,10 @@ import type { FilterConfig, FilterRuleConfig } from '@/types/pipeline-wizard'
|
||||||
type FilteringSectionProps = {
|
type FilteringSectionProps = {
|
||||||
config: FilterConfig
|
config: FilterConfig
|
||||||
onChange: (config: FilterConfig) => void
|
onChange: (config: FilterConfig) => void
|
||||||
|
isActive?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FilteringSection({ config, onChange }: FilteringSectionProps) {
|
export function FilteringSection({ config, onChange, isActive }: FilteringSectionProps) {
|
||||||
const updateConfig = (updates: Partial<FilterConfig>) => {
|
const updateConfig = (updates: Partial<FilterConfig>) => {
|
||||||
onChange({ ...config, ...updates })
|
onChange({ ...config, ...updates })
|
||||||
}
|
}
|
||||||
|
|
@ -57,7 +58,7 @@ export function FilteringSection({ config, onChange }: FilteringSectionProps) {
|
||||||
Deterministic rules that projects must pass. Applied in order.
|
Deterministic rules that projects must pass. Applied in order.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button type="button" variant="outline" size="sm" onClick={addRule}>
|
<Button type="button" variant="outline" size="sm" onClick={addRule} disabled={isActive}>
|
||||||
<Plus className="h-3.5 w-3.5 mr-1" />
|
<Plus className="h-3.5 w-3.5 mr-1" />
|
||||||
Add Rule
|
Add Rule
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -72,11 +73,13 @@ export function FilteringSection({ config, onChange }: FilteringSectionProps) {
|
||||||
placeholder="Field name"
|
placeholder="Field name"
|
||||||
value={rule.field}
|
value={rule.field}
|
||||||
className="h-8 text-sm"
|
className="h-8 text-sm"
|
||||||
|
disabled={isActive}
|
||||||
onChange={(e) => updateRule(index, { field: e.target.value })}
|
onChange={(e) => updateRule(index, { field: e.target.value })}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
value={rule.operator}
|
value={rule.operator}
|
||||||
onValueChange={(value) => updateRule(index, { operator: value })}
|
onValueChange={(value) => updateRule(index, { operator: value })}
|
||||||
|
disabled={isActive}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 text-sm">
|
<SelectTrigger className="h-8 text-sm">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
|
|
@ -94,6 +97,7 @@ export function FilteringSection({ config, onChange }: FilteringSectionProps) {
|
||||||
placeholder="Value"
|
placeholder="Value"
|
||||||
value={String(rule.value)}
|
value={String(rule.value)}
|
||||||
className="h-8 text-sm"
|
className="h-8 text-sm"
|
||||||
|
disabled={isActive}
|
||||||
onChange={(e) => updateRule(index, { value: e.target.value })}
|
onChange={(e) => updateRule(index, { value: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -103,6 +107,7 @@ export function FilteringSection({ config, onChange }: FilteringSectionProps) {
|
||||||
size="icon"
|
size="icon"
|
||||||
className="shrink-0 h-7 w-7 text-muted-foreground hover:text-destructive"
|
className="shrink-0 h-7 w-7 text-muted-foreground hover:text-destructive"
|
||||||
onClick={() => removeRule(index)}
|
onClick={() => removeRule(index)}
|
||||||
|
disabled={isActive}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -130,6 +135,7 @@ export function FilteringSection({ config, onChange }: FilteringSectionProps) {
|
||||||
<Switch
|
<Switch
|
||||||
checked={config.aiRubricEnabled}
|
checked={config.aiRubricEnabled}
|
||||||
onCheckedChange={(checked) => updateConfig({ aiRubricEnabled: checked })}
|
onCheckedChange={(checked) => updateConfig({ aiRubricEnabled: checked })}
|
||||||
|
disabled={isActive}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -196,6 +202,7 @@ export function FilteringSection({ config, onChange }: FilteringSectionProps) {
|
||||||
<Switch
|
<Switch
|
||||||
checked={config.manualQueueEnabled}
|
checked={config.manualQueueEnabled}
|
||||||
onCheckedChange={(checked) => updateConfig({ manualQueueEnabled: checked })}
|
onCheckedChange={(checked) => updateConfig({ manualQueueEnabled: checked })}
|
||||||
|
disabled={isActive}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,10 @@ import type { IntakeConfig, FileRequirementConfig } from '@/types/pipeline-wizar
|
||||||
type IntakeSectionProps = {
|
type IntakeSectionProps = {
|
||||||
config: IntakeConfig
|
config: IntakeConfig
|
||||||
onChange: (config: IntakeConfig) => void
|
onChange: (config: IntakeConfig) => void
|
||||||
|
isActive?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function IntakeSection({ config, onChange }: IntakeSectionProps) {
|
export function IntakeSection({ config, onChange, isActive }: IntakeSectionProps) {
|
||||||
const updateConfig = (updates: Partial<IntakeConfig>) => {
|
const updateConfig = (updates: Partial<IntakeConfig>) => {
|
||||||
onChange({ ...config, ...updates })
|
onChange({ ...config, ...updates })
|
||||||
}
|
}
|
||||||
|
|
@ -54,6 +55,12 @@ export function IntakeSection({ config, onChange }: IntakeSectionProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
{isActive && (
|
||||||
|
<p className="text-sm text-amber-600 bg-amber-50 rounded-md px-3 py-2">
|
||||||
|
Some settings are locked because this pipeline is active.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Submission Window */}
|
{/* Submission Window */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|
@ -68,6 +75,7 @@ export function IntakeSection({ config, onChange }: IntakeSectionProps) {
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
updateConfig({ submissionWindowEnabled: checked })
|
updateConfig({ submissionWindowEnabled: checked })
|
||||||
}
|
}
|
||||||
|
disabled={isActive}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -83,6 +91,7 @@ export function IntakeSection({ config, onChange }: IntakeSectionProps) {
|
||||||
lateSubmissionPolicy: value as IntakeConfig['lateSubmissionPolicy'],
|
lateSubmissionPolicy: value as IntakeConfig['lateSubmissionPolicy'],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
disabled={isActive}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
|
|
@ -115,7 +124,7 @@ export function IntakeSection({ config, onChange }: IntakeSectionProps) {
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Label>File Requirements</Label>
|
<Label>File Requirements</Label>
|
||||||
<Button type="button" variant="outline" size="sm" onClick={addFileReq}>
|
<Button type="button" variant="outline" size="sm" onClick={addFileReq} disabled={isActive}>
|
||||||
<Plus className="h-3.5 w-3.5 mr-1" />
|
<Plus className="h-3.5 w-3.5 mr-1" />
|
||||||
Add Requirement
|
Add Requirement
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -183,6 +192,7 @@ export function IntakeSection({ config, onChange }: IntakeSectionProps) {
|
||||||
size="icon"
|
size="icon"
|
||||||
className="shrink-0 text-muted-foreground hover:text-destructive"
|
className="shrink-0 text-muted-foreground hover:text-destructive"
|
||||||
onClick={() => removeFileReq(index)}
|
onClick={() => removeFileReq(index)}
|
||||||
|
disabled={isActive}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,10 @@ import type { LiveFinalConfig } from '@/types/pipeline-wizard'
|
||||||
type LiveFinalsSectionProps = {
|
type LiveFinalsSectionProps = {
|
||||||
config: LiveFinalConfig
|
config: LiveFinalConfig
|
||||||
onChange: (config: LiveFinalConfig) => void
|
onChange: (config: LiveFinalConfig) => void
|
||||||
|
isActive?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LiveFinalsSection({ config, onChange }: LiveFinalsSectionProps) {
|
export function LiveFinalsSection({ config, onChange, isActive }: LiveFinalsSectionProps) {
|
||||||
const updateConfig = (updates: Partial<LiveFinalConfig>) => {
|
const updateConfig = (updates: Partial<LiveFinalConfig>) => {
|
||||||
onChange({ ...config, ...updates })
|
onChange({ ...config, ...updates })
|
||||||
}
|
}
|
||||||
|
|
@ -36,6 +37,7 @@ export function LiveFinalsSection({ config, onChange }: LiveFinalsSectionProps)
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
updateConfig({ juryVotingEnabled: checked })
|
updateConfig({ juryVotingEnabled: checked })
|
||||||
}
|
}
|
||||||
|
disabled={isActive}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -52,6 +54,7 @@ export function LiveFinalsSection({ config, onChange }: LiveFinalsSectionProps)
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
updateConfig({ audienceVotingEnabled: checked })
|
updateConfig({ audienceVotingEnabled: checked })
|
||||||
}
|
}
|
||||||
|
disabled={isActive}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -91,6 +94,7 @@ export function LiveFinalsSection({ config, onChange }: LiveFinalsSectionProps)
|
||||||
cohortSetupMode: value as LiveFinalConfig['cohortSetupMode'],
|
cohortSetupMode: value as LiveFinalConfig['cohortSetupMode'],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
disabled={isActive}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
|
|
@ -115,6 +119,7 @@ export function LiveFinalsSection({ config, onChange }: LiveFinalsSectionProps)
|
||||||
revealPolicy: value as LiveFinalConfig['revealPolicy'],
|
revealPolicy: value as LiveFinalConfig['revealPolicy'],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
disabled={isActive}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import type { StageType } from '@prisma/client'
|
||||||
type MainTrackSectionProps = {
|
type MainTrackSectionProps = {
|
||||||
stages: WizardStageConfig[]
|
stages: WizardStageConfig[]
|
||||||
onChange: (stages: WizardStageConfig[]) => void
|
onChange: (stages: WizardStageConfig[]) => void
|
||||||
|
isActive?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const STAGE_TYPE_OPTIONS: { value: StageType; label: string; color: string }[] = [
|
const STAGE_TYPE_OPTIONS: { value: StageType; label: string; color: string }[] = [
|
||||||
|
|
@ -45,7 +46,7 @@ function slugify(name: string): string {
|
||||||
.replace(/^-|-$/g, '')
|
.replace(/^-|-$/g, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MainTrackSection({ stages, onChange }: MainTrackSectionProps) {
|
export function MainTrackSection({ stages, onChange, isActive }: MainTrackSectionProps) {
|
||||||
const updateStage = useCallback(
|
const updateStage = useCallback(
|
||||||
(index: number, updates: Partial<WizardStageConfig>) => {
|
(index: number, updates: Partial<WizardStageConfig>) => {
|
||||||
const updated = [...stages]
|
const updated = [...stages]
|
||||||
|
|
@ -95,15 +96,22 @@ export function MainTrackSection({ stages, onChange }: MainTrackSectionProps) {
|
||||||
Drag to reorder. Minimum 2 stages required.
|
Drag to reorder. Minimum 2 stages required.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button type="button" variant="outline" size="sm" onClick={addStage}>
|
<Button type="button" variant="outline" size="sm" onClick={addStage} disabled={isActive}>
|
||||||
<Plus className="h-3.5 w-3.5 mr-1" />
|
<Plus className="h-3.5 w-3.5 mr-1" />
|
||||||
Add Stage
|
Add Stage
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isActive && (
|
||||||
|
<p className="text-sm text-amber-600 bg-amber-50 rounded-md px-3 py-2">
|
||||||
|
Stage structure is locked because this pipeline is active. Use the Advanced editor for config changes.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{stages.map((stage, index) => {
|
{stages.map((stage, index) => {
|
||||||
const typeInfo = STAGE_TYPE_OPTIONS.find((t) => t.value === stage.stageType)
|
const typeInfo = STAGE_TYPE_OPTIONS.find((t) => t.value === stage.stageType)
|
||||||
|
const hasDuplicateSlug = stage.slug && stages.some((s, i) => i !== index && s.slug === stage.slug)
|
||||||
return (
|
return (
|
||||||
<Card key={index}>
|
<Card key={index}>
|
||||||
<CardContent className="py-3 px-4">
|
<CardContent className="py-3 px-4">
|
||||||
|
|
@ -115,7 +123,7 @@ export function MainTrackSection({ stages, onChange }: MainTrackSectionProps) {
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-5 w-5"
|
className="h-5 w-5"
|
||||||
disabled={index === 0}
|
disabled={isActive || index === 0}
|
||||||
onClick={() => moveStage(index, 'up')}
|
onClick={() => moveStage(index, 'up')}
|
||||||
>
|
>
|
||||||
<ChevronUp className="h-3 w-3" />
|
<ChevronUp className="h-3 w-3" />
|
||||||
|
|
@ -126,7 +134,7 @@ export function MainTrackSection({ stages, onChange }: MainTrackSectionProps) {
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-5 w-5"
|
className="h-5 w-5"
|
||||||
disabled={index === stages.length - 1}
|
disabled={isActive || index === stages.length - 1}
|
||||||
onClick={() => moveStage(index, 'down')}
|
onClick={() => moveStage(index, 'down')}
|
||||||
>
|
>
|
||||||
<ChevronDown className="h-3 w-3" />
|
<ChevronDown className="h-3 w-3" />
|
||||||
|
|
@ -143,12 +151,16 @@ export function MainTrackSection({ stages, onChange }: MainTrackSectionProps) {
|
||||||
<Input
|
<Input
|
||||||
placeholder="Stage name"
|
placeholder="Stage name"
|
||||||
value={stage.name}
|
value={stage.name}
|
||||||
className="h-8 text-sm"
|
className={cn('h-8 text-sm', hasDuplicateSlug && 'border-destructive')}
|
||||||
|
disabled={isActive}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const name = e.target.value
|
const name = e.target.value
|
||||||
updateStage(index, { name, slug: slugify(name) })
|
updateStage(index, { name, slug: slugify(name) })
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{hasDuplicateSlug && (
|
||||||
|
<p className="text-[10px] text-destructive mt-0.5">Duplicate name</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stage type */}
|
{/* Stage type */}
|
||||||
|
|
@ -158,6 +170,7 @@ export function MainTrackSection({ stages, onChange }: MainTrackSectionProps) {
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
updateStage(index, { stageType: value as StageType })
|
updateStage(index, { stageType: value as StageType })
|
||||||
}
|
}
|
||||||
|
disabled={isActive}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 text-xs">
|
<SelectTrigger className="h-8 text-xs">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
|
|
@ -186,7 +199,7 @@ export function MainTrackSection({ stages, onChange }: MainTrackSectionProps) {
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="shrink-0 h-7 w-7 text-muted-foreground hover:text-destructive"
|
className="shrink-0 h-7 w-7 text-muted-foreground hover:text-destructive"
|
||||||
disabled={stages.length <= 2}
|
disabled={isActive || stages.length <= 2}
|
||||||
onClick={() => removeStage(index)}
|
onClick={() => removeStage(index)}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ type NotificationsSectionProps = {
|
||||||
onChange: (config: Record<string, boolean>) => void
|
onChange: (config: Record<string, boolean>) => void
|
||||||
overridePolicy: Record<string, unknown>
|
overridePolicy: Record<string, unknown>
|
||||||
onOverridePolicyChange: (policy: Record<string, unknown>) => void
|
onOverridePolicyChange: (policy: Record<string, unknown>) => void
|
||||||
|
isActive?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const NOTIFICATION_EVENTS = [
|
const NOTIFICATION_EVENTS = [
|
||||||
|
|
@ -60,6 +61,7 @@ export function NotificationsSection({
|
||||||
onChange,
|
onChange,
|
||||||
overridePolicy,
|
overridePolicy,
|
||||||
onOverridePolicyChange,
|
onOverridePolicyChange,
|
||||||
|
isActive,
|
||||||
}: NotificationsSectionProps) {
|
}: NotificationsSectionProps) {
|
||||||
const toggleEvent = (key: string, enabled: boolean) => {
|
const toggleEvent = (key: string, enabled: boolean) => {
|
||||||
onChange({ ...config, [key]: enabled })
|
onChange({ ...config, [key]: enabled })
|
||||||
|
|
@ -88,6 +90,7 @@ export function NotificationsSection({
|
||||||
<Switch
|
<Switch
|
||||||
checked={config[event.key] !== false}
|
checked={config[event.key] !== false}
|
||||||
onCheckedChange={(checked) => toggleEvent(event.key, checked)}
|
onCheckedChange={(checked) => toggleEvent(event.key, checked)}
|
||||||
|
disabled={isActive}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue